尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

舌头分割数据集实战:从掩膜解析到训练流水线搭建

发布时间:2026/9/30 1:18:22

资讯中心
01
ARTICLE

舌头分割数据集实战:从掩膜解析到训练流水线搭建

舌头分割数据集实战:从掩膜解析到训练流水线搭建
简介这份资源面向从事医学图像处理、计算机视觉分割任务的开发者与研究者提供一套完整的舌头分割数据集可用于训练和评估二类语义分割模型。数据图像分辨率统一为640×640原图为jpg格式mask标签为png格式采用0背景、1舌头的阈值图像具体类别可在classes文本中查看。压缩包共约2000个文件以1998个png掩膜、1个txt类别说明和1个py可视化脚本为主整体约101.53MB。数据集划分为训练集与测试集训练集含2127张图片及2127个对应mask测试集含537张图片及537个对应mask目录结构清晰便于直接接入主流分割网络。随包附带的可视化脚本无需修改即可运行随机抽取一张图片同时展示原始图像、GT图像以及GT在原图上的蒙板效果并保存到当前目录方便快速检查标注质量。目前已有229人学习下载适合需要现成舌头分割数据、快速验证模型或搭建医学分割实验的读者参考使用。1. 舌头分割数据集上手640×640 的 2 类掩膜到底能干什么舌头分割这个方向做中医舌诊数字化、口腔内窥镜辅助分析、甚至智能牙刷的团队都会碰到。难点不在模型而在标注舌体边缘和嘴唇、牙齿、口腔黏膜的边界极其模糊让标注员凭肉眼抠图十个人能抠出十一种边界。这份资源给的就是一套已经标好的 2 类舌头分割数据集——2664 张 640×640 的 jpg 原图配 2664 张同名 png 掩膜像素值只有 0 和 10 是背景1 是舌头。训练集 2127 对测试集 537 对另外附一个可视化脚本跑一下就能把原图、GT、GT 叠在原图上的蒙板三张图并排存到当前目录。适合谁想快速验证 U-Net、DeepLab、SegFormer 这类分割网络在舌体场景下能不能跑通的人以及需要一份干净二分类掩膜做预处理流水线测试的人。它不解决“舌头关键点检测”或“舌苔分类”只解决“舌头在哪”这一件事但这件事做扎实了后面的活才好接。2. 拆开数据包目录结构、掩膜编码与类别定义2.1 训练/测试目录怎么摆文件名为什么带一串哈希拿到压缩包解压后常见做法是得到两个顶层目录一个训练集一个测试集每个下面再分images和masks。图片名形如-1847_30_-__jpg.rf.93ddfb0a0a77b1a007c024ea79eae2bc.jpg对应的掩膜就是把扩展名换成_mask.png前缀完全一致。那串rf.后面的哈希是数据导出时自动生成的唯一标识不要手动改也不要用它去反推原始来源——它只是防重名用的。真正要关心的是前缀里的数字段比如-1847_30这通常是采集时的样本编号和某种角度/批次标记但这份资源没有给出映射表所以别在这上面做文章按文件名配对就行。配对逻辑必须严格images里有多少张 jpgmasks里就该有多少张同名 png。我一般会先跑一段校验脚本确认没有孤儿文件再开始训练否则 DataLoader 会在某个 batch 突然报FileNotFoundError那时候再回头查很费时间。import os def check_pairs(img_dir, mask_dir): imgs {os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.endswith(.jpg)} masks {os.path.splitext(f)[0].replace(_mask, ) for f in os.listdir(mask_dir) if f.endswith(_mask.png)} only_img imgs - masks only_mask masks - imgs print(f图片数: {len(imgs)}, 掩膜数: {len(masks)}) print(f缺掩膜的图片: {len(only_img)} 张) print(f缺原图的掩膜: {len(only_mask)} 张) if only_img: print(示例:, list(only_img)[:3]) if only_mask: print(示例:, list(only_mask)[:3]) check_pairs(train/images, train/masks) check_pairs(test/images, test/masks)这段脚本做的是集合差运算。imgs收集所有 jpg 去掉扩展名的主名masks收集所有 png 去掉_mask后的主名两者相减就能暴露配对缺口。参数上唯一要注意的是路径分隔符Windows 下用反斜杠或原始字符串Linux/macOS 用正斜杠脚本里统一用正斜杠最省事。如果输出显示两边数量一致且差集为空说明目录结构是干净的可以进入下一步。2.2 掩膜像素值 0/1 意味着什么为什么不能直接当灰度图喂给网络掩膜是单通道 png像素值只有 0 和 1。0 代表背景1 代表舌头。这里有个血泪经验很多人用cv2.imread(mask_path)读进来默认 flag 是IMREAD_COLOR会把单通道复制成三通道值变成 0 和 255。如果你后面用binary_cross_entropy或dice_loss标签值突然从 0/1 变成 0/255loss 会直接炸掉梯度要么 NaN 要么把模型带偏。正确做法是显式指定cv2.IMREAD_GRAYSCALE读进来还是 0 和 1再按需转 float。import cv2 import numpy as np mask cv2.imread(train/masks/-1847_30_-__jpg.rf.93ddfb0a0a77b1a007c024ea79eae2bc_mask.png, cv2.IMREAD_GRAYSCALE) print(唯一值:, np.unique(mask)) # 应为 [0 1] print(形状:, mask.shape) # (640, 640) print(舌头像素占比: %.2f%% % (mask.sum() / mask.size * 100)) # 如果要做可视化叠加再转成 0/255 mask_vis (mask * 255).astype(np.uint8)IMREAD_GRAYSCALE保证不扩通道np.unique用来确认没有中间灰度值混进来。舌头像素占比这个指标值得看一眼如果某张图占比超过 80%可能是标注时把整个口腔都涂了如果低于 2%可能是漏标。这类异常样本在 2664 张里不会太多但一旦混进训练集模型会在这些图上产生很大的梯度干扰收敛。我一般会统计占比分布把超过 3 倍标准差的样本挑出来人工复查确认是真实场景还是标注失误。2.3 classes 文本与类别映射2 类到底怎么读资源里有一个 classes 文本文件里面列的就是类别名。按摘要描述0 是背景1 是舌头所以这个文件大概率只有两行或者一行写“background, tongue”。读的时候不要硬编码类别数而是从文件里解析这样以后如果扩展成多类比如舌苔、舌质分开代码不用大改。with open(classes.txt, r, encodingutf-8) as f: classes [line.strip() for line in f if line.strip()] print(类别列表:, classes) num_classes len(classes) print(类别数:, num_classes)解析时用strip()去掉行尾换行和空格空行跳过。num_classes后续传给模型的输出通道数——二分类分割通常输出 1 通道加 sigmoid或者 2 通道加 softmax两种都行但要和 loss 匹配。我一般用 1 通道 sigmoid BCE因为舌头分割本质是前景/背景二分类1 通道更省显存推理时阈值取 0.5 就够。3. 可视化脚本怎么跑三张图并排看 GT 叠原图3.1 脚本入口与依赖为什么说“不需要改直接运行”资源里带了一个可视化脚本描述说随机抽一张图展示原图、GT、GT 在原图上的蒙板并保存到当前目录。这类脚本通常依赖opencv-python、matplotlib、numpy有的还会用PIL。跑之前先确认环境里有这些包缺哪个补哪个。脚本“不需要改”的前提是它内部用了相对路径去读train/images和train/masks所以你得在数据集根目录下执行而不是在别的目录里用绝对路径调它。# 在数据集根目录下执行 python visualize.py如果脚本报FileNotFoundError九成是工作目录不对。先pwd确认自己在哪再ls看有没有train目录。另一种可能是脚本里写死了某个文件名但那份文件恰好不在——这种概率低因为描述说随机抽取一般会用random.choice从目录列表里选。3.2 三张图分别看什么原图、GT、叠加蒙板原图看的是舌头在画面里的位置、光照、有没有反光或遮挡。GT 看的是标注边界是否贴合舌体有没有把嘴唇或牙齿误标进来。叠加蒙板看的是对齐关系——如果 GT 和原图有偏移说明标注时可能用了不同的裁剪或缩放这种样本要警惕。我一般会连续跑几次可视化每次随机抽一张快速扫一遍不同批次的样本质量。import cv2 import numpy as np import matplotlib.pyplot as plt import random, os img_dir, mask_dir train/images, train/masks fname random.choice([f for f in os.listdir(img_dir) if f.endswith(.jpg)]) base os.path.splitext(fname)[0] img cv2.cvtColor(cv2.imread(os.path.join(img_dir, fname)), cv2.COLOR_BGR2RGB) mask cv2.imread(os.path.join(mask_dir, base _mask.png), cv2.IMREAD_GRAYSCALE) overlay img.copy() overlay[mask 1] [255, 0, 0] # 舌头区域涂红 fig, axes plt.subplots(1, 3, figsize(15, 5)) axes[0].imshow(img); axes[0].set_title(原图) axes[1].imshow(mask, cmapgray); axes[1].set_title(GT) axes[2].imshow(overlay); axes[2].set_title(GT 叠加) for ax in axes: ax.axis(off) plt.savefig(vis_result.png, dpi150, bbox_inchestight) plt.show()这段代码手动复现了可视化脚本的核心逻辑。random.choice随机选图cv2.cvtColor把 BGR 转 RGB 以便 matplotlib 正确显示overlay[mask 1] [255, 0, 0]把舌头像素涂成红色。保存时用bbox_inchestight去掉多余白边。如果你拿到的脚本输出和这个不一致比如颜色不对或图是黑的先检查通道顺序和 mask 读取 flag这两个地方最容易翻车。3.3 批量抽检把随机可视化变成固定流程单次随机看一张不够我一般会写个循环抽 20 张拼成网格快速判断整体标注质量。这样能在训练前就发现系统性问题比如某个批次的图普遍偏暗、或者某段编号的 mask 整体偏移。fig, axes plt.subplots(4, 5, figsize(20, 16)) files random.sample([f for f in os.listdir(img_dir) if f.endswith(.jpg)], 20) for ax, fname in zip(axes.ravel(), files): base os.path.splitext(fname)[0] img cv2.cvtColor(cv2.imread(os.path.join(img_dir, fname)), cv2.COLOR_BGR2RGB) mask cv2.imread(os.path.join(mask_dir, base _mask.png), cv2.IMREAD_GRAYSCALE) overlay img.copy() overlay[mask 1] [255, 0, 0] ax.imshow(overlay); ax.axis(off) plt.savefig(batch_check.png, dpi100, bbox_inchestight)random.sample保证不重复抽同一张4×5 网格一屏能看 20 张。如果发现超过 3 张有明显问题建议扩大抽检到 50 张确认问题比例再决定是否直接训练。4. 接进训练流水线Dataset 类、增强与损失函数选择4.1 写一个最小 Dataset把 640×640 的图对读进来PyTorch 的Dataset是标准入口。核心就三件事__init__里收集文件列表__len__返回样本数__getitem__读图、读 mask、做变换、返回 tensor。注意 mask 读进来是 0/1 的 uint8要转成 float32 并且加一个通道维度形状从(640, 640)变成(1, 640, 640)和模型输出对齐。import torch from torch.utils.data import Dataset import cv2, os import numpy as np class TongueSegDataset(Dataset): def __init__(self, img_dir, mask_dir, transformNone): self.img_dir img_dir self.mask_dir mask_dir self.files sorted([f for f in os.listdir(img_dir) if f.endswith(.jpg)]) self.transform transform def __len__(self): return len(self.files) def __getitem__(self, idx): fname self.files[idx] base os.path.splitext(fname)[0] img cv2.cvtColor(cv2.imread(os.path.join(self.img_dir, fname)), cv2.COLOR_BGR2RGB) mask cv2.imread(os.path.join(self.mask_dir, base _mask.png), cv2.IMREAD_GRAYSCALE) if self.transform: augmented self.transform(imageimg, maskmask) img, mask augmented[image], augmented[mask] img torch.from_numpy(img.transpose(2, 0, 1)).float() / 255.0 mask torch.from_numpy(mask).float().unsqueeze(0) return img, masksorted保证每次运行顺序一致方便复现。transpose(2, 0, 1)把 HWC 转 CHW/255.0归一化到 0~1。mask 用unsqueeze(0)加通道维值保持 0/1 不归一化——这一点很关键归一化 mask 会让 BCE loss 算错。4.2 增强怎么选几何变换要同步颜色变换只动原图舌头分割的增强有个铁律几何变换翻转、旋转、缩放必须同时作用在原图和 mask 上颜色变换亮度、对比度、色调只能动原图。用 albumentations 的话它天然支持image和mask一起传不会错位。如果手写增强翻转时记得对 mask 用同样的 flip 参数。import albumentations as A from albumentations.pytorch import ToTensorV2 train_transform A.Compose([ A.HorizontalFlip(p0.5), A.RandomRotate90(p0.3), A.ShiftScaleRotate(shift_limit0.05, scale_limit0.1, rotate_limit15, p0.5), A.RandomBrightnessContrast(p0.3), A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ToTensorV2() ])HorizontalFlip对舌头分割是安全的因为左右翻转后舌体结构仍然合理。ShiftScaleRotate的幅度不要太大shift_limit0.05、scale_limit0.1、rotate_limit15是我常用的保守值再大可能把舌头裁出画面。Normalize用 ImageNet 均值方差如果你从零训练可以换成数据集自身的统计值但用预训练 backbone 时保持一致更稳。4.3 损失函数与评估指标Dice BCE 组合IoU 看真实重叠二分类分割常用 BCE 或 Dice我一般两个加起来loss 0.5 * BCE 0.5 * Dice。BCE 对每个像素独立算Dice 看整体重叠组合起来对小目标和边界都更友好。评估时看 IoU 和 Dice 系数IoU 低于 0.7 说明边界还差得远0.85 以上在舌头分割里算可用。import torch.nn as nn class BCEDiceLoss(nn.Module): def __init__(self): super().__init__() self.bce nn.BCEWithLogitsLoss() def forward(self, logits, targets): bce_loss self.bce(logits, targets) probs torch.sigmoid(logits) intersection (probs * targets).sum() dice_loss 1 - (2 * intersection 1e-6) / (probs.sum() targets.sum() 1e-6) return 0.5 * bce_loss 0.5 * dice_lossBCEWithLogitsLoss内部做了 sigmoid所以传 logits 不要先过 sigmoid。Dice 里加1e-6防止除零。训练时如果 loss 震荡先把学习率降到 1e-4 试试再检查 mask 读取 flag 有没有搞错。5. 避坑与排查从 mask 读错到显存爆掉的五条记录5.1 现象loss 一直是 NaN或者第一轮就爆梯度原因mask 被读成 0/255 而不是 0/1BCE 对 255 的标签算出的 loss 极大梯度直接炸。解决统一用cv2.IMREAD_GRAYSCALE读 mask读完后np.unique确认只有 0 和 1。如果已经用了 0/255要么重新读要么在 Dataset 里加一句mask (mask 0).astype(np.uint8)二值化回来。5.2 现象训练集 loss 降得很快测试集 IoU 卡在 0.5 上不去原因训练集和测试集分布不一致或者增强过猛导致模型没学到真实边界。解决先关掉所有增强跑一轮看测试 IoU 有没有提升如果关增强后训练 loss 降不下去说明模型容量不够或学习率太低。另外检查测试集有没有被误当成训练集用过——我见过有人把test/images也加进train_loader结果评估时分数虚高换一批数据就崩。5.3 现象可视化脚本跑出来 mask 全黑或全白原因cv2.imread读 mask 时用了默认彩色 flag单通道被复制成三通道后值变成 0/255再用cmapgray显示时 255 显示为白、0 显示为黑看起来就是全黑或全白。解决读 mask 时显式加cv2.IMREAD_GRAYSCALE显示前确认np.unique输出是[0 1]。5.4 现象DataLoader 报FileNotFoundError但文件明明存在原因文件名里的哈希段包含特殊字符或者路径拼接时用了错误的扩展名。比如原图是.jpgmask 是_mask.png如果代码里把原图扩展名直接替换成.png而没加_mask就会找不到。解决打印出拼接后的完整路径和os.listdir的结果对比。另外注意 Windows 下路径长度限制数据集放在深层目录时可能超 260 字符挪到浅层目录即可。5.5 现象训练到一半显存爆掉batch size 明明设得很小原因640×640 的图在 U-Net 类模型里显存占用不低如果用了多尺度输出或深监督显存会成倍增加。解决先把 batch size 降到 2 或 1用torch.cuda.empty_cache()清理缓存如果还爆把输入 resize 到 512×512 或 384×384 训练推理时再恢复 640。另外检查有没有在验证阶段忘了torch.no_grad()验证图也会占显存。6. 进阶技巧用 537 张测试集做阈值搜索与边界后处理训练完模型只是第一步推理时的阈值和后处理对最终 IoU 影响很大。默认 0.5 阈值不一定最优我一般会在测试集上扫一遍 0.3 到 0.7每 0.05 一步看哪个阈值下 Dice 最高。537 张测试图足够做这个搜索不会过拟合——因为阈值只有一个参数搜索空间极小。import torch import numpy as np from tqdm import tqdm def search_threshold(model, loader, device): model.eval() all_probs, all_targets [], [] with torch.no_grad(): for imgs, masks in tqdm(loader): imgs imgs.to(device) logits model(imgs) probs torch.sigmoid(logits).cpu().numpy() all_probs.append(probs) all_targets.append(masks.numpy()) all_probs np.concatenate(all_probs) all_targets np.concatenate(all_targets) best_thr, best_dice 0.5, 0.0 for thr in np.arange(0.3, 0.75, 0.05): preds (all_probs thr).astype(np.float32) intersection (preds * all_targets).sum() dice (2 * intersection 1e-6) / (preds.sum() all_targets.sum() 1e-6) if dice best_dice: best_dice, best_thr dice, thr print(f最佳阈值: {best_thr:.2f}, Dice: {best_dice:.4f}) return best_thr这段代码把所有测试样本的预测概率和标签拼起来一次性算不同阈值下的 Dice。np.arange(0.3, 0.75, 0.05)生成 0.30、0.35 到 0.70 的候选值。注意拼接时内存占用537 张 640×640 的 float32 概率图大约 537×640×640×4 字节不到 900MB一般机器扛得住如果显存紧张可以分批算 Dice 再累加不必全拼。阈值定下来后还可以加一步形态学后处理先开运算去掉小噪点再闭运算填掉舌头内部的小孔。结构元大小取 3×3 或 5×5太大反而会腐蚀边界。import cv2 def postprocess(pred_mask, kernel_size3): kernel np.ones((kernel_size, kernel_size), np.uint8) pred_mask cv2.morphologyEx(pred_mask, cv2.MORPH_OPEN, kernel) pred_mask cv2.morphologyEx(pred_mask, cv2.MORPH_CLOSE, kernel) return pred_mask开运算先腐蚀后膨胀去掉孤立白点闭运算先膨胀后腐蚀填掉内部黑洞。顺序不能反反了会把该保留的边界磨掉。我一般只在推理后处理里用训练标签不动避免引入额外噪声。还有一个容易忽略的点测试集 537 张里可能包含训练集没出现过的光照条件或拍摄角度。如果阈值搜索后发现最佳阈值偏离 0.5 很远比如 0.35 或 0.65说明模型输出的概率分布偏了这时候除了调阈值更该回头检查训练集和测试集的亮度分布是否一致。我习惯在训练前把两个集合的均值方差各算一遍差太多就先做直方图匹配再喂给网络。从那以后我每次拿到新的分割数据集都强制走一遍“配对校验 → mask 唯一值检查 → 20 张批量可视化 → 阈值搜索”这四步少一步后面都可能翻车。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。