视觉云台追踪系统设计:坐标标定、PID整定与OpenCV实战
2026/9/17 7:33:39 网站建设 项目流程

简介:面向全国大学生电子设计竞赛参赛者,这份23年E题资源包提供赛题原题、解题方案与可直接运行的工程源码,适合正在备赛或计划参赛的本专科学生用于实战演练与技能提升。包内共1106个文件,以C语言源码和头文件为主体,同时包含汇编文件、ICF链接脚本、工程配置文件、编译输出文件及说明文档等,完整覆盖嵌入式程序从编写、配置到编译烧录的各个环节。压缩包整体35.11MB,目录结构清晰,便于按功能模块查找。目前已有826人学习使用。所有程序均来自实战案例并经过测试验证,读者可据此深入理解电赛E题的系统设计思路、外设驱动写法与调试排错方法,也可在官方平台基础上快速移植改造,显著缩短备赛周期。

1. 23电赛E题.zip 在解一道什么样的题

23电赛E题的全称是 2023 年全国大学生电子设计竞赛「运动目标控制与自动追踪系统」。场景一句话说清:二自由度舵机云台搭载激光笔和摄像头,摆在离白纸屏幕 1 米远的位置,先让激光光斑在 A4 白纸上自动画出指定边长的正方形,再追踪一个在纸面上移动的靶标。这个题表面是画线和追踪,本质是从像素误差到舵机角度的视觉闭环。23电赛E题.zip 资源包里不外乎主控固件、图像脚本、标定工具和设计报告,真正决定成绩的是标定准不准、闭环跟不跟得住。下面从硬件拆解开始,把坐标标定、目标检测、PID 整定和联调排错完整过一遍,适合备赛选手和做视觉云台追踪的工程人员参考。

2. 拆电赛E题系统:云台、激光与相机的主控分工

2.1 E题的两个任务与误差来源分解

E题拆开看是两个不同性质的控制问题。画正方形时目标静止、路径已知,难点在坐标标定的绝对精度,边长是现场给定的,要求从零点起步、沿边走完再回到起点,对响应速度要求不高,对到位精度要求高。追踪靶标时目标运动、路径未知,难点在检测延迟和闭环带宽,靶标沿屏幕上的曲线移动,激光必须持续跟上,现场按跟踪误差和响应时间打分。

同一个安装误差对两个任务的影响完全不同。画正方形时,如果相机光轴和云台转轴不平行,标定映射会在屏幕边缘产生偏移,方形越画越歪;追踪时,如果舵机慢半拍,激光会整体滞后于靶标,误差呈典型的“跟丢”趋势。多数队伍第一版都把相机固定在云台上随动,这样视野中心始终对准屏幕,缺点是图像坐标和云台角度强耦合,标定要反复迭代;另一种是相机固定在三脚架上拍整个屏幕,标定一次就能用,但屏幕在画面中的分辨率受安装距离限制。两种方案都能进国赛,区别只在标定投入的精力。

屏幕在画面里占多大比例,直接决定标定后每个像素对应多少毫米。640×480 分辨率下 A4 纸(297×210mm)占满画面时,每像素约 0.46mm,足够支撑 5mm 量级的误差预算;如果只裁切屏幕 ROI 处理,等效精度还能再提。摄像头选型不要追求超高像素,30~60fps、固定曝光、手动白平衡的型号比高像素但帧率低的更适合这个场景。

2.2 硬件选型对比:OpenMV、USB工业相机与主控的取舍

选型决定了后面调参的舒适度。给一个竞赛圈常见的配置对照:

部件常见型号优点缺点适用情况
视觉模块OpenMV H7一体式,MicroPython上手快算力低、帧率低、阈值调参繁琐队伍没接触过OpenCV,求快速出效果
视觉模块UVC免驱工业相机帧率可达60~120fps,OpenCV直接处理需要额外主控或上位机有树莓派/电脑做上位机,追求跟踪性能
主控STM32F103/F407定时器多、PWM精度高、实时性稳图像处理吃力,需要和上位机通信竞赛队最常用,资料最多
主控ESP32自带WiFi,无线调试方便浮点性能弱,ADC精度一般快速原型验证
主控树莓派直接跑Python+OpenCV启动慢,现场供电麻烦上位机方案,输出给舵机驱动板
舵机SG90模拟舵机便宜,带激光头够用死区大、回中差、延迟明显预算紧张,画方题够用
舵机MG996R数字舵机力矩大、响应快、重复精度好功耗高,需独立5V供电云台重心偏、需要高速追踪

