news 2026/10/5 7:16:40

舌头分割2类标签解析与UNet训练落地全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
舌头分割2类标签解析与UNet训练落地全流程

简介:本资源为舌头分割图像数据集,面向医学图像处理、计算机视觉方向的学习者与算法开发者,可用于训练和验证语义分割模型,解决舌头区域自动提取与二分类分割任务。数据图像分辨率统一为640×640,原图为jpg格式,掩膜标签为png格式,采用0/1阈值图像,0代表背景、1代表舌头,具体类别可在classes文本中查看。压缩包共约2000个文件,以1998个png掩膜、1个txt类别说明和1个py可视化脚本为主,整体约101.53MB。数据集划分为训练集与测试集:训练集含2127张图片及2127个对应掩膜,测试集含537张图片及537个对应掩膜,目录结构清晰。配套可视化脚本无需修改即可直接运行,随机抽取一张图片,展示原始图像、GT图像及GT在原图上的蒙板效果并保存到当前目录,便于快速检查标注质量。目前已有231人学习,适合入门到中级的图像分割实践者参考使用。

1. 舌头分割数据集:从 2 类标签到可视化,一套能直接跑通的落地路径

拿到一个「舌头分割(2类)」数据集,很多人第一反应是打开图片翻两页,然后卡在同一个地方:标签到底怎么读、类别 0 和 1 谁是谁、可视化代码跑出来一片黑。舌头分割在中医舌诊数字化、口腔影像分析里是个高频需求,2 类通常指背景与舌体,任务本质是二分类语义分割。它适合想快速验证 unet 图像分割流程、又不想从零标注的从业者。这篇笔记按「数据集结构 → 标签解析 → 可视化 → 训练接入 → 避坑」推一遍,目标是你照着能复现,而不是看完只知道有这么个东西。

2. 舌头分割数据集的结构与 2 类标签到底怎么读

2.1 目录组织与文件命名约定

常见做法是 images 与 masks(或 labels)两个平行目录,文件名一一对应。真正要确认的是三件事:图像格式、掩膜格式、命名是否严格对齐。我一般先跑一段统计脚本,把扩展名分布、尺寸分布、文件名交集一次性打出来,避免后面训练时才发现有图无标签。

import os from collections import Counter from PIL import Image root = "tongue_seg" # 数据集根目录 img_dir = os.path.join(root, "images") mask_dir = os.path.join(root, "masks") def scan(folder): files = [f for f in os.listdir(folder) if not f.startswith(".")] ext = Counter(os.path.splitext(f)[1].lower() for f in files) return files, ext imgs, img_ext = scan(img_dir) masks, mask_ext = scan(mask_dir) print("图像数量:", len(imgs), "扩展名:", img_ext) print("掩膜数量:", len(masks), "扩展名:", mask_ext) # 文件名(去扩展名)交集,检查是否一一对应 stem_i = {os.path.splitext(f)[0] for f in imgs} stem_m = {os.path.splitext(f)[0] for f in masks} print("仅图像有:", len(stem_i - stem_m), "仅掩膜有:", len(stem_m - stem_i))

逻辑说明:先看数量是否相等,再看扩展名是否统一,最后看文件名交集。参数上,root换成你的实际路径即可;如果仅图像有或仅掩膜有不为 0,说明存在孤儿文件,训练前必须处理,否则 dataloader 会在某个 batch 直接抛异常。

2.2 2 类标签的像素值与语义映射

2 类分割最常见的坑是像素值不是 0/1,而是 0/255,甚至 0/128。直接拿去算交叉熵,类别数对不上就会报错或静默学错。先统计掩膜里出现的唯一像素值,这一步是黑匣子打开的关键。

import numpy as np from PIL import Image sample = os.path.join(mask_dir, masks[0]) m = np.array(Image.open(sample)) vals, counts = np.unique(m, return_counts=True) print("唯一像素值:", vals) print("占比:", (counts / counts.sum()).round(4))

如果输出是[0 255],那类别 1 需要映射成 1;如果是[0 1],可直接用。映射方式:

def to_binary(mask): # 大于 127 视为舌体,否则背景;按你的实际像素值调整阈值 return (mask > 127).astype(np.uint8)

参数说明:阈值 127 适用于 0/255 的情况;若掩膜是 0/128,阈值取 64 更稳。判断依据是唯一像素值的中位数,不要凭感觉写。类别语义上,0 一般是背景,1 是舌体,但务必用可视化确认,别默认。

