先说句实在话:我在做车载视觉感知的头两年里,Stixel World 这个名字听过不少次,总觉得“不就是把像素列切成小竖条嘛,有什么好深究的”。直到我真正接手那个前装辅助驾驶项目,被算力、实时性和可解释性三重压力逼到墙角时,才把相关论文翻出来逐行啃了一遍,又亲手复现、移植、调参。那次经历让我彻底改观——这个算法的设计其实非常精妙,它用极简的参数描述了一个充满结构的世界,直到今天依然是车载视觉里最值得吃透的中间表示之一。
Stixel World 做的事情可以这样概括:从一帧由立体匹配得到的视差图(或者深度图)出发,把图像上每一列像素压缩成若干根竖直的“小柱子”。每根柱子自带四个关键信息:横向位置、底部与地面的接触点、顶部高度、纵向距离。有了这一堆柱子,车辆就能快速回答“前方哪里是能走的自由空间、障碍物在哪个距离、大概多高多宽”这类规划和控制真正关心的问题。
这篇文章适合正在做立体视觉、ADAS 感知算法,或者刚进入自动驾驶领域、对“传统几何方法到底在解决什么问题”感到好奇的读者。我会把 Stixel 从视差图到柱状世界的完整流程讲透,重点放在地面估计、自由空间、动态规划求解、工程实现细节和它在深度学习时代的第二次生命上。很多内容是论文里不会直接告诉你、只有踩过坑才知道的实践经验。
1. 这个算法到底在解决什么问题:被算力逼出来的“列压缩”表示
1.1 直接做像素级感知,为什么在车上行不通
先聊一个最根本的问题:为什么不直接用像素级语义分割或逐像素深度估计,然后在这个基础上做下游任务?
我刚开始做这个项目时,第一反应也是这个。硬件团队给到的算力预算大概是几十 TOPS 级别的嵌入式平台,摄像头每秒输出 30 帧、每帧接近两百万像素。如果用分割网络对每个像素做分类,再把这个分类结果和深度图对齐,内存消耗和计算延迟立刻失控。更重要的是,车辆决策根本不需要知道“前车第 137 行第 402 列的像素是红色车漆”——它只想知道前方 42 米处有一个高度约 1.4 米、宽度约 1.8 米的物体,这个物体的底边距离自由空间边界有多远。
像素级表示的信息密度太低。一个场景里真正影响驾驶决策的目标可能只有十几个,但像素级方法要求你处理几百万个点,其中绝大多数冗余。Stixel World 的思路本质上是一种“压缩感知”:它利用场景的物理结构先验,把一幅图像压缩成极少量的参数化图元,让你既保留了障碍物检测和距离测量所需的关键信息,又丢掉了对决策毫无帮助的细节。
1.2 Stixel 对世界的基本假设:路是平的,物是竖的
Stixel 这个名字本身有点意思,常见的解释是把 stick(小条)和 pixel(像素)拼在一起,意思是把一列像素“粘成”一根小条。这个命名很贴切,因为它暗示了一个强烈的结构假设。
场景里的大部分障碍物——车辆、行人、护栏、路沿、树干——都可以被近似成“底部落在路面上、向上竖直延伸”的结构。在三维世界中,一根 stixel 就是一面垂直于路面的小墙:它有宽度(对应图像的列数)、有底部(与地面的交点)、有顶部(物体的最高可见点)、有深度(离相机的纵向距离)。在图像平面上,这面小墙投影成一个竖直的矩形条带。
这个假设显然不是完美的:行人伸出的手臂、车辆倾斜的车顶、树冠的蓬松轮廓,都会让 stixel 的矩形近似产生误差。但恰恰是这个“强制粗糙化”带来了两个巨大的好处:一是把问题转化成可以用动态规划高效求解的优化问题;二是让输出天然具备几何一致性——每根柱子都“踩”在地面上,不存在漂浮在半空的碎片,这在像素级输出的后处理里反而很难保证。
我实际测下来,用 Stixel 做前车检测效果最自然,车辆侧面和正面的轮廓基本就是规整的竖条;行人在中远距离上也能近似,近距离会稍微糟糕,但是可以通过多层 stixel 或者后续分类网络弥补。场景先验不完美不要紧,关键是它把“大部分情况正确”做成了系统性的默认行为。
2. 从视差图到柱状世界:地面估计和自由空间是两个关键前提
2.1 v-disparity:用一维直线的拟合代替三维地面的估计
Stixel 的输入通常不是 RGB 图像,而是视差图。视差图里每个像素的值记录了双目相机左右图之间的像素位移,位移越大说明物体离相机越近。在“平路 + 相机光轴近似水平”的假设下,路面上每个点投影到图像上时,它的行坐标 v 和视差值 d 之间存在近似线性的关系:远处的路面行坐标更高、视差更小,近处的路面行坐标更低、视差更大。也就是说,在二维的 (v, d) 空间里,地面是一条斜线。
这就引出了经典的 v-disparity 方法。做法很简单:对视差图的每一行,统计该行内出现的所有视差值,做成一个直方图,再把每一行的直方图沿 v 轴排列起来,得到一张“行号 v——视差 d——像素计数”的二维累加图。在这张图里,地面因为像素数量巨大,会形成一条明显的亮斜线;而垂直障碍物(比如一辆车的背面)在图像的多行上视差基本不变,所以会形成一条接近竖直的亮线。
里面最有用的一点是:地面直线和障碍物竖直线在 v-disparity 空间里很容易区分。拟合地面时可以用 Hough 变换找最亮的那条斜线,也可以用 RANSAC 或加权最小二乘迭代拟合。我当时在项目里用的是加权 RANSAC,因为视差图充满噪声,Hough 变换对参数分辨率比较敏感,反而没有 RANSAC 直观。拟合完成后,你会得到一个函数 d_ground(v),表示“如果这一行是地面,它应该有多大视差”。这个函数是后续所有计算的地基。
2.2 自由空间:每根 stixel 的“脚”应该踩在哪
拿到地面模型后,接下来要解决的就是自由空间估计。自由空间是指从图像底部开始、直到第一个障碍物之前的所有可行驶区域。在几何上,它就是地面模型覆盖的范围;在 Stixel 框架里,它具体表现为每根 stixel 的底部行 v_b。
怎么找到 v_b?对每一列,从图像底部往上扫描,把每个像素的观测视差和地面模型给出的“预期视差”比较。如果连续若干行都明显偏离地面模型,说明这里被一个非地面的物体遮挡了——这个连续偏离的起始行,就是该列第一个障碍物的底部。这里“连续若干行”的阈值很重要,单像素判断会被视差噪声轻松打穿,我当时取的是连续 3 到 5 行显著偏离才确认进入障碍物区域。
从这一步开始,你应该能感受到 Stixel 的思维模式了:它不试图精确重建每个物体的形状,而是先把“可行驶区域”和“障碍物区域”的边界找出来,再用一根根柱子去近似障碍物。自由空间边界本身就是 ADAS 决策最核心的输入之一——很多自动紧急制动和自适应巡航的逻辑,本质上就是在追踪这个边界随时间的变化。
3. 一列像素如何切成一堆最优柱子:目标函数与动态规划求解
3.1 一根 stixel 怎么打分:从观测到代价函数
地面模型和自由空间把问题粗筛了一遍,但真正困难的地方在于:给定某一列的所有像素,你怎么决定哪里是地面、哪里切出一根 stixel、这根柱子顶到哪一行、深度取多少?
答案是把这些决策都放进一个代价函数里,然后求全局最小化。一根候选 stixel 的代价由三部分组成。
第一部分是匹配代价,即“这根柱子内部的观测视差,跟它声明的统一视差 d 一致的程度”。理想情况下,stixel 内部的像素应该来自同一个深度平面,所以它们的视差都接近 d。我把每个像素的视差和 d 的差做一个截断的二次损失:差值小的时候按平方惩罚,差值超过某个阈值后线性增长甚至直接截断。为什么要截断?因为视差图的噪声有很长的尾巴,阳光反光、弱纹理、遮挡边缘都会产生极端离谱的视差值。如果不截断,一根正常柱子可能被一个离群像素彻底带偏。
第二部分是地面贴合代价,即“柱子的底部视差应该和地面模型在 v_b 处的预期视差接近”。这根柱子要想稳定地立在路面上,它的底部不能悬空,也不能扎进路面以下。实际算的时候,我会把 v_b 处的地面视差和柱子底部观测视差之差算进代价,相当于给“踩稳地面”这件事加了一个软约束。
第三部分是结构惩罚,主要用来约束相邻列的 stixel 不要突跳。真实的障碍物在空间中是连续的,反映到图像上,相邻两根柱子的底部、顶部和深度都不会剧烈变化。这个约束我在后面工程节还会细说,它既是精度来源,也是实时系统稳定性的关键。
3.2 DP 递推:怎么把整列像素的决策变成全局最优
有了单根柱子的代价定义,整列像素切分成“地面段 + 若干物体段”的问题就是一个典型的一维序列分割问题。把每一行看成一个待打标签的样本,标签要么是“地面”,要么是“物体”。因为 stixel 是连续的,所以“物体”标签在纵向上会形成若干连续的区间,每个区间对应一根柱子。
这个结构用动态规划求解非常自然。我实际编码时的简化伪代码如下:
# 单列内 stixel 分割的简化示意 # 状态 0 表示当前行属于地面,状态 1 表示当前行属于某根物体 stixel # 为表达思想,这里省略了“同一物体段内深度一致”的深度维,只保留分段切换逻辑 def stixel_column_dp(obs_disparity, ground_disparity, lambda_obj, min_obj_height): n = len(obs_disparity) INF = float('inf') dp = [[INF, INF] for _ in range(n + 1)] dp[0][0] = 0 # 第一行之前视为地面 for i in range(1, n + 1): # 当前行是地面:可以延续地面状态,也可以从物体状态切回地面 dp[i][0] = min(dp[i - 1][0], dp[i - 1][1]) + ground_loss(i, obs_disparity[i], ground_disparity[i]) # 当前行是物体:延续物体状态,或从地面状态开启一个新物体段 continue_obj = dp[i - 1][1] + object_observation_loss(i, obs_disparity[i]) start_obj = dp[i - 1][0] + lambda_obj + object_observation_loss(i, obs_disparity[i]) dp[i][1] = min(continue_obj, start_obj) return min(dp[n][0], dp[n][1])真实实现里会复杂不少:物体段的深度 d 需要维护一个估计值,每个可能的段结束位置要和该段内所有像素的观测一起计算代价,所以朴素实现是 O(n^2) 甚至更高。工程上一般会限制 stixel 的最小高度、把候选深度离散化到有限的视差档位,再把扫描过程优化成近似线性,否则嵌入式平台扛不住。
这里有一个非常关键的直觉:DP 求的不是“每一行独立地最像什么”,而是“整列的分割方案全局代价最小”。这意味着算法愿意接受某个像素被强行分到“看起来不太像”的类别里,只要它换来相邻区域的整体一致性。这个性质让 Stixel 的输出特别稳定,几乎不会出现像素级方法那种空间上破碎闪烁的结果。
3.3 新物体“开户费”:为什么要给每个 stixel 加一笔惩罚
上面伪代码里有个 lambda_obj,我习惯叫它“开户费”。它表示每开启一个新物体 stixel 需要付出的固定代价。不加这笔代价,DP 会发现把一个物体段切成无数个一像素高的小段总能降低观测损失——因为每个小段的深度都可以精确贴合自己的像素,损失趋近于零。结果是每一列输出几十根细碎的小柱子,毫无物理意义。
lambda_obj 本质上是“模型复杂度惩罚”,和机器学习里的正则化是同一个道理。它的大小决定了算法愿意新增物体的保守程度。我喜欢把它理解成这句话的量化版本:“如果你认为这堆像素构成一个独立的新物体,你必须用足够的观测证据说服我。”
调这个参数是我整个项目里最有手感的部分。lambda_obj 太大,算法会把两个前后重叠的物体合并成一个大柱子,前方车辆的间距估计被污染;lambda_obj 太小,地面上的减速带、井盖、阴影边缘都会成为独立柱子。我这里给一个实践参考:可以先固定场景,扫描一个对数范围,画一条“柱子数量 vs 检测召回率”的曲线,取曲线拐点附近的值,再在几个典型场景上微调。通常这个值的效果和 stixel 最小高度强相关,需要一起调,不要只看单个参数。
4. 复现和移植中的真实踩坑:视差、外参与实时性
4.1 视差预处理比后续任何算法都重要
如果只能给一个关于 Stixel 的建议,我会说:花在视差图预处理上的时间,怎么都不算多。Stixel 的所有假设最终都落在视差图上,视差的错误会直接变成柱子的错误,而且很难在后面的步骤里修正。
我的第一版实现直接拿半全局匹配(SGM)的原始输出喂给 Stixel,结果柱子剧烈抖动。后来按照标准流程补了三件事:左右一致性检查,剔除因遮挡产生的误匹配;亚像素抛物线插值,把视差的离散台阶抹平;加权中值滤波,滤掉孤立噪声点。
这里有个容易被低估的细节:滤波核不能开大。我一开始用了 7x7 的中值滤波,视差图确实变得很干净,但物体边缘也被抹圆了,导致柱子底部不贴着真实物体轮廓,自由空间边界整体往里缩了一两米。后来换成 3x3 加权中值,效果立刻好很多。边缘保持和噪声抑制的平衡,在 Stixel 这种“按列压缩”的算法里尤其重要,因为边缘的每一行都可能成为柱子的边界。
另外,反光区域必须做置信度门控。我当时的做法是:SGM 会输出一个匹配代价,代价过高说明这个像素的视差不可信,直接给它一个“无效”标签,Stixel 在统计观测损失时跳过这些像素。如果硬要把反光处的错误视差算进代价,柱子会被拉长或者悬空。
4.2 外参敏感性与地面模型动态更新
Stixel 的地面模型假设相机高度和俯仰角是固定的。但真实车辆加减速、载重变化、过减速带时都会产生俯仰角变化,哪怕只有一两度,地面直线在 v-disparity 空间里的斜率和截距就会明显偏移,自由空间边界跟着晃,远处的柱子高度也会系统性错误。
我在某次测试里就碰到一个非常典型的 bug:前方大约 15 米处有一段轻微上坡,算法把这段坡面识别成了“悬浮物”。原因是地面模型还固定在上一次平路标定的参数上,坡面的真实视差和地面模型预期的视差差了一截,于是被当成了高出地面的障碍物。这个场景至今是我衡量一个 Stixel 实现是否合格的试金石。
工程解法是把地面模型的拟合从“标定一次用到底”改成“在线估计 + 时域平滑”。v-disparity 每次可以快速拟合出当前帧的地面直线,但单帧拟合受噪声影响大,所以我的做法是用卡尔曼滤波或者滑动平均对直线参数做平滑。车辆俯仰变化是慢变量,平滑后既跟得上趋势又不会被单帧噪声带偏。
4.3 并行化、量化与实时性优化的几个方向
Stixel 在实时性上的天然优势就是列间独立:每一列的 DP 只依赖本列数据和全局地面模型,列与列之间没有耦合。这意味着你可以把图像横向切块,分给多核并行计算。我当时把 1280 宽的图像切成四块,四核并行跑,延迟直接降到单核的四分之一左右。
还有几个实用的优化习惯。一是全程用视差而不是深度来算代价,省掉大量的除法运算,最后在输出阶段才把关键 stixel 的视差换算成距离。二是地面模型的预测视差按行预计算成一张查找表,每行扫描时直接查表,不用重复算直线参数。三是代价计算里的截断损失可以用查表法实现,把像素视差和模型视差的绝对差映射到损失值,避免实时计算平方根或 exp。四是精度优化上用 16 位浮点就够了,我试过 8 位整数在某些场景会明显损失底部位置的精度,16 位是安全的选择。
5. 这套“老方案”如何激活了深度学习时代的新价值
5.1 从像素级“软输出”到几何级“硬表示”的桥梁
很多做深度学习的同学对 Stixel 的第一反应是“过时”——既然语义分割和单目深度估计已经能把场景理解得很细,为什么还需要这种粗暴的柱状近似?
我的看法恰恰相反:深度学习输出的“软”结果,反而更缺 Stixel 这种“硬”变换。语义分割网络输出的是一张概率图,单目深度网络输出的是平滑的深度场,这些结果空间连续但缺乏几何一致性。你很难直接告诉规划模块“前方 30 米处那个物体的边界到底在哪”,因为概率图 0.7 和 0.8 之间没有明确的分界线。把 CNN 输出的深度场或者语义分割结果送进一个 Stixel 后处理层,软的概率被压成硬梆梆的柱子,每根柱子都明确地钉在路面上、带一个清晰的纵向距离,下游模块拿它做碰撞检测要顺手得多。
这种“软到硬”的转换还天然具备时间一致性。CNN 逐帧输出容易闪烁,同一辆车这一帧被分成两截、下一帧又合成一个整体;而 Stixel 的全局优化和结构惩罚天然抑制这种无意义抖动。我做过一个对比:同样的网络输出,直接做聚类得到的目标框抖动明显,经过 Stixel 层后再提取目标,帧间稳定性好了一个量级。
5.2 让网络直接输出 stixel:从两阶段到端到端
近年来有一类工作走得更远:直接把 Stixel 的参数(每一列是否有柱、底部位置、顶部位置、深度)作为网络的回归目标,不再依赖传统立体匹配。这类方法的训练标签可以用传统 Stixel 方法自动生成——先用高质量视差图跑一遍传统算法,得到几百万帧的柱子标签,再用这些标签训练一个编码器-解码器网络。相比传统两阶段方法,深度版本避免了低纹理区域匹配失效的问题,也在弱光、雨雾场景里表现得更有韧性。
对这个趋势我个人的判断是:最终量产方案大概率不会是“纯传统”或“纯端到端”,而是两者混合。传统 Stixel 提供一个基于几何的可解释结构,深度网络提供在困难场景下的感知能力,两者叠加后互相兜底。至少在我接触过的几个项目评估里,这种组合方案的落地风险远小于纯网络方案。
5.3 可解释性:安全工程里最容易被低估的资产
现在自动驾驶感知越来越倾向于端到端黑盒,这带来了一个很现实的问题:当系统做出一个奇怪决策时,你拿什么去分析?如果你手里只有一张网络的注意力热力图,排查效率会非常低。而 Stixel 的输出是人可以直接理解的物理表示——前方 42 米有一根柱子,底部贴地、高度 1.6 米、横向宽度 2 米,这就是“有一辆车”这句话的几何编码。测试回放时,我可以直接把柱子叠加在视频上,一眼看出哪些柱子是错的、错在什么地方。
这个可解释性在安全工程里的价值怎么强调都不为过。我遇到过不止一次:客户对算法的置信度有疑虑,最终说服他们的不是更复杂的网络指标,而是一段叠加 stixel 的实车视频——每根柱子都清清楚楚地标着距离和高度,非专业背景的人也能看懂系统看到了什么。Stixel 提供的这种“人类可读的中间表示”,是它在深度学习时代依然有生命力的根本原因。
最后分享一个个人建议:如果你想深入理解视觉感知里的几何方法,不妨把 Stixel World 当作一个训练课,亲手实现一遍从视差图预处理、地面拟合到 DP 分割的完整链路。它教给你的不是某个具体网络的拼装方式,而是“如何把一个物理世界的问题表达成可优化的数学形式”——这个能力不管技术潮流怎么变,都永远不过时。