简介:这是一套面向计算机视觉与目标检测学习者的带标注麻将识别数据集,支持YOLOv8这一主流目标检测格式,可直接用于训练麻将点数、花色及字牌识别模型,覆盖一条、八万、二饼以及东南西北中发白板等常见牌面类型,适合个人项目、课程设计或算法验证。数据集共包含2000个文件,其中894张JPEG图片、1105个文本格式的标注文件,以及1个YAML格式的配置说明文件,标注与图片一一对应,可直接按常见方式划分训练集与验证集;压缩包整体约65.24MB,结构清晰便于加载。目前已有三十八人学习使用,适合需要快速获得规范标注数据的目标检测初学者或研究者。借助该数据集,可省去自行采集与标注的大量时间,直接开展模型训练、精度调优与推理测试,尤其适用于练习目标检测系列模型的迁移学习与效果评估。
1. 麻将识别数据集 97.5% 识别率:这份带标注数据到底适不适合 YOLO v8
带标注的麻将识别数据集、97.5% 识别率、支持 YOLO v8 格式,这三个信息凑在一起,很多人的第一反应是“拿到就能跑出模型”。实际上你拿到的是标注好的图片和标签文件,模型要自己用 YOLO v8 训练;97.5% 是某个固定验证集上的成绩,换个光照、换个拍摄角度,分数可能直接掉到 90% 以下。这篇文章解决三件事:这份数据集里标注规范到底怎么理解,怎么把它整理成 YOLO v8 能直接训练的结构,以及训练和复现 97.5% 的过程中哪些坑最值得提前躲开。适合正在做棋牌检测、目标检测入门,或者想验证“公开数据集到自己业务场景”这条路的人读。
2. 带标注麻将数据集的真实构成:34 个类别边界与标注规范,决定 97.5% 的地基
2.1 数牌 27 类 + 字牌 7 类:类别空间先拆清楚,别急着合并
麻将识别数据集的核心是类别空间。按标题里的描述,要识别的内容包括“1条,8萬,2饼”以及“東南西北中發白板”,这其实是 34 个类别:条、万、饼三种数牌各 1 到 9,共 27 类;字牌里東、南、西、北、中、發、白板共 7 类。很多初学者看到“1条、2饼”觉得差不多,直接把 27 类数牌合并成“条、万、饼”3 类,再把字牌单列,最后只训出 9 个类别。结果模型确实能判断“这是一张条牌”,但分不清是 1 条还是 9 条——识别率看着还行,业务上根本不能用。
正确的做法是把 34 类作为独立的输出节点。类别名建议用拼音或者数字编号加中文注释,因为 YOLO 的 names 列表只要求字符串,不要求纯英文。下面是一个可用的类别映射,按这个顺序写到 data.yaml 里:
0: 1_tiao 1: 2_tiao 2: 3_tiao ... 8: 9_tiao 9: 1_wan ... 26: 9_bing 27: dong 28: nan 29: xi 30: bei 31: zhong 32: fa 33: baiban逻辑说明:这个顺序不是随便定的,数牌在前、字牌在后,方便训练时按区间做类别权重调整。比如字牌样本少,后面可以在 loss 权重上单独补偿。不要用纯中文类别名直接存成names: [一条, 二条...],虽然 YOLO v8 能读 UTF-8,但转 ONNX 部署时有些推理框架对中文类别名支持不友好,Visual Studio 或 C++ 环境容易乱码。用拼音做 id,中文做注释,部署时才不踩坑。
参数注意:类别顺序一旦确定,训练、验证、导出模型、推理脚本里的 id 必须全程一致。中途改顺序等于重新训练,没有后悔药。建议把类别表单独存一个classes.txt,转换标注脚本和 data.yaml 都从这个文件读取,避免手写两遍对不上。
2.2 标注框的边界:框住牌面还是框住整张牌,直接影响 mAP
同一个数据集,标注框怎么画,训练出来的 mAP 能差 10 个点以上。麻将牌常见两种标注方式:一种是框住整张牌的物理边缘,另一种是只框住牌面上的花色区域。标题里的数据集如果标注规范是“框整张牌”,那模型学到的特征是整张牌的纹理加边框;如果是“框牌面”,模型学到的是花色图案本身。
我的建议是优先框住牌面区域。原因很简单:实际部署时,摄像机拍到的是牌桌上一堆牌,牌与牌之间可能有缝隙、阴影,甚至手指遮挡。框住牌面区域,模型学到的特征更聚焦,对小目标检测更友好;而框整张牌会把牌间的阴影和缝隙也包进正样本,背景噪声被当成目标特征的一部分。但要注意,如果原数据集已经是框整张牌的规范,训练时不要自作主张把标签往牌面上缩,除非你重新标注,否则标签和图像内容不匹配,loss 会一直抖动。
还有一个容易踩坑的点是旋转边界框。牌在桌上不一定是正朝上的,YOLO v8 的标准检测头是 axis-aligned 矩形框,对带角度的目标用水平框表达会有冗余背景。如果数据集里大量牌是斜着放的,水平框会把相邻牌的边角也包进来。这个场景下要么在训练时做随机旋转增强,让模型适应角度变化;要么换 YOLO v8-OBB 旋转框检测。但标题写的是“支持 yolo v8 格式”,通常指标准水平框,所以本文后续按水平框的方案走。
2.3 标注工具选型:LabelImg / Labelme / CVAT,如何统一输出 YOLO 格式
拿到数据集后如果发现标注缺失或需要补充样本,大概率要自己动手标注。常见的标注工具有三个,适用场景完全不同:
| 工具 | 输出格式 | 适合场景 | 缺点 |
|---|---|---|---|
| LabelImg | Pascal VOC XML / YOLO txt | 单机小批量、快速补标 | 两三年没怎么维护,多边形不支持 |
| Labelme | JSON 多边形 | 精细标注、不规则区域 | 转 YOLO txt 必须写转换脚本 |
| CVAT | XML / COCO / YOLO 等 | 团队协作、自动预标注 | 要部署服务,本地跑较重 |
LabelImg 胜在轻量,一张麻将牌点一个框,同一个类别连续标很快,适合给已有数据集补缺。CVAT 适合从头标上万张图,因为它支持导入一个预训练模型做自动标注,人工只负责修正框,效率能提一倍。Labelme 的强项是精细的牌面轮廓,但转到 YOLO 要算外接矩形,多边形转矩形会丢失角度信息,如果不是特殊需求不建议绕这一圈。
无论用哪个工具,最终都要落到“每张图片一个同名 txt,每行一个目标,格式为 class_id x_center y_center width height,坐标归一化到 0 到 1”。这个格式是 YOLO v8 的唯一输入格式,第 3 章详细讲怎么转换。
提示:拿到数据集先统计一张图里平均标注框数量。麻将牌桌一张图经常 20 到 30 个框,如果标注密度远低于这个值,说明数据集可能只标了部分牌,训练时会被当成背景漏检。
3. 把标注转成 YOLO v8 格式:目录结构、txt 坐标与类别映射脚本
3.1 YOLO v8 的数据集目录:images 与 labels 严格一一对应
YOLO v8 训练时要求数据集目录里 images 和 labels 两个文件夹严格一一对应,同名图片必须有同名 txt 标签文件,否则训练直接报错。推荐的结构是这样的:
mahjong_dataset/ ├── images/ │ ├── train/ │ │ ├── table_001_frame_0001.jpg │ │ └── ... │ └── val/ │ ├── table_002_frame_0001.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── table_001_frame_0001.txt │ │ └── ... │ └── val/ │ ├── table_002_frame_0001.txt │ └── ... ├── data.yaml └── classes.txt逻辑说明:图片和标签只是目录路径不同,文件名必须完全一致,包含扩展名在内。labels/train里的 txt 和images/train里的 jpg 一一对应,训练时 ultralytics 会按这个规则去找标签。val 目录同样要完整,YOLO v8 在训练每个 epoch 结束后会在 val 上算 mAP,没有验证集就等于没有反馈信号,只能盲训。
一个常见的坑是有的数据集会把 xml 或 json 标签直接丢在 labels 目录里,忘记转成 txt。YOLO v8 训练器发现.txt不存在时会先写一条 warning 再把该图当成背景图,后果是 mAP 虚低。所以拿到数据集后第一件事是遍历 labels 目录,统计 txt 文件数量和 images 数量是否完全相等。
3.2 坐标换算原理:从 XML 的 bndbox 到归一化 x_center / y_center / w / h
如果数据集的原始标注是 LabelImg 生成的 Pascal VOC XML,或者网上公开仓库给的标签是 XML 格式,需要先转换成 YOLO txt。两种格式的核心差异是坐标表达方式完全不同。
Pascal VOC 的 bndbox 给的是像素绝对坐标:xmin、ymin、xmax、ymax。YOLO 给的是归一化后的中心点坐标加宽高:x_center、y_center、width、height,所有值除以图片宽高后落在 0 到 1 之间。换算公式:
x_center = ((xmin + xmax) / 2) / image_width y_center = ((ymin + ymax) / 2) / image_height width = (xmax - xmin) / image_width height = (ymax - ymin) / image_height注意两个极端情况。第一,如果标注框超出图片边界,比如 xmax 大于 image_width,算出来的 width 就会大于 1,YOLO 训练时会报 “Box coordinates out of bounds” 的告警,这类样本越多,训练越不稳定。第二,如果 xmin 和 xmax 写反了,width 算出来是负数,这种 txt 行会被直接丢弃。所以转换脚本里一定要做边界检查,超过 [0, 1] 的值要么裁剪要么剔除。
3.3 转换脚本与边界检查:一份可直接改的 Python 代码
下面这份脚本是我处理麻将数据集最常用的一套逻辑,读 XML 转 YOLO txt,带越界检查和类别映射:
import os import xml.etree.ElementTree as ET from pathlib import Path def load_class_map(classes_file): """从 classes.txt 读取类别名到 id 的映射""" with open(classes_file, 'r', encoding='utf-8') as f: lines = [line.strip() for line in f if line.strip()] return {name: idx for idx, name in enumerate(lines)} def xml_to_yolo(xml_path, class_map, output_dir, img_width, img_height): tree = ET.parse(xml_path) root = tree.getroot() lines = [] for obj in root.findall('object'): name = obj.find('name').text.strip() if name not in class_map: print(f'[skip] unknown class "{name}" in {xml_path.name}') continue bndbox = obj.find('bndbox') xmin = float(bndbox.find('xmin').text) ymin = float(bndbox.find('ymin').text) xmax = float(bndbox.find('xmax').text) ymax = float(bndbox.find('ymax').text) # 边界防御:非法框直接跳过 if xmax <= xmin or ymax <= ymin: print(f'[warn] reversed bbox in {xml_path.name}, skip') continue x_center = ((xmin + xmax) / 2) / img_width y_center = ((ymin + ymax) / 2) / img_height w = (xmax - xmin) / img_width h = (ymax - ymin) / img_height # 越界裁剪到合法范围,避免 YOLO 告警 x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) w = min(max(w, 0.0), 1.0) h = min(max(h, 0.0), 1.0) class_id = class_map[name] lines.append(f'{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}') output_path = Path(output_dir) / (Path(xml_path).stem + '.txt') with open(output_path, 'w', encoding='utf-8') as f: f.write('\n'.join(lines)) # 用法:把 images 和 xmls 放在同目录下 if __name__ == '__main__': xml_dir = 'annotations' out_dir = 'labels' img_dir = 'images' class_map = load_class_map('classes.txt') for xml_file in Path(xml_dir).glob('*.xml'): # 从 xml 同级目录读图片尺寸,注意有些 xml 自带 size 节点可优先用 img_file = Path(img_dir) / (xml_file.stem + '.jpg') if not img_file.exists(): print(f'[warn] image not found: {img_file}') continue from PIL import Image img_w, img_h = Image.open(img_file).size xml_to_yolo(str(xml_file), class_map, out_dir, img_w, img_h)逻辑说明:脚本先加载 classes.txt 建立类别名到 id 的映射,然后遍历 XML 文件,逐个提取 bndbox 并换算成归一化坐标。换算出结果后做两道防御:一是处理 xmax 小于 xmin 的反向框,直接跳过,避免生成负数宽度;二是把中心点和宽高裁剪到 0 到 1 之间,防止标注框略微越界导致训练告警。最后按图片名写入 txt,保证与 images 目录文件一一对应。
参数说明:img_width和img_height从图片实际尺寸读取,可以通过 PIL 或 cv2 获取,不建议直接读 XML 里的 size 节点,因为有些标注工具生成的 size 值可能是缩放前的。class_map必须和 data.yaml 里的 names 顺序一致,这是全文最容易出错的地方。如果原始数据集的类别名是“1条”“8萬”这种中文,只要 classes.txt 里面写的是同样的字符串,映射就能对上;若不一致,先把类别名统一。
4. 用 YOLO v8 训练麻将识别模型:data.yaml、关键超参数与数据划分
4.1 data.yaml 的常见报错:路径、nc 和 names 必须对应
data.yaml 是 YOLO v8 的数据集说明书,写错一个字训练都起不来。最常见的问题出在path字段和names长度上。下面是一份针对麻将识别数据集的完整示例:
path: ./mahjong_dataset # 数据集根目录,相对路径或绝对路径 train: images/train # 训练图片目录 val: images/val # 验证图片目录 nc: 34 # 类别数,必须和 names 列表长度一致 names: 0: 1_tiao 1: 2_tiao 2: 3_tiao 3: 4_tiao 4: 5_tiao 5: 6_tiao 6: 7_tiao 7: 8_tiao 8: 9_tiao 9: 1_wan 10: 2_wan 11: 3_wan 12: 4_wan 13: 5_wan 14: 6_wan 15: 7_wan 16: 8_wan 17: 9_wan 18: 1_bing 19: 2_bing 20: 3_bing 21: 4_bing 22: 5_bing 23: 6_bing 24: 7_bing 25: 8_bing 26: 9_bing 27: dong 28: nan 29: xi 30: bei 31: zhong 32: fa 33: baiban逻辑说明:path是根目录,train和val是相对根目录的路径。注意这两项指向的是图片目录而不是标签目录,标签目录 YOLO 会自动去同级的labels下找。如果数据集里 train 和 val 的划分已经做好,这里直接写目录即可;如果图片都在一个文件夹,可以把 val 写成单独的验证图片目录,或者用 txt 文件路径列表。
参数注意:nc写错会直接报 “names length does not match nc” 或训练时类别索引越界。names的键不一定是连续数字,但顺序必须和 classes.txt 一一对应。如果你的数据集类别名带了空格,比如 “1 条”,yaml 解析时要把名字加上引号。部署阶段类别名会被原样输出到检测结果里,提前用拼音可以省掉编码问题,这是我多次踩坑后的习惯。
4.2 训练命令与关键超参数:imgsz、batch、epochs、patience 怎么调
yaml 准备好之后,训练命令本身很短:
yolo detect train \ data=mahjong_dataset/data.yaml \ model=yolov8n.pt \ epochs=150 \ imgsz=960 \ batch=16 \ device=0 \ patience=30逻辑说明:model 参数用yolov8n.pt做预训练权重,n 是 nano 版本,参数量最小,适合麻将这种目标小但数量多的场景。如果显存充足可以换yolov8s.pt或yolov8m.pt,识别率会更高,但训练时间和推理时间都会涨。epochs 设 150 是因为麻将类别多、相似类别多,收敛比一般数据集慢。patience=30 表示连续 30 个 epoch 验证集 mAP 没有提升就早停,保存 best.pt。
超参数对最终识别率的影响需要按优先级排序:
| 参数 | 推荐值 | 调整方向 | 影响 |
|---|---|---|---|
| imgsz | 960 | 牌小就上调到 1280 | 参数越大小数牌越容易识别,显存占用翻倍 |
| batch | 16 | 显存不足减半 | 太小影响 BN 统计,容易震荡 |
| epochs | 150 | 看 loss 曲线决定 | 早停后可以续训 |
| patience | 30 | 显存小可改 20 | 防止过拟合和算力浪费 |
| optimizer | AdamW 或 SGD | 分类任务 SGD 更稳 | 这个数据集建议 AdamW 配 lr0=0.01 |
训练开始后要盯三个指标:训练 loss、验证 loss、验证 mAP。如果 loss 持续下降但 mAP 不动,通常是类别不平衡;如果 mAP 能到 0.93 以上但 97.5% 还差一截,问题往往出在验证集构成或某些相似类别混淆上,而不是训练轮次不够。
注意:YOLO v8 默认开启 AMP 混合精度训练,精度损失很小但省显存。如果发现训练 loss 曲线像锯齿一样剧烈抖动,尝试加一行
amp=False,有时数据噪声大会导致 fp16 精度溢出,尤其麻将牌这种高纹理目标容易出现。
4.3 按牌局切分训练/验证集:避免同源图片泄漏导致虚高 mAP
这是麻将识别数据集最容易翻车的地方。如果数据是按视频抽帧采集的,同一张牌桌的相邻帧内容极度相似。直接随机划分 train/val,这些相似帧会以极大概率同时出现在两边,验证集 mAP 会虚高 5 个点以上,实拍时立刻现原形。
常见的做法是给每张图片的文件名带上对局 ID,比如table_001_frame_0001.jpg、table_001_frame_0002.jpg,然后按 table ID 分桶,同一个 table 的所有帧只能进 train 或只能进 val。下面是按前缀切分的数据划分脚本:
import os import random from pathlib import Path image_dir = Path('all_images') label_dir = Path('all_labels') train_ratio = 0.8 random.seed(42) # 按文件名前两段作为 key,确保同一牌局不拆散 groups = {} for img_path in sorted(image_dir.glob('*.jpg')): table_id = '_'.join(img_path.stem.split('_')[:2]) groups.setdefault(table_id, []).append(img_path.stem) # 按 group 切分 group_names = list(groups.keys()) random.shuffle(group_names) train_count = max(1, int(len(group_names) * train_ratio)) train_groups = set(group_names[:train_count]) val_groups = set(group_names[train_count:]) for group in train_groups: for stem in groups[group]: os.rename(image_dir / f'{stem}.jpg', image_dir / 'train' / f'{stem}.jpg') os.rename(label_dir / f'{stem}.txt', label_dir / 'train' / f'{stem}.txt') # val 同理,省略逻辑说明:以table_001_frame_0001为例,split('_')[:2]取到table_001,同一个牌局的所有帧被归到同一个 group。随机划分发生在 group 层面而不是单帧层面,这样验证集里出现的牌桌不会在训练集里出现,测出来的 mAP 才是真实泛化水平。
参数说明:train_ratio设为 0.8 是常见的划分比例,麻将这种相似场景多的数据建议 val 稍微多留一点,0.75 也可以。random.seed(42)固定随机种子,保证每次划分结果一致,这是复现 97.5% 的前提之一。如果你的数据集不是按视频抽帧而是独立拍摄的照片,可以用文件名直接随机划分,但仍建议确认同一个角度、同一时间点连拍的相似照片没有被拆开,这类相似样本的“伪验证”会给你制造全部类别都很好用的幻觉。
5. 麻将识别训练避坑:5 条能省一周的血泪经验
5.1 现象:loss 一直降,mAP 卡在 0.8 上不去
训练前 50 个 epoch,loss 从 3 降到 0.8,看着很正常,但 mAP@0.5 卡在 0.8 附近怎么都不涨。打开验证集的预测图发现,字牌和中张数牌(4、5、6)偶尔被漏检,而 1条 和 9条 这类花色差异大的牌基本全对。
原因:数据集中数牌出现频率远高于字牌,東南西北中發白板在真实牌桌上出现概率天然低,训练时正样本数量不足。模型对高频类别充分拟合,对低频类别处于欠拟合状态,这是典型的类别不平衡。
解决:先做类别频率统计,画出每类样本数量柱状图。对于样本量低于平均 30% 的类别单独做增强,常见做法是把这些图片额外做一次 copy-paste 增强,或对包含字牌的图片用更大的 HSV 扰动倍数重新采样。不要只依赖训练轮次,加轮次对稀缺类别帮助有限。另一个有效手段是把 mosaic 关小一些,mosaic 增强会把目标缩小,字牌本来就小,再被 mosaic 缩小就更难学了,把mosaic=0.5直接写进训练命令可以让小目标学得更充分。
5.2 现象:训练中断,报错 “CUDA out of memory”
用 imgsz=960、batch=16 起训,第 20 个 epoch 显存溢出中断,重启后从头再来,浪费半天。原因不是显存真的不够,而是 mosaic 增强时同时拼了 4 张图,计算峰值比单图高很多。
解决:显存溢出时按顺序做三件事。第一,batch 减半到 8,loss 震荡一点但能完成训练;第二,imgsz 降到 960 以下,因为麻将牌虽然是目标但 640 尺寸下牌面区域可能小于 20 像素,降到 640 会导致小目标大量丢失,所以优先减 batch 而不是减分辨率;第三,在训练命令里写cache=True或cache=ram,把图片预加载到内存避免每轮从磁盘读图造成 IO 抖动。如果三件事都做了还是溢出,换显存更大的卡或者用yolov8n初始化而不是yolov8s。
5.3 现象:验证集 mAP 接近 0.95,实拍手机照片全挂
模型在验证集上 mAP@0.5 到了 0.95,把手机拍的新照片丢进去,漏检一大半。原因几乎都是数据分布不一致:训练集是固定机位俯拍,牌面正对镜头;实拍照片是斜着拍的,牌面有透视变形,且光线偏暖,牌面还有反光。
解决:这是数据集本身拍摄条件单一造成的,不能靠调参解决,只能扩充训练数据的角度和光照分布。最快的方式是给训练集加随机旋转增强和透视变换:把degrees=15、translate=0.1、perspective=0.0005写入训练命令,crop 和 fliplr 默认开着不用改。注意 degrees 不要超过 20,旋转太多会让牌面文字翻转,模型反而学歪。如果扩增后仍然翻车,说明原始数据集场景覆盖不够,需要自己补充不同角度的样本,这是麻将识别从“能跑”到“能用”的分水岭,没有捷径。
5.4 现象:白板和發总是搞混,1条 和 1萬 也互相误检
验证集 confusion matrix 里,白板和發这一对元素误检率偏高,1条 和 1萬 的小字区域也会混。这种现象在语义相近、纹理相似的类别上非常常见,属于类别间特征空间重叠。
原因:白板是一张空白的牌面没有纹理,發是绿色底加红色字,如果训练图分辨率不够,模型只能学到“颜色偏绿”“有点图案”这种粗粒度特征,无法区分细节。1条 和 1萬 在牌面下方都有相似的装饰纹样,这些区域被模型当成了同一个特征。
解决:针对混淆对做专项清洗和样本补充。看 badcase 图(YOLO 训练日志里会生成val_batch*.jpg和混淆矩阵热力图),找出被误检的图片,用裁剪放大这些区域重新标注,单独加入训练集。不要只堆通用数据,堆再多的正常牌对这对混淆帮助不大。另一个做法是提高图像分辨率,imgsz 从 960 调到 1280,牌面小字更清晰,模型能学习到更细的纹理差异,代价是训练时间增加约 70%。
注意:混淆矩阵是避坑的第一工具,训练结束后立刻看
runs/detect/val/confusion_matrix.png,比盯 loss 曲线有用得多。矩阵里对角线外的深色格子就是你下一步要补样本的地方。
5.5 现象:转换脚本生成的 txt 内容全是 0.0 或 1.0
跑完第 3 章的转换脚本,打开一个 txt 文件,看到x_center、y_center等坐标全部是 0 或 1,训练时 YOLO 报很多 “labels out of bounds”。原因有两个:一是脚本没有拿到真实的图片尺寸,img_width和img_height传成了 0 或者默认值,导致除法结果变成无穷大或 0;二是原始 XML 里的坐标本身不是像素值,而是 0 到 100 的百分比值。
解决:写一个校验函数打印前 5 个 txt 文件的坐标范围。如果存在大量坐标大于 1 或等于 0,先确认图片尺寸读取正确,再确认 XML 的 bndbox 节点里坐标值域。某些标注工具导出的 bndbox 是浮点百分比,需要乘回图片宽高再做归一化。对于已经是 YOLO txt 格式的数据集,用yolo detect train前先运行一次yolo data check或者写三行代码扫描所有 txt 中坐标是否都在 [0, 1] 区间,这一步能省下训练到一半才发现标签错乱的翻车时间。
6. 让 97.5% 可复现:验证协议、混淆矩阵与部署最后一公里
想复现标题里的 97.5%,先确认这个数字的统计口径。目标检测里常见两个指标:mAP@0.5 和 mAP@0.5:0.95。mAP@0.5 是 IoU 阈值 0.5 时各类别 AP 的平均值,相对宽松,绝大多数公开数据集宣传的高识别率都指这个;mAP@0.5:0.95 要求 IoU 从 0.5 到 0.95 每隔 0.05 算一次再平均,严格得多,通常比前者低 15 到 25 个点。标题写 97.5%,大概率是 mAP@0.5,跑验证时用下面命令对齐:
yolo detect val \ data=mahjong_dataset/data.yaml \ model=runs/detect/train/weights/best.pt \ imgsz=960 \ conf=0.25 \ iou=0.6输出里会同时给出 mAP50 和 mAP50-95 两列。如果你的 mAP50 距离 97.5% 差几个点,先看 per-class AP 列表,找出 AP 最低的类别,回到混淆矩阵看它和谁混。固定验证集、固定 imgsz、固定 conf 和 iou 阈值,这三个条件缺一个,复现出来的数字都会不一样。这也是公开数据集分数和自建数据分数不可直接比较的原因。
部署时还有一个中文环境下特有的细节:导出 ONNX 后推理结果里的类别名是1_tiao、fa这种拼音,需要映射回中文显示。直接cv2.putText写“萬”这种 Unicode 字符在 OpenCV 里会变成乱码,常见做法是用 PIL 绘制中文再贴回图像,或者干脆用拼音显示,UI 层单独维护一份 id 到中文的映射表。这个细节不处理,演示效果会很减分。
我的习惯是把验证协议固定成一个脚本,每次训练完只跑这一条命令,把 mAP50、mAP50-95、每类 AP 写入一个 CSV 存起来。97.5% 这种数字单独看没意义,和旧版本模型、不同 imgsz、不同数据划分放在一起对比才有参考价值。我最早做麻将识别时把 27 类数牌合并成 3 类想省事,结果模型永远分不清 1条 和 2条,后来全部重标才救回来——类别边界是数据集的地基,偷懒的代价最后都会在 badcase 里还。希望帮到你。
本文还有配套的精品资源,点击获取