これは 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にその画像が表示される
用意するもの
- 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
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から渡された
Array
とcv: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 さんの記事です。