すりごまの鯖缶?ブログ 2缶目

鯖管のことだったりプログラミングのことだったりマイクラのことだったり

画像処理でナンバープレートを消す

顔本ではさっくり記事しか書かなかったけど技術的?な解説します。


簡単に説明するとこんな感じです。(いらすとやさんthx)
f:id:surigomaxxxxxxx:20170104220338p:plain

今回は、C#+OpenCVSharp3を使い開発しました。
特徴点・特徴量にAKAZE(Accelerated-KAZE)、マッチングにBruteForceを用いました。(FlannBasedにしようと思ったけどうまくツリーを作ってくれなかったので今回は不採用)

OpenCVSharpはNuget経由でインクルード。便利。

解説

1.画像読み込み

OpenCV2→3にバージョンアップした際に画像はマトリクスとして表現するようになったようです。
なので

Mat image = Cv2.ImRead("ファイル名");

とやると読み込みます。楽。

このときにプレビュなんか表示したい場合は

Cv2.NamedWindow("ウィンドウ名");
Cv2.ImShow("ウィンドウ名", image);

でウィンドウ表示。
ただ等倍表示されるのでデジカメ画質だとバカでかいウィンドウが表示されるので

Cv2.ResizeWindow("ウィンドウ名", image.Width / 2, image.Height / 2);

とかやると1/2表示できる。

2.特徴点・特徴量を計算

AKAZEはKAZEを高速化したアルゴリズムらしく、KAZEの各耐性(拡縮・回転・輝度等)に強さをそのまま(若干強くなってるらしい)に高速化した検出アルゴリズムらしいです。
完全に専門外なので詳しくは知りません。
OpenCVから用いるには

AKAZE akaze = AKAZE.Create();
KeyPoint[] key_point;
Mat descriptor = new Mat();
akaze.DetectAndCompute(image, null, out key_point, descriptor);

これで勝手に計算してくれます。
さらに高速化したい人は、AKAZEのライブラリを直接使うと、GPU支援が使えるそうです。
C#では使えないと思います。はい。

3.マッチング
DescriptorMatcher matcher = DescriptorMatcher.Create("BruteForce");
DMatch[][] matches;
matches = matcher.KnnMatch(sample, image, 3);

こっから面倒です。
アルゴリズムはBruteForce。名前の通り全通り探査です。
そんでもってKnnMatchが上位N位の結果を返す関数だそう。
これでサンプルと探査先でのマッチ結果が取得できます。

4.マッチ結果の枝切り

ただ、マッチ結果と言っても一致率のとても低い結果も入ってしまっているので枝切りします。

const double match_per = 0.7;
List<DMatch> good = new List<DMatch>();
List<Point2f> p1 = new List<Point2f>(), p2 = new List<Point2f>();
foreach (var m in matches)
{
    double dis1 = m[0].Distance;
    double dis2 = m[1].Distance;
    if (dis1 <= dis2 * match_per)
    {
        good.Add(m[0]);
        p1.Add(s_points[m[0].QueryIdx].Pt);
        p2.Add(i_points[m[0].TrainIdx].Pt);
    }
}

これで枝切りとついでにデータの仕分けもできました。

5.ロバスト推定してホモグラフィーを算出

さて、一致した結果を知ってもどこにどういう状態であると知るには計算が必要です。

List<DMatch> inlinerMatch = new List<DMatch>();
Mat masks = new Mat(img.Size(),img.Type());
if (p1.Count > 0 && p2.Count > 0)
{
    Mat H = Cv2.FindHomography(InputArray.Create(p1), InputArray.Create(p2), HomographyMethods.Ransac, 2.5, masks);
    try
    {
        for (int i = 0; i < masks.Rows; i++)
        {
            if (masks.Row[i].CountNonZero() == 1)
            {
                inlinerMatch.Add(good[i]);
            }
        }
    }
    catch (OpenCVException e) { }
}

えー、、、思いっきりエラー握りつぶしてますが気にしないでください。
時々CountNonZero()でコケるんです。。。

さて、これだと前章で仕分けしたデータを使ってロバスト推定してホモグラフィーを算出してます。
ついでにこの時に使った一致データも取得しています。
ロバスト推定にはRansac(Random sample consensus)を用いてます。

6.切り抜く

ホモグラフィーまで出せたのでやっとこさぼかし作業に入れます。
ここで問題なのがどうやってぼかすのか。
ガウシアンブラーをつかってぼかすと、若干ながらナンバーが見えてしまいます。
かといって白で潰すと、暗く撮った写真でナンバープレートがやたらに光ってしまうのでNG。
なので、ナンバーをグラデーション加工してしまいます。
ここで数学の登場。
今、特定の平面をある特定の位置に変形移動できる行列を持っている。
とならば、逆に特定の位置にあるものを平面に戻せる行列を作れると。
ということで逆行列を用いてナンバーのみの画像を生成します。

Mat zoom = image.EmptyClone();
Cv2.WarpPerspective(image, zoom, H.Inv(), sample.Size());

はい、これでひっぱりだせます。
そしたら、ぼかします。

7.ぼかす

色取って平均色だしてなんて面倒なので画像を縮小拡大を用いて実現しましょう。

Cv2.Resize(zoom, zoom, new Size(1, 2), 0, 0, InterpolationFlags.Linear);
Cv2.Resize(zoom, zoom, sample.Size(), 0, 0, InterpolationFlags.Lanczos4);
Mat masked = image.EmptyClone();
Cv2.GaussianBlur(zoom, masked, new Size(53, 53), 10);

いやーわかりやすい。1x2pxにしてもとに戻して、ガウシアンブラー。
ちょっと補完の方式を変えてますが多分いらないと思います。(試してない)

8.合成

さて、画像とくっつけましょう。
ただ、このときに加算合成すると確実に消しきれないので、元画像のナンバーのみ切り抜きます。
そのためにマスク画像を生成します。

Point2f[] o_corners = new Point2f[4];
o_corners[0] = new Point2f(0, 0);
o_corners[1] = new Point2f(sample.Cols, 0);
o_corners[2] = new Point2f(sample.Cols, sample.Rows);
o_corners[3] = new Point2f(0, sample.Rows);
Point2f[] s_corners = null;
s_corners = Cv2.PerspectiveTransform(o_corners, H);
List<Point> p = new List<Point>();
p.Add(s_corners[0]);
p.Add(s_corners[1]);
p.Add(s_corners[2]);
p.Add(s_corners[3]);
Mat mask = image.EmptyClone();
Cv2.FillConvexPoly(mask, p, Scalar.White, LineTypes.AntiAlias);
Cv2.GaussianBlur(mask, mask, new Size(53, 53), 10);

ゴチャゴチャしてますね。
簡単に説明すると、o_cornersでサンプルサイズの四角を指定して、ホモグラフィーを用いて変形。
そのデータをListに突っ込んで白で塗りつぶしてもらっています。
そのあと同じ量でブラーをかけることでほぼおなじマスク領域を実現しています。

そんでもってOpenCVの楽なところ。行列計算が単純な演算表記で実現しているところ。

Mat image_preview = (image - mask) + masked;

これで元画像からmask分引いてぼかしたmaskedを足した結果をimage_previewに入れてくれます。
これで完成。

あとは

image_preview.SaveImage("ファイル名");

で保存。終わり。

感想

画像処理めんどくさいなぁ。。。
まあ、まだ楽な方なんだろうけどね。
ソースコードは汚すぎるのでしません。
実行ファイルも上げません。(ライセンスの確認とか一切してない)
個人的に欲しい方は無保証であげますのでTwitterなりで渡します。
ちなみに総行数は167行でした。