ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SAM本地部署实战:ViT选型、显存优化与自动分割调参

SAM本地部署实战:ViT选型、显存优化与自动分割调参 SAM 这个词这两年出现的频率太高了高到什么程度呢——很多时候它已经不是一个模型的意思而是变成了分割这个动作的代名词。我自己第一次接触 segment anything 是在做一个遥感地块提取的小项目当时想的是拿它当个免费的预标注工具结果从下载权重到跑通第一张图前后折腾了大半天踩的坑基本都是环境、显存、通道顺序这三类。后来陆陆续续在几台不同配置的机器上做过 SAM 本地部署从 8G 显存的笔记本到 24G 的工作站都试过也把手里的流程整理成了一套能复用的东西。这篇就把整个本地部署过程拆开讲清楚包括模型结构里那几个容易混淆的概念比如经常有人问的 PVS、版本怎么选、参数怎么调、8G 卡怎么救以及在实际业务场景里它到底能干什么、干不了什么。不管你是刚听说 SAM 想跑个 demo还是已经在做分割类项目想找个稳定的本地推理方案下面这些内容应该都能直接用上。1. 先说清楚SAM 本地部署到底解决什么问题1.1 SAM 和普通分割模型的本质区别传统的语义分割模型比如 DeepLab、UNet 那一类思路是训练一个模型 → 固定输出某几类结果。你要分割道路就得有道路的标注数据你要分割耕地就得有耕地的标注数据。类别一旦定死模型的能力边界也就定死了想加一个新类别不好意思重新标注、重新训练。SAM 走的是另一条路。它把分割这件事从分类问题重新定义成了提示响应问题——你给它一张图再给它一个提示点、框、粗略掩码它把提示对应的物体轮廓吐出来。它不知道这是个什么东西也不需要知道它只负责边界。这种能力在论文里叫 promptable segmentation也就是可提示分割。这个转变带来的实际价值非常直接。假设你手上有五千张图片要做标注传统流程是一张张手动画多边形一个人一天能画几十张就不错了。用 SAM 的话你只需要在每个目标上点一下掩码就出来了剩下的是人工修边效率能提高好几倍。这就是为什么 SAM 在数据标注团队里普及得这么快——它不是一个业务模型它是一个生产力工具。1.2 本地部署 vs 在线调用的取舍账很多人第一反应是我直接调接口不就行了确实云端调用上手最快写几行代码就能出结果。但真到了项目里问题会一个个冒出来。第一是数据问题。做遥感、医疗、工业质检这些方向的手上跑的影像往往不方便往外传有些还有明确的授权范围限制能不出本地就不出本地。第二是成本问题SAM 这种模型单张推理的计算量不小尤其是批量跑自动分割图片一多按量计费的账算下来很吓人。第三是稳定性接口有并发限制、有超时、有版本变更你的流水线跑到一半突然挂掉排查起来非常难受。本地部署一次搞定之后后面就是纯电费和显卡折旧了。批量跑一万张图机器开着跑一晚上成本基本可以忽略。而且你能完全控制预处理、后处理、批大小、精度想怎么优化就怎么优化。代价是前期要花时间把环境弄对这也是本文的主要篇幅所在。1.3 显存和硬件门槛的真实账单先把最现实的问题摆出来SAM 有三个官方版本ViT-B、ViT-L、ViT-H权重文件分别是 375MB、1.2GB、2.4GB 左右。显存占用不是跟着权重文件走的而是跟着图像编码器的前向计算走的因为输入被固定成了 1024×1024。我在几台机器上实测过单张图推理的峰值显存fp32batch1ViT-B 大约 2.5GB 到 3GBViT-L 大约 4.5GB 到 5.5GBViT-H 大约 6.5GB 到 7.5GB。注意这只是跑一张图的占用如果开自动全图分割points_per_batch 设成 64还会再往上加个一两 G。所以结论很清晰8G 显存可以用 ViT-H 做单点交互式分割但做自动全图分割会紧6G 显存建议 ViT-B 起步4G 显存就得考虑半精度或者换 MobileSAM 这类轻量版本了。CPU 也能跑但速度嘛ViT-H 在普通桌面 CPU 上编码一张 1024 的图要十几秒到几十秒交互体验基本没了。如果只是离线批处理、数量不大CPU 也能凑合但心理预期得放低。2. SAM 模型结构拆解搞懂三件套再动手2.1 Image Encoder真正吃显存的那一环SAM 的结构可以粗暴地理解成三个零件拼起来Image Encoder、Prompt Encoder、Mask Decoder。这三个的分工和开销差异巨大搞清楚了后面优化才有方向。Image Encoder 是一个 MAE 预训练过的 ViT。输入图像先被缩放到长边 1024短边补齐到 1024变成一张 1024×1024×3 的图。ViT-H 用的 patch 大小是 16所以切出来是 64×64 个 patch也就是 4096 个 token每个 token 的嵌入维度是 1280ViT-L 是 1024ViT-B 是 768。ViT-H 有 24 层 Transformer blockViT-B 只有 12 层。这个环节是整个模型里最贵的。参数量上 ViT-H 的编码器大约 6.3 亿参数ViT-B 大约 9000 万差了七倍。耗时上在消费级显卡上 ViT-H 编码一张图大概 0.5 到 1 秒ViT-B 大概是它的三分之一到四分之一。但关键的一点是对于同一张图Image Encoder 只需要跑一次。跑完之后得到的那个 64×64×256 的图像嵌入可以被反复使用。你在这张图上点一百次、画十个框编码器都不用再算一遍。这个特性决定了交互式标注场景下的体验非常流畅——第一次点击慢一点后面几乎瞬间出结果。2.2 Prompt Encoder点和框怎么变成模型能懂的向量Prompt Encoder 做的事情很轻量它负责把你的提示信息点坐标、框坐标、已有掩码编码成向量塞给后面的解码器。点提示的处理方式是位置编码加上一个可学习的前景/背景嵌入。你给的每个点有两个属性坐标和标签1 表示前景、0 表示背景。代码里就是你传point_labels数组1 和 0 混着来告诉模型这个点是我要的东西那个点是背景。框提示则是把它拆成左上角和右下角两个角点用一对特殊的学习嵌入来表示这是框的起点这是框的终点再加位置编码。掩码提示走的是另一条路用卷积把低分辨率掩码下采样后跟图像嵌入相加。论文里还提到了文本提示用的是 CLIP 的文本编码器但官方开源版本并没有放出文本提示的接口所以实际用不到。这个模块本身几乎不占显存计算量也小到可以忽略但它是整个交互逻辑的入口坐标格式、标签含义搞错了结果就会很奇怪。2.3 Mask Decoder为什么一次要吐三个掩码Mask Decoder 是一个很轻的 Transformer只有两层但它有个设计非常关键一次输出三个候选掩码附带三个预测 IoU 分数。原因很简单歧义。你在一件衣服上点一下这个点可能指的是整件衣服也可能是衣服上的一个口袋或者衣服上的一块印花。人看到提示的时候心里有明确指向但模型没有这个上下文。所以 SAM 干脆把几种可能的解释都输出出来用预测 IoU 排个序让使用者自己挑或者通过多轮交互加正负点逐步收敛。这三个掩码是从低分辨率的 logits 上采样来的原始输出是 256×256上采样 4 倍回到 1024×1024再裁掉前面的 padding 区域恢复成原图尺寸的二值掩码。multimask_outputTrue时返回三个False时只返回一个这个是三个里综合得分最高的那个的变体通常在框提示下更准。理解了这个三个掩码的机制你在写代码时就不会困惑于为什么我拿到了三个结果也知道该怎么按分数挑选。2.4 PVS 到底是什么和图像分割有什么关系经常有人在讨论里问 PVS 是什么意思这里单独说一下。PVS 是 Promptable Visual Segmentation 的缩写中文一般译成可提示视觉分割。它是 SAM 论文里提出的一个任务设定但不是图像上的任务是视频上的任务。做法是在视频的第一帧上给提示点、框、掩码都行模型生成一个初始掩码然后把这个掩码作为下一帧的提示传播下去逐帧生成分割结果。如果某一帧分错了你可以暂停在这一帧上补一个点模型会基于这个修正重新把后续帧刷一遍。所以 PVS 更像是 SAM 从静态图像往视频方向的一个能力延伸官方开源的那版代码并没有直接提供视频 PVS 的完整实现真正把这个方向做成可用产品的是后来的 SAM 2它在架构里加了记忆机制能实时处理视频流。如果你手上的任务只涉及单张图片PVS 这个概念知道就行不影响你部署和使用如果要做视频分割直接看 SAM 2 而不是原始 SAM。3. 版本选型ViT-B / ViT-L / ViT-H 怎么选3.1 三档权重的硬指标对比选版本本质上是在精度、速度、显存三者之间做取舍。下面这张表是我自己整理的实际参考值测试环境是一张 12G 显存的卡输入 1024×1024batch1fp32 精度版本权重文件编码器参数量单图编码耗时峰值显存自动分割耗时32×32 点ViT-B约 375MB约 0.9 亿约 0.2-0.3s2.5-3GB约 25-40sViT-L约 1.2GB约 3.1 亿约 0.5-0.7s4.5-5.5GB约 50-70sViT-H约 2.4GB约 6.3 亿约 0.8-1.2s6.5-7.5GB约 90-150s精度上的差距在常规自然图像上 ViT-H 确实明显更好尤其是小目标、细长结构比如栏杆、电线、树枝的边界更干净。但到了专业领域影像上比如遥感图或者工业检测图三者的差距会被拉小——因为训练数据分布本来就偏自然图像谁的泛化优势都有限。3.2 生态里的几个替代方案官方 SAM 之外社区里冒出了不少变体实际项目里值得关注的有这几个。MobileSAM 是把图像编码器换成了一个轻量化的 TinyViT权重只有 40MB 左右编码速度比 ViT-H 快十几倍显存占用降到 1G 以内。代价是细粒度边界会糊一些。如果你的场景是大致圈出目标区域比如遥感地块粗提取MobileSAM 完全够用。FastSAM 走的是完全不同的路线它基于 YOLOv8-seg为了速度牺牲了提示的灵活性不支持任意的点提示交互但对全图自动分割所有物体这类任务速度极快。EfficientSAM 是另一套轻量化思路用掩码图像预训练来蒸馏精度和速度的平衡做得不错。SAM 2 则是在原版基础上加了视频能力图像分割的精度也有提升如果你的任务里带视频或者时序影像建议直接上 SAM 2。缺点是显存开销比原版更大。3.3 我的选型建议给一个实操层面的决策路径先用手上的显卡跑一遍 ViT-H 的自动分割如果能过就用它如果 OOM 或者慢到不能接受退到 ViT-B 再试如果 ViT-B 也不行或者要做交互式标注需要极致响应速度换 MobileSAM。还有一个容易被忽略的点提示方式不同对模型版本的要求也不同。框提示下 ViT-B 和 ViT-H 的差距很小因为框已经把空间范围框死了模型只需要在里面找边界。点提示下差距才明显因为所有判断都得靠模型自己。所以如果你的流程里主要用框做提示比如从检测模型拿检测框再送进 SAM 精修边界用 ViT-B 是性价比最高的选择。4. 环境准备与依赖安装从驱动到 PyTorch4.1 显卡驱动与 CUDA 的对齐这一步是翻车重灾区一定要按顺序来。先在终端里确认驱动情况用nvidia-smi看输出里的 Driver Version 和 CUDA Version。注意这里显示的 CUDA Version 是驱动支持的最高运行时版本不是已经装好的版本很多人在这里理解错了。驱动版本决定了你最多能用哪个 CUDA 版本的 PyTorch。比如驱动显示支持 CUDA 12.1那你就不能装 cu124 版本的 PyTorch装了会在 import 时报错或者直接回退到 CPU。要么升级驱动要么选低版本的 PyTorch 预编译包。最省事的做法是不管系统 CUDA直接用 conda 或 pip 安装 PyTorch 官方提供的预编译 wheel它自带所需的 CUDA 运行时库跟你系统里装没装 CUDA Toolkit 关系不大。系统 CUDA 只在你要自己编译某些扩展时才需要。我遇到过一次很典型的问题机器上装了两个 CUDA 版本的 PyTorch其中一个还是 CPU 版结果 import torch 之后torch.cuda.is_available()一直是 False。排查办法就是打印torch.__version__和torch.version.cuda看一眼torch.version.cuda是不是 None是 None 就是装成 CPU 版了。4.2 创建独立环境并安装 PyTorch强烈建议用 conda 建独立环境不要往系统 Python 里装。SAM 对 PyTorch 版本不算特别挑1.7 以上的版本基本都能跑但版本太新可能会撞上一些 API 变更。conda create -n sam python3.10 -y conda activate sam然后按你的 CUDA 版本选对应的安装命令。以 CUDA 11.8 为例pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118CUDA 12.1 的话把 cu118 换成 cu121。装完之后验证一下python -c import torch; print(torch.__version__, torch.version.cuda, torch.cuda.is_available())三个输出分别是版本号、CUDA 版本、True才算对了。如果最后一个是 False先别往下走把驱动和 PyTorch 版本对齐了再说。4.3 安装 segment-anything 与依赖清单官方仓库可以直接从 GitHub 装git clone https://github.com/facebookresearch/segment-anything.git cd segment-anything pip install -e .-e是开发模式安装好处是后面你想改仓库里的代码比如调参、加日志可以直接改源码不用重新安装。另外还需要几个常用库pip install opencv-python numpy matplotlib pillow pycocotools这里要单独说pycocotools。自动掩码生成器里有个min_mask_region_area参数用来过滤掉面积过小的碎掩码这个过滤逻辑依赖 pycocotools。不装的话只要这个参数不为 0 就会报 ImportError。而且 pycocotools 在 Windows 上编译容易出问题Linux 上一般直接 pip 装就好。4.4 权重下载与目录规划权重文件不在 pip 包里要单独下。官方提供三个下载地址对应的文件名是sam_vit_h_4b8939.pthViT-Hsam_vit_l_0b3195.pthViT-Lsam_vit_b_01ec64.pthViT-B下完之后建议放一个统一的目录我自己习惯是这样组织的project/ ├── weights/ │ ├── sam_vit_h_4b8939.pth │ └── sam_vit_b_01ec64.pth ├── images/ ├── outputs/ └── scripts/结构简单但有个好处脚本里可以用相对路径引用权重换机器的时候整个目录拷过去就能跑不用改代码。文件名后面那串十六进制不要改sam_model_registry加载的时候虽然不校验文件名但保留原名字方便你以后确认版本。注意权重文件里的 model_type 必须和加载时指定的类型匹配。用sam_model_registry[vit_h]去加载 ViT-B 的权重会在加载时报一堆 key 不匹配的警告要么某些层随机初始化要么直接报错。改文件名不会解决问题得改成对应的类型字符串。5. 最小可用推理点提示和框提示跑通5.1 图像预处理最容易翻车的地方在写推理代码之前必须把通道顺序这件事说清楚这是新手最常踩的坑而且症状很隐蔽——不会报错只是分割结果莫名其妙。OpenCV 的cv2.imread读进来是 BGR 三通道而 SAM 和 PyTorch 的所有图像处理都默认 RGB。如果你直接把 cv2 读的结果喂给 SAM模型会看到一张颜色错乱的图边界结果可能还是大差不差但在颜色相近的区域会明显变差。所以一定要转image_bgr cv2.imread(images/test.jpg) image cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)另外一个细节是数据类型。SAM 期望输入是 uint8 的 HWC 格式 numpy 数组取值 0 到 255。如果你用的是 PIL 读图同样要np.array()转成 numpy。如果做浮点运算之后忘了转回 uint8输入会变成 0 到 1 的小数模型会得到一张几乎全黑的图分割结果基本是空的或者一团糊。还有一点SAM 内部会自己做 resize 和 paddingResizeLongestSide长边缩到 1024短边补零所以你不需要在外部再缩放直接喂原图就行。这一点很多人会多此一举地先 resize 一遍结果造成二次缩放边界精度反而下降。5.2 点提示分割的完整代码下面这份代码是最小可运行版本我自己在好几个项目里都从它改起import numpy as np import torch import cv2 from segment_anything import sam_model_registry, SamPredictor DEVICE cuda if torch.cuda.is_available() else cpu CKPT weights/sam_vit_h_4b8939.pth sam sam_model_registry[vit_h](checkpointCKPT) sam.to(deviceDEVICE) sam.eval() predictor SamPredictor(sam) bgr cv2.imread(images/test.jpg) image cv2.cvtColor(bgr, cv2.COLOR_BGR2RGB) with torch.inference_mode(): predictor.set_image(image) masks, scores, logits predictor.predict( point_coordsnp.array([[640, 360]]), point_labelsnp.array([1]), multimask_outputTrue, ) print(scores:, scores) print(mask shapes:, [m.shape for m in masks])几个要点解释一下。sam.eval()必须调不调的话模型处于训练模式虽然 SAM 里没有 BatchNorm 和 Dropout 之类的层理论上影响不大但养成习惯总是好的。set_image是那个贵的操作它内部跑了图像编码器把self.features缓存下来。同一张图后面所有的predict调用都复用这个缓存所以如果你的应用是在一张图上做多次交互set_image只调用一次就够了。point_coords的格式是[[x, y]]注意是 x 在前 y 在后跟 numpy 图像的[row, col]索引刚好相反。这个也很容易搞反搞反了会出现我点的是物体 A结果分割出了物体 B这种诡异现象。point_labels里 1 是前景、0 是背景。多轮交互的时候你可以把第一轮点错的地方标成 0 加进数组告诉模型这里不要。返回值里masks是一个形状为(3, H, W)的布尔数组scores是三个预测 IoU。挑哪个用取决于你的下游逻辑做一个可视化对比的话通常第一个分数最高的就是最合理的那个。5.3 框提示与组合提示的写法框提示在工程里用得比点提示更多因为它更稳定、更可自动化。典型场景是你先用一个检测模型YOLO 之类的把目标框出来再把框送进 SAM 精修边界这样就能把检测 分割串成一条流水线。with torch.inference_mode(): predictor.set_image(image) masks, scores, logits predictor.predict( boxnp.array([120, 80, 640, 520]), multimask_outputTrue, )框的格式是[x0, y0, x1, y1]左上角和右下角。框和点还能一起用。有个小技巧把框的两个角点坐标放进point_coords标签分别给 2 和 3再加上你自己的正负点就能在一次调用里同时传达大致范围在这里和这几个像素肯定是/肯定不是。这在框不准的时候特别有用——框给个大概点用来纠偏。with torch.inference_mode(): masks, scores, logits predictor.predict( point_coordsnp.array([[320, 240], [500, 400], [200, 150]]), point_labelsnp.array([1, 1, 0]), boxnp.array([120, 80, 640, 520]), multimask_outputTrue, )另外如果你要做多轮交互用户点一下、看一下、再补一个点可以把上一轮的logits作为mask_input传进去。注意要先挑一个 logits三个里选一个形状是(1, 256, 256)这样模型能利用上一轮的信息收敛更快。5.4 掩码后处理与结果导出拿到布尔掩码之后导出的格式取决于你的下游工具。要送进标注工具做 COCO 数据集就用 pycocotools 的 RLE 编码体积小、无损。要给人看或者做进一步图像处理存成 PNG 最省事def save_masks(masks, out_dir, prefix): import os os.makedirs(out_dir, exist_okTrue) for i, m in enumerate(masks): png (m.astype(np.uint8)) * 255 cv2.imwrite(f{out_dir}/{prefix}_{i:03d}.png, png)想叠在原图上看效果建议用带透明度的叠加而不是硬贴上纯色块因为边缘的对错在透明叠加下一眼就能看出来def overlay(image, mask, color(0, 255, 0), alpha0.5): out image.copy() layer np.zeros_like(out) layer[mask] color return cv2.addWeighted(out, 1 - alpha, layer, alpha, 0)这里再提醒一个细节cv2.imwrite写 PNG 的时候不会管通道顺序你如果是 RGB 的图直接写颜色会反。稳妥做法是在导出前转回 BGR。6. 全图自动分割Automatic Mask Generator 实战6.1 自动分割和交互分割的区别SamPredictor是你给提示它出结果。SamAutomaticMaskGenerator是你不给提示它把图里所有能分的东西都分出来。后者是很多离线任务的主力工具比如给一批图片做无监督的候选区域提取。它的工作原理是这样的在图像上打一个均匀的点阵每个点当成一个独立的点提示去调模型然后把所有结果按质量过滤、去重最后输出一堆掩码。因为图像嵌入只需要算一次所以真正的耗时大头在 mask decoder 上——点阵有 1024 个点批大小 64 的话要跑 16 批解码。这也是为什么很多人以为自动分割慢在编码器其实不是编码器只跑了一次。6.2 参数逐个讲清楚from segment_anything import SamAutomaticMaskGenerator mask_generator SamAutomaticMaskGenerator( modelsam, points_per_side32, points_per_batch64, pred_iou_thresh0.88, stability_score_thresh0.92, stability_score_offset1.0, box_nms_thresh0.7, crop_n_layers1, crop_nms_thresh0.7, crop_overlap_ratio0.34, crop_n_points_downscale_factor2, min_mask_region_area100, )points_per_side是点阵的边长32 表示 32×32 一共 1024 个点。这是影响结果密度和耗时最直接的参数。调到 64点数变成 4096耗时大约翻四倍小目标能分出来更多但重复掩码也更多需要 NMS 兜住。日常我用 32做精细的小目标提取时用 48 或 64。points_per_batch控制解码阶段一次处理多少个点提示。它跟显存直接相关显存紧张就调到 16 或 32速度慢一点但不会 OOM。pred_iou_thresh是预测 IoU 的过滤阈值低于这个值的掩码会被丢掉。默认 0.88设得太高会漏掉一些边界复杂的目标设得太低会混进大量噪声掩码。stability_score_thresh衡量的是掩码的稳定性做法是把 logits 在阈值 ±offset 的范围内变动看掩码变化的 IoU 有多大。分数越高说明边界越稳定。默认 0.95 偏严格实践中我一般放到 0.90 到 0.92因为有些边缘模糊的目标比如树冠、烟尘稳定性天然就低。box_nms_thresh是框级别的 NMS 阈值。因为同一片区域的多个点会生成高度重叠的掩码需要按 IoU 去重。默认 0.7如果你的目标是尽量多保留不同尺度的层次结构比如既要有整棵树也要有树叶可以调高一点到 0.8。crop_n_layers是裁剪层数用来处理小目标。设成 1 时图片会被裁成若干块每块单独跑一遍点阵所以点数大致翻五倍耗时代价很大。但如果你图里有大量小物体不裁剪的话点阵密度根本覆盖不到。设成 2 会再翻一般不建议超过 2。crop_overlap_ratio是裁剪块之间的重叠比例默认约 0.34是为了避免正好切在物体中间导致漏检。crop_n_points_downscale_factor控制每个裁剪层里点阵的稀疏程度设成 2 表示裁剪层里的点阵密度是原始的一半。min_mask_region_area是后处理过滤把面积小于这个像素数的掩码扔掉。注意这个值必须配合 pycocotools 才有用。6.3 一套可复用的调参起点与其一个个试不如从我常用的两套配置里挑一个改场景points_per_sidecrop_n_layersstability_score_threshmin_mask_region_area说明常规物体提取3200.92100速度快适合主体目标较大的图小目标密集场景4810.8850耗时长适合细碎目标多的图跑完之后每个掩码是一个字典字段包括segmentation布尔掩码、area像素面积、bboxxywh 格式的边界框、predicted_iou、stability_score、point_coords触发这个掩码的点、crop_box。按面积排序取前 N 个或者按 bbox 面积过滤掉过大过小的是最常用的后处理手段masks mask_generator.generate(image) masks sorted(masks, keylambda x: x[area], reverseTrue) keep [m for m in masks if 500 m[area] 200000]批量处理的时候有个实践建议把 mask generator 建一次在循环里反复用不要在循环里重复构造。构造本身开销不大但如果每次都重新加载模型就完了。另外记得处理大图时先把图缩到合理尺寸比如长边不超过 2000否则编码器的 resize 会把细节压掉而且点阵的单个点覆盖的原始像素范围会变得很大小目标全被吞掉。7. 小显存优化8G 卡也能跑起来的几种办法7.1 精度选择和推理模式最简单的降显存手段是控制计算精度。有几个层次torch.inference_mode()比torch.no_grad()更彻底它连中间激活的版本计数都不保存显存和速度都有收益。养成习惯推理代码一律用 inference_mode。半精度是真正能省一半显存的做法。但这里有个坑必须说清楚官方库的SamPredictor.predict内部会把坐标张量固定成torch.float32如果你把模型.half()了权重是 half 而输入是 float32会直接报expected scalar type Half but found Float。要绕过这个可以自己手动构造 prompt 并转换成 half或者干脆不用半精度换个更小的模型版本。更稳妥的半精度方案是走 ONNX 或者 TensorRT把整条链路按半精度导出就没有这种前后 dtype 不一致的问题了。torch.compile在较新的 PyTorch 上对编码器加速效果不错尤其是 ViT 这种规整结构实测有 20% 到 40% 的提速。首次编译会花几十秒适合长驻服务。7.2 分块推理和图像预缩放如果显存实在不够还有一个思路是让输入变小。但要注意SAM 的ResizeLongestSide把长边固定到 1024你如果在外部分块处理每块都要各自补齐到 1024反而会引入更多计算。真正能省显存的分块做法是把大图切成有重叠的小块每块单独跑自动分割最后把所有结果合并、跨块去重。这么做的好处不只是显存——因为每块在图内被放大到 1024等效于提高了局部的分辨率小目标的检出率会明显提升。代价是耗时和重复结果增多。重叠比例建议设成块尺寸的 10% 到 20%太小会漏掉跨边界的物体太大会产生大量重复。跨块去重可以用掩码的 IoU 做也可以用 bbox 的 NMS后者更快。还有一个常被忽略的省显存点处理完一张图之后把中间变量显式释放。Python 的垃圾回收在显存上不一定及时循环里处理大批图片的时候容易累积泄漏import gc import torch for path in image_paths: with torch.inference_mode(): predictor.set_image(load(path)) ... del predictor.features gc.collect() torch.cuda.empty_cache()torch.cuda.empty_cache()会释放缓存分配器持有的空闲块频繁调用会有一点性能代价但在批处理循环里每张图调一次是可以接受的。7.3 导出 ONNX 做工程化部署如果你要把它做成一个服务PyTorch 原生推理的启动开销和依赖体积都偏大。导出 ONNX 能解决两个问题一是可以脱离 Python 环境部署二是能用 ONNX Runtime 或者 TensorRT 做更激进的图优化。编码器最容易导出因为它的输入尺寸是固定的dummy torch.randn(1, 3, 1024, 1024, deviceDEVICE) torch.onnx.export( sam.image_encoder, dummy, sam_vit_b_encoder.onnx, input_names[input], output_names[embedding], opset_version17, )解码器导出麻烦一些因为它内部有对提示数量和类型的动态处理。常见做法是把点提示和解码器绑成一个固定接口比如最多 8 个点 1 个框这样形状就固定了导出会顺利很多。导出之后有个必须注意的点预处理和后处理要自己实现不能依赖原库。具体来说你要自己写 resize 长边到 1024、padding 到 1024×1024、归一化减均值除以标准差均值是 123.675/116.28/103.53标准差是 58.395/57.12/57.375、以及输出的掩码上采样回原尺寸。这部分逻辑如果写错输出会是一堆没意义的噪声而且不报错。建议先用同一张图对比 PyTorch 和 ONNX 的输出确认数值接近一般允许 1e-3 量级的差异再上线。7.4 显存实测对照表把前面这些优化手段的效果整理成一张表方便你按自己的硬件直接对号入座配置模型精度峰值显存单图编码耗时基准ViT-Hfp32约 7GB约 1.0s降精度ViT-Hfp16 导出约 3.8GB约 0.6s换小模型ViT-Bfp32约 2.8GB约 0.25s编译加速ViT-Hfp32 compile约 7GB约 0.65s轻量方案MobileSAMfp32约 1GB约 0.08s从这张表能看出来最有效的降显存手段其实是换模型版本而不是各种优化技巧。8G 卡上想跑 ViT-H 做自动分割与其折腾半精度不如老老实实用 ViT-B省下的时间可以做别的事。8. 踩坑记录与排查速查8.1 安装和运行期的典型报错下面这几个报错我在不同机器上都遇到过按出现频率排序。Could not run torchvision::nms with arguments from the CUDA backend出现在自动掩码生成器做 NMS 的时候。原因是 torch 和 torchvision 版本不匹配或者 torchvision 装成了 CPU 版。解决办法是卸载重装两者用同一条命令一起装让 pip 自己解析依赖。CUDA out of memory先看是什么阶段 OOM。如果是set_image就炸说明编码器太大退到 ViT-B。如果是生成掩码时炸先把points_per_batch从 64 降到 16。如果是处理了一批图之后才炸大概率是没释放缓存加上前面说的del和empty_cache。RuntimeError: input type (torch.cuda.FloatTensor) and weight type (torch.cuda.HalfTensor) should be the same模型用了 half 但输入还是 float32。要么统一精度要么别用 half。分割结果是一团噪声或者全空九成是图像格式问题。检查三件事是不是 RGB、是不是 uint8、取值范围是不是 0 到 255。用print(image.shape, image.dtype, image.min(), image.max())三秒钟定位。ImportError: pycocotoolsmin_mask_region_area不为 0 但没装 pycocotools。装一下或者把参数设成 0。8.2 分割质量不达预期的排查顺序遇到模型分出来的东西不对的时候别急着怀疑模型按这个顺序查一遍基本能定位到原因。第一步看提示。点打在哪了有没有落在背景上坐标的 x y 有没有写反框是不是把目标切掉了一半我遇到过好几次是框的下边界正好压在物体底部导致模型只能看到半个物体出来的掩码自然也是半个。第二步看输入。颜色通道对不对缩放是不是做了两次padding 有没有问题。有一个很隐蔽的情况是图片本身带 EXIF 旋转信息PIL 读的时候会自动应用旋转OpenCV 不会两边处理同一张图会得到不同朝向的结果提示坐标就全错位了。第三步换提示类型。点提示效果差的时候试试框框提示效果差的时候加上正负点。同一个目标不同提示方式的结果差异可能很大。第四步换模型版本。如果 ViT-B 效果明显不行换 ViT-H 试一下能区分是模型能力不够还是数据本身有歧义。第五步才是考虑领域适配。如果确认是数据分布差异导致的比如遥感影像、显微镜图像、工业 X 光片那就得上微调了零样本能力在这类数据上就是有限的。8.3 常用操作速查表问题快速处理想确认 CUDA 是否可用python -c import torch;print(torch.cuda.is_available())生成掩码时 OOMpoints_per_batch降到 16或points_per_side降到 16小目标分不出来开crop_n_layers1或points_per_side提到 48掩码太碎太多min_mask_region_area提到 200stability_score_thresh提到 0.93层次结构丢太多box_nms_thresh提到 0.8或stability_score_thresh降到 0.85批量处理崩在中间循环里加del featuresempty_cache结果和 PyTorch 对不上ONNX检查预处理归一化的均值和标准差9. 落地场景从标注提效到耕地识别9.1 数据标注流水线里的用法这是 SAM 目前最成熟、最容易见效的用法。流程大致是这样用SamAutomaticMaskGenerator对一批图片做粗分割得到一堆候选掩码再按业务规则筛选比如面积范围、长宽比、位置然后把筛选后的掩码转成标注工具能读的格式COCO JSON 或者 LabelMe JSON给标注员做初稿人工只做修正不做从零开始描边。我们实际测下来效率提升的幅度取决于场景。轮廓规整、对比度高的目标比如瓶子、箱子、车辆提升最明显标注速度能到原来的三到五倍边缘极不规则的比如树叶、裂隙提升有限因为人工修边的时间跟重画差不了太多。有几个工程细节值得注意。自动分割输出的掩码数量经常远超实际需要一定要做过滤不然标注员面对几百个碎片会崩溃。另外掩码是像素级的转成多边形的时候要简化顶点不然一个掩码几千个点标注工具直接卡死。OpenCV 的approxPolyDP是常用的简化手段epsilon设成周长的 0.5% 到 1% 比较合适。9.2 遥感与耕地识别里的用法和局限耕地识别这个方向我做过一段时间可以聊聊实际感受。遥感影像和自然图像的差异很大主要表现为分辨率跨度大、目标尺度差异悬殊、季节和光照变化剧烈、纹理特征比形状特征更重要。SAM 在自然图像上预训练的到了遥感上零样本效果会明显打折。比较靠谱的做法是把 SAM 当成候选区域生成器而不是分类器。先用自动分割把影像切成一块块形状规整的图斑然后对这些图斑提取特征——光谱特征、时序 NDVI 曲线、纹理统计量——再送进一个轻量的分类模型或者走规则判断判断哪些图斑是耕地。这条链路里 SAM 只负责划边界它的强项刚好在这里弱项识别语义类别交给后面的环节各司其职。局限也要说清楚。一是 SAM 对地块边界的分割精度受影像分辨率影响很大分辨率低的时候边界会很糊二是相邻同类地块之间的田埂、小路SAM 经常识别不出来会把两块地连成一片这对面积统计是致命的三是无纹理的大面积区域比如水面、裸地容易分出一堆无意义的碎片。所以整个流程后面必须有人工抽检环节。还有一个方向是利用时序信息。同一块地在不同季节的影像用 SAM 分别分割再取交集可以过滤掉很多季节性的干扰比如作物生长导致的地块内部变化让边界更稳定。9.3 从零样本到微调什么情况下该动手大部分场景下零样本的 SAM 已经够用。但如果你确认是数据分布差异导致的效果落后微调是值得投入的。微调的思路通常是冻结图像编码器只训 Mask Decoder因为编码器占了绝大部分参数全量微调显存和时间都吃不消而且容易在小数据集上过拟合。冻结编码器之后可训练参数只有几百万单卡就能跑几百到几千张标注图就能看到明显改善。训练数据的构造方式跟传统分割不太一样你需要的是图像 提示 目标掩码这样的三元组提示可以是从真实掩码里随机采样的点也可以是从掩码算出来的框。这样训练出来的模型才跟推理时的输入形式一致。还有一种更省事的适配方式是在输入侧做文章比如对遥感影像做增强对比度拉伸、波段组合让输入更接近自然图像的统计分布。这种办法没有任何训练成本效果不确定但值得先试一下毕竟成本几乎为零。回过头看SAM 本地部署这件事的技术门槛其实不高——真正花时间的是环境对齐和参数梳理这两块代码本身很短。一旦跑通它能提供的东西远超一个分割模型该有的价值本质上它是一个可编程的图像理解原语。我自己的体会是把它当成流水线里的一个通用零件而不是一个端到端的解决方案用起来会顺很多。
返回列表