简介:面向舌头目标检测与舌象分析场景,这份数据集汇集了8804张舌头检测图片,标签类别为shetou,并同时提供YOLO与VOC两种标注格式,矩形框标注清晰,标注框总数为8819个。压缩包按JPEGImages、Annotations、labels三个目录组织,分别存放图像、xml文件与txt文件,方便直接接入YOLO、SSD等常见检测框架;包体约211.79MB,文件列表共2000个,以xml标注和txt说明为主要文件类型,整体结构简单。图片分辨率清晰,标注覆盖不同舌头形态,适合作为医疗影像辅助诊断、舌诊智能化等项目的训练或微调数据,也可按目录直接划分训练集与验证集。目前已有537人学习下载,可帮助算法工程师、科研人员省去数据收集与格式转换的重复工作,直接用于模型训练与效果验证。
1. 拿舌头数据集训YOLO:8804张增强图到底能干什么
做目标检测的人最烦的一件事不是调参,是找数据。尤其舌头这种部位,公开数据集少得可怜,Medical Segmentation 那种标注颗粒度又太粗,真要拿来训 YOLO 检测舌头位置,标完一轮眼睛都快瞎了。这份舌头数据集是 YOLO + VOC 双格式,8804 张已增强图像,解压之后直接能喂给训练脚本,不用自己写转换,也不用蹲在标注软件里一框一框画。它的使用场景很明确:舌头检测、舌象分析、中医数字化诊断的前置环节,比如从一张口腔照片里先定位舌头区域,再做颜色、裂纹、齿痕的后续分类。适合正在做医疗影像检测、健康管理 App 原型验证,或者毕业论文需要自定义数据集的从业者和学生。它解决的核心问题,是把从零标数据的那一两周时间直接抹掉,让你能立刻走到训练和调优这一步。
2. 先看清数据格式:VOC 和 YOLO 标注怎么对齐
2.1 VOC 格式:XML 标注文件与目录结构
VOC 格式是 PASCAL VOC 比赛定下的标准,核心是每个图像配一个同名 XML 文件,里面用bndbox节点记录目标的左上角和右下角像素坐标。这个数据集的 VOC 部分,如果你解压看目录结构,大概率是JPEGImages放图、Annotations放 XML、ImageSets/Main放训练验证划分的 txt。这套结构的好处是通用,LabelImg、LabelStudio、Roboflow 导出都能接得住,而且 XML 是明文,你可以直接用文本编辑器打开看一眼标注是否合理。
下面这个是 VOC XML 标注的核心片段,舌头框的坐标就是从这里拿的:
<annotation> <folder>JPEGImages</folder> <filename>tongue_00001.jpg</filename> <size> <width>640</width> <height>480</height> <depth>3</depth> </size> <object> <name>tongue</name> <pose>Unspecified</pose> <truncated>0</truncated> <difficult>0</difficult> <bndbox> <xmin>156</xmin> <ymin>88</ymin> <xmax>512</xmax> <ymax>402</ymax> </bndbox> </object> </annotation>注意filename字段必须和 JPEGImages 目录里的实际文件名一一对应,size里的宽高是原始图像的尺寸,不是缩放后的。很多人用 OpenCV 读图后重写了图像尺寸,但忘了同步改 XML 里的size,转 YOLO 格式的时候坐标就错位了。我的习惯是:任何标注文件到手,第一步先抽查 3 到 5 个 XML,用cv2.rectangle把框画回原图看一眼,再确认size和图片实际分辨率一致,这一步不花两分钟,但能免掉后面训练时一堆莫名其妙的收敛问题。
XML 里的object可以出现多次,也就是一张图里有多个目标。但这个舌头数据集一般是单目标,因为一张口腔图里舌头只有一个。如果后面你自己扩数据,加了舌苔区域或者嘴唇区域的标注,就变成了多类多目标,XML 结构不变,只是object节点增多。VOC 格式对多目标支持得很好,这也是它比简单 CSV 标注更「抗造」的原因。
2.2 YOLO 格式:txt 归一化坐标怎么来的
YOLO 格式和 VOC 最大的区别在于坐标表示方式:YOLO 用的是归一化中心点加宽高,也就是说标注值全部是 0 到 1 之间的小数,和图像本身的像素分辨率无关。每张图对应一个 txt 文件,每行代表一个目标,格式是class_id x_center y_center width height,五个数字之间用空格隔开。
以下是一个 YOLO 标注文件的实例,对应上面那张 640x480 的图,框的左上角是 (156, 88),右下角是 (512, 402):
0 0.521875 0.510417 0.556250 0.654167这个数字怎么来的?x_center = (156 + 512) / 2 / 640 = 334 / 640 = 0.521875,y_center = (88 + 402) / 2 / 480 = 245 / 480 = 0.510417,width = (512 - 156) / 640 = 356 / 640 = 0.556250,height = (402 - 88) / 480 = 314 / 480 = 0.654167。这就是从 VOC 到 YOLO 的换算逻辑,没有任何玄学,纯算术,唯一要当心的是除数是size里的宽高,不是bndbox的宽高。
YOLO 格式在训练时的读取效率比 XML 高得多,因为它不需要解析节点树,直接按行读字符串就能灌入数据加载器。Ultralytics YOLOv8、YOLOv5、YOLOv9 的dataset.yaml都默认指向这种 txt 格式,所以这份数据集给到 YOLO 格式,省掉的就是你手动xml2txt这一步。不过我还是建议你别完全依赖数据集自带的转换脚本,自己手写一个voc2yolo.py过一遍,原因很简单:格式转换是检测项目里最容易被「差不多心态」搞砸的环节,自己跑一遍,坐标对不对心里有数。
2.3 增强操作:8804 张图像是怎么扩出来的
数据集标题里的「已增强」三个字,意思是这批图不是原始照片 8804 张,而是通过图像增强技术从一批原始图像扩出来的。常见的增强手段包括水平翻转、随机旋转、亮度饱和度扰动、高斯噪声、随机裁剪缩放等。做增强的目的是提升模型的泛化能力,让 YOLO 在光线差异大、角度偏转多的真实场景里依然能稳定找到舌头位置。
这里的增强有个关键细节:目标检测的增强和图像分类的增强不一样。分类任务可以随便翻转裁剪,但检测任务里你翻转了图像,对应的标注框坐标必须跟着变。比如水平翻转,x_center要变成1 - x_center,宽高不变;旋转 30 度,框的四角坐标要重新投影计算。这份数据集如果增强做得规范,那每一条增强后图像都会重新生成对应的 XML 和 txt,这个在文件命名上通常能看出来,比如tongue_00001_flip.jpg和tongue_00001_flip.txt成对出现。
你拿到数据后第一件事应该是验证增强的「成对性」,我一般是写一个脚本,逐个检查每张图是否同时存在对应的标注文件。如果发现图有了但标注缺失,直接删掉这张图,不要试图训练时跳过,因为数据加载器是按索引读图的,缺一个标注可能导致整个 batch 报错或者后续 epoch 训练中断。倒不是说增强本身有问题,而是文件在拷贝压缩解压过程中难免丢文件,这种「死人坑」我踩过不止一次了。
3. 把数据集跑通:从目录检查到 YOLOv8 训练
3.1 校验文件完整性:图和标注必须成对出现
拿到压缩包先别急着解压,先看压缩包大小和文件数量,心里有个数。解压之后进入根目录,第一步不是写训练脚本,是跑一个完整性校验。以下脚本是我每次拿到新数据集都会先跑一遍的,做两件事:检查每张图是否都有匹配的 YOLO 标注文件,以及标注的坐标是否全部落在 0 到 1 范围内:
import os from pathlib import Path data_root = Path("tongue_dataset") img_dir = data_root / "images" label_dir = data_root / "labels" imgs = sorted(img_dir.glob("*.jpg")) + sorted(img_dir.glob("*.png")) missing = [] bad_norm = [] for img in imgs: label = label_dir / (img.stem + ".txt") if not label.exists(): missing.append(img.name) continue with open(label, "r", encoding="utf-8") as f: for line in f: parts = line.strip().split() if len(parts) != 5: bad_norm.append(f"{img.name}: line format error") continue try: vals = [float(x) for x in parts[1:]] except ValueError: bad_norm.append(f"{img.name}: non-float value") continue if not all(0 <= v <= 1 for v in vals): bad_norm.append(f"{img.name}: coord out of [0,1] -> {line.strip()}") print(f"total images: {len(imgs)}") print(f"missing label: {len(missing)} -> {missing[:10]}") print(f"bad normalized coord: {len(bad_norm)} -> {bad_norm[:10]}")这段脚本的核心逻辑很简单:用Path.glob枚举所有图片,拿图片名匹配 labels 目录下同名 txt。找不到对应标签就记入missing,坐标解析失败或超出 [0,1] 就记入bad_norm。跑完看输出,如果bad_norm数量不是 0,说明数据集的 YOLO 标注有问题,训练之前必须先清理。
这种检查脚本看起来简单,但价值极高。YOLO 训练对标注格式异常非常敏感,一个坐标写成1.05或者-0.01,轻则这张图 loss 异常拉低精度,重则训练中途RuntimeError直接崩掉。特别是多卡训练的时候,一个坏样本会随机分配到某个 worker 上,导致错误时有时无,排查难度翻倍。我见过不止一个项目组为这种问题浪费了整整两天。
3.2 划分数据集:train、val、test 比例与随机种子
数据校验通过后,接下来是划分数据集。YOLO 训练需要 train 和 val 两个集合,test 可选。常见划分比例是 8:1:1 或 9:1,但要注意划分时必须设置随机种子,否则每次跑脚本得到的划分都不一样,实验结果没法对比。以下脚本直接按比例划分并生成三个 txt 文件:
import random from pathlib import Path random.seed(42) img_dir = Path("tongue_dataset/images") imgs = sorted(img_dir.glob("*.jpg")) + sorted(img_dir.glob("*.png")) random.shuffle(imgs) train_ratio, val_ratio = 0.8, 0.1 n_train = int(len(imgs) * train_ratio) n_val = int(len(imgs) * val_ratio) n_test = len(imgs) - n_train - n_val train_imgs = imgs[:n_train] val_imgs = imgs[n_train:n_train + n_val] test_imgs = imgs[n_train + n_val:] # 只存相对路径,方便换机器训练 for split, split_imgs in [("train", train_imgs), ("val", val_imgs), ("test", test_imgs)]: with open(f"{split}.txt", "w") as f: for img in split_imgs: f.write(str(img) + "\n") print(f"train: {len(train_imgs)}, val: {len(val_imgs)}, test: {len(test_imgs)}")这里把随机种子固定为 42,好处是复现实验时别人跑你的代码得到一模一样的划分。train.txt里存的是图片的相对路径,不是绝对路径,方便在本地训练、服务器推理之间切换。如果你用的是 Ultralytics YOLOv8 原生的 data.yaml 方式,其实不用手动生成 txt,直接在 yaml 里写train: path/to/images让框架自己按目录划分就行。
但手动划分有一个隐藏价值:你可以控制测试集是「没见过的场景」还是「同分布的随机抽样」。比如做舌头检测,原始数据里可能有不同光照条件和拍摄角度的图像,如果你知道哪些图来自同一批采集,尽量把同一批的放进同一集合,避免 train 和 test 之间数据泄露。数据泄露是检测项目里精度虚高的最主要原因,尤其是增强数据集,同一原始图的不同增强版本如果被随机分到了 train 和 test,模型在 test 上表现好是「作弊」的结果,不代表真实场景泛化能力。
3.3 配置 data.yaml 并启动训练
划分完成后,新建一个data.yaml文件,这是 Ultralytics 系列框架统一使用的数据集描述文件。它告诉训练脚本三件事:类别名是什么、训练图片去哪找、验证图片去哪找。舌头检测就一个类别,写起来最简单:
train: ./train.txt val: ./val.txt test: ./test.txt nc: 1 names: 0: tonguetrain、val、test指向刚才生成的 txt 文件,nc是类别数,names用字典形式把类别 ID 映射到名字。这里的缩进不能用 tab,必须用空格,YAML 解析器对缩进格式极其敏感,很多新手在这里翻车,报错信息还不直观,总以为是数据加载的问题。我个人的习惯是写完 yaml 先跑一段python -c "import yaml; print(yaml.safe_load(open('data.yaml')))"验证能不能正常解析。
最后启动训练,命令行如下:
yolo detect train \ model=yolov8s.pt \ data=data.yaml \ imgsz=640 \ epochs=100 \ batch=16 \ device=0 \ project=tongue_run \ name=exp1model=yolov8s.pt表示加载 YOLOv8s 预训练权重作为起点,这种迁移学习方式在小数据集上很有效,因为模型学到了通用特征,只需要微调就可以适配舌头检测。imgsz=640是输入图像尺寸,如果你的原始图分辨率低于 640,这里的值要降到和原图接近的尺寸,比如 416 或 320,否则 OpenCV 上采样会让图像变得模糊。batch=16取决于显存大小,显卡是 8G 显存用 16 基本安全,如果显存紧张降到 8。epochs=100对这个规模的数据集够用,而且 YOLO 有早停机制,loss 不再下降会自动停止,不会浪费太多时间。
训练过程中重点观察两个指标:box_loss和cls_loss是否持续下降,mAP50是否在一个合理的区间。舌头检测属于目标较大的任务,框占整张图的比例很高,mAP50 跑到 0.95 以上是完全可能的,如果跑完只有 0.7 左右,大概率是标注或者数据加载环节出了问题,而不是模型能力不够。训练日志最好保留下来,后面排查问题的时候回溯分析很方便。
4. 避坑指南:坐标错位、类别映射和增强翻车
4.1 坐标偏移:转格式时把原始像素当成了归一化坐标
现象:训练时 loss 前期下降正常,但 mAP 一直在低位徘徊,画出来的预测框整体偏离舌头中心,位置不对但框大小差不多。
原因:VOC 转 YOLO 时,脚本直接把 XML 里的xmin、ymin、xmax、ymax当作归一化坐标写入 txt,没有除以图像宽高。YOLO 训练时默认这些值都在 [0,1],读进去直接算出锚框匹配,坐标整体放大导致预测框飘到图像外面。
解决:检查 VOC 转 YOLO 的脚本,确认x_center = ((xmin + xmax) / 2) / img_width这一步没丢。最直接的办法是用上面第三节的校验脚本跑一遍所有 txt,如果坐标值大量出现大于 1 的,基本就是这个原因。
4.2 类别索引对不上:VOC 的类别名和 data.yaml 不一致
现象:训练正常启动,但所有预测框都被标成了错误类别,或者cls_loss始终不收敛。
原因:VOC XML 里的<name>写得是tongue,你也写了names: {0: tongue},看起来没问题。但如果原始 XML 里还有tongue_body这类相近的名字,排序之后索引就变了,VOC 转 YOLO 脚本没做类别映射,直接用了字符串排序后的编号,导致类别 ID 错位。
解决:转格式时写一个显式的类别映射字典,例如class_map = {"tongue": 0},再跑一遍检查。不要用sorted(set(names))这种方式做映射,两个不同类别的名字排序后可能和你想的完全不一样。单独类别的数据集也会踩这个坑,{"tongue": 0}和{"Tongue": 0}都会因为大小写不同产生两个类别。
4.3 增强导致标注失效:翻转和裁剪后标签没跟着变
现象:训练集里个别图片的标注框和舌头实际位置完全不重合,loss 对这些图特别大,验证集 mAP 偏低。
原因:数据集标注「已增强」可能是先用工具做了翻转、旋转、裁剪,但导出时某个环节丢了坐标变换。比如用 OpenCV 水平翻转图像,只把像素翻转了,忘记把标注框x_center改成1 - x_center。
解决:不要盲目信任「已增强」三个字。用第 3 章里的可视化脚本,随机挑 20 张增强图,把标注框画在原图上,人眼过一遍。如果发现框和图对不上,最靠谱的解决思路不是修标注,是直接删除这些坏样本,用剩余的正常数据训练,损失几个样本换来的是整个训练过程的稳定性。
4.4 数据集划分泄露:增强图的同源样本同时出现在 train 和 val
现象:训练 mAP 很高,验证 mAP 也很高,但部署到实际场景效果拉胯。
原因:增强操作从同一张原始图生成了多个变体,划分数据集时这些变体被随机分到了 train 和 val 两边。模型在 val 上看到的是「见过的图」的轻微变体,当然分数高,这属于典型的数据泄露。
解决:划分之前按文件名的原始前缀做分组,确保同源图像全部进入同一个集合。比如文件名格式是orig_001_flip.jpg和orig_001_bright.jpg,那就以orig_001为 key 做分组,整个组只进 train 或 val。二八划分看起来容易,但因为增强数据同源问题,实际上要按组划分才能拿到真实可信的验证分数。
5. 验证一个检测效果:推理脚本与关键参数解读
训练结束后,拿一张训练集之外的真实舌头照片做推理验证。以下脚本加载训练好的权重,输出检测结果和置信度,这部分是检测整个流程里最有成就感也最容易暴露问题的一步:
from ultralytics import YOLO import cv2 model = YOLO("tongue_run/exp1/weights/best.pt") img = cv2.imread("test_img.jpg") results = model.predict(img, conf=0.5, imgsz=640) boxes = results[0].boxes for box in boxes: x1, y1, x2, y2 = box.xyxy[0].tolist() conf = box.conf[0].item() cls_id = int(box.cls[0].item()) print(f"class={cls_id}, conf={conf:.2f}, box=({x1:.0f}, {y1:.0f}, {x2:.0f}, {y2:.0f})") annotated = results[0].plot() cv2.imwrite("output.jpg", annotated)这段代码里最有用的输出不是框坐标,是置信度conf。如果一张清晰的舌头正面图置信度低于 0.7,通常说明训练数据里缺少这种光线或角度的样本,需要补充相关场景的数据。conf阈值 0.5 是通用默认值,实际部署时建议调高到 0.6 或 0.7,因为舌头检测的误检代价高——健康检测场景里把一个非舌头区域当成舌头,后续的舌色分析全都会跟着错。
我自己的习惯是推理阶段额外加一个最小框面积的过滤逻辑,舌头在采集图片里通常占比很大,如果模型输出了面积只有几十像素的小框,大概率是误检。加上这个过滤条件,能明显减少部署时的误报。从那以后我在每个检测项目的推理脚本里都会做一遍框面积过滤,并且把置信度阈值调成一个显式变量而不是硬编码。这个习惯帮我避开了很多部署现场的低级问题,也希望它能帮到你。
本文还有配套的精品资源,点击获取