5.32. 保存与压缩

到目前为止,每一页讲述的都是在摄像头上对图像进行的处理:将图像捕获到帧缓冲区中,或在 MicroPython 堆上分配图像,通过 image 模块的方法对其进行操作,然后或者在 IDE 预览中显示,或者在同一脚本中将其送入下游处理阶段。大多数应用在某个时刻都需要做相反的事情:把当前位于 RAM 中的图像放到某个持久化的地方——SD 卡、USB 主机、或通过网络——使摄像头以外的东西能够读取它。

image 模块为这类工作提供了两条路径。save 路径将图像写入文件系统中的文件,文件格式由扩展名决定,编码细节则由方法处理。to-format 路径返回一个 Image 对象,其中包含已编码的字节流,适合直接交给流式处理或网络调用,而完全无需触及文件系统。两者各适用于不同的应用;它们底层都构建在同一套压缩引擎之上。

5.32.1. 保存到文件

save() 将图像写入文件系统中的某个路径:

img.save("/sdcard/capture.jpg")
img.save("/sdcard/capture.bmp")
img.save("/sdcard/region.jpg", roi=(40, 60, 200, 150), quality=85)

格式由文件扩展名选择。可识别五种扩展名:.bmp 写入 Windows 位图(无损、无压缩、与捕获的像素逐字节一致);.pgm 写入 portable graymap(无损、仅限灰度);.ppm 写入 portable pixmap(无损、RGB);.jpg.jpeg 都写入 JPEG(有损、压缩)。接收方图像必须已经处于所选容器对应的正确颜色格式——把彩色图像保存为 .pgm 是一个错误。

roi 将保存范围限制在图像的某个子矩形内,其方式与其他每个 image 模块方法的 roi 关键字相同。默认为整幅图像。在保存 JPEG 压缩图像时该关键字会被忽略,因为磁盘上的形式已经涵盖了整帧,而通过裁剪重新编码会违背保存现有压缩字节的初衷。

quality 是从 0100 的 JPEG 压缩质量,仅在输出为 JPEG 时才有意义(对于无损格式该关键字会被忽略)。默认值 50大多数 应用而言是合适的平衡点;7085 是追求更高视觉质量的区间,3050 适合小缩略图以及带宽受限的传输,而 90 及以上则保留给图像需要人工查看、或要送入对压缩伪影敏感的下游算法的场合。

返回的是接收方图像本身,因此调用可以链式进行:img.save("/sdcard/x.jpg").draw_string(0, 0, "saved")。返回的对象就是内存中那同一幅图像;保存是一个副作用。

一种典型用法是 捕获并记录(capture-and-log)模式。某个触发事件发生(检测到色块、按下按钮、定时器到期);脚本捕获一帧;它将时间戳追加到文件名中;然后调用 save() 把图像推送到 SD 卡。IDE 预览持续运行,下一个触发事件发生,保存的文件不断累积。

5.32.2. 编码到内存

当目标不是文件系统,而是一个网络连接、一个串口、或另一个模块的输入时,应用需要的是 内存中 的已编码字节流,而非磁盘上的文件。to_jpeg()to_png() 正好可以产生它:

encoded = img.to_jpeg(quality=80, copy=True)
bytes_to_send = encoded.bytearray()
sock.send(bytes_to_send)

默认行为是原地转换:接收方被转换为一幅 JPEG(或 PNG)图像,并返回该同一对象。使用 copy=True 时,转换结果写入一个新分配的堆对象;使用 copy_to_fb=True 时,输出落入帧缓冲区。这一选择与其他任何转换方法所提供的相同——默认原地转换,在之后还需要原图时则进行复制。

qualitysubsampling 与 save 路径所暴露的 JPEG 调节旋钮相同。subsampling 选择色度子采样方案:image.JPEG_SUBSAMPLING_AUTO 为所选质量挑选最佳方案,image.JPEG_SUBSAMPLING_444 将色度保持在全分辨率(文件最大,颜色精度最佳),image.JPEG_SUBSAMPLING_422image.JPEG_SUBSAMPLING_420 沿一个或两个坐标轴将色度分辨率减半(文件更小,带来轻微的颜色柔化,在通常的观看距离下不可见)。默认值 AUTO 是正确的选择,除非应用有特定需求。

通过 to_png() 生成的 PNG 是 无损 的,但编码更慢,而且对于照片类内容生成的文件比 JPEG 更大(照片类内容在 PNG 的预测方案下压缩效果很差)。当图像是线条图、屏幕截图,或在捕获帧之上绘制了硬边缘图形时,请使用 PNG——无损编码会保留 JPEG 会柔化掉的锐利边缘。否则 JPEG 才是正确的默认选择。

to_jpeg()to_png() 都接受其他转换方法所采用的同样的绘图风格位置参数和缩放关键字——x_scaley_scaleroirgb_channelalphacolor_palettealpha_palettehint——因此同一次调用就能一步编码出源图像的缩放、裁剪或调色板映射版本。compress()to_jpeg() 的旧式写法;两者接受相同的参数并产生相同的结果。

5.32.3. 压缩带来了什么

JPEG 与原始数据之间权衡背后的数字,值得认真推算一遍。

一帧 320×240 的 RGB565 帧为 153,600 字节(QVGA 下捕获的一帧)。一帧 640×480 的帧为 614,400 字节;一帧 1280×960 的帧为 2,457,600 字节。相较于桌面或手机显示屏,这些都算不上大,但放在一台总共只有几 MB RAM、SD 卡写入带宽有限、且主机链路通常运行在 USB CDC、UART 或速度一般的无线模块之上的摄像头的语境中,它们就相当可观了。

quality=50 下,JPEG 对照片类捕获帧的压缩比通常为 10 到 20 倍:那帧 614 KB 的 640×480 帧会变成 30 到 60 KB 的已编码字节流。在 quality=85 下,压缩比降到 5 到 10 倍(同一帧为 60 到 120 KB)。在 quality=10 下——充满伪影但仍可辨认——压缩比可达 30 到 50 倍(12 到 20 KB)。

这些数字决定了用保存下来的帧 能做 什么是切实可行的。一条持续 10 MB/s 的 SD 卡路径,可以游刃有余地处理每秒 30 帧 quality=50 JPEG 编码的 VGA 内容(约 1 到 2 MB/s);而以未压缩方式保存同样的内容需要超过 18 MB/s,超出了摄像头文件系统路径写入 SD 卡所能维持的能力。一台以 1 MB/s 通过 CDC 拉取 JPEG 编码帧的 USB 主机,能以大约每秒 15 到 30 帧的速度接收 30 到 60 KB 的帧;而以相同速率拉取原始帧时,每秒只能得到一两帧。

简而言之:压缩方法不仅仅是保存时的一种便利。它们正是让捕获帧能够以应用所关心的帧率在摄像头之外 可用 的关键。选择正确的压缩——通用记录用 JPEG 质量 50,质量要求高的工作用 80,线条图捕获用 PNG——是任何非平凡的摄像头应用日常工作的一部分。