news 2026/9/14 2:07:21

X光牙齿分割实战:从数据预处理到U-Net训练全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
X光牙齿分割实战:从数据预处理到U-Net训练全流程

简介:这是一份面向医学图像分割任务的高质量数据集,聚焦X光影像下的牙齿区域分割,属于背景与前景的二值分割任务(0为背景,255为前景目标)。数据集已规范划分为训练集与测试集:训练集包含1600张原始X光图像及对应掩膜,测试集400张图像及掩膜,可直接用于深度学习模型的训练与效果评估。整个压缩包共2000个文件,其中1999张PNG格式图像覆盖输入图片与标签,另附1个Python可视化脚本,可随机抽取样本将原始图、真实标签及标签叠加效果同屏展示,便于快速预览数据质量与标注情况。资源整体大小约434.8MB,体量适中,适合高校学生、科研人员及算法工程师开展牙齿影像分割实验或课题研究。当前已有566人学习使用,数据分布密集且前景占比大,能有效锻炼模型对细小结构的判别能力,是一份可直接落地的医学影像分割入门与进阶数据资源。

1. X光牙齿分割:先整理好数据,再谈模型

做医学图像分割,最容易被忽视的是数据本身。拿到一份“基于X光下的牙齿分割数据”,里面通常是几百张X光片和对应掩膜,标注只分背景和牙齿两类。但X光片的对比度差异、金属伪影、牙齿边缘与骨骼的灰度重叠,都会让模型在测试集上表现波动。只关注U-Net,忽略训练集和测试集的标签格式与划分逻辑,模型结果很难解释。

这里要梳理2类别分割数据的完整路径:训练集和测试集的目录约定、标签像素值检查、预处理与增强、U-Net训练与评估,以及针对牙齿重叠的进阶处理。适合刚接触医学图像分割的工程师,也适合需要整理医学影像数据的研究人员。

下面所有脚本都遵循“先确认数据,再训练模型”的顺序。换一批X光牙齿数据,也能照着这套流程复现。

2. 组织牙齿分割数据集:训练集和测试集的目录、标签与划分

2.1 目录结构:把原图与掩膜分开存放

拿到数据先看目录。常见做法是imagesmasks两个文件夹,或者每个样本一个子目录。我一般会先落成这样的结构:

teeth_data/ ├── train/ │ ├── images/ │ │ ├── 001.png │ │ └── 002.png │ └── masks/ │ ├── 001.png │ └── 002.png └── test/ ├── images/ │ ├── 201.png │ └── 202.png └── masks/ ├── 201.png └── 202.png

原图和掩膜文件名一一对应,这是最不会出错的约定。掩膜如果只有一份、没有区分训练和测试,就要自己拆分。把数据集放在train/test/下,后面写 Dataset 类时只需要扫描两个目录,不需要额外维护 CSV。

如果你的数据是 DICOM 或 nii.gz,需要先转成 PNG 再入训练,因为分割框架大多直接读普通图像格式。X光图往往是 12 位或 16 位灰度,直接存成 8 位 PNG 会损失窗位信息。建议在转换时按最大最小像素值做线性拉伸,而不是简单截断,这样能保证训练集和测试集的灰度范围一致,避免模型在训练时看到大量无效黑色区域。

2.2 标签编码:背景是0,牙齿是1还是255

2类别分割的标签必须是单通道掩膜,而不是三通道彩色标注。常见有两种编码:背景像素值为 0、目标为 1 的 label 模式,以及背景 0、目标 255 的 mask 模式。读取后用np.unique检查像素值集合,防止出现灰色边缘。我一般会在数据加载入口统一做一次检查:

import numpy as np from PIL import Image mask = np.array(Image.open("train/masks/001.png").convert("L")) values, counts = np.unique(mask, return_counts=True) print("像素值:", values, "数量:", counts) # 如果出现 0 和 255,则归一化到 0/1 if set(values) <= {0, 255}: mask = (mask > 0).astype(np.uint8)

