16
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

サイゼリヤの間違い探しをOpenCVで解いてみた

Last updated at Posted at 2020-10-04

初投稿になります
C#信者のQAエンジニアです
以後お見知りおきを…

さてさて学生の味方ともいえるサイゼリヤ
自分も学生の頃は部活の打ち上げやテスト勉強にてドリンクバーとドリアで何時間も粘ったものです

そんな時一度は見たことのある間違い探し

今回はこれを自動で解くアプリをC#で作成してみようと思います

実現したいこと

C#とOpenCVSharpを使用し
サイゼリアの間違い探しを右の画像と左の画像として撮影
その差分を画像としてわかりやすく出力する

プログラムの流れとしては
・画像の変換(撮影することを考慮して左右の画像のサイズを合わせる)
・画像の差分抽出(R,G,Bごとに差分を出す)
・差分と元画像を合成して表示

今回使う画像

テスト用にサイゼリヤ公式サイトの画像を変形させたものを用意

実行結果

アプリ画面

最終生成画像(result.jpg)

取り除ききれなかったごま塩ノイズが少しありますが多めに見てください…
ちなみに変形していない画像を使うと

きれいに取れますね
(変換によるビットの抜け?が原因でごま塩になっているものと思われます)

実行環境

C#
.Net Framework 4.7.2
OpenCvSharp3-AnyCPU Ver4.0.0

Windows 10 Home 64Bit
Intel Core i7 9700
RAM 16GB
GPU NVIDIA GeForce GTX 1660

アルゴリズムの詳細

以下に大まかな流れを紹介していきます
GitHubにソース類をおいてあるので参考にしてみてください
https://github.com/uechan16/SaizeriyaMachigaisagashi

画像の変換

画像は机にあるサイゼリアのメニューを撮影することを考えると上からとっても多少のゆがみが発生すると思います
そんなゆがみを射影変換である程度同じ角度、大きさに整えていきます

変換の手法は特徴点マッチングです
画像の特徴を割り出し、その特徴同士を比較する方法です

Form.cs
            AKAZE akaze = AKAZE.Create();
            KeyPoint[] keyPointsLeft;
            KeyPoint[] keyPointsRight;

            Mat descriptorLeft = new Mat();  
            Mat descriptorRight = new Mat();

            DescriptorMatcher matcher; //マッチング方法
            DMatch[] matches; //特徴量ベクトル同士のマッチング結果を格納する配列

            //画像をグレースケールとして読み込む 
            Mat Lsrc = new Mat(sLeftPictureFile, ImreadModes.Color);

            //画像をグレースケールとして読み込む
            Mat Rsrc = new Mat(sRightPictureFile, ImreadModes.Color);

            //特徴量の検出と特徴量ベクトルの計算
            akaze.DetectAndCompute(Lsrc, null, out keyPointsLeft, descriptorLeft);
            akaze.DetectAndCompute(Rsrc, null, out keyPointsRight, descriptorRight);

            //画像1の特徴点をoutput1に出力
            Cv2.DrawKeypoints(Lsrc, keyPointsLeft, tokuLeft);
            Image imageLeftToku = BitmapConverter.ToBitmap(tokuLeft);
            pictureBox3.SizeMode = PictureBoxSizeMode.Zoom;
            pictureBox3.Image = imageLeftToku;
            
            //画像2の特徴点をoutput1に出力
            Cv2.DrawKeypoints(Rsrc, keyPointsRight, tokuRight);
            Image imageRightToku = BitmapConverter.ToBitmap(tokuRight);
            pictureBox4.SizeMode = PictureBoxSizeMode.Zoom;
            pictureBox4.Image = imageRightToku;

この辺は特徴量マッチングで調べるとよく出てくるコード丸パクリです
こうして出てくる画像がこちら
LeftToku.jpg

RightToku.jpg

丸がついているところが画像の特徴を表しています

そしてこの特徴同士をマッチング

Form.cs
           //総当たりでマッチング
            matcher = DescriptorMatcher.Create("BruteForce");
            matches = matcher.Match(descriptorLeft, descriptorRight);
            Cv2.DrawMatches(Lsrc, keyPointsLeft, Rsrc, keyPointsRight, matches, output);

output.jpg

線で結ばれているところがマッチした特徴です

これらの情報をもとに変形

form1.cs
            int size = matches.Count();
            var getPtsSrc = new Vec2f[size];
            var getPtsTarget = new Vec2f[size];

            int count = 0;
            foreach (var item in matches)
            {
                var ptSrc = keyPointsLeft[item.QueryIdx].Pt;
                var ptTarget = keyPointsRight[item.TrainIdx].Pt;
                getPtsSrc[count][0] = ptSrc.X;
                getPtsSrc[count][1] = ptSrc.Y;
                getPtsTarget[count][0] = ptTarget.X;
                getPtsTarget[count][1] = ptTarget.Y;
                count++;
            }

            // SrcをTargetにあわせこむ変換行列homを取得する。ロバスト推定法はRANZAC。
            var hom = Cv2.FindHomography(
                InputArray.Create(getPtsSrc),
                InputArray.Create(getPtsTarget),
                HomographyMethods.Ransac);

            // 行列homを用いてSrcに射影変換を適用する。
            Mat WarpedSrcMat = new Mat();
            Cv2.WarpPerspective(
                Lsrc, WarpedSrcMat, hom,
                new OpenCvSharp.Size(Rsrc.Width, Rsrc.Height));

