5.25. 尋找色塊

閾值處理會將擷取到的影格轉換成二值遮罩:每個像素不是通過閾值測試,就是沒有通過。這回答了場景中出現了應用程式關心的哪些色彩,但沒有回答在哪裡——遮罩只是一片 1 與 0 的汪洋。下一步是色塊偵測:走訪遮罩、找出由通過測試的像素所構成的連續區域,並將每個區域以一個物件回傳,該物件帶有位置、大小、方向,以及應用程式可據以動作的其他屬性。

find_blobs() 是這個步驟的主力方法,也是進入 image 模組結果物件世界最常見的入口。追蹤一顆有色的球、循著畫在地面上的線前進、計算熱感測器看到多少個亮點、判斷一顆藍色 LED 是亮著還是熄滅——同一個呼叫就涵蓋了所有這些情境。輸入會變化(閾值、搜尋的區域、套用到結果上的篩選條件),但呼叫的模式始終相同。

5.25.1. 基本呼叫

find_blobs 接受一個閾值串列,並回傳一個色塊結果物件串列:

thresholds = [(30, 100, 15, 127, 15, 127)]  # LAB threshold for red
blobs = img.find_blobs(thresholds)

for b in blobs:
    img.draw_rectangle(b.rect, color=(255, 0, 0))
    img.draw_cross(b.cx, b.cy, color=(255, 0, 0))

每個閾值元組的形式都與傳給 binary() 的閾值相同——對 RGB565 影像而言是六個項目 (l_lo, l_hi, a_lo, a_hi, b_lo, b_hi)(界限以 LAB 表示),對灰階影像而言是兩個項目 (lo, hi)。單次呼叫最多可提供 32 組閾值,這正是 find_blobs() 如此靈活的原因:紅色、綠色與藍色信標可以同時被追蹤,每一種都將自己的色塊貢獻到回傳的串列中,而每個色塊的 code 屬性會標識它所匹配的是哪一組閾值。

上面的 draw_rectangle()draw_cross() 呼叫會在擷取到的影格上加上標註,供 IDE 預覽使用。色塊結果本身就帶有 b.rect(以 4 元組表示的邊界框)以及 b.cx / b.cy(整數質心),所以將偵測結果繪製回影格只需兩次方法呼叫。

5.25.2. 結果包含什麼

每個 Blob 都是一個屬性元組,將偵測器對該區域所量測到的一切打包在一起。這些屬性可分為四組。

邊界框與質心組——xywhrectcxcycxfcyf——描述色塊的位置。rect 是繪製方法所預期的 (x, y, w, h) 4 元組;cxcy 是以整數像素座標表示的質心;cxfcyf 是以次像素浮點座標表示的質心,當上游的校正流程在意小數位置時相當有用。

形狀描述子組——pixelsareadensityperimeterroundnesselongationcompactnessrotation——描述色塊的外觀。pixels 是通過測試的像素數量;area 是軸對齊邊界框的面積(w * h);density 是兩者的比值,對於實心矩形會趨近 1.0,而對於細長的對角線筆畫則會降向 0.0roundnesscompactness 都從不同的幾何角度評分色塊有多圓(roundness 來自二階矩,compactness 來自周長對面積的比值);elongation 為求方便就是 1.0 - roundnessrotation 是主軸方向(以弧度表示),它在細長色塊上最為準確,而在近乎圓形的色塊上則會變得雜亂(軸向模稜兩可時就沒有明確定義的方向)。

閾值與合併中繼資料組——codecount——標識匹配的是哪一組閾值,以及有多少個來源色塊被合併成回傳的這一個。code 是一個 32 位元的位元圖,每匹配一組閾值就設定一個位元(單一閾值會得到 code == 1;合併後的多色色塊可能會設定數個位元);count1,除非 merge=True 將數個偵測結果合併成一個。

角點組——cornersmin_corners——給出色塊的旋轉幾何。corners 是從色塊輪廓擷取出的 (x, y) 極值所構成的 4 元組,從左上角起按順時針排序;min_corners 是包圍色塊的最小面積旋轉矩形的角點 4 元組。最小面積矩形是緊密貼合的;軸對齊的 rect 則是與像素格網對齊的鬆散貼合。兩者各有用途,取決於下游階段需要的是有方向的框還是普通的框。

