4.14. CSI 基础

csi 模块是 Python 代码驱动摄像头传感器的途径。每一个采集帧的脚本都遵循同样的三段式结构:顶部的导入、中间的一次性配置,以及底部的一个 while True 循环,该循环逐个从摄像头拉取帧。

4.14.1. 典型的循环

import csi, image, time

csi0 = csi.CSI()
csi0.reset()
csi0.pixformat(csi.RGB565)
csi0.framesize(csi.QVGA)

clock = time.clock()
while True:
    clock.tick()
    img = csi0.snapshot()
    # process img here
    print(clock.fps())

4.14.2. 每个调用的作用

import csi, image, time

引入三个模块。csi 控制传感器,image 定义 snapshot() 返回的 Image 类,time 提供用于测量每秒帧数的 time.clock() 辅助工具。

csi.CSI()

构造一个 CSI 实例,它封装一个物理摄像头传感器。构造函数会占用摄像头外设并记录每个传感器的配置。带单个传感器的摄像头有一个 CSI 实例;带两个传感器的摄像头(彩色加热成像、彩色加事件)有两个实例,各自通过构造函数的 cid 参数选择。

csi0.reset()

给传感器上电并配置传感器。默认情况下它会脉冲拉动传感器的复位引脚,然后写入传感器的 I2C 寄存器,使其进入已知的起始状态。后续的配置调用——pixformatframesize 以及各种自动控制旋钮——会通过同一条 I2C 控制总线推送更多寄存器写入。

csi0.pixformat(csi.RGB565)

写入用于选择输出像素格式的传感器寄存器。可用选项就是 像素格式 页面介绍过的那些格式:RGB565GRAYSCALEBAYERYUV422,以及在支持的传感器上的 JPEG

csi0.framesize(csi.QVGA)

写入用于选择输出分辨率的寄存器。QVGA 是 320 × 240;在支持的传感器上,命名尺寸最高可达 WQXGA2(2592 × 1944,约 5 MP)。自定义的 (width, height) 元组也可以使用,只要它与传感器的输出能力相匹配即可。

clock = time.clock()

创建一个时钟辅助工具。循环内每次调用 clock.tick() 都会记录该次迭代的开始时间;time.clock.fps() 以每秒帧数报告最近的循环速率。

img = csi0.snapshot()

从传感器采集一帧,并将其作为 Image 返回。这一帧最终如何进入内存的机制值得更仔细地了解。

4.14.3. snapshot 如何填充内存

传感器以每秒数百兆字节的速率,通过 传感器总线 中描述的像素数据总线传送像素——这对于 CPU 用软件逐像素复制而言实在太快了。

取而代之的是,MCU 把传输工作卸载给直接内存访问(DMA)——一个独立于 CPU 的硬件引擎,它在 MCU 内部把字节从一处复制到另一处,全程完全不涉及 CPU。摄像头输入外设把每个传入的像素字节捕获到一个小型片上 FIFO 中;在 MCU 端运行的那些 ISP 阶段会在数据通过时对其进行处理;然后 DMA 引擎把成品像素按对应的像素偏移写入 RAM 中的帧缓冲区。一旦 DMA 通道被编程配置好,这条链路中就没有任何环节需要 CPU。

当调用 snapshot() 时:

  1. CSI 驱动用帧缓冲区的地址、传输长度(一帧像素的数据量)以及一个用于 DMA 完成中断的回调来对 DMA 引擎进行编程。

  2. 驱动启用摄像头输入外设,并等待传感器发出下一帧开始的信号。

  3. 当传感器把该帧流式输出时,外设把每个像素字节传过 ISP,再送往 DMA 引擎,DMA 引擎把结果写入 RAM 中的下一个帧缓冲区偏移处。在传输期间 CPU 可以自由运行其他代码。

  4. 当该帧的最后一个像素到达时,DMA 触发其完成中断,驱动把帧缓冲区封装进一个 Imagesnapshot() 随即将其返回给用户代码。

返回的 Image 并不拥有像素数据的副本——它指向摄像头在 RAM 中的某个帧缓冲区。摄像头保留多少个帧缓冲区,以及它们在每次调用 snapshot() 时如何在 DMA 和用户代码之间传递,取决于应用通过 framebuffers() 选择的缓冲模式。