多光谱事件摄像头

多光谱事件摄像头模块在单个模块上将 GENX320 事件传感器与一颗 1 MP 的 PAG7936 全局快门彩色传感器配对——这是一条同步的事件 + 彩色流水线,适用于高速物体跟踪、LED 跟踪、流体流动以及其他动态场景。

多光谱事件摄像头

完整数据手册、照片和订购信息请参阅 多光谱事件摄像头产品页面

备注

仅在 OpenMV N6 上受支持。

亮点

  • 320x320 事件传感器,>140 dB 动态范围,375 Hz+ 直方图

  • PAG7936 彩色:1280x800 @ 120 FPS,640x400 @ 240 FPS

  • 带有共享曝光触发的同步事件时间戳

  • 无需自动曝光即可在 5 lux 以下成像

  • 事件流功耗低至约 3 mW

  • 面向高速跟踪、LED 跟踪以及流体/粒子流动

用法

彩色传感器和 GENX320 事件传感器各自获得自己的 csi.CSI 实例。第一次调用默认使用主传感器(PAG7936);第二次通过传入 cid= csi.GENX320 绑定到 GENX320。使用 csi.CSI.reset (hard=True) 对彩色传感器执行硬复位以拉起电源轨,并以 hard=False 配置 GENX320,这样其驱动程序只会重新编程芯片,而不会再次切换复位。

GENX320 在直方图模式下输出 320x320;PAG7936 在 csi.QVGA 下输出 320x200。下面的基本叠加层会裁掉 GENX320 帧底部的 120 行。可使用单应性变换(见下文)实现贴合的叠加,或使用更大的 PAG7936 帧尺寸。

两个临时缓冲区在整个帧循环中保持不变——一个存储为 image.Image 的 256x1 alpha 调色板,使位于中灰基线(128)处的直方图像素变为透明,而 ON 事件高光和 OFF 事件阴影都变为不透明;以及一个用 image.Image 预分配的 GENX320 帧缓冲区,使 csi.CSI.snapshot (blocking=False, image=...) 能在每次迭代中就地填充它而无需重新分配:

import time
import csi
import image
import math

# V-shaped alpha: pixels far from the baseline 128 become opaque.
alpha_pal = image.Image(256, 1, image.GRAYSCALE)
for i in range(256):
    alpha_pal[i] = int(math.pow(abs(i - 128) / 128.0, 2) * 255)

# Setup the color camera sensor.
csi0 = csi.CSI()
csi0.reset(hard=True)  # force hardware reset.
csi0.pixformat(csi.RGB565)
csi0.framesize(csi.QVGA)

csi1 = csi.CSI(cid=csi.GENX320)
csi1.reset(hard=False)  # no hardware reset - just configure GENX320
csi1.pixformat(csi.GRAYSCALE)
csi1.framesize((320, 320))
csi1.brightness(128)  # histogram baseline (default)
csi1.contrast(64)     # per-event step

clock = time.clock()

img1 = image.Image(csi1.width(), csi1.height(), csi1.pixformat())

while True:
    clock.tick()
    img0 = csi0.snapshot()
    csi1.snapshot(blocking=False, image=img1)
    img0.draw_image(img1, 0, 0, color_palette=image.PALETTE_EVT_LIGHT,
                    alpha_palette=alpha_pal,
                    hint=image.BILINEAR)
    print(clock.fps())

每次迭代都会进行一次阻塞式彩色快照和一次非阻塞式 GENX320 快照。Image.draw_image 随后将两者合成:color_palette= image.PALETTE_EVT_LIGHT(或用于深色背景的 image.PALETTE_EVT_DARK)将 GENX320 的灰度直方图映射到一个色彩渐变中,alpha_palette= 使用 V 形 alpha 映射混合每个像素,从而让场景中的静态区域透出底层彩色图像,而 hint= image.BILINEAR 在彩色传感器分辨率高于 GENX320 时平滑放大效果。

GENX320 的偏置预设、AFK 滤波器、热像素校准和 STC 滤波器 ioctl 在这种双摄像头配置中的工作方式完全相同——在 csi.CSI.reset 之后对 csi1 调用它们即可。详情见下文各节。