这套配置里最容易出问题的不是相机也不是主控,而是舵机供电。SG90 满载电流能上 600mA,直接挂 STM32 的 5V 引脚或电脑 USB 口,电压跌落会让 PWM 脉宽跟着变,表现是“标定好好的,一跑就偏”。规范做法是舵机独立供电,电源地和主控共地,主控侧只出信号线。

2.3 通信与时序:帧率、波特率、PWM 更新频率的匹配

闭环链路是采图 → 图像处理出靶标像素坐标 → 坐标换算成舵机角度 → PWM 驱动云台。每一级的周期都要匹配,否则延迟在整条链路上累积。常见做法是图像处理在主控或上位机里跑,结果经串口发给舵机控制侧。协议写复杂了容易粘包,写简单了容易丢帧,我一般用固定 8 字节帧:

// 发送端打包:帧头 + 命令字 + 角度数据 + 校验 uint8_t tx_buf[8]; tx_buf[0] = 0xAA; // 帧头 tx_buf[1] = 0x01; // 命令:1=角度控制 tx_buf[2] = yaw_angle & 0xFF; // 水平角低字节 tx_buf[3] = (yaw_angle >> 8) & 0xFF; // 水平角高字节,单位0.1° tx_buf[4] = pitch_angle & 0xFF; // 垂直角低字节 tx_buf[5] = (pitch_angle >> 8) & 0xFF; // 垂直角高字节 tx_buf[6] = 0x00; // 保留 tx_buf[7] = tx_buf[0] ^ tx_buf[1] ^ tx_buf[2] ^ tx_buf[3] ^ tx_buf[4] ^ tx_buf[5] ^ tx_buf[6];

发送端把角度拆成高低字节,第 8 字节是前 7 字节的异或校验。接收端按状态机解析,以 0xAA 为同步头,校验通过才更新目标角度,校验失败直接丢整帧,不回包也不重传,因为控制量是每帧覆盖的,丢一帧比重传统治延迟更合理。

参数匹配有三条原则:第一,串口波特率要保证在一帧图像处理周期内传完至少 2 帧控制量,30fps 下周期 33ms,115200 波特率传 8 字节约 0.7ms,余量足够;第二,PWM 更新频率不要高于舵机响应极限,模拟舵机 50Hz、数字舵机 100Hz,再快只增加发热不增加精度;第三,曝光时间不要超过帧间隔的一半,否则运动靶标拖影,质心会向运动方向偏移。这三条是“看得准”和“跟得上”之间最直接的取舍。

3. 电赛E题坐标标定:像素坐标到舵机角度的两步映射

3.1 为什么像素误差不能直接喂 PID

很多第一次做这个题的队伍,思路是“看到激光偏了就往目标方向转一点”,拿像素误差直接当 PID 输入。这在屏幕中心勉强能跑,云台转到边缘就穿帮:屏幕是平面,云台绕轴转,光斑在屏幕上的位移和舵机转角是正切关系。假设云台到屏幕距离 L、偏移角 θ,屏幕位移 d = L·tan(θ)。小角度下 tan(θ)≈θ 成立,但 E 题要求在 A4 纸范围内活动,云台偏角能到 ±20° 以上,tan 的偏离超过 8%。表现就是中心调好的增益,转到屏幕边缘时激光抖动甚至振荡。

背后是眼在手上结构的固有属性:图像里的误差同时包含目标位置偏差和云台自身转动带来的视场变化。必须先在像素误差之前做坐标映射,把目标位置换算成云台应该转到的角度,再用角度差驱动闭环,而不是拿图像差值直接当控制器输入。

3.2 两步标定法:先求像素到屏幕,再求屏幕到舵机角度

常见做法是两步走,方便拆开排查问题。第一步标定图像坐标系到屏幕世界坐标系:屏幕上固定 A4 白纸,打印 6×6 棋盘格或圆点阵,云台保持不动拍一张,用 OpenCV 找角点,建立像素到屏幕物理坐标(毫米)的单应矩阵 H。H 是 3×3 矩阵,内含透视和缩放。第二步标定屏幕坐标到舵机角度:让激光分别打到屏幕上已知的 5~9 个点,记录每个点的屏幕坐标 (X, Y) 和对应舵机角度 (yaw, pitch)。云台到屏幕距离基本固定,yaw、pitch 可以分别用仿射模型独立拟合。

第一步代码:

import cv2 import numpy as np # 已拍的屏幕棋盘格图像,每格真实尺寸 20mm img = cv2.imread("grid.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners = cv2.findChessboardCorners(gray, (6, 6), None) if ret: # 屏幕物理坐标,单位毫米,原点在左上角 objp = np.zeros((6 * 6, 3), np.float32) objp[:, :2] = np.mgrid[0:6, 0:6].T.reshape(-1, 2) * 20.0 H, _ = cv2.findHomography(corners.reshape(-1, 2), objp[:, :2]) np.save("homography.npy", H) def pixel_to_screen(px, py): """像素坐标 -> 屏幕毫米坐标""" p = H @ np.array([px, py, 1.0]) return p[0] / p[2], p[1] / p[2]

关键在 objp 的物理坐标要和真实格子尺寸一致,单位统一用毫米,否则标定表会带一个固定比例误差。工业相机如果镜头畸变明显,标定前先做一次畸变校正,否则屏幕边缘点的映射误差能到厘米级。

第二步最小二乘拟合:

# 采集数据:激光对准屏幕上已知点,记录舵机角度 # 每行 [screen_x, screen_y, yaw, pitch],至少采集 5 组 samples = np.array([ [0, 0, 52.3, 68.1], [100, 0, 58.0, 68.0], [0, 100, 52.4, 61.5], ]) X = samples[:, :2] # 屏幕坐标列 A = np.hstack([X, np.ones((len(X), 1))]) # [x, y, 1] coef_yaw, _, _, _ = np.linalg.lstsq(A, samples[:, 2], rcond=None) coef_pitch, _, _, _ = np.linalg.lstsq(A, samples[:, 3], rcond=None) def screen_to_angle(sx, sy): yaw = coef_yaw[0] * sx + coef_yaw[1] * sy + coef_yaw[2] pitch = coef_pitch[0] * sx + coef_pitch[1] * sy + coef_pitch[2] return yaw, pitch

A 矩阵是 [sx, sy, 1] 的拼接,拟合出的三个系数分别表示:屏幕 x 方向位移对 yaw 的影响、y 方向位移对 yaw 的影响、零位偏移。yaw 和 pitch 分开拟合的原因是这样单个轴出问题时能单独排查。拟合完检查残差,如果某个点残差明显大于其他点,多半是采集时激光没对准或屏幕有褶皱,重新采集该点,而不是硬塞进拟合。

3.3 舵机角度到 PWM 脉宽换算与限幅

标定得到的是角度,最终驱动还要转成脉宽。数字舵机角度与脉宽近似线性,但不同品牌中位和范围差别不小,必须实测,不要照抄手册:

舵机类型常见脉宽范围对应角度
SG900.5ms~2.5ms0°~180°,中位1.5ms
MG996R0.5ms~2.5ms0°~180°
部分高压舵机0.4ms~2.4ms0°~270°

换算公式:

// 20ms 周期,0.5ms~2.5ms 脉宽对应 0°~180° // 调用前确保 angle 已经限幅到机械行程内 uint32_t pulse_us = 500 + (uint32_t)(angle / 180.0f * 2000.0f); // 定时器按 20ms 配置,arr_us 为满周期对应的计数值 __HAL_TIM_SET_COMPARE(&htim, TIM_CHANNEL_1, pulse_us * arr_us / 20000);

限幅要做在角度域而不是脉宽域:先算脉宽再限幅,反向和零偏抖动的代码会被绕过去。我一般在 screen_to_angle 返回后立即 clamp 到机械行程的 90% 以内,给机械限位留缓冲,防止堵转烧舵机。另一个容易漏的是反向设置,很多舵机安装方向相反,标定拟合出来的斜率是负的,直接套用会越追越远,现象是“朝目标转但角度差越来越大”。判断方法很简单:手动给一个角度增量,看激光往哪个方向走,记录符号,再补到换算公式里。

