资讯动态

舌头分割数据集实战:标签转换、可视化与训练接入

发布时间:2026/9/15 5:28:22 来源:尧图企业网站定制
简介面向计算机视觉与医学图像分割场景的舌头分割数据集图像分辨率统一为640×640jpg原图搭配png格式的0/1阈值mask标签像素分布为0背景、1舌头类别清晰可直接用于主流分割模型的训练与评估。数据已划分为训练集2127张图片2127个mask与测试集537张图片537个mask并附带类别说明txt与可直接运行的数据可视化py脚本——脚本会随机抽取一张图像将原图、GT掩膜以及GT叠加在原图上的效果保存到当前目录方便快速查验标注质量、生成效果对比图。资源共2000个文件以png图像/掩膜为主另有1个txt和1个py脚本压缩包总大小101.53MB。已有229人学习下载适合正在学习图像分割、需要构造舌头分割训练数据或进行分割算法验证的开发者与研究者。1. 舌头分割数据集为什么难在标签文件上做过医学图像分割的人大多有这种感觉模型结构反而不是瓶颈找一份能直接用的分割数据集才是。舌头分割2类表面上只是“舌头”和“背景”两个类别但真正拿图片去标的时候会发现舌头根部与咽喉暗区边界模糊、舌苔颜色和嘴唇颜色接近、患者伸出舌头的姿态差异大这些都会让标签文件出现肉眼难查的错位和漏标。越是不起眼的2类分割任务越考验数据集的目录结构、标签格式和可视化核查手段。这篇文章围绕“舌头分割2类”这个数据集讲清楚标签文件怎么组织、掩膜与多边形标注怎么互转、数据可视化代码怎么用、以及如何把整理好的数据集接到 Unet 或 YOLOv8 这类常见分割训练流程里。不要让“数据集”三个字停留在下载文件夹的层面把它变成可复现、可核查、可训练的训练原料这才是这篇文章要解决的事。2. 舌头分割数据集的目录结构与2类标签格式2.1 一张图像对应一个掩膜PNG、JSON、RLE 三种标签表示图像分割数据集的标签文件通常有三种存在形式分别对应不同的标注工具和训练框架标签形式文件后缀说明常见来源掩膜图像.png / .bmp每个像素一个类别编号舌头为 1、背景为 0LabelMe、自家标注脚本多边形标注.json / .xml保存轮廓点的坐标序列不直接用于训练LabelMe、CVAT运行长度编码.txt / .json 内嵌按“起点 长度”记录前景像素位置Kaggle 类竞赛对于舌头分割这种 2 类任务推荐直接使用二值 PNG 掩膜作为统一的标签格式。它的读取成本最低配合 OpenCV 或 PIL 就能完成可视化而且不依赖额外解析库。import cv2 import numpy as np # 读取原图和掩膜 image cv2.imread(images/tongue_001.jpg) image cv2.cvtColor(image, cv2.COLOR_BGR2RGB) mask cv2.imread(labels/tongue_001.png, cv2.IMREAD_GRAYSCALE) # 2类分割的掩膜只包含0和255两个值 unique_values np.unique(mask) print(掩膜中的像素值, unique_values)这里有个容易踩的坑标注工具导出时可能把舌头保存为白色255背景为黑色0。训练框架在计算损失时往往要求输入为0/1或者0/255连续值不同框架要求不同。因此在做数据检查时第一步就是确认掩膜中实际存在的像素类别数避免出现第三个灰度值混入。上面这段代码中np.unique(mask)用于列出掩膜内所有出现的像素值正常情况只有[0, 255]或[0, 1]。2.2 舌头分割数据集的目录与文件命名规范我自己整理分割数据集时通常按下面的结构组织文件目录。这个结构也是医学图像分割领域比较通用的做法方便后续统一读取tongue_segmentation/ ├── images/ │ ├── tongue_001.jpg │ ├── tongue_002.jpg │ └── ... ├── labels/ │ ├── tongue_001.png │ ├── tongue_002.png │ └── ... ├── masks_visual/ │ ├── tongue_001_overlay.jpg │ └── ... ├── split/ │ ├── train.txt │ ├── val.txt │ └── test.txt └── code/ ├── visualize.py ├── mask_to_json.py └── check_dataset.py文件名必须严格保持原图与掩膜一一对应且不要出现空格、中文字符或括号。空格和括号是 Linux 下批量脚本最常见的出错来源。划分文件train.txt中每一行只保存图像 ID例如tongue_001程序读取时分别拼上images/和labels/前缀这样训练时既不会错位也便于扩展新数据。2.3 掩膜到 JSON 的互转代码在标注工具和训练框架之间切换时多边形标注和掩膜之间的互转是刚需。LabelMe 导出的 JSON 需要转成掩膜才能训练反过来掩膜要交给医生复核时也常常需要转成多边形方便阅读。下面这段代码把二值掩膜转成 LabelMe 风格的 JSON 坐标import cv2 import json import numpy as np def mask_to_json(mask_path, image_path, json_path): mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) h, w mask.shape contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) shapes [] for contour in contours: # 过滤掉太小的轮廓避免保存噪声 if cv2.contourArea(contour) 50: continue # 轮廓点压平为 [x, y, x, y, ...] 格式 points contour.reshape(-1, 2).tolist() shapes.append({ label: tongue, points: points, shape_type: polygon }) data { version: 4.5.6, flags: {}, shapes: shapes, imagePath: image_path, imageHeight: h, imageWidth: w } with open(json_path, w, encodingutf-8) as f: json.dump(data, f, ensure_asciiFalse, indent2) mask_to_json(labels/tongue_001.png, tongue_001.jpg, labels/tongue_001.json)这段代码有两个值得注意的地方。第一cv2.findContours只取目标外轮廓RETR_EXTERNAL保证舌头内部的“空洞”不会被当成独立轮廓因为 2 类分割中舌头内部理论上不存在背景区域。第二contourArea小于 50 像素的轮廓会被直接丢弃这是为了防止标注时不小心留下的单点噪声生成无意义的多边形。如果你发现转换后的 JSON 在 LabelMe 里显示位置偏移请检查imagePath字段是否为相对路径LabelMe 按该字段定位原图。反向转换的思路对称解析 JSON 里的points在空白画布上用cv2.fillPoly填充为白色掩膜即可。需要注意 JSON 中的坐标可能是浮点数画掩膜前必须用np.int32转成整数否则 OpenCV 会报类型错误。3. 构建舌头分割训练集的关键步骤与参数设置3.1 类别不均衡与二类标签的标注规范舌头分割虽然只有 2 类但类别不均衡问题非常突出。在一张分辨率为 1920x1080 的舌像照片中舌头区域通常只占画面的 15%~30%背景占了大多数。深度学习模型如果直接拿原始像素做训练很容易收敛到一个“全预测为背景”的局部最优解。这也是医学图像分割任务中常见的问题和息肉分割数据集、皮肤病变分割面临的情况类似。针对 2 类舌象分割一般从两个方向入手一是损失函数上使用 Dice Loss 或 Focal Loss二是数据层面做区域裁剪和增强让舌头区域在训练样本中拥有更高的出现频率。下面是一个简单的 Dice Loss 实现import torch import torch.nn.functional as F def dice_loss(pred, target, smooth1.0): # pred: 模型输出概率形状 [B, 1, H, W] # target: 二值掩膜 [B, 1, H, W]取值 0/1 pred torch.sigmoid(pred) intersection (pred * target).sum() union pred.sum() target.sum() dice (2.0 * intersection smooth) / (union smooth) return 1.0 - diceintersection是预测概率与真实掩膜的逐元素乘积之和union是两者各自之和。加smooth的目的是防止分母为 0。Dice Loss 对前景区域大小的敏感度比交叉熵低在小目标分割任务中收敛更稳定但训练前期容易出现梯度不稳定建议在训练前 20 个 epoch 使用 Dice Loss 与交叉熵的加权组合。3.2 数据增强策略及参数设置舌象数据集的量级一般不大几十到几百张之间。直接训练分割网络几乎必然过拟合数据增强不是锦上添花而是必需品。以下参数是我在舌象分割任务中实测表现比较稳定的组合增强操作参数范围说明水平翻转概率 0.5舌象左右对称安全性最高旋转±15°超过 15° 会引入非自然的大角度舌位缩放0.8~1.2模拟拍摄距离变化亮度/对比度±30%抵抗拍摄环境光线差异弹性形变alpha30, sigma5模拟舌体轻微形变适合医学图像分割随机裁剪512x512 或 256x256限制训练尺寸控制显存占用旋转和弹性形变是对分割效果提升最明显的两个操作其中平移和旋转会导致掩膜出现边缘锯齿训练中模型会对这种锯齿产生容忍。推理时需要使用与训练一致的预处理流程否则真实场景中性能会明显下降。import albumentations as A train_transform A.Compose([ A.HorizontalFlip(p0.5), A.Rotate(limit15, p0.8), A.RandomScale(scale_limit0.2, p0.5), A.RandomBrightnessContrast(brightness_limit0.3, contrast_limit0.3, p0.5), A.ElasticTransform(alpha30, sigma5, p0.3), A.Resize(512, 512), ]) # 单次增强调用image 和 mask 需同步变换 augmented train_transform(imageimage, maskmask)Albumentations 的优点在于会自动同步增强原图和掩膜不需要手动保证变换参数一致。需要注意的是RandomScale缩放后图像尺寸会变化必须放在Resize前面。如果原图分辨率差异很大建议先统一 Resize 到固定尺寸再做旋转与翻转避免多次插值导致掩膜边缘质量下降。舌象的边缘本身就是模糊过渡插值越少越好。3.3 划分数据集和防止泄漏分割任务的数据划分相比分类任务要更谨慎。同一患者在不同角度、不同时间拍摄的多张舌象照片如果一部分进了训练集、另一部分进了验证集验证指标会有虚高。这种情况被称为数据泄漏。为避免泄漏划分维度应该是患者而不是单张图片。假如数据集的原始目录中每张图命名带有患者 ID划分脚本按照患者 ID 进行分组而不是直接随机分割图片。一个简单的方案是读取原图文件名中的患者标识按患者分组后再按比例划分import os import random from collections import defaultdict image_dir images/ patient_ids defaultdict(list) # 假设文件名格式patient01_20240112_001.jpg for fname in os.listdir(image_dir): patient_id fname.split(_)[0] patient_ids[patient_id].append(fname) all_patients list(patient_ids.keys()) random.shuffle(all_patients) train_cut int(len(all_patients) * 0.7) val_cut int(len(all_patients) * 0.85) train_patients all_patients[:train_cut] val_patients all_patients[train_cut:val_cut] test_patients all_patients[val_cut:]划分思路是先把所有患者 ID 打乱再按 70% / 15% / 15% 的比例切到train/val/test三个集合。注意一定不要在原图列表上直接shuffle后截断那种方式会让同一患者的不同照片出现在两个集合中。舌象数据通常不是大数据集测试集比例可以适当提高一张不规范的测试集划分会把整个模型的对比实验毁掉。4. 用数据可视化代码核查舌头掩膜质量4.1 叠加显示与通道分离可视化拿到数据集后第一件事不是写训练脚本而是把原图和掩膜叠加在一起逐张检查。掩膜和舌头轮廓错位 3~5 个像素在缩略图上看不出来但训练时误差会直接算进损失函数。以下代码把掩膜渲染为半透明的红色叠加层输出到单独目录供快速浏览import os import cv2 import numpy as np from tqdm import tqdm image_dir images/ label_dir labels/ output_dir masks_visual/ os.makedirs(output_dir, exist_okTrue) for fname in tqdm(os.listdir(image_dir)): img cv2.imread(os.path.join(image_dir, fname)) name os.path.splitext(fname)[0] mask_path os.path.join(label_dir, name .png) if not os.path.exists(mask_path): continue mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) # 归一化到 0~1避免掩膜存在255以外的值 mask_binary (mask 127).astype(np.uint8) overlay img.copy() # 红色通道增强 overlay[:, :, 2] np.maximum(overlay[:, :, 2], mask_binary * 255) # 绿色和蓝色通道减弱突出舌头区域 overlay[:, :, 1] np.where(mask_binary 1, overlay[:, :, 1] * 0.5, overlay[:, :, 1]) overlay[:, :, 0] np.where(mask_binary 1, overlay[:, :, 0] * 0.5, overlay[:, :, 0]) # 原图和掩膜半透明混合 blended cv2.addWeighted(img, 0.6, overlay, 0.4, 0) cv2.imwrite(os.path.join(output_dir, name _overlay.jpg), blended)这段代码做的事情很直白把掩膜区域映射为红色通道增强、绿蓝通道减弱再用cv2.addWeighted将原图与掩膜按 0.6 和 0.4 的权重混合。建议在核查时重点关注三个位置舌头边缘是否有 2 像素以上的系统性偏移、舌尖是否被截断、舌根与咽喉暗区边界是否出现大块漏标。纯红色叠加可能看不清暗色背景上的掩膜边界可以额外生成一个只保留边缘线的版本用cv2.Canny提取舌头轮廓并绘制在原图上那个对核查边界更直观。4.2 用统计分布定位坏样本人工逐张看图依然会漏掉异常样本一个更高效的办法是先做统计筛选再针对异常样本人工复核。用以下几项指标可以快速定位坏样本指标计算方式可疑区间舌头区域占比mask 像素数 / 图像总像素 5% 或 50%连通域数量OpenCV 连通域分析 1 个轮廓面积比最大轮廓面积 / 所有轮廓面积 0.9掩膜边缘锯齿度掩膜面积 / 轮廓长度明显低于同类样本下面这段代码同时计算了区域占比和连通域数量import cv2 import numpy as np def inspect_mask(mask_path): mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) mask_binary (mask 127).astype(np.uint8) area_ratio mask_binary.sum() / mask_binary.size num_labels, labels, stats, _ cv2.connectedComponentsWithStats(mask_binary, 8) # 背景被记为第0个连通域所以目标连通域数量要减1 obj_count num_labels - 1 return area_ratio, obj_count ratio, count inspect_mask(labels/tongue_001.png) print(f舌头面积占比: {ratio:.3f}, 连通域数量: {count})连通域数量大于 1 说明掩膜里存在与主舌头不相连的游离块可能是标注时把唾沫、嘴唇反光一起标了进去。面积占比过低或过高则提示标注可能漏掉了舌头大块区域或者把背景一起圈进了舌头区域。这两类样本如果不清理训练时损失函数会被个别大错误样本主导。建议在训练前完成一次全量统计并可视化异常样本的直方图分布再决定是删除还是重新标注而不是盲目相信数据集的标题。5. 用统一脚本把数据接入 Unet 与 YOLOv8 训练5.1 路径统一和训练前的标准目录要把整理后的舌头分割数据集接入常见训练框架建议先转成一套标准目录。YOLOv8 的分割训练支持直接读取带masks子目录的数据集结构这和 Unet 类框架的数据结构几乎一致tongue_dataset/ ├── images/ │ ├── train/ │ │ └── tongue_001.jpg │ └── val/ │ └── tongue_002.jpg └── labels/ ├── train/ │ └── tongue_001.txt 或 .png └── val/ └── tongue_002.txt 或 .png以 YOLOv8 为例训练前创建一个tongue_seg.yaml配置文件path: /data/tongue_dataset train: images/train val: images/val test: names: 0: tongue注意这里的关键点YOLOv8 分割的标签不是 PNG 掩膜而是与目标检测一致的 txt 格式每行表示一个多边形轮廓格式为class_id x1 y1 x2 y2 ...。因此第 2.3 节的掩膜转 JSON 代码还需要再进一步转成 YOLO 的归一化多边形格式。转换时YOLOv8 要求坐标归一化到 0~1且每个轮廓点必须保持闭合。一个数据文件包含多个轮廓行时表示一张图像中有多个舌头实例训练会自动按实例处理。5.2 快速验证训练结果的技巧训练完成后验证集的可视化展示和 Dice 指标同样重要。这里给出一个通用的验证思路把三列图拼在一张大图上左边是原图中间是真实掩膜右边是模型预测结果。用并排对比而不是叠加对比更容易发现皮肤、牙齿、嘴唇等易混淆区域的错误模式。import cv2 import numpy as np import torch model.eval() with torch.no_grad(): pred model(torch.from_numpy(image).unsqueeze(0).cuda()) pred_mask (torch.sigmoid(pred).squeeze().cpu().numpy() 0.5).astype(np.uint8) canvas np.zeros((h, w * 3, 3), dtypenp.uint8) canvas[:, 0:w] image canvas[:, w:w*2] cv2.cvtColor(gt_mask * 255, cv2.COLOR_GRAY2BGR) canvas[:, w*2:w*3] cv2.cvtColor(pred_mask * 255, cv2.COLOR_GRAY2BGR) cv2.imwrite(result_compare.png, canvas)这段验证代码里gt_mask是真实掩膜pred_mask是模型输出经过阈值 0.5 二值化后的结果。在正式实验里可以把阈值设为变量在 0.3~0.7 之间扫描观察不同置信度下舌头区域的边缘变化用于确定适用于你数据分布的最佳阈值。真实分割项目中把数据集、标签文件、可视化代码做成一个互相连接的整体整个训练过程才会有可追溯性。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价