2.3 尺寸、通道与长宽比分布

舌头图像往往来自手机或相机,尺寸不统一。训练前要么统一 resize,要么用 padding。先看分布再决定策略,比拍脑袋设 256×256 靠谱。

sizes = Counter() modes = Counter() for f in imgs[:200]: # 抽样 200 张足够看趋势 with Image.open(os.path.join(img_dir, f)) as im: sizes[im.size] += 1 modes[im.mode] += 1 print("尺寸 Top5:", sizes.most_common(5)) print("色彩模式:", modes)

如果尺寸种类超过 20 种,建议统一 resize 到 256×256 或 512×512;如果长宽比差异大,直接 resize 会拉伸舌体,改用短边 resize + 中心裁剪更稳。色彩模式若是 RGBA,记得转 RGB,否则三通道模型读进去会多一维。

3. 数据可视化代码:把标签叠回原图看清对错

3.1 单张叠加可视化的最小实现

可视化不是锦上添花,它是你发现标签错位、掩膜反色、类别颠倒的唯一低成本手段。下面这段把原图、掩膜、叠加图并排画出来。

import matplotlib.pyplot as plt import numpy as np from PIL import Image def 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")) mask_bin = (mask > 127).astype(np.uint8) overlay = img.copy() overlay[mask_bin == 1] = [255, 0, 0] # 舌体区域涂红 blended = (img * (1 - alpha) + overlay * alpha).astype(np.uint8) fig, ax = plt.subplots(1, 3, figsize=(12, 4)) ax[0].imshow(img); ax[0].set_title("原图") ax[1].imshow(mask_bin, cmap="gray"); ax[1].set_title("二值掩膜") ax[2].imshow(blended); ax[2].set_title("叠加") for a in ax: a.axis("off") plt.tight_layout(); plt.show() visualize(os.path.join(img_dir, imgs[0]), os.path.join(mask_dir, masks[0]))

逻辑说明:convert("L")保证掩膜单通道;alpha控制叠加透明度,0.5 通常够看。若叠加后红色区域明显偏移,说明图像与掩膜尺寸不一致或存在配准问题,这类样本要单独挑出来。

3.2 批量网格预览与异常样本筛查

单张看效率低,批量拼网格能快速扫出异常。重点看三类:全黑掩膜、全白掩膜、叠加后边界明显错位。

def grid_preview(n=16, cols=4): idx = np.random.choice(len(imgs), min(n, len(imgs)), replace=False) rows = (len(idx) + cols - 1) // cols fig, axes = plt.subplots(rows, cols, figsize=(cols * 3, rows * 3)) for ax, i in zip(axes.ravel(), idx): img = np.array(Image.open(os.path.join(img_dir, imgs[i])).convert("RGB")) mask = np.array(Image.open(os.path.join(mask_dir, masks[i])).convert("L")) mask_bin = (mask > 127).astype(np.uint8) overlay = img.copy() overlay[mask_bin == 1] = [255, 0, 0] ax.imshow((img * 0.5 + overlay * 0.5).astype(np.uint8)) ax.set_title(imgs[i][:12], fontsize=8); ax.axis("off") for ax in axes.ravel()[len(idx):]: ax.axis("off") plt.tight_layout(); plt.show() grid_preview()

参数说明:n是抽样数量,16 张够日常巡检;cols控制列数。若发现某张掩膜全黑,先查该文件是否损坏,再查是否像素值全为 0;全白同理。这类样本在训练中会拉低指标,建议先剔除或重新标注。

3.3 类别像素占比统计

2 类分割里,背景通常远多于舌体,类别不平衡会直接影响 loss 表现。先算全局占比,再决定要不要加权。

pos, total = 0, 0 for f in masks: m = np.array(Image.open(os.path.join(mask_dir, f)).convert("L")) pos += (m > 127).sum() total += m.size print("舌体像素占比: %.4f" % (pos / total))

如果占比低于 0.1,训练时建议用 Dice Loss 或带权交叉熵,否则模型容易全预测背景也能拿到高准确率,指标好看但没用。这一步的数值直接决定你后面 loss 怎么选,别跳过。

4. 接入 unet 图像分割训练:从 Dataset 到第一个 epoch

4.1 自定义 Dataset 与同步增强

