ARTICLE DETAIL

资讯详情

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

全景图地物分类实战:SAM-DINO-CLIP组合模型Python流水线搭建与调参

全景图地物分类实战:SAM-DINO-CLIP组合模型Python流水线搭建与调参 简介本资源面向计算机、人工智能、遥感与测绘等方向的在校学生、教师及企业研发人员提供一套基于SAM、Grounding DINO与CLIP组合模型的高分辨率全景图地物分类与实例分割完整实现。项目通过自定义文本提示驱动实现影像中任意类别目标的自动分割适合作为毕业设计、课程设计、项目立项演示或进阶学习案例。压缩包共68个文件约3.86MB以54个Python源码为核心辅以C与CUDA加速文件、模型头文件及少量示例图片并附README说明文档便于快速理解整体流程。目前已有146人学习下载。代码经过实际运行验证涵盖模型加载、推理、自动掩码生成与工具函数等模块读者可据此掌握组合模型调用方式、文本提示分割逻辑与结果可视化方法并在此基础上修改扩展完成自己的地物分类与实例分割任务。1. 全景图地物分类为什么让单模型集体翻车拼接好的全景图动辄上万像素宽同一张图里既有大面积天空、道路也有细窄的栏杆、电线、路牌。拿单个检测或分割模型直接跑常见结果是大目标框得挺准小目标漏一片换个场景光照一变类别又串了。这不是模型不行而是单一骨干的表达能力和提示方式撑不住这种尺度跨度。SAM-DINO-CLIP 组合模型就是冲着这个痛点来的DINO 负责把候选区域提出来SAM 负责把掩码抠干净CLIP 负责给每个掩码贴语义标签。三者各干各擅长的事拼成一条「检测—分割—分类」流水线。这篇笔记讲的就是这条流水线在 Python 里怎么搭、参数怎么调、坑在哪适合已经会跑通单个模型、想把它落到全景图这类复杂场景的从业者。下面所有代码都是可复现的最小骨架不依赖某个特定仓库。2. 三个模型各管一段SAM、DINO、CLIP 的职责边界2.1 为什么不是用一个模型端到端搞定端到端的分割模型比如各类 mask transformer在全景图上会遇到两个硬约束。第一全景图分辨率太高直接缩放到模型输入尺寸小地物就剩几个像素特征还没提取就糊了。第二语义类别一旦要扩展就得重新标注、重新训练成本极高。组合模型的思路是把「在哪里」和「是什么」解耦。DINO 这类开放词汇检测器先给出候选框它不关心最终类别名只负责「这里有东西」。SAM 拿到框或点提示后输出精细掩码它也不关心语义。CLIP 最后拿掩码裁剪出的小图去和文本提示做匹配输出类别。这样加一个新类别只需要改 CLIP 的文本提示词不用重训检测和分割。常见做法是DINO 出框 → SAM 出掩码 → 用掩码从原图抠出区域 → CLIP 对区域分类 → 把类别回填到掩码上。整条链路里DINO 和 SAM 是重计算CLIP 是轻量匹配所以工程上要把前两步的显存和耗时控制住。2.2 最小可跑通的 Python 骨架下面这段代码演示的是流水线的组织方式模型加载部分用占位接口表示实际替换成你环境里对应的推理封装即可。重点看数据怎么在三个模型之间流转。import numpy as np import cv2 class Pipeline: def __init__(self, detector, segmenter, classifier): # detector: 输入原图输出 N 个框 [x1,y1,x2,y2] # segmenter: 输入原图框输出 N 个二值掩码 # classifier: 输入裁剪区域列表输出每个区域的类别和分数 self.detector detector self.segmenter segmenter self.classifier classifier def run(self, image, text_prompts, box_thresh0.35): boxes self.detector(image, threshbox_thresh) if len(boxes) 0: return [] masks self.segmenter(image, boxes) crops [] for m in masks: ys, xs np.where(m 0) if len(xs) 16: # 过滤过小掩码避免噪声区域进分类器 crops.append(None) continue x1, x2 xs.min(), xs.max() y1, y2 ys.min(), ys.max() crop image[y1:y2, x1:x2].copy() # 用掩码把背景置零只保留目标像素减少 CLIP 误判 crop[m[y1:y2, x1:x2] 0] 0 crops.append(crop) valid [c for c in crops if c is not None] labels self.classifier(valid, text_prompts) results, idx [], 0 for i, c in enumerate(crops): if c is None: continue results.append({mask: masks[i], label: labels[idx]}) idx 1 return results逻辑说明box_thresh控制 DINO 的召回调低会多出框但误检增加掩码面积小于 16 像素直接丢弃这是全景图里过滤碎斑的关键一步裁剪时用掩码把背景置零是因为 CLIP 对背景纹理很敏感不抠干净容易把「道路」判成「停车场」。参数说明text_prompts是类别文本列表建议写成「a photo of a {类别}」这种模板CLIP 对完整句式的匹配比单词稳定。box_thresh在全景图上一般从 0.3 起调太高会漏小目标太低会引入大量背景框拖慢 SAM。2.3 分辨率与切片的取舍全景图不能整张喂进去。常见做法是滑窗切片窗口之间留重叠推理完再把掩码拼回去。窗口大小和重叠率直接决定小目标能不能被检出。参数建议范围影响窗口边长10241536太小丢上下文太大显存吃紧重叠率0.20.3太低边缘目标被切断太高重复计算缩放比例0.51.0缩太多小目标消失不缩显存翻倍切片后每个窗口独立跑流水线拼接时对重叠区的掩码做非极大值抑制或按置信度取优。这一步不做接缝处会出现重复框和断裂掩码。3. 把 DINO 的框喂给 SAM提示工程与掩码后处理3.1 框提示和点提示怎么选SAM 支持框、点、掩码三种提示。DINO 给的是框所以最自然的是框提示。但框提示在细长目标电线、栏杆上容易把周围背景一起框进去掩码边缘发虚。我一般会做一次「框 中心点」的双提示用框定大致范围再取框中心作为正点提示让 SAM 更聚焦。如果目标特别细可以在框内均匀撒几个正点但点数别超过 4 个多了反而让 SAM 分不清主次。def build_prompts(box, num_points1): x1, y1, x2, y2 box cx, cy (x1 x2) / 2, (y1 y2) / 2 points [(cx, cy)] if num_points 1: # 在框内按网格补点覆盖细长目标 for fx in np.linspace(0.25, 0.75, num_points): for fy in np.linspace(0.25, 0.75, num_points): points.append((x1 (x2 - x1) * fx, y1 (y2 - y1) * fy)) return {box: box, points: points[:4]}逻辑说明中心点作为强正提示网格点作为补充。num_points控制网格密度细长目标用 2普通目标用 1 就够。点数上限 4 是经验值超过后 SAM 的注意力会分散到背景。3.2 掩码后处理三个必做动作SAM 输出的原始掩码不能直接用全景图场景下至少要做三件事。第一按面积过滤。小于阈值的掩码多半是纹理噪声直接丢。第二做形态学闭运算填补掩码内部的小孔洞尤其是道路、水面这类大面积目标。第三对掩码边界做一次平滑减少锯齿。def postprocess_mask(mask, min_area64, kernel_size5): mask mask.astype(np.uint8) kernel np.ones((kernel_size, kernel_size), np.uint8) mask cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel) num, labels, stats, _ cv2.connectedComponentsWithStats(mask) clean np.zeros_like(mask) for i in range(1, num): if stats[i, cv2.CC_STAT_AREA] min_area: clean[labels i] 1 return clean逻辑说明闭运算的核大小kernel_size根据目标尺度调小目标用 3大目标用 7 以上。连通域分析把碎块合并或剔除min_area是面积门槛全景图里建议不低于 64 像素。参数说明min_area调大会丢小地物调小会留噪声需要结合你的地物清单试。闭运算核太大会把相邻目标粘在一起这是最常见的翻车点。3.3 掩码与框的对应关系维护DINO 出的框和 SAM 出的掩码是一一对应的但经过面积过滤后部分掩码被丢弃对应关系就断了。工程上要维护一个索引映射不能简单按顺序取。def align_results(boxes, masks, min_area64): aligned [] for box, mask in zip(boxes, masks): clean postprocess_mask(mask, min_areamin_area) if clean.sum() 0: continue aligned.append({box: box, mask: clean}) return aligned逻辑说明在同一个循环里做过滤和配对避免先过滤再对齐导致的索引错位。返回的列表里每个元素都同时带框和掩码后续 CLIP 分类直接用掩码裁剪。4. CLIP 分类提示词设计与置信度校准4.1 提示词模板决定分类上限CLIP 的分类本质是图文匹配提示词写得好不好直接决定准确率。全景图地物类别通常包括建筑、道路、植被、水体、车辆、行人等每类都要给一个稳定的文本描述。常见做法是用模板「a satellite/aerial photo of {类别}」。如果全景图是地面视角就换成「a street view photo of {类别}」。模板不统一CLIP 的分数分布会漂移同一类在不同模板下分数差很多。TEMPLATES [ a photo of a {}, an aerial photo of a {}, a street view photo of a {}, ] def build_text_features(class_names, templatesTEMPLATES): prompts [] for name in class_names: for t in templates: prompts.append(t.format(name)) return prompts # 送入 CLIP 文本编码器逻辑说明多模板集成能提升鲁棒性每个类别生成多个提示编码后取平均作为该类别的文本特征。模板数量控制在 35 个太多收益递减还增加计算。参数说明类别名用英文CLIP 的英文语料远多于中文中文提示需要额外的中文 CLIP 模型。类别名要具体「vehicle」不如「car」和「truck」分开。4.2 分数校准与阈值设定CLIP 输出的相似度分数不是概率直接取最大值容易在相似类别间摇摆比如「道路」和「停车场」。常见做法是做 softmax 归一化再设一个最低置信度阈值低于阈值的区域标为「未知」。def classify(crops, text_features, clip_model, min_score0.25): results [] for crop in crops: img_feat clip_model.encode_image(crop) sims img_feat text_features.T probs softmax(sims) best probs.argmax() if probs[best] min_score: results.append((unknown, probs[best])) else: results.append((best, probs[best])) return results逻辑说明min_score是置信度门槛低于它说明 CLIP 也拿不准强行给标签不如标未知后续人工复核。softmax 的温度系数可以调温度低会让分数更尖锐适合类别区分度高的场景。参数说明min_score从 0.2 起调太高会大量标未知太低会引入错标。全景图里建议配合人工抽检确定这个值。4.3 类别不平衡的处理全景图里天空、道路这类大类别占比极高小类别路牌、井盖样本少CLIP 容易把少见类别判成常见类别。缓解办法有两个一是给小类别多写几个提示模板增加它的文本特征覆盖二是在后处理阶段对稀有类别降低阈值宁可多召回再人工筛。5. 避坑与排查全景图流水线的五条血泪经验5.1 现象小目标整片漏检大目标正常原因DINO 的输入分辨率被压得太低小目标在特征图上只剩一两个像素。解决提高切片分辨率或降低缩放比例同时把box_thresh从 0.35 降到 0.25让 DINO 多出框再靠 SAM 和 CLIP 过滤误检。5.2 现象掩码边缘糊成一片相邻目标粘连原因SAM 的框提示太大把多个目标框进同一个提示里或者闭运算核开得太大。解决缩小框的 padding框提示不要外扩闭运算核从 3 开始试别一上来就用 7。5.3 现象CLIP 把道路判成停车场分数还很接近原因两个类别的文本提示太相似或者裁剪区域带了太多背景。解决提示词里加入区分性描述比如「a photo of a road with lane markings」裁剪时严格用掩码抠背景背景置零。5.4 现象拼接后接缝处出现重复框和断裂掩码原因滑窗重叠区的结果没有做融合每个窗口独立输出。解决重叠区按掩码 IoU 做非极大值抑制或者按 CLIP 置信度取优保留分数高的那个。5.5 现象显存爆掉跑到一半 OOM原因SAM 和 DINO 同时驻留显存加上全景图切片批量推理。解决把检测和分割分阶段跑DINO 出完框后释放检测模型显存再加载 SAM或者用半精度推理显存能省近一半。6. 进阶用掩码回填做全景图语义地图把流水线跑通后最有价值的产出是一张带类别标签的全景语义图。做法是把所有掩码按类别着色叠加到原图上再按地物类别统计面积占比。这个统计结果可以直接用于后续的变化检测或规划分析。def render_semantic_map(image, results, color_map): overlay image.copy() for r in results: color color_map.get(r[label], (128, 128, 128)) overlay[r[mask] 0] color blended cv2.addWeighted(image, 0.6, overlay, 0.4, 0) return blended def area_stats(results, pixel_to_meter0.1): stats {} for r in results: area r[mask].sum() * (pixel_to_meter ** 2) stats[r[label]] stats.get(r[label], 0) area return stats逻辑说明color_map是类别到颜色的映射未知类别统一灰色。pixel_to_meter是像素到实际面积的换算系数取决于你的全景图分辨率和拍摄高度这个值不准面积统计就没意义。验证方法挑几张有代表性的全景图人工标注一小块区域作为真值算各类别的 IoU 和分类准确率。重点看小类别大类别通常没问题小类别才是这条流水线的试金石。我自己踩得最狠的一次是忘了在裁剪时抠背景CLIP 把一大片带草地的道路判成了「植被」整张图的面积统计全偏了。后来养成习惯每次改完流水线先拿一张图把中间结果可视化一遍掩码、裁剪、分类分数都看一眼再批量跑。希望帮到你。本文还有配套的精品资源点击获取
返回列表