ArUcoマーカーで常時正面画像を確保する

# はじめに
2年前に公開した我がYouTube動画に今年になってコメントが付いた。
あるプロジェクトについてソースコードを求めるものだった。
それに応えるため今回の記事を著す。

<iframe width="560" height="315" src="https://www.youtube.com/embed/koSIMCdwD34" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

# ArUcoについて
ArUcoはOpenCVで使えるマーカーモジュール。ノーマル（？）のOpenCVではなく拡張モジュール**opencv_contrib**が必要。
Pythonなら`pip install opencv-contrib-python`でインストールする。
呼び出すのは普通に`import cv2`でOKだ。

# マーカー作成
`aruco.drawMarker()`でマーカー画像を作ることができる。作られたマーカーはBGRではなく`ndim=2`のグレースケール画像だ。

```Python:
import cv2
from cv2 import aruco

dic_aruco = aruco.Dictionary_get(aruco.DICT_4X4_50)
size = 100
ids = range(4)
for id in ids:
    mark = aruco.drawMarker(dic_aruco, id, size)
    label = f"id_{id:02}"
    cv2.imshow(label, mark)
    cv2.imwrite(label + ".png", mark)
    cv2.waitKey(0)

cv2.destroyAllWindows()
```

|id_00|id_01|id_02|id_03|
|:-:|:-:|:-:|:-:|
|![id_00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/e5bdcebb-5c0e-21d2-cd83-264efb048e9d.png)|![id_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/299df2fd-d551-ac5b-1f27-f732a73c680a.png)|![id_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/0bcd4325-eded-0b24-512a-dcb16f16e98d.png)|![id_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/533bf54d-47de-bbec-d5a8-668c50b4b560.png)|


# マーカー検出
マーカー検出は以下のようにする。
先ほど作ったマーカー画像をそのまま使うのでは芸がない。ここでは紙に印刷したものを斜めから撮影している。黒い机に埃が積もっているが気にしないでほしい。

|元画像|
|:-:|
|![photo.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/a1d76b6d-3a07-e2ac-6f03-4b7317213982.jpeg)|

```Python:
import cv2
from cv2 import aruco

filename = "photo.jpg"
image = cv2.imread(filename)
dic_aruco = aruco.Dictionary_get(aruco.DICT_4X4_50)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
corners, ids, _ = aruco.detectMarkers(gray, dic_aruco)

result = aruco.drawDetectedMarkers(image.copy(), corners, ids)
print(corners)
print(ids)
cv2.imshow("result", result)
cv2.waitKey(0)
cv2.destroyAllWindows()
```

## aruco.detectMarkers()
`aruco.detectMarkers()`でマーカーを検出する。対象はグレースケール画像だ。
戻り値は

 - `corners`　マーカーの四隅の座標
 - `ids`　マーカーのid
 - `rejectedImgPoints`　マーカーとして認識されなかった図形の座標
 
の3種類。`rejectedImgPoints`は主にデバッグに使う。上記コードでは使っていないが、ただの黒四角がここにカウントされていたりしてしっかり調べてみるとけっこう面白い。

### corners
`corners` は次のように出力される。今回の写真では紙にあらかじめ印刷してあるが、左上から時計回りにx座標とy座標が格納されている。だからリストのリストになるのは当然…と言いたいところだが、ブラケットが三重になっている。これは二次元配列ではなく、`shape=(1, 4, 2)`の三次元配列なのだ。なぜこのような仕様になっているのかはわからない。
一枚の画像の中に複数のマーカーがあると`(array(中略), array(中略), …)`となる。
まず使うことはないと思うが、`rejectedImgPoints`もこれと同じフォーマット。

```:corners
(array([[[171.,  64.],
        [277., 117.],
        [175., 195.],
        [ 72., 116.]]], dtype=float32),)
```

### ids
`ids`も単なるリストではなく、「個々のidが要素数1のリスト」のリストになっている。
```:ids
[[0]]
```

## aruco.drawDetectedMarkers()
`aruco.drawDetectedMarkers()`によってマーカーの四辺に線が、左上の頂点に小さい四角が、中央にidが描かれる。

|元画像|検出結果重ね書き|
|:-:|:-:|
|![photo.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/a1d76b6d-3a07-e2ac-6f03-4b7317213982.jpeg)|![result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/7d8f63ee-c080-9c34-20ae-649e1fc45a6e.png)|


### aruco.drawAxis()
`aruco.drawAxis()`で法線方向を含むマーカーの3軸を描画することができる。
斜めっているマーカーを検出できているのだから法線を計算できて当然だと思うのだが、どうやらそれだけでは駄目なようで、カメラをキャリブレーションして算出するカメラマトリックスなる行列など多くの引数を必要とする。
面倒なので未挑戦。

# 応用

