简介:面向计算机视觉与医学图像分割场景的舌头分割数据集,图像分辨率统一为640×640,jpg原图搭配png格式的0/1阈值mask标签,像素分布为0背景、1舌头,类别清晰,可直接用于主流分割模型的训练与评估。数据已划分为训练集(2127张图片+2127个mask)与测试集(537张图片+537个mask),并附带类别说明txt与可直接运行的数据可视化py脚本——脚本会随机抽取一张图像,将原图、GT掩膜以及GT叠加在原图上的效果保存到当前目录,方便快速查验标注质量、生成效果对比图。资源共2000个文件,以png图像/掩膜为主,另有1个txt和1个py脚本,压缩包总大小101.53MB。已有229人学习下载,适合正在学习图像分割、需要构造舌头分割训练数据或进行分割算法验证的开发者与研究者。
1. 舌头分割数据集为什么难在标签文件上
做过医学图像分割的人大多有这种感觉:模型结构反而不是瓶颈,找一份能直接用的分割数据集才是。舌头分割(2类)表面上只是“舌头”和“背景”两个类别,但真正拿图片去标的时候会发现,舌头根部与咽喉暗区边界模糊、舌苔颜色和嘴唇颜色接近、患者伸出舌头的姿态差异大,这些都会让标签文件出现肉眼难查的错位和漏标。越是不起眼的2类分割任务,越考验数据集的目录结构、标签格式和可视化核查手段。
这篇文章围绕“舌头分割(2类)”这个数据集,讲清楚标签文件怎么组织、掩膜与多边形标注怎么互转、数据可视化代码怎么用、以及如何把整理好的数据集接到 Unet 或 YOLOv8 这类常见分割训练流程里。不要让“数据集”三个字停留在下载文件夹的层面,把它变成可复现、可核查、可训练的训练原料,这才是这篇文章要解决的事。
2. 舌头分割数据集的目录结构与2类标签格式
2.1 一张图像对应一个掩膜:PNG、JSON、RLE 三种标签表示
图像分割数据集的标签文件通常有三种存在形式,分别对应不同的标注工具和训练框架:
| 标签形式 | 文件后缀 | 说明 | 常见来源 |
|---|---|---|---|
| 掩膜图像 | .png / .bmp | 每个像素一个类别编号,舌头为 1、背景为 0 | LabelMe、自家标注脚本 |
| 多边形标注 | .json / .xml | 保存轮廓点的坐标序列,不直接用于训练 | LabelMe、CVAT |
| 运行长度编码 | .txt / .json 内嵌 | 按“起点 长度”记录前景像素位置 | Kaggle 类竞赛 |
对于舌头分割这种 2 类任务,推荐直接使用二值 PNG 掩膜作为统一的标签格式。它的读取成本最低,配合 OpenCV 或 PIL 就能完成可视化,而且不依赖额外解析库。
import cv2 import numpy as np # 读取原图和掩膜 image = cv2.imread("images/tongue_001.jpg") image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) mask = cv2.imread("labels/tongue_001.png", cv2.IMREAD_GRAYSCALE) # 2类分割的掩膜只包含0和255两个值 unique_values = np.unique(mask) print("掩膜中的像素值:", unique_values)这里有个容易踩的坑:标注工具导出时可能把舌头保存为白色(255),背景为黑色(0)。训练框架在计算损失时往往要求输入为0/1或者0/255连续值,不同框架要求不同。因此在做数据检查时,第一步就是确认掩膜中实际存在的像素类别数,避免出现第三个灰度值混入。上面这段代码中,np.unique(mask)用于列出掩膜内所有出现的像素值,正常情况只有[0, 255]或[0, 1]。
2.2 舌头分割数据集的目录与文件命名规范
我自己整理分割数据集时,通常按下面的结构组织文件目录。这个结构也是医学图像分割领域比较通用的做法,方便后续统一读取:
tongue_segmentation/ ├── images/ │ ├── tongue_001.jpg │ ├── tongue_002.jpg │ └── ... ├── labels/ │ ├── tongue_001.png │ ├── tongue_002.png │ └── ... ├── masks_visual/ │ ├── tongue_001_overlay.jpg │ └── ... ├── split/ │ ├── train.txt │ ├── val.txt │ └── test.txt └── code/ ├── visualize.py ├── mask_to_json.py └── check_dataset.py文件名必须严格保持原图与掩膜一一对应,且不要出现空格、中文字符或括号。空格和括号是 Linux 下批量脚本最常见的出错来源。划分文件train.txt中每一行只保存图像 ID,例如tongue_001,程序读取时分别拼上images/和labels/前缀,这样训练时既不会错位,也便于扩展新数据。
2.3 掩膜到 JSON 的互转代码
在标注工具和训练框架之间切换时,多边形标注和掩膜之间的互转是刚需。LabelMe 导出的 JSON 需要转成掩膜才能训练;反过来,掩膜要交给医生复核时,也常常需要转成多边形方便阅读。下面这段代码把二值掩膜转成 LabelMe 风格的 JSON 坐标:
import cv2 import json import numpy as np def mask_to_json(mask_path, image_path, json_path): mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) h, w = mask.shape contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) shapes = [] for contour in contours: # 过滤掉太小的轮廓,避免保存噪声 if cv2.contourArea(contour) < 50: continue # 轮廓点压平为 [x, y, x, y, ...] 格式 points = contour.reshape(-1, 2).tolist() shapes.append({ "label": "tongue", "points": points, "shape_type": "polygon" }) data = { "version": "4.5.6", "flags": {}, "shapes": shapes, "imagePath": image_path, "imageHeight": h, "imageWidth": w } with open(json_path, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) mask_to_json("labels/tongue_001.png", "tongue_001.jpg", "labels/tongue_001.json")这段代码有两个值得注意的地方。第一,cv2.findContours只取目标外轮廓,RETR_EXTERNAL保证舌头内部的“空洞”不会被当成独立轮廓,因为 2 类分割中舌头内部理论上不存在背景区域。第二,contourArea小于 50 像素的轮廓会被直接丢弃,这是为了防止标注时不小心留下的单点噪声生成无意义的多边形。如果你发现转换后的 JSON 在 LabelMe 里显示位置偏移,请检查imagePath字段是否为相对路径,LabelMe 按该字段定位原图。
反向转换的思路对称:解析 JSON 里的points,在空白画布上用cv2.fillPoly填充为白色掩膜即可。需要注意 JSON 中的坐标可能是浮点数,画掩膜前必须用np.int32转成整数,否则 OpenCV 会报类型错误。
3. 构建舌头分割训练集的关键步骤与参数设置
3.1 类别不均衡与二类标签的标注规范
舌头分割虽然只有 2 类,但类别不均衡问题非常突出。在一张分辨率为 1920x1080 的舌像照片中,舌头区域通常只占画面的 15%~30%,背景占了大多数。深度学习模型如果直接拿原始像素做训练,很容易收敛到一个“全预测为背景”的局部最优解。这也是医学图像分割任务中常见的问题,和息肉分割数据集、皮肤病变分割面临的情况类似。
针对 2 类舌象分割,一般从两个方向入手:一是损失函数上使用 Dice Loss 或 Focal Loss;二是数据层面做区域裁剪和增强,让舌头区域在训练样本中拥有更高的出现频率。下面是一个简单的 Dice Loss 实现:
import torch import torch.nn.functional as F def dice_loss(pred, target, smooth=1.0): # pred: 模型输出概率,形状 [B, 1, H, W] # target: 二值掩膜 [B, 1, H, W],取值 0/1 pred = torch.sigmoid(pred) intersection = (pred * target).sum() union = pred.sum() + target.sum() dice = (2.0 * intersection + smooth) / (union + smooth) return 1.0 - diceintersection是预测概率与真实掩膜的逐元素乘积之和,union是两者各自之和。加smooth的目的是防止分母为 0。Dice Loss 对前景区域大小的敏感度比交叉熵低,在小目标分割任务中收敛更稳定,但训练前期容易出现梯度不稳定,建议在训练前 20 个 epoch 使用 Dice Loss 与交叉熵的加权组合。
3.2 数据增强策略及参数设置
舌象数据集的量级一般不大,几十到几百张之间。直接训练分割网络几乎必然过拟合,数据增强不是锦上添花,而是必需品。以下参数是我在舌象分割任务中实测表现比较稳定的组合:
| 增强操作 | 参数范围 | 说明 |
|---|---|---|
| 水平翻转 | 概率 0.5 | 舌象左右对称,安全性最高 |
| 旋转 | ±15° | 超过 15° 会引入非自然的大角度舌位 |
| 缩放 | 0.8~1.2 | 模拟拍摄距离变化 |
| 亮度/对比度 | ±30% | 抵抗拍摄环境光线差异 |
| 弹性形变 | alpha=30, sigma=5 | 模拟舌体轻微形变,适合医学图像分割 |
| 随机裁剪 | 512x512 或 256x256 | 限制训练尺寸,控制显存占用 |
旋转和弹性形变是对分割效果提升最明显的两个操作,其中平移和旋转会导致掩膜出现边缘锯齿,训练中模型会对这种锯齿产生容忍。推理时需要使用与训练一致的预处理流程,否则真实场景中性能会明显下降。
import albumentations as A train_transform = A.Compose([ A.HorizontalFlip(p=0.5), A.Rotate(limit=15, p=0.8), A.RandomScale(scale_limit=0.2, p=0.5), A.RandomBrightnessContrast(brightness_limit=0.3, contrast_limit=0.3, p=0.5), A.ElasticTransform(alpha=30, sigma=5, p=0.3), A.Resize(512, 512), ]) # 单次增强调用,image 和 mask 需同步变换 augmented = train_transform(image=image, mask=mask)Albumentations 的优点在于会自动同步增强原图和掩膜,不需要手动保证变换参数一致。需要注意的是RandomScale缩放后图像尺寸会变化,必须放在Resize前面。如果原图分辨率差异很大,建议先统一 Resize 到固定尺寸,再做旋转与翻转,避免多次插值导致掩膜边缘质量下降。舌象的边缘本身就是模糊过渡,插值越少越好。
3.3 划分数据集和防止泄漏
分割任务的数据划分相比分类任务要更谨慎。同一患者在不同角度、不同时间拍摄的多张舌象照片,如果一部分进了训练集、另一部分进了验证集,验证指标会有虚高。这种情况被称为数据泄漏。
为避免泄漏,划分维度应该是患者而不是单张图片。假如数据集的原始目录中每张图命名带有患者 ID,划分脚本按照患者 ID 进行分组,而不是直接随机分割图片。一个简单的方案是读取原图文件名中的患者标识,按患者分组后再按比例划分:
import os import random from collections import defaultdict image_dir = "images/" patient_ids = defaultdict(list) # 假设文件名格式:patient01_20240112_001.jpg for fname in os.listdir(image_dir): patient_id = fname.split("_")[0] patient_ids[patient_id].append(fname) all_patients = list(patient_ids.keys()) random.shuffle(all_patients) train_cut = int(len(all_patients) * 0.7) val_cut = int(len(all_patients) * 0.85) train_patients = all_patients[:train_cut] val_patients = all_patients[train_cut:val_cut] test_patients = all_patients[val_cut:]划分思路是先把所有患者 ID 打乱,再按 70% / 15% / 15% 的比例切到train/val/test三个集合。注意一定不要在原图列表上直接shuffle后截断,那种方式会让同一患者的不同照片出现在两个集合中。舌象数据通常不是大数据集,测试集比例可以适当提高,一张不规范的测试集划分会把整个模型的对比实验毁掉。
4. 用数据可视化代码核查舌头掩膜质量
4.1 叠加显示与通道分离可视化
拿到数据集后第一件事不是写训练脚本,而是把原图和掩膜叠加在一起逐张检查。掩膜和舌头轮廓错位 3~5 个像素在缩略图上看不出来,但训练时误差会直接算进损失函数。以下代码把掩膜渲染为半透明的红色叠加层,输出到单独目录供快速浏览:
import os import cv2 import numpy as np from tqdm import tqdm image_dir = "images/" label_dir = "labels/" output_dir = "masks_visual/" os.makedirs(output_dir, exist_ok=True) for fname in tqdm(os.listdir(image_dir)): img = cv2.imread(os.path.join(image_dir, fname)) name = os.path.splitext(fname)[0] mask_path = os.path.join(label_dir, name + ".png") if not os.path.exists(mask_path): continue mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) # 归一化到 0~1,避免掩膜存在255以外的值 mask_binary = (mask > 127).astype(np.uint8) overlay = img.copy() # 红色通道增强 overlay[:, :, 2] = np.maximum(overlay[:, :, 2], mask_binary * 255) # 绿色和蓝色通道减弱,突出舌头区域 overlay[:, :, 1] = np.where(mask_binary == 1, overlay[:, :, 1] * 0.5, overlay[:, :, 1]) overlay[:, :, 0] = np.where(mask_binary == 1, overlay[:, :, 0] * 0.5, overlay[:, :, 0]) # 原图和掩膜半透明混合 blended = cv2.addWeighted(img, 0.6, overlay, 0.4, 0) cv2.imwrite(os.path.join(output_dir, name + "_overlay.jpg"), blended)这段代码做的事情很直白:把掩膜区域映射为红色通道增强、绿蓝通道减弱,再用cv2.addWeighted将原图与掩膜按 0.6 和 0.4 的权重混合。建议在核查时重点关注三个位置:舌头边缘是否有 2 像素以上的系统性偏移、舌尖是否被截断、舌根与咽喉暗区边界是否出现大块漏标。纯红色叠加可能看不清暗色背景上的掩膜边界,可以额外生成一个只保留边缘线的版本,用cv2.Canny提取舌头轮廓并绘制在原图上,那个对核查边界更直观。
4.2 用统计分布定位坏样本
人工逐张看图依然会漏掉异常样本,一个更高效的办法是先做统计筛选,再针对异常样本人工复核。用以下几项指标可以快速定位坏样本:
| 指标 | 计算方式 | 可疑区间 |
|---|---|---|
| 舌头区域占比 | mask 像素数 / 图像总像素 | < 5% 或 > 50% |
| 连通域数量 | OpenCV 连通域分析 | > 1 个 |
| 轮廓面积比 | 最大轮廓面积 / 所有轮廓面积 | < 0.9 |
| 掩膜边缘锯齿度 | 掩膜面积 / 轮廓长度 | 明显低于同类样本 |
下面这段代码同时计算了区域占比和连通域数量:
import cv2 import numpy as np def inspect_mask(mask_path): mask = cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) mask_binary = (mask > 127).astype(np.uint8) area_ratio = mask_binary.sum() / mask_binary.size num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(mask_binary, 8) # 背景被记为第0个连通域,所以目标连通域数量要减1 obj_count = num_labels - 1 return area_ratio, obj_count ratio, count = inspect_mask("labels/tongue_001.png") print(f"舌头面积占比: {ratio:.3f}, 连通域数量: {count}")连通域数量大于 1 说明掩膜里存在与主舌头不相连的游离块,可能是标注时把唾沫、嘴唇反光一起标了进去。面积占比过低或过高则提示标注可能漏掉了舌头大块区域,或者把背景一起圈进了舌头区域。这两类样本如果不清理,训练时损失函数会被个别大错误样本主导。建议在训练前完成一次全量统计,并可视化异常样本的直方图分布,再决定是删除还是重新标注,而不是盲目相信数据集的标题。
5. 用统一脚本把数据接入 Unet 与 YOLOv8 训练
5.1 路径统一和训练前的标准目录
要把整理后的舌头分割数据集接入常见训练框架,建议先转成一套标准目录。YOLOv8 的分割训练支持直接读取带masks子目录的数据集结构,这和 Unet 类框架的数据结构几乎一致:
tongue_dataset/ ├── images/ │ ├── train/ │ │ └── tongue_001.jpg │ └── val/ │ └── tongue_002.jpg └── labels/ ├── train/ │ └── tongue_001.txt 或 .png └── val/ └── tongue_002.txt 或 .png以 YOLOv8 为例,训练前创建一个tongue_seg.yaml配置文件:
path: /data/tongue_dataset train: images/train val: images/val test: names: 0: tongue注意这里的关键点:YOLOv8 分割的标签不是 PNG 掩膜,而是与目标检测一致的 txt 格式,每行表示一个多边形轮廓,格式为class_id x1 y1 x2 y2 ...。因此第 2.3 节的掩膜转 JSON 代码还需要再进一步转成 YOLO 的归一化多边形格式。转换时,YOLOv8 要求坐标归一化到 0~1,且每个轮廓点必须保持闭合。一个数据文件包含多个轮廓行时,表示一张图像中有多个舌头实例,训练会自动按实例处理。
5.2 快速验证训练结果的技巧
训练完成后,验证集的可视化展示和 Dice 指标同样重要。这里给出一个通用的验证思路:把三列图拼在一张大图上,左边是原图,中间是真实掩膜,右边是模型预测结果。用并排对比而不是叠加对比,更容易发现皮肤、牙齿、嘴唇等易混淆区域的错误模式。
import cv2 import numpy as np import torch model.eval() with torch.no_grad(): pred = model(torch.from_numpy(image).unsqueeze(0).cuda()) pred_mask = (torch.sigmoid(pred).squeeze().cpu().numpy() > 0.5).astype(np.uint8) canvas = np.zeros((h, w * 3, 3), dtype=np.uint8) canvas[:, 0:w] = image canvas[:, w:w*2] = cv2.cvtColor(gt_mask * 255, cv2.COLOR_GRAY2BGR) canvas[:, w*2:w*3] = cv2.cvtColor(pred_mask * 255, cv2.COLOR_GRAY2BGR) cv2.imwrite("result_compare.png", canvas)这段验证代码里,gt_mask是真实掩膜,pred_mask是模型输出经过阈值 0.5 二值化后的结果。在正式实验里可以把阈值设为变量,在 0.3~0.7 之间扫描,观察不同置信度下舌头区域的边缘变化,用于确定适用于你数据分布的最佳阈值。真实分割项目中,把数据集、标签文件、可视化代码做成一个互相连接的整体,整个训练过程才会有可追溯性。
本文还有配套的精品资源,点击获取