Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add experimental support for WebAssembly #20

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
40 changes: 30 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
This is a JavaScript binding that exposes OpenCV library to the web. This project is made possible by support of Intel corporation. Currently, this is based on OpenCV 3.1.0.

### How to Build

You can build two different versions of OpenCV.js: the **asm.js** version, or the **WebAssembly** one (experimental). If you want to build the later, you will need the "incoming" version of Emscripten.
Since the "incoming" version of Emscripten can build both, *only the installation of Emscripten-incoming will be detailed here*, for simplicity.
If you absolutely want the "stable" one, just replace "incoming" with "master" everywhere it appears in the following command lines.

1. Get the source code

```
Expand All @@ -12,33 +17,48 @@ This is a JavaScript binding that exposes OpenCV library to the web. This projec
cd opencv
git checkout 3.1.0
```

2. Install emscripten. You can obtain emscripten by using [Emscripten SDK](https://kripken.github.io/emscripten-site/docs/getting_started/downloads.html).

```
./emsdk update
./emsdk install sdk-master-64bit --shallow
./emsdk activate sdk-master-64bit
./emsdk install sdk-incoming-64bit --shallow
./emsdk activate sdk-incoming-64bit
source ./emsdk_env.sh
```

3. Patch Emscripten & Rebuild.

```
patch -p1 < PATH/TO/patch_emscripten_master.diff
```
4. Rebuild emscripten
```
./emsdk install sdk-master-64bit --shallow
patch -p1 < PATH/TO/patch_emscripten.diff -d emscripten/incoming
./emsdk install sdk-incoming-64bit --shallow
```

4. Optionally, if you want to reduce the size of the built library, use *filter-bindings.py* to keep only the code that your JS files are actually using (if you want to cancel that and build the full library again, just remove the generated file *bindings2.cpp*).

```
python filter-bindings.py /path/to/files/*.js
```

5. Compile OpenCV and generate bindings by executing make.py script.

```
python make.py
```
a. WebAssembly version (experimental):
```
python make.py --wasm
```

b. asm.js version:
```
python make.py
```



### Tests
Test suite contains several tests and examples demonstrating how the API can be used. Run the tests by launching test/tests.html file usig a browser.

The file `tests/minimal-example.html` aims to be a minimal working example. It demonstrates the use of OpenCV JS, converting between ImageData and cv.Mat objects, and the use of the window.Module to initiate the runtime.

### Exported OpenCV Subset
Classes and functions that are intended for binding generators (i.e. come with wrapping macros such as CV_EXPORTS_W and CV_WRAP) are exposed. Hence, supported OpenCV subset is comparable to OpenCV for Python. Also, enums with exception of anonymous enums are also exported.

Expand Down
30 changes: 27 additions & 3 deletions bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ using namespace cv::ml;

namespace Utils{

double getScalarValue (const cv::Scalar_<double>& s, int i)
{
return s[i];
}

template<typename T>
emscripten::val data(const cv::Mat& mat) {
return emscripten::val(emscripten::memory_view<T>( (mat.total()*mat.elemSize())/sizeof(T), (T*) mat.data));
Expand Down Expand Up @@ -93,6 +98,12 @@ namespace Utils{
void convertTo(const Mat& obj, Mat& m, int rtype, double alpha, double beta) {
obj.convertTo(m, rtype, alpha, beta);
}
void copyMatTo(const Mat& obj, Mat& dst, const Mat& mask) {
obj.copyTo(dst, mask);
}
void setMatTo(Mat& obj, cv::Scalar_<double> sc, const Mat& mask) {
obj.setTo(sc, mask);
}
Size matSize(const cv::Mat& mat) {
return mat.size();
}
Expand Down Expand Up @@ -161,6 +172,9 @@ EMSCRIPTEN_BINDINGS(Utils) {
.constructor<int, int, int>()
.constructor(&Utils::createMat, allow_raw_pointers())
.constructor(&Utils::createMat2, allow_raw_pointers())

.function("setTo", select_overload<void(cv::Mat&, cv::Scalar_<double>, const cv::Mat&)>(&Utils::setMatTo))

.function("elemSize1", select_overload<size_t()const>(&cv::Mat::elemSize1))
//.function("assignTo", select_overload<void(Mat&, int)const>(&cv::Mat::assignTo))
.function("channels", select_overload<int()const>(&cv::Mat::channels))
Expand All @@ -174,8 +188,8 @@ EMSCRIPTEN_BINDINGS(Utils) {
.function("rowRange", select_overload<Mat(int, int)const>(&cv::Mat::rowRange))
.function("rowRange", select_overload<Mat(const Range&)const>(&cv::Mat::rowRange))

.function("copyTo", select_overload<void(OutputArray)const>(&cv::Mat::copyTo))
.function("copyTo", select_overload<void(OutputArray, InputArray)const>(&cv::Mat::copyTo))
.function("copyTo", select_overload<void(const Mat&, Mat&, const Mat&)>(&Utils::copyMatTo))

.function("elemSize", select_overload<size_t()const>(&cv::Mat::elemSize))

.function("type", select_overload<int()const>(&cv::Mat::type))
Expand Down Expand Up @@ -247,6 +261,15 @@ EMSCRIPTEN_BINDINGS(Utils) {
.element(&Point2f::x)
.element(&Point2f::y);

value_array<Size2f>("Size2f")
.element(&Size2f::height)
.element(&Size2f::width);

value_object<RotatedRect>("RotatedRect")
.field("angle", &RotatedRect::angle)
.field("center", &RotatedRect::center)
.field("size", &RotatedRect::size);

emscripten::class_<cv::Rect_<int>> ("Rect")
.constructor<>()
.constructor<const cv::Point_<int>&, const cv::Size_<int>&>()
Expand All @@ -264,7 +287,8 @@ EMSCRIPTEN_BINDINGS(Utils) {
.constructor<double, double, double>()
.constructor<double, double, double, double>()
.class_function("all", &cv::Scalar_<double>::all)
.function("isReal", select_overload<bool()const>(&cv::Scalar_<double>::isReal));
.function("isReal", select_overload<bool()const>(&cv::Scalar_<double>::isReal))
.function("get" , &Utils::getScalarValue);

function("matFromArray", &Utils::matFromArray);

Expand Down
Binary file added build/cv-wasm.data
Binary file not shown.
30 changes: 30 additions & 0 deletions build/cv-wasm.js

Large diffs are not rendered by default.

Binary file added build/cv-wasm.wasm
Binary file not shown.
59 changes: 59 additions & 0 deletions filter-bindings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/usr/bin/env python3

"""
Filters the EMSCRIPTEN_BINDINGS section of the bindings.cpp file, keeping only what is used in every file given as parameters.
Outputs the result to bindings2.cpp
"""

import sys
import os
import re

import argparse

SRC_BINDINGS = "bindings.cpp"
DST_BINDINGS = "bindings2.cpp"


parser = argparse.ArgumentParser()
parser.add_argument( "files", nargs = "+", default = None, help="The .js files to analyze" )

args = parser.parse_args()

props = set()
rePropFinder = re.compile( r"\bcv\s*\.\s*([a-zA-Z_][a-zA-Z0-9_]*)\b" )
#rePropFinder = re.compile( "\bcv\.([a-zA-Z_][a-zA-Z0-9_]*)" )

for f in args.files:
jssource = open( f, "r" ).read()
for match in rePropFinder.finditer( jssource ):
props.add( match.group( 1 ) )

#print( props )

def replace( match ):
lines = []
inSection = False
keepSection = False
# each "section" starts with a first line naming the prop, and ends with a line ending with a semicolon (it may be the same)
for line in match.group(2).split( "\n" ):
if not inSection:
inSection = bool( line.strip() )
if inSection: # start of a section
identifierMatched = re.search( r'"([^"]+)"', line )
keepSection = identifierMatched and identifierMatched.group(1) in props
if keepSection:
lines.append( "" ) # new section: insert a blank line before it

if inSection and keepSection:
lines.append( line )

if inSection and re.search( r";$", line ):
inSection = keepSection = False

return match.group(1) + "\n".join(lines) + match.group(3)

text = open( SRC_BINDINGS, 'r' ).read()
text = re.sub( r"(\bEMSCRIPTEN_BINDINGS\b\s*\(\s*\btestBinding\b\s*\)\s*\{)([^{}]*)(\})", replace, text )

open( DST_BINDINGS, 'w' ).write( text )
37 changes: 29 additions & 8 deletions make.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
#!/usr/bin/python
import os, sys, re, json, shutil
import argparse
from subprocess import Popen, PIPE, STDOUT


# parse the command-line options
parser = argparse.ArgumentParser()
parser.add_argument( "--wasm", action="store_true", help="Create a .wasm file (WebAssembly format, experimental) instead of asm.js format." )
clArguments = parser.parse_args()

# Startup
exec(open(os.path.expanduser('~/.emscripten'), 'r').read())

Expand Down Expand Up @@ -178,14 +185,27 @@ def stage(text):
]
include_dir_args = ['-I'+item for item in INCLUDE_DIRS]
emcc_binding_args = ['--bind']

emcc_binding_args += include_dir_args

emscripten.Building.emcc('../../bindings.cpp', emcc_binding_args, 'bindings.bc')
bindingsFile = '../../bindings.cpp'
bindingsFile2 = '../../bindings2.cpp'
if os.path.exists( bindingsFile2 ):
bindingsFile = bindingsFile2 # prefer "filtered" bindings file if it exists

emscripten.Building.emcc(bindingsFile, emcc_binding_args, 'bindings.bc')
assert os.path.exists('bindings.bc')

stage('Building OpenCV.js')
opencv = os.path.join('..', '..', 'build', 'cv.js')
data = os.path.join('..', '..', 'build', 'cv.data')

if clArguments.wasm:
emcc_args += "-s WASM=1".split( " " )
basename = "cv-wasm"
else:
basename = "cv"

destFiles = [ os.path.join('..', '..', 'build', basename + ext ) for ext in [ ".js", ".data", ".wasm" ] ]
opencv = destFiles[0]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about following directory structure?

build
  |----js
  |      |---- cv.js
  |      |---- cv.data
  |----wasm
            |----cv.wasm
            |----cv.data

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, putting all the files in the same folder with different names seemed simpler to me for the time being, as the .js file generated by emscripten expects to find its related .data (and .wasm, in case of WebAssembly) in the same folder as the .html file.
Having all the files in the same folder with different names depending on the technology used (WebAssembly or asm.js) simplifies the use: the user can just copy everything in build/ to the folder of his/her .html file, and then either load cv.js or cv-wasm.js depending on what technology s/he wants to use.


tests = os.path.join('..', '..', 'test')

Expand Down Expand Up @@ -215,7 +235,9 @@ def stage(text):
emscripten.Building.link(input_files, 'libOpenCV.bc')
emcc_args += '--preload-file ../../test/data/'.split(' ') #For testing purposes
emcc_args += ['--bind']
#emcc_args += ['--memoryprofiler']

#emcc_args += ['--memoryprofiler']
#emcc_args += ['--tracing'] # ability to use custom memory profiler, with hooks Module.onMalloc(), .onFree() and .onRealloc()

emscripten.Building.emcc('libOpenCV.bc', emcc_args, opencv)
stage('Wrapping')
Expand Down Expand Up @@ -245,10 +267,9 @@ def stage(text):
}));
""" % (out,)).lstrip())


shutil.copy2(opencv, tests)
if os.path.exists(data):
shutil.copy2(data, tests)
for f in destFiles:
if os.path.exists(f):
shutil.copy2(f, tests)

finally:
os.chdir(this_dir)
8 changes: 4 additions & 4 deletions patch_emscripten_master.diff → patch_emscripten.diff
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
--- ./emscripten/master/system/include/emscripten/bind.h 2016-12-20 13:05:20.498980029 -0800
+++ ./emscripten/master/system/include/emscripten/bind.h 2016-12-20 13:12:11.599146929 -0800
--- ./system/include/emscripten/bind.h 2016-12-20 13:05:20.498980029 -0800
+++ ./system/include/emscripten/bind.h 2016-12-20 13:12:11.599146929 -0800
@@ -999,7 +999,7 @@
};
}
Expand Down Expand Up @@ -54,8 +54,8 @@
template<typename WrapperType>
val wrapped_extend(const std::string& name, const val& properties) {

--- ./emscripten/master/src/embind/embind.js 2016-12-20 13:05:20.498980029 -0800
+++ ./emscripten/master/src/embind/embind.js 2016-12-20 13:12:11.599146929 -0800
--- ./src/embind/embind.js 2016-12-20 13:05:20.498980029 -0800
+++ ./src/embind/embind.js 2016-12-20 13:12:11.599146929 -0800
Copy link
Collaborator

@huningxin huningxin May 19, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this file change needed?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes the patch more generic, and usable with both emscripten/master and emscripten/incoming by changing just the patch command line (cf readme).

@@ -2077,6 +2077,7 @@
var invokerArgsArray = [argTypes[0] /* return value */, null /* no class 'this'*/].concat(argTypes.slice(1) /* actual params */);
var func = craftInvokerFunction(humanName, invokerArgsArray, null /* no class 'this'*/, rawInvoker, fn);
Expand Down
Binary file added test/data/lena.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading