OpenCV.jsのビルドとその軽量化

## 環境
- OpenCV 4.4.0
- LinuxMint 20

## やりたいこと
- OpenCVをブラウザで動かしたい
    - OpenCVをJavascriptにクロスコンパイルしたOpenCV.jsが欲しい
- OpenCV.jsはopencv.orgにおいて公式に配布されている[^1]が、全部入りであるため容量が8MB近くあって重い
    - 必要なモジュールだけを選択して軽量化したOpenCV.jsが欲しい

[^1]: [https://docs.opencv.org/3.4.0/opencv.js](https://docs.opencv.org/3.4.0/opencv.js)

## うまくいかなかったところ
- まずは公式のチュートリアル([OpenCV: Build OpenCV.js](https://docs.opencv.org/master/d4/da1/tutorial_js_setup.html))を読んでみた。
- このマニュアルの冒頭に書いてある方法で、Emscriptenをインストールし、パスを通し、opencvのリポジトリをコピーしてきてコンパイルを試みた。
    - しかし、何回やっても99％のところでエラーが発生しうまく行かなかった
    - その原因をはっきりさせることはできなかったが、おそらくEmscriptenのバージョンかPythonのバージョンの問題ではないかと推測している

## Dockerイメージをつかってコンパイルを試みる
- 前掲のチュートリアルのページの最下部に、「Building OpenCV.js with Docker」という一節がある。
- なんと、ビルドに必要な環境一式をひとつのDockerイメージにまとめてくれているというわけである！
    - [trzeci/emscripten - Docker Hub](https://hub.docker.com/r/trzeci/emscripten/)
- これを用いてコンパイルを行なうとうまくいった。
    - Dockerって便利！

```bash
git clone https://github.com/opencv/opencv.git
cd opencv
docker run --rm --workdir /code -v "$PWD":/code "trzeci/emscripten:latest" python ./platforms/js/build_js.py build
```

- コンパイルが終了すると、次のようなメッセージが表示され、この場合だと、`/code/build/bin`にファイルが生成される。

```console
[100%] Generating ../../bin/opencv.js
cd /code/build/modules/js && /usr/bin/python2.7 /code/modules/js/src/make_umd.py /code/build/bin/opencv_js.js /code/build/bin/opencv.js
make[3]: Leaving directory '/code/build'
[100%] Built target opencv.js
make[2]: Leaving directory '/code/build'
/opt/cmake/bin/cmake -E cmake_progress_start /code/build/CMakeFiles 0
make[1]: Leaving directory '/code/build'
=====
===== Build finished
=====
OpenCV.js location: /code/build/bin/opencv.js
```

## OpenCV.jsの軽量化
- Dockerイメージを使うと難しい環境構築なしにビルドが出来ることが判ったので、続いて生成物の軽量化を行なう。
- 軽量化を行なうには、ダウンロードした`opencv`フォルダの中の、`platforms/js/opencv_js.config.py`を編集すればよい。

```python:opencv_js.config.py
# Classes and methods whitelist

core = {'': ['absdiff', 'add', 'addWeighted', 'bitwise_and', 'bitwise_not', 'bitwise_or', 'bitwise_xor', 'cartToPolar',\
             'compare', 'convertScaleAbs', 'copyMakeBorder', 'countNonZero', 'determinant', 'dft', 'divide', 'eigen', \
             'exp', 'flip', 'getOptimalDFTSize','gemm', 'hconcat', 'inRange', 'invert', 'kmeans', 'log', 'magnitude', \
             'max', 'mean', 'meanStdDev', 'merge', 'min', 'minMaxLoc', 'mixChannels', 'multiply', 'norm', 'normalize', \
             'perspectiveTransform', 'polarToCart', 'pow', 'randn', 'randu', 'reduce', 'repeat', 'rotate', 'setIdentity', 'setRNGSeed', \
             'solve', 'solvePoly', 'split', 'sqrt', 'subtract', 'trace', 'transform', 'transpose', 'vconcat'],
        'Algorithm': []}

imgproc = {"": ["circle", "line", "getPerspectiveTransform", "warpPerspective"]}


imgproc = {'': ['Canny', 'GaussianBlur', 'Laplacian', 'HoughLines', 'HoughLinesP', 'HoughCircles', 'Scharr','Sobel', \
                'adaptiveThreshold','approxPolyDP','arcLength','bilateralFilter','blur','boundingRect','boxFilter',\
                'calcBackProject','calcHist','circle','compareHist','connectedComponents','connectedComponentsWithStats', \
                'contourArea', 'convexHull', 'convexityDefects', 'cornerHarris','cornerMinEigenVal','createCLAHE', \
                'createLineSegmentDetector','cvtColor','demosaicing','dilate', 'distanceTransform','distanceTransformWithLabels', \
                'drawContours','ellipse','ellipse2Poly','equalizeHist','erode', 'filter2D', 'findContours','fitEllipse', \
                'fitLine', 'floodFill','getAffineTransform', 'getPerspectiveTransform', 'getRotationMatrix2D', 'getStructuringElement', \
                'goodFeaturesToTrack','grabCut','initUndistortRectifyMap', 'integral','integral2', 'isContourConvex', 'line', \
                'matchShapes', 'matchTemplate','medianBlur', 'minAreaRect', 'minEnclosingCircle', 'moments', 'morphologyEx', \
                'pointPolygonTest', 'putText','pyrDown','pyrUp','rectangle','remap', 'resize','sepFilter2D','threshold', \
                'undistort','warpAffine','warpPerspective','warpPolar','watershed', \
                'fillPoly', 'fillConvexPoly'],
           'CLAHE': ['apply', 'collectGarbage', 'getClipLimit', 'getTilesGridSize', 'setClipLimit', 'setTilesGridSize']}

objdetect = {'': ['groupRectangles'],
             'HOGDescriptor': ['load', 'HOGDescriptor', 'getDefaultPeopleDetector', 'getDaimlerPeopleDetector', 'setSVMDetector', 'detectMultiScale'],
             'CascadeClassifier': ['load', 'detectMultiScale2', 'CascadeClassifier', 'detectMultiScale3', 'empty', 'detectMultiScale']}


video = {'': ['CamShift', 'calcOpticalFlowFarneback', 'calcOpticalFlowPyrLK', 'createBackgroundSubtractorMOG2', \
             'findTransformECC', 'meanShift'],
         'BackgroundSubtractorMOG2': ['BackgroundSubtractorMOG2', 'apply'],
         'BackgroundSubtractor': ['apply', 'getBackgroundImage']}

dnn = {'dnn_Net': ['setInput', 'forward'],
       '': ['readNetFromCaffe', 'readNetFromTensorflow', 'readNetFromTorch', 'readNetFromDarknet',
            'readNetFromONNX', 'readNet', 'blobFromImage']}


features2d = {'Feature2D': ['detect', 'compute', 'detectAndCompute', 'descriptorSize', 'descriptorType', 'defaultNorm', 'empty', 'getDefaultName'],
              'BRISK': ['create', 'getDefaultName'],
              'ORB': ['create', 'setMaxFeatures', 'setScaleFactor', 'setNLevels', 'setEdgeThreshold', 'setFirstLevel', 'setWTA_K', 'setScoreType', 'setPatchSize', 'getFastThreshold', 'getDefaultName'],
              'MSER': ['create', 'detectRegions', 'setDelta', 'getDelta', 'setMinArea', 'getMinArea', 'setMaxArea', 'getMaxArea', 'setPass2Only', 'getPass2Only', 'getDefaultName'],
              'FastFeatureDetector': ['create', 'setThreshold', 'getThreshold', 'setNonmaxSuppression', 'getNonmaxSuppression', 'setType', 'getType', 'getDefaultName'],
              'AgastFeatureDetector': ['create', 'setThreshold', 'getThreshold', 'setNonmaxSuppression', 'getNonmaxSuppression', 'setType', 'getType', 'getDefaultName'],
              'GFTTDetector': ['create', 'setMaxFeatures', 'getMaxFeatures', 'setQualityLevel', 'getQualityLevel', 'setMinDistance', 'getMinDistance', 'setBlockSize', 'getBlockSize', 'setHarrisDetector', 'getHarrisDetector', 'setK', 'getK', 'getDefaultName'],
              # 'SimpleBlobDetector': ['create'],
              'KAZE': ['create', 'setExtended', 'getExtended', 'setUpright', 'getUpright', 'setThreshold', 'getThreshold', 'setNOctaves', 'getNOctaves', 'setNOctaveLayers', 'getNOctaveLayers', 'setDiffusivity', 'getDiffusivity', 'getDefaultName'],
              'AKAZE': ['create', 'setDescriptorType', 'getDescriptorType', 'setDescriptorSize', 'getDescriptorSize', 'setDescriptorChannels', 'getDescriptorChannels', 'setThreshold', 'getThreshold', 'setNOctaves', 'getNOctaves', 'setNOctaveLayers', 'getNOctaveLayers', 'setDiffusivity', 'getDiffusivity', 'getDefaultName'],
              'DescriptorMatcher': ['add', 'clear', 'empty', 'isMaskSupported', 'train', 'match', 'knnMatch', 'radiusMatch', 'clone', 'create'],
              'BFMatcher': ['isMaskSupported', 'create'],
              '': ['drawKeypoints', 'drawMatches', 'drawMatchesKnn']}


photo = {'': ['createAlignMTB', 'createCalibrateDebevec', 'createCalibrateRobertson', \
              'createMergeDebevec', 'createMergeMertens', 'createMergeRobertson', \
              'createTonemapDrago', 'createTonemapMantiuk', 'createTonemapReinhard', 'inpaint'],
        'CalibrateCRF': ['process'],
        'AlignMTB' : ['calculateShift', 'shiftMat', 'computeBitmaps', 'getMaxBits', 'setMaxBits', \
                      'getExcludeRange', 'setExcludeRange', 'getCut', 'setCut'],
        'CalibrateDebevec' : ['getLambda', 'setLambda', 'getSamples', 'setSamples', 'getRandom', 'setRandom'],
        'CalibrateRobertson' : ['getMaxIter', 'setMaxIter', 'getThreshold', 'setThreshold', 'getRadiance'],
        'MergeExposures' : ['process'],
        'MergeDebevec' : ['process'],
        'MergeMertens' : ['process', 'getContrastWeight', 'setContrastWeight', 'getSaturationWeight', \
                          'setSaturationWeight', 'getExposureWeight', 'setExposureWeight'],
        'MergeRobertson' : ['process'],
        'Tonemap' : ['process' , 'getGamma', 'setGamma'],
        'TonemapDrago' : ['getSaturation', 'setSaturation', 'getBias', 'setBias', \
                          'getSigmaColor', 'setSigmaColor', 'getSigmaSpace','setSigmaSpace'],
        'TonemapMantiuk' : ['getScale', 'setScale', 'getSaturation', 'setSaturation'],
        'TonemapReinhard' : ['getIntensity', 'setIntensity', 'getLightAdaptation', 'setLightAdaptation', \
                             'getColorAdaptation', 'setColorAdaptation']
        }


aruco = {'': ['detectMarkers', 'drawDetectedMarkers', 'drawAxis', 'estimatePoseSingleMarkers', 'estimatePoseBoard', 'estimatePoseCharucoBoard', 'interpolateCornersCharuco', 'drawDetectedCornersCharuco'],
        'aruco_Dictionary': ['get', 'drawMarker'],
        'aruco_Board': ['create'],
        'aruco_GridBoard': ['create', 'draw'],
        'aruco_CharucoBoard': ['create', 'draw'],
        }

calib3d = {'': ['findHomography', 'calibrateCameraExtended', 'drawFrameAxes', 'estimateAffine2D', 'getDefaultNewCameraMatrix', 'initUndistortRectifyMap', 'Rodrigues']}


white_list = makeWhiteList([core, imgproc, objdetect, video, dnn, features2d, photo, aruco, calib3d])
```

- `opencv_js.config.py`には、OpenCV.jsに盛り込むモジュールが羅列されている
    - 最終行の配列`white_list`に入れられたモジュールがコンパイルされ、生成物たるOpenCV.jsに盛り込まれる
- ただ、不要なものも多い。
    - 物体検知や顔認識などをやらないのであれば`dnn`は不要
    - Webカメラをつかったプログラムをつくるのでなければ`video`は不要である
- そこでこのファイルを編集して、自分の必要なモジュールだけを選ぶことにする
    - 例を掲げる

```python:opencv_js.config.py
core = {'': ['absdiff', 'add', 'addWeighted', 'bitwise_and', 'bitwise_not', 'bitwise_or', 'bitwise_xor', 'cartToPolar',\
             'compare', 'convertScaleAbs', 'copyMakeBorder', 'countNonZero', 'determinant', 'dft', 'divide', 'eigen', \
             'exp', 'flip', 'getOptimalDFTSize','gemm', 'hconcat', 'inRange', 'invert', 'kmeans', 'log', 'magnitude', \
             'max', 'mean', 'meanStdDev', 'merge', 'min', 'minMaxLoc', 'mixChannels', 'multiply', 'norm', 'normalize', \
             'perspectiveTransform', 'polarToCart', 'pow', 'randn', 'randu', 'reduce', 'repeat', 'rotate', 'setIdentity', 'setRNGSeed', \
             'solve', 'solvePoly', 'split', 'sqrt', 'subtract', 'trace', 'transform', 'transpose', 'vconcat'],
        'Algorithm': []}

imgproc = {"": ["circle", "line", "getPerspectiveTransform", "warpPerspective"]}

calib3d = {"": ["findHomography"]}

white_list = makeWhiteList([core, imgproc, calib3d])
```

- `platforms/js/opencv_js.config.py`を編集した上で再度ビルドを行なうと、軽量化された生成物が出力される。
    - ちなみに、coreモジュール+若干の射影変換モジュールだけにした場合、2.6MBにまで軽量化をすることができた。
    - 2.6/8.6 = 0.30 で7割のファイルサイズの削減に成功した！[^2]

[^2]: coreモジュールも削れるものを削ったら更に小さく出来るのでは？

## まとめ
- OpenCV.jsはコンパイル済みのものが公式でも配布されているがファイルサイズが大きい
- 公式チュートリアルで案内されているDockerイメージを用いると面倒な環境構築なしにコンパイルすることが出来る
- 軽量化を図る際は設定ファイル`platforms/js/opencv_js.config.py`をいじれば良い

## 参考
- [OpenCV: Build OpenCV.js](https://docs.opencv.org/master/d4/da1/tutorial_js_setup.html)
- [trzeci/emscripten - Docker Hub](https://hub.docker.com/r/trzeci/emscripten/)
- [Ubuntuにdockerをインストールする - Qiita](https://qiita.com/tkyonezu/items/0f6da57eb2d823d2611d)