舌头分割数据集实战:640×640二类掩膜与U-Net训练全解析
简介这份资源面向从事医学图像处理、计算机视觉分割任务的研究者与开发者提供一套完整的舌头分割数据集可用于训练和评估二分类语义分割模型。数据图像分辨率统一为640×640原图为jpg格式掩膜为png格式像素值0代表背景、1代表舌头类别定义可在classes文本中查阅。压缩包共约2000个文件以1998个png掩膜、1个txt类别说明和1个py可视化脚本为主整体约101.53MB采用7z打包。数据集划分为训练集与测试集训练集含2127张图像及2127个对应掩膜测试集含537张图像及537个对应掩膜目录结构为images与masks并列便于直接接入主流分割框架。随包附带的可视化脚本无需修改即可运行随机抽取一张图片同时展示原图、GT掩膜以及掩膜叠加在原图上的效果并保存到当前目录方便快速检查标注质量。目前已有229人学习适合需要现成舌头分割数据与快速可视化验证的读者。1. 舌头分割数据集640×640 二类掩膜2127 张训练图能直接喂给 U-Net 吗舌头分割这个方向做中医舌诊数字化、口腔辅助诊断或者医学图像分割练手的人都会碰到。公开数据难找自己标又费时所以一份带 mask 的现成数据集价值就在这里。这份资源给的是 640×640 的 jpg 原图配 png 掩膜训练集 2127 对、测试集 537 对掩膜是 0/1 二值阈值图0 是背景、1 是舌头类别定义在 classes 文本里。它解决的是「从零标注」这个最耗时的环节适合想跑通 U-Net、DeepLab 这类分割网络的人也适合做舌象分析预研的从业者。下面按「拿到手怎么用、参数怎么设、哪里会翻车」拆开讲。2. 数据组织与掩膜语义先搞清 0/1 阈值图和目录结构2.1 目录结构与文件命名规律拿到压缩包解压后常见做法是训练集和测试集各自独立成目录每个目录下再分 images 和 masks 两个子目录。从项目正文给出的文件名能看出命名规律原图和掩膜共享同一段前缀掩膜在末尾加_mask后缀扩展名从 jpg 变成 png。比如-1847_30_-__jpg.rf.93ddfb0a0a77b1a007c024ea79eae2bc_mask.png对应的原图就是去掉_mask后的同名 jpg。这种「前缀一致、后缀区分」的命名是分割数据集里最省心的设计因为配对逻辑可以纯靠字符串处理不需要额外的映射表。我一般会先跑一段脚本核对配对完整性避免训练到一半才发现有孤儿文件。下面这段代码遍历 images 和 masks检查每个原图是否都能找到对应掩膜同时统计尺寸是否统一。import os from PIL import Image def check_pairing(img_dir, mask_dir): imgs {os.path.splitext(f)[0]: f for f in os.listdir(img_dir) if f.lower().endswith(.jpg)} masks {os.path.splitext(f)[0].replace(_mask, ): f for f in os.listdir(mask_dir) if f.lower().endswith(.png)} missing_mask [k for k in imgs if k not in masks] missing_img [k for k in masks if k not in imgs] print(f原图 {len(imgs)} 张掩膜 {len(masks)} 张) print(f缺掩膜: {len(missing_mask)}缺原图: {len(missing_img)}) # 抽查尺寸一致性 for k in list(imgs)[:5]: if k in masks: w1, h1 Image.open(os.path.join(img_dir, imgs[k])).size w2, h2 Image.open(os.path.join(mask_dir, masks[k])).size print(f{k}: 原图 {w1}x{h1}, 掩膜 {w2}x{h2}) return missing_mask, missing_img check_pairing(train/images, train/masks)这段逻辑的关键在于掩膜文件名去掉_mask后必须和原图主名完全一致replace只替换一次不会误伤主名里本身含_mask的情况这份数据主名是哈希串基本不会撞。参数上img_dir和mask_dir按你实际解压路径改。如果 missing 数量不为零先别急着训练多半是解压不完整或者目录层级搞错了。2.2 0/1 掩膜的读取陷阱掩膜是 png 格式的 0/1 阈值图这点必须重视。png 支持多种位深如果保存时用了调色板模式或者 16 位直接np.array读出来可能不是你以为的 0 和 1。常见做法是读取后先看唯一值分布确认只有 {0,1} 两个值再进网络。import numpy as np from PIL import Image mask np.array(Image.open(train/masks/xxx_mask.png)) print(dtype:, mask.dtype, shape:, mask.shape) print(唯一值:, np.unique(mask)) print(前景占比: %.4f % (mask.sum() / mask.size))如果np.unique输出的是[0 255]说明掩膜被存成了 8 位灰度需要除以 255 归一化到 0/1如果输出[0 1]且 dtype 是 uint8那就可以直接用。前景占比这个指标很实用舌头在 640×640 画面里通常占 10% 到 40%如果某张图占比接近 0 或接近 1大概率是标注异常训练前剔掉能省不少事。这份数据是二类分割输出通道数设 1 还是 2 取决于你的损失函数用 BCEWithLogitsLoss 就设 1 通道用 CrossEntropyLoss 就设 2 通道别混。3. 可视化脚本拆解原图、GT、叠加蒙板三合一怎么跑3.1 脚本的运行前提与依赖资源里带了一个可视化脚本随机抽一张图把原始图、GT 掩膜、GT 叠加在原图上的蒙板效果三张图展示出来并保存到当前目录。脚本号称不用改直接跑但「直接跑」有个前提你的工作目录得在数据集根目录或者脚本里写的是相对路径。我一般会先确认三件事——Python 环境有没有 PIL、matplotlib、numpy掩膜路径和图片路径是否和脚本里一致输出目录有没有写权限。pip install pillow matplotlib numpy python visualize.py如果报FileNotFoundError八成是路径问题。脚本里通常写死了类似train/images这样的相对路径你在别的目录下执行就会找不到。解决办法要么 cd 到数据集根目录再跑要么把脚本里的路径改成绝对路径。这一步没有玄学就是路径对齐。3.2 叠加蒙板的实现逻辑可视化里最有价值的是第三张「GT 在原图蒙板」的图它把掩膜以半透明色叠在原图上能一眼看出标注边界贴不贴合舌头轮廓。实现上就是把掩膜当布尔索引在原图对应位置涂色再按权重混合。import numpy as np import matplotlib.pyplot as plt from PIL import Image img np.array(Image.open(train/images/xxx.jpg).convert(RGB)) mask np.array(Image.open(train/masks/xxx_mask.png)) mask_bin (mask 0).astype(np.uint8) # 兼容 0/255 的情况 overlay img.copy() overlay[mask_bin 1] [255, 0, 0] # 前景涂红 blended (img * 0.6 overlay * 0.4).astype(np.uint8) fig, axes plt.subplots(1, 3, figsize(15, 5)) axes[0].imshow(img); axes[0].set_title(原图) axes[1].imshow(mask_bin, cmapgray); axes[1].set_title(GT) axes[2].imshow(blended); axes[2].set_title(叠加) for ax in axes: ax.axis(off) plt.savefig(vis_result.png, dpi150, bbox_inchestight) plt.show()mask 0这个判断同时兼容 0/1 和 0/255 两种存储是血泪经验——很多可视化脚本直接拿掩膜乘 255遇到本来就是 0/255 的图会过曝成全白。混合权重 0.6/0.4 是经验值前景色太淡看不清边界太浓又盖住原图纹理。保存用bbox_inchestight能去掉多余白边。跑完这张图你就能判断这份数据的标注质量到底能不能用比看任何文字描述都直接。4. 接入 U-Net 训练dataloader、损失函数与 640 分辨率取舍4.1 自定义 Dataset 的写法要把这份数据喂进 U-Net核心是写一个 Dataset 类把原图和掩膜同步读进来、同步做增强。同步是关键图像翻转了掩膜必须跟着翻否则标签就错位了。下面是一个最小可用的实现。import os import numpy as np import torch from torch.utils.data import Dataset from PIL import Image class TongueSegDataset(Dataset): def __init__(self, img_dir, mask_dir, transformNone): self.img_dir img_dir self.mask_dir mask_dir self.names [f for f in os.listdir(img_dir) if f.endswith(.jpg)] self.transform transform def __len__(self): return len(self.names) def __getitem__(self, idx): name self.names[idx] img Image.open(os.path.join(self.img_dir, name)).convert(RGB) mask_name os.path.splitext(name)[0] _mask.png mask Image.open(os.path.join(self.mask_dir, mask_name)).convert(L) img np.array(img, dtypenp.float32) / 255.0 mask (np.array(mask) 0).astype(np.float32) if self.transform: augmented self.transform(imageimg, maskmask) img, mask augmented[image], augmented[mask] img torch.from_numpy(img).permute(2, 0, 1) mask torch.from_numpy(mask).unsqueeze(0) return img, maskconvert(L)把掩膜强制转成单通道灰度避免三通道 png 带来的维度错乱。 0做二值化兼容不同存储。permute(2,0,1)把 HWC 转成 CHW这是 PyTorch 的要求。掩膜unsqueeze(0)加一个通道维配合 BCEWithLogitsLoss 使用。如果你用 albumentations 做增强记得 image 和 mask 一起传它内部会保证几何变换同步。4.2 损失函数与分辨率参数二类分割最常用的组合是 BCEWithLogitsLoss 加 Dice Loss。舌头区域占比不算极端纯 BCE 也能收敛但边界会偏糊加 Dice 能明显改善轮廓。640×640 这个分辨率对显存有一定要求batch size 设 4 到 8 比较稳显存不够就降到 2 或者把输入裁到 512。参数建议值说明输入尺寸640×640与原图一致避免缩放损失边界batch size4~8视显存调整8G 显存建议 4损失函数BCE Dice权重 0.5/0.5 起步优化器Adamlr1e-3配合余弦退火输出通道1配 BCEWithLogitsLoss训练时重点盯验证集的 Dice 和 IoU不要只看 loss。这份数据训练集 2127 张、测试集 537 张比例约 4:1测试集规模足够做可信评估。如果训练集 Dice 冲到 0.95 但测试集卡在 0.7那就是过拟合加增强或者加 dropout。常见做法是先把测试集当验证集用跑通流程后再从训练集里切一部分做验证。5. 避坑与排查掩膜读错、配对错位、显存爆掉这几件事5.1 掩膜读出来全是 0 或全是 255现象np.unique(mask)只有单一值或者前景占比接近 1。原因通常是 png 存成了调色板模式PIL 读出来是索引值而非真实像素或者掩膜本身标注就是全背景。解决用Image.open(...).convert(L)强制转灰度再读同时抽查几张图的可视化结果确认标注确实存在。如果整批都是全 0那这份掩膜可能没对齐得回去核对来源。5.2 原图和掩膜配对错位现象训练 loss 一直不降可视化叠加图里红色区域和舌头完全对不上。原因多半是排序不一致——os.listdir返回顺序不保证和掩膜目录一致如果你用索引去取掩膜而不是用文件名匹配就会错位。解决永远用文件名前缀匹配不要依赖列表顺序。上面 Dataset 里用os.path.splitext(name)[0] _mask.png就是靠名字配对稳。5.3 640 分辨率直接训练显存爆掉现象CUDA out of memory。原因640×640×3 的输入加上 U-Net 的多层特征图显存占用比 256 分辨率高好几倍。解决先把 batch size 降到 2还不够就用梯度累积模拟大 batch或者把输入随机裁到 512×512 训练、推理时再全图滑窗。别一上来就上 16 的 batch翻车概率极高。5.4 可视化脚本保存的图是空白现象跑完脚本保存的 png 打开是全白或全黑。原因matplotlib 在无显示环境下plt.show()不阻塞但保存时机不对或者掩膜归一化除了 255 导致全 0。解决保存放在show之前掩膜二值化用 0而不是/ 255并且确认dpi和bbox_inches参数没把内容裁掉。5.5 类别文本和实际掩膜对不上现象classes 文本里写的类别数和掩膜唯一值数量不一致。原因文本可能是模板没改或者掩膜里混入了其他灰度值。解决以掩膜实际唯一值为准先跑一遍全量统计确认只有 0 和 1 再训练。如果出现 2、3 这种值要么是标注工具导出问题要么是压缩伪影需要清洗。6. 进阶技巧用可视化脚本反查标注质量并做数据清洗可视化脚本不只是用来看效果的它其实是个标注质检工具。我的习惯是把它改成批量模式随机抽 20 张拼成网格图一眼扫过去就能发现哪些掩膜边界糊、哪些漏标、哪些把牙齿或嘴唇也标进了舌头。具体做法是把单张可视化逻辑包一层循环用subplot拼成 4×5 的网格每格显示叠加图保存成一张大图。import random import numpy as np import matplotlib.pyplot as plt from PIL import Image def batch_vis(img_dir, mask_dir, n20, cols5): names [f for f in os.listdir(img_dir) if f.endswith(.jpg)] sample random.sample(names, min(n, len(names))) rows (len(sample) cols - 1) // cols fig, axes plt.subplots(rows, cols, figsize(cols * 3, rows * 3)) for ax, name in zip(axes.flat, sample): img np.array(Image.open(os.path.join(img_dir, name)).convert(RGB)) mask_name os.path.splitext(name)[0] _mask.png mask np.array(Image.open(os.path.join(mask_dir, mask_name)).convert(L)) mask_bin (mask 0).astype(np.uint8) overlay img.copy() overlay[mask_bin 1] [255, 0, 0] blended (img * 0.6 overlay * 0.4).astype(np.uint8) ax.imshow(blended); ax.axis(off) for ax in axes.flat[len(sample):]: ax.axis(off) plt.savefig(batch_check.png, dpi120, bbox_inchestight) plt.close() batch_vis(train/images, train/masks, n20)跑完这张batch_check.png重点看三类问题边界是否贴合舌体轮廓、有没有把舌苔反光区域漏掉、有没有把口腔内壁误标成舌头。发现异常样本就记下文件名从训练集里移出去。这份数据 2127 张训练图移掉几十张问题样本对总量影响不大但能明显减少网络学偏的概率。清洗完再跑一次训练验证集 Dice 通常能涨一到两个点。另一个技巧是统计前景占比分布把占比低于 5% 或高于 60% 的样本单独拎出来看。舌头分割里占比过低的往往是拍摄角度偏或者舌头没伸出来占比过高的可能是把整个口腔都标了。用 numpy 批量算一遍几秒钟的事比训练完再回头找问题省太多时间。从那以后我每次拿到新的分割数据集都强制先跑一遍批量可视化加前景占比统计确认标注质量再动网络。这份舌头分割数据集结构清晰、命名规范、掩膜语义明确配上可视化脚本能快速完成质检适合直接拿来跑 U-Net 或者做舌象分析的预研基线。希望帮到你。本文还有配套的精品资源点击获取