简介:本资源为舌头分割图像数据集,面向医学图像处理、计算机视觉方向的学习者与算法开发者,可用于训练和评估语义分割模型,解决舌头区域自动提取与背景分离问题。包内共约2000个文件,以png掩膜标签为主,另含1个类别说明txt与1个可视化py脚本,压缩包约101.53MB。数据分为训练集与测试集:训练集含2127张jpg原图及2127张对应mask,测试集含537张原图及537张mask,图像分辨率统一为640×640,mask为0/1阈值图像,0代表背景、1代表舌头,具体类别可在classes文本中查看。配套可视化脚本无需修改即可直接运行,随机抽取一张图片,同时展示原始图像、GT图像以及GT在原图上的蒙板效果,并保存至当前目录,便于快速核验标注质量。目前已有229人学习下载,适合需要现成数据与可视化工具开展分割实验的读者。
1. 舌头分割数据集:从原始图像到二类掩码,一条能跑通的链路
舌头图像分割在中医舌诊数字化、口腔正畸评估、吞咽功能分析里都是绕不开的前置环节。拿到一份「图像分割数据集:舌头分割(2类)【数据集+标签文件+数据可视化代码】」,多数人第一反应是打开图片看看,然后卡在三个地方:标签到底怎么编码的、2类指的是哪两类、可视化代码怎么把掩码叠回原图。这份数据集的价值不在于图片数量,而在于它把「图像—标签—可视化」这条链路完整交到你手上,让你能直接进入训练环节,而不是花两周做标注。它适合三类人:想入门医学图像分割的算法工程师、需要舌体区域做下游特征提取的研究者、以及拿它当分割 pipeline 练手的学生。下面按「先看懂标签、再跑通可视化、最后接上训练」的顺序拆开讲,每一步都给出可复现的命令和参数。
2. 拆开数据集:2类标签的编码方式与目录结构
2.1 2类分割到底分的是哪两类
「2类」在分割任务里通常有两种含义:一种是前景/背景二分类,标签只有 0 和 1;另一种是语义类别数为 2,比如舌体与背景各占一个类别索引。舌头分割数据集绝大多数采用前者——舌体为前景(像素值 1 或 255),其余为背景(像素值 0)。判断方法很直接:读一张标签图,看唯一值集合。
import numpy as np from PIL import Image mask = np.array(Image.open("labels/0001.png")) print("shape:", mask.shape) print("dtype:", mask.dtype) print("unique values:", np.unique(mask))这段代码做三件事:读取标签、打印尺寸和数据类型、列出所有唯一像素值。如果unique values输出[0 1],说明是标准二值掩码;如果输出[0 255],训练前需要除以 255 归一化到 0/1;如果出现[0 1 2],那才是真正的多类语义标签,此时 2 类指的是类别索引 0 和 1 之外还有别的含义,要回去核对说明。dtype也要留意,bool类型直接可用,uint8需要确认取值范围。
2.2 目录结构与文件命名对齐
常见做法是 images 和 labels 两个平行目录,文件名一一对应。但血泪经验是:不少数据集图片是.jpg,标签是.png,扩展名不一致导致按文件名匹配时漏样本。先做一次对齐检查。
# 列出两个目录的文件名(去掉扩展名)并比对 ls images | sed 's/\.[^.]*$//' | sort > /tmp/img_names.txt ls labels | sed 's/\.[^.]*$//' | sort > /tmp/lbl_names.txt diff /tmp/img_names.txt /tmp/lbl_names.txtsed 's/\.[^.]*$//'去掉最后一个点及其后缀,sort保证顺序一致,diff输出差异。如果 diff 有输出,说明存在图片没有对应标签或反之,训练时 DataLoader 会在这些样本上翻车。处理办法是以标签为准过滤图片,或补齐缺失标签。这个检查花不到十秒,能省掉训练到一半报错的后悔药。
2.3 图像尺寸与标签尺寸必须一致
分割任务里图像和标签的宽高必须严格相等,否则叠加可视化会错位,训练时 resize 也会引入标签插值问题。批量检查一遍:
import os from PIL import Image img_dir, lbl_dir = "images", "labels" mismatch = [] for name in os.listdir(lbl_dir): stem = os.path.splitext(name)[0] img_path = None for ext in (".jpg", ".png", ".jpeg", ".bmp"): p = os.path.join(img_dir, stem + ext) if os.path.exists(p): img_path = p break if img_path is None: mismatch.append((name, "no image")) continue w1, h1 = Image.open(img_path).size w2, h2 = Image.open(os.path.join(lbl_dir, name)).size if (w1, h1) != (w2, h2): mismatch.append((name, (w1, h1), (w2, h2))) print("mismatch count:", len(mismatch)) for m in mismatch[:10]: print(m)逻辑是遍历标签目录,为每个标签找同名图片(尝试多种扩展名),比较尺寸。mismatch列表收集所有异常,打印前十条便于定位。参数上,扩展名元组按你数据集实际情况增删。如果尺寸不一致,常见做法是统一 resize 到 256×256 或 512×512,但标签必须用最近邻插值,否则会出现 0.5 这种非法类别值。
3. 数据可视化代码:把掩码叠回原图看清楚
3.1 单张可视化:轮廓叠加与彩色掩码
可视化代码的核心作用有两个:确认标签质量、给论文或报告出图。最实用的方式是原图上叠加半透明彩色掩码加轮廓线。
import numpy as np import matplotlib.pyplot as plt from PIL import Image def visualize_one(img_path, mask_path, alpha=0.45): img = np.array(Image.open(img_path).convert("RGB")) mask = np.array(Image.open(mask_path)) mask_bin = (mask > 0).astype(np.uint8) # 兼容 0/1 与 0/255 color = np.zeros_like(img) color[..., 0] = 255 # 红色掩码 overlay = img.copy() overlay[mask_bin == 1] = ( (1 - alpha) * img[mask_bin == 1] + alpha * color[mask_bin == 1] ).astype(np.uint8) fig, axes = plt.subplots(1, 3, figsize=(15, 5)) axes[0].imshow(img); axes[0].set_title("image") axes[1].imshow(mask_bin, cmap="gray"); axes[1].set_title("mask") axes[2].imshow(overlay); axes[2].set_title("overlay") for ax in axes: ax.axis("off") plt.tight_layout() plt.savefig("vis_one.png", dpi=150) plt.show() visualize_one("images/0001.jpg", "labels/0001.png")mask > 0把 0/255 和 0/1 两种编码统一成二值,避免因编码差异导致掩码全黑。alpha控制透明度,0.45 在多数舌象上既能看清掩码边界又不遮住舌质纹理。color[..., 0] = 255只改红色通道,得到红色掩码。三栏并排输出便于对比。保存用dpi=150,够报告用,论文可提到 300。
3.2 批量可视化:网格图快速筛查脏标签
单张看效率低,批量出网格图能一眼扫出标注异常样本。
import os import numpy as np import matplotlib.pyplot as plt from PIL import Image def grid_vis(img_dir, lbl_dir, n=16, cols=4, save="vis_grid.png"): names = sorted(os.listdir(lbl_dir))[:n] rows = (len(names) + cols - 1) // cols fig, axes = plt.subplots(rows, cols, figsize=(cols * 3, rows * 3)) axes = np.array(axes).reshape(-1) for ax, name in zip(axes, names): stem = os.path.splitext(name)[0] img_path = None for ext in (".jpg", ".png", ".jpeg", ".bmp"): p = os.path.join(img_dir, stem + ext) if os.path.exists(p): img_path = p break if img_path is None: ax.axis("off"); continue img = np.array(Image.open(img_path).convert("RGB")) mask = np.array(Image.open(os.path.join(lbl_dir, name))) mask_bin = (mask > 0).astype(np.uint8) overlay = img.copy() overlay[mask_bin == 1] = (0.55 * img[mask_bin == 1] + 0.45 * np.array([255, 0, 0])).astype(np.uint8) ax.imshow(overlay); ax.set_title(stem, fontsize=8); ax.axis("off") for ax in axes[len(names):]: ax.axis("off") plt.tight_layout() plt.savefig(save, dpi=120) plt.show() grid_vis("images", "labels", n=16, cols=4)n控制展示数量,cols控制列数,rows自动算。axes[len(names):]把多余子图关掉,避免空白格。批量看的时候重点盯三类问题:掩码缺失(全黑)、掩码溢出(覆盖到脸颊或牙齿)、掩码断裂(舌体中间有洞)。这些脏标签不处理,训练 loss 会震荡,模型学出玄学边界。
3.3 可视化代码里最容易忽略的通道顺序
PIL 读出来是 RGB,OpenCV 读出来是 BGR,matplotlib 按 RGB 显示。三者混用是可视化翻车的高频原因——掩码叠上去颜色对,但原图偏蓝。统一用 PIL 或统一用 cv2 加cv2.cvtColor(img, cv2.COLOR_BGR2RGB)。另外标签若是单通道,imshow要指定cmap="gray",否则 matplotlib 会套默认色图,看起来像多类标签,误导判断。
4. 接上训练:从这份数据集到可用的分割模型
4.1 转成通用训练格式
多数分割框架吃的是「图像 + 单通道掩码」配对,或 COCO/VOC 格式。这份数据集已经是配对形式,最省事的是写一个 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, lbl_dir, size=(256, 256)): self.img_dir = img_dir self.lbl_dir = lbl_dir self.size = size self.names = [n for n in sorted(os.listdir(lbl_dir)) if os.path.splitext(n)[0] + ".jpg" in os.listdir(img_dir) or os.path.splitext(n)[0] + ".png" in os.listdir(img_dir)] def __len__(self): return len(self.names) def __getitem__(self, idx): name = self.names[idx] stem = os.path.splitext(name)[0] img_path = None for ext in (".jpg", ".png", ".jpeg", ".bmp"): p = os.path.join(self.img_dir, stem + ext) if os.path.exists(p): img_path = p break img = Image.open(img_path).convert("RGB").resize(self.size, Image.BILINEAR) mask = Image.open(os.path.join(self.lbl_dir, name)).resize(self.size, Image.NEAREST) img = np.array(img, dtype=np.float32) / 255.0 mask = (np.array(mask) > 0).astype(np.float32) img = torch.from_numpy(img).permute(2, 0, 1) mask = torch.from_numpy(mask).unsqueeze(0) return img, mask关键参数:size统一到 256×256,图像用双线性插值,标签用最近邻,这是铁律。mask > 0兼容两种编码。permute(2,0,1)把 HWC 转 CHW 适配 PyTorch。unsqueeze(0)给掩码加通道维,变成 1×H×W。__init__里的 names 过滤保证只保留有配对图片的标签,避免运行时 FileNotFoundError。
4.2 训练时的损失与指标选择
二类分割用 BCEWithLogitsLoss 或 Dice Loss,两者结合更稳。BCE 对类别不平衡敏感,舌体占比小的时候背景会主导梯度,Dice 能拉回来。指标看 IoU 和 Dice,不要只看 accuracy——全预测背景也能有 80% 以上 accuracy,但 IoU 为 0。
import torch import torch.nn as nn bce = nn.BCEWithLogitsLoss() def dice_loss(logits, targets, eps=1e-6): probs = torch.sigmoid(logits) inter = (probs * targets).sum(dim=(2, 3)) union = probs.sum(dim=(2, 3)) + targets.sum(dim=(2, 3)) return 1 - ((2 * inter + eps) / (union + eps)).mean() def total_loss(logits, targets, w=0.5): return w * bce(logits, targets) + (1 - w) * dice_loss(logits, targets)eps防止除零,w=0.5是常用起点,舌体占比特别小时可调到 0.3 让 Dice 权重更大。sigmoid只在 Dice 里用,BCEWithLogitsLoss 内部已含 sigmoid,不要重复。
4.3 验证集划分与过拟合判断
这份数据集若样本量不大,按 8:1:1 划分,且要保证同一受试者的图片不跨集。判断过拟合看训练 loss 持续降而验证 IoU 连续多个 epoch 不升。此时优先加数据增强(水平翻转、小角度旋转、亮度抖动),而不是加 dropout。舌体左右近似对称,水平翻转安全;垂直翻转会让舌根朝上,不符合解剖结构,别用。
5. 避坑与排查:舌头分割数据集常见的五个翻车点
现象一:可视化掩码全黑。原因:标签编码是 0/255,代码里直接当 0/1 用,mask == 1匹配不到任何像素。解决:统一用mask > 0转二值,或在 Dataset 里除以 255。
现象二:训练 loss 一直不降。原因:图像和标签没对齐,比如 resize 时一个用双线性一个用最近邻导致边界错位,或文件名匹配错位读了别的样本的标签。解决:先跑 3.3 的网格可视化确认配对正确,再检查 resize 插值方式。
现象三:验证 IoU 高但实际预测全是背景。原因:只看 accuracy 或 loss,没看 IoU。背景占比大时模型偷懒全预测背景。解决:监控 Dice 和 IoU,损失里加 Dice 项,必要时对前景像素加权。
现象四:不同图片尺寸混在一起训练报错。原因:数据集里图像尺寸不统一,DataLoader 的 batch 拼接失败。解决:Dataset 里强制 resize 到固定尺寸,或自定义 collate_fn 做 padding。推荐前者,简单可靠。
现象五:可视化颜色正常但保存的图偏色。原因:matplotlib 保存时用了默认色彩空间,或 cv2 读图 BGR 没转 RGB。解决:统一 PIL 读图,保存前确认数组是 RGB 且 dtype 为 uint8,savefig不要开transparent叠在非白底上。
6. 进阶技巧:用可视化反推标签质量并做半自动清洗
可视化不只是出图,它能反过来当质检工具。我的习惯是训练前先跑一遍全量网格图,按掩码面积排序,面积异常小或异常大的排前面重点看。面积占比低于 1% 的可能是漏标,高于 60% 的可能是把整张脸都标进去了。用下面这段算面积分布,比一张张看快得多。
import os import numpy as np from PIL import Image areas = [] for name in sorted(os.listdir("labels")): mask = np.array(Image.open(os.path.join("labels", name))) ratio = (mask > 0).mean() areas.append((name, ratio)) areas.sort(key=lambda x: x[1]) print("smallest 5:", areas[:5]) print("largest 5:", areas[-5:])(mask > 0).mean()直接给出前景像素占比,排序后两头就是可疑样本。正常舌体在正面照里占比大概 15% 到 45%,超出这个范围先怀疑标签。发现脏标签后,可以只对这些样本重新标注或剔除,不必全量返工。
另一个技巧是把预测结果和原始标签叠在一起做对比图,训练几个 epoch 后跑一次,能直观看出模型是欠拟合(掩码比标签小一圈)还是过拟合(掩码贴着标签但泛化差)。我一般每 10 个 epoch 存一张对比图,比盯 loss 曲线直观。这套流程跑下来,从拿到数据集到出一个能用的二类分割模型,顺利的话半天到一天。真正花时间的从来不是写模型,而是把标签看清楚、把可视化跑通、把脏样本挑出来。希望帮到你。
本文还有配套的精品资源,点击获取