☰
YOLOv8实战:八段锦动作识别与练习指导系统完整实现
2026/9/28 2:02:08 网站建设 项目流程

简介:基于YOLOv8的八段锦练习指导系统是一套面向高校计算机、人工智能等专业毕业设计与课程设计的完整目标检测项目。它围绕八段锦动作识别场景,提供可运行的源码、完整数据集与可视化界面,支持模型训练、推理检测与结果展示,适合作为毕设或课设的起步框架。资源包共97个文件,以70个Python脚本为核心,覆盖模型训练、检测服务、工具函数与可视化页面等模块;另含4个pt模型权重、5个xml配置文件、12个pyc编译文件及说明文档,整体压缩后约24.21MB,结构清晰便于二次开发。目前已有295人学习使用。除直接部署外,系统还能输出核心指标曲线、混淆矩阵、F1曲线、精确率-召回率曲线、验证集预测结果与标签分布图,方便在答辩或评审中直观呈现训练效果。配套部署说明与README可帮助快速上手,简单部署即可运行,拿来即用。

1. YOLOv8八段锦练习指导:这套源码、数据集和界面到底能解决什么

做毕设最怕两件事:一是算法讲不深,被导师问两句就卡壳;二是代码跑不通,演示时当着全组同学的面翻车。这套基于YOLOv8的八段锦练习指导系统,正好把目标检测、动作识别、可视化界面和部署串成一条完整链路。你输入一段练习视频或打开摄像头,模型实时判断当前做的是八段锦的哪一式,比如“两手托天理三焦”还是“摇头摆尾去心火”,界面同步显示检测框和动作规范提示。它适合计算机视觉方向毕业设计、课程设计,也适合想快速上手YOLOv8但不想从零写数据管线的人。资源包里包含源码、完整数据集、可视化界面和部署教程,按步骤装上环境就能把Demo跑起来,剩下的时间可以拿去补原理和调参。

2. 系统结构拆解:YOLOv8选型理由与最小可运行环境

2.1 为什么动作指导选择YOLOv8而不是姿态估计网络

八段锦练习指导的实质,是把一段视频里的每一帧映射成“动作类别”。这里有两类技术路线:一类是姿态估计,先检测骨骼关键点,再对关键点序列做分类;另一类是目标检测,把整个人体框出来,直接把图像区域映射到动作标签。姿态估计方案在瑜伽、健身纠正里很常见,但工程量大,需要额外接骨骼序列分类器,而且对遮挡、侧身、摄像头视角都很敏感。八段锦的每个招式姿态相对固定,用目标检测的“框+类别”输出就够用,骨架模型反而是杀鸡用牛刀。

YOLOv8在这类任务里有几个很实际的优点。首先是anchor-free检测头,后处理逻辑比YOLOv5简单,训练和部署时少踩很多坑。其次是backbone里的C2f模块,在不明显增加计算量的前提下增强了梯度流,小模型yolov8n也能学到可靠的纹理特征,对CPU部署更友好。SPPF模块把不同尺度的特征池化后融合,动作区域的上下文信息更充分,八段锦中“手指交叉上托”这种细节也不容易丢。整个模型结构图在ultralytics官方仓库里有标注,初看可能觉得头大,但毕设答辩只需要讲清楚“检测头输出是什么、损失函数怎么约束”就够了。

选型上还有一个现实原因:生态完整。YOLOv8的训练、验证、导出都封装成了命令行,数据格式用YOLO txt,和LabelImg、labelme导出的格式转换成本低。我在做这类项目时一般先用官方预训练权重跑通流程,再换自定义数据集,整个过程不超过半小时。哪怕最后只训练出一个mAP 0.7左右的模型,配合界面也足够做演示。

2.2 环境搭建:Ubuntu 20.04 CPU版与GPU版的最小配置方案

资源包里的部署教程通常会推荐Ubuntu 20.04,因为conda和pytorch在这套系统上的兼容性最稳。对于只有CPU笔记本的学生,也不用担心跑不动,yolov8n模型在CPU上单帧推理可以控制在几百毫秒到一秒左右,作为毕设演示完全可用。

