5.3. 像素格式

偵測邊緣的演算法預期每個像素都帶有亮度值;追蹤彩色物件的演算法預期每個像素都帶有色彩;執行形態學閉運算的演算法則預期每個像素不是開就是關。Image 所帶有的像素格式(屬於 Vision Sensors 目錄列舉出的其中一種),正是讓這些預期能夠事先被檢驗的關鍵:格式預先說明了像素呈現的形式,從而決定哪些演算法可以在不經過轉換步驟的情況下直接對其執行。

本頁要談的是這項限制在實務上如何發揮作用。哪一種格式是正確的選擇,取決於管線(pipeline)要做什麼,而格式之間的轉換方法,則是讓一條需要同時用到多種格式的管線能將各個階段串接起來的方式。

五條帶標籤的位元組配置條垂直堆疊。BINARY 顯示一個位元組被切成八個單位元儲存格,標示為「每位元組 8 個像素」。GRAYSCALE 顯示三個帶標籤的單位元組儲存格,各標示為「1 個像素」。RGB565 顯示兩個相鄰位元組,位元欄位為 RRRRR GGGGGG BBBBB,標示為「1 個像素」。YUV422 顯示四個帶標籤的位元組儲存格 Y0、U、Y1、V,標示為「2 個像素」。BAYER 顯示兩列各四個帶標籤的位元組儲存格:上列為 R G R G,下列為 G B G B。

五種未壓縮的像素格式以及其位元組的封裝方式。JPEG 與 PNG 沒有畫在這裡,因為它們是長度可變的壓縮串流,而非固定大小的像素網格。

5.3.1. 灰階主力格式

古典機器視覺大多歸結於處理亮度值。邊緣偵測、樣板比對、AprilTag 解碼、光流估計、形態學運算子、色塊分析——在演算法運作的層次上,它們全都是在觀察每個像素有多亮,以及該亮度與鄰近像素亮度之間的比較關係。場景的色彩對於呼叫這些演算法的應用程式往往有用,但演算法本身並不需要它。

灰階格式正好把這些交給演算法,且沒有任何額外開銷。每個像素一個位元組,存放從 0(黑)到 255(白)的亮度值。此格式只有 RGB565 與 YUV422 的一半大小,是 RGB888 的三分之一,因此每項運算都經過較少的資料——既更快、緩衝區壓力也更小。在較小的相機上,影格緩衝區要與指令碼的其餘部分爭奪 RAM,這項佔用空間的差異可能正是決定一條管線是否裝得下的關鍵。如果色彩不是演算法所需的線索,灰階就是正確的答案。

5.3.2. 透過 RGB565 呈現色彩

當色彩正是所需的線索時——追蹤彩色標記、區分紅蘋果與青蘋果、依色相挑出某個 UI 元素——每像素兩個位元組買到的色彩,足以應付這些演算法所執行的分類。RGB565 是相機上的預設色彩格式,也是介面上具備色彩感知能力的方法所預期的格式。

繪製帶註解的影格——繪製偵測框、寫入診斷文字、將影格顯示到螢幕上或傳送給遠端檢視器——同樣自然會用到 RGB565。OpenMV IDE 預覽、板載顯示控制器,以及大多數網路目的地,要嘛直接使用此格式,要嘛能以低成本從它轉換。

5.3.3. 作為儲存格式的 Bayer

Bayer 影像是原始的感測器輸出,是在 ISP 將其去馬賽克(debayer)成完整色彩表示之前的狀態。每個像素是一個位元組,存放單一色彩通道——也就是該位置馬賽克上的彩色濾鏡所通過的那個通道。這使得 Bayer 影像與灰階影像同樣大小,且為 RGB888 的三分之一,這正符合 Bayer 真正的用途:當 RAM 是約束條件時,一次儲存大量影格。

問題在於 image 模組中的演算法並不直接在 Bayer 影像上運作。在尚未去馬賽克之前,沒有任何單一像素帶有足夠資訊可獨力做出色彩判斷,而演算法所尋找的樣式——邊緣、角點、色塊——會被馬賽克所扭曲。讀取或修改 Bayer 影像的唯一方式是 get_pixel()set_pixel();其餘一切都預期完整的表示。

由此衍生出的模式是:只要影格還需要待在佇列中,就以 Bayer 形式儲存,並在每個影格實際開始處理的那一刻,才將它轉換成灰階或 RGB565。轉換會耗費 CPU 週期,但省下了原本會因在應用程式生命週期內持有完整影格而被佔用的 RAM。

備註

image 模組唯一直接針對 Bayer 像素的運算是 get_pixel()set_pixel(),以及供應 OpenMV IDE 預覽或遠端檢視器的 JPEG 編碼路徑。繪製、分析與濾波全都需要先轉換成灰階、RGB565 或二值格式。

5.3.4. 兼顧兩者的管線適用 YUV422

YUV422 將每個像素的資訊分離成一個亮度通道(Y)與兩個色度通道(U 與 V),並對色度進行次取樣,使相鄰的像素對共用單一個 U 與單一個 V。每像素的平均位元組數為二——與 RGB565 相同——但它們的排列方式使得 Y 通道本身就是一張連續的 8 位元灰階影像,位於緩衝區中已知的位移處。