WarpedSrcMat.jpg
Warap.jpg
綺麗に変形できました
この時点で射影変換した画像はだいぶ画質が劣化しているのがわかります
これが前述のごま塩ノイズの原因です

画像の差分抽出

今回はMatのデータをRGBごとに抽出してそれぞれのチャンネルごとに差分を出し、
どこか一つのチャンネルでも差分があった部分は差分ありとしてマークするようにしました

form1.cs
            // 左右両方の画像を各チャンネルごとに分割
            Mat LmatFloat = new Mat();
            WarpedSrcMat.ConvertTo(LmatFloat, MatType.CV_16SC3);
            Mat[] LmatPlanes = LmatFloat.Split();

            Mat RmatFloat = new Mat();
            Rsrc.ConvertTo(RmatFloat, MatType.CV_16SC3);
            Mat[] RmatPlanes = RmatFloat.Split();

            Mat diff0 = new Mat();
            Mat diff1 = new Mat();
            Mat diff2 = new Mat();
            
            // 分割したチャンネルごとに差分を出す
            Cv2.Absdiff(LmatPlanes[0], RmatPlanes[0], diff0);
            Cv2.Absdiff(LmatPlanes[1], RmatPlanes[1], diff1);
            Cv2.Absdiff(LmatPlanes[2], RmatPlanes[2], diff2);

            // ブラーでノイズ除去
            Cv2.MedianBlur(diff0, diff0, 5);
            Cv2.MedianBlur(diff1, diff1, 5);
            Cv2.MedianBlur(diff2, diff2, 5);

diffs.PNG
射影変換した画像の劣化によりだいぶ関係ない部分も白くなっていますが
これは別の工程で緩和していきます

各チャンネルを統合し、どこかのチャンネルで差分がある場所は
すべて差分ありとしてCv2.BitwiseOr()でマスク画像を生成します

Form1.cs
            Mat wiseMat = new Mat();
            Cv2.BitwiseOr(diff0, diff1, wiseMat);
            Cv2.BitwiseOr(wiseMat, diff2, wiseMat);

wiseMat.jpg
wiseMat.jpg
だいぶ汚いですね

ここから汚いノイズを緩和していきます

Form1.cs
            //オープニング処理でノイズ緩和
            Mat openingMat = new Mat();
            Cv2.MorphologyEx(wiseMat, openingMat, MorphTypes.Open,new Mat());

            // スレッショルドで差分をきれいにくっきりと
            Mat dilationMat = new Mat();
            Cv2.Dilate(openingMat, dilationMat, new Mat());
            Cv2.Threshold(dilationMat, dilationMat, 100, 255, ThresholdTypes.Binary);

オープニング処理と二値化処理にて画像の汚い部分を消していきます
dilationMat.jpg
dilationMat.jpg

差分と元画像を合成して表示

ここからは画像の合成に入ります

Form1.cs
            // dilationMatはグレースケールなので合成先のMatと同じ色空間に変換する
            Mat dilationScaleMat = new Mat();
            Mat dilationColorMat = new Mat();
            Cv2.ConvertScaleAbs(dilationMat, dilationScaleMat);
            Cv2.CvtColor(dilationScaleMat, dilationColorMat, ColorConversionCodes.GRAY2RGB);

            // 元画像 3:差分画像 7 で合成
            Cv2.AddWeighted(WarpedSrcMat, 0.3, dilationColorMat, 0.7, 0, LaddMat);
            Cv2.AddWeighted(Rsrc, 0.3, dilationColorMat, 0.7, 0, RaddMat);

Cv2.AddWeighted()を使えば合成する画像の割合を変えることができるので便利です
今回は差分画像を多めに加算することで差分をわかりやすくしました

###完成!
Result.jpg

#いざ、サイゼリヤへ
実際に行って画像を撮影してきました
左の画像

右の画像

ジャン!!!!

キャプチャ_honban.PNG

ん~~~~~~~~~??

Result.jpg
ダメっすね

今回の敗因

以下のことが失敗のようです
(先人たちと同じ過ちをしてしまいました)
・3次元のゆがみ(紙のそり)を変換できていない
Result_2.jpg
赤枠で囲った部分が顕著に歪んでいますね

この辺のゆがみが2次元ワープやら弾性マッチングが有効みたいですが計算量が果てしないみたいですね

ちなみに今回のアプリ。
最後の撮影データを使った実験では体感で10秒ほど処理に時間がかかっていました
一番時間がかかっていたのは射影変換ですね
特徴量を総当たりするので
画素数が多くなる

特徴も多くなる

総当たりする計算量も多くなる
なので当たり前ですが…

最後に

今回はサイゼリヤの間違い探しをOpenCVを使って解いてみました
仕事でOpenCVを使う機会があったのでその知識を応用&コピペな部分が多いですが
皆さんの参考になると嬉しいです

参考にしたサイト

とても参考にしました
本当にありがとうございます

サイゼリヤの間違い探しをロバストな画像処理で解く
サイゼリヤの間違い探しを解く(ヒントになる)プログラムを作ってみた

16
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?