ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

工程机器人视觉系统源码拆解:识别、决策与串口调优

工程机器人视觉系统源码拆解:识别、决策与串口调优

简介:RoboMaster 2022 思玄机器人队工程机器人视觉系统源码,基于 Python 编写,面向机器人竞赛工程师、计算机视觉学习者及 RoboMaster 参赛队伍,聚焦工程机器人视觉感知与自主决策问题,覆盖从图像采集、预处理到目标检测、追踪与控制指令输出的完整链路。压缩包共 33 个文件,含 16 个 Python 脚本、8 张 PNG 图片、6 张 JPG 图片、1 份 JSON 配置、1 份许可证与 1 份说明文档,总大小仅 2.31MB。已有 164 人学习,适合想系统掌握 RoboMaster 视觉系统设计思路的开发者参考。源码完整覆盖图像预处理、特征检测与识别、目标追踪、深度学习分类、位姿估计与修正、通信控制等模块,目录结构清晰,并配有流程图、模板图片和自动调试脚本。从代码可学习如何将视觉信息转换为实际动作指令,也能了解竞赛机器人的系统分工与工程落地细节,适合用于备赛复盘、课程设计与算法验证,是一份可运行、可扩展的实战参考资料。

1. 一句话说清这套源码的用途:工程机器人的视觉系统到底管什么

拿到“RoboMaster2022 思玄机器人队 工程机器人视觉系统源码.zip”这个压缩包,第一反应别把它当成步兵或者英雄机器人的视觉代码。工程机器人在 RoboMaster 赛场上干的活不是“看见敌人就突突”,而是要在场地里找矿石、抢补弹点、救援队友,视觉系统负责把这些目标的像素坐标准确告诉电控和机械臂,让底盘和爪子在正确的时间伸向正确的位置。这个源码包的真正价值,是把“相机画面”转成“机械动作”的完整链路。适合电控、视觉、机械三个组的成员互相读,也适合刚入坑 RoboMaster 视觉的新手拿来做第一版状态机的参考。需要先说破的是:工程机器人视觉的性能瓶颈往往不在识别算法准不准,而在相机曝光和串口延迟这种看起来很基础的环节上,后面会逐一拆开。

2. 视觉系统架构拆解:图像采集、识别、串口与决策四层怎么分工

工程机器人视觉系统的代码,不管用什么语言写,最后几乎都会演化成四层结构:图像采集层、识别处理层、决策状态层、串口通信层。把这四层分清楚,比一开始就埋头调阈值重要得多。常见的一个坑是所有人把代码堆在一个 main 循环里,最后改一个参数要全局找,跑起来还不知道数据在哪一步丢了。我一般会在源码里这样组织:采集层单独一个文件负责出帧,识别层只接收图像返回目标列表,决策层维护一个由比赛阶段和任务优先级驱动的状态机,串口层只管按协议把结果发出去,不关心图像长什么样。

2.1 工程机器人的视觉任务列表与优先级

工程机器人在 2022 赛季里要管的事不少,最常见的有:识别场地中央的金矿和银矿、识别敌方机器人(用于躲避或警示)、识别救援区内的队友、识别己方补弹点的弹仓。这些任务不能同时跑,必须定优先级。工程机器人得分的大头是矿石兑换,所以“找矿石”的优先级永远最高;其次是救援区识别,因为救援成功也能拿分且在后期很关键;识别敌方机器人往往只是为了触发一个“注意规避”的信号,不需要特别精确。因此视觉源码里通常存在一个任务优先级枚举,状态机先看当前比赛阶段,再决定这一帧调哪一个识别函数。我见过有的队把五六个识别全跑一遍然后投票,结果帧率掉到 15,机械臂抓矿时动作一顿一顿,这就是没做优先级管理。

2.2 相机选型与图像采集:为什么工程机器人偏爱高帧率全局快门