逻辑说明:加载灰度掩膜,检查唯一值。若数据标注时用了 0/255,就通过阈值转成 0/1,方便后续损失函数计算。参数说明:convert("L")将所有通道合并为亮度值;astype(np.uint8)把布尔数组转成紧凑整数,减少显存占用。

如果掩膜是三通道 RGB 但每通道相同,先取第一通道再检查;如果发现值是 0/1 之外还有一个中间值,说明标注有抖动,需要回到标注工具里清理。这个编码决定了后面损失函数的输出,U-Net 最后的 sigmoid 输出会与 0/1 掩膜计算交叉熵。测试集掩膜是 0/255 时,也要同样处理。

2.3 训练集和测试集划分:随机抽样还不够

当数据没有预设划分时,很多人直接随机 split。但对于 X光牙齿数据,同一个患者的多个牙片会被重复采集,如果随机划分,训练集和测试集可能包含同一患者的图像,导致模型记忆患者特征而不是牙齿结构。正确的做法是先按患者 ID 分组,再做组级别划分。若数据文件名包含患者号,可以这样写:

import glob from sklearn.model_selection import GroupShuffleSplit images = sorted(glob.glob("raw/images/*.png")) groups = [img.split("/")[-1].split("_")[0] for img in images] # 文件名例如 P01_001.png splitter = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, test_idx = next(splitter.split(images, groups=groups)) train_files = [images[i] for i in train_idx] test_files = [images[i] for i in test_idx]

逻辑说明:先把文件名里的患者 ID 提取出来作为分组依据,GroupShuffleSplit保证同一组的样本不会同时出现在训练集和测试集。参数说明:n_splits=1表示只生成一组划分;test_size=0.2表示测试集占 20%;random_state固定种子,方便复现。

若数据中没有患者 ID,无法正确分组,只能按文件名前缀或采集时间近似分组。这种划分方式虽然会让测试集 Dice 分数比随机拆分低几个点,但它更接近真实临床场景,最终模型的可信度更高。尤其在训练集和测试集分布不平衡时,这一步省不掉。

3. X光图像的预处理与增强:让牙齿分割模型见多识广

3.1 归一化与对比度增强:CLAHE是X光图的默认选项

X光影像的动态范围大,不同设备采集的灰度分布差异明显。直接用原始像素喂给模型,容易让 batch normalization 层失效。最基本的做法是把图像像素归一化到 [0,1] 或使用 z-score。但牙齿分割里骨骼和牙齿在灰度上非常接近,仅在局部区域有边缘差异,单独做全局归一化提升有限。我一般用 CLAHE(对比度受限自适应直方图均衡)增强局部对比度,然后再做标准化。

CLAHE 比普通 HE 更克制,它把图像分成小块,每块内做直方图均衡,并通过 clip limit 限制噪点放大。对于 X光下的牙齿组织,clip_limit通常取 1.5 到 2.0,tileGridSize取 (8,8) 或 (4,4)。参数过大容易把软组织噪点放大,过小则没有明显增强效果。如果原图已带标注,预处理转换必须同时应用到 image 和 mask,但 CLAHE 只作用于 image,mask 保持二值。

3.2 数据增强:同步变换是分割任务的前提

分割任务的数据增强必须保证图像和掩膜做完全相同的几何变换。随机旋转、缩放、翻转都可以直接同步;但颜色抖动、对比度增强这类像素级增强只能作用于图像,不能作用于掩膜。牙齿 X光中牙齿之间的重叠区域很小,随机 crop 容易裁掉目标,建议先把图像缩放到固定尺寸,再做小角度旋转和水平翻转。弹性形变适合模拟患者姿态变化,但需要控制变形幅度,防止掩膜边缘断裂。

另一个实际问题是,训练数据里每张图的牙齿数量不同。2类别分割的标签只区分前景和背景,模型需要适应单颗牙和多颗牙的差异。增强里加入随机尺度变化,让模型对牙齿大小更鲁棒。我在实际项目中经常把输入尺寸定在 256×256 或 512×512,前者训练快,后者保留更多牙根细节。如果显存足够,512 是优先选择。

