Comparing version 1.0.0 to 1.0.1
1071
index.js
'use strict'; | ||
const {Transform} = require('stream'); | ||
const { Transform } = require('stream'); | ||
@@ -10,588 +10,585 @@ const PP = require('polygon-points'); | ||
class PamDiff extends Transform { | ||
/** | ||
* | ||
* @param [options] {Object} | ||
* @param [options.debug=false] {Boolean} - If true, debug info will be logged to console | ||
* @param [options.sync=false] {Boolean} - If true, pixel change detection will block the event loop instead of using a worker | ||
* @param [options.difference=5] {Number} - Pixel difference value 1 to 255 | ||
* @param [options.percent=5] {Number} - Percent of pixels or blobs that exceed difference value | ||
* @param [options.response=percent] {String} - Accepted values: percent or bounds or blobs | ||
* @param [options.draw=false] {Boolean} - If true and response is 'bounds' or 'blobs', return a pixel buffer with drawn bounding box | ||
* @param [options.regions] {Array} - Array of region objects | ||
* @param options.regions[i].name {String} - Name of region | ||
* @param [options.regions[i].difference=options.difference] {Number} - Difference value for region | ||
* @param [options.regions[i].percent=options.percent] {Number} - Percent value for region | ||
* @param options.regions[i].polygon {Array} - Array of x y coordinates [{x:0,y:0},{x:0,y:360},{x:160,y:360},{x:160,y:0}] | ||
* @param [options.mask=false] {Boolean} - Indicate if regions should be used as masks of pixels to ignore | ||
* @param [callback] {Function} - Function to be called when diff event occurs | ||
*/ | ||
constructor(options, callback) { | ||
super(options); | ||
Transform.call(this, {objectMode: true}); | ||
this.debug = PamDiff._parseOptions('debug', options);// output debug info to console. defaults to false | ||
this.response = PamDiff._parseOptions('response', options);//percent, bounds, blobs | ||
this.draw = PamDiff._parseOptions('draw', options);// return pixels with bounding box if response is bounds or blobs | ||
this.sync = PamDiff._parseOptions('sync', options);// should be processed before regions | ||
this.difference = PamDiff._parseOptions('difference', options);// global value, can be overridden per region | ||
this.percent = PamDiff._parseOptions('percent', options);// global value, can be overridden per region | ||
this.mask = PamDiff._parseOptions('mask', options);// should be processed before regions | ||
this.regions = PamDiff._parseOptions('regions', options);// can be zero regions, a single region, or multiple regions. if no regions, all pixels will be compared. | ||
this.callback = callback;// callback function to be called when pixel difference is detected | ||
this._parseChunk = this._parseFirstChunk;// first parsing will be reading settings and configuring internal pixel reading | ||
} | ||
/** | ||
* | ||
* @param [options] {Object} | ||
* @param [options.debug=false] {Boolean} - If true, debug info will be logged to console | ||
* @param [options.sync=false] {Boolean} - If true, pixel change detection will block the event loop instead of using a worker | ||
* @param [options.difference=5] {Number} - Pixel difference value 1 to 255 | ||
* @param [options.percent=5] {Number} - Percent of pixels or blobs that exceed difference value | ||
* @param [options.response=percent] {String} - Accepted values: percent or bounds or blobs | ||
* @param [options.draw=false] {Boolean} - If true and response is 'bounds' or 'blobs', return a pixel buffer with drawn bounding box | ||
* @param [options.regions] {Array} - Array of region objects | ||
* @param options.regions[i].name {String} - Name of region | ||
* @param [options.regions[i].difference=options.difference] {Number} - Difference value for region | ||
* @param [options.regions[i].percent=options.percent] {Number} - Percent value for region | ||
* @param options.regions[i].polygon {Array} - Array of x y coordinates [{x:0,y:0},{x:0,y:360},{x:160,y:360},{x:160,y:0}] | ||
* @param [options.mask=false] {Boolean} - Indicate if regions should be used as masks of pixels to ignore | ||
* @param [callback] {Function} - Function to be called when diff event occurs | ||
*/ | ||
constructor(options, callback) { | ||
super(options); | ||
Transform.call(this, { objectMode: true }); | ||
this.debug = PamDiff._parseOptions('debug', options); // output debug info to console. defaults to false | ||
this.response = PamDiff._parseOptions('response', options); // percent, bounds, blobs | ||
this.draw = PamDiff._parseOptions('draw', options); // return pixels with bounding box if response is bounds or blobs | ||
this.sync = PamDiff._parseOptions('sync', options); // should be processed before regions | ||
this.difference = PamDiff._parseOptions('difference', options); // global value, can be overridden per region | ||
this.percent = PamDiff._parseOptions('percent', options); // global value, can be overridden per region | ||
this.mask = PamDiff._parseOptions('mask', options); // should be processed before regions | ||
this.regions = PamDiff._parseOptions('regions', options); // can be zero regions, a single region, or multiple regions. if no regions, all pixels will be compared. | ||
this.callback = callback; // callback function to be called when pixel difference is detected | ||
this._parseChunk = this._parseFirstChunk; // first parsing will be reading settings and configuring internal pixel reading | ||
} | ||
/** | ||
* | ||
* @param option {String} | ||
* @param options {Object} | ||
* @return {*} | ||
* @private | ||
*/ | ||
static _parseOptions(option, options) { | ||
if (options && options.hasOwnProperty(option)) { | ||
return options[option]; | ||
} | ||
return null; | ||
/** | ||
* | ||
* @param option {String} | ||
* @param options {Object} | ||
* @return {*} | ||
* @private | ||
*/ | ||
static _parseOptions(option, options) { | ||
if (options && options.hasOwnProperty(option)) { | ||
return options[option]; | ||
} | ||
return null; | ||
} | ||
/** | ||
* | ||
* @param number {Number} | ||
* @param def {Number} | ||
* @param low {Number} | ||
* @param high {Number} | ||
* @return {Number} | ||
* @private | ||
*/ | ||
static _validateNumber(number, def, low, high) { | ||
if (isNaN(number)) { | ||
return def; | ||
} else if (number < low) { | ||
return low; | ||
} else if (number > high) { | ||
return high; | ||
} else { | ||
return number; | ||
} | ||
/** | ||
* | ||
* @param number {Number} | ||
* @param def {Number} | ||
* @param low {Number} | ||
* @param high {Number} | ||
* @return {Number} | ||
* @private | ||
*/ | ||
static _validateNumber(number, def, low, high) { | ||
if (isNaN(number)) { | ||
return def; | ||
} else if (number < low) { | ||
return low; | ||
} else if (number > high) { | ||
return high; | ||
} else { | ||
return number; | ||
} | ||
} | ||
/** | ||
* | ||
* @param bool | ||
* @return {boolean} | ||
* @private | ||
*/ | ||
static _validateBoolean(bool) { | ||
return (bool === true || bool === 'true' || bool === 1 || bool === '1'); | ||
} | ||
/** | ||
* | ||
* @param bool | ||
* @return {boolean} | ||
* @private | ||
*/ | ||
static _validateBoolean(bool) { | ||
return bool === true || bool === 'true' || bool === 1 || bool === '1'; | ||
} | ||
/** | ||
* | ||
* @param string {String} | ||
* @param strings {Array} | ||
* @return {String} | ||
* @private | ||
*/ | ||
static _validateString(string, strings) { | ||
if (strings.includes(string)) { | ||
return string; | ||
} | ||
return strings[0]; | ||
/** | ||
* | ||
* @param string {String} | ||
* @param strings {Array} | ||
* @return {String} | ||
* @private | ||
*/ | ||
static _validateString(string, strings) { | ||
if (strings.includes(string)) { | ||
return string; | ||
} | ||
return strings[0]; | ||
} | ||
/** | ||
* | ||
* @param bool {Boolean} | ||
*/ | ||
set debug(bool) { | ||
this._debug = PamDiff._validateBoolean(bool); | ||
this._processRegions(); | ||
this._configurePixelDiffEngine(); | ||
} | ||
/** | ||
* | ||
* @param bool {Boolean} | ||
*/ | ||
set debug(bool) { | ||
this._debug = PamDiff._validateBoolean(bool); | ||
this._processRegions(); | ||
this._configurePixelDiffEngine(); | ||
} | ||
/** | ||
* | ||
* @return {Boolean} | ||
*/ | ||
get debug() { | ||
return this._debug; | ||
} | ||
/** | ||
* | ||
* @return {Boolean} | ||
*/ | ||
get debug() { | ||
return this._debug; | ||
} | ||
/** | ||
* | ||
* @param bool {Boolean} | ||
* @return {PamDiff} | ||
*/ | ||
setDebug(bool) { | ||
this.debug = bool; | ||
return this; | ||
} | ||
/** | ||
* | ||
* @param bool {Boolean} | ||
* @return {PamDiff} | ||
*/ | ||
setDebug(bool) { | ||
this.debug = bool; | ||
return this; | ||
} | ||
/** | ||
* | ||
* @param string {String} | ||
*/ | ||
set response(string) { | ||
this._response = PamDiff._validateString(string, ['percent', 'bounds', 'blobs']); | ||
this._processRegions(); | ||
this._configurePixelDiffEngine(); | ||
} | ||
/** | ||
* | ||
* @param string {String} | ||
*/ | ||
set response(string) { | ||
this._response = PamDiff._validateString(string, ['percent', 'bounds', 'blobs']); | ||
this._processRegions(); | ||
this._configurePixelDiffEngine(); | ||
} | ||
/** | ||
* | ||
* @return {String} | ||
*/ | ||
get response() { | ||
return this._response; | ||
} | ||
/** | ||
* | ||
* @return {String} | ||
*/ | ||
get response() { | ||
return this._response; | ||
} | ||
/** | ||
* | ||
* @param string {String} | ||
* @return {PamDiff} | ||
*/ | ||
setResponse(string) { | ||
this.response = string; | ||
return this; | ||
} | ||
/** | ||
* | ||
* @param string {String} | ||
* @return {PamDiff} | ||
*/ | ||
setResponse(string) { | ||
this.response = string; | ||
return this; | ||
} | ||
/** | ||
* | ||
* @param bool {Boolean} | ||
*/ | ||
set sync(bool) { | ||
this._sync = PamDiff._validateBoolean(bool); | ||
this._processRegions(); | ||
this._configurePixelDiffEngine(); | ||
} | ||
/** | ||
* | ||
* @param bool {Boolean} | ||
*/ | ||
set sync(bool) { | ||
this._sync = PamDiff._validateBoolean(bool); | ||
this._processRegions(); | ||
this._configurePixelDiffEngine(); | ||
} | ||
/** | ||
* | ||
* @return {Boolean} | ||
*/ | ||
get sync() { | ||
return this._sync; | ||
} | ||
/** | ||
* | ||
* @return {Boolean} | ||
*/ | ||
get sync() { | ||
return this._sync; | ||
} | ||
/** | ||
* | ||
* @param bool {Boolean} | ||
* @return {PamDiff} | ||
*/ | ||
setSync(bool) { | ||
this.sync = bool; | ||
return this; | ||
} | ||
/** | ||
* | ||
* @param bool {Boolean} | ||
* @return {PamDiff} | ||
*/ | ||
setSync(bool) { | ||
this.sync = bool; | ||
return this; | ||
} | ||
/** | ||
* | ||
* @param bool {Boolean} | ||
*/ | ||
set draw(bool) { | ||
this._draw = PamDiff._validateBoolean(bool); | ||
this._processRegions(); | ||
this._configurePixelDiffEngine(); | ||
} | ||
/** | ||
* | ||
* @param bool {Boolean} | ||
*/ | ||
set draw(bool) { | ||
this._draw = PamDiff._validateBoolean(bool); | ||
this._processRegions(); | ||
this._configurePixelDiffEngine(); | ||
} | ||
/** | ||
* | ||
* @return {Boolean} | ||
*/ | ||
get draw() { | ||
return this._draw; | ||
} | ||
/** | ||
* | ||
* @return {Boolean} | ||
*/ | ||
get draw() { | ||
return this._draw; | ||
} | ||
/** | ||
* | ||
* @param bool {Boolean} | ||
* @return {PamDiff} | ||
*/ | ||
setDraw(bool) { | ||
this.draw = bool; | ||
return this; | ||
} | ||
/** | ||
* | ||
* @param bool {Boolean} | ||
* @return {PamDiff} | ||
*/ | ||
setDraw(bool) { | ||
this.draw = bool; | ||
return this; | ||
} | ||
/** | ||
* | ||
* @param number {Number} | ||
*/ | ||
set difference(number) { | ||
this._difference = PamDiff._validateNumber(parseInt(number), 5, 1, 255); | ||
this._configurePixelDiffEngine(); | ||
} | ||
/** | ||
* | ||
* @param number {Number} | ||
*/ | ||
set difference(number) { | ||
this._difference = PamDiff._validateNumber(parseInt(number), 5, 1, 255); | ||
this._configurePixelDiffEngine(); | ||
} | ||
/** | ||
* | ||
* @return {Number} | ||
*/ | ||
get difference() { | ||
return this._difference; | ||
} | ||
/** | ||
* | ||
* @return {Number} | ||
*/ | ||
get difference() { | ||
return this._difference; | ||
} | ||
/** | ||
* | ||
* @param number {Number} | ||
* @return {PamDiff} | ||
*/ | ||
setDifference(number) { | ||
this.difference = number; | ||
return this; | ||
} | ||
/** | ||
* | ||
* @param number {Number} | ||
* @return {PamDiff} | ||
*/ | ||
setDifference(number) { | ||
this.difference = number; | ||
return this; | ||
} | ||
/** | ||
* | ||
* @param number {Number} | ||
*/ | ||
set percent(number) { | ||
this._percent = PamDiff._validateNumber(parseInt(number), 5, 1, 100); | ||
this._configurePixelDiffEngine(); | ||
} | ||
/** | ||
* | ||
* @param number {Number} | ||
*/ | ||
set percent(number) { | ||
this._percent = PamDiff._validateNumber(parseInt(number), 5, 1, 100); | ||
this._configurePixelDiffEngine(); | ||
} | ||
/** | ||
* | ||
* @return {Number} | ||
*/ | ||
get percent() { | ||
return this._percent; | ||
} | ||
/** | ||
* | ||
* @return {Number} | ||
*/ | ||
get percent() { | ||
return this._percent; | ||
} | ||
/** | ||
* | ||
* @param number {Number} | ||
* @return {PamDiff} | ||
*/ | ||
setPercent(number) { | ||
this.percent = number; | ||
return this; | ||
} | ||
/** | ||
* | ||
* @param number {Number} | ||
* @return {PamDiff} | ||
*/ | ||
setPercent(number) { | ||
this.percent = number; | ||
return this; | ||
} | ||
/** | ||
* | ||
* @param array {Array} | ||
*/ | ||
set regions(array) { | ||
if (!array) { | ||
delete this._regions; | ||
delete this._regionObj; | ||
delete this._maskObj; | ||
} else if (!Array.isArray(array) || array.length < 1) { | ||
throw new Error(`Regions must be an array of at least 1 region object {name: 'region1', difference: 10, percent: 10, polygon: [[0, 0], [0, 50], [50, 50], [50, 0]]}`); | ||
} else { | ||
this._regions = array; | ||
this._processRegions(); | ||
} | ||
this._configurePixelDiffEngine(); | ||
/** | ||
* | ||
* @param array {Array} | ||
*/ | ||
set regions(array) { | ||
if (!array) { | ||
delete this._regions; | ||
delete this._regionObj; | ||
delete this._maskObj; | ||
} else if (!Array.isArray(array) || array.length < 1) { | ||
throw new Error(`Regions must be an array of at least 1 region object { name: 'region1', difference: 10, percent: 10, polygon: [[0, 0], [0, 50], [50, 50], [50, 0]] }`); | ||
} else { | ||
this._regions = array; | ||
this._processRegions(); | ||
} | ||
this._configurePixelDiffEngine(); | ||
} | ||
/** | ||
* | ||
* @return {Array} | ||
*/ | ||
get regions() { | ||
return this._regions; | ||
} | ||
/** | ||
* | ||
* @return {Array} | ||
*/ | ||
get regions() { | ||
return this._regions; | ||
} | ||
/** | ||
* | ||
* @param array {Array} | ||
* @return {PamDiff} | ||
*/ | ||
setRegions(array) { | ||
this.regions = array; | ||
return this; | ||
} | ||
/** | ||
* | ||
* @param array {Array} | ||
* @return {PamDiff} | ||
*/ | ||
setRegions(array) { | ||
this.regions = array; | ||
return this; | ||
} | ||
set mask(bool) { | ||
this._mask = PamDiff._validateBoolean(bool); | ||
this._processRegions(); | ||
this._configurePixelDiffEngine(); | ||
} | ||
set mask(bool) { | ||
this._mask = PamDiff._validateBoolean(bool); | ||
this._processRegions(); | ||
this._configurePixelDiffEngine(); | ||
} | ||
get mask() { | ||
return this._mask; | ||
get mask() { | ||
return this._mask; | ||
} | ||
setMask(bool) { | ||
this.mask = bool; | ||
return this; | ||
} | ||
/** | ||
* | ||
* @param func {Function} | ||
*/ | ||
set callback(func) { | ||
if (!func) { | ||
delete this._callback; | ||
} else if (typeof func === 'function' && func.length === 1) { | ||
this._callback = func; | ||
} else { | ||
throw new Error('Callback must be a function that accepts 1 argument.'); | ||
} | ||
} | ||
setMask(bool) { | ||
this.mask = bool; | ||
return this; | ||
/** | ||
* | ||
* @return {Function} | ||
*/ | ||
get callback() { | ||
return this._callback; | ||
} | ||
/** | ||
* | ||
* @param func {Function} | ||
* @return {PamDiff} | ||
*/ | ||
setCallback(func) { | ||
this.callback = func; | ||
return this; | ||
} | ||
/** | ||
* | ||
* @return {PamDiff} | ||
*/ | ||
resetCache() { | ||
delete this._engine; | ||
delete this._oldPix; | ||
delete this._newPix; | ||
delete this._width; | ||
delete this._height; | ||
delete this._depth; | ||
delete this._tupltype; | ||
delete this._regionObj; | ||
this._parseChunk = this._parseFirstChunk; | ||
return this; | ||
} | ||
/** | ||
* | ||
* @private | ||
*/ | ||
_processRegions() { | ||
if (!this._regions || !this._width || !this._height) { | ||
return; | ||
} | ||
/** | ||
* | ||
* @param func {Function} | ||
*/ | ||
set callback(func) { | ||
if (!func) { | ||
delete this._callback; | ||
} else if (typeof func === 'function' && func.length === 1) { | ||
this._callback = func; | ||
} else { | ||
throw new Error('Callback must be a function that accepts 1 argument.'); | ||
const regions = []; | ||
if (this._mask === true) { | ||
// combine all regions to form a single region of flipped 0's and 1's | ||
let minX = this._width; | ||
let maxX = 0; | ||
let minY = this._height; | ||
let maxY = 0; | ||
const wxh = this._width * this._height; | ||
const maskBitset = Buffer.alloc(wxh, 1); | ||
for (const region of this._regions) { | ||
if (!region.hasOwnProperty('polygon')) { | ||
throw new Error('Region must include a polygon property'); | ||
} | ||
const pp = new PP(region.polygon); | ||
const bitset = pp.getBitset(this._width, this._height); | ||
if (bitset.count === 0) { | ||
throw new Error('Bitset count must be greater than 0.'); | ||
} | ||
const bitsetBuffer = bitset.buffer; | ||
for (let i = 0; i < wxh; ++i) { | ||
if (bitsetBuffer[i] === 1) { | ||
maskBitset[i] = 0; | ||
} | ||
} | ||
} | ||
let maskBitsetCount = 0; | ||
for (let i = 0; i < wxh; ++i) { | ||
if (maskBitset[i] === 1) { | ||
const y = Math.floor(i / this._width); | ||
const x = i % this._width; | ||
minX = Math.min(minX, x); | ||
maxX = Math.max(maxX, x); | ||
minY = Math.min(minY, y); | ||
maxY = Math.max(maxY, y); | ||
maskBitsetCount++; | ||
} | ||
} | ||
if (maskBitsetCount === 0) { | ||
throw new Error('Bitset count must be greater than 0'); | ||
} | ||
regions.push({ | ||
name: 'mask', | ||
bitset: maskBitset, | ||
bitsetCount: maskBitsetCount, | ||
difference: this._difference, | ||
percent: this._percent, | ||
minX: minX, | ||
maxX: maxX, | ||
minY: minY, | ||
maxY: maxY, | ||
}); | ||
} else { | ||
for (const region of this._regions) { | ||
if (!region.hasOwnProperty('name') || !region.hasOwnProperty('polygon')) { | ||
throw new Error('Region must include a name and a polygon property'); | ||
} | ||
const pp = new PP(region.polygon); | ||
const bitset = pp.getBitset(this._width, this._height); | ||
if (bitset.count === 0) { | ||
throw new Error('Bitset count must be greater than 0'); | ||
} | ||
const difference = PamDiff._validateNumber(parseInt(region.difference), this._difference, 1, 255); | ||
const percent = PamDiff._validateNumber(parseInt(region.percent), this._percent, 1, 100); | ||
regions.push({ | ||
name: region.name, | ||
bitset: bitset.buffer, | ||
bitsetCount: bitset.count, | ||
difference: difference, | ||
percent: percent, | ||
minX: bitset.minX, | ||
maxX: bitset.maxX, | ||
minY: bitset.minY, | ||
maxY: bitset.maxY, | ||
}); | ||
} | ||
} | ||
this._regionObj = { length: regions.length, regions: regions }; | ||
} | ||
/** | ||
* | ||
* @return {Function} | ||
*/ | ||
get callback() { | ||
return this._callback; | ||
/** | ||
* | ||
* @private | ||
*/ | ||
_configurePixelDiffEngine() { | ||
if (!this._tupltype || !this._width || !this._height) { | ||
return; | ||
} | ||
/** | ||
* | ||
* @param func {Function} | ||
* @return {PamDiff} | ||
*/ | ||
setCallback(func) { | ||
this.callback = func; | ||
return this; | ||
let engine = this._tupltype; | ||
engine += `_${this._width}_x_${this._height}`; | ||
const config = { width: this._width, height: this._height, depth: this._depth, response: this._response, sync: this._sync }; | ||
if (this._regionObj) { | ||
engine += '_region'; | ||
// config.target = 'region'; | ||
config.regions = this._regionObj.regions; | ||
if (this._regionObj.length > 1) { | ||
engine += 's'; | ||
} | ||
} else { | ||
engine += '_all'; | ||
// config.target = 'all'; | ||
config.difference = this._difference; | ||
config.percent = this._percent; | ||
} | ||
/** | ||
* | ||
* @return {PamDiff} | ||
*/ | ||
resetCache() { | ||
delete this._engine; | ||
delete this._oldPix; | ||
delete this._newPix; | ||
delete this._width; | ||
delete this._height; | ||
delete this._depth; | ||
delete this._tupltype; | ||
delete this._regionObj; | ||
this._parseChunk = this._parseFirstChunk; | ||
return this; | ||
engine += `_${this._response}`; | ||
if ((this._response === 'bounds' || this._response === 'blobs') && this._draw) { | ||
config.draw = this._draw; | ||
engine += '_draw'; | ||
} | ||
engine += this._sync ? '_sync' : '_async'; | ||
const pixelChange = PC(config); | ||
this._engine = this._sync ? pixelChange.compareSync.bind(pixelChange) : pixelChange.compare.bind(pixelChange); | ||
if (this._debug) { | ||
console.dir(this, { showHidden: false, depth: 0, colors: true }); | ||
this._parseChunk = this._parsePixelsDebug; | ||
this._debugEngine = engine; | ||
this._debugCount = 0; | ||
} else { | ||
this._parseChunk = this._parsePixels; | ||
} | ||
} | ||
/** | ||
* | ||
* @private | ||
*/ | ||
_processRegions() { | ||
if (!this._regions || !this._width || !this._height) { | ||
return; | ||
} | ||
const regions = []; | ||
if (this._mask === true) {// combine all regions to form a single region of flipped 0's and 1's | ||
let minX = this._width; | ||
let maxX = 0; | ||
let minY = this._height; | ||
let maxY = 0; | ||
const wxh = this._width * this._height; | ||
const maskBitset = Buffer.alloc(wxh, 1); | ||
for (const region of this._regions) { | ||
if (!region.hasOwnProperty('polygon')) { | ||
throw new Error('Region must include a polygon property'); | ||
} | ||
const pp = new PP(region.polygon); | ||
const bitset = pp.getBitset(this._width, this._height); | ||
if (bitset.count === 0) { | ||
throw new Error('Bitset count must be greater than 0.'); | ||
} | ||
const bitsetBuffer = bitset.buffer; | ||
for (let i = 0; i < wxh; ++i) { | ||
if (bitsetBuffer[i] === 1) { | ||
maskBitset[i] = 0; | ||
} | ||
} | ||
} | ||
let maskBitsetCount = 0; | ||
for (let i = 0; i < wxh; ++i) { | ||
if (maskBitset[i] === 1) { | ||
const y = Math.floor(i / this._width); | ||
const x = i % this._width; | ||
minX = Math.min(minX, x); | ||
maxX = Math.max(maxX, x); | ||
minY = Math.min(minY, y); | ||
maxY = Math.max(maxY, y); | ||
maskBitsetCount++; | ||
} | ||
} | ||
if (maskBitsetCount === 0) { | ||
throw new Error('Bitset count must be greater than 0'); | ||
} | ||
regions.push( | ||
{ | ||
name: 'mask', | ||
bitset: maskBitset, | ||
bitsetCount: maskBitsetCount, | ||
difference: this._difference, | ||
percent: this._percent, | ||
minX: minX, | ||
maxX: maxX, | ||
minY: minY, | ||
maxY: maxY | ||
} | ||
); | ||
/** | ||
* | ||
* @param chunk | ||
* @private | ||
*/ | ||
_parsePixels(chunk) { | ||
this._newPix = chunk.pixels; | ||
this._engine(this._oldPix, this._newPix, (err, results, pixels) => { | ||
if (results.length) { | ||
const data = { trigger: results, pam: chunk.pam, headers: chunk.headers }; | ||
if (pixels) { | ||
data.pixels = pixels; | ||
} else { | ||
for (const region of this._regions) { | ||
if (!region.hasOwnProperty('name') || !region.hasOwnProperty('polygon')) { | ||
throw new Error('Region must include a name and a polygon property'); | ||
} | ||
const pp = new PP(region.polygon); | ||
const bitset = pp.getBitset(this._width, this._height); | ||
if (bitset.count === 0) { | ||
throw new Error('Bitset count must be greater than 0'); | ||
} | ||
const difference = PamDiff._validateNumber(parseInt(region.difference), this._difference, 1, 255); | ||
const percent = PamDiff._validateNumber(parseInt(region.percent), this._percent, 1, 100); | ||
regions.push( | ||
{ | ||
name: region.name, | ||
bitset: bitset.buffer, | ||
bitsetCount: bitset.count, | ||
difference: difference, | ||
percent: percent, | ||
minX: bitset.minX, | ||
maxX: bitset.maxX, | ||
minY: bitset.minY, | ||
maxY: bitset.maxY | ||
} | ||
); | ||
} | ||
data.pixels = chunk.pixels; | ||
} | ||
this._regionObj = {length: regions.length, regions: regions}; | ||
} | ||
/** | ||
* | ||
* @private | ||
*/ | ||
_configurePixelDiffEngine() { | ||
if (!this._tupltype || !this._width || !this._height) { | ||
return; | ||
if (this._callback) { | ||
this._callback(data); | ||
} | ||
let engine = this._tupltype; | ||
engine += `_${this._width}_x_${this._height}`; | ||
const config = {width: this._width, height: this._height, depth: this._depth, response: this._response, sync: this._sync}; | ||
if (this._regionObj) { | ||
engine += '_region'; | ||
//config.target = 'region'; | ||
config.regions = this._regionObj.regions; | ||
if (this._regionObj.length > 1) { | ||
engine += 's'; | ||
} | ||
} else { | ||
engine += '_all'; | ||
//config.target = 'all'; | ||
config.difference = this._difference; | ||
config.percent = this._percent; | ||
if (this._readableState.pipesCount > 0) { | ||
this.push(data); | ||
} | ||
engine += `_${this._response}`; | ||
if ((this._response === 'bounds' || this._response === 'blobs') && this._draw) { | ||
config.draw = this._draw; | ||
engine += '_draw'; | ||
if (this.listenerCount('diff') > 0) { | ||
this.emit('diff', data); | ||
} | ||
engine += this._sync ? '_sync' : '_async'; | ||
const pixelChange = PC(config); | ||
this._engine = this._sync ? pixelChange.compareSync.bind(pixelChange) : pixelChange.compare.bind(pixelChange); | ||
if (this._debug) { | ||
console.dir(this, {showHidden: false, depth: 0, colors: true}); | ||
this._parseChunk = this._parsePixelsDebug; | ||
this._debugEngine = engine; | ||
this._debugCount = 0; | ||
} | ||
}); | ||
this._oldPix = this._newPix; | ||
} | ||
/** | ||
* | ||
* @param chunk | ||
* @private | ||
*/ | ||
_parsePixelsDebug(chunk) { | ||
const debugCount = this._debugCount++; | ||
console.time(`${this._debugEngine}-${debugCount}`); | ||
this._newPix = chunk.pixels; | ||
this._engine(this._oldPix, this._newPix, (err, results, pixels) => { | ||
console.timeEnd(`${this._debugEngine}-${debugCount}`); | ||
if (results.length) { | ||
const data = { trigger: results, pam: chunk.pam, headers: chunk.headers }; | ||
if (pixels) { | ||
data.pixels = pixels; | ||
} else { | ||
this._parseChunk = this._parsePixels; | ||
data.pixels = chunk.pixels; | ||
} | ||
} | ||
if (this._callback) { | ||
this._callback(data); | ||
} | ||
if (this._readableState.pipesCount > 0) { | ||
this.push(data); | ||
} | ||
if (this.listenerCount('diff') > 0) { | ||
this.emit('diff', data); | ||
} | ||
} | ||
}); | ||
this._oldPix = this._newPix; | ||
} | ||
/** | ||
* | ||
* @param chunk | ||
* @private | ||
*/ | ||
_parsePixels(chunk) { | ||
this._newPix = chunk.pixels; | ||
this._engine(this._oldPix, this._newPix, (err, results, pixels) => { | ||
if (results.length) { | ||
const data = {trigger: results, pam: chunk.pam, headers: chunk.headers}; | ||
if (pixels) { | ||
data.pixels = pixels; | ||
} else { | ||
data.pixels = chunk.pixels; | ||
} | ||
if (this._callback) { | ||
this._callback(data); | ||
} | ||
if (this._readableState.pipesCount > 0) { | ||
this.push(data); | ||
} | ||
if (this.listenerCount('diff') > 0) { | ||
this.emit('diff', data); | ||
} | ||
} | ||
}); | ||
this._oldPix = this._newPix; | ||
} | ||
/** | ||
* | ||
* @param chunk | ||
* @private | ||
*/ | ||
_parseFirstChunk(chunk) { | ||
this._width = parseInt(chunk.width); | ||
this._height = parseInt(chunk.height); | ||
this._depth = parseInt(chunk.depth); | ||
this._oldPix = chunk.pixels; | ||
this._tupltype = chunk.tupltype; | ||
this._processRegions(); | ||
this._configurePixelDiffEngine(); | ||
} | ||
/** | ||
* | ||
* @param chunk | ||
* @private | ||
*/ | ||
_parsePixelsDebug(chunk) { | ||
const debugCount = this._debugCount++; | ||
console.time(`${this._debugEngine}-${debugCount}`); | ||
this._newPix = chunk.pixels; | ||
this._engine(this._oldPix, this._newPix, (err, results, pixels) => { | ||
console.timeEnd(`${this._debugEngine}-${debugCount}`); | ||
if (results.length) { | ||
const data = {trigger: results, pam: chunk.pam, headers: chunk.headers}; | ||
if (pixels) { | ||
data.pixels = pixels; | ||
} else { | ||
data.pixels = chunk.pixels; | ||
} | ||
if (this._callback) { | ||
this._callback(data); | ||
} | ||
if (this._readableState.pipesCount > 0) { | ||
this.push(data); | ||
} | ||
if (this.listenerCount('diff') > 0) { | ||
this.emit('diff', data); | ||
} | ||
} | ||
}); | ||
this._oldPix = this._newPix; | ||
} | ||
/** | ||
* | ||
* @param chunk | ||
* @param encoding | ||
* @param callback | ||
* @private | ||
*/ | ||
_transform(chunk, encoding, callback) { | ||
this._parseChunk(chunk); | ||
callback(); | ||
} | ||
/** | ||
* | ||
* @param chunk | ||
* @private | ||
*/ | ||
_parseFirstChunk(chunk) { | ||
this._width = parseInt(chunk.width); | ||
this._height = parseInt(chunk.height); | ||
this._depth = parseInt(chunk.depth); | ||
this._oldPix = chunk.pixels; | ||
this._tupltype = chunk.tupltype; | ||
this._processRegions(); | ||
this._configurePixelDiffEngine(); | ||
} | ||
/** | ||
* | ||
* @param chunk | ||
* @param encoding | ||
* @param callback | ||
* @private | ||
*/ | ||
_transform(chunk, encoding, callback) { | ||
this._parseChunk(chunk); | ||
callback(); | ||
} | ||
/** | ||
* | ||
* @param callback | ||
* @private | ||
*/ | ||
_flush(callback) { | ||
this.resetCache(); | ||
callback(); | ||
} | ||
/** | ||
* | ||
* @param callback | ||
* @private | ||
*/ | ||
_flush(callback) { | ||
this.resetCache(); | ||
callback(); | ||
} | ||
} | ||
@@ -603,2 +600,2 @@ | ||
*/ | ||
module.exports = PamDiff; | ||
module.exports = PamDiff; |
{ | ||
"name": "pam-diff", | ||
"version": "1.0.0", | ||
"version": "1.0.1", | ||
"description": "Measure differences between pixel arrays extracted from pam images", | ||
"main": "index.js", | ||
"scripts": { | ||
"pack": "npm --verbose pack", | ||
"preversion": "npm install && npm test", | ||
"postversion": "npm run doc", | ||
"doc": "./node_modules/.bin/jsdoc index.js -d docs && git commit -m \"update docs\" -- docs", | ||
"lint": "./node_modules/.bin/eslint --fix --ext js,md .", | ||
"examples": "node examples/example && node examples/example2 && node examples/example3 && node examples/example4 && node examples/example5", | ||
"doc": "jsdoc index.js -d docs && git commit -m \"update docs\" -- docs", | ||
"test": "npm run gray && npm run rgb && npm run rgba", | ||
@@ -101,5 +103,5 @@ "gray": "npm run gray:all && npm run gray:mask && npm run gray:region && npm run gray:regions", | ||
"pam:async": "npm run pam:gray:async && npm run pam:rgb:async && npm run pam:rgba:async", | ||
"pam:gray:async": "node --expose-gc examples/createJpegs --pixfmt gray --sync false --response bounds --draw false --target regions", | ||
"pam:rgb:async": "node --expose-gc examples/createJpegs --pixfmt rgb24 --sync false --response bounds --draw false --target regions", | ||
"pam:rgba:async": "node --expose-gc examples/createJpegs --pixfmt rgba --sync false --response bounds --draw false --target regions", | ||
"pam:gray:async": "node --expose-gc examples/createPam --pixfmt gray --sync false --response bounds --draw false --target regions", | ||
"pam:rgb:async": "node --expose-gc examples/createPam --pixfmt rgb24 --sync false --response bounds --draw false --target regions", | ||
"pam:rgba:async": "node --expose-gc examples/createPam --pixfmt rgba --sync false --response bounds --draw false --target regions", | ||
"pam:sync": "npm run pam:gray:sync && npm run pam:rgb:sync && npm run pam:rgba:sync", | ||
@@ -142,11 +144,16 @@ "pam:gray:sync": "node --expose-gc examples/createJpegs --pixfmt gray --sync true --response bounds --draw false --target regions", | ||
"dependencies": { | ||
"pixel-change": "^1.0.0", | ||
"pixel-change": "1.0.0", | ||
"polygon-points": "^0.6.0" | ||
}, | ||
"devDependencies": { | ||
"@ffmpeg-installer/ffmpeg": "^1.1.0", | ||
"dotenv": "^7.0.0", | ||
"ffmpeg-static": "^2.4.0", | ||
"jsdoc": "github:jsdoc3/jsdoc", | ||
"minimist": "^1.2.0", | ||
"pipe2pam": "^0.6.2" | ||
"eslint": "^7.32.0", | ||
"eslint-config-prettier": "^7.1.0", | ||
"eslint-plugin-markdown": "^2.2.1", | ||
"eslint-plugin-prettier": "^3.4.1", | ||
"jsdoc": "^3.6.7", | ||
"minimist": "^1.2.5", | ||
"pipe2pam": "^0.6.2", | ||
"prettier": "^2.5.0" | ||
}, | ||
@@ -153,0 +160,0 @@ "private": false, |
151
README.md
# pam-diff | ||
###### [![dependencies Status](https://david-dm.org/kevinGodell/pam-diff/master/status.svg)](https://david-dm.org/kevinGodell/pam-diff/master) [![Build Status](https://travis-ci.org/kevinGodell/pam-diff.svg?branch=master)](https://travis-ci.org/kevinGodell/pam-diff) [![Build status](https://ci.appveyor.com/api/projects/status/hu6qw285sm6vfwtd/branch/master?svg=true)](https://ci.appveyor.com/project/kevinGodell/pam-diff/branch/master) [![GitHub issues](https://img.shields.io/github/issues/kevinGodell/pam-diff.svg)](https://github.com/kevinGodell/pam-diff/issues) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/kevinGodell/pam-diff/master/LICENSE) [![npm](https://img.shields.io/npm/dt/pam-diff.svg?style=flat-square)](https://www.npmjs.com/package/pam-diff) | ||
Measure differences between pixel arrays extracted from pam images. Works well with node module [pipe2pam](https://www.npmjs.com/package/pipe2pam) to extract pam images from an ffmpeg pipe. Supported ***tupltypes*** are ***rgb***, ***rgb_alpha***, and ***grayscale***. It is currently being used for a video motion detection project. | ||
###### [![Build Status](https://github.com/kevinGodell/pam-diff/workflows/build/badge.svg)](https://github.com/kevinGodell/pam-diff/actions?query=workflow%3Abuild) [![Build Status](https://ci.appveyor.com/api/projects/status/hu6qw285sm6vfwtd/branch/master?svg=true)](https://ci.appveyor.com/project/kevinGodell/pam-diff/branch/master) [![GitHub Issues](https://img.shields.io/github/issues/kevinGodell/pam-diff.svg)](https://github.com/kevinGodell/pam-diff/issues) [![GitHub License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/kevinGodell/pam-diff/master/LICENSE) [![npm](https://img.shields.io/npm/dt/pam-diff.svg?style=flat-square)](https://www.npmjs.com/package/pam-diff) | ||
Measure differences between pixel arrays extracted from pam images. Works well with node module [pipe2pam](https://www.npmjs.com/package/pipe2pam) to extract pam images from an ffmpeg pipe. Supported **_tupltypes_** are **_rgb_**, **_rgb_alpha_**, and **_grayscale_**. It is currently being used for a video motion detection project. | ||
### Installation: | ||
``` | ||
``` | ||
npm install pam-diff@latest --save | ||
``` | ||
#### *Important Note:* The js-only version will no longer receive any updates. All future work will be dedicated to the n-api version because it is much more efficient. | ||
#### *New Feature:* Async made default in 0.13.6. ~~Starting with version 0.13.0, the option to use worker threads can be enabled by passing `{async: true}` to the pam-diff constructor.~~ | ||
#### *New Feature:* Starting with version 0.13.2, the option to get x y bounding box coordinates can be set by passing `{response: "bounds"}` to the pam-diff constructor. | ||
#### *New Feature:* Starting with version 0.13.5, the option to get the pixel buffer containing the drawn x y bounding box can be set by passing `{draw: true}` to the pam-diff constructor. | ||
#### *New Feature:* Starting with version 0.13.6, the option to filter results by connected component labelling can be set by passing `{response: "blobs"}` to the pam-diff constructor. | ||
#### *New Feature:* Starting with version 0.13.6, async behavior will now be default. If you need the pixel difference measurements to block the event loop, use `{sync: true}`. | ||
#### *New Feature:* Starting with version 1.0.0, pre-built binaries will be used. If binaries are not available, installation will fall back to node-gyp. | ||
### Usage Options: | ||
##### When comparing 2 equally sized buffers of grayscale, rgb, or rgba pixels, there are several options: | ||
1. all (default) | ||
#### _Important Note:_ The js-only version will no longer receive any updates. All future work will be dedicated to the n-api version because it is much more efficient. | ||
#### _New Feature:_ Async made default in 0.13.6. ~~Starting with version 0.13.0, the option to use worker threads can be enabled by passing `{async: true}` to the pam-diff constructor.~~ | ||
#### _New Feature:_ Starting with version 0.13.2, the option to get x y bounding box coordinates can be set by passing `{response: "bounds"}` to the pam-diff constructor. | ||
#### _New Feature:_ Starting with version 0.13.5, the option to get the pixel buffer containing the drawn x y bounding box can be set by passing `{draw: true}` to the pam-diff constructor. | ||
#### _New Feature:_ Starting with version 0.13.6, the option to filter results by connected component labelling can be set by passing `{response: "blobs"}` to the pam-diff constructor. | ||
#### _New Feature:_ Starting with version 0.13.6, async behavior will now be default. If you need the pixel difference measurements to block the event loop, use `{sync: true}`. | ||
#### _New Feature:_ Starting with version 1.0.0, pre-built binaries will be used. If binaries are not available, installation will fall back to node-gyp. | ||
## Usage Options: | ||
###### When comparing 2 equally sized buffers of grayscale, rgb, or rgba pixels, there are several options: | ||
### all (default) | ||
- All pixels will be targeted when checking for differences. | ||
- To use this option, set the configuration object without creating any regions. | ||
```javascript | ||
const pamDiff = new PamDiff({difference: 5, percent: 5}); | ||
const pamDiff = new PamDiff({ difference: 5, percent: 5 }); | ||
``` | ||
2. regions | ||
### regions | ||
- Specific regions of pixels will be targeted when checking for differences and the rest will be ignored. | ||
- To use this option, create a regions array and pass it to the constructor. | ||
```javascript | ||
const region1 = {name: 'region1', difference: 12, percent: 22, polygon: [{x: 0, y: 0}, {x: 0, y: 225}, {x: 100, y: 225}, {x: 100, y: 0}]}; | ||
const region2 = {name: 'region2', difference: 14, percent: 10, polygon: [{x: 100, y: 0}, {x: 100, y: 225}, {x: 200, y: 225}, {x: 200, y: 0}]}; | ||
const region1 = { | ||
name: 'region1', | ||
difference: 12, | ||
percent: 22, | ||
polygon: [ | ||
{ x: 0, y: 0 }, | ||
{ x: 0, y: 225 }, | ||
{ x: 100, y: 225 }, | ||
{ x: 100, y: 0 }, | ||
], | ||
}; | ||
const region2 = { | ||
name: 'region2', | ||
difference: 14, | ||
percent: 10, | ||
polygon: [ | ||
{ x: 100, y: 0 }, | ||
{ x: 100, y: 225 }, | ||
{ x: 200, y: 225 }, | ||
{ x: 200, y: 0 }, | ||
], | ||
}; | ||
const regions = [region1, region2]; | ||
const pamDiff = new PamDiff({regions : regions}); | ||
const pamDiff = new PamDiff({ regions: regions }); | ||
``` | ||
3. mask | ||
### mask | ||
- Specific regions of pixels will be ignored when checking for differences. | ||
- To use this option, create a regions array and set the mask option to true. | ||
- `difference` and `percent` of the individual region is ignored. Set a global value. | ||
```javascript | ||
const region1 = {name: 'region1', polygon: [{x: 0, y: 0}, {x: 0, y: 225}, {x: 100, y: 225}, {x: 100, y: 0}]}; | ||
const region2 = {name: 'region2', polygon: [{x: 100, y: 0}, {x: 100, y: 225}, {x: 200, y: 225}, {x: 200, y: 0}]}; | ||
const region1 = { | ||
name: 'region1', | ||
polygon: [ | ||
{ x: 0, y: 0 }, | ||
{ x: 0, y: 225 }, | ||
{ x: 100, y: 225 }, | ||
{ x: 100, y: 0 }, | ||
], | ||
}; | ||
const region2 = { | ||
name: 'region2', | ||
polygon: [ | ||
{ x: 100, y: 0 }, | ||
{ x: 100, y: 225 }, | ||
{ x: 200, y: 225 }, | ||
{ x: 200, y: 0 }, | ||
], | ||
}; | ||
const regions = [region1, region2]; | ||
const pamDiff = new PamDiff({difference: 12, percent: 10, mask: true, regions : regions}); | ||
const pamDiff = new PamDiff({ difference: 12, percent: 10, mask: true, regions: regions }); | ||
``` | ||
##### Getting results back from the pixel difference detection: | ||
1. event | ||
- A *diff* event will be emitted with a data object passed as the only argument. | ||
- A _diff_ event will be emitted with a data object passed as the only argument. | ||
```javascript | ||
pamDiff.on('diff', data => { | ||
console.log(data); | ||
}); | ||
console.log(data); | ||
}); | ||
``` | ||
2. callback | ||
- A *callback* function will be called with a data object passed as the only argument. | ||
- The callback can be passed as the 2nd argument to the constructor or it can be added later. | ||
- A _callback_ function will be called with a data object passed as the only argument. | ||
- The callback can be passed as the 2nd argument to the constructor or it can be added later. | ||
```javascript | ||
/* callback function */ | ||
/* callback function */ | ||
function myCallback(data) { | ||
console.log(data); | ||
console.log(data); | ||
} | ||
/* via the constructor */ | ||
const pamDiff = new pamDiff({difference: 10, percent: 20}, myCallback); | ||
const pamDiff = new pamDiff({ difference: 10, percent: 20 }, myCallback); | ||
@@ -70,8 +134,11 @@ /* via the setter */ | ||
``` | ||
##### Expected results: | ||
1. When targeting all pixels: | ||
``` | ||
{ | ||
trigger: [ | ||
{name: 'all', percent: 13} | ||
{ name: 'all', percent: 13 } | ||
], | ||
@@ -83,8 +150,10 @@ pam: <Buffer>, | ||
``` | ||
2. When targeting regions of pixels: | ||
``` | ||
{ | ||
trigger: [ | ||
{name: 'region1', percent: 13}, | ||
{name: 'region2', percent: 22} | ||
{ name: 'region1', percent: 13 }, | ||
{ name: 'region2', percent: 22 } | ||
], | ||
@@ -96,7 +165,9 @@ pam: <Buffer>, | ||
``` | ||
3. When targeting all pixels ignored by mask: | ||
``` | ||
{ | ||
trigger: [ | ||
{name: 'mask', percent: 13} | ||
{ name: 'mask', percent: 13 } | ||
], | ||
@@ -108,7 +179,9 @@ pam: <Buffer>, | ||
``` | ||
4. When targeting all pixels and setting {response: "bounds"}: | ||
``` | ||
{ | ||
trigger: [ | ||
{name: 'all', percent: 13, minX: 42, maxX: 399, minY: 113, maxY: 198} | ||
{ name: 'all', percent: 13, minX: 42, maxX: 399, minY: 113, maxY: 198 } | ||
], | ||
@@ -120,3 +193,5 @@ pam: <Buffer>, | ||
``` | ||
5. When targeting all pixels and setting {response: "blobs"}: | ||
``` | ||
@@ -157,5 +232,9 @@ { | ||
``` | ||
### Other Resources: | ||
View the [docs](https://kevingodell.github.io/pam-diff/PamDiff.html), [tests](https://github.com/kevinGodell/pam-diff/tree/master/tests), or [examples](https://github.com/kevinGodell/pam-diff/tree/master/examples) for more implementations. | ||
### Future Plans: | ||
- [x] Make pre-built binaries available when using node-gyp is not an option. | ||
@@ -166,2 +245,2 @@ - [x] Include option to return coordinates for bounding box of changed pixels. | ||
- [x] Include option to return pixel buffer containing bounding boxes around blobs. | ||
- [x] Make async worker threads the default. Can be overridden with {sync: true}. | ||
- [x] Make async worker threads the default. Can be overridden with {sync: true}. |
240
34498
10
555
+ Addednode-addon-api@1.7.2(transitive)
+ Addednode-gyp-build@3.9.0(transitive)
+ Addedpixel-change@1.0.0(transitive)
- Removednode-addon-api@4.3.0(transitive)
- Removednode-gyp-build@4.8.4(transitive)
- Removedpixel-change@1.1.0(transitive)
Updatedpixel-change@1.0.0