ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Stixel World:从视差图到柱状世界的轻量障碍物表示与工程实践

Stixel World:从视差图到柱状世界的轻量障碍物表示与工程实践

如果你啃过一阵子视觉感知,肯定见过那种五彩斑斓的视差图:每个像素一个深度值,整幅图看起来信息量爆棚。可一旦真要往车载嵌入式平台上部署,麻烦立刻来了——几十万像素的深度数据,怎么在十几毫秒内喂给下游?我在前几年的几个项目里都卡在这一步,直到接触了 Stixel World 这种表示方法,才体会到“用一列柱子描述世界”到底有多省事。它既不是稠密视差图,也不是稀疏特征点,而是介于两者之间的一种结构化中间表示,专门用来表达“地面之上有哪些垂直障碍物”。

简单说,Stixel World 把一张视差图压缩成几十到几百个垂直小柱子,每个柱子代表一段连续的障碍物,紧贴地面,带高度、宽度和深度。它对自由空间估计、障碍物检测、低速泊车和辅助驾驶都特别实用。这篇报告我会从原理、数学建模、完整算法流程到工程实施中的坑,一层层拆开讲。适合正在做立体视觉、想快速理解经典感知表示,或者在低算力平台上做感知落地的朋友。

1. 从视差图到柱状世界:为什么需要 Stixel

1.1 稠密视差图的信息冗余

双目相机匹配出来的视差图,每个像素都有一个视差值,换算一下就是深度。看起来很完整,但对下游任务来说,这种表达严重冗余。一个行人可能在图像里占几百个像素,一辆卡车占几千个像素,而这些像素的深度基本都在同一个连续范围内。逐像素去处理,意味着要重复计算大量几乎一样的信息,白白增加功耗和延迟。

后来出现了 U-V 视差(U-V Disparity)这种思路,把二维视差图压缩成两个方向的统计图:V 视差里,地面通常表现为一条斜线;U 视差里,竖直障碍物表现为一条条纵向曲线。这样一来,很多结构信息只需要做一维处理。Stixel World 本质上就是沿着这个思路再往前走了一步:它不满足于统计图,而是直接在一列像素上做出“这一段是障碍物、那一段是背景”的结构化分割,最终输出真正可用于规划的紧凑表示。

1.2 Stixel 模型的直观约定

Stixel 这个词来自 vertical stick + pixel,翻译过来就是“柱状像素”。一个 Stixel 的直观定义是:图像某一列上的一段连续像素区域,这段区域代表一个距离相机一定深度、具有一定高度的垂直物体片段。它的参数很少,一般只有四五个:所在列位置、底部行号、顶部行号、视差值,或者再加上固定的列宽。

打个比方,整幅图像就像一面墙,Stixel 是在墙面上贴的一排竖直条形码。每条码对应一个障碍物片段,码的高度代表障碍物在图像里的可见高度,码的粗细代表我们选取的列宽。编码整个世界只需要几百条码,运算量大大降低,同时保留了障碍物的轮廓和距离信息。

从几何上讲,Stixel 模型隐含了一个强假设:场景大致可以看作“地面平面 + 站立在地面上的垂直物体”。这个假设在自动驾驶的多数场景下是成立的,行人、车辆、护栏、路沿都可以近似为垂直表面。正因为这个假设,柱子才能牢靠地“长”在地面上。

2. Stixel World 原理拆解:数学建模与能量函数

2.1 几何模型与观测假设

先建立最基本的成像关系。双目立体视觉中,视差 (d) 与深度 (Z) 的关系是:

[ Z = \frac{f \cdot b}{d} ]

其中 (f) 是焦距(像素单位),(b) 是双目基线长度。Z 越大,视差越小,这个关系是反比。

对于地面,假设相机光心离地高度为 (h),安装俯仰角固定,那么第 (v) 行像素对应到地面上的点的视差可以写作:

[ d_{ground}(v) = \frac{f \cdot b}{h} \cdot (v - v_h) ]

(v_h) 是水平消失线所在行。这个式子说明,在 V 视差图里,地面是一条近似直线。直线斜率由相机高度和内外参决定。很多经典实现就是靠拟合这条直线完成地面估计。

Stixel 的观测模型也比较直接:假设第 (i) 个 Stixel 覆盖图像列 (x) 上从底边 (v_{b,i}) 到顶边 (v_{t,i}) 的像素,而这段像素对应的三维表面是一个距离相机深度 (Z_i) 的垂直平面。那么理想情况下,这段像素的视差值应该全部等于:

