
简介面向中医舌诊、医学影像分析与目标检测研发提供八百张舌头照片及一一对应的标注文件覆盖bobai、fenhong、houbai、houhuang、huihei五个类别每张图像均同时给出Pascal VOC格式的XML与YOLO格式的TXT两套标注无需手动转换即可直接接入常见检测框架。压缩包共两千个文件其中包含八百零二个标签文件、八百个XML标注文件以及三百九十八张JPG图片总大小约二十九点八四兆字节文件夹按图像、标注、类别说明分层存放并附有类别与框数统计信息便于批量读取、划分训练集以及快速掌握标注分布。目前该数据集已有一千二百四十四人学习浏览可作为舌象识别或医学目标检测任务的训练集、验证集或测试集也可用于对比不同标注格式对模型效果的影响。全部矩形框均由标注工具人工绘制总计八百个标注框五个类别框数从一百四十七到一百七十四不等分布较为均衡双格式文件设计能帮助初学者省去格式转换步骤也让有经验的开发者可以更灵活地选择输入方式整体实用性强。1. 舌头舌像检测数据集800张五类双格式先对齐标注再谈训练中医舌诊的客观化第一步通常是让模型稳定地找到舌头在哪里。真做过这个方向的人多半会先卡在数据上通用检测数据集里没有舌头场景自己标又太费时间最需要的其实是那种解压就能直接进训练的格式包。这份“舌头舌像检测数据集VOCYOLO格式800张5类别”恰好就是这种形态800张舌像图片五个检测类别同时交付VOC XML和YOLO txt两套标注组织方式图片与标注文件名一一对应。解压之后用YOLOv5或v8都能直接消费训练出问题又能回头查XML原始坐标不用跟黑匣子死磕。适用对象有两类一类是把目标检测接进中医望诊项目的开发者另一类是刚上手YOLO、想拿着带标注数据跑通完整流程的入门者。800张对深度学习不算大但胜在标注齐、双格式、类别语义集中做预训练微调和流程验证非常合适。下面按拆包、格式对照、YOLOv8训练、踩坑、进阶验证的顺序把这份数据完整过一遍。2. 双格式包为什么容易用错VOC XML 与 YOLO txt 的字段映射和转换实战拿到压缩包的第一件事不是把图片丢进训练脚本而是先打开几个标注文件做对照。VOC XML是树形结构、存像素绝对坐标YOLO txt是纯文本、存归一化中心坐标。两者转换本身不复杂但翻车往往集中在文件名映射和 size 字段上。先把这一层搞清楚后面训练才稳。2.1 解包后的第一件事核对类别清单和文件名映射我一般用 7z 命令行解压不用图形工具方便直接看目录结构。7z x 舌头舌像检测数据集VOCYOLO格式800张5类别.7z -o./tongue_data find ./tongue_data -type d | head -207z x会保留压缩包内的完整目录层次-o指定解压目标目录。head -20只是先看前 20 个目录确认是不是预期的 images / labels 或 JPEGImages / Annotations 布局不需要一次把全部文件列出来刷屏。目录确认后还要做一件很多人跳过的事统计 XML 里到底出现了哪些类别名以及每个类别出现多少次。grep -o name.*/name ./tongue_data/Annotations/*.xml | sort | uniq -c这条命令把所有 XML 里的name标签内容提取出来sort | uniq -c计数。输出结果就是整个数据集的类别清单和数量分布。这一步之所以重要是因为 YOLO 训练时类别编号严格依赖names列表的顺序比如classes.txt里第一行对应编号 0第二行对应编号 1。如果数据集的五个类别是胖大舌、齿痕舌、裂纹舌、红舌、白苔那你的data.yaml就必须按这个顺序写不能自己重新排更不能按拼音排。排序错了训练不报错但预测结果全部错位这是最隐蔽的坑之一。2.2 VOC XML 与 YOLO txt 的字段逐项对应两份标注格式的核心差异先看一张对照表。含义VOC XML 标签YOLO txt 字段值域说明图片宽度size/width无做归一化分母像素值如 960图片高度size/height无做归一化分母像素值如 540类别object/name第一列类别编号必须是 names 列表中的下标框左上角 xbndbox/xmin参与 x_center 计算像素坐标单位 px框左上角 ybndbox/ymin参与 y_center 计算像素坐标单位 px框右下角 xbndbox/xmax参与框宽计算像素坐标单位 px框右下角 ybndbox/ymax参与框高计算像素坐标单位 px换算关系就是下面四个式子x_center ((xmin xmax) / 2) / image_widthy_center ((ymin ymax) / 2) / image_heightw (xmax - xmin) / image_widthh (ymax - ymin) / image_height注意分母用的是图片真实宽高不是 XML 里 size 节点写的值。大部分情况两者一致但偶尔会出现 XML 的 size 与 JPEG 实际尺寸不一致尤其是从不同标注工具导出、经过裁剪或二次压缩后。这种不一致造成的偏移肉眼很难发现只有可视化时才会显现所以转换脚本里我习惯直接用 OpenCV 读图片尺寸而不是信 size 节点。2.3 一个可以抄走的 VOC 转 YOLO 脚本以下是我日常处理这类舌头舌像数据集的转换脚本不依赖任何标注工具Python 标准库加 OpenCV 就够。import os import cv2 import xml.etree.ElementTree as ET # class_names 必须与最终 data.yaml 里的 names 顺序严格一致 class_names [胖大舌, 齿痕舌, 裂纹舌, 红舌, 白苔] def voc_to_yolo(xml_path, out_dir): tree ET.parse(xml_path) root tree.getroot() # 用 OpenCV 读实际图片尺寸避免 XML size 字段不准确 img_path os.path.splitext(xml_path)[0] .jpg if not os.path.exists(img_path): img_path img_path.replace(.jpg, .png) img cv2.imread(img_path) if img is None: print(fWARN: cannot read {img_path}, skip {xml_path}) return img_h, img_w img.shape[:2] lines [] for obj in root.findall(object): name obj.find(name).text if name not in class_names: print(fWARN: unknown class {name} in {xml_path}) continue cls_id class_names.index(name) box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) # 如果出现出界坐标做一次裁剪避免归一化后数值超出 0~1 xmin max(xmin, 0) ymin max(ymin, 0) xmax min(xmax, img_w) ymax min(ymax, img_h) x_center ((xmin xmax) / 2) / img_w y_center ((ymin ymax) / 2) / img_h w (xmax - xmin) / img_w h (ymax - ymin) / img_h lines.append(f{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}) if not lines: print(fWARN: no valid box in {xml_path}, do NOT write empty txt) return out_path os.path.join(out_dir, os.path.splitext(os.path.basename(xml_path))[0] .txt) with open(out_path, w) as f: f.write(\n.join(lines))代码逻辑不复杂但有三个细节值得说明。第一类别名不在class_names里时直接跳过而不是报错这样能容忍数据集里混入个别脏标注。第二坐标做了上下限裁剪xmax大于图宽时压回图宽防止归一化后出现大于 1 的数值YOLO 对这类越界框处理并不友好。第三没有有效框的 XML 不生成空 txt 文件因为空 txt 在训练阶段会被当成“无标签图片”导致大量报错日志刷屏。这个脚本我一般会配合一个简单的目录扫描循环使用把Annotations/下的所有 XML 逐一遍历输出到labels/对应目录。如果你拿到的数据已经是 YOLO txt 版本那这一步可以跳过但建议抽几个文件手动算一遍坐标是否符合上面公式确认没有转换端引入的脏数据。3. 处理数据集用于 YOLOv8 训练800 张五类舌像的目录拆分与完整跑通格式确认完就可以正式进入训练环节。这一章围绕 YOLOv8 展开步骤拆得比较细新手可以完全照做熟手可以直接跳去看参数选择逻辑。3.1 目录结构设计与预训练权重选择YOLOv8 对数据目录有约定但不强制最省心的方式是直接用 images / labels 两级结构。mkdir -p tongue_data/{images/{train,val},labels/{train,val}}把数据集里的图片和标注分别放进images/train、images/val、labels/train、labels/val四个目录。图片和标注文件名必须完全一致只是扩展名不同a01.jpg对a01.txt。train与val的划分建议按 7:3 或 8:2但这个数据集本身可能已经划分好解压后先确认有没有现成的 train / val 子目录如果有就别再自己切一遍。预训练权重方面我一般会用yolov8n.pt而不是更大的yolov8s.pt或yolov8l.pt。原因是 800 张图的数据量撑不起大模型n 版本参数量小收敛快在 GTX 1660 这类 6GB 显存卡上也能跑而且用大模型在这个规模下不一定能带来 mAP 提升反而更容易过拟合。ultralytics 库会在第一次执行时自动拉取对应权重文件你也可以手动把权重放到当前目录指定modelyolov8n.pt即可。3.2 数据 yaml 与训练超参数设置数据描述文件是 YOLOv8 训练的关键一个典型的舌像数据 yaml 长这样。path: ./tongue_data train: images/train val: images/val names: 0: 胖大舌 1: 齿痕舌 2: 裂纹舌 3: 红舌 4: 白苔path是数据集根目录train和val是相对path的图片目录。names的顺序必须和 2.3 节class_names列表完全一致这是整个流程里最容易出错的一环前面错一步后面全乱。训练命令我常用下面这一组参数。yolo detect train \ modelyolov8n.pt \ datatongue.yaml \ epochs120 \ batch16 \ imgsz640 \ workers4 \ patience20 \ cacheTrue参数含义逐个说明。epochs120是针对 800 张图的经验值太短模型没收敛太长浪费时间反正有早停机制兜底。batch16需要看显存6GB 卡跑 640 分辨率时 16 是安全的如果显存只有 4GB降到 8。imgsz640是 YOLOv8 默认训练尺寸舌像数据里舌头通常占据画面主体不太需要切成 1280 大图去抠小目标640 性价比最高。patience20表示连续 20 个 epoch 验证集指标不提升就自动停止。cacheTrue把图片缓存到内存800 张图完全放得下能明显加快训练迭代速度。3.3 训练日志里的关键指标怎么看训练过程中终端会实时输出 box_loss、cls_loss 和各类 mAP 指标。对舌像检测来说我一般重点关注三处。第一是 box_loss 和 cls_loss 的下降曲线。正常情况两者在前 30 个 epoch 快速下降之后平稳波动。如果 box_loss 在某一步突然反弹并持续走高说明学习率可能偏大或者数据里存在标注坐标严重错误的框。第二是 mAP50 的收敛值。舌头检测框本身的形状主观性较强不同标注者对“舌头边界”的理解会有差异mAP50 达到 0.90 以上而 mAP50-95 只有 0.6 左右在舌象场景里是正常的不必强求高 IoU 下的指标。mAP50-95 计算的是从 0.5 到 0.95 多个 IoU 阈值的平均舌头边缘的框难以做到像素级精确所以这个值天然会被拉低。第三是 train 和 val 的 loss 差值。如果 train loss 一路下降但 val loss 在某个 epoch 后开始回升就是过拟合信号这时候可以提前终止训练增加数据增强强度或者换更小的模型。800 张图训练 120 轮过拟合风险真实存在我会结合 4.4 节的做法提前做干预。3.4 用最佳权重做推理验证训练结束后runs/detect/train/weights/下会有best.pt和last.pt。前者是验证集指标最好的权重后者是最后一个 epoch 的权重。部署和后续实验都用best.pt。from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) results model.predict( sourcetongue_data/images/val, conf0.25, iou0.5, saveTrue, save_txtTrue )conf是置信度阈值低于这个值的目标会被丢弃。舌像场景我建议先用 0.25 跑一遍看输出图里是不是漏掉了边缘较暗的舌头如果漏了再降到 0.15。iou0.5是 NMS 的 IoU 阈值两个重叠框的交并比超过 0.5 时只保留高置信度的那个。save_txtTrue会把检测结果同时存成 txt 文件方便和标注文件做对比看模型倾向在哪一类上误检。这里的核心技巧是每跑一个新数据集不要只盯着终端里那个 mAP 数字必须抽至少 20 张验证集图片看可视化结果。因为舌像的检测框和自然物体不一样模型可能在指标上表现很好但框的位置整体偏左或偏右这种系统性偏差在 mAP 里不容易暴露眼睛一扫就能看出来。4. 跑舌像检测最容易翻车的五个问题现象、原因与排查方法800 张数据集的规模不大训练本身不会太慢但真正消耗时间的经常是各种奇怪的报错和指标异常。这一章整理五条高频踩坑记录每一条都是按“现象 → 原因 → 解决”的顺序来写。4.1 训练开始就提示 labels 找不到或验证集 mAP 恒为 0现象训练刚启动终端大量输出类似image 001.jpg: found no labels的警告跑完 120 个 epoch验证集 mAP 始终是 0。原因最常见的有三种。一是labels/目录下缺少与图片同名的 txt 文件二是 txt 第一列的类别编号超过了names里定义的类别数量比如names写了 5 类但有 txt 文件里出现了编号 6三是 txt 文件本身是空文件或某一行只有一个数字没有完整的五个字段。解决先扫空白文件再检查编号越界。find labels -name *.txt -size 0 -print这条命令把所有 0 字节的 txt 文件列出来空标签文件直接删掉或对应到 XML 重新生成。编号越界和字段缺失用下面的 Python 片段检查。with open(labels/train/001.txt, r) as f: for i, line in enumerate(f): parts line.strip().split() if len(parts) ! 5: print(fbad line {i1}: {line})养成习惯很重要在训练前先跑这两段检查能省掉一整轮调试时间。4.2 从 VOC 转 YOLO 后所有框偏移到图像角落现象用 2.3 节脚本转换后挑了几张图可视化发现检测框全部跑到图像边缘或者框的长宽比例明显不对像是被压缩过。原因最常见的是 XML 的size节点和图片实际尺寸不一致。很多标注工具在导出时会写一个固定的宽高比如统一写 640×640但实际图片可能是 960×540。如果转换脚本用size做分母归一化坐标就会整体错位。解决这也是我在脚本里坚持用 OpenCV 读真实尺寸的原因。遇到这种情况先取一张图片用下面命令确认实际尺寸。import cv2 img cv2.imread(a01.jpg) print(img.shape) # 输出 (height, width, channels)对比 XML 里的size/width和size/height不一致就统一以实际尺寸为准重新转换。另外如果图片的拍摄设备存在 EXIF 旋转信息也可能出现宽高互换的问题这时候要在读取时先按cv2.ORIENTATION校正方向再做尺寸判断。4.3 少数类别总是检不出或 mAP 为 0现象五个类别里三个正常两个的精确率和召回率始终很低甚至 mAP 为 0单独看这两个类别的图片模型完全没有输出框。原因类别分布不均衡。舌像数据集中类别框数量差异较大比如“白苔”只有几十个样本“胖大舌”有两百多个模型天然会更倾向于学习样本量大的类别。YOLOv8 的损失函数虽然有类别权重但默认配置不会单独偏向少数类。解决我一般先做一次类别统计确认差异幅度。模型层面可以给少数类别提高存在感常见做法是用图像过采样复制也就是把包含少数类的图片在训练集里多放几份。但注意不要简单把同一张图片复制多遍最好配合 5.2 节的增强一起做否则容易导致模型对这几张图过拟合。另外训练时可以把cls损失权重从 0.5 调到 0.7让模型更重视分类分支。这里的血泪经验是不要一上来就调 loss先看清楚少数类到底是“质量差”还是“数量少”如果是质量差过采样只会放大错误标注的影响。4.4 训练中 BN 崩溃导致 loss 跳成 NaN现象训练到某个 epochbox_loss 突然变成nan后续所有指标都跟着变成nan重启训练又可能在更早或更晚的 epoch 复现。原因这是 YOLO 训练中 BN 崩溃的典型表现。常见诱发条件是学习率过大加上 batch size 过小batch normalization 的统计量在梯度更新幅度过大时失去稳定性数值溢出。在自定义数据集上如果使用了预训练权重但没冻结骨干网络初始学习率还保持 ultralytics 默认的 0.01遇到小 batch 时更容易触发。解决先确认是不是学习率问题把初始学习率降到 0.001 试一轮再看 batch size 是否小于 8。ultralytics 官方建议 batch size 不低于 16 时 BN 才稳定如果显存不够可以先降 imgsz 到 480 而不是降 batch。还有一个更直接的做法是换用yolov8n.yaml从零训练不加载预训练权重虽然收敛慢一些但能跳过预训练权重 BN 统计量与当前数据分布冲突的环节。这条问题是我自己被坑过两次之后才总结出来的最初一直怀疑是数据集标注有问题其实问题在优化器。4.5 验证集效果不错但换到现场拍摄的照片整体拉胯现象训练时 val 指标看着正常把模型换到手机或现场摄像头拍摄的新照片上检测框要么漏掉要么置信度极低看起来模型像是白训了。原因领域偏移。数据集的 800 张图可能来自特定拍摄设备、固定光照和固定姿势现场照片的色温、亮度、舌体占比都有差异。中医舌象尤其敏感白平衡不同会让同一舌头的颜色在 RGB 空间里差别很大而颜色正是模型区分类别的重要依据。解决部署侧尽量把输入图像的采集条件统一。第一步是定义统一的前处理无论原图多大先等比缩放后填充到 640×640不要直接拉伸。第二步是加白平衡归一化用灰度世界算法把RGB 三个通道的均值拉平这在舌象场景里比任何深度学习颜色校正都稳定。第三步关闭手机的自动美颜和锐化滤镜这类处理会改变舌体边缘纹理对检测框精度影响很大。这套做法不改变模型本身但能明显改善跨设备的泛化属于投入产出比非常高的部署工程处理。5. 800 张数据也能继续压出价值类别分布统计与舌像专属增强数据量固定在 800 张不太可能靠堆数据提升效果但有两件事能把数据集的利用效率拉高一截训练前做数据体检训练中用适合舌象语义的增强方式。5.1 先统计类别框分布拒绝盲训import os from collections import Counter class_names [胖大舌, 齿痕舌, 裂纹舌, 红舌, 白苔] box_counts Counter() for root, _, files in os.walk(labels): for f in files: if not f.endswith(.txt): continue path os.path.join(root, f) if os.path.getsize(path) 0: print(fempty label: {path}) continue with open(path) as fp: for line in fp: parts line.strip().split() if len(parts) ! 5: print(fbad line in {path}: {line}) continue box_counts[int(parts[0])] 1 for cls_id in range(len(class_names)): print(f{class_names[cls_id]}: {box_counts[cls_id]})这段脚本把每个类别的框总数统计出来同时标记空标签和格式异常行。看输出时重点关注两点最低频类别与最高频类别的比例如果是 1:10 以上就需要考虑过采样另外空标签数量如果超过 20 个说明标注过程存在漏标最好是回原图检查而不是直接删掉。5.2 舌象增强要保护颜色语义通用目标检测的随机增强套件在舌象上不能直接用尤其是 HSV 扰动幅度大了会把“红舌”变成“淡红舌”“白苔”变成“黄苔”把训练数据的语义直接改歪。我常用的增强组合是这样的。import albumentations as A transform A.Compose([ A.HueSaturationValue( hue_shift_limit5, sat_shift_limit10, val_shift_limit20, p0.5 ), A.RandomBrightnessContrast( brightness_limit0.1, contrast_limit0.1, p0.5 ), A.RandomSizedBBoxSafeCrop( width512, height512, erosion_rate0.2, p0.3 ), ], bbox_paramsA.BboxParams(formatyolo, label_fields[class_labels]))HueSaturationValue的hue_shift_limit控制在 5 而不是通用增强里常见的 30因为色相直接对应舌象类别小幅扰动可以让模型对光照色温更鲁棒大幅扰动则会摧毁类别边界。RandomSizedBBoxSafeCrop做的是带框安全裁剪裁剪过程保证所有已有的标注框不被切坏这比普通的随机裁剪对检测任务友好得多。5.3 用置信度扫描确定部署阈值而不是一直用默认值很多人在部署时直接用训练时的 conf0.25但验证集最优阈值和实际部署最优阈值往往不是同一个值。我习惯在验证集上做一个简单的阈值扫描。from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) for conf in [0.15, 0.25, 0.35, 0.5]: metrics model.val(confconf, iou0.5) print(fconf{conf} mAP50{metrics.box.map:.4f})输出后对比不同置信度下的 mAP50选择一个在保持召回率的前提下让误检最少的阈值。同时配合查看验证生成的混淆矩阵确认错检主要发生在哪些类别对之间比如白苔被误判成红舌就说明颜色特征在训练集里区分度不够这时优先检查 5.2 节的色相增强幅度是否仍然偏大或者是否需要增加对应类别的样本。从那以后我每次拆开一份新的舌像数据集都会先强制走一遍“类别统计 → 空标签扫描 → 置信度扫描”这套流程再决定怎么训练而不是拿到手就全量跑。800 张图看起来不多但只要格式对齐、训练参数合理、增强方式符合舌象语义训练出来的模型在目标场景里完全够用。希望帮到你。本文还有配套的精品资源点击获取