
简介这是一份结合OpenCV与YOLO的台球击球路线规划系统源码包面向人工智能、计算机视觉方向的毕业设计学生以及希望将目标检测应用于运动分析的开发者。项目中YOLO负责实时检测母球与目标球的位置OpenCV完成图像预处理系统再依据动量守恒与碰撞角度等物理原理计算最佳击球点与力度从而规划出清晰的击球路线。压缩包共10个文件包含1个Python主程序、8张效果示意图和1份Markdown部署说明整体大小7.64MB结构紧凑适合快速复现。目前已有362人学习浏览尤其适合用作毕业设计参考或目标检测与物理模拟结合的学习样例。通过阅读源码与部署教程可以掌握YOLO模型调用、视频帧处理、击球路线计算以及环境搭建等关键环节为后续进一步优化检测精度或加入旋转、摩擦等因素打下基础。1. 台球击球路线规划不是玄学OpenCVYOLO 系统拆解真实球桌上母球、目标球和袋口之间的那条直线多数人靠手感多练几杆才能稳定打出。而这个基于 Python 的毕业设计项目把 OpenCV 的图像预处理和 YOLO 的目标检测串在一起相当于一个简化版的人工智能台球教练摄像头拍下球桌YOLO 框出母球、彩球和袋口系统算出一条可以直接照着打的线路并标注力度方向。我拆完这份源码后的第一感觉是难度不在 YOLO 本身而在坐标对齐和物理参数标定。它适合正在做毕业设计的学生也适合想快速上手 OpenCVYOLO 实战的开发者。下面从模块拆解到部署坑位都过一遍。2. 从视频帧到击球路线四个核心模块的数据流与参数设置2.1 三个模块的职责划分与数据流向打开源码目录核心代码按功能分成三块图像获取与预处理、YOLO 检测、路线规划。三者的数据流是串行的摄像头采集的每一帧先经过 OpenCV 预处理再喂给 YOLO 做检测检测到的球和袋口坐标传入路线规划模块最后把计算结果绘制在原图上返回给用户。如果直接把 YOLO 的原始输出拿去做几何运算会得到一堆像素坐标根本没法直接和球桌尺寸对应。我读这份源码的习惯是先理清数据流再逐文件看。比较关键的一点是源码里把坐标变换单独抽了一个函数输入是视频帧的长宽输出是标准化到球桌矩形区域的相对坐标。这种设计的好处是摄像头分辨率变了也不用改业务逻辑只需要调整变换参数。项目根目录里还有 get_hole.py专门用来标定袋口位置这个脚本跑一遍之后生成的参考点数据会保存下来供主程序调用。# 数据流主循环的骨架对应 main.py 中的核心逻辑 capture cv2.VideoCapture(0) while capture.isOpened(): ret, frame capture.read() if not ret: break # 第一步预处理 cleaned preprocess_frame(frame) # 第二步YOLO 检测母球、彩球与袋口 balls, holes run_detection(model, cleaned) # 第三步调用路线规划输入是像素坐标列表 route plan_route(balls, holes) # 第四步绘制并显示 display_frame draw_route(frame, route) cv2.imshow(billiard planner, display_frame)这段主循环把四个步骤的边界画得很清晰。实际调试时建议在每步之间加日志观察检测到几个球、中心坐标是否合理不要等到最后一条路线画出来再去猜哪一步出了问题。2.2 图像预处理管线灰度化、均衡化与滤波的参数选择OpenCV 在系统里的定位是做好“图像保洁”。源码的预处理分三步灰度转换、直方图均衡化、高斯滤波。灰度转换有效降低后续计算量均衡化用来对抗室内灯光的明暗不均滤波则抑制摄像头传感器的热噪声。这三步做下来YOLO 的输入帧比原始画面干净不少检测速度也会有可感知的提升因为灰度图的数据量只有彩色图的约三成。import cv2 def preprocess_frame(frame): # BGR 转灰度减少三通道计算开销 gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) # 使用 CLAHE 替代全局均衡化防止台面反光过曝 clahe cv2.createCLAHE(clipLimit2.5, tileGridSize(8, 8)) enhanced clahe.apply(gray) # 高斯滤波核大小 5x5兼顾去噪与边缘保留 blurred cv2.GaussianBlur(enhanced, (5, 5), 0) return blurred这段代码有两个参数值得反复调clipLimit 和核大小。clipLimit 设到 2.5 左右时台面绿色纹理不会出现伪边缘超过 3.0 后库边和球边缘容易产生高对比的假轮廓直接干扰 YOLO 检测。高斯核大小建议保持在 5 以内核太大球边缘被抹平小球的检出框会明显偏移中心点。我一般会顺手把原始帧和预处理后的帧并排显示出来对比一下 CLAHE 前后的差异。如果预处理后的画面出现明显的块状色斑说明 tileGridSize 设得太小默认 8x8 已经足够。反光很严重的台呢可以换成 16x16但计算开销会高一点。2.3 YOLO 检测参数置信度与 NMS 阈值的实测取值YOLO 模型的原始输出包含大量低置信度框源码里统一做了过滤。conf 和 iou 这两个参数直接影响检测质量conf 决定一个框是否被保留iou 决定重叠框是否被合并。def run_detection(model, frame): results model(frame, conf0.35, iou0.45, verboseFalse) balls [] holes [] for *xyxy, conf, cls in results.xyxy[0].tolist(): item { box: [int(v) for v in xyxy], conf: conf, cls: int(cls) } if item[cls] 0: holes.append(item) else: balls.append(item) return balls, holes类别编号按训练数据里的标注顺序来定我上面把袋口放在 cls0。conf 设 0.35 是相对平衡的点位光线充足时可以调高到 0.5 减少误检框光线不足时低于 0.3 就会出现大量把库边阴影当球的情况。iou 维持在 0.45 即可球与球距离较近时 iou 阈值太高会被合并成一个框。两球紧挨着时YOLO 偶尔会把它们识别成一个目标这是检测模型的天花板。解决思路是调高输入分辨率到 720 或 960 重试代价是帧率下降。如果项目预算允许换用 YOLOv8 是更顺滑的升级路径后者的分类头对小目标更友好。不过原项目基于 YOLOv5改动成本要自行评估。3. 部署这台球系统依赖版本锁定的五个注意点3.1 依赖清单与版本锁定这份项目基于 Python 3.8 开发核心依赖为 OpenCV 4.x、YOLOv5、PyTorch 以及 NumPy。版本之间存在隐性约束OpenCV 4.5 以上对 YOLOv5 的输出解析更友好Python 3.8 与 NumPy 1.24 的组合表现稳定换到 NumPy 1.26 之后部分旧代码里的 np.int 写法会直接报错。依赖推荐版本说明Python3.8项目原始开发环境opencv-python4.8.1.78图像预处理与绘制numpy1.24.3科学计算基础torch2.0.1YOLO 推理后端torchvision0.15.2与 torch 配套ultralytics8.0.xYOLOv5 加载与推理接口安装完之后先跑一段最小验证代码确认 cv2 和 torch 都能正常导入。很多翻车现场是 torch 与 CUDA 版本不匹配GPU 推理跑不起来又默默退回 CPU处理速度直接变慢十倍。这台球系统对实时性要求不算苛刻CPU 推理勉强能用但每帧处理时间会从 50ms 拉到 500ms 以上体验差别明显。python -c import cv2; print(cv2.__version__) python -c import torch; print(torch.__version__, torch.cuda.is_available())第二行如果输出 False说明 CUDA 环境没配好可以先用 CPU 跑通全流程后面再回过头来修 GPU 驱动问题。Linux 环境下 OpenCV 还容易在 import 阶段报缺少 libGL.so.1 的错误执行 apt install libgl1 libglib2.0-0 就能解决这个坑在云服务器上尤其常见。3.2 数据组织与模型微调源码使用的 YOLO 权重是 yolov5s 预训练模型在自建数据集上微调得到的。首次运行需要先把训练好的权重放到 weights 目录下然后逐帧推理。权重路径建议用相对路径方便其他同学在自备电脑上直接复现。如果换用官方预训练权重检测效果会因为缺少台球类别而完全失效。# data.yaml类别按你的标注顺序定义 train: ./dataset/images/train val: ./dataset/images/val nc: 3 names: [hole, cue_ball, target_ball]自采数据不需要太多200 张不同角度、不同光照下的球桌照片就足够支撑毕业设计级别的演示。标注工具用 labelImg 或者 CVAT 都行导出 YOLO 格式的 txt 文件后直接在训练命令里指定 data.yaml 路径。python train.py --data data.yaml --weights yolov5s.pt --epochs 100 --batch-size 16epochs 100 对于小数据集来说足够覆盖过拟合风险batch-size 16 是在常见 8GB 显存显卡下的稳定值。训练完成后把 runs/train/exp/weights/best.pt 复制到项目 weights 目录模型部分就绪。3.3 运行流程与参数入口主程序通过命令行参数控制输入源和检测参数这是源码里设计得比较顺手的地方。python main.py --source 0 --model weights/best.pt --conf 0.35--source 支持摄像头索引和视频文件两种输入。我建议先用录制好的视频调试路线计算逻辑别一上来就对着实时画面调参把问题混在一起。调试完成后再切换到摄像头索引 0逐步验证实时环境下光线变化对检测的影响。演示环境里常见的问题是摄像头权限被占用比如 Windows 上微信或浏览器后台开着摄像头程序拿不到画面。关闭无关应用后重试优先级排在改代码之前。4. 击球路线怎么算坐标变换、碰撞模型与可视化实现4.1 像素坐标到球桌坐标的透视变换YOLO 输出的边界框是像素坐标要计算击球路线必须先把坐标映射到球桌平面坐标系。常见做法是选取球桌四个库边交点作为参考点用透视变换矩阵完成映射。源码在 get_hole.py 里实现了袋口标定流程本质上是为变换准备参考点。import cv2 import numpy as np def get_perspective_matrix(pixel_points, table_points): # pixel_points: 图像上四个角点坐标 # table_points: 球桌坐标系对应四个角点坐标 matrix cv2.getPerspectiveTransform( np.array(pixel_points, dtypenp.float32), np.array(table_points, dtypenp.float32) ) return matrix def pixel_to_table(point, matrix): # 单个点从像素坐标映射到球桌坐标 pts np.array([[point]], dtypenp.float32) transformed cv2.perspectiveTransform(pts, matrix) return transformed[0][0]src 点顺序必须与 dst 一一对应顺序错位会导致整个球桌画面变形。我按左上、右上、右下、左下顺时针取点dst 对应球桌真实尺寸例如标准九尺台 254cm 乘 127cm。变换矩阵只算一次摄像头固定后取一次点即可。如果摄像头被移动过必须重新标定这条血泪经验来自一次在演示前搬动摄像头后路线全部错乱的教训。4.2 直线击球计算接触点与方向判定击球路线规划在球桌坐标系下退化为平面几何问题。目标球要进袋母球撞击目标球后目标球必须沿目标球中心和袋口中心的连线方向运动。系统计算的核心就是这条连线的反向延长线。import math def plan_straight_shot(cue_ball, target_ball, hole, radius2.86): # 目标球到袋口的单位向量 dx hole[0] - target_ball[0] dy hole[1] - target_ball[1] dist math.hypot(dx, dy) ux, uy dx / dist, dy / dist # 母球接触点目标球中心沿反向延长线偏移一个球半径 contact_x target_ball[0] - ux * radius contact_y target_ball[1] - uy * radius # 母球当前位置到接触点的距离用于估计击球力度 approach math.hypot(contact_x - cue_ball[0], contact_y - cue_ball[1]) return (contact_x, contact_y), approach注意radius 这里用的是厘米制坐标标准台球直径 57.2mm半径约 2.86cm。如果你在标定时用的是毫米坐标就传 28.6。不偏移半径直接连中心线画出来的路线在可视化里看起来没问题实际击打时必然打偏因为母球中心到达目标球中心位置时目标球已经被撞飞了。4.3 路线绘制与可视化计算完成后源码用 OpenCV 在原图上绘制两条路线母球运动线和目标球运动线。这里传入的是经过逆透视变换转回像素坐标的点。def draw_route(frame, cue_px, contact_px, hole_px): # cv2 要求坐标是整数元组 cue_px tuple(int(v) for v in cue_px) contact_px tuple(int(v) for v in contact_px) hole_px tuple(int(v) for v in hole_px) # 母球到接触点绿色实线 cv2.line(frame, cue_px, contact_px, (0, 255, 0), 2) # 接触点到袋口黄色实线 cv2.line(frame, contact_px, hole_px, (0, 220, 255), 2) cv2.circle(frame, contact_px, 4, (0, 0, 255), -1) return frame红色点标出了接触点位置。如果你发现路线经常和目标球擦边优先检查透视变换矩阵是否还是旧的。另外绘制前先把坐标转成整数float 坐标传给 cv2.line 会直接报类型错误这也是新手经常卡住的地方。5. 避坑指南五个高频检测与投影问题排查记录5.1 检测框中心不等于球的中心现象算法画出的路线总是向左偏一点目标球擦边而过误差在几个像素以内但怎么都不进球。原因YOLO 的 xyxy 边界框在球边缘被阴影干扰时框的几何中心会偏离球心直接拿框中心做坐标必然偏。解决对检测框做轻微收缩让框贴近球的可见边缘再取中心。常见做法是把框的四边向内收缩 3% 到 5%再取几何中心作为球的像素坐标。同时把 CLAHE 的 clipLimit 调低到 2.0减少阴影对框的影响。5.2 路线穿过目标球内部打不进球现象系统判定可进球但执行时母球总是先撞到目标球边缘球被撞歪。原因计算接触点时没有偏移球半径路径穿过了目标球内部。这在可视化阶段看似没问题但物理碰撞是硬接触差一个半径就是差一颗球。解决在 plan_straight_shot 中按目标球指向袋口的反向延长线偏移一个 radius并且碰撞判定里加入两球半径之和作为最小距离约束。修改后拿一个已知角度做单元测试例如目标球与袋口、母球三点共线时路线必须完全重合。5.3 连续帧跳动导致路线抖动现象球静止不动时路线在画面上来回晃动规划结果每帧都不一样。原因YOLO 对静止小目标的检测本身存在逐帧波动直接使用单帧结果会导致规划不稳定。解决对检测结果做指数移动平均取最近几帧的加权位置作为当前球坐标。def smooth_position(history, new_pos, alpha0.6): # alpha 为新帧权重history 是之前平滑后的坐标 return (alpha * new_pos[0] (1 - alpha) * history[0], alpha * new_pos[1] (1 - alpha) * history[1])alpha 取 0.6 时新帧权重较高响应足够快alpha 取 0.3 时更平滑但滞后明显。台球运动速度不快我一般取 0.5 上下。5.4 库边阴影被识别成球现象球桌边缘频繁出现检测框路线规划时把阴影当成了球画出完全不可执行的路线。原因光照不均匀时库边在台面上投下长条形阴影形状和球相近。解决调高 conf 阈值到 0.45 试一下同时限制有效检测区域把 ROI 设置成球桌内缩一圈的矩形区域库边阴影自然落在 ROI 之外。ROI 在 get_hole.py 标定完成后顺手裁剪一劳永逸。5.5 摄像头角度变化后路线整体错乱现象摄像头明明没动过某一天运行后所有路线都偏了。原因透视变换矩阵依赖四个角点的像素坐标摄像头哪怕被碰歪一度矩阵就失效了之前标定的点不再匹配。解决每次开机运行前用一张固定画面确认四个角点和屏幕上的实际位置重合再进入主程序。我习惯把角点坐标写到一个 json 文件里启动时加载并实时叠加到画面上偏差超过 3 个像素就重新标定这样能在演示前发现问题而不是等到第一杆打歪。6. 进阶玩法多球筛选、库边反弹与回归验证习惯6.1 多球场景下的目标筛选当桌面上有多个可进袋球时可以在源码基础上加一个简单的打分逻辑。每条候选路线都计算母球到接触点的距离距离短意味着击球力度更易控制加上一个角度偏差惩罚项就能排序出最优解。由于 plan_straight_shot 是纯函数输入输出都是坐标直接复用即可不需要改动其他模块。6.2 库边反弹一颗星的扩展思路直线被打不了的球可以走一库反弹。核心是镜像法把袋口关于库边对称过去目标球与镜像袋口的连线与库边的交点就是反弹点。将反弹点代入与直线击球相同的计算流程只是目标球方向改从反弹点指向袋口。这个扩展在源码里没有内建但数据结构完全支持透视变换和路线绘制都是独立函数接进来只需要半天工作量。6.3 回归验收用录制视频代替盲测最后讲一个我每次部署必做的验证习惯录一段 30 秒的固定视频涵盖球在不同位置、不同光照的多个场景每当改完坐标变换或检测参数就重跑这段视频记录路线绘制成功率。成功率低于 90% 不进行实球测试。从那以后我每次改完参数都强制走一遍回归脚本而不是直接拿着球杆去实球验证时间成本低了一个数量级。这个习惯帮我避开了多次在人前翻车希望帮到你。本文还有配套的精品资源点击获取