Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9743

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

$
0
0

初投稿になります
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
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

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

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

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

output.jpg

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

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

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

画像の差分抽出

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

form1.cs
// 左右両方の画像を各チャンネルごとに分割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);

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

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

Form1.cs
MatwiseMat=newMat();Cv2.BitwiseOr(diff0,diff1,wiseMat);Cv2.BitwiseOr(wiseMat,diff2,wiseMat);

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

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

Form1.cs
//オープニング処理でノイズ緩和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.jpg

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

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

Form1.cs
// 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()を使えば合成する画像の割合を変えることができるので便利です
今回は差分画像を多めに加算することで差分をわかりやすくしました

完成!

Result.jpg

いざ、サイゼリヤへ

実際に行って画像を撮影してきました
左の画像

右の画像

ジャン!!!!

キャプチャ_honban.PNG

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

Result.jpg
ダメっすね

今回の敗因

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

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

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

特徴も多くなる

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

最後に

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

参考にしたサイト

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

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


Viewing all articles
Browse latest Browse all 9743

Trending Articles