资讯详情

资讯详情

2023电赛E题视觉追踪系统:像素坐标、PID与串口协议全解析

简介2023年电赛E题完整资源包面向电子信息类竞赛选手及嵌入式开发学习者尤其适合课程设计、毕设或工程实训参考。资源共5个文件两个zip压缩包分别存放主控程序与硬件驱动库两个py脚本覆盖OpenMV视觉处理的初始化及主循环便于理解摄像头采集、图像识别与串口通信的协作一份PDF设计报告则系统说明总体方案、模块划分、关键代码和测试数据方便完整复盘整个开发流程。压缩包总大小约23.67MB代码、脚本、文档齐全结构清晰下载后可直接解压运行。目前已有412人学习浏览所有源码均经博主测试验证运行稳定借助该资源既可快速上手电赛E题中视觉识别与伺服控制的开发链路也可在现有基础上扩展功能满足不同阶段的创新需求。如有使用问题可与博主直接沟通。1. 2023电赛E题把一套视觉追踪系统从 zip 包变成能跑、能调、能证明的工程备赛 2023 年电赛 E 题的队伍网盘里大概率都存过一份这样的 zip解压出来见不到统一工程只有 OpenMV 脚本、STM32 源码、几版阈值文档、零散的调参截图和答辩 PPT 半成品。E 题表面考的是“视觉追踪”真正拉开差距的是从摄像头到云台这一整条链路怎么串起来像素坐标能不能稳定换算成云台角度角度能不能被舵机快速执行丢帧时云台是保持还是乱扫。这三段里任何一段拍脑袋最终激光落点都会在目标周围画圈。这篇按“先立原理、再跑最小代码、再调参数、最后排现场坑”的顺序把 2023 电赛 E 题追踪系统的落地路径讲透适合正在备赛的参赛队也适合想快速复用一套视觉云台模板的嵌入式工程师。2. 先从原理拆开E题识别、坐标映射与云台执行的完整链路2.1 为什么E题的追踪系统要拆成三层而不是两层最直觉的写法是“看到目标偏右就向右打一点舵机”这个回路能跑但只能对付静止目标。E 题的目标运动路径是直线往返、变速甚至折线纯像素差的跳跃式驱动会带来两个必然问题第一像素偏差和舵机角速度之间没有确定映射速度没法控第二识别一旦丢帧执行端不知道该惯性继续还是停在原地。所以常见工程拆法是把系统分成三层——视觉识别层、坐标映射层、执行控制层。识别层只回答“目标在哪”输出目标中心的像素坐标、目标存在标志和一个置信度映射层负责把像素偏差换算成云台应转到的角度执行层只按角度驱动舵机内部再叠 PID。这个拆法也和赛题评分方式对得上。E 题最终看的是激光落点能否持续压住运动目标本质是“角度准确度动态响应”的叠加。三层之间用固定协议串口帧通信每层都能单独调试OpenMV 单独标定阈值STM32 单独调舵机舵量两边改完互不影响。后续几年只要赛题里出现目标追踪这套三段式结构基本不会变这也是我建议新手直接按这个框架搭而不是写裸循环的原因。2.2 坐标映射的常见简化像素偏差转角度增量映射层要做的事不复杂先看一个具体换算。假设摄像头画面中心为(cx, cy)目标中心为(tx, ty)得到像素偏差dx tx - cx、dy ty - cy。镜头水平视场角记为FOV_H_DEG图像宽度为IMG_W那么每个像素对应的角度分辨率为# 像素偏差到目标角度的换算 FOV_H_DEG 70.0 # 实际镜头的水平视场角用尺子量或查镜头手册 IMG_W 160 # QQVGA 宽度 deg_per_pixel FOV_H_DEG / IMG_W k 1.05 # 边缘畸变补偿系数画面中心取 1.0边缘略大于 1 target_yaw current_yaw dx * deg_per_pixel * k target_pitch current_pitch dy * deg_per_pixel * k这段代码里的current_yaw和current_pitch在多数队伍里没有真实角度反馈直接取上一次的目标角度作为程序内当前角度形成“程序内闭环”。这个假设很关键舵机真实位置可能会滞后但 E 题云台负载轻、速度要求不高程序内角度闭环足够用。dx的符号要特别注意OpenMV 图像坐标系原点在左上角dx为正说明目标在画面中心右侧云台应向右转也就是yaw增大dy为正说明目标偏下pitch应向减小方向调整。2.3 识别层到执行层的数据流与串口协议层与层之间传递的数据不能只在内存里搬E 题大多用串口把 OpenMV 和主控分开所以帧格式要固定。推荐一帧 6 字节的简单协议现场调试好用又不至于解析出错。字段长度说明帧头1 字节固定 0xA5模式1 字节0丢帧保持 1正常跟随 2低速巡扫dx1 字节目标中心相对画面中心横向偏差有符号dy1 字节纵向偏差有符号置信度1 字节0~255越大可信度越高置信度这一段最容易被省略它是整个系统稳定性的保险。光照变化、目标被遮挡时识别层要主动告诉执行层“我没把握”执行层此时保持上一拍角度而不是追着噪声跑。少了这 1 个字节丢帧时云台会朝错误方向猛转赛后复盘几乎没法定位是识别问题还是执行问题。主控侧解析按帧头对齐收到 0xA5 后再收 5 字节不合法数据直接丢弃避免串口错位导致整帧乱掉。3. 解包工程后的最小代码骨架先让识别跑起来3.1 E题资料包最常见的目录组织从各种渠道流出的 2023 电赛 E 题资料包解压后目录结构大同小异一般包含这几块OpenMV 侧的入口脚本、阈值配置文件、串口服务文件以及主控侧的 PID 和舵机驱动。拿到 zip 先不要急着改代码先把阈值配置从主脚本里独立出来。文件/目录常见作用main.pyOpenMV 主循环采集图像并调用识别函数thresholds.jsonLAB 阈值现场标定后单独保存uart_service.py帧封装、发送、接收应答pid/STM32 工程下的 PID 与舵机驱动README.md记录接线、串口协议和调参结论把阈值单独放文件的原因很实际比赛现场灯光从白炽灯换成日光灯阈值全变重写脚本不如改一个 JSON 再软重启。多支队伍共用一份代码时也能避免把个人现场阈值误提交进主工程。3.2 OpenMV侧色块识别与坐标输出识别层最小可用代码不用上深度学习E 题目标颜色单一LAB 颜色阈值加最大色块筛选就够了。下面这段是 OpenMV 侧的完整循环import sensor, image, time from pyb import UART sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QQVGA) # 160x120帧率优先 sensor.skip_frames(time2000) sensor.set_auto_gain(False) # 关自动增益防止阈值漂移 sensor.set_auto_whitebal(False) # 关自动白平衡 uart UART(3, 115200) # 典型室内红球阈值现场务必用 IDE 阈值编辑器重新标定 THRESHOLD_RED (30, 70, 25, 70, 10, 60) while True: img sensor.snapshot() blobs img.find_blobs([THRESHOLD_RED], pixels_threshold20, area_threshold20, mergeTrue) if blobs: b max(blobs, keylambda b: b.area()) dx b.cx() - sensor.width() // 2 dy b.cy() - sensor.height() // 2 conf min(1.0, b.area() / 800) # 面积越大可信度越高 # 有符号偏差直接取低字节STM32 侧再转回有符号数 data A5 01 01 %d %d %d\n % ( dx 0xFF, dy 0xFF, int(conf * 255) 0xFF) else: data A5 01 00 00 00 00\n # 丢帧保持 uart.write(data) print(data)这段代码里最值得解释的是set_auto_gain(False)。OpenMV 默认自动增益会让每一帧图像的亮度不同导致同一个目标的 LAB 值在亮帧和暗帧里差别很大阈值现场标定完过两分钟就识别不稳。关掉之后再手动固定曝光阈值才有一致性。pixels_threshold20用来滤掉散点噪点如果现场有反光碎片可以调到 40。mergeTrue会把相邻色块合并成一个大块防止目标被拆成两块导致中心偏移。在合理光照下QQVGA 分辨率实测帧率大多在 35~50fps 之间这个帧率足够支撑后面的 PID 控制。提示阈值不要直接在代码里改。用 IDE 的“阈值编辑器”打开一张现场截图拖动六个滑条把目标单独抠出来再把结果回填进THRESHOLD_RED。现场换光源后重新走一遍这个流程比重刷固件快得多。3.3 STM32侧串口解析帧并映射到舵机角度主控侧接收串口帧并做初步处理的 C 代码按 2.3 节的协议实现uint8_t buf[6]; // 帧头对齐 连续收满 6 字节 if (HAL_UART_Receive(huart3, buf[0], 1, 10) HAL_OK buf[0] 0xA5) { if (HAL_UART_Receive(huart3, buf[1], 5, 10) HAL_OK) { uint8_t mode buf[1]; int8_t dx (int8_t)buf[3]; // 低字节转有符号 int8_t dy (int8_t)buf[4]; uint8_t conf buf[5]; if (mode 0 || conf 50) { // 丢帧或置信度过低 servo_hold(last_yaw, last_pitch); } else { target_yaw last_yaw dx * deg_per_pixel * k; target_pitch last_pitch dy * deg_per_pixel * k; last_yaw target_yaw; // 程序内角度闭环 last_pitch target_pitch; servo_move_to(target_yaw, target_pitch); } } }(int8_t)buf[3]的写法把无符号低字节解释为有符号数对应 OpenMV 侧dx 0xFF两边符号语义一致。conf 50的阈值来自 2.3 节的置信度字段相当于要求识别层面积极大约占全画面的 6% 以上才执行动作。这里有个容易踩的坑主控判断“丢帧保持”时舵机要保持的是最后一次有效目标对应的角度而不是当前 PWM 输出值否则保持的是滞后的旧角度。另外如果舵机是 MG996R 这类模拟舵机机械行程有限target_yaw超过限位后要提前CLAMP不然舵机堵转会发热并烧坏舵机驱动板。4. 云台跟得上的关键PID调参与三档速度响应4.1 为什么识别准了云台仍然抖识别输出每帧刷新但把每个新目标角度直接砸给舵机输入信号就是阶跃串。机械结构对阶跃输入的响应天然是过冲加振荡表现在激光点上就是左右横摆看起来像识别在抖实际是执行层没有平滑。常见做法是在角度环上叠位置式 PID但要注意一个原则不要对像素偏差做 PID要对角度目标做 PID。对像素做 PIDOpenMV 帧率波动会原样进控制环对角度做 PID识别帧率和控制频率解耦主控可以按固定周期 20ms 跑一次 PID识别多少帧都不影响控制稳定性。4.2 位置式PID的最小实现通用的位置式 PID 增量形式入库前做个积分衰减和输出限幅就够了typedef struct { float kp, ki, kd; float integral, last_err; } PID; float pid_update(PID *pid, float target, float current, float dt) { float err target - current; // 0.95 为积分衰减系数防长时间丢帧导致积分饱和 pid-integral pid-integral * 0.95f err * dt; float deriv (err - pid-last_err) / dt; pid-last_err err; float out pid-kp * err pid-ki * pid-integral pid-kd * deriv; // 输出限幅防止角度指令超出舵机行程 if (out PWM_LIMIT_HIGH) out PWM_LIMIT_HIGH; if (out PWM_LIMIT_LOW) out PWM_LIMIT_LOW; return out; }积分衰减系数0.95是这里最关键的一行。E 题现场目标可能会短暂离开视野此时角度目标保持不变普通积分项会一直累加目标重新出现时积分已经冲到饱和云台会有一个明显窜动。加了衰减后积分项每拍缩水 5%长时间保持同一角度时积分自动回落。调参顺序固定先只给kp从小到大试到云台出现轻微来回振荡此时kp约为临界值再给kd把振荡压住最后给少量ki消除静态误差ki通常是kp的 1/20 起步不要一上来就按相等量级给。4.3 PID起点参数表与三档速度切换不同舵机和供电条件下参数绝对值没有意义下面的起点参考表按目标运动速度分为三档能覆盖 E 题绝大多数场景。模式触发条件KpKiKd适用场景低速保持目标像素位移 3px/帧0.600.1目标静止或极慢移动正常跟随位移 3~10px/帧1.20.050.25直线匀速往返段快速响应位移 10px/帧1.80.080.4启停瞬间或变速段三档切换的执行代码放在主控侧切换依据是相邻两帧目标中心像素位移估计的速度。OpenMV 端只需要在串口帧的置信度字段之后附带一个速度等级位或者在 main.py 里直接按位移阈值设置模式字段speed abs(dx - last_dx) / dt if speed 3: mode 0 # 低速保持 elif speed 10: mode 1 # 正常跟随 else: mode 2 # 快速响应速度档位切换时要注意dt必须和实际帧间隔一致。如果你在 OpenMV 里用time.clock()测到帧率只有 25fps那么dt就按 0.04s 算而不是想当然的 0.02s。档位切换那一拍 PID 的last_err要从当前误差重新初始化否则上一档的巨大误差会被微分项带进来产生一次跳变。5. zip里看不到的坑过曝、帧率与工程版本管理的验证技巧5.1 过曝识别“融化”的现场验证激光发射器打在白纸上过曝时画面中心会整片泛白RGB 三个通道同时饱和LAB 阈值看到的目标不再是圆斑而是“融化”的大色块。验证办法是打印最大色块的面积正常红球目标在 QQVGA 下大约 200~600 像素过曝后直接上千。处理方式是固定曝光而不是继续调阈值sensor.set_auto_exposure(False, exposure_us10000) # 从 5000 到 20000 试曝光时间从 5000us 起调每档加 1000us直到目标轮廓清晰且不至于丢失暗色边缘。固定曝光后昼夜光线变化只能靠重新标阈值兜底这就是阈值独立成文件的意义。5.2 帧率与串口波特率的匹配验证160x120 RGB565 单帧图像 38.4KB而串口每帧只有 6 字节115200 波特率下 6 字节约 0.5ms 发送耗时完全不是瓶颈。真正的限制在 OpenMV 内部处理时间。验证帧率的做法是在循环里用time.ticks_ms()计算实际帧间隔打印出来如果低于 30fps后续 PID 所有动态参数都不可信。此时优先把pixels_threshold调大减少候选色块数量再考虑降分辨率到 QVGA。5.3 把 zip 解压出来的工程挂成 git变基失败怎么处理从网上下载的 E 题资料包基本都不是 git 仓库改完代码想推到自己的远程仓库常遇到failed to push some refs原因是远程已有提交而本地历史不相关。正确的关联顺序是这样git init git add -A git commit -m import from zip git remote add origin 仓库地址 git fetch origin git branch --set-upstream-toorigin/main main git pull --rebase origin main git push origin mainfetch先把远端提交拉到本地但不合并--set-upstream-to建立跟踪关系pull --rebase把本地那一次 import 提交重放到远端最新提交之上历史是一条直线而不是两个分叉。出现冲突时多半是远端已经有人改过同名文件保留自己改动的那几个源文件其余以远端为准。5.4 加密 zip 先看注释再动手部分资料包发布时会设密码密码通常写在下载页或包内 README 注释里先用 7-Zip 打开看右侧注释栏或者直接联系发布者不要急着找暴力破解类工具E 题资料的核心价值在源码结构和调参记录为一份可能过时的赛题资料花几个小时跑字典破解完全不值。跑通上面这些验证后再回到比赛中你最该关心的只剩一个变量thresholds.json 里的三组 LAB 阈值。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →