多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于YOLOv8的手势识别实战:从数据集到部署的完整指南

基于YOLOv8的手势识别实战:从数据集到部署的完整指南 简介基于YOLOv8的手势识别应用包面向深度学习、图像识别方向的开发者与学生解决人机交互、自动驾驶、虚拟现实等场景中手势动作快速识别的问题。YOLOv8作为YOLO系列最新版本在保证实时检测速度的同时提升了精度特别适合手势这类小目标的识别任务代码与训练好的模型已封装成可直接运行的项目降低了入门门槛。压缩包共18个文件约11.18MB核心包含app.py主程序、两个PyTorch模型权重yolov8n.pt、best.pt、10张JPG手势样本图以及requirements.txt、packages.txt等环境配置与README说明文档目录结构清晰便于本地部署和二次开发。目前已有52人学习下载适合作为YOLOv8入门实践的参考案例。通过运行代码和观察模型输出读者可以掌握目标检测的基本流程并结合训练指标图理解模型调优思路为后续自建手势数据集提供基础。其中的README.md与依赖清单可帮助快速搭建环境避免常见配置问题非常适合课程设计或毕业设计的快速落地。1. 打开这个基于 YOLOv8 的手势识别应用我先确认的不是 Demo 而是边界第一次拿到这份基于 YOLOv8 的手势识别应用压缩包我常被问到要不要先解压运行一遍。运行当然要做但在此之前我更习惯把包的边界摸清楚它是只给推理脚本还是把训练、验证、实时识别串成了完整的图像识别链路。按目录结构看这份资源明显偏向后者。基于深度学习目标检测框架 YOLOv8 的实现里面包含手势数据集的 YOLO 格式标注、数据配置、训练入口和摄像头实时识别脚本适合有 Python 基础、想把手势类别快速落到业务原型里验证的从业者如果只是想在周末跑通一个 Demo这套工程反而显得过度设计。下面我按实际拆包的顺序来写先看文件结构和数据再动训练参数接后处理然后把常见坑和进阶部署一起收尾。2. 先拆文件结构和数据集组织训练脚本、标注格式与类目对应关系压缩包解压后我一般不做全盘读取先做目录盘点。常见做法是项目按 dataset / code / output 三层切割。dataset 单独放图像和标签训练脚本放根目录产出的权重统一进 runs这样后续发布时不会把几千张训练图全部打进部署包。2.1 目录逐项拆解图像目录、标注目录与三个关键文件hand_gesture/ ├── dataset/ │ ├── images/ │ │ ├── train/ # 训练图片jpg 或 png │ │ └── val/ # 验证图片 │ └── labels/ │ ├── train/ # 与图片同名的 txt 标注 │ └── val/ ├── handgesture.yaml # 数据配置与类别表 ├── train.py # 训练入口 ├── detect.py # 单图/摄像头推理入口 ├── requirements.txt # 依赖清单 └── runs/ # 训练输出权重、曲线、验证结果从这个树能看到三个容易忽略的细节。第一images 下只分了 train 和 val没有 test。手势识别这类目标检测项目里单独标一个 test 集的成本很高通常训练完拿 val 做早停判断真正上线前再手工抽查一段视频就够了。多数机器学习初学者看到没有 test 目录会很慌其实不影响资源包的正常使用我一般会在 val 里再留 10% 的图不参与训练作为冒烟样本。第二标注没有用 VOC 的 XML而是 YOLO 自带的 txt。这个格式把每张图片的所有目标写进一个文本文件里每行一个框和图片文件保持同名前缀。这样从训练到推理全程不需要写 XML 解析代码是压缩包里已经替你省掉的工序。第三runs 目录在首次训练前是空的训练脚本会自动创建。如果拿到的包是别人训练过的版本里面会同时带 best.pt 和 last.pt。best.pt 是按验证集指标选出的最优权重日常识别用它last.pt 是最后一轮权重适合断点续训。判断资源能直接用还是需要重训就看这两文件带不带。2.2 数据组织与标注格式labels 里的 txt 存什么怎么校验以某张编号 000001 的图片为例同目录下会有一个 000001.txt# dataset/labels/train/000001.txt每行一个目标框 0 0.421875 0.5078125 0.40625 0.7734375 1 0.578125 0.6484375 0.21875 0.3828125 2 0.742575 0.7436120 0.16353 0.21311每行五个数值依次是类别 id、框中心点 x、框中心点 y、框宽、框高。注意这里的 x、y、width、height 都是相对原图宽高的归一化坐标取值范围 0 到 1不能直接当作像素用。比如第一行类别 0中心点水平位置在 42.1875% 处实际像素 0.421875 × 图像宽垂直位置在 50.78% 处框宽占图像宽约 40.6%框高占图像高约 77.3%。手指横跨成像时这么宽的框是正常的。如果标注数据是从 LabelImg 或 Labelme 导出之后再转 YOLO完整转换逻辑里最容易出错的就是像素坐标没有除回来。我提供一段常用的转换函数def coco_to_yolo(coco_box, img_w, img_h): # coco_box 为 [x_min, y_min, width, height]单位是像素 x_min, y_min, w, h coco_box x_center (x_min w / 2.0) / img_w y_center (y_min h / 2.0) / img_h box_w w / img_w box_h h / img_h return f{x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}这段代码的逻辑是把 COCO 风格的左上角坐标和宽高换算成 YOLO 所需的中心坐标与归一化宽高。参数 img_w、img_h 必须传原图分辨率很多人习惯在此处传的是训练时缩放后的 640 或 416画框就全部偏移。比率保留 6 位小数已足够太多会无意义太少会在极小目标上产生像素级偏差。2.3 类别与配置映射六类手势和 yaml 的一次对齐包内 handgesture.yaml 是数据配置的锚点path: ./dataset train: images/train val: images/val nc: 6 names: 0: thumb 1: index 2: middle 3: ring 4: little 5: oknc 表示类别总数names 的排列顺序就是模型输出之后各通道对应的类别。训练脚本读的是这份 yaml推理脚本读的也是模型里存的 names 字段。如果训练时把 index 放在 1 而部署时把 names 写成别的顺位模型输出明明是对的界面却会把食指显示成中指这种问题排查起来非常隐蔽。我拿到包的第一件事就是在训练前后各打印一次 model.names确认它和 yaml 完全一致。另外做一次类别分布抽查也很值得防止某类样本数量过少。以下脚本按目录统计每个类的框数量from glob import glob from collections import Counter cls_counter Counter() for label in glob(dataset/labels/train/*.txt): with open(label) as f: for line in f: cls_counter[int(line.split()[0])] 1 print(cls_counter)逻辑与参数说明逐行读取 label取每行的第一个字段类别 id计数。如果发现某个类别数量只有另一个类别的十分之一训练时就要考虑细化重复该类或给该类别更多增强否则最后 loss 会被大类淹没小类别识别率非常低。这也是我判断资源包数据质量时会先看统计、再看 yaml 的原因。3. 训练参数与数据增强配置文件、batch 与 imgsz 怎么定数据对齐之后才进入训练环节。这里不看玄学主要看两件事yaml 是否正确被训练脚本读取以及 batch、imgsz 和增强参数是否落在硬件能承受的范围。下面按文件解读和命令行调用两步走。3.1 看懂配置文件与训练入口YAML 与 train.py 的对应关系train.py 常见写法是对 ultralytics 的二次封装核心代码很短from ultralytics import YOLO if __name__ __main__: # 加载预训练权重n/s/m 分别对应体积和精度 model YOLO(yolov8s.pt) model.train( datahandgesture.yaml, # 第 2 章对齐过的数据配置 epochs120, # 完整训练轮数 batch16, # 单批图片数受显卡显存限制 imgsz640, # 训练与推理统一用 640 cacheTrue, # 首次后把图片缓存进内存 patience20, # 验证指标连续 20 轮未提升则早停 device0, # 0 号 GPU seed42, )逻辑说明YOLO(yolov8s.pt)加载的是 COCO 预训练权重COCO 里没有手势类别加载它只是借用主干网络提取图像特征的能力最后几层会被重新初始化。data 指向第 2.3 节的 yaml训练脚本根据其中的 train 路径自动组织样本。epochs120 是在小数据集上足够覆盖收敛的轮数如果你的数据量只有几百张60 轮也会看到验证指标停滞。imgsz640 是官方默认值指尖这类细粒度特征对这个值非常敏感我建议不要低于 480。模型选型上yolov8n.pt 体积最小摄像头场景吞吐最高yolov8s.pt 精度常有明显提升显存占用也在 8G 卡的可承受范围内yolov8m.pt 则适合离线批量处理。资源包默认用的通常是 s如果你的机器只有 4G 显存第一次训练前建议替换成 n。3.2 从数据集路径到训练命令一次完整调用不带界面的命令行调用如下python train.py --data handgesture.yaml --weights yolov8s.pt \ --epochs 120 --batch 16 --imgsz 640 --device 0这段命令里 --data 是必填--weights 决定迁移起点--batch 和 --imgsz 共同决定显存开销。我一般把固定配置留在脚本里命令行只做覆盖方便记录每次实验用的是什么组合。训练开始后日志会输出每个 epoch 的 box loss、cls loss、dfl loss 以及 mAP50、mAP50-95判断是否收敛主要看验证集 mAP50而不是盯着训练集 loss。如果你要在 Windows 上跑注意 train.py 入口必须写成if __name__ __main__:的形式否则训练脚本在启动子进程时会把整个文件重新执行一遍轻则重复加载模型重则直接报 daemon 进程相关错误。Linux 服务器上一般不需要特别处理。训练中途 CtrlC 中断是很常见的事。再次执行时 ultralytics 会从 runs/detect/trainX 中找 last.pt 并继续前提是命令行中 weights 参数指向 last.pt。我不会清空 runs 目录重来因为中断位置可能已经收敛得不错断点续训省时间。3.3 数据增强与关键超参数batch、imgsz、mosaic 的实际影响yolov8 的内置增强默认值是为 COCO 这种多样场景设计的手势数据集必须单独收紧model.train( datahandgesture.yaml, epochs100, batch16, imgsz640, degrees5, # 旋转 5 度之内超过会破坏手指相对位置 scale0.5, # 缩放系数 0.5~1.5 translate0.1, # 平移范围 ±10%超过会让手部出框 flipud0.0, # 手势不做垂直翻转 hsv_h0.015, # 色调轻微扰动防止肤色过拟合 hsv_s0.4, # 饱和度扰动 hsv_v0.4, # 明度扰动 mosaic0.8, # 80% 概率进行 4 图拼接 mixup0.2, # 20% 概率与另一张图混合 )逻辑与参数说明degrees 为什么取 5因为手势是柔性目标旋转超过 15 度食指和中指的空间关系会被破坏模型学到的是角度特征而不是手势特征。取 5 度足够覆盖手掌在自然状态下的晃动。flipud 要关掉垂直翻转会把食指朝下变成食指朝上类别意义不变但训练分布和真实使用场景不匹配如果摄像头是俯拍的这类增强反而制造伪样本。mosaic 和 mixup 对数据量少的项目比较重要。mosaic 把四张图拼成一张相当于把小目标放大了训练但如果你的手部框占比已经很大mosaic 反而增加边框割裂改到 0.5 到 0.8 之间可以快速对比。我习惯把超参按以下窗口设置先跑通再谈最优参数推荐起始值什么时候调低batch168G 显存且 OOM降到 8imgsz640显存不足时降到 480不推荐 320degrees5出现手指方向误判时降到 2mosaic0.8小目标漏检但大目标正常时降到 0.5mixup0.2背景负样本较多时可提高到 0.4表格里的思路是先保证梯度能更新再考虑精度。显存不够只用调整 batch 和 imgsz 两项其余增强参数尽量不要在第一轮实验中动避免多个变量同时变化分不清是谁的影响。4. 实时推理与结果输出摄像头循环、后处理与本地接口训练结束真正能跑起来的还是推理端。这里我按从简单到复杂的顺序写先让摄像头循环稳定再自定义标注绘制最后接成一个 HTTP 接口给上位机用。4.1 摄像头读取与推理循环控制检测频率保住帧率detect.py 的主循环常见写法是import cv2 from ultralytics import YOLO model YOLO(best.pt) # 用训练保存的最优权重 cap cv2.VideoCapture(0) # 打开默认摄像头 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) frame_skip 2 # 每 3 帧推理一次 idx 0 while cap.isOpened(): success, frame cap.read() if not success: break idx 1 if idx % (frame_skip 1) ! 0: continue results model.predict(frame, conf0.45, iou0.5, verboseFalse) annotated results[0].plot() cv2.imshow(hand_gesture, annotated) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明frame_skip2 的含义是跳过两帧、处理第三帧摄像头本身 30fps推理 10fps 就能达到看起来流畅的交互。如果实际测试仍然卡先把 imshow 和 waitKey 放进一个开关比如按空格才显示画面平时只做识别服务。conf0.45 之所以比离线验证时低是因为摄像头运动模糊会把置信度整体拉低一截离线测 0.6线上我一般保留 0.4 或 0.45。iou0.5 是 NMS 阈值两个手势靠得很近时适当降这个值能去掉重复框但降太多会让重叠的相邻手指被合并成一个框。如果你想把识别结果与业务系统同步先在上位机固定推理输出的 JSON 字段再谈帧率。我习惯把每帧检测结果追加到一个环形缓冲区里物理按键按下时再取最近一帧结果这样不会出现手势已经结束才收到指令的情况。4.2 后处理与可视化自绘框和类别文字的注意点不依赖 plot()自绘框代码需要自己处理返回值gesture_names {0: thumb, 1: index, 2: middle, 3: ring, 4: little, 5: ok} for result in results: if result.boxes is None: continue for box in result.boxes: x1, y1, x2, y2 map(int, box.xyxy[0].tolist()) cls_id int(box.cls[0]) conf float(box.conf[0]) if conf 0.5: continue cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) label f{gesture_names[cls_id]}: {conf:.2f} cv2.putText(frame, label, (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)参数与边界box.xyxy 返回的是浮点张量不转 int 送给 cv2.rectangle 会抛类型异常box.conf[0] 是 0 到 1 的置信度过滤阈值放这里最省事不需要等到整张图推理完。如果你业务上只关心 OK 手势可以在这一层直接按 cls_id 过滤避免下游系统处理无关类目。值得注意的细节是 gesture_names 这组编号必须和训练 yaml 一致而不是和窗口显示顺序一致。模型预测的是 0 到 5 的整数编号编号对应的中文名只在这里发生第一次偏移这也是手写后处理最容易出错的一层。我在部署前会在若干张测试图上对比标注与显示结果能快速定位错层。4.3 把结果接出去用 HTTP 接口替代窗口显示让识别结果给其他进程用较常见的方式是包一层 Flask 服务from flask import Flask, request, jsonify import cv2, base64, numpy as np app Flask(__name__) model YOLO(best.pt) app.route(/infer, methods[POST]) def infer(): payload request.get_json() img_bytes base64.b64decode(payload[image]) img cv2.imdecode(np.frombuffer(img_bytes, np.uint8), cv2.IMREAD_COLOR) results model.predict(img, conf0.5, iou0.5, verboseFalse) out [] for r in results: for box in r.boxes: cls_id int(box.cls[0]) out.append({ class: cls_id, name: model.names[cls_id], conf: float(box.conf[0]), bbox: box.xyxy[0].tolist() }) return jsonify(out)逻辑说明这个接口接收 base64 编码的图像先解码成 numpy 数组再推理最后返回 JSON。好处是上游只要会 requests 就能调用不依赖摄像头设备。参数上注意服务端和客户端要保持同一套 base64 编码格式否则经常出现图片能 decode 但颜色通道反了的情况那是 BGR 与 RGB 顺序问题不是模型问题。并发量高的时候尽量避免走 base64直接 multipart 上传二进制图片能省一层转码开销CPU 占用会明显下降。单摄像头场景下这个接口足够支撑本地少量并发的调用再高就要上消息队列。5. 避坑与排查训练不收敛、显存溢出、误检和低帧率的解决记录下面的五个坑是我每次复现手势识别项目时几乎都会踩到的每一条都按现象、原因、解决的顺序记录。不一定全部命中你的环境但多数情况下能减少排查时间。5.1 现象训练 loss 正常下降验证集漏检却接近一半训练曲线看起来没有任何问题loss 一路走低但到 val 上 mAP50 只有 0.3。原因是数据集的背景高度相似比如五百张图里四百八十张都是同一张办公桌模型把桌角纹理当成手势的一部分。解决方法是先看 val 图片的错误样本如果错误框集中在图片固定位置基本就坐实了背景泄漏。我一般在训练时把背景增强打开mixup 调到 0.3 以上并且在手机拍摄翻转中引入负样本很快就能看到 mAP 回升。5.2 现象训练时提示 CUDA out of memory16G 显存跑 batch16、imgsz640 很稳8G 卡上会在第三个 epoch 附近直接爆。原因是 mosaic 会把不同尺寸图片拼成一个 640×640实际单 batch 峰值显存和整图尺寸成正比几乎等比于 batch。解决顺序是先 batch 16 降到 8如果还爆把 imgsz 降到 480同时确认没有开 cacheTrue。cache 在内存里存一份数据副本显存紧张时它并不会直接占用显存但 Windows 下会拉高物理内存占用导致数据排队变慢误以为又爆显存。遇到这种情况也检查一下显卡驱动是否支持混合精度ultralytics 默认开着 amp老显卡上反而可能不稳定。5.3 现象摄像头实时推理只有五帧每秒单线程 while 循环从摄像头读一帧、推理一帧、imshow 一帧帧率始终上不去。原因是摄像头读取本身阻塞加上 waitKey 强制等待推理时间被放大了三倍。解决是把图像采集单独丢到一个线程主线程只负责拉最新一帧推理import threading import queue frame_q queue.Queue(maxsize2) def capture_loop(cap, q): while True: ok, frame cap.read() if ok and not q.full(): q.put(frame) thread threading.Thread( targetcapture_loop, args(cap, frame_q), daemonTrue) thread.start()逻辑说明用长度 2 的队列做缓冲读线程只管往队尾放新帧推理线程从队头取最新的帧。如果取帧跟不上队列满了就直接抛弃旧帧保证推理落在最新画面上。这个改动在普通笔记本上能把 FPS 从 5 提到 12 左右。同时加上 cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)摄像头驱动的内置缓冲只留一帧避免视频流延迟越来越大。5.4 现象画面里没有手模型却报出一个高置信度手势背景中只要有类似肤色或形状的物体比如杂志封面的手、桌面反光模型就报出类别。原因是训练集几乎没有“没有手”的负样本模型把背景局部特征学习成了目标特征。解决方法是增加只含背景的图片作为负样本每张负样本的 txt 写为空文件并单独加一个 background 类别再把 conf 阈值从 0.3 提到 0.5。比较典型的处理从原训练集里抽出三分之一的背景区域直接存成负样本图重训一轮。这是我这几个项目里最见效的做法。5.5 现象中指和食指互相混淆错判集中在这两个类如果混淆矩阵显示错误集中在 index 和 middle 之间多半有两层原因一是标注框偏了框把两根手指都包进去了模型没法学到区分性特征二是 imgsz 太小640 下指尖间距在特征图上不足一个像素。解决是先人工抽十张混淆样本检查标注再在增强外把 imgsz 升到 768 训练一轮。如果两种措施都做完仍然混淆可以尝试在类别定义上合并为“二指手势”这一拍降低细粒度分类要求业务上往往更稳定。6. 更近一步导出 ONNX 跑边缘侧把手势识别结果接进业务系统资源包里的 detect.py 依赖 Python 和完整训练框架生产环境如果想嵌入到边缘设备更好的做法是导出 ONNX 权重用 OpenCV DNN 或 ONNX Runtime 做推理速度和体积都会显著改善。导出命令一行即可yolo export modelbest.pt formatonnx opset17 simplifyTrue导出后会生成同目录的 best.onnx我一般接着用 ONNX Runtime 做一个最小验证脚本import cv2 import numpy as np import onnxruntime as ort session ort.InferenceSession(best.onnx) img cv2.imread(demo.jpg) # 先 letterbox 到 640再按 YOLOv8 的输出格式解析写到这里主要想传达的信息是拿到资源包之后的路径应该是先小数据复现再大增量重训最后导出成更轻的部署格式。某次我把 best.pt 直接塞进边缘盒子里启动时间超过八秒换 ONNX 之后降到一点五秒这个差距在产线上非常明显。导出验证通过后可以把结果通过 HTTP 或消息队列接进业务系统。我之前在模拟项目X里就是让识别服务返回 JSON上位机拿到手势类别后触发语音播报和屏幕提示延迟控制在百毫秒以内。此类接法只需改 detect.py 的返回结构不需要重训模型。每次做手势识别原型验证我都强制把“小样本先跑通”和“ONNX 导出后做一次结果对比”这两步放进工作流避免模型形态变了但推理端还拿着旧解析逻辑。这条习惯帮我少踩了不少坑希望帮到你。本文还有配套的精品资源点击获取
返回列表