视觉机器狗识别赛全解析:从环境搭建到状态机调度的可复现方案
简介本资源为2021中国工程机器人大赛暨国际公开赛RoboWork视觉机器人项目——视觉机器狗识别赛的赛用代码包面向参加大学生机器人竞赛的选手及指导教师提供一套可参考的完整参赛实现方案。压缩包共267个文件约21.03MB以107个Python源码、75个d6a动作文件、46张jpg图像及少量md、pdf、csv、npz、caffemodel等为主涵盖识别脚本、步态动作数据、模型权重与设计文档。已有111人学习下载。读者可从中获取视觉识别与机器狗运动控制的代码框架、赛题设计文档、步态与转向动作数据以及模型配置与调试思路便于对照复现、二次开发与赛前查漏补缺适合具备一定Python与机器人视觉基础的中高级参赛者。1. 视觉机器狗识别赛从赛题到可复现方案的完整拆解视觉机器狗识别赛这个赛项核心任务其实就一句话让一台四足机器人比赛里常叫“机器狗”在场地里自主找到目标物、识别它、然后做出规定动作。听起来简单但真正上手你会发现它同时压着三条线——运动控制、视觉识别、状态机调度。任何一条线崩了整场比赛就是零分。2021 年那届 RoboWork 的视觉机器狗赛场地里通常会有数字标靶、颜色块或者特定图案机器狗需要走到指定区域用摄像头对准目标识别出结果后触发动作比如叫一声、转个圈、亮个灯。赛用代码包一般包含主控逻辑、视觉识别模块、串口通信协议、以及一套调试用的上位机脚本。适合谁看如果你手上有 ROS 基础、玩过 OpenCV、或者正在准备类似机器人竞赛这篇能帮你把“赛用代码”拆成能跑、能改、能排错的工程结构。下面我按实际落地顺序从环境搭到调参把踩过的坑一个个摊开讲。2. 赛用代码包的目录结构与运行环境搭建拿到一个.zip赛用代码包第一件事不是急着python main.py而是先看清楚它到底依赖什么。很多队伍翻车就翻在“环境不对跑起来全是红字”。我一般会先解压然后花十分钟把目录树和requirements.txt过一遍。2.1 典型目录树与各模块职责一个能打的视觉机器狗赛用代码目录结构通常长这样不同队伍命名有差异但职责划分大同小异robowork_dog_vision/ ├── main.py # 主入口状态机调度 ├── config/ │ ├── camera.yaml # 摄像头参数分辨率、曝光、增益 │ ├── serial.yaml # 串口波特率、端口号 │ └── vision.yaml # 识别阈值、ROI 区域 ├── vision/ │ ├── detector.py # 目标检测/颜色识别核心 │ ├── preprocess.py # 去畸变、白平衡、滤波 │ └── tracker.py # 目标跟踪防止丢帧 ├── control/ │ ├── motion.py # 运动指令封装前进、转向、停止 │ └── state_machine.py # 状态流转搜索→对准→识别→动作 ├── comm/ │ └── serial_comm.py # 与下位机STM32/Arduino通信 ├── tools/ │ └── debug_view.py # 上位机可视化调试 └── requirements.txt这里的关键是config 和 vision 分离。很多新手把阈值硬编码在detector.py里结果换一个场地光照就全废。正确做法是所有可调参数走 YAML代码里只读配置。2.2 环境搭建Python 版本与 OpenCV 编译选项赛用代码最常见的是 Python OpenCV。但 OpenCV 的 pip 版本和源码编译版本在性能上差距很大尤其是cv2.dnn和cv2.CAP_V4L2相关接口。# 建议用 Python 3.8~3.10太新的版本某些 ROS 包会冲突 conda create -n dog_vision python3.9 -y conda activate dog_vision # 先装基础依赖 pip install numpy1.23.5 pyyaml pyserial # OpenCV 用 pip 装但注意如果要调用 CUDA 加速必须源码编译 pip install opencv-python4.5.5.64 opencv-contrib-python4.5.5.64逻辑说明opencv-contrib-python包含cv2.aruco、cv2.xfeatures2d等扩展模块视觉机器狗识别赛里常用 ArUco 码做定位所以 contrib 包必装。参数上numpy锁 1.23.x 是因为 1.24 之后移除了np.float等别名老代码会报AttributeError。提示如果你用的是 ARM 平台比如 Jetson Nano 或树莓派pip 装的 OpenCV 没有 NEON 优化帧率可能只有源码编译的一半。建议用cmake -D WITH_NEONON自己编一次。2.3 串口权限与摄像头索引确认机器狗主控和视觉板之间通常走串口。Linux 下串口设备是/dev/ttyUSB0或/dev/ttyACM0但普通用户没权限读写。# 查看当前串口设备 ls /dev/ttyUSB* /dev/ttyACM* 2/dev/null # 把当前用户加入 dialout 组永久生效 sudo usermod -aG dialout $USER # 执行后需要重新登录或者临时用 sudo chmod 666 /dev/ttyUSB0 # 确认摄像头索引避免打开错误设备 v4l2-ctl --list-devices逻辑说明v4l2-ctl --list-devices会列出所有视频设备及其对应的/dev/videoX。很多工控机有两个摄像头接口一个接机器狗前置摄像头一个接调试用 USB 摄像头。代码里cv2.VideoCapture(0)的0就是索引写错就黑屏。参数上如果摄像头支持 MJPG 格式一定要在打开时设置否则默认 YUYV 会吃掉大量 USB 带宽导致帧率骤降。import cv2 cap cv2.VideoCapture(0) # 设置 MJPG 格式减少带宽占用 cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(M, J, P, G)) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FPS, 30) if not cap.isOpened(): raise RuntimeError(摄像头打开失败检查索引和权限)这段代码里CAP_PROP_FOURCC必须在设置分辨率之前调用否则某些摄像头驱动会忽略。FPS设 30 是保守值实际能跑多少要看光照和曝光时间。3. 视觉识别核心颜色阈值、轮廓筛选与目标定位视觉机器狗识别赛的识别对象通常是颜色块、数字、或者特定形状。赛用代码里最稳的方案不是深度学习而是HSV 颜色空间 轮廓分析。为什么因为比赛现场光照不可控深度学习模型如果没在类似光照下训练过泛化能力反而不如手工阈值调参来得直接。3.1 HSV 阈值怎么调从“玄学”到可复现RGB 对光照敏感HSV 把色调H、饱和度S、亮度V分开调阈值时逻辑清晰得多。但 HSV 的 H 通道在 OpenCV 里是 0~179不是 0~360这个坑每年都有人踩。import cv2 import numpy as np def detect_color(frame, lower, upper): frame: BGR 图像 lower/upper: HSV 下界和上界格式 [H, S, V] hsv cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask cv2.inRange(hsv, np.array(lower), np.array(upper)) # 形态学操作先开运算去噪再闭运算填洞 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (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) results [] for cnt in contours: area cv2.contourArea(cnt) if area 500: # 过滤小噪点 continue x, y, w, h cv2.boundingRect(cnt) # 宽高比筛选排除长条形干扰 aspect_ratio w / float(h) if 0.7 aspect_ratio 1.3: results.append((x, y, w, h, area)) return results, mask逻辑说明inRange生成二值掩膜后开运算先腐蚀后膨胀去掉孤立噪点闭运算先膨胀后腐蚀填补目标内部空洞。area 500是经验值640x480 分辨率下目标物至少占 500 像素才有稳定识别。宽高比 0.7~1.3 用来筛掉场地边线和长条形反光。参数怎么定我一般会写一个调试脚本用滑动条实时看 mask 效果# tools/debug_view.py 片段 cv2.namedWindow(trackbars) cv2.createTrackbar(H_min, trackbars, 0, 179, lambda x: None) cv2.createTrackbar(H_max, trackbars, 179, 179, lambda x: None) cv2.createTrackbar(S_min, trackbars, 0, 255, lambda x: None) cv2.createTrackbar(S_max, trackbars, 255, 255, lambda x: None) cv2.createTrackbar(V_min, trackbars, 0, 255, lambda x: None) cv2.createTrackbar(V_max, trackbars, 255, 255, lambda x: None)跑起来后把目标物放在场地不同位置记录下能稳定识别的 H/S/V 范围。血泪经验红色在 HSV 里跨越 0 和 179 两个区域需要分两段阈值再合并否则永远调不准。3.2 轮廓筛选与目标定位像素坐标转物理坐标识别到轮廓后下一步是把像素坐标转成机器狗能理解的物理坐标。赛用代码里通常用单目测距 几何映射因为场地是平面的摄像头高度固定。def pixel_to_world(px, py, cam_height, focal_length, img_center): px, py: 目标在图像中的像素坐标 cam_height: 摄像头离地高度米 focal_length: 焦距像素 img_center: 图像中心点 (cx, cy) # 相似三角形实际距离 高度 * 焦距 / 像素偏移 dx (px - img_center[0]) * cam_height / focal_length dy (py - img_center[1]) * cam_height / focal_length return dx, dy逻辑说明focal_length需要标定。简单做法是把目标物放在已知距离D处测量它在图像中的像素宽度w则focal_length w * D / 实际宽度。参数上cam_height要实测机器狗行走时会有俯仰所以最好加一个 IMU 补偿但赛用代码为了简洁通常假设地面平整。注意如果场地有反光地板HSV 阈值会飘。我一般会在镜头前加一个偏振片或者把曝光时间固定死关掉自动曝光。3.3 状态机调度搜索、对准、识别、动作四步走视觉识别不是每帧都跑而是由状态机驱动。赛用代码里典型的状态流转是状态触发条件动作退出条件SEARCH上电初始原地旋转摄像头扫描检测到目标轮廓ALIGN检测到目标调整机身朝向使目标居中目标在图像中心 ±50 像素RECOGNIZE对准完成连续 5 帧识别投票取众数识别结果稳定ACT识别完成执行规定动作叫、转、灯动作完成信号DONE动作完成停止等待裁判手动复位class StateMachine: def __init__(self): self.state SEARCH self.recognize_buffer [] def update(self, target_info): if self.state SEARCH: if target_info is not None: self.state ALIGN elif self.state ALIGN: if target_info and abs(target_info[cx] - 320) 50: self.state RECOGNIZE self.recognize_buffer.clear() elif self.state RECOGNIZE: if target_info: self.recognize_buffer.append(target_info[label]) if len(self.recognize_buffer) 5: # 投票取众数 label max(set(self.recognize_buffer), keyself.recognize_buffer.count) self.state ACT elif self.state ACT: # 发送动作指令 self.state DONE逻辑说明recognize_buffer存 5 帧结果用投票法抗单帧误识别。参数上±50像素的对准窗口不能太小否则机器狗会来回震荡也不能太大否则目标不在视野中心就触发识别容易受背景干扰。4. 避坑与排查视觉机器狗识别赛的 5 个典型翻车现场这一章全是血泪。每年比赛总有人栽在下面这几个坑里而且症状看起来都像“代码坏了”其实根因完全不同。4.1 现象识别框疯狂跳动目标明明没动原因自动曝光和自动白平衡在作祟。摄像头每帧都在重新计算曝光导致 HSV 值漂移阈值时灵时不灵。解决关掉自动模式手动锁死曝光和白平衡。# 用 v4l2-ctl 锁定曝光 v4l2-ctl -d /dev/video0 -c exposure_auto1 v4l2-ctl -d /dev/video0 -c exposure_absolute300 v4l2-ctl -d /dev/video0 -c white_balance_temperature_auto0 v4l2-ctl -d /dev/video0 -c white_balance_temperature4500参数上exposure_absolute的值要看场地亮度一般 200~500 之间。锁完之后HSV 阈值调一次就能管一整天。4.2 现象串口发送指令后机器狗延迟半秒才动原因串口缓冲区没清或者波特率不匹配导致重传。更隐蔽的是Python 的serial.write()默认是非阻塞的但下位机如果用了HAL_UART_Receive带超时就会攒够一批才处理。解决发送前清空输入输出缓冲区并加一个简单的帧头帧尾协议。import serial ser serial.Serial(/dev/ttyUSB0, 115200, timeout0.1) ser.reset_input_buffer() ser.reset_output_buffer() def send_cmd(cmd_id, param0): # 帧格式0xAA 0x55 | cmd_id | param | checksum frame bytearray([0xAA, 0x55, cmd_id, param]) checksum sum(frame) 0xFF frame.append(checksum) ser.write(frame) ser.flush() # 确保数据发出去逻辑说明reset_input_buffer()清掉下位机可能发来的残留数据flush()强制等待发送完成。校验和用最简单的累加和够用且计算快。4.3 现象识别率白天 90%下午 3 点后掉到 30%原因场地窗户在西边下午阳光斜射地面反光导致 V 通道整体抬高原本的阈值把背景也框进来了。解决不要只调 V 上限而是改用自适应阈值 颜色比例判断。或者更直接在vision.yaml里准备两套阈值按时间切换。# config/vision.yaml thresholds: morning: red: [[0, 80, 60], [10, 255, 255]] green: [[40, 80, 60], [80, 255, 255]] afternoon: red: [[0, 100, 100], [10, 255, 255]] green: [[40, 100, 100], [80, 255, 255]]参数上下午的 S 下限从 80 提到 100V 下限从 60 提到 100把反光背景滤掉。4.4 现象机器狗走到目标前 30cm 就停怎么都不识别原因摄像头的最近对焦距离不够。很多 USB 摄像头最近只能对焦到 50cm太近就糊了轮廓面积虽然大但边缘模糊findContours找不到闭合轮廓。解决换一个支持手动对焦的摄像头或者把摄像头安装角度调高让机器狗在 50cm 外就能看到目标。# 如果摄像头支持手动对焦用 v4l2 调 v4l2-ctl -d /dev/video0 -c focus_auto0 v4l2-ctl -d /dev/video0 -c focus_absolute30 # 值越小对焦越近4.5 现象代码在笔记本上跑得好好的上机器狗就卡死原因ARM 平台和 x86 的浮点性能差异。cv2.morphologyEx在大核 ARM 上如果没开 NEON单帧耗时可能从 5ms 涨到 50ms状态机直接超时。解决用cv2.setUseOptimized(True)确认优化开启并降低形态学核大小。import cv2 print(cv2.useOptimized()) # 应该是 True # 如果 False检查 OpenCV 编译选项参数上把kernel从(5,5)降到(3,3)面积阈值从 500 降到 300能省一半时间。5. 进阶技巧用 ArUco 码做定位补偿与识别结果校验如果你已经把基础识别跑通了下一步我强烈建议加ArUco 码。为什么因为纯颜色识别在复杂背景下误识别率很难压到 1% 以下而 ArUco 码自带 ID 和姿态估计可以作为“地面真值”来校验颜色识别的结果。赛用代码里我一般会在场地角落贴 2~3 个 ArUco 码机器狗先识别码算出自己在场地中的绝对位置再去搜索目标。这样即使颜色识别偶尔飘了也能用位置信息做二次判断。5.1 ArUco 码生成与检测的最小代码import cv2 import numpy as np # 生成 ArUco 码DICT_4X4_50 是最常用的字典 aruco_dict cv2.aruco.Dictionary_get(cv2.aruco.DICT_4X4_50) marker_image cv2.aruco.drawMarker(aruco_dict, 0, 200) # ID0, 200x200 像素 cv2.imwrite(aruco_0.png, marker_image) # 检测 def detect_aruco(frame): gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) corners, ids, rejected cv2.aruco.detectMarkers(gray, aruco_dict) if ids is not None: # 估计姿态需要相机内参这里用近似值 rvecs, tvecs, _ cv2.aruco.estimatePoseSingleMarkers( corners, 0.05, camera_matrix, dist_coeffs) return corners, ids, tvecs return None, None, None逻辑说明drawMarker生成 200x200 的码图打印出来贴在场地角落。detectMarkers返回角点和 IDestimatePoseSingleMarkers返回旋转向量和平移向量。参数上0.05是码的实际边长米camera_matrix和dist_coeffs需要提前标定用棋盘格标一次就行。5.2 用 ArUco 位置做颜色识别的“后悔药”假设机器狗识别到一个红色块但不确定是不是目标。这时候查一下 ArUco 算出的当前位置如果机器狗在场地 A 区而目标应该出现在 B 区那这个红色块大概率是干扰。代码逻辑def validate_target(target, dog_position, expected_zone): target: 颜色识别结果 (x, y, w, h) dog_position: ArUco 算出的 (x, y) expected_zone: 目标应该出现的区域 [(x1,y1), (x2,y2)] if dog_position is None: return True # 没有 ArUco 信息跳过校验 x, y dog_position (x1, y1), (x2, y2) expected_zone if x1 x x2 and y1 y y2: return True return False这个校验能砍掉 80% 的误识别。代价是需要在场地布置 ArUco 码并且提前标定相机。标定代码# 棋盘格标定拍 15~20 张不同角度的棋盘格图 ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera( objpoints, imgpoints, gray.shape[::-1], None, None) np.save(camera_matrix.npy, mtx) np.save(dist_coeffs.npy, dist)5.3 帧率与延迟的平衡什么时候该降分辨率最后说一个我踩过的坑为了识别远距离目标我把分辨率从 640x480 提到 1280x720结果帧率从 30 掉到 12状态机在 ALIGN 状态因为更新太慢机器狗直接转过头。后来我改成双分辨率策略SEARCH 状态用 640x480 快速扫描ALIGN 和 RECOGNIZE 状态切到 1280x720 精定位。def set_resolution(cap, width, height): cap.set(cv2.CAP_PROP_FRAME_WIDTH, width) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height) # 切换分辨率后需要重新设置 MJPG否则可能失效 cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(M, J, P, G)) # 状态切换时调用 if state SEARCH: set_resolution(cap, 640, 480) else: set_resolution(cap, 1280, 720)参数上切换分辨率会有 2~3 帧的延迟所以不要在 RECOGNIZE 中途切而是在进入 ALIGN 时一次性切好。这个技巧让我的机器狗在保持 25fps 的同时识别距离从 1.5 米拉到了 3 米。希望帮到你。本文还有配套的精品资源点击获取