脑PET图像疾病预测Python源码工程全流程解析
简介面向基于Python机器学习的脑PET图像分析与疾病预测任务这套资源提供一套可运行的工程包含模型训练、预测、图像裁剪与配置模块适合计算机相关专业学生用于课程设计、毕业设计或实训作业也适合医学图像入门者对照学习完整流程。压缩包共6个文件以4个Python脚本为主配有必要说明文档与依赖清单整体约10KB结构紧凑便于快速熟悉项目。目前已有84人学习下载。借助该资源使用者可了解PET图像预处理、特征提取与疾病预测模型的串联方法通过现成脚本减少环境搭建和重复编码时间说明文档和依赖列表降低复现门槛调整配置即可适配不同数据路径或模型参数方便在此基础上进行二次开发与实验扩展。1. 脑PET图像分析与疾病预测这份Python源码包能复现什么做医学图像相关的课程设计或毕业设计最头疼的不是算法本身而是从头搭一套能跑通的数据管道。脑PET图像又不是普通照片格式、预处理、特征提取每一环都能卡人。这份资源包恰好把从原始图像到疾病预测结果的全流程都写好了crop.py负责裁出感兴趣区域1_train.py完成训练2_predict.py加载模型做预测config.py统一管参数。适合三类人——要交毕业设计的学生、给学生布置实训作业的高校老师、以及刚入门医学图像分析想找个完整工程参考的开发者。拿到手你先看介绍.md再把requirements.txt装上就能跑通整个流程。2. 项目结构拆解从config.py到requirements.txt的依赖关系2.1 六个文件各自承担什么任务这份资源包的工程划分非常规矩六个文件各管一摊没有把代码堆在一个脚本里。我拆过不少课程设计源码这个结构算是同类中比较清晰的适合直接拿去改。文件职责运行时机介绍.md项目概述、安装步骤、运行方法动手前先读config.py全局参数路径、模型参数、输出配置所有脚本依赖crop.py对原始PET图像做感兴趣区域ROI裁剪数据处理阶段先于训练1_train.py数据预处理、特征提取、模型训练与保存训练阶段2_predict.py加载训练好的模型对输入图像做预测推理阶段requirements.txt锁定Python依赖库及版本首次运行前安装数据流是单向的原始PET图像先进crop.py裁剪然后1_train.py把裁剪结果转换为训练特征并训练模型模型保存后2_predict.py对新图像走同样预处理路径最后输出预测结果。我一般会先跑crop.py因为后两个脚本默认读的是裁剪后的图像。2.2 config.py所有可调参数的统一入口这个文件是你改配置的第一站不用动算法代码就能切换数据集、调整训练参数。常见的配置项大概是这几类# config.py 关键参数示意 import os # 数据集路径配置 RAW_DATA_DIR ./data/raw # 原始PET图像存放目录 CROPPED_DATA_DIR ./data/cropped # 裁剪后图像输出目录 TRAIN_SPLIT 0.8 # 训练/验证集划分比例 # 图像预处理参数 TARGET_SIZE (128, 128) # 统一缩放尺寸 NORMALIZE_RANGE (0, 1) # 像素值归一化范围 # 模型参数 MODEL_TYPE random_forest # 可选svm / random_forest / cnn N_ESTIMATORS 200 # 随机森林的树数量仅对RF生效 RANDOM_STATE 42 # 固定随机种子保证结果可复现 # 训练参数 BATCH_SIZE 32 # 批量大小CNN训练时使用 EPOCHS 50 # 迭代轮数CNN训练时使用 LEARNING_RATE 0.001 # 学习率 # 输出配置 MODEL_SAVE_PATH ./outputs/model.pkl # 模型保存路径 PREDICT_RESULT_PATH ./outputs/predictions.csv # 预测结果输出逻辑说明配置文件把所有可变项集中管理训练脚本和预测脚本都通过import config读取这些值。你要换数据集改RAW_DATA_DIR要换模型改MODEL_TYPE要调整资源占用改TARGET_SIZE和BATCH_SIZE。这样不用在训练脚本里翻找硬编码的路径和超参数。参数说明RANDOM_STATE 42这类随机种子务必固定否则每次训练结果不一致写论文时无法复现实验数据。TRAIN_SPLIT控制训练集占比这个资源包默认0.8如果数据量小可以适当调高到0.9。2.3 requirements.txt依赖安装与版本兼容这个文件是项目能跑起来的基础。你自己从零搭环境时经常被版本冲突折磨——某个库更新后API变了代码就报错。requirements.txt把这件事的后悔药提前备好了。# 在项目根目录执行 pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple先装依赖再跑代码这是固定动作。装完后可以用pip list检查一下核心包版本pip list | grep -E numpy|scikit-learn|torch|opencv逻辑说明requirements.txt里如果写了numpy1.21.6这种锁定版本的形式说明原作者在某个固定版本下测试过。你要是装最新版numpy很可能会遇到API不兼容的报错。遇到这类问题优先把依赖降到锁定版本而不是去改代码适配新版库。医学图像处理里OpenCV的版本尤其敏感cv2接口变化经常导致图像读取或裁剪逻辑出错。注意如果你的Python版本是3.10以上部分老版本依赖包可能找不到预编译的wheel需要降级Python版本或者用conda创建独立环境conda create -n pet_env python3.8 conda activate pet_env pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple我一般习惯用conda环境隔离项目不污染系统Python。医学图像领域的老代码对Python版本要求比较苛刻很多只能兼容3.6到3.8区间。3. 训练全流程1_train.py的数据管道与模型选择3.1 数据加载与预处理管道训练脚本是整个项目的核心。PET图像不同于普通自然图像它记录的是放射性示踪剂在脑组织中的分布所以预处理时不能简单resize。典型的数据加载流程如下# 1_train.py 数据加载与预处理示意 import os import numpy as np import cv2 from sklearn.model_selection import train_test_split from config import RAW_DATA_DIR, CROPPED_DATA_DIR, TARGET_SIZE, NORMALIZE_RANGE def load_pet_images(data_dir, target_size(128, 128)): images [] labels [] # 假设目录结构为data/cropped/正常/xxx.png 和 data/cropped/疾病/yyy.png for class_idx, class_name in enumerate([normal, disease]): class_dir os.path.join(data_dir, class_name) for file_name in os.listdir(class_dir): if not file_name.lower().endswith((.png, .jpg, .nii)): continue img_path os.path.join(class_dir, file_name) # 读取图像PET图像通常为灰度图 img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) img cv2.resize(img, target_size, interpolationcv2.INTER_LINEAR) img img.astype(np.float32) # 归一化到 [0, 1] 区间 img (img - np.min(img)) / (np.max(img) - np.min(img) 1e-8) images.append(img) labels.append(class_idx) return np.array(images), np.array(labels) X, y load_pet_images(CROPPED_DATA_DIR, TARGET_SIZE) X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, random_state42, stratifyy ) # 简单打平供传统机器学习模型使用 X_train_flat X_train.reshape(len(X_train), -1) X_val_flat X_val.reshape(len(X_val), -1)逻辑说明load_pet_images函数按类别子目录读取图像统一缩放尺寸再对每张图做线性对比度拉伸——即把最小像素值映射到0最大像素值映射到1。PET图像的原始像素范围因扫描设备、剂量差异而变化这一步归一化是训练收敛的前提。参数说明target_size(128, 128)是常见的折中尺寸——太小丢失空间细节太大增加计算量。如果你的显存充足可以尝试(224, 224)但训练时间会增加好几倍。stratifyy按类别比例划分训练集和验证集防止某一类在验证集中占比失衡这在医学数据类别不平衡的场景下非常重要。3.2 模型选择的思路SVM、随机森林还是CNN这个资源包的config.py里预留了多种模型类型。我在类似项目上的经验是数据量少于几千张时传统机器学习模型更稳数据量大且有GPU时再上CNN。# 1_train.py 模型选择示意 from sklearn.svm import SVC from sklearn.ensemble import RandomForestClassifier def build_model(model_typerandom_forest, n_estimators200, random_state42): if model_type svm: model SVC(kernelrbf, C1.0, gammascale, probabilityTrue, random_staterandom_state) print(使用SVM模型RBF核) elif model_type random_forest: model RandomForestClassifier( n_estimatorsn_estimators, max_depthNone, random_staterandom_state, n_jobs-1 ) print(f使用随机森林树数量{n_estimators}) elif model_type cnn: # 实际项目中此处会调用Keras或PyTorch构建卷积网络 model build_cnn_model() print(使用CNN模型) else: raise ValueError(f不支持的模型类型: {model_type}) return model逻辑说明SVM在小样本场景下泛化能力强适合特征维度不高的情况但训练时间随样本量增长明显。随机森林对特征噪声的容忍度高基本不需要做太多特征工程是目前医学图像基线实验的首选。CNN能端到端学习空间特征但需要足够数据支撑且训练调参复杂度更高。参数说明n_estimators200是随机森林的核心参数树越多模型越稳但超过300后边际收益递减、训练时间线性增长。probabilityTrue让SVM输出类别概率而不是只有硬预测标签这在后续分析预测置信度时很有用。如果你只有几百张图像不建议选CNN——过拟合风险极高验证集准确率会停留在60%上下玄学震荡。3.3 训练循环与模型保存# 1_train.py 训练与保存示意 from sklearn.metrics import accuracy_score, classification_report import joblib def train_model(model, X_train_flat, y_train, X_val_flat, y_val): print(f开始训练训练样本数: {len(X_train_flat)}) model.fit(X_train_flat, y_train) # 验证集评估 y_pred model.predict(X_val_flat) acc accuracy_score(y_val, y_pred) print(f验证集准确率: {acc:.4f}) print(classification_report(y_val, y_pred, target_names[normal, disease])) return model model build_model(MODEL_TYPE, N_ESTIMATORS, RANDOM_STATE) model train_model(model, X_train_flat, y_train, X_val_flat, y_val) # 保存模型到指定路径 os.makedirs(os.path.dirname(MODEL_SAVE_PATH), exist_okTrue) joblib.dump(model, MODEL_SAVE_PATH) print(f模型已保存到: {MODEL_SAVE_PATH})逻辑说明model.fit执行实际的训练过程传统机器学习模型是求解一个目标函数的优化问题。classification_report会输出每个类别的精确率、召回率和F1值——只看准确率在医学场景下会骗人如果疾病样本占比只有10%模型全部预测为正常也能拿到90%准确率报告里的召回率会立刻暴露这个问题。参数说明joblib.dump保存的是完整模型对象预测脚本直接用joblib.load读回来不需要重新训练。我一般还会用joblib.dump(model, MODEL_SAVE_PATH, compress3)加一层压缩模型文件体积能缩小一半以上加载速度反而更快。4. 预测与裁剪2_predict.py和crop.py的联动逻辑4.1 预测脚本的输入规范2_predict.py解决的是部署场景的问题——有了训练好的模型来一张新图像怎么输出结果。预测脚本的关键点在于数据预处理必须与训练时完全一致差一步都会导致结果偏掉。# 2_predict.py 预测流程示意 import numpy as np import cv2 import joblib from config import TARGET_SIZE, MODEL_SAVE_PATH, PREDICT_RESULT_PATH def preprocess_single_image(img_path, target_size(128, 128)): img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: raise FileNotFoundError(f无法读取图像: {img_path}) img cv2.resize(img, target_size, interpolationcv2.INTER_LINEAR) img img.astype(np.float32) img (img - np.min(img)) / (np.max(img) - np.min(img) 1e-8) return img.reshape(1, -1) # 单样本shape为(1, 特征数) def predict_single_image(model, img_path): features preprocess_single_image(img_path) # 输出类别概率或硬标签 proba model.predict_proba(features)[0] label model.predict(features)[0] class_names [normal, disease] result { image: img_path, predicted_class: class_names[int(label)], probability_normal: round(float(proba[0]), 4), probability_disease: round(float(proba[1]), 4) } return result model joblib.load(MODEL_SAVE_PATH) result predict_single_image(model, ./samples/test_patient.nii) print(result)逻辑说明preprocess_single_image必须与训练脚本里的load_pet_images保持完全相同的预处理逻辑——相同的缩放尺寸、相同的归一化方式。如果训练时用了(img - min) / (max - min)归一化预测时却用了img / 255特征分布完全错位模型的预测置信度会变得非常怪异。参数说明predict_proba在很多机器学习库中返回二维数组[0]取第一个样本的概率分布返回的是一个长度为2的数组分别表示正常和疾病的概率。我只建议在config.py中开启probabilityTrue时才调用这个接口否则SVM模型直接调用predict会更快。输出格式是字典实际项目中可以再把它写入CSV方便批量分析预测结果。4.2 crop.py感兴趣区域裁剪的工程细节医学图像不是整张图都有诊断价值。脑PET图像中大部分像素是背景和颅骨区域真正能反映神经退行性变化的是特定脑区。crop.py存在的意义就是把这些ROI区域提取出来减少模型输入中的无用信息提升分类准确率。# crop.py 自动裁剪ROI示意 import numpy as np import cv2 import os from config import RAW_DATA_DIR, CROPPED_DATA_DIR, TARGET_SIZE def find_roi_bbox(img, threshold30, margin10): 自动定位ROI区域返回边界框 (y1, y2, x1, x2) # 二值化像素值大于阈值的视为前景 _, binary cv2.threshold(img, threshold, 255, cv2.THRESH_BINARY) # 找到所有前景像素的坐标 ys, xs np.where(binary 0) if len(ys) 0: # 全黑图像回退到整张图 return 0, img.shape[0], 0, img.shape[1] y1 max(0, int(ys.min()) - margin) y2 min(img.shape[0], int(ys.max()) margin) x1 max(0, int(xs.min()) - margin) x2 min(img.shape[1], int(xs.max()) margin) return y1, y2, x1, x2 def crop_pet_images(raw_dir, output_dir, target_size(128, 128)): os.makedirs(output_dir, exist_okTrue) for class_name in os.listdir(raw_dir): class_input os.path.join(raw_dir, class_name) class_output os.path.join(output_dir, class_name) os.makedirs(class_output, exist_okTrue) for file_name in os.listdir(class_input): img_path os.path.join(class_input, file_name) img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: continue y1, y2, x1, x2 find_roi_bbox(img, threshold30, margin10) roi img[y1:y2, x1:x2] roi_resized cv2.resize(roi, target_size, interpolationcv2.INTER_LINEAR) output_path os.path.join(class_output, file_name) cv2.imwrite(output_path, roi_resized) print(f已处理: {file_name} - {output_path}) crop_pet_images(RAW_DATA_DIR, CROPPED_DATA_DIR, TARGET_SIZE)逻辑说明find_roi_bbox用固定阈值做二值化找出前景像素的边界然后加一个margin防止ROI紧贴影像边缘。阈值30是个经验值PET图像中脑组织的像素值通常明显高于背景但不同设备下可能差异很大。如果某些图像整体偏暗阈值要调低反之要调高。参数说明margin10是边界扩展像素数太小会裁掉边缘的脑皮质区域太大会引入过多背景噪声。我建议先用一两张图做可视化验证保存裁剪前后的对比图看ROI是否覆盖完整脑区。还有一个容易被忽略的坑如果原始图像是3D的NIfTI格式后缀.niicv2.imread无法直接读取需要先转成PNG序列或者用nibabel库加载后逐切片提取。4.3 预测结果解读概率输出与阈值调整拿到预测概率后不是直接看哪个类别概率大就完事。医学诊断场景对误判的代价很敏感通常需要调整决策阈值。# 预测结果后处理示意 CONFIDENCE_THRESHOLD 0.6 def make_decision(proba_disease, thresholdCONFIDENCE_THRESHOLD): 带置信度门槛的决策逻辑 if proba_disease threshold: return 建议复查disease elif proba_disease 1 - threshold: return 判定为normal else: return 不确定区间建议临床进一步检查逻辑说明概率值0.5是默认决策边界但当疾病类别在训练集中占比较低时模型预测的概率分布通常不会落在0.5附近。此时把决策阈值抬高到0.6或0.7能减少假阳性。输出三个区间而不是两个结论更贴合医学诊断的实际需求——模型不确定就该让临床手段介入而不是强行给一个结论。参数说明threshold0.6是经验值。你可以用验证集画出ROC曲线后根据AUC确定最优阈值原则是让FPR尽可能低的前提下保持较好TPR。这个脚本输出的CSV中记录了每个样本的预测概率值修改阈值后不需要重新训练模型直接重新跑一次决策逻辑就行。5. 避坑指南跑通这个项目的五个常见问题5.1 依赖包版本冲突导致cv2无法导入现象执行import cv2直接报错提示ImportError: DLL load failed或numpy版本冲突。原因OpenCV新版和旧版numpy的二进制接口不兼容。新装opencv-python 4.8会自动升级numpy到2.x但项目基于numpy 1.x开发的两个版本的ndarray内存布局有差异。解决严格按requirements.txt的版本安装。先卸载现有numpy和opencvpip uninstall numpy opencv-python -y pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple装完验证python -c import cv2; print(cv2.__version__)。如果还是报错就换Python 3.8版本用conda重建环境。5.2 数据集路径包含中文导致图像读取失败现象训练脚本能跑但读取的样本数量为0loss不下降。原因OpenCV的imread底层通过C实现对中文路径支持不好遇到非ASCII字符直接返回None。解决把数据路径改成纯英文目录比如E:/pet_project/data/raw不要放在D:/医学图像/数据集下面。处理大量文件时建议用pathlib库替代字符串拼接路径它会自动处理平台差异。这个坑在医学图像数据处理中出现频率极高我接手过的十来个项目里有半数栽在这里。5.3 PET图像归一化方式不一致导致预测结果失控现象训练时验证集准确率80%以上用2_predict.py预测一张新单图时输出概率永远偏向某一个类别。原因训练脚本里用的是min-max归一化每张图独立做预测脚本里如果写的是img / 255测试样本的特征分布和训练时完全不同。这个问题隐蔽性很强代码表面上不报错但预测概率毫无意义。解决检查训练和预测的预处理代码是否严格一致。统一封装成一个函数比如preprocess(img)训练和预测共用同一个函数。这是工程规范问题不要各写一份。5.4 类别不平衡导致模型全预测为正常现象训练完成后验证集准确率95%但查看classification_report时疾病类别的召回率是0%。原因数据集中正常样本占比远高于疾病样本比如1000张正常、80张疾病。模型学到的最优策略就是全部预测为正常整体准确率高但没有任何诊断价值。解决在train_test_split中已经用了stratifyy这只保证划分比例一致不能解决训练中的不平衡。需要在模型中加类别权重from sklearn.utils.class_weight import compute_class_weight weights compute_class_weight(class_weightbalanced, classes[0, 1], yy_train) # 传入模型参数SVC(class_weightbalanced) / RandomForest(class_weightbalanced)5.5 训练过程中显存或内存溢出现象跑CNN训练时程序运行十分钟后报OutOfMemoryError或者CUDA out of memory。原因load_pet_images把所有图像一次性加载进内存如果图像数量上千且尺寸较大内存占用可能达到几十GB。模型训练时中间特征图也会大量占用显存。解决第一种方案降低TARGET_SIZE从(128, 128)降到(64, 64)内存占用直接降到四分之一。第二种方案改数据加载方式使用flow_from_directory或自定义生成器每次只加载一个batchdef data_generator(X, y, batch_size16): while True: idx np.random.choice(len(X), batch_size) yield X[idx].reshape(len(X), -1), y[idx]6. 进阶验证用交叉验证与特征可视化确认模型真的可用6.1 从单次随机划分到交叉验证之前的工作流里只做一次train_test_split验证集结果存在偶然性。我在实际项目中一定会加交叉验证来评估稳定性。from sklearn.model_selection import cross_val_score from sklearn.ensemble import RandomForestClassifier model_rf RandomForestClassifier(n_estimators200, random_state42, n_jobs-1) scores cross_val_score(model_rf, X_flat, y, cv5, scoringf1) print(f5折F1均值: {scores.mean():.4f} ± {scores.std():.4f})逻辑说明5折交叉验证把数据分成5份轮流用4份训练、1份验证最终得到5个F1分数。均值和标准差能反映模型的真实水平——如果均值比单次验证低很多说明之前的结果只是随机划分的运气好。标准差过大说明模型对数据划分敏感要考虑增加样本量或改用正则化更强的模型。6.2 混淆矩阵与ROC曲线两张图看清模型底牌准确率会骗人但混淆矩阵与ROC不会。疾病预测场景下真阳性率与假阳性率的权衡关系全在ROC曲线里。import matplotlib.pyplot as plt from sklearn.metrics import roc_curve, auc, confusion_matrix import numpy as np # 获取验证集预测概率 probas model.predict_proba(X_val_flat)[:, 1] # 正样本疾病的概率 fpr, tpr, thresholds roc_curve(y_val, probas) roc_auc auc(fpr, tpr) plt.figure(figsize(6, 5)) plt.plot(fpr, tpr, labelfROC曲线 (AUC{roc_auc:.3f})) plt.plot([0, 1], [0, 1], k--, label随机分类器) plt.xlabel(假阳性率) plt.ylabel(真阳性率) plt.legend(loclower right) plt.tight_layout() plt.savefig(roc_curve.png, dpi150) y_pred model.predict(X_val_flat) cm confusion_matrix(y_val, y_pred) print(f混淆矩阵:\n{cm})逻辑说明AUC大于0.9说明模型有较好的判别能力在0.7到0.9之间说明有一定判别能力但临床使用需谨慎低于0.7基本和随机猜测差别不大。ROC曲线还可以帮你选最优阈值——在左上角拐点附近找一个点对应的阈值就是假阳性率和真阳性率的平衡点。混淆矩阵的四象限则让你直观看到模型具体错在哪里。6.3 一个实用技巧特征可视化定位模型依据如果你用的CNN模型可以提取最后一层卷积层的特征图用平均激活图Grad-CAM的简化版看模型关注哪些脑区。如果模型决策依据落在背景或颅骨区域说明预处理没做好ROI裁剪范围偏了。# 提取卷积特征图并做平均可视化 # 以Keras为例 from tensorflow.keras.models import Model # 假设 model 是已训练好的CNN模型 # feature_model Model(inputsmodel.input, outputsmodel.get_layer(last_conv).output) # features feature_model.predict(sample_img[None, ...]) # shape: (1, h, w, channels) # heatmap np.mean(features[0], axis-1) # heatmap cv2.resize(heatmap, (128, 128))从那以后我每跑完一轮训练都会强制走一遍这个流程先看混淆矩阵再画ROC曲线最后看一眼特征可视化。三件事加起来多花十分钟却能避免被单次验证分数误导。希望这个习惯也能帮到你。本文还有配套的精品资源点击获取