☰
全景图地物分类实战:SAM-DINO-CLIP组合模型从环境搭建到参数调优
2026/10/7 5:57:31 网站建设 项目流程

简介:这份资源围绕SAM、Grounding DINO与CLIP组合模型展开,面向计算机、人工智能、通信、自动化等专业的在校学生、教师及企业开发者,解决高分辨率全景影像场景下的地物分类与实例分割问题。项目在SAM基础上融合Grounding DINO和Clipseg生成的矩形框与掩码信息,通过自定义文本提示实现“分割一切”的自动分类效果,适合作为毕设、课程设计、作业或项目立项演示,也便于进阶学习者二次修改。压缩包共68个文件,约3.86MB,以54个Python源码为核心,辅以C++、CUDA与头文件用于模型推理加速,另含png、jpg示例图及README说明文档,目录涵盖segment_anyting、groundingdino、utils等模块,结构清晰。目前已有146人学习下载。代码经测试可运行,下载后可按README指引快速上手,理解文本提示驱动分割的完整流程与排错思路。

1. 全景图地物分类为什么值得用 SAM-DINO-CLIP 组合来做

拿到一张 360 度全景图,想把里面的天空、建筑、道路、植被、水体逐类抠出来,单靠一个模型往往顾此失彼。纯分割模型(比如 YOLO 实例分割)对训练集里没见过的地物类别直接哑火;纯开放词汇检测(比如 Grounding DINO)能靠文本提示找到目标,但边界粗糙、掩码质量差;CLIP 分类能力强,却天生不做像素级定位。把 SAM、DINO、CLIP 三个模型串起来,正好互补:DINO 负责「哪里有目标、是什么」,SAM 负责「目标的精确轮廓」,CLIP 负责「给每个掩码打上语义标签」。这套组合在遥感、街景、室内全景等场景里,是当前不依赖大量标注就能跑出可用结果的常见路线。这篇笔记就按「环境怎么搭 → 三个模型怎么串 → 全景图畸变怎么处理 → 参数怎么调 → 坑在哪」的顺序,把一条能复现的 Python 流水线讲清楚,适合已经会装 Python、想快速验证这套组合拳的从业者。

2. 环境搭建与三个模型的加载顺序

2.1 依赖清单与版本约束

这套组合对版本比较敏感,尤其是 PyTorch 和 CUDA 的匹配。我一般用 Python 3.8 到 3.10 之间的版本,太新的 Python 3.12 在部分 torch 轮子上会翻车。核心依赖如下表,装之前先确认显卡驱动支持的 CUDA 版本,再决定装哪个 torch 轮子。

依赖作用版本建议
torch / torchvision三个模型的推理底座与 CUDA 匹配的 2.x
segment-anythingSAM 掩码生成官方仓库 pip 安装
transformersDINO、CLIP 加载4.35 以上
opencv-python全景图读写与畸变处理4.8 以上
numpy数组运算1.24 左右
pillow图像格式转换任意稳定版

安装命令按顺序执行,先装 torch 再装其余,避免依赖解析把 torch 降级:

# 先按自己 CUDA 版本装 torch,这里以 cu118 为例 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 再装其余依赖 pip install opencv-python numpy pillow transformers pip install git+https://github.com/facebookresearch/segment-anything.git

逻辑说明:torch 单独指定 index-url 是为了拿到带 CUDA 的轮子,如果直接pip install torch很可能装成 CPU 版,后面推理慢到怀疑人生。segment-anything 官方没上 PyPI 主源,用 git 方式装。参数上,--index-url换成你驱动对应的版本即可,cu118、cu121 都常见。

2.2 三个模型的权重加载与显存分配

三个模型同时驻留显存,8G 卡会比较紧张。我的做法是 SAM 用 ViT-B 而不是 ViT-H,DINO 用 tiny 版本,CLIP 用 ViT-B/32。加载顺序上先加载 DINO 和 CLIP(常驻),SAM 按需调用。

