N消し — OpenCV でナンバープレートを認識してマスクする

| 開発記録 | N消し

タグ: #OpenCV #画像処理 #Flutter

写真に写り込んだ車のナンバープレートを自動検出してマスクするアプリ。OpenCV の形状検出と複数プレート対応の実装をまとめます。

なぜナンバープレート消しか

SNS や中古車掲載で、車のナンバープレートを手動でぼかすのは地味に面倒です。1 台ならまだしも、駐車場の写真で 10 台映っていたら、もう絶望的。

この面倒を自動化する、というのが「N消し」のコンセプトです。

OpenCV の形状検出

プレート検出は、エッジ検出 → 矩形検出 → アスペクト比フィルタリングの順で行います。日本のナンバープレートは規格化されているので、縦横比がほぼ一定です。

1. Canny edge detection
2. findContours で輪郭抽出
3. approxPolyDP で多角形近似
4. 4 頂点・凸・特定アスペクト比のものをプレート候補とする

このパイプラインを Dart 側から呼ぶために、OpenCV の Dart バインディングを使用しました。ネイティブ画像処理に比べると速度は劣りますが、リアルタイム性が要求される処理ではないので問題ありません。

アスペクト比フィルタリングは「日本の標準ナンバー(横:縦 ≒ 2:1)」を中心に少し幅を持たせて判定します。輪郭抽出から候補絞り込みまでをひとつの関数にまとめると次の通り。

List<Rect> detectPlateCandidates(Mat gray) {
  final edges = Mat();
  cv.canny(gray, edges, 50, 150);

  final contours = <MatOfPoint>[];
  cv.findContours(edges, contours, RetrievalModes.list,
      ContourApproximationModes.simple);

  const targetRatio = 2.0; // 横:縦
  const ratioTolerance = 0.5; // ±25%
  final candidates = <Rect>[];

  for (final c in contours) {
    final approx = MatOfPoint2f();
    cv.approxPolyDP(c.toMatOfPoint2f(), approx, 0.02 * cv.arcLength(c, true),
        true);
    if (approx.total != 4 || !cv.isContourConvex(approx.toMatOfPoint())) {
      continue;
    }
    final rect = cv.boundingRect(c);
    if (rect.width < 60) continue; // 小さすぎる候補は捨てる
    final ratio = rect.width / rect.height;
    if ((ratio - targetRatio).abs() > ratioTolerance) continue;
    candidates.add(rect);
  }
  return candidates;
}

マルチプレート対応

初期版は「画像から 1 枚のプレートを検出」する仕様でしたが、駐車場のような複数台が映る写真にも対応したい。

サービス層で MultiPlateDetector を実装し、複数候補を返すようにしました。検出された全プレートをマスク対象として処理します。

カメラと写真選択

UI 側はカメラ撮影と写真選択の両方をサポート。iOS Info.plist と AndroidManifest.xml にカメラ・写真ライブラリの権限を入れています。

i18n 対応

グローバル展開を意識し、内部メッセージは全て国際化(i18n)対応にしました。arb ファイルで日本語・英語を管理し、他言語も追加できる構成です。

教訓

  • OpenCV は使えるなら使う、自前で書くより圧倒的に楽
  • マルチ対応は最初から見据えてサービス層を設計
  • 権限・i18n は早めに整える

「目で見て分かる結果」が出るアプリは作っていて楽しいです。MVP の段階で「動く!」が見えるのが、画像処理アプリの醍醐味です。