色塊偵測示意圖,以二值閾值遮罩為背 景。左側面板顯示一個由通過測試的像 素構成的傾斜橢圓遮罩。右側面板顯示 同一個遮罩,並標註了圍繞它繪製的軸 對齊邊界框、在中央以十字標記的質 心、以真實角度緊貼橢圓的虛線最小面 積旋轉矩形,以及穿過質心並沿橢圓長 軸方向指向的主軸線。

色塊帶有軸對齊邊界框(rectxywh)、質心(cxcy 或次像素的 cxfcyf)、最小面積旋轉矩形(min_corners 加上 rotation),以及由下方模組層級輔助函式所計算的選用主軸/次軸線。

5.25.4. 合併重疊的色塊

merge=True 會對結果串列進行後處理,合併那些邊界矩形相互重疊的色塊。最自然的用途是偵測一個目標,由於鏡面高光、陰影線條,或物體上各處光照不一致,相機將其色彩看成多個閾值化區域:一顆紅色的球可能會被回傳為三、四個小的紅色色塊,而它們合在一起才描繪出整顆球。使用 merge=True 後,這三個色塊會變成一個大色塊,rect 涵蓋其聯集,code 是被合併色塊各自 code 的位元 OR(因此多色合併能標識出是哪些色彩參與其中),而 count 則回報有多少個來源色塊被合併。

margin 會在重疊測試前放大或縮小邊界矩形。使用 margin=2 時,邊界矩形彼此相距在 2 像素以內的色塊仍會合併;使用 margin=-2 時,只有邊界矩形重疊至少 2 像素的色塊才會合併。自然的調校方式是:正邊距用來處理被閾值拆成相鄰碎片的色塊;負邊距用來讓緊密成群的不同物體保持分離。

merge_cb 會在每一對候選色塊實際合併之前對其執行。回呼函式接收這兩個色塊,並回傳 True 以允許合併,或 False 以阻止合併。這是用來複查幾何規則遺漏之合併的合適工具——例如,拒絕合併兩個 rotation 角度相差超過某閾值的色塊,或在小色塊只是斑點時拒絕將它合併進一個大得多的色塊。

5.25.5. 投影直方圖

x_hist_bins_maxy_hist_bins_max 會為每個色塊附加選用的投影直方圖。投影直方圖是沿著某一軸的通過測試像素計數:X 軸直方圖統計色塊邊界框內每一欄的通過像素數,而 Y 軸直方圖則統計每一列的通過像素數。兩者都預設為零——除非提供非零的 max,否則不會計算這些直方圖,因為它們否則會為每次偵測增加額外工作。

當它們被計算出來時,這些直方圖提供了一種廉價的一維訊號,應用程式可在其上執行進一步分析:偵測色塊內部垂直條紋的位置、找出雙色目標的分界點、計算長軸方向上出現多少個間隙。它們會被填入每個 Blob 上的 x_hist_binsy_hist_bins 屬性。

5.25.6. 額外的幾何輔助函式

另有少數幾何量測以模組層級函式的形式存在,它們接受一個色塊並回傳所要求的量測值:

5.25.7. 自動學習閾值

色塊偵測器的好壞,取決於它所使用的閾值,而找出目標色彩正確閾值的工作本身就是另一個問題。有兩種常見的模式能減輕這項工作。

第一種是在 IDE 中互動式選取:擷取一個影格,在目標色彩的範例周圍拖曳出一個矩形,讓 IDE 的 閾值編輯器 回報它所看到的 LAB 界限。將那些界限放入指令碼中作為 find_blobs() 的閾值,偵測器便準備就緒。

第二種是程式化的自動學習:在相機上執行的校正常式擷取一個影格,對目標所在的已知色塊取得直方圖(以 roi= 呼叫 get_histogram()),並以 get_percentile() 從直方圖讀出該色塊的數值範圍。第 5 百分位設定每個通道的低界限,第 95 百分位設定其高界限,藉此忽略兩端零星的離群像素。在 RGB565 影像上,一次百分位呼叫便能同時回報全部三個 LAB 通道,因此兩次呼叫就能產生 find_blobs() 所預期的六個數字:

h = img.get_histogram(roi=patch)
lo = h.get_percentile(0.05)
hi = h.get_percentile(0.95)
threshold = (lo.l_value, hi.l_value,
             lo.a_value, hi.a_value,
             lo.b_value, hi.b_value)