[ d_i = \frac{f \cdot b}{Z_i} ]

如果某个像素的实测视差与 (d_i) 差别很大,就说明它不属于这个 Stixel,而是属于更远或更近的物体。这个差异会进入能量函数,作为分割的依据。

2.2 从分割到能量最小化

构造 Stixel 的问题,本质上是“把一列像素划分成若干段,并为每一段指定一个深度值”的优化问题。直接穷举所有分段和深度组合是不可行的,所以经典做法是把问题写成能量函数,然后用动态规划求最优解。

整体能量由三部分组成:数据项、平滑项和先验项。

[ E = \sum_{i} \left( \sum_{v \in S_i} \rho(d_{img}(v), d_i) \right) + \lambda \sum_{i} 1 + \mu \sum_{(i,j)} \psi(d_i, d_j) ]

第一项是数据项,衡量 Stixel (S_i) 内所有像素的视差与假设深度 (d_i) 是否一致。(\rho) 常用截断的 L1 或 L2 距离,这样个别噪声像素不会过分影响结果。

第二项是模型复杂度惩罚,每增加一个 Stixel 就付一次代价。如果没有这一项,算法会把每一列切成很多小段去贴合噪声,导致输出支离破碎。(\lambda) 控制“想不想合并”,取值越大,输出的 Stixel 越少、越长。

第三项是相邻 Stixel 之间的平滑性约束。真实世界里,相邻的障碍物常常具有相近深度,如果深度跳变剧烈,需要施加惩罚。不过这里要小心,惩罚太强会把真实的物体边界抹平,导致行人和背后的墙被合并成同一个柱子。

动态规划之所以能高效求解,是因为每列的分段是一维结构:从图像底部向上扫描,当前分段的最优代价只依赖前一个分段的位置和深度,具备最优子结构。复杂度通常是 (O(行数^2 \times 视差候选数)),按列并行处理后,可以在毫秒级完成。

2.3 关键参数:列宽、平滑权重与地面模型

真正动手实现时,几个关键参数会直接决定效果。

列宽是整个表示的基本单位。太窄(比如 2 像素),能保留细节但计算量大,且容易受视差噪声影响;太宽(比如 16 像素),速度快但容易把细杆路灯、行人手臂吞掉。我一般习惯先在图像宽度和期望 Stixel 数量之间做个折中,比如 1280 宽的图选 8 像素列宽,这样一列大约 160 个 Stixel,信息充分,计算压力也可控。

平滑权重 (\lambda) 对输出形态影响很大。(\lambda) 偏小,一个行人的手臂和身体会被拆成好几个 Stixel,看起来像锯齿;(\lambda) 偏大,远处两辆紧挨着的车会被并成一个长柱子,对后续目标聚类造成麻烦。这个参数没有通用值,建议在标定场地上摆几个不同距离的箱子,调参时看分割结果是否稳定。

地面模型方面,单平面假设在平路上好用,遇到坡道或者车辆颠簸就会出问题。实际工程中,我更推荐把 V 视差中的地面线做成分段直线拟合,或者用 B 样条表达非平坦地面。如果传感器还有 IMU,可以用俯仰角实时修正 (v_h),地面估计会稳健很多。

3. Stixel 构建的完整实操流程

3.1 输入准备:视差图与 V 视差

整个流程的第一步不是直接做 Stixel,而是先把原始双目图像变成干净的视差图。SGM 类算法是常用选择,但要注意输出中的误匹配和孔洞。我通常会在视差图上做一次 5x5 的中值滤波,把孤立的脉冲噪声消掉,再做左右一致性检查,把遮挡区域的无效值标记出来。

接着构建 V 视差图。具体做法是维护一个二维累加数组,横轴是视差离散值,纵轴是图像行号。遍历视差图,对每个有效像素 ((v, d)),在数组对应位置上加一。最后再沿视差轴做一下平滑,就能看到明显的直线结构。地面那条亮度线就是我们需要拟合的对象。

如果视差图质量太差,地面线会断断续续。一个补救办法是先做一个粗略的地面视差范围限制,比如只在“近处地面可能达到的视差区间”内搜索,避免远处噪声影响拟合。

