news 2026/8/21 9:05:33

【21届智能车赛轮腿穿越组】基于 CYT4B 的轮腿车开发笔记(六):视觉处理与循线

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【21届智能车赛轮腿穿越组】基于 CYT4B 的轮腿车开发笔记(六):视觉处理与循线

本期核心:灰度图像怎么变成车能用的信息?二值化、噪声滤波、左右边线提取、跳跃触发识别、视觉偏航修正——一帧 120×188 的图像,如何驱动偏航控制。


1. 先想清楚:摄像头能给我们什么

摄像头在车上的角色是**“眼睛”——提供 GPS 给不了的局部、高精度**信息:

  • GPS 精度是米级,赛道线是厘米级——循线必须靠视觉;
  • 跳跃台阶的位置,也要靠视觉提前发现。

但摄像头给的是一帧灰度图像,不是"线在哪"。中间要经过一条完整的图像处理流水线

原始灰度 → 二值化 → 噪声滤波 → 逐行边线扫描 → 中线提取 → 偏差→偏航修正

这套流水线在main.c里由三个函数接力完成:

if(camera_driver_frame_ready()){constuint8_t*raw=camera_driver_get_image();image_binarize(image_bin,(constuint8_t(*)[188])raw,car.img_threshold);/* 二值化 */image_noise_filter(image_bin);/* 去噪 */track_detect_process(&car,image_bin);/* 提取边线 */camera_driver_clear_flag();}

三个函数,一帧图,各干一件事。每帧图像只在主循环处理,不阻塞 1ms 控制中断——实时性由调度保证(第1期讲过)。


2. 第一步:二值化 —— 把灰度变成黑白

原始图像每个像素是 0~255 的灰度。循线场景下,我们只关心"是白还是黑"(白 = 赛道,黑 = 背景)。于是设定一个阈值:

voidimage_binarize(uint8_tout[120][188],constuint8_tin[120][188],intthreshold){for(inti=0;i<CAMERA_H;i++){for(intj=0;j<CAMERA_W;j++){out[i][j]=(in[i][j]>=threshold)?IMG_WHITE:IMG_BLACK;}}}

就这么简单:>= 阈值 → 白,否则 → 黑。全图遍历一遍,O(H×W)。

阈值本身就是个可调参数car.img_threshold),存进 Flash、可以在菜单里调、开机从 Flash 读回(第4期讲过)。

二值化的意义:把 256 级灰度的浮云变成"是/否"的布尔判断,后面所有算法瞬间变简单。


3. 第二步:噪声滤波 —— 3×3 邻域投票

摄像头画质不完美,图像里有椒盐噪声(孤立的黑白点)。用 3×3 邻域"投票"滤掉:

voidimage_noise_filter(uint8_timg[120][188]){for(inti=1;i<CAMERA_H-1;i++){for(intj=1;j<CAMERA_W-1;j++){/* 统计 8 邻域 + 自身的白色点数 */uint8_twc=0;wc+=(img[i-1][j-1]==IMG_WHITE);.../* 8 邻域 */wc+=(img[i][j]==IMG_WHITE);if(img[i][j]==IMG_WHITE&&wc<=2)img[i][j]=IMG_BLACK;/* 孤立白点→黑 */elseif(img[i][j]==IMG_BLACK&&wc>=6)img[i][j]=IMG_WHITE;/* 孤立黑点→白 */}}}

思路:看邻居投票。一个白点周围全是黑的,说明它是噪声,翻成黑;反过来黑点周围一片白,翻成白。

这是最朴素的滤波:O(H×W×8),简单可靠。真正复杂的场景才需要中值/均值/形态学,循线这种对比度高的图,投票法够了。


4. 第三步:逐行边线扫描 —— 从图像里"抠"出赛道边界

这是视觉模块的核心。思路是从下往上逐行扫描,每行在左右方向上找"黑白跳变点":

