opencv 4.8 pythonでのAR

最新のopencvをPythonから動かしたときネットに転がっているコードが関数名の変更などで動かなかったので2023.9時点で動くコードを載せる．
[公式ページ](https://docs.opencv.org/4.8.0/d5/dae/tutorial_aruco_detection.html)もあるがPython用の解説はあまり載っていないのでVSコードのサジェスチョンからそれっぽい関数名のものを探してきて動くようにしたもので動きはするが想定している使い方ではない可能性が無きにしも非ず．

# 環境
    python 3.8.7
    cv2 4.8.0
    numpy 1.24.4

# ARマーカーの作成
もっとも単純なもの
以下のような画像が生成される
![mark_id_00.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3494165/ea189925-0337-0c2a-132d-bc5bdc351a34.jpeg)

```python
# マーカーの保存先
dir_mark = "./marker"

# 生成するマーカー用のパラメータ
num_mark = 20 #個数
size_mark = 6 #マーカーのサイズ

# マーカー種類を呼び出し
dict_aruco = aruco.getPredefinedDictionary(aruco.DICT_4X4_50)

for id_mark in range(num_mark) :

    img_mark = aruco.generateImageMarker(dict_aruco, id_mark, size_mark)# 最新

    img_name_mark = 'mark_id_' + str(id_mark).zfill(2) + '.jpg'
    path_mark = os.path.join(dir_mark, img_name_mark)

    cv2.imwrite(path_mark, img_mark)
```


以下はマーカー自体に余白を設けて，マーカーのIDも画像に入れるようにしたもの．運用上は便利
```python
import numpy as np
import os
import cv2
from cv2 import aruco

# マーカーの保存先
dir_mark = "./marker"

# 生成するマーカーの種類
# サイズ：4x4~7x7,  枚数：50,100,250,1000
marker_type=aruco.DICT_4X4_50

# 生成するマーカー用のパラメータ
marker_num = 20 #個数
marker_size = 1000 #マーカーのサイズ

# 余白[%]
margin_right = 0.1
margin_left  = 0.1
margin_top   = 0.1
margin_bottom= 0.1

# id のプリント
font_size=1

# ディレクトリ作成
os.makedirs(dir_mark,exist_ok=True)

# マーカー種類を呼び出し
dict_aruco = aruco.getPredefinedDictionary(marker_type)

for id in range(marker_num) :
    # マーカー用背景画像（白）
    img_mark=np.ones((int(marker_size*(1+margin_top+margin_bottom)),
                      int(marker_size*(1+margin_left+margin_right))))*255
    
    # マーカーを書き込む場所の計算
    x1=int(margin_left*marker_size)
    x2=x1+marker_size
    y1=int(margin_top*marker_size)
    y2=y1+marker_size
    
    # マーカーの書き込み
    img_mark[x1:x2,y1:y2] = aruco.generateImageMarker(dict_aruco, id, marker_size)

    # ファイル名
    marker_name="aruco."+[i for i in aruco.__dict__.keys() if aruco.__dict__[i]==marker_type if "DICT" in i][0]+" - "+str(id).zfill(3)
    
    # ファイル名の画像への書き込み
    if font_size!=0:
        cv2.putText(img_mark,marker_name,(0,img_mark.shape[1]-3),cv2.FONT_HERSHEY_PLAIN,font_size*marker_size/1000,0,int(font_size*marker_size/1500))

    # 画像の保存
    path_mark = os.path.join(dir_mark, marker_name+".jpg")
    cv2.imwrite(path_mark, img_mark)
```

# ARマーカーを認識してマーカー上に立方体を表示
カメラのマトリックスはopencvでキャリブレーションで求めることができるが今回はめんどくさいのでうまく動く適当な値を入れてある．あまりに歪曲のあるカメラはを利用する場合はしっかりキャリブレーションする必要がある．
```python
# カメラの読込み
# 内蔵カメラがある場合、下記引数の数字を変更する必要あり
cap = cv2.VideoCapture(0)

ret, frame = cap.read()
size = frame.shape

focal_length = size[1]
center = (size[1]/2, size[0]/2)

fx,fy,cx,cy=focal_length,focal_length,center[0],center[1]
cameraMatrix=np.array([[fx,0,cx],[0,fy,cy],[0,0,1]])
k1,k2,p1,p2=0,0,0,0
distCoeff=np.array([[k1,k2,p1,p2]])
distCoeff=np.zeros((4,1))

detector=aruco.ArucoDetector(dict_aruco)

# 動画終了まで、1フレームずつ読み込んで表示する。
while(cap.isOpened()):
    # 1フレーム毎　読込み
    ret, frame = cap.read()

    corners, ids, rejectedCandidates = detector.detectMarkers(frame)

    if len(corners)>0:
        for points,id in zip(corners,ids):

            image_points_2D = np.array(points[0],dtype="double") #画像上の座標(マーカー認識の結果)
            figure_points_3D = np.array([ # 画像上の点の３次元空間での座標
                            ( 0.5, 0.5, 0.0),
                            ( 0.5,-0.5, 0.0),
                            (-0.5,-0.5, 0.0),
                            (-0.5, 0.5, 0.0),
                        ])
           
            objPoints=image_points_2D
             # 上記を対応させて姿勢などを算出する
            suc,rvec, tvec=cv2.solvePnP(figure_points_3D, image_points_2D,cameraMatrix,distCoeff)
            cv2.polylines(frame, np.array(points).astype(int), isClosed=True, color=(0, 255, 0), thickness=1)
            cv2.drawMarker(frame, np.array(points[0][0]).astype(int), color=(255, 0, 255), markerType=cv2.MARKER_SQUARE, thickness=1,markerSize=10)
            cv2.putText(frame,str(id[0]),np.array(points[0][0]).astype(int), fontFace=cv2.FONT_HERSHEY_SIMPLEX,fontScale=1.0,color=(255, 0, 0),thickness=2,lineType=cv2.LINE_AA)

            # 高さにあたる辺の描画
            for point2,point3 in zip(image_points_2D,figure_points_3D):

                end_point3D = point3+np.array([[0,0,1]])
                start_point2D= np.array([[point2]])

                end_point2D, jacobian = cv2.projectPoints(end_point3D, rvec, tvec,cameraMatrix,distCoeff)

                point1 = ( int(start_point2D[0][0][0]), int(start_point2D[0][0][1]))
                point2 = ( int(end_point2D[0][0][0]), int(end_point2D[0][0][1]))

                cv2.line(frame, point1, point2, (0,255,0), 1)

            # 上面に対応する辺の描画
            for i in range(4):
                end_point3D = figure_points_3D[i]+np.array([[0,0,1]])#[[-0.5  0.5  1. ]]
                end_point2D, jacobian = cv2.projectPoints(end_point3D, rvec, tvec,cameraMatrix,distCoeff)
                point1 = ( int(end_point2D[0][0][0]), int(end_point2D[0][0][1]))
                
                start_point3D= figure_points_3D[(i+1)%4]+np.array([[0,0,1]])
                start_point2D, jacobian = cv2.projectPoints(start_point3D, rvec, tvec,cameraMatrix,distCoeff)
                point2 = ( int(start_point2D[0][0][0]), int(start_point2D[0][0][1]))

                cv2.line(frame, point1, point2, (0,255,0), 1)

                if i==0:
                    cv2.drawMarker(frame, point1, color=(255, 255), markerType=cv2.MARKER_SQUARE, thickness=1,markerSize=10)

    # GUIに表示
    cv2.imshow("Camera", frame)
    # qキーが押されたら途中終了
    if cv2.waitKey(1) == ord('q'):
        break

# 終了処理
cap.release()
cv2.destroyAllWindows() 
```

# 動作の様子
<iframe width="640" height="480" src="https://www.youtube.com/embed/O5Tw9Hw0Ar8" title="AR 動作デモ" Frameborder="0" allowed="加速度計; 自動再生; クリップボード-書き込み; 暗号化メディア; ジャイロスコープ; ピクチャーインピクチャー; Web 共有"allowfullscreen></iframe>


# パラメータ調整
初期設定ではWebカメラレベルのカメラで高速にAR認識をするように調整されている．
以下は4Kカメラでの位置検出を目的にパラメータを調整した例．認識にかなりの計算量が必要になるがかなり小さいマーカーまで認識するように調整した．解説も載せているので参考までに．
コメントに載っている値がデフォルト値
識別機のオブジェクト作成時に設定を渡せばよい．

```python
# Aruco 設定
# https://docs.opencv.org/4.x/d5/dae/tutorial_aruco_detection.html

# 利用するマーカー種別
dict_aruco = aruco.getPredefinedDictionary(aruco.DICT_4X4_50)
# パラメータオブジェクト
params=aruco.DetectorParameters()

# 適応閾値処理のWindowサイズ
# 小さすぎると輪郭検出になってしまう
params.adaptiveThreshWinSizeMax      = 230     #23
params.adaptiveThreshWinSizeMin      = 20      #3
params.adaptiveThreshWinSizeStep     = 100      #10

# フィルタリングパラメータ
# このパラメータを小さくした方がしっかり検出するが後の計算コストがかかる
# 元の画像のサイズに対して比率で計算される 
params.minMarkerPerimeterRate        = 0.01 # 0.03 640x480 の画像だとマーカー周長が32px 
params.maxMarkerPerimeterRate        = 1      # 4.0  初期値だと画面全体になる 

# ポリゴン近似精度 
# 多角形が正方形に似ているかの閾値
# 候補の直線の長さに対して計算される
params.polygonalApproxAccuracyRate = 0.05 # 0.05 初期値だと100pxの直線で5px

# 最小コーナー距離
# マーカーの周囲に対する相対値
params.minCornerDistanceRate       = 0.05 #0.05 初期値だと周囲100pxの図形で5px

# マーカー間最小距離
# 2つの異なるマーカーからの角のペア間の最小距離
# これは2つのマーカーの小さいマーカー周囲長に対する相対値
# 2つの候補が近すぎる場合、小さい方は無視
params.minMarkerDistanceRate       = 0.05 #0.05

# マーカーのbit抽出時の拡大率
# 大きくすると少し精度が上がるが計算量が増える
params.perspectiveRemovePixelPerCell = 4 #4

# bit抽出時のcellごとの境界の余白
# 少し余白を持たせることにより正確性が増す
params.perspectiveRemoveIgnoredMarginPerCell = 0.4 #0.13

# そのほか姿勢推定のためのコーナー緻密か機能（defaultで無効）
# 誤り訂正の詳細設定などがある

# 識別器オブジェクト
detector=aruco.ArucoDetector(dictionary=dict_aruco,detectorParams=params)
```

以上．もう少し解説を書きたいところだが時間がないのでここまで．