简介:一套基于OpenCV视觉捕捉与贪心算法路径规划的网球自拾取机器人完整工程,面向计算机视觉、人工智能、自动化等专业的毕业生和机器人爱好者,解决网球场地上随机网球的位置识别、目标跟踪与最优拾取路径决策问题。资源包共80个文件,压缩包大小77.73MB,核心代码以18个cpp文件为主体,涵盖颜色检测、圆形检测、透视变换、轨迹生成、贪心算法路径规划,以及SVM训练与实时预测等完整流程;另配26张jpg与4张png测试图像、YAML配置、CMake编译脚本,还有两篇关于机器视觉轨迹识别与运动目标检测的PDF论文,便于从理论到工程实现逐层对照。目前已有123人学习下载。项目代码经测试运行成功,支持远程教学,可直接用于毕设答辩、课程设计或项目初期演示;在现有代码基础上也可按需扩展,快速搭建可运行的网球自动拾取系统,对视觉定位与路径规划的工程落地具有较强的参考价值。
1. 基于OpenCV视觉捕捉与贪心算法路径规划的网球自拾取机器人:拿到源码先看这条链路
网球场上的散落网球,靠人反复弯腰去捡效率很低。基于OpenCV视觉捕捉与贪心算法路径规划的网球自拾取机器人系统,解决的就是"球在哪、先去哪、怎么去"这三个问题:视觉部分用颜色过滤加圆检测算出每个球的像素位置,投影变换把像素坐标映射成场地坐标,贪心算法根据这些坐标排出一条不绕远的拾取顺序,运动模块再把顺序变成底盘移动指令。它不是只有一个演示效果的玩具代码,src目录里视觉、规划、运动三个模块分文件存放,ml目录下还有一套完整的SVM训练与预测流程,data目录里带了几十张测试图和一段tennis.avi测试视频。适合做毕设、课程设计,或者想完整跑一遍OpenCV图像处理项目链路的人。源码是纯C++写的,CMake可以直接构建,下面按我实际拆过的顺序讲。
2. 视觉捕捉链路:HSV过滤、Hough圆检测与投影变换的串联方式
2.1 颜色过滤:为什么第一步用HSV而不是RGB
网球在画面里最显眼的特征是黄绿色,直接用RGB做阈值分割,一旦光照条件变化就废——同一个黄色在阳光下和阴影里的RGB值差别极大。HSV色彩空间把色调H、饱和度S和亮度V分开,做颜色过滤时只需要固定H范围,S和V允许在一定区间浮动,这样同一个阈值在早上和下午的球场上还能勉强工作。
filter.cpp里核心就是inRange加形态学滤波:
// filter.cpp 核心:构建黄色网球掩膜 cv::Mat hsv, mask; cv::cvtColor(frame, hsv, cv::COLOR_BGR2HSV); // HSV阈值范围,实际调试时建议写入config/default.yaml cv::Scalar lower(20, 100, 100); // H: 20~35 覆盖黄绿色区间 cv::Scalar upper(35, 255, 255); cv::inRange(hsv, lower, upper, mask); cv::morphologyEx(mask, mask, cv::MORPH_OPEN, cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(5, 5)));这段先做色彩空间转换,再通过inRange生成二值掩膜。lower里H=20对应偏绿的方向,H=35对应偏橙的方向,中间正好覆盖网球的黄绿色。S和V的下界都设成100,用来滤掉灰暗区域。后面跟一个开运算,作用是去掉掩膜上的孤立噪点,同时保留球体轮廓的完整度。
这里有个细节值得注意:morphologyEx的核用椭圆而不是矩形,因为球在二值图里是近似圆,用椭圆核做开运算不会把边缘磨平。高反光场地会出现V通道频繁超上限的情况,我一般会把V的上界降一点,或者在转HSV之前先加一层高斯模糊。整个颜色过滤模块是后面一切判断的基础,掩膜质量差,圆检测就会跟着出问题。
2.2 圆检测与质心:HoughCircles和get_centroid配合的原因
掩膜只是说明"哪里颜色像球",但机器人需要的是"球心在哪个像素"。checkCircle.cpp用HoughCircles找候选圆,get_centroid.cpp用图像矩函数计算精确质心,两套结果交叉验证才能滤掉反光造成的假圆。
// checkCircle.cpp 核心:在掩膜上找圆 std::vector<cv::Vec3f> circles; cv::HoughCircles(mask, circles, cv::HOUGH_GRADIENT, 1.2, // dp:累加器分辨率,1.2表示比原图低一档 mask.rows / 8, // minDist:圆心最小间距,防止同一个球检出多次 100, // param1:Canny边缘检测的高阈值 30, // param2:圆心累加阈值,越小越容易检出弱圆 10, 50); // minRadius/maxRadius:网球在当前分辨率下的半径范围注意,HoughCircles是在二值掩膜上跑的,不是RGB原图。这样边缘检测阶段只会看到黄色区域的轮廓,不会被场地白线和背景干扰。minDist设成rows/8,意思是圆心距离小于图像高度八分之一的都视为同一个球。param2设30是偏宽容的值,如果背景杂物多,调到45能明显减少误检。半径范围10到50要根据摄像头距离实际量一下,球占画面越小,这个范围越要收紧。
质心计算在get_centroid.cpp里,用的是二值图像矩。代码写法是标准的:先算m00零阶矩拿到面积,再算m10和m01一阶矩,质心坐标就是m10/m00和m01/m00。用质心而不是直接信任Hough圆心,是因为Hough的圆心受边缘噪声影响大,而二值掩膜的质心对小球缺损不敏感。球被手挡了一半,质心依然在球体中心附近。
2.3 投影变换:把像素坐标映射到场地坐标
图像坐标对机器人没有意义,机器人需要的是"场地坐标系下的x、y"。ProjectiveTransform.cpp里用findHomography求单应矩阵,把图像上标定点的坐标映射到场地实际坐标。
// ProjectiveTransform.cpp 核心:计算单应矩阵 std::vector<cv::Point2f> src = { p1, p2, p3, p4 }; // 图像上的四个标定点 std::vector<cv::Point2f> dst = { q1, q2, q3, q4 }; // 场地坐标系对应的四个点 cv::Mat H = cv::findHomography(src, dst, cv::RANSAC); // 计算任意像素点的场地坐标 cv::Mat ballWorld = H * (cv::Mat_<double>(3,1) << px, py, 1.0);这里最少要4对对应点,我建议标6到8对然后启用RANSAC。手工选点一定有偏差,RANSAC能把偏差最大的点判定为外点丢掉,算出来的矩阵更稳。单应矩阵是3x3的,最后一行是透视归一化系数,乘完记得除以第三个分量才是真正的场地x和y。
单应矩阵的结果可以存到config/default.yaml,换场地时只改标定点,不用重新编译。get_Random_points.cpp在这个链路里承担的是测试角色:随机生成一组场地坐标,模拟球散落的分布,用来单独验证路径规划模块。这个文件名容易让人误以为它是正式视觉流程的一部分,其实是给贪心算法调参用的随机输入。
3. 贪心算法路径规划:为什么放弃穷举法,以及代价矩阵怎么建
3.1 穷举法 vs 贪心法:10个球就是分水岭
src目录下同时保留了1.Method_of_exhaustion.cpp和2.Greedy_Algorithm.cpp,这是典型的"先证明为什么不选A,再落地B"。穷举法枚举所有访问顺序,n个球的排列数是n!。10个球就是3628800种顺序,12个球达到4.79亿种,在嵌入式主控上完全不可行。而贪心算法每步只做一次线性扫描,复杂度只有O(n²)。
| 球数量 n | 穷举法排列数 | 贪心法决策次数 |
|---|---|---|
| 5 | 120 | 5 |
| 8 | 40320 | 8 |
| 10 | 3628800 | 10 |
| 12 | 479001600 | 12 |
我一般用一句口诀判断:8个球以内跑穷举法,8个以上必须上贪心。拾取机器人属于"捡一个球就消耗一次底盘移动"的场景,路径只要不绕远就能接受,最优和次优之间的路径长度差距通常不到10%,但计算耗时差好几个数量级。
3.2 贪心决策的核心代码
贪心的思路非常直接:从当前位置出发,下一站一定是离得最近且没被捡过的球。每次做这个决策只需要扫描一遍剩余球的列表,n个球就有n次扫描。
// 2.Greedy_Algorithm.cpp 核心:贪心选择下一个目标 std::vector<int> order; // 记录拾取顺序 std::vector<bool> visited(n, false); // 每个球是否已被规划 cv::Point2f cur = startPos; // 当前位置,单位:场地坐标(米) for (int i = 0; i < n; ++i) { int best = -1; double bestDist = DBL_MAX; for (int j = 0; j < n; ++j) { if (!visited[j]) { double d = cv::norm(cur - balls[j]); // 欧氏距离 if (d < bestDist) { bestDist = d; best = j; } } } visited[best] = true; order.push_back(best); cur = balls[best]; // 当前位置更新成刚选中的球 }这里的核心是cur每轮都在更新,每个决策都基于机器人当前的实际位置,而不是固定从原点出发。cv::norm是OpenCV的范数函数,算的是两点间的欧氏距离,默认走直线假设。如果你的场地中间有障碍物不能走直线,可以把cv::norm替换成A*或Dijkstra算出的实际路径代价,贪心框架本身不用改。
有一个容易被忽略的边界情况:如果两个球坐标完全相同,bestDist会出现相等的情况,代码会选中索引较小的那一个,这种行为是确定性的,不会导致死循环。
3.3 代价矩阵从哪来:视觉输出转规划输入
贪心算法需要知道每个球的场地坐标,这些坐标来自第2章的投影变换。实际项目中我建议这样串联:过滤和圆检测每帧产出候选圆心,findHomography把圆心像素坐标转成场地坐标,然后累计到一个列表中,直到连续几帧不再出现新的球,再把列表交给路径规划模块。
edge2list.cpp在这个链路里承担的角色是把场地边缘信息整理成有序列表,作用是边界约束。贪心算法本身不感知边界,规划的路径点一旦落在场地外,底盘就会执行无效移动。把场地边缘坐标转成列表后,每次贪心选点前先检查目标点是否在边界内部,越界的球直接排在最后处理。
这里有一个值得展开的点:路径规划模块的输入量纲必须统一。视觉输出的是像素坐标,投影变换输出的是米制场地坐标,运动模块期望的是速度指令。三个模块之间如果量纲不统一,贪心算法算出来的距离没有任何意义。我建议在main.cpp里加一个坐标标准化的步骤,所有中间变量都转成米制浮点数。
4. 运动执行:movemet、Trajectory与main.cpp的串行逻辑
4.1 movement模块:位置差到移动指令的换算
movemet.cpp做的事是把"下一个目标点"换算成底盘能执行的速度指令。我的处理方式是位置比例控制:距离越远,速度越大;距离小于死区阈值就认为到位,停下来切换下一个目标。
// movemet.cpp 核心:根据目标点生成底盘速度指令 void moveTo(cv::Point2f target, double maxSpeed) { double dx = target.x - currentPos.x; double dy = target.y - currentPos.y; double dist = std::sqrt(dx * dx + dy * dy); // 位置比例控制,kP是比例系数,dist过小时直接停车 if (dist < 0.05) { setMotorSpeed(0, 0); return; } double speed = std::min(maxSpeed, dist * kP); double vx = (dx / dist) * speed; double vy = (dy / dist) * speed; setMotorSpeed(vx, vy); }死区设成0.05米,意思是5厘米以内就算到位了。这个值根据机械结构精度调整,底盘带编码器反馈的可以收紧到2厘米,开环的底盘建议放宽到8厘米以上,否则会一直震荡。kP比例系数调大了会超调,调小了到位慢,经验值是0.5到1.0之间起步。
4.2 Trajectory模块:把规划路径变成连续轨迹
贪心算法输出的是一个离散的球点序列,Trajectory.cpp做的是补点和平滑,把"去3号球,再去7号球"变成一段连续可执行的轨迹。常见做法是用直线插值,在每个路径段中间生成若干个中间点,让底盘走起来不会突然转向。
轨迹模块里有一个参数容易被忽略:转弯半径。底盘在切换到下一个目标点时,如果角度变化太大,速度必须降下来。Trajectory.cpp里做的处理是计算当前运动方向和目标方向的夹角,夹角超过45度就先把速度降到一半再转弯。这个逻辑在只有一个摄像头的视觉方案里尤其重要,因为视野边缘的球坐标误差大,直接全速冲过去容易过冲。
4.3 main.cpp的主循环:视觉→规划→运动的顺序
main.cpp把整个流程串成一个有限状态机:采集图像、颜色过滤、圆检测、投影变换、贪心规划、运动执行,每个状态一个函数。这种写法最大的好处是调试点清晰——球检测不出来,问题一定在视觉三段;移动路径不对,问题一定在规划或运动。
// main.cpp 主循环伪代码结构 while (true) { cv::Mat frame = captureFrame(); // 1. 取一帧 cv::Mat mask = filterYellow(frame); // 2. 颜色过滤 std::vector<cv::Point2f> balls = detectAndProject(mask, H); if (balls.size() > 0 && needReplan) { plan = greedyPlan(balls, currentPos); // 3. 贪心规划 } if (!plan.empty()) { moveTo(balls[plan.front()], maxSpeed); // 4. 运动执行 } }这里我特别强调一下"需要重新规划"的判断。如果每帧都重新跑贪心,会因为视觉抖动导致路径频繁切换,底盘左右摇摆。我习惯的做法是:上一次规划的目标还没到达之前不重新规划,只有到达当前目标或者发现新的球数量变化超过阈值,才触发重规划。这个阈值一般设成球总数的20%,也就是场地上新出现了两三个球才值得重算路径。
5. 避坑:视觉参数、投影标定和掉帧问题的五个实战记录
5.1 换了场地 HSV阈值立刻失效
现象:在A场地调好的黄色掩膜非常好用,搬到B场地后网球检测率骤降,掩膜一片黑或者满屏噪点。
原因:不同场地的灯光色温不同,网球的H值偏移了5到10度;地面材质反光率不同,S和V分布也变了。
解决:把HSV阈值从代码里抽出来,放进config/default.yaml,调参的时候只改配置文件不重编译。用第6章里写的trackbar工具现场调,把H的上下界和S下限都调好后再固化到配置文件。
5.2 HoughCircles把场地白线误检成球
现象:路径规划里出现一个不存在的球,机器人走到一半对着白线停下来。
原因:白色划线在二值掩膜经过边缘检测后,会出现圆弧形状的片段,param2设太低时被当成弱圆接受了。
解决:把param2从30提到45以上,同时加一个面积过滤——真实网球在掩膜上的面积是有合理区间的,太小的是噪点,太大的十有八九是场地标记。get_centroid.cpp里已经算出了m00面积,顺手加个范围判断就行。
5.3 投影变换后坐标飘移,画面边缘球位置不准
现象:画面中心的球投影后坐标很准,画面边缘的球偏差超过半米,底盘走到位置却什么都没捡到。
原因:标定点集中在场地中央,周边区域是外插值,单应矩阵在图像边缘的估计误差放大。
解决:标定点改用6到8个,覆盖四角和场地中线交点,用RANSAC求矩阵。换场地后必须重新标定,不要复用旧矩阵。我见过有人把单应矩阵写在代码里,换场地只调颜色阈值不换投影参数,结果路径规划整个是歪的。
5.4 贪心算法出现明显绕路
现象:捡完一个球,路径又回到刚才经过的区域,整体路线像画圈。
原因:贪心只看一步,两个相邻球在视觉上距离近,实际路径被场地障碍物阻挡。或者视觉检测漏掉了一个球,导致贪心在漏检点附近反复犹豫。
解决:先检查是不是漏检,用tennis.avi回放确认每帧都检出了全部球。如果检测正常,就接受贪心的次优结果,或者加一个2-opt局部优化——把路径中相交的线段交换一下,能消除大部分绕路。
5.5 predictrealtime实时SVM推理导致画面掉帧
现象:启动ml目录下的predictrealtime.cpp后,采图频率明显下降,机器人反应迟钝。
原因:对每帧的每个候选目标都做了特征提取,SVM的RBF核推理在高分辨率图像上也很耗时,逐帧处理根本来不及。
解决:隔帧推理,每3帧做一次SVM确认。同时在进入SVM之前先利用颜色过滤阶段的结果,把明显不是球的小面积噪点直接丢弃,通常能过滤掉80%的候选目标,SVM只需要处理真正可疑的区域。
6. 验证技巧:从单图调参到全流程演示的三步走
这套系统的验证有固定顺序,别一上来就跑主循环。第一步,验证颜色过滤和圆检测,用data目录下的单张测试图片逐张调试。我一般会挂一个带trackbar的调参窗口,实时看HSV阈值对掩膜的影响:
// 调参窗口核心代码,挂滑动条实时观察掩膜 cv::namedWindow("tune", cv::WINDOW_NORMAL); int hLow = 20, hHigh = 35, sLow = 100; cv::createTrackbar("H_Low", "tune", &hLow, 180); cv::createTrackbar("H_High", "tune", &hHigh, 180); cv::createTrackbar("S_Low", "tune", &sLow, 255); // 回调中根据当前值重新inRange并显示mask cv::imshow("tune", mask); cv::waitKey(1);单张图片调好后,第二步验证投影变换是否正确。把场地四角的标定点画在图像上,再经过单应矩阵反投影回来,如果投射回去的点位和原始标定点重合,说明矩阵没问题。误差超过10个像素就重新标定。
第三步才是用tennis.avi跑完整的主循环,观察路径规划的连续性和运动模块的到位情况。我个人的习惯是每次改完视觉参数,都强制走一遍"单图调参→投影验证→视频测试"这个流程,再小的改动也不跳过。这个习惯帮我省掉了很多次"明明只改了一个阈值,为什么整个流程都不对了"的排查时间。希望帮到你。
本文还有配套的精品资源,点击获取