intstart_x=CAMERA_W/2;for(inti=CAMERA_H-1;i>=0;i--){/* 从图像底部往上扫 */intraw_left=0,raw_right=CAMERA_W-1;if(img[i][start_x]==IMG_BLACK)start_x=CAMERA_W/2;for(intj=start_x;j>=0;j--)/* 向左找第一个黑点 */if(img[i][j]==IMG_BLACK){raw_left=j;break;}for(intj=start_x;j<CAMERA_W;j++)/* 向右找第一个黑点 */if(img[i][j]==IMG_BLACK){raw_right=j;break;}/* 宽度约束:上一行知道赛道多宽,这行偏差过大就矫正 */intpw=s->right_line[i+1]-s->left_line[i+1];if(rji>5&&lji<5)raw_right=raw_left+pw;/* 右边界缺失→按宽度补 */...s->left_line[i]=raw_left;s->right_line[i]=raw_right;s->center_line[i]=(raw_left+raw_right)/2;start_x=s->center_line[i];/* 下一行从本行中线附近开始搜 */}

三个关键工程细节

  1. 从下往上:图像底部离车最近、最可靠,先扫出底部中线,作为上面行的起始搜索点;
  2. 宽度约束:如果这一行只找到一个边,就用上一行的赛道宽度补出另一边——防止断线导致边线突变;
  3. 逐行推进:每行用上一行的中线作为起点往左右找,"路径连续性"让扫描又快又稳。

这套"从底部往上、宽度约束、连续推进"的扫描策略,是所有循线算法的基础套路。你的项目可以照抄这套骨架,换成别的摄像头、别的赛道也一样。


5. 第四步:中线偏差 → 视觉偏航修正

拿到每行的左右边线,就能算中线。中线偏离图像中心多少,就是车"偏了"多少

/* 取有效区段(剔除底部遮挡/顶部噪声) */for(inti=CAMERA_H-1-IGNORE_BOTTOM_ROWS;i>=30;i--){if((right_line[i]-left_line[i])>MIN_VALID_WIDTH){if(valid_bottom==-1)valid_bottom=i;valid_top=i;}}if(valid_bottom!=-1&&(valid_bottom-valid_top)>10){/* 取约 40% 高度处的中线做误差 */intlar=valid_top+(int)((valid_bottom-valid_top)*0.4f);floatavg=5行中线平均;/* 抗噪 */floaterror=avg-(CAMERA_W/2);/* 中线 vs 图像中心 *//* PD 修正量,叠加到目标偏航 */s->vision_yaw_correction=-(KP_YAW_VISION*error+KD_YAW_VISION*(error-last_err));last_err=error;}else{s->vision_yaw_correction=0.0f;/* 看不到线,修正归零 */}

这里的精髓

  1. 只用 40% 高度、5 行平均:不用整幅图像,取"代表性的一行"做误差——用局部信息代表全局,省算力还抗噪
  2. PD 修正:比例项快速跟线,微分项抑制左右摆;
  3. 看不线就归零:摄像头丢线时(进入跳跃/出界)修正量为 0,不会把车带偏。

6. 视觉和 GPS 怎么配合:修正量叠加

关键问题:视觉循线和 GPS 路径跟踪会不会打架?

答案在balance.c的目标偏航合成里——视觉修正量是叠加上去的:

floatcur_yaw=s->yaw+s->gps_yaw_bias;floattarget=normalize_deg(s->target_yaw+s->vision_yaw_correction);floatyaw_err=normalize_deg(cur_yaw-target);PID_Calc(&s->pid_yaw,yaw_err,0);
最终目标偏航 = GPS路径的目标偏航 + 视觉偏航修正量

设计思路:GPS 管"宏观方向"(走哪条路),视觉管"微观纠偏"(贴哪条线)。视觉修正量是一个小量,叠加在 GPS 目标上做局部微调,两者不冲突。

这就是多传感器融合最简单的形态:“主从叠加”。主源定方向,从源做修正。不用卡尔曼也能让两个传感器协同工作。


7. 视觉的另一个任务:跳跃触发识别

摄像头不只是循线,还负责在跳跃任务里识别台阶/障碍,触发跳跃(第3期讲过跳跃状态机)。

