2. 事件数据格式:事件流、事件帧、时间面、体素网格的表示方法

好,咱们进入正题。上一章聊了事件相机怎么工作的,这一章我们得聊聊数据本身——毕竟你拿到的原始数据,不能直接扔给神经网络去训练。你得先搞清楚它长什么样,怎么存,怎么用。

我个人习惯把事件数据格式分成四类:事件流、事件帧、时间面、体素网格。这四种格式各有各的脾气,也各有各的适用场景。咱们一个一个来拆解。

2.1 事件流:最原始的“脉搏”

事件相机输出的原始数据,说白了就是一个一个的事件。每个事件包含四个信息:像素坐标 (x, y)、时间戳 t、极性 p(+1 或 -1)。

你可以把它想象成一条永不停歇的河流。每个事件就像一滴水,带着自己的坐标和时间标记。我刚开始接触事件相机时,看到这种数据格式还挺懵的——没有完整的图像帧,只有一堆离散的点。这怎么玩?

事件流的表示方法很简单,就是一个四元组:

(x, y, t, p)

其中:

  • x, y:事件发生的像素坐标,范围取决于传感器分辨率(比如 640×480)
  • t:时间戳,精度通常在微秒级
  • p:极性,+1 表示亮度增加,-1 表示亮度降低

在实际项目中,事件流通常以二进制文件存储。我见过不少新手直接拿文本存,结果一个 10 秒的数据文件能到几个 GB——这显然不现实。正确的做法是用压缩二进制格式,比如 .aedat.h5

我的经验: 如果你在做实时处理,事件流是最直接的输入。但如果你要做深度学习,直接拿事件流训练,那效率会很低。为什么?因为事件流是稀疏的、无序的,大多数深度学习框架不擅长处理这种数据。

2.2 事件帧:把事件“拍扁”成图像

既然事件流不好直接处理,那最简单的办法就是把它变成图像。这就是事件帧——把一段时间内的事件累积到一张图上。

具体怎么做?你设定一个时间窗口 Δt,把窗口内所有事件按像素位置累加。极性可以分开,也可以合并。我常用的做法是生成两通道的帧:一个通道放正事件,一个通道放负事件。

def events_to_frame(events, height, width, dt):
    frame = np.zeros((height, width, 2), dtype=np.float32)
    for x, y, t, p in events:
        if p == 1:
            frame[y, x, 0] += 1
        else:
            frame[y, x, 1] += 1
    return frame

这段代码看着简单,但实际项目中要注意几个坑:

  • 时间窗口怎么选? 窗口太小,事件太少,帧是稀疏的;窗口太大,事件太多,帧会模糊。我一般根据场景动态调整,比如快速运动用 10ms,慢速运动用 50ms。
  • 极性要不要分开? 如果你做的是目标检测,分开极性往往效果更好。因为正负事件代表不同的亮度变化方向,能提供更多信息。
  • 要不要归一化? 建议做。事件数量差异很大,不归一化的话,网络很难收敛。
避坑指南: 我曾经在一个项目中直接用事件帧做光流估计,结果效果很差。后来发现是因为事件帧丢失了时间信息——两个事件虽然在同一像素位置,但时间差可能很大。所以,事件帧适合做分类、检测这类任务,但不适合做需要精确时间信息的任务。

2.3 时间面:给事件加上“时间权重”

事件帧的问题在于,它把所有事件一视同仁。但你想,一个 1 毫秒前的事件和一个 10 毫秒前的事件,它们的重要性应该不一样吧?时间面就是来解决这个问题的。

时间面的核心思想是:每个像素位置只保留最近一次事件的时间戳。这样,每个像素的值就代表了“距离上次事件发生的时间”。

数学上,时间面可以表示为:

S(x, y, t) = exp(-(t - t_last(x, y)) / τ)

其中 τ 是时间常数,控制衰减速度。τ 越小,越关注近期事件;τ 越大,历史事件的影响越持久。

我个人的习惯是用两个时间面:一个正极性,一个负极性。这样既能保留时间信息,又能保留极性信息。

为什么时间面好用? 因为它保留了事件的时序关系。比如在动作识别中,时间面能捕捉到运动的先后顺序——先动哪里,后动哪里,这对理解动作意图很关键。

2.4 体素网格:把事件“堆”成三维

如果你觉得事件帧和时间面都太“二维”了,那体素网格就是三维的解决方案。它把时间轴也当作一个维度,把事件流离散化成一个个小立方体(体素)。

具体做法是:把时间轴分成 B 个 bins,每个 bin 内的事件累积到对应的通道中。这样你就得到了一个 B 通道的“三维”数据。

def events_to_voxel_grid(events, height, width, B):
    grid = np.zeros((B, height, width), dtype=np.float32)
    t_min = events[:, 2].min()
    t_max = events[:, 2].max()
    for x, y, t, p in events:
        bin_idx = int((t - t_min) / (t_max - t_min) * (B - 1))
        grid[bin_idx, y, x] += p
    return grid

体素网格的好处是:它同时保留了空间和时间信息,而且数据是稠密的、规则的,可以直接喂给 3D 卷积网络。我在做事件相机上的光流估计时,就经常用体素网格作为输入。

不过要注意,B 的选择很关键。B 太小,时间分辨率不够;B 太大,计算量暴增。我一般取 B=5 到 B=10 之间,具体看任务需求。

数据格式 维度 保留信息 适用场景
事件流 稀疏四元组 全部 实时处理、低延迟系统
事件帧 2D 图像 空间、极性 分类、检测
时间面 2D 图像 空间、时间、极性 动作识别、跟踪
体素网格 3D 张量 空间、时间、极性 光流估计、深度估计

2.5 怎么选?我的建议

说实话,没有一种格式是万能的。我做过不少项目,总结下来:

  • 如果你做实时系统,比如无人机避障,直接用事件流,延迟最低。
  • 如果你做图像级任务,比如目标检测,事件帧就够了,简单高效。
  • 如果你做时序敏感的任务,比如手势识别,时间面是更好的选择。
  • 如果你做密集预测任务,比如光流、深度,体素网格是标配。

嗯,这里还要提醒一点:不管用哪种格式,数据预处理都很重要。我见过有人直接把原始事件帧扔进网络,结果因为事件数量差异太大,训练一直不收敛。所以,归一化、数据增强这些常规操作,一个都不能少。

好了,这一章就到这里。下一章我们聊聊事件相机的标定——这可是个技术活,搞不好你的数据全白费。