我习惯先用conda创建独立环境,避免把系统Python弄坏:

# 创建独立环境,Python 3.9 与当前主流依赖兼容性最好 conda create -n bdj python=3.9 -y conda activate bdj # CPU版torch,注意必须指定cpu index,否则会装成GPU版然后运行时报错 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu # ultralytics 自带YOLOv8实现和训练指令 pip install ultralytics # 可视化界面和后续onnx部署需要 pip install pyqt5 opencv-python onnxruntime

这里有个容易翻车的点:如果直接执行pip install torch,在CUDA驱动不完整的机器上会装一个用不了的GPU版本,import时可能报错说找不到libc10_cuda.so。所以CPU机器一定要加--index-url参数指定CPU轮子。GPU机器则改成PyTorch官网对应的cu118或cu121地址,我这里不展开,因为毕设大部分人的诉求是先跑通,再考虑加速。

装完后不要急着跑自己的数据,先用官方权重验证环境:

# test_env.py from ultralytics import YOLO # 下载v8n权重,验证推理链路是否通畅 model = YOLO("yolov8n.pt") results = model.predict("bus.jpg", conf=0.25) print(results[0].boxes.cls)

如果打印出类别ID,说明torch和ultralytics都没问题。conf=0.25表示只保留置信度大于0.25的检测框,这个参数后面在界面上也会用到。bus.jpg是ultralytics官方示例图,第一次运行会自动下载,网络不通就换成本地任意一张含人的照片。注意这一步的产物是预训练权重,和八段锦无关,纯粹是验证环境。真正训练自己的数据要到第4章。

3. 数据集构建与标注:从原始视频到YOLO格式的完整流程

3.1 八段锦的类别定义与数据采集策略

八段锦由八式动作加上预备式和收势组成,我建议直接定义10个类别,不要合并。合并类别会导致不同动作特征互相干扰,训练出的模型在边界动作上摇摆不定。类别名最好全英文小写加下划线,一是避免某些框架对中文类别名的编码问题,二是YOLO的data.yaml里索引和名称对应更清晰。

下面是我在类似项目里常用的类别表:

ID类别名对应招式
0prepare预备式
1raise_hands两手托天理三焦
2draw_bow左右开弓似射雕
3single_raise调理脾胃须单举
4look_back五劳七伤往后瞧
5shake_head摇头摆尾去心火
6touch_feet两手攀足固肾腰
7punch攒拳怒目增气力
8heel_stand背后七颠百病消
9closing收势

数据采集上,最常见的方式是公开视频抽帧加自录补充。公开的八段锦教学视频一般背景规范、动作标准,适合做正样本;自录视频能补充不同角度、不同光线、不同体型,让模型泛化能力更好。抽帧时不要每秒都抽,我一般每秒抽2到3帧,且把连续帧间隔拉开,避免训练集里大量重复画面。每个类别至少留200张,总共2000张上下,目标检测模型的数据量需求不像分类那么夸张,但太少就会出现某类mAP特别低。

标注框要框住全身,而不是只框上半身。八段锦里“两手攀足固肾腰”需要弯腰触脚,“背后七颠百病消”涉及脚跟动作,只框躯干会把关键判别信息丢掉。

3.2 labelme标注与JSON转YOLO格式的Python脚本

标注工具我推荐labelme,因为它保存的是JSON格式,后面转换脚本自己可控。安装后选择“Create Rectangle”画矩形框,给每个框打上类别名。注意labelme的多边形工具也能画矩形,但转换脚本要兼容两种情况。

标注完的JSON和图片要放在同一目录里方便处理。转换脚本是把JSON里的points取最小最大坐标,转成YOLO需要的归一化中心坐标和宽高:

# labelme2yolo.py import json import os def labelme_to_yolo(json_path, out_dir, class_ids): with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) img_w = data['imageWidth'] img_h = data['imageHeight'] lines = [] for shape in data['shapes']: label = shape['label'] if label not in class_ids: continue pts = shape['points'] # 矩形框的四个顶点 x1 = min(p[0] for p in pts) y1 = min(p[1] for p in pts) x2 = max(p[0] for p in pts) y2 = max(p[1] for p in pts) # 防止坐标越界,标注时偶尔会画出图像边界 x1 = max(0, min(x1, img_w)) y1 = max(0, min(y1, img_h)) x2 = max(0, min(x2, img_w)) y2 = max(0, min(y2, img_h)) w = (x2 - x1) / img_w h = (y2 - y1) / img_h cx = ((x1 + x2) / 2) / img_w cy = ((y1 + y2) / 2) / img_h lines.append(f"{class_ids[label]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") name = os.path.splitext(os.path.basename(json_path))[0] with open(os.path.join(out_dir, name + '.txt'), 'w', encoding='utf-8') as f: f.write('\n'.join(lines)) if __name__ == '__main__': class_ids = { 'prepare': 0, 'raise_hands': 1, 'draw_bow': 2, 'single_raise': 3, 'look_back': 4, 'shake_head': 5, 'touch_feet': 6, 'punch': 7, 'heel_stand': 8, 'closing': 9 } os.makedirs('labels', exist_ok=True) for name in os.listdir('labelme'): if name.endswith('.json'): labelme_to_yolo(os.path.join('labelme', name), 'labels', class_ids)

脚本逻辑分三步:读取图像尺寸,遍历每个标注框,计算归一化的中心坐标和宽高。class_ids字典决定了标签文件里第一列的数字,这个数字必须和后面的data.yaml里的names索引完全一致,否则训练时类别会对不上。为什么用归一化坐标?因为YOLO输入会resize到统一尺寸,如果直接用像素坐标,不同分辨率的图片模型看到的比例就不一致,训练效果会差很多。min(p[0] for p in pts)这种写法对四边形的四个点都能取到边界,不需要区分矩形还是多边形,兼容性更好。

转换完成后,检查一下有没有空txt文件。空文件说明标注时的类别名和class_ids没对上,YOLO遇到空标签会直接报错。

3.3 数据集划分与增强检查

划分数据集时要保证训练集和验证集没有重复图片。如果从视频抽帧,同一段视频的帧只能出现在一边,否则验证集mAP会虚高到0.95以上,答辩时一测真实摄像头就露馅。

# split_data.py import os import random img_dir = 'images/' label_dir = 'labels/' imgs = [f for f in os.listdir(img_dir) if f.endswith('.jpg') or f.endswith('.png')] # 每个类别有多少标注框,用txt第一列统计 from collections import Counter counts = Counter() for f in os.listdir(label_dir): if not f.endswith('.txt'): continue with open(os.path.join(label_dir, f)) as fh: first = fh.readline().strip() if first: counts[first.split()[0]] += 1 print('标注分布:', dict(counts)) # 按视频源分集,这里简化为文件名前缀分组 groups = {} for name in imgs: prefix = name.split('_')[0] # 假设文件名按视频源前缀命名 groups.setdefault(prefix, []).append(name) train_files, val_files = [], [] for prefix, names in groups.items(): random.shuffle(names) split = int(len(names) * 0.8) train_files += names[:split] val_files += names[split:] # 逐一复制到train/val目录,保持图片和txt一一对应 for mode, file_list in [('train', train_files), ('val', val_files)]: os.makedirs(f'images/{mode}', exist_ok=True) os.makedirs(f'labels/{mode}', exist_ok=True) for name in file_list: base, ext = os.path.splitext(name) os.rename(f'images/{name}', f'images/{mode}/{name}') os.rename(f'labels/{base}.txt', f'labels/{mode}/{base}.txt')

统计分布这段代码很关键,因为经常有人标到一半发现某些类忘了标,训练时全部损失都被多数类主导。如果发现某个类别只有几十张,我一般的做法是回视频抽帧,再补标一轮,比离线增强更有效。YOLOv8自带的Mosaic、平移、翻转增强足够用,不需要额外接albumentations,尤其是Mosaic会把四张图拼成一张,等于免费扩充了背景多样性。需要注意的是,增强是训练时实时做的,不要提前离线增强,否则数据量看似多了,实际信息重复,验证指标也不能反映真实泛化。

4. 模型训练与可视化界面:从YOLOv8权重到练习指导应用

4.1 训练参数设置与损失曲线绘制

训练前先写data.yaml,它告诉YOLOv8去哪里找数据和类别名。一个典型的八段锦数据集配置长这样:

# bdj.yaml path: ./bdj_dataset train: images/train val: images/val nc: 10 names: - prepare - raise_hands - draw_bow - single_raise - look_back - shake_head - touch_feet - punch - heel_stand - closing

path是数据集根目录的绝对路径,train和val相对于path。nc必须和names的长度一致,有个常见玄学错误是类别id从0开始,但names顺序可能和标注时不一致,一定要用转换脚本里的class_ids顺序。训练命令按资源包里的部署教程跑就行:

# 在bdj.yaml所在目录执行 yolo train \ data=bdj.yaml \ model=yolov8n.pt \ epochs=120 \ imgsz=640 \ batch=16 \ device=0 \ cache=True

参数解释:model=yolov8n.pt表示在COCO预训练权重基础上微调,收敛更快;epochs=120对于2000张的小数据集足够,再多容易过拟合;imgsz=640是训练分辨率,默认值,不要轻易改小,否则框回归精度掉得厉害;batch=16在显存不够时降到8或4;device=0是GPU,CPU改成device=cpu,训练时间会拉长,但数据量小也能接受。cache=True会把图像加载到内存里,省去每轮读磁盘的时间,但内存不足时反而会拖慢甚至崩掉,这个留到下一章避坑里说。

训练完一次,很多人只看results.png就完了,但答辩时需要更细致的曲线。训练过程中ultralytics每轮都会写results.csv,直接用pandas画自定义损失曲线:

# plot_loss.py import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv('runs/detect/train/results.csv') fig, axes = plt.subplots(1, 2, figsize=(12, 4)) axes[0].plot(df['epoch'], df['train/box_loss'], label='train_box_loss') axes[0].plot(df['epoch'], df['val/box_loss'], label='val_box_loss') axes[0].set_title('Box Loss') axes[0].legend() axes[1].plot(df['epoch'], df['metrics/mAP50(B)'], label='mAP50') axes[1].plot(df['epoch'], df['metrics/mAP50-95(B)'], label='mAP50-95') axes[1].set_title('mAP') axes[1].legend() plt.savefig('custom_loss_curve.png', dpi=300)

这段代码能直接看出过拟合的拐点:训练损失继续下降、验证损失反弹时,说明模型在背训练集,此时应该减少epochs或加patience参数早停。mAP50比mAP50-95更直观,毕设答辩讲前者就够了。如果训练完mAP50连0.5都不到,首先检查数据标注,而不是调模型。

4.2 可视化界面:PyQt5窗口集成推理

资源包的可视化界面用PyQt5实现,主窗口包含图片显示区、按钮区和指导文本区。界面逻辑不复杂,但有一个线程问题必须处理:推理不能放在主线程里,否则点击按钮后窗口会转圈卡死。我会把推理封装成一个QThread,结果通过信号传回:

# gui_app.py import sys from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QFileDialog, QLabel, QVBoxLayout, QWidget from PyQt5.QtCore import QThread, pyqtSignal from ultralytics import YOLO class DetectThread(QThread): result_ready = pyqtSignal(object) def __init__(self, model, image_path): super().__init__() self.model = model self.image_path = image_path def run(self): # 在子线程里跑推理,conf设低一点能看到半蹲/转身的中间态,但界面里建议0.3以上 result = self.model.predict(self.image_path, conf=0.3, verbose=False) self.result_ready.emit(result[0]) class MainWindow(QMainWindow): def __init__(self): super().__init__() self.model = YOLO('runs/detect/train/weights/best.pt') self.thread = None btn = QPushButton('选择图片') btn.clicked.connect(self.pick_image) self.disp = QLabel('检测结果展示区域') layout = QVBoxLayout() layout.addWidget(btn) layout.addWidget(self.disp) container = QWidget() container.setLayout(layout) self.setCentralWidget(container) def pick_image(self): path, _ = QFileDialog.getOpenFileName(self, '选择图片', '', 'Images (*.jpg *.png)') if path: # 先启动线程,不阻塞界面 self.thread = DetectThread(self.model, path) self.thread.result_ready.connect(self.show_result) self.thread.start() def show_result(self, result): # result.plot() 返回画好检测框的numpy数组,转成QPixmap显示 from PyQt5.QtGui import QPixmap import cv2 img = result.plot() img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) h, w, ch = img.shape bytes_per_line = ch * w from PyQt5.QtGui import QImage qimg = QImage(img.data, w, h, bytes_per_line, QImage.Format_RGB888) self.disp.setPixmap(QPixmap.fromImage(qimg).scaled(self.disp.width(), self.disp.height()))