voidtrack_detect_process(CarState_t*s,constuint8_timg[120][188]){if(s->task_flag==1){if(s->jump_cnt==0||s->jump_cnt==2){/* 在图像中下方找"白块"(台阶/路障)底部 */for(inti=CAMERA_H-5;i>=0;i--){...统计中线附近白色像素数...if(wc>=MIN_STEP_WIDTH){step_bottom_y=i;break;}}}elseif(s->jump_cnt==1){/* 找黑块(跃过障碍后的路面) */...}/* 触发条件:台阶高度到达阈值行,或累计行驶距离到阈值 */if(s->jump_cnt==0&&step_bottom_y>=s->jump_trigger_row_1)s->jump_flag=1;elseif(s->jump_cnt==1&&s->all_distance>=s->jump_distance_1)s->jump_flag=1;elseif(s->jump_cnt==2&&s->all_distance>=s->jump_distance_2)s->jump_flag=1;}...}

触发跳跃的组合判据:

  1. 视觉判据:台阶的"行位置"(越往下越近)到达jump_trigger_row阈值;
  2. 里程判据:累计行驶距离all_distance到达jump_distance阈值(用于后续跳跃的等间距触发)。

视觉 + 里程双判据,互为冗余、互相兜底——一个判据丢失,另一个还能触发。这是"传感器冗余"的简单实践。


8. 完整视觉流水线回顾

原始灰度帧 │ 二值化(阈值可调) ▼ 黑白图像 │ 3×3 投票去噪 ▼ 干净二值图 │ 从底部向上逐行扫描(宽度约束) ▼ 左右边线 → 中线 │ ├──▶ 视觉偏航修正 ──叠加──▶ 目标偏航(贴线) └──▶ 台阶检测 ──▶ jump_flag(跳跃触发)

每一步都是"图进图出/图进数出"的纯函数,可以单独调试、单独可视化。


9. 本期小结

  • 二值化:阈值切分,灰度变黑白;
  • 投票滤波:3×3 邻域去椒盐噪声;
  • 边线扫描:从底部往上、宽度约束、连续推进;
  • 中线偏差 → PD 修正:40% 高度 + 5 行平均,丢线归零;
  • 视觉 + GPS 叠加:GPS 定方向,视觉做微调;
  • 视觉 + 里程双判据:跳跃触发互为冗余。

10. 下期预告

算法闭环基本齐了,最后一期讲人机交互与调参经验

LoRa 遥控的摇杆映射、按键/菜单系统怎么设计,以及整套系统在实车调试时踩过的坑和心得。

欢迎关注,持续连载中。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/21 9:04:30

【JMeter 学习打卡 Day 4】多用户登录 + 动态 token 关联

一、学习总览天数主题核心产出Day 1JMeter 入门与基础概念弄清 JMeter 是什么、能干什么、不能干什么Day 2第一个测试计划 HTTP 请求能独立搭出一个最小可运行的 HTTP 测试计划Day 3断言&#xff1a;让测试结果有意义理解为什么没有断言&#xff0c;错误率永远是 0Day 4参数化…

作者头像 李华
网站建设 2026/8/21 9:02:25

HEIR编译器实战:基于同态加密的私有化AI推理完整指南

在AI应用遍地开花的今天&#xff0c;数据隐私与安全已成为悬在开发者与企业头顶的“达摩克利斯之剑”。你是否遇到过这样的困境&#xff1a;想将AI能力集成到医疗、金融等敏感业务中&#xff0c;却因用户数据不能离境、模型参数需要保密等合规要求而束手无策&#xff1f;传统的…

作者头像 李华
网站建设 2026/8/21 8:57:30

【王道操作系统 | 第二章】进程管理、处理机调度与死锁

操作系统第二章的主线&#xff0c;是回答一个问题&#xff1a;多个程序同时运行时&#xff0c;操作系统如何管理它们、分配处理机、协调共享资源&#xff0c;并处理资源互相等待的情况&#xff1f; 本文从进程和线程出发&#xff0c;依次梳理进程控制、进程通信、处理机调度、同…

作者头像 李华
网站建设 2026/8/21 8:56:29

FastAPI实战指南:从零构建高性能Python API与Todo项目

在Python后端开发领域&#xff0c;选择一个高效、现代且易于上手的Web框架是项目成功的关键。如果你厌倦了传统框架的繁琐配置&#xff0c;或者正在寻找一个能快速构建高性能API的方案&#xff0c;那么FastAPI无疑是当前最值得投入学习的框架之一。它凭借其极简的设计、自动化的…

作者头像 李华