简介:HaGRID-HAnd手势识别图像数据集面向计算机视觉研究者、深度学习开发者与手势交互方向的算法工程师,用于训练和评估手势分类模型,覆盖智能家居、虚拟现实交互等实际场景。资源包共55个文件,以54个json标注文件和1个txt说明文件为主,压缩包约337.51MB;标注按测试、子采样、训练验证三组划分,涵盖rock、like、call、fist、palm、peace、stop、ok等常见手势类别,便于按需组织训练、验证与测试流程。目前已有469人学习下载。数据集图像覆盖多种光照、背景与手部姿态变化,标注结构清晰,可直接对接CNN或Transformer等模型的数据读取与预处理流程,帮助读者快速搭建手势识别基线、迭代模型并评估泛化性能,是入门与进阶手势识别任务的实用素材。
1. 手势识别数据集选型:HaGRID 到底解决了什么麻烦
做过手势交互的同行大概都有过这种体验:模型在实验室摄像头前准确率 98%,换到客厅灯光下、换个人、换个角度,直接掉到 60% 以下。问题往往不在网络结构,而在数据——你手里那几千张图,背景太干净、手势太标准、人太单一。HaGRID(HAnd Gesture Recognition Image Dataset)就是冲着这个痛点来的:它把手势识别从「玩具 demo」推向「能扛住真实场景」的规模。这个数据集覆盖了多种静态手势类别,采集时考虑了不同光照、不同背景、不同肤色和不同拍摄距离,图像数量在同类开源手势数据集中属于第一梯队。它适合谁?做 yolo手势识别数据集 训练目标检测的、做 miediapipe手势识别 关键点预训练的、以及需要工业图像数据集级别鲁棒性的落地团队。简单说,如果你受够了自建数据集标注到凌晨三点,HaGRID 值得认真评估。
2. HaGRID 的标注结构与格式拆解:先看懂再动手
拿到一个数据集压缩包,最忌讳的就是直接解压然后train.py一把梭。HaGRID 的组织方式有它自己的逻辑,不先摸清楚,后面格式转换、类别映射、划分训练集全是坑。这一章把它的骨架拆开讲。
2.1 目录组织与图像命名规律
HaGRID 的典型结构是根目录下按类别分文件夹,或者图像统一存放、标注单独用 JSON/CSV 描述。常见做法是图像放在images/或按手势类别分子目录,标注文件记录每张图的边界框坐标和类别标签。图像命名通常带有一串标识符,用于关联标注文件里的记录。你需要做的第一件事是统计:总图像数、类别数、每类样本量。这一步不做,后面类别不均衡会教你做人。
# 统计 HaGRID 各类别图像数量,快速发现长尾类别 import os from collections import Counter root = "ha_grid/images" counter = Counter() for cls in os.listdir(root): cls_dir = os.path.join(root, cls) if os.path.isdir(cls_dir): n = len([f for f in os.listdir(cls_dir) if f.lower().endswith((".jpg", ".png"))]) counter[cls] = n for cls, n in counter.most_common(): print(f"{cls:20s} {n}") print("总计:", sum(counter.values()))这段脚本遍历类别目录统计样本量。参数上注意root要指向你解压后的实际图像根目录,如果 HaGRID 版本是扁平存储加标注文件的,这段逻辑要改成读标注文件统计。跑完你会看到类别分布,如果某类只有几百张而其他类上万张,训练时就得考虑重采样或 focal loss。
2.2 标注字段含义与坐标系约定
HaGRID 的标注一般包含手势类别标签和手部区域的边界框。边界框格式可能是[x_min, y_min, x_max, y_max]绝对像素坐标,也可能是归一化的[x_center, y_center, width, height]。这两种格式搞混,训练时 loss 不降反升,而且不报错——这是最阴险的翻车方式。判断方法很简单:看数值范围。如果坐标值大量超过 1,基本是绝对像素;如果都在 0 到 1 之间,就是归一化坐标。
import json # 读取标注文件,检查坐标格式 with open("ha_grid/annotations.json", "r") as f: ann = json.load(f) # 取前几条看看字段结构 sample = ann[0] if isinstance(ann, list) else list(ann.values())[0] print(json.dumps(sample, indent=2, ensure_ascii=False)) # 判断坐标是否归一化 def is_normalized(bbox): return all(0 <= v <= 1 for v in bbox) # 假设 bbox 字段名为 'bbox' for item in (ann[:5] if isinstance(ann, list) else list(ann.values())[:5]): bbox = item.get("bbox") or item.get("boxes") if bbox: print("归一化" if is_normalized(bbox[0] if isinstance(bbox[0], list) else bbox) else "绝对像素")逻辑说明:先打印一条标注的完整结构,确认字段名(不同版本可能叫bbox、boxes、bboxes),再判断坐标范围。参数上注意ann可能是 list 也可能是 dict,取决于标注文件的组织方式。这一步花五分钟,能省后面五小时 debug。
2.3 类别体系与手势语义映射
HaGRID 的手势类别不是随便定的,它对应一套手势语义体系。你需要把数据集里的类别名映射到你业务场景需要的标签。比如数据集里有like、dislike、ok、peace、stop等,但你的产品可能只需要「确认」「取消」「暂停」三类。这时候要做类别合并,合并规则必须写死在配置里,不能靠脑子记。
| 数据集类别 | 业务标签 | 处理方式 |
|---|---|---|
| like / ok | 确认 | 合并为一类 |
| stop / palm | 暂停 | 合并为一类 |
| peace / dislike | 忽略 | 训练时过滤 |
| 其他 | 待定 | 按需映射 |
这张表建议直接写成 YAML 或 JSON 配置文件,训练脚本读取它做映射。硬编码在代码里的类别映射,换一个版本的数据集就得改代码,维护成本极高。
3. 从 HaGRID 到 YOLO 格式:转换脚本与四个边界坑
HaGRID 原始标注格式和 YOLO 训练所需的格式通常不一致。YOLO 要求每张图对应一个.txt文件,每行是class_id x_center y_center width height,全部归一化到 0 到 1。这个转换过程看起来简单,但边界情况特别多。这一章给你一个能直接用的转换脚本,再把坑一个个标出来。
3.1 转换脚本:JSON 标注转 YOLO txt
import json import os from PIL import Image # 配置区 ANN_FILE = "ha_grid/annotations.json" IMG_DIR = "ha_grid/images" OUT_LABEL_DIR = "ha_grid/labels" CLASS_MAP = {"like": 0, "ok": 1, "stop": 2, "peace": 3, "palm": 4} # 按需扩展 os.makedirs(OUT_LABEL_DIR, exist_ok=True) with open(ANN_FILE, "r") as f: ann = json.load(f) # 兼容 list 和 dict 两种标注结构 items = ann if isinstance(ann, list) else ann.get("annotations", list(ann.values())) skipped = 0 for item in items: img_name = item.get("image_id") or item.get("filename") img_path = os.path.join(IMG_DIR, img_name) if not os.path.exists(img_path): skipped += 1 continue # 读取图像尺寸用于反归一化(如果标注是归一化的则不需要) with Image.open(img_path) as im: W, H = im.size bboxes = item.get("bboxes") or [item.get("bbox")] labels = item.get("labels") or [item.get("label")] lines = [] for bbox, label in zip(bboxes, labels): if label not in CLASS_MAP: continue cls_id = CLASS_MAP[label] # 判断坐标格式并统一转为 YOLO 格式 if all(0 <= v <= 1 for v in bbox): # 已是归一化 xywh xc, yc, w, h = bbox else: # 绝对像素 xyxy -> 归一化 xywh x1, y1, x2, y2 = bbox xc = (x1 + x2) / 2 / W yc = (y1 + y2) / 2 / H w = (x2 - x1) / W h = (y2 - y1) / H # 裁剪到 [0,1] 防止越界 xc, yc = max(0, min(1, xc)), max(0, min(1, yc)) w, h = max(0, min(1, w)), max(0, min(1, h)) lines.append(f"{cls_id} {xc:.6f} {yc:.6f} {w:.6f} {h:.6f}") if lines: txt_name = os.path.splitext(img_name)[0] + ".txt" with open(os.path.join(OUT_LABEL_DIR, txt_name), "w") as f: f.write("\n".join(lines)) print(f"转换完成,跳过 {skipped} 张缺失图像")逻辑说明:脚本先兼容标注文件的两种常见结构,然后逐条处理。关键参数是CLASS_MAP,你必须根据实际类别名修改。坐标转换部分做了格式判断和越界裁剪,这两步在实际数据里几乎一定会触发。skipped计数用于发现图像和标注不匹配的问题。
3.2 坑一:坐标越界与负值
现象是训练时 loss 突然变成 NaN 或者检测框飞到图像外面。原因是原始标注里存在x2 > W或x1 < 0的情况,转换后归一化坐标超出 [0,1]。解决方式就是上面脚本里的裁剪逻辑,但更好的做法是统计越界比例,如果超过 5%,说明标注质量有问题,需要回头检查数据源。
3.3 坑二:图像与标注文件名不匹配
HaGRID 的图像文件名和标注里的image_id可能差一个扩展名,或者大小写不一致。现象是转换脚本跑完发现大量图像没有对应标注。解决方式是在匹配前统一做lower()和去扩展名处理,并且打印不匹配的样本名,人工抽查几条。
3.4 坑三:一张图多个手势框
有些图像里出现多只手或多个手势。如果你的业务只关心主手势,需要按面积或置信度筛选;如果全都要,YOLO 的 txt 格式天然支持多行,不用特殊处理。但要注意类别不均衡会加剧——多手势图的类别分布和单手势图不同。
3.5 坑四:类别 ID 从 0 还是 1 开始
YOLO 系列通常要求类别 ID 从 0 开始连续。如果你用的框架或预训练权重是从 1 开始的,需要在配置里改nc和类别偏移。这个坑不报错,但模型学出来的类别全是错的。验证方法:训练一个 epoch 后,用一张已知类别的图推理,看输出的 class_id 对不对。
4. 训练配置与参数调优:让 HaGRID 真正跑起来
格式转换完只是开始,训练配置才是决定模型能不能用的关键。这一章讲数据划分、增强策略和几个必调参数。
4.1 训练集验证集划分:别让同一只手出现在两边
HaGRID 的图像可能来自同一批采集对象,如果随机划分,同一个人或同一场景的图可能同时出现在训练集和验证集,导致验证指标虚高。常见做法是按采集批次或对象 ID 做分组划分。如果数据集没有提供分组信息,至少按图像文件名前缀或时间戳做粗粒度分组。
import os import random from sklearn.model_selection import GroupShuffleSplit # 假设文件名格式为 "subjectID_xxx.jpg",提取 subjectID 作为分组 files = [f for f in os.listdir("ha_grid/images") if f.endswith(".jpg")] groups = [f.split("_")[0] for f in files] gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, val_idx = next(gss.split(files, groups=groups)) with open("train.txt", "w") as f: f.write("\n".join(os.path.abspath(os.path.join("ha_grid/images", files[i])) for i in train_idx)) with open("val.txt", "w") as f: f.write("\n".join(os.path.abspath(os.path.join("ha_grid/images", files[i])) for i in val_idx)) print(f"训练集 {len(train_idx)} 张,验证集 {len(val_idx)} 张")参数说明:test_size=0.2是验证集比例,random_state固定后结果可复现。groups的提取逻辑要根据实际文件名调整,如果文件名没有分组信息,可以用图像感知哈希做聚类分组,但成本较高。
4.2 数据增强:手势识别特有的几个策略
通用增强(翻转、缩放、色彩抖动)都适用,但手势识别有几个特殊点。水平翻转要谨慎:有些手势左右手语义不同,翻转后标签就错了。旋转角度不宜过大,超过 30 度后手势语义可能改变。遮挡增强很有效,因为真实场景里手经常被部分遮挡。常见做法是用 Mosaic 和 MixUp,但 MixUp 在手势场景下可能把两只手混在一起,建议降低概率。
| 增强方式 | 建议概率 | 注意事项 |
|---|---|---|
| 水平翻转 | 0.3 | 确认手势无左右语义 |
| 随机旋转 | 0.2 | 角度限制 ±25° |
| 色彩抖动 | 0.5 | 模拟不同光照 |
| 随机遮挡 | 0.3 | 模拟真实遮挡 |
| Mosaic | 0.5 | 注意小目标手势 |
4.3 必调参数:学习率、batch size 与输入分辨率
学习率用余弦退火配合 warmup,初始值 0.01 对 SGD 或 0.001 对 Adam 是常见起点。batch size 受显存限制,但太小会导致 BN 层统计不稳定,建议至少 16。输入分辨率直接影响小目标手势的检测效果,HaGRID 图像里手部区域占比不一,分辨率太低会丢细节。常见做法是从 640 起步,如果小目标漏检严重再升到 768 或 896,但推理速度会下降。
# YOLOv8 训练命令示例 yolo detect train \ data=ha_grid.yaml \ model=yolov8m.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ lrf=0.01 \ warmup_epochs=3 \ cos_lr=True \ mosaic=0.5 \ degrees=25 \ fliplr=0.3 \ hsv_h=0.015 hsv_s=0.7 hsv_v=0.4 \ project=runs/ha_grid \ name=exp1参数说明:imgsz是输入分辨率,lr0是初始学习率,lrf是最终学习率比例,warmup_epochs是预热轮数。degrees控制旋转角度,fliplr控制水平翻转概率。这些值不是金科玉律,但作为起点能让你少走弯路。
5. 避坑与排查:HaGRID 实战中那些血泪经验
这一章记录几个我在用 HaGRID 和类似手势数据集时真实踩过的坑,每条按现象、原因、解决来写。
5.1 验证集指标很高但实际场景全错
现象:验证集 mAP 0.9 以上,但拿手机对着摄像头测试,识别率不到一半。原因:验证集和训练集来自同一分布,且数据增强不够贴近真实场景。解决:从真实场景采集一小批测试图(哪怕只有几十张),单独做测试集;增强策略里加入背景替换和光照模拟。
5.2 训练到一半 loss 突然爆炸
现象:前 20 个 epoch 正常下降,第 21 个 epoch loss 变成 NaN。原因:大概率是某张图的标注框宽高为 0 或负值,计算 loss 时除零。解决:在数据加载阶段加校验,过滤掉宽高小于 1 像素的框;同时检查学习率是否过大导致梯度爆炸。
5.3 模型只认识训练集里的那几个人
现象:换一个人做同样手势,模型就识别不出来。原因:HaGRID 虽然多样,但如果你只用了子集,人物多样性不够。解决:确保训练时覆盖尽可能多的采集对象;用 RandAugment 或风格迁移做人物外观增强。
5.4 推理速度慢到无法实时
现象:模型精度达标,但 FPS 只有个位数。原因:输入分辨率过高、模型 backbone 太重、后处理耗时。解决:先降分辨率到 416 或 320 测试精度损失;换轻量 backbone(如 YOLOv8n);用 TensorRT 或 ONNX Runtime 加速推理。
5.5 类别不均衡导致小类几乎不检出
现象:某些手势类别样本少,模型基本不预测这些类。原因:交叉熵损失被大类主导。解决:用 focal loss 或 class weight;对少样本类做过采样;如果某类样本少于 500 张,考虑合并到语义相近的类。
6. 进阶技巧:用 HaGRID 预训练 + 少量数据微调自己的手势
如果你有自己的手势场景,但标注数据只有几百张,直接训练肯定不够。一个实用技巧是:先用 HaGRID 全量数据训练一个预训练模型,然后冻结 backbone,只微调检测头。这样几百张图也能达到可用精度。
具体操作:加载 HaGRID 训练好的权重,把分类头替换成你的类别数,冻结前 N 层,用低学习率(比如 0.0001)训练 20 到 30 个 epoch。验证时用留出的真实场景图做测试,不要用训练集里的图。
# 冻结 backbone 微调示例(以 ultralytics 为例) from ultralytics import YOLO model = YOLO("runs/ha_grid/exp1/weights/best.pt") # 冻结前 10 层 for i, (name, param) in enumerate(model.model.named_parameters()): if i < 10: param.requires_grad = False # 替换检测头类别数(假设你的场景只有 3 类) model.model.model[-1].nc = 3 model.train( data="my_gesture.yaml", epochs=30, imgsz=640, batch=8, lr0=0.0001, freeze=10, project="runs/my_gesture", name=finetune )参数说明:freeze=10表示冻结前 10 层,具体层数要根据模型结构调。lr0用比预训练时小一个数量级的值,避免破坏已学特征。nc要改成你自己的类别数,同时确保my_gesture.yaml里的类别名和 ID 对应。
验证方法:微调后用混淆矩阵看各类的误检和漏检,特别关注少样本类。如果某个类 recall 低于 0.5,要么加数据,要么检查标注质量。我自己的习惯是每次微调都保留一个「回归测试集」——固定几十张图,每次训练完都跑一遍,看指标有没有退化。这个习惯帮我省了很多次「改完还不如不改」的后悔药。
希望帮到你。
本文还有配套的精品资源,点击获取