FEATURED · 精选文章

Python+OCR实现乒乓球比分识别与可视化分析

发布时间 / 2026/9/2 23:08:31
来源 / 创域科博编辑部
栏目 / 资讯中心
Python+OCR实现乒乓球比分识别与可视化分析 乒乓球比赛的讨论一直不缺热度但比起赛后情绪化的复盘更值得做的其实是用技术手段把比赛数据整理成可复用的分析资产。这次我们来看一个基于 Python 的乒乓球赛事数据可视化分析方案从视频抽帧、OCR 识别比分、数据清洗到最终生成统计图表一条链路全部打通。文章会重点展示如何用 OpenCV 做视频帧截取、用 PaddleOCR 识别画面中的计分板文字、用 pandas 做数据聚合最后通过 pyecharts 输出直观的可视化看板。这套方案不依赖昂贵设备普通办公电脑就能跑OCR 环节可以切换 CPU 或 GPU 推理。如果你平时关注体育数据相关项目或者想把某一场比赛视频变成结构化数据这篇文章可以直接收藏。1. 核心能力速览能力项说明项目类型乒乓球比赛视频数据提取与可视化分析主要功能视频抽帧、比分 OCR 识别、数据清洗、统计图表生成技术栈Python、OpenCV、PaddleOCR、pandas、pyecharts支持平台Windows / Linux / macOS推荐硬件普通 CPU 可跑有 NVIDIA GPU 可加速 OCR 推理显存占用取决于 OCR 模型默认约 1G 到 2G以实际环境为准启动方式命令行脚本逐个执行或 Flask 提供 API 服务是否支持 API支持可封装为 HTTP 接口是否支持批量任务支持可对目录内多个视频文件批量处理适合场景赛事数据整理、技术统计、内容制作辅助、体育数据教学这个方案的核心价值不是预测比赛结果而是把视频里肉眼可见的比分、回合、局数转成可以查询、统计、对比的结构化数据。后续无论是做数据看板、报告配图还是作为体育数据分析课程的教学案例都能复用。2. 适用场景与使用边界这类工具适合三类人第一类是体育内容创作者。拿到一场比赛视频后与其反复拖动进度条记录比分不如让脚本自动抽帧识别生成一份“每局比分变化曲线”。第二类是数据爱好者和学生。用乒乓球比赛数据练手能同时接触到 cv2、OCR、pandas、可视化库是一个完整的 Python 数据处理项目。第三类是体育媒体或数据服务团队可以把这套流程嵌入到已有采集管线里为比赛报道提供基础数据。需要注意这个方案并不适合用来做实时直播分析。抽帧加 OCR 的处理速度通常在秒级对于实时计分场景延迟会比较大。也不适合直接处理画面质量极差的视频比如低分辨率、角度遮挡严重、计分板被字幕遮挡等情况识别准确率会明显下降。使用边界方面需要特别说明视频素材必须有合法来源。如果处理的是公开比赛视频要注意平台版权要求如果涉及运动员肖像和姓名信息不要在未授权的情况下对外发布带有负面倾向的解读。本文只讨论数据提取和可视化方法不针对任何具体比赛结果做价值判断。3. 环境准备与前置条件开始之前先确认本机环境。操作系统建议 Windows 10/11 或 Ubuntu 20.04 以上macOS 也可以但部分依赖的安装方式略有不同。Python 版本建议 3.9 到 3.11PaddleOCR 对极端新版本 Python 的适配可能滞后所以不建议直接用 3.12 或更高版本。核心依赖如下opencv-python负责读取视频、抽取关键帧。paddleocr负责识别画面中的中文和数字文本用于比分读取。paddlepaddle 或 paddlepaddle-gpuOCR 底座纯 CPU 环境安装 CPU 版有 NVIDIA 显卡则安装 GPU 版。pandas数据清洗和聚合。pyecharts 或 matplotlib可视化输出。flask如果需要提供 API 服务。视频文件建议使用 MP4 格式分辨率不要低于 720P。如果视频帧率是 25fps 或 30fps抽帧时建议每秒取 1 到 2 帧冗余度较高。磁盘空间方面一段 1 小时比赛视频抽帧图片大约占用 200 到 400MB按实际帧率和图片大小估算。CUDA 环境不是必须的。纯 CPU 也能完成整个流程只是 OCR 环节会慢一些。如果有 NVIDIA 显卡建议安装 CUDA 11.8 配套的 PaddlePaddle GPU 版本识别速度会有明显提升显存占用一般在 1G 到 2G 之间具体以实际模型加载为准。4. 安装部署与启动方式安装过程建议使用独立的 Python 虚拟环境避免污染全局环境。下面给出 Windows 和 Linux 通用的命令流程。# 创建虚拟环境 python -m venv paddle_env # 激活环境 # Windows paddle_env\Scripts\activate # Linux/macOS source paddle_env/bin/activate # 升级 pip python -m pip install --upgrade pip接着安装基础依赖。# 安装视频处理和数据处理库 pip install opencv-python pandas flask # 安装可视化库pyecharts 用于生成 HTML 图表 pip install pyecharts # 安装 PaddlePaddle CPU 版本 pip install paddlepaddle # 如果使用 NVIDIA GPU则改为安装 GPU 版本注意版本需匹配 CUDA # pip install paddlepaddle-gpu2.6.1然后安装 PaddleOCR。pip install paddleocrPaddleOCR 首次使用时会下载检测和识别模型需要联网。如果下载失败可以手动下载模型文件放到本地指定目录然后在初始化时指定模型路径。本教程默认使用自动下载。安装完成后新建一个项目目录例如table_tennis_analysis目录内建议按下面的结构组织文件table_tennis_analysis/ ├── input/ │ └── match_demo.mp4 ├── frames/ ├── output/ │ ├── csv/ │ └── html/ ├── app.py ├── extract_frames.py ├── ocr_recognition.py ├── analyze_data.py └── requirements.txtrequirements.txt内容如下opencv-python4.8.1.78 paddleocr2.7.3 paddlepaddle2.6.1 pandas2.0.3 pyecharts2.0.3 flask2.3.2版本号建议参考实际环境锁定避免后续升级导致接口变化。项目启动流程分为三个阶段抽帧、识别、分析。先实现抽帧下面给出extract_frames.py的完整代码。import cv2 import os def extract_frames(video_path, output_dir, interval1): 从视频中按秒抽取关键帧。 :param video_path: 视频文件路径 :param output_dir: 抽帧输出目录 :param interval: 每隔多少秒抽取一帧 if not os.path.exists(output_dir): os.makedirs(output_dir) cap cv2.VideoCapture(video_path) fps cap.get(cv2.CAP_PROP_FPS) total_frames int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) print(f视频帧率: {fps}, 总帧数: {total_frames}) frame_id 0 saved_count 0 while True: ret, frame cap.read() if not ret: break # 每隔 interval 秒抽一帧 if frame_id % int(fps * interval) 0: output_path os.path.join(output_dir, fframe_{saved_count:05d}.jpg) cv2.imwrite(output_path, frame, [cv2.IMWRITE_JPEG_QUALITY, 90]) saved_count 1 print(f已保存: {output_path}) frame_id 1 cap.release() print(f抽帧完成共保存 {saved_count} 张图片) if __name__ __main__: extract_frames( video_pathinput/match_demo.mp4, output_dirframes, interval1 )这段代码会把视频转成一组带有序号的 JPG 图片。抽帧间隔可以根据需要调整如果比分变化频繁可以改成 0.5 秒如果是正常比赛节奏1 秒一次足够覆盖关键变化。5. 功能测试与效果验证5.1 抽帧功能验证运行命令python extract_frames.py成功标准是目标目录下生成连续帧图片并且日志显示保存数量与预期时间基本一致。例如一段 90 秒的演示视频间隔 1 秒抽帧理论上得到约 90 张图片。抽帧失败时优先检查视频路径是否正确、OpenCV 是否成功打开视频。可以打印cap.isOpened()确认视频是否可读。5.2 OCR 识别验证接下来写ocr_recognition.py对抽出的帧做文字识别并过滤出包含比分信息的关键文本。import os import pandas as pd from paddleocr import PaddleOCR def recognize_frames(frames_dir, output_csv): 对抽帧图片进行 OCR 识别把结果保存为 CSV。 ocr PaddleOCR(use_angle_clsTrue, langch, show_logFalse) results [] for img_name in sorted(os.listdir(frames_dir)): if not img_name.lower().endswith((.jpg, .jpeg, .png)): continue img_path os.path.join(frames_dir, img_name) result ocr.ocr(img_path, clsTrue) if not result: continue text_list [] for line in result: for word_info in line: if word_info and len(word_info) 2: text_list.append(word_info[1][0]) full_text | .join(text_list) results.append({frame: img_name, text: full_text}) print(f{img_name}: {full_text}) df pd.DataFrame(results) df.to_csv(output_csv, indexFalse, encodingutf-8-sig) print(f识别结果已保存至 {output_csv}) if __name__ __main__: recognize_frames( frames_dirframes, output_csvoutput/csv/ocr_results.csv )运行命令python ocr_recognition.py成功标准是 CSV 文件生成并且每一帧的文本列包含可读的文字。测试阶段建议找那种计分板清晰、画面无遮挡的视频。如果识别结果为空先用一张截图单独测试 OCR 是否正常工作排除模型下载不完整的问题。测试 OCR 时重点关注三类错误数字识别错误比如 11 识别成 1。中文识别漏字比如运动员名被截断。计分板位置存在图标干扰把赞助商标识误识别成比分。遇到问题时优先调整 OCR 的文本检测阈值或者对画面做裁剪预处理只保留计分板区域。示例代码如下# 裁剪计分板区域后再识别假设计分板位于画面右上角 1/4 区域 import cv2 image cv2.imread(frames/frame_00000.jpg) height, width image.shape[:2] crop image[0:int(height * 0.4), int(width * 0.5):width] cv2.imwrite(frames/crop_00000.jpg, crop)5.3 数据分析与可视化OCR 得到的是原始文本还要进一步清洗。比如把“比分 11:9”从整段文本中提取成player1_score和player2_score两列然后按时间轴统计比分变化。下面给出analyze_data.py的示例完成比分提取和折线图生成。import re import pandas as pd from pyecharts.charts import Line from pyecharts import options as opts def parse_score(text): 从 OCR 文本中提取比分返回 (player1_score, player2_score) 或 (None, None) pattern r([0-9]{1,2})\s*:\s*([0-9]{1,2}) match re.search(pattern, text) if match: return int(match.group(1)), int(match.group(2)) return None, None def build_timeline(csv_path): df pd.read_csv(csv_path, encodingutf-8-sig) df[player1_score] None df[player2_score] None for idx, row in df.iterrows(): score1, score2 parse_score(str(row[text])) df.at[idx, player1_score] score1 df.at[idx, player2_score] score2 # 丢弃未识别到比分的帧 df df.dropna(subset[player1_score, player2_score]) # 按帧序号排序 df[frame_num] df[frame].str.extract(r(\d)).astype(int) df df.sort_values(frame_num) df.to_csv(output/csv/score_timeline.csv, indexFalse, encodingutf-8-sig) print(df[[frame, player1_score, player2_score]]) line ( Line() .add_xaxis(df[frame_num].tolist()) .add_yaxis(选手A得分, df[player1_score].tolist(), is_smoothTrue) .add_yaxis(选手B得分, df[player2_score].tolist(), is_smoothTrue) .set_global_opts( title_optsopts.TitleOpts(title比赛比分变化), xaxis_optsopts.AxisOpts(name帧序号), yaxis_optsopts.AxisOpts(name得分), ) ) line.render(output/html/score_line.html) print(可视化页面已生成) if __name__ __main__: build_timeline(output/csv/ocr_results.csv)运行命令python analyze_data.py打开output/html/score_line.html后能看到一条随帧序号变化的得分曲线。如果曲线出现明显跳变比如从 5:5 直接跳到 11:9说明中间有若干帧 OCR 漏识别需要降低抽帧间隔或提高识别阈值。这里需要特别说明以上代码使用“选手A”“选手B”作为示例不针对任何真实运动员。实际使用时要把识别出的运动员姓名映射成 CSV 中的独立列。6. 接口 API 与批量任务6.1 Flask API 服务如果希望让其他程序调用这套分析能力可以写一个简单的 Flask 接口。下面给出app.py的示例提供两个接口/health用于健康检查/analyze用于提交视频并触发分析。import os import uuid from flask import Flask, request, jsonify from extract_frames import extract_frames from ocr_recognition import recognize_frames from analyze_data import build_timeline app Flask(__name__) BASE_DIR os.path.dirname(os.path.abspath(__file__)) INPUT_DIR os.path.join(BASE_DIR, input) FRAMES_DIR os.path.join(BASE_DIR, frames) OUTPUT_CSV_DIR os.path.join(BASE_DIR, output, csv) app.route(/health, methods[GET]) def health(): return jsonify({status: ok}) app.route(/analyze, methods[POST]) def analyze(): 请求体示例: { video_name: match_demo.mp4, ocr_interval: 1 } data request.get_json() if not data or video_name not in data: return jsonify({error: missing video_name}), 400 video_name data[video_name] interval int(data.get(ocr_interval, 1)) video_path os.path.join(INPUT_DIR, video_name) if not os.path.exists(video_path): return jsonify({error: video not found}), 404 task_id uuid.uuid4().hex[:8] task_frames_dir os.path.join(FRAMES_DIR, f{task_id}) task_csv_path os.path.join(OUTPUT_CSV_DIR, f{task_id}_ocr_results.csv) extract_frames(video_path, task_frames_dir, intervalinterval) recognize_frames(task_frames_dir, task_csv_path) build_timeline(task_csv_path) return jsonify({ task_id: task_id, ocr_csv: task_csv_path, message: analysis finished }) if __name__ __main__: app.run(host127.0.0.1, port8000, debugFalse)启动 API 服务python app.py用 curl 做一次接口验证curl -X POST http://127.0.0.1:8000/analyze \ -H Content-Type: application/json \ -d {\video_name\: \match_demo.mp4\, \ocr_interval\: 1}预期返回结果包含task_id和ocr_csv路径。接口能跑通之后就可以把这个服务接到自己的数据处理管线里比如用 Cron 定时扫描新视频目录、触发分析并把结果写入数据库。6.2 批量任务设计批量处理的核心是把“单条分析流程”循环执行同时做好日志和错误隔离。最简单的方式是在项目目录下放置多个视频文件然后写一个 Python 脚本统一处理。import os import traceback from extract_frames import extract_frames from ocr_recognition import recognize_frames def batch_process(input_dir, output_dir, interval1): os.makedirs(output_dir, exist_okTrue) for video_name in os.listdir(input_dir): if not video_name.lower().endswith(.mp4): continue video_path os.path.join(input_dir, video_name) task_name os.path.splitext(video_name)[0] task_frames_dir os.path.join(output_dir, frames, task_name) task_csv os.path.join(output_dir, csv, task_name .csv) os.makedirs(os.path.dirname(task_csv), exist_okTrue) try: print(f开始处理: {video_name}) extract_frames(video_path, task_frames_dir, intervalinterval) recognize_frames(task_frames_dir, task_csv) print(f处理完成: {video_name}) except Exception: print(f处理失败: {video_name}) traceback.print_exc() if __name__ __main__: batch_process(input, output)批量任务建议遵循三个原则每个视频单独一个输出目录避免帧文件互相覆盖。每个任务都写日志失败时保留 traceback。处理完一个再处理下一个避免同时占满内存。如果要提高并行度可以引入concurrent.futures或celery但乒乓球比赛视频分析通常不是高并发场景串行处理更稳定。7. 资源占用与性能观察性能观察主要看两个指标处理速度和内存占用。处理速度方面抽帧不会占用太多 CPU瓶颈在 OCR 识别环节。使用 PaddleOCR 默认模型时CPU 模式下单张图片识别耗时通常在 0.5 到 2 秒之间GPU 模式下可能降到 0.1 到 0.3 秒具体取决于图片分辨率和硬件。显存方面GPU 推理时 PaddleOCR 占用显存大约 1G 到 2G实际数字会因模型版本和输入图片尺寸而波动。如果担心显存不足可以限制 OCR 输入图片尺寸或者直接使用 CPU 模式牺牲部分速度换取极低的资源占用。下面几种情况会影响处理速度输入视频分辨率越高抽帧图片越大OCR 识别越慢。建议抽帧时把图片宽度限制到 1280 以内。画面中文字越多OCR 耗时越长。如果只需要比分先裁剪计分板区域再识别效率最高。抽帧间隔越短图片数量越多总耗时越长。建议先按 1 秒间隔跑一遍漏识别严重时再缩短间隔。多个进程同时跑 OCR 会显著增加内存占用。普通配置下建议一次只跑一个分析任务。观察资源占用Windows 用户可以打开任务管理器Linux 用户可以使用htop或nvidia-smi。定位性能瓶颈的通用方法是分阶段计时分别记录抽帧耗时、OCR 耗时、清洗耗时哪一步耗时最长就优化哪一步。8. 常见问题与排查方法问题现象可能原因排查方式解决方案启动虚拟环境失败Python 路径或虚拟环境损坏检查终端是否识别到正确的 Python 路径重新创建虚拟环境OpenCV 无法打开视频视频编码不支持或路径错误打印cap.isOpened()的结果转码为 MP4/H.264 后再试OCR 识别结果为空模型未下载完整或画面文字过小单独截一张图测试 OCR重新下载模型或裁剪放大文字区域中文识别乱码编码问题查看终端输出编码用 UTF-8 保存脚本输出 CSV 时用 utf-8-sigCSV 用 Excel 打开乱码编码问题检查 CSV 文件编码写入时使用encodingutf-8-sigGPU 模式下 OCR 报错CUDA 版本与 PaddlePaddle 不匹配检查paddle.utils.run_check()安装对应 CUDA 版本或退回 CPU 模式Flask 接口启动失败端口被占用或依赖缺失查看启动日志更换端口port8001或安装缺失依赖批量任务中途卡住某个视频无法读取导致异常未捕获查看任务日志批量循环里加 try/except 和超时机制比分曲线跳跃明显OCR 漏识别或识别错误检查 CSV 中对应帧的文本降低抽帧间隔、裁剪计分板、提高阈值内存持续上涨图片读取后未释放检查循环内是否持有大变量每次循环结束后删除图片变量并调用 gc.collect()排查 OCR 问题时建议先把单帧图片保存到本机用 PaddleOCR 命令行或脚本单独测试确认模型可用性。这样可以把“模型问题”和“视频处理问题”分开定位。9. 最佳实践与使用建议第一先跑通最小流程再处理正式视频。用一段 30 秒的比赛集锦验证抽帧、OCR、CSV 导出、图表生成四个环节都正常再对整场比赛数据做批量分析。这样能避免在大批量任务中途才发现问题。第二对输入素材做好分级管理。原始视频、抽帧图片、中间识别结果、最终输出文件放在不同目录并给文件名加上时间戳或任务 ID。批量分析时尤其重要否则几十个任务的结果会混在一起。第三OCR 识别结果必须做人工抽检。无论识别准确率多高都会存在数字混淆、文字截断等问题。批量任务完成后随机抽 10% 到 20% 的帧人工核对一遍确保统计数据的可信度。第四接口服务要限制访问范围。如果只是本机调用Flask 启动时绑定127.0.0.1即可如果需要局域网访问建议加一个简单的 Token 验证。对于包含运动员画面的视频素材不要把识别结果和原始视频随意公开。第五涉及比分统计时必须保持中立。数据可视化只能呈现客观数字不能把技术分析包装成针对任何选手或队伍的主观评价。合规使用素材和数据才是这类项目能持续做下去的前提。第六后续可以扩展的方向很多接入裁判板的实时比分接口、用mediapipe做运动员动作提取、用streamlit做交互式数据看板或者把结果写入 MySQL 用于长期趋势分析。当前这套方案已经预留了清晰的模块边界继续加功能时不需要改动核心抽帧和 OCR 逻辑。10. 总结这个乒乓球赛事数据可视化方案最值得尝试的地方在于它把视频内容结构化这件事做得比较完整从抽帧到 OCR 再到图表生成全流程代码量不大普通电脑就能跑。第一次使用时建议先用一段清晰度较高的比赛视频验证 OCR 识别准确率确认计分板文字能稳定提取再考虑批量扩到更多比赛。最容易踩的坑集中在 OCR 识别质量上画面模糊、文字过小、标识物遮挡都会影响结果所以抽帧后先人工看几张图确认这一票素材有没有救再决定是否继续处理。接口和批量任务的扩展方式已经写在上面按需增删即可。整体来说这是一套适合体育数据分析入门、内容生产辅助以及赛事数据结构化沉淀的方案维护成本低后续扩展空间也足够。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