DenoでOpenCVを型情報ありで使ってみる

# はじめに

DenoでOpenCVを使おうと思った時に、[こちらの記事](https://developer.mamezou-tech.com/deno/getting-started/05-using-wasm/)でOpenCV-Wasmというモジュールを以下のようにすれば利用可能であることを知りました。

```typescript
import { cv } from "https://deno.land/x/opencv@v4.3.0-10/mod.ts";
```

ただ、型情報が[こんな感じ](https://unpkg.com/opencv-wasm@4.3.0-10/types/opencv.ts)でanyばかりになっていてOpenCV初心者にとっては少々厳しい状況です。
```typescript
/** function */
export var findContours: any;
```

ということで、Deno上で型情報を使えるモジュールないかな、ということで調べてみることにしました。

# 長すぎるよ…という方へ
記事の一番下にある[結論](#結論)という見出しに飛んでください。


# 調べてみた
OpenCVの型情報に関しては[こちら](https://qiita.com/u1F992/items/8691d2f4b718191e7bfb)でまとめていただいているように以下のような状況のようです。
* 公式の実装(OpenCV.js)では型情報はない
* 有志のライブラリで型情報が使えるものは存在する

https://qiita.com/u1F992/items/8691d2f4b718191e7bfb

ただ、これらはTypeScriptで型情報を使えるというところの話で、それがNode.jsやDenoで動くかは別問題。実際に動かしてみながら確認してみました。

## OpenCV-Wasm

まずDenoやNode.js上での実績のある[OpenCV-Wasm](https://github.com/echamudi/opencv-wasm)。

https://github.com/echamudi/opencv-wasm

先に書いたようにこちらの型情報はanyばかりでしんどい、というのがこの記事のスタートですが、Node.jsでもDenoでも動くサンプルのコードが存在しているので、それをベンチマークにして確認してみます。

今回は[Examples](https://github.com/echamudi/opencv-wasm#examples)の2つめ「templateMatching.js」をサンプルにしてみます。

Denoでコードは[こちらの記事](https://developer.mamezou-tech.com/deno/getting-started/05-using-wasm/#opencv-wasm)に挙がっていますし、実際にこのコードのままで問題なく動作します。

## TechStark/opencv-js
まず[こちら](https://qiita.com/u1F992/items/8691d2f4b718191e7bfb#techstarkopencv-js)で紹介いただいている[TechStark/opencv-js](https://github.com/TechStark/opencv-js)です。

https://github.com/TechStark/opencv-js

Node.jsで動くかまず確認してみるためにvscodeでコードを書いていると…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83499/805fba1c-46e8-0797-e795-f21ccc1a0c02.png)
ん?
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83499/e898fb4c-57f2-5878-14bd-ad57862e5802.png)
んん??
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83499/ad375431-7f56-98e6-9c9e-1b142837b6cb.png)
`new MatVector()`の戻り型が`Mat`型になっている?

確認したところTechStark/opencv-jsが依存しているmiradaにて以下の内容が記述されているのが原因っぽい。
```typescript
// 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を返しているようです。
```typescript
// @ts-ignore
for (let i = 0; i < contours.size(); i += 1) {
```

動くのはいいとして、元々の、型情報が使える状態でOpenCVを使いたい、というモチベーションからはちょっとずれてくるので、とりあえず使用は先送り…

## opencv-ts
つぎに[こちら](https://qiita.com/u1F992/items/8691d2f4b718191e7bfb#theothergrantdavidsonopencv-ts)で紹介いただいている[opencv-ts](https://github.com/theothergrantdavidson/opencv-ts)です。

https://github.com/theothergrantdavidson/opencv-ts

今回のサンプルでは型情報も特に問題なく、Node.jsではランタイムの初期化完了を待ってから処理するというところだけ気をつければ、特に問題なく動作しました。

Node.jsで動作したコードはこちら。
```typescript
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を参照するように
```typescript
import cv from "npm:opencv-ts";
```
にしたところ、こんな感じで型情報をちゃんと見られていない模様。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83499/3676da88-67b7-ac82-68b7-d84b923158a0.png)

いろいろ試行錯誤していたところ、モジュール内のdefaultを参照すると型情報を使えることが判明。

```typescript
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の解釈がうまくできてないのかな…

```typescript
import cvModule from "npm:opencv-ts";

const cv = cvModule as unknown as typeof cvModule.default;
```

最終的にDenoで動作したコードはこちら。

```typescript
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
```