机械臂抛投乒乓球:从视觉识别到弹道计算的完整实现

发布时间:2026/8/27 9:59:53
机械臂抛投乒乓球:从视觉识别到弹道计算的完整实现 玩机械臂的人多少都有过这种冲动让它帮你干点正经事以外的活。我做这个啤酒乒乓机械臂项目起因其实特别简单——有一次朋友聚会大家围着桌子玩Beer Pong输的那队被罚酒我在旁边看着机械臂空转脑子里蹦出一个念头“要是让它替我投这一球输了算谁的”后来这个玩笑话变成了一个正经项目机械臂从识别桌面上的杯子到投掷乒乓球命中目标全自动完成。当时把这个项目发到社区后不少人来问原理和实现方式干脆把整套方案整理出来。这个项目涵盖了视觉识别、逆运动学、弹道计算、运动控制、机械结构设计这几块特别适合想做机械臂控制但不想只玩舵机开环动作的朋友。不管你是搞机器人竞赛的学生还是想给自己的机械臂加一点“感知能力”的创客这篇文章应该能给你一个完整的落地思路。1. 整体设计思路为什么这个项目比看上去复杂刚拿到这个需求时我以为是“机械臂过去把球放进去”就行了真正动手才发现完全不是这么回事。Beer Pong规则里进攻方要站在桌子一端把乒乓球投进远处的杯子里机械臂要做的不是“放置”而是“抛投”——这完全是两个难度级别。1.1 把任务拆成四个独立环节整个流程拆解下来是四个子问题目标定位视觉系统需要识别出杯子的位置桌面坐标同时也知道自己现在在哪。机械臂运动末端执行器要从当前位置移动到设定的抛球点这里需要逆运动学求解各关节角度。抛投动作末端要以特定角度和速度把球释放这决定了球的飞行轨迹是否能落到杯子里。自动循环投完一个杯子后系统要能自动调整目标继续下一个。很多方案死在了“只会抛投”上——机械臂对着固定坐标投一百次百发百中但换一个杯子位置就抓瞎。所以视觉定位不只是点缀而是整个系统的感知层没有它这个项目就失去了灵魂。1.2 关键决策为什么选择四自由度串联机械臂市面上现成的机械臂平台很多比如六自由度的工业臂、直角坐标臂、SCARA臂。我最终选了四自由度的关节型串联机械臂原因是四自由度足够覆盖Beer Pong的投掷需求基座旋转、肩关节、肘关节、腕关节俯仰。不需要第六轴末端滚转因为乒乓球是球形哪个方向释放都无所谓。控制复杂度适中。六自由度机械臂的逆运动学求解要处理多解和奇异位型四自由度用几何法就能解而且解算速度非常快。成本可控。舵机驱动的四自由度臂总材料成本能压在千元以内。注意这里说的自由度是“有效自由度”。很多网上卖的六轴机械臂真正能精确控制的可能就三四个轴选型时别被轴数忽悠了要看舵机性能和结构刚度。1.3 系统架构和执行流程整个系统的数据流是这样的摄像头拍下桌面画面 - 上位机笔记本/树莓派运行视觉算法识别杯子位置 - 上位机解算逆运动学计算各关节角度 - 通过串口发送控制指令给下位机 - 下位机驱动舵机完成动作。我这里用的是“上位机视觉下位机运动控制”的组合而不是全都在单片机里跑。原因很简单OpenCV和目标检测算法在树莓派上跑起来很吃力更别说普通的MCU了。PC负责“眼睛和大脑”单片机负责“肌肉”各干各的活调试起来也轻松。2. 机械臂硬件选型与结构方案硬件这部分我踩了不少坑先分享选型思路再给一套可以直接抄的配置清单。2.1 关节执行器舵机、步进还是伺服这是第一个需要做决定的点。三者在机械臂上的表现差异非常大类型扭矩/重量控制精度响应速度成本适用场景普通舵机中等低无反馈中等低玩具臂、休闲项目数字舵机较大中电位器反馈快中小型投掷臂步进电机中高开环可重复中中固定轨迹任务伺服电机很高高闭环编码器极快很高工业级我最终选了数字舵机具体型号是MG996R肩/肘 SG90腕/基座。为什么不用步进因为机械臂在抛投瞬间要承受较大的反向力矩步进电机开环控制容易丢步一旦丢步后面的动作全部错位。数字舵机有位置反馈虽然精度不如编码器但在这类动态任务里胜在响应快、扭矩大、成本低。实操心得不要把MG996R当“无限扭矩”用它标称是9.4kg/cm实际持续负载超过60%就容易发热。我试过连续抛投20次后肩部舵机温度明显升高后来在程序中加了“动作间隔保护”并在肩关节用了双舵机并联输出。2.2 结构设计3D打印还是铝型材结构材料的选择直接决定了机械臂的刚性和重量。我的方案是主体用PLA 3D打印关键受力关节处用铝合金定制件加固。如果你要复现这个项目我的建议是大臂和小臂用PLA材料打印壁厚设置3mm以上填充率40%左右。PLA便宜好打但耐热性差别放在夏天暴晒的车里。基座和肩关节支撑架用铝合金因为这两处承受的弯矩最大。所有舵机安装孔位要加金属衬套不然拧几次螺丝就把塑料孔拧滑了。2.3 末端执行器一个空心管就够了Beer Pong的“投球手”其实不需要复杂的夹爪。我设计的是一个塑料管槽内径比乒乓球大1mm末端稍微向上翘起。机械臂运动到抛球点时通过一个独立的弹射舵机或者直接利用腕关节的快速俯仰把球从管槽里甩出去。这种“滑槽释放”方案比夹爪抓取稳定得多。夹爪在开合瞬间会改变球的初速度方向而滑槽释放时球是自然滚出然后脱手轨迹可预测性高。3. 视觉识别与标定让机械臂“看见”杯子这一章是整个项目里最容易劝退人的部分。很多朋友一开始走的目标检测路线是用深度学习YOLO训练杯子模型但实际测试下来在大白墙上识别桌面上的红色杯子YOLO当然没问题问题在于部署复杂度和推理延迟。我后来换成了OpenCV传统视觉方案识别速度从200ms降到了30ms以内精度完全够用。3.1 摄像头选型与固定方式摄像头选了普通的USB广角摄像头分辨率调到1280x72030fps。固定位置是桌子正上方约1.2米处垂直向下拍摄俯视视角。为什么要俯视因为这样杯子形状不会因为透视而变形像素坐标到桌面实际坐标的标定转换最简单直接就是一次缩放加平移。3.2 颜色识别与噪点过滤杯子选的是红色程序里通过HSV颜色空间进行筛选。具体步骤是读取一帧图像把RGB转换到HSV空间。设定红色的HSV范围生成二值掩膜。对掩膜做形态学开运算腐蚀膨胀去掉小噪点。用cv2.findContours找轮廓按面积筛选出候选杯子区域。取轮廓外接圆圆心作为杯子在图像上的坐标。import cv2 import numpy as np def detect_cups(frame): hsv cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) # 红色在HSV中有两个区间这里合并处理 lower_red1 np.array([0, 120, 70]) upper_red1 np.array([10, 255, 255]) lower_red2 np.array([170, 120, 70]) upper_red2 np.array([180, 255, 255]) mask1 cv2.inRange(hsv, lower_red1, upper_red1) mask2 cv2.inRange(hsv, lower_red2, upper_red2) mask cv2.bitwise_or(mask1, mask2) 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) cups [] for cnt in contours: area cv2.contourArea(cnt) if area 500: # 过滤小噪点具体阈值根据相机高度调整 continue (x, y), radius cv2.minEnclosingCircle(cnt) cups.append((int(x), int(y), int(radius))) return cups这里的area 500阈值是实测调出来的。如果你的摄像头装得更高杯子在画面里变小阈值就要相应调低。我建议先打印一帧画面用画图工具量一下杯子占多少像素再反过来设阈值。3.3 像素坐标到桌面坐标的标定这一步是视觉部分最容易出错的地方。摄像头俯视安装后图像坐标系和桌面坐标系是平移缩放关系。标定方法是在桌面上放四个已知坐标的标记点比如(0,0)、(60,0)、(0,40)、(60,40)单位厘米。记录四个标记点对应的像素坐标。用cv2.getPerspectiveTransform计算单应矩阵。def calibrate(corners_pixel, corners_real): # corners_pixel: 图像像素坐标 (4, 2) # corners_real: 真实桌面坐标 (4, 2)单位厘米 H, _ cv2.findHomography(np.array(corners_pixel, dtypenp.float32), np.array(corners_real, dtypenp.float32)) return H def pixel_to_real(px, py, H): p np.array([[[px, py]]], dtypenp.float32) real cv2.perspectiveTransform(p, H) return real[0][0]这里强调一点用findHomography而不是简单的“缩放”是因为摄像头不可能是绝对垂直的只要有一点点倾斜画面边缘的杯子位置换算就会偏移。单应矩阵可以把透视和倾斜都消除掉。3.4 识别不稳定的坑与解决调试视觉部分时踩过几个坑反光误判桌面如果是亮面材质杯子旁边会出现高光反射区有时会被识别成一个很大的红色区域。解决办法是把HSV的饱和度下限调高因为反光区域的饱和度通常偏低。多目标选择逻辑Beer Pong的规则是从近到远依次投所以识别出多个杯子后要按y坐标排序优先选离机械臂最近的那个。别傻傻地全投了。光线变化白天和晚上同一个HSV参数识别效果完全不同。我在代码里加了“自动取色”功能——程序启动时正对桌面取一块杯子的颜色平均值动态设定HSV范围这样在不同时间段都能稳定识别。4. 逆运动学与抛投轨迹怎么把球送到杯子里视觉告诉我们杯子在哪里接下来要解决的是机械臂怎么动。这章是项目的技术核心我尽量用通俗的方式讲清楚。4.1 几何法求解逆运动学对于一个四自由度机械臂先忽略腕关节解大臂和小臂的两个角度。可以把它简化成一个二维平面内的两连杆机构目标点就是机械臂在水平面内需要到达的投掷点。已知大臂长度L1、小臂长度L2目标点的坐标(x, y)相对于肩关节。那么末端到肩关节的距离 r sqrt(x² y²)肘关节角度余弦定理cos(q2) (r² - L1² - L2²) / (2 * L1 * L2)肩关节角度q1 atan2(y, x) acos((L1² r² - L2²) / (2 * L1 * r))这就是经典的二维机械臂逆运动学解析解。基座旋转角则由目标点在水平面上的x、y直接取atan2(y, x)求得。整个过程只需要几行代码import math def inverse_kinematics(x, y, L1, L2): r math.hypot(x, y) if r L1 L2 or r abs(L1 - L2): return None # 超出可达范围 cos_q2 (r*r - L1*L1 - L2*L2) / (2 * L1 * L2) cos_q2 max(-1, min(1, cos_q2)) q2 math.acos(cos_q2) # 肘关节角度注意这里取的是“手臂向上”的解 q1 math.atan2(y, x) - math.atan2(L2 * math.sin(q2), L1 L2 * math.cos(q2)) return q1, q2这里的核心问题是目标点不应该直接设成杯子位置而应该设成“释放点”。机械臂不可能直接伸到杯子上方再把球放下去那样属于“放置”而不是“投掷”而且在现实中机械臂末端会挡住视觉也容易碰倒杯子。所以先把释放点算出来再用上面的逆运动学求解机械臂姿态。4.2 抛物线弹道计算释放点怎么定乒乓球飞行过程如果忽略空气阻力就是一个标准的斜抛运动。设释放点距离杯口的水平距离为d高度差为h释放点比杯口高为正初速度为v仰角为θ那么落点满足d v * cosθ * t h v * sinθ * t - 0.5 * g * t²消去时间t可以得到h d * tanθ - (g * d²) / (2 * v² * cos²θ)在这个方程里d和h是视觉几何算出来的已知量v和θ是我们要控制的量一个方程两个未知数所以需要先固定一个。我的策略是固定释放速度v求解θ。为什么固定速度而不是固定角度因为乒乓球的质量小、初速度对空气阻力很敏感速度一变轨迹就完全偏离。通过舵机PWM信号控制释放速度理论上可以大致稳定但误差还是比其他参数大。固定v后求解θ变成了一元二次方程tanθ [v² ± sqrt(v⁴ - g(g d² 2 h v²))] / (g d)有两个解对应“高抛”和“低平”两条轨迹。我选高抛解因为高抛的入杯角度大球不容易砸到杯沿弹出去。def launch_angle(d, h, v, g9.81): # 计算所需仰角返回弧度无解返回None v2 v * v discriminant v2 * v2 - g * (g * d * d 2 * h * v2) if discriminant 0: return None theta math.atan((v2 math.sqrt(discriminant)) / (g * d)) return theta4.3 释放速度的标定实验那个固定值v是怎么来的我是通过实测标定的。先给腕关节舵机一个PWM宽度让机械臂以固定速度甩球在桌面上铺一张大纸记录落点距离然后反推实际初速度。标定思路是把释放点固定在某个高度比如桌面上方40cm。给舵机PWM设为某个值连续抛10次记录落点水平距离的平均值。用公式反推初速度v d * sqrt(g / (2 * (d*tanθ - h)))……但这里θ也是我们自己设定的一个粗略值所以更准确的做法是直接用高速录像。如果手头有iPhone或者Android的慢动作模式录下抛球瞬间按帧找球的位置变化就能算出准确的出口速度。我实测下来MG996R在6V电压下腕舵机快速扫过60度时乒乓球出口速度约2.8m/s就以这个值作为v。4.4 别忘了空气阻力如果不考虑空气阻力乒乓球飞1.5米会有大约几厘米的偏差这个量在Beer Pong这种小杯口场景下可能就直接打到杯沿上了。我后来在弹道模型里加了一个线性空气阻力修正把落点距离d乘以一个1.08的系数。别小看这个8%它让命中率从五成提到了七成。更精确的做法是分段模拟把时间切成1ms的小段每段计算速度、阻力和位移但单片机算这个太吃力直接用经验系数就够了。5. 控制逻辑与代码实现从识别到落杯的完整闭环前面几章把各个模块讲清楚了这一章把它们串起来展示一个完整的控制循环。5.1 上位机主循环上位机用Python主循环逻辑大概是while True: frame cap.read() cups detect_cups(frame) if not cups: time.sleep(0.1) continue # 选最近的目标杯 target select_nearest(cups, robot_position_px) # 像素坐标转到桌面坐标 tx, ty pixel_to_real(target[0], target[1], H) # 加上释放点高度偏移计算释放点三维坐标 release_x tx OFFSET_X # 释放点比杯子偏近一点 release_y ty release_z RELEASE_HEIGHT # 40cm # 计算水平距离和高度差 d math.hypot(release_x, release_y) h release_z - CUP_HEIGHT # 释放点高度减去杯口高度 # 计算仰角 theta launch_angle(d, h, V_LAUNCH) # 计算机械臂末端相对基座的坐标 # 这里需要把release_x/release_y换算成机械臂坐标系坐标 arm_x release_x - ROBOT_BASE_X arm_y release_y - ROBOT_BASE_Y arm_r math.hypot(arm_x, arm_y) # 逆运动学求解 ik inverse_kinematics(arm_r, release_z, L1, L2) if ik is None: continue # 生成动作序列通过串口发送 send_arm_command(base_angle, ik[0], ik[1], wrist_angle) time.sleep(2) # 等待动作完成这段代码里OFFSET_X是释放点相对杯子在x方向的偏移量。为什么要偏近一点因为球在斜抛时会向前飞行释放点如果正好在杯口正上方球会越过杯口落到后面。释放点的水平投影应该落在杯子靠机械臂这一侧的某个距离上具体距离由弹道计算反推。5.2 串口通信协议下位机用的是Arduino或STM32。我自定义了一个简单的文本协议每帧数据类似#A90#B120#C60#D30#分别代表基座、肩、肘、腕四个关节的目标角度。上位机按照这个格式发送下位机解析后控制舵机。void loop() { if (Serial.available()) { String data Serial.readStringUntil(\n); // 解析 #Axx#Bxx#Cxx#Dxx# int base parseAngle(data, A); int shoulder parseAngle(data, B); int elbow parseAngle(data, C); int wrist parseAngle(data, D); myservo[0].write(base); myservo[1].write(shoulder); myservo[2].write(elbow); myservo[3].write(wrist); } }实操心得文本协议虽然简单但有个隐患——Arduino在主循环里阻塞等待串口数据播报完一个动作后才收下一个。如果机械臂动作还没完成上位机就发来下一帧会造成指令覆盖。解决办法是上位机在发送后sleep足够时间或者协议里加一个ACK应答。我用的是最简单的sleep方案时间设2秒保底够用。5.3 动作规划不是直接跳目标角度刚开始调试时我直接让舵机从当前角度跳到目标角度结果抛出来的球歪得离谱。原因是舵机在快速扫掠时中间轨迹是不受控的末端执行器的速度会在某个点突然被手腕舵机叠加产生不可预测的初速度。正确做法是分段插值运动先低速把机械臂移到释放点前的准备位置停顿0.2秒然后以固定速度快速扫过释放轨迹。投掷动作本身不是直接跳到目标角度而是从“准备角度”以固定角速度运动到“释放角度”在超过释放角度后的某一刻腕关节舵机做一次甩腕动作。这里最关键的是所有的运动时间必须固定下来。固定时间意味着固定角速度固定角速度意味着每次出手的初速度是一致的。如果哪个舵机迟到了整个轨迹就会变落点就会漂。为了保持同步四个舵机都要设置相同的角速度参数在舵机驱动库里调最大速度并且主控板不能同时处理太多串口中断。6. 调试实录这些坑你八成也会遇到调试这个项目花了大概三周前两周都在跟“差一点就中”作斗争。把踩过的坑和解决办法整理成一张表给你们做个速查。现象原因分析解决办法球总是落在杯子前方20cm释放速度v偏大或释放角度不够高降低PWM速度或提高仰角更可能是速度标定不准确球左右偏移不稳定基座旋转角度零位没校准基座安装后执行一次“零位校准”把机械臂正前方设为0度视觉识别时好时坏环境光线变化导致HSV范围失效加光源固定照明或增加自动取色校准机械臂投掷后机身抖动、位置偏移基座固定不牢舵机反作用力导致整体移动把底座用台虎钳夹住或加重底座配重同一个杯子第一次投中第二次偏了杯子被撞动后位置变了但程序还在用旧坐标每次投掷前重新识别目标位置投完立刻更新目标列表肩部舵机过热停摆连续投掷导致电机过载在动作序列里加“冷却延时”或降额使用肩部双舵机6.1 命中率低不是单一原因要学会定位问题调试过程中最忌讳“瞎子摸象”——一看到球没进就乱调释放速度。我的方法是把问题按环节拆开视觉环节在画面上绘制识别出的杯子中心点看看坐标是否稳定。如果中心点在图像上跳来跳去那就是识别不稳定跟机械臂无关。运动环节把机械臂末端装一支激光笔瞄准释放点看看激光是否每次都落在同一个位置。如果不是说明舵机重复精度不够或结构松动。弹道环节固定释放点连续抛10次观察落点散布。如果散布很大问题在释放速度一致性如果散布很小但偏离目标问题在弹道模型参数。这个定位思路适用于任何机器人项目比瞎调参数高效得多。6.2 结构件松动是精度杀手3D打印件的通病是“用久了变软”。PLA在承受反复冲击载荷后螺丝孔周围会产生蠕变导致关节间隙变大。间隙大到一定程度舵机的位置反馈显示到位了但实际关节角度差了2到3度。对于抛投来说这2到3度足以让落点偏出10厘米。我的解决办法有两个所有关键螺丝连接处加弹簧垫圈每10次投掷后检查一次紧固情况。关节连接件优先用金属材质实在要用塑料就改用PETG比PLA韧性好得多填充率调到60%以上。6.3 安全事项快拆装置必须有机械臂在抛投瞬间的速度很快末端瞬时速度可能达到每秒两三米。万一有人把手放在轨迹上被砸到不是闹着玩的。我在末端和手腕之间加了一个磁吸快拆结构一旦碰撞力度过大磁吸脱开末端不会硬顶着继续运动。如果你复刻这个项目我强烈建议至少加一个急停开关或者系一根保险绳限制活动范围。6.4 别忘了乒乓球本身的一致性这是一个大多数人忽略的细节不同品牌、不同新旧程度的乒乓球重量和弹性差异能达到10%以上。重量大的球在同样初速度下飞得更远弹性好的球撞击杯沿时更容易弹进去。我在项目里固定使用同一袋乒乓球并且每次投掷前放在同一个起始位置确保初始状态一致。7. 实测数据与性能调优整个系统跑通后我做了一组对照实验记录了不同参数下的命中率。测试条件60cm投掷距离红色杯子杯口直径9cm机械臂连续投20次取命中次数。速度补偿系数释放高度(cm)命中率备注1.0不考虑空气阻力4050%很多球砸在杯沿前沿1.08经验修正4075%主要命中1.083060%释放点太低轨迹太平1.085080%释放点高入杯角度大从结果看两个因素对命中率影响最大空气阻力修正系数和释放高度。释放高度的最优值在45到55cm之间太高了球下落速度快容易弹出来太低了轨迹太陡击打杯沿的概率变大。7.1 为什么释放高度高一点反而更准直觉上你可能觉得释放点越高球飞的时间越长误差越大。但实际情况不是这样。Beer Pong的杯口很窄球从斜上方下落时只要落点误差不超过杯口半径就能进。高释放点对应更大的入射角球进入杯口时与水平面的夹角入射角越大杯沿的有效接收宽度就越大。简单说高抛让杯口看起来“更宽”了。7.2 极限情况释放距离超过机械臂臂展我最初设计的机械臂臂展只有65cm这意味着它只能投掷离自己65cm以内的杯子。要覆盖整个桌面约1.2米要么把机械臂装在桌子中间要么加长手臂。我最终采用了一个取巧方案把整个底座装在一个可横向移动的导轨上需要投远距离目标时先滑动底座再投掷。这相当于把四自由度机械臂变成了四自由度加一个平移自由度的混合结构。导轨运动由另一个步进电机控制精度要求不高但省去了重新设计大臂的麻烦。如果你不想加导轨更省事的办法是让机械臂往杯子的方向“走一步”——在一张足够大的桌子上把目标人为规划成“先近后远”每次投完近处的杯子后底座自动前移一个杯距。这个方案对机械臂本体要求更低而且更加接近真人玩游戏时的节奏。这个项目让我对机械臂控制的理解提升了一个层次。以前做开环动作时总觉得机械臂就是“指哪打哪”的工具直到真正开始处理视觉标定、弹道补偿、舵机同步这些问题才意识到现实世界里的误差来源有多复杂。如果你也想做类似的项目我建议先从弹道模型入手再回头做视觉和结构——因为投不中时问题往往不是出在“看不见”而是出在“算不准”。最后再分享一个小技巧调试时先用一个固定的杯子位置跑通全流程再逐步引入视觉随机目标。先把简单的做稳定了后面加变量你会少掉很多头发。