|
| 1 | +First of all, any kind of contribution is highly appreciated, you don't have to be a pro in C++, neither am I. If you are totally new to native Node.js development and would like to get started, you can have a look at my article series as a quick introduction: |
| 2 | +<a href="https://medium.com/netscape/tutorial-building-native-c-modules-for-node-js-using-nan-part-1-755b07389c7c"><b>Tutorial to Native Node.js Modules with C++</b></a> |
| 3 | + |
| 4 | + |
| 5 | +Oftentimes adding bindings is done similarly to what already exists in the codebase. Thus, you can take the existing stuff as an example to help you to get started. In the following, you can find some basic guidelines for adding new OpenCV function bindings to the package. |
| 6 | + |
| 7 | +## API Design |
| 8 | +The API is designed such that |
| 9 | + |
| 10 | +A: Parameters passed to a function call are type checked and appropriate messages are displayed to the user in case an error occured. Nobody wants passing garbage to a function by coincidence to fail silently, which may produce unexpected results. |
| 11 | + |
| 12 | +B: A function, which takes more than a single parameter with default values, can conveniently be invoked by passing a JSON object with named parameters in substitution of the optional parameters. For example consider the following function signature from the official OpenCV 3 docs: |
| 13 | + |
| 14 | +``` c++ |
| 15 | +void GaussianBlur(InputArray src, OutputArray dst, Size ksize, double sigmaX, double sigmaY=0, int borderType=BORDER_DEFAULT) |
| 16 | +``` |
| 17 | +
|
| 18 | +The function should be invokable in the following ways: |
| 19 | +``` javascript |
| 20 | +const mat = new cv.Mat(...) |
| 21 | +
|
| 22 | +// required arguments |
| 23 | +const size = new cv.Size(...) |
| 24 | +const sigmaX = 1.2 |
| 25 | +
|
| 26 | +// optional arguments |
| 27 | +const sigmaY = 1.2 |
| 28 | +const borderType = cv.BORDER_CONSTANT |
| 29 | +
|
| 30 | +let dst |
| 31 | +
|
| 32 | +// with required arguments |
| 33 | +dst = mat.gaussianBlur(size, sigmaX) |
| 34 | +
|
| 35 | +// with optional arguments |
| 36 | +dst = mat.gaussianBlur(size, sigmaX, sigmaY) |
| 37 | +dst = mat.gaussianBlur(size, sigmaX, sigmaY, borderType) |
| 38 | +
|
| 39 | +// with named optional arguments as JSON object |
| 40 | +dst = mat.gaussianBlur(size, sigmaX, { sigmaY: 1.2 }) |
| 41 | +dst = mat.gaussianBlur(size, sigmaX, { borderType: cv.BORDER_CONSTANT }) |
| 42 | +dst = mat.gaussianBlur(size, sigmaX, { sigmaY: 1.2, borderType: cv.BORDER_CONSTANT }) |
| 43 | +``` |
| 44 | + |
| 45 | +Passing optional arguments as named parameters shall provide the convenience of being able to pass single optional parameters without having to pass every other optional paramater. |
| 46 | + |
| 47 | +## Adding Function Bindings |
| 48 | + |
| 49 | +With the Worker struct you can easily implement the sync and async bindings for a function. If you go conform with the struct pattern, it will automatically handle any typechecking of arguments and unwrapping them via Converters for you so you don't have to worry about checking them manually. |
| 50 | + |
| 51 | +In the .h file add the declaration of the bindings its' worker to the class definition: |
| 52 | + |
| 53 | +``` c++ |
| 54 | +class Mat : public Nan::ObjectWrap { |
| 55 | + |
| 56 | +... |
| 57 | + |
| 58 | + struct GaussianBlurWorker; |
| 59 | + NAN_METHOD(GaussianBlur); |
| 60 | + NAN_METHOD(GaussianBlurAsync); |
| 61 | + |
| 62 | +} |
| 63 | + |
| 64 | +``` |
| 65 | +
|
| 66 | +In the .cc file add the implementation of the worker: |
| 67 | +
|
| 68 | +``` c++ |
| 69 | +struct Mat::GaussianBlurWorker : public SimpleWorker { |
| 70 | +public: |
| 71 | + // instance of the class exposing the method |
| 72 | + cv::Mat mat; |
| 73 | + GaussianBlurWorker(cv::Mat mat) { |
| 74 | + this->mat = mat; |
| 75 | + } |
| 76 | +
|
| 77 | + // required function arguments |
| 78 | + cv::Size2d kSize; |
| 79 | + double sigmaX; |
| 80 | + // optional function arguments |
| 81 | + double sigmaY = 0; |
| 82 | + int borderType = cv::BORDER_CONSTANT; |
| 83 | +
|
| 84 | + // function return value |
| 85 | + cv::Mat blurMat; |
| 86 | +
|
| 87 | + // here the main work is performed, the async worker will execute |
| 88 | + // this in a different thread |
| 89 | + const char* execute() { |
| 90 | + cv::GaussianBlur(mat, blurMat, kSize, sigmaX, sigmaY, borderType); |
| 91 | + // if you need to handle errors, you can return an error message here, which |
| 92 | + // will trigger the error callback if message is not empty |
| 93 | + return ""; |
| 94 | + } |
| 95 | +
|
| 96 | + // return the native objects, handle all object wrapping stuff here |
| 97 | + v8::Local<v8::Value> getReturnValue() { |
| 98 | + return Mat::Converter::wrap(blurMat); |
| 99 | + } |
| 100 | +
|
| 101 | + // implement this method if function takes any required arguments |
| 102 | + bool unwrapRequiredArgs(Nan::NAN_METHOD_ARGS_TYPE info) { |
| 103 | + return ( |
| 104 | + Size::Converter::arg(0, &kSize, info) || |
| 105 | + DoubleConverter::arg(1, &sigmaX, info) |
| 106 | + ); |
| 107 | + } |
| 108 | +
|
| 109 | + // implement this method if function takes any optional arguments |
| 110 | + bool unwrapOptionalArgs(Nan::NAN_METHOD_ARGS_TYPE info) { |
| 111 | + return ( |
| 112 | + DoubleConverter::optArg(2, &sigmaY, info) || |
| 113 | + IntConverter::optArg(3, &borderType, info) |
| 114 | + ); |
| 115 | + } |
| 116 | +
|
| 117 | + // implement the following methods if function takes more than a single optional parameter |
| 118 | +
|
| 119 | + // check if a JSON object as the first argument after the required arguments |
| 120 | + bool hasOptArgsObject(Nan::NAN_METHOD_ARGS_TYPE info) { |
| 121 | + return FF_ARG_IS_OBJECT(2); |
| 122 | + } |
| 123 | +
|
| 124 | + // get the values from named properties of the JSON object |
| 125 | + bool unwrapOptionalArgsFromOpts(Nan::NAN_METHOD_ARGS_TYPE info) { |
| 126 | + FF_OBJ opts = info[2]->ToObject(); |
| 127 | + return ( |
| 128 | + DoubleConverter::optProp(&sigmaY, "sigmaY", opts) || |
| 129 | + IntConverter::optProp(&borderType, "borderType", opts) |
| 130 | + ); |
| 131 | + } |
| 132 | +}; |
| 133 | +``` |
| 134 | + |
| 135 | +After you have set up the worker, implementing the bindings is as easy as follows: |
| 136 | + |
| 137 | +``` c++ |
| 138 | +NAN_METHOD(Mat::GaussianBlur) { |
| 139 | + GaussianBlurWorker worker(Mat::Converter::unwrap(info.This())); |
| 140 | + FF_WORKER_SYNC("Mat::GaussianBlur", worker); |
| 141 | + info.GetReturnValue().Set(worker.getReturnValue()); |
| 142 | +} |
| 143 | + |
| 144 | +NAN_METHOD(Mat::GaussianBlurAsync) { |
| 145 | + GaussianBlurWorker worker(Mat::Converter::unwrap(info.This())); |
| 146 | + FF_WORKER_ASYNC("Mat::GaussianBlurAsync", GaussianBlurWorker, worker); |
| 147 | +} |
| 148 | +``` |
| 149 | +
|
| 150 | +## Using converters |
| 151 | +
|
| 152 | +For converting native types to v8 types and unwrapping/ wrapping objects and instances you can use the Converters. A Converter will perform type checking and throw an error if converting a value or unwrapping an object failed. If a converter returns true, an error was thrown. You should use the Converters in conjunction with a worker struct. Otherwise you will have to handle rethrowing the error manually. There are Converters for conversion of primitive types, for unwrapping/ wrapping class instances as well as arrays of primitive types and arrays of instances. For representation of a JS array in c++ we are using std::vector. |
| 153 | +
|
| 154 | +Some Usage examples: |
| 155 | +``` c++ |
| 156 | +// require arg 0 to be a Mat |
| 157 | +cv::Mat img; |
| 158 | +Mat::Converter::arg(0, &img, info); |
| 159 | +
|
| 160 | +// require arg 0 to be a Mat if arg is passed to the function |
| 161 | +cv::Mat img = // some default value |
| 162 | +Mat::Converter::optArg(0, &img, info); |
| 163 | +
|
| 164 | +// get the the property "image" of an object and convert its value to Mat |
| 165 | +cv::Mat img = // some default value |
| 166 | +Mat::Converter::optProp(&img, "image", optPropsObject); |
| 167 | +
|
| 168 | +// wrapping the Mat object |
| 169 | +cv::Mat img = // some mat |
| 170 | +v8::Local<v8::Value> jsMat = Mat::Converter::wrap(img); |
| 171 | +
|
| 172 | +// primitive type converters |
| 173 | +bool aBool; |
| 174 | +BoolConverter::arg(0, &aBool, info); |
| 175 | +double aDouble; |
| 176 | +DoubleConverter::arg(0, &aDouble, info); |
| 177 | +float aFloat; |
| 178 | +FloatConverter::arg(0, &aFloat, info); |
| 179 | +int anInt; |
| 180 | +IntConverter::arg(0, &anInt, info); |
| 181 | +uint anUint; |
| 182 | +UintConverter::arg(0, &anUint, info); |
| 183 | +std::string aString; |
| 184 | +StringConverter::arg(0, &aString, info); |
| 185 | +
|
| 186 | +// converting a std::vector of Points to a JS Array |
| 187 | +std::vector<cv::Point2d> points; |
| 188 | +v8::Local<v8::Array> jsPoints = ObjectArrayConverter<Point2, cv::Point2d>::wrap(points); |
| 189 | +
|
| 190 | +// for simplicity the Point2 class stores cv Points as cv::Point2d, in case you need to wrap a |
| 191 | +// std::vector<cv::Point2i> you can use the third template parameter to specify a conversion type |
| 192 | +std::vector<cv::Point2i> points; |
| 193 | +v8::Local<v8::Array> jsPoints = ObjectArrayConverter<Point2, cv::Point2d, cv::Point2i>::wrap(points); |
| 194 | +ObjectArrayConverter<Point2, cv::Point2d, cv::Point2i>::wrap(points); |
| 195 | +``` |
| 196 | + |
| 197 | +A class can be made convertable if you you add the typedef for an InstanceConverter the class definition. Example for the Mat class wrapper: |
| 198 | +``` c++ |
| 199 | +class Mat : public Nan::ObjectWrap { |
| 200 | +public: |
| 201 | + cv::Mat mat; |
| 202 | + |
| 203 | + ... |
| 204 | + |
| 205 | + cv::Mat* getNativeObjectPtr() { return &mat; } |
| 206 | + cv::Mat getNativeObject() { return mat; } |
| 207 | + |
| 208 | + typedef InstanceConverter<Mat, cv::Mat> Converter; |
| 209 | + |
| 210 | + static const char* getClassName() { |
| 211 | + return "Mat"; |
| 212 | + } |
| 213 | +}; |
| 214 | +``` |
| 215 | + |
| 216 | +## Unit Tests |
| 217 | + |
| 218 | +We test the bindings directly from JS with a classic mocha + chai setup. The purpose of unit testing is not to ensure correct behaviour of OpenCV function calls as OpenCV functionality is tested and none of our business. However, we want to ensure that our bindings can be called without crashing, that all parameters are passed and objects unwrapped correctly and that the function call returns what we expect it to. |
| 219 | + |
| 220 | +You can use 'generateAPITests' to easily generate default tests for a function binding that is implemented sync and async. This will generate the tests which ensure that the synchronous as well as the callbacked and promisified async bindings are called correctly. However, you are welcome to write additional tests. For the 'gaussianBlur' example generating unit tests can be done as follows: |
| 221 | + |
| 222 | +``` javascript |
| 223 | +describe('gaussianBlur', () => { |
| 224 | + const matData = [ |
| 225 | + [0, 0, 128], |
| 226 | + [0, 128, 255], |
| 227 | + [128, 255, 255] |
| 228 | + ] |
| 229 | + const mat = new cv.Mat(matData, cv.CV_8U) |
| 230 | + |
| 231 | + const expectOutput = (blurred) => { |
| 232 | + assertMetaData(blurred)(mat.rows, mat.cols, mat.type); |
| 233 | + expect(dangerousDeepEquals(blurred.getDataAsArray(), matData)).to.be.false; |
| 234 | + }; |
| 235 | + |
| 236 | + const kSize = new cv.Size(3, 3); |
| 237 | + const sigmaX = 1.2; |
| 238 | + |
| 239 | + generateAPITests({ |
| 240 | + getDut: () => rgbMat, |
| 241 | + methodName: 'gaussianBlur', |
| 242 | + methodNameSpace: 'Mat', |
| 243 | + getRequiredArgs: () => ([ |
| 244 | + kSize, |
| 245 | + sigmaX |
| 246 | + ]), |
| 247 | + getOptionalArgsMap: () => ([ |
| 248 | + ['sigmaY', 1.2], |
| 249 | + ['borderType', cv.BORDER_CONSTANT] |
| 250 | + ]), |
| 251 | + expectOutput |
| 252 | + }); |
| 253 | +}); |
| 254 | +``` |
| 255 | + |
| 256 | +## CI |
| 257 | + |
| 258 | +For continous integration we use Travis CI, which will run a rebuild of the package and run the unit tests for each of the different OpenCV minor versions with and without contrib (opencv3.0, opencv3.1, opencv3.2, opencv3.3, opencv3.3.1, opencv3.0-contrib, opencv3.1-contrib, opencv3.2-contrib, opencv3.3-contrib, opencv3.3.1-contrib). This ensures compatibility across the OpenCV 3 versions as in some minor cases the OpenCV interface may have changed or new features have been added. |
| 259 | + |
| 260 | +The build task will be executed on every push to your working branch as well as every pull request before merging to the master branch. If you have docker set up on your local machine you can run the build tasks on your local machine via the provided npm scripts. For example from the root directory run: |
| 261 | + |
| 262 | +``` bash |
| 263 | +npm run build-opencv3.0 |
| 264 | +# or |
| 265 | +npm run build-opencv3.0-contrib |
| 266 | +``` |
| 267 | + |
| 268 | +## Docs |
| 269 | + |
| 270 | +In the corresponding markdown file in the doc folder add some docs, so that people know how to use the new binding: |
| 271 | + |
| 272 | +<a name="gaussianBlur"></a> |
| 273 | + |
| 274 | +### gaussianBlur |
| 275 | +``` javascript |
| 276 | +Mat : mat.gaussianBlur(Size kSize, Number sigmaX, Number sigmaY = 0.0, Uint borderType = BORDER_CONSTANT) |
| 277 | +``` |
| 278 | + |
| 279 | +<a name="gaussianBlurAsync"></a> |
| 280 | + |
| 281 | +### gaussianBlurAsync |
| 282 | +``` javascript |
| 283 | +mat.gaussianBlurAsync(Size kSize, Number sigmaX, callback(Error err, Mat result)) |
| 284 | +mat.gaussianBlurAsync(Size kSize, Number sigmaX, ...opts, callback(Error err, Mat result)) |
| 285 | +mat.gaussianBlurAsync(Size kSize, Number sigmaX, { opts }, callback(Error err, Mat result)) |
| 286 | +``` |
0 commit comments