工程机器人移动速度不算快,但机械臂抓取矿石时对延迟极其敏感。视觉从“看到目标”到“机械臂落地”超过 50ms,矿石就可能被对手先抢走。所以相机选择上,工程机器人通常不追求高分辨率,反而更看重帧率和快门方式。全局快门能避免运动物体在画面里产生果冻感,抓取瞬间尤其重要。实际比赛里大量队伍用 640x480 分辨率跑 60fps,甚至 320x240 跑 120fps,因为识别目标颜色块根本不需要高分辨率,多出来的像素只会拖慢处理速度。这个源码包的采集层通常就是基于 OpenCV 的 VideoCapture 做封装,一个最小的采集发送循环长这样:

import cv2 import serial import time # 初始化相机,项目里常见的是USB3.0工业相机,枚举设备号一般是0或1 cap = cv2.VideoCapture(0, cv2.CAP_V4L2) cap.set(cv2.CAP_PROP_FPS, 60) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) # 串口初始化,波特率常见115200,要和下位机一致 ser = serial.Serial('/dev/ttyUSB0', 115200, timeout=0.1) while True: ret, frame = cap.read() if not ret: continue # 这里本应由识别层返回目标坐标,现在写死一个中间值做示例 x, y = 320, 240 # 构造一个简单串口帧:帧头 + 命令字 + 坐标拆包 data = bytes([0xAA, 0x01, x & 0xFF, (x >> 8) & 0xFF, y & 0xFF, (y >> 8) & 0xFF]) ser.write(data) time.sleep(0.001)

这段代码的逻辑很简单:每帧读图,然后直接把一个固定坐标发给下位机。实际源码不会这么粗糙,但它的框架是对的。参数说明里要留意的有:CAP_V4L2是 Linux 下常用后端,如果换成 Windows 或者 Jetson 上没装驱动,会采集失败;FPS 设置 60 是理想值,不少相机实际跑不到,采集层应该做自适应——看read()返回的实际时间戳间隔,而不是像这里靠sleep硬撑。坐标拆包用了两个字节,所以单维坐标最大能表示 65535,对 640 宽的画面来说完全够用,但如果你换成 1920 分辨率且不改拆包逻辑,高位字节就溢出了,这是很多人会踩的坑。

2.3 识别输出到机械动作:串口协议的帧设计与心跳包

视觉端算出的坐标最终要通过串口传到电控板,帧协议就是双方约定的“黑匣子”。一个可用的帧格式至少包含:帧头、数据长度、命令字、数据载荷、校验和、帧尾。帧头常用两字节固定值,比如0xA5 0x5A,用来让下位机在连续字节流里找对齐;校验最省事的是累加和,把帧头之后所有字节求和取低 8 位,虽然防不了突发干扰,但足够在比赛场景里揪出大部分丢包。心跳包一定要加:电控需要知道视觉程序是不是还活着。哪怕当前没有目标,也要每隔 100ms 发一帧“无目标”的命令,否则电控侧会因超时自动进入失控保护。下面这张表是我们队常用的帧字段设计,可以直接套用:

字段域字节数值/说明
帧头20xA5 0x5A
数据长度1从命令字到校验前的总字节数
命令字10x01 识别矿,0x02 识别敌人,0x10 无目标
数据载荷变长目标坐标、类别、置信度
校验和1帧头后所有字节累加和
帧尾20x0D 0x0A

这段协议在源码里一般会被封装成build_frame(cmd, data)和check_frame(buffer)两个函数,前者负责打包,后者在下位机收到后验证。要注意的是,串口波特率决定了字节传输速率,115200 波特率下传 10 个字节约 1ms,看起来很快,但电控侧中断处理、协议解析也会占用时间,实际端到端延迟不是算出来的,必须用示波器或者串口回环实测。协议里一定要保留命令字扩展空间,比赛前最怕加新功能时发现没有预留位,导致整个帧格式要重设计,这就是没有后悔药的环节。

3. 识别算法选型与实现:颜色阈值、轮廓筛选和圆检测的边界在哪

工程机器人识别矿石这件事,算法选型直接决定了你在赛前最后一天是睡个好觉还是通宵改参数。这一章讲讲传统视觉方案的实现路径和边界,以及为什么它至今仍是 RoboMaster 视觉的主流派做法。

