资讯动态

视觉定位与PID控制实战:从零搭建平衡球机器人系统

发布时间:2026/9/24 9:53:50 来源:尧图企业网站定制
1. 项目概述与整体设计思路1.1 这个项目到底在做什么平衡球机器人简单来说就是一块板子上放一个球通过倾斜板子让球保持在指定位置或者沿着预设轨迹滚动。听起来好像不难但你真正做起来就会发现这是一个把视觉、控制、机械、嵌入式全部串起来的完整闭环项目写进简历、放在博客里都非常有分量。整个系统的核心链路是这样的摄像头采集图像通过对颜色或形状的识别锁定球在图像中的像素坐标再通过标定把像素坐标换算成板子坐标系下的物理坐标然后把这个坐标和期望位置做差作为PID控制器的误差输入PID输出的控制量经过限幅和映射最终转换为舵机或者电机的PWM信号通过倾斜板子来改变球的受力状态形成闭环。这个项目的关键词有两个一个是视觉定位一个是PID控制。视觉定位负责感知PID控制负责决策。很多人分开做这两个模块都没问题——OpenCV调个色块追踪或者写个PID让电机转起来都不难。但把两者真正接起来让系统在动态扰动下依然稳定这才是整个项目最磨人的地方也是它最有价值的地方。1.2 系统组成与硬件选型我手里的这套系统配置供你参考非常典型也适合大多数入门者复现模块选型作用主控STM32F407跑PID控制逻辑、PWM输出、串口通信视觉普通USB摄像头 上位机(PC)图像采集、球体识别、坐标解算执行器2个数字舵机 万向节连杆双轴倾斜平台球橙色乒乓球高对比度便于视觉识别结构亚克力板或木板 3D打印连接件平台本体这里需要说明一个设计取舍我做的是上位机下位机架构即OpenCV在PC上处理图像把目标偏差通过串口发给STM32STM32负责跑PID循环。为什么不直接在STM32上做视觉因为嵌入式视觉的门槛和成本都更高。用F407跑简单的色块识别也不是不行但开发效率低调试不方便而且帧率很难提上去。分开了之后上位机只管识别下位机只管控制两边可以独立调优出问题了也容易定位。提示如果你用的是OpenMV或者K210这类带视觉能力的模组也可以省掉上位机整个系统的实时性会更好但开发方式会从PC IDE Opencv变成MicroPython 固件各有优劣看你自己更擅长哪条路线。2. 视觉定位模块先让机器人看见球2.1 摄像头选型与图像采集的坑摄像头这块我踩过一个很大的坑一开始用手机摄像头开USB调试结果帧率波动特别大有时候一卡就是几百毫秒PID一算基于的是过期的数据系统直接发散。后来换成一个固定的USB免驱摄像头1080P下跑30帧完全没问题。但这里有个细节分辨率不是越高越好。图像越大处理耗时越长FPGA你检测延时越大。检测延时一旦超过PID控制周期的1/3这个系统就很难调稳了。我最终采用的是640x480分辨率在这个尺度下球体占图像面积仍然足够识别稳定而且处理速度能跑到40到50帧。还有一个容易被忽略的点是摄像头的安装位置。理论上摄像头要固定在平台上空垂直于板面但这个在物理上很难做到——如果你把摄像头装在支架上那支架必然和运动的板子有耦合。我的方案是把摄像头固定在整体机架顶部和板子的转动中心刚性连接这样摄像头看着板子的视角是恒定的标定关系不会随倾斜角度变化而漂移。2.2 球体识别我用的是HSV阈值轮廓筛选球体识别我用的是最常见的HSV颜色空间阈值分割。BGR空间下光线变化对颜色影响太大同样的橙色球在阴影里和强光下RGB三通道的数值能差出一倍多。而HSV空间把色调(H)和亮度(V)分开了只要光照不偏色得离谱只做色调饱和度阈值就能稳定把球分割出来。处理流程是把BGR图像转换为HSV图像。设定橙色的H、S、V阈值范围生成二值掩膜。对掩膜做一次形态学开运算去掉噪点。用findContours提取轮廓筛选出面积最大的那个。用minEnclosingCircle或者轮廓的质心作为球的中心坐标。贴一段我当时用的核心代码方便你直接起步import cv2 import numpy as np cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) # 橙色球的HSV阈值 lower_orange np.array([5, 100, 100]) upper_orange np.array([25, 255, 255]) while True: ret, frame cap.read() if not ret: break hsv cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) mask cv2.inRange(hsv, lower_orange, upper_orange) # 开运算去噪 kernel np.ones((5, 5), np.uint8) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel) contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if contours: # 选面积最大的轮廓 largest max(contours, keycv2.contourArea) area cv2.contourArea(largest) if area 200: # 过滤小噪点 M cv2.moments(largest) if M[m00] ! 0: cx int(M[m10] / M[m00]) cy int(M[m01] / M[m00]) cv2.circle(frame, (cx, cy), 10, (0, 255, 0), -1) cv2.imshow(frame, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这里有几个非常关键的细节第一H的范围在OpenCV里是0-179不是0-360所以橙色的色相范围大概在5到25千万不要按360度制去设。第二面积阈值一定要有。我遇到过光照突变时地面反光也被分割出来如果只取最大轮廓而不设面积下限球一旦不在视野里系统会把反光当球然后整个控制就飞了。第三如果你想要更鲁棒可以加卡尔曼滤波追踪球的运动。这个不是必须的但当球被手短暂挡住、或者识别偶发丢帧时卡尔曼可以预测位置避免PID拿到一个跳变的误差。2.3 坐标映射从像素坐标到板面坐标识别出球的像素坐标之后你还需要把它映射到板子坐标系下。最粗糙的做法是等比例缩放比如图像是640x480板子是400mm x 300mm那么x方向比例系数是400/6400.625 mm/像素y方向是300/4800.625 mm/像素。这种办法能用但精度不够尤其是摄像头没有正对着板面的时候图像会有透视畸变。正确的做法是用OpenCV的标定函数找四个板面角落的像素位置然后用getPerspectiveTransform计算单应性矩阵或者用solvePnP做更完整的位姿解算。我当时用的是四点标定# 板面上四个角点在图像中的像素坐标 src np.array([[130, 80], [510, 80], [540, 420], [100, 420]], dtypenp.float32) # 对应的物理坐标单位mm dst np.array([[0, 0], [400, 0], [400, 300], [0, 300]], dtypenp.float32) matrix cv2.getPerspectiveTransform(src, dst) # 然后每个球坐标都做一次透视变换 ball_physical cv2.perspectiveTransform( np.array([[[cx, cy]]], dtypenp.float32), matrix)注意标定点的准确选取直接决定了系统的定位精度。建议把打印好的A4纸标记贴在板面四个角然后尽量选取图像的像素中心位置避免在图像边缘对标定因为广角畸变在边缘最严重。3. PID控制让板子学会伺候球3.1 位置式PID和增量式PID该怎么选视觉定位拿到了球的坐标下一步就是如何让板子倾斜让球运动到目标位置。这就到了PID控制出场的时候。PID的核心思想是根据误差的比例(P)、积分(I)、微分(D)三项来决定输出。用大白话说比例项看现在差多少积分项看以前欠了多少微分项看误差正在以什么趋势变化。在平衡球项目里我推荐用增量式PID而不是位置式PID。原因有两个第一增量式PID的输出是舵机角度的增量不是舵机的绝对角度这意味着切换目标点的时候不会产生巨大的阶跃输出球不会突然被猛推一下飞出去。第二增量式PID天然带有记忆效应因为它是累加上去的可以避免位置式PID中积分项因为目标突变造成的系统饱和。增量式PID的离散公式是Δu(k) Kp * [e(k) - e(k-1)] Ki * e(k) Kd * [e(k) - 2*e(k-1) e(k-2)] u(k) u(k-1) Δu(k)对应STM32上的实现我用的是float变量确保精度够typedef struct { float target; float current; float err; float err_last; float err_prev; float kp; float ki; float kd; float out; float out_last; } PID_TypeDef; float PID_Calc(PID_TypeDef *pid, float current, float target) { pid-target target; pid-current current; pid-err target - current; pid-out pid-out_last pid-kp * (pid-err - pid-err_last) pid-ki * pid-err pid-kd * (pid-err - 2 * pid-err_last pid-err_prev); pid-err_prev pid-err_last; pid-err_last pid-err; pid-out_last pid-out; return pid-out; }3.2 KP、KI、KD各自的角色很多人一上来就直接用试凑法调参然后发现怎么调都震荡。实际上应该先弄清楚每一个参数在这个系统里到底起什么作用。比例项KP决定了系统响应的力度。KP越大板子越敏感球偏移一点就会产生很大的倾斜角来纠正它。但KP不能无限加大因为舵机有物理响应极限而且球本身就是个纯积分环节——你倾斜板子球是加速度变化而不是速度直接变化一旦KP过大很容易形成持续振荡。积分项KI用来消除稳态误差。理想情况下如果球停在偏离目标点5mm的位置比例项和微分项给不出足够的纠正力这时候积分项会慢慢积累误差持续输出一个纠正角度直到把球推回目标点。但是KI过大会引起积分饱和表现就是球围绕目标点持续小幅度来回摆动很像低速下的抖动很难受。微分项KD是给系统加阻尼的。平衡球系统最大的问题就是球有惯性如果没有微分项球冲过头了再纠正就会来回震荡。KD能感知误差的变化趋势在误差还在增大时提前输出反向力。但KD对噪声非常敏感如果你的视觉定位有1个像素的抖动微分项算出来的值可能是实际值的几十倍所以KD通常需要配合低通滤波使用。3.3 为什么最终用的是级联PID单环PID能不能玩转平衡球在理想条件下能但效果非常勉强。原因是板子的倾斜才直接改变球的加速度而单环PID直接把球的位置误差映射到板子的倾斜角相当于让一个比例环节去控制一个二阶积分环节的中层量相位裕度不够。所以我用级联PID也叫串级PID外层控制位置内层控制速度。外环位置PID输入是目标位置-实际位置输出是目标速度。 内环速度PID输入是目标速度-实际速度输出是板子倾斜角度。你需要把视觉定位算出来的球坐标做差分得到球的速度估计。这个速度噪声非常大所以一定要做滤波。我用的是简单的均值滤波取最近5帧速度的平均实测效果可以接受。级联PID的代码结构比单环多一层// 外环位置 - 目标速度 float target_vel_x PID_PosX_Calc(ball_x, target_x); // 内环目标速度 - 舵机角度 float target_servo_x PID_VelX_Calc(ball_vel_x, target_vel_x); // 限幅后输出 target_servo_x constrain(target_servo_x, -30, 30);核心心得如果只搭单环就发现球一直往一个方向跑、又拉不回来先别急着加积分项而是检查你的坐标系方向是不是反了。这个错误我在调试中犯过不止一次——视觉坐标的Y轴方向和板子倾斜的正方向没对齐导致闭环变正反馈球当然越跑越远。4. 执行层与平台机构从PID输出到实际倾斜4.1 舵机驱动与PWM控制的核心要点PID算出来的是一个数值最终还得靠舵机去转。我的平台用的是两个数字舵机X轴和Y轴各一个。舵机的控制信号是50Hz的PWM方波脉冲宽度0.5ms到2.5ms对应0度到180度。STM32的定时器输出PWM很容易配我用的是通用定时器TIM3通道1输出X轴舵机PWM通道2输出Y轴舵机PWM。配置周期为20ms即50Hz。// TIM3 PWM初始化核心 TIM_TimeBaseInitTypeDef TIM_InitStructure; TIM_OCInitTypeDef OC_InitStructure; TIM_InitStructure.TIM_Period 2000 - 1; // 20ms / (1us) 2000 TIM_InitStructure.TIM_Prescaler 84 - 1; // 72MHz / 84 857kHz ≈ 1us计数 TIM_InitStructure.TIM_ClockDivision 0; TIM_InitStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM3, TIM_InitStructure); OC_InitStructure.TIM_OCMode TIM_OCMode_PWM1; OC_InitStructure.TIM_OutputState TIM_OutputState_Enable; OC_InitStructure.TIM_Pulse 1500; // 1.5ms 中间位置 TIM_OC1Init(TIM3, OC_InitStructure); TIM_OC2Init(TIM3, OC_InitStructure); TIM_Cmd(TIM3, ENABLE);我用的舵机是中位在1500us对应0度的类型1度的步进大约是5.5us脉宽变化。所以把PID输出的角度映射到脉冲宽度关系是pulse_us 1500 angle * (1000 / 180)即角度范围-90到90对应脉宽1000us到2000us中位1500us。4.2 输出限幅非常关键PID输出一定不能裸奔。这里得介绍一下限幅的工程意义。我在初期试调时PID输出异常大比如位置误差100像素的时候PID输出对应的角度达到了45度。板子瞬间倾成一个大坡球直接被甩出去。后来我强制限幅#define PLATE_MAX_ANGLE_X 15.0f #define PLATE_MAX_ANGLE_Y 15.0f float constrain(float value, float low, float high) { if (value low) return low; if (value high) return high; return value; } servo_angle_x constrain(servo_angle_x, -PLATE_MAX_ANGLE_X, PLATE_MAX_ANGLE_X); servo_angle_y constrain(servo_angle_y, -PLATE_MAX_ANGLE_Y, PLATE_MAX_ANGLE_Y);限幅到正负15度之后系统虽然牺牲了一部分快速性但稳定性大大提高。球不会因为过大的倾斜角而失控滑出板面。其实限幅也是PID工程实践中的标配任何执行器都有物理极限控制器输出必须尊重这个极限。4.3 机械结构的误差比你想象的更致命这个项目还有一个隐形杀手机械间隙。舵机的输出轴和连杆之间如果存在回程间隙PID输出一个角度板子实际倾斜的角度会偏小或者滞后。这会直接表现为系统有静差怎么加积分都消不掉或者高频抖动微分项形同虚设。所以搭建平台的时候尽量用紧凑的轴承结构减少虚位。我用的是双轴承支撑的万向节结构齿轮之间的背隙很小。如果你用的是便宜的塑料舵机臂螺丝硬连接那间隙会非常明显。实测经验我记得有一次整套系统怎么调都震荡最后发现是舵机臂上的固定螺丝松动了一点导致每次反向驱动的时候存在大约2度的空行程。拧紧之后同样的PID参数系统立刻稳定了。所以当你发现调参调不出来的时候先去看看机械结构往往花10分钟拧一颗螺丝比调半天参数更有效。5. 常见问题与调试实录5.1 问题速查表现象可能原因排查步骤球直接飞出去完全不受控坐标系方向反了手动给一个正的倾斜角观察球是往正方向还是负方向滚动球在目标点附近来回震荡微分项不足或相机帧率过低先加大KD值若无效检查帧率降到10帧/秒以下必须优化视觉球停在偏离目标点的地方积分项不足或机械静态摩擦过大逐步加大KI观察是否被推向目标点观察舵机响应是否滞后系统发出嗡嗡声舵机发抖KD过大或舵机死区过小减小KD或者把舵机死区设到1度视觉偶尔丢帧导致系统跳变阈值分割不够鲁棒增加面积过滤、形态学处理或加入卡尔曼追踪禁止直接输出跳变值5.2 参数整定的正确顺序我推荐严格按先内环后外环、先P后I再D的顺序整定。先只保留外环P让球能大体跟随目标点然后加内环低速的粗调接着调外环的D来压制振荡再逐步加I消除稳态误差。每次只调一个参数改变幅度要小调整完必须观察30秒以上再继续不要一次调三个参数然后不知道是哪个起的作用。一个比较实用的调参起点是外环位置P 2.0I 0D 0.2 内环速度P 0.3I 0D 0.1然后根据实际响应微调。这个参数组在我的平台上是稳定可用的但不同的机械结构、球重、摄像机安装方式都会影响最优值所以不要照抄。5.3 调试中习得的几个实战技巧第一个技巧是打日志比看现象更重要。我一开始只靠眼睛看球的运动来判断参数好坏这很有误导性。后来我把球的像素坐标、目标坐标、PID输出、实际舵机角度每50ms通过串口发到PC上用串口绘图工具画出来。看着曲线调参一眼就知道是振荡阻尼不够、还是响应过慢、还是纯延迟太大。强烈建议你也在代码里加上这层数据回传。第二个技巧是从静止起步逐步加扰。刚开始先让球停在目标点系统稳定后再手动把球推到旁边释放观察它能不能自己回到目标点。能回到之后再给一个阶跃目标比如从中心瞬间移到偏右100mm观察球的追踪表现。每个阶段都能通过也要能说明原因否则继续调参。第三个技巧是引入一点点死区来对抗舵机抖动。当PID输出很小的时候比如小于1mm的误差让输出直接保持上一次的值。这样可以避免舵机在一个角度附近来回微动既省舵机也让系统视觉上更稳定。6. 后续还能怎么玩这套系统做完之后它的价值并不止于让一个球停在板子中央。你可以继续往三个方向扩展轨迹跟踪、动态避障、多目标协调。轨迹跟踪只需把外环的目标从固定点改成时间序列上的移动点比如让球画一个圆形或者八字形这能引入前馈和路径规划会更有趣。如果不做改动至少也让控制系统的目标从镇定问题变成了跟踪问题控制器在加前馈馈等环节之后鲁棒性要求会更高你的理解也会更深一层。动态避障也不难启动——在板上放几个彩色障碍块视觉端把目标球和障碍都识别出来PID外环加入一个排斥力向量来避开障碍即可。这其实就已经在往人工势场法的方向靠了。还有一个容易忽略的方向是系统建模的深入。如果你用一阶或者二阶传递函数去近似球-板系统的动力学然后用MATLAB的pidtune工具去解算理论PID参数再和手调的结果对比会发现自己对整个系统的理解上一个台阶。典型的球板系统传递函数可以近似为G(s) K / (s^2)因为球在倾斜板上加速度近似与倾角成正比。认识到这个二阶积分特性你就会明白为什么单环P控制很难稳定为什么速度内环会显著改善阻尼。这些扩展方向都不需要更换太多硬件主要还是软件和算法的迭代。项目完成后把代码、机械图纸、调参记录整理好整个过程的含金量远高于照着教程跑通一个demo。我个人觉得平衡球机器人最迷人的地方在于它的系统规模不大却几乎涵盖了一个自主系统全部的要素——感知、状态估计、控制、执行。你亲手把一个混沌振荡的系统调成平稳状态的那一刻会有一种非常踏实的成就感这是光看论文得不到的。如果读到这里你也准备动手做一套只想提醒你一点硬件焊接好、视觉调通之后先用最保守的PID参数让系统动起来再想着做花活稳住了一切就都有了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价