高精度タグ認識技術AprilTagの検証とその可能性

IXEの大木です。今回、AprilTagについてご紹介 及び 検証結果を共有します。

AprilTagとは?

AprilTagは、二次元バーコードの一種で、その高精度な認識能力からロボット工学やAR(拡張現実)などの分野で広く利用されています。AprilTagの最大の特徴は、小さなサイズでも高精度に認識できることと、各タグが一意のIDを持つため、多数のタグを同時に認識できることです。
二次元バーコードと言えばQRコードのほうが日常生活で広く利用されていて馴染み深いですね。QRコードは任意の情報を埋め込むことができるので様々なケースで活躍できます。一方AprilTagは設定できる情報量は少ないですが、検出精度はQRコードよりもはるかに高いとされています。

AprilTagの活用実例

AprilTagの活用例は多岐にわたります。ロボットの位置認識やARマーカーとしての利用が一般的ですが、その他にもドローンの飛行制御や、物体追跡、空間認識など、さまざまな場面でその高精度な認識能力が活かされています。このようなロボティックな分野だけではなく、最近では自分の体にAprilTagをいくつか貼り付け自分の動きをトラッキングし、VR上のキャラクターに動きをトレースさせる、なんていう研究も進んでいるらしいです。
我々の日常生活の中でAprilTagを見かけることはまだ少ないですが、上記のような流行りの分野でどんどん活用されていっているみたいですね。

現状、下記の分野で使用されているようです。

【物流と倉庫管理】
Amazonなどの大手eコマース企業は、倉庫内での商品管理やロボットによるピッキングのためにAprilTagを使用しています。AprilTagを利用することで、ロボットは正確に商品の位置を特定し、効率的に商品を取り扱う。

【自動運転】
自動運転車の開発企業は、車両のナビゲーションや道路標識の認識にAprilTagを使用しています。AprilTagは車両にとって理想的な視覚マーカーであり、照明や視覚ノイズに影響されにくく、リアルタイムでの高速認識が可能な点を活用しているようです。

【ARとVR】
ARやVRの開発企業は、ユーザーに対する没入感を高めるため、また現実世界とのシームレスなインタラクションを提供するためにAprilTagを使用しています。AprilTagによって、物理空間の特定の地点に対してデジタルコンテンツを配置することが可能。

【製造業】
製造業では、自動化された生産ラインでの部品や製品の追跡、ロボットアームのナビゲーションなどにAprilTagが活用されています。これにより、生産効率を向上させ、エラーを最小限に抑えることが可能。

従来、設備の稼働状況をカウントするのに、PLCとPCを接続したり、設備の電気的な信号を取得する方法が主でしたが
カメラを置くだけで、連続的な動きを検知し簡単なサイクルタイムの取得等。簡単な仕掛けだけであれば、これだけで十分ですよね。
いろんな箇所にタグを貼り付け、それをカウントさせるといった手法です。

検証の目的

今回の検証の主な目的は、社内の技術力向上です。AprilTagの高精度なタグ認識能力を実際に試してみることで、その可能性を探り、新たな技術応用のアイデアを得られればと考えて検証を実施しました。

開発の流れ

開発環境は以下の通りです。

  • 使用したカメラ:BUFFALO BSW305MBK
  • OS:Windows11
  • プログラミング言語:Python 3.11.0
  • 検証に使用したタグファミリー:tag16h5

まず任意のディレクトリにPythonプロジェクトフォルダを作成します。
その中にPythonファイルを新規作成します。今回はmain.pyという名前でファイルを作成しました。
次に、コマンドプロンプトで上記で作成したプロジェクトのルートディレクトリに移動し、pupil-apriltags及びopencv-pythonの2つのライブラリをインストールします。
以下はライブラリ取得のために使用したコマンドです。

pip install pupil-apriltags
pip install opencv-python


その後、Pytonでコードを書きました。コマンドライン引数を自分の環境に合った設定にする必要があります。

import copy
import time
import argparse

import cv2 as cv
from pupil_apriltags import Detector

# コマンドライン引数を解析する関数
def get_args():
    parser = argparse.ArgumentParser()  # コマンドライン引数のパーサーを生成

    # 各種引数を設定
    parser.add_argument("--device", type=int, default=0)
    parser.add_argument("--width", help='cap width', type=int, default=1024)
    parser.add_argument("--height", help='cap height', type=int, default=576)
    parser.add_argument("--families", type=str, default='tag16h5')
    parser.add_argument("--nthreads", type=int, default=1)
    parser.add_argument("--quad_decimate", type=float, default=2.0)
    parser.add_argument("--quad_sigma", type=float, default=0.0)
    parser.add_argument("--refine_edges", type=int, default=1)
    parser.add_argument("--decode_sharpening", type=float, default=0.25)
    parser.add_argument("--debug", type=int, default=0)

    args = parser.parse_args()  # 引数を解析

    return args  # 解析結果を返す

