MapLibre GL JS + OpenCV 

:::note info
これは MIERUNE Advent Calendar 2024 の19日目の記事です。
昨日は @geo_jagaimo さんによる ドローンによる空撮画像を最大活用！WebODM のすゝめ でした。
:::


アドベントカレンダーを書く季節になったので、なにか題材がないかな？ なんて探してみた。
最近はほとんど `C/C++` のコードを書かなくなったが、昔 `OpenCV` でいろいろプログラミングしていた頃の残骸コードが残っていたので調べてみると、地図ライブラリと `OpenCV` を使った簡単なコードが残っていたので記事にしてみた。（ちょっと古いしお試し版なのは・・・）
`OpenCV.js` なるものがあり、これを使うとオールJsで書けるらしいが、過去のC++コードを利用したいのか、`Emscripten`を使って`WebAssembly`で作ってある。

`SDL`のサーフェイスはそのままHTML側の`canvas`に表現されるらしく（この辺詳しくない）
- MapLibleのcanvasの映像をOpenCV(c++)側に送る
- OpenCV(C++)で画像変換処理をして（今回は簡単なグレースケール化）SDLで表現
- もう一方のcanvasにその画像が表示される

以下ような感じ。
![move.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2810146/77e0e9a7-71a3-756a-5d26-77b66f282708.gif)

## 用意するもの 
- emscripten　
    - コンパイラ
- openCVの静的ライブラリー（Static Library）
    - libopencv_core.a
    - libopencv_imgproc.a
- メインのページ (MapLibre側のJSコード)
    - index.html
- OpenCV側のC++コード
    - sample.cpp

## Vscode利用を前提で、emscriptenの環境をdevcontainerで作成
devcontainerはローカル環境を汚染しないので大変便利、かつ面倒なセットアップは不要、一発で環境が出来上がる。

.devcontainerの内容は以下

- devcontainer.json
``` json
{
	"workspaceFolder": "/home/emscripten/workspace",
	"dockerComposeFile": "docker-compose.yml",
	"service": "emscripten",
}

```
- Dockerfile
``` Docker
FROM emscripten/emsdk:3.1.23

ENV DEBIAN_FRONTEND=noninteractive

EXPOSE 5500

RUN     apt-get update && \
        apt-get install -y --no-install-recommends\
        sudo \
        tzdata \
        curl \
        ca-certificates \
        ssh \
        git \
        wget \
        unzip \
        iputils-ping \
        net-tools \
        vim \
        make

ARG username=emscripten
ARG wkdir=/home/emscripten

RUN echo "root:root" | chpasswd && \
    echo "${username}:${username}" | chpasswd && \
    echo "%${username}    ALL=(ALL)   NOPASSWD:    ALL" >> /etc/sudoers.d/${username} && \
    chmod 0440 /etc/sudoers.d/${username}

WORKDIR ${wkdir}
RUN chown ${username}:${username} ${wkdir}
USER ${username}

CMD ["bash"]

```


## OpenCVの静的ライブラリー（Static Library）の作成

- 作業はすべてコンテナ内で行う
- OpenCVのフルセットコンパイルは無理なので、ほぼ素の状態でコンパイルする
- 動的ライブラリーもWebAssemblyで利用できるらしいが、よくわからんのでスタティックビルド用に作成

