尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

智能车图像处理核心:八邻域算法原理与工程实现

发布时间:2026/9/30 1:13:18

资讯中心
01
ARTICLE

智能车图像处理核心:八邻域算法原理与工程实现

智能车图像处理核心:八邻域算法原理与工程实现
1. 为什么智能车图像处理离不开八邻域1.1 从像素到赛道图像处理到底在干什么先聊点实在的。做智能车视觉归根结底就一件事把摄像头拍到的画面变成车能理解的路径信息。摄像头输出的一帧图像在单片机里就是一张二维数组每个元素是0到255的灰度值。但车不是靠“看”来跑的它靠的是“算”——算出赛道的边界在哪里算出中线在哪里算出弯道曲率是多少。这里就绕不开一个最基础的操作像素与像素之间的关系。我刚开始接触这个方向时也犯过傻以为图像处理的目的就是把图像变好看、变清晰。但调到第五版、第六版的时候才真正明白清晰不是目的可计算的结构才是目的。什么叫可计算的结构就是你的程序能从一帧图像里稳定地找出“哪些像素是赛道边界”“哪些像素属于同一个障碍物”“哪些像素是噪声”。这些判断全都建立在邻域分析上。所谓邻域就是围绕某个像素的一圈邻居。你处理任何一个像素都不能孤立地看这个点本身要看它和周围像素的组合关系。八邻域就是最常用的一种邻域定义把当前像素周围上、下、左、右、左上、右上、左下、右下八个位置全部纳入分析范围。在智能车赛场上除了极少数用激光雷达的方案绝大多数摄像头组队伍用的都是这套逻辑。你去看那些开源代码从入门级的边缘提取到高级的连通域分析和元素识别底层跑得最频繁的就是八邻域遍历。可以说八邻域是智能车图像处理里面最基础也最能拉开差距的一环。优化得好整帧图像处理只要两三毫秒写得粗糙一个连通域标记就能把单片机跑死。1.2 四邻域和八邻域一字之差带来什么不同有朋友可能问为什么非要用八个方向四个方向不够吗这里得先明确四邻域和八邻域的定义差异。四邻域只考虑上下左右四个正方向八邻域额外加入四个对角方向。在二值图像里四邻域判断的是“上下左右有没有和当前像素相同的点”八邻域判断的则是“周围一圈有没有和当前像素相同的点”。这个差异在实际场景中影响非常大。拿智能车最常见的赛道二值图来说假设你提取到的赛道边界是黑色像素或白色像素边界在图像上是一条连续但倾斜的曲线。如果只用四邻域倾斜的边界会被拆成很多段不连通的小块因为对角相邻的像素在四邻域里不算邻居连通性判断会直接断掉。举一个我在实验室实际跑过的例子。摄像头从侧面拍赛道边界线在画面里是斜的倾斜角度大概30度。用四邻域做连通域分析时同一个边界被拆成了四十多段碎片中线和曲率计算出来的结果噪声大得没法用。换成八邻域之后整个边界成为一个完整连通域数据干净了很多。但是八邻域也不是没有代价。引入对角方向之后两个本来隔着一条缝隙的区域可能被误判成连通这在噪声明显的图像里会产生很多“伪连通”。尤其当赛道附近有光线反光、阴影边缘或者场地上的杂色时八邻域的误连率比四邻域高不少。所以关于四邻域和八邻域的选择没有绝对的好坏要看你的图像质量、场景特点以及后续算法的容错能力。对于大部分智能车摄像头方案八邻域是主流选择因为赛道元素普遍是连续结构倾斜边界和曲线非常多。但如果你做的场景是室内结构化赛道、画面干净、元素方正四邻域反而可能更稳。1.3 八邻域在智能车上的典型应用场景聊完概念再来梳理一下八邻域在智能车图像处理里到底用在哪几个地方。我按实际使用频率排个序连通域标记把二值图中所有独立的白色或黑色区域编号用于识别赛道、障碍和元素。八邻域在这里是核心因为赛道、斑马线、圆环这类元素都是连续区域必须用八邻域才能把它们各自完整地“圈”出来。边缘提取与边界跟踪从赛道二值图中逐行扫描找到左右边界点。八邻域用于判断当前边界点下一个可能的走向顺着边界点一直“爬”到图像顶部。断线修补与噪声滤除二值化之后赛道上经常会出现反光断裂区域可以用八邻域统计当前像素周围同类像素的数量如果周围同类像素太少就判定为孤立噪声点。元素识别前的结构分析比如判断十字路口、环岛、坡道等元素时需要分析边界的拓扑结构八邻域提供的连通关系是这种分析的基础。以我自己的代码为例一张80x60的二值图逐行扫一遍提取边界要跑一次八邻域判断连通域标记又要跑一次八邻域遍历边缘跟踪阶段还得再跑一次。可以说八邻域的性能直接决定了整帧图像处理的上限。这样讲可能有点抽象后面我把八邻域的原理、代码实现、踩过的坑整体拆开一条一条说明白。2. 八邻域的基础实现思路与代码参考2.1 邻域遍历的基本实现先来看最基础的实现。假设我们处理一张二值图像素值只有0和255两种图像尺寸是COL列、ROW行数据存在一个二维数组里。遍历一个像素的八邻域最直接的写法是两层循环#define COL 80 #define ROW 60 uint8_t image[ROW][COL]; // 二值图255为白0为黑 int neighbor_count(int x, int y, uint8_t target_value) { int count 0; for (int dy -1; dy 1; dy) { for (int dx -1; dx 1; dx) { if (dx 0 dy 0) continue; // 跳过自身 int nx x dx; int ny y dy; if (nx 0 || nx COL || ny 0 || ny ROW) continue; // 越界保护 if (image[ny][nx] target_value) { count; } } } return count; }这段代码的逻辑很直白遍历当前像素周围3x3的九个格子排除自身统计其中等于目标值的像素数。边界像素要加越界保护否则数组访问越界会导致程序跑飞这在单片机上是非常头疼的问题。不过在实际工程里这种写法效率并不高。双层循环加越界判断在60x80分辨率下逐点调用一帧图像要执行几千次单片机算力有限这里需要做优化。优化方案有这么几种第一把八邻域的判断改写成固定偏移量查表。因为遍历的顺序是固定的完全可以预先定义一个偏移数组每次直接按偏移取值省掉内层循环。const int dx8[8] {-1, 0, 1, -1, 1, -1, 0, 1}; const int dy8[8] {-1, -1, -1, 0, 0, 1, 1, 1};这样neighbor_count就变成一个固定八次的循环没有嵌套也没有多余的边界判断。第二如果图像边缘不需要做邻域统计实际项目里边缘很少是关键区域可以把边缘一圈单独处理内部像素全部不判断越界能省掉大量比较指令。第三用查表法代替比较运算。八邻域统计的结果只有0到8九种可能可以预先把当前像素周围8个像素拼成一个字节直接用查表得到连通数或边缘标记。这些优化方法对单片机性能的提升非常明显。我实际测过未优化版本处理一帧图像需要约12毫秒优化后降到3毫秒以内效果立竿见影。2.2 从八邻域到连通域标记连通域标记是八邻域应用里最经典的一个场景。它的目标很明确把二值图中所有彼此连通的同色像素块标上同一个编号。在智能车场景中二值图里通常有赛道、背景、可能的障碍物、边界噪声等连通域标记可以把这些区域区分开方便后续筛选。连通域标记的主流算法有两种两遍扫描法和种子填充法。两遍扫描法先正向扫描图像为每个新遇到的连通区域赋予临时编号同时记录编号之间的等价关系第二遍再统一合并等价编号。种子填充法则更直观——遇到一个未标记的目标像素就以它为种子用八邻域递归或栈迭代向四周扩展把连通的像素全部标记为同一编号。对智能车来说我推荐种子填充法。原因有二第一二值图中目标区域占比不大种子填充法只需遍历目标像素所在连通域不用全图扫描多遍第二赛道图像通常只有一个或少数几个主要连通域种子填充法可以提前终止适合在算力紧张的MCU上运行。这里给出一个基于栈的种子填充法伪代码实现int label[ROW][COL]; // 标记数组0为未标记 uint8_t image[ROW][COL]; // 二值图255为前景 int stack_x[ROW * COL]; int stack_y[ROW * COL]; int top 0; void flood_fill(int sx, int sy, int current_label) { top 0; stack_x[top] sx; stack_y[top] sy; top; while (top 0) { top--; int x stack_x[top]; int y stack_y[top]; if (x 0 || x COL || y 0 || y ROW) continue; if (label[y][x] ! 0 || image[y][x] ! 255) continue; label[y][x] current_label; for (int i 0; i 8; i) { int nx x dx8[i]; int ny y dy8[i]; if (nx 0 nx COL ny 0 ny ROW) { if (label[ny][nx] 0 image[ny][nx] 255) { stack_x[top] nx; stack_y[top] ny; top; } } } } }这里有一个容易被忽略的坑栈空间的大小一定要足够。在极限情况下一块连通域可能占据整幅图像栈容量至少要超过图像总像素数。我见过有同学把栈数组定义小了导致图像复杂时栈溢出程序直接卡死排查了很久才发现是这个问题。种子填充的扩展顺序也会影响性能。如果八邻域按固定顺序压栈会把很多重复的像素多次压入栈中增加了无效访问。一个常见的优化是在入栈前先判断是否已标记虽然不能完全避免重复压栈但能显著减少栈操作次数。2.3 八邻域边缘提取让赛道边界真正可读连通域标记解决的是“图像里有哪些目标区域”的问题。但智能车光知道区域还不够还要知道区域的边界在什么位置、方向怎么变化。边缘提取做的就是这件事。常用的边缘提取方法有Sobel算子、Canny算子等但这些梯度算法在单片机上运算量大而且对噪声敏感。在智能车竞赛里我更推荐一种更“接地气”的思路基于八邻域搜索的边界跟踪算法。流程大致是这样的在二值图中从图像底部开始逐行扫描找到第一个左边界点即从黑变白的跳变点。以这个点为当前点记录其坐标。在当前点的八邻域中寻找下一个满足条件的边界点。搜索顺序很关键一般按固定的方向顺序搜索比如从右上开始顺时针转一圈。找到新边界点后更新当前点继续搜索直到到达图像顶部或没有新的边界点。这个思路类似走迷宫——始终沿着墙边走就可以把整段墙的轨迹记录下来。相较于逐行扫描后拼接边界点这种跟踪方式得到的边界点是顺序排列的天然适合后续的曲率计算、断线修补和元素特征判断。这里有一个重要的细节八邻域搜索方向的选择。如果一幅图像中赛道在边界点的左侧那么搜索时应该优先搜索左前、正前、右前三个方向也就是延续当前边界走向的方向这样可以避免搜索到反向的路径。如果每次都是无差别地搜索八个方向边界跟踪容易在弯道处“绕回去”形成死循环或者错误的边界轨迹。我在实际代码里会给边界跟踪加一个方向限制// 当前搜索方向序号0~7对应八邻域8个方向 int search_direction last_direction 3; // 从上一个方向的左偏45度开始 for (int i 0; i 8; i) { int dir (search_direction i) % 8; int nx current_x dx8[dir]; int ny current_y dy8[dir]; if (is_boundary(nx, ny)) { // 记录下一个边界点 last_direction dir; break; } }这样既保证了搜索的连续性又不会漏掉转角处的边界点。很多开源代码里用的“八邻域跟踪”就是这个思路的变体。3. 实战案例用八邻域提取单边赛道边界3.1 图像预处理的关键细节前面讲了太多原理现在用一个具体的案例把整个过程串起来。假设我们要做的是从一张智能车摄像头拍摄的赛道图像中用八邻域算法提取出左侧赛道边界线并生成一条可用于后续中线计算的边界序列。第一步不是八邻域而是图像预处理。摄像头的原始图像是灰度图上面有光照不均、反光、噪声等问题直接做二值化必然出现各种零碎噪点。我的预处理方案分三步灰度校正、滤波、二值化。灰度校正的作用是把图像亮度分布拉平。我用的是大津法OTSU求动态阈值再结合近几帧的阈值做滑动平均避免单帧阈值抖动导致边界位置来回跳。这一步对八邻域算法影响很大——如果阈值不稳定边界像素会频繁在黑白之间切换八邻域搜索时的连续性难以保证。滤波则是用中值滤波把孤立噪点去掉。中值滤波在MCU上实现稍慢但3x3窗口在60x80分辨率下运行时间可以控制在1毫秒内可以接受。它比均值滤波好在能保留边缘锐度不会把边界模糊掉。二值化之后图像应该是黑白分明的。这时候还不能直接跑八邻域算法因为图像边缘一圈往往会有反光或阴影残留。我的做法是把图像最外圈像素全部置为背景色这样后续八邻域遍历时就不用反复做越界判断了。这是很多库函数里都会做的图像“内缩”操作效果很好。3.2 从左边线搜索到边界序列生成预处理完成之后开始跑核心的边界跟踪。代码逻辑用伪代码表示如下int left_boundary_x[ROW]; // 每行左边界的x坐标 int left_boundary_valid[ROW]; // 该行是否有有效边界 void extract_left_boundary() { // 从底部往上找到第一个左边界点 int start_x -1; int start_y -1; for (int y ROW - 1; y 0; y--) { for (int x 0; x COL; x) { if (image[y][x] WHITE image[y][x - 1] BLACK) { start_x x; start_y y; break; } } if (start_x ! -1) break; } if (start_x -1) { // 没找到边界全部置为无效 return; } // 边界跟踪 int cur_x start_x; int cur_y start_y; int dir 0; // 上一次搜索方向 while (cur_y 0 cur_y ROW) { left_boundary_x[cur_y] cur_x; left_boundary_valid[cur_y] 1; // 从当前方向偏左45度开始搜索 int search_dir (dir 3) % 8; int found 0; for (int i 0; i 8; i) { int d (search_dir i) % 8; int nx cur_x dx8[d]; int ny cur_y dy8[d]; if (nx 0 || nx COL || ny 0 || ny ROW) continue; if (image[ny][nx] WHITE image[ny][nx - 1] BLACK) { cur_x nx; cur_y ny; dir d; found 1; break; } } if (!found) { // 没有找到下一个边界点跟踪结束 break; } } }这段代码看起来简单但有一些细节要重点说。首先是“边界点”的判定image[ny][nx] WHITE image[ny][nx-1] BLACK含义是当前像素是白色赛道左侧像素是黑色背景。也就是说我们要找的是赛道白色区域的左边缘点。这个判定条件跟赛道提取方向有关不要写反。其次search_dir的计算方法用的是上一次方向加3。如果上一次走的是方向0正上加3就变成方向3左下意思是搜索从左下方向开始顺时针扫描。这种策略保证了搜索会优先沿着“继续向图像上部走”的大方向进行不容易回头绕圈。在实际测试中这种单边跟踪方式对“左边界连续、无明显大断点”的赛道非常管用。但遇到十字路口、环岛这种有多个边界候选点的场景跟踪算法会迷路因为当前点的八邻域内可能同时存在多个满足边界条件的点。遇到这种情况需要靠后面讲的断线处理和元素识别来做兜底。3.3 边界序列的数据后处理拿到left_boundary_x[]数组之后还不能直接送给控制算法用因为原始跟踪结果会存在跳变和丢失。我一般会做三个后处理步骤。第一步是边界行坐标的限幅滤波。前后两行的边界x坐标如果突变超过一定阈值比如8个像素就把后一行标记为无效等后续修补。这样做的原因是正常赛道的边界在相邻行之间不会突然大范围横跳突变多半是噪声或误检。第二步是中值填补。对标记为无效的行取上下最近的有效边界值的平均值作为该行边界的估计值。这样可以在赛道反光导致局部断裂时让边界序列保持连续。第三步是小窗口平滑。用3到5行的滑窗对边界坐标做均值处理让边界线更平滑减少曲率和控制计算的抖动。这里要注意平滑窗口不能开太大否则弯道处的真实曲率会被抹平车过弯会延迟反应。对于60x80分辨率的图像3行窗口就够用了。后处理做完之后left_boundary_x[]就是一条相对平滑、连续、按行排列的边界轨迹。用同样方法提取右边界二者平均就得到中线。中线两侧的边界宽度信息还能用来判断当前赛道是直道还是弯道甚至是环岛入口的曲率突变。整个流程在8位单片机上一帧图像处理总耗时约4到5毫秒剩下的时间足够跑控制算法和元素判断完全满足竞赛需求。4. 常见问题与排查技巧实录4.1 小连通域噪声爆炸怎么处理这是所有做八邻域连通域分析的人都会遇到的第一道坎。二值化之后图像上会有很多孤立的白色噪点或者细碎的黑色裂缝用八邻域标记时这些噪点会被标记成一个个独立的连通域。如果不对它们做处理后面的元素筛选就会非常痛苦——明明赛道上只有三四个目标区域结果连通域编号列出来有五十多个。我的处理策略分两步走。第一步是从源头上减少噪声也就是把预处理做好。中值滤波和边缘腐蚀可以去掉很大一部分孤立点。但注意不要为了去噪反复腐蚀否则会把细小的赛道元素也腐蚀没了比如斑马线边缘的细节。第二步才是用八邻域做后置过滤。统计每个连通域的面积像素数量把面积小于某个阈值的连通域直接丢弃。面积阈值一般取图像总像素数的0.5%到1%。对于60x80图像阈值设在30到50像素之间比较合适。这个值不能太大否则赛道在远处成像面积小容易被误删。用种子填充法时面积统计可以放在填充过程中顺便完成每压入一个像素就计数器加一填充结束后直接判断计数器是否超过阈值。这样不用额外写一遍全图扫描计算量没有增加。4.2 边界断线重连策略智能车赛场上最头痛的问题之一就是赛道边界在图像中断裂。断裂的原因很杂阳光直射导致反光、赛道边缘和地面颜色相近、摄像头曝光参数没调好甚至场地地缝的阴影都可能让某一段边界在二值化后消失。八邻域边界跟踪遇到断线时表现就是跟踪到一半突然“找不到下一个边界点”提前结束。这会导致边界序列后半段全是无效值中线计算自然也会出错。应对方案分三个层次第一层是前面提到过的后处理中值填补。适用于断线较短3到5行的情况。这是最简单也最常用的一招大部分普通场景下的边界断裂都能靠它解决。第二层是在八邻域搜索时允许跳过一定数量的“断点”继续搜索。具体做法是当八邻域内找不到下一个边界点时不立即结束而是在更大的范围内搜索——比如把搜索半径扩大到5x5区域如果能找到边界点且中间间隔的距离小于阈值就把中间的行用线性插值补上。这种做法相当于让跟踪算法“跨过”断线继续前进处理中等长度的断线效果好。第三层是针对长距离断线的策略——比如某一段赛道被强光照射导致十几行全部丢失。这种情况下靠局部搜索是救不回来的必须结合赛道宽度信息做推断。也就是用断线前一行的边界x坐标加上赛道平均宽度估算断线区域内的边界位置。这种方法精度有限但至少能让控制算法拿到一个合理的估计值不至于因为边界数据缺失而出现剧烈转向。这里要特别提醒一点断线填补必须区分场景。如果图像里根本没有赛道比如车冲出赛道了就不应该做填补而应该触发停车或者重新寻线逻辑。否则车辆会沿着一条完全不存在的赛道边界“幻觉”行驶后果很严重。4.3 性能优化把八邻域跑在单片机上的经验很多同学第一次在单片机上跑八邻域算法都会遇到一个严重问题帧率太低。一帧图像处理耗时几十毫秒整车控制周期被拉得很长过弯时压根反应不过来。这里分享一些我在实际调优中积累的经验。第一把图像分辨率降到够用就行。很多队伍的摄像头输出是QVGA320x240但实际处理根本不需要这么高分辨率。我自己的方案是把摄像头配置成80x60每像素8bit灰度。处理数据量只有原始分辨率的十六分之一八邻域遍历的速度直接提升一个量级。分辨率降低后远处赛道细节确实会损失但配合合理的元素识别算法影响完全可控。第二数据类型尽量用8位无符号整型。在32位单片机上int运算是4字节内存访问和数据搬运都更耗时。60x80的灰度图用uint8_t存一个数组才4800字节L1缓存都能装下访问效率很高。中间计算结果能转成uint8_t就转尽量不要用浮点。第三避免重复计算。八邻域处理中同一个像素会被多次访问比如边界跟踪时下一轮搜索会把上一轮已经检查过的邻域像素再检查一遍。减少这种重复访问的方式是记录上一轮的方向下一轮只搜索五个方向而不是八个。对于边界跟踪五个方向完全够用因为边界不可能突然180度掉头。第四用固定循环展开代替循环。八邻域遍历只有八次完全可以把代码写死八次判断分别处理八个方向省去循环变量递增和比较的开销。代码会显得啰嗦但对性能提升的帮助非常大。我测试过一组数据在STM32F103 72MHz环境下60x80图像未优化八邻域边界跟踪耗时约7.3毫秒经过分辨率和循环优化之后降到1.8毫秒。整个图像处理流水线预处理加边界提取加后处理控制在4毫秒以内帧率做到100fps以上毫无压力。4.4 八邻域方向定义不一致的暗坑最后再说一个容易被忽视的细节——八邻域的方向编号和坐标定义。不同资料里八邻域的方向图可能完全不一样。有的是从正上方开始顺时针编号有的是从右方开始逆时针编号。如果你的代码里既有查表数组又有方向编号运算比如方向加3、减1之类的操作方向定义不一致会直接导致搜索逻辑错乱。我的建议是写代码之前先把八邻域方向图定死并且画出来贴在代码注释里方向编号屏幕坐标x向右为正y向下为正 3 2 1 4 0 0 5 6 7注意编号顺序要和dx8、dy8两个数组严格对应。调试时如果发现边界跟踪轨迹“绕圈”或者在某一点反复横跳大概率就是方向编号错位了。这个坑我在项目早期踩过排查花了整整一个下午。最后是打印每个跟踪点的坐标和方向对着图像逐点核对才发现的。后来我学乖了在代码里加了一个简单的自检函数给定一个已知的模拟图像让算法走一遍确认输出的边界序列和预期一致。这样每次改完代码都能快速验证不用等到上赛道才暴露问题。5. 下一步扩展思路八邻域讲到这里基础部分算是比较完整了。但有两点想单独展开说一下因为在后续的项目优化中它们是真正拉开差距的地方。第一点是八邻域在元素识别中的位置。很多人写完边界提取就急着跑PID但我建议中间加一道“元素粗分类”。方法和思路就是利用边界序列的几何特征——比如某几行边界的跳变方向非常剧烈可能遇到了十字边界宽度突然扩大又收窄可能是圆环入口边界完全丢失但前方有其他连通域可能是障碍物。这些特征本质上都是基于八邻域提取出来的边界连通性来分析的。所以八邻域不仅仅是图像处理的基础算法它输出的连通关系和边界轨迹就是后面所有元素识别的输入素材。第二点是八邻域到底够不够用。在某些复杂场景下比如赛道反光严重、场地有强烈光影变化八邻域因为只考虑局部像素关系会出现判断“短视”的问题。这时可以考虑引入一些增强手段比如基于方向链码的边界描述、Freeman链码的走势预测、或者把连续多帧的八邻域结果做时间域累计。但这些都是八邻域之上的上层建筑基础没打牢上层建筑也立不住。我在实际使用中最受益的一个习惯是每写一个图像处理模块就准备一张标准的测试图把模块的输出结果固化下来做回归对比。这样做的好处是算法参数调整后能立刻发现影响范围不至于把一个地方修好了、另外三个地方弄坏了。智能车调试最怕的就是这种“按下葫芦浮起瓢”的连锁问题。最后再分享一个小技巧八邻域算法里最容易被忽略的性能瓶颈不在计算本身而在数据访问模式。嵌入式平台对连续内存的访问速度远高于随机访问所以如果能把二值图打包成位图格式——每8个像素存成一个字节——不仅能节省存储空间还能利用位运算一次处理多个像素。我之前做连通域标记时把全图遍历改成位图操作处理时间又缩短了近一半。这个优化思路值得你试试。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。