☰
舌头分割数据集实战:2类掩码从数据校验到边界优化
2026/9/30 1:39:07 网站建设 项目流程

简介:这份资源面向计算机视觉方向的开发者与图像分割学习者,提供一套完整的舌头分割数据集,可用于训练和验证二类语义分割模型。数据图像分辨率统一为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. 舌头分割数据集到底能解决什么问题:从中医齿痕识别到 2 类掩码落地

拿到「舌头分割(2类)」这个数据集标题时,很多人第一反应是:舌头分割能有多大市场?但如果你做过中医舌诊仪、口腔内窥镜辅助诊断、或者舌象健康 App,就会知道舌体区域的像素级掩码几乎是所有下游任务的前置依赖——舌质颜色统计、齿痕计数、舌苔厚薄分级,全都建立在「先把舌头从背景里抠出来」这一步上。这个数据集的核心价值在于把问题简化成 2 类语义分割:背景与舌体,标签文件是单通道掩码图,配套数据可视化代码让你在训练前就能肉眼确认标注质量。它适合三类人:想入门医学图像分割的算法工程师、需要快速验证舌象分析原型的在校学生、以及要给现有 pipeline 补一个稳定 ROI 提取模块的开发者。2 类设定意味着你不用纠结多标签冲突,交叉熵和 Dice 就能跑通,但边界细粒度才是真正的难点。

2. 2 类舌头分割的数据组织与标签格式:先看懂再动手

2.1 图像与掩码的目录约定和命名对齐

绝大多数舌头分割数据集采用「图像目录 + 掩码目录」平行结构,文件名一一对应。常见做法是图像存为 JPG 或 PNG,掩码存为 PNG 单通道 8 位灰度图,像素值 0 表示背景、1 或 255 表示舌体。这里有个血泪经验:如果掩码用 JPG 存,压缩会引入中间灰度值,你的 2 类标签就变成了事实上的多类噪声。所以拿到数据集第一件事不是写模型,而是写一个校验脚本,确认掩码唯一值集合。

import os import numpy as np from PIL import Image img_dir = "data/images" mask_dir = "data/masks" def check_mask_values(mask_dir): bad = [] for name in os.listdir(mask_dir): m = np.array(Image.open(os.path.join(mask_dir, name))) uniq = np.unique(m) # 2类掩码唯一值应只含 0 和 1(或 0 和 255) if not set(uniq.tolist()).issubset({0, 1, 255}): bad.append((name, uniq.tolist())) return bad bad_list = check_mask_values(mask_dir) print(f"异常掩码数量: {len(bad_list)}") for item in bad_list[:5]: print(item)

这段脚本的逻辑很直接:遍历掩码目录,读取每个 PNG 的唯一像素值,只要出现 0/1/255 之外的值就记录。参数上,issubset里的集合要根据你数据集的真实编码调整——有的数据集用 0/1,有的用 0/255,还有的用 0/128。如果发现大量异常,说明掩码被 JPG 污染过,需要重新找原始标注或做阈值化修复。

2.2 标签文件里容易忽略的三个属性

除了像素值,标签文件还有三个属性决定你后面会不会翻车:位深、调色板、尺寸。位深必须是 8 位,16 位掩码在 PyTorch 里读进来会变成 int32,直接送进损失函数会报类型错误。调色板模式(P 模式)的 PNG 看起来是彩色,实际索引值才是标签,用convert("L")转灰度时可能发生映射偏移。尺寸必须和原图严格一致,有些数据集图像是 512×512,掩码却是 256×256,训练时广播会静默出错。

属性正确值错误后果检查方法
位深8 位损失函数类型报错Image.open().mode
模式L 或 P标签值偏移np.unique校验
尺寸与原图一致广播静默错误img.size == mask.size
编码0/1 或 0/255类别数误判统计唯一值

我一般会在数据加载类里加一个断言,尺寸不一致直接抛异常,而不是等到 loss 变成 NaN 才回头查。这个习惯帮我省过至少两次通宵排查。

