UNPKG

getuserbarcode

Version:

An advanced barcode-scanner written in JavaScript

608 lines (532 loc) 19.1 kB
import ImageWrapper from '../common/image_wrapper'; import { calculatePatchSize, otsuThreshold, hsv2rgb, cluster, topGeneric, imageRef, halfSample, computeImageArea } 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'; const vec2 = { clone: require('gl-vec2/clone'), dot: require('gl-vec2/dot'), scale: require('gl-vec2/scale'), transformMat2: require('gl-vec2/transformMat2') }; const mat2 = { copy: require('gl-mat2/copy'), create: require('gl-mat2/create'), invert: require('gl-mat2/invert') } 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; 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 = 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 (ENV.development && _config.debug.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 (ENV.development && _config.debug.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.copy(mat2.create(), [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 (ENV.development && _config.debug.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 (ENV.development && _config.debug.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 (ENV.development && _config.debug.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() { otsuThreshold(_currentImageWrapper, _binaryImageWrapper); _binaryImageWrapper.zeroBorder(); if (ENV.development && _config.debug.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 (ENV.development && _config.debug.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 (ENV.development && _config.debug.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 (ENV.development && _config.debug.showRemainingPatchLabels) { for ( j = 0; j < patches.length; j++) { patch = patches[j]; hsv[0] = (topLabels[i].label / (maxLabel + 1)) * 360; 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 = cluster(moments, 0.90); var topCluster = 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, imageRef(x, y)); _skeletonizer.skeletonize(); // Show skeleton if requested if (ENV.development && _config.debug.showSkeleton) { _skelImageWrapper.overlay(_canvasContainer.dom.binary, 360, 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 (ENV.development && _config.debug.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; 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) { 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 = 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 = calculatePatchSize(config.patchSize, size); if (ENV.development) { 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); } };