☰
基于OpenCV与贪心算法的网球自拾取机器人:视觉识别与路径规划实战
2026/10/22 5:57:54 网站建设 项目流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与开发者的网球自拾取机器人完整项目源码,核心融合OpenCV视觉捕捉与贪心算法路径规划,可用于毕业设计、课程设计或机器人视觉入门进阶。压缩包共80个文件,约77.73MB,包含18个cpp源文件、26张jpg测试图、4个xml与5个json配置、2个h头文件及pdf论文资料,覆盖颜色检测、圆检测、投影变换、质心提取、轨迹规划与贪心/穷举算法对比等模块,并附带SVM训练与实时预测脚本。项目代码均经测试运行成功,答辩评审平均分达96分,已有123人学习。读者可获取完整视觉识别与路径规划实现、可复用工具脚本及排错思路,适合在此基础上修改扩展功能。

1. 从一场捡球噩梦说起:这套 OpenCV 网球自拾取机器人到底能干什么

去年帮一个校队的朋友收拾场地,两个小时训练下来,散落在角落的网球超过一百个。弯腰捡球这件事,对业余选手是体力活,对做视觉的工程师来说,其实是一道可以拆解的工程题:球在哪、怎么走过去、按什么顺序捡最省时间。这套「基于 OpenCV 视觉捕捉与贪心算法路径规划的网球自拾取机器人系统」就是围绕这三个问题展开的完整源码包,包含视觉识别、坐标标定、路径规划、运动控制几个模块,还附带一份 SVM 分类的训练与预测代码,以及两篇轨迹识别与目标跟踪方向的参考论文。

它适合谁?做计算机视觉课程设计、毕设、自动化相关课题的学生,或者想找一个「视觉 + 规划 + 控制」闭环练手项目的开发者。整套代码用 C++ 写,构建系统是 CMake,视觉部分依赖 OpenCV,规划部分实现了穷举法和贪心算法两套对照方案。下面我按「先看懂结构、再跑通识别、再理解规划、最后避坑」的顺序拆一遍,尽量让新手能照着复现,熟手能看到参数边界。

2. 工程结构与构建链路:先搞清楚每个 cpp 在干什么

拿到一个源码包,最忌讳上来就cmake ..然后对着报错发呆。这套工程的文件命名其实挺直白,但模块之间的调用关系需要先理一遍,否则改一个参数可能牵动整条链路。

2.1 目录分层与模块职责

工程根目录下有几个关键位置:include放公共头文件,src放视觉与规划的核心实现,ml放 SVM 相关的数据准备、训练和预测代码,data放测试图片和一段tennis.avi视频,paper放两篇参考论文,bin是编译产物目录,config下有default.yaml配置文件。

src里的文件可以按功能分成四组:

模块分组涉及文件职责
颜色与圆形检测checkColour.cpp、checkCircle.cpp从图像中筛出网球候选区域
形态学与滤波filter.cpp、edge2list.cpp去噪、边缘转列表、连通域处理
坐标与投影ProjectiveTransform.cpp、get_centroid.cpp像素坐标到场地坐标的映射、质心计算
路径规划与轨迹1.Method_of_exhaustion.cpp、2.Greedy_Algorithm.cpp、Trajectory.cpp、movemet.cpp穷举/贪心规划、轨迹生成、运动指令
入口与辅助main.cpp、get_Random_points.cpp主流程调度、随机点生成测试

include_all.h和function_declare.h是总头文件,前者把常用 OpenCV 头一次性引入,后者声明跨文件函数。这种写法在毕设项目里很常见,好处是改函数签名时只动一处,坏处是编译依赖变重,改一个头文件会触发大面积重编。

2.2 CMake 构建与依赖确认

工程有两处CMakeLists.txt,根目录一处,src一处,ml下还有独立的一份。根目录负责整体目标,src负责把视觉与规划源码编成库或可执行文件。构建前先确认 OpenCV 版本,这套代码用的是 OpenCV 3.x/4.x 的经典 API(cv::Mat、cv::inRange、cv::findContours等),4.x 下基本能编,但个别常量名有变化。

# 在工程根目录下执行 mkdir -p build && cd build cmake .. make -j4

cmake ..会读取根目录的CMakeLists.txt,make -j4用四线程并行编译。如果报Could NOT find OpenCV,说明 CMake 没找到 OpenCV 的配置文件,常见做法是在CMakeLists.txt里显式指定:

# 在 find_package(OpenCV REQUIRED) 之前加上 set(OpenCV_DIR "/usr/local/lib/cmake/opencv4") # 按实际安装路径改 find_package(OpenCV REQUIRED) include_directories(${OpenCV_INCLUDE_DIRS}) target_link_libraries(你的目标名 ${OpenCV_LIBS})

OpenCV_DIR指向的是 OpenCV 安装后cmake子目录,不是include目录,这是新手最容易填错的地方。填错会报找不到OpenCVConfig.cmake。编译产物默认落在bin下,libtennis.so是编出来的动态库,主程序链接它。

提示:如果只想先跑通视觉部分,可以单独进src目录编译,减少 SVM 模块带来的依赖干扰。

2.3 配置文件 default.yaml 的作用

config/default.yaml是运行期参数入口,常见做法是把颜色阈值、圆形检测的半径范围、贪心算法的距离权重放进去,避免每次调参都重新编译。虽然源码里读取 yaml 的部分需要 OpenCV 的FileStorage支持,但参数集中管理这个思路值得保留。改阈值时优先改 yaml,不要直接改 cpp 里的魔法数字,否则下次换光照条件你会找不到自己改过哪里。

3. OpenCV 视觉捕捉:从像素到球坐标的完整链路

视觉部分是整套系统的入口,识别不准,后面规划再漂亮也是白搭。这一章把「颜色筛选 → 形态学去噪 → 圆形确认 → 质心与投影」这条链路拆开讲,每一步都给可调参数和失败时的排查方向。

3.1 颜色空间选择与阈值分割

网球是黄绿色,在 RGB 空间里受光照影响大,同一颗球在阴影下和阳光下 RGB 值能差出一大截。常见做法是转到 HSV 空间做阈值分割,因为 H(色调)对光照变化相对不敏感。

// checkColour.cpp 核心逻辑示意 cv::Mat hsv, mask; cv::cvtColor(frame, hsv, cv::COLOR_BGR2HSV); // 黄绿色在 HSV 中的大致范围,需按实际光照微调 cv::Scalar lower(25, 80, 80); cv::Scalar upper(75, 255, 255); cv::inRange(hsv, lower, upper, mask);

lower和upper是 HSV 三通道的下上限。H 通道范围 0-179(OpenCV 里 H 是 0-180),S 和 V 是 0-255。H 取 25-75 覆盖黄到绿,S 下限 80 是为了滤掉灰白色干扰,V 下限 80 滤掉过暗区域。如果识别结果里混进了地板反光,先把 S 下限往上提;如果球在阴影里漏检,把 V 下限降到 50 左右试试。这一步的输出是一张二值 mask,白色是候选区域。

3.2 形态学滤波与边缘转列表

阈值分割出来的 mask 往往有噪点和小孔洞,filter.cpp里一般用开运算去小噪点、闭运算填孔洞。开运算是先腐蚀后膨胀,闭运算是先膨胀后腐蚀。

cv::Mat kernel = cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(5, 5)); cv::morphologyEx(mask, mask, cv::MORPH_OPEN, kernel); // 去噪点 cv::morphologyEx(mask, mask, cv::MORPH_CLOSE, kernel); // 填孔洞

kernel大小 5x5 是经验值,球在画面里越大,kernel 可以适当加大,但超过 9x9 容易把相邻的球粘连成一个区域。edge2list.cpp负责把 mask 里的连通域边缘转成点列表,供后续圆形拟合和质心计算使用。这一步如果发现边缘断裂,多半是 mask 本身不连续,回到上一步调阈值,而不是在这里加复杂的边缘连接逻辑。

3.3 圆形确认与质心计算

颜色筛选只能说明「这块区域是黄绿色」,不能说明「这是个球」。checkCircle.cpp用霍夫圆变换或轮廓圆度判断来确认。霍夫圆对参数敏感,param2越小越容易检出但也越多误检。