3.1 为什么工程机器人不用深度学习而用传统视觉

Jetson Nano 或者树莓派上跑深度学习模型不是不行,但工程机器人的目标识别有特殊性:场地里的金矿和银矿颜色非常固定,形状也很规整,传统视觉用 HSV 颜色阈值加轮廓提取,就能拿到稳定结果。深度学习模型需要标注数据、训练时间,而且模型在比赛场地光照变化下同样会失效,一旦失效你很难快速解释是哪个批次的问题。传统视觉的好处是可解释性强:识别率低了,直接把二值化中间结果打印出来看,一眼就知道是阈值漂了还是噪点太多。当然这不是说深度学习一无是处,如果要识别敌方机器人那种姿态多变的物体,或者要在复杂背景里找特定图案,轻量级模型比如 MobileNet-SSD 是值得考虑的选择。但工程机器人的核心任务里,传统视觉的性价比更高,这也是很多源码包把矿石识别写成纯 OpenCV 的原因。

3.2 HSV颜色阈值:一套可复用的矿石识别流程

OpenCV 里 HSV 的 H 范围是 0 到 180,S 和 V 是 0 到 255,这和网上很多教程直接搬 0 到 360 不一样,一个典型翻车点就是用了 Photoshop 的参数导致结果空白。工程机器人场地里的金矿通常呈黄色,在 HSV 空间中 H 大约在 20 到 40;银矿接近灰白色,S 很低,V 偏高。识别流程一般是:BGR 转 HSV,用inRange生成二值掩膜,然后用形态学操作去掉细小噪点和空洞,最后找轮廓。代码实现很直接:

import cv2 import numpy as np def find_yellow_ore(frame): # BGR转HSV,OpenCV的H范围是0-180 hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) # 黄色阈值,H在20-40之间,S和V取较高值以排除暗角 lower = np.array([20, 80, 80]) upper = np.array([40, 255, 255]) mask = cv2.inRange(hsv, lower, upper) # 开运算去噪,闭运算补洞 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) # 只检测外轮廓,减少内嵌边缘干扰 contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) detections = [] for cnt in contours: area = cv2.contourArea(cnt) if area < 500: continue x, y, w, h = cv2.boundingRect(cnt) detections.append((x + w // 2, y + h // 2)) return detections

这里有几个参数要解释一下:lower里的 S 和 V 下限设成 80,是为了把纯白和高光区域排除在外,否则反光点会被当成矿石;面积阈值 500 是经验值,在 640x480 画面里矿石占几百到几千像素,低于这个面积的通常是被照射出的杂质或者远处误报。形态学 kernel 大小也得跟着分辨率走,如果改成 1920x1080,kernel 和面积阈值都应该翻倍,否则识别结果会明显变碎。

这个方法最大的坑是 HSV 阈值不是一组走天下的。上午十点的自然光、中午的顶棚灯、傍晚的偏暖灯光,会让同一块黄色矿石在 HSV 空间里漂移很大。更稳的做法是把 V 通道的影响去掉,只靠 H 和 S 建立掩膜,或者改用 LAB 颜色空间,只取 AB 通道做判定,因为 LAB 的色度通道更能抗亮度变化。源码包里如果只有一套固定阈值,那它大概率只适配了当时测试场地的光线,你拿回来第一件事应该是重新标定。

3.3 轮廓筛选与最小外接圆:处理重叠与遮挡的两种策略

矿石在场地里经常挤成一堆,多块金属叠在一起时轮廓会连成一片,单纯用外接矩形会给出一个错误的中心坐标。这时候要分两种处理思路。第一种是轮廓筛选:用面积、长宽比、圆形度过滤掉不合理候选。圆形度公式是常见的4 * pi * area / (perimeter^2),圆形度接近 1 的候选更可能是完整矿石,而两个矿石叠出的融合轮廓通常呈哑铃形,圆形度明显下降。第二种是找最小外接圆:遇到轮廓粘连严重时,用cv2.minEnclosingCircle(cnt)获得最小外接圆,这个圆的圆心比外接矩形中心更接近真正矿石的重心。

