ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

灰度图到赛道掩码:二值化与断线补全全流程解析

灰度图到赛道掩码:二值化与断线补全全流程解析 在竞赛机器人或者入门级自动驾驶小车的感知栈里“从灰度图到可爬赛道”这六个字本质上是在回答一个问题怎么让机器人在一堆环境噪声中快速找到自己该走的那条路。上一篇文章我们聊完了相机选型和灰度图是怎么来的这篇直接进入正题把二值化和断线补全这两个关键环节掰开揉碎了讲清楚。尤其是断线补全很多人在二值化调参上花了大把时间却忽略了真正让赛道“连起来”的后处理步骤。这篇会把我的完整处理链路、参数选择逻辑和踩过的坑都放出来希望你们看完就能直接用在自己战队的开源视觉框架里。先说说这条链路的核心逻辑。很多人以为二值化就是把灰度图按一个阈值切成黑白两色实际上这只是一个开始。真正的难点在于你切出来的白色区域必须是一个连续、完整、可被上层逻辑直接用的赛道掩码——这就牵扯到噪声抑制、断口修复、误检剔除等一系列操作。所以我不打算只讲一个threshold函数而是把从灰度图到干净赛道掩码的完整流程拆成四步灰度预处理、二值化算法选型、形态学清理、断线补全。每一步都有它的“为什么”也有它的“别乱来”。1. 从原始图像到灰度图为什么这一步比你想的更关键1.1 灰度化不是“丢信息”而是把信息压到最有用的维度赛道识别这个场景核心任务其实是区分“赛道”和“非赛道”。如果赛道是白色地面是深色那亮度和对比度天然就是最好的分类特征。RGB三个通道在这个任务里其实是有冗余的——颜色信息本身并不比亮度信息更能区分赛道和地面反而会把计算量放大三倍。灰度图的意义就是把像素从三维的颜色空间投影到一维的亮度空间让后续所有阈值分割、边缘检测都建立在最简单也最稳定的特征上。我用的是一个标准加权公式Gray 0.299 * R 0.587 * G 0.114 * BOpenCV里的cvtColor(img, gray, COLOR_BGR2GRAY)用的就是这个系数组合。这个权重不是随便拍的它是根据人眼对不同颜色敏感度标定出来的绿色权重最高蓝色权重最低。对赛道场景来说白色赛道的RGB三个通道数值都比较高转换后灰度值自然高地形和背景的灰度值通常偏低这样赛道和背景在灰度直方图上就会形成比较明显的双峰。1.2 16位灰度图一个容易被忽略的坑热词里面出现了“16位灰度图”这个必须提一下。工业相机或者某些深度相机输出的原始数据可能是16位的也就是灰度值范围是0到65535而不是常见的0到255。如果你直接把16位图喂给大津法或者固定阈值那就坏了——阈值尺度完全对不上二值化结果几乎没法看。正确做法是先把16位数据归一化或截断到8位。注意这里有个细节简单的右移8位相当于除以256会让图像整体偏暗更好的做法是先看一下直方图的分布找到灰度值的主要分布区间做线性拉伸后再映射到0到255。比如# 假设 raw 是 16-bit numpy 数组 min_val, max_val raw.min(), raw.max() norm ((raw.astype(np.float32) - min_val) / (max_val - min_val) * 255) gray norm.astype(np.uint8)实测下来做不做这个直方图拉伸对后续二值化的稳定性影响非常大。如果你用的是普通的USB摄像头一般就是8位YUV或RGB不会遇到这个问题但如果是自己焊的板子接了MT9V034之类的传感器这个坑早晚会踩到。灰度化之后不要急着二值化。我会先看一眼灰度直方图确认赛道和地面是否形成了明显的峰。如果双峰不明显就需要考虑是不是曝光问题、反光问题或者赛道颜色本身和背景太接近。直方图是二值化能否成功的前置指标这一步观察能省掉后面大量瞎调参的时间。2. 二值化从连续灰度到清晰赛道掩码的核心算法选型2.1 固定阈值、自适应阈值、大津法怎么选二值化的本质是在灰度空间里找一个分界面把像素划成前景和背景两类。赛道识别的分界面就是赛道灰度值和背景灰度值之间的那道缝。固定阈值是最直接的办法比如灰度值大于200就算赛道。好处是简单粗暴、运行极快坏处是对光照极其敏感——同一块场地上午十点和下午三点的光照强度可能差好几倍你上午调好的阈值200下午赛道灰度值可能整体跌到180以下赛道直接变成一片黑色。自适应阈值adaptiveThreshold会给每个像素单独算一个局部阈值适合光照不均的图。但我个人在赛道场景里用得比较少因为赛道边缘附近容易产生大量伪前景且计算量明显大于全局阈值实时性有压力。大津法Otsu是我在这个项目里的首选。它的思路是遍历所有可能的灰度阈值找到一个值使得分割出的前景和背景两类之间的类间方差最大。直白地讲大津法会自动去“找那道最明显的缝”不需要你手动指定而且它对光照的整体偏移有一定鲁棒性——光照变化如果只是整体平移类间方差最大的那个阈值往往也会跟着平移。拿表格说事三个方案放在一起看更直观方案适用场景优点缺点固定阈值室内恒定光照、灯条检测实现简单、速度最快光照一变就废自适应阈值光照不均匀、变化剧烈的图局部适应能力强噪声多、计算慢大津法赛道识别、目标分割自动选阈值、抗整体亮度偏移双峰不明显时效果差赛道场景下我的默认配置是大津法。只有在比赛场地光照极其稳定、且场地背景非常干净的情况下我才会考虑用固定阈值来压榨那一点点性能。2.2 大津法的数学原理与OpenCV实现细节大津法底层逻辑可以这样理解设图像总像素数为N某一个阈值t把像素分成C1灰度≤t和C2灰度t两类C1的像素占比为w1灰度均值为μ1C2的占比为w2均值是μ2那么全局灰度均值μ w1μ1 w2μ2。类间方差定义为σ² w1 * (μ1 - μ)² w2 * (μ2 - μ)²大津法做的就是遍历所有可能的t找到让σ²最大的那个阈值。这个方法的直觉很漂亮如果两个类的均值差距大说明分割效果好类间方差就大如果分割位置选得差两个类的均值都会贴近全局均值方差就小。OpenCV里用起来极其简单thresh, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU)这里有个细节必须讲清楚thresh参数填0只是占位因为加了THRESH_OTSU标志位之后OpenCV会忽略你传入的阈值自动计算最优阈值并且通过thresh这个返回值把它返回来。所以你可以顺手打印这个值看看赛道在不同光照下自动选的阈值是多少——这个数据对后面调曝光和判断环境变化非常有用。另外要注意大津法有个前提假设图像的灰度直方图应该是“双峰”的——一个峰是背景一个峰是前景。如果现场出现大面积阴影或者强反光直方图可能变成多峰这时大津法选出的阈值可能落在错误位置。我的处理办法是在二值化前先做一个轻度的中值滤波或者高斯滤波把细颗粒噪声压掉让双峰更干净。2.3 二值化后的形态学清理开运算与闭运算的次序不能搞错二值化之后图像上通常有两类瑕疵一类是背景区域里的独立白色噪点比如地面的石子反射、草地里的亮斑另一类是赛道内部的黑色小洞比如轮胎印、阴影斑点。前者需要“开运算”去掉后者需要“闭运算”补上。开运算 先腐蚀后膨胀。腐蚀会把白色区域边缘向内收缩一圈所有小于核大小的独立白点直接消失然后再膨胀把原本的大块白色区域恢复原尺寸。效果就是“去掉小噪点不动大区域”。闭运算 先膨胀后腐蚀。膨胀会让白色区域向外扩张一圈把相邻的碎块黏连起来断开的小缝隙被填上然后再腐蚀把多余扩张的边缘收回来。效果就是“补上小洞连上小断裂”。关键点在于顺序先开运算后闭运算。如果先闭运算背景噪点会被膨胀放大再开运算虽然能去噪但可能会让赛道边缘变得更加不规则。我实测下来先开后闭的形态学处理流程在保持赛道边缘平滑度和清除噪声之间的平衡是最好的。核的大小也要控制。我的经验是3x3到5x5的矩形核在大多数场景够用。核太大赛道边缘会被过度圆滑宽度信息失真直接影响后面计算赛道中线和曲率。具体用多大要看你的图像分辨率——640x480的图像用3x3或5x5如果分辨率拉到1280x720可以适当换成5x5。OpenCV里对应的实现# 先开运算去噪 opened cv2.morphologyEx(binary, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8)) # 后闭运算补洞 closed cv2.morphologyEx(opened, cv2.MORPH_CLOSE, np.ones((5, 5), np.uint8))3. 断线补全让断开的主赛道恢复成完整可行驶区域3.1 断线是怎么产生的五种典型来源形态学处理能解决小洞和微裂缝但解决不了真正的“大段断线”。你要先理解断线从哪里来才谈得上怎么补。第一类是曝光过曝。白色赛道在强光直射下高光区域灰度值可能超过250甚至逼近255在这种区域里赛道和背景的对比反而消失了——因为背景也可能被照得很亮。大津法在这种情况下选出的阈值可能过高导致原本应该是白色的赛道区域被判定成黑色。第二类是赛道本身的磨损。白色赛道的边缘被轮胎磨得发灰灰度值下降和深色地面的差距缩小二值化时边缘部分可能漏判。第三类是远场透视。赛道在图像远处会非常窄可能只有三四个像素宽。这几个像素的灰度值一旦受噪声影响波动就会整段整段地消失因为一个像素的波动就足以让那一小截赛道低于阈值。第四类是逆向光照产生的阴影。赛道边缘被护栏或者场地里的设备挡住形成一道深色阴影带把原本连续的赛道切断了。这在大场地比赛中非常常见。第五类是阈值方向搞反。如果赛道是深蓝色或灰色地面是浅色那么赛道灰度值反而低于地面你需要用THRESH_BINARY_INV取反之后赛道才是白色。很多人第一次做深色赛道时忘了这一步结果二值图里赛道是黑的地面是白的自然处处断裂。# 深色赛道场景 thresh, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV cv2.THRESH_OTSU)3.2 先救急形态学闭运算的极限在哪里对于一到几个像素宽的断口、孔洞闭运算是最快的解决方案。举个例子赛道在远场边缘有一处三像素宽的黑洞用一个5x5的核做闭运算膨胀会把周围白色像素向外扩张三到四像素黑洞被填上随后腐蚀把扩出来的边界缩回原位原本的赛道轮廓基本不变。整个过程没有任何复杂的几何计算一次腐蚀一次膨胀就搞定了在嵌入式平台上只消耗几十微秒。但闭运算对“宽断口”无能为力。如果赛道中间断开了十个像素甚至更宽那膨胀之后断裂带可能还是填不上或者填上了但边缘变形太严重。这个时候就要用更聪明的办法了。还有一个隐患闭运算会把赛道和赛道旁边一块不该连的白色噪点连到一起形成“假赛道”。这种场景下我倾向于放弃大核闭运算改用下面讲的扫描线补全。3.3 基于扫描线的断线补全从掩码到赛道骨架的修复扫描线补全的核心思想是赛道在图像坐标系里通常是沿纵向延伸的车辆向前行驶赛道从近到远延伸。那么我们对二值图逐行扫描每一行上赛道区域的左右边界点就是有效数据。当某一行的赛道区域为空说明这里可能出现了断层这时候用相邻行的边界位置做线性插值就能补出这一行的赛道区间。这个方法比形态学精准得多因为你补的不是“像素块”而是“赛道在每一行的位置”。它对远场窄赛道断裂的效果非常好因为远场车道只有几像素宽只要上一行和下一行都检测到了赛道位置中间那一行的位置几乎一定能用插值算出来。我写了一个简单的Python实现思路def scanline_fill(binary): h, w binary.shape filled binary.copy() for y in range(h): # 找到当前行所有白色像素的 x 坐标 xs np.where(binary[y] 0)[0] if len(xs) 0: # 当前行没有赛道像素尝试从上下相邻行插值 ys_above max(0, y - 1) ys_below min(h - 1, y 1) xs_above np.where(binary[ys_above] 0)[0] xs_below np.where(binary[ys_below] 0)[0] if len(xs_above) 0 and len(xs_below) 0: # 用相邻行赛道中心点做线性插值 left_above, right_above xs_above[0], xs_above[-1] left_below, right_below xs_below[0], xs_below[-1] center_x int((left_above right_above left_below right_below) / 4) half_width max(2, int((right_above - left_above right_below - left_below) / 4)) filled[y, max(0, center_x - half_width):min(w, center_x half_width)] 255 return filled代码只考虑了上下各一行的插值实际操作中可以把搜索范围扩展到上下三到五行效果更好。要注意的是扫描线补全的前提是赛道在画面中大体呈纵向延伸。如果转弯太急赛道变成近乎横向逐行扫描就不适用了这时要换成按列扫描或者先做图像旋转。更稳妥的做法是先对整幅二值图做连通域分析只对属于主赛道连通域的断裂做插值。否则背景里一个独立的小白块附近出现断行会被错误地插值成一个假赛道。3.4 基于连通域与轮廓距离的断线桥接扫描线补全解决的是“逐行丢失”型断线但实际比赛中还有一种情况赛道被阴影带横向切断变成一个大的上段和大的下段两段都是完整的大连通域中间隔着一条十几像素宽的深渊。闭运算填不上扫描线插值也因为断口太宽而效果差。我的做法是用连通域分析找到赛道碎片然后做邻近桥接。流程分四步第一步connectedComponentsWithStats或者findContours提取所有白色连通域记录每个连通域的外接矩形、面积和中心点。第二步筛选出面积大于某个阈值的连通域作为有效赛道段。这个阈值要根据图像分辨率来定640x480下我一般取500像素以上小于这个面积的碎片大概率是噪点。第三步对筛选出的连通域两两计算距离。如果两个连通域在图像垂直方向上相邻、水平方向上有重叠区域、且它们之间的距离小于设定阈值就认定它们原本属于同一条赛道。第四步用一条宽度与赛道宽度相近的白色矩形带连接两个连通域。矩形带的位置可以简单取两个连通域中心连线的法向投影更精细的做法是用两个连通域的端点坐标做样条插值然后用cv2.line沿插值曲线画线。这个方法的核心逻辑是“距离与方向同时约束”只对满足“位置相邻、水平重叠、距离合理”的主赛道碎片做桥接。如果说闭运算是无论好坏一并连上那么轮廓桥接就是对“该不该连”做了一次判断误连的概率大幅下降。我记得有一次在赛道上试车有一段是被场地边的设备阴影直接切断的切断宽度大概有15像素。闭运算把周围所有白色噪点扩成了一团赛道边缘完全糊掉。换成轮廓桥接之后只用了一根辅助线就把上段和下段接上了二值图干净利落后续中线提取基本没延迟。4. 效果验证与实战排查不只是调参更是建验证体系4.1 用图像序列确认补全效果而不是单张图调试二值化和断线补全最容易犯的错是在单张图片上反复调参调到这张图完美了就觉得大功告成。但实际上赛道是运动的光照是连续变化的你需要的不是一个在单张图上完美表现的参数而是一组在连续图像序列上稳定工作的参数。我的做法是这样把比赛场地录一段两三分钟的视频包含直道、弯道、逆光、顺光、阴影等各种情况。然后在离线环境里批量跑这段视频逐帧输出二值化结果和断线补全结果最后用几个统计指标去衡量整套参数的表现。指标含义计算公式简述骨架完整度主赛道连通域在视频帧中存在的比例有效连通域帧数 / 总帧数有效面积占比主赛道掩码面积在画面中占比稳定性掩码面积方差中线跳变度相邻帧之间赛道中线的横向位移大小中线x坐标差分绝对值反色误检率二值化结果与人工标注不一致的比例离线标注后对比这些指标不需要做到100%但在竞速场景里中线跳变度如果频繁超过图像宽度的10%说明二值化参数在光照变化下不够稳转向控制会抖动得非常厉害。实际调试时我会把二值化后二值图和一个半透明的原图叠加起来显示一步到位看清哪些区域被识别成赛道了、哪些真正的赛道被丢了。如果发现远场总是断我会按3.2和3.3顺序检查闭运算核大小和扫描线搜索范围。4.2 六条高频问题与对应解法我把做这个项目以来踩过的坑和群里高频出现的求助问题整理成一张速查表基本上是排查时最常用的参考现象可能原因解决方案二值化后赛道区域大面积变黑阈值取反方向错误检查使用的是THRESH_BINARY还是THRESH_BINARY_INV远场赛道频繁断裂远场像素较少、噪声影响大加大闭运算核或开启扫描线补全近场二值图边缘抖动相机自动曝光和自动增益固定曝光时间关闭自动增益赛道边缘出现大量毛刺原图噪声大二值化前加中值滤波或高斯滤波形态学处理后赛道变宽失真核太大改用3x3核或改为轮廓桥接补全后出现明显横杠扫描线插值未约束连通域方向增加方向约束只对纵向相邻的赛道段补全第二条值得多说一句。远场断裂很多时候不是二值化阈值的问题而是物理分辨率的问题。赛道在远场只有三四个像素宽像素值在白色和灰色之间波动。处理这类断线我个人的经验是做两层保险先用一个较大的核比如5x5的椭圆核做闭运算把远场断裂粘合再用扫描线补全兜底。两层组合拳下来远场的赛道完整性会明显提升。4.3 从二值图到“可爬赛道”补全后还要做什么标题里写的“从灰度图到可爬赛道”其实还有一个隐含问题断线补全之后这些像素怎么变成上层导航要用的赛道模型这是我自己在战队里负责视觉时输出给决策层的信息结构也在开源框架里做成了统一接口。二值化补全的输出我习惯称为赛道掩码mask。决策层真正要用的通常是三类信息第一类是赛道中线。我在图像上对掩码做逐行扫描取每行白色区域的中心点连接成一条折线。近场的中线直接用来计算车辆相对赛道的横向偏差和航向角这是PID控制的主输入。第二类是赛道边界。把每行的左右端点提取出来分别拟合成两道边界线。边界线用于防止车辆跑出赛道尤其是在弯道处边界的位置比中线更能反映赛道走向。第三类是赛道曲率。在EKF或者纯几何方式下对中线的点集做二次曲线拟合解出二次项系数即可估算前方赛道曲率用于提前降速或调整转向增益。在视觉SLAM这个更大的维度上二值化与断线补全算是最轻量的“语义分割”方案——它不做特征点匹配、不建稠密地图但输出的赛道掩码天然就是可行驶语义区域。在很多算力受限的嵌入式平台上这条路仍然是最可靠、最快速的方案。2025年不少嵌入式开源项目开始把深度学习分割模型往MCU级别硬件上推但推理帧率和功耗仍然是个硬约束。所以这套传统视觉方案在近期内依然有价值它不需要GPU、不需要NPU一块几百块钱的开发板就能跑得很稳。5. 写在最后这套视觉链路真正教会我的东西说实话二值化和断线补全看起来都是入门级的算法但真正把它们调到能稳定跟着赛道跑完一圈要比想象中曲折得多。我在这个项目里最深的体会是算法本身只是骨架真正让视觉链路稳定的是对图像数据的理解——你知道灰度直方图该长什么样知道大津法选出的阈值在什么情况下会失灵知道断线该用形态学还是扫描线去补这些判断力才是实打实的积累。如果各位在自己的开源项目里复刻这套链路我建议调试顺序这样走先录一段真实赛道视频离线把灰度图和直方图看熟然后跑大津法把每帧自动算出来的阈值打出来看看波动有多大再上形态学从3x3核开始一次只调一个参数最后上断线补全优先用闭运算不够再加扫描线。每一步都在图像序列上验证不要对着一张图死磕。还有一个小技巧在比赛前一天的场地适应期一定要多留时间做光照测试。上午、中午、下午各录一段视频回放的时候你就会发现大津法的阈值波动其实是可以预测的——它通常和太阳高度角变化呈某种关系。掌握这个规律之后你甚至可以提前给阈值加一个时间相关的补偿系数这算是个比较进阶的玩法了但效果真的不错。这条视觉路线接下来我打算整理下赛道边缘拟合和中线提取的部分那个部分涉及到的数学细节更多也更接近最终控制效果。希望这套开源方案能帮到正在做同类项目的队伍少走点弯路。
返回列表