【Python】OpenCVで間違い探しをする

# はじめに
OpenCVを使って、いわゆる「間違い探し」をしてみます。

# 出来たこと
2枚の画像を読み込ませると、間違い（差分）の部分が白く表示されます。

1枚目
![a.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/5103cdcf-4f2e-f425-f7a0-83e17c1b2622.jpeg)

2枚目
![b.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/021f4582-a8e4-57d5-0e83-b4b2a5a5ed14.jpeg)

出力
![結果.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/7e4a7893-8880-2260-7275-90c8adf02c34.png)

# 環境
- macOS: 11.6
- Python: 3.9.5
    - opencv-python: 4.5.3.56
    - matplotlib: 3.4.3
    - jupyter: 1.0.0

# 実装
### ライブラリのインポート
``` python
import cv2
import datetime
import matplotlib.pyplot as plt
import numpy as np
import os
```

### 画像読み込み
画像を読み込みます。OpenCVはBRGフォーマットなので、RGBへ変換します。

``` python
imgA = cv2.imread('img/a.jpg')
imgB = cv2.imread('img/b.jpg')
# OpenCVはBRGフォーマットなので、RGBへ変換する
imgA = cv2.cvtColor(imgA, cv2.COLOR_BGR2RGB)
imgB = cv2.cvtColor(imgB, cv2.COLOR_BGR2RGB)
# 画像サイズを取得
hA, wA, cA = imgA.shape[:3]
hB, wB, cA = imgB.shape [:3]
```
imgAはこちら。
![imgA.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/66883ece-9b8c-eec8-5672-893331224490.png)

imgBはこちら。
![imgB.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/b11ee4d8-5613-cb03-1ab0-5586fa04faa1.png)

ちなみに、cvt.Colorしなかった場合の出力はこのような感じ。
![not_convert.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/97aa2464-5df0-81b5-bb3a-58b638c0fdfb.png)

### 特徴点を抽出
```python
# 特徴量検出器を作成
akaze = cv2.AKAZE_create()
# 二つの画像の特徴点を抽出
kpA, desA = akaze.detectAndCompute(imgA,None)
kpB, desB = akaze.detectAndCompute(imgB,None)
```
特徴点については、私が中途半端なことを書くより[OpenCVのチュートリアル](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_feature2d/py_features_meaning/py_features_meaning.html#features-meaning)をご覧いただいた方が良いと思います。AKAZEという検出器を使って特徴点を抽出します。imgAの特徴検出結果は以下のようになります。
![akaze_imgA.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/bdcd96ad-7c89-9036-7c0f-d124038962b4.png)

### なんで特徴点を抽出するの？
なんで特徴点抽出などという面倒なことをする必要があるのでしょうか。少し先の話になりますが、間違い探しのために2枚の画像の差分を取ります。しかし、2枚目の画像が微妙に傾いていたり、サイズが違っていたりすると、うまく差分が取れません。

そこで、imgB（2枚目の画像）からimgAと一致する部分を抽出する必要があります。そのために2枚の画像の特徴点を抽出し、特徴点をマッチングする必要があります。

下の例は[OpenCVチュートリアル](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_feature2d/py_feature_homography/py_feature_homography.html#feature-homography)からお借りしたもので、左にあるクッキーの箱か何かの画像を、右の画像の中から検出しています。
![物体検出の例.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/92aa38dc-393d-f70f-3b23-773679444f46.png)

### 特徴点のマッチングと物体検出
```python
# imageBを透視変換する
# 透視変換: 斜めから撮影した画像を真上から見た画像に変換する感じ
# BFMatcher型のオブジェクトを作成する
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
# 記述子をマッチさせる。※スキャン画像(B2)の特徴抽出はforループ前に実施済み。
matches = bf.match(desA,desB)
# マッチしたものを距離順に並べ替える。
matches = sorted(matches, key = lambda x:x.distance)
# マッチしたもの（ソート済み）の中から上位★%（参考：15%)をgoodとする。
good = matches[:int(len(matches) * 0.15)]
# 対応が取れた特徴点の座標を取り出す？
src_pts = np.float32([kpA[m.queryIdx].pt for m in good]).reshape(-1,1,2)
dst_pts = np.float32([kpB[m.trainIdx].pt for m in good]).reshape(-1,1,2)
# findHomography:二つの画像から得られた点の集合を与えると、その物体の投射変換を計算する
M, mask = cv2.findHomography(dst_pts, src_pts, cv2.RANSAC,5.0) # dst_img作成の際だけ使う。warpperspectiveの使い方がわかってない。
# imgBを透視変換。
imgB_transform = cv2.warpPerspective(imgB, M, (wA, hA))
```

この部分は[OpenCVチュートリアル](http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_feature2d/py_feature_homography/py_feature_homography.html#feature-homography)からほぼコピペしました。imgB_transformはこのようになります。
![imgB_transform.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/ac282024-7702-384f-7b02-697d8d13fe08.png)

わかりにくいのですが、imgBの左端にあった余計な部分が無くなっています。
![imgB_.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/cdbcb6cc-011f-dfa0-24c7-7d2ae86b3d03.png)

### 差分をとる
```python
# imgAとdst_imgの差分を求めてresultとする。グレースケールに変換。
result = cv2.absdiff(imgA, imgB_transform)
result_gray = cv2.cvtColor(result, cv2.COLOR_BGR2GRAY)
# 二値化
_, result_bin = cv2.threshold(result_gray, 50, 255, cv2.THRESH_BINARY) # 閾値は50
```

2枚の画像の差分を取り、グレースケールに変換し、さらに二値化しました。　result_binは以下のようになります。
![result_bin.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/2a96c800-375d-691b-dd64-defcd8514ecd.png)

このままでも良いのですが、赤枠の箇所に少しノイズが残っています。2枚の画像を重ねているので、どうしてもこういう部分ができてしまいます。

### ノイズ除去
```python
# カーネルを準備（オープニング用）
kernel = np.ones((2,2),np.uint8)
# オープニング（収縮→膨張）実行 ノイズ除去
result_bin = cv2.morphologyEx(result_bin, cv2.MORPH_OPEN, kernel) # オープニング（収縮→膨張）。ノイズ除去。
```
ノイズ除去には良い方法があるような気がしますが、今回はオープニングをしました。（画像処理の講義で習ったなあ）
![opening.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/7dd04345-57ef-d3f4-0457-612c600d044c.png)

### 後処理
```python
# 二値画像をRGB形式に変換し、2枚の画像を重ねる。
result_bin_rgb = cv2.cvtColor(result_bin, cv2.COLOR_GRAY2RGB)
result_add = cv2.addWeighted(imgA, 0.3, result_bin_rgb, 0.7, 2.2) # ２.２はガンマ値。大きくすると白っぽくなる
```

imgAに先程の画像を重ねます。
![result_add.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/689015/3634dbf4-53cd-8ab3-1935-5e1f8a972879.png)

右上のコーヒーメーカーに置いてある「カップの取っ手がない」や、コップを置いている「トレー（？）の幅が違う」といった細かいところも一応、検出できています。が、変数の設定が結構シビアです。