5.11. 影格差分¶
影格差分會將每個新影格與儲存的參考影格進行比較,以找出場景中發生變化的部分。它是各種「監看某件事發生」的相機應用的主力——例如由動作觸發的擷取、入侵警示、「當有物體移動時就儲存影片」——而它完全建構在前面介紹過的逐像素運算之上:取絕對差、套用閾值,以及區域搜尋,並在每個影格上執行。
5.11.1. 基本管線¶
第一階段是取得參考影格。在啟動後的某個時間點——最好是在場景處於「無變化」所代表的狀態時——應用會擷取一個影格並保留下來。這個影格成為後續每次擷取都會用來比較的基準。
reference = csi0.snapshot().copy()
.copy() 很重要。csi0.snapshot() 本身會傳回一個 Image,其緩衝區位於影格緩衝區中,下一次呼叫 snapshot 時會覆寫它。.copy() 會為參考影格配置一個獨立的緩衝區,讓這個影格的像素能在下一次擷取之後依然存在。
第二階段在每個影格上執行:擷取一張新的影像,然後計算它與參考影格之間的絕對差。這正是 difference() 所做的事:
current = csi0.snapshot()
current.difference(reference)
在這次呼叫之後,current 會持有一張影像,其中非零的像素標記出自參考影格擷取以來場景發生變化的每個位置,而每個像素的大小則與該位置變化的程度成正比。
第三階段對差分影像套用閾值。原始的差分結果總是含有一些雜訊:來自感測器散粒雜訊的微小亮度變化、來自光線漂移的梯度變化、來自相機輕微移動的次像素抖動。一次閾值處理——使用 binary() 並將閾值設在該雜訊底限之上——只會保留大到足以視為真正動作的變化,並捨棄其餘的部分,產生一張二值影像,其中非零的像素就是實際發生變化的位置。
第四階段從該二值遮罩中擷取連通區域——也就是形成連續區塊的相鄰非零像素群組。find_blobs() 一次呼叫就能完成這件事,傳回一個動作區域的清單,每個區域都帶有一個邊界框與像素計數,供應用的其餘部分據以運作。
影格差分管線:一個參考影格加上一個目前影格構成一張差分影像;套用閾值將差分轉換成一張標記變化位置的二值遮罩;連通區域步驟再將遮罩轉換成一個動作區域的清單。¶
5.11.2. 記憶體內與磁碟上的參考影格¶
基本管線會將參考影格保存在 RAM 中。當參考影格是在指令碼的這次執行中擷取,且只需要在指令碼持續執行期間存在時,這就是正確的做法。
對於長時間執行的應用——一台應在電源循環後恢復變化偵測的相機,一個需要偵測自某個較早時刻以來任何變化的間歇性指令碼——參考影格必須比正在執行的指令碼存活得更久。其模式是將參考影格儲存到磁碟:
csi0.snapshot().save("/sdcard/reference.bmp")
並在每次執行開始時將它載入回來:
reference = image.Image("/sdcard/reference.bmp")
差分邏輯並未改變;改變的只是參考影格在兩次擷取之間存放的位置。有幾項改進可以很自然地延伸這個磁碟上的變體——例如以計時器自動重新擷取參考影格、可選的滾動平均以追蹤緩慢的光線漂移——但核心的替換做法是相同的。
5.11.3. 光源隔離¶
相同的相減模式也出現在一個略為不同的情境中:將某個光源從場景的其餘部分中隔離出來。其訣竅是擷取一張「燈滅」的參考影格——也就是在被偵測的對象(一個紅外線信標、一個螢幕像素、一個狀態指示燈)未發亮時拍下的影格——並從後續每個影格中減去這張參考影格。其結果在兩次擷取中場景相同之處亮度為零,只有在光源實際發亮之處亮度才不為零。
5.11.4. 選擇 difference 還是 sub¶
關於該選哪一種算術運算的實務說明。difference() 傳回變化的絕對值——不帶正負號——這使它對任一方向的變化(變亮或變暗)都敏感,但代價是無法告訴應用變化往哪個方向發生。對於純粹的動作偵測而言這正是正確的做法:任何移動的東西都值得注意,無論亮度往哪個方向偏移。
對於光源偵測,發亮的像素總是比燈滅參考影格更亮,因此 sub()(會在零處裁切)是較為誠實的選擇。凡是目前影格比參考影格更暗之處(這會是未發亮值附近的感測器雜訊)都會裁切為零,而不會回報出虛假的「燈亮了」訊號。