ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

舌头分割数据集实战:2664张标注图与0/1掩膜训练全流程

舌头分割数据集实战:2664张标注图与0/1掩膜训练全流程 简介本资源为舌头分割图像数据集面向医学图像处理、计算机视觉方向的学习者与算法开发者可用于训练和验证语义分割模型解决舌头区域自动提取与背景分离的问题。数据图像分辨率统一为640×640原图为jpg格式mask标签为png格式采用0/1阈值图像0代表背景、1代表舌头具体类别可在classes文本中查看。压缩包共约2000个文件以1998个png掩膜、1个txt类别说明和1个py可视化脚本为主整体约101.53MB分为训练集2127张图片及对应mask、测试集537张图片及对应mask。随包提供的可视化脚本无需修改即可直接运行随机抽取一张图片同时展示原始图像、GT图像以及GT在原图上的蒙板效果并保存到当前目录便于快速核对标注质量。目前已有231人学习适合需要现成数据快速开展分割实验或复现网络结构的读者。1. 舌头分割数据集从 2664 张标注图到可复现的二类分割流程舌头分割在中医舌诊数字化、口腔正畸术前评估、吞咽功能分析里都是绕不开的前置环节。我最近拆了一份二类舌头分割数据集2664 张 640×640 的 jpg 原图配 2664 张同名 png 掩膜训练集 2127 对、测试集 537 对掩膜是 0/1 阈值图0 是背景、1 是舌头类别定义单独放在 classes 文本里。它解决的是「没有干净标注就训不出可用分割模型」这个最实际的问题适合想跑通 unet 图像分割、yolov8 训练自己的数据集这类流程的从业者也适合做医学图像方向课程设计的学生。下面按「是什么 → 怎么用 → 坑在哪」把这份资源拆开讲透。2. 数据组织与掩膜语义先看懂 0/1 阈值图再谈训练2.1 目录结构与文件命名规律拿到压缩包解压后常见做法是看到两个顶层目录分别对应训练集和测试集每个目录下再分 images 和 masks 两个子目录。文件名形如-1847_30_-__jpg.rf.93ddfb0a0a77b1a007c024ea79eae2bc_mask.png原图与掩膜靠同一段哈希前缀对齐只是掩膜多了_mask后缀。这个命名方式决定了你写数据加载器时不能靠顺序索引必须靠文件名匹配否则训练集和测试集一旦打乱顺序就会图文错位。项目训练集测试集原图数量2127537掩膜数量2127537原图格式jpgjpg掩膜格式pngpng分辨率640×640640×640掩膜取值0 背景 / 1 舌头0 背景 / 1 舌头提示掩膜是单通道 png像素值只有 0 和 1不是 0/255。很多分割框架默认读进来是 0/255直接拿去算损失会出问题后面第 4 章会专门讲这个坑。2.2 0/1 掩膜与 0/255 掩膜的区别二类分割里掩膜有两种常见存法一种是 0/1 的整型图一种是 0/255 的灰度图。这份资源用的是前者好处是省空间、语义干净坏处是直接可视化时几乎全黑肉眼分不清舌头在哪。所以资源里附带了可视化脚本把 0/1 掩膜映射成可见的蒙板叠加在原图上。理解这一点很关键训练时你要的是 0/1 的标签可视化时你要的是 0/255 的显示两者不能混用。常见做法是在 Dataset 的__getitem__里把掩膜读成单通道断言取值集合是{0, 1}再转成 float32 或 long 张量。如果你用的是 unet 图像分割这类逐像素分类网络输出通道设 2标签用 long 张量配合交叉熵如果你用 sigmoid 单通道输出标签就得是 float 的 0/1配合二值交叉熵。选哪种取决于你的损失函数不是随便定的。2.3 类别定义与 classes 文本资源里有一个 classes 文本写明标签类别。二类分割的类别就是背景和舌头索引 0 和 1。别小看这个文件它决定了你后面画混淆矩阵、算 IoU 时类别名怎么对应。我一般会先把它读进来打印一遍确认没有多余空行或编码问题再写进配置。如果 classes 里写的是中文注意用 utf-8 读取Windows 下默认 gbk 会直接报错这是血泪经验。3. 可视化脚本拆解一张图看清原图、GT 与叠加蒙板3.1 脚本运行方式与输出资源自带一个图像分割可视化脚本随机抽一张图把原始图、GT 掩膜、GT 叠加在原图上的蒙板三张图展示出来并保存在当前目录。脚本不需要改路径直接运行即可。我一般会先跑这个脚本确认数据没坏再动训练代码。运行命令就是最普通的 python 调用python visualize.py跑完当前目录会多出几张 png分别是原图、GT、叠加图。如果这一步就报错说明环境缺 opencv 或 matplotlib先补依赖再往下走。3.2 可视化代码的关键逻辑脚本核心逻辑不复杂但有几个点值得抄。下面是我按这份资源的掩膜特性重写的等价逻辑关键行都加了注释import os import random import cv2 import numpy as np import matplotlib.pyplot as plt # 掩膜目录按实际解压路径改 mask_dir train/masks mask_files [f for f in os.listdir(mask_dir) if f.endswith(.png)] mask_name random.choice(mask_files) # 由掩膜文件名反推原图名去掉 _mask 后缀png 换 jpg img_name mask_name.replace(_mask.png, .jpg) img_path os.path.join(train/images, img_name) mask_path os.path.join(mask_dir, mask_name) # 原图按 BGR 读入转 RGB 供 matplotlib 显示 img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 掩膜必须按灰度单通道读否则 0/1 会被当三通道处理 mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) # 断言取值只有 0 和 1防止读到 0/255 的脏数据 assert set(np.unique(mask)).issubset({0, 1}), 掩膜不是 0/1 阈值图 # 把 0/1 映射成 0/255 才能肉眼看见 mask_vis (mask * 255).astype(np.uint8) # 叠加舌头区域涂红alpha 控制透明度 overlay img.copy() overlay[mask 1] [255, 0, 0] blended cv2.addWeighted(img, 0.6, overlay, 0.4, 0) fig, axes plt.subplots(1, 3, figsize(15, 5)) axes[0].imshow(img); axes[0].set_title(origin) axes[1].imshow(mask_vis, cmapgray); axes[1].set_title(GT mask) axes[2].imshow(blended); axes[2].set_title(overlay) for ax in axes: ax.axis(off) plt.savefig(vis_result.png, dpi150, bbox_inchestight) plt.show()逻辑说明第一步靠文件名反推原图这是这份数据集图文对齐的唯一可靠方式第二步用IMREAD_GRAYSCALE读掩膜保证拿到的是单通道第三步断言取值集合这是防止脏数据混入的第一道闸。参数说明addWeighted的 0.6/0.4 是原图与蒙板的权重想让舌头更明显就把蒙板权重调大cmapgray只影响显示不影响数据本身。3.3 从可视化反推数据质量跑完可视化别急着关盯着叠加图看几件事舌头边缘是否贴合、有没有整张全黑或全白的掩膜、有没有原图和掩膜明显不是同一张的情况。我一般会连续跑十几次随机抽样如果十几次里出现一张掩膜全 0那这批数据里大概率有漏标样本训练前得筛掉。这一步花五分钟能省后面几小时排查 loss 不下降的时间。4. 训练集与测试集划分2127 对与 537 对的加载器写法4.1 为什么不能按顺序索引配对前面提过文件名带哈希原图和掩膜不是简单的一一对应顺序。如果你写images[i]配masks[i]只要两个目录的os.listdir返回顺序不一致就会图文错位。正确做法是以掩膜文件名为基准反推原图名或者以原图名为基准加_mask后缀找掩膜。我一般用后者因为原图是训练输入以它为主更直观。4.2 一个可直接抄的 Dataset 实现下面这个 Dataset 按这份资源的命名规律写训练集测试集通用只改根目录即可import os import cv2 import numpy as np import torch from torch.utils.data import Dataset class TongueSegDataset(Dataset): def __init__(self, root, img_size640, transformNone): self.img_dir os.path.join(root, images) self.mask_dir os.path.join(root, masks) self.img_size img_size self.transform transform # 以原图为基准只保留能找到对应掩膜的样本 self.names [] for f in os.listdir(self.img_dir): if not f.lower().endswith(.jpg): continue mask_name f.replace(.jpg, _mask.png) if os.path.exists(os.path.join(self.mask_dir, mask_name)): self.names.append(f) assert len(self.names) 0, 没有找到图文配对样本 def __len__(self): return len(self.names) def __getitem__(self, idx): img_name self.names[idx] mask_name img_name.replace(.jpg, _mask.png) img cv2.imread(os.path.join(self.img_dir, img_name)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask cv2.imread(os.path.join(self.mask_dir, mask_name), cv2.IMREAD_GRAYSCALE) # 统一缩放到 640掩膜用最近邻避免插值出非 0/1 值 img cv2.resize(img, (self.img_size, self.img_size)) mask cv2.resize(mask, (self.img_size, self.img_size), interpolationcv2.INTER_NEAREST) # 掩膜转 float32 的 0/1配合 BCE 损失 mask (mask 0).astype(np.float32) img img.astype(np.float32) / 255.0 # HWC - CHW img torch.from_numpy(img).permute(2, 0, 1) mask torch.from_numpy(mask).unsqueeze(0) return img, mask逻辑说明初始化时做了一次配对校验找不到掩膜的原图直接跳过避免训练中途报错__getitem__里掩膜缩放必须用INTER_NEAREST用默认双线性会插出 0.3、0.7 这种值破坏 0/1 语义。参数说明img_size默认 640 与原图一致想省显存可以降到 320 或 256但掩膜同样要最近邻缩放transform留空要加增强自己接 albumentations。4.3 训练集与测试集的使用边界训练集 2127 对用来训测试集 537 对只用来评估别混。常见误用是把测试集也丢进训练指标虚高上线就翻车。我一般会再切一小部分训练集当验证集比如 10%用来早停和调学习率测试集留到最后只跑一次。这份资源没单独给验证集所以验证集得自己从训练集里分别偷懒直接用测试集调参。5. 避坑与排查0/1 掩膜、通道错位和指标虚高5.1 掩膜读进来全是 0 或全是 1现象可视化叠加图整张全红或全黑训练 loss 几乎不动。原因掩膜被当成 0/255 读或者缩放时用了双线性插值把 0/1 抹平。解决读掩膜强制IMREAD_GRAYSCALE缩放强制INTER_NEAREST并在 Dataset 里加断言set(np.unique(mask)).issubset({0,1})脏数据当场暴露。5.2 原图与掩膜对不上现象叠加图里舌头位置和原图明显错位或者掩膜形状和原图完全无关。原因按顺序索引配对两个目录os.listdir顺序不一致。解决以文件名哈希为基准配对原图名去掉.jpg加_mask.png找掩膜找不到就跳过别硬配。5.3 训练 loss 下降但 IoU 很低现象loss 一路降验证集 IoU 卡在 0.3 上不去。原因类别极不平衡舌头区域占比小模型全预测背景也能让 loss 降。解决换 Dice Loss 或 BCEDice 组合或者在 BCE 里加pos_weight给舌头类加权。我一般先用 Dice 跑一版看 IoU再用 BCE 微调。5.4 可视化脚本报编码错误现象Windows 下跑脚本报UnicodeDecodeError。原因classes 文本或路径含中文默认 gbk 解码失败。解决读文本统一加encodingutf-8路径尽量用英文别在中文目录下解压。5.5 测试集指标虚高现象测试集 IoU 比验证集高一大截。原因测试集被反复用来调参等于变相训练。解决测试集只在最后跑一次调参全在训练集切出的验证集上做。这是最常见的玄学来源别自己骗自己。6. 进阶技巧把 0/1 掩膜接进 unet 与 yolov8 分割流程6.1 接 unet 图像分割的最小改动unet 图像分割这类网络输出逐像素分类结果接这份数据只需两处改动输出通道设 2背景舌头损失用交叉熵标签转 long或者输出通道设 1 加 sigmoid损失用 BCE标签保持 float 0/1。我一般选后者因为这份掩膜本来就是 0/1省一次转换。学习率从 1e-3 起batch size 按显存给640 分辨率下 8 到 16 比较稳。6.2 接 yolov8 训练自己的数据集做分割yolov8 训练自己的数据集做分割时标签格式不是 png 掩膜而是多边形坐标的 txt。所以你得先把 0/1 掩膜转成多边形。常见做法是用cv2.findContours提取舌头轮廓再归一化成 yolov8 要求的格式import cv2 import numpy as np mask cv2.imread(mask.png, cv2.IMREAD_GRAYSCALE) # 二值化确保轮廓提取稳定 _, binary cv2.threshold(mask, 0, 255, cv2.THRESH_BINARY) contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) h, w mask.shape lines [] for cnt in contours: if cv2.contourArea(cnt) 50: # 过滤噪点小轮廓 continue cnt cnt.squeeze() if cnt.ndim ! 2 or len(cnt) 3: continue # 归一化坐标类别索引 0 表示舌头 pts .join(f{x/w:.6f} {y/h:.6f} for x, y in cnt) lines.append(f0 {pts}) with open(label.txt, w) as f: f.write(\n.join(lines))逻辑说明findContours拿外轮廓面积过滤掉噪点坐标除以宽高归一化类别索引按 classes 文本对应。参数说明面积阈值 50 是经验值舌头区域大可以调高CHAIN_APPROX_SIMPLE省点想更贴合可以换CHAIN_APPROX_NONE但文件会变大。6.3 验证方法先过拟合一张图不管接哪个框架我习惯先拿一张图过拟合看 loss 能不能降到接近 0、IoU 能不能到 0.9 以上。这一步能排除数据加载、标签语义、损失函数三类问题。如果一张图都过拟合不了别急着上全量先回头查掩膜是不是 0/1、配对是不是错位。从那以后我每次接新分割数据集都强制先跑一遍单图过拟合和十次随机可视化这两步过了再开全量训练。希望帮到你。本文还有配套的精品资源点击获取
返回列表