N消し — OpenCV でナンバープレートを認識してマスクする
| 開発記録 | N消し
タグ: #OpenCV #画像処理 #Flutter
駐車場の写真に車が 10 台。ナンバープレートを手でぼかしていたら日が暮れます。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 の段階で「動く!」が見えるのが、画像処理の醍醐味だと思います。手でぼかしていた時間が、そのまま返ってきました。