OpenCVとTypeScriptの組み合わせで快適に開発するためにJupyter Notebookを使う
この記事は、 Luup Advent Calendar の24日目の記事です。
こんにちは、フリーランスでソフトウェアエンジニアをしているfuji44です。
最近は、LuupのServerチームでバックエンドエンジニアをしています。
今回は、OpenCVとTypeScriptの組み合わせで快適に開発するためにJupyter Notebookを使う方法についてまとめてみました。
はじめに
画像解析する際にOpenCVとJupyter Notebookを使い、ビジュアライズしながら試行するのは非常に便利です。
この組み合わせではPythonでコーディングすることが多いと思います。OpenCVだけで検索しても出てくるのはPythonでコーディングされたものがほとんどです。
Luupではバックエンドの主だったコードはTypeScriptで記述されているため、TypeScriptで同じことがしたい!ということでやってみました。
応用で、OCRもやります。
ここで書いたコードは、こちらで公開しています。
前提
作業環境は以下のとおりです。
- Debian 11 (bullseye)
- Node.js 20.3.1
- npm 9.6.7
- Python 3.9.2
- jupyter core 4.7.1
- jupyter-notebook 6.2.0
Jupyter NotebookでTypeScriptを実行する
Jupyter Notebookは使用するカーネルを切り替えることで様々な言語を使って記述できるようになっています。Jupyterのアーキテクチャについて興味のある方は以下のページをご覧ください。
つまり、TypeScript用のカーネルを使えばよいということで、探してみるといくつかあるみたいです。今回はtslabを使ってみようと思います。
まず、tslabをインストールします。
$ npm install -g tslab
....
$ tslab install
Running python3 /usr/local/share/npm-global/lib/node_modules/tslab/python/install.py --tslab=tslab
Installing TypeScript kernel spec
Installing JavaScript kernel spec
$ tslab --version
tslab 1.0.21
Jupyterのカーネルとしてtslabが認識されていることを確認します。
$ jupyter kernelspec list
Available kernels:
jslab /home/node/.local/share/jupyter/kernels/jslab
tslab /home/node/.local/share/jupyter/kernels/tslab
python3 /usr/share/jupyter/kernels/python3
次に、Notebook サーバーを起動します。
$ jupyter notebook
[I 01:46:45.914 NotebookApp] Serving notebooks from local directory: /workspaces/sample-node-typescript-opencv
[I 01:46:45.914 NotebookApp] Jupyter Notebook 6.2.0 is running at:
[I 01:46:45.914 NotebookApp] http://localhost:8888/?token=7f61e3425b775165bd12ac3e9f9e5908b038f2402f5c026e
[I 01:46:45.914 NotebookApp] or http://127.0.0.1:8888/?token=7f61e3425b775165bd12ac3e9f9e5908b038f2402f5c026e
[I 01:46:45.914 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
[C 01:46:45.921 NotebookApp]
To access the notebook, open this file in a browser:
file:///home/node/.local/share/jupyter/runtime/nbserver-10364-open.html
Or copy and paste one of these URLs:
http://localhost:8888/?token=7f61e3425b775165bd12ac3e9f9e5908b038f2402f5c026e
or http://127.0.0.1:8888/?token=7f61e3425b775165bd12ac3e9f9e5908b038f2402f5c026e
起動が出来たらコンソールに表示されたURLをブラウザでアクセスします。
TypeScriptのnotebookを作成して実行できることを確認します。
実行できますね。
OpenCVで処理した画像を表示する
次は、OpenCVを使って処理した画像を出力してみます。
まず、必要なnpm packageをインストール。
npm i @techstark/opencv-js jimp tslab
OpenCVには公式のJavaScriptバインディングであるOpenCV.jsがあります。ただ、npm packageは提供されていないので、サードパーティーのものを利用します。今回は、TypeScriptサポートもしていてメンテナンスされていそうな@techstark/opencv-jsを使用します。
OpenCV.jsだけでは画像ファイルを読み込むことができないため、Jimpを合わせて使用します。
Notebook上で画像を出力するために、tslabのnpm packageもインストールします。
これで、準備ができたのでコードを書いていきます。
まずは、インポート。
import { display } from "tslab"
import Jimp from "jimp"
import cv from "@techstark/opencv-js"
次に、画像表示用のユーティリティー関数。
function toImage(imageMat: cv.Mat): Jimp {
return new Jimp({
width: imageMat.cols,
height: imageMat.rows,
data: Buffer.from(imageMat.data)
})
}
async function print(imageMat: cv.Mat) {
const invoke = async () => {
const outputImage = toImage(imageMat)
display.png((await outputImage.getBufferAsync(outputImage.getMIME())))
}
if (imageMat.type() === cv.CV_8UC1) {
cv.cvtColor(imageMat, imageMat, cv.COLOR_GRAY2RGBA, 0)
await invoke()
cv.cvtColor(imageMat, imageMat, cv.COLOR_RGBA2GRAY, 0)
return
}
await invoke()
}
最後に、画像ファイルを読み取ってそのままの画像とグレースケールした画像を表示します。
const image = await Jimp.read("../images/apple.jpg")
const imageMat = cv.matFromImageData(image.bitmap)
await print(imageMat)
const workImage = new cv.Mat()
cv.cvtColor(imageMat, workImage, cv.COLOR_RGBA2GRAY, 0)
await print(workImage)
記述が出来たら、ステップを順番に実行。
これで、OpenCV+TypeScriptでもビジュアライズしながら試行できるようになりました!
応用: 表の画像から文字を抽出する
主題としてはこれで終わりなのですがこれだけだと面白くないので、表の画像からOCRでセルの値を読み取ってみます。
OCRにはtesseract.jsを使用します。
npm i tesseract.js
インポートを追加します。
import { display } from "tslab"
import Jimp from "jimp"
import cv from "@techstark/opencv-js"
+import { createWorker } from "tesseract.js"
読み込む画像ファイルを変更して表示してみます。
画像は、うどん県統計情報コーナーのExcelファイルを変換して作成しました。
-const image = await Jimp.read("../images/apple.jpg")
+const image = await Jimp.read("../images/udonjouhou_data1-1.png")
const imageMat = cv.matFromImageData(image.bitmap)
await print(imageMat)
-
-const workImage = new cv.Mat()
-cv.cvtColor(imageMat, workImage, cv.COLOR_RGBA2GRAY, 0)
-await print(workImage)
ここから画像解析処理です。画像解析処理については、話の本筋から外れるため、詳細な解説は省略させていただきます。ご理解いただければ幸いです。
まずは、前処理を行います。はじめはグレースケール。
const workImage = imageMat.clone()
// grayscale
cv.cvtColor(workImage, workImage, cv.COLOR_RGBA2GRAY, 0)
await print(workImage)
次に、二値化。
// threshold
const thresh = 220
cv.threshold(
workImage,
workImage,
thresh,
255,
cv.THRESH_BINARY
)
await print(workImage)
スキャナーで取り込んだ画像のようにノイズがあるわけではないので、前処理はこのくらいにします。
次に、表のセルの座標を取得するために、輪郭を検出します。
// Contour detection
const contours = new cv.MatVector()
const hierarchy = new cv.Mat()
cv.findContours(
workImage,
contours,
hierarchy,
cv.RETR_TREE,
cv.CHAIN_APPROX_SIMPLE
)
const overrideImageMat = imageMat.clone()
const rects: cv.Rect[] = []
for (let i = 0; i < contours.size(); i++) {
// 輪郭の面積をもとに、セルらしい面積のものだけを処理する
// 実務では、もう少し条件を考えたほうが良い
const area = cv.contourArea(contours.get(i))
if (area < 1000 || area > 35000) {
continue
}
const rect = cv.boundingRect(contours.get(i))
rects.push(rect)
const color = new cv.Scalar(
Math.random() * 255,
Math.random() * 255,
Math.random() * 255,
255
)
cv.drawContours(overrideImageMat, contours, i, color, 3)
}
await print(overrideImageMat)
検出したセルの座標情報をもとにOCRを実行していきます。
const worker = await createWorker("jpn")
try {
const ocrData: { text: string, image: cv.Mat }[] = []
for (const rect of rects) {
const cellImageMat = imageMat.roi(rect).clone()
const cellImage = toImage(cellImageMat)
const result = await worker.recognize(await cellImage.getBufferAsync(cellImage.getMIME()))
ocrData.push({
text: result.data.text,
image: cellImageMat
})
}
for (let i = 0; i < 5; i++) {
const ocrDatum = ocrData[i]
await print(ocrDatum.image)
console.log(ocrDatum.text)
}
} finally {
worker.terminate()
}
無事、文字を抽出することが出来ました。
おわりに
以上、OpenCVとTypeScriptの組み合わせで快適に開発するためにJupyter Notebookを使う方法についてでした。
PythonでOpenCVを使う場合は、何も考えずにNotebookで検証して実際のコードに落とし込むという作業をしていました。はじめはNotebookでTypeScriptを実行できないと思っていたのでNotebookなしで開発していたのですがなかなかつらかったです。試行結果の確認が即座にできるのはやっぱりありがたいのだなぁとしみじみと感じました。
OpenCV.jsについての記事自体も少ないので、ここで書いた内容が誰かの役に立てばうれしく思います。
Discussion