3.3 可复用的预处理与增强代码

下面这段代码可以用在 PyTorch 或 TensorFlow 的 Dataset 里,包含 CLAHE、归一化、随机旋转和翻转:

import cv2 import numpy as np from albumentations import Compose, CLAHE, Normalize, HorizontalFlip, Rotate def build_transforms(phase="train"): transforms_list = [ CLAHE(clip_limit=2.0, tile_grid_size=(8, 8), always_apply=True), ] if phase == "train": transforms_list += [ HorizontalFlip(p=0.5), Rotate(limit=15, border_mode=cv2.BORDER_CONSTANT, mask_value=0, p=0.8), ] transforms_list.append(Normalize(mean=0.5, std=0.25, max_pixel_value=255.0)) return Compose(transforms_list, additional_targets={"mask": "mask"}) transforms = build_transforms("train") sample = transforms(image=image, mask=mask) image = sample["image"] mask = sample["mask"]

逻辑说明:albumentationsCompose会按顺序执行增强,additional_targets指定mask作为第二个输入,保证几何变换同步。参数说明:Rotateborder_mode设为BORDER_CONSTANT,旋转产生的空白区域用常量填充,mask_value=0让背景填充保持一致;Normalizemean=0.5std=0.25把灰度分布拉到零附近,适合 X光片。注意 CLAHE 用always_apply=True强制生效,因为这项增强对 X光分割几乎总是正向收益。

使用这个增强流程后,训练集每个 epoch 看到的是不同版本的图像。测试集只用 CLAHE 和 Normalize,不做随机增强。另外,如果你不需要albumentations这个依赖,也可以用 OpenCV 自行实现:先旋转矩阵,再cv2.warpAffine分别处理图像和掩膜,插值方法图像用INTER_LINEAR,掩膜用INTER_NEAREST。后者能避免插值带来伪标签。

4. 用U-Net训练牙齿分割模型:损失函数、训练脚本与测试集评估

4.1 U-Net为什么是牙齿分割的默认基线

U-Net 是医学图像分割最常见的基线模型,编码器逐步下采样提取上下文,解码器通过跳跃连接恢复空间细节。对于 X光牙齿分割,牙齿边界在灰度上不连续,U-Net 的跳跃连接能把低级边缘信息传递给深层特征,比普通 FCN 更适合。如果你只是 2类别分割,U-Net 的第一层输入通道是 1,输出通道是 1,不需要预训练权重也能从头开始在几百张图上收敛到可用的 Dice 指标。

数据量少时可以先用 U-Net 跑通流程,再尝试 DeepLabV3+ 或 TransUNet。但牙齿分割的公开 SoTA 不容易复现,很多增益来自数据清洗与增强。因此我这里给一个最小可训练的 U-Net 框架,配合上一章的预处理,你的训练集和测试集都能直接使用。如果你已经在用 YOLOv8 这类检测模型做牙齿检测,可以把检测框作为先验,裁剪出单颗牙区域再做分割,但这不是本标题的默认路线。

4.2 损失函数与评估指标:Dice Loss和IoU

分割训练最怕类别不平衡。牙齿区域占整张 X光的面积比例低,如果直接用CrossEntropyLoss,模型会倾向于预测背景。Dice Loss 直接优化分割区域的重合程度,对前景占比不敏感。另一个选择是 Focal Loss,但实验下来 Dice Loss + BCE 的组合更稳。测试指标用 Dice 系数和 IoU,Dice 对重叠更敏感,IoU 对边界误差更敏感。可以先给一个常用的 DiceLoss 实现:

import torch import torch.nn as nn class DiceLoss(nn.Module): def __init__(self, smooth=1e-6): super().__init__() self.smooth = smooth def forward(self, pred, target): pred = torch.sigmoid(pred) pred = pred.contiguous().view(-1) target = target.contiguous().view(-1) intersection = (pred * target).sum() dice = (2.0 * intersection + self.smooth) / (pred.sum() + target.sum() + self.smooth) return 1.0 - dice