``` bash
apt install ninja-build 

source /emsdk/emsdk_env.sh

git clone https://github.com/opencv/opencv.git
cd opencv
mkdir build
cd build
OPTS='-O2'
emcmake cmake \
  -DCMAKE_BUILD_TYPE=RELEASE \
  -DBUILD_opencv_highgui=OFF \
  -DBUILD_DOCS=OFF \
  -DBUILD_EXAMPLES=OFF \
  -DBUILD_PACKAGE=OFF \
  -DBUILD_WITH_DEBUG_INFO=OFF \
  -DBUILD_opencv_cuda=OFF \
  -DBUILD_opencv_cudaarithm=OFF \
  -DBUILD_opencv_cudabgsegm=OFF \
  -DBUILD_opencv_cudacodec=OFF \
  -DBUILD_opencv_cudafeatures2d=OFF \
  -DBUILD_opencv_cudafilters=OFF \
  -DBUILD_opencv_cudaimgproc=OFF \
  -DBUILD_opencv_cudaoptflow=OFF \
  -DBUILD_opencv_cudastereo=OFF \
  -DBUILD_opencv_cudawarping=OFF \
  -DBUILD_opencv_python2=OFF \
  -DBUILD_opencv_python3=OFF \
  -DENABLE_PRECOMPILED_HEADERS=OFF \
  -DWITH_1394=OFF \
  -DWITH_CUDA=OFF \
  -DWITH_CUFFT=OFF \
  -DWITH_EIGEN=OFF \
  -DWITH_FFMPEG=OFF \
  -DWITH_GIGEAPI=OFF \
  -DWITH_GSTREAMER=OFF \
  -DWITH_GTK=OFF \
  -DWITH_JASPER=OFF \
  -DWITH_JPEG=OFF \
  -DWITH_OPENCL=OFF \
  -DWITH_OPENCLAMDBLAS=OFF \
  -DWITH_OPENCLAMDFFT=OFF \
  -DWITH_OPENEXR=OFF \
  -DWITH_PNG=OFF \
  -DWITH_PVAPI=OFF \
  -DWITH_TIFF=OFF \
  -DWITH_LIBV4L=OFF \
  -DWITH_WEBP=OFF \
  -DWITH_PTHREADS_PF=OFF \
  -DWITH_GDAL=OFF \
  -DWITH_V4L=OFF \
  -DWITH_TBB=OFF \
  -DBUILD_opencv_apps=OFF \
  -DBUILD_PERF_TESTS=OFF \
  -DBUILD_TESTS=OFF \
  -DBUILD_SHARED_LIBS=OFF \
  -BUILD_ZLIB =OFF \
  -DENABLE_SSE=OFF \
  -DENABLE_SSE2=OFF \
  -DENABLE_SSE3=OFF \
  -DENABLE_SSE41=OFF \
  -DENABLE_SSE42=OFF \
  -DENABLE_AVX=OFF \
  -DENABLE_AVX2=OFF \
  -DCMAKE_CXX_FLAGS=$OPTS \
  -DCMAKE_EXE_LINKER_FLAGS=$OPTS \
  -DCMAKE_CXX_FLAGS_DEBUG=$OPTS \
  -DCMAKE_CXX_FLAGS_RELWITHDEBINFO=$OPTS \
  -DCMAKE_C_FLAGS_RELWITHDEBINFO=$OPTS \
  -DCMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO=$OPTS \
  -DCMAKE_MODULE_LINKER_FLAGS_RELEASE=$OPTS \
  -DCMAKE_MODULE_LINKER_FLAGS_DEBUG=$OPTS \
  -DCMAKE_MODULE_LINKER_FLAGS_RELWITHDEBINFO=$OPTS \
  -DCMAKE_SHARED_LINKER_FLAGS_RELEASE=$OPTS \
  -DCMAKE_SHARED_LINKER_FLAGS_RELWITHDEBINFO=$OPTS \
  -DCMAKE_SHARED_LINKER_FLAGS_DEBUG=$OPTS \
  ../

emmake make -j
```
作成されたライブラリーを　`build_wasm/lib` フォルダーを作成して配置する
- libopencv_core.a
- libopencv_imgproc.a

OpenCVのヘッダーファイルがコンパイル時に必要なのでこれも　`build_wasm` にコピーする。

## Makefileの作成
以下のMakefileを見ていただくとソースの配置状況はわかると思われる。

``` make
SHELL		=/bin/bash
EMSDK_SH	=/emsdk/emsdk_env.sh
CC			= emcc
CFLAGS		= -O3 -Wall -g -std=c++20 -s USE_BOOST_HEADERS=1
DEST        = dest/
SRCS        = src/
PROGRAM     = sample

all: clean copy $(PROGRAM) 

$(PROGRAM):
	source $(EMSDK_SH) && \
	$(CC) \
		build_wasm/lib/libopencv_core.a \
		build_wasm/lib/libopencv_imgproc.a \
		$(SRCS)$(PROGRAM).cpp \
		$(CFLAGS) \
		-s EXPORTED_RUNTIME_METHODS=['ccall','UTF8ToString','setValue'] \
		-s EXPORTED_FUNCTIONS=['_malloc','_free'] \
		-I build_wasm/opencv/include \
		-o $(DEST)$(PROGRAM).js

clean:
	rm -rf $(DEST)
	mkdir -p $(DEST)

copy:
	cp $(SRCS)index.html $(DEST)index.html

```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2810146/2dcdb3dc-ed7b-8638-3618-962f1a79bfdc.png)

