4.16. 影像預覽

影格緩衝區池是應用程式讀取影格的地方。當應用程式正在處理那些影格時,連接到相機以預覽它們的任何裝置也需要每個影格的一份副本。相機為此目的設有第二個專用緩衝區,並有一條關於何時填入它的單一規則:每當應用程式呼叫 snapshot() 時,前一個擷取的影格會被複製到預覽緩衝區,然後才把新影格交回。

應用程式與預覽器絕不會爭用同一塊記憶體。應用程式從池中讀取它的影格;預覽器從預覽緩衝區讀取它的影格。兩者並行發生。

4.16.1. 串流影格緩衝區

預覽緩衝區——即串流影格緩衝區——是一塊與影格緩衝區池分開、固定大小的 RAM 區域。其大小在韌體建置時設定,且不會隨 framesize()pixformat() 而改變。在較新的 OpenMV Cam 上通常約為一百萬位元組——足以容納中等解析度的預覽,但遠小於在最大感測器尺寸下的全解析度影格。

應用程式碼不會直接讀寫此緩衝區;相機驅動程式會作為 snapshot() 的副作用填入它。

4.16.2. 快照為預覽所做的事

在每次呼叫 snapshot() 時,驅動程式會在把應用程式的前一個影格緩衝區釋放回池中並交出新的緩衝區之前,將前一個影格複製到預覽緩衝區——連同應用程式在處理過程中疊加在其上的任何繪製內容一併保留在影像上。有兩個可能的分支。執行哪一個是由預覽器選定的,而非由相機決定:開啟預覽的消費端會告訴驅動程式它想要原始影像還是 JPEG,以及它能接受的原始視窗尺寸為何。

  • 原始降尺寸副本。 當預覽器要求原始影格時,驅動程式會以其原生像素格式(RGB565、灰階等)複製前一個影格。若該影格大於預覽器要求的原始視窗,驅動程式會以雙線性濾波將其縮小直到符合;否則像素會原封不動傳遞。沒有壓縮假影;預覽器看到的就是應用程式正在處理的同一批像素。

  • JPEG 壓縮。 當預覽器要求 JPEG 時——或當原始副本根本無法塞進串流緩衝區時——驅動程式會將前一個影格以其完整解析度進行 JPEG 壓縮後存入串流緩衝區。品質會逐影格自適應地調整,以使壓縮輸出維持在串流緩衝區的容量之內。當某個影格塞得下時,驅動程式會把品質朝著一個上限往回提升一級,該上限取決於擷取影格的像素尺寸(較小的影格允許較高的品質;較大的影格上限則壓得較低,以免在內容微幅變動時溢位)。當某個影格塞下時,驅動程式會將目前的品質減半,並在接下來數十個影格中維持此降低後的設定,讓新設定有時間穩定下來,同時將溢位的影格從預覽中捨棄。應用程式迴圈會持續不受影響地運行;只有預覽器會錯過被捨棄的影格。

相機以本已壓縮的格式產生的影格(在直接送出 JPEG 的感測器上的 JPEG 像素格式)會跳過這兩個分支:已編碼的位元串流會原樣直接複製到預覽緩衝區。

預覽器依其自身的排程輪詢,通常遠比相機擷取來得慢,因此它會對原始擷取率進行次取樣:只有它恰好及時讀出的那些快照才會被顯示。若在預覽器讀出前一個影格之前,一個新鮮的 snapshot() 抵達預覽緩衝區,該緩衝區仍被預覽器鎖住,於是新的預覽更新會被略過——該次擷取便從預覽串流中遺失。應用程式自己的影格緩衝區池不受影響;擷取到的影格仍會正常送至應用程式。

4.16.3. 手動推送最後一個影格

由於預覽是作為 snapshot() 的副作用而更新的,因此一個未再次呼叫快照就結束的指令碼,會讓它最後送至預覽的內容無限期地停留在預覽器上——對於那種在第一次快照之前就完成工作然後退出的指令碼來說,這就是一個空白的預覽。image.Image.flush()(或 CSI 物件上等效的 flush())可在不擷取新影格的情況下,按需將應用程式影格緩衝區的目前內容複製到串流緩衝區:

img = csi0.snapshot()
# process the image and draw on it
img.flush()                               # previewer sees the annotated frame

當某個長時間運行的操作位於兩次快照之間,否則預覽器在這整段期間都會顯示過時的預覽時,同一個呼叫也很有用。

備註

預覽應用程式必須在指令碼退出之前,將影格從串流緩衝區讀出。短指令碼結尾的一次 flush 只是把影格暫存起來;若指令碼隨後在預覽器輪詢之前就把控制權交回相機,該緩衝區會在下一次執行時被重複使用,那個最後的影格便會遺失。對於指令碼結尾的預覽,請在指令碼結束前給預覽器一點時間把影格取走(flush 之後短暫睡眠片刻,或乾脆不要立即退出)。