逻辑说明:DiceLoss 将预测与目标展平后计算交集与并集,返回1 - Dice作为损失。训练时 mask 需要转成 float 类型,网络输出 logits,在损失函数内部做 sigmoid。参数说明:smooth=1e-6的作用是防止某个 batch 没有前景区域时除零;如果连续几个 epoch 损失都是 0.99 附近的常数,优先检查 mask 的像素值是否为 0/1,而不是 0/255。

4.3 训练脚本关键片段

这里给 PyTorch 风格的关键片段:

model = UNet(in_channels=1, out_channels=1) optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-5) loss_fn = DiceLoss() for epoch in range(epochs): for image, mask in train_loader: image, mask = image.to(device), mask.float().to(device) logits = model(image) loss = loss_fn(logits, mask) optimizer.zero_grad() loss.backward() optimizer.step()

逻辑说明:训练循环中,imagemask被移入设备,mask使用float()转成浮点型,因为 DiceLoss 内部需要浮点乘法。logits来自模型输出,没有直接经过 sigmoid,真正的概率转换在损失函数中完成。参数说明:AdamW 是 Adam 的权重衰减修正版,lr=1e-4是医学图像分割里的常用初始值;weight_decay对 2类别任务设置 1e-5 防止过拟合。若 GPU 显存有限,把batch_size调到 4 或 8,同时学习率按线性比例降低。

训练中还需要记录每个 epoch 的 Dice 分数,在验证集上保存最佳权重。判断过拟合看训练集和测试集的 loss 差,如果训练 Dice 持续上涨而测试 Dice 停滞,就要回到增强和正则化。常见训练参数参考:

参数推荐值说明
image_size256/512256训练快,512保留更多细节
batch_size4/8根据显存和lr联动调整
epochs50-100配合验证集早停
lr1e-4AdamW初始值
weight_decay1e-5防止过拟合

4.4 测试集评估:计算Dice和IoU并保存可视化结果

模型训练结束后,在测试集上逐张推理并计算指标。典型评估代码:

def calculate_metrics(pred_mask, true_mask, eps=1e-6): pred = pred_mask > 0.5 true = true_mask > 0.5 intersection = (pred & true).sum() union = (pred | true).sum() dice = (2 * intersection + eps) / (pred.sum() + true.sum() + eps) iou = (intersection + eps) / (union + eps) return dice, iou model.eval() dice_scores, iou_scores = [], [] with torch.no_grad(): for image, mask in test_loader: logits = model(image.to(device)) pred = (torch.sigmoid(logits) > 0.5).cpu().numpy() dice, iou = calculate_metrics(pred, mask.numpy()) dice_scores.append(dice) iou_scores.append(iou) print(f"Test Dice: {np.mean(dice_scores):.4f} ± {np.std(dice_scores):.4f}") print(f"Test IoU: {np.mean(iou_scores):.4f} ± {np.std(iou_scores):.4f}")

逻辑说明:calculate_metrics先用 0.5 阈值把概率变成二值掩膜,然后分别计算交集、并集与前景面积。Dice 和 IoU 都加eps防止样本全为背景时除零。参数说明:测试时不用做随机增强,只用与训练相同的 Normalize;np.std反映测试集样本间差异,如果标准差超过 0.1 说明部分牙片出现严重伪影。最终建议把预测掩膜与原图叠成 PNG 保存,方便后续查看错误样本。

5. 牙齿分割的实用技巧:处理重叠牙、根尖与标注噪声

5.1 用边界损失提升分割边缘精度

X光下牙齿互相重叠,边界在灰度上没有清晰分界。常见做法是在 Dice Loss 上叠加一个基于距离变换的边界损失,让网络更关注靠近边界的像素。实现时先对掩膜做距离变换,边界像素给出更高权重。这类损失在牙齿分割中值得尝试,能让测试集 Dice 提升 1 到 2 个点。你需要把它和主损失按 0.8/0.2 加权,而不是简单相加,否则网络会过度拟合边缘噪声。

5.2 测试时增强与多模型投票

