はじめに
DenoでOpenCVを使おうと思った時に、こちらの記事でOpenCV-Wasmというモジュールを以下のようにすれば利用可能であることを知りました。
import { cv } from "https://deno.land/x/opencv@v4.3.0-10/mod.ts";
ただ、型情報がこんな感じでanyばかりになっていてOpenCV初心者にとっては少々厳しい状況です。
/** function */
export var findContours: any;
ということで、Deno上で型情報を使えるモジュールないかな、ということで調べてみることにしました。
長すぎるよ…という方へ
記事の一番下にある結論という見出しに飛んでください。
調べてみた
OpenCVの型情報に関してはこちらでまとめていただいているように以下のような状況のようです。
- 公式の実装(OpenCV.js)では型情報はない
- 有志のライブラリで型情報が使えるものは存在する
ただ、これらはTypeScriptで型情報を使えるというところの話で、それがNode.jsやDenoで動くかは別問題。実際に動かしてみながら確認してみました。
OpenCV-Wasm
まずDenoやNode.js上での実績のあるOpenCV-Wasm。
先に書いたようにこちらの型情報はanyばかりでしんどい、というのがこの記事のスタートですが、Node.jsでもDenoでも動くサンプルのコードが存在しているので、それをベンチマークにして確認してみます。
今回はExamplesの2つめ「templateMatching.js」をサンプルにしてみます。
Denoでコードはこちらの記事に挙がっていますし、実際にこのコードのままで問題なく動作します。
TechStark/opencv-js
まずこちらで紹介いただいているTechStark/opencv-jsです。
Node.jsで動くかまず確認してみるためにvscodeでコードを書いていると…
ん?
んん??
new MatVector()
の戻り型がMat
型になっている?
確認したところTechStark/opencv-jsが依存しているmiradaにて以下の内容が記述されているのが原因っぽい。
// Hack: expose Mat super classes like Mat_, InputArray, Vector, OutputArray we make them alias of Mat to simplify and make it work
export { Mat as InputArray, Mat as InputArrayOfArrays, Mat as InputOutputArray, Mat as InputOutputArrayOfArrays, Mat as MatVector, Mat as OutputArray, Mat as OutputArrayOfArrays } from './Mat'
なお、型エラーが出るものの以下のようにしてエラーを握りつぶして動かしてみると、contours自体はMatVectorとして生成されて、contours.size()も問題なくnumberを返しているようです。
// @ts-ignore
for (let i = 0; i < contours.size(); i += 1) {
動くのはいいとして、元々の、型情報が使える状態でOpenCVを使いたい、というモチベーションからはちょっとずれてくるので、とりあえず使用は先送り…
opencv-ts
今回のサンプルでは型情報も特に問題なく、Node.jsではランタイムの初期化完了を待ってから処理するというところだけ気をつければ、特に問題なく動作しました。
Node.jsで動作したコードはこちら。
import Jimp from 'jimp';
import cv from 'opencv-ts';
cv.onRuntimeInitialized = () => {
(async () => {
try {
const imageSource = await Jimp.read('./image-sample.png');
const imageTemplate = await Jimp.read('./image-sample-template.png');
const src = cv.matFromImageData(imageSource.bitmap);
const templ = cv.matFromImageData(imageTemplate.bitmap);
const processedImage = new cv.Mat();
const mask = new cv.Mat();
cv.matchTemplate(src, templ, processedImage, cv.TM_CCOEFF_NORMED, mask);
cv.threshold(processedImage, processedImage, 0.999, 1, cv.THRESH_BINARY);
processedImage.convertTo(processedImage, cv.CV_8UC1);
const contours = new cv.MatVector();
const hierarchy = new cv.Mat();
cv.findContours(
processedImage,
contours,
hierarchy,
cv.RETR_EXTERNAL,
cv.CHAIN_APPROX_SIMPLE,
);
for (let i = 0; i < contours.size(); i += 1) {
const countour = contours.get(i).data32S; // Contains the points
const x = countour[0];
const y = countour[1];
const color = new cv.Scalar(0, 255, 0, 255);
const pointA = new cv.Point(x, y);
const pointB = new cv.Point(x + templ.cols, y + templ.rows);
cv.rectangle(src, pointA, pointB, color, 2, cv.LINE_8, 0);
}
new Jimp({
width: src.cols,
height: src.rows,
data: Buffer.from(src.data),
}).write('./template-matching.png');
} catch (err) {
console.log(err);
}
})();
};
Node.jsで動作できたので、この状態をDenoで動くように移植。
単純にnpmを参照するように
import cv from "npm:opencv-ts";
にしたところ、こんな感じで型情報をちゃんと見られていない模様。
いろいろ試行錯誤していたところ、モジュール内のdefaultを参照すると型情報を使えることが判明。
import cvModule from "npm:opencv-ts";
const cv = cvModule.default;
とすると型エラーは消えました。
ですが、この状態で実行すると、
> deno run --allow-env --allow-net --allow-read --allow-write sample.ts
TypeError: Cannot read properties of undefined (reading 'matFromImageData')
TypeError: Cannot read properties of undefined (reading 'matFromImageData')
error: Uncaught "abort(TypeError: Cannot read properties of undefined (reading 'matFromImageData')). Build with -s ASSERTIONS=1 for more info."
というエラーが出てしまう…
デバッグ動作させてみると、cvModule.defaultではなくて、cvModuleに上記の関数ができていることが確認できたので、関数等の実体と型情報の取得場所をずらす意味で以下のように変更したところ、型情報も実行時動作も期待通りになりました。defaultの解釈がうまくできてないのかな…
import cvModule from "npm:opencv-ts";
const cv = cvModule as unknown as typeof cvModule.default;
最終的にDenoで動作したコードはこちら。
import cvModule from "npm:opencv-ts";
import Jimp from "npm:jimp";
const cv = cvModule as unknown as typeof cvModule.default;
const imageSource = await Jimp.read("./image-sample.png");
const imageTemplate = await Jimp.read("./image-sample-template.png");
const src = cv.matFromImageData(imageSource.bitmap);
const templ = cv.matFromImageData(imageTemplate.bitmap);
const processedImage = new cv.Mat();
const mask = new cv.Mat();
cv.matchTemplate(src, templ, processedImage, cv.TM_CCOEFF_NORMED, mask);
cv.threshold(processedImage, processedImage, 0.999, 1, cv.THRESH_BINARY);
processedImage.convertTo(processedImage, cv.CV_8UC1);
const contours = new cv.MatVector();
const hierarchy = new cv.Mat();
cv.findContours(
processedImage,
contours,
hierarchy,
cv.RETR_EXTERNAL,
cv.CHAIN_APPROX_SIMPLE,
);
for (let i = 0; i < contours.size(); ++i) {
const countour = contours.get(i).data32S; // Contains the points
const x = countour[0];
const y = countour[1];
const color = new cv.Scalar(0, 255, 0, 255);
const pointA = new cv.Point(x, y);
const pointB = new cv.Point(x + templ.cols, y + templ.rows);
cv.rectangle(src, pointA, pointB, color, 2, cv.LINE_8, 0);
}
new Jimp({
width: src.cols,
height: src.rows,
data: src.data,
}).write("template-matching.png");
結論
- npmからopencv-tsを使うとDenoでも型情報ありでOpenCVを使えました。
- だけど、
import cv from "npm:opencv-ts"
というやり方ではうまくいかなくて、
import cvModule from "npm:opencv-ts"
const cv = cvModule as unknown as typeof cvModule.default;
みたいなやり方をしたら上手くいきました。
参考: 確認環境
> deno --version
deno 1.31.1 (release, x86_64-apple-darwin)
v8 11.0.226.13
typescript 4.9.4
> node --version
v18.14.2