3. 用可视化代码在训练前验数据:把黑匣子打开

3.1 叠加可视化:三行代码看出标注偏移

数据可视化代码的价值不是好看,而是让你在花几小时训练前发现标注问题。最有效的可视化是原图与掩码半透明叠加,舌体区域用红色覆盖,边界是否贴合一眼就能看出来。

import numpy as np from PIL import Image import matplotlib.pyplot as plt def overlay_visualize(img_path, mask_path, alpha=0.5): img = np.array(Image.open(img_path).convert("RGB")) mask = np.array(Image.open(mask_path).convert("L")) # 将掩码二值化,兼容 0/1 和 0/255 两种编码 binary = (mask > 0).astype(np.uint8) color_mask = np.zeros_like(img) color_mask[binary == 1] = [255, 0, 0] overlay = (img * (1 - alpha) + color_mask * alpha).astype(np.uint8) plt.figure(figsize=(12, 4)) plt.subplot(1, 3, 1); plt.imshow(img); plt.title("原图") plt.subplot(1, 3, 2); plt.imshow(binary, cmap="gray"); plt.title("掩码") plt.subplot(1, 3, 3); plt.imshow(overlay); plt.title("叠加") plt.show() overlay_visualize("data/images/001.jpg", "data/masks/001.png")

逻辑说明:先统一把掩码二值化,避免 0/1 和 0/255 两种编码导致可视化全黑或全红。alpha控制叠加透明度,0.5 是通用值,如果舌体颜色本身偏红,可以降到 0.3 让边界更清楚。参数上唯一要注意的是convert("L"),如果原掩码是 P 模式,这一步会按调色板转灰度,可能不是你想要的索引值,稳妥做法是先convert("RGB")再取单通道。

3.2 批量抽检与边界统计

单张看不够,我一般会随机抽 20 张拼成网格,同时统计每张掩码的舌体像素占比。占比异常小(低于 1%)或异常大(高于 80%)的样本大概率是标注错误或图像本身有问题。

import random def batch_inspect(img_dir, mask_dir, n=20): names = os.listdir(mask_dir) sample = random.sample(names, min(n, len(names))) ratios = [] for name in sample: m = np.array(Image.open(os.path.join(mask_dir, name)).convert("L")) ratio = (m > 0).sum() / m.size ratios.append((name, round(ratio, 4))) ratios.sort(key=lambda x: x[1]) print("占比最低:", ratios[:3]) print("占比最高:", ratios[-3:]) return ratios batch_inspect("data/images", "data/masks")

这段代码输出占比排序,最低和最高的几个就是重点复查对象。舌体在正常拍摄下占比通常在 15% 到 50% 之间,低于 5% 可能是拍摄距离过远或标注漏标,高于 70% 可能是把嘴唇、下巴一起标进去了。这一步做完,你对数据集的信任度才有底。

4. 训练 2 类舌头分割模型的参数怎么设:从损失函数到边界优化

4.1 损失函数选择:Dice 与 BCE 的组合逻辑

2 类分割最常见的坑是类别极度不平衡——舌体占比 20%,背景 80%,纯 BCE 会让模型倾向于全预测背景也能拿到 80% 准确率。我一般用 BCE + Dice 组合,BCE 稳定梯度,Dice 直接优化重叠度。

import torch import torch.nn as nn class BCEDiceLoss(nn.Module): def __init__(self, bce_weight=0.5): super().__init__() self.bce = nn.BCEWithLogitsLoss() self.bce_weight = bce_weight def forward(self, logits, targets): # logits: (B,1,H,W) targets: (B,1,H,W) float bce_loss = self.bce(logits, targets) probs = torch.sigmoid(logits) intersection = (probs * targets).sum(dim=(2, 3)) union = probs.sum(dim=(2, 3)) + targets.sum(dim=(2, 3)) dice = (2 * intersection + 1e-6) / (union + 1e-6) dice_loss = 1 - dice.mean() return self.bce_weight * bce_loss + (1 - self.bce_weight) * dice_loss