GPU 加速对齐

Image.draw_image 接受一个 transform= 参数——一个以二维 ulab.numpy 数组表示的 3x3 单应性矩阵。在 OpenMV N6 上,GPU 会在同一次绘制中运行逐像素投影,因此 GENX320 帧无需单独的扭曲过程即可重新对齐到彩色摄像头的视角——当两个传感器的光学特性或视场略有差异,或彩色摄像头以更高分辨率运行时,这非常有用。可使用 GenX320 叠加校准工具 为每个摄像头标定该矩阵,该工具会显示一个闪烁的棋盘格,使事件传感器无需任何物理移动即可产生角点事件:

import time
import csi
import image
from ulab import numpy as np
import math

# Calibration matrix from the GenX320 Overlay Calibration tool.
m = np.array([
    [2.000000, 0.000000,   0.000000],
    [0.000000, 2.000000,  80.000000],
    [0.000000, 0.000000,   1.000000],
])

alpha_pal = image.Image(256, 1, image.GRAYSCALE)
for i in range(256):
    alpha_pal[i] = int(math.pow(abs(i - 128) / 128.0, 2) * 255)

# Setup the color camera sensor.
csi0 = csi.CSI()
csi0.reset(hard=True)
csi0.pixformat(csi.RGB565)
csi0.framesize(csi.VGA)

csi1 = csi.CSI(cid=csi.GENX320)
csi1.reset(hard=False)
csi1.pixformat(csi.GRAYSCALE)
csi1.framesize((320, 320))
csi1.brightness(128)
csi1.contrast(64)

clock = time.clock()

img1 = image.Image(csi1.width(), csi1.height(), csi1.pixformat())

while True:
    clock.tick()
    img0 = csi0.snapshot()
    csi1.snapshot(blocking=False, image=img1)
    img0.draw_image(img1, 0, 0, color_palette=image.PALETTE_EVT_LIGHT,
                    alpha_palette=alpha_pal,
                    hint=image.BILINEAR,
                    transform=m)
    print(clock.fps())

此变体让彩色摄像头以 csi.VGA(640x480)运行,GENX320 以其原生 320x320 运行——单应性将较小的 GENX320 帧作为绘制的一部分投影到较大的彩色帧中,因此放大系数被烘焙进矩阵本身,而不是单独应用。

事件摄像头详情

GENX320 是一种基于事件的视觉传感器——它不会按固定帧时钟读出整个 320x320 阵列,而是在每个像素检测到亮度变化的瞬间报告异步“事件”。每个事件都携带一个 X/Y 坐标、一个 ON/OFF 极性(由亮→暗或由暗→亮)以及一个微秒级时间戳。这正是该传感器具备微秒级时间精度、无运动模糊、极高动态范围以及功耗随活动量缩放等特性的来源。静态场景不产生任何数据。

OpenMV 固件通过带有 cid= csi.GENX320csi.CSI 公开 GENX320。有两种工作模式可用:

  • 直方图模式(默认)——事件在片上累积到逐像素的桶中,并以可配置的速率(约 20-350 FPS)报告为一帧 320x320 灰度图像。该传感器表现得像一个普通摄像头,因此所有标准图像处理例程(Image.find_blobs、调色板等)都可直接使用。

  • 事件模式——原始事件以完整的微秒级时间戳流入一个 numpy ndarray,适用于需要时间细节而非预先分桶的帧的应用。

直方图模式

在直方图模式下,GENX320 输出灰度帧,其中每个像素编码该位置最近的事件活动。高于亮度基线的像素是 ON 事件(亮度上升),低于基线的是 OFF 事件(亮度下降)。默认基线亮度为 128,每个事件的对比度步进为 16——调高对比度可让事件更突出:

import csi
import time

csi0 = csi.CSI(cid=csi.GENX320)
csi0.reset()
csi0.pixformat(csi.GRAYSCALE)
csi0.framesize((320, 320))
csi0.brightness(128)  # baseline (default 128)
csi0.contrast(16)     # per-event step
csi0.framerate(50)    # 20-350 FPS

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