4. E题追踪闭环:阈值分割、质心提取与 PID 整定

4.1 双色场景下的色彩隔离:YCrCb 阈值与光斑处理

追踪场景里激光光斑和靶标同时在画面中,图像处理第一步是分清楚谁是谁。靶标是屏幕上的色块,光斑是画面里的高亮白点,亮度差异大,但激光正好落在靶标上时会“盖住”靶标,画面里是一团亮,这时很难直接分清中心。

我一般把颜色空间换成 YCrCb 而不是 RGB,原因有两个:亮度分量 Y 单独做光斑检测,色度分量 Cr/Cb 做靶标检测,两者互不干扰;相机固定白平衡后,YCrCb 对光照变化比 RGB 更鲁棒。阈值不靠手工猜,先框一个 ROI 采样统计 Cr、Cb 的均值和方差,用均值±3 倍标准差作为阈值范围。

import cv2 import numpy as np frame = cv2.imread("scene.jpg") ycc = cv2.cvtColor(frame, cv2.COLOR_BGR2YCrCb) # 靶标:红色块,阈值来自 ROI 采样统计 mask_target = cv2.inRange(ycc, (0, 138, 100), (255, 175, 135)) mask_target = cv2.morphologyEx(mask_target, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8)) # 光斑:只看亮度分量 Y,取高亮阈值 _, mask_laser = cv2.threshold(ycc[:, :, 0], 230, 255, cv2.THRESH_BINARY) # 分别取最大连通域,防止反光点干扰 ct, _ = cv2.findContours(mask_target, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cl, _ = cv2.findContours(mask_laser, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

这段代码里 morphologyEx 的开运算用来去掉靶标边缘的孤立噪点;findContours 只取最大连通域,防止屏幕反光被误判成靶标。阈值上下界不要照抄数字,不同屏幕亮度和激光色温下差别很大。现场通用做法是放一个白色参考物在镜头前,抽它的 Y 值作为光斑阈值的下限基准,再按实测微调。

4.2 质心提取与目标丢失处理

拿到轮廓后直接取矩,得到亚像素级质心:

M = cv2.moments(ct[0]) if M["m00"] > 1e-6: cx = M["m10"] / M["m00"] cy = M["m01"] / M["m00"]

m00 是连通域面积,m10/m00、m01/m00 分别是 x、y 方向的质心。质心比“包围盒中心”抗噪能力强得多,尤其靶标被激光盖住一半时,轮廓只剩下半边,但质心仍然落在几何中心附近。如果画面里同时出现两个面积接近的连通域,用最近邻匹配:上一帧质心与本帧两个候选质心的距离差,小的那个才是当前目标。

目标丢失是追踪题最常见的扣分点,处理要分两级。短时间丢失(连续 3 帧以内),保留上一帧质心,用历史速度外推一个预测位置,激光继续往预测位置走;长时间丢失(超过 10 帧),放弃追踪,云台回到屏幕中心安全位,上报丢失标志,等靶标重现再切回追踪。预测外推用一阶低通加速度估计就够,不需要上卡尔曼:

# 速度估计:帧间质心差,再做一阶低通平滑 vx = cx - prev_cx vy = cy - prev_cy vx_smooth = 0.7 * vx_smooth_prev + 0.3 * vx vy_smooth = 0.7 * vy_smooth_prev + 0.3 * vy # 丢失时用预测位置代替实测质心 predict_x = cx + vx_smooth * 2 predict_y = cy + vy_smooth * 2

0.7/0.3 是平滑系数,越大越平滑但滞后越大。注意这里的速度单位是像素/帧,如果相机帧率不稳定,速度值会在帧间抖动,所以追踪时相机必须固定帧率、关闭自动曝光。

4.3 增量式 PID 与整定顺序

闭环核心是把激光与目标之间的屏幕误差,经 PID 变成舵机角度增量。位置式 PID 在云台上容易积分饱和,我一般用增量式,输出每次只加一个角度变化量,天然抗饱和:

// 增量式 PID,返回本次需要增加的角度增量 // T 为控制周期,单位秒,由定时器固定驱动 float pid_step(float setpoint, float measure, float *prev_err, float *integral, float kp, float ki, float kd, float T) { float err = setpoint - measure; *integral += err * T; float d_err = (err - *prev_err) / T; *prev_err = err; return kp * err + ki * (*integral) + kd * d_err; }

