ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

舌头分割数据集实战:从2类标签到U-Net基线,避开医学图像分割的5个坑

舌头分割数据集实战:从2类标签到U-Net基线,避开医学图像分割的5个坑 简介本资源面向计算机视觉学习者与图像分割开发者提供一套完整的舌头分割数据集适用于语义分割模型训练、医学图像预处理及算法验证等场景。数据图像分辨率统一为640×640原图为jpg格式mask标签为png格式采用0/1阈值图像0表示背景、1表示舌头具体类别可在classes文本中查看。压缩包共约2000个文件以1998个png掩膜、1个txt类别说明和1个py可视化脚本为主整体约101.53MB采用7z格式打包。数据集划分为训练集与测试集训练集含2127张图片及2127个对应mask测试集含537张图片及537个对应mask目录结构清晰便于直接接入分割网络。配套可视化脚本无需修改即可运行随机抽取一张图片展示原始图像、GT图像及GT在原图上的蒙板效果并保存至当前目录方便快速检查标注质量。目前已有229人学习下载适合需要现成舌头分割数据与可视化工具的中高级开发者参考使用。1. 舌头分割数据集到底能解决什么问题从中医齿痕识别到医学图像分割入门舌头分割tongue segmentation在图像分割任务里属于一个被低估的细分方向。它不像息肉分割、视网膜血管分割那样有铺天盖地的论文但在中医舌诊数字化、口腔正畸术前评估、舌体运动分析这些场景里它是一个绕不开的前置步骤。你拿到的这份「舌头分割数据集2类」本质上是把一张口腔照片里的像素分成两类背景和舌体。听起来简单但真正跑起来你会发现嘴唇、牙齿、口腔内壁的阴影都会干扰模型光照不均和拍摄角度变化更是让 IoU 掉得厉害。这份数据集配套了标签文件和数据可视化代码意味着你不需要从零标注可以直接进入训练和调参环节。适合谁刚接触医学图像分割的工程师、做中医智能诊断的产品团队、以及想用一个小数据集快速验证 U-Net 或 DeepLab 系列模型的人。它不追求 SOTA追求的是让你在半天内跑通一个可用的舌头分割基线。2. 2类标签的掩码设计为什么背景和舌体的边界比你想的更难分2.1 2类分割的定义与标签编码方式2类分割意味着输出是一个二值掩码像素值为0代表背景像素值为1代表舌体。但这里有一个容易被忽略的细节——标签文件的存储格式。常见做法有两种一种是单通道灰度 PNG像素值直接是0和1另一种是调色板模式的 PNG视觉上是黑白但实际存储的是索引值。如果你用 OpenCV 的imread默认参数读取调色板 PNG拿到的可能是三通道 BGR 图像值域变成0和255直接送进损失函数会出大问题。我一般会先写一段检查脚本确认标签的真实格式import cv2 import numpy as np import os label_dir labels for fname in os.listdir(label_dir)[:5]: path os.path.join(label_dir, fname) img cv2.imread(path, cv2.IMREAD_UNCHANGED) print(f{fname} shape{img.shape} dtype{img.dtype} unique{np.unique(img)[:10]})逻辑说明cv2.IMREAD_UNCHANGED会保留原始位深和通道数避免自动转成三通道。参数说明如果输出shape(H,W)且unique[0,1]说明是标准灰度标签如果shape(H,W,3)且unique[0,255]说明是调色板或三通道存储需要手动转成单通道并归一化到0/1。这一步不做后面算 Dice 系数时会出现「看起来 loss 在降但预测全黑」的玄学现象。2.2 舌体边界的模糊性嘴唇和牙齿为什么会被误分舌体和嘴唇在 RGB 空间里的颜色分布高度重叠尤其是唇色偏红、舌色偏淡的人群。牙齿虽然颜色差异大但反光区域的高光会被模型当成舌体的湿润表面。更麻烦的是口腔内壁的阴影它的亮度和舌体边缘接近卷积核在浅层根本分不开。常见做法是在数据增强阶段加入颜色抖动和随机亮度调整让模型学会忽略绝对颜色转而依赖纹理和形状。但要注意颜色抖动的幅度不能太大否则舌体的红色特征被破坏模型会退化成只认轮廓遇到舌头伸出长度不同的样本就翻车。我一般会这样配置增强import albumentations as A train_transform A.Compose([ A.RandomBrightnessContrast(brightness_limit0.15, contrast_limit0.15, p0.5), A.HueSaturationValue(hue_shift_limit8, sat_shift_limit15, val_shift_limit10, p0.4), A.Resize(256, 256), A.HorizontalFlip(p0.5), A.ShiftScaleRotate(shift_limit0.05, scale_limit0.1, rotate_limit15, p0.5), ])逻辑说明RandomBrightnessContrast的幅度控制在0.15以内避免过曝或过暗导致舌体纹理丢失。HueSaturationValue的色相偏移限制在8度饱和度15亮度10这个范围是我在舌诊数据上试出来的经验值再大就会把淡红舌变成暗红舌模型学到的特征就偏了。ShiftScaleRotate的旋转限制在15度因为舌头拍摄通常不会大角度倾斜过度旋转会引入不真实的边界。参数说明Resize(256,256)是权衡显存和细节的选择如果你用 512 分辨率舌体边缘的乳头状纹理更清晰但 batch size 要降到4以下。HorizontalFlip对舌头分割是安全的因为舌体左右对称性较强翻转不会改变语义。2.3 标签文件与图像文件的命名对齐数据集常见的一个坑是图像和标签的命名不一致比如图像是001.jpg标签是001_mask.png或者图像在images/下标签在masks/下但文件名多了前缀。如果你直接按文件名匹配会漏掉大量样本。我一般会写一个配对检查函数在训练前跑一遍import os img_dir images lbl_dir labels img_files {os.path.splitext(f)[0] for f in os.listdir(img_dir)} lbl_files {os.path.splitext(f)[0].replace(_mask, ) for f in os.listdir(lbl_dir)} missing img_files - lbl_files print(f图像数{len(img_files)} 标签数{len(lbl_files)} 缺失{len(missing)}) if missing: print(缺失样本:, list(missing)[:10])逻辑说明用集合差集找出没有对应标签的图像。参数说明replace(_mask, )是为了处理标签文件名带后缀的情况如果你的数据集命名规则不同改这一行即可。这一步能避免训练到一半发现某个 batch 的标签是空的导致 loss 突然变成 NaN。3. 用 U-Net 跑通舌头分割基线从数据加载到 Dice 系数的完整链路3.1 数据加载器的实现与通道顺序陷阱舌头分割的图像通常是 RGB 三通道但如果你用 OpenCV 读取默认是 BGR 顺序。直接送进 PyTorch 的预训练模型比如 ResNet 编码器会出问题因为预训练权重是在 ImageNet 的 RGB 上训的。BGR 和 RGB 的通道顺序颠倒会让模型在第一层卷积就学到错误的颜色特征。我一般会在 Dataset 类里显式转换import torch from torch.utils.data import Dataset import cv2 import numpy as np import os class TongueDataset(Dataset): def __init__(self, img_dir, lbl_dir, transformNone): self.img_dir img_dir self.lbl_dir lbl_dir self.transform transform self.names [os.path.splitext(f)[0] for f in os.listdir(img_dir)] def __len__(self): return len(self.names) def __getitem__(self, idx): name self.names[idx] img cv2.imread(os.path.join(self.img_dir, name .jpg)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 关键BGR转RGB lbl cv2.imread(os.path.join(self.lbl_dir, name _mask.png), cv2.IMREAD_UNCHANGED) if lbl.ndim 3: lbl lbl[:, :, 0] lbl (lbl 0).astype(np.uint8) # 统一到0/1 if self.transform: augmented self.transform(imageimg, masklbl) img augmented[image] lbl augmented[mask] img torch.from_numpy(img).permute(2, 0, 1).float() / 255.0 lbl torch.from_numpy(lbl).long() return img, lbl逻辑说明cv2.cvtColor把 BGR 转成 RGB这是最容易被跳过的一步。lbl.ndim 3的判断是为了兼容调色板 PNG 被读成三通道的情况取第一个通道即可。(lbl 0)把任何非零值统一成1避免标签里有255或其他索引值。permute(2,0,1)把 HWC 转成 CHW这是 PyTorch 的标准输入格式。参数说明/255.0把像素值归一化到0-1如果你用ToTensor()也可以但手动写更可控。long()是交叉熵损失要求的标签类型如果用 Dice 损失可以改成float()。3.2 U-Net 模型的最小实现与输出通道数U-Net 是舌头分割最常用的基线因为它的跳跃连接能保留边缘细节。2类分割的输出通道数是2背景和舌体但如果你用 Sigmoid BCE 损失输出通道可以是1。我一般用2通道 CrossEntropyLoss因为这样更容易扩展到多类。import torch.nn as nn class DoubleConv(nn.Module): def __init__(self, in_ch, out_ch): super().__init__() self.conv nn.Sequential( nn.Conv2d(in_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), nn.Conv2d(out_ch, out_ch, 3, padding1), nn.BatchNorm2d(out_ch), nn.ReLU(inplaceTrue), ) def forward(self, x): return self.conv(x) class UNet(nn.Module): def __init__(self, in_ch3, out_ch2): super().__init__() self.down1 DoubleConv(in_ch, 64) self.down2 DoubleConv(64, 128) self.down3 DoubleConv(128, 256) self.pool nn.MaxPool2d(2) self.up1 nn.ConvTranspose2d(256, 128, 2, stride2) self.up_conv1 DoubleConv(256, 128) self.up2 nn.ConvTranspose2d(128, 64, 2, stride2) self.up_conv2 DoubleConv(128, 64) self.out nn.Conv2d(64, out_ch, 1) def forward(self, x): d1 self.down1(x) d2 self.down2(self.pool(d1)) d3 self.down3(self.pool(d2)) u1 self.up1(d3) u1 torch.cat([u1, d2], dim1) u1 self.up_conv1(u1) u2 self.up2(u1) u2 torch.cat([u2, d1], dim1) u2 self.up_conv2(u2) return self.out(u2)逻辑说明这是一个简化版 U-Net只用了两次下采样和两次上采样因为舌头分割的图像分辨率通常不高256或512更深的网络反而容易过拟合。torch.cat是跳跃连接把编码器的特征和解码器的特征拼接。out_ch2对应背景和舌体两类。参数说明BatchNorm2d在小 batch size 下可能不稳定如果显存不够只能用 batch size2建议换成GroupNorm。ConvTranspose2d的stride2负责上采样kernel_size2保证输出尺寸正好翻倍。3.3 Dice 系数和 IoU 的计算与监控舌头分割的评估不能只看准确率因为背景像素通常占70%以上模型全预测背景也能拿到高准确率。Dice 系数和 IoU 是更可靠的指标。def dice_coeff(pred, target, smooth1e-6): pred torch.argmax(pred, dim1).float() target target.float() intersection (pred * target).sum() return (2. * intersection smooth) / (pred.sum() target.sum() smooth) def iou_score(pred, target, smooth1e-6): pred torch.argmax(pred, dim1).float() target target.float() intersection (pred * target).sum() union pred.sum() target.sum() - intersection return (intersection smooth) / (union smooth)逻辑说明torch.argmax把2通道输出转成0/1掩码。smooth防止除零。Dice 系数对类别不平衡不敏感适合舌头分割这种背景占主导的任务。参数说明如果你用 Sigmoid BCEpred需要先做阈值化比如pred 0.5而不是argmax。Dice 系数在0.85以上通常说明分割质量可用0.75以下需要检查标签质量或增强策略。4. 数据可视化代码怎么用把标签叠加到原图上做质检4.1 叠加可视化的实现与颜色映射数据可视化代码的核心作用是在训练前快速检查标签是否对齐、边界是否准确。我一般会把标签以半透明红色叠加到原图上这样一眼就能看出舌体区域是否被正确标注。import cv2 import numpy as np def overlay_mask(img_path, mask_path, alpha0.4): img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask cv2.imread(mask_path, cv2.IMREAD_UNCHANGED) if mask.ndim 3: mask mask[:, :, 0] mask (mask 0).astype(np.uint8) color_mask np.zeros_like(img) color_mask[mask 1] [255, 0, 0] # 舌体区域标红 overlay cv2.addWeighted(img, 1 - alpha, color_mask, alpha, 0) return overlay result overlay_mask(images/001.jpg, labels/001_mask.png) cv2.imwrite(check_001.png, cv2.cvtColor(result, cv2.COLOR_RGB2BGR))逻辑说明color_mask创建一个全黑图像只在舌体区域填红色。addWeighted把原图和颜色掩码按比例混合alpha0.4表示红色占40%透明度既能看清边界又不遮挡原图纹理。参数说明alpha可以调到0.3-0.5之间太低看不清掩码太高遮挡原图。如果你要批量检查把这段逻辑包一个循环输出到一个check/目录用图片浏览器快速翻看。4.2 边界框和轮廓的辅助检查有时候叠加图看不出问题但模型训练后边界就是不准。这时候需要把标签的轮廓画出来和原图的舌体边缘对比。def draw_contour(img_path, mask_path): img cv2.imread(img_path) mask cv2.imread(mask_path, cv2.IMREAD_UNCHANGED) if mask.ndim 3: mask mask[:, :, 0] mask (mask 0).astype(np.uint8) * 255 contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) cv2.drawContours(img, contours, -1, (0, 255, 0), 2) return img逻辑说明findContours提取舌体区域的外轮廓drawContours用绿色线条画在原图上。如果轮廓和实际舌体边缘有明显偏移说明标签有问题需要重新标注或剔除。参数说明RETR_EXTERNAL只提取最外层轮廓忽略内部空洞。linewidth2在256分辨率下比较清晰512分辨率可以加到3。4.3 批量质检脚本与异常样本筛选单张检查效率太低我一般会写一个批量脚本自动计算每张标签的舌体像素占比把占比过高或过低的样本挑出来。import os import numpy as np import cv2 def batch_check(img_dir, lbl_dir): stats [] for fname in os.listdir(lbl_dir): mask cv2.imread(os.path.join(lbl_dir, fname), cv2.IMREAD_UNCHANGED) if mask.ndim 3: mask mask[:, :, 0] ratio (mask 0).sum() / mask.size stats.append((fname, ratio)) stats.sort(keylambda x: x[1]) print(舌体占比最低的5个:, stats[:5]) print(舌体占比最高的5个:, stats[-5:]) return stats逻辑说明舌体像素占比通常在15%-45%之间低于10%可能是标签错误或图像里舌头太小高于60%可能是把嘴唇也标进去了。参数说明这个范围不是绝对的取决于拍摄距离和舌头伸出程度但极端值一定值得人工复查。5. 避坑与排查舌头分割训练中最容易翻车的5个地方5.1 现象loss 降到0.1以下但预测全黑原因标签读取时没有归一化到0/1而是保留了0/255CrossEntropyLoss 把255当成第255类但输出只有2类导致所有像素都被归为背景。解决在 Dataset 里加(lbl 0).astype(np.uint8)确保标签值只有0和1。5.2 现象Dice 系数在0.6附近震荡不上升原因学习率太大模型在最优解附近跳来跳去。舌头分割的数据量通常不大Adam 的默认学习率1e-3偏高。解决降到1e-4并加ReduceLROnPlateau调度器耐心设5因子0.5。5.3 现象验证集 Dice 比训练集低0.2以上原因数据增强过度尤其是颜色抖动幅度太大训练集分布和验证集差异明显。解决把HueSaturationValue的色相偏移从8降到5饱和度从15降到10重新训练。5.4 现象舌体边缘有锯齿状毛刺原因上采样用了最近邻插值或者ConvTranspose2d的 kernel_size 和 stride 不匹配导致棋盘效应。解决把ConvTranspose2d换成nn.Upsample(modebilinear)Conv2d或者确保 kernel_size2, stride2。5.5 现象某些图像预测出多个舌体区域原因模型把嘴唇或牙齿的某些区域也预测成了舌体导致连通域分裂。解决在后处理阶段用cv2.connectedComponents保留最大连通域其余置为背景。这个操作在推理时加不影响训练。6. 从2类到多类舌苔、齿痕、裂纹的扩展思路与验证习惯2类舌头分割只是起点。实际中医舌诊需要区分舌质和舌苔甚至识别齿痕、裂纹、瘀斑。扩展路径很直接把标签从0/1改成0/1/2/3输出通道数从2改成4损失函数不变。但这里有一个关键问题——类别不平衡会急剧恶化。舌苔区域通常只占舌体的30%-50%齿痕和裂纹更是细粒度特征像素占比可能不到5%。我一般会先用2类模型跑一个基线把 Dice 做到0.9以上再逐步加类。每加一类先冻结编码器只训解码器10个epoch观察新类的 Dice 是否在上升。如果新类 Dice 低于0.5说明标签质量或特征可分辨性有问题不要硬训。验证方法上除了 Dice 和 IoU我习惯把验证集的预测结果按 Dice 排序挑最差的10张做可视化。这些样本通常暴露了数据集的边界情况舌头伸出过长导致舌尖过曝、拍摄角度偏侧导致舌体被牙齿遮挡、或者标签本身把舌根和咽喉区域混淆。把这些样本单独拿出来看比盯着平均指标有用得多。还有一个习惯每次调整增强策略或学习率后固定用同一个随机种子跑3次取 Dice 的中位数。舌头分割的数据量小单次结果波动可能达到0.05不看多次运行的结果很容易被单次的高分骗到。这个习惯帮我省了很多后悔药——曾经有一次单次 Dice 冲到0.93换了个种子直接掉到0.81后来发现是某张验证集图像和训练集高度相似导致的泄漏。如果你打算把这个数据集用在产品里建议先把推理速度测出来。256分辨率的 U-Net 在单张 GPU 上大概能跑50-80 FPS但如果你用 512 分辨率会降到15-20 FPS。移动端部署需要转 ONNX 或 TensorRT量化到 INT8 后 Dice 通常会掉0.02-0.04这个损失在舌诊场景里可以接受但要在验证集上确认边界没有明显退化。希望帮到你。本文还有配套的精品资源点击获取
返回列表