5.18. 直方圖與統計量

除了會改變影像像素的運算之外,Image 類別還提供一系列用來「測量」像素的方法 —— 概括像素值的分布、回傳平均與中位數亮度、找出暗像素與亮像素之間的最佳分界點、回報各色彩通道的離散程度。這些測量結果以兩種方式供應用程式使用:一是作為決策程式碼的輸入,用來決定要採用什麼閾值、設定什麼增益、場景的色調輪廓看起來如何;二是作為診斷訊號 —— 「場景夠亮嗎?」 —— 讓應用程式能據以動作,而不必針對任何特定像素做出判斷。

幾乎每一項測量的起點都是「直方圖」。

5.18.1. 直方圖

影像的直方圖就是統計有多少像素具有每個可能的亮度值。對於灰階影像,這是一份以 0255 的值作為索引的計數清單;對於彩色影像,則是三份這樣的清單 —— 每個通道一份。

get_histogram() 會計算出一個直方圖:

h = img.get_histogram()

回傳的物件是一個 histogram 結果,會公開各通道的箱(bin)計數清單,以及幾項在其上的高階查詢。箱計數經過「正規化」,使其總和為 1.0 —— 直方圖描述的是分布的輪廓,而非絕對的像素數量,這使得不同尺寸影像之間的測量結果可以相互比較。

對灰階影像而言,直方圖有一個箱通道,可透過 h.bins() (或等效的 h[0])取得。對 RGB565 影像而言,直方圖是在二值化閾值頁面所介紹的 LAB 色彩空間中計算的,可透過 h.l_bins()h.a_bins()h.b_bins() (或 h[0]h[1]h[2])取得三個箱通道。LAB 與閾值及追蹤方法所採用的色彩空間相同;直方圖在色彩於哪個空間中被測量這一點上,與閾值是一致的。

5.18.2. 箱與箱數

預設的直方圖每個可能的像素值各有一個箱 —— 對 8 位元通道而言即為 256 個箱。有時這比應用程式所需的解析度還要精細。一個只在意分布大致輪廓的分類器,採用較少的箱數 —— 32 個甚至 8 個箱 —— 可能更合用,這樣既執行得更快,又能在面對雜訊時產生更乾淨的結果。bins 關鍵字(以及色彩專用的各通道 l_binsa_binsb_bins)可設定箱數:

h = img.get_histogram(bins=32)

ROI 與閾值的範圍限定方式,與其他每一種測量方法都相同。傳入 roi 可將直方圖侷限在某個矩形範圍的像素內;傳入 thresholds 清單則只納入符合那些範圍的像素。閾值形式正是讓「只計算符合像素的直方圖」成為單次呼叫操作的關鍵 —— 當應用程式想要刻畫某個已偵測區域的紋理特性,又不必自行逐一走訪像素時,這是常見的模式。

一個以一排長條繪製的灰階直方圖,橫跨 0 到 255 的亮度範圍。此分布有兩個峰 —— 一個較小的暗峰與一個較大的亮峰 —— 中間以明顯的谷分隔。圖上疊加了三條垂直線:位於谷中的 Otsu 閾值、偏向較大亮峰的平均值,以及更靠右、累積像素數達到一半之處的中位數。

一個疊加了三項摘要測量的灰階直方圖:Otsu 閾值(最能將暗、亮兩群分開的分界點)、平均值,以及中位數。每一項測量對同一個分布所表達的意義各不相同。

5.18.3. 統計量

直方圖描述的是每個值出現的頻繁程度;「統計量」則是由它推導出的數值摘要。get_statistics() 回傳的 statistics 物件帶有一組標準的統計量:

  • mean —— 像素值的算術平均。

  • median —— 有一半像素低於此值。

  • mode —— 最常見的單一值。

  • stdev —— 標準差,用以衡量環繞平均值的離散程度。

  • minmax —— 影像中存在的最亮與最暗像素值。

  • lquq —— 下四分位數與上四分位數的分界值。

對 RGB565 影像而言,各通道形式(l_meana_medianb_mode 等等)會逐一通道提供相同的測量結果。

這些數字大多會在特定情境下用到。meanstdev 合在一起可提供雜訊估計:本應均勻的場景其 stdev 很小,而雜訊較大的感測器會使同一場景的 stdev 變大。minmax 給出影像的「對比」:兩者越接近,場景越平淡;兩者相差越大,演算法可運用的動態範圍就越大。當分布有離群值時,median 是穩健的中心(少數幾個極亮的像素不會像拉動平均值那樣拉動中位數)。mode 是最常見的單一值,對於背景占據大多數像素的影像,可用來找出其背景亮度。

get_statistics() 會在內部執行一次直方圖計算,然後加以概括;傳入與先前所計算直方圖相同的 thresholdsroi 引數,便會針對同一組像素產生統計量。

5.18.4. 百分位數與 CDF 查詢

histogram 物件公開了一個 get_percentile() 方法,可將一個比例轉換成像素值 —— 即所要求比例的像素低於此值。h.get_percentile(0.5) 即為中位數;h.get_percentile(0.05)h.get_percentile(0.95) 合在一起,可給出一組忽略最低與最高 5% 離群值的穩健最小/最大值。

當應用程式想要刻畫像素值的「範圍」,又不希望讓少數零星的過亮或過暗像素扭曲結果時,便會使用這種形式。由第 5 與第 95 百分位數得到的穩健最小/最大值,也是對比拉伸(contrast-stretching)運算的天然輸入 —— 也就是色調校正頁面所涵蓋的逐像素重新映射。

5.18.5. Otsu 法

直方圖還能回答另一個值得單獨提出的問題:給定一張像素分裂為「暗」群與「亮」群的影像,兩者之間的分界點在哪裡?閾值頁面已經以其結果為這個機制命名 —— 一個應用程式可交給 binary() 使用的單一全域閾值 —— 但把「如何求得」這部分留待後述。求法即為 Otsu 法,它就存在於直方圖之上。

其直覺如下:具有清晰前景與背景的影像,其亮度直方圖中會有「兩」個群,中間隔著一個谷。適合進行閾值化的位置就在谷底 —— 也就是兩群分離得最開的那個值。Otsu 法會搜尋每一個可能的分界點,並選出群內變異數最小的那一個(這等同於說群間變異數最大),其結果便是針對該特定影像分布的最佳二值分割。

histogram 物件透過 get_threshold 公開 Otsu 法:

h = img.get_histogram()
t = h.get_threshold()

回傳的 threshold 物件具有 value (灰階用)或 l_value / a_value / b_value (彩色用)屬性,攜帶所選出的分界值。將此結果直接回饋給 binary(),便可得到一個由影像本身決定分界值的自我調校全域閾值:

img.binary([(t.value, 255)])

這種模式並未解決以濾波器為基礎的自適應閾值所解決的不均勻照明問題;它所解決的是當全域閾值化本就是正確做法時,「我該以什麼值來切分?」這個問題。對於前景/背景區分明確的場景而言,Otsu 所選出的值通常與人眼所選的值相差不到幾個單位。

5.18.6. 在差異影像上進行計算

關於 get_histogram()get_statistics() 有一個實用的細節:兩者都接受一個 difference 關鍵字,可傳入另一張 Image,並計算來源影像與該影像之間逐像素差異的直方圖(或統計量),「而不」配置一張獨立的差異影像。這是用來詢問「自參考影格以來場景變化了多少?」的省成本做法,無需付出明確呼叫 difference() 以產生一張其唯一用途就是被測量的影像之代價。對於持續運行的動態偵測指令碼而言,這樣的節省會逐漸累積。