画像の特徴点情報をファイルに保存してマッチングに利用する方法

## はじめに
OpenCVの特徴点抽出及びマッチングを用いる機会が多いのですが、
複数画像を用いる場合、特徴点抽出に時間がかかることがよく起こりました。
そこで今回は特徴点情報をファイルに出力し、マッチングに利用するプログラムを作りました。

今回は例として、女の子たち（12枚のソース画像）から赤めがねの女の子（ターゲット画像）を見つけてみましょう！

ソース画像⬇︎
![youngwoman_37.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/422993/052eba80-ac77-1124-26bd-1a7bb9fe01cb.jpeg "youngwoman_37.jpg")![youngwoman_38.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/422993/ed11c87e-53ce-4524-2566-c9e7e30a169a.jpeg "youngwoman_38.jpg")![youngwoman_39.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/422993/73a2ecdf-dff6-ad53-2038-788aacc39db6.jpeg "youngwoman_39.jpg")![youngwoman_40.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/422993/b8c8f7b0-3430-70c9-274f-b3decebb4417.jpeg "youngwoman_40.jpg")

![youngwoman_41.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/422993/8563bd4b-dbfa-01a1-513f-f67d7732d12d.jpeg "youngwoman_41.jpg")![youngwoman_42.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/422993/04a89575-c810-aa17-e8c8-00eda0cc059b.jpeg "youngwoman_42.jpg")![youngwoman_43.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/422993/12252918-7428-5fb4-aed5-a41bf2e82ae3.jpeg "youngwoman_43.jpg")![youngwoman_44.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/422993/1f2862bb-b524-d733-8276-7f941105db41.jpeg "youngwoman_44.jpg")

![youngwoman_45.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/422993/c0268beb-f4e1-7d42-0012-a7ecc5b9ef3a.jpeg "youngwoman_45.jpg")![youngwoman_46.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/422993/53ac9de9-8923-4052-7658-9aefe1a9573c.jpeg "youngwoman_46.jpg")![youngwoman_47.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/422993/76ed277a-673e-fb23-484c-179a9b5f27b8.jpeg "youngwoman_47.jpg")![youngwoman_48.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/422993/9d398625-917e-e1df-a1f3-aab6f3a5c653.jpeg "youngwoman_48.jpg")

ターゲット画像⬇︎
![target_girl.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/422993/217a89c6-76bc-08b9-1abf-cf825f0ecfaa.jpeg "target_girl.jpg")


## 開発環境
今回は以下の環境で開発しました。
OpenCV 4.1.2
Python 3.8.1


##構成
以下プログラムの流れです。次節から順を追って解説します。
1. ソース画像の特徴点情報を取得しファイルに保存
2. ターゲット画像の特徴点情報を取得
3. ファイルからソース画像の特徴点情報を取得
4. マッチング

## 1. ソース画像の特徴点情報を取得しファイルに保存

まずはsave_features.pyにて、ソース画像（女の子たち）の特徴点情報を取得しファイルに保存します。
今回はOpenCVに実装されている特徴点記述子、AKAZEを用いました。
`keypoint`をファイル保存するにあたり、`cv::KeyPoint`にアクセスしてリスト化する必要があります。

```python:save_features.py
# keypointをlist化
keypoint = []
for p in features[0]:
    temp = (p.pt, p.size, p.angle, p.response, p.octave, p.class_id)
    keypoint.append(temp)
```

また、今回はメモリの消費量を抑えるために特徴点情報をbyte型に変換しました。

```python:save_features.py
# keypointをbytesに変換
map(bytes, keypoints)
```  
  
  
以下、ソースコード全体です。

```python:save_features.py
import cv2 as cv
import pickle

SOURCE_FILES = [
    "youngwoman_37.jpg",
    "youngwoman_38.jpg",
    "youngwoman_39.jpg",
    "youngwoman_40.jpg",
    "youngwoman_41.jpg",
    "youngwoman_42.jpg",
    "youngwoman_43.jpg",
    "youngwoman_44.jpg",
    "youngwoman_45.jpg",
    "youngwoman_46.jpg",
    "youngwoman_47.jpg",
    "youngwoman_48.jpg",
]


def get_features(img_file_name):
    """Get features of master images

        Args:
            img_file_name(list): Master image

        Returns:
            keypoints, descriptors, img

    """
    img = cv.imread("images/" + img_file_name)

    # 特徴点情報抽出
    akaze = cv.AKAZE_create()
    kp, des = akaze.detectAndCompute(img, None)

    features = [kp, des]

    return features


sources = {}
for item in SOURCE_FILES:
    features = get_features(item)
    # keypointをlist化
    keypoints = []
    for p in features[0]:
        temp = (p.pt, p.size, p.angle, p.response, p.octave, p.class_id)
        keypoints.append(temp)

    # keypointsをbytesに変換
    map(bytes, keypoints)
    # 特徴点情報を辞書化
    sources[item] = {
        "src": item,
        "keypoint": keypoints,
        "descriptor": features[1],
    }

# 特徴点情報をファイルに書き込み
with open("sources_data.pickle", mode="wb") as f:
    pickle.dump(sources, f)
```  


## 2.ターゲット画像の特徴点情報を取得