舌头分割数据量通常不大,增强是刚需。关键是图像和掩膜必须同步变换,否则标签就废了。下面用 albumentations 做同步增强。

import cv2 import torch from torch.utils.data import Dataset import albumentations as A from albumentations.pytorch import ToTensorV2 train_tf = A.Compose([ A.Resize(256, 256), A.HorizontalFlip(p=0.5), A.RandomBrightnessContrast(p=0.3), A.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)), ToTensorV2(), ]) class TongueSeg(Dataset): def __init__(self, img_dir, mask_dir, files, tf): self.img_dir, self.mask_dir = img_dir, mask_dir self.files, self.tf = files, tf def __len__(self): return len(self.files) def __getitem__(self, i): name = self.files[i] img = cv2.imread(os.path.join(self.img_dir, name))[:, :, ::-1] mask = cv2.imread(os.path.join(self.mask_dir, name), 0) mask = (mask > 127).astype("float32") out = self.tf(image=img, mask=mask) return out["image"], out["mask"].unsqueeze(0)

逻辑说明:cv2.imread读进来是 BGR,[:, :, ::-1]转 RGB;掩膜用灰度读,阈值化后转 float32。A.Compose同时接收 image 和 mask,保证几何变换一致。参数上,Resize(256,256)可按显存调整;Normalize用 ImageNet 均值方差,是分割任务的常规起点。

4.2 训练循环与 loss 选择

2 类分割常用 BCEWithLogitsLoss 或 Dice Loss。数据不平衡时,两者结合更稳。

import torch.nn as nn bce = nn.BCEWithLogitsLoss() def dice_loss(logits, targets, eps=1e-6): probs = torch.sigmoid(logits) num = 2 * (probs * targets).sum(dim=(2, 3)) den = probs.sum(dim=(2, 3)) + targets.sum(dim=(2, 3)) + eps return 1 - (num / den).mean() def criterion(logits, targets): return bce(logits, targets) + dice_loss(logits, targets)

参数说明:eps防止除零;两项相加时若 Dice 波动大,可给 Dice 乘 0.5 权重。训练循环里记得optimizer.zero_grad()、loss.backward()、optimizer.step()三步齐全,别漏。第一个 epoch 后先看验证集 Dice,低于 0.5 通常说明标签映射或增强有问题,回去查第 2 章。

4.3 验证指标与可视化回查

训练中只看 loss 不够,2 类分割要看 Dice 和 IoU。每个 epoch 后在验证集上算一次,并抽几张预测图叠加回看。

@torch.no_grad() def evaluate(model, loader, device): model.eval() inter, union = 0, 0 for x, y in loader: x, y = x.to(device), y.to(device) pred = (torch.sigmoid(model(x)) > 0.5).float() inter += (pred * y).sum().item() union += (pred + y - pred * y).sum().item() return inter / (union + 1e-6) # IoU

参数说明:阈值 0.5 是默认起点,若召回偏低可降到 0.4。IoU 低于 0.6 时,优先回查掩膜是否反色、图像掩膜是否错位,这两类问题在舌头分割里出现频率最高。

5. 舌头分割落地避坑:5 个真实翻车现场

5.1 掩膜反色导致模型学反

现象:训练 loss 下降但验证 Dice 一直 0.3 左右,预测图里舌体被标成背景。原因:部分掩膜像素值是 0/255,但语义与预期相反,或阈值化时把舌体判成背景。解决:用第 2.2 节的唯一像素值统计,逐张确认语义,必要时对调映射。

5.2 图像与掩膜尺寸不一致

现象:叠加可视化时红色区域整体偏移或缩放。原因:图像和掩膜在采集或后处理时被分别 resize,尺寸对不上。解决:训练前统一检查img.size == mask.size,不一致的样本要么重新对齐,要么剔除。

5.3 文件名不对齐导致静默丢样本

现象:训练集数量比预期少,但程序不报错。原因:Dataset 用文件名交集构建,孤儿文件被静默跳过。解决:第 2.1 节的交集检查必须跑,仅图像有和仅掩膜有都为 0 才继续。

5.4 增强不同步污染标签

现象:训练几个 epoch 后 Dice 突然掉。原因:图像做了翻转或旋转,掩膜没跟着变。解决:用 albumentations 的 Compose 同时传 image 和 mask,不要分别处理。

5.5 类别不平衡导致全背景预测

