ReactとOpenCV.jsで枠認識をしてみよう

はじめに

「ブラウザで何か撮影をする際にフロントサイドのみで画像を加工・解析をしたい」といった需要が一部あると思いますが、公開されている技術記事はあまりないと感じています。
そのため、前回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()で画像に実際に輪郭を描画します。
描画した結果が以下のような形です。

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

一覧へ戻る

お問い合わせ