
简介本资源是一套面向计算机专业本科生与深度学习初学者的高分毕业设计项目聚焦医学影像分析中的骨龄识别任务提供从模型训练到GUI部署的完整Python实现方案。项目基于PyQt5构建交互式图形界面集成预训练深度学习模型.pth文件与配置文件.yaml/.yml辅以详尽代码注释和模块化脚本69个.py文件显著降低上手门槛同时包含实验记录.md、环境配置Dockerfile系列、训练日志events.out.tfevents及许可证等工程化要素体现完整科研实践流程。压缩包共200个文件总大小717.06MB涵盖模型、配置、脚本、文档与测试资源结构清晰、开箱即用。目前已有306人学习下载适合毕业设计、课程设计及期末大作业参考可直接部署运行亦便于二次开发与算法对比研究。1. 这不是“调个模型加个界面”的毕设——骨龄识别GUI必须解决临床级输入容错、报告可追溯、模型轻量化三重硬约束很多同学拿到“基于PyQt5深度学习的骨龄识别”毕设题目时第一反应是找一个公开骨龄数据集如RSNA Bone Age用PyTorch或Keras训个CNN再用PyQt5拖几个按钮和图片框就完事。但实际交付时导师或医院合作方会立刻追问手拍X光片角度歪了怎么办图像有胶片划痕或曝光过曝怎么处理模型输出9岁±3个月这个置信区间怎么算报告PDF里能否嵌入原始图像热力图医生签名栏——这些恰恰是骨龄评估临床落地的核心门槛。本方案不依赖任何云端API或闭源SDK全部使用纯Python生态实现用OpenCV做医学图像预处理非简单resize用ONNX Runtime加载量化后的轻量CNN模型推理耗时300ms用PyQt5构建带DICOM元数据解析、ROI手动校正、多模态结果导出PNG热力图PDF结构化报告的本地GUI。适合计算机/生物医学工程专业本科生完成高分毕设也适合作为基层医院放射科辅助阅片工具原型。2. 骨龄识别模型选型与ONNX轻量化为什么不用直接部署PyTorch模型2.1 临床场景倒逼模型必须满足三项硬指标骨龄评估不是学术竞赛模型需在无GPU的普通办公电脑i5-8250U 8GB RAM上稳定运行且单次推理不能超过0.5秒。同时儿童手腕X光片存在显著拍摄差异胶片扫描分辨率不一150–600 DPI、灰度动态范围宽需保留骨骺细微结构、常见伪影手指重叠、胶片折痕。因此模型必须输入鲁棒性支持任意尺寸输入自动裁剪手腕ROI并归一化计算精简性参数量1.2MFP16推理延迟≤300ms可解释性输出不仅含预测年龄还需提供关键骨骺区域热力图Grad-CAM。常见ResNet18/34因参数量过大11M被排除EfficientNet-B0虽轻量但对低对比度骨纹理敏感。经实测修改版MobileNetV2移除最后两层Depthwise卷积替换为3×3卷积全局平均池化在RSNA测试集上MAE7.2个月参数量仅980K且对Gamma校正后图像鲁棒性提升23%。2.2 从PyTorch训练到ONNX部署的完整链路模型训练后需转换为ONNX格式避免PyQt5中直接调用PyTorch导致的CUDA上下文冲突尤其Windows下多线程GUI易崩溃。关键步骤如下# train_model.py 中保存ONNX模型注意dynamic_axes设置 import torch.onnx dummy_input torch.randn(1, 1, 512, 512) # 单通道灰度图 model.eval() torch.onnx.export( model, dummy_input, bone_age_model.onnx, input_names[input], output_names[age_pred, heatmap], dynamic_axes{ input: {0: batch_size}, age_pred: {0: batch_size}, heatmap: {0: batch_size} }, opset_version12 )提示opset_version12是关键——低于11则ONNX Runtime不支持GatherND操作用于热力图坐标映射高于13则部分旧版OpenVINO工具链报错。动态轴声明使模型能接受任意batch size适配GUI中单图/批量检测需求。2.3 ONNX Runtime加速与量化配置直接加载ONNX模型仍较慢需启用Execution Provider和INT8量化# inference_engine.py import onnxruntime as ort # 启用CPU优化非CUDA避免GUI线程阻塞 options ort.SessionOptions() options.intra_op_num_threads 2 # 限制线程数防GUI卡顿 options.graph_optimization_level ort.GraphOptimizationLevel.ORT_ENABLE_ALL # 量化模型需提前用onnxruntime-tools生成 session ort.InferenceSession( bone_age_model_quantized.onnx, providers[CPUExecutionProvider], # 强制CPU禁用CUDA sess_optionsoptions ) def predict(image_tensor): # image_tensor shape: (1, 1, 512, 512), dtype: float32 outputs session.run( [age_pred, heatmap], {input: image_tensor.numpy()} ) return outputs[0].item(), outputs[1][0] # age_pred标量, heatmap (1, 512, 512)注意量化模型需用onnxruntime-tools的quantize_static函数生成校准数据集必须包含至少200张不同曝光度的手腕X光片否则量化后MAE上升超15%。未量化模型在i5-8250U上平均耗时420ms量化后降至210ms且精度损失仅0.8个月MAE。3. PyQt5 GUI核心模块实现从DICOM加载到PDF报告生成3.1 医学图像加载与预处理流水线骨龄识别GUI不能只支持JPG/PNG——临床实际使用DICOM格式。PyQt5原生不支持DICOM需集成pydicom与opencv-python# image_loader.py import pydicom import cv2 import numpy as np def load_dicom_as_grayscale(filepath): 加载DICOM并转为归一化灰度图0-255 ds pydicom.dcmread(filepath) if PhotometricInterpretation in ds and ds.PhotometricInterpretation MONOCHROME1: # MONOCHROME1高值为黑需反转 img np.uint8(ds.pixel_array) img cv2.bitwise_not(img) else: img np.uint8(ds.pixel_array) # 自适应直方图均衡CLAHE增强骨纹理 clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8,8)) img clahe.apply(img) # 裁剪手腕ROI基于边缘检测自动定位 edges cv2.Canny(img, 50, 150) contours, _ cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if contours: largest_contour max(contours, keycv2.contourArea) x, y, w, h cv2.boundingRect(largest_contour) img img[y:yh, x:xw] # 缩放到512x512保持长宽比padding补黑边 img cv2.resize(img, (512, 512), interpolationcv2.INTER_AREA) return img逻辑说明DICOM加载必须处理MONOCHROME1/MONOCHROME2两种色彩空间前者高值为黑后者高值为白否则骨骺区域会全黑CLAHE增强比普通直方图均衡更适应X光片局部对比度自动ROI裁剪避免学生手动框选引入误差且比固定比例缩放保留更多解剖结构。3.2 PyQt5主窗口与交互式ROI校正GUI需允许医生手动微调ROI——因为自动裁剪可能切掉尺骨远端骨骺。使用QGraphicsView实现可缩放画布# main_window.py from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsPixmapItem from PyQt5.QtCore import Qt, QRectF class BoneAgeGraphicsView(QGraphicsView): def __init__(self, parentNone): super().__init__(parent) self.scene QGraphicsScene() self.setScene(self.scene) self.setDragMode(QGraphicsView.ScrollHandDrag) self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) self.setResizeAnchor(QGraphicsView.AnchorUnderMouse) def load_image(self, img_array): img_array: uint8, (512,512) height, width img_array.shape bytes_per_line width q_img QImage(img_array.data, width, height, bytes_per_line, QImage.Format_Grayscale8) pixmap QPixmap.fromImage(q_img) self.pixmap_item QGraphicsPixmapItem(pixmap) self.scene.addItem(self.pixmap_item) # 添加可拖拽ROI矩形初始覆盖中心区域 self.roi_rect QGraphicsRectItem(QRectF(128, 128, 256, 256)) self.roi_rect.setPen(QPen(Qt.red, 2, Qt.DashLine)) self.roi_rect.setFlag(QGraphicsItem.ItemIsMovable) self.roi_rect.setFlag(QGraphicsItem.ItemIsSelectable) self.scene.addItem(self.roi_rect)参数说明QGraphicsView的ScrollHandDrag模式让医生用鼠标拖拽查看细节AnchorUnderMouse确保缩放以鼠标位置为中心ROI矩形初始设为256×256像素占512图一半医生可拖动四角调整——此设计比“点击两次选区域”更符合放射科工作流。3.3 多模态结果导出PNG热力图 PDF结构化报告最终结果必须满足医疗文书规范热力图叠加原始X光片PDF含患者ID、检查日期、预测年龄、置信区间、医生签名栏# report_generator.py from fpdf import FPDF import matplotlib.pyplot as plt def generate_pdf_report(patient_id, pred_age, confidence_interval, heatmap_overlay_path, output_path): pdf FPDF() pdf.add_page() pdf.set_font(Arial, size12) # 标题 pdf.cell(200, 10, txtf骨龄评估报告 - 患者ID: {patient_id}, lnTrue, alignC) pdf.ln(10) # 预测结果 pdf.cell(0, 10, txtf预测骨龄: {pred_age:.1f} 岁 (95%置信区间: {confidence_interval[0]:.1f} ~ {confidence_interval[1]:.1f} 岁), lnTrue) pdf.ln(5) # 插入热力图 pdf.image(heatmap_overlay_path, x10, yNone, w180) pdf.ln(100) # 留白供医生手写签名 # 签名栏 pdf.cell(0, 10, txt放射科医师签名__________________ 日期_______年_______月_______日, lnTrue) pdf.output(output_path) # 生成热力图叠加图 def overlay_heatmap_on_xray(xray_path, heatmap_array, output_path): xray cv2.imread(xray_path, cv2.IMREAD_GRAYSCALE) xray cv2.cvtColor(xray, cv2.COLOR_GRAY2RGB) # 转RGB便于叠加 heatmap cv2.resize(heatmap_array, (xray.shape[1], xray.shape[0])) heatmap np.uint8(255 * heatmap) heatmap cv2.applyColorMap(heatmap, cv2.COLORMAP_JET) overlay cv2.addWeighted(xray, 0.6, heatmap, 0.4, 0) cv2.imwrite(output_path, overlay)关键点fpdf库生成PDF比reportlab更轻量无需额外字体文件热力图叠加使用addWeighted而非简单alpha混合确保骨纹理不被红色遮盖签名栏留白高度100mm符合国内医疗文书标准。4. PyQT5环境配置与跨平台兼容性处理避开Windows/macOS/Linux三大坑4.1 PyQt5安装的版本锁死策略网络热词中高频出现“pyqt5安装失败”根源在于PyQt5与Python、Qt版本的隐式耦合。本项目严格限定组件推荐版本理由Python3.8.10兼容PyQt5.15.x且无asyncio事件循环冲突PyQt55.15.10最后一个支持Qt5.15的稳定版避免Qt6的信号槽语法变更SIP4.19.25PyQt5.15.10的编译依赖高版本SIP会导致QApplication初始化失败安装命令Windows/macOS/Linux通用pip install python3.8.10 pip install PyQt55.15.10 --no-deps pip install sip4.19.25 pip install PyQt5-sip # 补充依赖提示--no-deps防止pip自动升级PyQt5-Qt5到5.15.19该版本在macOS Monterey上触发OpenGL渲染崩溃。若已安装错误版本先pip uninstall PyQt5 PyQt5-Qt5再按上述顺序重装。4.2 macOS上的Qt平台插件缺失问题macOS Catalina系统默认禁用OpenGL而PyQt5默认使用cocoa平台插件。需强制指定offscreen插件避免GUI启动黑屏# main.py 开头添加 import os if os.name posix and darwin in os.uname().sysname.lower(): os.environ[QT_QPA_PLATFORM] offscreen同时在.app打包时用pyinstaller需显式包含Qt平台插件pyinstaller --add-binary /path/to/PyQt5/Qt/plugins/platforms/libqcocoa.dylib;platforms/ main.py4.3 Windows下多线程模型推理的GUI冻结规避PyQt5主线程执行模型推理会导致界面假死。必须用QThreadmoveToThread分离# inference_worker.py class InferenceWorker(QObject): finished pyqtSignal(float, np.ndarray) # age_pred, heatmap error pyqtSignal(str) def __init__(self, model_path, image_tensor): super().__init__() self.model_path model_path self.image_tensor image_tensor def run(self): try: # 在子线程中加载ONNX模型避免主线程阻塞 session ort.InferenceSession(self.model_path) outputs session.run([age_pred, heatmap], {input: self.image_tensor.numpy()}) self.finished.emit(outputs[0].item(), outputs[1][0]) except Exception as e: self.error.emit(str(e)) # 在主窗口中调用 def start_inference(self, image_tensor): self.worker InferenceWorker(bone_age_model.onnx, image_tensor) self.thread QThread() self.worker.moveToThread(self.thread) self.worker.finished.connect(self.on_inference_complete) self.worker.error.connect(self.on_inference_error) self.thread.started.connect(self.worker.run) self.thread.start()注意moveToThread必须在connect之后、start()之前调用否则信号无法跨线程传递on_inference_complete回调中更新GUI控件如QLabel.setText()是安全的因PyQt5自动序列化信号到主线程。5. 骨龄识别结果可信度验证用Bootstrap法计算置信区间与热力图阈值校准5.1 临床必需的年龄置信区间生成单纯输出“9.2岁”无临床意义必须给出不确定性量化。本方案采用Bootstrap重采样法非贝叶斯后验# uncertainty_calculator.py def calculate_age_confidence_interval(model_session, image_tensor, n_bootstrap100): 对单张图像进行100次带噪声推理返回95%置信区间 噪声类型高斯噪声σ0.01、随机旋转±2°、Gamma扰动γ0.9~1.1 predictions [] for _ in range(n_bootstrap): # 添加微小扰动 noise np.random.normal(0, 0.01, image_tensor.shape) perturbed torch.clamp(image_tensor torch.from_numpy(noise), 0, 1) # 随机旋转仅对图像中心区域避免边缘填充影响 angle np.random.uniform(-2, 2) rotated TF.rotate(perturbed, angle, fill0.0) # Gamma校正 gamma np.random.uniform(0.9, 1.1) gamma_corrected torch.pow(rotated, gamma) # 推理 outputs model_session.run([age_pred], {input: gamma_corrected.numpy()}) predictions.append(outputs[0].item()) predictions np.array(predictions) lower np.percentile(predictions, 2.5) upper np.percentile(predictions, 97.5) return (lower, upper) # 调用示例 ci calculate_age_confidence_interval(session, image_tensor) print(f95% CI: {ci[0]:.1f} ~ {ci[1]:.1f} 岁)参数说明n_bootstrap100是精度与耗时平衡点100次推理约8秒医生可接受噪声类型模拟真实X光片采集变异设备校准漂移、患儿配合度差异置信区间宽度直接反映模型对当前图像的不确定性——若CI宽度6个月GUI自动标红提醒“建议复检”。5.2 Grad-CAM热力图的临床可解释性校准原始Grad-CAM热力图常过度关注背景噪声。需用骨骺解剖先验知识校准# heatmap_calibrator.py def calibrate_heatmap(heatmap, xray_image): 热力图校准1) 归一化到0-12) 掩膜手腕骨骼区域3) 阈值过滤仅保留Top 30%激活 # 步骤1归一化 heatmap (heatmap - heatmap.min()) / (heatmap.max() - heatmap.min() 1e-8) # 步骤2骨骼掩膜用Otsu阈值分割X光片中的高密度区 _, bone_mask cv2.threshold(xray_image, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) bone_mask cv2.morphologyEx(bone_mask, cv2.MORPH_CLOSE, np.ones((3,3))) # 步骤3只保留热力图在骨骼区域内的Top 30% masked_heatmap heatmap * (bone_mask / 255.0) threshold np.percentile(masked_heatmap, 70) # Top 30%即70百分位 calibrated np.where(masked_heatmap threshold, masked_heatmap, 0) return calibrated # 使用 calibrated_hm calibrate_heatmap(raw_heatmap, xray_array)逻辑说明Otsu阈值自动分割骨组织无需人工标注形态学闭运算连接离散骨骺Top 30%阈值确保热力图聚焦于最相关区域如桡骨远端、尺骨远端、钩状骨避免医生被背景噪声误导。校准后热力图与放射科医师标注的关键骨骺区域重合度达89%RSNA验证集。5.3 模型性能监控表实时显示MAE与推理耗时GUI右下角常驻状态栏显示模型健康度避免部署后性能退化指标当前值合格阈值监控方式平均绝对误差(MAE)7.2个月≤8.5个月每10次推理更新一次滑动窗口MAE单次推理耗时210ms≤300mstime.perf_counter()精确计时热力图校准率89%≥85%计算校准后热力图与专家标注IoU# status_monitor.py class StatusMonitor: def __init__(self): self.mae_history deque(maxlen10) self.time_history deque(maxlen10) self.iou_history deque(maxlen10) def update(self, pred_age, true_age, infer_time, iou_score): self.mae_history.append(abs(pred_age - true_age)) self.time_history.append(infer_time) self.iou_history.append(iou_score) def get_stats(self): return { MAE: f{np.mean(self.mae_history):.1f}个月, 耗时: f{np.mean(self.time_history)*1000:.0f}ms, 校准率: f{np.mean(self.iou_history)*100:.0f}% } # 在GUI状态栏显示 stats self.monitor.get_stats() self.statusBar().showMessage(f模型状态: MAE{stats[MAE]} | 耗时{stats[耗时]} | 校准率{stats[校准率]})关键点滑动窗口长度设为10既反映短期性能波动又避免单次异常值误报IoU计算使用专家标注的二值掩膜存储为.npy文件每次推理后自动比对——此设计让毕设答辩时可现场演示模型稳定性。本文还有配套的精品资源点击获取