首页 / 资讯中心 / 文章详情

Stixel World 原理与实战:从像素柱到实时障碍物检测

Stixel World 原理与实战:从像素柱到实时障碍物检测 ★ FEATURED ARTICLE
1. 从像素到“柱子”Stixel World 到底在解决什么问题第一次接触 Stixel World 这个概念是在做一个低成本障碍物检测方案的时候。当时手头只有一个单目摄像头和一个算力相当有限的嵌入式板子激光雷达太贵双目立体匹配跑起来帧率又惨不忍睹。翻了不少资料之后Stixel 这个词反复出现它的全称是Stixel Stick Pixel直译过来就是“像素柱”。这个名字其实已经把它的核心思想说透了它不再把图像当成一个个孤立的像素去处理而是把地面之上、具有一定高度和宽度的竖直条带当作一个整体单元来描述。你可以这样理解普通的目标检测是画一个框把车、人、路障圈出来语义分割是给每个像素打标签而 Stixel World 走的是第三条路——它把整个场景切成一排排竖直的“柱子”每根柱子告诉你“这里从地面往上到某个高度是障碍物再往上就是天空或者背景”。这种表示方式的好处非常直接它天然区分了地面ground和障碍物obstacle而且每根柱子只需要记录底端位置、顶端高度和所属类别数据量极小非常适合实时系统。我之所以愿意花时间深挖它是因为它解决了一个很实际的痛点在算力受限的平台上你既想要稠密的障碍物信息又想要足够快的推理速度传统的稠密立体匹配和深度学习方法往往两头不讨好。Stixel World 用“列式建模 动态规划”的组合把问题降维成了一个一维的最优化问题速度能跑到几十甚至上百帧同时还能给出障碍物的高度和距离估计。它适合谁适合做 ADAS 前向碰撞预警、低速自动紧急制动、机器人避障、以及任何需要在嵌入式设备上跑实时障碍物检测的开发者。不过要提醒一句Stixel World 不是“拿来即用”的现成模型它更像是一套建模框架里面有很多可以替换的模块比如视差计算、地面估计、代价函数设计、动态规划求解等。不同论文和工程实现之间的差异可能非常大。下面我就按照自己实际复现和调优的顺序把整套原理和落地细节拆开讲清楚。2. 核心原理拆解为什么是“柱子”而不是“框”2.1 从视差图到 U-V 视差空间的基本盘Stixel World 的输入通常是一对校正后的立体图像输出是若干根 Stixel。整个流程的第一步是计算稠密视差图disparity map。视差和深度的关系是 Z f·B / d其中 f 是焦距像素单位B 是基线长度d 是视差。这个公式很基础但它是后面所有几何推理的根基必须记牢。拿到视差图之后Stixel 方法并不会直接在原图上操作而是把它投影到U-V 视差空间里。这里 U 是图像列坐标V 是视差值每个点 (u, v) 的强度表示“第 u 列中视差为 v 的像素数量”。这个变换的妙处在于地面在 U-V 空间里会呈现出一条明显的斜线因为地面点的视差随行坐标图像纵坐标线性变化而竖直的障碍物则会形成一条竖直的“柱状”分布。我第一次看到这个变换的时候就觉得这不就是把三维场景的几何结构用一种更利于分割的方式重新排列了嘛。提示U-V 视差空间对立体校正的质量非常敏感。如果极线校正没做好地面线会变粗、变弯后续的地面估计和 Stixel 分割都会跟着崩。所以前期标定和校正这一步千万别偷懒。2.2 地面估计一切从“找到地面”开始Stixel World 有一个非常强的先验假设场景中大部分区域是地面障碍物是立在地面上的。所以整个算法的第一步是估计地面。常见做法是在 U-V 视差空间里用 RANSAC 或者霍夫变换拟合一条直线这条线就代表地面视差随列坐标的变化关系。为什么用直线拟合因为在校正后的图像里平坦地面上的点满足视差 d 与图像行坐标 r 近似线性关系在忽略镜头畸变和俯仰角变化的前提下。拟合出这条线之后对于任意一列 u我们都能知道“地面在该列的视差应该是多少”从而把地面和地面以上的区域分开。这里有个实操细节地面拟合通常不是对整幅图做的而是先划分成若干竖直条带比如每 20 像素一列在每个条带内单独拟合然后再做平滑。这样做是为了适应地面起伏和车辆俯仰变化。我在实际测试中发现如果直接用全局直线拟合遇到坡道或者减速带时误检率会明显上升。2.3 动态规划把二维分割变成一维最优化地面估计完之后每一列 u 上就只剩下“地面以上”的那部分视差数据。接下来的任务是在这一列里找到若干个障碍物段每个段有底端和顶端。Stixel 的做法是把这一列的视差按行排列然后定义一个代价函数用**动态规划Dynamic Programming**去求解最优的分割点。代价函数通常由三部分组成数据项该段内视差是否一致、是否符合障碍物模型、平滑项相邻段之间的高度变化是否平滑、以及先验项障碍物高度不能太离谱。动态规划的状态是“当前行作为某个障碍物段的顶端或底端”转移就是从前一个分割点跳到当前分割点。因为每一列是独立处理的所以整个过程可以高度并行化。我个人的理解是这一步本质上是在做一维的、带约束的变点检测。相比在二维图像上做分割一维动态规划的计算量小得多而且能保证全局最优在给定代价函数下。这也是 Stixel 能跑得这么快的关键原因。2.4 Stixel 的表示与输出含义一根 Stixel 通常包含以下信息所属列 u、底端行坐标对应地面接触点、顶端行坐标对应障碍物顶部、以及一个类别标签比如障碍物、地面、天空、未知。有些实现还会附带视差均值和置信度。从这些信息可以反推出障碍物的物理属性底端行坐标结合地面模型可以算出障碍物与相机的距离顶端和底端之差结合相机内参可以估算障碍物的实际高度。比如一个行人在图像里占 80 个像素高在已知焦距和距离的情况下就能反推出他大概 1.7 米高。这个能力在只做“有没有障碍物”判断之外还能支持“障碍物是否可以通过”这类决策。3. 实操落地从零搭建一套可跑的 Stixel 流程3.1 环境与数据准备我建议的起步配置是这样的一台带 CUDA 的机器哪怕是很老的显卡也行OpenCV 4.xPython 3.8 以上外加一个立体数据集。如果手头没有真实双目相机可以先用公开的立体数据集做验证比如 KITTI 的立体子集。数据准备阶段最关键的是校正你需要左右相机的内参、外参和畸变系数然后用cv2.stereoRectify和cv2.initUndistortRectifyMap生成校正映射表。import cv2 import numpy as np # 假设已经读入了 K, D, R, T 等标定参数 R1, R2, P1, P2, Q, _, _ cv2.stereoRectify( K1, D1, K2, D2, image_size, R, T, alpha0) map1x, map1y cv2.initUndistortRectifyMap( K1, D1, R1, P1, image_size, cv2.CV_32FC1) map2x, map2y cv2.initUndistortRectifyMap( K2, D2, R2, P2, image_size, cv2.CV_32FC1) left_rect cv2.remap(left_img, map1x, map1y, cv2.INTER_LINEAR) right_rect cv2.remap(right_img, map2x, map2y, cv2.INTER_LINEAR)校正完之后我习惯先目视检查一下极线对齐情况在左图某一行画一条水平线右图对应行上的同名点应该在同一条水平线上。如果偏差超过一两个像素后面视差计算就会引入系统性误差。3.2 视差计算与置信度过滤视差计算可以用 SGBM半全局块匹配OpenCV 里直接有cv2.StereoSGBM_create。参数方面minDisparity一般设 0numDisparities要是 16 的倍数blockSize取 5 到 11 之间。我实测下来blockSize5在纹理丰富场景下细节更好但噪声也更多blockSize7相对稳一些。stereo cv2.StereoSGBM_create( minDisparity0, numDisparities128, blockSize7, P18 * 3 * 7 ** 2, P232 * 3 * 7 ** 2, disp12MaxDiff1, uniquenessRatio10, speckleWindowSize100, speckleRange2, modecv2.STEREO_SGBM_MODE_SGBM_3WAY ) disparity stereo.compute(left_rect, right_rect).astype(np.float32) / 16.0算完视差后一定要做左右一致性检查把遮挡区域和误匹配点剔掉。这一步对 Stixel 的地面估计影响极大因为地面拟合对离群点很敏感。我一般会把一致性检查不通过的点视差设为无效值后续在 U-V 空间里只统计有效点。3.3 U-V 视差空间构建与地面线拟合构建 U-V 空间其实就是做一个直方图统计。对每一列 u遍历所有行把有效视差累加到对应的 bin 里。bin 的宽度可以取 1 个视差单位也可以取 0.5 做插值。def build_uv_disparity(disparity, max_disp128): h, w disparity.shape uv np.zeros((max_disp, w), dtypenp.float32) for u in range(w): col disparity[:, u] valid col[col 0] for d in valid: bin_idx int(round(d)) if 0 bin_idx max_disp: uv[bin_idx, u] 1 return uv地面线拟合我推荐用 RANSAC 拟合直线因为地面点在 U-V 空间里占多数RANSAC 能很好地排除障碍物点的干扰。拟合出来的直线 d a·u b 就是地面模型。之后对每一列 u地面视差就是 a·u b地面以上的行对应的视差如果大于这个值就可能是障碍物。3.4 逐列动态规划求解 Stixel这是整个流程里最核心也最容易出错的一步。我先把每一列的有效视差按行排列然后定义代价。数据项我通常用视差与段内均值的绝对差之和平滑项用相邻段高度差的平方先验项则限制障碍物高度在一个合理范围内比如 0.3 米到 3 米。动态规划的状态设计有很多种我用的是比较直观的一种dp[i]表示“从地面到第 i 行为止的最优分割代价”转移时枚举上一个分割点 j计算 j 到 i 这一段作为障碍物的代价。最终回溯得到所有分割点就得到了这一列的 Stixel 序列。def solve_stixel_column(disp_col, ground_disp, max_height200): # disp_col: 该列从地面往上每一行的视差值 # ground_disp: 该列地面视差 n len(disp_col) dp np.full(n 1, np.inf) dp[0] 0 parent np.zeros(n 1, dtypeint) for i in range(1, n 1): for j in range(i): seg disp_col[j:i] if len(seg) 0: continue data_cost np.sum(np.abs(seg - np.median(seg))) smooth_cost 0.1 * (i - j) cost dp[j] data_cost smooth_cost if cost dp[i]: dp[i] cost parent[i] j # 回溯 segments [] cur n while cur 0: prev parent[cur] segments.append((prev, cur)) cur prev return segments[::-1]这段代码是简化版实际工程里还要加入地面约束、最小段长度限制、以及类别判定逻辑。但核心思想就是这样把一列里的分割问题变成一个序列决策问题用 DP 求全局最优。3.5 后处理与可视化DP 出来的原始 Stixel 往往会有一些抖动和碎片需要做后处理。我常用的手段有三种一是时间滤波用卡尔曼滤波或者简单的滑动平均平滑每根 Stixel 的高度二是空间滤波把相邻列高度差异过大的 Stixel 合并或剔除三是最小长度约束太短的段直接归为噪声。可视化的时候我习惯把 Stixel 画成一根根竖直的彩色条颜色表示距离近红远蓝高度表示障碍物高度。这样一眼就能看出前方有没有障碍物、大概多远、多高。对于调试来说把 U-V 空间、地面线和最终 Stixel 叠在一起看能快速定位问题出在哪一步。4. 常见问题与排查技巧实录4.1 地面估计跑偏了怎么办这是最常见的问题表现就是地面线明显偏离真实的 U-V 地面分布导致大量地面点被误判为障碍物或者障碍物被误判为地面。排查顺序我一般是这样的先看立体校正是否准确再看视差图在平坦地面区域是否平滑最后检查 RANSAC 的迭代次数和阈值是否合理。如果地面本身有起伏全局直线拟合肯定不够用。这时候可以把图像按列分成若干块每块单独拟合再做中值滤波平滑。我在一个有减速带的地下车库场景里测试过分块拟合比全局拟合的误检率低了差不多一半。4.2 动态规划结果碎片化严重DP 出来的 Stixel 如果碎成很多小段通常是因为代价函数里的平滑项权重太低或者数据项对噪声太敏感。我的经验是把平滑项权重适当调高同时在数据项里用中值而不是均值这样对离群点更鲁棒。另外可以在 DP 之前先对视差列做一次中值滤波把孤立的噪声点去掉。还有一个容易被忽略的点最小段长度。如果允许 DP 把每一行都切成一段那结果必然碎片化。我一般会强制每段至少 5 到 10 行具体数值根据图像分辨率和障碍物尺度来定。4.3 远处障碍物检测不到Stixel 对远处小目标的检测能力受限于视差精度。视差误差和距离的关系是 ΔZ Z²·Δd / (f·B)距离越远同样的视差误差导致的深度误差越大。所以远处障碍物要么视差本身就很小要么被噪声淹没。改善方法有几个一是提高立体匹配的亚像素精度比如用 Census 变换加二次插值二是对远处区域单独调参比如减小 blockSize、放宽唯一性约束三是引入时间累积用多帧信息增强远处目标的信噪比。我在实际项目里用的是第三种配合简单的帧间对齐远处检测率能提升不少。4.4 常见问题速查表问题现象可能原因排查方向解决建议地面线明显偏移校正不准 / 视差噪声大检查极线对齐、视差图质量重新标定增加一致性检查Stixel 碎片化平滑项权重低 / 噪声多查看 DP 代价曲线提高平滑权重预滤波远处漏检视差精度不足分析远处视差分布亚像素优化时间累积障碍物高度跳变地面模型不稳检查地面拟合残差分块拟合时间平滑帧率过低DP 复杂度过高统计每列 DP 耗时限制段数降采样4.5 几个我踩过的坑第一个坑是过度依赖默认参数。SGBM 的默认参数在 KITTI 上表现还行但换到自己的相机和场景后numDisparities和uniquenessRatio几乎一定要重新调。我建议先用一小段视频做参数扫描把视差图质量调到肉眼看着舒服为止。第二个坑是忽略时间一致性。单帧 Stixel 看起来没问题但连续播放时高度和距离会跳。后来我加了一个简单的卡尔曼滤波对每根 Stixel 的高度和距离做预测更新画面立刻就稳了。第三个坑是把 Stixel 当成万能检测器。它擅长的是“地面上的竖直障碍物”对于悬空物体、地面上的平面物体比如躺着的箱子、以及透明物体效果都不好。所以如果你的场景里有这类目标得额外加别的检测手段。5. 性能优化与工程化建议5.1 计算瓶颈在哪里Stixel 流程里最耗时的通常是三块稠密立体匹配、U-V 空间构建、逐列动态规划。立体匹配可以用 GPU 加速OpenCV 的 CUDA 版本或者自己写 kernel 都行。U-V 空间构建本质上是直方图统计可以用并行归约加速。DP 部分因为每列独立天然适合多线程我一般用 OpenMP 或者线程池把列分块并行。如果实在跑不动可以考虑降采样把图像缩小一半再做 Stixel速度能提升三到四倍代价是远处小目标更难检测。这个取舍要根据具体应用来定。5.2 参数调优的优先级我的经验是参数调优要按影响程度排序立体校正 视差参数 地面拟合阈值 DP 代价权重 后处理参数。前面几步没调好后面怎么调都是白搭。很多人一上来就调 DP 权重结果地面线都是歪的调半天也没用。5.3 与其他模块的配合Stixel 的输出很适合作为下游模块的输入。比如做前向碰撞预警可以直接用 Stixel 的距离和高度判断是否有碰撞风险做可通行区域分析可以把地面 Stixel 标记为可通行障碍物 Stixel 标记为不可通行。它输出的数据量小传给下游的延迟也低。我在一个低速机器人项目里把 Stixel 和超声波传感器做了融合Stixel 负责中远距离的障碍物定位超声波负责近距离的补盲。两者互补效果比单用任何一个都好。6. 一些个人体会与后续可扩展方向Stixel World 这套东西我越用越觉得它的价值不在于某个具体算法而在于它提供了一种**“列式建模”的思维方式**。把二维问题降维到一维用动态规划求全局最优这个思路可以迁移到很多其他问题上比如车道线检测、可通行区域分割、甚至某些工业检测场景。后续如果想继续深挖我觉得有几个方向值得尝试。一是把深度学习的视差估计替换掉传统的 SGBM现在很多轻量级立体匹配网络在精度和速度上都很有竞争力二是把 DP 的代价函数用学习的方式训练出来而不是手工设计三是把 Stixel 扩展到多相机或者鱼眼相机覆盖更大的视野范围。最后分享一个小技巧调试 Stixel 的时候一定要把中间结果都可视化出来尤其是 U-V 空间和地面线。很多时候你以为是 DP 的问题其实根源在地面拟合你以为是地面拟合的问题其实根源在立体校正。把每一步都摊开看排查效率会高很多。这套流程我前后调了大概两个月踩过的坑基本都写在上面了希望能帮你少走点弯路。
阅读完成 · 觉得有帮助?
咨询建站