现象:准确率很高但 Dice 很低,预测图几乎全黑。原因:舌体像素占比过低,模型学到全预测背景。解决:先算占比,低于 0.1 时加 Dice Loss 或给正类加权,权重可设为1 / 占比量级。

6. 把 2 类舌头分割推到可用:阈值调优与边界后处理

训练跑通只是起点,真正决定落地效果的是推理阶段的阈值和后处理。2 类分割的边界往往毛糙,直接二值化会有零星噪点。我一般先扫一遍阈值,再决定要不要做形态学处理。

import numpy as np import cv2 def postprocess(prob, thr=0.5, kernel=3): mask = (prob > thr).astype(np.uint8) k = np.ones((kernel, kernel), np.uint8) mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, k) # 去噪点 mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, k) # 补空洞 return mask

阈值扫描可以这样写:在验证集上从 0.3 到 0.7 每隔 0.05 算一次 Dice,取最高点。参数上,kernel=3适合 256 尺寸;若原图是 512,可调到 5。开运算去孤立噪点,闭运算补内部空洞,顺序不要反,反了会把噪点连成片。

阈值典型 Dice适用场景
0.3召回高、边界偏大舌体占比小、宁可多标
0.5均衡默认起点
0.7精度高、边界偏紧噪点多、要求干净

后处理不是必须,但舌头分割的边界通常需要它。我的习惯是:先不加后处理跑一版基线,再加上去对比 Dice 和视觉效果,只有提升明显才保留。曾经为了追求边界平滑把 kernel 调到 9,结果舌体边缘被啃掉一圈,Dice 反而掉了 4 个点,这个后悔药我吃过一次就够了。阈值和后处理都要用验证集说话,别凭肉眼调。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 7:15:29

VOC转YOLO实战:钢筋计数数据集的密集小目标检测训练指南

简介:这组VOC格式标注文件面向钢筋计数与智能盘点场景,供计算机视觉算法工程师、深度学习研究者及相关专业学生用于钢筋目标检测与计数模型的训练与验证。压缩包内共568个xml标注文件,打包后仅1.07MB,rar格式便于快速下载与本地部…

作者头像 李华
网站建设 2026/10/5 7:14:44

OpenClaw Gateway源码拆解:模型路由与协议转换实战

最近这段时间一直在折腾 OpenClaw,把它从“装好能跑”一路啃到“知道每一层在干嘛”。圈子里的教程大多是部署保姆级,但一旦涉及到src/gateway这个目录,愿意往深里讲的人很少。我自己的体会是:OpenClaw 的网关层才是整个系统的“大…

作者头像 李华
网站建设 2026/10/5 7:14:44

DeepSeek BIM智能审查全解析:从图纸数据到模型微调与系统落地

简介:《DeepSeek建筑行业BIM智能化方案——基于大模型技术的工程图纸自动审查系统》(272页)是一份体系化专业技术文档,面向建筑行业信息化负责人、BIM工程师及AI算法工程师,给出基于DeepSeek大模型实现工程图纸自动审查…

作者头像 李华
网站建设 2026/10/5 7:13:42

DeepSeek私有化部署与LoRA微调实战:从硬件选型到vLLM调优

简介:这是一份面向机器学习工程师、数据科学家及软件开发者的大模型实操指南,聚焦DeepSeek的私有化部署与基于自有数据的训练全流程。文档共25页,从环境准备、模型代码与权重获取,到单机/分布式部署、数据清洗与标注、训练参数配置…

作者头像 李华
网站建设 2026/10/5 7:13:35

DeepSeek私有化部署实战:vLLM推理、LoRA微调与避坑指南

简介:面向中小型企业的DeepSeek私有化部署与全栈实战资料,适合希望低成本引入大模型能力的程序员、架构师与IT决策者。内容围绕数据安全保障、定制化能力与成本效益三条主线,从DeepSeek发展历程、技术架构与能力特点讲起,再展开私…

作者头像 李华
网站建设 2026/10/5 7:12:29

MWC观察:通用算力超节点时代的架构转型与落地实践

1. 先说结论:这届MWC我看到的不是“更强的芯片”,而是“更大的整机”今年巴塞罗那的MWC逛下来,我最大的感受其实不在芯片展台,而在服务器与网络设备厂商的角落里——越来越多的厂商开始把“一整柜算力”当成一个产品来展示&#x…

作者头像 李华