1. 从逐行扫线到八邻域:一次“看得见路,却认不出路”的困境突围
做智能车摄像头组的同学,十有八九都经历过这种崩溃瞬间:车子在普通弯道上跑得顺顺当当,一到十字或者环岛,图像处理出的赛道边界就像喝醉了酒一样,左边跳右边,明明前方是一大片开阔的铺装路面,程序却告诉你“无边界可循”。车子当场“原地思考人生”,然后在赛道上画出一个优美的弧线冲进草地。
我在21届、22届连续带了两届智能车竞赛的队伍,摄像头组从TC264换到TC377,传感器从总钻风换成蓝宙,图像分辨率从188×120调到188×80再调到160×120,折腾了一大圈,最终发现真正把车从“能跑”变成“跑得稳”的转折点,就是换掉了早期的逐行扫线,全面转向八邻域图像算法。
这篇文章不聊太多高深理论,也不摆数学公式吓人,就从一个实际调车人的角度,讲讲八邻域算法在智能车摄像头循迹里到底怎么落地、怎么写代码、怎么解决十字环岛这些老顽固问题。如果你正在备战全国大学生智能车竞赛,尤其是摄像头组,这篇内容应该能帮你省下不少调参踩坑的时间。
先搞清楚一件事:八邻域不是某一个官方指定的标准算法,而是图像处理里“边缘跟踪”这一类方法的统称。它的核心思路是,从图像中某个已知的边界点出发,按照特定的邻域搜索顺序,沿着边界一点一点“摸”下去,最终得到一条完整的边界线。这个思路放到智能车赛道上,就是完美的边界追踪方案。
传统的逐行扫线本质上是把图像当成一个二维数组,一行一行从左往右扫描,找到每一行的黑白色跳变点,把这些跳变点连起来当赛道边界。这种方法在光线均匀、赛道干净的条件下确实简单有效,代码量小、跑得快,很多入门教程都这么教。但它的致命弱点是:每一行都是独立工作的,行与行之间没有任何连续性约束,一旦某一行因为反光、阴影、车体倾斜出现误判,这一行的边界点就会突然飞出去,导致整体边界毛刺严重。更麻烦的是,遇到十字路口这种大面积连通区域时,逐行扫描往往会把对面那条赛道也扫进来,边界直接“串门”。
八邻域算法呢,它不追求每一行独立作战,而是从前一行的边界点出发,只在当前位置的八个相邻像素里找下一个边界点。这相当于给边界追踪加了一个“惯性约束”:我允许你有点小波动,但你不会突然从图像左边蹦到右边。就这一个特性,让它在赛道连续性和抗干扰方面,比逐行扫线高出一个量级。
2. 八邻域到底是什么:从像素的“朋友圈”说起
2.1 八邻域与四邻域的本质区别
在说算法之前,先建立像素之间的“社交关系”。在一张二值化之后的赛道图像里,每个像素只有两种状态:黑(赛道)或白(背景)。对于图像中任意一个像素点,它周围有8个相邻像素,这8个像素就是它的“八邻域”。如果只算上下左右4个,那就是“四邻域”。
四邻域和八邻域看起来只是差了4个对角方向的像素,但在边界追踪场景里差别巨大。四邻域追踪出来的边界是“像素级锯齿形”的,走一步转一个90度,路径不光滑;用八邻域追踪,可以对角走,追踪路径更顺滑地贴着边界走,表现的连续性更好。举个例子,如果边界是45度斜线,四邻域只能走“下下右右”这种阶梯路径,而八邻域可以一路斜着下去,追踪效率高得多。
八邻域另一个重要的隐性优势是鲁棒性。对于一个给定边界点,四邻域最多只有3个可候选的“下一个点”,八邻域则有7个。这意味着即便当前点的某个方向的邻域因为噪点被污染了,你仍然有很大概率在另一个方向找到真正的边界延续,不容易断线。
2.2 八邻域边界追踪的基本走法
八邻域边界追踪的经典实现思路是:从已知边界点出发,按照固定的方向顺序依次检查8个邻居,找到第一个符合边界条件的像素,把它标记为新的边界点,然后从新点继续重复这个过程,直到到达图像边界或者追踪到足够多的点。
这里有一个决定成败的细节:搜索方向顺序的选择。如果每次都固定从正上方开始顺时针搜索,边界可能会在某些局部形状上“绕远路”或者陷入死循环。更稳的做法是保存“上一个边界点相对于当前点”的方向,然后从这个方向的垂直方向开始按顺序搜索。
这个技巧有个专业术语叫“跟踪方向记忆”,它背后的逻辑很直白:既然你已经从左下方走到了当前点,那么下一个边界点大概率还在左下方或附近,而不是突然出现在右上角。从你来的方向的左右两侧开始搜,搜索命中率高,效率也快;从正上方开始固定顺序搜索,边界一旦出现小波动,就可能搜错对象。
还有一个硬性要求:图像一定要做好二值化。八邻域搜索每次只判断邻居是“黑”还是“白”,如果原始图像没做二值化而是直接用灰度值比较,阈值选不好,整个追踪就会变成在灰蒙蒙的噪声里猜谜。实际竞赛中用的摄像头,输出原始灰度图像后,第一步必然是做大津法或者自适应阈值二值化,把赛道和背景彻底拆开。
2.3 八邻域和逐行扫线的性能对比
用一组直观的数据来说明两者差异。假设图像分辨率为188×120,逐行扫线的计算量是每一行都从左扫到右,最坏情况下要做188×120约22560次像素判断,并且还要处理每一行的极值点搜索。八邻域只看边界附近,假设赛道边界大约每行1到2个候选点,追踪完全部边界大约只需要200到400次邻域检查,计算量小一个数量级。
| 对比项 | 逐行扫线 | 八邻域追踪 |
|---|---|---|
| 计算量 | 全图扫描,量级大 | 仅边界邻域搜索,量级小 |
| 行间连续性 | 无约束,易飞线 | 方向记忆约束,连续性好 |
| 十字路口处理 | 容易串线 | 可设置切断条件,可区分 |
| 实现复杂度 | 简单 | 中等 |
| 抗噪能力 | 差,单点误判直接飞 | 好,局部污染不易断 |
| 环岛处理 | 边界容易粘连 | 方向变化可识别特征 |
对于21届、22届的智能车竞赛规则,赛道元素越来越多,逐行扫线的复用性和可靠性渐渐跟不上了。八邻域算是一套更通用、更接近“人眼跟踪赛道”逻辑的方案,只要把细节调顺了,十字、环岛、坡道都能在这套框架下统一解决。
3. 从“能跑直线”到“认得出十字”:八邻域的赛道边界追踪实践
3.1 总体思路:不再逐行扫,而是逐点“摸”
我在实际代码里,边界追踪的核心流程只有四步:
- 在图像底部(离车最近的一行)找到左右两个边界起始点。
- 分别从两个起始点出发,向左上和右上方向追踪边界,得到连续的左右边界点数组。
- 根据左右边界的连续性和方向变化,识别出十字、环岛、坡道等特殊元素。
- 如果追踪过程中遇到丢线(找不到下一个点),记录丢线位置,用上一次的有效边界做补线处理。
这个流程的好处在于,有了“边界是一条连续曲线”这个概念之后,所有后续的逻辑都变得非常直观。比如计算赛道中线,不需要像逐行扫线那样每一行把左右边界点相加除以2那么机械,可以直接从边界点数组里按行号查表;计算偏差和曲率时,也只需要取边界的几个特征点做差分即可。
3.2 八邻域搜索方向的具体实现
图像坐标我统一用直角坐标系来描述:原点在图像左上角,x轴向右,y轴向下。8个邻居的方向定义如下:
| 方向编号 | dx | dy | 说明 |
|---|---|---|---|
| 0 | 0 | -1 | 上 |
| 1 | 1 | -1 | 右上 |
| 2 | 1 | 0 | 右 |
| 3 | 1 | 1 | 右下 |
| 4 | 0 | 1 | 下 |
| 5 | -1 | 1 | 左下 |
| 6 | -1 | 0 | 左 |
| 7 | -1 | -1 | 左上 |
追踪左边界的时候,车的中线在边界右侧,我们希望在图像上沿着边界“向上走”,同时保持边界点在搜索点的“右侧”;反过来,追踪右边界时,希望边界点保持在搜索点的“左侧”。
方向记忆的思路是:维护一个变量last_dir,记录“上一次搜索到边界点的方向”。下一次搜索时,从last_dir对应的那个邻居开始,加上一个固定的偏移量,按顺时针或者逆时针把8个方向都查一遍。对左边界追踪,我测试下来的最优搜索顺序是逆时针,从“来向的下一格”开始;对右边界追踪则反过来,顺时针。
这个搜索起始方向的选择,直接决定了算法对曲率的适应能力。如果从错误的方向开始搜索,在急弯处可能要找完大半个邻域才能找到下一个边界点,速度变慢不说,还容易在边界点密集的区域选错邻居。实际调试时我对比过从“自身方向开始搜索”和从“来向偏移2格开始搜索”两种方式,后者在S弯和连续发卡弯的追踪稳定性明显更强。
3.3 左边界和右边界的对称与镜像
八邻域边界追踪有个天然优势:左边界和右边界只要搜索方向做镜像对称处理即可,逻辑完全复用。我实际代码里只写了一个追踪函数tracedge(left_start_x, left_start_y, is_left_boundary),is_left_boundary为真时从左边界起始点向左上搜索,为假时从右边界起始点向右上搜索。
这里有个非常容易踩的坑:图像坐标系原点在左上角,y轴向下。这意味着所谓的“向上追踪”实际上是y值递减的操作。很多初学同学在写八邻域时容易搞反方向,把边界一路追到图像底部去,然后发现边界数组乱成一团。记住一件事:车在图像底部,赛道延伸方向是图像顶部,所以边界的终点是y=0附近,起点是y=height-1附近,方向始终是y递减。
3.4 起始点怎么选:固定寻边与自适应寻边
八邻域追踪需要一个起始点,这是整个算法成败的关键之一。我在项目里用的是固定寻边+动态校准的组合方案:在图像最底部的几行内,分别从图像左边缘向右扫描、从右边缘向左扫描,找到第一次出现黑白跳变的位置作为左右边界的起始点。
但有一个细节要注意,固定从最底行扫描,如果车在入弯时车身倾斜,最底行可能已经压到了赛道边沿外,导致寻边失败或者找到错误边界。我处理的办法是:不只在最后一行找,而是在图像底部5行范围内寻找“最佳起始点”,判断标准是这几行内边界点x坐标的方差最小。如果最后一行找不到有效边界,就往上多找几行,直到找到连续稳定的边界起始段。
这种“多行候选+方差最小”的寻边策略,比单行固定寻边稳健很多。实际比赛中车子在高速过弯时会有一点侧倾,摄像头视野中的赛道会跟着平移,单行寻边很容易失效,多行候选有效解决了这个问题。
3.5 核心代码:一个可直接移植的八邻域边界追踪函数
下面是我在工程中实际用过的边界追踪核心代码,语言用的是C,开发环境是逐飞库的TC264/TC377平台,图像数据来自总钻风摄像头输出灰度图做二值化之后的二值图数组。这个函数的核心逻辑不绑定具体硬件,移植到其他平台只需要改数组大小和图像访问方式。
#define IMG_W 188 #define IMG_H 120 // 8个方向的偏移量 const int dx8[8] = {0, 1, 1, 1, 0, -1, -1, -1}; const int dy8[8] = {-1, -1, 0, 1, 1, 1, 0, -1}; // 左边界搜索方向(逆时针),右边界搜索方向(顺时针) const int search_order_left[8] = {2, 1, 0, 7, 6, 5, 4, 3}; const int search_order_right[8] = {6, 7, 0, 1, 2, 3, 4, 5}; // 二值图像数组,black为赛道,white为背景 // 返回追踪到的边界点个数,points数组存放边界点坐标 int trace_edge(unsigned char binary_img[IMG_H][IMG_W], int start_x, int start_y, int is_left, int points_x[], int points_y[], int max_points) { int cur_x = start_x; int cur_y = start_y; int dir = is_left ? 6 : 2; // 初始搜索方向,左边界从左上开始,右边界从右上开始 int count = 0; points_x[count] = cur_x; points_y[count] = cur_y; count++; while (count < max_points) { // 搜索方向序列:从记忆方向的偏移位置开始 int found = 0; for (int i = 0; i < 8; i++) { // 计算实际搜索方向 int offset = is_left ? search_order_left[i] : search_order_right[i]; int search_dir = (dir + offset) % 8; int nx = cur_x + dx8[search_dir]; int ny = cur_y + dy8[search_dir]; // 检查边界范围 if (nx < 0 || nx >= IMG_W || ny < 0 || ny >= IMG_H) continue; // 找到新边界点:必须是黑点(赛道) if (binary_img[ny][nx] == 0) { cur_x = nx; cur_y = ny; dir = search_dir; found = 1; break; } } if (!found) { // 8个方向都没找到,说明边界断线 break; } points_x[count] = cur_x; points_y[count] = cur_y; count++; // 如果到了图像顶部,终止追踪 if (cur_y <= 1 || cur_y >= IMG_H - 2) break; } return count; }这段代码有几个值得细说的设计点。第一,搜索顺序数组search_order_left和search_order_right并不是简单的0到7顺序,而是经过了实际赛道测试后调整过的,优先搜索与当前行进方向夹角较小的邻居,这样在弯道追踪时更顺。第二,视角方向记忆的dir变量每次找到新点都会更新,相当于记录“我正朝哪个方向走”,这样在下一轮搜索时就有了先验。第三,追踪到图像顶部前一行就主动停下,避免数组越界也避免追到视野外的无效区域。
实测效果:在188×120分辨率的二值图上,追踪整条边界(大约100到110个点)只需要0.2到0.4毫秒,比逐行扫线快很多,余下的算力可以交给元素识别和PID控制。
3.6 为什么从底往上追而不是从上往下追
一个容易忽略但非常关键的设计细节:追踪方向是从图像底部向顶部追,也就是从离车近的地方向远处追。为什么不是反过来?
有两个原因。第一,离车近的图像底部区域,赛道宽度最大、透视畸变最小、二值化最稳定,起始点最容易找对。从近处往远处追,起始点可靠,后续追踪的误差累积也小。反过来从远处往近处追,远处赛道投影到图像上可能只有几个像素宽,很容易把噪点当边界,起始点一旦错了后面越追越乱。第二,智能车的控制决策主要依赖近处赛道信息(底盘PID用近处偏差,转向控制也主要看近处),远处信息更多是预判用途。先追近处,即使远处追踪失败丢了线,控制逻辑依然有近处数据可用。
4. 十字、环岛、坡道:特殊元素识别与八邻域的配合
4.1 十字路口:八邻域怎么防“串门”
十字路口是八邻域算法最容易出问题的地方。原因在于,十字路口本质上是两条赛道垂直交叉,从摄像头的透视视角看过去,前方会出现一个“T”形或者“十”形的黑色连通区域。如果八邻域追踪不做任何限制,边界追踪到十字中心附近时会“沿着对面的赛道边缘”继续追过去,导致左右边界在十字区域完全错乱。
我验证过几种方案,最终稳定下来的是“最大追踪长度限幅+边界方向突变检测”组合拳。具体来说:
- 设定一个边界追踪的最大点数限制(我设的是图像高度+20),防止在十字区域无限追踪。
- 每追踪一个新点,计算它与前一个点的向量方向,如果相邻边界点的方向变化超过45度,就认为进入了特殊区域,停止追踪。
- 检测到边界方向突变后,记录突变点位置,用于后续元素分类。
结合方向突变点的位置和左右边界的对称性,可以区分十字、环岛等元素。十字路口两条边界几乎同时发生方向突变,而且突变方向是对称的;环岛则是一条边界突变,另一条边界正常。
4.2 环岛入口:八邻域天然的方向优势
环岛应该是很多队伍最头疼的元素。逐行扫线在环岛入口处通常会因为赛道大面积粘连而直接放弃治疗。八邻域算法因为追踪的是连续的边界线,环岛入口在图像上的表现是边界线出现了明显的曲率变化,但边界线本身不会断,所以天然适合处理环岛。
实战中,我用了两个特征来识别环岛入口:
第一个是边界点曲率的累积变化量。左边界在正常直道上的方向变化很小,如果连续10个边界点内方向变化累积超过90度,就判定进入环岛入口。第二个是左右边界之间的宽度突变。环岛入口处,靠近环岛一侧的边界会突然向外拐,导致局部行上的赛道宽度从正常的40到50像素膨胀到80像素以上。
方向突变检测和宽度突变检测配合使用,误判率很低。实际测试中,在赛道上跑了20多圈,环岛入口识别成功率在95%以上,误判主要是把S弯当成环岛,这个可以通过S弯的边界方向变化是平滑连续、环岛是突变这一点来区分。
4.3 坡道:八邻域追踪的“高度错觉”
坡道对八邻域算法本身的干扰不大,因为坡道上的赛道边界依然是连续清晰的。麻烦的是坡道会导致摄像头俯仰角变化,图像中赛道的位置会整体偏移,进而影响起始点寻边。
坡道识别我用的方案很简单:统计边界追踪过程中各行的赛道宽度,如果宽度整体缩小到正常值的60%左右,同时边界点数量明显减少,就判定进入坡道。坡道上八邻域追踪逻辑不用变,只需要调整PID参数的切换逻辑即可。
这里顺带提一个和八邻域不直接相关但必须注意的硬件点:上坡时摄像头俯仰角变大,如果固定阈值二值化,很容易因为远处光线变化导致边界误判。我对坡道场景单独用了一套自适应阈值参数,保证坡道上的二值化质量。很多队伍坡道翻车不是因为算法不行,而是二值化就挂了,八邻域再强也架不住“垃圾进,垃圾出”。
4.4 元素识别在八邻域框架内的统一
一旦边界追踪统一用八邻域,元素识别就变成了边界特征的分支判断,非常有条理。我在代码里建立了一个结构体,把一次完整图像处理的结果全部封装起来:
typedef struct { int left_points_x[MAX_TRACE_POINTS]; int left_points_y[MAX_TRACE_POINTS]; int left_count; int right_points_x[MAX_TRACE_POINTS]; int right_points_y[MAX_TRACE_POINTS]; int right_count; int start_left_x; int start_right_x; int track_width; // 赛道宽度(像素) int center_offset; // 中线偏差(像素) int element_type; // 赛道元素类型:直道/弯道/十字/环岛/坡道 int lost_left; int lost_right; } TrackInfo;每一帧图像处理完,所有上层控制逻辑需要的输入都在这一个结构体里。转向PID拿center_offset,速度决策拿track_width和element_type,元素触发拿左右边界的状态组合。代码结构清晰,调参也方便。以前用逐行扫线的时候,每加一个新元素识别就要重新扫一遍图像,代码越写越乱;换到八邻域框架之后,元素识别全部在边界特征上做文章,新增元素只需要在分支判断里加一个case就行。
5. 参数调试与常见问题排查:那些踩过的坑
5.1 追踪方向参数(搜索起始方向偏移)的调试
八邻域算法里最玄学的参数就是搜索起始方向的偏移量。我给左边界用的search_order_left数组,是经过实地测试调整过的,但换一个赛道环境、换一个摄像头安装角度,这个顺序可能需要微调。
一个靠谱的调试方法:先在静止状态下,手动把车放在赛道的不同位置(直道、弯道、十字入口),采集摄像头图像,用PC端的上位机工具查看边界追踪结果。如果边界点在某个位置开始“绕圈”或者反复横跳,说明搜索方向顺序在该曲率下不匹配,需要调整对应方向的优先级。
调试的时候建议一次只改一个参数的顺序,改完保存一帧处理结果对比图。不要同时调多个位置的方向顺序,会把自己绕进去,很难定位问题。
5.2 八邻域追踪“死循环”的陷阱与防护
八邻域边界追踪有一个经典bug:如果搜索顺序写得不好,边界点可能陷入“两点反复横跳”或者“小环循环”。比如边界是一个孤立的黑色噪点区域,追踪点可能绕着这个噪点转圈,永远走不出去。
我的防护方案是:每个边界点入数组前,检查它与前两个点的坐标是否构成循环。具体判断方法是,如果连续3个点中出现了相同的坐标对,立即终止追踪并标记该次追踪为失败。这个检查的计算量可以忽略不计,但能避免很多追踪逻辑的“假死”问题。
更深层的防护是给追踪加上最大点数限制。我的max_points设的是IMG_H+20,正常情况下边界追踪不会超过这个数。如果连续多帧都触发了最大点数限制,说明图像中有异常连通区域(比如十字口的误识别),要回到元素识别逻辑去排查。
5.3 十字路口的“边界飞线”怎么根治
十字路口最典型的故障是:边界追踪进十字后不再沿着当前赛道追,而是沿着对面的赛道中线一路追上去,导致计算的赛道宽度凭空翻倍。
这个问题我用“双条件切断”解决,代码逻辑如下:
// 追踪过程中检查 if (count > 3) { // 条件1:边界方向突变超过阈值 int dir_change = abs(calc_direction(points_x[count-1], points_y[count-1], points_x[count-2], points_y[count-2]) - calc_direction(points_x[count-2], points_y[count-2], points_x[count-3], points_y[count-3])); // 条件2:左右边界宽度突变超过阈值 int width_now = get_track_width_at_row(points_y[count]); int width_prev = get_track_width_at_row(points_y[count] - 2); if (dir_change > 45 && abs(width_now - width_prev) > 20) { // 判定为十字区域,切断本次追踪 break; } }这个方案的依据是:进入十字区域,边界会同时满足方向和宽度的双重突变。只满足方向突变可能是急弯,只满足宽度突变可能是反光干扰,两者同时满足才触发切断。用上这个逻辑之后,十字路口边的边界追踪错误率大幅下降。
5.4 噪点和反光对八邻域的干扰有多大
先说结论:八邻域算法本身对单点噪点的抗性远好于逐行扫线,但对抗反光这种“成片噪点”依然力不从心,必须在二值化阶段解决。
单点噪点的场景:八邻域搜索时,如果某个方向上有一个孤立的噪点被判为“黑”,边界追踪可能会短暂地被带偏一步。但因为后续搜索依然是连续性的,追踪大概率会在下一步重新回到正确的边界上。这就是八邻域“自纠错”能力的体现。
成片反光的场景:阳光直射赛道,形成一个白色亮斑,二值化后亮斑区域变成白色,相当于赛道中间被“挖”掉了一块。八邻域追踪到亮斑边缘时,会以为边界到了,直接断线。这种情况必须在二值化层做处理,我的方案是“动态窗口阈值”:以图像中心区域的亮度均值为基准,动态调整二值化阈值,能有效抑制大面反光。
5.5 边界丢线后的补线策略
八邻域追踪偶尔还是会断线的,尤其是在坡道顶部、十字出口这些图像信息混乱的位置。断线之后不能直接放弃,否则控制逻辑会因为没有边界数据而“失明”。
我的补线策略分三级:
第一级,短距离补线:如果丢线位置在图像中上部(远处),直接用最近一个有效边界点向上延伸,用上一次的边界斜率外推补几个点。第二级,中距离借线:如果一侧边界完全丢失,另一侧正常,可以根据赛道宽度,用正常侧边界减去或者加上标准宽度,镜像生成丢失侧的预测边界。第三级,全面丢线的兜底:如果两侧边界都丢了,说明图像处理已经基本失效,这时候只能依赖上一帧的赛道信息,保持上一帧的转向输出并强制降速。
这三个级别对应的控制策略不同:短距离补线后可以继续正常跑,中距离借线需要稍微降速并限制转向幅度,全面丢线的兜底策略是保命用的,保证车不冲出赛道等下一帧恢复。
6. 从21届到22届,八邻域算法在竞赛中的演进与实用性思考
6.1 竞赛规则变化对图像算法的要求变化
21届智能车竞赛的赛道元素种类相对常规,十字、环岛、坡道都有了,规则解析能力的要求主要在线性跑完赛道。到了22届,赛道复杂度进一步提升,元素密度更高、组合更多,单纯依赖逐行扫线加各种特判的队伍,代码维护成本急剧上升。
我们队伍在21届后期彻底淘汰了逐行扫线,全面转向八邻域框架。22届备赛时,新增的元素识别(比如三岔路口)就是在八邻域框架上直接加了一个新的分支判断,几乎没有改动原有的边界追踪核心逻辑。反观同校另一支还在用逐行扫线的队伍,每次加新元素就要在新规则下反复调整扫线逻辑,代码越来越难维护,最后成绩反而不如预期。
6.2 八邻域的局限性和替代方案
必须客观地说,八邻域算法不是银弹。它在赛道边界清晰、二值化稳定的场景下表现近乎完美,但在以下几种场景下会力不从心:
第一,极其恶劣的光照条件。如果赛道一半亮一半暗,二值化本身就很艰难,八邻域的连续性也无从发挥。第二,边界本身模糊不清,比如新旧赛道胶带颜色相近、赛道外区域和赛道颜色对比度过低。第三,对多个相似元素的区分,八邻域输出的是边界曲线,元素识别仍然需要额外的特征工程。
如果预算和算力允许,一些高水平队伍开始尝试深度学习方案,用卷积神经网络直接回归赛道中线。但竞赛场景对实时性和可控性要求极高,神经网络的黑盒特性让调参变得困难。我个人认为,在人力和时间有限的竞赛周期里,八邻域依然是性价比最高的选择,深度学习可以作为探索方向。实际队伍中能正经跑深度学习的,背后通常有一个研究生学长带的课题组在支撑,普通本科队伍很难复制。
6.3 我的个人调车体会与建议
最后聊一点实际心得。八邻域算法从原理上并不复杂,难点在于把它和你的电路、机械、控制策略真正融合成一套完整的系统。很多队伍抄到一份八邻域代码就以为万事大吉,结果跑起来各种问题,然后开始怀疑算法不行。其实问题往往出在细节上,比如摄像头安装的高度和角度影响了二值化效果,比如起始点寻边的逻辑没有针对自己的图像分辨率适配。
我给后面参赛同学的建议是:图像处理只是整个系统的一环,不要迷信任何单一算法。你最应该花时间的,是建立一套好的调试工具链——能实时看二值化图像、能实时看边界追踪结果、能回放历史数据那种。有了好的调试工具,八邻域算法参数调起来事半功倍,没有的话就只能靠猜和试,既慢又痛苦。
根据我的实际经验,一套完整的八邻域边界追踪从写到稳定,大概需要一到两周的集中调试时间。第一天写通基本逻辑,后面连续几天都在处理二值化、起始点、方向顺序这些看似琐碎但决定成败的细节。如果你现在还在用逐行扫线并且已经在十字和环岛问题上卡了挺久,我真心建议你花几天时间切换到八邻域,前期投入的适应成本,在后续的调试效率上一定会赚回来。