测试时增强(TTA)不需要改训练代码。推理时将输入做水平翻转和 ±10 度旋转,得到多份预测概率,取平均后再阈值化。TTA 能抹平单次推理因平移敏感性造成的抖动。与 TTA 类似的操作是训练两个不同 seed 的 U-Net,对概率图取平均。这两种方法都适合牙齿数据量少、模型方差大的情况。注意 TTA 会增加推理时间,批量处理时按需开启;如果模型要跑实时推理,TTA 只适合离线分析。

5.3 通过误差热图定位训练集标注问题

如果测试集上预测结果整体不错,但个别图 Dice 突然下降,先用误差热图定位。将预测掩膜和真实掩膜异或,得到错误像素分布,再叠加到原图上。若错误位置总集中在牙齿边缘,属于边界拟合不足;若错误位置是整块牙齿缺失,多半是训练集没有同类型样本或标注遗漏。下表是一个简单的错误归类参考:

现象可能原因处理方式
边界细线错切掩膜标注不精确清理训练集mask边缘
单颗牙缺失训练集该类样本少增加该形态数据增强
金属伪影处误检伪影灰度与牙根接近加入对比度调整
整张全黑掩膜像素值读到0/1以外检查标签编码

5.4 快速推理脚本:单张X光的牙齿分割验证

训练完成后,需要一个不依赖 GPU 的快速验证脚本:

import cv2 import torch from model import UNet # 替换为你自己的模型文件 model = UNet(in_channels=1, out_channels=1) model.load_state_dict(torch.load("best_model.pth", map_location="cpu")) model.eval() image = cv2.imread("test/images/201.png", cv2.IMREAD_GRAYSCALE) image = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)).apply(image) image = image.astype("float32") / 255.0 image = (image - 0.5) / 0.25 image = torch.from_numpy(image).float().unsqueeze(0).unsqueeze(0) with torch.no_grad(): raw_mask = model(image) pred_mask = (torch.sigmoid(raw_mask) > 0.5).squeeze().numpy().astype("uint8") * 255 cv2.imwrite("pred_mask.png", pred_mask)

逻辑说明:脚本把单张图缩放归一化后直接送入模型,输出 0/255 掩膜。注意预处理参数要与训练时完全一致,包括 CLAHE 的clipLimit和 Normalize 的mean/std。参数说明:unsqueeze(0)(H,W)变成(1,1,H,W),满足卷积层输入的 NCHW 格式;map_location="cpu"让没有 CUDA 的设备也能加载权重。这个脚本可以在拿到新 X光片时立即判断当前模型能不能直接用于预测。

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

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

SSM+Vue公交管理系统源码拆解:从数据库设计到前后端联调

简介&#xff1a;面向计算机、数学、电子信息等专业学生&#xff0c;这套基于SSM&#xff08;Spring、SpringMVC、MyBatis&#xff09;与Vue.js开发的公交车信息管理系统源码&#xff0c;附带毕业论文、开题报告和任务书&#xff0c;能够直接用于课程设计、期末大作业或毕业设计…

作者头像 李华
网站建设 2026/9/14 2:05:03

企业数据防泄漏实战:从终端禁用U盘到网络外发拦截

干IT这一行&#xff0c;最怕的不是系统崩&#xff0c;而是数据悄悄从你眼皮子底下出去了。我这边接手过不少公司的安全整改&#xff0c;老板第一句话往往就是&#xff1a;把U盘禁掉、移动硬盘禁掉&#xff0c;聊天软件、邮件附件、网盘上传外发文件全部堵死。要真只靠“禁”就能…

作者头像 李华
网站建设 2026/9/14 2:04:33

Unity ECS 实现 Boids 群集模拟:从数据布局到性能优化

简介&#xff1a;一份基于Unity ECS实现的Boids群体模拟示例&#xff0c;面向希望掌握ECS架构并用Job System/Burst优化大规模群体行为的Unity开发者。示例从传统MonoBehaviour逐步过渡到纯ECS、Jobify、Burst及实体生成等实现&#xff0c;覆盖分离、对齐、聚拢三大规则&#x…

作者头像 李华