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 内所有小方块的特征取平均,得到一个"猫的原型向量"------可以理解为"猫的平均身份证"。
第四步:双向验证找候选区域
对目标图的每个小方块:
- 前向:这个小方块和"猫的原型"像不像?像的话就可能是猫。
- 后向:这个小方块在参考图里最像哪个小方块?那个小方块在不在 mask 里?在的话就可能是猫。
- 取交集:两个方向都认为是猫的,才是真正的候选。
为什么要双向?防止"长得像但不是"的误检(比如车灯旁边的反光也挺像车灯的)。
第五步:聚类分组
把目标图的 4096 个小方块按"长得像不像"分成若干组。比如:
- 聚类 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
解决:
- 开启代理
- 使用
--depth 1浅克隆减少下载量 - 如果目录有残留
.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")
问题:
- DINOv3 是 gated model,需要 HF token
- 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 源码到本地,我们可以:
- 先让 torch.hub 下载代码(只需一次网络访问)
- 从本地缓存加载模型结构
- 用本地的 .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 可能的改进方向
- CRF 后处理:利用颜色相似性和平滑性约束优化边界
- 降低 image_size:更小的输入 → 更多的 patch → 更精细的边界(但会丢失细节)
- 调整聚类参数 :修改
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)。
解决方案(待验证):
- 安装 MSVC 2022 BuildTools
- 或使用
-allow-unsupported-compilerflag
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 关键经验
-
不要手写模型实现:除非完全理解架构,否则直接用官方实现。手写 encoder 容易在 MLP/Attention 细节上犯错。
-
权重格式转换是可行的:只要理解两种格式的 key 映射关系,就可以安全转换。转换后缓存避免重复计算。
-
DINOv3 架构要点:
- 标准 FFN(up_proj/down_proj),不是 SwiGLU
- 使用 RoPE 位置编码,没有 position_embeddings
- 有 4 个 register tokens(storage_tokens)
- qkv 合并为一个线性层
-
边缘精度是 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 相关资源
- INSID3 论文:https://arxiv.org/abs/2603.28480
- INSID3 代码:https://github.com/visinf/INSID3
- DINOv3 代码:https://github.com/facebookresearch/dinov3
- ModelScope DINOv3:https://www.modelscope.cn/models/facebook/dinov3-vitl16-pretrain-lvd1689m
- CRF 仓库:https://github.com/netw0rkf10w/CRF
附录: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