手順２からget_features_from_file.pyにて処理を行なっています。
ソース画像同様、特徴点記述子であるAKAZEを用いて特徴点情報を取得します。

```python:get_features_from_file.py
# ターゲット画像読み込み
target_img = cv.imread("images/target_girl.jpg")
# 特徴量取得
akaze = cv.AKAZE_create()
target_keypoint, target_descriptor = akaze.detectAndCompute(target_img, None)
```

## 3.ファイルからソース画像の特徴点情報を取得

`get_sources()`でファイルから特徴点情報を取得します。
pickle化するために`keypoints`をbyte化したので、listに変換し元の構造に戻します。

```python:get_features_from_file.py
def get_sources():
    """Get source's features from file

        Returns:
            sources(list): source's keypoints, descriptors,and img
        """
    # 特徴点情報をファイルから取得
    with open("sources_data.pickle", mode="rb") as f:
        sources = pickle.load(f)

    for n in sources:
        items = sources[n]
        # keypointsをbytesからlistに直す
        list(map(list, items["keypoint"]))
        # keypointsを元の構造に復元
        keypoints = []
        for p in items["keypoint"]:
            temp = cv.KeyPoint(
                x=p[0][0],
                y=p[0][1],
                _size=p[1],
                _angle=p[2],
                _response=p[3],
                _octave=p[4],
                _class_id=p[5],
            )
            keypoints.append(temp)
        items["keypoint"] = keypoints

    return sources
```

## 4.マッチング
ソース画像ごとにターゲット画像の特徴点情報とマッチングを行います。
データを間引きし、マッチングした特徴点の数が設定した閾値（今回は20に設定）以上であればマッチング成功とします。

```python:get_features_from_file.py
for n in sources:
    source = sources[n]
    source_img = cv.imread("images/" + source["src"])
    matches = matcher.knnMatch(source["descriptor"], target_des, k=2)
    # データを間引きする
    ratio = 0.5
    matched_keypoints = []
    for m, n in matches:
        if m.distance < ratio * n.distance:
            matched_keypoints.append([m])

    # 任意の閾値よりgoodが多い場合結果画像を出力
    if len(matched_keypoints) > 20:
        out = cv.drawMatchesKnn(
            source_img,
            source["keypoint"],
            target_img,
            target_kp,
            matched_keypoints,
            None,
            flags=2,
        )
```  
  
  
以下、ソースコード全体です。

```python:get_features_from_file.py
import cv2 as cv
import pickle


def get_sources():
    """Get source's features from file

        Returns:
            sources(list): source's keypoints, descriptors,and img
        """
    # 特徴点情報をファイルから取得
    with open("sources_data.pickle", mode="rb") as f:
        sources = pickle.load(f)

    for n in sources:
        items = sources[n]
        # keypointsをbytesからlistに直す
        list(map(list, items["keypoint"]))
        # keypointsを元の構造に復元
        keypoints = []
        for p in items["keypoint"]:
            temp = cv.KeyPoint(
                x=p[0][0],
                y=p[0][1],
                _size=p[1],
                _angle=p[2],
                _response=p[3],
                _octave=p[4],
                _class_id=p[5],
            )
            keypoints.append(temp)
        items["keypoint"] = keypoints

    return sources


matcher = cv.BFMatcher()

# ターゲット画像読み込み
target_img = cv.imread("images/target_girl.jpg")
# 特徴量取得
akaze = cv.AKAZE_create()
target_kp, target_des = akaze.detectAndCompute(target_img, None)
# ソース画像の特徴点情報をファイルから読み込み
sources = get_sources()

for n in sources:
    source = sources[n]
    source_img = cv.imread("images/" + source["src"])
    matches = matcher.knnMatch(source["descriptor"], target_des, k=2)
    # データを間引きする
    ratio = 0.5
    matched_keypoints = []
    for m, n in matches:
        if m.distance < ratio * n.distance:
            matched_keypoints.append([m])

    # 任意の閾値よりgoodが多い場合結果画像を出力
    if len(matched_keypoints) > 20:
        out = cv.drawMatchesKnn(
            source_img,
            source["keypoint"],
            target_img,
            target_kp,
            matched_keypoints,
            None,
            flags=2,
        )

cv.imwrite("images/result.jpg", out)
cv.waitKey()
```

# 結果
以下の結果画像では、ソース画像とターゲット画像とでマッチングした特徴点同士を描画しています。
無事ターゲットの女の子を見つけることができました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/422993/ff6d686b-b7dd-e0f0-8bb8-db0ce7ca3d53.png "result.jpg")


# 終わりに
特徴点をファイルに保存する肝は、
`cv::KeyPoint`にアクセスすること
アクセスした情報をもとにリスト化すること
です。
OpenCVで画像処理する機会があればぜひ試してみてください。

# 参考ページ
[Python3版OpenCVのKeyPointをファイルに書き出す](https://qiita.com/mrsaa2000/items/a9cb70c36064a82ddef5)

[python3,opencv3で特徴点マッチング(AKAZE, KNN)]
(https://techtech-sorae.com/python3opencv3%E3%81%A7%E7%89%B9%E5%BE%B4%E7%82%B9%E3%83%9E%E3%83%83%E3%83%81%E3%83%B3%E3%82%B0akaze-knn/)