5.28. QR code 與 AprilTag

目前為止介紹的偵測器——色塊、線條、圓形、矩形——尋找的是幾何特徵:位置與輪廓,再交由後續階段加以解讀。剩下的偵測器則尋找符號特徵:其視覺結構專為編碼某段酬載而存在的印刷圖案。相機定位這些圖案,解碼器讀出位元,回傳的不是位置,而是圖案印製者刻意選定的字串(或ID)。

在小型相機應用中,有兩大類符號最為常見。QR code 可攜帶任意文字、URL、聯絡名片或二進位酬載——這是出現在海報、包裝與登機證上、面向消費者的 2D 碼。AprilTag 則攜帶一個取自固定小集合的數字 ID,即使在遠距離也能快速解碼,並且(在提供鏡頭內參的情況下)可回報相機座標系中的 6-DoF 姿態(pose)——這是面向機器人領域的 2D 碼,用來標記無人機、校正標靶與基準標記。兩種偵測器都會回傳結果物件,使用與色塊及矩形偵測器相同的邊界框詞彙,但其酬載讓它們與目前介紹過的任何東西都截然不同。

5.28.1. QR code

find_qrcodes() 會掃描影格中的 QR code,並回傳一份 QRCode 結果物件的清單:

codes = img.find_qrcodes()

for c in codes:
    img.draw_rectangle(c.rect, color=(0, 255, 0))
    for corner in c.corners:
        img.draw_circle((corner[0], corner[1], 4),
                        color=(0, 255, 0))
    print(c.payload)

此偵測器接受單一可選的 roi 參數以限制搜尋範圍。它需要灰階輸入——色彩影格會在解碼前於內部轉換。

每個偵測結果都帶有邊界框(xywhrect)、四個偵測到的角點(corners,即 QR code 定位圖案所描繪出的投影四邊形),以及以字串表示的已解碼酬載。標註偵測結果時,角點才是該繪製的對象——偏離正視角度觀看的 QR code 並非與軸對齊,邊界框只給出一個寬鬆的輪廓。

解碼器中繼資料涵蓋 QR 解碼器一路上得知的所有資訊。version 是 QR code 版本,範圍 1 -- 40,它決定模組格點大小(版本 1 的碼寬 21 個模組,版本 40 的碼寬 177 個模組)。ecc_level 是錯誤更正等級(0 -- 3 對應 L / M / Q / H);等級越高保留越多碼字用於錯誤更正,能承受更多損壞,但代價是酬載空間更少。mask 是編碼器為了將解碼器混淆降到最低而挑選的遮罩圖案(0 -- 7)。data_type 是解碼器回報的編碼方式——數字、文數字、二進位或漢字——而 is_numeric / is_alphanumeric / is_binary / is_kanji 旗標則以更友善的布林值揭露相同的值。

eci 是延伸通道解讀(Extended Channel Interpretation)值,用以辨識位元組所採用的文字編碼(UTF-8、ISO-8859-1 等等)。來自任意印刷品的 QR code 未必保證是 UTF-8;需要正確解碼位元組的應用程式會檢查 eci 並據以解碼。漢字的情況尤其特殊:MicroPython 並不剖析漢字編碼,因此 is_kanji 的酬載必須當作位元組陣列來處理,並由應用程式自行解碼。

一個典型用途:相機讀取輸送帶上的 QR code,並把已解碼酬載回報給主機。Cam 每影格執行一次 find_qrcodes(),迭代回傳的清單,挑出 data_type 符合應用程式預期的碼,並透過 UART 或 USB 轉發 c.payload。邊界框與角點資料對 IDE 預覽很有用,但並非主機所關心的內容。

5.28.2. AprilTag

find_apriltags() 會掃描影格中的 AprilTag,並回傳一份 AprilTag 結果物件的清單:

tags = img.find_apriltags(families=image.TAG36H11)

for t in tags:
    img.draw_rectangle(t.rect, color=(0, 255, 0))
    img.draw_cross(t.cx, t.cy, color=(0, 255, 0))
    print(t.id, t.decision_margin)

AprilTag 在設計目標上與 QR code 不同。QR code 的設計是要在單一密集符號中編碼任意資料,使用者在近距離一次讀取。AprilTag 的設計則是要在稀疏符號中編碼一個小型 ID,讓相機從遠處連續讀取,並具備其家族漢明碼所允許的最大容錯能力。這項取捨在兩個方向上都顯而易見:QR code 可攜帶數百位元組,但需要近距離讀取;AprilTag 僅攜帶數百個唯一 ID,卻能在數公尺外可靠讀取。