拟合地面线可以用 Hough 变换,也可以先取若干行峰值点再最小二乘。我个人偏向于取离散点再做 RANSAC 直线拟合,因为 RANSAC 能容忍一部分误检峰值,比直接最小二乘更皮实。

3.2 自由空间边界估计

得到地面模型后,接下来要算出自由空间边界,也就是“车能开到哪儿”。自由空间的定义是:在每一列图像上,从画面底部往上,第一次出现“明显高于地面的物体”的位置,这个位置的下方就是安全区域。

直接逐列找阈值会得到锯齿状边界,因为单列视差噪声太大。更稳的做法是把自由空间边界看作一条从图像底部向上延伸的路径,用动态规划在列方向和平滑约束下寻找最优边界。

具体来说,对每一列 (x) 和行 (v),定义一个代价:

[ C(x,v) = w_{free}(x,v) \cdot P_{free} + w_{occ}(x,v) \cdot P_{occ} ]

其中 (P_{free}) 表示“该位置是自由空间”的置信度,(P_{occ}) 表示“该位置有障碍物”的置信度。置信度可以通过视差与地面视差的偏差来计算。再加一个横向相邻边界的平滑惩罚,防止左右列之间跳变过大。最后从图像底部向上搜索一条累计代价最低的路径,回溯就能得到平滑的自由空间边界。

自由空间边界是 Stixel World 最值钱的产品之一,因为它直接能输出给路径规划模块。边界下方的像素不需要再做任何障碍物判断,大大缩小了后续需要精细分割的区域。

3.3 障碍物 Stixel 分割

自由空间边界之上,才开始真正构建障碍物 Stixel。对每一列,从自由空间边界对应的行开始向上扫描,用前面说的能量函数做分段。

一个比较直观的实现框架是:

for each column x: for each candidate segment (bottom, top): compute mean disparity of the segment compute data cost against pixels in segment DP-search over possible segmentations output stixel list for column x

实际做的时候,候选段的深度不能直接用整段像素均值,否则会把一个跨越不同深度的区域强行融合。通常先对每一列的视差做纵向的“边缘检测”,在视差跳变明显的行处放置候选分割点,再在这些点之间选择最优组合。包含边缘先验的动态规划,效果明显好过对每个位置穷举深度。

分割完成后,每个 Stixel 的深度可以用该段视差中位数表示,比均值更耐噪。之后还可以做一步后处理:删除高度小于某个阈值(比如 20 厘米)的极短 Stixel,它们多半是视差噪声或远处小目标;以及把空间上相邻、深度接近的 Stixel 合并,减少下游聚类压力。

3.4 坐标转换与时间跟踪

有了图像上的 Stixel,还需要把它们投影到世界坐标系,才能用于规划。对每个 Stixel,取底边的像素行 (v_{b}) 和视差 (d),结合相机内参和地面方程,就能计算出它在世界坐标下的位置:

[ Z = \frac{f \cdot b}{d}, \quad X = \frac{(x - c_x) \cdot Z}{f}, \quad Y = \frac{(v_b - c_y) \cdot Z}{f} ]

底边 Y 坐标通常接近于 0(在地面附近),顶边 Y 坐标减去底边 Y 坐标得到障碍物高度。

在时序上,单帧 Stixel 会闪烁,尤其是路面阴影或者远处小目标。更好的做法是给每个 Stixel 关联一个卡尔曼滤波器,用恒定速度模型做跟踪。这样输出不仅有当前帧的位置和高度,还有估计速度,可以直接预测下一帧的位置,也有助于过滤掉单帧误检。

我在实际项目中,通常维护一个全局 Stixel 列表,每次新帧到来先做数据关联,再更新滤波器状态。关联距离阈值设在 Stixel 宽度的 1.5 倍以内,深度阈值设在 0.5 米以内。这样既能稳定输出,又不会把相邻的两个不同物体错误绑定。

4. 工程实现中的常见问题与调参经验

4.1 视差噪声对边界的破坏

视差噪声是 Stixel 的第一大敌。尤其是低纹理路面和天空区域,SGM 经常给出错误深度。一个孤立噪点如果恰好出现在障碍物顶部附近,会让 Stixel 顶部异常凸起,像是凭空长了一根刺。

针对这个问题,我做过几种处理。最有效的是在视差图上引入置信度掩膜:计算每个像素的匹配代价曲线,如果最小代价和次小代价很接近,就认为匹配不可靠,并将其排除出数据项计算。只保留高置信度像素参与能量函数,动态规划就会稳定很多。

