[Python + OpenCV] 🍔エブリバーガーまちがいさがし攻略🍔

[エブリバーガーまちがいさがし](https://www.bourbon.co.jp/burger.kikori/burger/)（むずかしい🍔🍔🍔）の攻略記事です。
🙇‍♀️n番煎じかもしれませんがご容赦ください🙇‍♂️

特に [サイゼリヤの間違い探しが難しすぎたので大人の力で解決した](http://kawalabo.blogspot.com/2014/11/blog-post.html) という記事は当時も見た記憶があり参考にさせていただいています。

2020/3/13 追記:
続き書きました。
 [【Python + OpenCV】🍔エブリバーガーまちがいさがし攻略🍔 ~ テンプレートマッチングで攻略 ~](https://qiita.com/ari3/items/9255b5f3459601c3b8d4)

# はじめに

本記事の内容はエブリバーガーまちがいさがしの最高難易度の「むずかしい🍔🍔🍔」 を解くことだけに特化しています。
具体的には下図の間違い探しにチャレンジします。
まちがいは4個あるらしいのですがあとひとつを目視では見つけることができなかったので今回はこのような攻略法をとることになりました。
![img.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174132/8121f681-826a-8a1e-42e2-8af630dc9455.png)
https://www.bourbon.co.jp/burger.kikori/burger/

また、あらかじめここに最終的な成果物も置いておきたいと思います。
グレースケールで見にくいですが矩形で囲んだ箇所にまちがいがあります。
あー右下草の成長具合が微妙に違ってたんですね（わかるか👊）
![answer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174132/f59f6df1-cf3a-f5dc-76c4-f57d38657e71.png)


# 環境など
+ Windows 10
+ Python 3.8.1

venv で作成した仮想環境下で下記のパッケージをインストールしています。
opencv-python をインストールすると numpy もインストールされました。

```python:requirements.txt
numpy==1.18.1
opencv-python==4.2.0.32
```

# 方針
「左の絵」と「右の絵」の差分画像からまちがいを検出してみます。
そのためにまずは一枚の画像となっている問題の画像から「左の絵」と「右の絵」をそれぞれ抽出する必要があります。
このときの左右の絵の抽出の精度によっては差分画像に対して何らかの画像処理によって補正をかけてやる必要も考えられます。

※前提として、はじめにでも少し言及していますが、各種パラメータの設定や画像処理に関しては今回の問題を解くことに特化してもよいこととします。

# 左右の絵の抽出
※ソースコードは最後にまとめて貼ります。

まず、それぞれの絵は枠で囲まれていますのでこちらの輪郭を抽出するという方向性でいきます。
[pythonで一から画像処理 (4)輪郭抽出](https://qiita.com/fugunoko/items/7e5056449e172cbeadd9) では同じように OpenCV4 を使用した輪郭抽出をされており参考にしました。

ただし、今回の攻略においては二値化の際の閾値を適応的に決定する必要はなく、あくまで絵の枠を抽出するということを目的とします。具体的には下図のように草原部分と枠線とが境界となるような値に閾値を決定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174132/1bd58039-4c10-5336-afa9-f1ce6e70a9a1.png)

また、抽出した輪郭で囲まれる領域群から絵の枠部分のみを抽出するために領域の大きさを閾値として判定します。こちらもまずはすべての領域の大きさを print して確認し手動で適当に決定すれば構いません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174132/2ac384b0-de0c-53ed-2106-c97f79ace77c.png)

思いの外うまくそれぞれの絵の枠が抽出できたのでそれぞれ元画像（グレースケール化）から切り出します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174132/455a6a3b-7bf8-3032-8a33-8ac0ad4faead.png)

# 左右の絵の差分画像の取得

差分画像の取得に関しては `cv2.absdiff` を使用すればよいでしょう。
グレースケールで比較しているので輝度の差分になっていますが、まちがい箇所のみが差分として検出される結果となりました。右下も分かりづらいですが検出されていますね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174132/fadcf88c-530f-a7b8-35de-efc639471063.png)

この結果は完全に予想外で、左右の絵に対して位置合わせや変形処理あるいは差分画像に対するモルフォロジー変換処理等を適用してのノイズ除去は少なからず必要かと思っていました。

# 最終成果

最後に差分画像から、枠の抽出をしたのと同じように、まちがいの領域を囲む矩形をプロットしてあげれば完成です🎉
右下草の部分に関してはちぎれて2つの領域として検出されていますが今回の目的的には構いません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174132/132fc52e-b6e7-7e58-cce2-59b5106022de.png)

# おまけ

「やさしい🍔」「ふつう🍔🍔」の問題ではどうなるでしょうか？

「やさしい🍔」
![easyline.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174132/d4f76771-c561-b5f1-fac4-16e10ee3db3b.png)

「ふつう🍔🍔」
![normalline.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/174132/a1956ab3-80f5-f2d4-a32f-00d9f3f613d6.png)

奇跡的に「むずかしい🍔🍔🍔」に限って最小限の努力（プログラム的には）で解けていただけのようです😂

# まとめ

エブリバーガーまちがいさがし「むずかしい🍔🍔🍔」攻略しました。
「やさしい🍔」「ふつう🍔🍔」は目視で解きましょう。

ある程度汎用的なプログラムにするとなると、おまけの内容に関してもそうですがもう少し工夫する必要がありそうです。また時間があればそうした部分も含めて改善して投稿できれば良いと思います。その際には、先人の皆様の様々な記事を参考にさせていただきたいと思います🙇‍♂️

# ソースコード

```python
import cv2
import numpy as np

img = cv2.imread("images/img.png")
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 閾値は手動で調整
retVal, img_binary = cv2.threshold(img_gray, 50, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(img_binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

roi = []
for i in range(0, len(contours)):
    area = cv2.contourArea(contours[i])
    # 閾値は手動で調整
    # print(area)
    if area < 100000:
        continue

    if len(contours[i]) > 0:
      rect = contours[i]
      x, y, w, h = cv2.boundingRect(rect)
      # print(x, y, w, h)
      roi.append(img_gray[y:y + h, x:x + w])
      cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)

# 注目領域は2つ検出できた前提
img_right = roi[0]
img_left = roi[1]

# 2つの注目領域のサイズが同じ前提
# print(img_right.shape)
# print(img_left.shape)
img_diff = cv2.absdiff(img_left, img_right)

retVal, img_diff_binary = cv2.threshold(img_diff, 50, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(img_diff_binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

answer = img_left.copy()
for i in range(0, len(contours)):
    area = cv2.contourArea(contours[i])

    if len(contours[i]) > 0:
      rect = contours[i]
      x, y, w, h = cv2.boundingRect(rect)
      cv2.rectangle(answer, (x, y), (x + w, y + h), 0, 2)

cv2.imwrite("images/answer.png", answer)

# 途中経過
# cv2.imshow("gray", img_gray)
# cv2.imshow("binary", img_binary)
# cv2.imshow("left", img_left)
# cv2.imshow("right", img_right)
# cv2.imshow("rect_angle", img)
# cv2.imshow("diff", img_diff)
# cv2.imshow("diff_binary", img_diff_binary)

cv2.imshow("answer", answer)

cv2.waitKey(0)
cv2.destroyAllWindows()

```

# 参考

[エブリバーガー](https://www.bourbon.co.jp/burger.kikori/burger/)
[サイゼリヤの間違い探しが難しすぎたので大人の力で解決した](http://kawalabo.blogspot.com/2014/11/blog-post.html)
[pythonで一から画像処理 (4)輪郭抽出](https://qiita.com/fugunoko/items/7e5056449e172cbeadd9)