はじめに
「ブラウザで何か撮影をする際にフロントサイドのみで画像を加工・解析をしたい」といった需要が一部あると思いますが、公開されている技術記事はあまりないと感じています。
そのため、前回ReactとOpenCV.jsで画像の加工をしてみようを投稿しました。
今回はその続きとして、書類などの四角形の被写体が撮影された際の枠認識を行います。
前提
前回のReactとOpenCV.jsで画像の加工をしてみようの記事で作成したデモページに機能追加する形で機能を紹介します。
デモページのソースを改めて貼ります。
デモページのコード
import React, { useState } from 'react';
import './App.css'
const App: React.FC = () => {
const [selectedImage, setSelectedImage] = useState<File | null>(null);
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
console.log(selectedImage);
const handleImageChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
setSelectedImage(file);
// 画像のプレビューURLを作成
const imageUrl = URL.createObjectURL(file);
setPreviewUrl(imageUrl);
}
};
// 画像を白黒変換する
const convertBlackAndWhite = () => {
// OpenCV準備
const win = window;
const cv = win.cv;
// 画像の準備
const img = new Image();
if (previewUrl) {
img.src = previewUrl;
}
const src = cv.imread(img); // OpenCV読み込み
const dst = new cv.Mat(); // 画像の情報を格納する多次元配列「Mat」の準備
cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY); // 画像を白黒に変換
cv.imshow('cvCanvas', dst); // Canvasに表示
// メモリを明示的に開放する
src.delete();
dst.delete();
};
return (
<div style={{ textAlign: 'center', marginTop: '20px' }}>
<h1>画像アップロード</h1>
<input
type="file"
accept="image/*"
onChange={handleImageChange}
style={{ marginBottom: '20px' }}
/>
{previewUrl && (
<div style={{ display: 'flex' }}>
<div>
<img
src={previewUrl}
alt="Uploaded Preview"
style={{ maxWidth: '100%', maxHeight: '300px', margin: '20px 5px' }}
/>
</div>
<div>
<canvas
id="cvCanvas"
style={{ maxWidth: '100%', maxHeight: '300px', margin: '20px 5px' }}
/>
<br />
<button onClick={convertBlackAndWhite}>
白黒変換
</button>
</div>
</div>
)}
{!previewUrl && <p>画像を選択してください。</p>}
</div>
);
};
export default App;
枠認識用のUI追加
枠認識を実行するボタンを追加します。
// 画像に枠認識を実行し可視化する
const recognizeBorder = () => {
// OpenCV準備
const win = window;
const cv = win.cv;
// 画像の準備
const img = new Image();
if (previewUrl) {
img.src = previewUrl;
}
const src = cv.imread(img); // OpenCV読み込み
cv.imshow('cvCanvas', src); // Canvasに表示
// メモリを明示的に開放する
src.delete();
};
// 中略
<button
onClick={convertBlackAndWhite}
style={{ marginRight: '5px' }}
>
白黒変換
</button>
<button onClick={recognizeBorder }>枠認識</button>
被写体の枠を取得し、画像に描画する
recognizeBorder()に枠認識・描画するロジックを追加します。
// 画像の被写体の枠を認識する
const correctTrapezoid = () => {
// OpenCV準備
const win = window as any; // eslint-disable-line
const cv = win.cv;
// 画像の準備
const img = new Image();
if (previewUrl) {
img.src = previewUrl;
}
const src = cv.imread(img); // OpenCV読み込み
const dst = new cv.Mat(); // 画像の情報を格納する多次元配列「Mat」の準備
const hierarchy = new cv.Mat();
const contours = new cv.MatVector(); // Matをさらに格納する配列
cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY); // 画像を白黒に変換
cv.threshold(dst, dst, 10, 255, cv.THRESH_OTSU); // しきい値処理により画像を二値化
// 輪郭検出
cv.findContours(
dst, // 検出対象の画像
contours, // 輪郭の座標が格納される
hierarchy, // 輪郭線のベクトル情報が階層として格納される
cv.RETR_EXTERNAL, // 最も外側の輪郭線のみを取得するよう定数指定
cv.CHAIN_APPROX_TC89_L1, // 輪郭近似法の指定オプション
);
cv.threshold()により、二値化処理を行い画像を白と黒で描かれた単純な図形にします。
その後cv.findContours()により白と黒の境界を座標の集合体として取得します。第二引数のcontoursに複数の座標が多次元配列として格納されます。
contours内にある大量の座標から、被写体の輪郭の座標を絞り込みます。
// 輪郭検出
cv.findContours(
dst,
contours,
hierarchy,
cv.RETR_EXTERNAL,
cv.CHAIN_APPROX_TC89_L1,
);
for (let i = 0; i < contours.size(); i++) {
const area = cv.contourArea(contours.get(i), false);
if (area > 19000) { // ある程度のサイズ以上の輪郭のみ処理
// 被写体の輪郭を赤で描画
cv.drawContours(
src, // 描画対象の画像
contours, // findContoursで取得した座標
i, // 取得した輪郭の内、何番目の輪郭を指定するかのインデックス
new cv.Scalar(255, 0, 0, 255), // 輪郭の色指定
30, // 輪郭の線の太さ
cv.LINE_8, // 線の種類指定
hierarchy, // 輪郭の階層構造
100, // 描画される輪郭の最大レベル指定。階層構造の内、どの程度の深さまで描画するかの指定
);
}
}
cv.imshow('cvCanvas', src); // Canvasに表示
// メモリを明示的に開放する
src.delete();
dst.delete();
hierarchy.delete();
contours.delete();
};
drawContours()で画像に実際に輪郭を描画します。
描画した結果が以下のような形です。

次回は取得した座標を用いて画像の加工など、さらなる応用を効かせた解説を行いたいです。
