
简介2023年电赛E题「运动目标控制与自动追踪系统」配套资料包聚焦矩形框识别与外围边线查找模块面向准备电赛或学习嵌入式视觉的读者项目基于Jetson Nano完成目标检测通过串口与舵机控制联动可快速理解运动目标追踪的工程实现思路。压缩包共54个文件以Python源码、测试图像为主辅以Markdown说明和PDF文档文档系统梳理了目标检测的任务定义、Two stage / One stage 代表算法并解释NMS、IoU、mAP等关键指标。源码脚本覆盖矩形框识别、外围边线查找、串口通信等子任务便于对照学习和二次开发包体仅5.1MB适合快速下载与按需查阅。已有2152人学习下载。读者可从中获得完整赛题方案的局部实现、实拍测试图像、算法流程说明以及从目标检测原理到嵌入式部署的完整链路结合舵机控制与通信联调思路赛前冲刺或项目复现都能找到参考。1. 2023电赛E题不是一道图像题运动目标控制的最小闭环2023年电赛E题给的场景很朴素一张A4纸贴在深色底板上两个激光笔一个作为运动目标一个装在二维云台上作为被控光源。系统要用摄像头同时读出两个光斑的位置先让被控光斑沿着纸面画一个周长50cm的矩形再去自动追踪自由移动的目标点。很多队伍看到“识别”两个字就扎进图像算法里结果发现识别做得很准舵机却一直在抖原因是这道题考的是整个闭环伺服链路不只是视觉感知。图像、坐标映射、云台控制、轨迹生成、状态切换任何一个环节延迟过大或参数失配最终都表现为光斑画不出矩形、追不上目标。我习惯把所有环节拆成能单独验证的模块先标定再检测双光斑然后调云台PID最后加轨迹生成和丢光重捕状态机全部打通后再合并跑。这样每个环节出问题时能很快定位是图像丢了、坐标映射错了还是控制参数不合适。下面这套方案以Python加OpenCV加串口控制为例思路可以平移到OpenMV、K210或者树莓派方案上重点是可复现的调试路径而不是某个特定板卡。2. 像素坐标到平面坐标运动目标控制从标定开始2.1 为什么不能直接拿像素误差驱动云台初版方案最常见的做法是摄像头固定云台带着激光笔转动看到画面里光斑离目标点差100像素就直接给舵机一个和100像素成比例的PWM增量。短距离移动看起来能用光斑一旦跑到画面边缘同样的像素偏差对应的实际角度偏差完全不同。摄像头镜头有透视效应A4纸靠近画面边缘时每毫米对应的像素数在急剧变化同时云台转动中心和激光笔出光点不重合角度到位后光斑位置还有一个跟随过程这种情况下用像素误差直接驱动舵机往往在边缘区域产生持续振荡。正确做法是建立两层映射。第一层把像素坐标映射到A4纸平面上的毫米坐标解决透视变形也方便后续画矩形时直接用物理坐标生成目标点。第二层建立毫米坐标与云台舵机角度的近似关系我一般用“毫米增量与角度增量比例系数”来描述也就是让光斑在纸面上移动一毫米需要舵机转多少度。这套方案比完整的相机内外参标定快很多现场换位置、换纸面角度时只需要重新点四个角就能恢复。2.2 用A4纸的四个角建立单应性变换实现第一层映射的核心是单应性矩阵。摄像头画面里能稳定找到A4纸的四个角点它们的像素坐标记为src物理坐标记为dst单位统一用毫米。用OpenCV的findHomography得到一个3x3矩阵H之后任意光斑的像素坐标都能通过perspectiveTransform投影到纸面毫米坐标。这套逻辑在比赛现场比棋盘格标定更实用因为A4纸本身就是最大的标定参照物。import cv2 import numpy as np def setup_paper_mapping(clicked_points, paper_mm(210, 297)): # clicked_points按左上、右上、右下、左下顺序传入 src np.array(clicked_points, dtypenp.float32) dst np.array([ [0, 0], [paper_mm[0], 0], [paper_mm[0], paper_mm[1]], [0, paper_mm[1]] ], dtypenp.float32) H, status cv2.findHomography(src, dst) return H def pixel_to_paper(pts_pixel, H): pts np.array(pts_pixel, dtypenp.float32).reshape(-1, 1, 2) return cv2.perspectiveTransform(pts, H).reshape(-1, 2)这里findHomography用四个点对求单应矩阵perspectiveTransform把任意像素坐标投影到毫米坐标。最需要注意的是四个角的点选顺序顺序不一致会出现镜像或旋转180度画矩形方向全反。另一个容易忽略的是paper_mm单位整个控制回路都该用毫米不要中途混入厘米。标定完成后我用一个简单的测试把鼠标移到A4纸四个角上看换算出来的毫米坐标是否分别接近(0,0)、(210,0)、(210,297)、(0,297)能对上说明标定正确。拿到H之后矩形轨迹的目标点生成就简单了。A4纸长边297mm、短边210mm周长50cm的矩形不可能完全落在纸面内部所以目标矩形需要根据光斑实际位置动态规划四个顶点必须经过纸张边界裁剪避免光斑追出纸面。2.3 云台角度模型与两个关键系数像素到毫米只解决了“目标点在哪”控制舵机还需要知道“光斑移动1mm要转多少度”。我不用严格的三角函数推导而是把云台放在固定高度激光笔近似垂直照向纸面在纸面上沿x方向移动光斑记录对应的云台角度差拟合出k_x 角度增量 / 毫米增量同样方式得到k_y。这两个系数在云台换位置、换激光笔安装角度后都会变所以标定脚本里要把它们存成可配置参数。参数含义现场调整依据H矩阵像素坐标到毫米坐标的映射摄像头或纸面移动后重新点四角k_x云台水平角增量与毫米增量比值改动云台安装后重测k_y云台俯仰角增量与毫米增量比值改动云台安装后重测dead_zone舵机空回和光斑噪声死区根据回稳效果微调每次换场地我重新贴A4纸手动点四个角然后让光斑在纸面左上和右下各打一个点记录两组角度差算出k_x和k_y整个过程两分钟。比赛现场改动云台高度或摄像头位置后这两个系数必须重新测否则前面标定得再准控制输出也是偏的。3. 双光斑识别与质心提取自动追踪系统的“眼睛”3.1 用HSV阈值把两个红色光斑分开图像处理部分的难点不是找到光斑而是同时找到两个红色激光光斑并且不把中心高亮的白色区域拆成另一个目标。红色激光在画面上通常是中心发白、外围一圈红色的形态直接做RGB阈值容易把亮白部分单独切出去导致一个光斑被识别成两个。我先把图像从BGR转到HSV再用两个区间做红色掩膜一个对应低色调区一个对应高色调区最后合并两张掩膜。OpenCV的H分量范围是0到180不是0到360所以红色必须分段处理。import cv2 import numpy as np def detect_red_spots(frame): hsv cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask1 cv2.inRange(hsv, (0, 100, 100), (10, 255, 255)) mask2 cv2.inRange(hsv, (170, 100, 100), (180, 255, 255)) mask cv2.bitwise_or(mask1, mask2) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8)) return maskinRange的参数分别是H、S、V的下限和上限S和V下限设为100目的是滤掉环境光和纸张阴影。现场环境偏暗时可以把S和V下限降到80但降太多会把红色桌面、皮肤反光一并纳入误检率反而上升。MORPH_OPEN操作消除光斑边缘的孤立噪点结构元大小取3x3就够取大了会把小光斑整个滤掉。红色区间H范围S范围V范围覆盖场景低色调区0到10100到255100到255偏橙红、粉红激光高色调区170到180100到255100到255偏紫红、玫红激光两个区间合并后再用bitwise_or得到最终掩膜。建议在调试界面上把掩膜单独显示出来如果光斑在画面里是白色圆圈说明阈值正确如果出现大量杂散白点优先调S下限。3.2 用连通域提取稳定质心拿到二值掩膜后用connectedComponentsWithStats做连通域分析比直接找轮廓再包最小外接圆更稳定。激光光斑运动时有轻微拖尾形态不是标准圆连通域的统计信息能同时给出面积、外接框和中心坐标。过滤掉面积过小和过大的噪声后按照面积排序把最大的两个连通域当作被控光斑和目标点。num, labels, stats, centroids cv2.connectedComponentsWithStats(mask, 8) spots [] for i in range(1, num): area stats[i, cv2.CC_STAT_AREA] if area 2 or area 500: continue x, y centroids[i] spots.append((x, y, area)) spots.sort(keylambda s: -s[2]) laser_spot spots[0][:2] target_spot spots[1][:2]八连通参数8能把光斑边缘断点连接成同一片区域。面积阈值2到500需要按分辨率调整640x480下正常光斑在20到80像素升级720p后面积会放大两到三倍建议按总像素比例设置。按面积排序后被控光斑通常比目标光斑更靠近画面中心所以不能只依赖面积还要结合上一帧位置做最近邻匹配防止两个光斑交叉时身份互换。3.3 光斑粘连与自动曝光干扰两个光斑靠得很近时连通域会合并spots里只剩一个面积很大的目标。处理顺序是先判断连通域数量如果只有一个且面积超过单独光斑两倍用外接矩形的左右两半分别计算质心如果两个光斑几乎重合就沿用上一帧目标点位置暂停跟踪控制等两帧后重新分开再恢复。这套逻辑不需要额外训练模型只靠简单的面积和位置约束就能处理绝大多数情况。另一个常被忽略的是摄像头自动曝光。自动曝光模式下激光光斑中心会过曝成一大片白色质心偏移其实不大但面积变化剧烈导致面积排序不稳定。我会在摄像头初始化时把曝光手动调低两到三档让光斑半径控制在5像素以内。质心抖动一旦超过2像素在毫米坐标里就是好几毫米的跳动控制器会持续输出修正最后表现为系统自发振荡。4. 云台闭环控制PID参数与跟踪速度匹配4.1 反馈滤波与目标点速率限制PID控制的是毫米坐标误差但光斑质心在运动过程中有抖动直接拿原始误差计算比例项舵机会一直小幅震动。我在反馈路径上对两个光斑的坐标分别做一阶低通滤波公式是x_filtered alpha * x_new (1 - alpha) * x_filteredalpha取0.3到0.5。alpha太大会让跟踪反应迟钝alpha太小等于没滤波。滤波处理的是被控光斑坐标因为它是控制回路里的反馈量。目标点坐标的处理方式相反。自动追踪时目标点可能快速移动甚至瞬移直接做低通滤波会引入滞后。我会对目标点单独加“最大速率限制”每帧最多移动8毫米换算到30帧控制周期大约是240mm/s的跟踪上限。这样系统既能避免目标点跳变引起的输出突变又能保持对真实运动的响应速度。4.2 一个可复用的位置式PID实现云台舵机工作在位置模式我用位置式PID。滤波后的当前光斑坐标与目标坐标做差得到两个方向的误差x和y分别由云台偏航舵机和俯仰舵机控制两个通道参数独立。下面的实现把输出限幅和积分限幅放在一起方便现场直接套用。class PidChannel: def __init__(self, kp, ki, kd, out_limit, integral_limit): self.kp, self.ki, self.kd kp, ki, kd self.out_limit out_limit self.integral_limit integral_limit self.last_err 0.0 self.integral 0.0 def update(self, err, dt): self.integral err * dt self.integral max(-self.integral_limit, min(self.integral_limit, self.integral)) derivative (err - self.last_err) / dt if dt 0 else 0.0 out self.kp * err self.ki * self.integral self.kd * derivative self.last_err err return max(-self.out_limit, min(self.out_limit, out))dt必须由主循环实际周期计算不能写死成0.02秒。把dt写死换电脑或换摄像头后PID参数会变得不可解释。积分限幅放在误差累加之后防止长时间误差导致积分饱和。现场初始值我一般取x方向kp1.2、ki0.05、kd0.2y方向相近但这是30Hz控制周期下的经验值帧率变化后需要整体按比例调整。4.3 速度前馈是快速追踪的关键目标点持续匀速移动时纯PID一定有固定滞后因为比例项只有在存在误差时才有输出。解决办法是叠加速度前馈连续两帧目标点坐标的差除以时间就是目标速度再乘以一个系数加到PID输出上。系数通常取0.6到0.9留一部分误差给PID修正。前馈加入后积分项的作用被弱化可以把ki降一半。目标速度前馈系数说明慢速小于50mm/s0.6以减小超调为主中速50到200mm/s0.8兼顾滞后与稳定性快速大于200mm/s0.9优先压低跟踪滞后如果同时加大前馈和积分系统会出现低频振荡光斑围着目标点来回划圈这种症状几乎都是前馈与积分打架导致。调试顺序应是先关积分把前馈调到目标匀速运动时误差不超过10mm再逐步加回ki。每次加积分后跑一段固定轨迹看误差曲线是否出现振荡出现就退回上一组参数。5. 矩形轨迹生成与丢光重捕状态机5.1 用毫米坐标规划矩形顶点画矩形任务在毫米坐标下实现最方便。A4纸210x297mm50cm周长可以选择200mm乘50mm的矩形也可以选择150mm乘100mm关键是矩形必须完整落在纸面内。我一般以光斑当前位置作为起点向后规划矩形所有顶点经过A4纸边界裁剪。如果起始点太靠近边缘就沿边缘方向生成保证整个轨迹始终可见。def plan_rect(start, size(200, 50)): x0, y0 start pts [(x0, y0), (x0 size[0], y0), (x0 size[0], y0 size[1]), (x0, y0 size[1])] pts [clip_to_paper(p) for p in pts] return ptsclip_to_paper把超出纸面的坐标拉回边界同时尽量保持矩形形状完整。很多队伍发现光斑在某一侧突然飞出去都是因为顶点超出纸面但代码没有限制。常见矩形尺寸组合如下表选择时要看光斑起始点离哪条边更近。长(mm)宽(mm)周长(mm)使用建议20050500推荐A4纸内余量充足150100500可用面积更居中18070500起始点靠近角落时使用5.2 用状态机管理画矩形和自动追踪系统运行时有三个状态空闲、画矩形、自动追踪。上电后进入空闲状态检测到两个光斑且目标点稳定后开始画矩形。画完一个闭环后如果目标点仍然存在切换到自动追踪。追踪过程中若被控光斑或目标点连续丢失超过10帧回到空闲状态并做重捕搜索。用枚举状态机管理比一堆if嵌套清晰得多。typedef enum { STATE_IDLE, STATE_DRAW_RECT, STATE_TRACK } sys_state_t; sys_state_t state STATE_IDLE; int lost_cnt 0; void state_tick(int spot_valid, int target_valid) { switch (state) { case STATE_IDLE: if (spot_valid target_valid) state STATE_DRAW_RECT; break; case STATE_DRAW_RECT: lost_cnt spot_valid ? 0 : lost_cnt 1; if (lost_cnt 10) state STATE_IDLE; break; case STATE_TRACK: lost_cnt (spot_valid target_valid) ? 0 : lost_cnt 1; if (lost_cnt 10) state STATE_IDLE; break; } }丢光判定用连续帧数而不是瞬时丢失是因为光斑偶尔被激光笔遮挡一下10帧约等于0.3秒足够区分瞬时遮挡和真正丢失。状态机内部还要记录当前矩形顶点索引画完最后一个点后先把光斑停下来再切换状态避免切换瞬间舵机输出突变。状态切换时不直接改目标坐标而是清除PID积分项这是防止跳变的关键一步。5.3 矩形四角用圆弧过渡让光斑严格沿直角折线走舵机在角点处会瞬间满速反向造成明显过冲。我的处理是每个角点前提前减速在角点附近用一小段圆弧过渡半径取5到10mm。代码上相当于在两段直线之间插值若干点插值点数量由该段长度决定。这个方法不改闭环结构只把期望轨迹改成圆角矩形但能显著减少角点处的振荡次数。6. 现场调试的四个陷阱与帧率优化技巧6.1 曝光锁定比调PID更优先现场调参遇到“PID乱跳”优先检查摄像头是否处于自动曝光模式。自动曝光下激光光斑从画面一侧移到另一侧亮度变化会导致光斑面积和质心偏移几个像素换算到毫米就是5mm级噪声。先把曝光和增益固定再去看PID输出这一步能排除大量假性问题。6.2 用正弦轨迹回放验证控制性能验证自动追踪性能时让目标光斑按程序生成的正弦轨迹运动系统进入追踪模式把跟踪误差实时画出来。误差曲线呈固定相位滞后但没有振荡说明前馈系数偏小误差曲线出现等幅振荡说明kd或前馈系数偏大。这个方法比盯着屏幕看光斑要可靠得多现场排查时能在两分钟内判断参数调整方向。6.3 分辨率与帧率取舍640x480在30帧下跑阈值分割和连通域分析延迟能控制在50ms以内。为了“看得清”把分辨率调到1280x720帧率可能掉到15帧以下控制周期变长PID参数全部要重调。我的选择是固定640x480先做ROI裁剪只保留A4纸区域再跑识别帧率能稳定在30帧以上。ROI可以粗一些把A4纸整体包住即可不需要精确到边缘。6.4 串口协议加帧号定位丢帧上位机下发的控制指令里带上自增帧号每帧数据末尾追加一个字节的计数器。舵机表现异常时通过帧号能直接判断是通信丢帧、图像处理卡顿还是单片机解析出错。常见场景是USB转串口和摄像头共用带宽帧号能快速定位上位机调度问题避免在舵机机械结构上浪费排查时间。串口波特率我一般选115200每帧数据控制在12字节以内一帧完整指令的发送时间可以忽略不计。本文还有配套的精品资源点击获取