## index.htmを作成

``` js
<!DOCTYPE html>
<html>
<meta charset="utf-8" />
<title>Display a map</title>
<meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
<script src="https://unpkg.com/maplibre-gl@2.4.0/dist/maplibre-gl.js"></script>
<link href="https://unpkg.com/maplibre-gl@2.4.0/dist/maplibre-gl.css" rel="stylesheet" />
<style>
  body {
    margin: 0;
    padding: 0;
  }

  #map {
    width: 256px;
    height: 256px;
  }

  #canvas {
    width: 256px;
    height: 256px;
  }

  .window {
    visibility: hidden;
  }
</style>
</head>

<body>
  <div id="map"></div>
  <canvas id="canvas2"></canvas>
  <canvas id="canvas3" class='window'></canvas>

  <script type='text/javascript'>

    const SIZE_Y = 256;
    const SIZE_X = 256;

    function callTestcpp(imageData) {
      //Int32Array.BYTES_PER_ELEMENT 要素の大きさを数値で返します。Int32Array の場合は 4 です。
      const bytesPerElement = Module.HEAP32.BYTES_PER_ELEMENT;
      const arrayLength = imageData.data.length;
      // メモリー確保してデーターをセット
      const arrayPointer = Module._malloc((arrayLength * bytesPerElement));
      for (var i = 0; i < arrayLength; i++) {
        Module.setValue(arrayPointer + i * bytesPerElement, imageData.data[i], 'i32');
      }
      // C++の関数を呼び出し
      const isValid = Module.ccall('testcpp',
        'number',
        ['string', 'number', 'number', 'number', 'number'],
        ['', arrayPointer, arrayLength, SIZE_Y, SIZE_X]);

      // メモリー開放
      Module._free(arrayPointer);
      return (isValid === 1);
    }

    function setData() {
      const canvas = map.getCanvas();
      //画像オブジェクトを生成
      var image = new Image();
      image.src = canvas.toDataURL('image/png');
      // 隠しキャンバスにセット
      var cvs = document.getElementById('canvas3');
      cvs.width = SIZE_X;
      cvs.height = SIZE_Y;
      var ctx = cvs.getContext('2d');
      //画像をcanvasに設定
      image.onload = function () {
        ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
        var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
        callTestcpp(imageData);
      }
    }

    var map = new maplibregl.Map({
      container: 'map', // container id
      style: 'https://demotiles.maplibre.org/style.json', // style URL
      center: [0, 0], // starting position [lng, lat]
      zoom: 1, // starting zoom
      preserveDrawingBuffer: true// required for safe snapshot 
    });

    map.on('drag', function () {
      setData();
    });

    map.on('zoom', function () {
      setData();
    });

    var Module = {
      preRun: [],
      postRun: [],
      printErr: function (text) {
        if (arguments.length > 1)
          text = Array.prototype.slice.call(arguments).join(' ');
        console.error(text);
      },
      canvas: (function () {
        var canvas = document.getElementById('canvas2');
        return canvas;
      })(),
      setStatus: function (text) { }
    };
    Module.setStatus('Downloading...');
    window.onerror = function (event) {
      Module.setStatus('Exception thrown, see JavaScript console');
      Module.setStatus = function (text) {
        if (text) console.error('[post-exception status] ' + text);
      };
    };
  </script>
  <script src="sample.js"></script>
</body>
</html>
```
ここでやっていることは `MapLiblre` のキャンバスからデータを引っこ抜いて
`callTestcpp` 関数にデータを引き渡し　C++側の関数　`testcpp`　に渡しているだけ。

:::note warn
<font color="red">**preserveDrawingBuffer: true**</font>
 **注意**!  これがないとcanvasのデータが取得できないので注意！
:::

## sample.cppの作成

``` c++
#include <stdio.h>
#include <cstdlib>
#include <cstring>
#include <iostream>
#include <fstream>
#include <boost/lexical_cast.hpp>
#include <boost/format.hpp>
#include <SDL/SDL.h>
#include "opencv2/opencv.hpp"

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif

auto drawMat(cv::Mat &image)
{
    const auto cols = image.cols;
    const auto rows = image.rows;

    SDL_Init(SDL_INIT_VIDEO);
    auto *screen = SDL_SetVideoMode(rows, cols, 32, SDL_SWSURFACE);

    if (SDL_MUSTLOCK(screen))
        SDL_LockSurface(screen);

    Uint32 pix_pos = 0;
    for (auto j = 0; j < rows; j++)
    {
        for (auto i = 0; i < cols; i++)
        {
            // R,G,B,A
            *(reinterpret_cast<Uint32 *>(screen->pixels) + pix_pos) =
                SDL_MapRGBA(screen->format,
                            image.at<cv::Vec3b>(j, i)[2], // 青
                            image.at<cv::Vec3b>(j, i)[1], // 緑
                            image.at<cv::Vec3b>(j, i)[0], // 赤
                            255 /* image.at<cv::Vec4b>(j, i)[3] */);
            pix_pos++;
        }
    }
    if (SDL_MUSTLOCK(screen))
        SDL_UnlockSurface(screen);

    SDL_Flip(screen);
    SDL_Quit();
    return 0;
}

auto setMat(int *array, int size_y, int size_x)
{
    // array    は R,G,B,A
    // cv::Mat  は B,G,R,A
    cv::Mat image(size_y, size_x, CV_8UC3);

    const auto cols = image.cols;
    const auto rows = image.rows;
    auto pos = 0;
    for (auto j = 0; j < rows; j++)
    {
        for (auto i = 0; i < cols; i++)
        {
            image.at<cv::Vec3b>(j, i)[0] = array[pos + 2]; // 青
            image.at<cv::Vec3b>(j, i)[1] = array[pos + 1]; // 緑
            image.at<cv::Vec3b>(j, i)[2] = array[pos];     // 赤
            // image.at<cv::Vec4b>(j, i)[3] = array[pos + 3]; // アルファー
            pos += 4;
        }
    }

    cv::Ptr<cv::Mat> outMat(new cv::Mat(image));
    return outMat;
}

#ifdef __cplusplus
extern "C" // マングリング対策
{
#endif

#ifdef __EMSCRIPTEN__
    EMSCRIPTEN_KEEPALIVE
#endif
    int testcpp(char *base64, int *array, int arraylength, int size_y, int size_x)
    {
        auto inputImage = setMat(array, 256, 256);
        cv::Mat grayImage, outImage;
        cv::cvtColor(*inputImage, grayImage, cv::COLOR_BGR2GRAY);
        cv::cvtColor(grayImage, outImage, cv::COLOR_GRAY2BGR);
        drawMat(outImage);
        return 1;
    }

#ifdef __cplusplus
}
#endif
```
:::note warn
- Jsから呼び出される関数は　<font color="red">**extern "C"**</font> してマングリング対策が必ず必要
- <font color="red">**EMSCRIPTEN_KEEPALIVE**</font> マクロを利用してEmscriptenが最適化時に不要と判断して削除してしまう関数やシンボルを保持するように指示
:::


- `testcpp` 最初の引数はメモ代わりの文字列を渡している（デバッグなどに有効か）
- JSから渡された `Array` と `cv:Mat` の形式は違うので `setMat` 関数で `cv:Mat` に変換
   - array は R,G,B,A
   - cv:Mat  は B,G,R,A
- 変換後、OpenCVの関数をつかってグレースケール化
:::note info
ここでOpoenCV関数を使ってなんでもできちゃうのが嬉しい (^o^)
:::
- `drawMat` 関数でSDLサーフェイスに描画
- 描画された画像がHTML側で表示される

## 結論として
- どこかでメモリーリークしているらしく。しばらく使っていると表示がとまる
- なんか動作が遅い。（使いもんにならん）

### 驚いたのは以下の点

```
CFLAGS = -O3 -Wall -g -std=c++20 -s USE_BOOST_HEADERS=1
```
- 全然使いこなせる気がしないが一応、<font color="#1e90ff">**c++20**</font> で採用された構文が使える
- <font color="#1e90ff">**-s USE_BOOST_HEADERS=1**</font> で　なにもしなくても　BOOST　が使えるのは大変よい
　　（この記事のコードではつかっていませんが、実際利用できました。）

- SDLのサーフェイスがなにもしなくても勝手にcanvasで描かれる
音楽とかも流れるのかな？　
- sample.wasm は　3.08 MB　だった

:::note info
明日は @shiori0436 さんの記事です。
:::