2026/10/11 2:03:50

Python+OpenCV+YOLO实现台球击球路线规划全流程

Python+OpenCV+YOLO实现台球击球路线规划全流程 简介一套基于OpenCV与YOLO的台球击球路线规划系统源码包面向人工智能方向毕业设计、计算机视觉入门者以及台球辅助分析爱好者。项目以Python为主实现语言利用YOLO目标检测识别母球与目标球位置借助OpenCV完成图像预处理并结合动力学模型计算最佳击球角度与力度兼顾学术研究与实际游戏场景应用。压缩包共10个文件包含1个Python主脚本、1个Markdown说明文档及8张过程/效果示意图整体体积仅7.64MB轻量便于快速部署与阅读。已有362人学习适合需要完整项目参考或二次开发的人群。内容涵盖环境配置、代码结构解析、模型测试及部署教程同时附有路线规划效果展示图可帮助理解系统如何通过边界框定位、碰撞预测与物理模拟输出可行击球策略。对于希望将机器学习知识落地到具体项目的读者这套资源提供了可直接运行的源码和清晰的配套说明便于在此基础上扩展摩擦力、旋转球等更复杂的物理模型。1. 台球击球路线规划这个题难的不是几何是让机器先看清球很多人一听“Python OpenCV YOLO 做台球击球路线规划”第一反应是“不就是算个反射角吗”。真上手做一遍才发现几何计算五分钟能写完真正卡住你的是另一件事怎么从一帧乱七八糟的台球桌画面里稳定地把母球、目标球、袋口、库边的坐标抠出来。这套系统解决的就是这个完整链路——用YOLO做目标检测用OpenCV做几何校正和路线可视化最后在画面上画出一条可执行的击球线。它适合两类人一类是想给台球教学视频自动加路线标注的开发者另一类是正在做桌面体育视觉分析、需要一个可复现模板的工程师。系统不是万能的但这条链路值得你完整走一遍。2. 先搭环境再谈算法OpenCV与YOLO的版本选型与最小可用配置2.1 为什么选YOLO做球体检测而不是用霍夫找圆传统做法里检测台球最常见的手段是OpenCV的HoughCircles。听起来很合理球是圆的找圆就行。但实际跑起来你会发现这个思路在台球桌上基本翻车球与球紧贴时边缘互相遮挡、球体上有高光反光、球在库边附近有阴影这些都会让霍夫变换要么漏检要么误检。更麻烦的是HoughCircles对半径范围敏感台球在画面里大小不是固定的离摄像头近的球大、远的球小你很难用一个固定半径区间覆盖整张球桌。YOLO是数据驱动的检测方式训练好之后对“球”和“袋口”的识别不再依赖几何形态而是依赖纹理、颜色、边缘的组合特征。台球虽然长得像但不同球的花色纹理差异是很大的YOLO能把这些差异学进去。另外台球场景还有几个特殊物体——袋口、球杆、库边它们根本不是圆HoughCircles完全无能为力而YOLO可以统一检测。选YOLO的另一个现实理由是这个项目要落地到可运行状态YOLO系列的推理代码成熟权重文件不大CPU也能跑不需要为一个小工具专门配一台带GPU的机器。我一般会选YOLOv5的官方仓库作为基础不是因为它最新而是因为它的部署文档最全遇到问题搜得到答案。YOLOv8更新但自定义数据集训练的配置路径略有差异对新手来说yolov5的坑更浅。2.2 最小环境Python、OpenCV、YOLO的安装与版本配套环境搭建这件事没有玄学就是版本配套的问题。推荐用Python 3.8到3.10之间的版本太新或太旧都会遇到某个依赖包没编译好对应轮子的问题。OpenCV用pip装官方预编译包就行不需要自己源码编译——除非你要用一些contrib里才有的冷门函数。YOLOv5官方仓库会拉取PyTorch依赖PyTorch的CPU版安装包很大但不难。常见做法是先在项目目录建一个虚拟环境避免把系统Python搞乱。下面是Ubuntu和Windows通用的最小安装流程python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate pip install --upgrade pip pip install opencv-python opencv-contrib-python numpy git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt这段逻辑很直接先创建隔离环境再装OpenCV和Numpy然后拉YOLOv5仓库并安装它的依赖。requirements.txt里包含torch、torchvision、matplotlib等安装时间取决于网络状况。需要注意opencv-python和opencv-contrib-python两个包可以共存但不要同时装两个不同来源的OpenCV否则import cv2时会出现符号冲突。装完可以快速验证一次python -c import cv2; print(cv2.__version__) python -c import torch; print(torch.__version__)看到输出版本号就说明环境通了。Windows用户如果遇到“No module named cv2”八成是虚拟环境没激活或者pip装到了全局Python里后面避坑章节我会展开讲。2.3 验证环境让YOLO跑通第一张台球桌图片环境装好后不要急着训练自己的模型先跑一次官方预训练权重确认推理链路是完整的。拿一张台球桌照片用YOLOv5的detect脚本指定用最小的yolov5s权重cd yolov5 python detect.py --weights yolov5s.pt --source ../snooker_table.jpg --conf 0.35detect.py会自动下载yolov5s.pt权重文件大约15MB。跑完后结果保存在runs/detect/exp目录下。--conf参数是置信度阈值0.35是我在台球场景下的常用起点。官方预训练权重是在COCO数据集上训的COCO里没有台球类别所以可能会把球误检成别的类这很正常。这一步的目的只是验证CUDA或CPU推理链路没断、图像读写正常、结果能输出。真正能用的检测器得用自己的数据集训练。如果这一步跑出来提示“Normalize”相关报错通常是你输入图片路径写错了或者图片格式不是常规的jpg/png。detect.py对中文路径支持得不好项目路径和图片路径都尽量用英文。3. 球桌视觉感知用YOLO识别球、袋口与球杆并输出坐标3.1 数据准备标注类别与数据集组织想让YOLO在台球桌上干活必须先给它喂一批标注好的台球图像。常见做法是采集你自己球房场景的图片再混入一些公开的台球数据集组合训练。类别设计上我建议至少分四类cue_ball母球、target_ball目标球可以细分到具体号码也可以统一为一类、pocket袋口、cue_stick球杆用于判断击球方向或排除干扰。标注工具用labelImg或者labelme都行导出成YOLO格式的txt文件每行是“class_id x_center y_center width height”坐标是归一化到0-1的浮点数。注意台球在画面里比较小标注框要贴着球体边缘框大了会让训练时IoU计算产生偏差。数据集目录结构按YOLO惯例组织datasets/ snooker/ images/ train/ val/ labels/ train/ val/然后写一个data.yaml文件内容是指向这些目录的路径和类别名。类别顺序一定要和训练时保持一致不然模型输出全乱。训练集和验证集不要有同源图片我见过有人直接从一段视频里抽帧结果训练帧和验证帧来自同一段画面模型在验证集上指标虚高一到真实球房就废。3.2 训练检测器关键参数与损失函数收敛观察训练命令本身不复杂复杂的是参数。我常用的最小训练配置如下cd yolov5 python train.py --data ../datasets/snooker/data.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --device cpu # 有GPU就写0这里img 640是输入分辨率球桌场景里球体算小目标640是底线想更准可以用1280但推理会变慢。batch 16在CPU上可能吃力显存不足就把batch降到8或4。epochs 100对新手够用观察训练输出的loss曲线——重点看box_loss和cls_loss是否持续下降。如果loss在前30个epoch就跌到平台期然后反复震荡说明学习率没配好或者数据量太少常见做法是换yolov5s自带的学习率配置它会自动做cosine衰减一般不需要手调。训练结束后模型权重保存在runs/train/exp/weights/best.pt。后续推理都用best.pt而不是最后一轮的last.pt。best.pt是根据验证集mAP挑选的。3.3 推理与坐标换算从检测框到台球圆心模型训练完写一个推理脚本加载best.pt对每一帧画面输出检测框然后把YOLO的xyxy格式转成球心和半径。下面是我常用的处理片段import cv2 import torch import numpy as np model torch.hub.load(ultralytics/yolov5, custom, pathruns/train/exp/weights/best.pt, force_reloadTrue) def get_balls_and_pockets(frame, conf_thres0.4): # 推理得到检测结果 results model(frame, size640) boxes results.xyxy[0].cpu().numpy() # (N, 6): x1, y1, x2, y2, conf, cls balls, pockets [], [] for x1, y1, x2, y2, conf, cls_id in boxes: if cls_id 0: # 母球 cx (x1 x2) / 2.0 cy (y1 y2) / 2.0 r (y2 - y1) / 2.0 # 用高度算半径更稳 balls.insert(0, (cx, cy, r)) elif cls_id 1: cx (x1 x2) / 2.0 cy (y1 y2) / 2.0 r (y2 - y1) / 2.0 balls.append((cx, cy, r)) elif cls_id 2: # 袋口这里保留矩形中心 pockets.append(((x1 x2) / 2.0, (y1 y2) / 2.0)) return balls, pockets几个细节值得说明。第一半径为什么用高度算而不是宽度台球检测框的宽度经常被阴影和球间反光撑大而高度受库边和球桌边缘影响更小实测用高度算半径误差更小。第二母球放在balls列表首位因为后续路线规划要把母球单独拿出来。第三conf_thres设0.4是平衡点太低会混入误检太高会漏掉远端的球这个值你可以按自己的球桌画面微调。3.4 球体定位的精度边界检测框偏移带来的连锁问题YOLO检测框是一个矩形但台球是圆形矩形和球体之间存在固定的系统偏差——尤其是在球的边缘有阴影时检测框会往阴影方向偏出去几个像素。这几个像素在视觉上微不足道但换算成球心坐标后会直接导致击球路线偏出袋口。解决思路有两个一是把检测框做得更准训练数据里尽量包含不同光线条件让模型见过各种阴影形态二是在拿到检测框后对球心坐标做一次修正——常见做法是在检测框内再次用灰度质心法或最小二乘拟合圆精确定位球心。def refine_ball_center(frame, x1, y1, x2, y2): roi frame[int(y1):int(y2), int(x1):int(x2)] gray cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY) _, thresh cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) moments cv2.moments(thresh) if moments[m00] 0: cx int(moments[m10] / moments[m00]) cy int(moments[m01] / moments[m00]) return x1 cx, y1 cy return (x1 x2) / 2.0, (y1 y2) / 2.0这段代码的原理是在YOLO给出的检测框内做一次OTSU二值化把球体从背景中分离出来然后计算二值图的质心。质心比矩形中心更接近真实球心。但要注意如果球体表面有强烈反光二值化会把高光区域漏成黑点质心会偏移。真遇到这种情况可以在反光区域做一次闭运算填补。这一步属于细节优化对最终路线的落地准确率有直接影响。4. 击球路线规划镜面反射、碰撞点搜索与可视化叠加4.1 直接击球母球到目标球的碰撞点计算视觉识别把球的位置拿到手以后路线规划就是纯粹的几何问题了。先讲最简单的直接击球路径。假设目标球T要进袋口P那么目标球被撞击后必须沿着T到P的方向运动。根据球的碰撞物理母球必须击中目标球球心与袋口连线的延长线上、距离目标球球心一个半径的位置。这个点就是碰撞点C。母球的击球方向就是母球球心到C点的连线方向。import numpy as np def direct_cue_point(target_center, pocket_center, ball_radius): # 目标球球心到袋口的方向向量 tx, ty target_center px, py pocket_center dx, dy px - tx, py - ty dist np.hypot(dx, dy) if dist 1e-6: return None # 球在袋口里不需要击球 nx, ny dx / dist, dy / dist # 碰撞点在目标球球心沿袋口方向一个半径的位置 cx tx nx * ball_radius cy ty ny * ball_radius return (cx, cy)逻辑很简单归一化方向向量再沿方向推出一个半径的距离。这个碰撞点是母球中心要去的位置。真正击球时球杆击打母球母球向前滚动当母球中心到达碰撞点C时两个球刚好接触能量沿两球心连线传递目标球就沿TP方向运动。这里默认两球半径相同台球确实是这样。要注意如果母球、目标球、袋口三点几乎共线且目标球挡在中间这个碰撞点可能在母球身后这种情况下直接击球打不到目标球后面会讲怎么处理。4.2 一库解球镜像点法与库边反弹搜索直接击球路线被阻挡时最常见的手段是打一库球——母球先撞一次库边反弹后再命中目标球。求解这个路线有一个特别优雅的经典做法镜像点法。先把母球A关于目标库边做镜像得到A然后连接A和目标球的碰撞点C这条线段与库边的交点R就是母球应该撞击的库边位置。母球沿A到R方向击出碰库后反弹反弹路线恰好经过碰撞点C。def reflect_point(point, line_p1, line_p2): # 求点关于直线的镜像点 p np.array(point, dtypefloat) a np.array(line_p1, dtypefloat) b np.array(line_p2, dtypefloat) ab b - a t np.dot(p - a, ab) / np.dot(ab, ab) projection a t * ab return 2 * projection - p def one_cushion_route(cue, target, pocket, cushion_p1, cushion_p2, r): # 求母球镜像 cue_mirror reflect_point(cue, cushion_p1, cushion_p2) # 目标球碰撞点 cue_point direct_cue_point(target, pocket, r) if cue_point is None: return None # 镜像点与碰撞点连线与库边的交点 # 用线段交点公式求解 ...这里不把完整交点代码贴完核心是理解镜像法的物理意义。库边碰撞可以看成镜面反射入射角等于出射角。用镜像点把“撞库反弹”转化为“直线穿过库边到达镜像点”几何问题就变成了求两条直线交点。这个做法对四个库边都成立分别对上下左右四条库边做镜像就能枚举四种一库解球路线。实际做的时候有个陷阱镜像点A与碰撞点C的连线不一定真的和该库边相交可能交点在库边延长线上这时要判断交点坐标是否落在球桌库边的有效线段范围内。判断方法是检验交点的x坐标是否在库边两个端点的x范围之间y坐标同理。4.3 多解排序与推荐给用户画哪条线一库解球最多能产生四条候选路线直接击球也可能有两条目标球有两个相邻袋口可选。系统不可能把所有路线都画在画面上否则屏幕一片乱。推荐逻辑我一般按三个维度排序路径长度、碰撞角度、袋口距离。路径长度越短母球需要走的距离越短能量损耗越小成功率越高。碰撞角度指母球撞击目标球时的入射角与目标球进袋方向的夹角接近90度时能量传递效率最高擦边球则效果差。袋口距离是目标球到袋口的距离越近越容易进。把这三维度做加权评分最简单的做法是score w1 * 1/distance w2 * angle_score w3 * 1/pocket_dist权重根据实测效果调。没有固定最优权重我一般用w10.5w20.3w30.2起步然后用第6章说的离线回放法调。4.4 把路线画回图像OpenCV可视化叠加规划结果最终要画在视频帧上给人看。用OpenCV的line函数和circle函数把母球路径、库边碰撞点、目标球进袋路径用不同颜色画出来。母球路径用白色碰撞点画一个高亮圆目标球进袋路径用绿色。def draw_route(frame, cue, cushion_point, target, pocket, color(0, 255, 0)): # cue: 母球中心, cushion_point: 库边碰撞点, target: 目标球中心, pocket: 袋口中心 if cushion_point is not None: cv2.line(frame, (int(cue[0]), int(cue[1])), (int(cushion_point[0]), int(cushion_point[1])), color, 2, cv2.LINE_AA) cv2.circle(frame, (int(cushion_point[0]), int(cushion_point[1])), 6, (0, 0, 255), -1) cv2.line(frame, (int(cue[0]), int(cue[1])), (int(target[0]), int(target[1])), color, 2, cv2.LINE_AA) cv2.line(frame, (int(target[0]), int(target[1])), (int(pocket[0]), int(pocket[1])), (0, 255, 255), 2, cv2.LINE_AA)绘图本身没什么难度关键在于线条宽度和颜色在球桌绿色背景下要足够醒目。LINE_AA抗锯齿让斜线看起来平滑避免出现明显锯齿感。实时视频处理时这些绘制操作都在内存里的帧副本上做原帧不要动便于后面做记录。这样每一帧都能看到母球路径、库边碰撞点、目标球进袋路径的完整流程。5. 部署与避坑从源码到能跑的系统的5个关键问题5.1 坑1No module named cv2 与 YOLO 环境冲突现象终端执行python detect.py报错“ModuleNotFoundError: No module named cv2”但 pip list 里明明有opencv-python。原因最常见的是虚拟环境没激活或者激活的不是当前终端所在的Python环境。另一种情况是你机器上装了多个Python版本pip install安装到了Python 3.8而脚本用Python 3.10跑。还有一种隐蔽情况是conda和pip混合使用conda环境里用pip装的包conda的Python也是能import的但如果base环境与虚拟环境搞混就会踩坑。解决先明确当前解释器路径用python -c import sys; print(sys.executable)查看用的是哪个Python再用这个Python执行python -m pip install opencv-python强制装到当前环境。装完后在同一终端里用python -c import cv2; print(cv2.version)验证。如果还不行把venv目录删掉重建这种翻车十有八九是环境串了重建最省事。5.2 坑2连续帧检测框抖动导致规划路线跳变现象球没动但画面里画的击球路线每隔几帧就跳一下有时指向左边袋口过两帧又指向右边袋口。原因YOLO对同一个球的检测框在连续帧里会存在几个像素的随机波动置信度低的时候框的位置波动更大。而路线规划对母球和目标球位置极其敏感球心坐标偏差2个像素路线方向就能偏出好几度到远端袋口可能偏出去十几厘米。解决做两层过滤。第一层是提高置信度阈值从0.35提到0.45过滤掉那些模型自己都没把握的检测结果。第二层是对检测到的球心坐标做指数移动平均EMA对第t帧的坐标进行平滑alpha 0.6 # 平滑系数越大越信任历史值 smoothed_cx alpha * prev_cx (1 - alpha) * current_cx smoothed_cy alpha * prev_cy (1 - alpha) * current_cyalpha取0.6时历史帧权重占六成当前帧占四成能有效抑制抖动又不至于让轨迹响应变慢太多。如果画面里球真的在运动alpha太大比如0.9会让路线滞后手感很差需要在平滑和响应速度之间找平衡。5.3 坑3球体粘连导致NMS误删现象两个球紧贴在一起时检测结果只输出一个框把两个球当成一个球处理路线规划直接把目标球位置算错。原因YOLO默认的NMS非极大值抑制IoU阈值是0.45两个球挨得近时两个检测框的重叠度很高NMS会把置信度较低的那个框干掉。台球在比赛场景里经常两颗球贴在一起这是这个场景的特有频率。解决把NMS的IoU阈值调低比如0.3让两个重叠检测框更可能被保留。YOLOv5的detect.py里--iou-thres参数就是干这个的。但调太低会引入重复检测同一个球出现两个框。更稳的办法是在后处理里加一个规则如果两个保留的检测框中心距离小于1.5倍球直径就视为粘连球分别保留并做质心修正。另外训练数据里多包含粘连场景的标注模型自己学会了区分效果比调NMS更好。5.4 坑4绿色台呢干扰让库边检测失效现象系统用的是HSV颜色分割找库边结果球杆上的绿色握把、观众席的绿植都被误判成库边路线画到球桌外面去了。原因这个坑不是YOLO的锅是OpenCV颜色分割的经典误判。台呢的绿色和很多环境物体的绿色在HSV空间里重叠单纯靠颜色判断库边位置不可靠。解决颜色分割只做辅助主判断交给YOLO检测出的pocket袋口和球的位置来约束。库边只能出现在球桌范围内球桌范围可以用四个袋口坐标拟合出来。拿到袋口坐标后把库边搜索限制在袋口连线的四边形区域内区域外的绿色像素一律忽略。HSV阈值也要收敛绿色台呢的H值通常在40到80之间S和V范围需要根据现场光照标定不要直接用网上的通用值。5.5 坑5摄像头视角畸变导致路线偏出袋口现象摄像头装在球桌正上方偏侧的位置画面里库边不是平行的球在下半场和上半场的大小不一致。路线在画面中央区域看着准到了边库附近就明显偏离。原因这是透视投影造成的。方案默认检测到的坐标是图像坐标但路线规划用的几何模型假设是俯视正投影两者之间存在一个透视变换误差。如果不校正远端目标的坐标误差会被放大。解决做一次四点标定。选台面上四个已知物理位置的参考点一般是四个袋口中心用cv2.getPerspectiveTransform求透视变换矩阵把图像坐标映射到俯视台面坐标算完路线后再把结果用逆变换映射回图像坐标绘制。如果用的是深度相机也可以用solvePnP求相机外参但台球桌是平面场景四点标定已经足够用solvePnP属于过度设计。6. 让系统更可用离线回放验证与合成数据测试的训练系统能跑通和系统能用中间隔着一大段验证工作。我最后分享两个自己常用的验证方法这两个方法能帮你把参数调明白而不是靠肉眼感觉。第一个是离线回放验证。录制几段真实击球视频包含不同光线、不同角度、母球贴库、球堆密布这些典型场景。离线逐帧跑检测和路线规划把每一帧的规划结果叠加保存成新视频然后对照原视频逐秒观察路线是否稳定、是否画在正确方向、实际击球后路线预测与真实运动轨迹的偏差有多大。这个方法最大的价值是让你能反复看同一帧调参数时有一个客观参照。我调EMA平滑系数和置信度阈值时都是靠离线回放对比前后两版视频而不是在屏幕上实时改参数那样压力太大根本看不准。第二个是合成数据回归测试。找一个台球模拟器生成带标注的合成帧或者自己写脚本在空台上渲染随机摆放的球和袋口位置。合成数据的好处是坐标是真值可以直接计算路线规划的误差——规划路线与理论最优路线之间的夹角偏差、碰撞点位置偏差作为量化指标。这样每改动一次检测后处理或几何算法都能跑一套回归测试看看误差是变大还是变小。这个习惯一开始会嫌麻烦但当你改了库边碰撞点的判定条件导致一库路线全部偏掉的时候就知道后悔药多重要了。我做这个系统的血泪经验是台球击球路线规划的重心在“感知”而不是“规划”。几何算法是确定性的写一遍就稳定视觉识别才是黑匣子光照一变、桌面颜色一变、摄像头角度一变同样的代码表现出天壤之别。如果你的路线画不准先别怀疑反射角算错了去检查YOLO输出的球心坐标是不是稳定。先把检测精度做到极致再回头看几何这个顺序能帮你少走很多弯路。希望这些踩坑记录能帮你把这个系统顺利跑起来。本文还有配套的精品资源点击获取