ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于俯视相机与YOLOv8的台球自动计分系统实战解析

2026/9/16 4:49:29 拓冰建站 浏览量
基于俯视相机与YOLOv8的台球自动计分系统实战解析 我做了一套AI台球自动计分系统用俯视相机盯着整个球台靠视觉识别和球体跟踪在进球瞬间自动判定并更新比分。起因很简单去年冬天我在球房和球友打中式八球一颗球在袋口晃了十几秒才落袋两个人为了“这球到底算不算有效”争得面红耳赤。与其靠人眼扯皮不如让机器来记分。这篇文章从硬件选型、相机标定、检测模型、跟踪算法到进袋判定和规则引擎的完整实现都会讲到坑也一并交代清楚。适合正在研究台球视觉计分、想做球类检测跟踪或者准备在球房部署智能设备的开发者参考。1. 为什么是俯视相机台球计分的场景决定了答案1.1 侧视方案为什么被淘汰最开始我并没有直接上俯视相机。第一版方案是把相机架在球台侧上方想着利用球房现有的吊灯杆安装也方便。结果实际跑起来问题一个接一个。侧视画面里球与球之间互相遮挡非常严重。只要球离得稍近两颗球就会在画面里叠成一团检测框直接合并。更麻烦的是袋口侧视时袋口是一个斜向的开口球掉进去之后画面里往往还能看到半颗球的残影系统很难判断这颗球“是不是真的入了袋”。我试过在袋口单独画ROI区域去判断但球在袋口的投影形状随角度变化规则根本写不稳定。还有个分辨率问题。球台是2.54米长侧视相机距离远端库边四五米一颗直径57.15毫米的台球在画面里只占十几个像素别说分辨实色球和花色球连稳定检测都做不到。1.2 俯视视角带来的三个结构性优势改成俯视之后整个视觉结构完全变了。第一没有遮挡问题。从正上方往下看每颗球在图像里都是一个近似圆形除非球与球紧贴否则几乎不会被其他物体挡住。球杆和手臂虽然也会进入画面但只在击球瞬间出现在局部区域对全局检测的影响小得多。第二球台是标准矩形。中式八球台内沿尺寸固定为2540×1270毫米俯视画面里四个库边就是矩形的四条边。我可以把图像坐标通过透视变换映射到毫米坐标之后所有逻辑都建立在真实空间距离上比如球速度、球间距、袋口距离这对后续跟踪和规则判定帮助极大。第三进袋等于“消失”。俯视视角下一颗球掉入袋口后会从台面平面里彻底消失。计分系统只需要检测“球进入袋口ROI”且“该球的跟踪轨迹终止”这两个条件就能判定进球。这比侧视时需要分析球是否被袋口遮挡要干净得多。1.3 俯视对规则判断的隐藏优势俯视还有一个容易忽略的好处球台所有区域都在画面内对很多规则判断天然友好。比如中式八球里母球落袋后对方获得自由球自由球需要在开球线后摆放这个位置关系俯视图一眼就能看出来。再比如判断目标球是否贴库、是否越过中线也都依赖全局空间信息。所以我的结论是如果你只想做一个“进袋自动亮灯”的玩具侧视勉强能对付但要做真正的自动计分系统俯视几乎是唯一可靠的选择。2. 硬件布局与相机标定算法跑起来之前先把坐标搞准2.1 相机、镜头、支架怎么选我用的是一台500万像素的全局快门USB工业相机配4毫米焦距镜头。选择全局快门不是因为台球速度快到需要卷帘校正而是因为球被击打时瞬时速度很高卷帘快门会把圆形球体拉成椭圆形直接影响检测框的圆心定位。如果预算有限用高端运动相机或者手机摄像头也能做实验但一定要固定机位并且确认畸变在可接受范围内。支架方面我直接用膨胀螺丝在球台上方天花板做了吊装相机距离台面大约2.8米。这个高度能保证整个台面都在画面内同时球在画面里的直径大概在40到60像素之间足够YOLO识别类别定位。补光我用两排LED灯带斜向45度往台面打光这样能尽量减少球面的镜面高光。需要提一句球房如果本身灯光复杂最好在相机下方加一个环形光源或者用亚克力柔光板把点光源打散。选型参数可以参照这张表组件我的选择备选方案注意事项相机500万像素全局快门USB工业相机高端运动相机、手机全局快门优先避免球体拖影镜头4mm定焦广角6mm定焦保证覆盖全台面畸变不宜过大支架天花板吊装龙门架球台周围落地支架高度建议2.5米以上补光LED灯带斜向45度环形无影灯避免球面产生大面积高光计算设备RTX 4090训练Jetson Orin Nano部署普通PC GPU推理端需要支持TensorRT2.2 用OpenCV做镜头畸变校正与透视映射相机安装好之后第一步不是跑检测而是标定。广角镜头边缘的桶形畸变如果不处理球在库边附近的位置会偏移好几厘米后面算进袋距离、判断贴库都会出错。我用的张正友标定法OpenCV里有现成接口。先打印一张9×6的棋盘格贴在硬纸板上从不同角度拍15到20张照片然后调用calibrateCamera得到相机内参和畸变系数。import cv2 import numpy as np objp np.zeros((6 * 9, 3), np.float32) objp[:, :2] np.mgrid[0:9, 0:6].T.reshape(-1, 2) objpoints [] imgpoints [] for fname in chessboard_images: img cv2.imread(fname) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners cv2.findChessboardCorners(gray, (9, 6), None) if ret: objpoints.append(objp) imgpoints.append(corners) ret, mtx, dist, rvecs, tvecs cv2.calibrateCamera(objpoints, imgpoints, gray.shape[::-1], None, None)标定完成之后把内参和畸变系数保存下来。后续每一帧图像先做去畸变再做检测。这一步不能省尤其是广角镜头边缘位置的球位置偏差非常大。2.3 从图像坐标到球台坐标的毫米级映射去畸变只是第一步真正让系统“懂空间”的是透视映射。因为球台是固定尺寸的标准矩形我直接在图像里找到球台四个库边角点然后做透视变换把图像映射到2540×1270毫米的虚拟平面。src np.float32([[x1, y1], [x2, y2], [x3, y3], [x4, y4]]) dst np.float32([[0, 0], [2540, 0], [2540, 1270], [0, 1270]]) M cv2.getPerspectiveTransform(src, dst)有了透视矩阵之后每一帧检测到的球心像素坐标都可以换算成球台上的毫米坐标。从这以后所有逻辑都不再依赖像素坐标。比如两个球是否相撞不再用“像素距离小于某个阈值”这种拍脑袋方式而是直接用毫米距离判断物理意义清晰参数也方便调。这里有个容易忽略的细节透视变换要建立在去畸变后的图像上顺序反了会导致映射误差被放大。另外球台四个角点最好用标定板或者高对比度标记物来选不要在画面里靠肉眼点否则误差可能达到2到3厘米进袋判定容易出问题。3. 球体检测YOLOv8只解决了一半问题3.1 为什么不用霍夫圆检测很多人一听“圆形检测”第一反应是OpenCV里的HoughCircles。我也试过。在光线均匀的午后它能检测出大部分球但一到傍晚灯光复杂就开始崩。原因有几个。第一台球表面是高反光曲面高光点会破坏圆的梯度方向HoughCircles很容易把高光边缘误认为圆边界。第二HoughCircles对半径范围敏感而俯视画面里球在图像不同位置时半径会有几个像素的波动参数窗口必须设得很大误检率随之上升。第三它只能输出位置和半径完全无法区分实色球、花色球、黑八和白球。而自动计分必须要知道进的是哪一类球。所以最终还是选了目标检测模型。3.2 目标类别设计实色、花色、黑八、白球我使用YOLOv8类别设置为四类cue白色母球eight黑色八号球solid1到7号实色球stripe9到15号花色球为什么不把序号也识别出来直接分15类因为俯视工业相机下想看清球面上的数字需要很高的分辨率而且数字印在球面弧度上透视形变明显分类难度大。实际打中式八球计分逻辑只需要知道进的是实色还是花色即可四分类已经能驱动规则引擎。如果你做的是九球或者需要按号码进球的规则那就得把球号识别做出来。方案有两个一是换1200万像素以上的相机让球面数字在图像里占到足够像素二是先检测出每个球然后把球的小图抠出来单独接一个分类网络去识别1到15号。3.3 数据集采集与标注的实操细节模型要可靠数据必须自己采。我分几轮采集白天自然光下采一批晚上灯光下采一批不同球位状态各拍一些贴库的、袋口的、散开的、两球紧贴的基本覆盖了比赛里能遇到的情况。总共采了1000多张图。标注工具用的LabelImg。标注类别就是上面说的四类。建议把球边缘完整框进去不需要太紧贴球体YOLO对标注框的宽容度比较好。数据增强方面我用了YOLO自带的Mosaic、随机翻转、亮度调整另外还额外加了运动模糊和随机过曝。运动模糊模拟击球瞬间的拖影过曝模拟灯光直射球面的高光。这一步很关键因为实际比赛里球被击打瞬间的模糊画面和强光下的高光画面是漏检高发区提前在训练阶段把这些情况喂给模型能显著减少部署后的漏检。3.4 模型训练、部署与性能调优训练命令很简单yolo detect train datapool.yaml modelyolov8s.pt epochs80 imgsz640 batch16我试过yolov8n和yolov8s。n在Jetson上更快但对小目标的检测能力弱一些白球漏检率偏高。最终选了s在RTX 4090上训练大约半小时后mAP50就到0.95以上了。部署时转成TensorRT FP16在Jetson Orin Nano上推理耗时大约15毫秒一帧完全满足实时需求。这里有个经验台球检测任务的难点不在模型本身而在数据域的一致性。同样一张球台白天和晚上灯光下的图像分布差异就很大。我在训练集里混入了不同灯光条件的数据后模型才真正稳定。4. 球体跟踪与进袋事件判定计分系统的灵魂4.1 单帧检测之外为什么要跟踪单帧检测只能告诉你当前画面里有哪些球但计分必须知道“画面里的这颗球和上一帧那颗球是不是同一个”以及每一颗球的运动轨迹。举一个很典型的例子两颗球在画面里离得很近击球后它们一左一右分开。如果没有跟踪下一帧检测器输出两颗球系统根本不知道哪颗是刚才左边那颗、哪颗是刚才右边那颗。更麻烦的是如果一颗球被球杆瞬间遮挡检测框短暂丢失重新出现后如果没有轨迹关联它就会变成一个“新球”。新球出现、旧球消失进袋判定和剩余球数统计全都会乱套。所以检测之后必须要接多目标跟踪器。4.2 ByteTrack在台球场景的适配我最终选了ByteTrack。原因很简单台球场景里所有同类球的外表几乎一样DeepSORT那种依赖表观特征做ReID的思路在这里使不上劲。ByteTrack靠运动和位置关联用卡尔曼滤波预测球下一帧位置再用检测框和预测框的IoU做匹配即使检测置信度较低只要位置对得上也会保留轨迹。这非常适合台球球是刚性球体运动轨迹连续不会凭空跳动。我在ByteTrack开源实现上做了两点改动。第一把原来的线性分配求解器换成了更轻量的匈牙利匹配减少CPU开销。第二增加轨迹存活时间参数当球被遮挡超过1秒就删除轨迹避免在袋口区域积压大量“幽灵轨迹”。跟踪器的输入是检测框输出是带ID的轨迹列表。轨迹里保存了球的历史位置可以算速度、加速度这些是进袋判定和碰撞分析的基础。4.3 进袋事件判定候选、确认、撤销进袋判定的核心逻辑我用“候选、确认、撤销”三态来做。先定义袋口ROI。中式八球台有六个袋口我在毫米坐标系里为每个袋口定义一个圆形ROI半径大约60毫米。然后按下面这个流程处理每一帧某颗球的球心进入袋口ROI把它标记为“候选进袋”。后续连续5帧里这颗球的球心仍然在ROI内并且全局跟踪器里已经没有这条轨迹确认为进球。如果球进入ROI后停了一下又离开且跟踪器仍然保留该球的ID撤销候选状态。这个设计的价值在于处理两个高频误判场景。第一球在袋口边缘晃动但没有掉进去它的轨迹始终存在系统永远不会计分。第二球真正落袋后检测框和跟踪框同时消失系统在ROI内找不到对应轨迹就把进球事件落地。“连续5帧”这个参数需要根据相机帧率调。30帧每秒的相机5帧就是约0.17秒对计分体验来说几乎无感同时又能过滤大部分抖动场景。如果帧率降到15FPS可以把确认帧数降低到3帧。袋口ROI的代码结构大概是这样pockets { top_left: (x_tl, y_tl, 60), top_right: (x_tr, y_tr, 60), mid_left_up: (x_mlu, y_mlu, 60), mid_right_up: (x_mru, y_mru, 60), bottom_left: (x_bl, y_bl, 60), bottom_right: (x_br, y_br, 60), } def is_in_pocket(ball_pos, pocket_center, radius): return (ball_pos[0] - pocket_center[0])**2 (ball_pos[1] - pocket_center[1])**2 radius**24.4 计分规则引擎以中式八球为例进袋事件只是原始信号真正的“计分”在规则引擎里完成。我用一个类管理中式八球的计分状态核心字段包括当前玩家花色、双方剩余球数、黑八状态、犯规状态。基础逻辑用伪代码表达class ChineseEightBallScorer: def __init__(self): self.player_side None # None / solid / stripe self.remaining {solid: 7, stripe: 7} self.black_potted False self.foul False def on_ball_potted(self, ball_type): if ball_type in (solid, stripe): if self.player_side is None: self.player_side ball_type if ball_type self.player_side: self.remaining[ball_type] - 1 elif ball_type eight: if self.remaining[self.player_side] 0: return win else: return lose elif ball_type cue: self.foul True return opponent_free_ball实际实现要比这复杂一些因为要考虑回合切换、开球局是否有效、母球先碰到对方花色算犯规等情况。我的建议是先把基础计分跑通再逐步补规则不要一开始就想把完整的裁判规则全部塞进去。5. 从“能检测”到“不误判”我在实战中填平的坑模型和跟踪器都跑通之后系统仍然会有各种边角问题。真正让系统可用的不是模型精度而是这些细节问题的处理。5.1 高光反射导致白球漏检的完整排查有一次晚上连续三杆白球漏检我以为是模型崩了。回看原始图像才发现白球在灯光直射下变成了一团过曝的白色斑块球面纹理完全消失检测器无法提取到有效特征置信度骤降到0.2以下。我做了三层处理。第一降低相机曝光时间从默认的5000微秒调到3000微秒避免高光区域完全溢出。第二把LED灯带从正上方改成斜向45度打光减少镜面反射。第三在训练数据增强里加入过曝模拟。处理之后白球漏检率大幅下降。排查这类问题不要只看模型的输出一定要把原始帧图像拉出来看。很多视觉问题看检测结果图根本看不出原因看原图一眼就明白了。5.2 两颗球紧贴合并成一个框打球时经常出现两球紧贴的情况目标检测的NMS很容易把两个重叠框合并成一个。YOLO默认的NMS阈值是0.7我调到了0.4缓解了一部分问题。但阈值调太低密集场景又会漏检。更稳的做法是后期分裂。如果检测框的长宽比明显接近2倍球径且置信度处于中等水平就把它当作两个重叠球用椭圆拟合拆成两个中心点。这个方案需要在标定阶段把球径的毫米值换算成像素值框的宽度接近两倍球径时就触发分裂逻辑。直接调NMS阈值最简单适合快速验证分裂法更稳但工程实现复杂度高一些。5.3 球杆与手臂遮挡导致ID漂移击球瞬间球杆会先于母球进入画面球杆前端是深色有时会被误检成一个球有时会把母球框顶开。跟踪器匹配时母球框消失下一帧重新检测出的母球位置和上一帧预测位置不一致ID就换了。我用慢动作视频逐帧回放确认了这个过程。解决办法分三层第一训练数据里专门加入球杆进入画面的样本让模型学会不把球杆当球同时把被球杆遮挡的球也检测出来。第二跟踪器的匹配距离从30像素放宽到50像素匹配前先用卡尔曼预测位置做优先搜索。第三当母球短时丢失但预测位置附近出现高置信度检测时强制恢复原ID。做了这三层处理后ID漂移事件明显减少。5.4 袋口悬停球的误判处理袋口悬停是计分系统最容易误报的场景。最常见的情况是球卡在袋口边缘重心还在台面上人用手碰了一下它才掉进去或者球在袋口剧烈晃动但没有真正落袋。我的策略就是不着急判定。候选状态持续期间每一帧都检查球是否真的消失。如果球只是在原地抖动检测框始终在ROI内永远不会触发进球。只有当轨迹消失这个条件满足后才真正进袋。同时我在前端App上把“确认进球”的延迟控制在1秒以内视觉上不会觉得系统反应迟钝。5.5 网上预训练模型在自家球台上的失灵一开始我图省事从GitHub找了一个别人训练好的台球检测权重直接放进系统。结果在自己的球台上一测准确率不到50%。原因很简单对方用的是美式九球的配色方案、不同的光照、不同的相机高度和分辨率数据分布天差地别。台球检测不是通用的图像分类环境和球台类型对数据分布的影响极大。后来我重新用自己拍的数据训练哪怕只有几百张图mAP就恢复到90%以上。所以做这类项目第一个结论永远是用自己的相机、自己所在场景的数据才是可靠的基础。6. 实测效果评估与未来方向6.1 五百次击球的实测统计系统稳定运行后我做了一组500次击球的测试覆盖不同走位、不同灯光条件。结果如下指标数值测试击球次数500实际进球数387正确判定次数380漏判次数4误报次数3判定准确率98.2%漏判主要发生在击球瞬间手臂正好挡在袋口上方进球过程被完全遮挡。误报则几乎都出在袋口附近的物理晃动比如贴库球被下一杆震进袋但系统没有把这次进球和上一杆动作关联起来。6.2 误差来源分析从统计结果看漏判和误报的比例已经很低但离“电子裁判”的标准还有距离。漏判的核心是遮挡单路俯视相机在手臂或球杆完全挡住袋口时无能为力。要解决可以考虑加一路侧视摄像头专门盯袋口或者用多视角融合。误报的核心是袋口附近的物理状态复杂球是否真的离开台面纯视觉判断存在不确定区间。更彻底的方案是在袋口下方加震动传感器但这类物理传感器会破坏球台结构一般球房不一定愿意配合。我更倾向多视角融合俯视负责全局跟踪和规则判断侧视负责袋口进球的最后确认。6.3 后续扩展思路下一步我有几个明确方向。第一训练球杆与手部关键点检测模型识别击球动作和母球碰撞顺序这样能自动判断“母球先碰到哪颗球”把犯规判定做出来。第二支持九球和斯诺克的计分模式九球要求按球号顺序进球这就需要把球号识别加进来。第三把模型进一步压缩部署到更低功耗的设备上方便球房大面积安装。还有一个方向是给每个球单独建立历史轨迹数据库这样不仅能看到比分还能复现每一杆的走位和进球过程对球员复盘训练很有用。最后分享一个小经验如果你想复刻这套系统别一开始就追求把所有规则做完。先做一个“只进袋、不计花色”的原型在真实球台上跑通再把规则一层层加进去。台球计分真正难的不是检测而是袋口抖动、球杆遮挡、高光反射这些边角情况。我在这些细节上花的时间最多也正是它们决定了一套系统到底能不能在真实球房里稳定用下去。做这类视觉项目永远要把“现场跑”放在“模型刷分”前面。