當一條管線的某些階段做灰階處理、某些階段又需要色彩時,這種排列正是它想要的。為灰階階段直接讀取 Y 值,省去了明確轉換的成本;而當後續階段真正需要色彩時,U 與 V 通道也都在那裡。在這個特定模式之外,色彩處理通常以 RGB565 為較簡單的選擇,純亮度處理則以灰階為較簡單的選擇——YUV422 的價值在於能同時把兩者都做好。

備註

image 模組對 YUV422 的運作方式比對灰階、RGB565 或二值格式更受限——僅限為灰階處理直接讀取 Y 通道,以及供應 OpenMV IDE 預覽或遠端檢視器的 JPEG 編碼路徑。具備色彩感知能力的方法預期 RGB565;YUV422 影格在進行色彩分析或繪製之前,需要先經過明確的轉換。

5.3.5. 二值、遮罩與閾值化輸出

二值影像是每像素一個位元:每個像素不是 0 就是 1。此格式很少作為感測器擷取的形式出現;它通常作為閾值化的自然輸出(其中色彩或亮度測試會將每個像素分類為「是,符合」或「否,不符合」),以及作為形態學運算與許多方法所接受的 mask 引數的自然輸入。

此格式的實務優勢在於其大小。二值影像只佔灰階影像八分之一的空間,因此攜帶一個大型遮罩——逐像素地選擇某個下游運算應觸及哪些位置——成本低廉。許多運算接受二值影像作為 mask= 關鍵字引數,這一點正是同一觀點的另一面:此格式很小,而將一個階段的二值輸出串接到另一個階段的遮罩輸入,是一種常見的管線模式。

5.3.6. 處於邊界的 JPEG 與 PNG

JPEG 與 PNG 的 Image 物件與目錄上的其他格式不同。它們不是像素網格;它們是壓縮的位元組串流,以一種像素層級運算無法讀取的形式編碼像素資料。對 JPEG 呼叫 get_pixel() 並不會回傳某位置的像素;該像素並未以解封裝形式存放在緩衝區中的任何地方供該方法擷取。

JPEG 與 PNG 出現在影像處理的邊界,也就是像素資料以壓縮形式離開或進入相機之處。將影格以 JPEG 存檔可讓檔案保持小巧;以 JPEG 透過網路傳送影格可讓傳輸成本低廉;從 JPEG 檔案載入參考影格可讓它以比原始像素小得多的形式留在磁碟上。對上述任何一種使用情境而言,壓縮表示都是正確的答案。不過,若要對 JPEG 進行任何實際處理,應用程式得先將它轉換成可處理的格式——而那次轉換正是壓縮位元組被展開成像素、緩衝區膨脹(一個 30 KB 的 JPEG 可能變成 600 KB 的 RGB565)實際發生的地方。

5.3.7. 在格式之間轉換

轉換路徑正是將不同格式縫合成單一管線的方式。Image 類別上有五個方法,它們接受一張既有影像並回傳一張採用不同格式的新影像:

  • to_grayscale() 產生每像素單位元組的影像,也就是古典演算法所要的格式。

  • to_rgb565() 產生每像素兩位元組的色彩格式,也就是具備色彩感知能力的方法與 OpenMV IDE 預覽兩者都通用的格式。

  • to_bitmap() 產生每像素一位元的二值影像,也就是形態學運算與 mask 引數所接受的格式。

  • to_jpeg() 產生 JPEG 壓縮影像,適合用於存檔或傳輸。

  • to_png() 在偏好無損編碼而非 JPEG 的較小檔案時,產生 PNG 壓縮影像。

預設情況下,每次轉換都是就地執行:來源影像的緩衝區會被轉換結果覆寫,呼叫返回後來源原本的像素就消失了。這對 CPU 與記憶體而言都是成本最低的選項,且當來源影格不會再被用於其他用途時,這就是正確的答案。

當來源仍然需要時——當管線的後續階段必須看到原始影格時——有兩個關鍵字引數可覆寫就地的預設行為。copy=True 會在 Python 堆積上為轉換後的影像配置一個獨立的緩衝區,並讓來源保持完整。copy_to_fb=True 進行相同的配置,但改放在影格緩衝區中而非堆積上——當轉換後的影像需要呈現在 OpenMV IDE 預覽中時,應用程式就會採用這個方式,因為 OpenMV IDE 是從影格緩衝區讀取的。

另有兩個方法會透過調色盤(palette)而非直接轉換來為 RGB565 影像上色。to_rainbow() 將每個單通道輸入值對映到一條貫穿可見光譜的平滑漸層上的色彩。to_ironbow() 將每個輸入值對映到非線性的熱像儀調色盤,該調色盤從黑色經暗紅與橘色一路到白色。兩者都是視覺化工具而非量測工具;其重點在於讓一張單通道影像(其原始值原本對肉眼是不可見的)變得一目了然。

5.3.8. 緩衝區大小

關於格式,最後還有一個值得明確說明的細節。size() 永遠回報的是位元組緩衝區大小,而非像素數。對於未壓縮格式,這直接由尺寸與每像素位元組數推得:width * height * bytes_per_pixel。對於 JPEG 與 PNG,它是壓縮串流的大小,會隨場景內容逐影格變化。從位元組預算配置緩衝區的程式碼,前一種情況使用 size();而從相機串流壓縮影格輸出的程式碼,則在每次壓縮後讀取它,以得知串流實際包含多少位元組。