csi.CSI.brightnesscsi.CSI.contrastcsi.CSI.framerate 是塑造直方图输出的三个调节旋钮。

彩色化输出

csi.CSI.color_palette 设为 image.PALETTE_EVT_LIGHT 以使用浅色背景,或设为 image.PALETTE_EVT_DARK 以使用深色背景——驱动程序会直接使用该调色板输出 RGB565 帧:

csi0.color_palette(image.PALETTE_EVT_LIGHT)

热像素校准

事件传感器会累积一些虚假触发的“热像素”。针对一个静态场景运行 csi.IOCTL_GENX320_CALIBRATE 即可将它们禁用。驱动程序会构建一个 320x320 的逐像素命中计数,计算均值和标准差,并禁用任何计数高于 mean + sigma * stddev 的像素——随后这些被禁用的像素会在传感器层面停止发出事件。

有两个参数控制校准:

  • event_count —— 在计算统计量之前要累计多少个事件。循环会持续捕获帧,直到累计的事件总数超过此预算。计数越高,估计越可靠,但校准时间也越长。10000 是一个合理的起点。

  • sigma —— 标准差上的阈值乘数。值越低越激进(禁用的像素更多);值越高越保守。0.5 是一个不错的默认值。

请先将传感器对准一个静态场景,这样任何由运动驱动的事件就不会被计入那些实际上正常的像素上:

csi0.snapshot(time=5000)  # let the user steady the camera
disabled = csi0.ioctl(csi.IOCTL_GENX320_CALIBRATE, 10000, 0.5)
print(f"disabled {disabled} hot pixels")

抗闪烁(AFK)滤波器

周期性光源(荧光灯、LED 驱动的显示屏)会产生大量冗余事件。AFK 滤波器会拒绝那些像素切换频率落在某个频带内的事件——可通过 csi.IOCTL_GENX320_SET_AFK 并以赫兹为单位给出频带边缘来启用它:

csi0.ioctl(csi.IOCTL_GENX320_SET_AFK, 1, 130, 160)  # 130-160 Hz
csi0.ioctl(csi.IOCTL_GENX320_SET_AFK, 0)            # disable

偏置预设

GenX320 中的每个像素都运行一个带有若干可配置偏置的模拟前端。它们共同决定灵敏度、噪声、像素带宽和事件率——正确的组合取决于场景。各个偏置如下:

  • DIFF_ON —— 正向比较器对比度阈值。当一个像素的对数照度上升达到此值时,它会发出一个 ON 事件。值越低 = 对明亮跳变越敏感。

  • DIFF_OFF —— 负向比较器对比度阈值(OFF 事件的对称对应项)。值越低 = 对变暗跳变越敏感。

  • FO —— 像素的低通截止频率。值越高 = 像素带宽越宽(响应更快、延迟更低),但背景噪声活动也越多。

  • HPF —— 高通截止频率。值越高 = 对缓慢亮度变化的抑制越强;只有快速跳变才能到达比较器。适用于忽略环境漂移。

  • REFR —— 不应期。一个像素触发后,它会在此时长内保持复位状态,然后才能再次触发。值越高 = 死区时间越长,适用于限制逐像素事件率的上限。

csi.CSI.reset 之后,驱动程序会应用 csi.GENX320_BIASES_LOW_NOISE,而不是 csi.GENX320_BIASES_DEFAULT——数据手册默认值会产生高得多的背景事件率,因此使用 LOW_NOISE 作为起点以保持事件流安静。当应用需要更高灵敏度或带宽时,可使用不同的预设调用 csi.IOCTL_GENX320_SET_BIASES