conf这个参数在界面上很微妙。调成0.1会看到一堆误检框,调成0.8又可能漏掉某些动作。我一般先用验证集做过一次阈值扫描,找F1最高的点,然后把这个值写死在界面里。verbose=False是关闭每帧控制台输出,否则推理时会刷屏。这个界面的核心就一句话:模型只加载一次,主线程只管界面,推理全部交给子线程。

4.3 部署:导出ONNX让CPU推理速度翻倍

如果只靠ultralytics的predict跑CPU,单帧耗时可能会到一两秒,这在演示现场很不体面。一个常见做法是先导出ONNX,再用ONNX Runtime推理,速度通常提升20%到50%。导出命令很简洁:

# 把训练好的best.pt导出成onnx yolo export model=runs/detect/train/weights/best.pt format=onnx dynamic=True

如果是CPU部署,导出后要做两件事:第一是确认输入输出节点名,第二是把预处理和后处理接上。不同版本ultralytics导出的输出格式可能有差异,有的是[1, 84, 8400]原始张量,有的带了NMS。我习惯用原始张量,自己实现简单的置信度过滤和NMS,这样每一步都可控:

# onnx_infer.py import numpy as np import cv2 import onnxruntime as ort sess = ort.InferenceSession('best.onnx', providers=['CPUExecutionProvider']) input_name = sess.get_inputs()[0].name def preprocess(img): img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (640, 640)) img = img.astype(np.float32) / 255.0 img = np.transpose(img, (2, 0, 1))[None] return img img0 = cv2.imread('test_frame.jpg') output = sess.run(None, {input_name: preprocess(img0)})[0] # shape: 1,84,8400 output = output[0] # 前4行是cx,cy,w,h,后面80行是类别置信度 boxes = output[:4].T cls_scores = output[4:].T # 取每个框的最大类别分值,然后过滤和NMS

这段代码里最关键的是理解输出维度:8400是三个尺度特征图上的候选框总数,4是中心坐标和宽高,80是类别数。因为八段锦是10类,这里会变成10。providers=['CPUExecutionProvider']是强制ONNX Runtime用CPU执行,避免依赖CUDA。预处理里最坑的是张量布局,YOLOv8输入是[N,3,640,640],颜色通道在最前面,所以必须transpose。我见过不少人漏掉这步,推理结果全是乱框,其实就是HWC和CHW的问题。

5. 避坑:从环境配置到界面卡顿的六条实战踩坑记录

5.1 数据标注不统一导致训练时mAP上不去

现象:训练完mAP50一直在0.3上下,验证集上的画面错漏百出。原因:第一是标注框有的框全身、有的只框上半身,模型不知道到底该学什么;第二是类别名有同义词,比如hands_up和raise_hands同时存在,标注员混用。解决:转换脚本统一映射到一个class_ids字典,并且标注前就约定好“必须框全身、不得有歧义类别名”。我一般还会写个脚本扫描所有txt,统计每类的框数,如果发现某些框的宽高比极端异常,多半是标注时误点了单个点。

5.2 CPU推理慢到界面像死机

