初投稿になります
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
画像の変換
画像は机にあるサイゼリアのメニューを撮影することを考えると上からとっても多少のゆがみが発生すると思います
そんなゆがみを射影変換である程度同じ角度、大きさに整えていきます
変換の手法は特徴点マッチングです
画像の特徴を割り出し、その特徴同士を比較する方法です
AKAZEakaze=AKAZE.Create();KeyPoint[]keyPointsLeft;KeyPoint[]keyPointsRight;MatdescriptorLeft=newMat();MatdescriptorRight=newMat();DescriptorMatchermatcher;//マッチング方法DMatch[]matches;//特徴量ベクトル同士のマッチング結果を格納する配列//画像をグレースケールとして読み込む MatLsrc=newMat(sLeftPictureFile,ImreadModes.Color);//画像をグレースケールとして読み込むMatRsrc=newMat(sRightPictureFile,ImreadModes.Color);//特徴量の検出と特徴量ベクトルの計算akaze.DetectAndCompute(Lsrc,null,outkeyPointsLeft,descriptorLeft);akaze.DetectAndCompute(Rsrc,null,outkeyPointsRight,descriptorRight);//画像1の特徴点をoutput1に出力Cv2.DrawKeypoints(Lsrc,keyPointsLeft,tokuLeft);ImageimageLeftToku=BitmapConverter.ToBitmap(tokuLeft);pictureBox3.SizeMode=PictureBoxSizeMode.Zoom;pictureBox3.Image=imageLeftToku;//画像2の特徴点をoutput1に出力Cv2.DrawKeypoints(Rsrc,keyPointsRight,tokuRight);ImageimageRightToku=BitmapConverter.ToBitmap(tokuRight);pictureBox4.SizeMode=PictureBoxSizeMode.Zoom;pictureBox4.Image=imageRightToku;この辺は特徴量マッチングで調べるとよく出てくるコード丸パクリです
こうして出てくる画像がこちら
LeftToku.jpg
RightToku.jpg
丸がついているところが画像の特徴を表しています
そしてこの特徴同士をマッチング
//総当たりでマッチングmatcher=DescriptorMatcher.Create("BruteForce");matches=matcher.Match(descriptorLeft,descriptorRight);Cv2.DrawMatches(Lsrc,keyPointsLeft,Rsrc,keyPointsRight,matches,output);output.jpg
線で結ばれているところがマッチした特徴です
これらの情報をもとに変形
intsize=matches.Count();vargetPtsSrc=newVec2f[size];vargetPtsTarget=newVec2f[size];intcount=0;foreach(variteminmatches){varptSrc=keyPointsLeft[item.QueryIdx].Pt;varptTarget=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。varhom=Cv2.FindHomography(InputArray.Create(getPtsSrc),InputArray.Create(getPtsTarget),HomographyMethods.Ransac);// 行列homを用いてSrcに射影変換を適用する。MatWarpedSrcMat=newMat();Cv2.WarpPerspective(Lsrc,WarpedSrcMat,hom,newOpenCvSharp.Size(Rsrc.Width,Rsrc.Height));WarpedSrcMat.jpg
綺麗に変形できました
この時点で射影変換した画像はだいぶ画質が劣化しているのがわかります
これが前述のごま塩ノイズの原因です
画像の差分抽出
今回はMatのデータをRGBごとに抽出してそれぞれのチャンネルごとに差分を出し、
どこか一つのチャンネルでも差分があった部分は差分ありとしてマークするようにしました
// 左右両方の画像を各チャンネルごとに分割MatLmatFloat=newMat();WarpedSrcMat.ConvertTo(LmatFloat,MatType.CV_16SC3);Mat[]LmatPlanes=LmatFloat.Split();MatRmatFloat=newMat();Rsrc.ConvertTo(RmatFloat,MatType.CV_16SC3);Mat[]RmatPlanes=RmatFloat.Split();Matdiff0=newMat();Matdiff1=newMat();Matdiff2=newMat();// 分割したチャンネルごとに差分を出す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);
射影変換した画像の劣化によりだいぶ関係ない部分も白くなっていますが
これは別の工程で緩和していきます
各チャンネルを統合し、どこかのチャンネルで差分がある場所は
すべて差分ありとしてCv2.BitwiseOr()でマスク画像を生成します
MatwiseMat=newMat();Cv2.BitwiseOr(diff0,diff1,wiseMat);Cv2.BitwiseOr(wiseMat,diff2,wiseMat);ここから汚いノイズを緩和していきます
//オープニング処理でノイズ緩和MatopeningMat=newMat();Cv2.MorphologyEx(wiseMat,openingMat,MorphTypes.Open,newMat());// スレッショルドで差分をきれいにくっきりとMatdilationMat=newMat();Cv2.Dilate(openingMat,dilationMat,newMat());Cv2.Threshold(dilationMat,dilationMat,100,255,ThresholdTypes.Binary);オープニング処理と二値化処理にて画像の汚い部分を消していきます
dilationMat.jpg
差分と元画像を合成して表示
ここからは画像の合成に入ります
// dilationMatはグレースケールなので合成先のMatと同じ色空間に変換するMatdilationScaleMat=newMat();MatdilationColorMat=newMat();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()を使えば合成する画像の割合を変えることができるので便利です
今回は差分画像を多めに加算することで差分をわかりやすくしました
完成!
いざ、サイゼリヤへ
ジャン!!!!
ん~~~~~~~~~??
今回の敗因
以下のことが失敗のようです
(先人たちと同じ過ちをしてしまいました)
・3次元のゆがみ(紙のそり)を変換できていない
赤枠で囲った部分が顕著に歪んでいますね
この辺のゆがみが2次元ワープやら弾性マッチングが有効みたいですが計算量が果てしないみたいですね
ちなみに今回のアプリ。
最後の撮影データを使った実験では体感で10秒ほど処理に時間がかかっていました
一番時間がかかっていたのは射影変換ですね
特徴量を総当たりするので
画素数が多くなる
↓
特徴も多くなる
↓
総当たりする計算量も多くなる
なので当たり前ですが…
最後に
今回はサイゼリヤの間違い探しをOpenCVを使って解いてみました
仕事でOpenCVを使う機会があったのでその知識を応用&コピペな部分が多いですが
皆さんの参考になると嬉しいです
参考にしたサイト
とても参考にしました
本当にありがとうございます