参数说明:bce_weight默认 0.5,如果训练初期 loss 震荡大,可以调到 0.7 让 BCE 主导;如果最终 Dice 上不去,调到 0.3 让 Dice 主导。1e-6是平滑项,防止空掩码时除零。注意 targets 必须是 float 且值域 0/1,如果掩码是 0/255,要在 Dataset 里除以 255。

4.2 输入尺寸与增强策略:舌头边界的细粒度保护

舌头分割的难点在边界——齿痕、舌苔边缘、反光区域都会让边界模糊。输入尺寸我一般用 512×512,再小边界信息丢失严重,再大显存吃不消。增强策略里,随机旋转和水平翻转是安全的,但垂直翻转要慎用,舌头上下颠倒不符合解剖先验,可能让模型学到错误特征。颜色抖动幅度要小,因为舌质颜色本身就是下游任务要分析的特征,抖太狠会破坏颜色分布。

增强方式推荐参数理由
随机旋转±30 度拍摄角度变化
水平翻转p=0.5左右对称合理
垂直翻转不建议解剖先验不符
颜色抖动亮度±0.1保护舌质颜色
随机裁剪缩放 0.8-1.2模拟距离变化

4.3 评价指标:Dice 之外还要看边界

Dice 和 IoU 是常规指标,但舌头分割我还会看边界 F1(Boundary F1)。因为 Dice 对内部像素不敏感,边界错几个像素 Dice 可能只掉 0.01,但下游齿痕计数就废了。常见做法是用scipy.ndimage做形态学腐蚀得到边界带,再算边界区域的 F1。

from scipy.ndimage import binary_erosion def boundary_f1(pred, target, width=3): # pred/target: (H,W) 二值 kernel = np.ones((width, width), bool) pred_bound = pred ^ binary_erosion(pred, kernel) target_bound = target ^ binary_erosion(target, kernel) tp = (pred_bound & target_bound).sum() fp = (pred_bound & ~target_bound).sum() fn = (~pred_bound & target_bound).sum() precision = tp / (tp + fp + 1e-6) recall = tp / (tp + fn + 1e-6) return 2 * precision * recall / (precision + recall + 1e-6)

width控制边界带宽度,3 像素适合 512 输入,如果输入是 256 就调到 2。这个指标能暴露 Dice 掩盖的边界问题,我一般要求边界 F1 至少 0.7 才认为模型可用。

5. 舌头分割数据集常见踩坑与排查:标注、编码、过拟合

5.1 掩码全黑或全白:编码与阈值问题

现象:可视化时掩码要么全黑要么全白,但用图片查看器打开又正常。原因通常是掩码是 P 模式调色板图,convert("L")后索引值被映射到 0 或 255 之外,或者掩码实际是 0/255 编码但代码按 0/1 处理。解决:先用np.unique打印真实唯一值,再根据结果调整二值化阈值,不要假设编码。

5.2 训练 loss 不降:标签尺寸与类型不匹配

现象:训练几个 epoch loss 几乎不动,或者直接 NaN。原因多半是掩码尺寸和图像不一致,广播后标签错位;或者掩码是 int64,BCE 要求 float。解决:在 Dataset 的__getitem__里加断言assert img.size == mask.size,并强制mask = mask.float() / 255.0。

5.3 验证集 Dice 高但可视化边界差:过拟合与增强过强

现象:验证 Dice 0.95,但叠加可视化发现边界锯齿严重。原因是训练集和验证集同分布,Dice 对边界不敏感,加上颜色抖动过强让模型学到了颜色捷径。解决:引入边界 F1 指标,降低颜色抖动幅度,增加边界区域的损失权重。

5.4 舌体与嘴唇粘连:标注规范不一致

