
简介本资源为舌头分割图像数据集面向医学图像处理、计算机视觉方向的学习者与算法开发者可用于训练和验证语义分割模型解决舌头区域自动提取与二分类分割任务。数据图像分辨率统一为640×640原图为jpg格式掩膜标签为png格式采用0/1阈值图像0代表背景、1代表舌头具体类别可在classes文本中查看。压缩包共约2000个文件以1998个png掩膜、1个txt类别说明和1个py可视化脚本为主整体约101.53MB。数据集划分为训练集与测试集训练集含2127张图片及2127个对应掩膜测试集含537张图片及537个对应掩膜目录结构清晰。配套可视化脚本无需修改即可直接运行随机抽取一张图片展示原始图像、GT图像及GT在原图上的蒙板效果并保存到当前目录便于快速检查标注质量。目前已有231人学习适合入门到中级的图像分割实践者参考使用。1. 舌头分割数据集从 2 类标签到可视化一套能直接跑通的落地路径拿到一个「舌头分割2类」数据集很多人第一反应是打开图片翻两页然后卡在同一个地方标签到底怎么读、类别 0 和 1 谁是谁、可视化代码跑出来一片黑。舌头分割在中医舌诊数字化、口腔影像分析里是个高频需求2 类通常指背景与舌体任务本质是二分类语义分割。它适合想快速验证 unet 图像分割流程、又不想从零标注的从业者。这篇笔记按「数据集结构 → 标签解析 → 可视化 → 训练接入 → 避坑」推一遍目标是你照着能复现而不是看完只知道有这么个东西。2. 舌头分割数据集的结构与 2 类标签到底怎么读2.1 目录组织与文件命名约定常见做法是 images 与 masks或 labels两个平行目录文件名一一对应。真正要确认的是三件事图像格式、掩膜格式、命名是否严格对齐。我一般先跑一段统计脚本把扩展名分布、尺寸分布、文件名交集一次性打出来避免后面训练时才发现有图无标签。import os from collections import Counter from PIL import Image root tongue_seg # 数据集根目录 img_dir os.path.join(root, images) mask_dir os.path.join(root, masks) def scan(folder): files [f for f in os.listdir(folder) if not f.startswith(.)] ext Counter(os.path.splitext(f)[1].lower() for f in files) return files, ext imgs, img_ext scan(img_dir) masks, mask_ext scan(mask_dir) print(图像数量:, len(imgs), 扩展名:, img_ext) print(掩膜数量:, len(masks), 扩展名:, mask_ext) # 文件名去扩展名交集检查是否一一对应 stem_i {os.path.splitext(f)[0] for f in imgs} stem_m {os.path.splitext(f)[0] for f in masks} print(仅图像有:, len(stem_i - stem_m), 仅掩膜有:, len(stem_m - stem_i))逻辑说明先看数量是否相等再看扩展名是否统一最后看文件名交集。参数上root换成你的实际路径即可如果仅图像有或仅掩膜有不为 0说明存在孤儿文件训练前必须处理否则 dataloader 会在某个 batch 直接抛异常。2.2 2 类标签的像素值与语义映射2 类分割最常见的坑是像素值不是 0/1而是 0/255甚至 0/128。直接拿去算交叉熵类别数对不上就会报错或静默学错。先统计掩膜里出现的唯一像素值这一步是黑匣子打开的关键。import numpy as np from PIL import Image sample os.path.join(mask_dir, masks[0]) m np.array(Image.open(sample)) vals, counts np.unique(m, return_countsTrue) print(唯一像素值:, vals) print(占比:, (counts / counts.sum()).round(4))如果输出是[0 255]那类别 1 需要映射成 1如果是[0 1]可直接用。映射方式def to_binary(mask): # 大于 127 视为舌体否则背景按你的实际像素值调整阈值 return (mask 127).astype(np.uint8)参数说明阈值 127 适用于 0/255 的情况若掩膜是 0/128阈值取 64 更稳。判断依据是唯一像素值的中位数不要凭感觉写。类别语义上0 一般是背景1 是舌体但务必用可视化确认别默认。2.3 尺寸、通道与长宽比分布舌头图像往往来自手机或相机尺寸不统一。训练前要么统一 resize要么用 padding。先看分布再决定策略比拍脑袋设 256×256 靠谱。sizes Counter() modes Counter() for f in imgs[:200]: # 抽样 200 张足够看趋势 with Image.open(os.path.join(img_dir, f)) as im: sizes[im.size] 1 modes[im.mode] 1 print(尺寸 Top5:, sizes.most_common(5)) print(色彩模式:, modes)如果尺寸种类超过 20 种建议统一 resize 到 256×256 或 512×512如果长宽比差异大直接 resize 会拉伸舌体改用短边 resize 中心裁剪更稳。色彩模式若是 RGBA记得转 RGB否则三通道模型读进去会多一维。3. 数据可视化代码把标签叠回原图看清对错3.1 单张叠加可视化的最小实现可视化不是锦上添花它是你发现标签错位、掩膜反色、类别颠倒的唯一低成本手段。下面这段把原图、掩膜、叠加图并排画出来。import matplotlib.pyplot as plt import numpy as np from PIL import Image def visualize(img_path, mask_path, alpha0.5): img np.array(Image.open(img_path).convert(RGB)) mask np.array(Image.open(mask_path).convert(L)) mask_bin (mask 127).astype(np.uint8) overlay img.copy() overlay[mask_bin 1] [255, 0, 0] # 舌体区域涂红 blended (img * (1 - alpha) overlay * alpha).astype(np.uint8) fig, ax plt.subplots(1, 3, figsize(12, 4)) ax[0].imshow(img); ax[0].set_title(原图) ax[1].imshow(mask_bin, cmapgray); ax[1].set_title(二值掩膜) ax[2].imshow(blended); ax[2].set_title(叠加) for a in ax: a.axis(off) plt.tight_layout(); plt.show() visualize(os.path.join(img_dir, imgs[0]), os.path.join(mask_dir, masks[0]))逻辑说明convert(L)保证掩膜单通道alpha控制叠加透明度0.5 通常够看。若叠加后红色区域明显偏移说明图像与掩膜尺寸不一致或存在配准问题这类样本要单独挑出来。3.2 批量网格预览与异常样本筛查单张看效率低批量拼网格能快速扫出异常。重点看三类全黑掩膜、全白掩膜、叠加后边界明显错位。def grid_preview(n16, cols4): idx np.random.choice(len(imgs), min(n, len(imgs)), replaceFalse) rows (len(idx) cols - 1) // cols fig, axes plt.subplots(rows, cols, figsize(cols * 3, rows * 3)) for ax, i in zip(axes.ravel(), idx): img np.array(Image.open(os.path.join(img_dir, imgs[i])).convert(RGB)) mask np.array(Image.open(os.path.join(mask_dir, masks[i])).convert(L)) mask_bin (mask 127).astype(np.uint8) overlay img.copy() overlay[mask_bin 1] [255, 0, 0] ax.imshow((img * 0.5 overlay * 0.5).astype(np.uint8)) ax.set_title(imgs[i][:12], fontsize8); ax.axis(off) for ax in axes.ravel()[len(idx):]: ax.axis(off) plt.tight_layout(); plt.show() grid_preview()参数说明n是抽样数量16 张够日常巡检cols控制列数。若发现某张掩膜全黑先查该文件是否损坏再查是否像素值全为 0全白同理。这类样本在训练中会拉低指标建议先剔除或重新标注。3.3 类别像素占比统计2 类分割里背景通常远多于舌体类别不平衡会直接影响 loss 表现。先算全局占比再决定要不要加权。pos, total 0, 0 for f in masks: m np.array(Image.open(os.path.join(mask_dir, f)).convert(L)) pos (m 127).sum() total m.size print(舌体像素占比: %.4f % (pos / total))如果占比低于 0.1训练时建议用 Dice Loss 或带权交叉熵否则模型容易全预测背景也能拿到高准确率指标好看但没用。这一步的数值直接决定你后面 loss 怎么选别跳过。4. 接入 unet 图像分割训练从 Dataset 到第一个 epoch4.1 自定义 Dataset 与同步增强舌头分割数据量通常不大增强是刚需。关键是图像和掩膜必须同步变换否则标签就废了。下面用 albumentations 做同步增强。import cv2 import torch from torch.utils.data import Dataset import albumentations as A from albumentations.pytorch import ToTensorV2 train_tf A.Compose([ A.Resize(256, 256), A.HorizontalFlip(p0.5), A.RandomBrightnessContrast(p0.3), A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ToTensorV2(), ]) class TongueSeg(Dataset): def __init__(self, img_dir, mask_dir, files, tf): self.img_dir, self.mask_dir img_dir, mask_dir self.files, self.tf files, tf def __len__(self): return len(self.files) def __getitem__(self, i): name self.files[i] img cv2.imread(os.path.join(self.img_dir, name))[:, :, ::-1] mask cv2.imread(os.path.join(self.mask_dir, name), 0) mask (mask 127).astype(float32) out self.tf(imageimg, maskmask) return out[image], out[mask].unsqueeze(0)逻辑说明cv2.imread读进来是 BGR[:, :, ::-1]转 RGB掩膜用灰度读阈值化后转 float32。A.Compose同时接收 image 和 mask保证几何变换一致。参数上Resize(256,256)可按显存调整Normalize用 ImageNet 均值方差是分割任务的常规起点。4.2 训练循环与 loss 选择2 类分割常用 BCEWithLogitsLoss 或 Dice Loss。数据不平衡时两者结合更稳。import torch.nn as nn bce nn.BCEWithLogitsLoss() def dice_loss(logits, targets, eps1e-6): probs torch.sigmoid(logits) num 2 * (probs * targets).sum(dim(2, 3)) den probs.sum(dim(2, 3)) targets.sum(dim(2, 3)) eps return 1 - (num / den).mean() def criterion(logits, targets): return bce(logits, targets) dice_loss(logits, targets)参数说明eps防止除零两项相加时若 Dice 波动大可给 Dice 乘 0.5 权重。训练循环里记得optimizer.zero_grad()、loss.backward()、optimizer.step()三步齐全别漏。第一个 epoch 后先看验证集 Dice低于 0.5 通常说明标签映射或增强有问题回去查第 2 章。4.3 验证指标与可视化回查训练中只看 loss 不够2 类分割要看 Dice 和 IoU。每个 epoch 后在验证集上算一次并抽几张预测图叠加回看。torch.no_grad() def evaluate(model, loader, device): model.eval() inter, union 0, 0 for x, y in loader: x, y x.to(device), y.to(device) pred (torch.sigmoid(model(x)) 0.5).float() inter (pred * y).sum().item() union (pred y - pred * y).sum().item() return inter / (union 1e-6) # IoU参数说明阈值 0.5 是默认起点若召回偏低可降到 0.4。IoU 低于 0.6 时优先回查掩膜是否反色、图像掩膜是否错位这两类问题在舌头分割里出现频率最高。5. 舌头分割落地避坑5 个真实翻车现场5.1 掩膜反色导致模型学反现象训练 loss 下降但验证 Dice 一直 0.3 左右预测图里舌体被标成背景。原因部分掩膜像素值是 0/255但语义与预期相反或阈值化时把舌体判成背景。解决用第 2.2 节的唯一像素值统计逐张确认语义必要时对调映射。5.2 图像与掩膜尺寸不一致现象叠加可视化时红色区域整体偏移或缩放。原因图像和掩膜在采集或后处理时被分别 resize尺寸对不上。解决训练前统一检查img.size mask.size不一致的样本要么重新对齐要么剔除。5.3 文件名不对齐导致静默丢样本现象训练集数量比预期少但程序不报错。原因Dataset 用文件名交集构建孤儿文件被静默跳过。解决第 2.1 节的交集检查必须跑仅图像有和仅掩膜有都为 0 才继续。5.4 增强不同步污染标签现象训练几个 epoch 后 Dice 突然掉。原因图像做了翻转或旋转掩膜没跟着变。解决用 albumentations 的 Compose 同时传 image 和 mask不要分别处理。5.5 类别不平衡导致全背景预测现象准确率很高但 Dice 很低预测图几乎全黑。原因舌体像素占比过低模型学到全预测背景。解决先算占比低于 0.1 时加 Dice Loss 或给正类加权权重可设为1 / 占比量级。6. 把 2 类舌头分割推到可用阈值调优与边界后处理训练跑通只是起点真正决定落地效果的是推理阶段的阈值和后处理。2 类分割的边界往往毛糙直接二值化会有零星噪点。我一般先扫一遍阈值再决定要不要做形态学处理。import numpy as np import cv2 def postprocess(prob, thr0.5, kernel3): mask (prob thr).astype(np.uint8) k np.ones((kernel, kernel), np.uint8) mask cv2.morphologyEx(mask, cv2.MORPH_OPEN, k) # 去噪点 mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, k) # 补空洞 return mask阈值扫描可以这样写在验证集上从 0.3 到 0.7 每隔 0.05 算一次 Dice取最高点。参数上kernel3适合 256 尺寸若原图是 512可调到 5。开运算去孤立噪点闭运算补内部空洞顺序不要反反了会把噪点连成片。阈值典型 Dice适用场景0.3召回高、边界偏大舌体占比小、宁可多标0.5均衡默认起点0.7精度高、边界偏紧噪点多、要求干净后处理不是必须但舌头分割的边界通常需要它。我的习惯是先不加后处理跑一版基线再加上去对比 Dice 和视觉效果只有提升明显才保留。曾经为了追求边界平滑把 kernel 调到 9结果舌体边缘被啃掉一圈Dice 反而掉了 4 个点这个后悔药我吃过一次就够了。阈值和后处理都要用验证集说话别凭肉眼调。希望帮到你。本文还有配套的精品资源点击获取