opencv.js入門。画像データの入力、出力、変換から

# はじめに

opencvは画像処理に使うライブラリーであり、本来主にC++とPythonで使われるためですが、実は「opencv.js」というJavaScriptで使えるopencvも、あまり知られていないようですが、存在しています。

opencvはPythonで書いた方が一番使いやすくて使い勝手がいいのですが、ウェブアプリを作る場合ブラウザで走らせる場合も多くて、その時JavaScriptで書く必要があります。opencv.jsを使うことでサーバーに画像データを送る必要せずに、ブラウザ上だけで画像処理することができて便利です。

ただしPythonのopencvと比べて書き方は色々違って複雑なところがあって使いやすいというわけではないようです。今までずっとPythonでopencvを使っていても、opencv.jsを使おうとしたら、色々勉強し直さなければならないことが意外と沢山あります。

問題は、今でもopencv.js関連の情報はまだ少なくて、検索してもやはPythonのopencvの方ばかり出てきます。

その中で一冊書籍があります。丁寧に説明してあるので、これを読んでとても参考になりました。基礎からきちんと勉強したい人にはおすすめです。

https://www.amazon.co.jp/dp/B0D7Z1NTDR

今回の記事はこの本で読んだ内容と更に自分で調べた知識を纏めて書いたopencv.js入門となります。

ただしopencvの機能を紹介するのではなく、「opencvがどうやってJavaScriptで導入する」がメインです。これがわかったら後はPython版やC++版の資料を読んでもopencv.jsでできるはずです。

opencv.jsを使う時一番Pythonと違うのは、入力、出力、変換など、基本的な画像データの取り扱いです。だからこの入門はこれをメインとして説明します。


# 導入と前提

## 導入し方それぞれ

opencv.jsを導入する方法は色んな方法があります。基本的に他のJavaScriptと同じです。ただしファイルサイズは10MB以上も大きいので、軽量化でビルトする方法を使う人もいます。でも私は普通にそのまま全部読み込んでも特に重いと感じないのでそこまで必要ないと思います。だから簡単にライブラリー全体を読み込んですぐ使います。

コードはopencvの公式サイトに載っているので、これを直接読み込んで

今この記事を書いた時点一番新しいバージョンは4.10.0です。URLは：

> https://docs.opencv.org/4.10.0/opencv.js

これを`<script>`タグで読み込んだらすぐ使えます。勿論実際に使う時はこのファイルをダウンロードして自分のサイトに置いた方がいいですが、この記事のコードはすぐ簡単に使えるようにこのままこれを使います。