现象:部分样本把嘴唇标进舌体,部分没有,模型学出模糊边界。原因:标注人员对舌体边界定义不一致。解决:抽检占比最高的样本,统一标注规范,必要时重新标注或剔除异常样本。这个坑最耗时间,但跳过它后面所有指标都不可信。

5.5 显存溢出:输入尺寸与 batch size 不匹配

现象:512×512 输入配 batch size 16 直接 OOM。原因:2 类分割虽然输出通道少,但高分辨率特征图显存占用大。解决:用混合精度训练,或者把 batch size 降到 4 配合梯度累积。

from torch.cuda.amp import autocast, GradScaler scaler = GradScaler() for imgs, masks in loader: optimizer.zero_grad() with autocast(): logits = model(imgs) loss = criterion(logits, masks) / accum_steps scaler.scale(loss).backward() if step % accum_steps == 0: scaler.step(optimizer) scaler.update()

accum_steps设为 4 时,等效 batch size 是实际 batch 的 4 倍,显存只增加少量。这个技巧在单卡 8G 上跑 512 输入很实用。

6. 把 2 类掩码用到位:从 ROI 裁剪到齿痕计数的进阶技巧

训练出舌头分割模型只是开始,真正让这个数据集产生价值的是下游用法。我一般会把分割掩码做成 ROI 裁剪器:用掩码的 bounding box 把舌体裁出来,再送进颜色分析或齿痕检测模块。这样下游模型不用处理背景干扰,输入尺寸统一,准确率通常能涨 5 到 10 个点。

def crop_by_mask(img, mask, pad=10): ys, xs = np.where(mask > 0) if len(ys) == 0: return img y1, y2 = max(0, ys.min()-pad), min(img.shape[0], ys.max()+pad) x1, x2 = max(0, xs.min()-pad), min(img.shape[1], xs.max()+pad) return img[y1:y2, x1:x2] # 批量裁剪并保存 for name in os.listdir(mask_dir): img = np.array(Image.open(f"data/images/{name.replace('.png','.jpg')}")) mask = np.array(Image.open(f"data/masks/{name}").convert("L")) cropped = crop_by_mask(img, mask) Image.fromarray(cropped).save(f"data/crops/{name.replace('.png','.jpg')}")

pad参数留 10 像素边距,防止边界信息被裁掉。这个裁剪步骤在齿痕计数场景特别有用,因为齿痕就在舌体边缘,裁太紧会丢特征。

另一个进阶用法是把 2 类掩码转成边界带,专门训练一个边界细化模型。做法是对掩码做形态学梯度,得到 3 到 5 像素宽的边界带,然后在这个窄带上做二分类。这样模型容量集中在最难的地方,边界 F1 通常能再涨 3 到 5 个点。我试过在 512 输入下把边界 F1 从 0.72 推到 0.78,代价是训练时间增加约 40%。

还有一个容易被忽略的技巧:用分割掩码做数据增强的引导。比如只对舌体区域做颜色抖动,背景保持不变,这样既增加了样本多样性,又不会破坏背景的分布。实现上就是生成一个随机颜色偏移图,用掩码做加权融合。

def masked_color_jitter(img, mask, strength=0.1): jitter = np.random.uniform(1-strength, 1+strength, size=img.shape) jittered = np.clip(img * jitter, 0, 255).astype(np.uint8) m = (mask > 0)[..., None] return np.where(m, jittered, img).astype(np.uint8)

strength控制抖动幅度,0.1 对应 ±10%,舌质颜色分析场景建议不超过 0.15。这个函数在 Dataset 里调用,能让模型对光照变化更鲁棒,同时不破坏背景一致性。

最后说一个我自己的习惯:每次拿到新的舌头分割数据集,先跑一遍可视化抽检,再跑一遍边界 F1 基线,最后才动模型结构。顺序反了,后面全是后悔药。这个 2 类数据集看起来简单,但边界质量和标注一致性决定了它的天花板。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询