csi.IOCTL_GENX320_SET_BIASES 应用五种预设之一:

  • csi.GENX320_BIASES_DEFAULT —— GenX320 数据手册默认值。针对一般场景在灵敏度、噪声和带宽之间取得平衡。

  • csi.GENX320_BIASES_LOW_LIGHT —— 两个对比度阈值都被放宽以获得更高灵敏度,FO 被降低以抑制噪声,HPF 设为 0 以便缓慢的亮度变化仍能被记录——低光场景本身产生的事件很少,所以我们希望尽可能多地让事件通过。

  • csi.GENX320_BIASES_ACTIVE_MARKER —— 针对跟踪高对比度闪烁 LED 进行调优。对比度阈值被提高,使得只有锐利的跳变才会触发;FO 和 HPF 被调得很高,以最大化像素带宽并拒绝任何缓慢的环境漂移;REFR 被拉到 0,从而连续捕获每一个闪烁边沿。其结果是:一个几乎全是 LED 边沿、易于跟踪的事件流。

  • csi.GENX320_BIASES_LOW_NOISE —— 驱动程序默认值。两个对比度阈值相对于 DEFAULT 都被提高(灵敏度更低),且 FO 被降低(像素更慢 = 像素更安静)。最适合静态或缓慢场景,否则虚假事件会占主导。

  • csi.GENX320_BIASES_HIGH_SPEED —— FO 被调高使每个像素能更快响应,HPF 被提高以拒绝缓慢的亮度漂移,REFR 被提高,从而单个快速移动的边沿不会淹没读出——更长的死区时间使重度运动下的事件量保持在可控范围内。

使用 csi.IOCTL_GENX320_SET_BIAS 加上 csi.GENX320_BIAS_DIFF_ONcsi.GENX320_BIAS_DIFF_OFFcsi.GENX320_BIAS_FOcsi.GENX320_BIAS_HPFcsi.GENX320_BIAS_REFR 之一以及一个 DAC 值来覆盖单个偏置。每个偏置都是独立设置的——选一个预设作为起点,然后调整你的场景所需的任意偏置:

csi0.ioctl(csi.IOCTL_GENX320_SET_BIASES, csi.GENX320_BIASES_LOW_LIGHT)
csi0.ioctl(csi.IOCTL_GENX320_SET_BIAS, csi.GENX320_BIAS_HPF, 20)

跟踪

由于直方图模式的输出就是一幅灰度图像,常规的色块跟踪可直接使用。要跟踪一个有源标记 LED,加载有源标记偏置预设,并在直方图的明亮端查找色块:

import csi
import time

csi0 = csi.CSI(cid=csi.GENX320)
csi0.reset()
csi0.pixformat(csi.GRAYSCALE)
csi0.framesize((320, 320))
csi0.brightness(128)
csi0.contrast(16)
csi0.framerate(200)
csi0.ioctl(csi.IOCTL_GENX320_SET_BIASES, csi.GENX320_BIASES_ACTIVE_MARKER)

clock = time.clock()
while True:
    clock.tick()
    img = csi0.snapshot()
    for blob in img.find_blobs([(120, 140)], invert=True,
                               pixels_threshold=2, area_threshold=4,
                               merge=True):
        img.draw_detection(blob)
    print(clock.fps())

事件模式

事件模式绕过片上直方图,将原始事件流入一个 numpy ndarray。每个事件是一行六个 uint16 列:

  • [0] 事件类型——见下文

  • [1] 秒时间戳

  • [2] 毫秒时间戳

  • [3] 微秒时间戳

  • [4] X 坐标,0-319

  • [5] Y 坐标,0-319

驱动程序在 [0] 列中输出六种事件类型:

  • csi.PIX_OFF_EVENT —— 一个像素检测到亮度下降(越过了 DIFF_OFF 比较器阈值)。X/Y 指向触发的像素。

  • csi.PIX_ON_EVENT —— 一个像素检测到亮度上升(越过了 DIFF_ON 阈值)。X/Y 指向该像素。

  • csi.EXT_TRIGGER_FALLING —— 传感器的外部触发引脚检测到一个下降沿。X/Y 未使用。

  • csi.EXT_TRIGGER_RISING —— 传感器的外部触发引脚检测到一个上升沿。X/Y 未使用。

  • csi.RST_TRIGGER_FALLING —— 像素复位触发,下降沿。X/Y 未使用。目前固件不生成此类型。

  • csi.RST_TRIGGER_RISING —— 像素复位触发,上升沿。X/Y 未使用。目前固件不生成此类型。