整定顺序有讲究,不要上来就上 Ziegler-Nichols。竞赛调试时间以小时计,我的顺序是:先把 I、D 置 0 只调 P,从 0.1 起步逐步加大,直到目标静止时激光能稳定在目标附近不振荡;再加 D 抑制动态“画龙”,加到动态误差明显减小、靶标突然停止时激光不抖为止;最后加一点 I 消静差,画正方形时如果每个顶点都能停在 2mm 内,I 不加反而更好,加了会在换向时超调。增量式 PID 的控制周期必须固定,用定时器驱动而不是在 while 循环里 delay,否则 D 项会被图像处理耗时抖动污染。

一组可直接起步的参数区间:

工况KpKiKd备注
画正方形低速高精度0.6~1.20~0.051.5~3到位不振荡优先
追踪慢速靶标1.0~2.00.01~0.022~4跟随误差小优先
追踪快速靶标2.0~4.004~8不过冲优先,必要时加前馈

PID 输入如果是像素误差,不同相机分辨率下同一组参数表现完全不同;输入是屏幕毫米误差时,参数才具备跨装置的可移植性。这也是前面坚持先标定后闭环的另一个原因:标定完成后 PID 输入量纲统一,换一台相机不用重新整定整套参数。

5. E题联调顺序、常见陷阱与三个提速技巧

5.1 联调顺序:从静止点开始,最后才追靶

现场最忌讳直接上追踪。固定顺序四步:第一步静态光斑瞄准,手动下发一组角度,激光必须打到网格点,这一步只验证 PWM 和角度换算;第二步标定验证,让激光自动走 9 个网格点,偏差超过 5mm 就回去重做标定;第三步画正方形,先低速画大的确认每个顶点到位,再逐步提速缩小;第四步追踪,先直线慢速再曲线快速。每步的问题都能收敛到单一模块,排查时间能省一半。

5.2 zip 里最常见的三个陷阱

第一,图像坐标系 Y 轴方向。OpenCV 的原点在左上角、Y 轴向下,而云台 pitch 角习惯正方向朝上。不反转 Y 轴,会出现“目标往下走、激光往上走”的反向跟随,而且越追越离谱,这个符号错误在调 PID 之前就应排除。

第二,串口粘包和校验缺失。现场 USB 转串口的缓冲时序不稳定,不做帧头同步和校验,偶尔会出现舵机突然甩到极限角,不仅扣分,还容易撞坏云台机械结构。进 PWM 的角度必须限幅,而且限幅要放在主控里而不是上位机里,因为上位机可能来不及响应。

第三,掉电不保存标定参数。标定系数硬编码在代码里的话,每次改代码都要重标一遍。规范做法是把系数存入 Flash 或独立配置文件,上电自动加载,并提供一键重标定功能,把现场时间留给真正影响性能的参数。

提示:所有代码里的角度单位与坐标系以上位机约定为准,联调前先打印一次换算结果,确认符号和量纲,再接入闭环。

5.3 三个立竿见影的提速技巧

第一个是 ROI 裁剪。靶标只在屏幕范围内活动,屏幕只占画面一部分,把检测区域裁到屏幕包围盒内,处理量能减少一半以上。OpenCV 直接对子图像做阈值,质心坐标记得加回 ROI 偏移,否则角度换算会整体偏掉。

第二个是目标运动预测。追踪延迟主要来自图像处理耗时和舵机响应,舵机难在短时间优化,图像处理段的延迟可以用速度外推补偿:输出角度前预测靶标 1~2 帧后的位置,再送入 PID。前瞻超过 2 帧,靶标突然反向时会大幅超调。

第三个是位置前馈。画正方形的轨迹是已知的,把下一时刻期望位置直接换算成角度增量,与 PID 输出并联,开环保证轨迹形状、闭环修正累积误差,高速绘制时正方形不会在顶点被 PID 切角。验证方法是用录像回放逐帧记录激光与靶标的位置差并画出误差曲线,峰值误差小于题目阈值说明闭环带宽够用;不够时优先检查曝光时间和 ROI 余量,而不是盲目加 PID 增益。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询