简介:本资源是一套面向医学影像AI初学者与课程设计者的肺结节检测实践系统,基于YOLOv11在LUNA16数据集上完成端到端开发,解决CT图像中肺结节自动定位与可视化诊断的典型任务,适用于高校人工智能、生物医学工程类大作业及毕业设计场景。压缩包含2000个文件(599.33MB),涵盖190个Python训练/推理脚本、138张结构图与界面截图(png)、88个模型配置yaml、355份说明文档(md)及3个可直接套用的PPT模板,支撑从环境搭建、多模型对比(YOLOv5/v8/v11-nano/small)、图形化/WEB界面调用到报告撰写的全流程。已有1429人学习下载,提供完整可运行代码、预训练模型、cache缓存文件及C++/Python混合推理模块(inference.h/cpp),并配套B站实操视频链接,显著降低部署门槛与调试成本。
1. 项目缘起:为什么用YOLO11做肺结节检测?
最近在整理一些医学影像相关的项目,发现很多朋友对肺部CT影像的自动分析很感兴趣,尤其是肺结节的检测。这确实是个刚需,无论是辅助医生筛查,还是用于教学研究,一个能跑起来的、带界面的检测系统都很有价值。传统的检测方法要么流程复杂,要么对硬件要求高,部署起来很麻烦。而目标检测领域的YOLO系列模型,以其“快、准、狠”的特点,在工业界已经遍地开花,那能不能把它搬到医学影像这个相对专业的领域呢?
答案是肯定的,而且已经有不少研究在做了。我这次选择基于最新的YOLO11来构建一个肺结节检测系统,数据集则使用了业内公认的基准——LUNA16。这个组合的优势很明显:YOLO11提供了当下最前沿、优化最好的检测框架之一,而LUNA16则保证了我们是在一个公开、标准的数据集上工作,结果有可比性,也方便其他人复现。更重要的是,我想做的不是一个“黑盒子”模型,而是一个完整的解决方案:从数据集准备、模型训练,到最终封装成一个带有图形化界面的可执行程序,让即使不懂深度学习的医生或研究人员,也能拖入一张CT图像,直观地看到检测结果。
这背后有几个实际的考虑。第一,效率。医生看一个病人的CT影像,往往有几百张切片,肉眼逐张筛查耗时耗力。一个训练良好的模型可以在秒级时间内完成初筛,标出可疑结节,大大提升工作效率。第二,一致性。人工判读难免会受到经验、疲劳度的影响,而算法模型一旦训练完成,其判断标准是稳定一致的,有助于减少漏诊。第三,可及性。一个封装好的、带GUI的工具,可以降低技术门槛,让更多基层医院或研究机构能够应用这项技术。
所以,这个项目的目标很明确:利用YOLO11的强大检测能力,在LUNA16数据集上训练一个高精度的肺结节检测模型,并最终打包成一个用户友好的桌面应用程序。接下来,我会拆解整个过程,包括数据怎么处理、模型怎么训、界面怎么搭,以及我趟过的那些坑。
2. 基石:深入理解LUNA16数据集与预处理
任何机器学习项目,数据都是地基。LUNA16(Lung Nodule Analysis 16)是肺结节检测领域最权威的公开数据集之一,它源于更大的LIDC-IDRI数据集,并进行了严格的筛选和标注。在动手写代码之前,我们必须吃透这个数据集的结构和特点。
2.1 LUNA16数据集解构:不止是图片和标签
LUNA16提供的不是一张张普通的JPG或PNG图片,而是医学数字成像和通信(DICOM)格式的CT扫描序列,或者更常见的,它已经转换成了MetaImage(.mhd/.raw)格式。每个病例对应一个.mhd文件(头文件,包含图像尺寸、间距、原点等信息)和一个.raw文件(原始的图像体数据)。这种三维体数据(3D Volume)才是医学影像的常态,我们的肺结节也存在于这个三维空间中。
数据集的标注信息保存在annotations.csv文件里。关键字段包括:
seriesuid: 病例的唯一标识符,对应一个.mhd文件。coordX, coordY, coordZ: 结节中心点在三维世界坐标(毫米单位)下的位置。diameter_mm: 结节直径(毫米)。
这里第一个关键点就来了:标注给的是三维世界坐标和物理尺寸,而我们的模型输入是二维切片图像和像素级的边界框。这是一个核心的坐标转换问题。我们需要利用.mhd头文件中的信息(特别是ElementSpacing像素间距和Offset原点偏移),将世界坐标(coordX, coordY, coordZ)转换到体数据的三维索引坐标(indexZ, indexY, indexX),再根据CT扫描的轴向(通常是横断面)提取出对应的二维切片。
我写了一个关键的转换函数,你可以直接拿去用:
import numpy as np import SimpleITK as sitk # 处理.mhd文件的利器 def world_to_voxel_coord(world_coord, origin, spacing): """ 将世界坐标(毫米)转换为体数据内的体素索引坐标。 参数: world_coord: 世界坐标,形如 (x, y, z) origin: .mhd文件中的Offset,即 (origin_x, origin_y, origin_z) spacing: .mhd文件中的ElementSpacing,即 (spacing_x, spacing_y, spacing_z) 返回: voxel_coord: 体素索引坐标,形如 (z, y, x) 【注意顺序!】 """ # 首先进行平移,减去原点 translated = np.array(world_coord) - np.array(origin) # 然后除以间距,得到索引(四舍五入取整) voxel = np.round(translated / np.array(spacing)).astype(int) # SimpleITK和Numpy的数组索引顺序是 (z, y, x),对应 (depth, height, width) # 而世界坐标通常是 (x, y, z),所以需要调整顺序 return voxel[::-1] # 返回 (z, y, x) # 示例用法 mhd_path = 'subset0/1.3.6.1.4.1.14519.5.2.1.6279.6001.100225287222365663678666836860.mhd' image = sitk.ReadImage(mhd_path) origin = image.GetOrigin() # (x, y, z) 原点 spacing = image.GetSpacing() # (x, y, z) 间距 world_coord = (10.0, 20.0, 30.0) # 假设一个标注坐标 voxel_coord = world_to_voxel_coord(world_coord, origin, spacing) print(f"世界坐标 {world_coord} 对应的体素索引是 {voxel_coord}")注意:坐标轴的顺序是最大的坑之一。DICOM/MetaImage的世界坐标系通常是
(x, y, z),而NumPy数组的索引顺序是(z, y, x)。在提取切片时,image_array[voxel_coord[0], :, :]提取的就是coordZ对应的那个横断面切片。
2.2 从3D到2D:切片提取与结节标注生成
拿到体素坐标后,我们就可以提取二维切片了。但并不是所有切片都有结节,为了构建一个均衡的数据集,我们通常采取“正样本切片+负样本切片”的策略。
- 提取正样本切片:遍历一个病例的所有标注,对于每个结节的
coordZ(转换后的voxel_coord[0]),提取该索引对应的横断面切片。这个切片上可能包含多个结节。 - 生成YOLO格式标签:YOLO需要的标签格式是归一化的
(class_id, x_center, y_center, width, height)。我们需要为当前切片上的每个结节计算这个值。class_id: 肺结节我们可以设为0。x_center, y_center: 结节中心在该切片上的(indexX, indexY)坐标,需要除以图像的宽度和高度进行归一化。width, height: 结节的宽度和高度(像素)。这里有个近似处理:标注给的diameter_mm是三维直径。我们通常假设结节在横断面近似为圆形,用直径除以spacing_x和spacing_y得到像素宽度和高度。更严谨的做法是参考原始LIDC-IDRI中的二维标注,但LUNA16未提供,所以这个近似是通用做法。
def create_yolo_label_for_slice(slice_index, nodules_in_slice, slice_shape): """ 为指定切片生成YOLO格式的标签字符串。 参数: slice_index: 当前切片在体数据中的Z轴索引。 nodules_in_slice: 一个列表,每个元素是结节在该切片上的(x_center_pixel, y_center_pixel, width_pixel, height_pixel)。 slice_shape: 切片的形状 (height, width)。 返回: label_lines: 列表,每个元素是一行YOLO标签字符串。 """ label_lines = [] img_h, img_w = slice_shape for nodule in nodules_in_slice: x_c, y_c, w, h = nodule # 归一化 x_c_norm = x_c / img_w y_c_norm = y_c / img_h w_norm = w / img_w h_norm = h / img_h # YOLO格式: class_id x_center y_center width height label_lines.append(f"0 {x_c_norm:.6f} {y_c_norm:.6f} {w_norm:.6f} {h_norm:.6f}") return label_lines # 假设我们提取了第100张切片,上面有两个结节 slice_idx = 100 slice_shape = (512, 512) # 典型CT切片尺寸 # nodules_in_slice 中的坐标是像素坐标,需要提前计算好 nodules = [ (256, 300, 40, 40), # (x_center, y_center, width, height) (400, 150, 30, 30) ] labels = create_yolo_label_for_slice(slice_idx, nodules, slice_shape) # labels: ['0 0.500000 0.585938 0.078125 0.078125', '0 0.781250 0.292969 0.058594 0.058594']采集负样本切片:随机选取不包含任何结节的切片。正负样本的比例需要控制,比如1:1或1:2,防止模型过于偏向背景。
图像预处理:CT图像的像素值是亨氏单位(HU),反映了组织的密度。我们需要进行窗宽窗位调整,将感兴趣的软组织(如肺、结节)范围映射到0-255的灰度图。通常使用肺窗(窗宽1500HU,窗位-600HU)或纵隔窗。同时,需要将图像缩放到统一的输入尺寸(如640x640),并做归一化。
def apply_window(image_array, window_center, window_width): """应用CT窗宽窗位""" img_min = window_center - window_width // 2 img_max = window_center + window_width // 2 windowed = np.clip(image_array, img_min, img_max) windowed = (windowed - img_min) / (img_max - img_min) * 255.0 return windowed.astype(np.uint8) # 使用肺窗 lung_window_center = -600 lung_window_width = 1500 processed_slice = apply_window(raw_slice_array, lung_window_center, lung_window_width)这个过程需要为LUNA16的10个子集(subset0-subset9)批量进行,最终得到一套标准的图像(.jpg/.png)和对应的标签(.txt)文件,用于YOLO训练。
3. 核心引擎:YOLO11模型训练策略与调优
数据准备好了,接下来就是重头戏:模型训练。YOLO11作为YOLO家族的新成员,在精度和速度上肯定有它的优势。我们的任务就是把它“调教”成一位优秀的“肺结节侦察兵”。
3.1 YOLO11环境搭建与数据配置
首先,你需要从Ultralytics的官方GitHub仓库克隆YOLO11的代码。我强烈建议创建一个独立的Python虚拟环境(用conda或venv),然后根据requirements.txt安装依赖。这一步可能会遇到PyTorch版本、CUDA版本匹配的问题,务必保持一致性。
数据配置遵循YOLO的标准格式。你需要创建一个data.yaml文件,这是告诉YOLO去哪里找数据的关键。
# path: 数据集的根目录路径 # train: 训练集图片路径列表文件(或目录) # val: 验证集图片路径列表文件(或目录) # nc: 类别数量 # names: 类别名称列表 path: /path/to/your/luna16_yolo_dataset/ train: images/train/ val: images/val/ # test: images/test/ # 如果有测试集也可以加上 nc: 1 # 我们只有‘肺结节’一个类别 names: ['nodule'] # 类别名目录结构应该像这样:
/path/to/your/luna16_yolo_dataset/ ├── images/ │ ├── train/ │ │ ├── case1_slice100.jpg │ │ └── ... │ └── val/ │ ├── case2_slice50.jpg │ └── ... └── labels/ ├── train/ │ ├── case1_slice100.txt │ └── ... └── val/ ├── case2_slice50.txt └── ...3.2 训练参数详解与肺结节场景适配
直接使用默认参数训练往往得不到最佳效果。针对肺结节检测这个特定任务,我们需要调整一些关键参数。
from ultralytics import YOLO # 加载一个预训练模型,推荐使用YOLO11n/s/m/l/x中的‘s’或‘m’,在精度和速度间取得平衡 model = YOLO('yolo11m.pt') # 使用中等尺寸的预训练权重 # 开始训练 results = model.train( data='path/to/data.yaml', epochs=300, # 医学图像数据量可能不大,可以适当增加轮数 imgsz=640, # 输入图像尺寸,与预处理时保持一致 batch=16, # 根据你的GPU显存调整,越大训练越稳定,但可能爆显存 workers=8, # 数据加载的进程数,提高CPU利用率,加速数据读取 device='0', # 使用GPU 0,如果是多卡可以用‘0,1’ seed=42, # 固定随机种子,确保实验可复现 optimizer='AdamW', # 可以尝试AdamW,有时比SGD更稳定 lr0=0.001, # 初始学习率,对于微调任务可以设小一点 lrf=0.01, # 最终学习率衰减系数 (lr0 * lrf) weight_decay=0.0005, # 权重衰减,防止过拟合 warmup_epochs=3, # 学习率热身轮数,让训练初期更稳定 box=7.5, # 框损失权重,可以微调 cls=0.5, # 分类损失权重,我们单分类任务,这个可以相对低一点 dfl=1.5, # 分布焦点损失权重,YOLOv8/v11引入的,一般不动 hsv_h=0.015, # 色相增强幅度,医学灰度图增强幅度要小! hsv_s=0.7, # 饱和度增强,灰度图无效,可设为0 hsv_v=0.4, # 明度增强,这是对灰度图有效的增强! translate=0.1, # 平移增强 scale=0.5, # 缩放增强 fliplr=0.5, # 水平翻转,对于肺结节,左右翻转是有意义的 mosaic=1.0, # Mosaic数据增强,强烈建议开启,能极大提升小目标检测能力 mixup=0.0, # MixUp增强,可以尝试0.1-0.2,但可能不适合所有医学图像 copy_paste=0.0, # 复制粘贴增强,对于目标一致性的医学图像慎用 erasing=0.4, # 随机擦除,模拟病灶被部分遮挡,可以开启 name='luna16_yolo11m_exp1' # 实验名称,用于保存结果 )关键调参经验:
- 数据增强是核心:肺结节是小目标,且在不同病例中形态、密度差异大。
mosaic增强能有效模拟多尺度、多上下文环境,对提升小目标召回率至关重要。hsv_v(明度)和translate/scale(几何变换)是主要增强手段。切记将hsv_s(饱和度)设为0,因为灰度图没有饱和度概念。 - 损失权重:
box损失权重可以适当调高,因为我们的核心任务是框得准。cls损失权重可以调低,因为我们是单分类任务,分类难度相对较低。 - 学习率与优化器:从预训练模型微调,学习率
lr0不宜过大,1e-3或1e-4是常用起点。使用AdamW优化器搭配Cosine或Linear学习率调度,通常比SGD收敛更快更稳。 - 早停与保存:YOLO内置了早停(
patience=50)和最佳模型保存机制。一定要关注验证集上的mAP50-95(平均精度)和Recall(召回率)指标。对于结节检测,召回率往往比单纯的高精度更重要,因为漏诊的代价很高。
3.3 训练监控与模型评估
训练启动后,除了观察命令行输出,Ultralytics还提供了优秀的可视化工具。你可以使用TensorBoard或它自带的results.csv和绘图功能来监控训练过程。
# 训练完成后,加载最佳模型进行评估 best_model = YOLO('runs/detect/luna16_yolo11m_exp1/weights/best.pt') metrics = best_model.val(data='path/to/data.yaml', split='val') print(metrics.box.map) # mAP50-95 print(metrics.box.map50) # mAP50 print(metrics.box.mr) # 召回率 Recall评估时,要特别关注小目标(Small Object)的检测性能。YOLO的评估报告里会给出。如果小目标AP值很低,可能需要回头检查数据预处理(切片是否丢失了小结节?)、增强策略(mosaic是否开启?)、或者尝试更小的模型输入尺寸(如从640降到512)来相对增大目标像素面积。
一个常见的坑是假阳性过多。这可能是负样本不足,或者数据增强过于激进,导致模型将一些血管断面、组织交叉点误认为结节。解决方法是:1) 增加高质量的负样本切片;2) 在验证集上调整置信度阈值(conf参数);3) 引入更复杂的后处理,如非极大值抑制(NMS)的参数调优。
4. 从模型到应用:构建图形化界面(GUI)
模型训练好了,精度也不错,但一个.pt文件对最终用户来说毫无用处。我们的目标是打造一个“开箱即用”的工具,这就需要图形化界面。这里我选择用PyQt5来搭建,因为它跨平台、功能强大、界面美观。
4.1 使用PyQt5设计主界面
设计思路要清晰:一个主窗口,包含菜单栏、工具栏、图像显示区域、结果列表和日志区域。
import sys from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QWidget, QFileDialog, QListWidget, QTextEdit, QAction, QToolBar) from PyQt5.QtGui import QPixmap, QImage from PyQt5.QtCore import Qt import cv2 from ultralytics import YOLO class NoduleDetectionApp(QMainWindow): def __init__(self): super().__init__() self.model = None self.current_image_path = None self.initUI() self.load_model() # 初始化时加载模型 def initUI(self): self.setWindowTitle('基于YOLO11的肺结节检测系统') self.setGeometry(100, 100, 1200, 800) # 中心部件 central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QHBoxLayout(central_widget) # 左侧:图像显示区域 left_panel = QVBoxLayout() self.image_label = QLabel('请加载CT图像') self.image_label.setAlignment(Qt.AlignCenter) self.image_label.setStyleSheet("border: 2px solid gray;") self.image_label.setMinimumSize(600, 600) left_panel.addWidget(self.image_label) # 图像控制按钮 btn_layout = QHBoxLayout() self.load_btn = QPushButton('加载图像 (DICOM/JPG/PNG)') self.detect_btn = QPushButton('执行检测') self.clear_btn = QPushButton('清除结果') self.load_btn.clicked.connect(self.load_image) self.detect_btn.clicked.connect(self.detect_nodules) self.clear_btn.clicked.connect(self.clear_display) btn_layout.addWidget(self.load_btn) btn_layout.addWidget(self.detect_btn) btn_layout.addWidget(self.clear_btn) left_panel.addLayout(btn_layout) # 右侧:结果面板 right_panel = QVBoxLayout() # 检测结果列表 result_label = QLabel('检测到的结节:') self.result_list = QListWidget() self.result_list.setMaximumHeight(200) right_panel.addWidget(result_label) right_panel.addWidget(self.result_list) # 结节详情/日志 log_label = QLabel('处理日志:') self.log_text = QTextEdit() self.log_text.setReadOnly(True) right_panel.addWidget(log_label) right_panel.addWidget(self.log_text) main_layout.addLayout(left_panel, 70) # 左侧占70%宽度 main_layout.addLayout(right_panel, 30) # 右侧占30%宽度 # 创建菜单栏和工具栏 self.create_menu_bar() self.create_tool_bar() def create_menu_bar(self): menubar = self.menuBar() file_menu = menubar.addMenu('文件') open_action = QAction('打开', self) open_action.triggered.connect(self.load_image) file_menu.addAction(open_action) exit_action = QAction('退出', self) exit_action.triggered.connect(self.close) file_menu.addAction(exit_action) model_menu = menubar.addMenu('模型') load_model_action = QAction('加载模型...', self) load_model_action.triggered.connect(self.load_model_dialog) model_menu.addAction(load_model_action) def create_tool_bar(self): toolbar = QToolBar() self.addToolBar(toolbar) toolbar.addAction('打开', self.load_image) toolbar.addAction('检测', self.detect_nodules) toolbar.addSeparator() toolbar.addAction('清除', self.clear_display) def load_model(self, model_path='best.pt'): """加载训练好的YOLO模型""" try: self.log_text.append(f'正在加载模型: {model_path}') self.model = YOLO(model_path) self.log_text.append('模型加载成功!') self.detect_btn.setEnabled(True) except Exception as e: self.log_text.append(f'模型加载失败: {e}') self.detect_btn.setEnabled(False) def load_model_dialog(self): """通过对话框选择模型文件""" path, _ = QFileDialog.getOpenFileName(self, '选择模型文件', '', 'PyTorch Model (*.pt)') if path: self.load_model(path)这个框架搭建了基本的界面布局和功能连接。load_model方法在程序启动时或通过菜单加载我们训练好的best.pt权重文件。
4.2 集成检测功能与结果可视化
核心功能在detect_nodules方法里。我们需要处理用户加载的图像(可能是DICOM、JPG、PNG),用模型进行推理,并将检测结果(带框的图片和结节信息)展示出来。
def load_image(self): """加载图像文件并显示""" path, _ = QFileDialog.getOpenFileName(self, '打开图像文件', '', 'Images (*.png *.jpg *.jpeg *.bmp *.dcm);;All Files (*)') if not path: return self.current_image_path = path self.log_text.append(f'已加载图像: {path}') # 处理DICOM文件 if path.lower().endswith('.dcm'): try: import pydicom ds = pydicom.dcmread(path) # 简单处理,提取像素数组并应用窗宽窗位 image_array = ds.pixel_array.astype(float) # 这里需要根据DICOM标签获取正确的窗宽窗位,或使用默认肺窗 image_array = (np.clip(image_array, -1000, 400) + 1000) / 1400 * 255 image_array = image_array.astype(np.uint8) # 如果是16位灰度,可能需要转换到8位 if image_array.max() > 255: image_array = (image_array / image_array.max() * 255).astype(np.uint8) height, width = image_array.shape bytes_per_line = width q_img = QImage(image_array.data, width, height, bytes_per_line, QImage.Format_Grayscale8) except Exception as e: self.log_text.append(f'读取DICOM文件失败: {e}') return else: # 处理普通图像文件 pixmap = QPixmap(path) if pixmap.isNull(): self.log_text.append('无法加载图像文件!') return # 调整显示大小 scaled_pixmap = pixmap.scaled(self.image_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation) self.image_label.setPixmap(scaled_pixmap) self.image_data = cv2.imread(path) # 保存一份OpenCV格式用于检测 return # 显示处理后的DICOM图像 pixmap = QPixmap.fromImage(q_img) scaled_pixmap = pixmap.scaled(self.image_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation) self.image_label.setPixmap(scaled_pixmap) # 将处理后的数组保存为self.image_data供检测用 self.image_data = cv2.cvtColor(image_array, cv2.COLOR_GRAY2BGR) if len(image_array.shape) == 2 else image_array def detect_nodules(self): """执行肺结节检测""" if self.model is None: self.log_text.append('错误:请先加载模型!') return if not hasattr(self, 'image_data') or self.image_data is None: self.log_text.append('错误:请先加载图像!') return self.log_text.append('开始检测肺结节...') # 使用YOLO模型进行预测 results = self.model(self.image_data, conf=0.25, iou=0.45) # 可调整置信度和IoU阈值 # 清空结果列表 self.result_list.clear() detection_count = 0 # 遍历检测结果 for result in results: boxes = result.boxes if boxes is not None: for box in boxes: detection_count += 1 # 获取坐标、置信度、类别 x1, y1, x2, y2 = box.xyxy[0].cpu().numpy().astype(int) conf = box.conf[0].cpu().numpy() cls_id = int(box.cls[0].cpu().numpy()) # 在结果列表中显示 item_text = f"结节{detection_count}: 位置({x1},{y1})-({x2},{y2}), 置信度{conf:.3f}" self.result_list.addItem(item_text) # 在图像上绘制矩形框和标签 label = f"nodule {conf:.2f}" cv2.rectangle(self.image_data, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(self.image_data, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) self.log_text.append(f'检测完成,共发现 {detection_count} 个疑似结节。') # 更新显示带检测框的图像 height, width, channel = self.image_data.shape bytes_per_line = 3 * width q_img = QImage(self.image_data.data, width, height, bytes_per_line, QImage.Format_BGR888) pixmap = QPixmap.fromImage(q_img) scaled_pixmap = pixmap.scaled(self.image_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation) self.image_label.setPixmap(scaled_pixmap) def clear_display(self): """清除当前图像和结果""" self.image_label.setText('请加载CT图像') self.image_label.setPixmap(QPixmap()) self.result_list.clear() self.log_text.append('已清除显示。') self.current_image_path = None if hasattr(self, 'image_data'): del self.image_data这段代码实现了核心的检测流程。有几个细节需要注意:
- DICOM支持:通过
pydicom库读取.dcm文件,并进行了简单的窗宽窗位调整和灰度映射。在实际产品中,这部分需要更健壮,比如读取DICOM标签中的WindowCenter和WindowWidth。 - 图像格式转换:OpenCV使用BGR格式,而PyQt的QImage使用RGB格式。我们在绘制框时用的是OpenCV(BGR),最后显示时通过
QImage.Format_BGR888正确转换。如果遇到颜色问题,记得检查格式。 - 模型推理参数:
conf=0.25和iou=0.45是常用起始值。对于严谨的医学检测,建议通过验证集调整这两个阈值,在召回率和精确率之间找到业务平衡点。你可以考虑在GUI中添加滑块让用户微调。 - 性能:如果检测单张高分辨率CT切片速度慢,可以考虑在推理前将图像缩放到模型训练尺寸(如640x640),检测完成后再将坐标映射回原图进行绘制。
4.3 打包与部署:生成可执行文件
最后一步,我们要把这个PyQt应用打包成独立的可执行文件(.exe),方便在没有Python环境的电脑上运行。PyInstaller是最佳选择。
首先,确保你的代码入口点正确。通常我们创建一个main.py:
# main.py import sys from app import NoduleDetectionApp # 假设上面的GUI类保存在app.py中 from PyQt5.QtWidgets import QApplication if __name__ == '__main__': app = QApplication(sys.argv) ex = NoduleDetectionApp() ex.show() sys.exit(app.exec_())然后,使用PyInstaller打包。由于项目依赖了Ultralytics YOLO(它又依赖PyTorch),打包体积会比较大,这是正常的。
# 基本打包命令 pyinstaller --onefile --windowed --name "LungNoduleDetector" main.py # 更推荐的命令,添加必要的隐藏导入和排除不必要的模块以减小体积和避免错误 pyinstaller --onefile --windowed ^ --name "LungNoduleDetector" ^ --hidden-import ultralytics ^ --hidden-import torch ^ --hidden-import torchvision ^ --hidden-import PIL ^ --hidden-import cv2 ^ --collect-all ultralytics ^ --add-data "best.pt;." ^ # 将模型文件打包进去 main.py打包踩坑实录:
- 模型文件路径:打包后,
best.pt模型文件需要能被程序找到。上面的--add-data参数将其复制到打包后的程序根目录。在代码中,你需要用sys._MEIPASS(PyInstaller运行时临时目录)或基于可执行文件位置(os.path.dirname(sys.executable))来构建模型路径。def load_model(self, model_path=None): if model_path is None: # 打包后,模型可能在可执行文件同目录 if getattr(sys, 'frozen', False): base_path = sys._MEIPASS else: base_path = os.path.dirname(__file__) model_path = os.path.join(base_path, 'best.pt') # ... 其余加载代码 - 动态库缺失:PyTorch和OpenCV依赖很多动态库(.dll, .so)。PyInstaller有时不能自动抓取全。如果运行时报错找不到某个库,需要在
.spec文件中手动添加。 - 体积巨大:一个包含PyTorch、OpenCV、PyQt5的程序很容易超过500MB。可以使用
pip install torch --index-url https://download.pytorch.org/whl/cu118安装CUDA 11.8版本的PyTorch(如果用户有NVIDIA GPU),或者安装CPU版本以减小体积。也可以尝试使用pip install ultralytics的轻量版依赖(如果可用)。
完成打包后,你会得到一个单独的.exe文件(Windows)或可执行程序(macOS/Linux)。将这个文件和README使用说明一起分发,一个完整的“基于YOLO11的肺结节检测系统”就诞生了。用户只需双击,就能加载CT图像进行结节检测,无需配置任何复杂环境。
本文还有配套的精品资源,点击获取