现象:在CPU笔记本上,用model.predict()跑一张640x640的图要1.5秒,界面手动处理无法接受。原因:ultralytics的predict包含数据预处理、模型推理、后处理、批量逻辑,纯CPU下开销很大。解决:导出ONNX后用ONNX Runtime推理,并考虑把输入分辨率降到416。416会让mAP稍微掉一点,但动作识别任务本身不是高精度检测,速度换流畅度值得。实测下来,ONNX Runtime在CPU上比原始PyTorch部署快30%到50%,这是毕设演示最划算的一笔优化。

5.3 中文路径和中文类别名的玄学报错

现象:训练时提示FileNotFoundError或UnicodeDecodeError,但路径明明存在。原因:Windows下中文路径编码与Python的默认编码不一致,或data.yaml里names直接写中文,导致标签文件读取时解码失败。解决:整个项目路径强制英文,数据集目录不要放在桌面某个“新建文件夹”里;类别名也用英文。这是血泪经验,很多毕设卡在这里,白白浪费半天时间。

5.4 摄像头识别时动作标签跳变

现象:视频流里模型预测结果在raise_hands和prepare之间反复横跳,指导语闪烁。原因:单帧分类没有时序约束,相邻帧动作本身就存在过渡态,置信度稍一波动就会换标签。解决:做一个长度为5到10帧的滑动窗口投票,输出窗口内出现次数最多的标签。代码放在第6章,效果比单纯调conf好得多,而且答辩时还可以讲这是“时序平滑优化”。

5.5 GPU训练时CUDA out of memory

现象:跑几十个batch就报CUDA out of memory。原因:显存不足,batch=16在笔记本显卡上可能已经越界。解决:先设batch=8,还不行就设batch=4;同时把cache=True改成cache=False,因为图像缓存也会占用大量显存。另一种思路是显存只够推理不够训练,那就直接在CPU上训练,数据量小的时候也就多花一两个小时。

5.6 PyQt界面点击开始识别就无响应

现象:点“选择图片”后界面变白,过几秒才恢复。原因:推理代码写在了按钮槽函数里,主线程被model.predict阻塞。解决:所有推理必须放进QThread,并通过信号把结果传回主线程。我在4.2给的代码结构可以直接照搬,线程、信号、槽的嵌套逻辑看一遍应该就懂。如果用了线程还卡,检查是不是在run()里直接操作了界面控件,Qt里只有主线程能访问UI对象。

6. 进阶玩法:把八段锦指导系统从“能跑”做到“能答辩”

这层工夫花下去,答辩效果会明显不一样:把单帧检测改成滑动窗口投票,再补一张混淆矩阵图。滑动窗口的本质是把连续几帧的预测结果按类计数,只有某个标签连续出现达到阈值才切换指导语。代码不长,但能彻底消除标签跳变:

# action_smoother.py from collections import Counter class ActionSmoother: def __init__(self, window_size=5, min_count=3): self.window = [] self.window_size = window_size self.min_count = min_count def update(self, label): self.window.append(label) if len(self.window) > self.window_size: self.window.pop(0) # 统计窗口内每个标签出现次数,满足min_count才输出 counter = Counter(self.window) label, count = counter.most_common(1)[0] return label if count >= self.min_count else None

min_count设成3或4,表示5帧里有3到4帧都预测为同一个动作,才把这个动作作为最终结果。返回None时,界面可以保持上一帧的指导文字,而不是显示空白或乱跳。这套方案的原理就是时序一致性假设,在八段锦这种动作持续时间长的场景里,几乎不会误伤,又能滤掉过渡帧。答辩时如果被问“怎么证明你的系统不是瞎猜”,把测试视频逐帧预测结果用sklearn的confusion_matrix画出来,按10个类别展示准确率和召回率,说服力远大于几张测试截图。

还有一个小技巧是给指导语加分。在拿到best.pt和滑动窗口标签后,界面代码里维护一个动作规范提示映射表,比如raise_hands对应“双手交叉上托,目视前方,膝盖微曲”,这样每次输出动作名称的同时,给出可执行的口头指导。这不算难,但在演示时非常加分。从那以后我每次做这类识别项目,都会强制自己加一段时序平滑和混淆矩阵评估,宁可前期多花两小时,也不要现场翻车。希望帮到你。

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

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

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

立即咨询