另一个技巧是在构建 V 视差前,先把视差图做一次形态学开运算,消除小而碎的白色噪点。我试过把开运算内核设为 3x3,对细长障碍物几乎无损伤,但能把大部分孤立噪声清掉。

4.2 地面估计为什么容易崩

地面估计是整个流程的地基,地基一旦歪了,后面全是空中楼阁。最常见的崩溃场景是坡道:车辆上坡时,地面在 V 视差里不再是直线,而是折线。如果用单直线拟合,拟合结果会把远处的路面抬高,导致自由空间被严重缩短。

一种补救是检测 V 视差中的多条线段,然后根据相机高度和俯仰角约束选择最合理的那条。另一种更通用的做法是放弃全局地面模型,改为局部地面估计:把图像分成若干个横向带,每个带独立估计地面高度,再做平滑。对低速泊车场景,这种方法特别实用。

再补充一个易踩的坑:路面上有大面积反光或者积水时,立体匹配会把镜面区域匹配到无穷远,V 视差里出现一条低于真实地面的伪直线。如果你盲目信任直线拟合,自由空间边界会变得异常激进,直接穿过前方车辆下方。我的经验是加入地面视差的物理上下限,超出理论范围的点直接丢弃。

4.3 遮挡与列对齐陷阱

Stixel 假设障碍物是“贴着图像列”的垂直结构,但现实中的物体往往斜着摆放,比如道路护栏、斜停的车。斜向物体会造成两个问题:一是同一列上可能同时存在两个不同深度的物体,二是左右目的遮挡区域填充了错误背景视差。

遮挡问题主要通过左右一致性检查解决。左图在物体左边缘处,由于双目视差导致右图看不到对应点,会把该区域标记为无效。经过左右一致性检查后,无效区域不会参与数据项计算,但 Stixel 的分割边界仍然可以跨过无效区,这会保留物体的基本轮廓。

斜向物体导致的“锯齿”则通过后处理合并来缓解。我通常会在横向方向对相邻 Stixel 做一次小范围深度平滑,把深度跳变小于阈值的相邻柱子合并为一个更宽的 Stixel。这样输出的轮廓更干净,也更接近真实物体边界。

4.4 参数调整的实操心得

调参这件事,不同平台和场景差异极大,但我可以分享一套比较实用的起步参数,然后按场景微调。

参数初始建议值调整说明
Stixel 列宽8 像素图像分辨率越高可适当增大;细障碍物多则减小
数据项截断阈值1.5 像素视差视差噪声大时上调到 2.5
模型复杂度惩罚 (\lambda)0.02输出碎片多则增大,目标被粘连则减小
平滑项权重 (\mu)0.01配合 (\lambda) 一起调
最小 Stixel 高度0.2 米低速场景可降低到 0.15 米
自由空间最小障碍高度0.3 米路沿检测场景要降到 0.1 米

调整时不要只看最终效果,最好把中间输出(V 视差、地面线、自由空间边界、Stixel 分割图)全部可视化,一层层定位问题。如果自由空间边界乱,先查地面线;如果 Stixel 高度乱,先查视差置信度和数据项截断阈值。我曾经花了一整天调平滑惩罚,结果发现是视差图没做中值滤波导致边界抖动,属于典型的调参方向错位。

在性能优化上,最有效的是限制视差搜索范围。对车载前视场景,视差往往集中于一个区间,比如 2 到 64 像素。把动态规划的深度候选限制在这个区间内,计算量能减少一半以上。其次是按列并行,Stixel 的每一列计算相互独立,很适合用 SIMD 指令或 GPU 加速。我在嵌入式平台上用 NEON 优化过,单目 752x480 分辨率的视差图,Stixel 构建可以跑到 5 毫秒左右。

5. 应用场景、替代方案与 Stixel 的今天

5.1 典型应用:自由空间、目标检测与占据栅格

Stixel World 最直接的应用是自由空间检测。输出边界后,可以直接生成可行驶区域的多边形,用于避障和路径规划。相比深度学习分割网络,Stixel 的几何约束更硬,不会出现“把墙上的门洞误判为可通行区域”这种离谱错误。

在目标检测方面,Stixel 可以作为候选区域生成器。相比在整幅图上滑窗,基于 Stixel 的聚类结果,只需要在少数几个柱子上做分类,计算量小很多。经典做法是把每个 Stixel 代表的图像块送入一个小型分类器,判断它是行人、车辆还是其他物体。即使在深度学习时代,这种做法仍然适合极低功耗场景。

