diff --git a/.gitignore b/.gitignore index 69a057c..550bb51 100755 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ build .DS_Store node_modules npm-debug.log -out.jpg +out*.jpg +examples/*.avi diff --git a/.travis.yml b/.travis.yml index 1068af6..de80599 100755 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ node_js: - 0.6 before_install: + - sudo apt-get update - sudo apt-get install libcv-dev - sudo apt-get install libopencv-dev - sudo apt-get install libhighgui-dev diff --git a/binding.gyp b/binding.gyp index 0807aab..e664224 100755 --- a/binding.gyp +++ b/binding.gyp @@ -9,6 +9,7 @@ , "src/Contours.cc" , "src/Point.cc" , "src/VideoCaptureWrap.cc" + , "src/CamShift.cc" ] , 'libraries': [ '>", x, ":" , rec) + if (x % 10 == 0){ + m2.rectangle([rec[0], rec[1]], [rec[2], rec[3]]) + m2.save('./out-motiontrack-' + x + '.jpg') + } + if (x<100) + iter(); + }) + } + iter(); +}) + diff --git a/examples/motion.mov b/examples/motion.mov new file mode 100644 index 0000000..68d4650 Binary files /dev/null and b/examples/motion.mov differ diff --git a/smoke/smoke.sh b/smoke/smoke.sh index 993de2b..5ac35f8 100755 --- a/smoke/smoke.sh +++ b/smoke/smoke.sh @@ -1,6 +1,8 @@ #!/bin/bash -node-gyp rebuild && echo '-- Compiled OK -- +node-gyp build && echo '-- Compiled OK -- ' && node smoke/smoketest.js && echo '-- Smoke Done, running tests -- -' && npm test +' && npm test && echo '-- Tests Run, runnning examples -- +(building example data) +' && ./examples/make-example-files.sh && node examples/motion-track.js diff --git a/smoke/smoketest.js b/smoke/smoketest.js index de8b7a3..c114528 100755 --- a/smoke/smoketest.js +++ b/smoke/smoketest.js @@ -1,4 +1,7 @@ var cv = require('../lib/opencv') + + + /* new cv.VideoCapture(0).read(function(mat){ @@ -15,7 +18,7 @@ new cv.VideoCapture(0).read(function(mat){ }) }) -*/ + cv.readImage("./examples/stuff.png", function(err, im){ @@ -32,3 +35,4 @@ cv.readImage("./examples/stuff.png", function(err, im){ console.log(features) im.save('./out.jpg'); }); +*/ diff --git a/src/CamShift.cc b/src/CamShift.cc new file mode 100644 index 0000000..15fd628 --- /dev/null +++ b/src/CamShift.cc @@ -0,0 +1,191 @@ +#include "CamShift.h" +#include "OpenCV.h" +#include "Matrix.h" + + +#define CHANNEL_HUE 0 +#define CHANNEL_SATURATION 1 +#define CHANNEL_VALUE 2 + + +Persistent TrackedObject::constructor; + +void +TrackedObject::Init(Handle target) { + HandleScope scope; + + // Constructor + constructor = Persistent::New(FunctionTemplate::New(TrackedObject::New)); + constructor->InstanceTemplate()->SetInternalFieldCount(1); + constructor->SetClassName(String::NewSymbol("TrackedObject")); + + // Prototype + //Local proto = constructor->PrototypeTemplate(); + + NODE_SET_PROTOTYPE_METHOD(constructor, "track", Track); + target->Set(String::NewSymbol("TrackedObject"), constructor->GetFunction()); +}; + + +Handle +TrackedObject::New(const Arguments &args) { + HandleScope scope; + + if (args.This()->InternalFieldCount() == 0){ + JSTHROW_TYPE("Cannot Instantiate without new") + } + + Matrix* m = ObjectWrap::Unwrap(args[0]->ToObject()); + cv::Rect r; + int channel = CHANNEL_HUE; + + if (args[1]->IsArray()){ + Local v8rec = args[1]->ToObject(); + r = cv::Rect( + v8rec->Get(0)->IntegerValue(), + v8rec->Get(1)->IntegerValue(), + v8rec->Get(2)->IntegerValue() - v8rec->Get(0)->IntegerValue(), + v8rec->Get(3)->IntegerValue() - v8rec->Get(1)->IntegerValue()); + } else { + JSTHROW_TYPE("Must pass rectangle to track") + } + + if (args[2]->IsObject()){ + Local opts = args[2]->ToObject(); + + if (opts->Get(String::New("channel"))->IsString()){ + v8::String::Utf8Value c(opts->Get(String::New("channel"))->ToString()); + std::string cc = std::string(*c); + + if (cc == "hue" || cc == "h"){ + channel = CHANNEL_HUE; + } + + if (cc == "saturation" || cc == "s"){ + channel = CHANNEL_SATURATION; + } + + if (cc == "value" || cc == "v"){ + channel = CHANNEL_VALUE; + } + } + } + + TrackedObject *to = new TrackedObject(m->mat, r, channel); + + + to->Wrap(args.This()); + return args.This(); +} + + +void update_chann_image(TrackedObject* t, cv::Mat image){ + // Store HSV Hue Image + cv::cvtColor(image, t->hsv, CV_BGR2HSV); // convert to HSV space + //mask out-of-range values + int vmin = 65, vmax = 256, smin = 55; + cv::inRange(t->hsv, //source + cv::Scalar(0, smin, MIN(vmin, vmax), 0), //lower bound + cv::Scalar(180, 256, MAX(vmin, vmax) ,0), //upper bound + t->mask); //destination + + //extract the hue channel, split: src, dest channels + std::vector hsvplanes; + cv::split(t->hsv, hsvplanes); + t->hue = hsvplanes[t->channel]; + + +} + +TrackedObject::TrackedObject(cv::Mat image, cv::Rect rect, int chan){ + channel = chan; + update_chann_image(this, image); + prev_rect = rect; + + // Calculate Histogram + int hbins = 30, sbins = 32; + int histSizes[] = {hbins, sbins}; + //float hranges[] = { 0, 180 }; + // saturation varies from 0 (black-gray-white) to + // 255 (pure spectrum color) + float sranges[] = { 0, 256 }; + const float* ranges[] = { sranges }; + + cv::Mat hue_roi = hue(rect); + cv::Mat mask_roi = mask(rect); + + cv::calcHist(&hue_roi, 1, 0, mask_roi, hist, 1, histSizes, ranges, true, false); + +} + + + +Handle +TrackedObject::Track(const v8::Arguments& args){ + SETUP_FUNCTION(TrackedObject) + + if (args.Length() != 1){ + v8::ThrowException(v8::Exception::TypeError(v8::String::New("track takes an image param"))); + return Undefined(); + } + + + Matrix *im = ObjectWrap::Unwrap(args[0]->ToObject()); + cv::RotatedRect r; + + if (self->prev_rect.x <0 || + self->prev_rect.y <0 || + self->prev_rect.width <= 1 || + self->prev_rect.height <= 1){ + return v8::ThrowException(v8::Exception::TypeError(v8::String::New("OPENCV ERROR: prev rectangle is illogical"))); + } + + update_chann_image(self, im->mat); + + cv::Rect backup_prev_rect = cv::Rect( + self->prev_rect.x, + self->prev_rect.y, + self->prev_rect.width, + self->prev_rect.height); + + float sranges[] = { 0, 256 }; + const float* ranges[] = { sranges }; + int channel = 0; + cv::calcBackProject(&self->hue, 1, &channel, self->hist, self->prob, ranges); + + r = cv::CamShift(self->prob, self->prev_rect, + cv::TermCriteria(CV_TERMCRIT_EPS | CV_TERMCRIT_ITER, 10, 1)); + + cv::Rect bounds = r.boundingRect(); + if (bounds.x >=0 && bounds.y >=0 && bounds.width > 1 && bounds.height > 1){ + self->prev_rect = bounds; + } else { + //printf("CRAP> %i %i %i %i ;", self->prev_rect.x, self->prev_rect.y, self->prev_rect.width, self->prev_rect.height); + + // We have encountered a bug in opencv. Somehow the prev_rect has got mangled, so we + // must reset it to a good value. + self->prev_rect = backup_prev_rect; + } + + + v8::Local arr = v8::Array::New(4); + + + arr->Set(0, Number::New(bounds.x)); + arr->Set(1, Number::New(bounds.y)); + arr->Set(2, Number::New(bounds.x + bounds.width)); + arr->Set(3, Number::New(bounds.y + bounds.height)); + + /* + cv::Point2f pts[4]; + r.points(pts); + + for (int i=0; i<8; i+=2){ + arr->Set(i, Number::New(pts[i].x)); + arr->Set(i+1, Number::New(pts[i].y)); + } +*/ + + return scope.Close(arr); + +} diff --git a/src/CamShift.h b/src/CamShift.h new file mode 100644 index 0000000..649a65f --- /dev/null +++ b/src/CamShift.h @@ -0,0 +1,23 @@ +#include "OpenCV.h" + + +class TrackedObject: public node::ObjectWrap { + public: + int channel; + cv::Mat hsv; + cv::Mat hue; + cv::Mat mask; + cv::Mat prob; + + cv::Mat hist; + cv::Rect prev_rect; + + static Persistent constructor; + static void Init(Handle target); + static Handle New(const Arguments &args); + + TrackedObject(cv::Mat image, cv::Rect rect, int channel); + + JSFUNC(Track); + +}; diff --git a/src/OpenCV.h b/src/OpenCV.h index bcd1618..183fbb0 100755 --- a/src/OpenCV.h +++ b/src/OpenCV.h @@ -27,6 +27,8 @@ using namespace node; #define JSFUNC(NAME) \ static Handle NAME(const Arguments& args); +#define JSTHROW_TYPE(ERR) \ + return v8::ThrowException(v8::Exception::TypeError(v8::String::New(ERR))); class OpenCV: public node::ObjectWrap{ diff --git a/src/VideoCaptureWrap.cc b/src/VideoCaptureWrap.cc index 74bbd9c..0087037 100755 --- a/src/VideoCaptureWrap.cc +++ b/src/VideoCaptureWrap.cc @@ -46,7 +46,10 @@ VideoCaptureWrap::New(const Arguments &args) { if (args[0]->IsNumber()){ v = new VideoCaptureWrap(args[0]->NumberValue()); - } else {} + } else { + //TODO - assumes that we have string, verify + v = new VideoCaptureWrap(std::string(*v8::String::AsciiValue(args[0]->ToString()))); + } v->Wrap(args.This()); @@ -64,6 +67,16 @@ VideoCaptureWrap::VideoCaptureWrap(int device){ } } +VideoCaptureWrap::VideoCaptureWrap(const std::string& filename){ + HandleScope scope; + cap.open(filename); + // TODO! At the moment this only takes a full path - do relative too. + if(!cap.isOpened()){ + v8::ThrowException(v8::Exception::Error(String::New("Video file could not be opened (opencv reqs. non relative paths)"))); + } + +} + Handle VideoCaptureWrap::Read(const Arguments &args) { diff --git a/src/init.cc b/src/init.cc index 1e3bf9f..9e4494a 100755 --- a/src/init.cc +++ b/src/init.cc @@ -4,6 +4,7 @@ #include "CascadeClassifierWrap.h" #include "VideoCaptureWrap.h" #include "Contours.h" +#include "CamShift.h" extern "C" void @@ -14,7 +15,8 @@ init(Handle target) { Matrix::Init(target); CascadeClassifierWrap::Init(target); VideoCaptureWrap::Init(target); - Contour::Init(target); + Contour::Init(target); + TrackedObject::Init(target); }; NODE_MODULE(opencv, init) diff --git a/test/unit.js b/test/unit.js index 60cfbcf..e02f6bf 100755 --- a/test/unit.js +++ b/test/unit.js @@ -9,6 +9,16 @@ assertDeepSimilar = function(res, exp){ assert.deepEqual(res, exp) } +assertWithinRange = function(res, exp, range){ + assert.ok((res - exp) < range || (res - exp) > -range, "Not within range:" + res + " (" + exp + "+- " + range + ")") +} + +assertWithinRanges = function(res, exp, range){ + for (var i =0; i