实际代码里,我会对每个轮廓同时算外接矩形和最小外接圆,然后用一个规则投票:如果轮廓面积与圆面积之比在 0.7 到 1.2 之间,就认为这个轮廓是单个矿石,取圆心做目标;否则判定为重叠,暂时不输出或者取最大凸包的中心。这种保守策略在抢矿阶段很有用,因为与其给一个错误坐标让机械臂扑空,不如少发一发目标,等运动模糊过去再识别。如果源码包里有“目标丢失后保留最后坐标 N 帧”的逻辑,就是对这个场景的处理。

4. 源码里最该调的三组参数:曝光、ROI与串口波特率

很多队伍把时间花在调颜色阈值上,但阈值再准,曝光乱了全白搭。这一章直接列出工程机器人视觉源码里最有杠杆效应的三组参数,每一组都能让识别效果产生质变。

4.1 相机曝光与增益的联动关系:自动曝光的坑

工业相机和普通摄像头都有曝光时间、增益、白平衡三个参数。曝光时间决定进光量,增益决定信号放大倍数,白平衡决定色偏。工程机器人视觉里最常见的错误是开着自动曝光打比赛,然后发现画面亮度一直在抽风。自动曝光对前景的暗色物体和背景亮区会很敏感,机械臂伸进视野时画面立刻变暗,矿石识别直接雪崩。正确做法是赛前两天固定在目标场地手动调好曝光,并且把自动白平衡也关掉。如果你的相机 SDK 支持设置最大曝光上限,也要写上,比如在 V4L2 里可以这么设:

# 关闭自动曝光并固定曝光值,V4L2控制参数 v4l2-ctl -d /dev/video0 -c exposure_auto=1 v4l2-ctl -d /dev/video0 -c exposure_absolute=120 v4l2-ctl -d /dev/video0 -c white_balance_auto=0

这组命令背后的逻辑是让相机处于完全手工状态,环境亮度变化全交给后续的阈值处理去适应。参数exposure_auto=1是关闭自动曝光,不同相机驱动值可能不同,有的用 0 或 3,必须查自己的 SDK;exposure_absolute=120的单位是微秒级的抽象值,不是直接秒数,需要在实际场地里拍几张看效果。增益参数也要跟着调,原则是优先加大曝光而不是增益,因为增益会放大噪声,让颜色阈值边缘变得毛糙。这个环节属于“血泪经验”:我见过整队赛前一天因为自动白平衡失效而在现场手忙脚乱调阈值的,最后只能靠损失分辨率换稳定性。

4.2 动态ROI:把算力用在刀刃上的区域裁剪策略

工程机器人抓矿时,目标大概率出现在图像中下部;识别敌方机器人时,目标可能在视野边缘。如果每一帧都对全分辨率做颜色转换和轮廓查找,浪费的算力就太多了。动态 ROI 的意义在于:上一帧找到目标后,下一帧只在上一个坐标附近的一个小窗口里寻找,目标跟丢时再逐渐扩大搜索范围。这样可以很轻松把处理帧率从 30 提到 60。一个简短实现:

last_center = None search_size = 100 def process_frame(frame, last_center): if last_center is None: roi = frame # 没有历史目标,只能全图找 else: x, y = last_center x1 = max(0, x - search_size) y1 = max(0, y - search_size) x2 = min(frame.shape[1], x + search_size) y2 = min(frame.shape[0], y + search_size) roi = frame[y1:y2, x1:x2] # 在roi内做颜色检测,返回结果时需要把roi的局部坐标转回全图坐标 mask = extract_mask(roi) centers = find_ore_centers(mask) if centers: last_center = (centers[0][0] + x1, centers[0][1] + y1) if last_center else centers[0] return last_center

这里的核心参数是search_size,它决定了目标丢失后找回的速度。设太大,ROI 约等于全图,失去加速意义;设太小,目标稍微被遮挡一下就找不到,需要配合“连续 N 帧未找到则扩大搜索”的策略。search_size 一般取上一目标运动的像素最大速度乘以处理间隔帧数,工程机器人抓矿时目标基本静止,设 80 到 120 像素就够。如果你用的是 320x240 分辨率,这个值要减半。另外要注意坐标转换:很多人直接在 ROI 图上返回中心点,忘了加 ROI 原点偏移,导致机械臂越抓越偏,这是最典型的低级错误。