import torch from transformers import AutoProcessor, AutoModelForZeroShotObjectDetection from transformers import CLIPModel, CLIPProcessor from segment_anything import sam_model_registry, SamPredictor device = "cuda" if torch.cuda.is_available() else "cpu" # DINO:开放词汇检测,输入图像+文本提示,输出框 dino_id = "IDEA-Research/grounding-dino-tiny" dino_processor = AutoProcessor.from_pretrained(dino_id) dino_model = AutoModelForZeroShotObjectDetection.from_pretrained(dino_id).to(device) # CLIP:给掩码打语义标签 clip_id = "openai/clip-vit-base-patch32" clip_model = CLIPModel.from_pretrained(clip_id).to(device) clip_processor = CLIPProcessor.from_pretrained(clip_id) # SAM:根据框生成精细掩码,ViT-B 省显存 sam = sam_model_registry["vit_b"](checkpoint="sam_vit_b.pth").to(device) sam_predictor = SamPredictor(sam)

逻辑说明:DINO 的文本提示用点号分隔多个类别,比如"building. road. tree. water.",末尾的点号不能省,这是它的输入格式要求。CLIP 的标签集要和 DINO 的提示保持一致,否则掩码和标签对不上。SAM 的 checkpoint 需要自己下载对应权重文件,ViT-B 约 375MB,ViT-H 超过 2G,显存不够就老老实实用 B。参数上,sam_model_registry的键必须是vit_b、vit_l、vit_h之一,写错直接报 KeyError。

3. 从全景图到地物掩码:DINO 检测与 SAM 分割的串联

3.1 用 DINO 拿到候选框和类别

DINO 的输入是 RGB 图像加一段文本提示,输出是若干带分数的框。全景图分辨率通常很高(比如 4096×2048),直接喂进去显存吃不消,常见做法是先缩放到长边 1024 再检测,框坐标按比例还原。

import cv2 import numpy as np def detect_with_dino(image_bgr, text_prompt, box_threshold=0.3, text_threshold=0.25): image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB) inputs = dino_processor(images=image_rgb, text=text_prompt, return_tensors="pt").to(device) with torch.no_grad(): outputs = dino_model(**inputs) # 后处理:把输出转成框+标签+分数 results = dino_processor.post_process_grounded_object_detection( outputs, inputs.input_ids, box_threshold=box_threshold, text_threshold=text_threshold, target_sizes=[image_rgb.shape[:2]] )[0] return results["boxes"], results["labels"], results["scores"]

逻辑说明:box_threshold控制框的置信度门槛,调高漏检变多、误检变少;text_threshold控制文本匹配的严格程度,类别词写得越具体越要调高。全景图里「天空」这种大面积类别,框往往很大,阈值可以适当放低到 0.25;「行人」这种小目标反而要提到 0.35 以上。返回的 boxes 是 xyxy 格式的 tensor,labels 是字符串列表。

3.2 用 SAM 把框变成像素级掩码

拿到框之后,把每个框作为 prompt 喂给 SAM,得到该框内的精细掩码。SAM 的SamPredictor需要先set_image一次,之后可以对多个框复用图像特征,这是省时间的关键。

def boxes_to_masks(image_bgr, boxes): image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB) sam_predictor.set_image(image_rgb) # 图像特征只算一次 masks = [] for box in boxes: box_np = box.cpu().numpy() # multimask_output=True 会返回3个候选掩码,取分数最高的 m, scores, _ = sam_predictor.predict( box=box_np, multimask_output=True ) best = m[np.argmax(scores)] masks.append(best) return masks

逻辑说明:set_image是整条流水线里最耗时的一步,全景图缩放到 1024 后大约 1 到 2 秒,之后每个框的 predict 只要几十毫秒。multimask_output=True会给出整体、局部、细节三个候选,通常取分数最高的那个,但遇到细长地物(比如道路)时,分数最高的未必是边界最贴合的,可以按面积筛。参数上,box 必须是 numpy 的 xyxy 格式,传 tensor 会报类型错误。

3.3 用 CLIP 给掩码打语义标签

SAM 只给掩码,不给类别。把每个掩码区域裁剪出来,用 CLIP 在候选类别文本里做零样本分类,取相似度最高的作为标签。

