简介:这是一套面向电子设计竞赛的2023年E题完整项目资料,适合正在备战电赛的参赛队伍、嵌入式系统与机器视觉方向的学生,以及对单片机开发和图像识别感兴趣的进阶学习者,能帮助读者快速理解赛题要求并搭建可运行的原型代码。压缩包共包含五个文件,其中两个工程压缩包分别对应主控逻辑与舵机伺服控制模块,两个Python脚本用于视觉采集与数据交互,另附一份PDF参赛报告,梳理了设计思路、硬件连接和调试过程,整个资源仅23.67MB,便于下载部署,目录划分清楚,可快速定位所需代码与文档。该资料目前已有412人学习,属于具备真实使用反馈的赛题项目。所有源码均经过测试可以正常运行,省去大量环境配置和底层排错时间,可直接运行验证预期效果,上手门槛较低。在此基础上,读者可沿用清晰的工程分层,完成参数调整、控制策略更换、视觉追踪或自动循迹等二次开发,并保留作者交流渠道,使用中遇到的问题能得到及时解答,让学习与实战过程更加顺畅。
1. 2023年电赛E题:一套红绿激光追踪系统的完整源码包
真正在2023年电赛E题赛场上待过的人,对这套题的第一印象往往是“方案谁都想得出来,但让两个光点在A4纸上安安稳稳地互相咬住,才是噩梦”。E题要求用两个二维云台分别控制红色和绿色激光笔,先让绿色光点按矩形或圆形轨迹移动,再让红色光点实时追踪绿色光点。这个zip包就是当时一支参赛队留下的完整工程:两个OpenMV脚本、两套舵机控制封装、一份参赛文档PDF。它适合正在备赛电赛、想复现激光追踪项目、或者刚接触OpenMV视觉闭环控制的人。源码已经跑通过,直接烧进去就能看到两个舵机云台跟着色块转动,接下来要做的就是理解它为什么这样设计。
2. 赛题到底考什么:追踪误差、双云台和坐标系
2.1 从题目要求反推系统设计
2023年电赛E题“运动目标控制与自动追踪系统”,核心指标不是“能不能看见”,而是“红绿两个光点的距离误差”。基础部分要求绿色光点按给定路径运动时,红色光点能持续跟随;发挥部分往往增加路径复杂度或运动速度。也就是说,这是一个典型的伺服跟踪问题,难点集中在三个环节:视觉定位是否准确、云台角度解算是否正确、控制闭环是否收敛。
从题目反推系统结构,完整的信号链应该是:
- 两个独立的二维云台,每个云台至少两个舵机,分别控制偏航和俯仰。
- 一个摄像头观察A4纸区域,识别色点坐标。
- 一个控制核心读取视觉坐标,计算误差,输出舵机PWM。
- 两只激光笔固定在云台前端,随云台转动改变光点位置。
从压缩包内容看,first_openmv.py和第二套Second_Servo.zip明显对应两个独立闭环:一套负责绿色光点的轨迹产生,一套负责红色光点的追踪。这样拆分的好处是调试时互不干扰,先让绿点跑起来,再专心调红点跟丢的问题。
2.2 为什么OpenMV是多数队伍的第一选择
很多第一次接触这个题的人会问:为什么不用树莓派加USB摄像头,或者用K210?从实际比赛场景看,电赛只有四天三夜,树莓派启动要几十秒,Python生态虽然全,但写驱动、处理摄像头、接舵机,每一步都可能耗掉宝贵的调试时间。K210能跑色块识别,但OpenMV IDE的阈值调节工具、内置的find_blobs接口、以及MicroPython环境下直接操作PWM引脚的能力,让OpenMV在色块追踪这个细分场景里几乎是最低上手成本的选择。
另一个关键点是OpenMV可以脱机运行。比赛现场不允许一直连电脑,而OpenMV插上电池就能跑,遇到问题再用USB连电脑打印坐标。项目里两个OpenMV脚本分别烧录到两个摄像头,各自承担自己的视觉任务,比单摄像头加双云台的主从结构少一条通信链路,可靠性更高。
2.3 硬件选型与接线要点
这份项目能跑起来,硬件上并不需要昂贵设备。常见配置如下:
| 部件 | 型号参考 | 数量 | 说明 |
|---|---|---|---|
| 视觉核心 | OpenMV H7 Plus | 2 | 一个负责轨迹侧,一个负责追踪侧 |
| 舵机 | SG90(小扭矩)或MG996R | 4 | 每个云台两个舵机,分别控制水平和垂直 |
| 激光笔 | 650nm红色、532nm绿色 | 各1 | 光点越小,识别越稳 |
| 云台结构件 | 3D打印或金属云台支架 | 2 | 舵机叠起来构成二维自由度 |
| 供电 | 5V/3A以上稳压模块 | 1 | 舵机启动电流大,不能直接用USB供电 |
| 纸张 | A4白色卡纸 | 若干 | 哑光纸反光更轻 |
接线时最容易被忽略的是共地。OpenMV、舵机和稳压模块的地必须连到一起,否则舵机转动时的噪声会让色块检测出现跳动。舵机电源要单独从稳压模块供,不能和OpenMV共用同一路5V,否则舵机堵转瞬间能把电压拉到4V以下,OpenMV直接重启。
3. 视觉坐标提取:first_openmv.py和second_openmv.py的分工
3.1 色块检测的核心:LAB阈值和形态学过滤
OpenMV识别激光点,本质是在RGB565图像上做颜色阈值分割。激光颜色很纯,但环境光和纸张反光会把一个光点扩散成一个大色斑。所以在写识别代码前,先把摄像头固定到云台上,用OpenMV IDE打开“阈值编辑器”工具,对着A4纸上的红色和绿色光点调出合适的LAB阈值。LAB色彩空间把亮度、红绿、蓝黄分量拆开,比RGB抗光照变化能力强很多。
一个轨道上常见的绿色光点识别代码结构如下:
import sensor, image, time sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.skip_frames(time=2000) sensor.set_auto_whitebal(False) # LAB阈值,分别对应 L通道、A通道、B通道的范围 GREEN_TH = (30, 80, -40, -15, -5, 40) while True: img = sensor.snapshot() blobs = img.find_blobs([GREEN_TH], pixels_threshold=15, area_threshold=15, merge=True) if blobs: # 选面积最大的色块,过滤噪点 b = max(blobs, key=lambda x: x.area()) img.draw_cross(b.cx(), b.cy(), color=(255, 0, 0)) print("gx=%d gy=%d area=%d" % (b.cx(), b.cy(), b.area()))这段代码先关闭白平衡,因为自动白平衡会自动调节颜色分量,导致同一个激光点在不同环境光下阈值失效。find_blobs里的pixels_threshold和area_threshold用来过滤只有几个像素的随机噪点,merge=True表示把相邻的同一颜色区域合并成一个整体。取面积最大的色块是按最大概率锁定真实光点,因为激光点通常会比反光点更亮更大。
3.2 双OpenMV的协作逻辑
从项目命名可以看出,first_openmv.py和second_openmv.py分别承担不同任务,这种拆分方式很符合电赛的调试节奏。
3.2.1 轨迹侧脚本:让绿点按路径移动
first_openmv.py负责绿色激光笔所在的云台。一种常见做法是开环轨迹规划:把矩形或圆形的路径离散成一系列舵机角度,按时间逐帧输出。开环控制的好处是逻辑简单,不依赖识别结果,现场定位不准时微调几个角度系数就能改轨迹。缺点是没有反馈,舵机机械误差会累积,所以这块代码通常还会在关键节点加延时或隔帧视觉校正。
另一种做法是把轨迹侧也做成闭环:OpenMV识别绿色点在A4纸上的位置,与目标路径点比较,偏差经PID换算为舵机转角。这种方式比开环稳定,但调试量也会翻倍。从这套项目只放了一个first_openmv.py来看,开环加简单校正的可能性更大。
3.2.2 追踪侧脚本:锁定绿色光点
second_openmv.py的核心逻辑是追踪。摄像头固定在红色云台上,视野中心基本就是红色激光指的位置。OpenMV持续寻找绿色色块,求出绿色点与画面中心的坐标偏差,这个偏差就是控制云台转动的输入信号。当绿色点出现在画面右侧,说明云台需要向右转;绿色点偏上,云台需要向上抬。这套逻辑可以表述为:
error_x = green_cx - img.width() // 2 error_y = green_cy - img.height() // 2这里的error_x和error_y是像素误差,不能直接当舵机角度用,需要经过比例换算和PID整形后才能输出。
3.3 像素坐标不是最终答案
很多第一次调的人会把像素误差直接乘以一个系数就给舵机,结果发现绿点明明在画面左边,舵机却转到右边,或者原地抖动。原因是像素坐标和物理角度不是线性关系。OpenMV的镜头是广角,画面边缘的像素对应更大的角度变化;而且激光笔固定在云台上时,光点移动量还取决于A4纸到镜头的距离。最稳的做法是先在几个固定位置记录“像素偏移对应舵机角度偏移”的实测值,拟合出一个近似映射系数。更精细的做法是用单应性矩阵做坐标变换,把图像坐标映射到A4纸平面坐标,再做角度换算。
4. 云台控制:从像素误差到舵机角度的完整链路
4.1 舵机控制基础:PWM与角度映射
OpenMV控制舵机通常有两种方式:直接用内置的Servo对象,或者用定时器输出PWM。电赛环境下,内置Servo对象更省事,但自定义PWM能控制更多参数,尤其方便统一两个云台的行为。舵机控制信号是50Hz的方波,脉宽1.0ms到2.5ms对应0°到180°,中间值1.5ms对应90°。
常见初始化代码如下:
from pyb import Timer, Pin # 定时器2,频率50Hz,对应舵机控制信号周期20ms tim = Timer(2, freq=50) ch = tim.channel(1, Timer.PWM, pin=Pin("P7")) def set_angle(ch, angle, min_us=1000, max_us=2500): # 将0~180度映射到1000us~2500us us = min_us + angle / 180.0 * (max_us - min_us) ch.pulse_width_us(int(us)) set_angle(ch, 90)pulse_width_us设置的是单个PWM周期里的高电平时长。舵机内部有一个电位器反馈电路,高电平宽度决定了它转动位置。写好这个函数后,云台转角就是纯粹的数学问题,不用再关心底层寄存器。对于两个云台,可以封装一个类,分别绑定不同定时器通道,避免互相干扰。
4.2 像素-角度映射与PID调参
4.2.1 比例系数怎么定
假设图像宽度是320像素,镜头水平视场角约60°,那么每个像素对应的角度近似60/320=0.1875°。如果绿点偏离画面中心40像素,云台就需要转约7.5°。但在实际项目中,云台距离A4纸大概30cm到50cm,这个近似值要现场实际测量校准。
控制代码里的比例项Kp代表“一个像素误差输出多少舵机角度”。Kp过小,云台反应慢,绿点跑出视野;Kp过大,云台在平衡点附近来回震荡,光点画圈。一般从Kp=0.1起步,观察追踪误差变化,再按0.05步进调整。
4.2.2 微分项和惯性补偿
云台带着激光笔转动,舵机自身有惯性和机械延迟,如果只靠比例控制,红色光点会滞后绿色光点一个固定的相位差。微分项Kd根据误差变化率提前减速或加速,能明显抵消滞后。Ki积分项用在静态误差场景,比如当红色光点和绿色光点距离较近但一直差一点点时,积分项会慢慢补上这个差值。但积分项非常容易导致震荡,必须加输出限幅。
可以把PID参数和输出范围做成一张调参卡,方便现场记录:
| 参数 | 建议初始值 | 调整方向 |
|---|---|---|
| Kp | 0.1 | 增大可提高响应速度,过大会震荡 |
| Kd | 0.02 | 增大可抑制超调,过大会抖动 |
| Ki | 0 | 只在静态误差明显时使用 |
| 输出上限 | ±20° | 防止舵机突然转到位 |
4.3 把PID写进OpenMV循环
一个完整的追踪控制循环应该包含采样、计算、输出三步,并且控制频率保持稳定。下面这段是追踪侧的核心结构:
import sensor, image, time from pyb import Timer, Pin # 初始化摄像头 sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) sensor.skip_frames(time=2000) # 初始化两个舵机通道 tim_yaw = Timer(2, freq=50) yaw = tim_yaw.channel(1, Timer.PWM, pin=Pin("P7")) tim_pit = Timer(3, freq=50) pit = tim_pit.channel(1, Timer.PWM, pin=Pin("P8")) kp, ki, kd = 0.1, 0.0, 0.02 last_err_x, last_err_y = 0, 0 def clamp(v, limit): return max(-limit, min(limit, v)) while True: img = sensor.snapshot() blobs = img.find_blobs([GREEN_TH], pixels_threshold=10, merge=True) if not blobs: continue b = max(blobs, key=lambda x: x.area()) err_x = b.cx() - img.width() // 2 err_y = b.cy() - img.height() // 2 # 对偏航通道做PID计算 p = kp * err_x d = kd * (err_x - last_err_x) delta_yaw = clamp(p + d, 20) yaw_us = yaw_us + delta_yaw yaw.pulse_width_us(int(yaw_us)) last_err_x = err_x # 俯仰通道相同,省略 time.sleep_ms(20)这个循环把绿色点的像素偏差转换为舵机脉宽增量,而不是绝对角度。使用增量式PID的好处是,即使初始角度设置有偏差,只要每一帧都在修正,系统也能自动收敛到目标位置。clamp函数把单次修正量限制在±20°,防止绿点特别偏时舵机猛转引起机械冲击。
5. 标定、曝光与延迟:E题翻车率最高的三个地方
5.1 激光光点的曝光和阈值
激光点打到白纸上,往往因为过曝变成一个白斑,颜色信息丢失。过曝后的白斑在LAB颜色空间里接近中性色,红色阈值和绿色阈值都识别不准确。建议在OpenMV里关闭自动曝光,手动设定曝光时间:
sensor.set_auto_exposure(False, exposure_us=12000)具体曝光值要根据现场光线微调。眼睛看激光点感觉不明显,但OpenMV的感光芯片动态范围有限,适当压低曝光能明显改善色块面积稳定性。另外,A4纸要是顺滑的铜版纸,激光光晕会很大,换成哑光卡纸效果更好。
5.2 云台零点与机械回差
舵机中位标定是调通项目的第一道坎。先把绿色激光云台的舵机调到90°,手动转动舵机臂,让激光光点落在A4纸中心,再固定舵机臂。这个过程必须在安装云台支架时完成,否则代码里所有角度都会叠加一个固定偏差。
机械回差是舵机齿轮组的固有间隙,表现为从左边转过来和从右边转过来,同一个角度指令落点不同。常见处理办法是在PID输出加上一个固定的方向补偿,或者让云台追踪方向保持单向逼近。如果红点在绿点左边,就让它从左边慢慢靠近,不要来回穿插。
5.3 串口调试与远程观察
电赛现场不能一直连屏幕,最有效的调试手段是串口打印坐标。OpenMV默认的USB口就是串口,可以用print输出绿点坐标、误差和PWM值。但高频打印会阻塞主循环,建议每10帧打印一次,并且只打印关键量:
frame_count += 1 if frame_count % 10 == 0: print("err_x=%d err_y=%d pwm_yaw=%d" % (err_x, err_y, yaw_us))拿到串口数据后,观察误差应该呈现收敛趋势。如果误差来回振荡,优先降低Kp增加Kd;如果误差恒定在一个小范围,再考虑加一点Ki。这种调试路径比盲目改参数高效得多。
6. 把源码变成自己的:复现清单与扩展方向
6.1 按顺序复现的检查清单
拿到这个zip包后,建议按照下面的顺序操作,不要一上来就烧录全部代码:
| 压缩包文件 | 作用 | 复现时的注意点 |
|---|---|---|
| first_openmv.py | 轨迹侧视觉/轨迹控制 | 先单独跑这个脚本,确认绿点能否画出矩形或圆形 |
| second_openmv.py | 追踪侧识别和PID控制 | 先用手拿着绿激光在纸面移动,测试红点是否跟随 |
| First_Servo.zip | 轨迹云台舵机控制封装 | 直接导入后检查舵机接线顺序是否和代码一致 |
| Second_Servo.zip | 追踪云台舵机控制封装 | 追踪异常时先检查云台零点和PWM频率 |
| E2023241039_*.pdf | 项目报告 | 里面记录了现场调参过程和最终误差,先读再改 |
单舵机测试通过后,才开始联调。联调时把绿色轨迹速度调到最低,让红色追踪稳定后,再逐级加速。
6.2 向更高性能扩展的路线
这套项目最值得改的地方是把追踪侧从“当前帧误差”升级为“对目标轨迹的预测”。可以在PID前面加一个简单的线性预测,用最近三帧的绿色点坐标拟合出一个速度,再把未来50ms的目标位置作为控制目标。绿点移动越平滑,这个预测效果越好。更进一步可以在OpenMV上接九轴IMU,把云台姿态角引入反馈,弱化机械回差的影响。
如果想要更通用,可以把两个OpenMV的数据都通过串口发到一块STM32或树莓派上,由上位机做单应性变换和轨迹规划,OpenMV只当传感器。这样项目就从“两个独立追踪器”变成“一个完整的视觉伺服系统”,拿来改造成课程设计或毕设的底子也更厚实。
最后说一个实际技巧:把second_openmv.p里的积分项Ki先保持为0,用KD值把云台调到轻微抖动的边界,再往回退20%,这个微调点往往是追踪效果最好的甜点区。改完参数后把整个项目导出成可烧录的工程,留给后续复用。
本文还有配套的精品资源,点击获取