693 lines
23 KiB
JavaScript
693 lines
23 KiB
JavaScript
//
|
|
// Shinobi - fork of pam-diff
|
|
// Copyright (C) 2018 Kevin Godell
|
|
// Author : Kevin Godell, https://github.com/kevinGodell
|
|
// npmjs : https://www.npmjs.com/package/pam-diff
|
|
// Github : https://github.com/kevinGodell/pam-diff
|
|
//
|
|
'use strict';
|
|
|
|
const { Transform } = require('stream');
|
|
|
|
const PP = require('polygon-points');
|
|
/**
|
|
*
|
|
* @param chunk
|
|
* @private
|
|
*/
|
|
var _getMatrixFromPoints = function(thisRegion) {
|
|
var coordinates = [
|
|
thisRegion.topLeft,
|
|
{"x" : thisRegion.bottomRight.x, "y" : thisRegion.topLeft.y},
|
|
thisRegion.bottomRight
|
|
]
|
|
var width = Math.sqrt( Math.pow(coordinates[1].x - coordinates[0].x, 2) + Math.pow(coordinates[1].y - coordinates[0].y, 2));
|
|
var height = Math.sqrt( Math.pow(coordinates[2].x - coordinates[1].x, 2) + Math.pow(coordinates[2].y - coordinates[1].y, 2))
|
|
return {
|
|
x: coordinates[0].x,
|
|
y: coordinates[0].y,
|
|
width: width,
|
|
height: height,
|
|
tag: thisRegion.name
|
|
}
|
|
}
|
|
class PamDiff extends Transform {
|
|
|
|
/**
|
|
*
|
|
* @param [options] {Object}
|
|
* @param [callback] {Function}
|
|
*/
|
|
constructor(options, callback) {
|
|
super(options);
|
|
Transform.call(this, {objectMode: true});
|
|
this.difference = PamDiff._parseOptions('difference', options);//global option, can be overridden per region
|
|
this.percent = PamDiff._parseOptions('percent', options);//global option, can be overridden per region
|
|
this.regions = PamDiff._parseOptions('regions', options);//can be no regions or a single region or multiple regions. if no regions, all pixels will be compared.
|
|
this.drawMatrix = PamDiff._parseOptions('drawMatrix', options);//can be no regions or 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 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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @deprecated
|
|
* @param string {String}
|
|
*/
|
|
setGrayscale(string) {
|
|
console.warn('grayscale option has been removed, "average" has proven to most accurate and is the default');
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param number {Number}
|
|
*/
|
|
set difference(number) {
|
|
this._difference = PamDiff._validateNumber(parseInt(number), 5, 1, 255);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @return {Number}
|
|
*/
|
|
get difference() {
|
|
return this._difference;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @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);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @return {Number}
|
|
*/
|
|
get percent() {
|
|
return this._percent;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param number {Number}
|
|
* @return {PamDiff}
|
|
*/
|
|
setPercent(number) {
|
|
this.percent = number;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param array {Array}
|
|
*/
|
|
set regions(array) {
|
|
if (!array) {
|
|
if (this._regions) {
|
|
delete this._regions;
|
|
delete this._regionsLength;
|
|
delete this._minDiff;
|
|
}
|
|
this._diffs = 0;
|
|
} 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 = [];
|
|
this._minDiff = 255;
|
|
for (const region of array) {
|
|
if (!region.hasOwnProperty('name') || !region.hasOwnProperty('polygon')) {
|
|
throw new Error('Region must include a name and a polygon property');
|
|
}
|
|
const polygonPoints = new PP(region.polygon);
|
|
const difference = PamDiff._validateNumber(parseInt(region.difference), this._difference, 1, 255);
|
|
const percent = PamDiff._validateNumber(parseInt(region.percent), this._percent, 1, 100);
|
|
this._minDiff = Math.min(this._minDiff, difference);
|
|
this._regions.push(
|
|
{
|
|
name: region.name,
|
|
polygon: polygonPoints,
|
|
difference: difference,
|
|
percent: percent,
|
|
diffs: 0
|
|
}
|
|
);
|
|
}
|
|
this._regionsLength = this._regions.length;
|
|
this._createPointsInPolygons(this._regions, this._width, this._height);
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @return {Array}
|
|
*/
|
|
get regions() {
|
|
return this._regions;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param array {Array}
|
|
* @return {PamDiff}
|
|
*/
|
|
setRegions(array) {
|
|
this.regions = array;
|
|
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.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @return {Function}
|
|
*/
|
|
get callback() {
|
|
return this._callback;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param func {Function}
|
|
* @return {PamDiff}
|
|
*/
|
|
setCallback(func) {
|
|
this.callback = func;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @return {PamDiff}
|
|
*/
|
|
resetCache() {
|
|
//delete this._oldPix;
|
|
//delete this._newPix;
|
|
//delete this._width;
|
|
//delete this._length;
|
|
this._parseChunk = this._parseFirstChunk;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param regions {Array}
|
|
* @param width {Number}
|
|
* @param height {Number}
|
|
* @private
|
|
*/
|
|
_createPointsInPolygons(regions, width, height) {
|
|
if (regions && width && height) {
|
|
this._pointsInPolygons = [];
|
|
for (const region of regions) {
|
|
const bitset = region.polygon.getBitset(this._width, this._height);
|
|
region.pointsLength = bitset.count;
|
|
this._pointsInPolygons.push(bitset.buffer);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param chunk
|
|
* @private
|
|
*/
|
|
_blackAndWhitePixelDiff(chunk) {
|
|
this._newPix = chunk.pixels;
|
|
for (let y = 0, i = 0; y < this._height; y++) {
|
|
for (let x = 0; x < this._width; x++, i++) {
|
|
const diff = this._oldPix[i] !== this._newPix[i];
|
|
if (this._regions && diff === true) {
|
|
for (let j = 0; j < this._regionsLength; j++) {
|
|
if (this._pointsInPolygons[j][i]) {
|
|
this._regions[j].diffs++;
|
|
}
|
|
}
|
|
} else if (diff === true) {
|
|
this._diffs++;
|
|
}
|
|
}
|
|
}
|
|
if (this._regions) {
|
|
const regionDiffArray = [];
|
|
for (let i = 0; i < this._regionsLength; i++) {
|
|
const percent = Math.floor(100 * this._regions[i].diffs / this._regions[i].pointsLength);
|
|
if (percent >= this._regions[i].percent) {
|
|
regionDiffArray.push({name: this._regions[i].name, percent: percent});
|
|
}
|
|
this._regions[i].diffs = 0;
|
|
}
|
|
if (regionDiffArray.length > 0) {
|
|
const data = {trigger: regionDiffArray, pam: chunk.pam};
|
|
if (this._callback) {
|
|
this._callback(data);
|
|
}
|
|
if (this._readableState.pipesCount > 0) {
|
|
this.push(data);
|
|
}
|
|
if (this.listenerCount('diff') > 0) {
|
|
this.emit('diff', data);
|
|
}
|
|
}
|
|
} else {
|
|
const percent = Math.floor(100 * this._diffs / this._length);
|
|
if (percent >= this._percent) {
|
|
const data = {trigger: [{name: 'percent', percent: percent}], pam: chunk.pam};
|
|
if (this._callback) {
|
|
this._callback(data);
|
|
}
|
|
if (this._readableState.pipesCount > 0) {
|
|
this.push(data);
|
|
}
|
|
if (this.listenerCount('diff') > 0) {
|
|
this.emit('diff', data);
|
|
}
|
|
}
|
|
this._diffs = 0;
|
|
}
|
|
this._oldPix = this._newPix;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param chunk
|
|
* @private
|
|
*/
|
|
_grayScalePixelDiffWithMatrices(chunk) {
|
|
this._newPix = chunk.pixels;
|
|
for (let j = 0; j < this._regionsLength; j++) {
|
|
this._regions[j].topLeft = {
|
|
x: this._width,
|
|
y: this._height
|
|
}
|
|
this._regions[j].bottomRight = {
|
|
x: 0,
|
|
y: 0
|
|
}
|
|
}
|
|
this.topLeft = {
|
|
x: this._width,
|
|
y: this._height
|
|
}
|
|
this.bottomRight = {
|
|
x: 0,
|
|
y: 0
|
|
}
|
|
for (let y = 0, i = 0; y < this._height; y++) {
|
|
for (let x = 0; x < this._width; x++, i++) {
|
|
if (this._oldPix[i] !== this._newPix[i]) {
|
|
const diff = Math.abs(this._oldPix[i] - this._newPix[i]);
|
|
if (this._regions && diff >= this._minDiff) {
|
|
for (let j = 0; j < this._regionsLength; j++) {
|
|
if (this._pointsInPolygons[j][i] && diff >= this._regions[j].difference) {
|
|
var theRegion = this._regions[j]
|
|
theRegion.diffs++;
|
|
if(theRegion.topLeft.x > x && theRegion.topLeft.y > y){
|
|
theRegion.topLeft.x = x
|
|
theRegion.topLeft.y = y
|
|
}
|
|
if(theRegion.bottomRight.x < x && theRegion.bottomRight.y < y){
|
|
theRegion.bottomRight.x = x
|
|
theRegion.bottomRight.y = y
|
|
}
|
|
}
|
|
}
|
|
} else if (diff >= this._difference) {
|
|
this._diffs++;
|
|
if(this.topLeft.x > x && this.topLeft.y > y){
|
|
this.topLeft.x = x
|
|
this.topLeft.y = y
|
|
}
|
|
if(this.bottomRight.x < x && this.bottomRight.y < y){
|
|
this.bottomRight.x = x
|
|
this.bottomRight.y = y
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (this._regions) {
|
|
const regionDiffArray = [];
|
|
for (let i = 0; i < this._regionsLength; i++) {
|
|
var thisRegion = this._regions[i]
|
|
const percent = Math.floor(100 * thisRegion.diffs / thisRegion.pointsLength);
|
|
if (percent >= thisRegion.percent) {
|
|
// create matrix from points >>
|
|
thisRegion._matrix = _getMatrixFromPoints(thisRegion)
|
|
// create matrix from points />>
|
|
regionDiffArray.push({name: thisRegion.name, percent: percent, matrix: thisRegion._matrix});
|
|
}
|
|
thisRegion.diffs = 0;
|
|
}
|
|
if (regionDiffArray.length > 0) {
|
|
this._matrix = _getMatrixFromPoints(this)
|
|
const data = {trigger: regionDiffArray, pam: chunk.pam, matrix: this._matrix};
|
|
if (this._callback) {
|
|
this._callback(data);
|
|
}
|
|
if (this._readableState.pipesCount > 0) {
|
|
this.push(data);
|
|
}
|
|
if (this.listenerCount('diff') > 0) {
|
|
this.emit('diff', data);
|
|
}
|
|
}
|
|
} else {
|
|
const percent = Math.floor(100 * this._diffs / this._length);
|
|
if (percent >= this._percent) {
|
|
this._matrix = _getMatrixFromPoints(this)
|
|
const data = {trigger: [{name: 'percent', percent: percent, matrix: this._matrix}], pam: chunk.pam};
|
|
if (this._callback) {
|
|
this._callback(data);
|
|
}
|
|
if (this._readableState.pipesCount > 0) {
|
|
this.push(data);
|
|
}
|
|
if (this.listenerCount('diff') > 0) {
|
|
this.emit('diff', data);
|
|
}
|
|
}
|
|
this._diffs = 0;
|
|
}
|
|
this._oldPix = this._newPix;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param chunk
|
|
* @private
|
|
*/
|
|
_grayScalePixelDiff(chunk) {
|
|
this._newPix = chunk.pixels;
|
|
for (let y = 0, i = 0; y < this._height; y++) {
|
|
for (let x = 0; x < this._width; x++, i++) {
|
|
if (this._oldPix[i] !== this._newPix[i]) {
|
|
const diff = Math.abs(this._oldPix[i] - this._newPix[i]);
|
|
if (this._regions && diff >= this._minDiff) {
|
|
for (let j = 0; j < this._regionsLength; j++) {
|
|
if (this._pointsInPolygons[j][i] && diff >= this._regions[j].difference) {
|
|
this._regions[j].diffs++;
|
|
}
|
|
}
|
|
} else {
|
|
if (diff >= this._difference) {
|
|
this._diffs++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (this._regions) {
|
|
const regionDiffArray = [];
|
|
for (let i = 0; i < this._regionsLength; i++) {
|
|
const percent = Math.floor(100 * this._regions[i].diffs / this._regions[i].pointsLength);
|
|
if (percent >= this._regions[i].percent) {
|
|
regionDiffArray.push({name: this._regions[i].name, percent: percent});
|
|
}
|
|
this._regions[i].diffs = 0;
|
|
}
|
|
if (regionDiffArray.length > 0) {
|
|
const data = {trigger: regionDiffArray, pam: chunk.pam};
|
|
if (this._callback) {
|
|
this._callback(data);
|
|
}
|
|
if (this._readableState.pipesCount > 0) {
|
|
this.push(data);
|
|
}
|
|
if (this.listenerCount('diff') > 0) {
|
|
this.emit('diff', data);
|
|
}
|
|
}
|
|
} else {
|
|
const percent = Math.floor(100 * this._diffs / this._length);
|
|
if (percent >= this._percent) {
|
|
const data = {trigger: [{name: 'percent', percent: percent}], pam: chunk.pam};
|
|
if (this._callback) {
|
|
this._callback(data);
|
|
}
|
|
if (this._readableState.pipesCount > 0) {
|
|
this.push(data);
|
|
}
|
|
if (this.listenerCount('diff') > 0) {
|
|
this.emit('diff', data);
|
|
}
|
|
}
|
|
this._diffs = 0;
|
|
}
|
|
this._oldPix = this._newPix;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param chunk
|
|
* @private
|
|
*/
|
|
_rgbPixelDiff(chunk) {
|
|
this._newPix = chunk.pixels;
|
|
for (let y = 0, i = 0, p = 0; y < this._height; y++) {
|
|
for (let x = 0; x < this._width; x++, i += 3, p++) {
|
|
if (this._oldPix[i] !== this._newPix[i] || this._oldPix[i + 1] !== this._newPix[i + 1] || this._oldPix[i + 2] !== this._newPix[i + 2]) {
|
|
const diff = Math.abs(this._oldPix[i] + this._oldPix[i + 1] + this._oldPix[i + 2] - this._newPix[i] - this._newPix[i + 1] - this._newPix[i + 2])/3;
|
|
if (this._regions && diff >= this._minDiff) {
|
|
for (let j = 0; j < this._regionsLength; j++) {
|
|
if (this._pointsInPolygons[j][p] && diff >= this._regions[j].difference) {
|
|
this._regions[j].diffs++;
|
|
}
|
|
}
|
|
} else {
|
|
if (diff >= this._difference) {
|
|
this._diffs++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (this._regions) {
|
|
const regionDiffArray = [];
|
|
for (let i = 0; i < this._regionsLength; i++) {
|
|
const percent = Math.floor(100 * this._regions[i].diffs / this._regions[i].pointsLength);
|
|
if (percent >= this._regions[i].percent) {
|
|
regionDiffArray.push({name: this._regions[i].name, percent: percent});
|
|
}
|
|
this._regions[i].diffs = 0;
|
|
}
|
|
if (regionDiffArray.length > 0) {
|
|
const data = {trigger: regionDiffArray, pam: chunk.pam};
|
|
if (this._callback) {
|
|
this._callback(data);
|
|
}
|
|
if (this._readableState.pipesCount > 0) {
|
|
this.push(data);
|
|
}
|
|
if (this.listenerCount('diff') > 0) {
|
|
this.emit('diff', data);
|
|
}
|
|
}
|
|
} else {
|
|
const percent = Math.floor(100 * this._diffs / this._length);
|
|
if (percent >= this._percent) {
|
|
const data = {trigger: [{name: 'percent', percent: percent}], pam: chunk.pam};
|
|
if (this._callback) {
|
|
this._callback(data);
|
|
}
|
|
if (this._readableState.pipesCount > 0) {
|
|
this.push(data);
|
|
}
|
|
if (this.listenerCount('diff') > 0) {
|
|
this.emit('diff', data);
|
|
}
|
|
}
|
|
this._diffs = 0;
|
|
}
|
|
this._oldPix = this._newPix;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param chunk
|
|
* @private
|
|
*/
|
|
_rgbAlphaPixelDiff(chunk) {
|
|
this._newPix = chunk.pixels;
|
|
for (let y = 0, i = 0, p = 0; y < this._height; y++) {
|
|
for (let x = 0; x < this._width; x++, i += 4, p++) {
|
|
if (this._oldPix[i] !== this._newPix[i] || this._oldPix[i + 1] !== this._newPix[i + 1] || this._oldPix[i + 2] !== this._newPix[i + 2]) {
|
|
const diff = Math.abs(this._oldPix[i] + this._oldPix[i + 1] + this._oldPix[i + 2] - this._newPix[i] - this._newPix[i + 1] - this._newPix[i + 2])/3;
|
|
if (this._regions && diff >= this._minDiff) {
|
|
for (let j = 0; j < this._regionsLength; j++) {
|
|
if (this._pointsInPolygons[j][p] && diff >= this._regions[j].difference) {
|
|
this._regions[j].diffs++;
|
|
}
|
|
}
|
|
} else {
|
|
if (diff >= this._difference) {
|
|
this._diffs++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (this._regions) {
|
|
const regionDiffArray = [];
|
|
for (let i = 0; i < this._regionsLength; i++) {
|
|
const percent = Math.floor(100 * this._regions[i].diffs / this._regions[i].pointsLength);
|
|
if (percent >= this._regions[i].percent) {
|
|
regionDiffArray.push({name: this._regions[i].name, percent: percent});
|
|
}
|
|
this._regions[i].diffs = 0;
|
|
}
|
|
if (regionDiffArray.length > 0) {
|
|
const data = {trigger: regionDiffArray, pam: chunk.pam};
|
|
if (this._callback) {
|
|
this._callback(data);
|
|
}
|
|
if (this._readableState.pipesCount > 0) {
|
|
this.push(data);
|
|
}
|
|
if (this.listenerCount('diff') > 0) {
|
|
this.emit('diff', data);
|
|
}
|
|
}
|
|
} else {
|
|
const percent = Math.floor(100 * this._diffs / this._length);
|
|
if (percent >= this._percent) {
|
|
const data = {trigger: [{name: 'percent', percent: percent}], pam: chunk.pam};
|
|
if (this._callback) {
|
|
this._callback(data);
|
|
}
|
|
if (this._readableState.pipesCount > 0) {
|
|
this.push(data);
|
|
}
|
|
if (this.listenerCount('diff') > 0) {
|
|
this.emit('diff', data);
|
|
}
|
|
}
|
|
this._diffs = 0;
|
|
}
|
|
this._oldPix = this._newPix;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param chunk
|
|
* @private
|
|
*/
|
|
_parseFirstChunk(chunk) {
|
|
this._width = parseInt(chunk.width);
|
|
this._height = parseInt(chunk.height);
|
|
this._oldPix = chunk.pixels;
|
|
this._length = this._width * this._height;
|
|
this._createPointsInPolygons(this._regions, this._width, this._height);
|
|
switch (chunk.tupltype) {
|
|
case 'blackandwhite' :
|
|
this._parseChunk = this._blackAndWhitePixelDiff;
|
|
break;
|
|
case 'grayscale' :
|
|
if(this.drawMatrix === "1"){
|
|
this._parseChunk = this._grayScalePixelDiffWithMatrices;
|
|
}else{
|
|
this._parseChunk = this._grayScalePixelDiff;
|
|
}
|
|
break;
|
|
case 'rgb' :
|
|
this._parseChunk = this._rgbPixelDiff;
|
|
//this._increment = 3;//future use
|
|
break;
|
|
case 'rgb_alpha' :
|
|
this._parseChunk = this._rgbAlphaPixelDiff;
|
|
//this._increment = 4;//future use
|
|
break;
|
|
default :
|
|
throw Error(`Unsupported tupltype: ${chunk.tupltype}. Supported tupltypes include grayscale(gray), blackandwhite(monob), rgb(rgb24), and rgb_alpha(rgba).`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param chunk
|
|
* @param encoding
|
|
* @param callback
|
|
* @private
|
|
*/
|
|
_transform(chunk, encoding, callback) {
|
|
this._parseChunk(chunk);
|
|
callback();
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param callback
|
|
* @private
|
|
*/
|
|
_flush(callback) {
|
|
this.resetCache();
|
|
callback();
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @type {PamDiff}
|
|
*/
|
|
module.exports = PamDiff;
|
|
//todo get bounding box of all regions combined to exclude some pixels before checking if they exist inside specific regions
|