简介:本资源是一套面向医学影像AI方向初学者与毕业设计学生的3D牙齿CBCT图像分割完整实现方案,基于PyTorch框架解决临床场景中高精度三维牙齿结构自动分割问题。资源包共59个文件,涵盖39个Python核心模块(含3D UNet/V-Net模型定义、数据增强、训练/推理/可视化脚本)、7个CSV路径管理文件、4张关键流程图与结果示意图、3份Markdown说明文档及1份设计报告压缩包,整体大小28.44MB,结构清晰、模块解耦,便于按数据预处理→模型训练→Flask部署→评估分析路径系统学习。已有73人下载学习,提供从.nii.gz原始数据读取、尺寸归一化、Excel驱动的数据集划分、Dice/Lovasz损失训练、日志监控到Web接口封装的端到端代码实现,并附详细readme与data_readme,显著降低医学图像分割项目落地门槛。 CBCT去做牙齿分割,这个项目我在本地跑通了好几轮。先说结论:不管是做正畸前的牙齿排布测量、种植牙的解剖结构评估,还是做口腔疾病病灶定位,基于PyTorch的3D U-Net方案都是投入产出比最高的路子之一。标题里这套“基于Pytorch实现3D牙齿CBCT图像分割项目源码+数据集+说明资料、报告.zip”我拿到手先扫了一遍目录,源码结构、数据集说明、训练好的权重、报告文档都比较齐,不是那种只有一堆散乱代码的“伪开源”。这篇博文我就围绕这个项目,把我自己跑通的完整流程、踩过的坑、改过的参数一一写出来,给同样在做3D医学图像分割、尤其是口腔CBCT方向的朋友们做个参考。
先说给谁看。如果你是刚接触医学影像分割的学生,或者是在医院信息科、口腔医疗器械公司做算法预研的工程师,再或者你只是好奇CBCT三维数据怎么喂给深度学习模型,这篇都适用。它不需要你有很强的医学背景,但最好懂一点Python和基础卷积神经网络。我会尽量把每个关键选择背后的为什么讲清楚,而不是只丢一堆代码让你自己琢磨。
1. 项目整体设计与技术选型思路
1.1 CBCT影像的特点,决定了必须用3D方案
CBCT全称是Cone Beam Computed Tomography,也就是锥形束CT。它和传统螺旋CT最大的区别是射线呈锥形束发射,一次旋转就能采集完整的三维体数据,非常适合口腔颌面这种小范围、高分辨率的扫描场景。但它的数据有非常鲜明的特点,我列几个直接影响建模的点:
- 各向异性特别明显:很多CBCT设备的层内分辨率能做到0.1~0.2mm,层间距却可能到0.25~0.4mm,体素不是正方体。你不能像处理常规自然图像那样直接套网络,预处理阶段必须做重采样或统一分辨率。
- 灰度值范围大但软组织对比度低:CT值范围通常在-1000到3000之间,但牙齿和骨骼的灰度比较接近,边界模糊,尤其牙根周围和牙槽骨之间的过渡带,想靠传统阈值分割基本不可能。
- 数据量少且标注成本高:高质量的三维标注需要医生一帧一帧在冠状面、矢状面、轴状面上勾画,一颗一颗牙齿标,非常耗时。这个项目里用的数据集是公开的,但即便如此,数据量也就几十例到一百例左右,比自然图像分割动辄几万张的规模小得多。
正是因为CBCT本身是三维体数据,单纯用2D卷积网络逐切片分割,会丢失切片之间的空间连续性——牙根在轴状面上看可能只是一小块椭圆,但在矢状面或冠状面上能看出完整的走向,2D网络很难学到这个信息。所以项目选型时直接锁定了3D卷积路线,具体来说就是3D U-Net。
1.2 为什么选PyTorch和3D U-Net,而不是其他方案
先聊框架。PyTorch在医学影像分割领域基本是事实标准,原因很实际:
- 动态计算图让调试方便。医学图像处理的pipeline里经常要动态改输入尺寸、按样本调整预处理参数,PyTorch写着顺手。
- 社区生态好。MONAI、nnUNet这些专门做医学影像的库都是基于PyTorch的,后面扩展改造很轻松。
- 分布式和混合精度训练支持成熟,单卡也能舒服地把模型训起来。
再说网络结构。牙齿CBCT分割任务里,目标结构是牙齿,区域占比小且边界复杂。标准U-Net的编码器-解码器加跳跃连接结构,对这类小目标稠密预测任务非常有效。这里选择了3D U-Net而不是更重的V-Net或者nnU-Net,主要是考虑到数据集规模不大,3D U-Net参数量适中,训练速度快,调参空间也足够。我自己后面也试过把主干换成ResNet风格的3D编码器,但在这个数据集上提升有限,反而增加了显存压力,所以最终方案还是以3D U-Net为核心,在通道数和深度上做了微调。
1.3 整体pipeline和项目文件结构
拿到的压缩包解压之后,目录大概长这样:
project_root/ ├── data/ │ ├── images/ # CBCT原始体数据,NIfTI格式 │ ├── labels/ # 对应标注,全牙/单牙两种版本 │ └── train_val_test.txt ├── src/ │ ├── dataset.py # 数据加载、重采样、增强 │ ├── model.py # 3D U-Net定义 │ ├── loss.py # Dice Loss、CE Loss、Boundary Loss │ ├── train.py # 训练主入口 │ ├── inference.py # 推理滑窗和后处理 │ └── utils.py # 指标计算、可视化 ├── checkpoints/ # 训练好的权重 ├── docs/ │ ├── 技术报告.pdf │ └── 数据集说明.md └── requirements.txt整体pipeline分成五个阶段:环境搭建、数据预处理、网络构建与训练、推理与后处理、评估与可视化。我下面按这个顺序讲。
2. 环境搭建与数据预处理细节
2.1 PyTorch和CUDA版本怎么配套,才不折腾
这个项目用GPU训练,环境配置是最容易被忽略但又最容易卡住的一步。我直接给一套我实测没问题的组合:
- Python 3.8 / 3.10 均可(3.8更稳)
- CUDA 11.7 或 11.8
- PyTorch 1.13 或 2.0.1
- 显卡驱动建议 515 以上
安装命令我建议这样:
# CUDA 11.8 + PyTorch 2.0.1 pip install torch==2.0.1 torchvision==0.15.2 --index-url https://download.pytorch.org/whl/cu118这里有个新手常踩的坑:不要先去官网下载CUDA Toolkit再单独装PyTorch,PyTorch的pip包自带了核心的CUDA runtime库,你只需要保证显卡驱动版本够新就行。官网的CUDA Toolkit是给编译CUDA扩展用的,跑PyTorch用不上,装错版本反而容易把系统环境搞乱。
装完验证一下:
import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))如果能输出版本号和显卡型号,环境就OK了。我在一台RTX 3090和一台RTX A5000上都跑过,24GB显存训这个项目比较舒服。
2.2 数据读取、重采样和统一分辨率
CBCT原始数据一般是NIfTI格式(.nii.gz),用nibabel读取:
import nibabel as nib img = nib.load("data/images/patient_001.nii.gz") data = img.get_fdata() affine = img.affine # 保留空间信息读取后第一件事是重采样。前面提到CBCT体素是各向异性的,如果直接输入网络,卷积核的感受野在三个方向对应的物理尺度不一致,分割效果会打折扣。我的做法是把所有数据统一重采样到各向同性分辨率,比如1.0 x 1.0 x 1.0 mm:
import nibabel as nib import numpy as np from scipy.ndimage import zoom def resample_volume(volume, original_spacing, target_spacing=(1.0, 1.0, 1.0)): resize_factor = np.array(original_spacing) / np.array(target_spacing) new_shape = np.round(volume.shape * resize_factor).astype(int) return zoom(volume, new_shape / volume.shape, order=1) # 使用示例 spacing = img.header.get_zooms()[:3] resampled = resample_volume(data, spacing)重采样后用order=1也就是线性插值,对灰度图像够用且不会太模糊;但标签mask一定用order=0最近邻插值,否则会产生很多介于0和1之间的中间值,把分割边界搞脏。
重采样之后,体数据尺寸可能变成300x300x400这种量级,不能整块塞进显卡,需要切成小块来训练。我用的是TTA(Test-Time Augmentation)后面细说,训练时的输入块尺寸建议设成128x128x96左右,这是内存和性能的折中。
2.3 灰度截断和归一化,不要瞎做
CBCT影像的灰度值范围很大,直接送进网络的话,梯度要么爆炸要么消失。我的预处理习惯是:
- 先把超出牙齿和骨组织范围的灰度截断掉,一般取
[-500, 2000]或[0, 3000],具体要看设备类型。 - 再线性归一化到
[0, 1]区间。 - 用全局的均值和标准差做标准化,而不是逐样本算,避免网络对样本间灰度差异过拟合。
这里一个容易被忽略的点是:牙齿和骨骼在截断范围内都偏高,你如果不做对比度增强,网络很难区分它们。我在项目中加了一步简单的CLAHE(对比度受限自适应直方图均衡化),虽然会增加一点预处理时间,但对牙齿边缘的分割准确率提升明显。
import cv2 import numpy as np def preprocess_volume(volume, ct_min=-500, ct_max=2000): volume = np.clip(volume, ct_min, ct_max) volume = (volume - ct_min) / (ct_max - ct_min) # 逐切片做CLAHE增强 for i in range(volume.shape[2]): slice_2d = volume[:, :, i].astype(np.uint8) clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) volume[:, :, i] = clahe.apply(slice_2d) / 255.0 return volume注意CLAHE的tileGridSize不要设太大,8x8是比较稳妥的选择,太大容易在牙齿根管这种细小结构上产生噪声伪影。
2.4 数据增强策略,针对小数据集强行“注水”
这个数据集样本量不大,必须做增强,否则过拟合会很严重。我用的增强方案是:
- 随机旋转:绕z轴旋转±15度,模拟患者摆位偏差。
- 随机缩放:缩放因子0.9~1.1,模拟不同设备体素尺寸误差。
- 随机弹性形变:这个对牙齿分割尤其重要,因为不同患者牙齿排列差异很大,刚体变换学不到的那种复杂形变,用弹性形变能补充。
- 随机裁剪:训练时每次随机从大体积中裁剪一个patch,既增强又控制了显存。
- 强度扰动:给灰度加一点高斯噪声和随机偏移,提升模型对扫描条件差异的鲁棒性。
Monai库里已经有现成的这些变换实现,比我自己写稳定得多:
import monai train_transforms = monai.transforms.Compose([ monai.transforms.LoadImageD(keys=["image", "label"]), monai.transforms.SpacingD(keys=["image", "label"], pixdim=(1.0, 1.0, 1.0), mode=("bilinear", "nearest")), monai.transforms.RandRotateD(keys=["image", "label"], range_x=0.26, mode=("bilinear", "nearest")), monai.transforms.RandZoomD(keys=["image", "label"], min_zoom=0.9, max_zoom=1.1, mode=("bilinear", "nearest")), monai.transforms.RandGaussianNoiseD(keys=["image"], prob=0.3), monai.transforms.RandAffineD(keys=["image", "label"], prob=0.5, rotate_range=(0.1, 0.1, 0.1)), ])注意一个实操细节:label的插值方式永远是nearest,不能和image一样用bilinear。
3. 3D U-Net网络实现与损失设计
3.1 网络结构:编码器、解码器和跳跃连接怎么搭
3D U-Net的核心是编码器下采样提特征,解码器上采样恢复分辨率,跳跃连接把同尺度的底层细节传入高层,弥补下采样丢失的空间信息。我用的版本是经典四层结构:
- 编码器:每层两个3x3x3卷积,逐层下采样,通道数从32开始翻倍:32→64→128→256。
- 瓶颈层:两个3x3x3卷积,通道512。
- 解码器:每层先上采样再接跳跃连接,再两个3x3x3卷积,通道数逐层减半。
- 输出层:1x1x1卷积把通道数映射到类别数,接Softmax。
关键参数是初始通道数。我在这个项目里从32开始,而不是论文原版的64。原因很简单:牙齿分割任务目标相对简单,通道数太大容易过拟合,而且显存和训练时间都吃紧。32起步的实验效果已经足够好,DSC能到0.85以上(后面细说)。如果你的显卡只有12GB,初始通道数还可以降到16,代价是分割精度会略降。
3.2 损失函数:Dice Loss为主,配合CE和Boundary Loss
牙齿分割中最大的问题是类别不均衡——整张CBCT里牙齿区域可能只占5%不到,如果直接用Cross Entropy Loss,模型会把所有体素预测成背景,也能得到一个非常低的loss,但分割结果毫无意义。
所以项目里用了Dice Loss作为主损失。Dice系数本质上是两个集合重叠度的度量,它对小目标更敏感,不会让模型为了“省事”而忽略前景:
import torch import torch.nn as nn import torch.nn.functional as F class DiceLoss(nn.Module): def __init__(self, smooth=1.0): super(DiceLoss, self).__init__() self.smooth = smooth def forward(self, pred, target): num_classes = pred.shape[1] pred = F.softmax(pred, dim=1) target_onehot = F.one_hot(target.long(), num_classes=num_classes).permute(0, 4, 1, 2, 3).float() intersection = (pred * target_onehot).sum(dim=(2, 3, 4)) union = pred.sum(dim=(2, 3, 4)) + target_onehot.sum(dim=(2, 3, 4)) dice = (2.0 * intersection + self.smooth) / (union + self.smooth) return 1.0 - dice.mean()我实际用的损失是加权组合:
loss = 0.7 * dice_loss + 0.3 * ce_loss为什么是0.7和0.3?我一开始用纯Dice Loss,训练曲线震荡厉害;后来加了0.3权重的CE Loss,梯度更稳定,收敛也更快。CE Loss能提供逐体素的梯度信号,Dice Loss则负责让小目标区域不被背景淹没。这个组合在医学分割里是很经典的配比。
后来我还在牙齿边界上试过加Boundary Loss,原理是通过计算预测和真值之间的距离图损失来强化边界约束,但对这个数据集提升不太明显,可能因为牙齿和周围组织边界本身就比较清楚,主要难点在于牙根和牙槽骨的灰度太接近。最终没把这个Loss纳入正式训练配置。
3.3 评估指标:DSC、HD95和IOU各自的意义
训练过程中要实时监控指标,不能光看loss。这套项目里记录了三个指标:
- DSC(Dice Similarity Coefficient):预测和真值的体素重叠度,0到1之间,越接近1越好。这是医学分割最常用的指标,但它对体积大的结构占比敏感,单看DSC容易被“整体接近”假象迷惑。
- HD95(95% Hausdorff Distance):衡量预测表面和真值表面之间的距离,单位是毫米。这个指标对边界质量非常敏感,牙根尖这种细长结构,DSC可能看起来还行,但HD95可能很高,说明边界飘了。
- IOU(Intersection over Union):交并比,和DSC类似但更严格,可以看作DSC的“监督版”。
我自己在训练时的习惯是:每个epoch结束,在验证集上同时计算DSC和HD95。如果DSC涨但HD95不降,说明模型在学“大块区域”而边界还是粗糙,这时候需要检查是不是增强太强把边界结构打乱了。
4. 训练全过程与调参记录
4.1 训练超参数配置,一组能直接抄作业的搭配
直接给一组我实验下来稳定的配置:
- 优化器:AdamW,初始学习率
lr=3e-4 - 学习率调度:Cosine Annealing,最小学习率
1e-6 - 批大小:2(输入块128x128x96,24GB显存足够了)
- Epochs:300
- 混合精度:开启(AMP)
- 梯度裁剪:max_norm=12(防止牙根边界附近梯度异常)
- 早停:验证集DSC连续30个epoch不上升就停
关于学习率,我强烈建议开始不要用1e-3。三维医学影像数据少、模型又深,1e-3很容易让训练爆炸或者收敛到不好的局部最优。AdamW配3e-4是我跑很多医学分割项目积攒下来的经验值。
训练启动大概长这样:
python train.py --data_dir ./data --batch_size 2 --lr 3e-4 --epochs 300 --amp训练过程中,我一般打开TensorBoard观察三类曲线:Dice Loss下降曲线、验证集DSC曲线、学习率变化曲线。正常的收敛过程是DSC前50个epoch快速上升,然后逐渐趋缓,到200个epoch之后基本平台期。
4.2 显存不够怎么办:混合精度、梯度累积和激活检查点
如果你的显卡只有12GB显存,比如TITAN V或者RTX 3070,跑128x128x96的batch size=2也有可能爆显存。三个实用技巧按优先级排:
第一,混合精度(AMP)。这个几乎是免费的午餐,PyTorch自带实现:
scaler = torch.cuda.amp.GradScaler() for batch in dataloader: with torch.cuda.amp.autocast(): outputs = model(images) loss = criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()代价就是一点点精度损失,至少在牙齿分割这种任务上可以忽略,但显存能省接近一半,训练速度还能提升20%-30%。
第二,梯度累积。如果你想把有效batch size变大,但显存装不下大batch:
accumulation_steps = 4 # 每次真实更新前累积4个小batch for i, batch in enumerate(dataloader): with torch.cuda.amp.autocast(): loss = criterion(model(batch["image"]), batch["label"]) loss = loss / accumulation_steps scaler.scale(loss).backward() if (i + 1) % accumulation_steps == 0: scaler.step(optimizer) scaler.update() optimizer.zero_grad()第三,激活检查点。这个稍微硬核一点,用计算换显存。通过torch.utils.checkpoint在反向传播时重新计算中间激活值,而不是一直保存在显存里:
from torch.utils.checkpoint import checkpoint def forward_with_checkpoint(x): # 把每个encoder block包进checkpoint return checkpoint(conv_block, x)对3D卷积这种显存大头,能省不少。代价是训练时间变长,因为前向传播时保存的激活被丢弃了,反向时要重新计算。
4.3 训练曲线诊断:什么情况正常,什么情况要停
观察训练曲线是门手艺活,我总结三种最常见的模式和应对:
- Loss下降但验证集DSC不涨:典型的过拟合信号。这个数据集小,一般从第150个epoch左右开始出现。应对方法是加大数据增强强度、加Dropout、或者直接减模型通道数。
- 训练一开始DSC就卡在0.2左右不涨:很可能是标签预处理出问题,比如重采样时label用了线性插值产生了中间值,或者类别索引从1开始而不是0。优先检查dataloader输出。
- Loss剧烈震荡下降缓慢:学习率太大或者batch size太小导致梯度噪声大。先降到1e-4试一下,再不行就检查标签类别是否均衡。
我训练过程中还有一次有意思的排查:DSC在70个epoch后停在0.7附近怎么都上不去。后来发现是训练时用的重采样分辨率和推理时不一致导致的数据分布偏移。所以训练和推理的预处理必须完全一致,这个建议写成代码注释提醒自己。
5. 推理、后处理与常见问题排查
5.1 滑窗推理和测试时增强,让分割结果更稳
训练时的输入是128x128x96的patch,推理时面对的是完整300x300x400的体数据。如果直接resize再输入网络,会丢失细节和影响边界精度。正确做法是滑窗(sliding window)推理:用一个固定大小的窗口在体数据上滑动,每个patch单独推理,再把结果拼回完整体积。
滑窗的关键参数是重叠率。我的经验是相邻patch之间至少要有50%的重叠,否则patch边缘的预测会很不稳定(因为边界处的体素看到的信息不完整)。重叠太高的代价是推理时间变长,50%是一个比较平衡的点。
实现片段:
def sliding_window_inference(model, volume, window_size=(128, 128, 96), overlap=0.5): strides = [int(ws * (1 - overlap)) for ws in window_size] # 计算每个方向上的patch起点 z_start = list(range(0, volume.shape[0] - window_size[0] + strides[0], strides[0])) if z_start[-1] != volume.shape[0] - window_size[0]: z_start.append(volume.shape[0] - window_size[0]) # y、x类似... output = np.zeros((num_classes, *volume.shape), dtype=np.float32) count = np.zeros(volume.shape, dtype=np.float32) # 对于重叠区域,取平均 for z0 in z_start: for y0 in y_start: for x0 in x_start: patch = volume[z0:z0+window_size[0], y0:y0+window_size[1], x0:x0+window_size[2]] patch_input = torch.from_numpy(patch).unsqueeze(0).unsqueeze(0).cuda() with torch.no_grad(): pred = model(patch_input) output[:, z0:z0+window_size[0], y0:y0+window_size[1], x0:x0+window_size[2]] += pred[0].cpu().numpy() count[z0:z0+window_size[0], y0:y0+window_size[1], x0:x0+window_size[2]] += 1 output /= count return output测试时增强也能稳定涨点。我的做法是对同一个体数据做三种变换(原图、水平翻转、垂直翻转),分别推理后再平均,最终预测结果会平滑不少,DSC通常能提升1-2个点。
5.2 后处理:连通域分析和形态学操作
网络输出的softmax概率图,argmax之后可能有一些零散的小块,特别是在牙根尖或者骨小梁区域。这些是假阳性,需要清理。我的后处理流程是:
- 先做连通域分析,只保留最大的N个连通域(牙齿一般是28-32颗,但部分智齿可能没长出,可以取最大25个左右)。
- 用3D形态学闭运算,填充牙齿内部的细小空洞。
- 可选:根据每颗牙齿的解剖位置做规则化,比如牙冠朝下、牙根朝上,这在牙位编号任务中有用。
形态学操作用scipy.ndimage就好:
from scipy import ndimage labeled, num_features = ndimage.label(pred_mask) sizes = ndimage.sum(pred_mask, labeled, range(1, num_features + 1)) keep_labels = [i + 1 for i, s in enumerate(sizes) if s > 500] # 去掉体素数小于500的散点 cleaned = np.isin(labeled, keep_labels).astype(np.uint8) cleaned = ndimage.binary_closing(cleaned, structure=np.ones((3, 3, 3))).astype(np.uint8)体素数500这个阈值不是随便定的,我统计过数据集中最细的牙根尖区域也有数百个体素,阈值设太高会把牙根尖切掉,设太低又起不到去噪作用。
5.3 常见问题速查表(直接抄作业)
我整理了在跑这个项目过程中遇到过的问题,做成速查表:
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 训练时CUDA out of memory | 输入patch太大 / batch size太大 | 减patch或用AMP,或用梯度累积 |
| 验证集DSC在0.5以下不动 | 标签mask预处理错位(插值方式错误) | label重采样改用order=0 |
| 预测结果全是背景 | 类别索引不匹配(背景=0但模型输出从0开始) | 检查数据集的label映射表 |
| 边界粗糙、HD95很大 | 滑窗重叠太小 / 后处理形态学太强 | 重叠率调到50%,结构元素不要超过3x3x3 |
| 训练和验证预处理不一致导致性能落差 | 推理时没做CLAHE或归一化参数不一致 | 把预处理逻辑抽成同一个函数,训练推理共用 |
| 牙齿之间粘连分不开 | 形态学闭运算过强 / 网络对小间隔不敏感 | 减小闭运算强度,检查标注本身是否粘连 |
| 只有背景在训练 | 损失函数权重配比失衡或学习率过大 | 用Dice Loss为主、CE为辅,学习率降到3e-4 |
这些坑里面,label插值方式错误是我见过最多人踩的。很多开源项目里都藏着这个bug,数据集本身没问题,就是预处理时图省事用了默认的线性插值,导致mask上的边界糊成一片灰,模型当然学不出好效果。
6. 数据集说明与扩展方向
6.1 数据集怎么来的,标注是什么形式
项目里的数据集我看了下,是公开的口腔CBCT分割数据集,具体来源在docs/数据集说明.md里有详细描述。数据以NIfTI格式存放,每例包含原始影像和对应的标注文件。
标注分两种形式:
- 全牙齿mask:32颗牙(含智齿)整体一个标签,适合做口腔全景分割、牙齿总数统计。
- 单牙mask:每颗牙一个独立标签(1-32编号),适合做牙位识别、牙齿排列分析、正畸规划。
如果你是用这个项目做毕业设计,我建议直接用单牙标注版本,因为单牙分割的可视化效果比整体mask好很多,可以用不同颜色渲染每一颗牙,展示时也更直观。但单牙标注对边界要求更高,如果模型预测相邻牙齿之间有一点粘连,后处理阶段就需要做“分水岭”切割。
6.2 拿到源码后怎么快速跑通全流程
按照我的经验,新拿到这套项目,别急着改代码,按这个顺序做:
- 先读
README.md和requirements.txt,把依赖装齐。 - 用项目自带的预训练权重直接跑一次推理,在验证集上把DSC指标打印出来。这一步是确认环境无误。
- 找一张CBCT体数据,跑一次推理,用ITK-SNAP或者3D Slicer看分割结果。这一步是确认模型输出合理。
- 确认无误之后,再尝试从头训练。从头训练前先删掉
checkpoints里的预训练权重,否则load_state_dict会把权重覆盖,你以为模型从头训,其实还是从预训练继续。 - 训练时用TensorBoard盯着验证集DSC,达到0.85以上就算任务达标了。
配套的技术报告写得挺规范,包含了实验设计、对比实验、主观评价标准等。如果你是准备拿来做课程设计或者毕业设计,这些文档可以直接作为报告底稿,但建议自己重新训练一遍,因为替换掉原始权重重新训练得到的指标更有说服力。
6.3 这个项目后期还能怎么扩展
单看“3D牙齿CBCT图像分割”本身,可以做很多衍生任务:
- 牙位识别与编号:基于单牙mask,把每颗牙齿和FDI牙位编号对齐,可以做成自动牙位对应系统。
- 牙齿三维重建:分割结果转成STL/PLY模型,接3D打印或CAD设计流程,做种植导板、隐形牙套设计。
- 龋齿/根尖病灶检测:在牙齿分割的基础上叠加一个分类或检测头,自动标记可疑病灶区域。
- 多序列交叉验证:CBCT往往还有对应的全景片或口扫数据,多模态对齐后联合分析。
我在实际推进中,把分割结果转成了STL网格,配合CloudCompare查看牙齿的三维模型,效果比二维切片图直观很多。如果你做的是毕业设计答辩,这一步会让展示效果提升一个档次,评分老师一眼能看出你完全理解了3D医学影像处理的完整链路。
有一点我必须提醒:医学AI项目最终如果要做临床辅助诊断,除了模型本身,还需要大量临床验证、伦理审批和设备适配,这个项目里的模型更偏技术验证和科研性质,离临床落地还有距离。
最后再分享一个实际项目里的小技巧
训完模型后,不要只盯DSC这个数字。我每次都会把预测出来的牙齿表面mesh和真值mesh叠在一起,用颜色渐变标注表面距离误差——红色是预测多了,蓝色是预测少了。这样能非常直观地看到模型在哪个解剖位置出问题。我自己的模型在第二磨牙的牙根尖处总是会出现微小的“毛刺”,后来发现是训练时弹性形变增强强度太大,牙根尖这种细长结构被拉变形了,把弹性形变的幅度从默认的sigma=15降到sigma=8之后,毛刺明显减少。这种细微的经验,光看数字指标是发现不了的。
做三维医学图像分割,最大的坑不是网络搭不出来,而是数据预处理和工程细节。同样的网络,预处理不同,DSC可以从0.6差到0.9。希望这篇能帮你少走弯路。
本文还有配套的精品资源,点击获取