ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

舌头分割数据集实战:从2类掩码到U-Net训练与避坑指南

舌头分割数据集实战:从2类掩码到U-Net训练与避坑指南 简介本资源面向图像分割方向的算法学习者与工程开发者提供一套完整的舌头分割数据集可用于语义分割模型的训练、验证与效果对比。数据涵盖训练集与测试集两部分训练集含2127张jpg原图及2127张对应png掩膜测试集含537张原图及537张掩膜图像分辨率统一为640×640掩膜为0/1阈值图像0代表背景、1代表舌头具体类别可在classes文本中查看。压缩包共约2000个文件以1998个png掩膜、1个txt类别说明和1个py可视化脚本为主整体约101.53MB采用7z格式打包。配套脚本可直接运行随机抽取一张图片并展示原图、GT图像及GT在原图上的蒙板效果结果自动保存至当前目录无需修改即可观察掩膜质量。目前已有229人学习适合需要快速获取标注数据、搭建分割训练流程或验证模型精度的读者参考使用。1. 舌头分割数据集到底能解决什么从口腔影像到 2 类掩码的落地路径拿到一份标注好的舌头分割数据集最直接的用途是训练一个能把「舌体」和「背景」分开的二分类语义分割模型。医学图像分割里舌头区域的分割常被用于舌诊客观化、口腔手术导航、语音发音研究甚至正畸前后的软组织对比。但真正动手时你会发现公开的舌头数据远没有息肉分割、视网膜血管那么丰富很多团队卡在第一步没有像素级标签。这份「2 类」数据集的价值就在于它把问题简化成前景与背景让你能快速验证 U-Net、DeepLabV3 这类结构在口腔场景下的收敛性而不是一上来就纠结多类舌苔、舌质分类。适合谁适合刚接触医学图像分割、想找一个体量可控、标注干净的二分类任务练手或者需要快速搭一套舌头分割 baseline 的工程师。下面从数据组织、标签格式、可视化代码到训练踩坑一步步拆开讲。2. 舌头分割数据集的目录结构与标签文件格式先看懂再动手2.1 图像与掩码的对应关系怎么组织常见做法是把原图和标签分两个文件夹平铺文件名一一对应。比如images/下放tongue_001.jpgmasks/下放tongue_001.png。掩码是单通道 8 位灰度图像素值只有 0 和 1或 0 和 2550 代表背景非零代表舌体。这里有个容易翻车的点有些标注工具导出的是调色板模式的 PNG用 OpenCV 读出来是三通道直接送进模型会报通道不匹配。我一般会在 Dataset 类里强制转成单通道并做一次二值化。import cv2 import numpy as np def load_mask(mask_path): # 以灰度模式读取避免调色板 PNG 变成三通道 mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) if mask is None: raise FileNotFoundError(mask_path) # 统一二值化大于 127 视为前景 mask (mask 127).astype(np.uint8) return mask逻辑说明IMREAD_GRAYSCALE保证无论原图是 RGB 还是调色板都压成单通道。 127这个阈值不是玄学是因为多数标注导出时前景为 255背景为 0取中间值最稳。参数上如果你拿到的掩码前景是 1那阈值改成 0即可但建议先统计一下唯一值再决定。2.2 标签文件里可能藏着的三个坑第一掩码尺寸和原图不一致。有些流程会先缩放原图再标注导致掩码比原图小一圈。训练前必须用cv2.resize把掩码最近邻插值回原图尺寸千万别用双线性否则边缘会出现 0.5 这种中间值。第二文件名大小写或扩展名不统一tongue_001.JPG和tongue_001.png在 Linux 下是两回事。第三存在空掩码全黑或全前景的极端样本这类样本在二分类里会让 loss 震荡建议先统计前景像素占比低于 0.5% 或高于 99.5% 的直接剔除。import os import numpy as np import cv2 def check_mask_stats(mask_dir): ratios [] for name in os.listdir(mask_dir): if not name.lower().endswith((.png, .jpg)): continue mask cv2.imread(os.path.join(mask_dir, name), cv2.IMREAD_GRAYSCALE) if mask is None: continue fg (mask 127).sum() ratios.append(fg / mask.size) ratios np.array(ratios) print(f样本数: {len(ratios)}) print(f前景占比 min/mean/max: {ratios.min():.4f}/{ratios.mean():.4f}/{ratios.max():.4f}) # 标记异常样本 bad np.where((ratios 0.005) | (ratios 0.995))[0] print(f建议剔除的异常索引: {bad[:20]})这段脚本跑一遍你就能对数据质量心里有数。参数上0.005 和 0.995 是我在多个医学二分类任务里总结的经验阈值不是硬标准但能帮你快速定位那些「几乎全黑」或「几乎全白」的脏数据。3. 用可视化代码把图像和标签叠在一起肉眼验收比指标更早发现问题3.1 叠加显示的最小实现训练前把原图和掩码叠在一起看是最省时间的验收手段。很多分割翻车不是模型不行而是标签本身就错位了。下面这段代码把掩码以半透明红色叠在原图上保存成对比图。import cv2 import numpy as np import os def overlay_mask(image_path, mask_path, save_path, alpha0.5): img cv2.imread(image_path) mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) if img is None or mask is None: print(f读取失败: {image_path} 或 {mask_path}) return # 确保尺寸一致 if img.shape[:2] ! mask.shape[:2]: mask cv2.resize(mask, (img.shape[1], img.shape[0]), interpolationcv2.INTER_NEAREST) binary (mask 127).astype(np.uint8) # 生成红色掩码层 color_mask np.zeros_like(img) color_mask[:, :, 2] binary * 255 # OpenCV 是 BGR2 通道为红 # 叠加 overlay cv2.addWeighted(img, 1 - alpha, color_mask, alpha, 0) cv2.imwrite(save_path, overlay) if __name__ __main__: img_dir images mask_dir masks out_dir vis os.makedirs(out_dir, exist_okTrue) for name in os.listdir(img_dir)[:20]: # 先看前 20 张 stem os.path.splitext(name)[0] mask_name stem .png overlay_mask( os.path.join(img_dir, name), os.path.join(mask_dir, mask_name), os.path.join(out_dir, stem _overlay.jpg) )逻辑说明addWeighted的alpha控制掩码透明度0.5 意味着原图和红色各占一半既能看清舌体轮廓又不至于盖住纹理。参数上如果你发现红色区域明显偏移先别怀疑代码去检查掩码是不是被水平翻转或旋转过——标注工具导出时方向搞反是高频事故。3.2 批量可视化时怎么快速定位错标一张张看效率太低。我一般会生成一张网格图把 16 张叠加结果拼成 4x4一眼扫过去错位的、漏标的、多标的都会跳出来。实现思路是用np.hstack和np.vstack拼图注意每张图先 resize 到统一尺寸否则拼接会报错。这一步做完你基本能确定这份舌头分割数据集能不能直接开训还是需要先返工清洗。4. 训练舌头分割模型的参数怎么设从 U-Net 到损失函数的取舍4.1 输入尺寸与归一化的实际选择舌头图像通常来自口内相机或手机拍摄分辨率参差不齐。常见做法是统一缩放到 256x256 或 512x512。256 训练快适合先跑通流程512 能保留舌体边缘细节但显存占用翻倍。归一化用 ImageNet 的均值和方差即可因为多数 backbone 是在 ImageNet 上预训练的。注意掩码不要做归一化只做 resize 和二值化。import torch from torch.utils.data import Dataset import cv2 import numpy as np import os class TongueDataset(Dataset): def __init__(self, img_dir, mask_dir, size256): self.img_dir img_dir self.mask_dir mask_dir self.size size self.names [n for n in os.listdir(img_dir) if n.lower().endswith((.jpg, .png))] def __len__(self): return len(self.names) def __getitem__(self, idx): name self.names[idx] stem os.path.splitext(name)[0] img cv2.imread(os.path.join(self.img_dir, name)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask cv2.imread(os.path.join(self.mask_dir, stem .png), cv2.IMREAD_GRAYSCALE) img cv2.resize(img, (self.size, self.size)) mask cv2.resize(mask, (self.size, self.size), interpolationcv2.INTER_NEAREST) img img.astype(np.float32) / 255.0 mean np.array([0.485, 0.456, 0.406]) std np.array([0.229, 0.224, 0.225]) img (img - mean) / std img img.transpose(2, 0, 1) mask (mask 127).astype(np.float32) return torch.from_numpy(img).float(), torch.from_numpy(mask).unsqueeze(0).float()逻辑说明INTER_NEAREST用于掩码 resize保证不产生中间灰度值。unsqueeze(0)给掩码加通道维变成[1, H, W]和模型输出对齐。参数上size根据你的显存来8G 显存跑 512 的 U-Net batch size 大概只能到 4跑 256 可以到 16。4.2 损失函数选 BCE 还是 Dice二分类舌头分割前景占比通常不小BCE 够用。但如果你的数据里舌体只占画面一小部分Dice Loss 或 BCEDice 组合会更稳。我一般先用 BCE 跑一轮看验证集 Dice 能不能过 0.9过不了再换组合损失。优化器选 Adam学习率 1e-3 起步配合 ReduceLROnPlateau耐心值设 5。指标别只看准确率二分类里背景占大头准确率容易虚高盯住 Dice 和 IoU。5. 舌头分割训练与推理的避坑排查血泪经验五条5.1 现象loss 降到 0.1 但预测全是背景原因前景像素占比过低模型学会了「全预测背景」这个偷懒解。解决先统计前景占比如果低于 5%在损失里给前景加权或者改用 Dice Loss。也可以过采样含舌体的样本。5.2 现象验证集 Dice 很高但推理时边缘一塌糊涂原因训练时用了双线性插值 resize 掩码导致边缘出现 0.3、0.7 这类值模型学到的边界是模糊的。解决掩码 resize 必须用最近邻且二值化后再送网络。5.3 现象同一张图OpenCV 读出来是 3 通道PIL 读出来是单通道原因PNG 调色板模式在不同库里的解析行为不一致。解决统一用cv2.IMREAD_GRAYSCALE读掩码或者在 Dataset 里强制convert(L)。别混用库否则你会花一晚上排查一个通道数问题。5.4 现象训练到一半 loss 突然变 NaN原因学习率太大或者某张图里有全黑掩码导致 Dice 分母为零。解决加梯度裁剪学习率降到 1e-4并在 Dataset 里过滤掉前景像素为 0 的样本。全黑掩码在 Dice Loss 里是定时炸弹。5.5 现象推理结果比验证时差很多原因推理时的预处理和训练时不一致比如忘了归一化或者 resize 的插值方式不同。解决把预处理封装成一个函数训练和推理共用同一份代码。别凭记忆手写两套。6. 把舌头分割数据集用出进阶价值伪标签与边缘后处理的组合技巧当你用这份 2 类数据把 baseline 跑到 Dice 0.92 以上下一步可以考虑半监督扩展。具体做法是用当前模型对未标注的口腔图像做推理取置信度高于 0.95 的区域作为伪标签再人工抽检修正扩充训练集。这个循环跑两三轮通常能把 Dice 再推 1 到 2 个点。另一个技巧是推理后的边缘后处理对模型输出的概率图做条件随机场CRF或者简单的形态学闭运算能明显改善舌体边缘的毛刺。我一般先用cv2.morphologyEx做开运算去噪再保留最大连通域把零散的小预测块滤掉。import cv2 import numpy as np def postprocess(prob_map, threshold0.5, kernel_size5): # prob_map: [H, W] 浮点概率图 binary (prob_map threshold).astype(np.uint8) # 开运算去噪 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size)) opened cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) # 保留最大连通域 num_labels, labels, stats, _ cv2.connectedComponentsWithStats(opened, connectivity8) if num_labels 1: return opened largest 1 np.argmax(stats[1:, cv2.CC_STAT_AREA]) result (labels largest).astype(np.uint8) return result逻辑说明MORPH_OPEN先腐蚀后膨胀能去掉孤立的噪点。connectedComponentsWithStats找出所有连通区域取面积最大的那个假设舌体是画面里最大的前景块。参数上kernel_size根据你的分辨率调256 的图用 3 到 5 就够512 的图可以用 7。阈值 0.5 是默认值如果你发现模型偏保守可以降到 0.4 让边缘更完整。这套组合拳打下来一份看似简单的 2 类舌头分割数据集能支撑从入门到半监督的完整链路。我自己踩过最深的坑是掩码通道数问题白白浪费一晚上希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进