又、npmやyarnを使う場合、[@techstark/opencv-js](https://www.npmjs.com/package/@techstark/opencv-js)というパッケージがあって、これでインストールできます。TypeScriptで使うこともできます。
```
npm i @techstark/opencv-js
```


## opencv.jsスクリプトを読み込み

ではまず試してみます。このようにコードを書いてブラウザで開けます。
```html:index.html
<script src="https://docs.opencv.org/4.10.0/opencv.js"></script>
<script>
console.log(cv);
</script>
```

そしてコンソールにこのように表示されたら読み込み成功でしょう。
```javascript
Object { HEAP8: Int8Array(134217728), HEAP16: Int16Array(67108864), HEAP32: Int32Array(33554432), HEAPU8: Uint8Array(134217728), HEAPU16: Uint16Array(67108864), HEAPU32: Uint32Array(33554432), HEAPF32: Float32Array(33554432), HEAPF64: Float64Array(16777216), preloadedImages: {}, preloadedAudios: {}, … }
```

ただし普段読み込んですぐその中の機能が呼び出せるようになるわけではないので、その時点でまだ使えません。

全部の機能の準備が整って使えるようになったらすぐ実行するコードがあれば、このように`onRuntimeInitialized`関数として定義するという方法もあります。
```html
<script async src="https://docs.opencv.org/4.10.0/opencv.js"></script>
<script>
let Module = {
  onRuntimeInitialized() {
    let mat = new cv.Mat(); // 行列オブジェクトを作成
    console.log(mat.size()); // サイズを出力してみる
  },
};
</script>
```

コンソールでの出力。
```java
Object { width: 0, height: 0 }
```

`cv.Mat`はopencv.jsにおいて行列として画像データを扱うためのオブジェクトです。`.size()`は行列のサイズを返すメソッドですが、ここでただの空っぽな行列を作ったので0、0になります。

`onRuntimeInitialized`に関してはemscriptenの仕様です。詳しく説明は割愛します。

これがわかりにくいので、今回の記事ではこのように書くことにします。
```html
<button onclick="jikkou()">実行</button>
<script async src="https://docs.opencv.org/4.10.0/opencv.js"></script>
<script>
function jikkou() {
  let mat = new cv.Mat();
  console.log(mat.size());
}
</script>
```

単に関数を作って、ボタンを押して起動するという形です。

![jikkou_botan.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/8e55aebf-a06d-2ea0-e4ff-bd6799f49997.png)


## CORS問題の対策のためサーバーを立てる

ブラウザ上で画像処理をするためにはもう一つ課題があります。これはopencv.jsだけの問題ではないのですが、普段画像データを読み込む時によく[CORS](https://qiita.com/Hirohana/items/9b5501c561954ad32be7)（Cross-origin resource sharing）問題にぶつかります。他のウェブサイトからデータを読み込む時だけでなく、たとえ自分のパソコンにあるデータを読み込もうとする場合も駄目です。面倒ですが、これはセキュリティのためですね。

だから画像を読み込むためには、自分でサーバーを立てて画像もウェブページもその中に置く必要があります。サーバーで起動したウェブページならそのサーバーに置いた画像などのファイルを読み込むことができます。

方法サーバーを簡単に立てる方法は色々ありますが、私がいつも使っているのはFastAPIです。他の方法を使っている人やPythonを使っていない人は他の方法を使ってもいいです。ここでは一応FastAPIで使う方法を手短に説明します。

まずライブラリーのインストールです。
```python
pip install fastapi uvicorn
```

そして、あるフォルダを作って、その中で例えばこのようにファイルを入れます。
```
web.py
index.html
gazou.jpg
```

`gazou.jpg`は処理したい画像ファイルで、`index.html`は今回opencv.jsコードを書くウェブページです。

そして`web.py`はサーバーを起動するPythonコードで、こう書きます。
```python:web.py
from fastapi import FastAPI,staticfiles

app = FastAPI()
app.mount('/',staticfiles.StaticFiles(directory='.',html=True))
```

そしてコマンドラインでこれを実行したらすぐ使えます。
```
uvicorn web:app
```

これで`index.html`で書いたJavaScriptのコードは`gazou.jpg`や他にこのフォルダに置いたファイルを読み込むことができるようになります。


# 関わるオブジェクトの種類

ブラウザ上で画像を扱う時に関わる可能性があるデータやオブジェクトの種類は色々ありますので、ここでひとまず纏めておきます。詳しくは後で説明していきます。
<table>
<tr><td style="text-align: center"><strong>オブジェクト</strong></td><td style="text-align: center"><strong>JavaScriptクラス</strong></td></tr>
<tr><td>imgタグ</td><td>HTMLImageElement</td></tr>
<tr><td>canvasタグ</td><td>HTMLCanvasElement</td></tr>
<tr><td>canvas2Dコンテキスト</td><td>CanvasRenderingContext2D</td></tr>
<tr><td>canvas上の画像データ</td><td>ImageData</td></tr>
<tr><td>データURL</td><td>string</td></tr>
<tr><td><a href="https://ja.wikipedia.org/wiki/バイナリ・ラージ・オブジェクト">blob</a></td><td>Blob、File</td></tr>
<tr><td>opencv.jsの行列</td><td>cv.Mat</td></tr>
</table>

その中でcv.Mat以外はJavaScriptの既存のクラスで、opencv.jsを使わなくても普段使えます。


# 行列オブジェクトMatに読み込む方法

画像データは色んな形で現れますが、opencv.jsで画像データを扱う際まず行列として読み込む必要があります。だからここではまず既存の画像を行列に読み込む色んな方法について説明します。

## imgタグから画像読み込み

opencv.jsには`imread()`という関数があって、これを使ったら簡単にimgタグの中の画像を読み込めます。（ただし上述のCORS問題対策は忘れてはならない）

例えばウェブページの中に`gazou.jpg`という画像があって、隣に実行するためのボタンがある例です。
```html
<img id="img" src="gazou.jpg">
<button onclick="jikkou()">実行</button>
<script src="https://docs.opencv.org/4.10.0/opencv.js"></script>
<script>
function jikkou() {
  let imgelm = document.getElementById("img");
  let mat = cv.imread(imgelm);
  console.log(mat.size());
}
</script>
```
![gazou01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/e3d15d18-1c0f-572d-efc2-b925c079c145.jpeg)
（因みにこの画像は[FLUX.1](https://qiita.com/tags/flux.1)で生成したもの）

実行結果
```
Object { width: 150, height: 200 }
```

今回の例ではただ読み込めることを確認するためだけなので、まずサイズ（解像度）を測るだけでまだ特に何もしません。以下の例もそうします。


## 仮のimgタグで画像読み込み

予めウェブページに置いたimgタグではなく、その場で作ったimgタグも`imshow`で読み込むことができます。

ただし画像の読み込みは非同期処理で行われるので、書き方は複雑になりま。例えば`async`と`await`を使う方法です。
```html
<button onclick="jikkou()">実行</button>
<script src="https://docs.opencv.org/4.10.0/opencv.js"></script>
<script>
async function jikkou() {
  let imgelm = new Image();
  imgelm.src = "gazou.jpg";
  await imgelm.decode();
  let mat = cv.imread(imgelm);
  URL.revokeObjectURL(imgelm);
  console.log(mat.size());
}
</script>
```

ここで`.decode()﻿﻿`メソッドは画像をその場でimgタグに読み込むためです。`await`を使うことで読み込みが終わるまで待つことになります。そうしないと後で`imread()`を書いても画像を行列に読み込むことはできません。

行列に読み込まれた後imgタグに残る画像データはもう必要ないので、`URL.revokeObjectURL`で削除しておいた方がいいです。そうしなければずっとメモリーを占めたままで自動的に消えません。

もう一つは`.onload`メソッドを定義する方法です。`async`と`await`が必要ないですが、書き方は更に複雑に見えます。
```html
<button onclick="jikkou()">実行</button>
<script src="https://docs.opencv.org/4.10.0/opencv.js"></script>
<script>
function jikkou() {
  let imgelm = new Image();
  imgelm.onload = () => {
    let mat = cv.imread(imgelm);
    URL.revokeObjectURL(imgelm);
    console.log(mat.size());
  }
  imgelm.src = "gazou.jpg";
}
</script>
```

`.onload`は画像がimgタグに読み込み終わった後に発動する関数です。コードの順番が書いたのと違って可読性が低くなるので、`decode()`を使った方法の方がいいと思います。


## ファイルinputタグから画像読み込み

次はファイルを選択するためのinputタグを使って、選択されたファイルを読み込む方法です。この方法はCORS問題の対策が必要ないです。

inputタグによって読み込んだファイルは**blob**という形になるが、これを直接行列に読み込むことはできません。まずblobをimgタグにしてから又`imread`読み込みます。

blobをimgタグに入れるには**データURL**に変換する必要があります。そのためには`URL.createObjectURL`関数を使うという方法と、`FileReader`クラスを使うという方法があります。

違いに関しては。
> https://scrapbox.io/mrsekut-p/URL.createObjectURL()%E3%81%A8FileReader.readAsDataURL()

違いを見ると、`URL.createObjectURL`の方がいいと思いますが、ここではとりあえず両方の方法を載せます。

まずは`URL.createObjectURL`を使う方法。
```html
<input id="finput" type="file"><br><br>
<button onclick="jikkou()">実行</button>
<script src="https://docs.opencv.org/4.10.0/opencv.js"></script>
<script>
async function jikkou() {
  let file = document.getElementById("finput").files[0];
  let imgelm = new Image();
  imgelm.src = URL.createObjectURL(file);
  await imgelm.decode();
  let mat = cv.imread(imgelm);
  URL.revokeObjectURL(imgelm);
  console.log(mat.size());
}
</script>
```

![upload_botan.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/caee599d-5e70-0c3e-bafd-2de5198092d4.png)

`FileReader`を使う方法。
```html
<input id="finput" type="file"><br><br>
<button onclick="jikkou()">実行</button>
<script src="https://docs.opencv.org/4.10.0/opencv.js"></script>
<script>
function jikkou() {
  let reader = new FileReader();
  let file = document.getElementById("finput").files[0];
  reader.readAsDataURL(file);
  reader.onload = async () => {
    let imgelm = new Image();
    imgelm.src = reader.result;
    await imgelm.decode();
    let mat = cv.imread(imgelm);
    URL.revokeObjectURL(imgelm);
    console.log(mat.size());
  }
}
</script>
```

`FileReader`は非同期処理なので`onload`を使うのです。更にもし`imgelm`も`onload`を使ったらもっとわかりにくくなりそうです。


## canvasから画像読み込み

画像を表示する時にimgタグの他にcanvasタグもよく使われています。canvasタグの中で書かれた画像を直接行列に読み込むことはできなくて、まずimgタグ又はcanvasの中の2d画像コンテキスト`CanvasRenderingContext2D`を経由する必要があります。

### CanvasRenderingContext2D経由

canvasの中で画像は`CanvasRenderingContext2D`というクラスのオブジェクトの中で描かれるのです。`.getImageData`メソッドで画像データを取得してopencv.jsの`matFromImageData()`関数で読み込めます。

```html
<canvas id="canvas" width="100" height="200" style="background-color: #DD9;"></canvas>
<button onclick="jikkou()">実行</button>
<script src="https://docs.opencv.org/4.10.0/opencv.js"></script>
<script>
let canvaselm = document.getElementById("canvas");
let ctx = canvaselm.getContext("2d");
ctx.fillStyle = "#44F";
ctx.arc(0, 100, 100, -Math.PI/2, Math.PI/2);
ctx.fill();

function jikkou() {
  let imgdata = ctx.getImageData(0, 0, canvaselm.width, canvaselm.height); 
  let mat = cv.matFromImageData(imgdata);
  console.log(mat.size()); // Object { width: 100, height: 200 }
  console.log(imgdata); // ImageData { width: 100, height: 200, data: Uint8ClampedArray(80000) }
  console.log(imgdata.data); // Uint8ClampedArray(80000) [ 68, 68, 255, 223, 68, 68, 255, 223, 67, 67, … ]
  console.log(mat.data); // Uint8Array(80000) [ 68, 68, 255, 223, 68, 68, 255, 223, 67, 67, … ]
}
</script>
```
![gazou02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/6f0f6b7a-4fa2-df33-bcf9-9b63a9c3be32.png)


`.getImageData`で取得する画像の範囲を指定する必要があるので書き方は面倒ですが、もしただ一部だけ欲しい場合は簡単にできて便利かもしれません。

因みに`.getImageData`の返り値は`ImageData`というクラスのオブジェクトで、その中で`Uint8ClampedArray`というクラスの配列で画像データを格納しています。これはopencv.jsで主に扱う`Uint8Array`と大体同じで、自動的に変換されます。


### imgタグ経由

blobの場合と同じように、imgタグになったら簡単に`imshow()`関数で読み込むことができます。canvasには`.toDataURL()`というメソッドがあります。これを使ってcanvasの中の画像をデータURLにしてimgタグに入れることができます。
```html
<canvas id="canvas" width="100" height="200" style="background-color: #DD9;"></canvas>
<button onclick="jikkou()">実行</button>
<script src="https://docs.opencv.org/4.10.0/opencv.js"></script>
<script>
let canvaselm = document.getElementById("canvas");
let ctx = canvaselm.getContext("2d");
ctx.fillStyle = "#44F";
ctx.arc(0, 100, 100, -Math.PI/2, Math.PI/2);
ctx.fill();

async function jikkou() {
  let imgelm = new Image();
  imgelm.src = canvaselm.toDataURL();
  await imgelm.decode();
  let mat = cv.imread(imgelm);
  URL.revokeObjectURL(imgelm);
  console.log(mat.size());
}
</script>
```


## 外から取得した画像データを読み込み

`fetch`やaxiosなどでAPIや外部のウェブサイトから画像データを取得する時も、普段inputタグと同じようにblobという形なので、`URL.createObjectURL`でimgタグに画像を入れたら読み込むことになります。

今回は例普通にgetメソッドで同じサイトの静的ファイルょ読み込むこと例を挙げます。こんな場合は実は直接imgタグを使ってもいいのですが、postを使う時など条件によって返ってくるデータが違う場合はimgタグだけで対応できません。

`async`と`await`を使う例。
```html
<button onclick="jikkou()">実行</button>
<script async src="https://docs.opencv.org/4.10.0/opencv.js"></script>
<script>
async function jikkou() {
  let blob = await (await fetch("/gazou.jpg")).blob();
  let imgelm = new Image();
  imgelm.src = URL.createObjectURL(blob);
  await imgelm.decode();
  let mat = cv.imread(imgelm);
  console.log(mat.size());
}
</script>
```

`onload`と`then`を使う例。
```html
<button onclick="jikkou()">実行</button>
<script async src="https://docs.opencv.org/4.10.0/opencv.js"></script>
<script>
function jikkou() {
  let imgelm = new Image();
  imgelm.onload = () => {
    let mat = cv.imread(imgelm);
    console.log(mat.size());
    URL.revokeObjectURL(imgelm);
  }
  fetch("/gazou.jpg")
    .then((res) => res.blob())
    .then((blob) => {
      imgelm.src = URL.createObjectURL(blob);
    });
}
</script>
```



# opencvの行列オブジェクトMatから出力

opencv.jsの中で行列という形で画像データを処理した後、表示や保存のために次は行列から変換し返す必要があります。

## Matからcanvasへ

行列からの出力は、一番基本的なのは`imshow()`関数を使うという方法です。これを使ってcanvasタグで画像表示できます。

例えばimgタグの中の画像を読み込んで何等かの処理をしてcanvasに表示するとします。
```html
<img id="img" src="gazou.jpg">
<button onclick="jikkou()">実行</button>
<canvas id="canvas"></canvas>
<script src="https://docs.opencv.org/4.10.0/opencv.js"></script>
<script>
function jikkou() {
  let mat = cv.imread(document.getElementById("img"));
  cv.convertScaleAbs(mat, mat, 0.7); // 何等かの処理
  cv.imshow("canvas", mat);
  mat.delete();
}
</script>
```

![gazou03.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/c601007d-4937-21c6-e223-65a6b7adbf9e.jpeg)

ここではボタンの左側は元画像のimgタグでボタンの右側は編集した後のcanvasです。

又、普段出力に使ったMatはもう使わないなら`.delete()`メソッドを使ってメモリーから削除する必要があります。そうしないと、自動的に消されるわけでなく、ページが閉じるまでメモリーの中に残るからです。

第1引数のcanvasの指定はcanvasのIDを入れることもできますが、その他にもそのcanvasのオブジェクトを入れることもできます。

上の例をこのように書き換えられます。
```html
<img id="img" src="gazou.jpg">
<button onclick="jikkou()">実行</button>
<canvas id="canvas"></canvas>
<script src="https://docs.opencv.org/4.10.0/opencv.js"></script>
<script>
function jikkou() {
  let mat = cv.imread(document.getElementById("img"));
  cv.convertScaleAbs(mat, mat, 0.7); // 何等かの処理
  let canvaselm = document.getElementById("canvas");
  cv.imshow(canvaselm, mat);
  mat.delete();
}
</script>
```

## Matからimgへ

`imshow()`関数はcanvasだけに使えて、imgタグに使えるわけではないので、imgタグに画像を入れたい場合は一旦canvasにしてから又変換するという過程になります。

使う例。
```html
<img id="img" src="gazou.jpg">
<button onclick="jikkou()">実行</button>
<img id="img2">
<script src="https://docs.opencv.org/4.10.0/opencv.js"></script>
<script>
function jikkou() {
  let mat = cv.imread(document.getElementById("img"));
  let canvaselm = document.createElement("canvas");
  cv.convertScaleAbs(mat, mat, 0.7);
  cv.imshow(canvaselm, mat);
  mat.delete();
  let imgelm2 = document.getElementById("img2");
  imgelm2.src = canvaselm.toDataURL();
}
</script>
```

上の例と同じ見た目の結果ですが、今回出力はcanvasではなく、もう一つのimgタグにします。結果も見た目は完全に同じです。


## Matからblobへ。そしてファイル保存

Matから直接blobに出力する方法はなく、まず一旦canvasにする必要があります。canvasには`.toBlob`メソッドを持って、これわ使ってblobが作れます。blobになったファイルとして送ったり、保存したりすることができます。

imgタグから読み込んで処理したMatをblobにして出力して保存する例です。
```html
<img id="img" src="gazou.jpg">
<button onclick="jikkou()">実行</button>
<script src="https://docs.opencv.org/4.10.0/opencv.js"></script>
<script>
function jikkou() {
  let imgelm = document.getElementById("img");
  let mat = cv.imread(imgelm);
  let canvaselm = document.createElement("canvas");
  cv.convertScaleAbs(mat, mat, 0.7);
  cv.imshow(canvaselm, mat);
  mat.delete();
  
  canvaselm.toBlob((blob) => {
    let a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    a.download = "gazou.jpg";
    a.click();
    URL.revokeObjectURL(a.href);
    canvaselm.remove();
  }, "image/jpeg");
}
</script>
```

尚、出力画像ファイルのタイプを指定することができます。ここでは`"image/jpeg"`にしていますが、指定しない場合の既定値はpngとなります。


# データ型とチャンネル数

今までの説明ではデータの種類について触れていませんが、実はopencv.jsでは色んなデータ種類が使われています。今はこれについて説明します。

opencv.jsの画像データは主に2つの分け方で分けられています。
- データ型
- チャンネル数

データ型は7つあります。以下の表で表します。
<table>
<tr><td style="text-align: center"><strong>変数</strong></td><td style="text-align: center"><strong>定数</strong></td><td style="text-align: center"><strong>意味</strong></td><td style="text-align: center"><strong>JavaScriptのデータ型</strong></td></tr>
<tr><td>CV_8U</td><td>0</td><td>8ビット符号なし整数</td><td>Uint8Array</td></tr>
<tr><td>CV_8S</td><td>1</td><td>8ビット符号あり整数</td><td>Int8Array</td></tr>
<tr><td>CV_16U</td><td>2</td><td>16ビット符号なし整数</td><td>Uint16Array</td></tr>
<tr><td>CV_16S</td><td>3</td><td>16ビット符号あり整数</td><td>Int16Array</td></tr>
<tr><td>CV_32S</td><td>4</td><td>32ビット符号あり整数</td><td>Int32Array</td></tr>
<tr><td>CV_32F</td><td>5</td><td>32ビット浮動小数点数</td><td>Float32Array</td></tr>
<tr><td>CV_64F</td><td>6</td><td>64ビット浮動小数点数</td><td>Float64Array</td></tr>
</table>

ここに書いた変数と定数は`.convertTo()`などデータ型を変えるための関数やメソッドに使われます。例えば「32ビット浮動小数点数」に変換したい時`cv.CV_32F`を入れても`5`を入れても同じになります。

チャンネル数は1から4です。1は普段グレースケール、3はRGB3色で、4はアルファも加えるRGBAです。

`matFromArray()`関数などで行列を作る時にデータ型とチャンネル数の組み合わせで種類を指定する必要があります。その組み合わせは7×4=28あります。

このように表で表します。
<table>
<tr><td style="text-align: center"><strong>変数</strong></td><td style="text-align: center"><strong>定数</strong></td><td style="text-align: center"><strong>データ型</strong></td><td style="text-align: center"><strong>チャンネル数</strong></td></tr>
<tr><td>CV_8UC1</td><td>0</td><td>8ビット符号なし整数</td><td rowspan="7" style="text-align: center">1</td></tr>
<tr><td>CV_8SC1</td><td>1</td><td>8ビット符号あり整数</td></tr>
<tr><td>CV_16UC1</td><td>2</td><td>16ビット符号なし整数</td></tr>
<tr><td>CV_16SC1</td><td>3</td><td>16ビット符号あり整数</td></tr>
<tr><td>CV_32SC1</td><td>4</td><td>32ビット符号あり整数</td></tr>
<tr><td>CV_32FC1</td><td>5</td><td>32ビット浮動小数点数</td></tr>
<tr><td>CV_64FC1</td><td>6</td><td>64ビット浮動小数点数</td></tr>
<tr><td>CV_8UC2</td><td>8</td><td>8ビット符号なし整数</td><td rowspan="7" style="text-align: center">2</td></tr>
<tr><td>CV_8SC2</td><td>9</td><td>8ビット符号あり整数</td></tr>
<tr><td>CV_16UC2</td><td>10</td><td>16ビット符号なし整数</td></tr>
<tr><td>CV_16SC2</td><td>11</td><td>16ビット符号あり整数</td></tr>
<tr><td>CV_32SC2</td><td>12</td><td>32ビット符号あり整数</td></tr>
<tr><td>CV_32FC2</td><td>13</td><td>32ビット浮動小数点数</td></tr>
<tr><td>CV_64FC2</td><td>14</td><td>64ビット浮動小数点数</td></tr>
<tr><td>CV_8UC3</td><td>16</td><td>8ビット符号なし整数</td><td rowspan="7" style="text-align: center">3</td></tr>
<tr><td>CV_8SC3</td><td>17</td><td>8ビット符号あり整数</td></tr>
<tr><td>CV_16UC3</td><td>18</td><td>16ビット符号なし整数</td></tr>
<tr><td>CV_16SC3</td><td>19</td><td>16ビット符号あり整数</td></tr>
<tr><td>CV_32SC3</td><td>20</td><td>32ビット符号あり整数</td></tr>
<tr><td>CV_32FC3</td><td>21</td><td>32ビット浮動小数点数</td></tr>
<tr><td>CV_64FC3</td><td>22</td><td>64ビット浮動小数点数</td></tr>
<tr><td>CV_8UC4</td><td>24</td><td>8ビット符号なし整数</td><td rowspan="7" style="text-align: center">4</td></tr>
<tr><td>CV_8SC4</td><td>25</td><td>8ビット符号あり整数</td></tr>
<tr><td>CV_16UC4</td><td>26</td><td>16ビット符号なし整数</td></tr>
<tr><td>CV_16SC4</td><td>27</td><td>16ビット符号あり整数</td></tr>
<tr><td>CV_32SC4</td><td>28</td><td>32ビット符号あり整数</td></tr>
<tr><td>CV_32FC4</td><td>29</td><td>32ビット浮動小数点数</td></tr>
<tr><td>CV_64FC4</td><td>30</td><td>64ビット浮動小数点数</td></tr>
</table>

こんなにも沢山ありますが、普段いつも使われるのは主に`CV_8U`（8ビット符号なし整数）系です。その他に`CV_32FC2`（32ビット浮動小数点数）もよく使われています。

`imread()`関数で読み込んだ場合は`CV_8UC4`、つまり「4チャンネル8ビット符号なし整数」となります。たとえ無色の画像を読み込んでもそうなります。

Matの画像データの種類は`.type()`メソッドで調べられます。返してくるのはただの数値でこれだけ見てもわかりにくいので、上の表を参考にしてください。

色空間を変換する`cvtColor()`関数を使ったらチャンネル数は変わりますが、データ型はそのままです。
```html
<img id="img" src="gazou.jpg">
<button onclick="jikkou()">実行</button>
<script src="https://docs.opencv.org/4.10.0/opencv.js"></script>
<script>
function jikkou() {
  let imgelm = document.getElementById("img");
  let mat = cv.imread(imgelm);
  console.log(mat.type()); // 24
  console.log(mat.data); // Uint8Array(120000) [ 49, 75, 76, 255, 69, 95, 92, 255, 85, 108, … ]
  cv.cvtColor(mat, mat, cv.COLOR_RGBA2RGB);
  console.log(mat.type()); // 16
  console.log(mat.data); // Uint8Array(90000) [ 49, 75, 76, 69, 95, 92, 85, 108, 100, 44, … ]
  cv.cvtColor(mat, mat, cv.COLOR_RGB2GRAY);
  console.log(mat.type()); // 0
  console.log(mat.data); // Uint8Array(30000) [ 67, 87, 100, 57, 37, 54, 45, 47, 42, 59, … ]
}
</script>
```

`.data`の中で中身を見る時にチャンネル数によってサイズが変わりますね。サイズは「縦×横×チャンネル数」だから。

尚`cvtColor()`に関しては使い方は殆どPythonの時と同じなので説明は割愛します。

データ型の変換はMatオブジェクトにある`.convertTo()`メソッドでできます。ただし色の表現は整数の場合と浮動小数点数の場合とは違うので注意です。整数では0~255で、浮動小数点数では0~1です。変換する時この値の範囲が自動的に計算されるわけではないが、`.convertTo()`メソッドではデータ型変換と同時にスケールを指定することもできます。

例えば`CV_8UC`から`CV_32FC4`に変換する時に`1/255`を入れます。

使う例。
```html
<img id="img" src="gazou.jpg">
<button onclick="jikkou()">実行</button>
<canvas id="canvas"></canvas>
<script src="https://docs.opencv.org/4.10.0/opencv.js"></script>
<script>
function jikkou() {
  let imgelm = document.getElementById("img");
  let mat = cv.imread(imgelm);
  console.log(mat.type()); // 24
  console.log(mat.data); // Uint8Array(120000) [ 49, 75, 76, 255, 69, 95, 92, 255, 85, 108, … ]
  mat.convertTo(mat, cv.CV_32F, 1 / 255);
  console.log(mat.type()); // 29
  console.log(mat.data); // Uint8Array(480000) [ 198, 196, 68, 62, 151, 150, 150, 62, 153, 152, … ]
  mat.convertTo(mat, cv.CV_64F);
  console.log(mat.type()); // 30
  console.log(mat.data); // Uint8Array(960000) [ 0, 0, 0, 192, 152, 152, 200, 63, 0, 0, … ]
  cv.imshow("canvas", mat);
}
</script>
```

Matで扱った時のデータ型が変わってもcanvasに出力したら同じです。この例では変換した後処理せずに出力したので、左側のimgタグの元画像と、右側の出力canvasは全く同じですね。でももし変換の時にスケールを`1/255`に指定していなければ真っ白になるでしょう。

![gazou04.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202903/826a2662-e014-a75f-139b-d711705a9fa8.jpeg)

ただ、`.data`で中身を見たらどれも`Uint8Array`で、変わったのは配列のサイズです。中で全てUint8から合成するという仕組みですね。`CV_32FC4`では4つの「8ビット符号なし整数」が1つの「32ビット浮動小数点数」として解釈されるのです。

例えば`matFromArray`関数で配列から行列を作成してみます。
```html
<button onclick="jikkou()">実行</button>
<script src="https://docs.opencv.org/4.10.0/opencv.js"></script>
<script>
function jikkou() {
  let arr = [205, 49, 71, 74, 116, 126, 161, 95, 216, 88, 44, 178, 198, 17, 90, 195, 104, 66];
  let mat = cv.matFromArray(2, 3, cv.CV_8UC3, arr);
  console.log(mat.data); // Uint8Array(18) [ 205, 49, 71, 74, 116, 126, 161, 95, 216, 88, … ]
  console.log(mat.size()); // Object { width: 3, height: 2 }
  mat = cv.matFromArray(2, 3, cv.CV_32FC3, arr);
  console.log(mat.data); // Uint8Array(72) [ 0, 0, 77, 67, 0, 0, 68, 66, 0, 0, … ]
  console.log(mat.size()); // Object { width: 3, height: 2 }
}
</script>
```

中身のデータは`CV_8UC3`の場合は入れたままの数値ですが、`CV_32FC3`では全然違う4つの数値に変換されます。


# 終わりに

以上opencv.jsの入門です。とはいっても、まだデータの入力、出力、変換やデータ型のことしか説明していなくて、肝心の画像処理は殆ど触れていません。

でもopencvによる画像処理は既に沢山記事が書いてあります。ただ殆どはPythonとC++なだけ。JavaScript特有の入力、出力、変換がわかればPythonのopencvの資料を読んでもopencv.jsで行うためにどうやって書き換えるかわかるでしょう。


# 参考

### JavaScriptにおけるファイルとデータ処理
- [blobとは（フロントエンド編）](https://qiita.com/kouta222/items/0469ce8b7f3e0807349d)
- [Image.onloadはやめてImage.decodeを使おうね](https://qiita.com/generosennin/items/ac7797b2af1baa97886e)
- [Blobとは何か理解していない方へ](https://zenn.dev/kazu1/articles/736991963449d6)
- [Blob と File と TypeScript と私](https://qiita.com/yokra9/items/a629f416caf044d4a5db)
- [Canvas2Dについてさっくり復習してみた](https://qiita.com/ryokio0129/items/33f2d8cba1d4c09fb9d0)
- [JavaScriptで生成した画像配列をHTMLのimgタグで表示する](https://qiita.com/aa_debdeb/items/38da48a48bec5efbc1a9)
- [WebAssemblyの使用方法と初期化、エラーハンドリング](https://medium.com/axip/webassemblyの使用方法と初期化-エラーハンドリング-5b0a8c3968df)
- [JavaScriptで画像データ（ImageData）を使ってみる](https://blog.codecamp.jp/javascript-image-data)

### opencv.js
- [OpenCV.jsをサクッと試す](https://qiita.com/puku0x/items/8b71efa61fce72e22188)
- [OpenCV.jsで全ての画像に白黒つけてやる](https://qiita.com/MasaoBlue/items/1c83c2a57455e4808857)
- [OpenCV.jsをChrome拡張で動かしてみた](https://qiita.com/Freedom645/items/74dbff34c3ffd8d3a9fd)
- [DenoでOpenCVを型情報ありで使ってみる](https://qiita.com/masaminh/items/20ace39a5a6da827638c)
- [【OpenCV.js】ななめに写った文書をピッタリさせる](https://blog.capilano-fw.com/?p=8924)
- [opencv.jsの作法](https://qiita.com/ohisama@github/items/bf143e567ea72d03ab88)
- [openCV.js を使ってスマホ上で画像処理をしてみた](https://qiita.com/pythonista/items/2b30c1beb99eea93f496)
- [バーコード画像を解読するWebアプリケーション](https://qiita.com/stoyosawa/items/e2173907809d409bf74c)
- [OpenCV.js のGaussianBlurが遅すぎたのでWebAssemblyで作ってみた](https://qiita.com/OECkoec/items/796bb06302c53391f172)

- [OpenCV.jsのビルドとその軽量化](https://qiita.com/YuukiToriyama/items/747df5721536373be76a)
- [OpenCV.js をちょっとだけ試してみた。](https://qiita.com/hon_no_mushi/items/0edb47b46f827f9fca26)
- [WebAssembly版OpenCV（OpenCV.js）について](https://qiita.com/yoshimi0227/items/932102201299c418a2d3)