4.3 串口波特率与帧间延迟:数据多久到不了下位机就会翻车

串口波特率看似是电控的问题,但视觉端也要懂。115200 波特率是 RoboMaster 最常见的默认值,它的理论传输速率约 11.52 KB/s,一帧 10 字节的数据传完不到 1ms。但问题是视觉端发送频率不能无脑拉高。电控侧常用的串口中断是 FIFO 缓冲,如果视觉每 5ms 发一帧,而电控主循环每 10ms 才收一次,缓冲里积压的数据就会混在下一包解析里,轻则丢帧,重则解析出错误坐标。我见过的方案是视觉端将发送频率稳定在 30 到 50Hz,也就是每 20ms 到 33ms 发一帧,和电控的 50Hz 控制周期对齐。如果算法处理很快,也要自己节流,不能 fast 到什么程度就发什么程度。

延迟测量可以做在视觉端,用时间戳打印每帧的处理时间。这段代码可以放到主循环里做黑匣子记录:

import time prev = time.time() while True: ret, frame = cap.read() now = time.time() dt_ms = (now - prev) * 1000 prev = now if dt_ms > 33: # 超过33ms意味着实际帧率低于30fps,需要告警 print(f"Frame interval {dt_ms:.1f}ms")

这个输出会告诉你视觉链路本身有没有掉帧。如果出现连续告警,先查采集层有没有缓冲堆积,再查识别层有没有在每一帧创建大数组导致内存拷贝。注意打印本身也会占时间,所以只在调试模式下开,比赛时关掉。串口延迟还有一个隐蔽来源:USB 转串口芯片的驱动缓冲区默认 4096 字节,上位机write返回不代表数据已经到电控,如果下电控立刻读就可能读到旧数据。解决办法是在电控侧维护一个最近数据的时间戳,超过 100ms 没新数据直接丢弃旧坐标,不要用旧坐标驱动机械臂。

5. 常见问题与避坑排查:光线翻转、过曝与机械联动的5个现场

这一章是实战排障记录,每一条都来自真实比赛或调试现场。如果你遇到相似症状,照着“现象 → 原因 → 解决”的顺序走,能少走很多弯路。

5.1 傍晚光线变黄,矿石识别率骤降

现象:白天识别率稳在 90%,傍晚太阳西斜后,黄色矿石和场地胶垫的颜色混在一起,连轮廓都找不全。

原因:日落时阳光色温从冷白变成暖黄,相机白平衡还在自动状态,导致整个画面偏黄,原本设定的黄色阈值范围被稀释,H 值偏向 10 到 25 区间,而阈值下限是 20,所以一半像素被滤掉了。

解决:在赛前把相机的白平衡固定在晴天模式,并重新采集傍晚光线下的样本再次标定阈值。如果不想多维护两组阈值,用 LAB 颜色空间替代 HSV,只取 a、b 通道做掩膜,能显著降低亮度变化影响。这个改动通常半小时就能完成,收益却很直接。

5.2 相机过曝后画面全白,手动曝光也不管用

现象:机器人从室内调试区推到室外比赛场,画面瞬间全白,调曝光参数画面没有反应。

原因:一开自动曝光,相机自动把曝光时间推到了几毫秒,到了强光下直接过曝。手动调节时又因为程序里某处调用了cap.set但参数名写错,没有真正生效。

解决:先检查相机 SDK 提供的 V4L2 参数,用v4l2-ctl -l列全部支持项,确认曝光控制名称是exposure_auto还是exposure_auto_priority;手动模式要先禁用自动优先。另外在代码里加一行启动时强制写三遍cap.set,并把 set 的返回值打印出来,避免静默失败。物理上还可以在镜头前加偏振片,或者把遮阳罩做长一点,挡住直射光。

