Stixel World原理详解:从视差图到自动驾驶障碍物检测 做自动驾驶感知的工程师应该都听过 Stixel World 这个名字。第一次接触这个技术是在一个道路障碍物检测的项目方案评审会上当时我满脑子还是逐像素分类那一套看到团队里一位老哥把整幅道路场景压成几十根竖着的彩色柱子第一反应是这也太粗糙了吧。后来花时间把 Stixel World 的原理彻底吃透又实车调了大半年才意识到这种“粗糙”恰恰是工程上最聪明的取舍。Stixel World 的中文叫柱状世界表示核心思路是把三维场景从像素级压缩成一组垂直小柱每一根柱子用底边位置、高度、宽度、距离四个量来描述百万像素的视差图最终变成几百个带物理意义的单元。这年头做自动驾驶感知算力永远不够用而 Stixel World 的价值恰恰在于以极低的计算代价把“哪里是地面、哪里立着东西、立着的东西离我多远、大概多大”这几个最关键的几何问题一次性回答清楚。这篇文章我不会照着论文念公式而是从一个实际项目经历的角度拆解它的原理、代码落点、工程实战和踩坑经验。无论你是刚入门视觉感知的学生还是在做 ADAS 前装量产的老兵应该都能从里面找到点有用的东西。1. 先搞清楚 Stixel World 到底解决什么问题1.1 像素级表示的算力账绝大多数项目根本算不过来先来看看朴素的感知方案是什么思路。拿双目相机拿到视差图之后最直接的做法是把每个像素都分类这个像素是路面、是车辆、是行人、还是天空。听起来很美好做起来很绝望。假设图像分辨率是 1280x720那就是接近 92 万个像素点。对每个像素提取特征、跑分类器、再做条件随机场平滑这一套下来单帧耗时普遍在几十毫秒到上百毫秒。放到嵌入式平台上帧率直接掉到个位数更为致命的是逐像素分类结果里到处是散粒噪声同一辆车表面有的像素被判成障碍物有的像素被判成地面后续聚类算法拿到这种输入最常干的事就是给你把一辆车拆成七八块。我自己测过同样的视差图输入逐像素高斯混合模型分类加后处理在桌面级 GPU 上跑一帧大约 45 毫秒。可到了车规级芯片上这个数字直接翻三倍都不止。而 Stixel World 的做法完全绕开了这个泥潭它不关心每一个像素到底是什么类别只关心整列像素的视差轮廓怎么分段。这一下就把问题规模从“几十万像素分类”降成了“几百列动态规划”。1.2 直接上 3D 目标检测其实也有隐含假设有人会说那我不做像素分类直接上 3D 目标检测不行吗现在基于点云或基于视觉的 3D 检测确实火精度也高。但这类方法通常假设场景里有若干个明确的离散目标比如车、人、自行车。真上了路你会发现真实道路场景还有大量没法归到某个语义类别的障碍物路边半米高的水泥墩子、墙面伸出的一截空调外机、施工区域那种歪歪扭扭的锥桶队列。这些物体你很难给它们一个准确的类别标签但你又绝对不敢让车撞上去。Stixel World 的哲学是我不管你是什么我只知道你是一根竖着的、有一定高度的、挡住了行驶路线的柱子这就足够了。这个思路特别适合做“保底感知”。在深度学习模型漏检、误检甚至完全失效的时候Stixel World 这种基于几何的方法依然能给规划控制一个最低限度的障碍物轮廓。它不跟你讲语义只跟你讲几何。1.3 中间表示把三维世界折叠成几百根信息柱Stixel World 之所以叫“World”是因为它构建的是一个轻量级的三维世界模型。你看那根柱子不是图像上的一个 2D 框。柱子有一个底边的三维位置也就是它触地的那条线距离自车多远柱子有一个三维高度表示它从地面向上“长”了多高柱子还有一个深度距离表示它在空间里的远近。整个场景被描述成一组垂直于路面的、宽度固定的柱子阵列。路面本身用自由空间模型表达障碍物用柱子表达。就是这么一组极简表达让下游模块——无论是紧急制动 AEB、路径规划还是自由空间检测——都能直接读取物理量而不是再去做反投影。2. 原理拆解从视差图到 Stixel 的三步走2.1 第一件事拿到一张靠谱的视差图Stixel World 不是从单目图像凭空变出来的它的基础是视差图。视差图里每个像素的值表示左右目图像中对应匹配点的水平位移单位是像素。有了视差 d深度 Z 就能通过相机参数算出来Z f × B / d。f 是焦距像素单位B 是双目基线长度米。这个公式很好理解两件事离得越近d 越大、相机基线拉得越开B 越大、镜头越长焦f 越大测距就越精确。这里有个很容易犯的认知错误视差图越“干净”Stixel 效果越好。但真实场景里大面积弱纹理区域白墙、天空、光滑路面视差是缺失的雨雾天、夜间更是重灾区。LOW 纹理区域在立体匹配时就容易出现空洞。所以很多成熟的 Stixel 实现第一步反而不是急着做动态规划而是先对视差图做一次预处理插值和中值滤波把孤立噪声去掉把空洞区域用邻域背景填上。我常用的策略是先做 5x5 中值滤波再对残余空洞做横向近邻填充实测能把后续动态规划里因为视差噪声导致的“柱子断层”减少一大半。2.2 第二件事不等不靠先估计出自由空间边界Stixel 分割的重要前提是知道哪些区域是“可以走的”哪些区域是“被物体占着的”。这个任务在算法里一般叫自由空间估计Free Space Estimation。传统思路里有个很经典的工具叫 V-视差图。把视差图按行统计视差值直方图就得到一个 V-disparity 图像横轴是视差值纵轴是图像行号。在这个图上路面有一个非常漂亮的几何性质路面上的点在视差空间里基本落在一条直线上。因为路面的三维高度处处为 0视差和图像行号近似成线性关系。通过在这张 V-disparity 图像上只拟合一条直线就可以得到整幅图里路面每个行对应的“地面视差期望值”。然后把原始视差图和这个期望值逐像素比较凡是视差明显大于地面期望的像素说明它和自车更近基本可以断定是这个垂直位置上有物体“捅破了”地面。把所有满足条件的像素连起来就得到了每条列上的自由空间上边界。这一步是整个算法后续动态规划的重要初值搞错了地面后面所有 Stixel 的高度和底边位置全都会漂移。我自己在这块踩过最大的坑是上下坡时路面直线变成折线所以正规项目里地面模型一般会扩展成多段线性或二次曲线模型。2.3 第三件事动态规划把每一列切成最优分段地面模型给出来了每个像素“是不是地面”也有一个大概的判别了接下来怎么变成那几根干净的柱子此时就要上动态规划了。对每一列像素单独处理这一列从上到下可以看成一组视差值序列。观察这个序列你会看到它在“地面区域”有比较平滑、贴合地面模型的视差值一旦遇到障碍物视差值会突然变大障碍物离车更近。我们要做的就是在这个序列上找到若干条分界线把这一列切成一段段。每一段要么是“地面段”要么是“障碍物段”。障碍物段就是一根 Stixel。切分不是随便切的而是要最小化一个能量函数这个能量函数通常由两部分构成数据项切出来的每个分段其内部像素的视差值应当与该分段的模型地面模型或物体平面模型拟合良好。物体段可以假设近似一个平面甚至常数视差这一步最常用到中值估计。平滑项相邻列的切分边界不应该跳来跳去毕竟真实世界里墙体、车辆扫过连续几列底部边界应当连续变化。这一项我们一般用相邻列的分界点之间的差值的绝对值来惩罚。动态规划的优势在于全局最优不会像贪心算法那样在某一列上拍脑袋切一刀结果整面墙参差不齐。一次列扫描里每一列内部做一维 DP复杂度是 O(列数 × 行数 × 状态数)。因为是独立列的天然可以并行处理。我实测过在四核嵌入式处理器上用线程池把图像分成四条带单帧完整的 Stixel 提取大概能控制在 5 到 8 毫秒内这在当时已经是很可观的性能了。3. 数据结构与伪代码让 Stixel 真正跑起来3.1 一根柱子的“五脏六腑”长什么样看了很多开源实现它们给 Stixel 定义的数据结构五花八门但核心字段永远是那几样。我会用一个精简但完整的 C 结构体来定义。这个结构体之所以这么设计是因为下游模块AEB、泊车、路径规划最关心的就是这几个物理量struct Stixel { int colStart; // 起始列索引块内第一列 int colEnd; // 结束列索引 int rowBottom; // 底部触地行坐标像素 int rowTop; // 顶部行坐标像素 float disparity; // 该柱子代表的视差值可近似为平均视差 float depth; // 换算后的深度单位米 float height3D; // 三维高度单位米 float width3D; // 三维宽度单位米 bool valid; // 有效标志 float getWidthPixels() const { return static_castfloat(colEnd - colStart 1); } };这里有两个容易搞混的概念一定要分清楚rowTop 是图像行号数值越小越靠图像上方而 height3D 是三维世界里的物理高度。有的人做可视化时直接把图像上柱子的高度当成三维高度结果会闹出“整栋楼是根 40 米高柱子”的笑话。换算高度时需要通过 rowBottom 和 rowTop 各自反投影出三维坐标再做差这里底座必须在同一个地面上。如果没有严格的地面模型宁可不输出高度也不要输出一个凭空拍脑袋的高度值。3.2 完整处理流程从视差图到柱子列表下面用伪代码把最朴素的 Stixel 提取流程串一遍。这不是某个特定开源库的实现而是把论文方法落成可执行逻辑的通用做法输入视差图 dispfloat无效值标记为0或负值 1. 对视差图做中值滤波消除孤立噪声点 2. 构建 V-disparity 图按行统计直方图 3. 在 V-disparity 图中拟合地面直线或用RANSAC鲁棒拟合 4. for col in [0, width, columnStride]: a. 提取该列像素的视差序列 dispSeq b. 用动态规划将 dispSeq 分为若干段地面段/物体段 c. 记录每个物体段的底部行 rowBottom、顶部行 rowTop d. 计算该物体段的平均视差 dispMean 5. 对相邻列生成的物体段做合并同一连贯区域合并成宽Stixel 6. 将每个合并后的 Stixel 做三维反投影计算 depth 和 height3D 7. 输出 ListStixel这段流程里步骤 5 特别容易被初学者忽略。如果不做列间合并最终只会输出一堆单列宽、形状像牙签的柱子视觉上很碎规划模块也没法用。合并的规则非常简单只有相邻列在 rowBottom、rowTop、disparity 三个维度上都足够接近才判定为同一根柱子。这个接近阈值我建议用比例而不是绝对像素底部行差不超过当前列高度的 5%顶部行差不超过 10%视差差不超过 2 个像素。实测这个比例在变道、转弯、上下坡时都能保持较好的稳定性。3.3 关键参数怎么定直接给参考值参数的取值直接决定 Stixel 系统表现这里把我在项目里调过最核心的几个参数列成表给出经验值区间和典型配置。请注意不同相机的分辨率和基线下数值会有变化不要无脑照抄但可以当起点。参数作用经验值/典型值备注columnStride / 列宽每多少列取一根柱子2~8 像素数值越小精度越高代价是计算量大地面拟合行数范围只统计哪些行来拟合地面图像底部 60%让天空和远处物体不干扰地面模型V-disparity 直线拟合点数阈值判定一个视差值行是否为路面该行像素数 3%以上太低容易被误判为路面DP 数据项权重拟合度在能量函数里的占比0.6~0.8太高柱子会贴着单像素噪声走DP 平滑项权重相邻列边界连续性0.2~0.4太高会抹平真实物体的边界细节最小 Stixel 高度低于此高度的物体段直接丢弃图像高度的 1.5%过滤地面碎屑、阴影位置一致性合并阈值相邻列合并判定rowBottom 5%rowTop 10%视差 2px详见上文关于 columnStride 我要多说一句。很多论文里为了强调“独立列并行”喜欢把列宽设为 1。实际工程里1 像素的列宽会使得噪声被当作 1 像素宽的细小柱子输出掉了满地的噪点。我调下来发现如果分辨率是 1280 宽取 stride 为 4 是性价比比较高的选择既保留了大多数真实的窄障碍物形态又大幅度抑制了单列噪声。要是做远距离探测100 米以外我甚至会把 stride 放到 6~8因为远处目标在图像上本来就小列宽加大会减少计算量而且对远处小目标的检测也不会损失太多。4. 工程落地Stixel 在实车感知里的几种典型玩法4.1 快速障碍物预筛给 3D 检测节省算力直接上 3D 检测器话费是很贵的尤其是像 1280 × 720 输入全图推理一帧就得几十毫秒 TensorRT 推理时间。而 Stixel 只需要几毫秒。那能不能让 Stixel 先“扫一眼”告诉检测器“图像这个区域有东西你重点看看”这个思路我们在项目里叫 ROI 预筛。具体做法是把 Stixel 结果投影回图像坐标生成一个包围盒区域。只在这些包围盒区域上调运检测模型。没有 Stixel 覆盖的区域呢大概率是空旷路面不需要浪费算力。实测在某些城市场景下能把后级感知算力节省 50% 以上而漏检率几乎不变。当然这套做法的前提是 Stixel 的召回率足够高。我常说要保证“宁多勿漏”把 DP 阈值调得稍微松一些宁可多输出几根假柱子也要保证真障碍物全部落在 ROI 里。因为后面还有类别分类器把关多余的框最多增加一点算力开销而漏掉的框直接意味着系统对那个障碍物完全失明这是安全层面无法接受的。4.2 自由空间划分给规控模块画可行驶边界自由空间估计是 Stixel 最强的一项能力。因为 Stixel 的底边位置直接给出了每个障碍物触地的边界线把所有柱子的底边点连起来就得到了场景里一条不规则的可行驶边界。这条边界对路径规划来说极其宝贵因为它的计算快、物理意义清晰而且不受光照变化影响——它完全依赖于双目视差几何不看颜色不看纹理。行人、骑行者、车辆、路缘石无论外观差异多大只要它们在物理上是“凸起的、高于路面的一坨”Stixel 就会在边界线上留一个“尖刺”。这些“尖刺”恰恰是规划算法最需要避开的点。在这类应用里障碍物语义分类反而不是重点。更重要的是把底边接触点的世界坐标精确输出。我一般会做一个轻量级的卡尔曼滤波对每一根 Stixel 的底边点做时间平滑。这样即便某一帧出现一根假柱子规划的参考线也不会被一带就跑偏。时间平滑的增益我建议控制在 0.2~0.5太小有延迟太大会导致真的撞上障碍物时边界线退让不及时。4.3 时代进化Stixel 给深度学习当先验这几年深度学习模型满天飞有些人觉得 Stixel 这种老派几何方法该被扫进历史了。实际上不完全是那么回事Stixel 在深度学习框架里重新找到了自己的生态位。最常见的是把 Stixel 结果当成注意力机制的先验输入。深度模型的目标检测分支只在 Stixel 标记的区域赋予更高的注意力权重提升远距离小目标的召回率。同时Stixel 提供的深度先验也可以用来引导单目深度估计网络缓解单目深度估计尺度模糊的问题。还有团队把 Stixel 当作路径规划决策的“几何安全带”专门用了与深度学习模型完全独立的置信度通道模型说前方无障碍但 Stixel 显示前方有一根 0.5 米高的柱子最终决策以 Stixel 为准。这其实就是功能安全里“异构冗余”的思想。从架构演进的角度看Stixel 这种中间表示不会因为深度学习的流行而消失它反而会以“几何基础层”的形态沉淀下来。因为视觉模型的软肋恰恰是几何单目深度可能飘分割边界可能毛糙而 Stixel 直接回到底层几何约束上用三角测量给出实打实的空间位置。两者结合属于取长补短。5. 常见问题与排查技巧实录5.1 车身抖动导致 Stixel 边缘像锯齿实车上最普遍的现象车辆过减速带、转弯侧倾的时候Stixel 的边缘会突然参差不齐看着像锯齿一样。根因在相机运动。双目视觉本身假设两帧之间是静止的车一颠地面模型和前一帧的几何先验就不完全对得上了DP 切分边界随之跳变。排查时不要急着调滤波先看地平线的位置。把车停稳看 Stixel 提取结果是否稳定。如果稳定那基本可以断定是运动干扰。解法主要有三类接入惯导数据把每帧的相机外参做一次运动补偿或把 DP 的平滑项权重和时间平滑的增益调大或者在 V-disparity 地面拟合时使用上一帧的地面模型做初始值用一个小权重更新这样能抵抗单帧抖动。我在项目里选的是第三种实现简单效果稳定实测锯齿减少了 80% 左右。5.2 细长物体被“吞掉”或一分为二电线杆、路灯杆、锥桶这类目标在图像上可能只占 2~3 列像素本来就不够宽。两种典型故障一是被当成噪声直接滤掉二是虽然找到了但因为在某些行上视差突变被 DP 从中间切了一刀一根柱子变成上下两截。排查建议从两个方向入手。一是降低中值滤波窗口从 5x5 改成 3x3避免细长物体被邻域背景抹平。二是在 DP 之后的合并阶段增加一个“上下段拼接”步骤如果在同一列上下方段底部接触地面、上方段紧挨着下方段的顶部且视差值接近比如差小于 3px就把两者合并成一根 Stixel。这个逻辑在掉电杆、扫把类物体上极其常用因为细长物体经常在某个高度上因为反光、遮挡导致测得的一段高度不可靠。5.3 上下坡场景地面模型失效怎么办平地上 V-disparity 拟合直线效果很好。但一上下坡地面投影在图像上就不再是一条直线而是一条折线甚至曲线。如果只做直线拟合坡顶的物体可能整个被判断为路面的一部分Stixel 直接漏检坡底的物体底边位置也会算错。排查时如果发现固定位置的障碍物在上下坡时底边跳动超过三五个像素基本就是地面模型错了。解决方法有两个一是把 V-disparity 的直线拟合改成多段线性拟合每一段用 RANSAC 找局部直线段与段之间平滑衔接二是引入 IMU 的俯仰角先验用车辆姿态直接修正地面模型参数。我个人的经验是多段拟合在绝大多数非铺装路面都能应付IMU 辅助更适合城市快速路那种高频小起伏场景。两个方法可以叠加但优先保地面模型本身的鲁棒性毕竟 IMU 也有漂移。5.4 夜晚、欠曝、雨雾天视差空洞灾难晚上在城市道路路灯照不到的区域基本就是双目的“盲区”大片视差是 0Stixel 提取器会把这些区域全部判为地面——对一片空洞意味着没有阻挡视线的物体但真实情况可能是有一辆黑色车停在那里。单纯靠 Stixel 的几何信息是救不回来的因为输入本身就是残缺的。遇到这种情况最务实的处理方式是“失明报警”统计整张视差图有效像素比例如果低于某个阈值比如 20%就在系统层上报感知置信度下降提醒决策模块降低车速或采用冗余传感器比如毫米波雷达接管。不要幻想算法层面能无中生有。这个认知非常重要很多新手在这个问题上死磕给空洞插值、给低纹理区域填充假视差折腾一圈反而容易出现更致命的大面积误检。我现在的原则是视差不够时宁可靠别的传感器兜底也不要强行让 Stixel 交出错误的答案。6. 我自己踩过的坑与调参建议6.1 先别急着调参先保证标定和同步是好的刚开始做 Stixel 项目时我花了两周调参数效果一直是“一半好一半烂”。后来被一位做相机标定的同事提醒去查了双目标定参数果然发现有一台相机的外参在车辆震动后偏了几毫米。这个误差在近距离只有几厘米偏差但到了 50 米外直接造成视差计算误差放大到十几个像素。Stixel 的一切基础都是视差视差错了柱子位置、高度全错。所以如果你发现无论怎么调 DP 权重都压不住噪声先回头做一个静态标定验证把棋盘格放在 5 米、10 米、20 米远处确认深度误差曲线是否平缓。磨刀不误砍柴工这个检查通常半小时就能做完。6.2 宁要物理意义不要“像素美观”可视化时有的人喜欢让 Stixel 和图像边缘严丝合缝地贴合还把柱子边界描得特别精确。但做感知不是做 UI过度追求像素级的边缘拟合只会让算法对噪声更敏感。我在实测中发现真正让下游好用的 Stixel 反而是“稍微丰腴”一点的那种底边位置稳、高度保守偏高一点、边缘有一定模糊容差。因为规划算法要的是“这里绝对不能走”如果才刚够到障碍物边缘就会导致规划路径贴得太极限很容易擦碰。不少项目里我们故意在 Stixel 的深度和宽度上各加一个安全缓冲量比如深度加 20 厘米、宽度左右各加 10 厘米这样规控跑起来会从容很多。6.3 个人最愿意复盘的收获先定义“好”再谈“优化”这是整个 Stixel 项目里让我收获最大的一点。项目刚开始团队对“好结果”的定义是模糊的。有人觉得 Stixel 数量少了就是好有人觉得障碍物都被框出来就是好。结果不同模块对参数的要求互相打架。后来我们把“好”拆成三个可量化的指标一是召回率场景里的真实障碍物有多少比例生成了 Stixel二是误检率没有障碍物的地方有多少假柱子三是最小障碍物高度检测成功率像路沿上 15 厘米高的障碍物也要能识别。先把指标定清楚再去调参整个项目的进展顺畅了很多因为每个人心里都有同一个标尺。如果你现在正准备在自己的项目里上 Stixel我的建议也是先花半小时把评价指标写下来再做任何参数调整。没有指标调参就像蒙着眼睛投篮全凭手感结果不可能稳定。Stixel World 不是花瓶算法它真正解决的是“在有限算力和不完美输入下如何快速得到可靠物理障碍物描述”这个工程上最实际的问题。这篇报告里写的原理、代码逻辑、调参经验和避坑心得都是我亲自跑过之后沉淀下来的希望对正在研究视觉感知的你有所帮助。如果后面你也在做类似的项目欢迎带着具体问题再来交流。