Stixel 还可以投影到鸟瞰图,形成局部占据栅格。每个 Stixel 的底边坐标和高度换算成占据概率,叠加到栅格地图中。与激光雷达栅格相比,视觉 Stixel 栅格的好处是能直接区分地面与障碍物,而纯激光点云还需要先做地面分割。

5.2 与深度学习方案对比,什么时候仍该用 Stixel

这几年端到端网络在 BEV 感知上表现确实惊艳,但 Stixel 依然有它的生存空间。深度学习方案需要高算力、大规模标注数据和充分的域泛化能力,在资源受限的硬件上、在少见场景里、在需要低延迟响应的应用中,经典方案的稳定性和可解释性反而成了优势。

我见过好几个量产项目,最终方案都是深度学习和经典几何混合:神经网络负责高语义任务,比如识别红绿灯和车道线;而自由空间边界和紧急避障仍保留 Stixel 逻辑做兜底。原因很简单,Stixel 的几何假设是静态世界模型,不容易被数据分布变化带偏。

更重要的是,Stixel 可以作为一种结构化监督信号来帮助网络训练。把 Stixel 输出当作一种轻量标签,配合弱监督学习网络预测同构表示,既保留了几何约束,又享受深度网络的特征提取能力。这个方向我自己试过,训练出的模型在域迁移场景下比纯黑盒网络稳不少。

5.3 扩展思路:多帧融合与传感器融合

Stixel 输出的单帧信息还是不够稳定,一个实用的扩展是拼接多帧结果。把连续几帧的 Stixel 投影到车体坐标系,在局部占据栅格中做时间累加,能够大幅减少单帧误检造成的闪烁。这个过程有点像激光雷达的栅格累积,只不过输入是视觉 Stixel,处理起来更轻。

传感器融合方面,如果车上有激光雷达,可以用激光雷达生成的地面模型替代 V 视差拟合。这样即使在坡道和颠簸路面上,Stixel 的底边也能牢牢贴住真实地面。雷达点云还可以提供更准确的深度,修正双目视差在远端的漂移。融合后的 Stixel 质量,比我单独用任何单一传感器都要好得多。

另外一个我推崇的扩展方式,是把 Stixel 与多目标跟踪结合,形成“结构化占位列表”输入预测模块。因为每个 Stixel 都带有高度和深度,预测模块很容易判断它是否会影响自车未来轨迹。与纯图像目标框相比,这种表达更接近决策层需要的感知格式。

6. 调试图与效果评估的小技巧

如果前面几章的算法你已经读懂了,大概率会想立刻动手试试。这里分享几个调试和评估时特别有用的实操技巧。

第一,调试时一定要把 Stixel 叠加到彩色图像上,并按照深度值着色显示。深蓝色表示远,红色表示近。你会发现动态规划的过分割和欠分割一目了然,比盯着数字排查高效得多。我习惯把视差图、V 视差、自由空间边界、Stixel 结果放在同一张调试页面上,任何时候出问题,先截图,再逐层回放。

第二,在评估效果时不要只看平均高度误差,更要看自由空间边界的横向一致性。规划模块最怕的是同一帧里某几列边界突然凸出来一块。我在项目里会特意统计边界曲线的一阶导数变化率,一旦超过阈值就认为这帧结果不合格,然后回溯是哪一层引入的波动。

第三,调参时准备一套“受控场景”,比如在空旷场地放几个纸箱,分别距离 5 米、15 米、30 米。跑一遍离线数据,统计三个距离下的召回和误检。这个数据非常能说明问题,尤其是能够发现地面估计在大距离下是否出现系统性偏移。只要这套场景稳定通过,再面对真实路况就会自信很多。

我个人在实际项目中的体会是,Stixel World 并不是什么高深莫测的模型,它的价值在于把物理世界的强先验用最经济的方式表达出来。这种经典思路在今天依然有生命力,尤其适合那些算力有限、却要求感知结果稳定可解释的场合。如果你也在做视觉感知,建议你动手实现一版纯 CPU 的 Stixel 流程,跑几个真实场景的视差图,把中间可视化打开,跟着调一遍参。你会发现,它比很多黑盒网络更能教会你“什么是几何约束”。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进