std::vector<cv::Vec3f> circles; cv::HoughCircles(mask, circles, cv::HOUGH_GRADIENT, 1, // dp,累加器分辨率,1 表示与原图同分辨率 mask.rows / 8, // minDist,圆心最小间距 100, // param1,Canny 高阈值 30, // param2,累加器阈值,越小越敏感 10, 100); // minRadius, maxRadius

minDist设成图像高度的八分之一,是为了避免同一个球被检出多个圆。param2从 30 开始调,误检多就加大,漏检多就减小。minRadius和maxRadius按球在画面里的实际像素半径设,设窄一点能大幅减少误检。确认圆之后,get_centroid.cpp算质心,ProjectiveTransform.cpp把像素坐标映射到场地平面坐标——这一步通常需要一个单应性矩阵,用场地四个角点做标定。标定不准,后面规划出来的路径会整体偏移,这是血泪经验里最常见的一类问题。

4. 贪心算法路径规划:为什么不用穷举,贪心边界在哪

视觉给出球的位置列表后,机器人要决定按什么顺序去捡。这是一个典型的旅行商问题(TSP)变体:从当前位置出发,经过所有球的位置,使总移动距离最短。球的数量少时穷举可行,数量一多就爆炸。这套代码同时给了穷举法和贪心算法两套实现,正好用来理解两者的取舍。

4.1 穷举法的复杂度与适用规模

1.Method_of_exhaustion.cpp实现的是全排列枚举。n 个球有 n! 种访问顺序,n=8 时是 40320 种,n=10 时是 362 万种,n=12 就上亿了。所以穷举只适合球数很少的场景,比如 6 个以内,用来做小规模验证或给贪心结果做对照。

// 穷举法核心:递归生成全排列并计算总距离 double bestDist = std::numeric_limits<double>::max(); std::vector<int> bestOrder; std::sort(order.begin(), order.end()); do { double d = pathDistance(start, order, balls); if (d < bestDist) { bestDist = d; bestOrder = order; } } while (std::next_permutation(order.begin(), order.end()));

std::next_permutation要求序列先升序排列,否则会漏掉排列。pathDistance累加起点到第一个球、球与球之间、最后一个球的距离。这段代码的价值在于:当你怀疑贪心结果不好时,用小规模穷举结果做基准,能直观看出贪心差了多少。

4.2 贪心策略的实现与距离度量

2.Greedy_Algorithm.cpp的思路是:从当前位置出发,每次选最近的未访问球,走过去,再选下一个最近的。实现简单,复杂度 O(n²),球数上百也能秒出结果。

std::vector<int> greedyOrder(const cv::Point2f& start, std::vector<cv::Point2f>& balls) { std::vector<int> order; std::vector<bool> visited(balls.size(), false); cv::Point2f cur = start; for (size_t i = 0; i < balls.size(); ++i) { int nearest = -1; double minD = std::numeric_limits<double>::max(); for (size_t j = 0; j < balls.size(); ++j) { if (visited[j]) continue; double d = cv::norm(cur - balls[j]); // 欧氏距离 if (d < minD) { minD = d; nearest = j; } } visited[nearest] = true; order.push_back(nearest); cur = balls[nearest]; } return order; }

cv::norm算的是欧氏距离,如果场地有障碍物,距离度量要换成实际可行走路径长度,否则规划出的顺序看着近、走起来绕。贪心的边界在于:它每一步都选局部最优,不保证全局最优。典型反例是三个球呈「近-远-近」分布时,贪心可能先捡近的,导致后面多绕一段。球分布越均匀,贪心越接近最优;球分布越极端,贪心越容易翻车。

4.3 轨迹生成与运动指令衔接

规划出访问顺序后,Trajectory.cpp负责生成从当前点到目标点的轨迹,movemet.cpp把轨迹转成运动指令。常见做法是直线插值加朝向调整:先转向目标点,再直行,到达后微调对准球。

// 轨迹生成示意:直线插值 std::vector<cv::Point2f> genTrajectory(const cv::Point2f& from, const cv::Point2f& to, double step) { std::vector<cv::Point2f> traj; double dist = cv::norm(to - from); int n = static_cast<int>(dist / step); for (int i = 0; i <= n; ++i) { float t = static_cast<float>(i) / n; traj.push_back(from + t * (to - from)); } return traj; }

step是插值步长,太小会生成大量点拖慢控制循环,太大则轨迹不平滑。一般取 0.05 到 0.1 米。如果机器人运动学约束强(比如不能原地转向),这里要换成圆弧或样条插值,直线插值会导致转向时走「折线」。这一步和硬件强相关,源码里给的是通用框架,实际落地要按底盘参数改。

5. 避坑与排查:跑这套代码最容易翻车的五个地方

这套代码在作者的环境里跑通了,但换一台机器、换一个光照条件,问题就会冒出来。下面五条是我在复现这类视觉+规划项目时反复遇到的,按「现象 → 原因 → 解决」写清楚。

5.1 编译报 OpenCV 常量未定义

现象:make时报'CV_BGR2HSV' was not declared或类似常量找不到。原因:OpenCV 4.x 把部分常量从CV_前缀改成了cv::COLOR_前缀,老代码用的是 3.x 写法。解决:全局搜索CV_开头的颜色转换常量,替换成cv::COLOR_版本,比如CV_BGR2HSV改成cv::COLOR_BGR2HSV。如果不想改代码,装 OpenCV 3.x 也能绕过,但新项目不建议。

5.2 识别结果里混入地板和鞋子

现象:mask 里除了球,还有大片地板反光或深色鞋子区域。原因:HSV 阈值范围开太宽,或者 S/V 下限设太低。解决:先把 S 下限从 80 提到 120,观察反光是否消失;再把 V 下限提到 100 滤掉暗区。如果鞋子是深色,V 下限提高就能滤掉。调完阈值记得用data里的test1.jpg到test4.jpg逐张验证,不要只看一张图就定参数。

5.3 霍夫圆检测同一个球出多个圆

现象:一颗球被检出两三个重叠的圆,质心算出来偏移。原因:minDist设太小,或者param2太低导致累加器过于敏感。解决:把minDist提到图像高度的六分之一到八分之一,param2从 30 提到 40 到 50。如果球在画面里大小差异大,minRadius和maxRadius的范围要覆盖实际尺寸,范围设太宽也会增加误检。

5.4 贪心规划结果绕远路

现象:球数不多时,贪心顺序明显比穷举差,总距离多出一截。原因:贪心只看当前最近,不考虑后续分布。解决:球数少于 8 个时直接用穷举法;球数多时可以在贪心基础上加一步「2-opt」局部优化,交换访问顺序中两段看总距离是否下降,迭代几次通常能明显改善。源码里没带 2-opt,但改起来不难,在greedyOrder结果上再跑一轮交换即可。

5.5 投影变换后坐标整体偏移

现象:规划出的路径方向对,但整体平移了一段,机器人总是停在球旁边。原因:单应性矩阵标定的四个角点坐标和实际场地不匹配,或者标定时相机位置动了。解决:重新标定,标定用的角点要在图像里清晰可见,且四个点尽量分散、覆盖整个工作区域。标定完用get_Random_points.cpp生成几个已知点验证映射误差,误差超过球半径就要重标。标定这件事没有后悔药,相机一动就得重来。

6. 进阶玩法:用 SVM 模块做球与非球分类,以及验证习惯

视觉阈值法在光照稳定时够用,但换场地、换时段就容易失效。这套代码里带了一个ml目录,包含数据准备、SVM 训练和实时预测三部分,思路是把「是不是球」从规则判断升级成学习判断。1.get_data.cpp负责从图像里截取候选区域并提取特征,2.training_svm.cpp训练分类器,3.predict.cpp和4.predictrealtime.cpp分别做离线预测和实时预测。non_linear_svms_test.cpp是非线性核的测试,0.allinone.cpp把流程串在一起。

用 SVM 替代阈值法的关键在特征设计。常见做法是提取候选区域的 H/S/V 均值、方差、圆形度、面积占比这几个特征,正样本是球,负样本是地板、鞋子、其他圆形物体。训练时注意正负样本数量要均衡,否则分类器会偏向多数类。

// 特征提取示意:HSV 均值 + 圆形度 cv::Scalar meanHSV = cv::mean(hsv(roi), mask(roi)); double circularity = 4 * CV_PI * area / (perimeter * perimeter); cv::Mat feature = (cv::Mat_<float>(1, 4) << meanHSV[0], meanHSV[1], circularity, areaRatio);

meanHSV取候选区域在 mask 内的 HSV 均值,circularity越接近 1 越像圆,areaRatio是候选区域面积占 ROI 的比例。这四个特征维度低、计算快,适合实时场景。训练完的模型用3.predict.cpp在data里的测试图上验证,准确率能到多少取决于样本质量,不要指望一上来就完美。

验证习惯这块,我自己的做法是:每次改完视觉参数,先用data里的静态图跑一遍,确认识别框位置对;再用tennis.avi跑视频,看连续帧里识别是否稳定;最后才接规划模块看路径。跳过静态图直接跑全流程,出了问题你分不清是识别错还是规划错。从那以后我每次调视觉参数都强制走一遍「静态图 → 视频 → 全流程」三步,省下的排查时间比调参本身多得多。这套代码的README.md和readme.txt里有一些运行说明,下载后先看这两个文件,再按上面的顺序复现,能少走不少弯路。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询