## 斜め画像を矩形にする
以前、[**投影変換で画像を変形する　～モニタ画面をハックする～**](https://qiita.com/mo256man/items/27d0a44071aee5f06933)という記事を書いた。

https://qiita.com/mo256man/items/27d0a44071aee5f06933

ArUcoマーカーと投影変換を組み合わせれば、自分で四角形を指定せずとも斜めから撮影した画像を正面から撮影したように補正することができる。

## 完成品
今回のためにベルナルド兄貴に一肌脱いでもらいました。

![fitboxing.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/13bfbf87-7263-0f16-95d4-3d3ca1566bb4.gif)



### 工夫1
マーカー検出結果はid順に並んでいるわけではない。
それを左上はid=0、右上はid=1、…という四角形として扱うのに苦心した。
`in`演算子でリストの中に特定の要素があるかどうかを調べることができる。そのことは知っていたが、`set()`を使えば部分集合的な扱いを判定できるということを知ったときには感動した。

### 工夫2
画像1枚ではなく動画の毎フレームに対してマーカー検出しようとすると、マーカーを4点検出できず四角形を取得できないことがある。
それを避けるため、四角形を正しく取得できたときはその画像を保持しておき、次のフレームで四角形が得られなかったときに前の図形を使うという処理を織り込んだ。

以上の工夫を織り込んだソースはこちら。
2年前の動画では使わなかったクラスを使っている。

### ソースコード

<details><summary>折りたたみ</summary>

```Python
import cv2
from cv2 import aruco
import numpy as np

class Camera():
    def __init__(self):
        self.cam = cv2.VideoCapture(0)
        self.dic_aruco = aruco.Dictionary_get(aruco.DICT_4X4_50)
        self.frameW = int(self.cam.get(cv2.CAP_PROP_FRAME_WIDTH))
        self.frameH = int(self.cam.get(cv2.CAP_PROP_FRAME_HEIGHT))
        
        # マーカー四角形の指定
        self.preset_ids = [0, 1, 2, 3]              # 現実世界で貼るマーカーのid
        self.preset_corner_ids = [2, 3, 0, 1]       # 各マーカーのどの頂点の座標を取得するか
        self.rectW, self.rectH = 600, 360           # 長方形のサイズ
        self.pts2 = np.float32(
            [(0,0), (self.rectW,0), (self.rectW, self.rectH), (0,self.rectH)])   # 長方形座標

        # 一つ前の有効画像の初期値　画像取得できなかったときに使う
        self.last_frame = np.zeros((self.frameH, self.frameW, 3), np.uint8)
        self.last_rect = np.zeros((self.rectH, self.rectW, 3), np.uint8)

    def get_rect(self):
        ret, frame = self.cam.read()                    # カメラ映像を取得する
        if ret:                                         # 映像を正しく取得できたら
            self.last_frame = frame                     # 一つ前のフレームとしても覚えておく
        else:                                           # 映像を正しく取得できなかったら
            frame = self.last_frame                     # 前のフレームを使う

        # マーカー検出
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)                  # グレースケールにする
        corners, ids, _ = aruco.detectMarkers(gray, self.dic_aruco)     # マーカー検出
        image1 = aruco.drawDetectedMarkers(frame.copy(), corners, ids)  # 検出結果を重ね書き

        # 4個のマーカーから四角形を得る
        list_ids = np.ravel(ids)                        # idsを一次元化する
        if set(self.preset_ids) <= set(list_ids):       # 検出結果に4個のidが含まれていたら
            pt = {}
            for id, corner in zip(list_ids, corners):   # まずは検出結果について
                pt[id] = corner[0]                      # idとcornerを紐づける
            pts = []                                    # 次に事前登録した4個のidについて
            for id, corner_id in zip(self.preset_ids, self.preset_corner_ids):
                pts.append(pt[id][corner_id])           # 特定の頂点の座標を順にリストに追加する
            pts1 = np.float32(pts)                      # 投影変換する前の四角形

            M = cv2.getPerspectiveTransform(pts1, self.pts2)                # 投影行列
            rect = cv2.warpPerspective(frame, M, (self.rectW, self.rectH))  # 長方形画像を得る

            pts1 = np.int32(pts1)                                           # 今度は整数にして
            image1 = cv2.polylines(image1, [pts1], True, (0,0,255), 2)      # 各頂点を線で結ぶ  
            self.image2 = rect                          # 投影変換で得られた長方形画像
            self.last_rect = rect                       # 一つ前の長方形画像としても覚えておく

        else:                                           # 検出結果に4個のidが含まれていなかったら
            self.image2 = self.last_rect                # 前の長方形画像を使う

        self.image1 = image1 

    def show(self):
        cv2.imshow("origin", self.image1)
        cv2.imshow("rect", self.image2)

def main():
    camera = Camera()
    while True:
        camera.get_rect()
        camera.show()
        key = cv2.waitKey(1) & 0xFF
        if key == 27:                   # esc key
            break
    cv2.destroyAllWindows()

if __name__ == "__main__":
    main()
```
</details>


## マーカーエリアに別画像を貼り合わせる
このテクニックを逆にすれば斜めっているエリアにそれっぽく別画像を貼ることができる。
昔なつかしバザールでござーるのマウスパッドがスクリーンに早変わり。これぞARだ！ 
ただし動画撮影のために上のコードを少し変更しただけで完成度は低いのでソースコードの公開は無し。

動画は[NHKクリエイティブ・ライブラリー](https://www2.nhk.or.jp/archives/creative/material/view.cgi?m=D0002060309_00000)より。

![aruco.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/ccfc57a3-588e-0a70-c9ff-755066f55c95.gif)

# 終わりに
昔の制作物を人様に見せるためにリファインするのもなかなかいいものだ。
OCRの部分はまた別の記事で。