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 之后稍作 sleep,或者干脆不立即退出)。