def label_masks_with_clip(image_bgr, masks, candidate_labels): image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB) text_inputs = clip_processor( text=[f"a photo of {c}" for c in candidate_labels], return_tensors="pt", padding=True ).to(device) with torch.no_grad(): text_feat = clip_model.get_text_features(**text_inputs) text_feat = text_feat / text_feat.norm(dim=-1, keepdim=True) labeled = [] for mask in masks: ys, xs = np.where(mask) if len(ys) == 0: continue crop = image_rgb[ys.min():ys.max()+1, xs.min():xs.max()+1] crop_input = clip_processor(images=crop, return_tensors="pt").to(device) with torch.no_grad(): img_feat = clip_model.get_image_features(**crop_input) img_feat = img_feat / img_feat.norm(dim=-1, keepdim=True) sim = (img_feat @ text_feat.T).squeeze(0) labeled.append((candidate_labels[int(sim.argmax())], float(sim.max()))) return labeled

逻辑说明:文本模板"a photo of {c}"是 CLIP 零样本分类的常用写法,比裸类别词效果好。特征都做了 L2 归一化,点积即余弦相似度。裁剪时用掩码的包围盒而不是掩码本身,是为了避免把背景像素混进去,代价是包围盒内可能包含其他类别,所以相似度低于 0.2 的结果建议人工复核。参数上,candidate_labels要和 DINO 的文本提示对齐,多一个少一个都会让标签体系混乱。

4. 全景图畸变处理与坐标还原的避坑

4.1 等距柱状投影带来的形变

全景图常见格式是等距柱状投影(equirectangular),越靠近上下两极,水平方向被拉伸得越厉害。直接在上面跑 DINO,两极附近的物体会被拉成横向长条,检测框严重失真。常见做法有两种:一是把全景图切成若干透视投影的小图分别检测,再拼回球面;二是接受形变,但在后处理时对两极区域的框做宽度压缩。前者精度高但工程量大,后者快但两极精度差。我一般先用第二种快速验证,效果不够再上切图方案。

4.2 切图检测与掩码拼接的坐标换算

切图方案的核心是把等距柱状图按经度切成 N 份,每份做透视投影,检测完再逆变换回原图坐标。这里最容易翻车的是坐标换算,经度方向要按 360/N 度偏移,纬度方向要按视场角换算。

def equirect_to_perspective(img, fov_deg, theta_deg, phi_deg, out_size=1024): # theta: 经度, phi: 纬度, fov: 视场角 h, w = img.shape[:2] fov = np.deg2rad(fov_deg) theta = np.deg2rad(theta_deg) phi = np.deg2rad(phi_deg) f = 0.5 * out_size / np.tan(fov / 2) # 生成透视平面上的像素网格 x, y = np.meshgrid(np.arange(out_size) - out_size/2, np.arange(out_size) - out_size/2) z = np.full_like(x, f, dtype=np.float32) # 旋转到球面方向 xyz = np.stack([x, y, z], axis=-1).astype(np.float32) # 绕 y 轴转 theta,绕 x 轴转 phi Ry = np.array([[np.cos(theta),0,np.sin(theta)],[0,1,0],[-np.sin(theta),0,np.cos(theta)]]) Rx = np.array([[1,0,0],[0,np.cos(phi),-np.sin(phi)],[0,np.sin(phi),np.cos(phi)]]) xyz = xyz @ Ry.T @ Rx.T lon = np.arctan2(xyz[...,0], xyz[...,2]) lat = np.arcsin(xyz[...,1] / np.linalg.norm(xyz, axis=-1)) u = ((lon / (2*np.pi) + 0.5) * w).astype(np.float32) v = ((0.5 - lat / np.pi) * h).astype(np.float32) return cv2.remap(img, u, v, cv2.INTER_LINEAR)

逻辑说明:这段把等距柱状图上的任意经纬度中心、任意视场角,投影成一张透视小图。fov_deg一般取 90,太小拼接缝多,太大边缘畸变重。theta_deg按 360/N 步进遍历一圈,phi_deg取 0 覆盖赤道带,两极单独处理。逆变换时把检测框的四个角点用同样的球面公式反算回原图坐标,注意经度跨越 0/360 边界时要加偏移修正,否则框会横跨整张图。

4.3 掩码回贴时的插值选择

透视小图上得到的掩码,回贴到等距柱状图时要用最近邻插值,不能用双线性。因为掩码是 0/1 二值,双线性会在边界产生 0.5 之类的中间值,二值化后边界锯齿反而更严重。回贴后相邻小图的掩码在接缝处会有重叠或空隙,重叠取并集,空隙用形态学闭运算补一下,闭运算核大小取小图重叠像素数的两倍左右。