def main():
    # 引数解析 #################################################################
    args = get_args()  # 引数を取得

    # 各種引数を変数に格納
    cap_device = args.device
    cap_width = args.width
    cap_height = args.height

    families = args.families
    nthreads = args.nthreads
    quad_decimate = args.quad_decimate
    quad_sigma = args.quad_sigma
    refine_edges = args.refine_edges
    decode_sharpening = args.decode_sharpening
    debug = args.debug

        # カメラ準備 ###############################################################
    # 指定されたデバイスのカメラをオープン
    cap = cv.VideoCapture(cap_device)
    # カメラの解像度を指定
    cap.set(cv.CAP_PROP_FRAME_WIDTH, cap_width)
    cap.set(cv.CAP_PROP_FRAME_HEIGHT, cap_height)

    # Detector準備 #############################################################
    # AprilTagの検出器を生成
    at_detector = Detector(
        families=families,
        nthreads=nthreads,
        quad_decimate=quad_decimate,
        quad_sigma=quad_sigma,
        refine_edges=refine_edges,
        decode_sharpening=decode_sharpening,
        debug=debug,
    )

    elapsed_time = 0

    # カメラからの映像を連続的に処理
    while True:
        start_time = time.time()  # 処理開始時間

        # カメラキャプチャ #####################################################
        # カメラからフレームを取得
        ret, image = cap.read()
        if not ret:
            break  # フレームが取得できなかった場合、ループを抜ける
        debug_image = copy.deepcopy(image)  # デバッグ用の画像を生成

        # 検出実施 #############################################################
        # 画像をグレースケールに変換し、AprilTagを検出
        image = cv.cvtColor(image, cv.COLOR_BGR2GRAY)

        # 画像を二値化
        _, image = cv.threshold(image, 0, 255, cv.THRESH_BINARY+cv.THRESH_OTSU)

        tags = at_detector.detect(
            image,
            estimate_tag_pose=False,
            camera_params=None,
            tag_size=None,
        )

        # 描画 ################################################################
        # 検出したAprilTagにマーキングを施した画像を生成
        debug_image = draw_tags(debug_image, tags, elapsed_time)

        elapsed_time = time.time() - start_time  # 処理時間を計算

        # キー処理(ESC:終了) #################################################
        # キーボードの入力をチェック
        key = cv.waitKey(1)
        if key == 27:  # ESCが押された場合、ループを抜ける
            break

        # 画面反映 #############################################################
        # 画像をウィンドウに表示
        cv.imshow('AprilTag Detect Demo', debug_image)

    cap.release()  # カメラをリリース
    cv.destroyAllWindows()  # ウィンドウを全て閉じる


# AprilTagにマーキングを施す関数
def draw_tags(
    image,  # 元画像
    tags,  # 検出したAprilTagのリスト
    elapsed_time,  # 処理時間
):
    # 各AprilTagに対して処理を行う
    for tag in tags:
        tag_family = tag.tag_family  # AprilTagのファミリー
        tag_id = tag.tag_id  # AprilTagのID
        center = tag.center  # AprilTagの中心座標
        corners = tag.corners  # AprilTagの四隅の座標

        # 各座標を整数に変換
        center = (int(center[0]), int(center[1]))
        corner_01 = (int(corners[0][0]), int(corners[0][1]))
        corner_02 = (int(corners[1][0]), int(corners[1][1]))
        corner_03 = (int(corners[2][0]), int(corners[2][1]))
        corner_04 = (int(corners[3][0]), int(corners[3][1]))

        # 中心を描画
        cv.circle(image, (center[0], center[1]), 5, (0, 0, 255), 2)

        # 各辺を描画
        cv.line(image, (corner_01[0], corner_01[1]),
                (corner_02[0], corner_02[1]), (255, 0, 0), 2)
        cv.line(image, (corner_02[0], corner_02[1]),
                (corner_03[0], corner_03[1]), (255, 0, 0), 2)
        cv.line(image, (corner_03[0], corner_03[1]),
                (corner_04[0], corner_04[1]), (0, 255, 0), 2)
        cv.line(image, (corner_04[0], corner_04[1]),
                (corner_01[0], corner_01[1]), (0, 255, 0), 2)

        # タグIDを描画
        cv.putText(image, str(tag_id), (center[0] - 10, center[1] - 10),
                   cv.FONT_HERSHEY_SIMPLEX, 0.75, (0, 0, 255), 2, cv.LINE_AA)

    # 処理時間を描画
    cv.putText(image,
               "Elapsed Time:" + '{:.1f}'.format(elapsed_time * 1000) + "ms",
               (10, 30), cv.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2,
               cv.LINE_AA)

    return image  # 描画した画像を返す


# スクリプトとして実行されたときの処理
if __name__ == '__main__':
    main()  # メインの処理を実行

コードが書けたら、カメラで読み取るためのAprilTagマーカーを用意します。AprilTag公式のGitHubページからダウンロードすることができます。
使用したPythonのライブラリやAprilTag導入方法についてネットで情報収集してみると、日本語でも英語でも多くの参考情報を簡単に得ることができました。

検証結果

以下に検証動画を挿入します。1マス3.7×3.7cmのマーカーを最大約2m離れた位置から撮影しても、しっかりIDを認知できています。マーカーを様々な角度から撮影しても高い精度を保持できていることも分かりますね。

センシング関連の技術開発に力を入れている弊社としては今回の検証を通じて、AprilTagの可能性を大きく感じることができました。
また、前知識がほぼゼロの状態から大きく時間をかけずに、WEBカメラでマーカーを認知させることができるところまで導入することができました。
ここまでであれば、学習コストが低いことも分かりました。
実務では検知した情報から、傾き算出/移動量算出/位置算出/動線算出/検知した情報からアクションを起こす等など応用が必要です。

最近はAIを用いた動画解析(画像解析)が主流ですが、機械学習させる必要があるというデメリットがあります。
それに対し、AprilTag技術であれば、機械学習させずとも 簡単に検知 及び 識別が可能というところが、AIを用いた画像処理との違う点かなと感じました。

現在、AprilTagを利用して何か社会のお役に立つことはできないか?と弊社メンバー内で可能性を模索中です。
今後も、このような新しい技術の検証を通じて、社内の技術力向上 及び その技術を社会に提供できる組織を目指していきます。

AprilTagはミシガン大学によって開発され、BSDライセンスの下で提供されており、著作権表示およびライセンス条項の遵守が必要です。
弊社の技術記事は、良識のある範囲で断りなくリンクや引用を行っていただいて構いません。