mirror of
https://github.com/serratus/quaggaJS.git
synced 2026-08-13 05:31:37 +08:00
Moved files into meaningful folders
This commit is contained in:
@@ -0,0 +1,588 @@
|
||||
import ImageWrapper from '../common/image_wrapper';
|
||||
import CVUtils from '../common/cv_utils';
|
||||
import ArrayHelper from '../common/array_helper';
|
||||
import ImageDebug from '../common/image_debug';
|
||||
import Rasterizer from './rasterizer';
|
||||
import Tracer from './tracer';
|
||||
import skeletonizer from './skeletonizer';
|
||||
import glMatrix from 'gl-matrix';
|
||||
|
||||
var _config,
|
||||
_currentImageWrapper,
|
||||
_skelImageWrapper,
|
||||
_subImageWrapper,
|
||||
_labelImageWrapper,
|
||||
_patchGrid,
|
||||
_patchLabelGrid,
|
||||
_imageToPatchGrid,
|
||||
_binaryImageWrapper,
|
||||
_patchSize,
|
||||
_canvasContainer = {
|
||||
ctx: {
|
||||
binary: null
|
||||
},
|
||||
dom: {
|
||||
binary: null
|
||||
}
|
||||
},
|
||||
_numPatches = {x: 0, y: 0},
|
||||
_inputImageWrapper,
|
||||
_skeletonizer,
|
||||
vec2 = glMatrix.vec2,
|
||||
mat2 = glMatrix.mat2;
|
||||
|
||||
function initBuffers() {
|
||||
var skeletonImageData;
|
||||
|
||||
if (_config.halfSample) {
|
||||
_currentImageWrapper = new ImageWrapper({
|
||||
x: _inputImageWrapper.size.x / 2 | 0,
|
||||
y: _inputImageWrapper.size.y / 2 | 0
|
||||
});
|
||||
} else {
|
||||
_currentImageWrapper = _inputImageWrapper;
|
||||
}
|
||||
|
||||
_patchSize = CVUtils.calculatePatchSize(_config.patchSize, _currentImageWrapper.size);
|
||||
|
||||
_numPatches.x = _currentImageWrapper.size.x / _patchSize.x | 0;
|
||||
_numPatches.y = _currentImageWrapper.size.y / _patchSize.y | 0;
|
||||
|
||||
_binaryImageWrapper = new ImageWrapper(_currentImageWrapper.size, undefined, Uint8Array, false);
|
||||
|
||||
_labelImageWrapper = new ImageWrapper(_patchSize, undefined, Array, true);
|
||||
|
||||
skeletonImageData = new ArrayBuffer(64 * 1024);
|
||||
_subImageWrapper = new ImageWrapper(_patchSize,
|
||||
new Uint8Array(skeletonImageData, 0, _patchSize.x * _patchSize.y));
|
||||
_skelImageWrapper = new ImageWrapper(_patchSize,
|
||||
new Uint8Array(skeletonImageData, _patchSize.x * _patchSize.y * 3, _patchSize.x * _patchSize.y),
|
||||
undefined, true);
|
||||
_skeletonizer = skeletonizer((typeof window !== 'undefined') ? window : (typeof self !== 'undefined') ? self : global, {
|
||||
size: _patchSize.x
|
||||
}, skeletonImageData);
|
||||
|
||||
_imageToPatchGrid = new ImageWrapper({
|
||||
x: (_currentImageWrapper.size.x / _subImageWrapper.size.x) | 0,
|
||||
y: (_currentImageWrapper.size.y / _subImageWrapper.size.y) | 0
|
||||
}, undefined, Array, true);
|
||||
_patchGrid = new ImageWrapper(_imageToPatchGrid.size, undefined, undefined, true);
|
||||
_patchLabelGrid = new ImageWrapper(_imageToPatchGrid.size, undefined, Int32Array, true);
|
||||
}
|
||||
|
||||
function initCanvas() {
|
||||
if (_config.useWorker || typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
_canvasContainer.dom.binary = document.createElement("canvas");
|
||||
_canvasContainer.dom.binary.className = "binaryBuffer";
|
||||
if (_config.showCanvas === true) {
|
||||
document.querySelector("#debug").appendChild(_canvasContainer.dom.binary);
|
||||
}
|
||||
_canvasContainer.ctx.binary = _canvasContainer.dom.binary.getContext("2d");
|
||||
_canvasContainer.dom.binary.width = _binaryImageWrapper.size.x;
|
||||
_canvasContainer.dom.binary.height = _binaryImageWrapper.size.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a bounding box which encloses all the given patches
|
||||
* @returns {Array} The minimal bounding box
|
||||
*/
|
||||
function boxFromPatches(patches) {
|
||||
var overAvg,
|
||||
i,
|
||||
j,
|
||||
patch,
|
||||
transMat,
|
||||
minx =
|
||||
_binaryImageWrapper.size.x,
|
||||
miny = _binaryImageWrapper.size.y,
|
||||
maxx = -_binaryImageWrapper.size.x,
|
||||
maxy = -_binaryImageWrapper.size.y,
|
||||
box,
|
||||
scale;
|
||||
|
||||
// draw all patches which are to be taken into consideration
|
||||
overAvg = 0;
|
||||
for ( i = 0; i < patches.length; i++) {
|
||||
patch = patches[i];
|
||||
overAvg += patch.rad;
|
||||
if (_config.showPatches) {
|
||||
ImageDebug.drawRect(patch.pos, _subImageWrapper.size, _canvasContainer.ctx.binary, {color: "red"});
|
||||
}
|
||||
}
|
||||
|
||||
overAvg /= patches.length;
|
||||
overAvg = (overAvg * 180 / Math.PI + 90) % 180 - 90;
|
||||
if (overAvg < 0) {
|
||||
overAvg += 180;
|
||||
}
|
||||
|
||||
overAvg = (180 - overAvg) * Math.PI / 180;
|
||||
transMat = mat2.clone([Math.cos(overAvg), Math.sin(overAvg), -Math.sin(overAvg), Math.cos(overAvg)]);
|
||||
|
||||
// iterate over patches and rotate by angle
|
||||
for ( i = 0; i < patches.length; i++) {
|
||||
patch = patches[i];
|
||||
for ( j = 0; j < 4; j++) {
|
||||
vec2.transformMat2(patch.box[j], patch.box[j], transMat);
|
||||
}
|
||||
|
||||
if (_config.boxFromPatches.showTransformed) {
|
||||
ImageDebug.drawPath(patch.box, {x: 0, y: 1}, _canvasContainer.ctx.binary, {color: '#99ff00', lineWidth: 2});
|
||||
}
|
||||
}
|
||||
|
||||
// find bounding box
|
||||
for ( i = 0; i < patches.length; i++) {
|
||||
patch = patches[i];
|
||||
for ( j = 0; j < 4; j++) {
|
||||
if (patch.box[j][0] < minx) {
|
||||
minx = patch.box[j][0];
|
||||
}
|
||||
if (patch.box[j][0] > maxx) {
|
||||
maxx = patch.box[j][0];
|
||||
}
|
||||
if (patch.box[j][1] < miny) {
|
||||
miny = patch.box[j][1];
|
||||
}
|
||||
if (patch.box[j][1] > maxy) {
|
||||
maxy = patch.box[j][1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
box = [[minx, miny], [maxx, miny], [maxx, maxy], [minx, maxy]];
|
||||
|
||||
if (_config.boxFromPatches.showTransformedBox) {
|
||||
ImageDebug.drawPath(box, {x: 0, y: 1}, _canvasContainer.ctx.binary, {color: '#ff0000', lineWidth: 2});
|
||||
}
|
||||
|
||||
scale = _config.halfSample ? 2 : 1;
|
||||
// reverse rotation;
|
||||
transMat = mat2.invert(transMat, transMat);
|
||||
for ( j = 0; j < 4; j++) {
|
||||
vec2.transformMat2(box[j], box[j], transMat);
|
||||
}
|
||||
|
||||
if (_config.boxFromPatches.showBB) {
|
||||
ImageDebug.drawPath(box, {x: 0, y: 1}, _canvasContainer.ctx.binary, {color: '#ff0000', lineWidth: 2});
|
||||
}
|
||||
|
||||
for ( j = 0; j < 4; j++) {
|
||||
vec2.scale(box[j], box[j], scale);
|
||||
}
|
||||
|
||||
return box;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a binary image of the current image
|
||||
*/
|
||||
function binarizeImage() {
|
||||
CVUtils.otsuThreshold(_currentImageWrapper, _binaryImageWrapper);
|
||||
_binaryImageWrapper.zeroBorder();
|
||||
if (_config.showCanvas) {
|
||||
_binaryImageWrapper.show(_canvasContainer.dom.binary, 255);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over the entire image
|
||||
* extract patches
|
||||
*/
|
||||
function findPatches() {
|
||||
var i,
|
||||
j,
|
||||
x,
|
||||
y,
|
||||
moments,
|
||||
patchesFound = [],
|
||||
rasterizer,
|
||||
rasterResult,
|
||||
patch;
|
||||
for (i = 0; i < _numPatches.x; i++) {
|
||||
for (j = 0; j < _numPatches.y; j++) {
|
||||
x = _subImageWrapper.size.x * i;
|
||||
y = _subImageWrapper.size.y * j;
|
||||
|
||||
// seperate parts
|
||||
skeletonize(x, y);
|
||||
|
||||
// Rasterize, find individual bars
|
||||
_skelImageWrapper.zeroBorder();
|
||||
ArrayHelper.init(_labelImageWrapper.data, 0);
|
||||
rasterizer = Rasterizer.create(_skelImageWrapper, _labelImageWrapper);
|
||||
rasterResult = rasterizer.rasterize(0);
|
||||
|
||||
if (_config.showLabels) {
|
||||
_labelImageWrapper.overlay(_canvasContainer.dom.binary, Math.floor(360 / rasterResult.count),
|
||||
{x: x, y: y});
|
||||
}
|
||||
|
||||
// calculate moments from the skeletonized patch
|
||||
moments = _labelImageWrapper.moments(rasterResult.count);
|
||||
|
||||
// extract eligible patches
|
||||
patchesFound = patchesFound.concat(describePatch(moments, [i, j], x, y));
|
||||
}
|
||||
}
|
||||
|
||||
if (_config.showFoundPatches) {
|
||||
for ( i = 0; i < patchesFound.length; i++) {
|
||||
patch = patchesFound[i];
|
||||
ImageDebug.drawRect(patch.pos, _subImageWrapper.size, _canvasContainer.ctx.binary,
|
||||
{color: "#99ff00", lineWidth: 2});
|
||||
}
|
||||
}
|
||||
|
||||
return patchesFound;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds those connected areas which contain at least 6 patches
|
||||
* and returns them ordered DESC by the number of contained patches
|
||||
* @param {Number} maxLabel
|
||||
*/
|
||||
function findBiggestConnectedAreas(maxLabel){
|
||||
var i,
|
||||
sum,
|
||||
labelHist = [],
|
||||
topLabels = [];
|
||||
|
||||
for ( i = 0; i < maxLabel; i++) {
|
||||
labelHist.push(0);
|
||||
}
|
||||
sum = _patchLabelGrid.data.length;
|
||||
while (sum--) {
|
||||
if (_patchLabelGrid.data[sum] > 0) {
|
||||
labelHist[_patchLabelGrid.data[sum] - 1]++;
|
||||
}
|
||||
}
|
||||
|
||||
labelHist = labelHist.map(function(val, idx) {
|
||||
return {
|
||||
val: val,
|
||||
label: idx + 1
|
||||
};
|
||||
});
|
||||
|
||||
labelHist.sort(function(a, b) {
|
||||
return b.val - a.val;
|
||||
});
|
||||
|
||||
// extract top areas with at least 6 patches present
|
||||
topLabels = labelHist.filter(function(el) {
|
||||
return el.val >= 5;
|
||||
});
|
||||
|
||||
return topLabels;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
function findBoxes(topLabels, maxLabel) {
|
||||
var i,
|
||||
j,
|
||||
sum,
|
||||
patches = [],
|
||||
patch,
|
||||
box,
|
||||
boxes = [],
|
||||
hsv = [0, 1, 1],
|
||||
rgb = [0, 0, 0];
|
||||
|
||||
for ( i = 0; i < topLabels.length; i++) {
|
||||
sum = _patchLabelGrid.data.length;
|
||||
patches.length = 0;
|
||||
while (sum--) {
|
||||
if (_patchLabelGrid.data[sum] === topLabels[i].label) {
|
||||
patch = _imageToPatchGrid.data[sum];
|
||||
patches.push(patch);
|
||||
}
|
||||
}
|
||||
box = boxFromPatches(patches);
|
||||
if (box) {
|
||||
boxes.push(box);
|
||||
|
||||
// draw patch-labels if requested
|
||||
if (_config.showRemainingPatchLabels) {
|
||||
for ( j = 0; j < patches.length; j++) {
|
||||
patch = patches[j];
|
||||
hsv[0] = (topLabels[i].label / (maxLabel + 1)) * 360;
|
||||
CVUtils.hsv2rgb(hsv, rgb);
|
||||
ImageDebug.drawRect(patch.pos, _subImageWrapper.size, _canvasContainer.ctx.binary,
|
||||
{color: "rgb(" + rgb.join(",") + ")", lineWidth: 2});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return boxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find similar moments (via cluster)
|
||||
* @param {Object} moments
|
||||
*/
|
||||
function similarMoments(moments) {
|
||||
var clusters = CVUtils.cluster(moments, 0.90);
|
||||
var topCluster = CVUtils.topGeneric(clusters, 1, function(e) {
|
||||
return e.getPoints().length;
|
||||
});
|
||||
var points = [], result = [];
|
||||
if (topCluster.length === 1) {
|
||||
points = topCluster[0].item.getPoints();
|
||||
for (var i = 0; i < points.length; i++) {
|
||||
result.push(points[i].point);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function skeletonize(x, y) {
|
||||
_binaryImageWrapper.subImageAsCopy(_subImageWrapper, CVUtils.imageRef(x, y));
|
||||
_skeletonizer.skeletonize();
|
||||
|
||||
// Show skeleton if requested
|
||||
if (_config.showSkeleton) {
|
||||
_skelImageWrapper.overlay(_canvasContainer.dom.binary, 360, CVUtils.imageRef(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts and describes those patches which seem to contain a barcode pattern
|
||||
* @param {Array} moments
|
||||
* @param {Object} patchPos,
|
||||
* @param {Number} x
|
||||
* @param {Number} y
|
||||
* @returns {Array} list of patches
|
||||
*/
|
||||
function describePatch(moments, patchPos, x, y) {
|
||||
var k,
|
||||
avg,
|
||||
eligibleMoments = [],
|
||||
matchingMoments,
|
||||
patch,
|
||||
patchesFound = [],
|
||||
minComponentWeight = Math.ceil(_patchSize.x / 3);
|
||||
|
||||
if (moments.length >= 2) {
|
||||
// only collect moments which's area covers at least minComponentWeight pixels.
|
||||
for ( k = 0; k < moments.length; k++) {
|
||||
if (moments[k].m00 > minComponentWeight) {
|
||||
eligibleMoments.push(moments[k]);
|
||||
}
|
||||
}
|
||||
|
||||
// if at least 2 moments are found which have at least minComponentWeights covered
|
||||
if (eligibleMoments.length >= 2) {
|
||||
matchingMoments = similarMoments(eligibleMoments);
|
||||
avg = 0;
|
||||
// determine the similarity of the moments
|
||||
for ( k = 0; k < matchingMoments.length; k++) {
|
||||
avg += matchingMoments[k].rad;
|
||||
}
|
||||
|
||||
// Only two of the moments are allowed not to fit into the equation
|
||||
// add the patch to the set
|
||||
if (matchingMoments.length > 1
|
||||
&& matchingMoments.length >= (eligibleMoments.length / 4) * 3
|
||||
&& matchingMoments.length > moments.length / 4) {
|
||||
avg /= matchingMoments.length;
|
||||
patch = {
|
||||
index: patchPos[1] * _numPatches.x + patchPos[0],
|
||||
pos: {
|
||||
x: x,
|
||||
y: y
|
||||
},
|
||||
box: [
|
||||
vec2.clone([x, y]),
|
||||
vec2.clone([x + _subImageWrapper.size.x, y]),
|
||||
vec2.clone([x + _subImageWrapper.size.x, y + _subImageWrapper.size.y]),
|
||||
vec2.clone([x, y + _subImageWrapper.size.y])
|
||||
],
|
||||
moments: matchingMoments,
|
||||
rad: avg,
|
||||
vec: vec2.clone([Math.cos(avg), Math.sin(avg)])
|
||||
};
|
||||
patchesFound.push(patch);
|
||||
}
|
||||
}
|
||||
}
|
||||
return patchesFound;
|
||||
}
|
||||
|
||||
/**
|
||||
* finds patches which are connected and share the same orientation
|
||||
* @param {Object} patchesFound
|
||||
*/
|
||||
function rasterizeAngularSimilarity(patchesFound) {
|
||||
var label = 0,
|
||||
threshold = 0.95,
|
||||
currIdx = 0,
|
||||
j,
|
||||
patch,
|
||||
hsv = [0, 1, 1],
|
||||
rgb = [0, 0, 0];
|
||||
|
||||
function notYetProcessed() {
|
||||
var i;
|
||||
for ( i = 0; i < _patchLabelGrid.data.length; i++) {
|
||||
if (_patchLabelGrid.data[i] === 0 && _patchGrid.data[i] === 1) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return _patchLabelGrid.length;
|
||||
}
|
||||
|
||||
function trace(currentIdx) {
|
||||
var x,
|
||||
y,
|
||||
currentPatch,
|
||||
idx,
|
||||
dir,
|
||||
current = {
|
||||
x: currentIdx % _patchLabelGrid.size.x,
|
||||
y: (currentIdx / _patchLabelGrid.size.x) | 0
|
||||
},
|
||||
similarity;
|
||||
|
||||
if (currentIdx < _patchLabelGrid.data.length) {
|
||||
currentPatch = _imageToPatchGrid.data[currentIdx];
|
||||
// assign label
|
||||
_patchLabelGrid.data[currentIdx] = label;
|
||||
for ( dir = 0; dir < Tracer.searchDirections.length; dir++) {
|
||||
y = current.y + Tracer.searchDirections[dir][0];
|
||||
x = current.x + Tracer.searchDirections[dir][1];
|
||||
idx = y * _patchLabelGrid.size.x + x;
|
||||
|
||||
// continue if patch empty
|
||||
if (_patchGrid.data[idx] === 0) {
|
||||
_patchLabelGrid.data[idx] = Number.MAX_VALUE;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_patchLabelGrid.data[idx] === 0) {
|
||||
similarity = Math.abs(vec2.dot(_imageToPatchGrid.data[idx].vec, currentPatch.vec));
|
||||
if (similarity > threshold) {
|
||||
trace(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// prepare for finding the right patches
|
||||
ArrayHelper.init(_patchGrid.data, 0);
|
||||
ArrayHelper.init(_patchLabelGrid.data, 0);
|
||||
ArrayHelper.init(_imageToPatchGrid.data, null);
|
||||
|
||||
for ( j = 0; j < patchesFound.length; j++) {
|
||||
patch = patchesFound[j];
|
||||
_imageToPatchGrid.data[patch.index] = patch;
|
||||
_patchGrid.data[patch.index] = 1;
|
||||
}
|
||||
|
||||
// rasterize the patches found to determine area
|
||||
_patchGrid.zeroBorder();
|
||||
|
||||
while (( currIdx = notYetProcessed()) < _patchLabelGrid.data.length) {
|
||||
label++;
|
||||
trace(currIdx);
|
||||
}
|
||||
|
||||
// draw patch-labels if requested
|
||||
if (_config.showPatchLabels) {
|
||||
for ( j = 0; j < _patchLabelGrid.data.length; j++) {
|
||||
if (_patchLabelGrid.data[j] > 0 && _patchLabelGrid.data[j] <= label) {
|
||||
patch = _imageToPatchGrid.data[j];
|
||||
hsv[0] = (_patchLabelGrid.data[j] / (label + 1)) * 360;
|
||||
CVUtils.hsv2rgb(hsv, rgb);
|
||||
ImageDebug.drawRect(patch.pos, _subImageWrapper.size, _canvasContainer.ctx.binary,
|
||||
{color: "rgb(" + rgb.join(",") + ")", lineWidth: 2});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
export default {
|
||||
init: function(inputImageWrapper, config) {
|
||||
_config = config;
|
||||
_inputImageWrapper = inputImageWrapper;
|
||||
|
||||
initBuffers();
|
||||
initCanvas();
|
||||
},
|
||||
|
||||
locate: function() {
|
||||
var patchesFound,
|
||||
topLabels,
|
||||
boxes;
|
||||
|
||||
if (_config.halfSample) {
|
||||
CVUtils.halfSample(_inputImageWrapper, _currentImageWrapper);
|
||||
}
|
||||
|
||||
binarizeImage();
|
||||
patchesFound = findPatches();
|
||||
// return unless 5% or more patches are found
|
||||
if (patchesFound.length < _numPatches.x * _numPatches.y * 0.05) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// rasterrize area by comparing angular similarity;
|
||||
var maxLabel = rasterizeAngularSimilarity(patchesFound);
|
||||
if (maxLabel < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// search for area with the most patches (biggest connected area)
|
||||
topLabels = findBiggestConnectedAreas(maxLabel);
|
||||
if (topLabels.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
boxes = findBoxes(topLabels, maxLabel);
|
||||
return boxes;
|
||||
},
|
||||
|
||||
checkImageConstraints: function(inputStream, config) {
|
||||
var patchSize,
|
||||
width = inputStream.getWidth(),
|
||||
height = inputStream.getHeight(),
|
||||
halfSample = config.halfSample ? 0.5 : 1,
|
||||
size,
|
||||
area;
|
||||
|
||||
// calculate width and height based on area
|
||||
if (inputStream.getConfig().area) {
|
||||
area = CVUtils.computeImageArea(width, height, inputStream.getConfig().area);
|
||||
inputStream.setTopRight({x: area.sx, y: area.sy});
|
||||
inputStream.setCanvasSize({x: width, y: height});
|
||||
width = area.sw;
|
||||
height = area.sh;
|
||||
}
|
||||
|
||||
size = {
|
||||
x: Math.floor(width * halfSample),
|
||||
y: Math.floor(height * halfSample)
|
||||
};
|
||||
|
||||
patchSize = CVUtils.calculatePatchSize(config.patchSize, size);
|
||||
console.log("Patch-Size: " + JSON.stringify(patchSize));
|
||||
|
||||
inputStream.setWidth(Math.floor(Math.floor(size.x / patchSize.x) * (1 / halfSample) * patchSize.x));
|
||||
inputStream.setHeight(Math.floor(Math.floor(size.y / patchSize.y) * (1 / halfSample) * patchSize.y));
|
||||
|
||||
if ((inputStream.getWidth() % patchSize.x) === 0 && (inputStream.getHeight() % patchSize.y) === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new Error("Image dimensions do not comply with the current settings: Width (" +
|
||||
width + " )and height (" + height +
|
||||
") must a multiple of " + patchSize.x);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,195 @@
|
||||
import Tracer from './tracer';
|
||||
|
||||
/**
|
||||
* http://www.codeproject.com/Tips/407172/Connected-Component-Labeling-and-Vectorization
|
||||
*/
|
||||
var Rasterizer = {
|
||||
createContour2D: function() {
|
||||
return {
|
||||
dir: null,
|
||||
index: null,
|
||||
firstVertex: null,
|
||||
insideContours: null,
|
||||
nextpeer: null,
|
||||
prevpeer: null
|
||||
};
|
||||
},
|
||||
CONTOUR_DIR: {
|
||||
CW_DIR: 0,
|
||||
CCW_DIR: 1,
|
||||
UNKNOWN_DIR: 2
|
||||
},
|
||||
DIR: {
|
||||
OUTSIDE_EDGE: -32767,
|
||||
INSIDE_EDGE: -32766
|
||||
},
|
||||
create: function(imageWrapper, labelWrapper) {
|
||||
var imageData = imageWrapper.data,
|
||||
labelData = labelWrapper.data,
|
||||
width = imageWrapper.size.x,
|
||||
height = imageWrapper.size.y,
|
||||
tracer = Tracer.create(imageWrapper, labelWrapper);
|
||||
|
||||
return {
|
||||
rasterize: function(depthlabel) {
|
||||
var color,
|
||||
bc,
|
||||
lc,
|
||||
labelindex,
|
||||
cx,
|
||||
cy,
|
||||
colorMap = [],
|
||||
vertex,
|
||||
p,
|
||||
cc,
|
||||
sc,
|
||||
pos,
|
||||
connectedCount = 0,
|
||||
i;
|
||||
|
||||
for ( i = 0; i < 400; i++) {
|
||||
colorMap[i] = 0;
|
||||
}
|
||||
|
||||
colorMap[0] = imageData[0];
|
||||
cc = null;
|
||||
for ( cy = 1; cy < height - 1; cy++) {
|
||||
labelindex = 0;
|
||||
bc = colorMap[0];
|
||||
for ( cx = 1; cx < width - 1; cx++) {
|
||||
pos = cy * width + cx;
|
||||
if (labelData[pos] === 0) {
|
||||
color = imageData[pos];
|
||||
if (color !== bc) {
|
||||
if (labelindex === 0) {
|
||||
lc = connectedCount + 1;
|
||||
colorMap[lc] = color;
|
||||
bc = color;
|
||||
vertex = tracer.contourTracing(cy, cx, lc, color, Rasterizer.DIR.OUTSIDE_EDGE);
|
||||
if (vertex !== null) {
|
||||
connectedCount++;
|
||||
labelindex = lc;
|
||||
p = Rasterizer.createContour2D();
|
||||
p.dir = Rasterizer.CONTOUR_DIR.CW_DIR;
|
||||
p.index = labelindex;
|
||||
p.firstVertex = vertex;
|
||||
p.nextpeer = cc;
|
||||
p.insideContours = null;
|
||||
if (cc !== null) {
|
||||
cc.prevpeer = p;
|
||||
}
|
||||
cc = p;
|
||||
}
|
||||
} else {
|
||||
vertex = tracer
|
||||
.contourTracing(cy, cx, Rasterizer.DIR.INSIDE_EDGE, color, labelindex);
|
||||
if (vertex !== null) {
|
||||
p = Rasterizer.createContour2D();
|
||||
p.firstVertex = vertex;
|
||||
p.insideContours = null;
|
||||
if (depthlabel === 0) {
|
||||
p.dir = Rasterizer.CONTOUR_DIR.CCW_DIR;
|
||||
} else {
|
||||
p.dir = Rasterizer.CONTOUR_DIR.CW_DIR;
|
||||
}
|
||||
p.index = depthlabel;
|
||||
sc = cc;
|
||||
while ((sc !== null) && sc.index !== labelindex) {
|
||||
sc = sc.nextpeer;
|
||||
}
|
||||
if (sc !== null) {
|
||||
p.nextpeer = sc.insideContours;
|
||||
if (sc.insideContours !== null) {
|
||||
sc.insideContours.prevpeer = p;
|
||||
}
|
||||
sc.insideContours = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
labelData[pos] = labelindex;
|
||||
}
|
||||
} else if (labelData[pos] === Rasterizer.DIR.OUTSIDE_EDGE
|
||||
|| labelData[pos] === Rasterizer.DIR.INSIDE_EDGE) {
|
||||
labelindex = 0;
|
||||
if (labelData[pos] === Rasterizer.DIR.INSIDE_EDGE) {
|
||||
bc = imageData[pos];
|
||||
} else {
|
||||
bc = colorMap[0];
|
||||
}
|
||||
} else {
|
||||
labelindex = labelData[pos];
|
||||
bc = colorMap[labelindex];
|
||||
}
|
||||
}
|
||||
}
|
||||
sc = cc;
|
||||
while (sc !== null) {
|
||||
sc.index = depthlabel;
|
||||
sc = sc.nextpeer;
|
||||
}
|
||||
return {
|
||||
cc: cc,
|
||||
count: connectedCount
|
||||
};
|
||||
},
|
||||
debug: {
|
||||
drawContour: function(canvas, firstContour) {
|
||||
var ctx = canvas.getContext("2d"),
|
||||
pq = firstContour,
|
||||
iq,
|
||||
q,
|
||||
p;
|
||||
|
||||
ctx.strokeStyle = "red";
|
||||
ctx.fillStyle = "red";
|
||||
ctx.lineWidth = 1;
|
||||
|
||||
if (pq !== null) {
|
||||
iq = pq.insideContours;
|
||||
} else {
|
||||
iq = null;
|
||||
}
|
||||
|
||||
while (pq !== null) {
|
||||
if (iq !== null) {
|
||||
q = iq;
|
||||
iq = iq.nextpeer;
|
||||
} else {
|
||||
q = pq;
|
||||
pq = pq.nextpeer;
|
||||
if (pq !== null) {
|
||||
iq = pq.insideContours;
|
||||
} else {
|
||||
iq = null;
|
||||
}
|
||||
}
|
||||
|
||||
switch (q.dir) {
|
||||
case Rasterizer.CONTOUR_DIR.CW_DIR:
|
||||
ctx.strokeStyle = "red";
|
||||
break;
|
||||
case Rasterizer.CONTOUR_DIR.CCW_DIR:
|
||||
ctx.strokeStyle = "blue";
|
||||
break;
|
||||
case Rasterizer.CONTOUR_DIR.UNKNOWN_DIR:
|
||||
ctx.strokeStyle = "green";
|
||||
break;
|
||||
}
|
||||
|
||||
p = q.firstVertex;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p.x, p.y);
|
||||
do {
|
||||
p = p.next;
|
||||
ctx.lineTo(p.x, p.y);
|
||||
} while (p !== q.firstVertex);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default Rasterizer;
|
||||
@@ -0,0 +1,209 @@
|
||||
/* @preserve ASM BEGIN */
|
||||
/* eslint-disable eqeqeq*/
|
||||
function Skeletonizer(stdlib, foreign, buffer) {
|
||||
"use asm";
|
||||
|
||||
var images = new stdlib.Uint8Array(buffer),
|
||||
size = foreign.size | 0,
|
||||
imul = stdlib.Math.imul;
|
||||
|
||||
function erode(inImagePtr, outImagePtr) {
|
||||
inImagePtr = inImagePtr | 0;
|
||||
outImagePtr = outImagePtr | 0;
|
||||
|
||||
var v = 0,
|
||||
u = 0,
|
||||
sum = 0,
|
||||
yStart1 = 0,
|
||||
yStart2 = 0,
|
||||
xStart1 = 0,
|
||||
xStart2 = 0,
|
||||
offset = 0;
|
||||
|
||||
for ( v = 1; (v | 0) < ((size - 1) | 0); v = (v + 1) | 0) {
|
||||
offset = (offset + size) | 0;
|
||||
for ( u = 1; (u | 0) < ((size - 1) | 0); u = (u + 1) | 0) {
|
||||
yStart1 = (offset - size) | 0;
|
||||
yStart2 = (offset + size) | 0;
|
||||
xStart1 = (u - 1) | 0;
|
||||
xStart2 = (u + 1) | 0;
|
||||
sum = ((images[(inImagePtr + yStart1 + xStart1) | 0] | 0)
|
||||
+ (images[(inImagePtr + yStart1 + xStart2) | 0] | 0)
|
||||
+ (images[(inImagePtr + offset + u) | 0] | 0)
|
||||
+ (images[(inImagePtr + yStart2 + xStart1) | 0] | 0)
|
||||
+ (images[(inImagePtr + yStart2 + xStart2) | 0] | 0)) | 0;
|
||||
if ((sum | 0) == (5 | 0)) {
|
||||
images[(outImagePtr + offset + u) | 0] = 1;
|
||||
} else {
|
||||
images[(outImagePtr + offset + u) | 0] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
function subtract(aImagePtr, bImagePtr, outImagePtr) {
|
||||
aImagePtr = aImagePtr | 0;
|
||||
bImagePtr = bImagePtr | 0;
|
||||
outImagePtr = outImagePtr | 0;
|
||||
|
||||
var length = 0;
|
||||
|
||||
length = imul(size, size) | 0;
|
||||
|
||||
while ((length | 0) > 0) {
|
||||
length = (length - 1) | 0;
|
||||
images[(outImagePtr + length) | 0] =
|
||||
((images[(aImagePtr + length) | 0] | 0) - (images[(bImagePtr + length) | 0] | 0)) | 0;
|
||||
}
|
||||
}
|
||||
|
||||
function bitwiseOr(aImagePtr, bImagePtr, outImagePtr) {
|
||||
aImagePtr = aImagePtr | 0;
|
||||
bImagePtr = bImagePtr | 0;
|
||||
outImagePtr = outImagePtr | 0;
|
||||
|
||||
var length = 0;
|
||||
|
||||
length = imul(size, size) | 0;
|
||||
|
||||
while ((length | 0) > 0) {
|
||||
length = (length - 1) | 0;
|
||||
images[(outImagePtr + length) | 0] =
|
||||
((images[(aImagePtr + length) | 0] | 0) | (images[(bImagePtr + length) | 0] | 0)) | 0;
|
||||
}
|
||||
}
|
||||
|
||||
function countNonZero(imagePtr) {
|
||||
imagePtr = imagePtr | 0;
|
||||
|
||||
var sum = 0,
|
||||
length = 0;
|
||||
|
||||
length = imul(size, size) | 0;
|
||||
|
||||
while ((length | 0) > 0) {
|
||||
length = (length - 1) | 0;
|
||||
sum = ((sum | 0) + (images[(imagePtr + length) | 0] | 0)) | 0;
|
||||
}
|
||||
|
||||
return (sum | 0);
|
||||
}
|
||||
|
||||
function init(imagePtr, value) {
|
||||
imagePtr = imagePtr | 0;
|
||||
value = value | 0;
|
||||
|
||||
var length = 0;
|
||||
|
||||
length = imul(size, size) | 0;
|
||||
|
||||
while ((length | 0) > 0) {
|
||||
length = (length - 1) | 0;
|
||||
images[(imagePtr + length) | 0] = value;
|
||||
}
|
||||
}
|
||||
|
||||
function dilate(inImagePtr, outImagePtr) {
|
||||
inImagePtr = inImagePtr | 0;
|
||||
outImagePtr = outImagePtr | 0;
|
||||
|
||||
var v = 0,
|
||||
u = 0,
|
||||
sum = 0,
|
||||
yStart1 = 0,
|
||||
yStart2 = 0,
|
||||
xStart1 = 0,
|
||||
xStart2 = 0,
|
||||
offset = 0;
|
||||
|
||||
for ( v = 1; (v | 0) < ((size - 1) | 0); v = (v + 1) | 0) {
|
||||
offset = (offset + size) | 0;
|
||||
for ( u = 1; (u | 0) < ((size - 1) | 0); u = (u + 1) | 0) {
|
||||
yStart1 = (offset - size) | 0;
|
||||
yStart2 = (offset + size) | 0;
|
||||
xStart1 = (u - 1) | 0;
|
||||
xStart2 = (u + 1) | 0;
|
||||
sum = ((images[(inImagePtr + yStart1 + xStart1) | 0] | 0)
|
||||
+ (images[(inImagePtr + yStart1 + xStart2) | 0] | 0)
|
||||
+ (images[(inImagePtr + offset + u) | 0] | 0)
|
||||
+ (images[(inImagePtr + yStart2 + xStart1) | 0] | 0)
|
||||
+ (images[(inImagePtr + yStart2 + xStart2) | 0] | 0)) | 0;
|
||||
if ((sum | 0) > (0 | 0)) {
|
||||
images[(outImagePtr + offset + u) | 0] = 1;
|
||||
} else {
|
||||
images[(outImagePtr + offset + u) | 0] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
function memcpy(srcImagePtr, dstImagePtr) {
|
||||
srcImagePtr = srcImagePtr | 0;
|
||||
dstImagePtr = dstImagePtr | 0;
|
||||
|
||||
var length = 0;
|
||||
|
||||
length = imul(size, size) | 0;
|
||||
|
||||
while ((length | 0) > 0) {
|
||||
length = (length - 1) | 0;
|
||||
images[(dstImagePtr + length) | 0] = (images[(srcImagePtr + length) | 0] | 0);
|
||||
}
|
||||
}
|
||||
|
||||
function zeroBorder(imagePtr) {
|
||||
imagePtr = imagePtr | 0;
|
||||
|
||||
var x = 0,
|
||||
y = 0;
|
||||
|
||||
for ( x = 0; (x | 0) < ((size - 1) | 0); x = (x + 1) | 0) {
|
||||
images[(imagePtr + x) | 0] = 0;
|
||||
images[(imagePtr + y) | 0] = 0;
|
||||
y = ((y + size) - 1) | 0;
|
||||
images[(imagePtr + y) | 0] = 0;
|
||||
y = (y + 1) | 0;
|
||||
}
|
||||
for ( x = 0; (x | 0) < (size | 0); x = (x + 1) | 0) {
|
||||
images[(imagePtr + y) | 0] = 0;
|
||||
y = (y + 1) | 0;
|
||||
}
|
||||
}
|
||||
|
||||
function skeletonize() {
|
||||
var subImagePtr = 0,
|
||||
erodedImagePtr = 0,
|
||||
tempImagePtr = 0,
|
||||
skelImagePtr = 0,
|
||||
sum = 0,
|
||||
done = 0;
|
||||
|
||||
erodedImagePtr = imul(size, size) | 0;
|
||||
tempImagePtr = (erodedImagePtr + erodedImagePtr) | 0;
|
||||
skelImagePtr = (tempImagePtr + erodedImagePtr) | 0;
|
||||
|
||||
// init skel-image
|
||||
init(skelImagePtr, 0);
|
||||
zeroBorder(subImagePtr);
|
||||
|
||||
do {
|
||||
erode(subImagePtr, erodedImagePtr);
|
||||
dilate(erodedImagePtr, tempImagePtr);
|
||||
subtract(subImagePtr, tempImagePtr, tempImagePtr);
|
||||
bitwiseOr(skelImagePtr, tempImagePtr, skelImagePtr);
|
||||
memcpy(erodedImagePtr, subImagePtr);
|
||||
sum = countNonZero(subImagePtr) | 0;
|
||||
done = ((sum | 0) == 0 | 0);
|
||||
} while (!done);
|
||||
}
|
||||
|
||||
return {
|
||||
skeletonize: skeletonize
|
||||
};
|
||||
}
|
||||
|
||||
export default Skeletonizer;
|
||||
/* eslint-enable eqeqeq*/
|
||||
/* @preserve ASM END */
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* http://www.codeproject.com/Tips/407172/Connected-Component-Labeling-and-Vectorization
|
||||
*/
|
||||
var Tracer = {
|
||||
searchDirections: [[0, 1], [1, 1], [1, 0], [1, -1], [0, -1], [-1, -1], [-1, 0], [-1, 1]],
|
||||
create: function(imageWrapper, labelWrapper) {
|
||||
var imageData = imageWrapper.data,
|
||||
labelData = labelWrapper.data,
|
||||
searchDirections = this.searchDirections,
|
||||
width = imageWrapper.size.x,
|
||||
pos;
|
||||
|
||||
function trace(current, color, label, edgelabel) {
|
||||
var i,
|
||||
y,
|
||||
x;
|
||||
|
||||
for ( i = 0; i < 7; i++) {
|
||||
y = current.cy + searchDirections[current.dir][0];
|
||||
x = current.cx + searchDirections[current.dir][1];
|
||||
pos = y * width + x;
|
||||
if ((imageData[pos] === color) && ((labelData[pos] === 0) || (labelData[pos] === label))) {
|
||||
labelData[pos] = label;
|
||||
current.cy = y;
|
||||
current.cx = x;
|
||||
return true;
|
||||
} else {
|
||||
if (labelData[pos] === 0) {
|
||||
labelData[pos] = edgelabel;
|
||||
}
|
||||
current.dir = (current.dir + 1) % 8;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function vertex2D(x, y, dir) {
|
||||
return {
|
||||
dir: dir,
|
||||
x: x,
|
||||
y: y,
|
||||
next: null,
|
||||
prev: null
|
||||
};
|
||||
}
|
||||
|
||||
function contourTracing(sy, sx, label, color, edgelabel) {
|
||||
var Fv = null,
|
||||
Cv,
|
||||
P,
|
||||
ldir,
|
||||
current = {
|
||||
cx: sx,
|
||||
cy: sy,
|
||||
dir: 0
|
||||
};
|
||||
|
||||
if (trace(current, color, label, edgelabel)) {
|
||||
Fv = vertex2D(sx, sy, current.dir);
|
||||
Cv = Fv;
|
||||
ldir = current.dir;
|
||||
P = vertex2D(current.cx, current.cy, 0);
|
||||
P.prev = Cv;
|
||||
Cv.next = P;
|
||||
P.next = null;
|
||||
Cv = P;
|
||||
do {
|
||||
current.dir = (current.dir + 6) % 8;
|
||||
trace(current, color, label, edgelabel);
|
||||
if (ldir !== current.dir) {
|
||||
Cv.dir = current.dir;
|
||||
P = vertex2D(current.cx, current.cy, 0);
|
||||
P.prev = Cv;
|
||||
Cv.next = P;
|
||||
P.next = null;
|
||||
Cv = P;
|
||||
} else {
|
||||
Cv.dir = ldir;
|
||||
Cv.x = current.cx;
|
||||
Cv.y = current.cy;
|
||||
}
|
||||
ldir = current.dir;
|
||||
} while (current.cx !== sx || current.cy !== sy);
|
||||
Fv.prev = Cv.prev;
|
||||
Cv.prev.next = Fv;
|
||||
}
|
||||
return Fv;
|
||||
}
|
||||
|
||||
return {
|
||||
trace: function(current, color, label, edgelabel) {
|
||||
return trace(current, color, label, edgelabel);
|
||||
},
|
||||
contourTracing: function(sy, sx, label, color, edgelabel) {
|
||||
return contourTracing(sy, sx, label, color, edgelabel);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default (Tracer);
|
||||
Reference in New Issue
Block a user