参考图分割踩坑记录——INSID3 本地部署教程

INSID3 本地部署教程 + 车灯分割踩坑记录

项目:INSID3 (CVPR 2026) Training-Free In-Context Segmentation

硬件:RTX 4060 Ti 16GB

环境:Windows 11, Anaconda, CUDA 12.6

日期:2026-06-17 ~ 2026-06-18

文章目录

  • [INSID3 本地部署教程 + 车灯分割踩坑记录](#INSID3 本地部署教程 + 车灯分割踩坑记录)
    • [1. 项目简介](#1. 项目简介)
    • [2. INSID3 原理详解](#2. INSID3 原理详解)
      • [2.1 专业版:完整技术流程](#2.1 专业版:完整技术流程)
        • [2.1.1 特征提取](#2.1.1 特征提取)
        • [2.1.2 位置去偏(Positional Debiasing)](#2.1.2 位置去偏(Positional Debiasing))
        • [2.1.3 参考原型计算](#2.1.3 参考原型计算)
        • [2.1.4 双向匹配(Forward + Backward Matching)](#2.1.4 双向匹配(Forward + Backward Matching))
        • [2.1.5 层次聚类](#2.1.5 层次聚类)
        • [2.1.6 种子选择与聚合](#2.1.6 种子选择与聚合)
        • [2.1.7 上采样与后处理](#2.1.7 上采样与后处理)
      • [2.2 通俗版:形象化解释](#2.2 通俗版:形象化解释)
      • [2.3 流程图](#2.3 流程图)
      • [2.4 关键参数说明](#2.4 关键参数说明)
    • [3. 环境准备](#3. 环境准备)
      • [3.1 创建 conda 环境](#3.1 创建 conda 环境)
      • [3.2 安装 PyTorch (CUDA 12.6)](#3.2 安装 PyTorch (CUDA 12.6))
    • [4. 克隆代码与安装依赖](#4. 克隆代码与安装依赖)
      • [4.1 克隆仓库](#4.1 克隆仓库)
      • [4.2 安装依赖](#4.2 安装依赖)
    • [5. DINOv3 预训练权重获取](#5. DINOv3 预训练权重获取)
      • [5.1 权重下载选项](#5.1 权重下载选项)
      • [5.2 从 ModelScope 下载](#5.2 从 ModelScope 下载)
      • [5.3 权重格式转换](#5.3 权重格式转换)
    • [6. 踩坑记录:权重加载失败](#6. 踩坑记录:权重加载失败)
      • [6.1 问题现象](#6.1 问题现象)
      • [6.2 根因分析](#6.2 根因分析)
        • [错误 1:MLP 架构错误(SwiGLU vs 标准 FFN)](#错误 1:MLP 架构错误(SwiGLU vs 标准 FFN))
        • [错误 2:权重加载不完整](#错误 2:权重加载不完整)
        • [错误 3:key 格式不匹配](#错误 3:key 格式不匹配)
      • [6.3 错误的修复尝试](#6.3 错误的修复尝试)
        • [尝试 1:修改 SwiGLU 为标准 FFN](#尝试 1:修改 SwiGLU 为标准 FFN)
        • [尝试 2:用 transformers 加载](#尝试 2:用 transformers 加载)
        • [尝试 3:手动映射 key](#尝试 3:手动映射 key)
    • [7. 最终解决方案:torch.hub 官方实现](#7. 最终解决方案:torch.hub 官方实现)
      • [7.1 核心思路](#7.1 核心思路)
      • [7.2 torch.hub 缓存位置](#7.2 torch.hub 缓存位置)
      • [7.3 权重格式转换](#7.3 权重格式转换)
      • [7.4 完整的 `_build_encoder` 函数](#7.4 完整的 _build_encoder 函数)
      • [7.5 关键映射表](#7.5 关键映射表)
      • [7.6 转换后权重统计](#7.6 转换后权重统计)
    • [8. 推理验证](#8. 推理验证)
      • [8.1 猫示例](#8.1 猫示例)
      • [8.2 车灯示例](#8.2 车灯示例)
      • [8.3 验证指标](#8.3 验证指标)
    • [9. 边缘精度问题分析](#9. 边缘精度问题分析)
      • [9.1 边缘不准的原因](#9.1 边缘不准的原因)
      • [9.2 可能的改进方向](#9.2 可能的改进方向)
    • [10. CRF 后处理(待解决)](#10. CRF 后处理(待解决))
      • [10.1 安装 CRF](#10.1 安装 CRF)
      • [10.2 编译问题](#10.2 编译问题)
      • [10.3 使用 CRF](#10.3 使用 CRF)
    • [11. 完整代码修改清单](#11. 完整代码修改清单)
      • [11.1 修改的文件](#11.1 修改的文件)
      • [11.2 新增的文件](#11.2 新增的文件)
      • [11.3 完整的 `models/init.py`](#11.3 完整的 models/__init__.py)
    • [12. 经验总结](#12. 经验总结)
      • [12.1 关键经验](#12.1 关键经验)
      • [12.2 踩坑清单](#12.2 踩坑清单)
      • [12.3 相关资源](#12.3 相关资源)
    • [附录:DINOv3 预训练权重 key 完整列表](#附录:DINOv3 预训练权重 key 完整列表)

1. 项目简介

INSID3 是 CVPR 2026 Oral 论文,实现 Training-Free In-Context Segmentation

  • 输入:一张参考图 + 参考图的分割 mask + 一张目标图
  • 输出:目标图中对应物体的分割 mask
  • 原理:利用冻结的 DINOv3 backbone 提取特征,通过位置去偏 + 双向匹配 + 聚类实现分割
  • 优势:无需训练、无需分割 decoder、单个 backbone 搞定

我们的场景:给一张车灯标注图,自动分割其他图片中的车灯。

论文:https://arxiv.org/abs/2603.28480

代码:https://github.com/visinf/INSID3


2. INSID3 原理详解

2.1 专业版:完整技术流程

2.1.1 特征提取

DINOv3 是一个自监督预训练的 ViT-Large,输入 1024×1024 图像,patch_size=16,输出 64×64×1024 的特征图(4096 个 patch,每个 1024 维)。

python 复制代码
# 代码: insid3.py:227-231
fmaps = self.encoder.get_intermediate_layers(x, n=1, reshape=True)[0]
fmaps_norm = F.normalize(fmaps, p=2, dim=2)  # L2 归一化

关键点 :DINOv3 使用 RoPE 位置编码 (旋转位置编码),而非绝对位置编码。这意味着特征同时编码了语义信息 (这是什么物体)和位置信息(物体在哪里)。

2.1.2 位置去偏(Positional Debiasing)

核心洞察:DINOv3 的特征会同时响应"语义相似"和"位置相同"的 patch。例如,参考图左上角的猫耳朵,不仅会匹配目标图的猫耳朵,还会错误地匹配目标图左上角的任何东西(即使不是猫)。

解决方案:SVD 找到位置子空间,投影掉。

python 复制代码
# 代码: insid3.py:236-261
# 1. 对噪声图(全零)提取特征
noise_img = normalize(torch.zeros(1, 3, H, W), ...)
noise_fmaps = encoder.get_intermediate_layers(noise_img, ...)

# 2. 对特征做 SVD
E = noise_fmaps.reshape(C, -1)  # (C, N)
U, _, _ = torch.linalg.svd(E)   # U 的列向量就是位置子空间的基

# 3. 投影到正交补
P_perp = I - U @ U.T  # 正交投影矩阵
X_deb = P_perp @ X    # 去除位置信息

数学原理:噪声图的特征只包含位置信息(因为没有语义内容)。SVD 分解后,前 500 个主成分(U:, :500)就是位置子空间的基。将真实图像的特征投影到这个子空间的正交补上,就去除了位置信息,只保留语义信息。

2.1.3 参考原型计算
python 复制代码
# 代码: insid3.py:144-153
# 参考图前景 mask 内的所有 patch 特征取平均
fg = feat_refs_deb[0, s, :, mask_s]  # (C, M) M=前景 patch 数
ref_prototypes.append(fg.mean(dim=1)) # (C,) 一个原型向量

# 多张参考图取平均
ref_prototype = F.normalize(torch.stack(ref_prototypes).mean(dim=0), p=2, dim=0)

作用:将参考图的前景(车灯/猫)压缩成一个 1024 维的"原型向量",代表"这就是我要找的东西"。

2.1.4 双向匹配(Forward + Backward Matching)

前向匹配:目标图每个 patch 与参考原型的相似度

python 复制代码
# 代码: insid3.py:276
sim_fwd = einsum('bchw,cd->bhw', feat_tgt_deb, ref_prototype)
forward_mask = sim_fwd > 0  # 相似度 > 0 的候选

后向匹配:目标图每个 patch 在参考图找最近邻,投票决定它是否在 mask 内

python 复制代码
# 代码: insid3.py:282-296
for m in range(S):  # 遍历每张参考图
    # 目标图每个 patch 在参考图中找最相似的 patch
    best_idx = sim_t_to_r.reshape(h, w, -1).argmax(dim=2)
    rows = best_idx // Ws
    cols = best_idx % Ws
    # 检查参考图中该位置是否在 mask 内
    votes += ref_mask_m[rows, cols].to(torch.int32)

# 多数投票:超过半数参考图认为在 mask 内才算
backward_mask = votes >= majority_thresh

交集candidate_mask = forward_mask & backward_mask

为什么要双向?

  • 只有前向:会把背景中语义相似的区域也选进来(如车灯附近的反光)
  • 只有后向:参考图 mask 边界不准时会漏选
  • 双向取交集:既语义相似,又位置对应,大幅减少误检
2.1.5 层次聚类
python 复制代码
# 代码: clustering.py:8-25
# 计算 patch 间的余弦距离矩阵
S = (X @ X.T).clamp(-1, 1)  # 相似度矩阵
D = 1.0 - S                 # 距离矩阵

# 层次聚类,阈值 = 1 - tau
ac = AgglomerativeClustering(
    n_clusters=None, metric='precomputed',
    linkage='average', distance_threshold=float(1.0 - tau),
)
labels = ac.fit_predict(D)

作用:将目标图的 4096 个 patch 分成若干个语义一致的区域(聚类)。例如,车灯是一个聚类,车身是另一个聚类,背景是第三个聚类。

2.1.6 种子选择与聚合
python 复制代码
# 代码: insid3.py:301-354
# 1. 计算每个聚类与参考原型的跨图像相似度
cross_sim[k] = fg_sim[idx].mean()  # 聚类 k 的平均相似度

# 2. 选择相似度最高的聚类作为"种子"
seed_id = matched_ids[torch.argmax(cross_sim_matched)]

# 3. 计算种子与其他聚类的图内相似度(原始特征空间)
intra_sim = einsum('c,kc->k', orig_protos[seed_id], orig_protos)

# 4. 综合评分 = 跨图像相似度 × 图内相似度 × 面积权重
combined = cross_sim * intra_sim * area_weights

# 5. 评分 > 阈值的聚类合并到最终 mask
final_mask[valid] = combined[cluster_labels[valid]] > merge_threshold

作用

  • 种子选择:找到与参考图最匹配的聚类(车灯的主体部分)
  • 聚合:将与种子语义相似的相邻聚类也合并进来(车灯的边缘、反光等)
2.1.7 上采样与后处理
python 复制代码
# 代码: refinement.py:7-12
# 双线性插值上采样到原始分辨率
up = F.interpolate(mask[None, None].float(), size=(H, W), mode='bilinear')

2.2 通俗版:形象化解释

想象你在玩"找不同"游戏,但任务是"根据左边的猫,在右边的图里把猫圈出来"。

第一步:让 AI 看图

把两张图都扔进 DINOv3(一个超强大的视觉 AI),它会把每张图切成 4096 个小方块(16×16 像素),每个小方块输出一个 1024 维的"特征向量"------可以理解为这个小方块的"身份证"。

第二步:去除位置干扰

DINOv3 有个问题:它不仅会记住"这是猫耳朵",还会记住"猫耳朵在左上角"。如果我们直接用这个特征去匹配,就会出现:

  • ✅ 匹配到右边图的猫耳朵(正确)
  • ❌ 匹配到右边图左上角的任何东西(错误)

所以我们要做"位置去偏":拿一张纯噪声图(什么都没有),分析 DINOv3 会输出什么特征------这些特征就代表"位置信息"。然后从真实图像的特征中减去这部分,只保留"这是什么东西"的语义信息。

第三步:建立"目标档案"

参考图里有猫的 mask,我们把 mask 内所有小方块的特征取平均,得到一个"猫的原型向量"------可以理解为"猫的平均身份证"。

第四步:双向验证找候选区域

对目标图的每个小方块:

  1. 前向:这个小方块和"猫的原型"像不像?像的话就可能是猫。
  2. 后向:这个小方块在参考图里最像哪个小方块?那个小方块在不在 mask 里?在的话就可能是猫。
  3. 取交集:两个方向都认为是猫的,才是真正的候选。

为什么要双向?防止"长得像但不是"的误检(比如车灯旁边的反光也挺像车灯的)。

第五步:聚类分组

把目标图的 4096 个小方块按"长得像不像"分成若干组。比如:

  • 聚类 1:车灯主体(玻璃、灯泡)
  • 聚类 2:车灯边缘(金属边框)
  • 聚类 3:车身
  • 聚类 4:背景

第六步:种子 + 扩散

  1. 在候选区域里找到与"猫的原型"最像的聚类(种子)
  2. 看看这个种子和其他聚类像不像(图内相似度)
  3. 综合评分 = 与参考图的相似度 × 与其他聚类的相似度 × 面积
  4. 评分超过阈值的聚类,全部合并到最终 mask

第七步:放大回原图

AI 是在 64×64 的小图上工作的,最后用双线性插值放大回 1960×3497 的原图分辨率。

一句话总结:INSID3 = DINOv3 提取语义特征 → 去掉位置干扰 → 双向匹配找候选 → 聚类分组 + 种子扩散 → 输出 mask。

2.3 流程图

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                        INSID3 流程图                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────────┐    ┌──────────────┐                           │
│  │  参考图+Mask  │    │   目标图      │                           │
│  └──────┬───────┘    └──────┬───────┘                           │
│         │                   │                                   │
│         └─────────┬─────────┘                                   │
│                   ▼                                             │
│         ┌─────────────────┐                                     │
│         │  DINOv3 特征提取 │  (patch_size=16, 1024维)            │
│         └────────┬────────┘                                     │
│                  ▼                                              │
│         ┌─────────────────┐                                     │
│         │   位置去偏 (SVD)  │  去除位置信息,保留语义              │
│         └────────┬────────┘                                     │
│                  ▼                                              │
│    ┌─────────────┴─────────────┐                                │
│    ▼                           ▼                                │
│  ┌─────────────┐         ┌─────────────┐                        │
│  │ 参考原型计算  │         │  目标图特征   │                        │
│  │ (前景平均)   │         │             │                        │
│  └──────┬──────┘         └──────┬──────┘                        │
│         │                       │                               │
│         └───────────┬───────────┘                               │
│                     ▼                                           │
│           ┌─────────────────┐                                   │
│           │   双向匹配       │                                   │
│           │  前向 & 后向     │                                   │
│           └────────┬────────┘                                   │
│                    ▼                                            │
│           ┌─────────────────┐                                   │
│           │    层次聚类      │  (tau=0.6)                        │
│           └────────┬────────┘                                   │
│                    ▼                                            │
│           ┌─────────────────┐                                   │
│           │  种子选择+聚合   │                                   │
│           └────────┬────────┘                                   │
│                    ▼                                            │
│           ┌─────────────────┐                                   │
│           │   上采样到原图   │  (bilinear)                       │
│           └────────┬────────┘                                   │
│                    ▼                                            │
│           ┌─────────────────┐                                   │
│           │   输出 Mask     │                                   │
│           └─────────────────┘                                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

2.4 关键参数说明

参数 默认值 说明
image_size 1024 输入图像分辨率,越大越精细但越慢
svd_components 500 位置子空间的维度,越大去偏越强
tau 0.6 聚类阈值,越大聚类越细(更多小聚类)
merge_threshold 0.2 聚合阈值,越大分割越保守(更少区域被选中)
mask_refiner "bilinear" 后处理方式,可选 "crf" 精细化边界

3. 环境准备

3.1 创建 conda 环境

bash 复制代码
conda create --name reset3 python=3.12.0 -y
conda activate reset3

3.2 安装 PyTorch (CUDA 12.6)

bash 复制代码
pip install torch>=2.7 torchvision>=0.22 --index-url https://download.pytorch.org/whl/cu126

验证:

bash 复制代码
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"
# 期望输出: 2.7.1+cu126 True

4. 克隆代码与安装依赖

4.1 克隆仓库

bash 复制代码
cd D:\zero_track\Reset
git clone --depth 1 https://github.com/visinf/INSID3.git
cd INSID3

踩坑 1:GitHub clone 超时

复制代码
error: RPC failed; curl 28/56 ... Operation timed out

解决

  1. 开启代理
  2. 使用 --depth 1 浅克隆减少下载量
  3. 如果目录有残留 .git/,先清理再重新 clone

4.2 安装依赖

bash 复制代码
pip install -r requirements.txt

requirements.txt 内容:

复制代码
--index-url https://download.pytorch.org/whl/cu126
--extra-index-url https://pypi.org/simple

torch>=2.7
torchvision>=0.22
numpy>=2.0
Pillow>=10.0
einops>=0.8
scikit-learn>=1.5
pycocotools>=2.0
tqdm>=4.60
termcolor>=1.1.0
torchmetrics>=0.11
matplotlib

5. DINOv3 预训练权重获取

5.1 权重下载选项

来源 状态 说明
HuggingFace facebook/dinov3-vitl16-pretrain-lvd1689m ❌ 403 Forbidden Gated model,需要 HF token 授权
HuggingFace 镜像 (hf-mirror.com) ❌ 403 同样是 gated model
torch.hub 自动下载 ❌ 403 dl.fbaipublicfiles.com 下载,同样被禁
ModelScope 魔搭社区 ✅ 成功 https://www.modelscope.cn/models/facebook/dinov3-vitl16-pretrain-lvd1689m

5.2 从 ModelScope 下载

ModelScope 上的文件是 safetensors 格式,需要转换为 .pth。

踩坑 2:safetensors 下载不完整

  • 首次下载仅 86MB(应为 1.13GB)
  • 解决:重新下载,确保文件完整

踩坑 3:ModelScope 上没有代码仓库

  • ModelScope 只有模型权重文件,没有 DINOv3 的实现代码
  • 解决:代码通过 torch.hub 缓存获取(见第 6 节)

5.3 权重格式转换

原始代码期望 .pth 格式,转换方法:

python 复制代码
from safetensors.torch import load_file, save_file

# 加载 safetensors
state_dict = load_file("dinov3_vitl16_pretrain_lvd1689m.safetensors")

# 保存为 .pth
torch.save(state_dict, "pretrain/dinov3_vitl16_pretrain_lvd1689m-8aa4cbdd.pth")

最终权重文件放在:

复制代码
INSID3/pretrain/dinov3_vitl16_pretrain_lvd1689m-8aa4cbdd.pth  (1156MB)

6. 踩坑记录:权重加载失败

6.1 问题现象

克隆代码、安装依赖、下载权重后,运行作者示例:

python 复制代码
from models import build_insid3
model = build_insid3()
model.set_reference("assets/ref_cat_image.jpg", "assets/ref_cat_mask.png")
model.set_target("assets/target_cat_image.jpg")
pred_mask = model.segment()

结果:分割完全错误!绿色掩码覆盖整个图像(过度分割)。

6.2 根因分析

查看 models/__init__.py,原始代码通过 torch.hub.load 加载 DINOv3:

python 复制代码
encoder = torch.hub.load("facebookresearch/dinov3", "dinov3_vitl16", pretrained=True)

问题:需要外网访问 GitHub,国内环境无法使用。

作者提供的解决方案是手写一个自定义 DINOv3Encoder,但实现有严重错误:

错误 1:MLP 架构错误(SwiGLU vs 标准 FFN)

自定义实现用了 SwiGLU:

python 复制代码
class SwiGLU(nn.Module):
    def __init__(self, embed_dim, hidden_dim):
        self.w1 = nn.Linear(embed_dim, hidden_dim, bias=False)
        self.w2 = nn.Linear(hidden_dim, embed_dim, bias=False)
        self.w3 = nn.Linear(embed_dim, hidden_dim, bias=False)  # ← 问题!

预训练权重实际是标准 FFN:

python 复制代码
# 权重 key: layer.0.mlp.up_proj.weight, layer.0.mlp.down_proj.weight
mlp.up_proj = nn.Linear(embed_dim, hidden_dim)
mlp.down_proj = nn.Linear(hidden_dim, embed_dim)

后果w3 没有对应的预训练权重,随机初始化。约 4M 参数 × 24 层 = 96M 参数全错。

错误 2:权重加载不完整
复制代码
Loaded 366/414 weights (88%)
Missing: 48 weights (MLP bias 项)
错误 3:key 格式不匹配
预训练权重 key 自定义实现 key 状态
embeddings.cls_token cls_token ❌ 未映射
embeddings.register_tokens register_tokens ❌ 未映射
layer.N.attention.q_proj.weight 分离的 q/k/v ❌ 未合并
layer.N.mlp.up_proj.weight SwiGLU.w1.weight ❌ 名称不同
layer.N.layer_scale1.lambda1 layer_scale1

6.3 错误的修复尝试

尝试 1:修改 SwiGLU 为标准 FFN
python 复制代码
class DINOv3MLP(nn.Module):
    def __init__(self, embed_dim, hidden_dim):
        self.up_proj = nn.Linear(embed_dim, hidden_dim)
        self.down_proj = nn.Linear(hidden_dim, embed_dim)

问题:key 名称仍然不匹配,需要手动映射 414 个 key。

尝试 2:用 transformers 加载
python 复制代码
from transformers import AutoModel
model = AutoModel.from_pretrained("facebook/dinov3-vitl16-pretrain-lvd1689m")

问题

  1. DINOv3 是 gated model,需要 HF token
  2. transformers 4.50.0 只有 DinoV2,没有 DinoV3
尝试 3:手动映射 key

写了 ~100 行映射代码,但发现:

  • 预训练权重中 k_proj 只有 weight 没有 bias
  • 合并 qkv bias 时需要用零填充 k 部分
  • mask_token 形状差异:(1,1,1024) vs (1,1024)

结论:手写 encoder + 手动映射太容易出错,不是正确方案。


7. 最终解决方案:torch.hub 官方实现

7.1 核心思路

既然 torch.hub 会自动缓存 DINOv3 源码到本地,我们可以:

  1. 先让 torch.hub 下载代码(只需一次网络访问)
  2. 从本地缓存加载模型结构
  3. 用本地的 .pth 权重文件加载参数

7.2 torch.hub 缓存位置

复制代码
C:\Users\Administrator\.cache\torch\hub\facebookresearch_dinov3_main\

关键文件:

  • dinov3/hub/backbones.py --- 模型定义和加载函数
  • dinov3/models/vision_transformer.py --- DinoVisionTransformer 类

7.3 权重格式转换

预训练 .pth 的 key 格式与 torch.hub 模型完全不同,需要完整映射:

python 复制代码
def convert_weights(pretrained_dict, model, depth=24):
    """将预训练权重转换为 torch.hub 模型格式"""
    model_dict = model.state_dict()
    new_state_dict = {}

    for k, v in pretrained_dict.items():
        if k.startswith("layer."):
            parts = k.split(".")
            layer_num = int(parts[1])
            rest = ".".join(parts[2:])

            # 跳过 q/k/v(需要合并)
            if "attention.q_proj" in rest or "attention.k_proj" in rest or "attention.v_proj" in rest:
                continue

            # 映射 key
            mapping = {
                "attention.o_proj.weight": f"blocks.{layer_num}.attn.proj.weight",
                "attention.o_proj.bias": f"blocks.{layer_num}.attn.proj.bias",
                "mlp.up_proj.weight": f"blocks.{layer_num}.mlp.fc1.weight",
                "mlp.up_proj.bias": f"blocks.{layer_num}.mlp.fc1.bias",
                "mlp.down_proj.weight": f"blocks.{layer_num}.mlp.fc2.weight",
                "mlp.down_proj.bias": f"blocks.{layer_num}.mlp.fc2.bias",
                "norm1.weight": f"blocks.{layer_num}.norm1.weight",
                "norm1.bias": f"blocks.{layer_num}.norm1.bias",
                "norm2.weight": f"blocks.{layer_num}.norm2.weight",
                "norm2.bias": f"blocks.{layer_num}.norm2.bias",
                "layer_scale1.lambda1": f"blocks.{layer_num}.ls1.gamma",
                "layer_scale2.lambda1": f"blocks.{layer_num}.ls2.gamma",
            }
            if rest in mapping:
                new_state_dict[mapping[rest]] = v

        elif k.startswith("embeddings."):
            mapping = {
                "cls_token": "cls_token",
                "register_tokens": "storage_tokens",
                "patch_embeddings.weight": "patch_embed.proj.weight",
                "patch_embeddings.bias": "patch_embed.proj.bias",
            }
            if k == "embeddings.mask_token":
                new_state_dict["mask_token"] = v.squeeze(1)  # (1,1,1024) -> (1,1024)
            elif k.replace("embeddings.", "") in mapping:
                new_state_dict[mapping[k.replace("embeddings.", "")]] = v

        elif k.startswith("norm."):
            new_state_dict[k] = v

    # 合并 qkv weights 和 biases
    for layer_num in range(depth):
        q = pretrained_dict.get(f"layer.{layer_num}.attention.q_proj.weight")
        k = pretrained_dict.get(f"layer.{layer_num}.attention.k_proj.weight")
        v = pretrained_dict.get(f"layer.{layer_num}.attention.v_proj.weight")
        if q is not None and k is not None and v is not None:
            new_state_dict[f"blocks.{layer_num}.attn.qkv.weight"] = torch.cat([q, k, v], dim=0)

        q_bias = pretrained_dict.get(f"layer.{layer_num}.attention.q_proj.bias")
        k_bias = pretrained_dict.get(f"layer.{layer_num}.attention.k_proj.bias")
        v_bias = pretrained_dict.get(f"layer.{layer_num}.attention.v_proj.bias")
        if q_bias is not None and v_bias is not None:
            k_bias = k_bias if k_bias is not None else torch.zeros_like(q_bias)
            new_state_dict[f"blocks.{layer_num}.attn.qkv.bias"] = torch.cat([q_bias, k_bias, v_bias], dim=0)

    return new_state_dict

7.4 完整的 _build_encoder 函数

修改 models/__init__.py

python 复制代码
def _build_encoder(model_size: str = "large"):
    import sys
    import os
    import torch

    # 添加 torch.hub DINOv3 到 path
    hub_path = os.path.join(os.path.expanduser("~"), ".cache", "torch", "hub",
                            "facebookresearch_dinov3_main")
    if hub_path not in sys.path:
        sys.path.insert(0, hub_path)

    from dinov3.hub.backbones import _make_dinov3_vit

    _MODEL_CONFIGS = {
        "small": {"embed_dim": 384, "depth": 12, "num_heads": 6},
        "base": {"embed_dim": 768, "depth": 12, "num_heads": 12},
        "large": {"embed_dim": 1024, "depth": 24, "num_heads": 16},
    }
    config = _MODEL_CONFIGS[model_size]

    # 创建模型(不加载预训练权重)
    model = _make_dinov3_vit(
        img_size=224, patch_size=16,
        embed_dim=config["embed_dim"], depth=config["depth"],
        num_heads=config["num_heads"], ffn_ratio=4, ffn_layer="mlp",
        ffn_bias=True, proj_bias=True, n_storage_tokens=4,
        layerscale_init=1e-5, norm_layer="layernormbf16",
        pretrained=False,
    )

    # 加载转换后的权重(如果存在)
    weights_path = _WEIGHTS[model_size]
    converted_path = weights_path.replace(".pth", "_converted.pth")

    if os.path.exists(converted_path):
        print(f"Loading converted weights from {converted_path}...")
        state_dict = torch.load(converted_path, map_location="cpu")
        result = model.load_state_dict(state_dict, strict=False)
        print(f"Loaded (missing: {len(result.missing_keys)}, unexpected: {len(result.unexpected_keys)})")
    elif os.path.exists(weights_path):
        print(f"Loading and converting weights from {weights_path}...")
        pretrained_dict = torch.load(weights_path, map_location="cpu")
        new_state_dict = convert_weights(pretrained_dict, model, config["depth"])
        torch.save(new_state_dict, converted_path)
        print(f"Saved converted weights to {converted_path}")
        result = model.load_state_dict(new_state_dict, strict=False)
        print(f"Loaded (missing: {len(result.missing_keys)}, unexpected: {len(result.unexpected_keys)})")
    else:
        print(f"Warning: Weights not found at {weights_path}")

    return model

7.5 关键映射表

预训练 .pth key torch.hub 模型 key 说明
embeddings.cls_token cls_token 直接映射
embeddings.register_tokens storage_tokens 4 个 register tokens
embeddings.mask_token mask_token 需 squeeze: (1,1,1024)→(1,1024)
embeddings.patch_embeddings.weight patch_embed.proj.weight Conv2d 权重
layer.N.attention.{q/k/v}_proj.weight blocks.N.attn.qkv.weight 合并: cat(q,k,v, dim=0)
layer.N.attention.{q/k/v}_proj.bias blocks.N.attn.qkv.bias 合并, k 可能缺失用零填充
layer.N.attention.o_proj.weight blocks.N.attn.proj.weight 直接映射
layer.N.mlp.up_proj.weight blocks.N.mlp.fc1.weight 标准 FFN
layer.N.mlp.down_proj.weight blocks.N.mlp.fc2.weight 标准 FFN
layer.N.norm1.weight blocks.N.norm1.weight 直接映射
layer.N.norm2.weight blocks.N.norm2.weight 直接映射
layer.N.layer_scale1.lambda1 blocks.N.ls1.gamma Layer Scale
norm.weight norm.weight Final LayerNorm

7.6 转换后权重统计

复制代码
预训练 .pth key 数量: 415
torch.hub 模型 key 数量: 344
转换后 key 数量: 343
缺失 key: 1 (rope_embed.periods --- RoPE 自动生成,不影响推理)
意外 key: 0

8. 推理验证

8.1 猫示例

python 复制代码
from models import build_insid3
from utils.visualization import visualize_prediction_segmentation as visualize

model = build_insid3(model_size="large", device="cuda")

model.set_reference("assets/ref_cat_image.jpg", "assets/ref_cat_mask.png")
model.set_target("assets/target_cat_image.jpg")
pred_mask = model.segment()

visualize(
    "assets/ref_cat_image.jpg",
    "assets/ref_cat_mask.png",
    "assets/target_cat_image.jpg",
    pred_mask,
    "test/cat_result.png",
)

结果:✅ 分割正确,不再过度分割

8.2 车灯示例

python 复制代码
model = build_insid3(model_size="large", device="cuda")

ref_image = "test/ref/img_0000_5f73f69ea44b7f58.jpg"
ref_mask = "test/ref/img_0000_5f73f69ea44b7f58_mask.png"
target_image = "test/dst/img_0002_bead7827ad586680.jpg"

model.set_reference(ref_image, ref_mask)
model.set_target(target_image)
pred_mask = model.segment()

visualize(ref_image, ref_mask, target_image, pred_mask, "test/car_light_result.png")

结果:✅ 成功定位并分割大灯,边界基本准确

8.3 验证指标

python 复制代码
print(f"Prediction shape: {pred_mask.shape}")  # (1960, 3497)
print(f"True pixels: {pred_mask.sum().item()} / {pred_mask.numel()}")  # 821120 / 6854120

9. 边缘精度问题分析

9.1 边缘不准的原因

原因 说明
分辨率瓶颈 patch_size=16,每个特征点对应 16×16 像素区域,边界天然锯齿状
聚类边界不贴合 层次聚类按特征相似性分组,不感知物体轮廓
无边界细化 Training-free 方案没有学习边界的 decoder

9.2 可能的改进方向

  1. CRF 后处理:利用颜色相似性和平滑性约束优化边界
  2. 降低 image_size:更小的输入 → 更多的 patch → 更精细的边界(但会丢失细节)
  3. 调整聚类参数 :修改 tau(聚类阈值)和 merge_threshold(合并阈值)

10. CRF 后处理(待解决)

10.1 安装 CRF

bash 复制代码
git clone https://github.com/netw0rkf10w/CRF.git
cd CRF
python setup.py install

10.2 编译问题

踩坑 4:CUDA 12.6 + MSVC 18 不兼容

复制代码
nvcc fatal error C1189: #error: -- unsupported Microsoft Visual Studio version!
Only the versions between 2017 and 2022 (inclusive) are supported!

原因:当前安装的 MSVC BuildTools 18 超出 CUDA 12.6 支持范围(只支持 2017-2022)。

解决方案(待验证)

  1. 安装 MSVC 2022 BuildTools
  2. 或使用 -allow-unsupported-compiler flag

10.3 使用 CRF

python 复制代码
model = build_insid3(model_size="large", mask_refiner="crf", device="cuda")
# 后续推理自动使用 CRF 精细化边界

11. 完整代码修改清单

11.1 修改的文件

文件 修改内容
models/__init__.py 重写 _build_encoder 函数,改用 torch.hub 官方实现 + 权重转换

11.2 新增的文件

文件 说明
pretrain/dinov3_vitl16_pretrain_lvd1689m-8aa4cbdd.pth 原始 DINOv3 预训练权重(从 ModelScope 下载并转换)
pretrain/dinov3_vitl16_pretrain_lvd1689m-8aa4cbdd_converted.pth 转换后的权重(torch.hub 格式,自动生成)

11.3 完整的 models/__init__.py

python 复制代码
"""Model construction utilities for INSID3."""

import torch
from models.insid3 import INSID3

_HUB_NAMES = {
    "small": "dinov3_vits16",
    "base": "dinov3_vitb16",
    "large": "dinov3_vitl16",
}

_WEIGHTS = {
    "small": "pretrain/dinov3_vits16_pretrain_lvd1689m-08c60483.pth",
    "base": "pretrain/dinov3_vitb16_pretrain_lvd1689m-73cec8be.pth",
    "large": "pretrain/dinov3_vitl16_pretrain_lvd1689m-8aa4cbdd.pth",
}


def convert_weights(pretrained_dict, model, depth=24):
    """将预训练权重转换为 torch.hub 模型格式"""
    model_dict = model.state_dict()
    new_state_dict = {}

    for k, v in pretrained_dict.items():
        if k.startswith("layer."):
            parts = k.split(".")
            layer_num = int(parts[1])
            rest = ".".join(parts[2:])

            if "attention.q_proj" in rest or "attention.k_proj" in rest or "attention.v_proj" in rest:
                continue

            mapping = {
                "attention.o_proj.weight": f"blocks.{layer_num}.attn.proj.weight",
                "attention.o_proj.bias": f"blocks.{layer_num}.attn.proj.bias",
                "mlp.up_proj.weight": f"blocks.{layer_num}.mlp.fc1.weight",
                "mlp.up_proj.bias": f"blocks.{layer_num}.mlp.fc1.bias",
                "mlp.down_proj.weight": f"blocks.{layer_num}.mlp.fc2.weight",
                "mlp.down_proj.bias": f"blocks.{layer_num}.mlp.fc2.bias",
                "norm1.weight": f"blocks.{layer_num}.norm1.weight",
                "norm1.bias": f"blocks.{layer_num}.norm1.bias",
                "norm2.weight": f"blocks.{layer_num}.norm2.weight",
                "norm2.bias": f"blocks.{layer_num}.norm2.bias",
                "layer_scale1.lambda1": f"blocks.{layer_num}.ls1.gamma",
                "layer_scale2.lambda1": f"blocks.{layer_num}.ls2.gamma",
            }
            if rest in mapping:
                new_state_dict[mapping[rest]] = v

        elif k.startswith("embeddings."):
            mapping = {
                "cls_token": "cls_token",
                "register_tokens": "storage_tokens",
                "patch_embeddings.weight": "patch_embed.proj.weight",
                "patch_embeddings.bias": "patch_embed.proj.bias",
            }
            if k == "embeddings.mask_token":
                new_state_dict["mask_token"] = v.squeeze(1)
            elif k.replace("embeddings.", "") in mapping:
                new_state_dict[mapping[k.replace("embeddings.", "")]] = v

        elif k.startswith("norm."):
            new_state_dict[k] = v

    for layer_num in range(depth):
        q = pretrained_dict.get(f"layer.{layer_num}.attention.q_proj.weight")
        k = pretrained_dict.get(f"layer.{layer_num}.attention.k_proj.weight")
        v = pretrained_dict.get(f"layer.{layer_num}.attention.v_proj.weight")
        if q is not None and k is not None and v is not None:
            new_state_dict[f"blocks.{layer_num}.attn.qkv.weight"] = torch.cat([q, k, v], dim=0)

        q_bias = pretrained_dict.get(f"layer.{layer_num}.attention.q_proj.bias")
        k_bias = pretrained_dict.get(f"layer.{layer_num}.attention.k_proj.bias")
        v_bias = pretrained_dict.get(f"layer.{layer_num}.attention.v_proj.bias")
        if q_bias is not None and v_bias is not None:
            k_bias = k_bias if k_bias is not None else torch.zeros_like(q_bias)
            new_state_dict[f"blocks.{layer_num}.attn.qkv.bias"] = torch.cat([q_bias, k_bias, v_bias], dim=0)

    return new_state_dict


def _build_encoder(model_size: str = "large"):
    import sys
    import os
    import torch

    hub_path = os.path.join(os.path.expanduser("~"), ".cache", "torch", "hub",
                            "facebookresearch_dinov3_main")
    if hub_path not in sys.path:
        sys.path.insert(0, hub_path)

    from dinov3.hub.backbones import _make_dinov3_vit

    _MODEL_CONFIGS = {
        "small": {"embed_dim": 384, "depth": 12, "num_heads": 6},
        "base": {"embed_dim": 768, "depth": 12, "num_heads": 12},
        "large": {"embed_dim": 1024, "depth": 24, "num_heads": 16},
    }
    config = _MODEL_CONFIGS[model_size]

    model = _make_dinov3_vit(
        img_size=224, patch_size=16,
        embed_dim=config["embed_dim"], depth=config["depth"],
        num_heads=config["num_heads"], ffn_ratio=4, ffn_layer="mlp",
        ffn_bias=True, proj_bias=True, n_storage_tokens=4,
        layerscale_init=1e-5, norm_layer="layernormbf16",
        pretrained=False,
    )

    weights_path = _WEIGHTS[model_size]
    converted_path = weights_path.replace(".pth", "_converted.pth")

    if os.path.exists(converted_path):
        print(f"Loading converted weights from {converted_path}...")
        state_dict = torch.load(converted_path, map_location="cpu")
        result = model.load_state_dict(state_dict, strict=False)
        print(f"Loaded (missing: {len(result.missing_keys)}, unexpected: {len(result.unexpected_keys)})")
    elif os.path.exists(weights_path):
        print(f"Loading and converting weights from {weights_path}...")
        pretrained_dict = torch.load(weights_path, map_location="cpu")
        new_state_dict = convert_weights(pretrained_dict, model, config["depth"])
        torch.save(new_state_dict, converted_path)
        print(f"Saved converted weights to {converted_path}")
        result = model.load_state_dict(new_state_dict, strict=False)
        print(f"Loaded (missing: {len(result.missing_keys)}, unexpected: {len(result.unexpected_keys)})")
    else:
        print(f"Warning: Weights not found at {weights_path}")

    return model


def build_insid3(
    *,
    model_size: str = "large",
    image_size: int = 1024,
    svd_components: int = 500,
    tau: float = 0.6,
    merge_threshold: float = 0.2,
    mask_refiner: str = "bilinear",
    resize_to_orig_size: bool = True,
    device: str = "cuda",
):
    encoder = _build_encoder(model_size)
    model = INSID3(
        encoder=encoder,
        image_size=image_size,
        svd_components=svd_components,
        tau=tau,
        merge_threshold=merge_threshold,
        mask_refiner=mask_refiner,
        resize_to_orig_size=resize_to_orig_size,
        device=device,
    )
    for param in model.parameters():
        param.requires_grad = False
    return model


def build_insid3_from_args(args):
    return build_insid3(
        model_size=args.model_size,
        image_size=args.image_size,
        svd_components=int(args.svd_comps),
        tau=args.tau,
        merge_threshold=args.merge_thresh,
        mask_refiner='crf' if getattr(args, 'crf_mask_refinement', False) else 'bilinear',
        resize_to_orig_size=False,
        device=args.device,
    )

12. 经验总结

12.1 关键经验

  1. 不要手写模型实现:除非完全理解架构,否则直接用官方实现。手写 encoder 容易在 MLP/Attention 细节上犯错。

  2. 权重格式转换是可行的:只要理解两种格式的 key 映射关系,就可以安全转换。转换后缓存避免重复计算。

  3. DINOv3 架构要点

    • 标准 FFN(up_proj/down_proj),不是 SwiGLU
    • 使用 RoPE 位置编码,没有 position_embeddings
    • 有 4 个 register tokens(storage_tokens)
    • qkv 合并为一个线性层
  4. 边缘精度是 training-free 方法的固有局限:patch_size=16 导致特征图分辨率低,CRF 后处理可以改善但需要额外编译。

12.2 踩坑清单

问题 原因 解决方案
GitHub clone 超时 无代理 开代理 + --depth 1 浅克隆
HuggingFace 403 Gated model 改用 ModelScope 下载
safetensors 下载不完整 网络问题 重新下载,检查文件大小
自定义 encoder 输出错误 SwiGLU vs 标准 FFN 改用 torch.hub 官方实现
权重 key 不匹配 格式不同 编写完整映射 + qkv 合并
CRF 编译失败 MSVC 18 不兼容 CUDA 12.6 安装 MSVC 2022 或用 flag 绕过

12.3 相关资源


附录:DINOv3 预训练权重 key 完整列表

复制代码
embeddings.cls_token                          # (1, 1, 1024)
embeddings.mask_token                         # (1, 1, 1024)
embeddings.patch_embeddings.weight            # (1024, 3, 16, 16)
embeddings.patch_embeddings.bias              # (1024,)
embeddings.register_tokens                    # (1, 4, 1024)
layer.0.attention.k_proj.weight               # (1024, 1024)
layer.0.attention.o_proj.bias                 # (1024,)
layer.0.attention.o_proj.weight               # (1024, 1024)
layer.0.attention.q_proj.bias                 # (1024,)
layer.0.attention.q_proj.weight               # (1024, 1024)
layer.0.attention.v_proj.bias                 # (1024,)
layer.0.attention.v_proj.weight               # (1024, 1024)
layer.0.layer_scale1.lambda1                  # (1024,)
layer.0.layer_scale2.lambda1                  # (1024,)
layer.0.mlp.down_proj.bias                   # (1024,)
layer.0.mlp.down_proj.weight                 # (1024, 4096)
layer.0.mlp.up_proj.bias                     # (4096,)
layer.0.mlp.up_proj.weight                   # (4096, 1024)
layer.0.norm1.bias                           # (1024,)
layer.0.norm1.weight                         # (1024,)
layer.0.norm2.bias                           # (1024,)
layer.0.norm2.weight                         # (1024,)
... (重复 24 层)
norm.bias                                    # (1024,)
norm.weight                                  # (1024,)

总计:5 + 24 × 17 + 2 = 415 个 key


最后更新:2026-06-18