GENX320 的外部触发输入连接到摄像头的帧同步线,该线同时也被布线到处理器和引脚排针上的 P10——驱动 P10 即可向事件流中注入同步边沿,并将它们与像素数据一起作为 EXT_TRIGGER_RISING / EXT_TRIGGER_FALLING 事件读取出来。

大多数应用只关心 PIX_OFF_EVENTPIX_ON_EVENT;触发类型则让你能将事件与外部时序信号关联起来。

以形状 (EVT_res, 6) 分配事件缓冲区,其中 EVT_res 是 1024 到 65536 之间的 2 的幂,然后通过 csi.IOCTL_GENX320_SET_MODE 配合 csi.GENX320_MODE_EVENT 和缓冲区大小进入事件模式。使用 csi.IOCTL_GENX320_READ_EVENTS 读取事件,它会将缓冲区填充至其容量上限,并返回有效行的数量。

Image.draw_event_histogram 将事件光栅化为一幅灰度图像——对每个 ON 事件,它向桶中加上 contrast;对每个 OFF 事件则减去。clear=True 会先将图像重置为 brightnessclear=False 则在多次调用间累积:

import csi
import image
import time
from ulab import numpy as np

img = image.Image(320, 320, image.GRAYSCALE)
events = np.zeros((2048, 6), dtype=np.uint16)

csi0 = csi.CSI(cid=csi.GENX320)
csi0.reset()
csi0.ioctl(csi.IOCTL_GENX320_SET_MODE, csi.GENX320_MODE_EVENT, events.shape[0])

clock = time.clock()
while True:
    clock.tick()
    n = csi0.ioctl(csi.IOCTL_GENX320_READ_EVENTS, events)
    img.draw_event_histogram(events[:n], clear=True, brightness=128, contrast=64)
    img.flush()
    print(n, clock.fps())

直方图模式的偏置预设、AFK 滤波器和热像素校准 ioctl 在事件模式下的工作方式完全相同——在 csi.IOCTL_GENX320_SET_MODE 之后调用它们即可。

按极性过滤

用 ulab 对事件数组切片,以仅保留 ON 事件(进入更亮状态的运动)或仅保留 OFF 事件:

TARGET = csi.PIX_ON_EVENT  # or csi.PIX_OFF_EVENT

events_slice = events[:n]
indices = np.nonzero(events_slice[:, 0] == TARGET)[0]
if len(indices):
    target_events = np.take(events_slice, indices, axis=0)
    img.draw_event_histogram(target_events, clear=True,
                             brightness=128, contrast=64)

长曝光累积

设置 clear=False 以在多帧间将事件持续堆叠到同一幅图像中——结果是一种运动轨迹可视化。定期复位以开始一次新的曝光:

EXPOSURE_FRAMES = 30
i = 0
while True:
    n = csi0.ioctl(csi.IOCTL_GENX320_READ_EVENTS, events)
    clear = (i % EXPOSURE_FRAMES) == 0
    img.draw_event_histogram(events[:n], clear=clear, brightness=128, contrast=64)
    img.flush()
    i += 1

高速处理

去掉可视化,把 CPU 腾出来用于事件处理。仅每隔 N 次迭代打印一次统计信息——在每次迭代都推送一行打印输出,会在高事件率下成为瓶颈:

csi0 = csi.CSI(cid=csi.GENX320)
csi0.reset()
csi0.ioctl(csi.IOCTL_GENX320_SET_MODE, csi.GENX320_MODE_EVENT, events.shape[0])

clock = time.clock()
i = 0
while True:
    clock.tick()
    n = csi0.ioctl(csi.IOCTL_GENX320_READ_EVENTS, events)
    i += 1
    if not i % 10:
        print(f"{n} events  {clock.fps()} fps")

时空对比度(STC)滤波器

一个真实的移动对比度边沿往往会在同一像素上、在很短的时间窗口内触发一阵嘈杂的事件爆发——像素失配和模拟噪声会在真正的跳变周围产生对应用无用的额外事件。STC 滤波器是一种片上后处理,它只保留每次爆发中的一个(或几个)事件,并丢弃其余的。

它实现了三种策略,通过 csi.IOCTL_GENX320_SET_STC 和一个 GENX320_STC_* 常量选择。每种模式由它从一次爆发中转发哪些事件来定义:

