Comparing version 0.2.0 to 0.3.2
232
index.js
@@ -6,2 +6,3 @@ // jshint esversion: 6, globalstrict: true, strict: true | ||
const Transform = require('stream').Transform; | ||
const PP = require('polygon-points'); | ||
@@ -13,6 +14,8 @@ function PamDiff(options) { | ||
Transform.call(this, {objectMode: true}); | ||
this.setGrayscale(this._parseOptions('grayscale', options)); | ||
this.setDifference(this._parseOptions('difference', options)); | ||
this.setPercent(this._parseOptions('percent', options)); | ||
this._parseChunk = this._parseFirstChunk; | ||
this.setGrayscale(this._parseOptions('grayscale', options));//global option | ||
this.setDifference(this._parseOptions('difference', options));//global option, can be overridden per region | ||
this.setPercent(this._parseOptions('percent', options));//global option, can be overridden per region | ||
this.setRegions(this._parseOptions('regions', options));//can be no regions or a single region or multiple regions. if no regions, all pixels will be compared. | ||
this._parseChunk = this._parseFirstChunk;//first parsing will be reading settings and configuring internal pixel reading | ||
//todo add option for break on first region so that pixel is not measured in multiple overlapping regions | ||
} | ||
@@ -45,9 +48,40 @@ | ||
PamDiff.prototype.setDifference = function (value) { | ||
this._difference = this._validateNumber(value, 5, 1, 255); | ||
this._difference = this._validateNumber(parseInt(value), 5, 1, 255); | ||
}; | ||
PamDiff.prototype.setPercent = function (value) { | ||
this._percent = this._validateNumber(value, 5, 1, 100); | ||
this._percent = this._validateNumber(parseInt(value), 5, 1, 100); | ||
}; | ||
PamDiff.prototype.setRegions = function (regions) { | ||
if (!regions) { | ||
if (this._regions) { | ||
delete this._regions; | ||
delete this._regionsLength; | ||
} | ||
this._diffs = 0; | ||
return; | ||
} else if (!Array.isArray(regions) || regions.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]]}`); | ||
} | ||
this._regions = []; | ||
for (const region of regions) { | ||
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); | ||
this._regions.push( | ||
{ | ||
name: region.name, | ||
polygon: polygonPoints, | ||
pointsLength: polygonPoints.pointsLength(), | ||
difference: this._validateNumber(parseInt(region.difference), this._difference, 1, 255), | ||
percent: this._validateNumber(parseInt(region.percent), this._percent, 1, 100), | ||
diffs: 0 | ||
} | ||
); | ||
} | ||
this._regionsLength = this._regions.length; | ||
}; | ||
PamDiff.prototype._parseOptions = function (option, options) { | ||
@@ -105,16 +139,39 @@ if (options && options.hasOwnProperty(option)) { | ||
this._newPix = chunk.pixels; | ||
let diffPix = []; | ||
for (let i = 0, x = 0, y = 0; i < this._length; i++, x++) { | ||
if (x === this._width) { | ||
x = 0; | ||
y++; | ||
for (let y = 0, i = 0; y < this._height; y++) { | ||
for (let x = 0; x < this._width; x++, i++) { | ||
if (this._regions) { | ||
for (let j = 0; j < this._regionsLength; j++) { | ||
if (this._regions[j].polygon.containsPoint([x,y]) === true) { | ||
if (this._oldPix[i] !== this._newPix[i]) { | ||
this._regions[j].diffs++; | ||
} | ||
break;//todo add option for break on first region | ||
} | ||
} | ||
} else { | ||
if (this._oldPix[i] !== this._newPix[i]) { | ||
this._diffs++; | ||
} | ||
} | ||
} | ||
if (this._oldPix[i] !== this._newPix[i]) { | ||
diffPix.push([x, y]); | ||
} | ||
if (this._regions) { | ||
const regionDiffArray = []; | ||
for (let i = 0; i < this._regionsLength; i++) { | ||
const percent = Math.ceil(this._regions[i].diffs / this._regions[i].pointsLength * 100); | ||
if (percent >= this._regions[i].percent) { | ||
regionDiffArray.push({name: this._regions[i].name, percent: percent}); | ||
} | ||
this._regions[i].diffs = 0; | ||
} | ||
if (regionDiffArray.length > 0) { | ||
this.emit('diff', {trigger: regionDiffArray, pam: chunk.pam}); | ||
} | ||
} else { | ||
const percent = Math.ceil(this._diffs / this._length * 100); | ||
if (percent >= this._percent) { | ||
this.emit('diff', {trigger: [{name: 'percent', percent: percent}], pam: chunk.pam}); | ||
} | ||
this._diffs = 0; | ||
} | ||
let percent = Math.ceil(diffPix.length / this._length * 100); | ||
if (percent >= this._percent) { | ||
this.emit('diff', {diffPix: diffPix, percent: percent}); | ||
} | ||
this._oldPix = this._newPix; | ||
@@ -125,16 +182,39 @@ }; | ||
this._newPix = chunk.pixels; | ||
let diffPix = []; | ||
for (let i = 0, x = 0, y = 0; i < this._length; i++, x++) { | ||
if (x === this._width) { | ||
x = 0; | ||
y++; | ||
for (let y = 0, i = 0; y < this._height; y++) { | ||
for (let x = 0; x < this._width; x++, i++) { | ||
if (this._regions) { | ||
for (let j = 0; j < this._regionsLength; j++) { | ||
if (this._regions[j].polygon.containsPoint([x,y]) === true) { | ||
if (Math.abs(this._oldPix[i] - this._newPix[i]) >= this._regions[j].difference) { | ||
this._regions[j].diffs++; | ||
} | ||
break;//todo add option for break on first region | ||
} | ||
} | ||
} else { | ||
if (Math.abs(this._oldPix[i] - this._newPix[i]) >= this._difference) { | ||
this._diffs++; | ||
} | ||
} | ||
} | ||
if (Math.abs(this._oldPix[i] - this._newPix[i]) >= this._difference) { | ||
diffPix.push([x, y]); | ||
} | ||
if (this._regions) { | ||
const regionDiffArray = []; | ||
for (let i = 0; i < this._regionsLength; i++) { | ||
const percent = Math.ceil(this._regions[i].diffs / this._regions[i].pointsLength * 100); | ||
if (percent >= this._regions[i].percent) { | ||
regionDiffArray.push({name: this._regions[i].name, percent: percent}); | ||
} | ||
this._regions[i].diffs = 0; | ||
} | ||
if (regionDiffArray.length > 0) { | ||
this.emit('diff', {trigger: regionDiffArray, pam: chunk.pam}); | ||
} | ||
} else { | ||
const percent = Math.ceil(this._diffs / this._length * 100); | ||
if (percent >= this._percent) { | ||
this.emit('diff', {trigger: [{name: 'percent', percent: percent}], pam: chunk.pam}); | ||
} | ||
this._diffs = 0; | ||
} | ||
let percent = Math.ceil(diffPix.length / this._length * 100); | ||
if (percent >= this._percent) { | ||
this.emit('diff', {diffPix: diffPix, percent: percent}); | ||
} | ||
this._oldPix = this._newPix; | ||
@@ -145,16 +225,39 @@ }; | ||
this._newPix = chunk.pixels; | ||
let diffPix = []; | ||
for (let i = 0, x = 0, y = 0; i < this._length; i+=3, x++) { | ||
if (x === this._width) { | ||
x = 0; | ||
y++; | ||
for (let y = 0, i = 0; y < this._height; y++) { | ||
for (let x = 0; x < this._width; x++, i += 3) { | ||
if (this._regions) { | ||
for (let j = 0; j < this._regionsLength; j++) { | ||
if (this._regions[j].polygon.containsPoint([x,y]) === true) { | ||
if (Math.abs(this._grayscale(this._oldPix[i], this._oldPix[i + 1], this._oldPix[i + 2]) - this._grayscale(this._newPix[i], this._newPix[i + 1], this._newPix[i + 2])) >= this._regions[j].difference) { | ||
this._regions[j].diffs++; | ||
} | ||
break;//todo add option for break on first region | ||
} | ||
} | ||
} else { | ||
if (Math.abs(this._oldPix[i] - this._newPix[i]) >= this._difference) { | ||
this._diffs++; | ||
} | ||
} | ||
} | ||
if (Math.abs(this._grayscale(this._oldPix[i], this._oldPix[i + 1], this._oldPix[i + 2]) - this._grayscale(this._newPix[i], this._newPix[i + 1], this._newPix[i + 2])) >= this._difference) { | ||
diffPix.push([x, y]); | ||
} | ||
if (this._regions) { | ||
const regionDiffArray = []; | ||
for (let i = 0; i < this._regionsLength; i++) { | ||
const percent = Math.ceil(this._regions[i].diffs / this._regions[i].pointsLength * 100); | ||
if (percent >= this._regions[i].percent) { | ||
regionDiffArray.push({name: this._regions[i].name, percent: percent}); | ||
} | ||
this._regions[i].diffs = 0; | ||
} | ||
if (regionDiffArray.length > 0) { | ||
this.emit('diff', {trigger: regionDiffArray, pam: chunk.pam}); | ||
} | ||
} else { | ||
const percent = Math.ceil(this._diffs / this._length * 100); | ||
if (percent >= this._percent) { | ||
this.emit('diff', {trigger: [{name: 'percent', percent: percent}], pam: chunk.pam}); | ||
} | ||
this._diffs = 0; | ||
} | ||
let percent = Math.ceil(diffPix.length / this._length * 100); | ||
if (percent >= this._percent) { | ||
this.emit('diff', {diffPix: diffPix, percent: percent}); | ||
} | ||
this._oldPix = this._newPix; | ||
@@ -165,16 +268,39 @@ }; | ||
this._newPix = chunk.pixels; | ||
let diffPix = []; | ||
for (let i = 0, x = 0, y = 0; i < this._length; i+=4, x++) { | ||
if (x === this._width) { | ||
x = 0; | ||
y++; | ||
for (let y = 0, i = 0; y < this._height; y++) { | ||
for (let x = 0; x < this._width; x++, i += 4) { | ||
if (this._regions) { | ||
for (let j = 0; j < this._regionsLength; j++) { | ||
if (this._regions[j].polygon.containsPoint([x,y]) === true) { | ||
if (Math.abs(this._grayscale(this._oldPix[i], this._oldPix[i + 1], this._oldPix[i + 2]) - this._grayscale(this._newPix[i], this._newPix[i + 1], this._newPix[i + 2])) >= this._regions[j].difference) { | ||
this._regions[j].diffs++; | ||
} | ||
break;//todo add option for break on first region | ||
} | ||
} | ||
} else { | ||
if (Math.abs(this._oldPix[i] - this._newPix[i]) >= this._difference) { | ||
this._diffs++; | ||
} | ||
} | ||
} | ||
if (Math.abs(this._grayscale(this._oldPix[i], this._oldPix[i + 1], this._oldPix[i + 2]) - this._grayscale(this._newPix[i], this._newPix[i + 1], this._newPix[i + 2])) >= this._difference) { | ||
diffPix.push([x, y]); | ||
} | ||
if (this._regions) { | ||
const regionDiffArray = []; | ||
for (let i = 0; i < this._regionsLength; i++) { | ||
const percent = Math.ceil(this._regions[i].diffs / this._regions[i].pointsLength * 100); | ||
if (percent >= this._regions[i].percent) { | ||
regionDiffArray.push({name: this._regions[i].name, percent: percent}); | ||
} | ||
this._regions[i].diffs = 0; | ||
} | ||
if (regionDiffArray.length > 0) { | ||
this.emit('diff', {trigger: regionDiffArray, pam: chunk.pam}); | ||
} | ||
} else { | ||
const percent = Math.ceil(this._diffs / this._length * 100); | ||
if (percent >= this._percent) { | ||
this.emit('diff', {trigger: [{name: 'percent', percent: percent}], pam: chunk.pam}); | ||
} | ||
this._diffs = 0; | ||
} | ||
let percent = Math.ceil(diffPix.length / this._length * 100); | ||
if (percent >= this._percent) { | ||
this.emit('diff', {diffPix: diffPix, percent: percent}); | ||
} | ||
this._oldPix = this._newPix; | ||
@@ -185,2 +311,3 @@ }; | ||
this._width = parseInt(chunk.width); | ||
this._height = parseInt(chunk.height); | ||
this._oldPix = chunk.pixels; | ||
@@ -197,5 +324,7 @@ this._length = this._oldPix.length; | ||
this._parseChunk = this._rgbPixelDiff; | ||
//this._increment = 3;//future use | ||
break; | ||
case 'rgb_alpha' : | ||
this._parseChunk = this._rgbAlphaPixelDiff; | ||
//this._increment = 4;//future use | ||
break; | ||
@@ -221,2 +350,3 @@ default : | ||
module.exports = PamDiff; | ||
module.exports = PamDiff; | ||
//todo get bounding box of all regions combined to exclude some pixels before checking if they exist inside specific regions |
{ | ||
"name": "pam-diff", | ||
"version": "0.2.0", | ||
"version": "0.3.2", | ||
"description": "Measure differences between pixel arrays extracted from pam images", | ||
@@ -18,6 +18,9 @@ "main": "index.js", | ||
"pixel", | ||
"difference" | ||
"difference", | ||
"motion", | ||
"detection", | ||
"region" | ||
], | ||
"author": "Kevin Godell <kevin.godell@gmail.com>", | ||
"license": "Apache-2.0", | ||
"license": "MIT", | ||
"bugs": { | ||
@@ -27,6 +30,8 @@ "url": "https://github.com/kevinGodell/pam-diff/issues" | ||
"homepage": "https://github.com/kevinGodell/pam-diff#readme", | ||
"dependencies": {}, | ||
"dependencies": { | ||
"polygon-points": "0.0.1" | ||
}, | ||
"devDependencies": { | ||
"pipe2pam": "^0.2.1" | ||
"pipe2pam": "^0.3.0" | ||
} | ||
} |
# 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 rgb24, rgb_alpha, grayscale, and monob. | ||
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 rgb24, rgb_alpha, grayscale, and monob. It is currently being used for a video motion detection project. | ||
@@ -15,3 +15,3 @@ | ||
The following [example](https://github.com/kevinGodell/pam-diff/tree/master/examples/example.js) uses ffmpeg's testsrc to simulate a video input and generate 1000 downscaled grayscale pam images at a rate of 1 per second. The pam images are piped from ffmpeg's stdout into pipe2pam to parse them into into pam objects. The pam objects are then piped into pam-diff to measure pixel differences. For each compared pixel that has a **difference** that exceeds the setting, it will be added to an array of x y coordinates. If the **percent** of changed pixels exceeds the setting, a **diff** event will be emitted which contains an array of pixel coordinates that have changed. | ||
The following [example](https://github.com/kevinGodell/pam-diff/tree/master/examples/example.js) uses ffmpeg to connect to a rtsp ip camera video feed and generates 1000 downscaled rgb24 pam images at a rate of 1 per second. The pam images are piped from ffmpeg's stdout into pipe2pam to parse them into into pam objects. The pam objects are then piped into pam-diff to measure pixel differences. For each compared pixel that has a **difference** that exceeds the setting, it will be calculated to determine the percent of difference. If the **percent** of changed pixels exceeds the setting, a **diff** event will be emitted which contains a data object containing details. This example also shows how to take the pam image that triggered the diff event and convert it to a jpeg using ffmpeg. | ||
@@ -21,3 +21,5 @@ ``` | ||
const PamDiff = require('pam-diff'); | ||
const spawn = require('child_process').spawn; | ||
const ChildProcess = require('child_process'); | ||
const spawn = ChildProcess.spawn; | ||
const execFile = ChildProcess.execFile; | ||
@@ -27,29 +29,31 @@ const params = [ | ||
'quiet', | ||
/* use hardware acceleration */ | ||
//'-hwaccel', | ||
//'auto', //vda, videotoolbox, none, auto | ||
'-hwaccel', | ||
'auto', //vda, videotoolbox, none, auto | ||
/* use an artificial video input */ | ||
'-re', | ||
'-f', | ||
'lavfi', | ||
'-i', | ||
'testsrc=size=1920x1080:rate=15', | ||
/*'-re', | ||
'-f', | ||
'lavfi', | ||
'-i', | ||
'testsrc=size=1920x1080:rate=15',*/ | ||
/* use an rtsp ip cam video input */ | ||
/*'-rtsp_transport', | ||
'tcp', //udp, http, tcp | ||
'-rtsp_transport', | ||
'tcp', | ||
'-i', | ||
'rtsp://192.168.1.22:554/user=admin_password=pass_channel=1_stream=0.sdp',*/ | ||
'rtsp://192.168.1.4:554/user=admin_password=pass_channel=1_stream=0.sdp', | ||
/* set output flags */ | ||
'-an', | ||
'-c:v', | ||
'pam', | ||
'-pix_fmt', | ||
'rgb24',//rgba, rgb24, gray | ||
'-f', | ||
'image2pipe', | ||
'-pix_fmt', | ||
'gray',//rgb24, rgba, monob, gray | ||
'-vf', | ||
'fps=1,scale=iw*1/6:ih*1/6', | ||
'fps=1,scale=320:180',//1920:1080 scaled down: 400:225, 384:216, 368:207, 352:198, 336:189, 320:180 | ||
//'fps=1,scale=iw*1/6:ih*1/6', | ||
'-frames', | ||
@@ -77,11 +81,29 @@ '1000', | ||
p2p.on('pam', function(data) { | ||
//you do not have to do anything here if you are just piping this data to pam-diff | ||
console.log('received pam', ++counter); | ||
//you do not have to listen to this event if you are just piping this data to pam-diff | ||
console.log(`received pam ${++counter}`); | ||
}); | ||
const pamDiff = new PamDiff({grayscale: 'average', difference: 4, percent: 5}); | ||
const pamDiff = new PamDiff({grayscale: 'average', difference: 5, percent: 5}); | ||
pamDiff.on('diff', function(data) { | ||
//further analyze the pixels for regions or trigger motion detection from this event | ||
console.log(`${data.diffPix.length} pixels different, ${data.percent}%`); | ||
console.log(data); | ||
//comment out the following line if you want to use ffmpeg to create a jpeg from the pam image that triggered an image difference event | ||
if(true){return;} | ||
const date = new Date(); | ||
let name = `${date.getUTCFullYear()}-${date.getUTCMonth() + 1}-${date.getUTCDate()}_${date.getHours()}-${date.getUTCMinutes()}-${date.getUTCSeconds()}-${date.getUTCMilliseconds()}`; | ||
for (const region of data.trigger) { | ||
name += `(${region.name}=${region.percent})`; | ||
} | ||
const jpeg = `${name}.jpeg`; | ||
const ff = execFile('ffmpeg', ['-report', '-f', 'pam_pipe', '-c:v', 'pam', '-i', 'pipe:0', '-c:v', 'mjpeg', '-pix_fmt', 'yuvj422p', '-q:v', '1', '-huffman', 'optimal', jpeg]); | ||
ff.stdin.end(data.pam); | ||
ff.on('exit', function (data) { | ||
if (data === 0) { | ||
console.log(`FFMPEG clean exit after creating ${jpeg}`); | ||
} else { | ||
throw new Error('FFMPEG is not working with current parameters'); | ||
} | ||
}); | ||
}); | ||
@@ -88,0 +110,0 @@ |
Sorry, the diff of this file is not supported yet
318
108
18533
1
+ Addedpolygon-points@0.0.1
+ Addedpolygon-points@0.0.1(transitive)