diff --git a/binding.gyp b/binding.gyp index 69b4d12..d0e3d17 100755 --- a/binding.gyp +++ b/binding.gyp @@ -20,7 +20,8 @@ "src/Calib3D.cc", "src/ImgProc.cc", "src/Stereo.cc", - "src/LDAWrap.cc" + "src/LDAWrap.cc", + "src/Histogram.cc", ], "libraries": [ diff --git a/examples/calc-hist.js b/examples/calc-hist.js new file mode 100644 index 0000000..2e0c38c --- /dev/null +++ b/examples/calc-hist.js @@ -0,0 +1,67 @@ +var cv = require('../lib/opencv'); + +// (B)lue, (G)reen, (R)ed +var histSize = 256; + +cv.readImage('./files/car1.jpg', function(err, im) { + if (err) throw err; + if (im.width() < 1 || im.height() < 1) throw new Error('Image has no size'); + + var bgrPlanes = im.split(); + + var size = 256, + range = [0, 256], + uniform = true, + histFile = 'files/chart2.png'; + + + /// Compute a 3 dimension histogram + var hist64 = cv.histogram.calcHist( im, [0, 1, 2], [4, 4, 4], [[0, 256], [0, 256], [0, 256]], uniform); + + /// Compute 3 histograms + var bHist = cv.histogram.calcHist( im, [0], [size], [range], uniform); + var gHist = cv.histogram.calcHist( im, [1], [size], [range], uniform); + var rHist = cv.histogram.calcHist( im, [2], [size], [range], uniform); + +////// +// Uncommentand run `npm install chartjs-node` to draw the histogram ! +/// +/* + var ChartjsNode = require('chartjs-node'); + var chartNode = new ChartjsNode(1200, 1200); + chartNode.drawChart({ + type: 'bar', + data: { + labels: bHist.map(function(a,i){return i.toString()}), + datasets : [{ + data : bHist, + backgroundColor : "#4183c4", + borderColor : "#0c4b8a", + label : 'Blue' + },{ + data : gHist, + backgroundColor : "#83c441", + borderColor : "#0c4b8a", + label : 'Green' + },{ + data : rHist, + backgroundColor : "#c44183", + borderColor : "#0c4b8a", + label : 'Red' + }] + }, + options: { + title: { + display: true, + text: 'RGB Histograms' + } + } + }).then(function(){ + return chartNode.writeImageToFile('image/png', histFile); + }).then(function(){ + console.log("result has been written in "+histFile) + }).catch(function(e){ + console.log("error",e) + }); +*/ +}); diff --git a/examples/emd.js b/examples/emd.js new file mode 100644 index 0000000..e13685c --- /dev/null +++ b/examples/emd.js @@ -0,0 +1,161 @@ +var cv = require('../lib/opencv'); + +// +// Example of use of EMD distance using histograms +// 1. Build 2 histograms from images using calcHist +// 2. Transform each histogram to a 64 x 4 (hist, b, g, r) x 1 normalized signatures in BGR space +// 3. Compute the cost matrix (64 x 64 x 1), calculating the cost in LUV space +// 4. Run EMD algorithm +// + +/// Useful flatten function for step 2 + +function flatten(array, accu) { + if(!accu){ + accu = []; + } + array.forEach(function(a){ + if(Array.isArray(a)) { + flatten(a, accu) + } else { + accu.push(a) + } + }); + return accu +}; + +cv.readImage('./files/car1.jpg', function(err, im1) { + if (err) throw err; + if (im1.width() < 1 || im1.height() < 1) throw new Error('Image has no size'); + cv.readImage('./files/car2.jpg', function(err, im2) { + if (err) throw err; + if (im2.width() < 1 || im2.height() < 1) throw new Error('Image has no size'); + + /////////////////// + // 1. Build 2 histograms from images using calcHist + ////////////////// + + var size = [4, 4, 4], + channels = [0, 1, 2], + range = [[0, 256], [0, 256], [0, 256]], + uniform = true, + accumulate = true, + histFile = 'files/chart2.png'; + + /// Compute 64 (=4^3) histograms: + var firstImageHist64 = cv.histogram.calcHist(im1, channels, size, range, uniform); + var secondImageHist64 = cv.histogram.calcHist(im2, channels, size, range, uniform); + + ////////////// + // 2. Transform each histogram to a 64 x 4 (hist, b, g, r) x 1 normalized signatures in BGR space + //////////////// + + var step = 256/4; + var halfStep = Math.round(step/2); + + var sum1 = 0; + var sum2 = 0; + + firstImageHist64.map(function(bHist, bIndex){ + return bHist.map(function(bgHist, gIndex){ + return bgHist.map(function(bgrHist, rIndex){ + sum1 += bgrHist; + }) + }) + }) + + var sig1 = flatten(firstImageHist64.map(function(bHist, bIndex){ + return bHist.map(function(bgHist, gIndex){ + return bgHist.map(function(bgrHist, rIndex){ + return { + data :[ + [bgrHist/sum1], + [bIndex*step + halfStep], + [gIndex*step + halfStep], + [rIndex*step + halfStep] + ] + } + }) + }) + })).map(function(a){ + // trick to avoid flattening and get a 64 x 4 x 1 image as needed + return a.data; + }); + + secondImageHist64.map(function(bHist, bIndex){ + return bHist.map(function(bgHist, gIndex){ + return bgHist.map(function(bgrHist, rIndex){ + sum2 += bgrHist; + }) + }) + }) + + var sig2 = flatten(secondImageHist64.map(function(bHist, bIndex){ + return bHist.map(function(bgHist, gIndex){ + return bgHist.map(function(bgrHist, rIndex){ + return { + data : [ + [bgrHist/sum2], + [bIndex*step + halfStep], + [gIndex*step + halfStep], + [rIndex*step + halfStep] + ] + }; + }) + }) + })).map(function(a){ + // trick to avoid flattening and get a 64 x 4 x 1 image as needed + return a.data; + }); + + ///////////// + // 3. Compute the cost matrix (64 x 64 x 1), calculating the cost in LUV space + ///////////// + + //middles is a 1 x 64 x 3 array of the middles positions in RGB used to change to LUV + var middles = [flatten(firstImageHist64.map(function(bHist, bIndex){ + return bHist.map(function(bgHist, gIndex){ + return bgHist.map(function(bgrHist, rIndex){ + return { + data : [ + bIndex*step + halfStep, + gIndex*step + halfStep, + rIndex*step + halfStep + ] + } + }) + }) + })).map(function(a){ + // trick to avoid flattening and get a 1 x 64 x 3 image as needed + return a.data; + })]; + + var mat = cv.Matrix.fromArray(middles, cv.Constants.CV_8UC3); + mat.cvtColor("CV_BGR2Luv"); + + //luvValues is a 1 x 64 x 3 array of the middles positions in LUV + var luvMiddles = mat.toArray(); + + var distance = function(luv1, luv2){ + return Math.sqrt((luv1[0]-luv2[0])*(luv1[0]-luv2[0]) + (luv1[1]-luv2[1])*(luv1[1]-luv2[1]) + (luv1[2]-luv2[2])*(luv1[2]-luv2[2])); + }; + + var costs = luvMiddles[0].map(function(luvMiddle1){ + return luvMiddles[0].map(function(luvMiddle2){ + return [distance(luvMiddle1, luvMiddle2)]; + }) + }); + + ////// + // 4. Run EMD algorithm + ///// + + var matCosts = cv.Matrix.fromArray(costs, cv.Constants.CV_32FC1); + var matSig1 = cv.Matrix.fromArray(sig1, cv.Constants.CV_32FC1); + var matSig2 = cv.Matrix.fromArray(sig2, cv.Constants.CV_32FC1); + + var dist = cv.Constants.CV_DIST_L2; + var emd = cv.histogram.emd(matSig1, matSig2, dist);//, matCosts); + console.log("EMD is ", emd) + }); +}); diff --git a/examples/mat-array-conversion.js b/examples/mat-array-conversion.js new file mode 100644 index 0000000..ee70afa --- /dev/null +++ b/examples/mat-array-conversion.js @@ -0,0 +1,20 @@ +var cv = require('../lib/opencv'); + +cv.readImage("./files/mona.png", function(err, orig) { + if (err) throw err; + + var a = orig.toArray(); + var type = orig.type(); + var doubleConversion = cv.Matrix.fromArray(a, type).toArray(); + + for(var i = 0 ; i < a.length; i++){ + for(var j = 0 ; j < a[i].length; j++){ + for(var k = 0 ; k < a[i][j].length; k++){ + if(a[i][j][k] !== doubleConversion[i][j][k]){ + throw(new Error("double conversion is not equal to original")); + } + } + } + } + +}); diff --git a/lib/opencv.js b/lib/opencv.js index dc043a0..6184fbe 100755 --- a/lib/opencv.js +++ b/lib/opencv.js @@ -1,7 +1,8 @@ var Stream = require('stream').Stream , Buffers = require('buffers') , util = require('util') - , path = require('path'); + , path = require('path') + , os = require('os'); var cv = module.exports = require('./bindings'); @@ -33,6 +34,120 @@ Matrix.prototype.inspect = function(){ return "[ Matrix " + size + " ]"; } +// we use the Opencv constants naming convention to extract the number of bytes (8, 16, 32, 64), and the number of channels from constants names +var getNumberOfBytesAndChannelsPerType = function(type){ + var regExp = /CV_([0-9]+)([A-Z]+)([0-9]+)/; + for(var k in cv.Constants) if(cv.Constants.hasOwnProperty(k) && k.match(regExp) && cv.Constants[k] === type){ + var bytes, channels, dataType; + k.replace(regExp, function(all, b, l, c){ + bytes = b; + channels = c; + dataType = l[0] + }); + + return { + bytes : parseInt(bytes), + channels : !isNaN(parseInt(channels)) && parseInt(channels), + dataType : dataType, + label : k + }; + } +}; + +var getBufferMethodName = function(bytes, dataType, endianness, read){ + var fnName; + + if(read){ + fnName = "read"; + } else { + fnName = "write"; + } + + if (bytes === 32 && (dataType === "F" || dataType === "S")){ + if(dataType === "F"){ + fnName += "Float"+endianness; + } else {//dataType === "S" + fnName += "Int32"+endianness; + } + } else if(bytes === 8){ + fnName += (dataType === "U" ? "U" : "")+"Int8"; + } else if(bytes === 16){ + fnName += (dataType === "U" ? "U" : "")+"Int16"+endianness; + } else { + throw("This matrix type (CV_"+bytes+dataType+") is not compatible with fromArray/toArray") + } + return fnName; +}; + +Matrix.fromArray = function(arr, type){ + var n_bytes; + + var bytesAndChannels = getNumberOfBytesAndChannelsPerType(type); + var bytes = bytesAndChannels.bytes; + var channels = bytesAndChannels.channels; + var dataType = bytesAndChannels.dataType; + var label = bytesAndChannels.label; + + if(!Array.isArray(arr) ||!Array.isArray(arr[0]) || !Array.isArray(arr[0][0]) || (channels && arr[0][0].length !== channels)) { + throw(new Error("Input array must be a 3-level array/matrix with size rows x cols x channels corresponding to dataType ("+label+")")); + } + + if(!channels){ + channels = 1; + } + + var rows = arr.length; + var cols = arr[0].length; + + var mat = new cv.Matrix(rows, cols, type); + + var n_bytes = bytes/8; + var buf = new Buffer(rows * cols * channels * n_bytes); + + buf.fill(0); + + var fnName = getBufferMethodName(bytes, dataType, os.endianness(), false) + + for(var i=0;i target) { CONST_INT(CV_DIST_C); CONST_INT(CV_DIST_L1); CONST_INT(CV_DIST_L2); + CONST_INT(CV_DIST_USER); CONST_INT(CV_DIST_MASK_3); CONST_INT(CV_DIST_MASK_5); diff --git a/src/Histogram.cc b/src/Histogram.cc new file mode 100644 index 0000000..6b2caa5 --- /dev/null +++ b/src/Histogram.cc @@ -0,0 +1,135 @@ +#include "Histogram.h" +#include "Matrix.h" + +void Histogram::Init(Local target) { + Nan::Persistent inner; + Local obj = Nan::New(); + inner.Reset(obj); + + Nan::SetMethod(obj, "calcHist", CalcHist); + Nan::SetMethod(obj, "emd", Emd); + + target->Set(Nan::New("histogram").ToLocalChecked(), obj); +} + +NAN_METHOD(Histogram::CalcHist) { + Nan::EscapableHandleScope scope; + + try { + // Arg 0 is the image + Matrix* m0 = Nan::ObjectWrap::Unwrap(info[0]->ToObject()); + cv::Mat inputImage = m0->mat; + + // Arg 1 is the channel + Local nodeChannels = Local::Cast(info[1]->ToObject()); + const unsigned int dims = nodeChannels->Length(); + int channels[dims]; + for (unsigned int i = 0; i < dims; i++) { + channels[i] = nodeChannels->Get(i)->IntegerValue(); + } + + // Arg 2 is histogram sizes in each dimension + Local nodeHistSizes = Local::Cast(info[2]->ToObject()); + int histSize[dims]; + for (unsigned int i = 0; i < dims; i++) { + histSize[i] = nodeHistSizes->Get(i)->IntegerValue(); + } + + // Arg 3 is array of the histogram bin boundaries in each dimension + Local nodeRanges = Local::Cast(info[3]->ToObject()); + /// Set the ranges ( for B,G,R) ) + float histRanges[dims][2]; + const float* ranges[dims]; + + for (unsigned int i = 0; i < dims; i++) { + Local nodeRange = Local::Cast(nodeRanges->Get(i)->ToObject()); + float lower = nodeRange->Get(0)->NumberValue(); + float higher = nodeRange->Get(1)->NumberValue(); + histRanges[i][0] = lower; + histRanges[i][1] = higher; + ranges[i] = histRanges[i]; + } + + // Arg 4 is uniform flag + bool uniform = info[4]->BooleanValue(); + + // Make a mat to hold the result image + cv::Mat outputHist; + + // Perform calcHist + cv::calcHist(&inputImage, 1, channels, cv::Mat(), outputHist, dims, histSize, ranges, uniform); + + v8::Local arr = Nan::New(histSize[0]); + + if(dims < 1 || dims > 3){ + return Nan::ThrowTypeError("OPENCV nodejs binding error : only dimensions from 1 to 3 are allowed"); + } + + for (unsigned int i=0; i < (unsigned int) histSize[0]; i++) { + if(dims <= 1){ + arr->Set(i, Nan::New(outputHist.at(i))); + } else { + v8::Local arr2 = Nan::New(dims); + for (unsigned int j=0; j < (unsigned int) histSize[1]; j++) { + if(dims <= 2){ + arr2->Set(j, Nan::New(outputHist.at(i,j))); + } else { + v8::Local arr3 = Nan::New(dims); + for (unsigned int k=0; k < (unsigned int) histSize[1]; k++) { + arr3->Set(k, Nan::New(outputHist.at(i,j,k))); + } + arr2->Set(j, arr3); + } + } + arr->Set(i, arr2); + } + } + + info.GetReturnValue().Set(arr); + } catch (cv::Exception &e) { + const char *err_msg = e.what(); + Nan::ThrowError(err_msg); + return; + } +} + +// cv::distanceTransform +NAN_METHOD(Histogram::Emd) { + Nan::EscapableHandleScope scope; + + try { + // Arg 0 is the first signature + //std::vector> sig1 = nodeArrayToVec(info[0]->ToObject()); + Matrix* m0 = Nan::ObjectWrap::Unwrap(info[0]->ToObject()); + cv::Mat sig1 = m0->mat; + + // Arg 1 is the second signature + //std::vector> sig2 = nodeArrayToVec(info[1]->ToObject()); + Matrix* m1 = Nan::ObjectWrap::Unwrap(info[1]->ToObject()); + cv::Mat sig2 = m1->mat; + + // Arg 2 is the distance type + int distType = info[2]->IntegerValue(); + + float emd; + + // Arg 3 is the cost matrix + if (info.Length() > 3) { + Matrix* m3 = Nan::ObjectWrap::Unwrap(info[3]->ToObject()); + cv::Mat costs = m3->mat; + + emd = cv::EMD(sig1, sig2, distType, costs); + info.GetReturnValue().Set(emd); + } else { + emd = cv::EMD(sig1, sig2, distType); + } + + //printf("similarity %5.5f %%\n, DistanceType is %i\n", (1-emd)*100, distType); + info.GetReturnValue().Set(emd); + + } catch (cv::Exception &e) { + const char *err_msg = e.what(); + Nan::ThrowError(err_msg); + return; + } +} diff --git a/src/Histogram.h b/src/Histogram.h new file mode 100644 index 0000000..d2cf20c --- /dev/null +++ b/src/Histogram.h @@ -0,0 +1,16 @@ +#ifndef __NODE_HISTOGRAM_H +#define __NODE_HISTOGRAM_H + +#include "OpenCV.h" + +/** + * Implementation of histogram.hpp functions + */ +class Histogram: public Nan::ObjectWrap { +public: + static void Init(Local target); + static NAN_METHOD(CalcHist); + static NAN_METHOD(Emd); +}; + +#endif diff --git a/src/Matrix.cc b/src/Matrix.cc index 80aec83..3bf2c47 100755 --- a/src/Matrix.cc +++ b/src/Matrix.cc @@ -69,7 +69,7 @@ void Matrix::Init(Local target) { Nan::SetPrototypeMethod(ctor, "dct", Dct); Nan::SetPrototypeMethod(ctor, "idct", Idct); Nan::SetPrototypeMethod(ctor, "addWeighted", AddWeighted); - Nan::SetPrototypeMethod(ctor, "add", Add); + Nan::SetPrototypeMethod(ctor, "add", Add); Nan::SetPrototypeMethod(ctor, "bitwiseXor", BitwiseXor); Nan::SetPrototypeMethod(ctor, "bitwiseNot", BitwiseNot); Nan::SetPrototypeMethod(ctor, "bitwiseAnd", BitwiseAnd); @@ -117,6 +117,7 @@ void Matrix::Init(Local target) { Nan::SetPrototypeMethod(ctor, "release", Release); Nan::SetPrototypeMethod(ctor, "subtract", Subtract); Nan::SetPrototypeMethod(ctor, "compare", Compare); + Nan::SetPrototypeMethod(ctor, "mul", Mul); target->Set(Nan::New("Matrix").ToLocalChecked(), ctor->GetFunction()); }; @@ -608,7 +609,7 @@ NAN_METHOD(Matrix::PixelCol) { int height = self->mat.size().height; int x = info[0]->IntegerValue(); v8::Local < v8::Array > arr; - + if (self->mat.channels() == 3) { arr = Nan::New(height * 3); for (int y = 0; y < height; y++) { @@ -2853,7 +2854,23 @@ NAN_METHOD(Matrix::Compare) { cv::Mat res = cv::Mat(width, height, CV_8UC1); cv::compare(self->mat, other->mat, res, cmpop); - + Local out = Nan::NewInstance(Nan::GetFunction(Nan::New(Matrix::constructor)).ToLocalChecked()).ToLocalChecked(); + Matrix *m_out = Nan::ObjectWrap::Unwrap(out); + m_out->mat = res; + + info.GetReturnValue().Set(out); + return; +} +NAN_METHOD(Matrix::Mul) { + SETUP_FUNCTION(Matrix) + + if (info.Length() < 1) { + Nan::ThrowTypeError("Invalid number of arguments"); + } + + Matrix *other = Nan::ObjectWrap::Unwrap(info[0]->ToObject()); + + cv::Mat res = self->mat.mul(other->mat); Local out = Nan::NewInstance(Nan::GetFunction(Nan::New(Matrix::constructor)).ToLocalChecked()).ToLocalChecked(); Matrix *m_out = Nan::ObjectWrap::Unwrap(out); m_out->mat = res; diff --git a/src/Matrix.h b/src/Matrix.h index 9cd73ab..81ee0ff 100755 --- a/src/Matrix.h +++ b/src/Matrix.h @@ -135,6 +135,7 @@ public: JSFUNC(Subtract) JSFUNC(Compare) + JSFUNC(Mul) /* static Handle Val(const Arguments& info); static Handle RowRange(const Arguments& info); diff --git a/src/init.cc b/src/init.cc index 7d91bbe..10f4d76 100755 --- a/src/init.cc +++ b/src/init.cc @@ -15,6 +15,7 @@ #include "Stereo.h" #include "BackgroundSubtractor.h" #include "LDAWrap.h" +#include "Histogram.h" extern "C" void init(Local target) { Nan::HandleScope scope; @@ -30,6 +31,7 @@ extern "C" void init(Local target) { Constants::Init(target); Calib3D::Init(target); ImgProc::Init(target); + Histogram::Init(target); #if CV_MAJOR_VERSION < 3 StereoBM::Init(target); StereoSGBM::Init(target); diff --git a/test/unit.js b/test/unit.js index 9637dfb..eee0e22 100755 --- a/test/unit.js +++ b/test/unit.js @@ -398,15 +398,16 @@ test('Mean', function(assert) { }); test('Compare', function(assert) { - var b = new cv.Matrix.Zeros(2, 2, cv.Constants.CV_8UC1); - var a = new cv.Matrix.Zeros(2, 2, cv.Constants.CV_8UC1); + var a = cv.Matrix.fromArray([[[0],[-20]],[[2],[-18]]], cv.Constants.CV_8SC1); + var b = cv.Matrix.fromArray([[[3],[-20]],[[0],[-16]]], cv.Constants.CV_8SC1); - a.set(0, 0, 3); + var compare1 = a.compare(b, cv.Constants.CMP_EQ); + var compare2 = a.compare(b, cv.Constants.CMP_GT); + var compare3 = a.compare(b, cv.Constants.CMP_LE); - var compare = a.compare(b, cv.Constants.CMP_EQ); - var buf = compare.getData(); - compare.save("./test.png"); - console.log(compare.norm()); + assert.deepEqual(compare1.toArray(), [[[0],[255]],[[0],[0]]]); + assert.deepEqual(compare2.toArray(), [[[0],[0]],[[255],[0]]]); + assert.deepEqual(compare3.toArray(), [[[255],[255]],[[0],[255]]]); assert.end(); }); @@ -463,5 +464,24 @@ test('setColor works will alpha channels', function(assert) { }); }); +test('toArray/fromArray working in both ways', function(assert) { + var cv = require('../lib/opencv'); + + cv.readImage("./examples/files/mona.png", function(err, orig) { + if (err) throw err; + + var a = orig.toArray(); + var type = orig.type(); + var doubleConversion = cv.Matrix.fromArray(a, type).toArray(); + + var randomI = Math.floor(Math.random()*a.length) + var randomJ = Math.floor(Math.random()*a[randomI].length) + var randomK = Math.floor(Math.random()*a[randomI][randomJ].length) + + assert.equal(a[randomI][randomJ][randomK], doubleConversion[randomI][randomJ][randomK]); + assert.end(); + }); +}); + // Test the examples folder. require('./examples')()