SAM2实战:5种交互式图片分割技巧全解析(附Python代码)
如果你在计算机视觉项目里折腾过物体分割,肯定遇到过那种让人头疼的场景:目标边缘模糊不清,背景复杂得像一团乱麻,或者需要手动标注几百张图片才能开始训练。传统的分割方法要么需要大量标注数据,要么对特定场景的泛化能力有限,每次换个数据集就得重新来过。
去年Meta发布的Segment Anything Model(SAM)确实让人眼前一亮,它那种“点哪分哪”的交互方式彻底改变了游戏规则。但SAM主要针对静态图像,对于视频或者需要跨帧一致性的场景就有些力不从心了。现在,SAM 2来了——它不仅继承了SAM的所有优点,还增加了对视频分割的原生支持,更重要的是,它在处理复杂场景时的稳定性和准确性都有了显著提升。
我最近在实际项目中深度使用了SAM 2,发现它的五种交互式提示方法(单点、多点、框选、混合、批量)组合起来,能解决90%以上的分割难题。特别是面对那些边缘模糊、背景干扰严重的“硬骨头”图片时,正确的提示策略比单纯调参有效得多。
这篇文章我会带你从零开始,用实际的Python代码演示这五种技巧,重点解决那些在实际项目中真正让人头疼的问题。我会分享一些踩坑经验,比如如何处理SAM 2的多掩码输出、怎么用负向提示排除干扰区域,以及批量处理时的内存优化技巧。代码都是可以直接在Colab上运行的完整示例,你可以边看边试。
1. 环境配置与基础准备
开始之前,我们需要先搭建好SAM 2的运行环境。Meta官方提供了两种主要的使用方式:通过原始的GitHub仓库安装,或者通过Ultralytics的YOLO框架集成。我个人更推荐后者,因为Ultralytics的封装更加友好,而且与YOLO生态的无缝集成能让后续的检测-分割流水线更加顺畅。
1.1 安装与依赖
如果你打算使用原生的SAM 2实现,需要从GitHub克隆仓库并安装依赖:
# 克隆官方仓库
git clone https://github.com/facebookresearch/segment-anything-2.git
cd segment-anything-2
# 安装依赖(建议使用虚拟环境)
pip install -e .
不过我更常用的是Ultralytics的集成版本,安装起来更简单:
pip install ultralytics
安装完成后,验证一下是否成功:
import ultralytics
print(ultralytics.__version__) # 应该输出8.x.x或更高版本
注意:SAM 2模型文件比较大(最小的tiny版本约78MB,最大的large版本约375MB),首次运行时会自动下载。如果你在国内,下载速度可能较慢,建议提前准备好模型文件,或者使用镜像源。
1.2 模型选择与加载
SAM 2提供了多个预训练模型,从轻量级到高精度版本都有。选择哪个版本取决于你的具体需求:
| 模型版本 | 参数量 | 文件大小 | 适用场景 |
|---|---|---|---|
| SAM 2 tiny | 38.9M | 78.1MB | 移动端部署、快速原型验证 |
| SAM 2 small | 约50M | 约100MB | 平衡速度与精度 |
| SAM 2 base | 80.8M | 162MB | 大多数生产环境 |
| SAM 2 large | 93.7M | 375MB | 最高精度要求 |
在实际项目中,我通常从base版本开始,如果速度不够再降级到small,如果精度不够再升级到large。tiny版本虽然快,但在复杂场景下的分割质量会有明显下降。
加载模型非常简单:
from ultralytics import SAM
# 加载base版本模型
model = SAM('sam2_b.pt')
# 查看模型信息
model.info()
如果你需要更细粒度的控制,比如指定设备、精度等,可以这样:
import torch
# 指定使用GPU,并启用混合精度
device = 'cuda' if torch.cuda.is_available() else 'cpu'
model = SAM('sam2_b.pt', device=device)
# 如果你有多个GPU,可以指定具体哪一个
# model = SAM('sam2_b.pt', device='cuda:0')
1.3 基础工具函数
在实际使用中,有几个辅助函数能大大提升开发效率。我整理了一套自己常用的工具集:
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
import cv2
def load_and_preprocess_image(image_path, target_size=None):
"""加载并预处理图像"""
image = Image.open(image_path).convert('RGB')
if target_size:
# 保持宽高比调整大小
original_size = image.size
ratio = min(target_size[0]/original_size[0], target_size[1]/original_size[1])
new_size = (int(original_size[0]*ratio), int(original_size[1]*ratio))
image = image.resize(new_size, Image.Resampling.LANCZOS)
return np.array(image)
def visualize_masks(image, masks, scores=None, points=None, boxes=None,
point_labels=None, alpha=0.6, save_path=None):
"""可视化分割结果"""
fig, axes = plt.subplots(1, len(masks)+1, figsize=(5*(len(masks)+1), 5))
# 显示原图
axes[0].imshow(image)
axes[0].set_title('Original Image')
axes[0].axis('off')
# 显示每个掩码
for i, mask in enumerate(masks):
axes[i+1].imshow(image)
# 创建彩色掩码覆盖层
color = np.array([30/255, 144/255, 255/255, alpha])
mask_layer = mask.reshape(mask.shape[-2:], 1) * color.reshape(1, 1, -1)
# 叠加掩码
axes[i+1].imshow(mask_layer)
# 添加轮廓
if mask.ndim == 2:
contours, _ = cv2.findContours(mask.astype(np.uint8),
cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
axes[i+1].contour(contours, colors='red', linewidths=1)
# 显示分数(如果有)
title = f'Mask {i+1}'
if scores is not None and i < len(scores):
title += f' (Score: {scores[i]:.3f})'
axes[i+1].set_title(title)
axes[i+1].axis('off')
plt.tight_layout()
if save_path:
plt.savefig(save_path, dpi=300, bbox_inches='tight')
print(f"结果已保存到: {save_path}")
plt.show()
def calculate_iou(mask1, mask2):
"""计算两个掩码之间的IoU(交并比)"""
intersection = np.logical_and(mask1, mask2).sum()
union = np.logical_or(mask1, mask2).sum()
return intersection / union if union > 0 else 0
这些工具函数会在后面的示例中反复使用。特别是visualize_masks函数,它能清晰地展示多个候选掩码,帮助你直观地比较不同提示策略的效果。
2. 单点提示:快速定位与多候选选择
单点提示是SAM 2最基本也最常用的交互方式。你只需要在目标物体上点一下,模型就会给出几个可能的分割结果。听起来简单,但这里面有不少门道。
2.1 基础单点分割
让我们从一个简单的例子开始。假设我们有一张包含卡车的图片,想要分割出卡车:
from ultralytics import SAM
import numpy as np
# 加载模型
model = SAM('sam2_b.pt')
# 加载图片
image_path = 'truck.jpg'
image = load_and_preprocess_image(image_path)
# 设置单点提示(前景点)
input_point = np.array([[500, 375]]) # 格式:[x, y]
input_label = np.array([1]) # 1表示前景,0表示背景
# 执行预测
results = model(image, points=input_point, labels=input_label, multimask_output=True)
# 解析结果
masks = results[0].masks.data.cpu().numpy() # 形状: [3, H, W]
scores = results[0].scores.cpu().numpy() # 置信度分数
print(f"生成了 {len(masks)} 个候选掩码")
print(f"置信度分数: {scores}")
# 可视化结果
visualize_masks(image, masks, scores, points=input_point, point_labels=input_label)
运行这段代码,你会看到模型输出了三个候选掩码,每个都有一个置信度分数。这是SAM 2处理模糊性提示的典型方式——当提示不够明确时,它会给出多个可能的结果。
2.2 处理模糊性与多候选选择
单点提示最大的挑战就是模糊性。比如你点在卡车的一个轮子上,模型可能理解为:
- 整个卡车
- 单个轮子
- 轮子所在的区域
这时候就需要根据置信度分数和视觉检查来选择最佳掩码。我通常用这个策略:
def select_best_mask(masks, scores, strategy='highest_score'):
"""根据策略选择最佳掩码"""
if strategy == 'highest_score':
# 选择置信度最高的
best_idx = np.argmax(scores)
return masks[best_idx], best_idx
elif strategy == 'largest_area':
# 选择面积最大的(适用于想分割整个物体的情况)
areas = [mask.sum() for mask in masks]
best_idx = np.argmax(areas)
return masks[best_idx], best_idx
elif strategy == 'most_compact':
# 选择最紧凑的(面积/周长比最大)
compactness = []
for mask in masks:
# 计算轮廓
contours, _ = cv2.findContours(mask.astype(np.uint8),
cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
if contours:
perimeter = cv2.arcLength(contours[0], True)
area = mask.sum()
compactness.append(area / perimeter if perimeter > 0 else 0)
else:
compactness.append(0)
best_idx = np.argmax(compactness)
return masks[best_idx], best_idx
else:
raise ValueError(f"未知策略: {strategy}")
# 使用不同策略选择最佳掩码
strategies = ['highest_score', 'largest_area', 'most_compact']
for strategy in strategies:
best_mask, idx = select_best_mask(masks, scores, strategy)
print(f"策略 '{strategy}' 选择了掩码 {idx+1}, 分数: {scores[idx]:.3f}")
# 可视化选择结果
plt.figure(figsize=(8, 4))
plt.subplot(1, 2, 1)
plt.imshow(image)
plt.imshow(best_mask, alpha=0.5, cmap='jet')
plt.title(f'策略: {strategy}')
plt.axis('off')
plt.subplot(1, 2, 2)
plt.bar(range(len(scores)), scores)
plt.axvline(x=idx, color='red', linestyle='--', label=f'选择: {idx+1}')
plt.xlabel('掩码索引')
plt.ylabel('置信度')
plt.title('候选掩码分数')
plt.legend()
plt.tight_layout()
plt.show()
在实际项目中,我通常先用highest_score策略,如果结果不理想再尝试其他策略。对于卡车这种大物体,largest_area往往效果更好;而对于小物体或精细结构,most_compact可能更合适。
2.3 负向单点提示
有时候我们想排除某些区域。比如我们想分割卡车,但不想包括车窗:
# 前景点:卡车的车身
foreground_point = np.array([[500, 375]])
# 背景点:车窗

1058

被折叠的 条评论
为什么被折叠?