5. 参数调优与常见问题排查

5.1 三个模型的关键参数怎么定

参数没有万能值,但有一套起手式。DINO 的box_threshold从 0.3 起调,text_threshold从 0.25 起调;SAM 的multimask_output建议开,靠分数筛;CLIP 的相似度阈值 0.2 以下的结果直接丢。全景图切图时fov_deg取 90、切 4 到 6 份是精度和耗时的平衡点。显存不够时优先降 SAM 的 ViT 等级,其次降输入分辨率,最后才考虑把 DINO 换成更小的版本。

5.2 检测框漂移与掩码错位

现象:SAM 生成的掩码明显偏离 DINO 给的框,或者掩码只覆盖了目标的一半。原因通常是 DINO 的框本身就偏,或者图像缩放后坐标没还原。解决:先可视化 DINO 的原始框,确认框准不准;如果框准而掩码偏,检查set_image用的图像和predict时是不是同一张、同一尺寸。缩放还原时注意target_sizes要传原图尺寸,不是缩放后的尺寸。

5.3 类别标签张冠李戴

现象:明明是建筑,CLIP 却打成道路。原因多半是裁剪区域包含了大量背景,或者候选类别词太相近。解决:裁剪时用掩码做背景抑制,把掩码外的像素置灰再送 CLIP;候选类别词之间语义距离要拉开,比如「道路」和「人行道」在全景图里容易混,可以合并成一个「路面」类别。

5.4 全景图接缝处目标被切断

现象:一栋建筑正好跨在两张切图的接缝上,两张图各检测到一半,回贴后掩码断裂。原因:切图时没有重叠。解决:相邻切图之间保留 10% 到 15% 的重叠视场角,回贴时对重叠区域的同类掩码做并集,跨缝目标就能接上。重叠比例太高会拖慢速度,10% 是常用起点。

5.5 显存溢出与推理变慢

现象:跑几张图后显存爆掉,或者越跑越慢。原因:SAM 的set_image特征没释放,或者 DINO 的中间张量累积。解决:每张图处理完手动torch.cuda.empty_cache(),SAM 的 predictor 在换图时重新set_image而不是复用。批量处理时把图像列表分块,每块处理完清一次缓存。如果还是不够,把 DINO 和 CLIP 的推理放到torch.no_grad()里,这个必须加,否则显存占用翻倍。

6. 让组合模型跑得更稳的两个进阶技巧

第一个技巧是提示词工程。DINO 对文本提示的措辞很敏感,同一类地物换个说法,召回率能差一截。我的习惯是给每个类别准备三到五个同义提示,比如建筑用"building. house. skyscraper.",跑一遍取并集再去重。去重时用框的 IoU 做非极大值抑制,IoU 阈值 0.5 左右。这一步能让漏检明显下降,代价是推理次数翻几倍,适合对召回要求高的场景。

第二个技巧是用 CLIP 的相似度做二次过滤。DINO 给的框里难免有误检,把每个框裁剪出来送 CLIP,如果最高相似度低于 0.15,基本可以判定是误检,直接丢掉。这个阈值不要设太高,0.2 以上会误杀小目标。我一般把 DINO 分数和 CLIP 相似度做一个加权,权重各 0.5,综合分低于 0.25 的框丢弃,实测比单看 DINO 分数稳。

验证这套流水线是否靠谱,别只看单张图。挑十张覆盖不同光照、不同地物密度的全景图,人工标一遍主要地物的掩码,算一下 mIoU 和分类准确率。mIoU 能到 0.5 以上、分类准确率 0.7 以上,这套组合就算能用了。达不到就回头查是检测漏了还是掩码偏了,分开定位比整体调参高效得多。

我自己踩得最狠的一次坑,是忘了给 DINO 的文本提示加末尾点号,结果模型把整句话当成一个类别,输出全是空框,排查了半天以为是权重没加载对。从那以后我养成了一个习惯:任何开放词汇模型的文本输入,先拿一张最简单的图跑通再上真实数据。这套 SAM-DINO-CLIP 组合不是银弹,全景图畸变和显存是两个绕不开的坎,但把切图重叠、坐标还原、显存释放这三件事做扎实,它确实能在不标数据的前提下跑出能看的结果。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询