十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

OpenMV视觉识别与PID控制在板球控制系统中的实战设计

OpenMV视觉识别与PID控制在板球控制系统中的实战设计 简介这套板球控制系统项目以STM32F103C8T6和OpenMV为核心面向全国大学生电子设计竞赛参赛者及嵌入式视觉开发者注释丰富、结构清晰便于快速读懂并二次开发。资源包共189个文件包含STM32端C源文件、头文件、Keil工程文件、编译生成的hex/axf/map以及OLED显示与串口通信相关代码整体仅3.31MB解压后即可打开工程查看。系统利用OpenMV进行颜色识别通过串口把目标坐标传给STM32完成板球控制OLEDIIC协议提供实时显示控制效果达到过一等奖水平但受摄像头清晰度与帧率限制尚未完美有条件可升级硬件。目前已有1988人学习下载适合希望在竞赛中快速搭建原型或深入研究视觉-运动控制结合的开发者。需注意本工程不含OpenMV侧程序需自行用颜色识别DEMO配合串口发送坐标。1. 赛题拆解与方案选型为什么是OpenMV1.1 板球控制系统到底在考什么全国大学生电子设计竞赛里的板球控制系统属于典型的“视觉控制”综合题。题目核心描述起来很简单一块平板下方用两个舵机或步进电机支撑板面上放一个小球系统要控制板子的倾斜角度让球从起点滚到指定位置或者沿给定轨迹走一圈。听起来像个小玩具但真要拿高分背后涉及机器视觉、坐标变换、PID控制、通信协议、机械结构设计一整条链路。我当年备赛最深的感受是这道题真正拉开差距的不是谁PID调得熟而是谁能在有限时间内把“视觉识别”和“运动控制”稳定地串起来。很多队伍倒在了第一步——球都识别不稳后面积分分离、前馈补偿全成了空中楼阁。所以视觉方案选型直接决定了整个项目的上限和下限。1.2 视觉方案的取舍OpenMV对比K210、树莓派先说结论如果现在再让我选一次我仍然会选OpenMV作为视觉前端。理由很实在OpenMV在电赛场景下有不可替代的优势对比维度OpenMV树莓派K210 MaixPy开发上手速度快MicroPython直接写较慢需要Linux环境中等资料相对散图像处理实时性中等QVGA帧率30fps左右高但系统开销大较高自带KPU功耗与供电低5V USB即可高需要独立电源低但开发板兼容性参差与主控通信串口/SPI/I2C协议简单需要额外处理看具体板子电赛资料丰富度高历年赛题大量方案多但偏通用少尤其针对板球控制注释友好与源码可读性高Python代码直观看工程习惯中等树莓派性能确实强但在电赛这种时间就是分数的赛场上光环境配置就能折腾掉一天。K210性能不错但资料太散遇到问题很难快速找到参考。OpenMV在“够用”和“好用”之间拿捏得最稳。它跑MicroPython图像处理API高度封装色块识别、圆形检测这些高频需求基本一条指令就能调出来。再加上OpenMV官方文档针对巡线、圆形追踪、AprilTag定位有大量示例代码套用到板球控制只需要改改阈值和坐标映射逻辑。1.3 整体系统架构与数据流我们的最终方案是用OpenMV做视觉前端STM32F407做运动控制核心。之所以坚持分离式架构而不是让OpenMV直接控制舵机是因为控制周期不能被视觉处理的不确定性干扰。视觉处理偶尔会卡一下帧但控制环路必须稳定运行否则PID参数整定就是白费功夫。系统数据流是这样的OpenMV负责采集图像识别小球位置和靶心坐标然后通过SPI把球心坐标、靶心坐标、小球半径这几个关键数据一次性打包发给STM32。STM32收到后把像素坐标转换成平板坐标系下的物理坐标再算出当前误差喂给PID控制器输出给舵机调整平板角度。整个过程一帧一周期控制在20到33毫秒内完成。这套架构最核心的设计哲学是OpenMV只管“看”STM32只管“动”。视觉端代码再乱只要能在规定时间给出准确坐标控制端就能稳定工作。反过来控制端不管图像处理逻辑有多复杂只认坐标输入。这样的解耦让两队人可以并行开发联调时问题定位也快。2. 硬件搭建与OpenMV基础配置2.1 物料清单与机械结构要点板球控制系统的硬件不算复杂但每一步都有坑。先说物料清单OpenMV Cam H7 Plus推荐Plus版本帧缓冲更大图像处理更稳STM32F407核心板或者任何你熟悉的Cortex-M4平台开发板2个MG996R舵机平板支撑用扭矩足够平板板面推荐亚克力板或轻质木板尺寸不要太大A4左右最佳万向球头或者球铰连接件连接舵机臂和板面5V/3A电源模块舵机瞬间电流很大必须要稳压OpenMV与STM32之间的连接线若干机械结构上我踩过最大一个坑是板面的支撑方式。一开始用了两个舵机直接推板面结果舵机运动范围有限板面倾角根本不够球动不动就原地不动或者加速过快。后来改成球铰连接用舵机臂推动板面边缘通过杠杆放大行程效果立刻不一样了。板面尺寸控制在300mm×200mm左右太大惯性大控制滞后严重太小球容易滚出去几乎没有容错空间。2.2 OpenMV上电与固件检查OpenMV上电这块很多人觉得简单到不值得写但电赛现场因为供电问题翻车的队伍我见过不止一次。OpenMV支持三种供电方式USB直接供电、通过排针接5V引脚供电、通过VIN接电池供电。USB供电最省事调试阶段直接用USB连电脑就行。但上板联调时OpenMV和舵机共用一个电源舵机一转动电压瞬间被拉低OpenMV直接掉电重启这是一定会遇到的问题。解决思路很简单给OpenMV单独用一个USB充电头供电或者用降压模块从主电源分一路独立稳压给OpenMV。电赛现场我是准备了两个充电宝一个专门喂OpenMV一个给STM32舵机单独接大功率电源。三路独立供电互不干扰。另外一个容易忽略的点是OpenMV在持续工作一段时间后会热外壳摸着烫手这时候图像传感器噪声会明显变大色块识别开始闪烁。建议备一个小的USB风扇对着摄像头吹实测能显著提高长时间运行的稳定性。固件检查方面新的OpenMV板子基本开箱即用。连接电脑后打开OpenMV IDE如果能看到摄像头实时画面说明固件正常。如果显示找不到设备先换USB线再检查驱动。这里要特别强调OpenMV IDE里点“连接”按钮之前要先给板子断电再重新上电很多连接失败是因为板子处于开机自检的忙碌状态复位一下就好。2.3 OpenMV与主控的SPI通信实战OpenMV支持串口、SPI、I2C三种通信方式。板球控制这种场景我强烈推荐SPI。原因很简单数据量大、频率高、实时性要求强。串口一帧数据要用ASCII编码再解析效率低且容易出错SPI是硬件级全双工直接发二进制数据配合DMA几乎不占CPU。OpenMV在SPI链路中通常作为从机STM32作为主机。这样设计是因为STM32的控制循环需要主动拉取视觉数据而不是等OpenMV推送。OpenMV端拉低CS引脚后只需要不断监听主机的时钟信号收到请求就往发送缓冲区填充数据。OpenMV端SPI从机初始化代码的核心逻辑如下from pyb import SPI, Pin import struct # 初始化SPI2为从机模式8位数据宽模式0 spi SPI(2, SPI.SLAVE, polarity0, phase0, bits8) cs Pin(P3, Pin.IN, Pin.PULL_UP) while True: # 等待主机发送请求帧收到0xA5表示请求坐标数据 data spi.recv(1) if data and data[0] 0xA5: # 打包坐标x(2字节) y(2字节) 半径(2字节) buf struct.pack(hhh, ball_x, ball_y, ball_r) spi.send(buf)注意SPI模式必须主从一致。STM32那边如果配置成了模式3OpenMV这边还是模式0通信就会全乱。联调前先确认两边polarity和phase设置完全相同。STM32主机端因为我不想阻塞主循环所以用的SPI加DMA。这里有一个非常实用的经验主机每20ms发送一次请求帧如果超过50ms没收到OpenMV的响应就判定视觉超时控制系统沿用上一次的坐标数据并置一个置信度标志。这样即使OpenMV偶发卡顿舵机也不会因为收不到数据而剧烈抖动。3. 视觉识别代码详解大量注释版3.1 球体识别的核心思路板球控制系统里的视觉识别任务本质上是两个找球、找靶心。球和靶心在图像里呈现为色块或圆形区域最直接的思路是用find_blobs()做色块检测。这个函数OpenMV封装得非常好输入一个阈值元组它就返回所有满足颜色范围的连通区域。但实际场景比想象中复杂。比赛现场的灯光不是稳定的光线忽明忽暗会导致同一颗球在不同时刻的颜色阈值偏差很大。我的解决办法是“动态阈值校正”在板上固定几个已知颜色的参考点每启动一次系统就先用参考点重新校准阈值。当然更简单的做法是直接用OpenMV IDE里的“阈值编辑器”工具把球放在板上手动调到刚刚好圈住球然后保存阈值。比赛现场如果时间紧这个手动方案也够用。色块识别核心代码我加了很详细的注释import sensor, image, time # 黄球的阈值格式为LAB颜色空间的 (L_min, L_max, A_min, A_max, B_min, B_max) # 这个值需要根据实际球的颜色用阈值编辑器调 yellow_threshold (30, 80, 15, 60, 30, 90) sensor.reset() sensor.set_pixformat(sensor.RGB565) # RGB565格式便于颜色识别 sensor.set_framesize(sensor.QVGA) # 320x240分辨率兼顾精度与速度 sensor.skip_frames(time2000) # 跳过前2秒等待自动白平衡稳定 sensor.set_auto_whitebal(False) # 关闭自动白平衡防止颜色漂移 # 关闭镜头畸变校正减少运算量 sensor.set_lens_correction(False) clock time.clock() def find_ball(): img sensor.snapshot() blobs img.find_blobs([yellow_threshold], pixels_threshold100, area_threshold100) if blobs: # 取面积最大的色块通常就是球 largest_blob max(blobs, keylambda b: b.area()) # 在图像上画个框方便调试 img.draw_rectangle(largest_blob.rect()) img.draw_cross(largest_blob.cx(), largest_blob.cy()) return largest_blob.cx(), largest_blob.cy(), largest_blob.w() / 2 return None while True: clock.tick() result find_ball() if result: x, y, r result print(ball: (%d, %d) r%d % (x, y, r))一个关键细节set_auto_whitebal(False)。不开这个OpenMV会在每帧画面里自动调整白平衡导致同一个黄色球在不同时间识别出来的颜色阈值飘忽不定。关闭之后颜色一致性会大幅提高这是色块识别稳定性的分水岭操作。3.2 坐标提取与滤波处理拿到色块的中心坐标后不要直接用。原始坐标存在大量噪声原因有两个一是传感器的ISO噪声二是球在板上滚动时色块边缘的像素点会抖动导致质心偏移。直接用原始坐标计算误差PID会跟着噪声疯狂抖动舵机声音会很难听控制效果也会很差。我用的滤波器是“一阶低通中值滤波”组合。中值滤波解决突发性异常值比如某一帧误识别到背景里的干扰目标一阶低通解决持续性的随机噪声。具体实现如下# 中值滤波器缓存最近5帧坐标取中值抗突发异常 class MedianFilter: def __init__(self, size5): self.buffer [] self.size size def process(self, value): self.buffer.append(value) if len(self.buffer) self.size: self.buffer.pop(0) sorted_buf sorted(self.buffer) return sorted_buf[len(sorted_buf) // 2] # 一阶低通滤波器alpha越小越平滑但响应变慢 class LowPassFilter: def __init__(self, alpha0.4): self.alpha alpha self.last_value None def process(self, value): if self.last_value is None: self.last_value value else: self.last_value self.alpha * value (1 - self.alpha) * self.last_value return self.last_value # 使用示例 median_x MedianFilter(5) lp_x LowPassFilter(alpha0.4) smooth_x lp_x.process(median_x.process(raw_x))这里alpha的调整逻辑需要解释一下。alpha越大滤波器响应越快但对噪声的抑制越弱alpha越小越平滑但会有明显的延迟。板球控制中球的运动速度不算快20ms一帧的控制周期里40ms左右的延迟是可以接受的所以我选alpha0.4。如果你的球速很快适当增大到0.6牺牲一点平滑度换取响应速度。3.3 巡线识别与综合判断逻辑备赛后期如果追求更高分数题目往往会有“沿轨迹运动”的环节。这时候OpenMV就需要识别板面上的轨迹线。OpenMV官方有find_lines()和巡线示例但我实际测试后发现直接find_lines()对直线效果好对圆环、S弯这类轨迹识别效果一般。更稳妥的方案是用“阈值分割最大连通域”的方式。我们当时在板面上贴了一圈黑色电工胶带作为轨迹OpenMV找色块时先通过LAB阈值把黑色轨线提取出来然后找所有满足阈值的像素点集合里面积最大的那个连通域再把连通域的骨架提取出来形成轨迹线。这里有个特别容易犯的错误黑色标签在图像里的阈值和球很容易混。因为球是黄色阈值在LAB空间中离黑色很远理论上不会冲突。但如果球正好滚到轨迹线上黑色和黄色同时存在find_blobs会一次性返回多个色块。处理逻辑必须要加“面积最大优先位置预测辅助”的判断如果当前帧同时识别到黄色块和黑色块优先选择与上一帧球位置最近的黄色块。这个预测机制用简单的欧几里得距离就能实现非常有效。4. 控制策略与调参实战4.1 PID控制器的离散化实现PID控制是板球系统的灵魂但很多同学在电赛时对PID的理解停留在“调三个参数”的层面结果就变成了玄学调参。实际上数字PID的实现本身非常固定关键在积分限幅、微分滤波这些看似次要的细节。我用的位置式PID离散化公式是output Kp * error Ki * integral Kd * derivative其中integral是误差的累加derivative是误差的变化率。这个公式看着简单但直接实现会有两个坑第一个坑是积分饱和。球被挡在目标点附近动不了的时候误差持续存在积分项会一直累加最终导致舵机输出饱和等障碍移开时系统要很久才能恢复。解决办法是积分限幅把积分值限制在某个范围内超出部分直接截断。第二个坑是微分项放大噪声。球的像素坐标经滤波后依然有微小抖动直接算差分噪声会被瞬间放大。最好对微分项再做一个一阶低通滤波或者使用“测量值微分”而不是“误差微分”能明显减小噪声。核心实现class PID: def __init__(self, kp0.0, ki0.0, kd0.0, integral_limit100.0): self.kp kp self.ki ki self.kd kd self.integral_limit integral_limit self.last_error 0.0 self.integral 0.0 self.last_output 0.0 def update(self, error, dt): dt max(dt, 0.001) # 防止dt为0导致除零 self.integral error * dt # 积分限幅 if self.integral self.integral_limit: self.integral self.integral_limit elif self.integral -self.integral_limit: self.integral -self.integral_limit derivative (error - self.last_error) / dt self.last_error error output self.kp * error self.ki * self.integral self.kd * derivative return output4.2 坐标变换与目标点映射OpenMV给出的坐标是像素坐标原点在图像左上角x轴右正、y轴下正。但这个坐标不能直接给PID用因为板面物理坐标系和图像坐标系之间存在两个问题原点位置不同、尺度不同。我的做法是先做一个标定把板面左上角、右下角在图像里的像素坐标找出来然后做线性映射。假设板面物理坐标范围是X∈[0,300]mmY∈[0,200]mm相应在图像中对应像素范围是X∈[20,300]Y∈[30,210]那么映射公式就是physical_x (pixel_x - pixel_x_min) / (pixel_x_max - pixel_x_min) * physical_width physical_y (pixel_y - pixel_y_min) / (pixel_y_max - pixel_y_min) * physical_height这个映射在STM32端做。实测下来只要OpenMV安装角度固定映射误差在2mm以内完全够用。安装角度如果变化了重新标定一次就行。这里有一个经验OpenMV安装时尽量让镜头光轴垂直于板面。如果倾斜太大透视效应会导致板面中心的映射误差增大。安装好后用螺丝固定死不要依赖双面胶。4.3 调参顺序与经验值参考PID调参是很多人头疼的部分但按照固定顺序做会快很多。我的调参经验如下先只加P从小到大慢慢增加直到球能往目标点方向移动但开始震荡。记下这个P的临界值然后取它的60%-70%作为系统P值。接着加DD的作用是抑制震荡。慢慢增加D直到球接近目标点时能平稳停住不再来回摆动。最后加I。I的作用是消除稳态误差也就是球在目标点附近小范围徘徊但不精确到位的情况。我最终的参数参考值控制轴KpKiKdX轴350.812Y轴320.610注意这些参数只对特定板面大小、舵机响应速度有效。如果你的板子尺寸不同参数会有明显变化。但调的思路是一样的千万不要照抄。还有一个被很多人忽略的细节控制周期。我前面提到OpenMV约20ms发一次数据STM32控制循环也跑在20ms左右那dt就取0.02。如果控制周期不稳定PID的dt参数会引入随机误差。所以在STM32端最好用定时器中断固定20ms执行一次控制而不是在while(1)里靠延时凑后者误差太大了。5. 常见问题排查与避坑指南5.1 视觉识别的典型问题与处理视觉这块最容易翻车的就是阈值漂移。第一天晚上调好的阈值第二天白天到赛场一测球直接识别不到了。原因就是灯光变了。解决思路有两个一个是开赛前到现场使用OpenMV IDE重新调整阈值花十几分钟另一个是在代码里加入“自动曝光锁定”把传感器的曝光时间、增益固定住不随环境光自动变化。这个操作能显著减少阈值漂移问题。如果现场光线实在太差打一个大功率白光LED在板面上方效果立竿见影。另一个典型问题是镜头反光。球在滚动时表面会有高光这些高光区域呈现白色会导致球体看起来不连续色块识别被拆成碎片。处理手段是球上喷一层哑光漆或者用磨砂材质的球。如果不想换球就在find_blobs里把色块的合并阈值调大一点让相邻的碎片合并成一个色块。我当时是给球裹了一层磨砂塑料外壳识别效果立刻稳定很多。5.2 通信与供电的隐性坑SPI通信的坑多半出在电平不匹配上。OpenMV的IO口是3.3V电平如果你的STM32是5V容忍的还好如果是纯5V电平直接接上去可能烧OpenMV的IO口。稳妥的方案是加一个3.3V转5V的电平转换模块或者直接用3.3V供电的STM32系列。供电方面的坑前面提过这里再补充一条舵机供电电流不足时舵机不仅会抖还会反向干扰控制信号。如果你的舵机用的是LM2596降压模块一定要把电流调大到3A以上或者干脆配一个独立的开关电源模块。千万别指望STM32的5V引脚去带动两个MG996R舵机那个电流量级是灾难级的。5.3 竞赛现场的应急技巧比赛现场时间紧不可能像备赛时那样从容调参。我总结了两条应急技巧第一提前准备好“一键恢复默认阈值”的按键处理。在OpenMV上接一个按键按一下就把阈值恢复成赛前标定好的出厂值。如果现场调试把阈值调乱了一键就能回到稳定状态不用慌着重新标定。第二准备一份“快速改参数”的菜单。用OpenMV IDE的串行终端配合按键在开发板上直接调整KP、KI、KD的值不用反复插拔烧录器。实测这个操作能极大缩短联调时间毕竟电赛最后一晚拼的就是谁在有限时间内迭代得快。最后一个细节比赛前把所有线材用扎带固定好防止舵机转动时拉扯到OpenMV的数据线。有一次联调时OpenMV突然掉线排查了半小时才发现是舵机转动把SPI线拽松了。这种事情不发生在赛场上就是万幸发生了真的欲哭无泪。本文还有配套的精品资源点击获取
返回列表