9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

これは 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

用意するもの

  • 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
{
	"workspaceFolder": "/home/emscripten/workspace",
	"dockerComposeFile": "docker-compose.yml",
	"service": "emscripten",
}

  • Dockerfile
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で利用できるらしいが、よくわからんのでスタティックビルド用に作成
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を見ていただくとソースの配置状況はわかると思われる。

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

index.htmを作成

<!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 に渡しているだけ。

preserveDrawingBuffer: true
注意! これがないとcanvasのデータが取得できないので注意!

sample.cppの作成

#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
  • Jsから呼び出される関数は extern "C" してマングリング対策が必ず必要
  • EMSCRIPTEN_KEEPALIVE マクロを利用してEmscriptenが最適化時に不要と判断して削除してしまう関数やシンボルを保持するように指示
  • testcpp 最初の引数はメモ代わりの文字列を渡している(デバッグなどに有効か)
  • JSから渡された Arraycv:Mat の形式は違うので setMat 関数で cv:Mat に変換
    • array は R,G,B,A
    • cv:Mat は B,G,R,A
  • 変換後、OpenCVの関数をつかってグレースケール化

ここでOpoenCV関数を使ってなんでもできちゃうのが嬉しい (^o^)

  • drawMat 関数でSDLサーフェイスに描画
  • 描画された画像がHTML側で表示される

結論として

  • どこかでメモリーリークしているらしく。しばらく使っていると表示がとまる
  • なんか動作が遅い。(使いもんにならん)

驚いたのは以下の点

CFLAGS = -O3 -Wall -g -std=c++20 -s USE_BOOST_HEADERS=1
  • 全然使いこなせる気がしないが一応、c++20 で採用された構文が使える

  • -s USE_BOOST_HEADERS=1 で なにもしなくても BOOST が使えるのは大変よい
      (この記事のコードではつかっていませんが、実際利用できました。)

  • SDLのサーフェイスがなにもしなくても勝手にcanvasで描かれる
    音楽とかも流れるのかな? 

  • sample.wasm は 3.08 MB だった

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

9
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?