
简介一套面向舌象智能分析的目标检测数据集提供800个VOC格式xml标注与800个YOLO格式txt标注适合中医舌诊研究、目标检测算法训练和迁移学习等方向。压缩包共2000个文件主要包含jpg图片、xml标注与txt标签三类文件整体大小约29.84MB文件命名规范、对应关系清晰解压后即可接入主流检测框架。目前已有1244人学习/下载适合计算机视觉方向的学生、研究者和算法工程师作为基础训练数据。标注覆盖bobai、fenhong、houbai、houhuang、huihei五类舌象共计800个矩形框使用labelImg工具统一绘制可显著节省数据准备成本方便快速开展舌象检测、分类与模型效果对比实验。1. 舌头舌像检测数据集是什么800张图训模型够不够用拿到一份“舌头舌像检测数据集VOCYOLO格式800张5类别.7z”先别急着解压。800张图对目标检测来说是个很敏感的数字COCO级别的数据集动辄几十万张800张连零头都不到但舌像检测有它的特殊性——背景相对干净目标就是舌头本身类别差异集中在舌质和舌苔的纹理特征上800张图如果标注质量在线配合YOLOv8预训练权重完全足够把中医舌诊识别原型跑起来。这篇笔记按我自己的落地顺序来讲先拆格式再校验标注然后训练最后把参数和坑讲透。适合谁想做中医舌诊AI的工程师、目标检测入门者以及正在找可直接训练数据集做毕业设计或产品原型的同学。2. VOC与YOLO双格式同一个数据集为什么给两份标注很多刚入门的人看到“VOCYOLO格式”会愣一下心想一份数据给一份标注不就行了为什么搞两套其实这是数据集分发时的常见做法。VOC格式PASCAL VOC用XML存每张图的目标框和类别适合用mmdetection、Detectron2这类框架直接加载也方便做人眼可读的标注检查YOLO格式用txt存归一化坐标是ultralytics系列YOLOv5/v8/v11开箱即用的格式。同一个标注内容用两种格式表达为的是拿到手就能在主流框架里跑不用再写转换脚本。但反过来说凡是“转换过的”标注就有转错的风险所以我拿到双格式数据集第一步永远是校验两份标注是否对得上。2.1 VOC格式的XML里到底存了什么VOC格式的核心是一个与图片同名的XML文件。舌像检测数据集里每个XML通常长这样annotation filenametongue_001.jpg/filename size width640/width height480/height depth3/depth /size object nameteeth_marked/name bndbox xmin120/xmin ymin95/ymin xmax420/xmax ymax380/ymax /bndbox /object /annotationXML里最关键的信息是size和bndbox。size保存的是图片真实宽高bndbox存的是目标的像素坐标左上角xmin/ymin右下角xmax/ymax。很多舌像数据集的标注习惯是“把整条舌头标成一个框”这对检测舌头位置没问题但如果类别里包含齿痕、裂纹这类局部特征整舌框会把大量无关纹理包进去模型学到的是“舌头区域长什么样”而不是“齿痕和裂纹长什么样”。这一点在训练前就要想清楚后面第5章我会专门讲这个坑。还要注意filename节点。我见过不少数据集的XML里写的是tongue_001.jpg但实际文件名是tongue_001.JPG或001_tongue.jpg大小写、命名对不上训练时FileNotFoundError一个接一个。VOC转YOLO时如果按文件名匹配不到XML脚本会静默跳过最后你会发现图片有800张、标注只有780份数据悄悄缩水了。2.2 YOLO格式的txt标注为什么必须归一化YOLO格式的标注是纯文本每行一个目标格式固定为五段class_id x_center y_center width height# 每行前两个是类别和中心点后两个是宽高全部除以图片宽高归一化 # 0 0.421875 0.494792 0.468750 0.593750这里的x_center、y_center是 bndbox 中心点除以图片宽高得到的 0~1 小数width、height也是归一化后的相对宽度。为什么必须归一化因为目标检测网络在训练时会做多尺度缩放把输入统一缩放到640或960如果标注存的是像素值一旦图片缩放坐标全都要跟着算一遍容易出错也容易溢出归一化之后坐标只与目标的相对位置和大小有关训练时无论输入尺寸怎么变标注都天然有效。从VOC的XML转成YOLO格式核心逻辑是import xml.etree.ElementTree as ET # 转换前先定义好类别映射class_names的顺序就是YOLO格式里的类别编号 class_names [light_red, red, pale, teeth_marked, cracked] def voc_to_yolo(xml_path, output_txt): tree ET.parse(xml_path) root tree.getroot() # 注意这里要以XML里的size为准但如果XML的size和图片实际分辨率不一致要先用cv2读图修正 size root.find(size) width int(size.find(width).text) height int(size.find(height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in class_names: continue cls_id class_names.index(name) 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) # VOC存的是左上角和右下角YOLO要的是中心点和宽高 x_center (xmin xmax) / 2.0 / width y_center (ymin ymax) / 2.0 / height box_w (xmax - xmin) / width box_h (ymax - ymin) / height lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) with open(output_txt, w, encodingutf-8) as f: f.write(\n.join(lines))这个脚本有两个容易翻车的地方。第一class_names的顺序必须和YOLO训练时的data.yaml里的names一一对应顺序错一个所有类别全错位模型还能训练但推理出来的类别全是错的。第二XML里的size如果和图片实际分辨率不一致比如XML写的是640图片实际是800x600转换出来的归一化坐标全偏训练时框的位置全对不上。所以转完一定要抽样可视化几张贴出来看别只看数值。2.3 双格式同步校验我如何判断两份标注没对错位既然VOC和YOLO是同一份标注的两种表达那它们就必须严格一致类别数量一致、框的数量一致、坐标换算后一致。我一般会先跑一个快速的对齐脚本import os import xml.etree.ElementTree as ET voc_dir Annotations # VOC格式XML目录 yolo_txt_dir labels # YOLO格式txt目录 img_dir JPEGImages # 图片目录 voc_objs {} for xml_name in os.listdir(voc_dir): if not xml_name.endswith(.xml): continue tree ET.parse(os.path.join(voc_dir, xml_name)) root tree.getroot() objs root.findall(object) stem xml_name[:-4] voc_objs[stem] len(objs) yolo_objs {} for txt_name in os.listdir(yolo_txt_dir): if not txt_name.endswith(.txt): continue with open(os.path.join(yolo_txt_dir, txt_name), r, encodingutf-8) as f: content f.read().strip() box_count len(content.splitlines()) if content else 0 stem txt_name[:-4] yolo_objs[stem] box_count # 一一比对各图片的标注数出现不一致说明其中一份转换时丢框了 for stem in voc_objs: if stem not in yolo_objs: print(f[缺失] {stem}: 有VOC标注但没有YOLO标注) elif voc_objs[stem] ! yolo_objs[stem]: print(f[不一致] {stem}: VOC{voc_objs[stem]}个框, YOLO{yolo_objs[stem]}个框, 图片{stem}.jpg)跑完这个脚本只要输出为空说明两份标注的“数量”对得上。但数量对得上不代表坐标对得上我再抽取五到十张图把VOC的XML转绘成框把YOLO的txt也绘制成框叠在同一张图上肉眼比对。实操里最常见的偏差是小数点精度不同导致框边缘差几个像素这可以接受但如果框的位置明显偏移那基本是转换脚本里分辨率取错了得回到2.2去查。3. 从.7z压缩包到可训练数据集解包、校验与真值核对拿到压缩包解压这步看似简单其实暗藏不少坑。.7z格式在Windows下可以用7-Zip直接解Linux下如果没有装p7zip光靠系统自带的归档管理器会解压失败。我一般直接在命令行里操作避免图形界面解压到一半没反应的尴尬。3.1 解包后的目录结构长什么样# 先测试压缩包完整性再解压顺序别反 7z t 舌头舌像检测数据集VOCYOLO格式800张5类别.7z 7z x 舌头舌像检测数据集VOCYOLO格式800张5类别.7z -o./tongue_dataset7z t是测试压缩包是否有损坏-o后面跟的是输出目录注意-o和目录路径之间没有空格这是7z命令的一个反直觉之处写错了会被当成压缩包名称。解压完第一件事是数文件800张图的资料包图片目录里就应该是800个图片文件少一个多一个都要追查。我见过的大多数双格式舌像数据集解压后是下面这种组织方式tongue_dataset/ ├── Annotations/ # VOC格式XML标注 ├── JPEGImages/ # 原始图片 ├── ImageSets/ │ └── Main/ │ ├── train.txt # 训练集文件名列表 │ └── val.txt # 验证集文件名列表 ├── images/ │ ├── train/ # YOLO格式训练图片可能是JPEGImages的拷贝或软链 │ └── val/ ├── labels/ │ ├── train/ # YOLO格式txt标注 │ └── val/ ├── classes.txt # 类别列表一行一个 └── data.yaml # 有些作者会直接附上YOLO训练配置注意一个细节annotations/和images/往往只是目录名字不同内容可能完全重复。有些作者为了省空间只放一份图片images/train下是空的或只有软链接实际靠ImageSets/Main/train.txt来划分训练验证。如果解包后发现images目录是空的别慌看ImageSets和JPEGImages就够了。3.2 用Python脚本按类别统计5个类别到底各自有多少训练之前必须做一次类别分布统计。800张图听起来不少但如果5个类别里某个类只有30张框那这个类大概率训练不好。统计脚本import os from collections import defaultdict label_dir labels/train # 换成实际标注目录 # key为类别编号value是[框数量, 出现该框的图片集合] stats defaultdict(lambda: [0, set()]) for fname in os.listdir(label_dir): if not fname.endswith(.txt): continue img_name fname[:-4] with open(os.path.join(label_dir, fname), r, encodingutf-8) as f: lines f.read().strip().splitlines() for idx, line in enumerate(lines, 1): parts line.split() if len(parts) 5: print(f{fname} 第{idx}行格式不完整: {line}) continue try: cls int(parts[0]) except ValueError: print(f{fname} 类别编号不是整数: {parts[0]}) continue stats[cls][0] 1 stats[cls][1].add(img_name) for cls in sorted(stats.keys()): cnt, imgs stats[cls] print(fclass {cls}: {cnt} 个框, 分布在 {len(imgs)} 张图中)这个脚本同时会暴露格式问题某一行不是5个数字、类别编号出现6或负数、文件为空导致splitlines()返回空列表。类别统计的价值在于确定训练策略——如果某个类样本明显偏少后面训练时要考虑类别加权或数据增强而不是无脑开训。3.3 最小可用校验图片可读性检查与划分文件核对解压包里的图片不一定每一张都能用。训练中最讨厌的报错是训练到一半某张图损坏DataLoader直接崩掉。我一般先跑一遍OpenCV可用性检查import os import cv2 img_dir JPEGImages bad_images [] for fname in os.listdir(img_dir): if not fname.lower().endswith((.jpg, .jpeg, .png, .bmp, .webp)): continue img_path os.path.join(img_dir, fname) img cv2.imread(img_path) if img is None: bad_images.append(fname) print(f读取失败图片数: {len(bad_images)}) for name in bad_images: print(name)cv2.imread返回None的原因通常是图片文件损坏、后缀与真实编码不符或者文件名带中文在Linux下读取失败。舌像数据集如果来源是手机拍摄或扫描仪偶尔会有奇怪的色域和损坏图这个脚本能帮你提前清理掉。之后核对ImageSets/Main/train.txt和val.txt每一行是否对应真实存在的图片划分出的图片数和标注数是否匹配。更稳妥的做法是丢掉作者给的划分自己按8:2重新划分。因为800张小数据集如果作者划分时没做类别均衡验证集里可能整个类别只有一两张图训练时验证mAP忽高忽低你都不知道是模型问题还是验证集问题。4. 用这800张图训练YOLOv8参数设置与训练验证数据集校验完毕接下来是训练阶段。主流做法是用YOLOv8或YOLOv5我习惯用ultralytics的YOLOv8配置简单日志清晰对小数据集也友好。有意思的是网上搜“yolov8训练自己的数据集”能找到一堆教程但大部分教程用的是几百张自拍图跑个demo参数全是默认能出图但精度很虚。4.1 data.yaml怎么写类别顺序决定一切ultralytics框架通过一个data.yaml文件描述数据集里面的类别顺序必须和标注文件里的class_id完全一致否则全盘错乱。# data.yaml 示例 # path用绝对路径或相对路径都行但绝对路径最省心 path: /home/user/tongue_dataset train: images/train val: images/val names: 0: light_red 1: red 2: pale 3: teeth_marked 4: cracked上面的类别名是示例不同数据集的实际类别可能是“淡红舌、红舌、淡白舌、齿痕舌、裂纹舌”也可能是“苔色、苔质、舌色、胖瘦、齿痕”写yaml前先打开classes.txt或随便挑几个XML看name节点把这些名字按顺序填进names。注意names的编号从0开始列表顺序不能改哪怕把cracked写在前面让后续类别编号全变标注里的class_id也得跟着改。如果解压出来没有images/train和images/val目录只有JPEGImages和ImageSets/Main/train.txt就需要先自己划分。常见做法是把JPEGImages按文件清单分成两个目录或者直接在yaml里写训练图片目录和验证图片目录指向同一个JPEGImages然后用train.txt和val.txt控制参与训练的文件列表。ultralytics支持train: images/这种目录写法也支持传一个txt路径但txt里必须是图片路径用起来没有目录直观我一般还是先划分目录再写yaml。4.2 训练命令与关键参数说明yolo detect train \ data/home/user/tongue_dataset/data.yaml \ modelyolov8m.pt \ epochs100 \ imgsz640 \ batch16 \ patience20 \ lr00.01 \ freeze10 \ project./runs/tongue \ nameexp_01 \ seed42逐项说明我的考虑modelyolov8m.pt从COCO预训练权重开始。800张自定义数据冷启动几乎不可能训好必须用预训练权重。我用m而不是n或s是因为舌象的类别差异很多是细微纹理n模型容量太小特征提取容易丢细节。如果显存小s也可以但别用n做主力训练。epochs100800张小数据集50轮太少200轮以上又容易过拟合。100轮是比较稳的中间值配合早停让模型自己决定什么时候停在最优。patience20验证集mAP连续20轮没有提升就停止。小数据集早停经常在60~80轮触发这是正常的别觉得没跑满100轮就亏了。freeze10冻结主干前10层的参数。800张图的数据量撑不起全网络大范围微调冻结主干让模型只调整检测头和高层特征训练更稳定。v100一类的卡跑m模型很快但小数据集上快慢不是主要矛盾精度稳定才是。seed42固定随机种子。小数据集训练本身就玄学不固定种子两次训练结果能差出两三个点mAP固定了至少能复现。4.3 必调的5个训练参数速查参数建议值说明epochs80~120少于50轮欠拟合超过200轮小数据极易过拟合imgsz640训练960验证齿痕、裂纹是小目标验证用960能看得更清楚batch8~32800张图不需要大batch16足够显存小的用8lr00.005~0.01预训练模型建议别用默认0.01以上容易在初期震荡freeze10~12冻结主干前若干层实测小数据集稳定性明显提升参数里最容易被忽视的是imgsz训练和验证不一致这个技巧。训练时用640batch能开大一点模型收敛快验证时用960mAP会比分640高一点因为小目标在960下更容易被检出。如果最终部署算力紧张那就以640为准验证也用640别自己骗自己。4.4 训练结果怎么看PR曲线、混淆矩阵与yolo损失函数训练完不要只看最后的mAP把runs/tongue/exp_01/里的PR_curve.png和confusion_matrix.png拉出来看。PR曲线如果整体向右上角凸起说明各个类别的查准率和查全率都不错如果曲线在中间凹进去说明某个类别查得准就漏检查得全就误检。混淆矩阵要特别注意一个问题——网上搜“yolo混淆矩阵总合不唯一”会发现很多人在问为什么矩阵每一行加起来不是100%。这是正常的因为ultralytics的混淆矩阵里多了一个background行还被做了类别归一化行和可能大于或小于100%别对着矩阵怀疑自己训练坏了。真正要看的是对角线以外的格子比如cracked被大量识别成light_red说明模型没有抓到裂纹的纹理特征只学会了舌色。训练日志里还有一类信息值得追box_loss、cls_loss、dfl_loss三条损失曲线。yolo的损失函数由这三块组成box损失管框的位置精度cls损失管分类dfl损失管框的边缘锐利度。小数据集常见问题是cls_loss降得很快box_loss一直在高位震荡这说明数据里框的标注质量参差不齐或者存在2.2节说的坐标错位问题回到数据校验环节查不要盲目加训练轮数。5. 避坑指南小样本舌像检测的5个真实踩坑记录800张的数据集属于典型的小样本场景训练过程中的许多问题在十万张级数据集上不会暴露在小数据集上却会被无限放大。以下是我在这个数据量级上反复踩过的5个坑按“现象→原因→解决”写清楚。5.1 类别不平衡齿痕舌只有40个框训练后几乎不预测这类现象训练结束后各类别的验证结果里某个类别的precision和recall全是0甚至confusion矩阵里整行整列为0模型完全没学到这一类。原因5个类别里如果某一个或两个类别的框数明显少比如其他类各有200个框齿痕类只有40个框模型在训练时会把大部分学习容量分配给多数类少数类即使训练了置信度也一直被抑制在一个很低的水平。解决先看类别统计结果再决定要不要做类别加权。ultralytics自带的loss_scale对少量类别不敏感我一般先尝试最简单的过采样——把少数类的图片在训练目录里复制两到三份注意改文件名让DataLoader多看到几回。另一种做法是训练命令里不配额外权重而是把增强参数调猛一点给少数类多做随机旋转、缩放、局部遮挡。如果这些都不行找工具把舌像中齿痕、裂纹的局部区域裁出来额外做一张“局部特征图”扩充进数据集比硬调loss权重更有效。5.2 舌体标注框太大mAP50虚高但框完全不能用现象验证集mAP50到了0.85看起来相当漂亮但实际推理时模型输出的框总是把整条舌头包住甚至把嘴唇也框进来。对于“识别有没有齿痕”这个需求这种大框几乎没有用处。原因标注时标注员把整舌的外包围框当成了目标的bbox。齿痕、裂纹只占舌体边缘很小一部分但标注框覆盖了整个舌面模型需要预测的是一个“舌头框”而不是“齿痕区域框”。大框的IoU天然容易高mAP50虚高掩盖了模型根本没有学到局部特征的事实。解决打开每个XML框和图片叠加检查框的标注粒度。如果类别是齿痕、裂纹这类局部特征框应该紧贴特征区域而不是套在整舌上。如果数据集实在没法重新标注就在训练时把这类局部特征的框裁出来生成一组“局部特征裁剪图”单独作为一个小尺度训练集和整舌检测并行。这里没有后悔药标注粒度不改正后面所有优化都白费。5.3 训练时突然出现lossNaN或边界框坐标全越界现象训练到第20轮左右cls_loss突然跳成NaN或者训练能跑完但验证时很多框的中心点落在图片外、宽高超过1输出的检测框是个占据整张图的大色块。原因VOC转YOLO格式时归一化后的坐标超出了0~1的有效范围。最常见的是转换脚本里(xmax - xmin) / width在xmax大于width时没有做裁剪或者XML中size节点的尺寸和真实图片分辨率不一致导致除法用错了分母。解决写一个坐标合法性校验把越界的box值拉回有效范围import numpy as np def clamp_yolo_bbox(cx, cy, w, h, eps1e-4): # 先把四个值全部夹到0~1 cx float(np.clip(cx, 0.0, 1.0)) cy float(np.clip(cy, 0.0, 1.0)) w float(np.clip(w, eps, 1.0)) h float(np.clip(h, eps, 1.0)) # 宽度接近1说明框几乎占满整张图多半是坐标公式错误不是clip能解决的 if w 0.95 or h 0.95: print(f警告: 框宽高异常, cx{cx:.4f} cy{cy:.4f} w{w:.4f} h{h:.4f}) # 保证中心点不因为宽高过大而被挤出边界 cx min(max(cx, w / 2.0), 1.0 - w / 2.0) cy min(max(cy, h / 2.0), 1.0 - h / 2.0) return cx, cy, w, h这段代码的逻辑是先用np.clip把越界数值拉回0~1再根据框的宽高约束中心点的位置避免出现中心点在0.9、宽度0.8这种框体一大半在画面外的情况。如果clip完成后仍然有大量框w或h超过0.95基本可以断定转换脚本里分辨率取错或标注本身有问题此时应回到VOC源文件核查原始坐标。5.4 验证集mAP高但实际单张推理时框乱跳现象验证集mAP50有0.82但把一张训练时没见过的舌像图喂进去连续推理几次框的位置每次都有偏移置信度在0.3上下浮动看起来像是模型没学过这张图的特征。原因这是小数据集的典型翻车现场。验证集mAP高是因为验证集和训练集来自同一个数据分布模型在训练时已经“见过”类似样本而实际舌像图的拍摄条件光源、镜头距离、曝光和数据集里的样本差异往往很大模型泛化能力不足。另一个原因是训练时开的mosaic增强在小数据集上会让模型看到大量“拼接图”真实单图的分布反而学得不稳。解决推理时做多尺度测试把imgsz从640提到960甚至1280有时候框会稳定很多。同时检查训练配置里mosaic1.0是否开得太大小数据集建议把mosaic降到0.5或者在前10轮关闭mosaic只做平移、缩放、翻转等轻量增强让模型先把真实分布学个大概再用mosaic做泛化。如果推理现场还是晃动就把conf阈值从默认0.25提高到0.5宁可漏检也不输出一堆摇摆框。5.5 .7z解压到一半报错图片数和标注数对不上现象7z x解压到80%时突然报错或者解压成功但统计图片发现只有773张少了27张。原因下载的压缩包不完整或是传输过程中文件损坏。7z格式虽然自带校验信息但很多下载工具不会在下载完成后自动校验看起来文件大小正常解压到某个文件时就露馅了。解决养成先7z t再解压的习惯。如果7z t报错重新下载如果反复下载都报同样的错误检查磁盘剩余空间和内存。解压完成后用3.2节的统计脚本核对图片数是否等于800同时比对JPEGImages里的文件名和Annotations里的XML文件名两边数量一致才能进入训练。这一步花不了两分钟但能避免后面所有踩坑。6. 让800张图发挥1200张的价值预训练权重、增强与半自动标注技巧800张图做到这一步模型基本能出活了。但如果想让mAP再涨几个点方向不是继续加大训练轮数而是从数据本身做文章。先利用预训练权重做一次半自动标注。用当前训练好的模型去跑一批不在数据集中、但同样是舌象特写的图片会得到一批带置信度的预测框。筛选出置信度高于0.6且框大小合理的预测人工快速修正后并入数据集。这种“模型辅助标注”的方式在舌象这类目标集中、背景相对简单的任务上效果很好单张修正时间可以控制在十秒内。注意新并入的图片在色调、光照上要和原数据集尽量一致如果差异太大模型会学到“新背景新类别”这种错误关联。其次是增强策略的精细化调整。舌像有一个天然特性——左右翻转不会改变任何类别语义齿痕在左边还是右边裂纹在舌尖还是舌根类别不变。所以我可以把flipud上下翻转关掉而把fliplr左右翻转开满前后对比能稳定提升一到两个点的mAP。同理hsv增强里的hsv_h色相对舌色识别是伤敌一千自损八百舌质的淡红、红、淡白差异正是靠色相区分把hsv_h调低到0.01以下只保留少量饱和度扰动模型对舌色的判断会明显更有区分度。最后是一个不算技巧的技巧验证集不要用作者原来划分好的那几份。800张图的随机划分方差很大我通常按类别做分层划分——每个类别至少保证验证集里有10%的样本然后固定下来写进yaml以后每次训练都用同一份划分对比实验。我现在拿到任何数据包第一件事永远是跑清单和校验脚本而不是急着开训这个习惯帮我躲开了好多次标注错位导致的无效训练。800张舌像数据集不算大但把格式、校验、参数、标注粒度都处理好足够撑起一个能演示、能测试、能继续迭代的舌诊AI原型希望帮到你。本文还有配套的精品资源点击获取