模式

保留

丢弃

csi.GENX320_STC_DISABLE

每一个事件

csi.GENX320_STC_ONLY

一次爆发的第二个事件

第一个 + 后续事件

csi.GENX320_STC_TRAIL_ONLY

一次爆发的第一个事件

后续事件

csi.GENX320_STC_TRAIL

第一个 + 后续边沿

仅冗余噪声

详细说明:

  • csi.GENX320_STC_DISABLE —— 滤波器关闭,每个事件都通过(默认)。

  • csi.GENX320_STC_ONLY —— 保留一次爆发的第二个事件。参数:stc_threshold(毫秒)。如果某像素上的新事件在前一个事件之后的 stc_threshold 之内到达,它就被视为一次爆发的“第二个”并被转发——第一个事件以及同一爆发中任何后续事件都会被过滤掉。当你想要一个经过噪声确认的跳变而非最初命中时最适用。

  • csi.GENX320_STC_TRAIL_ONLY —— 保留一次爆发的第一个事件。参数:trail_threshold(毫秒)。一个像素触发后,同一像素上的后续事件会被丢弃,直到经过 trail_threshold。它保留了前沿的精确时序——当极性切换时刻比爆发确认更重要时很有用。

  • csi.GENX320_STC_TRAIL —— 两者结合。参数:stc_thresholdtrail_threshold(均为毫秒)。按 Trail 模式保留前沿,再按 STC 模式保留后续边沿,因此一次爆发中的多个事件仍能通过——事件吞吐量高于单一模式的滤波器,但信号最丰富。

两个阈值必须保持在大致 13:1 的比率之内——传感器会拒绝其中一个超过另一个约 13 倍的配置:

csi0.ioctl(csi.IOCTL_GENX320_SET_STC, csi.GENX320_STC_TRAIL, 1, 2)
csi0.ioctl(csi.IOCTL_GENX320_SET_STC, csi.GENX320_STC_DISABLE)

缓冲区深度

当事件率激增时,默认的三重缓冲流水线会偏向最新的帧并丢弃旧的帧。可通过 csi.CSI.framebuffers 加大 FIFO 深度以改为对事件排队——代价是当主机跟不上时会处理稍旧一些的数据:

csi0.framebuffers(10)  # FIFO depth, > 3 enables queueing

桌面流式传输与可视化

要在主机 PC 上进行实时 GUI 可视化,openmv-projects 仓库中的 GenX320 事件流式传输工具 将摄像头与一个 DearPyGui 前端配对。该 PC GUI 并排运行两种可视化:一个事件累积画布(与 Image.draw_event_histogram 思路相同,但带有可选调色板以及滑动窗口与自动清除两种模式)以及一个由 IIR 带通滤波器驱动的逐像素频率图——可用于直接在事件流中发现周期性信号(旋转的风扇、闪烁的 LED 等)。

它附带两个在摄像头上运行的流式传输脚本:

  • 已处理模式genx320_event_mode_streaming_on_cam.py)—— 摄像头使用 csi.IOCTL_GENX320_READ_EVENTS 解码事件,并将每一行作为 12 字节通过 USB 流式传输([0] 类型、[1] 秒、[2] 毫秒、[3] 微秒、[4] x、[5] y)。由于线缆格式与摄像头上的 ndarray 格式一致,因此在 PC 上易于消费。

  • 原始模式genx320_raw_event_mode_streaming_on_cam.py)—— 摄像头通过 csi.IOCTL_GENX320_READ_EVENTS_RAW 流式传输芯片原生的 32 位打包事件字。这是每个事件 4 字节,而已处理模式为 12 字节(通过 USB 的数据量约少 3 倍),因此当链路是瓶颈时可实现约 3 倍更高的事件率。PC 使用向量化的 numpy 将打包的字解码回相同的 6 列事件布局,因此下游的可视化器代码完全相同。

原始模式是 GUI 中的默认模式,因为在 GenX320 能够产生的速率下,USB 吞吐量是约束性瓶颈;如果你需要将处理逻辑插入摄像头端的脚本中,可切换到已处理模式。