5.3 识别正常但机械臂抓不准:坐标变换与相机安装偏差

现象:视觉在屏幕上框出的矿石中心很正,机械臂下爪却偏离 3 厘米,而且偏移方向随矿石离相机远近变化。

原因:相机安装位置和机械臂基座不平行,相当于相机坐标系与机械臂坐标系之间有旋转和平移,我们只把像素坐标当成机械臂坐标用了。近处目标受旋转影响大,远处目标受平移影响大,所以误差看起来飘忽不定。

解决:先做相机畸变校准,用棋盘格拍十几张照片,调cv2.calibrateCamera拿到内参和畸变系数,再在源码里执行undistort或undistortPoints。然后做手眼标定,常见做法是在机械臂末端固定一个彩色标记,带相机走到多个位置,记录标记像素坐标和机械臂实际坐标,最后用cv2.estimateAffine2D拟合一个 2D 到 2D 的仿射变换。只要场地是平面,仿射变换足够用,不要一开始就上复杂的透视模型。

5.4 串口数据偶尔丢包,机械臂偶发抖动

现象:电控收到的坐标时有时无,机械臂运行中会突然抽搐一下,像是收到了一个异常大的坐标。

原因:帧协议里没有校验,或者上位机发送频率太高,电控串口缓冲溢出,把后半帧错当新帧头解析,于是合成一个离谱的坐标值。

解决:给帧加校验和并在电控侧做解析保护:校验失败直接丢弃整帧,不进入控制逻辑。发送频率降到 30 到 50Hz,并且电控侧增加“坐标超限保护”和“目标超时屏蔽”,比如坐标超出场地范围就保持上一帧值或停机。这套双保险能挡住 95% 的串口脏数据问题。

5.5 程序跑一会卡顿,内存持续上涨

现象:开机时帧率 60,跑二十分钟后掉到 30,再后来机械臂动作都有粘滞感。

原因:cv2.VideoCapture在部分 Linux 驱动下默认缓冲多帧,如果read()的处理速度跟不上采集速度,缓冲里堆了很多旧帧;或者识别层里每次把frame放进一个全局列表用于调试显示,没有及时pop。

解决:采集层开启小缓冲模式,设置cv2.CAP_PROP_BUFFERSIZE为 1,强制覆盖旧帧。调试用的帧列表只保留最近 5 帧,新进旧出。若内存还在涨,用psutil每 5 秒打印进程 RSS,对比内存峰值和当前值,定位到具体是哪个函数在分配大数组。这个问题不难修复,但如果你不盯内存曲线,它会在比赛最关键时刻突然发作。

6. 最后一个落地技巧:用连续帧率自检和标定板做赛前闭环验证

前面讲了半天,最后给你一个我每次比赛前必做的验证技巧:不依赖比赛场地,只用一张 A4 纸打印的棋盘格或 ArUco 标记,就能在家检测整套视觉链路的健康度。做法是在固定距离上挂一张标定板,让机器人在静止状态下对着它运行你的源码,观察视觉输出坐标是否稳定、帧率是否达标、串口收发是否有丢包。这个测试能提前暴露 80% 的软硬件衔接问题,而且不需要等场地空出来才能调。

我自己的习惯是写一个极简的连续帧率日志,在赛前热身时打开,每 100 帧记录一次平均处理时间,如果平均超过 20ms,就降低分辨率或缩小 ROI,直到稳定在 30fps 以上。最后再对着标定板中心点,发一个固定坐标给机械臂,看爪子是否落在标记中心,偏移超过 5mm 就重新标定外参。这套动作做完,我才会把状态机切到比赛模式。

用第一人称说的话,我原来也迷信“算法越高级越好”,直到有一次因为忘记关自动曝光,在淘汰赛第一场把工程机器人当步兵开出去,结果识别全花,整场只搬回两块矿石。那种翻车经历让我明白:视觉系统在 RoboMaster 里是“软硬咬合”的活,曝光、串口、坐标变换这些不起眼的参数才是真正的胜负手。希望这套思路能帮你在调试时少熬几个通宵,把时间留给真正有价值的策略优化。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进