Added clipping area; added buffer for efficient memory-allocations;

feature/image-source
Christoph Oberhofer 8 years ago
parent 4a56fff2cb
commit 22f48ea541

@ -146,10 +146,11 @@ $(function() {
inputStream: { inputStream: {
type : "LiveStream", type : "LiveStream",
constraints: { constraints: {
width: {ideal: 640}, width: {ideal: 480},
height: {ideal: 480}, height: {ideal: 480},
zoom: {exact: 2},
facingMode: "environment", facingMode: "environment",
aspectRatio: {min: 1, max: 2} aspectRatio: 1,
} }
}, },
locator: { locator: {
@ -157,10 +158,10 @@ $(function() {
halfSample: true halfSample: true
}, },
numOfWorkers: 2, numOfWorkers: 2,
frequency: 10, frequency: 2,
decoder: { decoder: {
readers : [{ readers : [{
format: "code_128_reader", format: "ean_reader",
config: {} config: {}
}] }]
}, },

@ -0,0 +1,45 @@
import {DEBUG, log} from './log';
const debug = log.bind(null, DEBUG, "buffers.js");
let buffers = [];
export function aquire(bytes) {
const allocation = findBySize(bytes);
if (allocation) {
debug(`reusing ${bytes}`, debugSize);
return allocation;
}
debug(`allocating ${bytes}`, debugSize);
const buffer = new ArrayBuffer(bytes);
return buffer;
}
export function release(buffer) {
if (!buffer) {
throw new Error("Buffer not defined");
}
buffers.push(buffer);
debug('release', debugSize);
}
export function releaseAll() {
buffers = [];
}
function debugSize() {
return "size: " + Object
.keys(buffers)
.filter((key) => buffers[key] !== null)
.length;
}
function findBySize(bytes) {
for (let i = 0; i < buffers.length; i++) {
if (buffers[i].byteLength === bytes) {
let allocation = buffers[i];
buffers.splice(i, 1);
return allocation;
}
}
return null;
}

@ -0,0 +1,16 @@
export const DEBUG = "debug";
export function log(level, scope, ...rest) {
if (level !== DEBUG) {
console.log(`${level}: ${scope} - ${msg(rest)}`);
}
}
function msg(args) {
return args.map(arg => {
if (typeof arg === 'function') {
return arg();
}
return arg;
}).join(', ');
}

@ -2,6 +2,7 @@ module.exports = {
numOfWorkers: 0, numOfWorkers: 0,
locate: true, locate: true,
target: '#interactive.viewport', target: '#interactive.viewport',
frequency: 5,
constraints: { constraints: {
width: 640, width: 640,
height: 640, height: 640,

@ -1,8 +1,10 @@
import {memoize} from 'lodash';
import { import {
computeGray computeGray,
computeImageArea,
} from '../common/cv_utils'; } from '../common/cv_utils';
import {sleep} from '../common/utils'; import {sleep, getViewport} from '../common/utils';
import {getViewport} from '../common/utils'; import {aquire} from '../common/buffers';
function adjustCanvasSize(input, canvas) { function adjustCanvasSize(input, canvas) {
if (input instanceof HTMLVideoElement) { if (input instanceof HTMLVideoElement) {
@ -43,7 +45,6 @@ export function fromSource(source, {target = "#interactive.viewport"} = {}) {
var drawable = source.getDrawable(); var drawable = source.getDrawable();
var $canvas = null; var $canvas = null;
var ctx = null; var ctx = null;
var bytePool = [];
if (drawable instanceof HTMLVideoElement if (drawable instanceof HTMLVideoElement
|| drawable instanceof HTMLImageElement) { || drawable instanceof HTMLImageElement) {
@ -56,24 +57,14 @@ export function fromSource(source, {target = "#interactive.viewport"} = {}) {
ctx = drawable.getContext('2d'); ctx = drawable.getContext('2d');
} }
function nextAvailableBuffer() { function nextAvailableBuffer(bytesRequired) {
var i; return new Uint8Array(aquire(bytesRequired));
var buffer;
var bytesRequired = ($canvas.height * $canvas.width);
for (i = 0; i < bytePool.length; i++) {
buffer = bytePool[i];
if (buffer && buffer.buffer.byteLength === bytesRequired) {
return bytePool[i];
}
}
buffer = new Uint8Array(bytesRequired);
bytePool.push(buffer);
console.log("Added new entry to bufferPool", bytesRequired);
return buffer;
} }
return { return {
grabFrameData: function grabFrameData({buffer, clipping}) { grabFrameData: function grabFrameData({clipping} = {}) {
const {viewport, canvas: canvasSize} = source.getDimensions(); const {viewport, canvas: canvasSize} = source.getDimensions();
const sx = viewport.x; const sx = viewport.x;
const sy = viewport.y; const sy = viewport.y;
@ -84,20 +75,38 @@ export function fromSource(source, {target = "#interactive.viewport"} = {}) {
const dWidth = canvasSize.width; const dWidth = canvasSize.width;
const dHeight = canvasSize.height; const dHeight = canvasSize.height;
console.time("clipp")
clipping = clipping ? clipping(canvasSize) : {
x: 0,
y: 0,
width: canvasSize.width,
height: canvasSize.height,
};
adjustCanvasSize(canvasSize, $canvas); adjustCanvasSize(canvasSize, $canvas);
if ($canvas.height < 10 || $canvas.width < 10) { if ($canvas.height < 10 || $canvas.width < 10) {
console.log('$canvas not initialized. Waiting 100ms and then continuing');
return sleep(100).then(grabFrameData); return sleep(100).then(grabFrameData);
} }
if (!(drawable instanceof HTMLCanvasElement)) { if (!(drawable instanceof HTMLCanvasElement)) {
ctx.drawImage(drawable, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight); ctx.drawImage(drawable, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
} }
var imageData = ctx.getImageData(0, 0, $canvas.width, $canvas.height).data; var imageData = ctx.getImageData(
var imageBuffer = buffer ? buffer : nextAvailableBuffer(); clipping.x,
clipping.y,
clipping.width,
clipping.height
).data;
var imageBuffer = nextAvailableBuffer(clipping.width * clipping.height);
computeGray(imageData, imageBuffer); computeGray(imageData, imageBuffer);
return Promise.resolve({ return Promise.resolve({
width: $canvas.width, width: clipping.width,
height: $canvas.height, height: clipping.height,
dimensions: {
viewport,
canvas: canvasSize,
clipping,
},
data: imageBuffer, data: imageBuffer,
}); });
}, },

@ -14,6 +14,11 @@ import ImageDebug from '../common/image_debug';
import Rasterizer from './rasterizer'; import Rasterizer from './rasterizer';
import Tracer from './tracer'; import Tracer from './tracer';
import skeletonizer from './skeletonizer'; import skeletonizer from './skeletonizer';
import {DEBUG, log} from '../common/log';
const debug = log.bind(null, DEBUG, "barcode_locator.js");
const vec2 = { const vec2 = {
clone: require('gl-vec2/clone'), clone: require('gl-vec2/clone'),
dot: require('gl-vec2/dot'), dot: require('gl-vec2/dot'),
@ -563,38 +568,44 @@ export default function createLocator(inputImageWrapper, config) {
} }
}; };
} }
export function checkImageConstraints(inputStream, config) { export function checkImageConstraints({canvasSize, area, patchSize, halfSample: half}) {
var patchSize, var patchSize,
width = inputStream.getWidth(), width = canvasSize.width,
height = inputStream.getHeight(), height = canvasSize.height,
halfSample = config.halfSample ? 0.5 : 1, half = half ? 0.5 : 1,
size, size,
area; clipping = {
x: 0,
// calculate width and height based on area y: 0,
if (inputStream.getConfig().area) { width,
area = computeImageArea(width, height, inputStream.getConfig().area); height,
inputStream.setTopRight({x: area.sx, y: area.sy}); };
inputStream.setCanvasSize({x: width, y: height});
width = area.sw; if (area) {
height = area.sh; const imageArea = computeImageArea(width, height, area);
clipping.x = imageArea.sx;
clipping.y = imageArea.sy;
clipping.width = width = imageArea.sw;
clipping.height = height = imageArea.sh;
} }
size = { size = {
x: Math.floor(width * halfSample), x: Math.floor(width * half),
y: Math.floor(height * halfSample) y: Math.floor(height * half)
}; };
patchSize = calculatePatchSize(config.patchSize, size); patchSize = calculatePatchSize(patchSize, size);
if (ENV.development) { if (ENV.development) {
console.log("Patch-Size: " + JSON.stringify(patchSize)); debug("Patch-Size: " + JSON.stringify(patchSize));
} }
inputStream.setWidth(Math.floor(Math.floor(size.x / patchSize.x) * (1 / halfSample) * patchSize.x)); clipping.width = Math.floor(Math.floor(size.x / patchSize.x) * (1 / half) * patchSize.x);
inputStream.setHeight(Math.floor(Math.floor(size.y / patchSize.y) * (1 / halfSample) * patchSize.y)); clipping.height = Math.floor(Math.floor(size.y / patchSize.y) * (1 / half) * patchSize.y);
clipping.x = Math.floor((canvasSize.width - clipping.width) / 2);
clipping.y = Math.floor((canvasSize.height - clipping.height) / 2);
if ((inputStream.getWidth() % patchSize.x) === 0 && (inputStream.getHeight() % patchSize.y) === 0) { if ((clipping.width % patchSize.x) === 0 && (clipping.height % patchSize.y) === 0) {
return true; return clipping;
} }
throw new Error("Image dimensions do not comply with the current settings: Width (" + throw new Error("Image dimensions do not comply with the current settings: Width (" +

@ -1,23 +1,33 @@
import {merge, memoize} from 'lodash';
import ImageWrapper from './common/image_wrapper'; import ImageWrapper from './common/image_wrapper';
import createLocator, {checkImageConstraints} from './locator/barcode_locator'; import createLocator, {checkImageConstraints} from './locator/barcode_locator';
import BarcodeDecoder from './decoder/barcode_decoder'; import BarcodeDecoder from './decoder/barcode_decoder';
import createEventedElement from './common/events'; import createEventedElement from './common/events';
import CameraAccess from './input/camera_access'; import {release, aquire, releaseAll} from './common/buffers';
import ImageDebug from './common/image_debug';
import ResultCollector from './analytics/result_collector';
import Config from './config/config'; import Config from './config/config';
import InputStream from 'input_stream'; import CameraAccess from './input/camera_access';
import FrameGrabber from 'frame_grabber';
import {merge} from 'lodash';
const vec2 = { const vec2 = {
clone: require('gl-vec2/clone') clone: require('gl-vec2/clone')
}; };
const getDecoder = memoize((decoderConfig, _inputImageWrapper) => {
return BarcodeDecoder.create(decoderConfig, _inputImageWrapper);
}, (decoderConfig, _inputImageWrapper) => {
return JSON.stringify(Object.assign({}, decoderConfig, {width: _inputImageWrapper.size.x, height: _inputImageWrapper.size.y}));
});
const _checkImageConstraints = memoize((opts) => {
return checkImageConstraints(opts);
}, (opts) => {
return JSON.stringify(opts);
});
function createScanner(pixelCapturer) { function createScanner(pixelCapturer) {
var _inputStream, var _stopped = true,
_framegrabber,
_stopped = true,
_canvasContainer = { _canvasContainer = {
ctx: { ctx: {
image: null image: null
@ -28,7 +38,6 @@ function createScanner(pixelCapturer) {
}, },
_inputImageWrapper, _inputImageWrapper,
_boxSize, _boxSize,
_decoder,
_workerPool = [], _workerPool = [],
_onUIThread = true, _onUIThread = true,
_resultCollector, _resultCollector,
@ -39,20 +48,14 @@ function createScanner(pixelCapturer) {
const source = pixelCapturer ? pixelCapturer.getSource() : {}; const source = pixelCapturer ? pixelCapturer.getSource() : {};
function setup() { function setup() {
// checkImageConstraints(_inputStream, _config.locator);
return adjustWorkerPool(_config.numOfWorkers) return adjustWorkerPool(_config.numOfWorkers)
.then(() => { .then(() => {
if (_config.numOfWorkers === 0) { if (_config.numOfWorkers === 0) {
initializeData(); initBuffers();
} }
}); });
} }
function initializeData(imageWrapper) {
initBuffers(imageWrapper);
_decoder = BarcodeDecoder.create(_config.decoder, _inputImageWrapper);
}
function initBuffers(imageWrapper) { function initBuffers(imageWrapper) {
if (imageWrapper) { if (imageWrapper) {
_inputImageWrapper = imageWrapper; _inputImageWrapper = imageWrapper;
@ -174,7 +177,8 @@ function createScanner(pixelCapturer) {
boxes = getBoundingBoxes(); boxes = getBoundingBoxes();
if (boxes) { if (boxes) {
result = _decoder.decodeFromBoundingBoxes(boxes); result = getDecoder(_config.decoder, _inputImageWrapper)
.decodeFromBoundingBoxes(boxes);
result = result || {}; result = result || {};
result.boxes = boxes; result.boxes = boxes;
publishResult(result, _inputImageWrapper.data); publishResult(result, _inputImageWrapper.data);
@ -183,6 +187,14 @@ function createScanner(pixelCapturer) {
} }
} }
function calculateClipping(canvasSize) {
const area = _config.detector.area;
const patchSize = _config.locator.patchSize || "medium";
const halfSample = _config.locator.halfSample || true;
return _checkImageConstraints({area, patchSize, canvasSize, halfSample});
}
function update() { function update() {
var availableWorker; var availableWorker;
@ -195,17 +207,20 @@ function createScanner(pixelCapturer) {
return Promise.resolve(); return Promise.resolve();
} }
} }
const buffer = availableWorker ? availableWorker.imageData : _inputImageWrapper.data; return pixelCapturer.grabFrameData({clipping: calculateClipping})
return pixelCapturer.grabFrameData({buffer})
.then((bitmap) => { .then((bitmap) => {
if (bitmap) { if (bitmap) {
console.log(bitmap.dimensions);
// adjust image size!
if (availableWorker) { if (availableWorker) {
availableWorker.imageData = bitmap.data;
availableWorker.busy = true; availableWorker.busy = true;
availableWorker.worker.postMessage({ availableWorker.worker.postMessage({
cmd: 'process', cmd: 'process',
imageData: availableWorker.imageData imageData: availableWorker.imageData
}, [availableWorker.imageData.buffer]); }, [availableWorker.imageData.buffer]);
} else { } else {
_inputImageWrapper.data = bitmap.data;
locateAndDecode(); locateAndDecode();
} }
} }
@ -250,7 +265,7 @@ function createScanner(pixelCapturer) {
const captureSize = pixelCapturer.getCaptureSize(); const captureSize = pixelCapturer.getCaptureSize();
const workerThread = { const workerThread = {
worker: undefined, worker: undefined,
imageData: new Uint8Array(captureSize.width * captureSize.height), imageData: new Uint8Array(aquire(captureSize.width * captureSize.height)),
busy: true busy: true
}; };
@ -261,13 +276,13 @@ function createScanner(pixelCapturer) {
if (e.data.event === 'initialized') { if (e.data.event === 'initialized') {
URL.revokeObjectURL(blobURL); URL.revokeObjectURL(blobURL);
workerThread.busy = false; workerThread.busy = false;
workerThread.imageData = new Uint8Array(e.data.imageData); release(e.data.imageData);
if (ENV.development) { if (ENV.development) {
console.log("Worker initialized"); console.log("Worker initialized");
} }
return cb(workerThread); return cb(workerThread);
} else if (e.data.event === 'processed') { } else if (e.data.event === 'processed') {
workerThread.imageData = new Uint8Array(e.data.imageData); release(e.data.imageData);
workerThread.busy = false; workerThread.busy = false;
publishResult(e.data.result, workerThread.imageData); publishResult(e.data.result, workerThread.imageData);
} else if (e.data.event === 'error') { } else if (e.data.event === 'error') {
@ -350,16 +365,6 @@ function createScanner(pixelCapturer) {
return window.URL.createObjectURL(blob); return window.URL.createObjectURL(blob);
} }
function setReaders(readers) {
if (_decoder) {
_decoder.setReaders(readers);
} else if (_onUIThread && _workerPool.length > 0) {
_workerPool.forEach(function(workerThread) {
workerThread.worker.postMessage({cmd: 'setReaders', readers: readers});
});
}
}
function adjustWorkerPool(capacity) { function adjustWorkerPool(capacity) {
return new Promise((resolve) => { return new Promise((resolve) => {
const increaseBy = capacity - _workerPool.length; const increaseBy = capacity - _workerPool.length;
@ -397,7 +402,7 @@ function createScanner(pixelCapturer) {
if (imageWrapper) { if (imageWrapper) {
_onUIThread = false; _onUIThread = false;
initializeData(imageWrapper); initBuffers(imageWrapper);
return cb(); return cb();
} else { } else {
return setup().then(cb); return setup().then(cb);
@ -412,6 +417,7 @@ function createScanner(pixelCapturer) {
stop: function() { stop: function() {
_stopped = true; _stopped = true;
adjustWorkerPool(0); adjustWorkerPool(0);
releaseAll();
if (source.type === "CAMERA") { if (source.type === "CAMERA") {
CameraAccess.release(); CameraAccess.release();
} }

Loading…
Cancel
Save