families 關鍵字接受要解碼的標記家族的位元遮罩。可用的家族有 image.TAG16H5image.TAG25H9image.TAG36H10image.TAG36H11image.TAGCIRCLE21H7image.TAGCIRCLE49H12image.TAGCUSTOM48H12image.TAGSTANDARD41H12image.TAGSTANDARD52H13。每個家族都在 ID 數量與穩健性之間做取捨。名稱中的 H 數字是該家族中任兩個碼之間的最小漢明距離——亦即須翻轉多少位元,一個有效碼才會變成另一個——TAG16H5 在距離 5 下有 30 個 ID,TAG25H9 在距離 9 下有 35 個 ID,而 TAG36H11(預設且最常用)在距離 11 下有 587 個 ID。無論哪個家族,偵測器最多更正兩個位元錯誤,因此距離決定了這項更正的風險高低:在雜訊影格中的隨機圖案,只要落在某個有效碼的兩位元範圍內就會被解碼為誤判,而距離較高的家族把碼分布得更為稀疏,使這類碰撞變得罕見——這正是建議選用 TAG36H11 的原因。偵測時間會隨啟用的家族數量增加,因此應用程式只應啟用實際印製的家族。當單次呼叫需要多個家族時,此位元遮罩就是各家族常數的位元 OR。

每個偵測結果都帶有邊界框詞彙——xywhrectarea,以及整數與次像素質心(cxcycxfcyf)——還有四個偵測到的角點(corners)。接著是辨識欄位:id 是家族內的數字 ID(TAG36H11 為 0 -- 586),family 是數字家族常數,而 name 是以字串表示的家族名稱。

匹配品質欄位是應用程式用來過濾偵測結果的依據。decision_margin 是 0.0 -- 1.0 的信心分數;越高越好,過濾掉 decision_margin > 0.1 以下的偵測結果可在零成本下清除大多數虛假命中。hamming 計算解碼器為此標記接受的位元錯誤數——越低越好,0 表示完美解碼。goodness 是一個歷史性的影像品質指標,目前的解碼器已不再計算;它永遠是 0.0,可以忽略。

5.28.3. 由內參求姿態

find_apriltags() 的關鍵特性——也是讓 AprilTag 成為機器人首選基準標記的原因——在於此方法能直接從偵測到的角點與一小組校正內參,還原出標記在相機座標系中的 6-DoF 姿態。這些內參是相機以像素為單位的 X 與 Y 焦距(fxfy)以及以像素為單位的光學中心(cxcy),這四項皆由應用程式以校正程序量測一次後便寫死沿用。

提供內參後,回傳的 AprilTag 會以標記相對於相機的位置填入其 x_translationy_translationz_translation 欄位,並以標記的方向填入 x_rotationy_rotationz_rotation(以及為對稱而重複的 rotation)。若未提供內參,這六個欄位皆為 0.0,姿態估計需由應用程式自行負責。

平移欄位以標記寬度為單位回報:解碼器將標記視為 1 個單位寬,因此應用程式須將每個平移量乘以印製標記的實際寬度才能得到公制距離。一個印製寬度為 100 mm、回報 z_translation = 8.3 的標記距離相機 830 mm;同一標記若以 50 mm 寬印製並置於相同距離,則會回報 z_translation = 16.6。旋轉欄位以弧度表示,無需縮放。

姿態估計是各式機器人應用的基礎:讓機器人停靠到以標記標示的充電站、沿著印製的航點路徑行進、從環境中多個已知標記還原相機自身的姿態。一台知道內參、看見某個標記、且擁有該標記真實世界位置的相機,依照同樣的運算,便也擁有了自己的真實世界位置。

5.28.4. 何時該選哪一種

QR code 與 AprilTag 解決的是不同的問題。兩者之間的抉擇歸結於印製符號攜帶的是什麼

當應用程式需要透過印製符號攜帶任意資料——一個 URL、一串序號字串、一筆聯絡記錄——QR code 是正確的選擇。一個中等大小的碼可容納數百位元組,其編碼公開且每支智慧型手機都支援,而且解碼器能應付旋轉、中度損壞與傾斜角度。

當應用程式需要一個從遠處連續讀取、並可選擇性附帶姿態的小型 ID——移動機器人上的基準標記、室內的校正標靶、充電站上的停靠標記——AprilTag 是正確的選擇。數百個 ID 對這類使用情境綽綽有餘,漢明碼能從會擊垮 QR code 的位元錯誤中復原,而且一旦校正好內參,姿態估計便是免費附帶的。

有些應用程式兩者並用:一個 AprilTag 標示某個已知位置,而旁邊一併印製的相關 QR code 則攜帶關於該位置意義的中繼資料。這兩個偵測器在同一影格上各自獨立執行,應用程式再藉由相互關聯它們的邊界框,把每個標記與其搭配的碼配對起來。