
简介本资源是一套面向医学影像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数据集与预处理任何机器学习项目数据都是地基。LUNA16Lung 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(f0 {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( datapath/to/data.yaml, epochs300, # 医学图像数据量可能不大可以适当增加轮数 imgsz640, # 输入图像尺寸与预处理时保持一致 batch16, # 根据你的GPU显存调整越大训练越稳定但可能爆显存 workers8, # 数据加载的进程数提高CPU利用率加速数据读取 device0, # 使用GPU 0如果是多卡可以用‘0,1’ seed42, # 固定随机种子确保实验可复现 optimizerAdamW, # 可以尝试AdamW有时比SGD更稳定 lr00.001, # 初始学习率对于微调任务可以设小一点 lrf0.01, # 最终学习率衰减系数 (lr0 * lrf) weight_decay0.0005, # 权重衰减防止过拟合 warmup_epochs3, # 学习率热身轮数让训练初期更稳定 box7.5, # 框损失权重可以微调 cls0.5, # 分类损失权重我们单分类任务这个可以相对低一点 dfl1.5, # 分布焦点损失权重YOLOv8/v11引入的一般不动 hsv_h0.015, # 色相增强幅度医学灰度图增强幅度要小 hsv_s0.7, # 饱和度增强灰度图无效可设为0 hsv_v0.4, # 明度增强这是对灰度图有效的增强 translate0.1, # 平移增强 scale0.5, # 缩放增强 fliplr0.5, # 水平翻转对于肺结节左右翻转是有意义的 mosaic1.0, # Mosaic数据增强强烈建议开启能极大提升小目标检测能力 mixup0.0, # MixUp增强可以尝试0.1-0.2但可能不适合所有医学图像 copy_paste0.0, # 复制粘贴增强对于目标一致性的医学图像慎用 erasing0.4, # 随机擦除模拟病灶被部分遮挡可以开启 nameluna16_yolo11m_exp1 # 实验名称用于保存结果 )关键调参经验数据增强是核心肺结节是小目标且在不同病例中形态、密度差异大。mosaic增强能有效模拟多尺度、多上下文环境对提升小目标召回率至关重要。hsv_v明度和translate/scale几何变换是主要增强手段。切记将hsv_s饱和度设为0因为灰度图没有饱和度概念。损失权重box损失权重可以适当调高因为我们的核心任务是框得准。cls损失权重可以调低因为我们是单分类任务分类难度相对较低。学习率与优化器从预训练模型微调学习率lr0不宜过大1e-3或1e-4是常用起点。使用AdamW优化器搭配Cosine或Linear学习率调度通常比SGD收敛更快更稳。早停与保存YOLO内置了早停patience50和最佳模型保存机制。一定要关注验证集上的mAP50-95平均精度和Recall召回率指标。对于结节检测召回率往往比单纯的高精度更重要因为漏诊的代价很高。3.3 训练监控与模型评估训练启动后除了观察命令行输出Ultralytics还提供了优秀的可视化工具。你可以使用TensorBoard或它自带的results.csv和绘图功能来监控训练过程。# 训练完成后加载最佳模型进行评估 best_model YOLO(runs/detect/luna16_yolo11m_exp1/weights/best.pt) metrics best_model.val(datapath/to/data.yaml, splitval) 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_pathbest.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, conf0.25, iou0.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 fnodule {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格式。我们在绘制框时用的是OpenCVBGR最后显示时通过QImage.Format_BGR888正确转换。如果遇到颜色问题记得检查格式。模型推理参数conf0.25和iou0.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._MEIPASSPyInstaller运行时临时目录或基于可执行文件位置os.path.dirname(sys.executable)来构建模型路径。def load_model(self, model_pathNone): 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图像进行结节检测无需配置任何复杂环境。本文还有配套的精品资源点击获取