早几年我在嵌入式平台上调视觉感知模块,最头疼的不是算法精度,而是数据量本身的压力。稠密视差图一帧就是几十兆字节,传到下游做障碍物检测时,单纯一个聚类就可能把CPU吃满。后来看到了Stixel World这套表示法,说实话第一反应是"居然还能这么压缩"。它把一整幅视差图浓缩成几十根竖条,每根竖条就是路面上的一个"柱状像素",存储量直接掉到原来的几十分之一。这篇博文就围绕Stixel World的原理、建模思路、求解过程和工程落地展开,适合做自动驾驶感知、机器人导航、嵌入式视觉的算法工程师,也适合正在研究中间层三维表示的学生朋友。
Stixel World本质上解决的是一个"表示取舍"问题:稠密深度图精度高但太重,二维锚框轻但丢掉了障碍物底边、高度、距离这些几何细节。Stixel正好卡在两者之间——它假设道路场景里的大多数物体(车、人、护栏、路肩)都近似垂直,于是把三维空间压缩成一堆"贴在竖直平面上的小柱子",每个柱子只用底部行、顶部行和视差三个量就能描述。下文我会拆开讲它的几何建模、动态规划求解、不同变体的演进,以及我在实际工程里踩过的那些坑。
1. Stixel World在稠密深度表示中的生态位
在讲Stixel的数学之前,得先弄明白它解决的问题到底是什么。自动驾驶感知早期常用两种障碍物表示方式:一种是直接上稠密视差图,把每个像素的深度都算出来再做聚类;另一种是只跑二维目标检测,输出带类别和置信度的锚框。前者信息量大,后处理复杂度和存储带宽都是问题;后者轻巧,但障碍物的真实尺寸、距离、切入轨迹只能靠猜测。
Stixel World出现的初衷,就是在这两者之间找一个"刚刚好"的层级。它先把视差图按列压缩,每一列生成一个或几个竖直柱体,每个柱体对应一个独立的障碍物面片。柱体的几何含义非常直接:底端是障碍物与地面的接触点,顶端是障碍物的最高可见点,视差则是该柱体到相机的距离。这样一来,一列像素被压缩成了一个"竖条",大量冗余被剔除,但障碍物边缘、高度、距离、遮挡关系这些下游任务真正关心的信息全部保留。
这里的核心先验是"道路场景中障碍物近似垂直"。汽车是垂直的,行人是垂直的,路肩、护栏、灯柱也基本都是垂直的。即便有倾斜的卡车车厢或者伸出的后视镜,整体轮廓在列方向上仍然可以近似成一段垂直柱体。这个假设沿用至今,依然是Stixel能高效工作的根基。
在实际工程里,Stixel World的价值还不止于省内存。因为它输出的模型是结构化的,直接以"障碍物+自由空间边界"的形式存在,后续做目标追踪时可以非常自然地用卡尔曼滤波或其他滤波方法去跟踪每根柱子的底边和视差变化,而不需要回到像素级别的匹配。换句话说,它是连接"几何计算"和"语义决策"之间的一座轻量级桥梁。
2. Stixel建模的几何假设与代价函数:从视差到柱体
2.1 极线约束与归一化视差
在双目前视相机配置下,极线约束保证了左右图对应的匹配点在同一条扫描线上,视差d与深度Z满足:
[ Z = f \cdot B / d ]
其中f是焦距,B是基线距离。这个式子意味着,视差越大,物体离相机越近。Stixel建模的第一步,就是把原始视差图做一个排序和归一化,使得同一列上的像素视差可以按照从远到近的顺序参与分割。如果做BEV视角的想象,这一步相当于把所有像素按"距离层"排开,后续DP分割找的就是层与层之间的跳变点。
2.2 一个Stixel的几何定义
一个Medium Stixel可以用三元组表示:
- (v_b):柱体底端行坐标,指示障碍物与地面接触的位置;
- (v_t):柱体顶端行坐标,指示障碍物最高可见点;
- (d):柱体中心视差,换算为到相机距离。
某些实现还会附带一个置信度或者平均列宽。Stixel的宽度并非固定为单个像素,而是在后续步骤中通过合并相邻列成一个更宽的柱体(例如5像素、10像素),最终每个柱体仍然保留底端、顶端和距离,但列宽变大了,存储进一步压缩。
2.3 动态规划中的数据项与平滑项
每一列可以独立地看成一条从图像底端到顶端的路径分割问题。我们要把这一列像素在竖直方向上划分成一系列连续的段,每一段对应一个"背景路面"或者一个"前景Stixel"。最经典的形式化是定义能量函数:
[ E(D) = \sum_{i} C(d_i) + \sum_{i \ne j} V(d_i, d_j) ]
- (C(d_i))是数据项,衡量给某个视差分配一个柱体段的代价,通常用视差残差的绝对值、平方值或者一个截断的代价函数;
- (V(d_i, d_j))是平滑项,惩罚相邻段之间视差跳变过大或者与"垂直障碍物"假设不符的分割方式。
为什么要分开定义这两项?因为如果只看数据项,那么每一行都选择当前残差最小的视差,结果会非常碎片化,噪声敏感。如果只看平滑项,则整列会被推成一个均一视差,障碍物边界全丢了。只有同时考虑两项,才能既容忍微小的噪声,又保留真实的深度跳变。在工程实现里,我通常把数据项截断到几像素以内,避免远处大深度对能量产生不成比例的影响。
2.4 为什么动态规划能够在全局求解
动态规划能在多项式时间内求出整列的最优分割,是因为每一列的分割满足最优子结构:当处理到第k行时,只需要知道前k行的最优能量和最后一个段的终止视差,就能递推地得到前k+1行的最优能量。这个过程会生成一张三维的递推表,维度分别是行坐标、当前段视差、当前段类型(前景或背景),最终从最顶行回溯,得到完整的分段结果。
一个容易忽略的细节是搜索空间的范围。视差分级的粒度不能太粗也不能太细,太粗导致距离分辨力差,太细则DP表爆炸。常见的做法是把视差按0.5到1个像素分级,一般前视车载摄像头基线在0.2-0.6米之间时,这个粒度已经能带来不错的距离精度。
3. 两阶段求解:逐列DP分割与列间宽度聚合
3.1 第一步:逐列动态规划
单列动态规划的目标,是找出这一列在给定能量函数下的最优分割。实际实现中,可以把每一列看作一个序列,从图像底部开始向上扫描。底部通常是路面或保险杠边缘,视差连续;向上遇到障碍物时视差发生突变,这时能量函数会给出一个明显的边界。
如果用伪代码简化,核心递推大概是这样的:
初始化: dp[0][d] = 当前像素视差为d的代价 递推: for row = 1 to H-1: for d in 视差区间: dp[row][d] = data_cost(row, d) + min(dp[row-1][prev_d] + transition_cost(prev_d, d)) 回溯: 从最后一个row的min(dp)开始,反推每个row最有可能是背景还是前景这里有一个值得注意的细节:transition_cost的设定直接影响分割质量。如果只惩罚视差跳变的绝对值,那么远处的路面和近处的障碍物之间哪怕只差几个像素也会被强行平滑掉。我的做法是对跳变加一个"阈值门槛",只有跳变超过一定像素数时才施加惩罚,小跳变视为噪声忽略。
3.2 第二步:相邻列合并
单列分割得到的Stixel很细,如果直接输出会非常碎片化。第二步是把相邻、且视差相近、底端和顶端高度也相近的列合并成同一个宽Stixel。合并的过程不是简单地求平均,而是要找一组分段常量表示,使得合并后的柱体在高度、视差上与原来的列尽可能一致。
这一步可以用一个二维DP实现,不过它在计算量上比第一步小得多,因为候选边界已经由第一阶段的断点位置给出了。实际工程里我习惯在合并的同时对柱体宽度设一个上下限,比如最小4像素,最大50像素。太窄的合并没有意义,太宽的柱体容易把两个距离接近的障碍物糊在一起。这个宽度阈值需要根据相机分辨率做标定,不宜直接抄别人代码里的参数。
3.3 消失线约束的作用
动态规划本身并不局限于"障碍物必须垂直"这一条约束,但消失线可以作为硬约束参与进来。一般做法是先估计地平线或消失线的位置,然后限制柱体底端必须位于消失线下方,顶端可以在消失线之上。这样有几个好处:一是可以排除远处天空或建筑造成的错误分割;二是可以减少DP搜索空间,让计算集中在有意义的区域。
消失线的估计有两种常用方式:一种是利用IMU或相机外参的直接投影,另一种是从视差图本身拟合路面平面方程。后者的好处是不依赖外部传感器,坏处是遇到连续坡道时容易出现系统性偏差。我在项目里更倾向于将两者做一个加权融合,保留IMU的高频信息,用视差拟合的方法做低频修正,效果比较稳。
4. 从Medium Stixel到Layer Stixel:扩展中的坑与收益
4.1 Medium Stixel的局限与Layer Stixel的动机
Medium Stixel对大多数道路障碍物足够用,但有一个天然的局限:它默认每一列里只有一个前景障碍物。如果前车后面还紧跟着一辆摩托车,或者一辆巴士旁边站着一个行人,那么这一列实际上存在两个距离层。Medium Stixel只能选择能量最低的那一层,另一个层的信息直接丢失。
Layer Stixel的核心改动,是允许同一列出现多个Stixel,每个Stixel对应一个深度层。动态规划的维度从"二分类(背景/前景)"扩展到"多分类(背景/第一层/第二层/...)"。这样一棵树的分割问题就变成了一个带层数约束的序列分割问题,求解复杂度会显著上升,但换取的是遮挡场景下的更完整感知。
4.2 支撑点模型给DP带来的改进
在实际实现中,直接在像素级做层次化DP容易产生错误的层次顺序。常见的改进是引入"支撑点"概念,用地面和障碍物的交点作为层次划分的强约束。简单说,如果检测到某一行既是前一障碍物的底端,又是后一障碍物的顶端,那么这个点就可以锁定为一个支撑点,DP只能在支撑点之间的区间内分段。
加入支撑点约束后,原本可能出现的"不同层互相穿插"的伪分割基本消失了。代价是需要额外的算法来预检测这些关键点,常用方法是先对每列求一个低通滤波后的视差梯度,梯度局部极值处就是候选支撑点。这部分在实现时要特别注意噪声带来的假支撑点,我一般会在梯度极值检测后加一个最小值间隔限制。
4.3 各变体对比与适用场景
在工程选型时,不必两话不说就上Layer Stixel。Medium Stixel计算量低,调参简单,应对稀疏路口和高速路场景绰绰有余;Layer Stixel适合城市复杂场景,尤其是存在大量遮挡的十字路口。下表能比较直观地展示区别:
| 表示形式 | 每列最大层数 | 求解复杂度 | 典型场景 | 主要风险 |
|---|---|---|---|---|
| Medium Stixel | 1 | O(行数×视差级数) | 高速、快速路 | 遮挡时漏检 |
| Layer Stixel | 2-3 | O(行数×层数×视差级数) | 城市道路、路口 | 计算量增大 |
| 支撑点增强Layer | 2-3 | 中等偏上 | 严重遮挡场景 | 支撑点检测不稳定 |
如果项目刚起步,我的建议是先实现从Single-Layer入手,跑通全流程后再扩展层次。直接上Layer Stixel会引入太多变量,出了问题很难定位是DP求解的问题,还是支撑点检测的问题。
5. 工程落地实录:性能瓶颈、标定陷阱与失效场景
5.1 计算效率优化
Stixel的DP看起来复杂度不高,但在大分辨率图像上逐列跑DP压力依然不小。以1920×1080像素的视差图为例,假设视差级数64级,逐列DP的理论操作次数大约在千万级,纯C++单线程实现可以上实时,但CPU占用率却很难看。
几个有效的加速手段,按性价比排序:
- 视差图降采样:模型对外观细节不敏感,对几何边界敏感,所以可以用2×2平均池化或中值池化先把视差图缩小,再上Stixel提取,最后把结果映射回原分辨率。这个做法在工程里非常普遍,边界损失控制在1-2像素。
- 金字塔DP:先在高斯金字塔顶层跑一次粗分割,把可能的边界区间缩小,再在底层精分割。实现复杂度略高,但能进一步减少DP搜索空间。
- 积分图加速数据项:数据项常需要计算一个区间内视差与给定值的差异总和,采用积分图可以把这个计算从O(区间长度)降到O(1)。
5.2 标定误差对Stixel的放大效应
Stixel极其依赖准确的视差输入。视差误差在远距离上会被Z = fB/d放大成很大的深度误差,底端行坐标一旦偏移,柱体在被投影回三维时就会产生一个"悬空"或者"陷入地面"的假障碍物。
我在项目里吃过一次亏:两三个像素的视差偏移让200米外的一个护栏柱体被测成80米,导致系统在高速巡航时误判前方车辆急刹。后来排查发现不是算法问题,而是某个驱动更新后视差图多了系统性的亚像素偏移。从那以后我坚持在Stixel提取前加入一个自检步骤,用路面视差的拟合结果反推相机外参是否漂移。
更稳妥的做法是定期用路面平面的整体视差残差作为一个"健康度指标",如果某段时间内残差均值明显高于标定时的基线,就触发重新标定或者降级到保守模式。
5.3 视差噪声、孔洞与Stixel的断裂问题
立体匹配生成的视差图通常存在孔洞——无纹理区域匹配失败、反射面失效、遮挡边界不连续。这些孔洞如果不处理,动态规划会在断裂处产生错误的柱体边界。通用做法是先对视差图进行孔洞填充,比较保守的策略是用邻域中值填充,激进一点可以用双边滤波或联合上采样。填充程度需要控制,过度填充会把真正的低纹理障碍物也抹平,导致Stixel高度被严重低估。
另外一个常见问题在高光区域,比如车尾的镀铬条、雨后湿润的路面,这些区域视差经常是无效值。我个人经验是不要盲目填充成周围值,应该标记为"未知",让DP在代价函数里对这些区域给一个较大的惩罚,但允许它们存在。这样处理比硬填一个视差值更能保持几何真实性。
5.4 失效场景归纳
从我的测试经验看,Stixel World在以下场景中容易出现系统性失效:
- 极端倾斜视角:如果相机安装角度过大,障碍物在成像平面不再是近似垂直的,Stixel的高度估计会明显失真;
- 大曲率弯道:路面的消失线快速变化,路面拟合残差增大,底端位置容易上下抖动;
- 密集人流或非刚性物体:人挤人的场景里Stixel会合并成一整条长柱体,后处理很难拆分;
- 逆光或夜间:视差图噪声剧增,DP分割的稳定性显著下降,这类场景通常需要引入红外或毫米波雷达做深度融合。
这些失效场景并不是Stixel特有的,但它的垂直先验使得这些问题比在稠密深度图上表现得更明显。因此在实际工程中,Stixel World通常不是作为独立的感知输出,而是作为多传感器融合中的一个几何先验。它可以用来筛选雷达点云、引导目标检测的ROI,或者给后期追踪提供稳定的底边约束。
最后分享一个个人体会:Stixel World真正价值不在"输出障碍物框",而在它把图像的三维几何压缩成极低维度的结构后,下游算法可以省掉大量无意义的处理。如果你正在考虑在项目里引入它,不必一上来就追求最复杂的层次化版本,先把单列的DP分割和列合并做到扎实,再去考虑扩展。因为无论表示形式怎么演进,底层那套"几何假设+动态规划求分割"的思想,才是Stixel最值得吃透的东西。