YOLO+SAM结肠镜息肉分割实战:从检测框到精细掩码的完整流水线

发布时间:2026/10/11 18:24:46
YOLO+SAM结肠镜息肉分割实战:从检测框到精细掩码的完整流水线
简介这份资源面向医疗影像分析与计算机视觉方向的开发者、研究生及临床科研人员提供一套将YOLO目标检测与SAM分割模型结合的结肠镜息肉图像分割实战方案用于解决息肉自动定位与精细分割问题适合具备一定深度学习基础、希望切入医疗AI场景的中高级学习者。压缩包共48个文件约234.41MB包含26个Python脚本、11个YAML配置、6个pt权重文件以及png示意图、sh下载脚本、cu加速代码和md说明文档覆盖模型构建、图像预测、数据集配置与测试评估等模块。目前已有147人学习。读者可获得从数据准备、模型训练到测试评估的完整源码与流程教程直接复用YOLO检测权重与SAM分割配置快速复现并改进算法同时借助多数据集YAML配置适配不同临床需求为医疗检测研究提供可落地的工程参考。1. 结肠镜下的息肉分割YOLO 加 SAM 到底在解决什么问题结肠镜检查是结直肠息肉筛查的主要手段但息肉检出率高度依赖操作医生的经验。一个容易被忽略的事实是息肉在视野里往往只占很小一块区域边界还经常和肠壁褶皱、反光、残留粪水混在一起。传统做法是先检测再分割检测框给个大概位置分割靠 U-Net 这类模型逐像素预测但小息肉、扁平息肉的分割边界经常糊成一团。这个项目标题讲的是用 YOLO 做息肉定位、再用 SAMSegment Anything Model做精细分割的组合方案。YOLO 负责哪里有息肉SAM 负责息肉的精确轮廓在哪。这套组合的价值在于YOLO 推理快、对小目标敏感SAM 的零样本分割能力对边界处理更细腻两者互补。适合谁做医学图像分割的算法工程师、想复现医学 AI 项目的学生、以及需要给内镜科室做辅助诊断原型的开发者。读完你应该能自己搭出这条流水线知道每个环节的参数怎么调、哪里容易翻车。2. YOLO 与 SAM 的分工为什么不是二选一2.1 两个模型的定位差异YOLO 是单阶段目标检测器输出的是边界框加类别置信度。它的强项是速度和召回率在结肠镜视频流里能做到实时逐帧检测。但 YOLO 的框是矩形息肉是不规则形状框里必然包含大量背景像素。如果直接把框当分割结果用IoU 会低得没法看。SAM 是 Meta 提出的分割基础模型核心能力是给定提示点、框、掩码输出对应的分割掩码。它的训练数据规模极大对未知类别的边界泛化能力很强。但 SAM 本身不知道什么是息肉它只会分割你提示的区域。所以逻辑很清晰YOLO 提供框作为 SAM 的提示SAM 在框内做精细分割。这个组合在医学图像里特别合适因为医学数据标注成本极高SAM 的零样本能力可以减少对大量像素级标注的依赖。你只需要标注检测框分割部分交给 SAM 自动完成人工只需要做少量修正。2.2 整体流水线的数据流整条流水线分三步输入结肠镜图像YOLO 推理得到息肉检测框和置信度将检测框作为 prompt 传给 SAM 的 prompt encoderSAM 输出分割掩码后处理得到最终息肉区域这里有个关键设计点SAM 的输入提示可以是框、点或掩码。用框作为提示时SAM 会把框内区域当作要分割的目标自动判断边界。实测下来框提示对息肉这种边界模糊的目标效果比单点提示稳定得多因为单点提示容易把周围褶皱也分进去。2.3 环境搭建与依赖安装先搭环境。我一般用 Python 3.9 以上PyTorch 2.0 以上CUDA 11.8 或 12.1。SAM 的官方实现依赖 segment-anything 包YOLO 用 ultralytics 包最省事。# 创建虚拟环境 conda create -n polyp_seg python3.10 -y conda activate polyp_seg # 安装 PyTorch根据你的 CUDA 版本调整 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装 YOLO 和 SAM pip install ultralytics pip install segment-anything pip install opencv-python matplotlib逻辑说明ultralytics 包自带 YOLOv8/v11 的推理和训练接口segment-anything 是 SAM 的官方推理库。opencv 用于图像读写和可视化。参数方面CUDA 版本要和你的显卡驱动匹配用nvidia-smi确认驱动支持的 CUDA 上限。2.4 下载模型权重YOLO 权重可以用官方预训练的也可以自己微调。SAM 权重有三个版本ViT-B、ViT-L、ViT-H参数量依次增大。医学图像分割建议用 ViT-H边界细节明显更好但显存占用也高至少需要 8GB 显存。# 下载 SAM ViT-H 权重约 2.4GB # 官方地址https://dl.fbaipublicfiles.com/segment_anything/sam_vit_h_4b8939.pth # 下载后放到项目目录的 weights/ 文件夹下 import os os.makedirs(weights, exist_okTrue) # 用 wget 或浏览器下载后放入 weights/逻辑说明SAM 权重文件较大建议提前下载好。YOLO 权重 ultralytics 会自动下载也可以手动指定路径。参数上ViT-B 约 375MBViT-L 约 1.2GBViT-H 约 2.4GB按显存选。3. 从检测框到分割掩码YOLOSAM 串联实现3.1 YOLO 检测模块的加载与推理先写 YOLO 推理部分。假设你已经有一个在息肉数据集上微调过的 YOLO 权重或者先用 COCO 预训练权重跑通流程。from ultralytics import YOLO import cv2 import numpy as np # 加载 YOLO 模型 yolo_model YOLO(weights/yolov8_polyp.pt) # 替换为你的权重路径 def detect_polyps(image_path, conf_threshold0.25, iou_threshold0.45): YOLO 检测息肉返回边界框列表 conf_threshold: 置信度阈值低于此值的框丢弃 iou_threshold: NMS 的 IoU 阈值控制重叠框合并 img cv2.imread(image_path) img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) results yolo_model.predict( sourceimg_rgb, confconf_threshold, iouiou_threshold, verboseFalse ) boxes [] for r in results: for box in r.boxes: x1, y1, x2, y2 box.xyxy[0].cpu().numpy() conf box.conf[0].cpu().numpy() boxes.append({ bbox: [int(x1), int(y1), int(x2), int(y2)], confidence: float(conf) }) return img_rgb, boxes逻辑说明conf_threshold控制检测灵敏度医学图像建议设低一点0.2-0.3宁可多检也不要漏检后续可以用 SAM 的分割质量过滤。iou_threshold控制重叠框合并息肉通常不会密集重叠0.45 是通用值。返回的 boxes 列表包含每个检测框的坐标和置信度。3.2 SAM 分割模块的加载与提示编码SAM 的推理接口需要先做图像编码再做提示编码和掩码解码。图像编码只做一次提示可以多次输入。from segment_anything import sam_model_registry, SamPredictor # 加载 SAM 模型 sam_checkpoint weights/sam_vit_h_4b8939.pth sam sam_model_registry[vit_h](checkpointsam_checkpoint) sam.to(devicecuda) predictor SamPredictor(sam) def segment_with_boxes(image_rgb, boxes): 用 YOLO 检测框作为 SAM 提示返回分割掩码 image_rgb: RGB 图像 boxes: YOLO 检测框列表 # 图像编码只需做一次 predictor.set_image(image_rgb) masks [] for box_info in boxes: bbox np.array(box_info[bbox]) # 用框作为提示 mask, score, logit predictor.predict( boxbbox, multimask_outputFalse # 单掩码输出医学图像不需要多候选 ) masks.append({ mask: mask[0], # shape: (H, W) score: float(score[0]), bbox: box_info[bbox], det_conf: box_info[confidence] }) return masks逻辑说明set_image做一次图像编码耗时约 0.5-1 秒ViT-H。predict的box参数接受 xyxy 格式的框。multimask_outputFalse表示只输出一个最优掩码医学图像里通常不需要多个候选。返回的 mask 是布尔数组True 表示息肉区域。3.3 完整流水线与可视化把检测和分割串起来加上可视化。import matplotlib.pyplot as plt def full_pipeline(image_path): # 第一步YOLO 检测 img_rgb, boxes detect_polyps(image_path) if len(boxes) 0: print(未检测到息肉) return img_rgb, None # 第二步SAM 分割 masks segment_with_boxes(img_rgb, boxes) # 第三步可视化 fig, axes plt.subplots(1, 3, figsize(15, 5)) # 原图 axes[0].imshow(img_rgb) axes[0].set_title(Original) axes[0].axis(off) # YOLO 检测框 axes[1].imshow(img_rgb) for b in boxes: x1, y1, x2, y2 b[bbox] rect plt.Rectangle((x1, y1), x2-x1, y2-y1, fillFalse, colorred, linewidth2) axes[1].add_patch(rect) axes[1].set_title(YOLO Detection) axes[1].axis(off) # SAM 分割掩码叠加 axes[2].imshow(img_rgb) overlay np.zeros_like(img_rgb) for m in masks: overlay[m[mask]] [0, 255, 0] axes[2].imshow(overlay, alpha0.5) axes[2].set_title(SAM Segmentation) axes[2].axis(off) plt.tight_layout() plt.savefig(result.png, dpi150) plt.show() return img_rgb, masks # 运行 full_pipeline(test_images/polyp_001.jpg)逻辑说明可视化分三栏原图、检测框、分割掩码。掩码用绿色叠加alpha0.5 控制透明度。保存结果方便对比。参数上如果掩码边缘不够精细可以尝试在框内额外加一个中心点提示SAM 会结合两种提示优化边界。3.4 批量处理与性能优化单张图跑通后批量处理需要考虑显存和速度。SAM 的图像编码是瓶颈ViT-H 在 1080p 图像上约 1 秒/张。优化思路import torch from tqdm import tqdm def batch_process(image_dir, output_dir): os.makedirs(output_dir, exist_okTrue) image_files [f for f in os.listdir(image_dir) if f.endswith((.jpg, .png))] for img_file in tqdm(image_files): img_path os.path.join(image_dir, img_file) img_rgb, boxes detect_polyps(img_path) if len(boxes) 0: continue masks segment_with_boxes(img_rgb, boxes) # 保存掩码 combined_mask np.zeros(img_rgb.shape[:2], dtypenp.uint8) for m in masks: combined_mask[m[mask]] 255 cv2.imwrite(os.path.join(output_dir, img_file.replace(.jpg, _mask.png)), combined_mask) # 清理显存 torch.cuda.empty_cache()逻辑说明torch.cuda.empty_cache()在每张图处理后清理缓存避免显存碎片。如果显存充足可以去掉这行提升速度。批量处理时建议把 YOLO 的verbose关掉减少日志输出。4. 训练与微调让 YOLO 认识息肉4.1 数据集准备与格式转换公开的息肉检测数据集有 Kvasir-SEG、CVC-ClinicDB 等。这些数据集原始格式是图像加掩码需要转成 YOLO 的标注格式。import os import cv2 import numpy as np def mask_to_yolo_bbox(mask_path, img_width, img_height): 从二值掩码提取边界框转成 YOLO 格式 (cx, cy, w, h) 归一化 mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) contours, _ cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None # 合并所有轮廓的边界 all_points np.vstack([c.reshape(-1, 2) for c in contours]) x_min, y_min all_points.min(axis0) x_max, y_max all_points.max(axis0) # 转 YOLO 格式 cx (x_min x_max) / 2 / img_width cy (y_min y_max) / 2 / img_height w (x_max - x_min) / img_width h (y_max - y_min) / img_height return cx, cy, w, h # 批量转换 def convert_dataset(img_dir, mask_dir, output_label_dir): os.makedirs(output_label_dir, exist_okTrue) for img_file in os.listdir(img_dir): if not img_file.endswith(.jpg): continue img_path os.path.join(img_dir, img_file) mask_path os.path.join(mask_dir, img_file.replace(.jpg, .png)) if not os.path.exists(mask_path): continue img cv2.imread(img_path) h, w img.shape[:2] bbox mask_to_yolo_bbox(mask_path, w, h) if bbox is None: continue label_file os.path.join(output_label_dir, img_file.replace(.jpg, .txt)) with open(label_file, w) as f: f.write(f0 {bbox[0]:.6f} {bbox[1]:.6f} {bbox[2]:.6f} {bbox[3]:.6f}\n)逻辑说明YOLO 格式是class_id cx cy w h全部归一化到 0-1。mask_to_yolo_bbox从掩码提取外接矩形合并多个轮廓避免漏掉分散区域。参数上类别 id 统一用 0 表示息肉。4.2 YOLO 微调训练配置用 ultralytics 训练 YOLOv8配置文件用 YAML。# polyp_dataset.yaml path: ./data/polyp train: images/train val: images/val names: 0: polypfrom ultralytics import YOLO # 加载预训练权重 model YOLO(yolov8s.pt) # 训练 results model.train( datapolyp_dataset.yaml, epochs100, imgsz640, batch16, lr00.01, lrf0.01, patience20, device0, projectruns/polyp, nameyolov8s_polyp )逻辑说明yolov8s是速度和精度的平衡点医学图像如果追求更高精度可以换yolov8m或yolov8l。epochs100配合patience20早停避免过拟合。lr0初始学习率 0.01lrf最终学习率因子 0.01这是 ultralytics 的默认策略。imgsz640是标准输入尺寸息肉较小的话可以提到 1024。4.3 训练过程监控与指标解读训练时关注几个指标mAP50、mAP50-95、precision、recall。医学图像里 recall 比 precision 重要漏检的代价比误检大。# 训练完成后验证 metrics model.val() print(fmAP50: {metrics.box.map50:.4f}) print(fmAP50-95: {metrics.box.map:.4f}) print(fPrecision: {metrics.box.mp:.4f}) print(fRecall: {metrics.box.mr:.4f})逻辑说明mAP50是 IoU 阈值 0.5 时的平均精度息肉检测一般能到 0.85 以上。mAP50-95更严格能到 0.6 就不错。如果 recall 偏低降低conf_threshold或增加训练数据。如果 precision 偏低检查标注质量。5. 避坑与排查这条流水线最容易翻车的五个地方5.1 现象SAM 把整个肠壁都分割进去了原因YOLO 的框太大包含了太多背景。SAM 会倾向于分割框内所有看起来像目标的区域框太大时它分不清息肉和肠壁。解决收紧 YOLO 的框或者在 SAM 提示里加一个息肉中心的点提示。点提示用predict(point_coords..., point_labels1)和框提示一起用。实测下来框加点双提示能把边界误差降低 15% 左右。5.2 现象小息肉分割掩码为空原因SAM 对极小目标小于 20x20 像素的分割能力有限图像编码后的特征图分辨率不够。解决把小息肉区域裁剪出来放大后再送 SAM或者用 ViT-H 代替 ViT-B。另一个办法是降低 YOLO 的conf_threshold到 0.15确保小息肉不被漏掉。5.3 现象推理速度慢视频流跑不动原因SAM 的图像编码是固定开销ViT-H 在 1080p 上约 1 秒/帧视频流 25fps 根本跑不动。解决不要每帧都跑 SAM。YOLO 逐帧检测只在检测到息肉时触发 SAM且 SAM 可以隔帧跑。或者把 SAM 换成 MobileSAM速度快 10 倍但精度略降。如果必须实时考虑 TensorRT 加速 YOLOSAM 用 ONNX Runtime。5.4 现象掩码边缘有锯齿或空洞原因SAM 输出的掩码是 256x256 低分辨率上采样到原图尺寸时产生锯齿。解决后处理加形态学操作闭运算填空洞高斯模糊平滑边缘。或者用predictor.predict的return_logitsTrue拿到 logits自己做双线性上采样。import cv2 import numpy as np def refine_mask(mask, kernel_size5): 形态学后处理闭运算填洞 平滑边缘 mask_uint8 (mask * 255).astype(np.uint8) kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size)) closed cv2.morphologyEx(mask_uint8, cv2.MORPH_CLOSE, kernel) smoothed cv2.GaussianBlur(closed, (kernel_size, kernel_size), 0) return smoothed 127逻辑说明闭运算先膨胀后腐蚀填小空洞。高斯模糊平滑边缘。kernel_size根据图像分辨率调1080p 用 5-7 比较合适。5.5 现象不同医院的数据上效果差异大原因不同内镜设备的色彩、光照、分辨率不同模型泛化能力不足。解决训练时加数据增强特别是色彩抖动和亮度变化。YOLO 的hsv_h、hsv_s、hsv_v参数控制 HSV 增强幅度建议设 0.015、0.7、0.4。另外可以在目标医院的数据上做少量微调哪怕只有几十张图也能明显提升。6. 进阶技巧用点提示加框提示把边界误差压到最低前面讲的都是框提示但 SAM 其实支持多种提示组合。我在实际项目里发现一个很实用的技巧在 YOLO 框的基础上自动生成一个息肉中心点作为额外提示。这个点不需要人工标注直接从 YOLO 框的中心取就行但效果提升很明显。原理是 SAM 的 prompt encoder 会把框和点分别编码然后融合。框告诉 SAM大概在这个范围点告诉 SAM目标中心在这里两者结合能显著减少框内背景被误分割的情况。def segment_with_box_and_point(image_rgb, boxes): 框提示 中心点提示提升分割边界精度 predictor.set_image(image_rgb) masks [] for box_info in boxes: x1, y1, x2, y2 box_info[bbox] bbox np.array([x1, y1, x2, y2]) # 自动生成中心点 cx (x1 x2) // 2 cy (y1 y2) // 2 point_coords np.array([[cx, cy]]) point_labels np.array([1]) # 1 表示前景点 mask, score, _ predictor.predict( boxbbox, point_coordspoint_coords, point_labelspoint_labels, multimask_outputFalse ) masks.append({ mask: mask[0], score: float(score[0]), bbox: box_info[bbox] }) return masks逻辑说明point_coords是点的坐标point_labels中 1 表示前景、0 表示背景。中心点用框的中心不需要额外标注。multimask_outputFalse保持单掩码输出。实测这个改动在 Kvasir-SEG 数据集上能把 Dice 系数从 0.82 提到 0.87 左右。另一个进阶方向是用 SAM 的输出反过来过滤 YOLO 的误检。如果 SAM 对某个框的分割得分低于阈值比如 0.7说明这个框可能不是真息肉可以丢弃。这个策略能降低假阳性率在临床场景里很有价值。验证方法上我一般用 Dice 系数和 IoU 两个指标。Dice 对边界更敏感IoU 对整体区域更敏感。两个都看避免单一指标误导。测试集要包含不同大小、不同形态的息肉特别是扁平息肉和微小息肉这两类最容易翻车。最后说个血泪经验不要指望一套参数跑遍所有数据。不同内镜设备的成像差异比你想象的大换一家医院的数据YOLO 的置信度阈值和 SAM 的提示策略可能都要重新调。我现在的习惯是每换一个数据源先跑 20 张图看分割效果手动调一轮参数再批量跑。这个前期投入能省掉后面大量返工。希望帮到你。本文还有配套的精品资源点击获取