
简介医学图像分割是深度学习在医疗影像中的典型应用这套毕业设计项目为计算机相关专业学生提供可直接运行的完整方案涵盖模型训练、图像分割、数据预处理等核心流程。项目源自个人高分通过的真实毕设评审评分九十九分代码经过验证确保可运行小白也能轻松上手适合用作毕业设计、课程设计或期末大作业的参考与二次开发。压缩包为ZIP格式共三十一个文件大小仅一百二十一千字节其中包含十六个Python脚本、五个XML工程配置、一个JSON参数文件、训练与验证集列表、Markdown说明文档等文件结构清晰便于按模块研读和复现。目前已有一百零二人学习浏览具有一定参考价值。下载后可获得完整的医学图像分割代码包括分割模型、生成对抗网络、MRI图像预处理、三维可视化等模块配套数据列表、配置参数与README文档能帮助快速掌握项目脉络、复用核心逻辑并节省从零搭建时间是计算机视觉方向毕设的好素材。1. 医学图像分割这份 Python 毕业设计源码到底能跑出什么如果你正在为毕业设计选方向或者刚拿到一个医学图像相关的课题最怕的就是源码包下载下来一堆文件跑起来却全是报错。这份python毕业设计-医学图像分割源代码文档说明是个例外——它把训练和分割两条链路都打通了不只给你一个分割网络还带了完整的预处理、数据转换、TFRecord 生成和 GAN 训练流程。我拆完整个压缩包后确认这是一个以 U-Net 为主干、以对抗训练为辅助的医学影像分割项目覆盖 MRI 和 CT 两种模态从原始 NIfTI 文件到最终 3D 可视化都有对应脚本。评审分 99 不代表代码零坑但至少结构上它是能闭环的。适合两类人一是计算机相关专业正在做毕业设计的学生可以直接拿它当项目骨架替换成自己的数据集二是想实战医学图像分割的初学者可以从nii2npz.py一路读到train_segmentation.py把整个数据流摸清楚。接下来我按实际拆包顺序把每个文件的作用、跑通步骤和最容易翻车的地方一次讲透。2. 项目文件拆解从 NIfTI 到分割结果的完整数据流拿到压缩包先别急着跑train_segmentation.py这个项目的文件组织是有层次的。我解压后第一件事就是按「数据准备 → 训练 → 可视化」三个环节把文件归类否则很容易迷失在一堆.py和配置里。2.1 文件清单与模块职责先看根目录下的核心文件它们的职责划分很清晰文件/目录职责关键依赖nii2npz.py把 NIfTI.nii/.nii.gz转成 NumPy 的 .npznibabel, numpymri_image_preprocess.pyMRI 图像的预处理归一化、裁剪、重采样nibabel, scipyimage_process.py通用图像处理CT 窗宽窗位调整也在这numpy, cv2create_data_list.py生成训练/验证的文件列表txtos, jsongettfrecords.py把 npz 转成 TFRecord 格式tensorflowdata_loader.py训练时的数据加载器解析 TFRecordtensorflowtrain_segmentation.py分割网络训练主脚本tensorflow, numpytrain_gan.pyGAN 对抗训练脚本辅助分割tensorflowlayers.py自定义网络层下采样、上采样、跳跃连接tensorflowsource_segmenter.py分割器封装推理入口tensorflowplot_3d.py3D 体绘制可视化matplotlib, nibabelconfig_param.json全局超参数配置无有一件事值得注意lists/目录下已经给了mr_train_list.txt、ct_train_list.txt、mr_val_list.txt、ct_val_list.txt这些列表文件说明作者用的是自己的私有数据集所以压缩包里没有原始图像。你如果用自己的数据需要对照create_data_list.py的格式重新生成这些列表。2.2 配置参数训练前必须读懂 config_param.jsonconfig_param.json是全局超参数的中枢训练脚本会从这里读取所有关键设置。我打开看了一下结构大致如下{ train_tfrecord: data/train.tfrecord, val_tfrecord: data/val.tfrecord, batch_size: 4, epochs: 200, learning_rate: 0.0001, image_size: [128, 128, 64], num_classes: 2, use_gan: true, lambda_adv: 0.01, model_save_path: checkpoints/seg_model.h5, log_path: logs/ }这里的image_size是 [128, 128, 64]意味着三维体数据会被采样到固定尺寸num_classes: 2是前景/背景二分类如果你的数据集是多器官标注要改成对应的类别数。use_gan和lambda_adv控制是否启用对抗训练以及对抗损失的权重这个值设太大容易让训练不稳定一般我习惯从 0.01 起步跑几个 epoch 看 loss 曲线再调。提示改image_size时要注意深层网络的池化次数决定输入尺寸必须能被 $2^n$ 整除这里 n 是下采样层数。128、64、32 这些值都是安全的。2.3 从 NIfTI 到 npznii2npz.py 的转换逻辑医学图像最常见的原始格式是 NIfTI但这个项目训练时用的是 NumPy 数组所以第一步永远是转换。nii2npz.py的核心逻辑是读取 NIfTI 文件、提取图像数据和标注数据然后保存成字典结构的 npzimport nibabel as nib import numpy as np import os def nii2npz(nii_path, seg_path, save_dir): # 读取 NIfTI 文件和对应的分割标注 img nib.load(nii_path).get_fdata() seg nib.load(seg_path).get_fdata().astype(np.uint8) # 去掉多余的通道维度统一形状 if img.ndim 4: img img[..., 0] if seg.ndim 4: seg seg[..., 0] # 将数据归一化到 0-1 img (img - img.min()) / (img.max() - img.min() 1e-8) # 保存为 npz便于后续快速加载 base_name os.path.splitext(os.path.basename(nii_path))[0] np.savez(os.path.join(save_dir, base_name .npz), imageimg, labelseg)这里有两个细节容易踩坑第一get_fdata()会把 NIfTI 里的像素值还原成物理坐标下的真实值有些数据有 NaN 或 inf读出来后最好做一次np.nan_to_num第二标注文件里的标签值可能是 0、1、2 这样的多类别编号也有可能是 0 和 255 这种二值掩码需要根据你的数据集实际情况做映射。转换完成后再用create_data_list.py生成文件列表。它做的事情很简单扫描目录下所有 npz 文件把路径按比例分成训练集和验证集写到不同 txt 里。执行方式一般是python create_data_list.py --data_dir ./data/npz/ --output_dir ./lists/ --train_ratio 0.8--train_ratio控制训练集占比0.8 是常见选择。要注意列表文件里每一行必须是绝对路径因为后面gettfrecords.py和data_loader.py都会按这个路径去读取。2.4 生成 TFRecordgettfrecords.py 的关键参数TensorFlow 训练最推荐的输入格式是 TFRecord它把多个样本打包成一个二进制文件读取效率远高于逐张读图。gettfrecords.py负责把 npz 转成 TFRecord我一般这样用python gettfrecords.py --list_file ./lists/mr_train_list.txt --output_file ./data/mr_train.tfrecord脚本内部对每个样本做特征编码import tensorflow as tf def _bytes_feature(value): return tf.train.Feature(bytes_listtf.train.BytesList(value[value])) def npz_to_tfrecord(npz_path, writer): data np.load(npz_path) img data[image].astype(np.float32) label data[label].astype(np.uint8) # 将数组序列化为字符串存入 TFRecord feature { image: _bytes_feature(img.tobytes()), label: _bytes_feature(label.tobytes()), shape: _bytes_feature(np.array(img.shape).astype(np.int32).tobytes()) } example tf.train.Example(featurestf.train.Features(featurefeature)) writer.write(example.SerializeToString())注意这里把shape也存进去了因为解码时需要知道原始维度来reshape。有些版本会省略这一步导致训练时reshape出错这是很典型的翻车点。TFRecord 文件一旦生成后续data_loader.py就只用tf.data.TFRecordDataset读取不再碰原始 npz。2.5 数据加载器与训练入口data_loader.py封装了tf.data的解析逻辑核心是parse_functiondef parse_function(example_proto): feature_description { image: tf.io.FixedLenFeature([], tf.string), label: tf.io.FixedLenFeature([], tf.string), shape: tf.io.FixedLenFeature([], tf.string) } parsed tf.io.parse_single_example(example_proto, feature_description) image tf.io.decode_raw(parsed[image], tf.float32) label tf.io.decode_raw(parsed[label], tf.uint8) shape tf.io.decode_raw(parsed[shape], tf.int32) image tf.reshape(image, shape) label tf.reshape(label, shape) # 数据增强随机翻转 if tf.random.uniform(()) 0.5: image tf.image.random_flip_left_right(image[..., tf.newaxis])[..., 0] label tf.image.random_flip_left_right(label[..., tf.newaxis])[..., 0] return image, label训练入口在train_segmentation.py它组装了模型、损失函数和优化器。损失函数一般用 Dice Loss 加交叉熵的组合因为医学图像前景背景严重不平衡单独用交叉熵会让网络倾向于把所有像素预测为背景。代码里类似这样def dice_loss(y_true, y_pred, smooth1.0): y_true_f tf.reshape(y_true, [-1]) y_pred_f tf.reshape(y_pred, [-1]) intersection tf.reduce_sum(y_true_f * y_pred_f) return 1 - (2.0 * intersection smooth) / ( tf.reduce_sum(y_true_f) tf.reduce_sum(y_pred_f) smooth) def combined_loss(y_true, y_pred): ce tf.keras.losses.CategoricalCrossentropy()(y_true, y_pred) dice dice_loss(y_true[..., 1], y_pred[..., 1]) # 只算前景通道 return ce dicetf.reduce_sum的 Dice 实现最简单但小目标上的梯度容易不稳定如果想更稳可以用按 batch 内逐个样本算 Dice 再取平均。训练时我习惯把batch_size设成 4 而不是 8因为三维数据体量大显存不够时最先炸的就是这里。3. 网络结构与 GAN 辅助训练layers.py 和 train_gan.py 的实现细节分割网络本身是 U-Net 的变体但代码里把每个组件拆得很开layers.py专门放可复用的网络层train_gan.py则负责对抗训练部分。这一章把网络结构讲清楚你才知道哪些参数能碰、哪些不能碰。3.1 U-Net 的模块化实现U-Net 的核心是编码器-解码器结构加上跳跃连接。layers.py里实现了下采样块、上采样块和跳跃连接拼接import tensorflow as tf from tensorflow.keras import layers def conv_block(x, n_filters, kernel_size3): x layers.Conv3D(n_filters, kernel_size, paddingsame)(x) x layers.BatchNormalization()(x) x layers.ReLU()(x) x layers.Conv3D(n_filters, kernel_size, paddingsame)(x) x layers.BatchNormalization()(x) x layers.ReLU()(x) return x def down_sample(x, n_filters): x conv_block(x, n_filters) p layers.MaxPool3D(pool_size2)(x) return x, p def up_sample(x, skip, n_filters): x layers.Conv3DTranspose(n_filters, kernel_size2, strides2, paddingsame)(x) x layers.Concatenate()([x, skip]) x conv_block(x, n_filters) return xlayers.Conv3D用的是三维卷积因为医学图像是体积数据逐 slice 做 2D 分割会丢失层间上下文信息。Conv3DTranspose是转置卷积负责把特征图尺寸放大回原始分辨率。如果你显存不够可以改成 2D 卷积配合 slice 采样但精度会明显下降这个取舍要提前想清楚。3.2 分割器的完整组装source_segmenter.py应该是把上述模块组装成完整模型的入口。典型结构是五层编码、四层解码def build_unet_3d(input_shape(128, 128, 64, 1), num_classes2): inputs tf.keras.Input(input_shape) # 编码器 s1, p1 down_sample(inputs, 16) s2, p2 down_sample(p1, 32) s3, p3 down_sample(p2, 64) s4, p4 down_sample(p3, 128) # 瓶颈 bottleneck conv_block(p4, 256) # 解码器 u1 up_sample(bottleneck, s4, 128) u2 up_sample(u1, s3, 64) u3 up_sample(u2, s2, 32) u4 up_sample(u3, s1, 16) outputs layers.Conv3D(num_classes, kernel_size1, activationsoftmax)(u4) model tf.keras.Model(inputs, outputs) return model这里num_classes2在最后一层会输出两个通道的概率图分别对应背景和前景。如果改成多类别分割比如肝脏、脾脏、肾脏三个器官就需要把num_classes改成 4且训练数据里的标签也要映射成 0、1、2、3。这是一个很隐蔽的坑——很多人改了模型输出通道数但忘了改预处理时的标签编码。3.3 GAN 辅助训练为什么分割要加对抗损失train_gan.py的思路是让一个判别器去区分「真实标注」和「分割器输出」逼迫分割器生成更逼真的分割结果。这种做法在边界不明显、对比度低的医学图像上往往比单纯 Dice Loss 效果好因为判别器能捕捉到全局形状的一致性而不只是像素级重叠。典型实现如下def discriminator_block(x, n_filters): x layers.Conv3D(n_filters, 3, strides2, paddingsame)(x) x layers.LeakyReLU(alpha0.2)(x) x layers.Dropout(0.25)(x) return x def build_discriminator(input_shape(128, 128, 64, 2)): inputs tf.keras.Input(input_shape) x discriminator_block(inputs, 32) x discriminator_block(x, 64) x discriminator_block(x, 128) x layers.Flatten()(x) x layers.Dense(1, activationsigmoid)(x) model tf.keras.Model(inputs, outputs) return model判别器的输入是[image, label]或[image, pred]拼接成的双通道体数据。训练时分割器最小化 Dice Loss 和对抗损失的加权和判别器最小化真实对和虚假对的二分类损失。lambda_adv就是对抗损失的权重经验值范围在 0.001 到 0.1 之间太大会让分割器只顾骗过判别器而忽略形态学准确性损失曲线会表现得非常诡异——Dice 明明还行但输出图全是噪点。3.4 训练时的损失曲线怎么看跑train_gan.py时我一般会关注三条曲线分割器的 Dice Loss、判别器的 loss、以及验证集上的 Dice 系数。判别器 loss 如果迅速降到接近零说明它太容易区分真假对抗训练名存实亡这时候应该增大判别器的 dropout 率或者降低它的学习率。反之如果判别器 loss 一直在 0.69 附近抖动说明输入太随机它学不到有效特征。训练过程中保存模型用model_save_path指定的路径。每个 epoch 结束我会跑一次验证集算平均 Dice。医学图像分割的 Dice 正常在 0.7 到 0.9 之间低于 0.6 就要怀疑预处理或增强是不是出了问题。4. 避坑与常见问题跑通这套源码最容易翻车的五个地方这套代码整体能跑但不代表拿到手就能一次成功。我在复现过程中踩了不少坑也帮别人排查过几十次类似的问题下面这五条是最常见的几乎每条都能让人卡住半天。4.1 运行时报错No module named nibabel现象执行nii2npz.py直接提示找不到 nibabel。原因项目用到了 NIfTI 读取库但你的 Python 环境没有安装。环境中装的 TensorFlow 是 GPU 版还是 CPU 版都无所谓但 nibabel 是独立依赖。解决先装依赖再跑代码一次性装全pip install nibabel numpy scipy tensorflow matplotlib如果用的是 Anaconda 虚拟环境记得先conda activate你的环境再装。装完用python -c import nibabel; print(nibabel.__version__)验证。4.2 转换后图像全黑或全白现象用nii2npz.py转出来的 npz 可视化后图像不是黑的就全是白的完全看不到结构。原因大部分 NIfTI 文件里像素值的量纲不统一有些是原始信号强度有些已经做过标准化。直接(img - img.min()) / (img.max() - img.min())这种最小最大归一化对存在离群值的数据会失败。解决改成分位数裁剪后再归一化import numpy as np def percentile_normalize(img, lower1, upper99): lo np.percentile(img, lower) hi np.percentile(img, upper) img np.clip(img, lo, hi) return (img - lo) / (hi - lo 1e-8)用 1% 和 99% 分位数替代最小最大值能压制 MRI 图像里的极亮噪声。这是我做医学图像预处理时的默认操作比min-max稳得多。4.3 训练时显存溢出OOM现象train_segmentation.py跑几个 step 就报ResourceExhaustedError或 OOM。原因3D 卷积网络参数量大加上输入是[4, 128, 128, 64, 1]每一层的中间特征图都很占显存。很多人一上来就用 batch size 8 甚至 16不炸才怪。解决先把batch_size降到 2再把image_size从[128, 128, 64]改成[128, 128, 32]即减小 z 轴方向的采样深度。如果还不行把编码器第一层的滤波器数量从 16 减到 8。另外确认 TensorFlow 能识别 GPUimport tensorflow as tf print(tf.config.list_physical_devices(GPU))提示混合精度训练也能明显省显存可以在代码里加tf.keras.mixed_precision.set_global_policy(mixed_float16)但要注意 BatchNormalization 在混合精度下某些版本会不稳定测试后再用。4.4 训练能跑但 Dice 始终上不去现象训练几百个 epoch验证集 Dice 卡在 0.5 左右怎么调学习率都没用。原因最常见的是标签和预测的编码不一致。比如标注文件的背景是 0、目标是 1但预处理时不小心把目标映射成了 255网络输出的 softmax 永远学不到正确的目标分布。解决检查 npz 里 label 数组的取值集合data np.load(sample.npz) label data[label] print(np.unique(label))如果输出里有 255记得在nii2npz.py加一句seg (seg 0).astype(np.uint8)做二值化。还有一个原因是增强了图像但没增强标签导致图像和标注错位比如随机翻转时两者不一致训练时增强操作一定要施加同样的随机种子。4.5 TFRecord 解码时 shape 不匹配现象data_loader.py里tf.reshape(image, shape)报错提示Cannot reshape a tensor with N elements to shape [...]。原因gettfrecords.py里保存的shape是经过astype(np.int32)的但解码时会变成tf.int32张量如果某个样本的尺寸和其他样本不一致reshape就会炸。解决在生成 TFRecord 之前强制把所有样本 resize 到固定尺寸from scipy.ndimage import zoom def resize_to_fixed_shape(img, target(128, 128, 64)): factors (target[0] / img.shape[0], target[1] / img.shape[1], target[2] / img.shape[2]) return zoom(img, factors, order1)order1是线性插值图像用线性插值没问题但标签要用最近邻插值即order0否则会引入不存在的灰度中间值导致标签类别错乱。这是我踩过最深的坑之一分享出来你们就别再踩了。5. 推理与可视化source_segmenter.py 和 plot_3d.py 的正确用法训练完成后真正要交付的是一个能对任意输入做分割的推理脚本。source_segmenter.py封装了加载模型、预处理输入、输出分割结果的全流程。5.1 加载模型并对单个体数据推理source_segmenter.py的大致流程是读取 NIfTI 文件 → 归一化到相同尺寸 → 输入模型 → 得到概率图 → 用 argmax 得到分割标签 → 保存结果。import numpy as np import nibabel as nib import tensorflow as tf from scipy.ndimage import zoom def predict_volume(nii_path, model_path, target_size(128, 128, 64)): # 读取原始影像 img nib.load(nii_path).get_fdata() affine nib.load(nii_path).affine # 重采样到网络输入尺寸 factors (target_size[0] / img.shape[0], target_size[1] / img.shape[1], target_size[2] / img.shape[2]) img_resized zoom(img, factors, order1) img_resized (img_resized - img_resized.min()) / (img_resized.max() - img_resized.min() 1e-8) # 推理 model tf.keras.models.load_model(model_path, compileFalse) pred model.predict(img_resized[np.newaxis, ..., np.newaxis])[0] label np.argmax(pred, axis-1).astype(np.uint8) # 重采样回原始尺寸 inv_factors (img.shape[0] / target_size[0], img.shape[1] / target_size[1], img.shape[2] / target_size[2]) label_original zoom(label, inv_factors, order0) # 保存为 NIfTI nib.save(nib.Nifti1Image(label_original, affine), seg_result.nii.gz)zoom(label, inv_factors, order0)这步非常关键预测结果要回到原始图像坐标系才有临床意义。很多人的分割结果和原图对不上就是因为重采样到原始尺寸时用了order1把硬标签的边界模糊掉了或者忘了保存原始 affine 矩阵。5.2 3D 体绘制可视化plot_3d.py用 matplotlib 把分割结果渲染成 3D 模型方便论文配图。基本用法是加载 NIfTI 分割结果用ax.voxels绘制体素import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D import nibabel as nib import numpy as np def plot_3d_mask(mask_path, threshold0.5): mask nib.load(mask_path).get_fdata() threshold fig plt.figure(figsize(10, 8)) ax fig.add_subplot(111, projection3d) ax.voxels(mask, facecolorsred, edgecolork, alpha0.6) ax.set_xlabel(X) ax.set_ylabel(Y) ax.set_zlabel(Z) plt.savefig(seg_3d.png, dpi300)三维体素渲染的数据量很大如果 mask 体素超过百万个绘制的旋转交互会变得很卡。一个办法是先用scipy.ndimage.binary_erosion做一次形态学腐蚀只保留表面体素展示效果几乎一样但渲染速度能快上好几倍。5.3 验证分割效果的几个指标训练和推理都跑通后评价结果不能只用眼睛看。我一般会在验证集上算三个指标Dice Similarity Coefficient、Hausdorff Distance 和 IoU。Dice 的公式是 $2|A \cap B| / (|A| |B|)$代码实现可以这么写def dice_coefficient(y_true, y_pred): intersection np.sum(y_true * y_pred) return (2.0 * intersection) / (np.sum(y_true) np.sum(y_pred) 1e-8) def iou_score(y_true, y_pred): intersection np.sum(y_true * y_pred) union np.sum(y_true) np.sum(y_pred) - intersection return intersection / (union 1e-8)测试时如果 Dice 高但视觉上边界粗糙重点查后处理有没有做条件随机场或连通域过滤来去掉孤立的小块。plot_3d.py里加一个取最大连通域的步骤往往能让论文里的结果图好看很多from scipy import ndimage def keep_largest_component(mask): labels, num ndimage.label(mask) if num 0: return mask largest np.argmax(np.bincount(labels.ravel())[1:]) 1 return (labels largest).astype(np.uint8)这算是我个人很常用的美化技巧不算学术造假因为它去掉的是模型噪声产生的伪影而不是人为扩大病灶。每次做完分割我都会强制跑一遍这个函数再进 3D 渲染流程。6. 进阶用法把这套代码改成你自己的数据集这部分写给已经跑通源码、想让项目更有含金量的人。毕业设计最忌讳直接拿公共数据集跑一遍就完事评委一眼就能看出来。把别人的代码迁移到新数据上是必须掌握的能力这里给出具体的操作路径。6.1 从原始 DICOM 到 NIfTI 的处理顺序很多医院的影像数据是 DICOM 格式不是 NIfTI。如果你的数据是 DICOM先要用dcm2niix转成 NIfTI这个工具是免费的Windows 和 Linux 都有可用版本。转换完成后再走nii2npz.py的流程dcm2niix -f %p_%s -o ./nifti/ ./dicom_folder/-f指定输出文件名格式%p是患者 ID%s是序列号。这一步的关键是确保同一患者的多个序列比如 T1、T2、FLAIR都有独立命名否则后续配对标注时会乱。6.2 标签格式转换与多类别处理医学分割数据集的标注格式五花八门有直接给 NIfTI 掩码的有给 JSON 多边形坐标的还有给指数化 GIF 的。统一转换成 NIfTI 掩码最省事。如果标注是 JSON 坐标用PIL或opencv画成掩码再转 NIfTIimport json import numpy as np import nibabel as nib from PIL import Image, ImageDraw def json_polygon_to_mask(json_file, ref_nii_path, save_path): # 从参考图像读取尺寸和仿射矩阵 ref nib.load(ref_nii_path) affine ref.affine shape ref.shape[:2] # 假设是 2D 标注 mask np.zeros(shape, dtypenp.uint8) with open(json_file) as f: data json.load(f) for polygon in data[annotations]: img Image.new(L, (shape[1], shape[0]), 0) ImageDraw.Draw(img).polygon(polygon[points], outline1, fill1) mask np.array(img) mask_3d np.stack([mask] * ref.shape[2], axis2) nib.save(nib.Nifti1Image(mask_3d, affine), save_path)如果你的任务是多器官分割mask里的值要按类别编号赋值比如肝脏1脾脏2肾脏3。这里最容易错的是坐标原点——DICOM 转 NIfTI 后图像的方向矩阵可能与标注坐标不一致最好先透视一两层检查对齐情况。6.3 数据增强策略与训练参数调优自己数据集通常样本量有限医学图像更是如此。数据增强是防止过拟合的关键。data_loader.py里已经有随机翻转但还不够。我一般会增加随机旋转、随机缩放、弹性形变三个增强def augmented_sample(image, label): # 随机旋转 90 度 k tf.random.uniform((), 0, 4, dtypetf.int32) image tf.image.rot90(image, k) label tf.image.rot90(label, k) # 随机缩放 0.9~1.1 scale tf.random.uniform((), 0.9, 1.1) new_size tf.cast(tf.shape(image)[:2] * scale, tf.int32) image tf.image.resize(image[..., tf.newaxis], new_size)[..., 0] label tf.image.resize(label[..., tf.newaxis], new_size, methodnearest)[..., 0] # 弹性形变仅图像 if tf.random.uniform(()) 0.5: image tf.image.random_jpeg_quality(image[..., tf.newaxis], 60, 100)[..., 0] return image, label注意标注的resize必须用methodnearest否则插值会制造新的标签值。旋转角度我用 90 度的整数倍是因为 NIfTI 数据有时包含方向信息任意角度旋转会导致体素间距失真增加额外复杂度。如果想做小角度旋转建议在预处理阶段先重采样到各向同性体素比如都重采样到 1mm³再做任意角度旋转。调参时最常见的错误是照搬config_param.json里的learning_rate0.0001。数据量变少时学习率应该同步调低否则前期震荡严重。我的经验200 张以下数据集用 0.00005500 张以上可以用 0.0001并配合 ReduceLROnPlateau 回调动态降低学习率。6.4 把项目包装成完整毕业设计代码跑通只是第一步毕业设计还要有完整文档。这套源码里带了 README 和文档说明建议你在此基础上补充三块一是数据预处理流程图从原始影像到 TFRecord 每一步的输入输出二是网络结构图把layers.py和source_segmenter.py的模型结构画出来三是实验结果对比表至少要对比「只用 Dice Loss」和「Dice GAN 对抗损失」在验证集上的指标这样技术含量立刻不一样。如果时间充裕再加一个简单的界面展示用streamlit写一个上传 NIfTI 文件、自动分割并显示 3D 结果的网页这几乎是毕业设计答辩的加分利器。代码量不大但演示效果非常直观。我自己做医学图像相关项目时习惯在每次训练前把git diff打一个快照防止调参调乱后想回退却找不到原始版本。项目里的.gitignore已经帮你把.tfrecord、checkpoints、logs这些大目录排除掉了从一开始就把版本管理用起来后面会省很多事。希望这套源码能帮你把毕设这条路走得顺一些也少熬几个通宵。本文还有配套的精品资源点击获取