智能车“走马观碑”赛题全复盘:视觉巡线与碑文识别实战解析
发布时间:2026/9/8 11:30:50 锦皓数字建站

代码写了不少、熬夜调参也不少但真正把第21届智能车竞赛“走马观碑”这个赛题完整落地还是在赛后重新梳理才觉得通透。这个赛题属于室外创意类核心玩法可以概括成一句话小车要在赛道上跑得快同时还要“看清”路边的碑文最后到终点把内容报出来。听起来不算复杂但把“跑得快”和“看得清”放在一起难度立刻上来了。这篇文章就来复盘一下我们在“走马观碑”赛题中的完整技术方案包括任务拆解、系统架构、感知与控制实现、核心代码、现场踩坑和后续改进方向。如果你是准备下一届比赛的同学或者正在做视觉巡线小车、室外目标识别相关的项目这篇文章应该能帮你少走不少弯路。1. 赛题背景与任务拆解1.1 走马观碑赛题是什么“走马观碑”这个名称取自典故形容骑马路过时还能看清碑上的文字。在智能车竞赛里它被设计成一个综合性任务智能车在室外赛道或模拟场地中自主行驶赛道两侧布置若干“碑”也就是带有文字或符号的展板、标牌小车行驶经过时需要完成对碑文的采集、识别和存储完成赛道行驶后在终点区域输出识别到的碑文内容。也就是说比赛不仅考察循迹能力和速度还考察视觉感知、目标检测、文字识别以及多任务调度能力。相比传统竞速组只关注“跑得快”走马观碑更接近真实无人车场景中的“边行驶边感知”。从任务类型上看它属于视觉组创意组的结合体。很多队伍会把它拆成两个方向来做一组人负责把车调稳一组人专注训练识别模型。但从实际工程角度看这两部分不能割裂因为车速、曝光时间、运动模糊、识别距离这些因素会互相影响必须整体设计。1.2 任务流程与核心难点把整个比赛过程拆开大致是这样的流程车辆上电自检摄像头、编码器、显示屏、通信模块初始化小车从起点出发进入赛道循迹模式行驶过程中检测两侧的“碑”判断是否需要降速或保持速度对碑文区域进行拍摄、预处理、识别识别结果与当前任务状态绑定保存到本地或发送到上位机到达终点后停止并上报识别结果裁判或评分系统比对识别内容与真实碑文计算得分。听起来流程清晰但实际难点集中在以下几点运动模糊小车在行驶中拍照尤其是中高速状态下图像模糊严重光照变化室外场地的阳光、阴影、反光会造成同一块碑在不同角度下亮度差异巨大小目标识别碑在较远距离时在画面中占比很小检测模型容易漏检实时性识别不能太慢否则车已经冲过碑区信息就丢了多任务并发循迹、避障、识别、状态切换同时进行主控资源紧张终点对齐识别结果要与碑的顺序一一对应顺序错误会直接丢分。这些难点决定了我们不能只堆算力还要在算法和工程实现上做大量优化。1.3 本文复盘范围本文的技术复盘围绕以下内容展开硬件平台和软件框架的选择赛道循迹与碑文识别的具体实现方案核心代码的编写思路和关键片段现场调试中遇到的问题与解决过程对下一届备赛的工程建议。考虑到不同队伍使用的硬件差异较大本文不会绑定某一块开发板而是以“主控 摄像头 编码器/IMU”的通用架构为例重点讲解思路和代码组织方式。实际使用时你只需要把硬件相关的 API 替换成自己平台对应的驱动即可。2. 系统总体架构设计2.1 整车硬件平台在“走马观碑”这个赛题里硬件平台的选择直接决定算法上限。常见的方案有以下几种方案主控摄像头优点缺点单片机方案STM32 / TC264 / Infineon灰度摄像头 / OpenMV功耗低、稳定、与原有智能车体系兼容算力有限难以跑深度模型嵌入式Linux方案树莓派 / 香橙派 / Jetson NanoUSB摄像头 / CSI摄像头算力强可跑轻量模型和OCR启动慢功耗高需要额外稳压混合方案STM32 上位机协处理独立视觉模块低层控制实时性好视觉交给上位机通信调试成本高我们最终采用的是混合方案底层用单片机负责电机控制和编码器采集上层用一块支持OpenCV和轻量级推理的Linux小主机负责图像处理和碑文识别。这样既保证了转向、速度控制的实时性又不至于让单片机去承担复杂的视觉计算。如果你用的是一体化方案比如OpenMV或K210直接做主控也是可行的但要注意两个问题一是分辨率不能太高否则帧率上不去二是OCR模型基本跑不了更适合用模板匹配或特征匹配来做碑文识别。2.2 软件分层结构软件部分我们按照“硬件驱动 - 图像处理 - 决策控制 - 任务调度”四层来设计。任务调度层状态机、任务优先级、计时管理 决策控制层PID控制、速度规划、状态切换逻辑 图像处理层图像预处理、赛道中线提取、碑文检测、OCR识别 硬件驱动层摄像头采集、编码器读取、电机PWM输出、串口通信每一层只依赖下一层提供的接口不跨层调用。比如图像处理层不直接控制电机而是把“当前赛道偏差”和“是否检测到碑文”这两个结果交给决策控制层。这样做的最大好处是后续换摄像头、换主控、换电机驱动都只需要改对应层的代码不需要重写整个工程。2.3 数据流与任务时序比赛过程中的数据流大概是这样的摄像头采集一帧图像图像处理层执行透视变换、二值化提取赛道中线计算偏差如果当前处于“碑文识别”状态则从图像中裁剪碑文区域送入识别模块识别结果与当前里程或计时信息绑定存入结果队列控制层根据偏差和目标速度计算PWM输出到达终点后从结果队列中按顺序导出识别内容。这里需要注意的是碑文识别不能占用完整的一帧时间。如果一帧图像处理耗时50ms而车速是2m/s那车辆每帧之间已经前进了10cm这会导致识别区域对齐出现偏差。所以我们在设计时采用了“异步识别”的思路图像采集和循迹是同步的碑文识别则放在单独的线程或分时任务中处理必要的时候让小车在碑前轻微减速以保证成像质量。3. 感知方案如何又快又稳地“看见”碑文3.1 传感器选型思路传感器的选择要围绕三个问题展开目标是什么、需要多大的视场角、在什么光照条件下工作。走马观碑赛题里车要同时完成两件事看赛道、看碑。这两个任务的视场需求是不同的看赛道需要较大的视场角看到近处的赛道边缘和中远处的弯道趋势看碑需要较高的分辨率碑文细节要清晰。如果你只有一个摄像头那就必须在视场角和分辨率之间做折中。我们的做法是把摄像头安装在车头部较高的位置俯仰角稍微下压让画面同时覆盖近处赛道和前方略远处的碑。如果预算允许也可以做成双摄像头方案一个广角看路一个长焦看碑但两个摄像头的坐标标定会麻烦一些。对于摄像头类型普通USB摄像头在室外强光下表现一般最好选择带自动曝光且支持手动调节曝光时间的型号。灰度摄像头的优势是处理速度快但在碑文识别上很难区分彩色内容所以除非你确定碑文只有黑白两色否则还是建议使用彩色摄像头。3.2 赛道路线识别赛道识别是智能车的基础功能走马观碑赛题里也不例外。我们使用的赛道方案是经典的“图像二值化 中线提取”。基本思路如下将BGR图像转换为灰度图根据赛道颜色与背景颜色的差异使用阈值分割得到赛道区域对二值图像做形态学处理去除噪声按行扫描查找赛道左右边界计算每一行的赛道中心点形成中线对中线做平滑处理得到平滑的路径曲线。如果赛道与背景对比度不高比如浅色路面加浅色赛道可以使用HSV颜色空间做颜色提取效果会比灰度二值化更稳定。在室外场景中光照变化是二值化最大的敌人。同一个阈值在上午和下午、阴影区和阳光直射区表现完全不同。解决思路有两种动态阈值每次取图像中感兴趣区域的灰度直方图通过大津法OTSU计算自适应阈值色彩空间转换在HSV中锁定颜色的H分量范围减少亮度影响。我们采用了第二种思路并在H分量基础上叠加S分量的过滤效果比单纯灰度二值化好了很多。3.3 碑文检测与识别碑文检测是“走马观碑”赛题与普通循迹最大的区别点。它的目标是在行驶过程中发现画面里的“碑”并定位碑文区域。我们的检测策略分两个阶段第一阶段是“粗检测”。利用碑的外形特点比如矩形边框、颜色统一、尺寸较大用颜色过滤和轮廓提取的方式快速定位候选区域。这样做的好处是不需要标注大量训练数据也不依赖高算力模型。第二阶段是“精识别”。对候选区域做透视矫正把倾斜的碑面拉正然后送入文字识别模块。关于碑文识别要看你实际的碑文内容类型如果是印刷体汉字可以使用轻量级OCR方案比如PaddleOCR的mobile版本、Tesseract等如果是自定义符号或固定模板字符模板匹配反而更稳定更快如果碑文是字母和数字还可以训练一个简单的CNN分类器。我们的场景里碑文以汉字为主所以用了PaddleOCR的移动端模型。需要提醒的是OCR模型在树莓派这类设备上运行速度有限实际使用时要控制输入图片的尺寸不能直接把整幅图像送进去而是只裁剪碑文区域后送入识别。3.4 光照与运动模糊处理室外比赛最难受的两个问题强光过曝和运动模糊。针对过曝优先调整摄像头曝光参数而不是依赖算法。在比赛开始前对场地光照做一次采样把固定曝光时间设置好。如果场地光照变化大则使用自动曝光但限制最大曝光时间避免高亮区域完全白掉。针对运动模糊常用的方法有降速拍摄检测到碑文区域后让车辆适当减速在低速状态下完成拍摄缩短曝光时间提高传感器帧率降低单帧曝光时间多帧连续拍摄连续抓取多张图像选清晰度最高的一张进行识别图像去模糊使用拉普拉斯算子计算图像清晰度对模糊帧直接丢弃。我们在实际调试中发现最简单有效的组合是“检测到碑后提前降速 连续抓三帧选择清晰度最高帧”识别率提升非常明显。4. 控制策略让车在高速下保持稳定4.1 赛道中线提取与偏差计算控制层不需要处理整幅图像只需要一个数值当前车辆相对赛道中线的横向偏差。这个偏差的计算方法如下def calc_deviation(midline, img_width): # 取图像底部若干行的中线点计算平均位置 rows len(midline) bottom_rows midline[int(rows * 0.7):] valid_points [p for p in bottom_rows if p is not None] if not valid_points: return None avg_x sum(valid_points) / len(valid_points) deviation avg_x - img_width / 2 return deviation偏差值的正负代表车辆偏左还是偏右。将这个偏差输入给PID控制器就能输出方向盘的转角或后轮的差速值。这里有个细节不要只在图像底部取一行算偏差因为底部信息太近车辆转向会非常急促出现“画龙”现象。取底部70%区域的中线平均值相当于同时看到了近处和远处减少了转向抖动。4.2 转向与速度PID我们使用了两套PID转向PID和速度PID。转向PID负责根据偏差调整方向速度PID负责根据当前赛道情况调整目标速度。这里是一个典型的转向PID代码// 文件路径src/control/steer_pid.c typedef struct { float kp; float ki; float kd; float integral; float prev_error; float output_max; } PidController; float pid_update(PidController *pid, float error, float dt) { pid-integral error * dt; float derivative (error - pid-prev_error) / dt; float output pid-kp * error pid-ki * pid-integral pid-kd * derivative; pid-prev_error error; if (output pid-output_max) { output pid-output_max; } else if (output -pid-output_max) { output -pid-output_max; } return output; }速度PID和转向PID结构相同但输入变成了“目标速度与当前速度的误差”。在实际调参过程中我们遵循先调Kp、再调Kd、最后加Ki的顺序。Ki一般用得很少因为赛道不会像倒立摆那样存在持续稳态误差加太多反而会引起低频摆动。4.3 状态机调度比赛流程需要用一个简单的状态机来管理START - RUN_TRACK - DETECT_BEI - RECOGNIZE - RUN_TRACK - ... - FINISH状态机的作用是把“循迹”“检测碑”“识别碑文”这三件事按优先级排好避免互相抢占资源。实际代码中我们把状态定义成枚举类型主循环每一帧根据当前状态执行对应的处理函数。在“检测碑”状态下车辆会降低目标速度同时开启多帧抓拍在“识别碑”状态下车辆会继续保持低速直到识别线程返回结果或超时。这里的关键是“超时保护”如果OCR一直不返回车不能一直停在原地必须设置一个最大等待时间超时后切换到下一状态继续行驶。5. 核心代码实现5.1 图像预处理与赛道中线提取下面是一段基于OpenCV的赛道中线提取代码思路是HSV颜色过滤加轮廓提取。在实际项目中你可能需要根据赛道颜色调整HSV阈值。# 文件路径src/vision/lane_detect.py import cv2 import numpy as np def detect_lane_midline(frame, color_range): 输入原始BGR帧返回每一行的赛道中线x坐标列表。 color_range: ((H_low, S_low, V_low), (H_high, S_high, V_high)) hsv cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask cv2.inRange(hsv, color_range[0], color_range[1]) # 形态学开运算去除噪声点 kernel np.ones((5, 5), np.uint8) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 按行扫描找到赛道边界 height, width mask.shape[:2] midline [None] * height for row in range(height): row_data mask[row, :] indices np.where(row_data 255)[0] if len(indices) 0: continue left indices[0] right indices[-1] # 如果左右边界太近说明这一行可能是噪声跳过 if right - left 20: continue midline[row] int((left right) / 2) return midline这段代码的关键点在于用HSV比用灰度二值化更抗光照干扰形态学处理可以去掉小噪点也能补全断裂的赛道区域对每一行单独扫描而不是整体找轮廓是因为我们真正需要的是“纵向的赛道中心线”而不是赛道外接矩形。5.2 碑文检测与识别碑文识别模块我们分成“候选区域提取”和“文字识别”两步。候选区域提取的代码思路如下# 文件路径src/vision/bei_detect.py import cv2 import numpy as np def find_bei_candidates(frame, bei_color_range): hsv cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask cv2.inRange(hsv, bei_color_range[0], bei_color_range[1]) # 找外轮廓 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) candidates [] for cnt in contours: area cv2.contourArea(cnt) if area 500: continue x, y, w, h cv2.boundingRect(cnt) # 过滤长宽比明显不像碑的矩形 if h 30 or w 30: continue candidates.append((x, y, w, h)) return candidates拿到候选区域后下一步是透视矫正。由于车辆在行驶中碑面可能有一定的倾斜角度直接做OCR识别效果不好。我们可以通过四角点检测把碑面拉正# 文件路径src/vision/bei_correct.py import cv2 import numpy as np def perspective_correct(frame, rect_points): rect_points: 4个角点顺序为左上、右上、右下、左下 pts_src np.array(rect_points, dtypenp.float32) width 200 height 100 pts_dst np.array([[0, 0], [width - 1, 0], [width - 1, height - 1], [0, height - 1]], dtypenp.float32) matrix cv2.getPerspectiveTransform(pts_src, pts_dst) result cv2.warpPerspective(frame, matrix, (width, height)) return result透视矫正之后就可以把结果图缩放成适合OCR的尺寸再送入识别模块。我们使用的识别模块是基于PaddleOCR的但因为它依赖较多没有放在车端完整部署而是通过RPC或者串口把裁剪后的图像发给上位机识别。如果你的车端算力足够也可以直接调用OCR模型进行推理。5.3 状态机与主循环主循环是整车程序的“心脏”。每一帧图像进来后依次执行采集、赛道检测、状态更新、控制输出。下面是一个简化版的主循环逻辑# 文件路径src/main_loop.py import cv2 from vision.lane_detect import detect_lane_midline from vision.bei_detect import find_bei_candidates from vision.bei_correct import perspective_correct from control.steer_pid import PidController class CarState: SEARCH_TRACK 0 RUN_TRACK 1 DETECT_BEI 2 RECOGNIZE 3 FINISH 4 def main(): cap cv2.VideoCapture(0) steer_pid PidController(0.0, 0.0, 0.0, 0.0, 0.0, 1.0) state CarState.SEARCH_TRACK result_queue [] while True: ret, frame cap.read() if not ret: continue # 第一步提取赛道中线 midline detect_lane_midline(frame, lane_color_range) deviation calc_deviation(midline, frame.shape[1]) # 第二步状态机处理 if state CarState.RUN_TRACK: if deviation is not None: steer_output steer_pid.update(deviation, dt) set_steer(steer_output) # 检查是否进入碑文区域 candidates find_bei_candidates(frame, bei_color_range) if candidates: state CarState.DETECT_BEI elif state CarState.DETECT_BEI: # 降速准备抓拍 set_speed(0.8) if candidates: correct_img perspective_correct(frame, candidate_points) text ocr_recognize(correct_img) if text: result_queue.append(text) state CarState.RUN_TRACK # 检测终点条件 if reach_finish_line(): state CarState.FINISH break cap.release() print(识别结果, result_queue)这段代码省略了部分硬件控制函数但整体流程已经体现出来。实际工程中你还需要考虑线程安全因为识别过程可能比较耗时不能在主循环里阻塞等待可以引入一个单独的识别线程。5.4 上位机与联调除了车端代码我们还在PC端写了一个简单的串口调试上位机用来实时查看车辆状态和识别结果。这个上位机不是必须的但它能在现场调试时帮你节约大量时间。上位机的主要功能显示摄像头原始画面和处理后的二值化图像实时显示赛道偏差、当前速度、状态机状态显示识别到的碑文和置信度保存日志方便赛后回溯问题。我们使用的是Python PyQt5 OpenCV开发串口通信用pyserial。这里不贴完整代码了因为界面布局部分和你的实际需求关联较大核心思路是车端通过串口按照固定协议周期发送JSON字符串上位机收到后解析并刷新界面。{speed: 1.5, steer: 0.2, state: 3, ocr_text: 智能车, confidence: 0.92}6. 现场踩坑与排查复盘6.1 常见问题汇总问题现象常见原因解决思路识别成功率低运动模糊或曝光问题降速拍摄、缩短曝光时间、连续抓帧选清晰帧赛道中线频繁跳变光照变化导致二值化阈值失效改用HSV颜色过滤或使用自适应阈值车辆转弯抖动明显PID的Kd过大或中线提取过近减小Kd取图像70%区域的中线到达碑前来不及减速检测距离太近抬高摄像头俯仰角扩大检测范围OCR识别速度太慢输入图片尺寸过大裁剪后缩放限制最长边识别结果顺序错乱状态机时间戳没有对齐为每个识别结果绑定里程或帧号车在强光下丢失赛道镜头逆光画面过曝调整摄像头安装角度加遮光罩6.2 典型案例分析第一类典型案例是运动模糊导致的识别率暴跌。我们一开始在场地测试时车辆以1.5m/s的速度通过碑文区域识别率只有60%左右很多汉字被识别成错字。排查后发现主要原因是曝光时间太长车辆前进时图像拖影严重。后来我们做了两个改动将曝光时间从10ms降到3ms检测到碑文区域后目标速度从1.5m/s降到0.8m/s。改动后识别率提升到90%以上。这个过程让我们意识到识别率不只是一个算法问题它和车速、曝光参数紧密耦合必须整体调试。第二类典型案例是赛道中线“画龙”。最初我们只取了图像最底部一行作为偏差车辆转向特别急直线都走不稳。后来把偏差改为底部70%区域的平均中线位置并增加了一阶低通滤波转向立刻平顺了很多。第三类案例是状态机卡死。有一次在场地测试时车辆检测到碑文区域后进入识别状态但OCR迟迟没有返回结果车辆直接停在原地不动。原因是我们没有设置超时保护。后来在状态机中加入了“最大等待时间”超过2秒就强制切换回循迹状态才彻底解决。6.3 排查清单如果你在调试过程中遇到问题可以按以下顺序快速定位先看图像把摄像头画面、二值化画面输出到屏幕上确认算法当前“看到”了什么再看数据检查偏差值、PID输出、当前状态是否在合理范围再看日志查看OCR结果、置信度、帧号和状态切换记录最后改参数调整速度、曝光、PID参数每次只改一个变量跑回放录制现场视频和串口日志回放定位问题发生时刻。这个排查顺序非常重要。很多同学一出问题就去改代码参数但实际上根源可能是摄像头曝光或者传感器安装位置的问题。先确认输入数据再改逻辑才不容易陷入“调参死循环”。7. 工程最佳实践与改进方向7.1 开发流程建议我们在整个备赛过程中最大的经验是不要直接上整车调试一定要分模块验证。推荐流程如下先在PC端用录制好的比赛场景视频测试图像处理算法把识别模块单独做成命令行工具用一批离线图片测试识别率再把图像处理和控制逻辑放到车端先用低速测试最后逐步提高车速加入状态机切换和完整比赛流程。每一步验证通过后再进入下一步能减少大量“算法没问题、控制出问题”的混淆场景。在代码管理上建议从一开始就使用版本控制工具比如Git。比赛代码迭代速度非常快可能上午刚调好的参数下午改了个代码就全乱了。有了版本控制随时能回退效率会高很多。7.2 鲁棒性优化建议针对走马观碑赛题以下几个鲁棒性优化点值得关注ROI区域限制赛道中线提取只在图像下半部分做碑文检测只在预期高度范围做减少无关区域的干扰低通滤波对偏差值做一阶低通滤波减少单帧噪声带来的抖动多帧投票碑文识别结果做多帧投票避免单帧误识别导致结果错误数据回传每帧图像在车上尽量压缩后保存下来赛后可以离线分析失败原因。另外硬件层面的稳定性不能忽视。室外比赛中电池电压波动会影响摄像头曝光和电机输出建议在电源部分做好稳压并给摄像头单独供电。车辆线束要用扎带固定好否则行驶过程中线束松动摄像头画面会出现瞬时黑屏或花屏。7.3 下一步改进方向如果继续在这个赛题上深入有几个方向值得探索端到端深度学习方案用轻量级目标检测模型替代传统的颜色轮廓检测泛化能力更好多传感器融合在视觉基础上加入IMU和编码器对车辆位姿进行实时估计即使视觉短暂丢失也能保持轨迹更完整的OCR流程接入更大规模的OCR模型或者针对比赛字体微调一个轻量分类器提高特定碑文的识别率路径规划不止停留在循迹层面而是结合已知地图做全局规划。这些方向都需要一定的算法和硬件基础但每一块都能显著提升整车能力。如果你准备下一届继续打这个赛题可以考虑提前做技术储备。最后想说智能车比赛的最终成绩固然重要但真正让你长期受益的其实是这套“拆解任务 - 分模块实现 - 整车联调 - 复盘迭代”的工程方法。赛后回头看那些调参的夜晚踩过的每一个坑都是比奖项更值钱的东西。希望这份复盘能帮你少走一些弯路祝下一届备赛顺利。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。