RealCat

· 96 min read

论文解读:RIPE,用「是否同一场景」的二元标签训练关键点

TL;DR RIPE 的核心主张是:训练关键点检测器不需要深度、位姿、单应,甚至不需要像素级对应,只需要告诉网络两张图是不是同一个场景[1]。它把检测建模成强化学习策略——热图分 8×8 格,每格对 64 个位置做 categorical 采样,再用选中 logit 的 sigmoid 接受门过滤;描述子取 VGG-19 四层特征的 hypercolumn;奖励是把两图描述子做 MNN、再用 RANSAC 基础矩阵筛出的极线内点,正对给 +1、负对给 -1,用 REINFORCE 回传。结果:MegaDepth-1500 AUC@5° 55.11(ALIKED 56.71,DeDoDe 55.02),HPatches AUC@1px 37.93(DeDoDe 39.88),Aachen 夜间 0.25m/2° 52.9(加入 Tokyo 24/7 后 60.2),Boreas 暴雪 3cm/3° 3.22 为表中最高;代价是单张 A100 训练 3 天、单图推理 0.47 s(DeDoDe 0.45 s)。代码与权重已开源,但协议仅限非商业学术用途。

一、动机:关键点学习卡在监督上

局部特征的学习一直有一个昂贵的隐含前提:网络要知道哪些像素是同一个 3D 点。SuperPoint 用单应把同一张图变换两次,从而免费得到像素对应[2];DISK 和 DaD 在 MegaDepth 上用深度与相机位姿算出对应,再用可重复性奖励训练检测器[3, 4];DeDoDe 进一步用 SfM 三角化出的 3D track 做监督[5]。这些做法都很有效,但数据门槛也清楚:要么有精确的几何标注,要么能构造出可信的合成变换,要么有足够好的 SfM 模型。

RIPE 想把这层前提也去掉。给定一对图 ,它只需要一个二元标签

这里的「同一场景」不是「同一栋建筑的两张随手拍」这么宽松,论文的定义是两图之间有足够多的 2D 关键点对应到同一个 3D 点,也就是有实质共视。但比起深度、位姿、单应或像素级对应,这仍然便宜得多:互联网照片、机器人日志、白天—黑夜序列、雨雪天气数据,只要知道拍摄地点或时段,就能自动打上这一位标签。

这个信号为什么可能够用?因为极线几何本身就是一个免费的一致性检验。如果两张图确实在同一场景,那么好的检测器和描述子应该产生一批互相匹配、并且能被同一个基础矩阵解释的点;如果两张图来自不同场景,就不存在这样一个基础矩阵,匹配越少越好。于是奖励可以定义为「通过极线检验的匹配数」,检测器只需要最大化它。论文的关键洞察就在这里:把 epipolar constraint 当作奖励,而不是当作监督标签。

这也解释了 RIPE 与之前强化学习路线的关系。Reinforced Feature Points 把检测、描述、匹配、位姿估计串成完整 pipeline,用已知位姿算最终任务奖励[6];DISK 用已知相对位姿算可重复性奖励[3];DaD 再用蒸馏和多样性把 DISK 的检测器做得更稳[4]。它们都还需要位姿。RIPE 把奖励换成「MNN + RANSAC 基础矩阵的内点数」,位姿就不再是训练的必需品。

与之相对,站内解读过的 LightGlue 和 LoMa 走的是「强检测器 + 学习匹配器」路线:LightGlue 用自注意力让匹配随难度自适应地变快或变准,LoMa 则用 DaD 检测、DeDoDe 描述和 LightGlue 式匹配器,把训练数据与模型容量堆到新的水平;它发表于 RIPE 之后,不在本文的对比表中。RIPE 关心的是更上游的问题——只靠图像对标签,检测器本身能不能被训出来;所以它的奖励仍然停在 MNN + RANSAC,不引入可学习的匹配器。

难点也随之而来。关键点选择是离散的,RANSAC 和 MNN 都不可导;如果直接对所有像素做全局 softmax,网络很容易塌缩到少数峰,甚至退化到极线汇聚的极点(epipole)附近。RIPE 的答案由三部分组成:分格采样保证空间覆盖和可分解的 log-prob,接受门让网络学会「这个格子不值得出点」,描述子辅助损失给稀疏的 RL 奖励补上稠密梯度。下面按这三条线拆。

同一地点的白天与夜晚图像
图 1论文 teaser 中的同一地点昼夜图像(特拉法加广场)。RIPE 只用「是否同场景」这一位标签训练,不需要深度、位姿或像素对应;昼夜、天气和运动物体带来的差异正是它要覆盖的场景。

二、核心方法

2.1 总体流程

RIPE 总体流程
图 2RIPE 的一次训练迭代。两张图共享权重,分别经过 encoder–decoder 得到热图;每个 8×8 格子采样一个关键点,并由接受门决定是否保留;关键点处的 hypercolumn 特征拼成描述子;两图描述子做 MNN,再用 RANSAC 估计基础矩阵并筛选极线内点;内点数变成奖励矩阵,最后用 REINFORCE 回传给检测器。

给定一对图,RIPE 的流程可以写成:

  1. 共享权重的 hourglass 网络对每张图输出一张单通道热图;
  2. 热图切成 的格子,每个格子按 categorical 分布采样一个位置,再用接受门决定是否保留;
  3. 在每个采样位置,从 encoder 的四个尺度双线性插值出 hypercolumn 特征,拼接后压到 256 维并 L2 归一化;
  4. 两图描述子做 mutual nearest neighbor(MNN),用 RANSAC 估计基础矩阵,保留极线内点;
  5. 正对的内点给正奖励,负对的对应匹配给负奖励,组成 的稀疏奖励矩阵;
  6. 用 REINFORCE 的 log-prob 加权奖励更新检测器,同时用对比损失直接训练描述子。

论文的架构细节:encoder 是 torchvision 的 VGG-19[7],取 strides 的四层,通道数为 64/128/256/512;decoder 沿用 DeDoDe 和 DKM 的 depthwise convolutional refiner[5, 8],每个尺度 8 个 block,内部维度也是 64/128/256/512,最后输出 1 通道热图。输入 resize + pad 到 ,不做随机裁剪或旋转。

检测与描述分支
图 3单张图上的检测(上)与描述(下)。热图被分成规则网格,每格一个采样点;橙点是接受门拒绝的点,绿点是保留的点。描述子不是取 decoder 的最后一层,而是从 encoder 的多个尺度在关键点位置做双线性插值,拼成 hypercolumn 后再 1×1 卷积降维。

2.2 分格检测:每格恰好一个点

记热图为

把 切成 的格子,,格子总数

在 的输入上,。每个格子 的 64 个 logit 定义一个 categorical 分布,网络从中采样一个位置:

也就是说,每个格子必定出一个点,点的位置在 64 个像素里选。这个设计有三个直接后果:

  • 空间覆盖。格子之间互不竞争,关键点不会全部挤在纹理最强的几个峰上。论文用它对比 DeDoDe:DeDoDe 在定性图里明显把点聚在少数区域,而 RIPE 和 ALIKED 的分布更均匀。
  • log-prob 可分解。每格的选择是独立的 categorical,整张图的策略就是 个分布的乘积;REINFORCE 的 log-prob 可以直接按格子相加,后面的 矩阵才有清晰的解释。
  • 抗退化。如果所有点都塌到极线汇聚的极点,格子采样会强制其他格子也出点,描述子损失又会把这些点的描述子拉开;论文在补充材料里专门分析了这一点,结论是训练中从未观察到塌缩。

注意,这是训练时的采样方式。推理时并不按格子采样,而是在整张热图上做 NMS,再按原始 logit 取 top-(MegaDepth 2048,HPatches 1024)。训练和推理的选点机制并不完全一致,这一点后面会作为自己的观察展开。

2.3 接受门:不是每个格子都值得出点

只有 categorical 采样还不够。每个格子都会选一个位置,哪怕这个格子是天空、过曝的灯箱或纯色墙面;这些点既不可重复,也很难匹配。RIPE 给每个格子加了一个接受门:用选中位置的 logit 过一个 sigmoid,得到接受概率

再从这个伯努利分布采样一个指示变量 。最终一个格子被保留的概率是

被保留时的对数概率是

这里有一个实现上的巧思:接受门没有单独的 head,而是复用被选中位置的 logit。同一个 既参与「在 64 个位置里选哪个」的排序,又决定「这个格子要不要出点」。这带来两个好处:不增加参数;门的训练信号和点的位置直接绑定——如果一个格子选出的点确实可匹配,那么它的 logit 会被 RL 奖励推高,接受概率也一起变大。反过来,天空格子的 logit 会被压低,门自然关闭。

论文的公式写作 ,其中 是概率;代码里采样的指示变量也叫 accepted_choices。读代码时不要把这二者混淆:log_probs 是 categorical log-prob 与 Bernoulli log-prob 的和,被拒绝的格子同样有 log-prob,只是不进入后续匹配。

代码:分格采样与接受门(官方 ripe/models/ripe.py,原文照录)
    def sample(self, grid):
        """
        Sample keypoints given a grid where each cell has logits stacked in last dimension
        Input
          grid: [B, C, H//w, W//w, w*w]

        Returns
          log_probs: [B, C, H//w, W//w ] - logprobs of selected samples
          choices: [B, C, H//w, W//w] indices of choices
          accept_mask: [B, C, H//w, W//w] mask of accepted keypoints

        """
        chooser = torch.distributions.Categorical(logits=grid)
        choices = chooser.sample()
        logits_selected = torch.gather(grid, -1, choices.unsqueeze(-1)).squeeze(-1)

        flipper = torch.distributions.Bernoulli(logits=logits_selected)
        accepted_choices = flipper.sample()

        # Sum log-probabilities is equivalent to multiplying the probabilities
        log_probs = chooser.log_prob(choices) + flipper.log_prob(accepted_choices)

        accept_mask = accepted_choices.gt(0)

        return (
            log_probs.squeeze(1),
            choices,
            accept_mask.squeeze(1),
            logits_selected.squeeze(1),
        )

下面的部件把「接受门」压缩成一个可以拖的阈值。玩具图里有 312 个格子:上方是天空(其中一小块是云),中间是建筑立面,下方是地面。每个格子有一个「纹理分数」,代表网络学到的 ;阈值 以下的门被关掉。真实训练用的是伯努利采样,这里用确定性阈值代替,方便看清精度—召回的变化。

默认 时,104 个立面格子全部保留,地面混入约 30 个、天空混入约 10 个,精度约 73%;把 拉到 0.7,天空和地面全部被关掉,精度 100%,召回仍是 100%;再拉到 0.8,召回开始掉到 87%,0.9 时只剩 47%。把门关掉,312 个格子全部出点,精度跌到 33%。这就是接受门的作用:它不是让检测器「更敏感」,而是给它一个在无纹理区域弃权的机制。

2.4 Hypercolumn 描述子:从四个尺度取特征

检测到位置之后,下一步是给每个点一个描述子。最直接的做法是把 decoder 最后一层特征在关键点处双线性插值,但这一层分辨率低,空间细节在多次下采样和 refine 中已经损失。RIPE 改用 hypercolumn[9]:在 encoder 的四个尺度上分别插值,把 64+128+256+512=960 维拼起来,再用 卷积压到 256 维,最后 L2 归一化。

这里的插值是在归一化坐标上做的:关键点位置先除以输出图的 映射到 ,再对每个尺度的特征图调用 grid_sample。因为归一化坐标表示的是相对图像位置,同一个坐标在低分辨率特征图上会落到对应的相对位置,不需要为每个尺度单独换算。

为什么非要多尺度?论文的补充材料给了一个很硬的消融:把 hypercolumn 换成「encoder 最后一层的双线性上采样」,训练直接失败,描述子不够有区分度。作者把这一点归因于单尺度特征的感受野和语义层级太单一:低层特征定位准但缺少上下文,高层特征上下文强但定位糊,拼起来才能同时支撑匹配和定位。Poje 等人的 DEAL 也用过 encoder 特征的上采样[10],但 RIPE 认为在他们的设定下不够,必须显式保留多个尺度。

代码:hypercolumn 采样与降维(官方 ripe/models/upsampler/hypercolumn_features.py 与 ripe/models/ripe.py,原文节选)
    def normgrid(self, x, H, W):
        return 2.0 * (x / (torch.tensor([W - 1, H - 1], device=x.device, dtype=x.dtype))) - 1.0

    def extract_values_at_poses(self, x, pos, H, W):
        """Extract values from tensor x at the positions given by pos.

        Args:
        - x (Tensor): Tensor of size (C, H, W).
        - pos (Tensor): Tensor of size (N, 2) containing the x, y positions.

        Returns:
        - values (Tensor): Tensor of size (N, C) with the values from f at the positions given by p.
        """

        # check if grid is float32
        if x.dtype != torch.float32:
            x = x.to(torch.float32)

        grid = self.normgrid(pos, H, W).unsqueeze(-2)

        x = F.grid_sample(x, grid, mode=self.mode, align_corners=True)
        return x.permute(0, 2, 3, 1).squeeze(-2)

    def forward(self, x, pos, H, W):
        descs = []

        for layer in x:
            desc = self.extract_values_at_poses(layer, pos, H, W)
            descs.append(desc)

        return descs
    def get_descs(self, feature_map, guidance, kpts, H, W):
        descs = self.upsampler(feature_map, kpts, H, W)

        if isinstance(self.conv_dim_reduction_coarse_desc, nn.ModuleList):
            # individual descriptor convolutions for each layer
            desc_conv = []
            for desc, conv in zip(descs, self.conv_dim_reduction_coarse_desc):
                desc_conv.append(conv(desc.permute(0, 2, 1)).permute(0, 2, 1))
            desc = torch.cat(desc_conv, dim=-1)
        else:
            desc = torch.cat(descs, dim=-1)
            desc = self.conv_dim_reduction_coarse_desc(desc.permute(0, 2, 1)).permute(0, 2, 1)

        desc = F.normalize(desc, dim=2)

        return desc

2.5 极线奖励:用 RANSAC 内点数当分数

现在到了 RIPE 最核心的部分:奖励怎么算。给定两图的描述子,先用 L2 距离做 mutual nearest neighbor,得到候选匹配集合 。然后在这些匹配上估计基础矩阵 :

  • 匹配用 Kornia 的 DescriptorMatcher(match_mode="mnn", th=0.8)[11];
  • 基础矩阵用 PoseLib 的 8 点算法 + RANSAC[12],官方配置的极线阈值是 1.0 px;
  • RANSAC 返回一个布尔掩码 ,表示匹配 是否能被同一个 解释。

奖励定义为

其中 。正对时内点得 ,负对时内点得 ;没有通过极线检验的匹配得 0。所有 放进一个 的矩阵 ,行是图 1 的格子、列是图 2 的格子。由于只有 MNN 匹配的格子非零,这个矩阵极度稀疏: 时理论上有 万项,但每对图只有 个非零。

论文的 Eq. 7 写的是「 且 为真」,也就是负对也只惩罚通过极线检验的匹配。这和代码的默认行为一致:默认配置 no_filtering_negatives: False 会把 None 传给 matcher,负对同样跑 RANSAC,只有内点被乘上 。代码里还有一条可选路径:把 no_filtering_negatives 设为 True,matcher 收到标签后会对负对跳过 RANSAC,把所有 MNN 都标成内点,奖励先取 0.5 再乘 ,于是每个 MNN 都得 。这条路径的注释说是为了「强制更可区分的描述子」,但它不是默认配置;读论文时容易忽略这个区别。

还有一个很小的项:fp_penalty = -1e-7。正对里没有匹配的格子会被填上 ,负对里则填 。它不是主奖励,作用是给「什么都没匹配上」一个极轻微的梯度,避免零奖励区域完全无信号。因为量级太小,打印矩阵时看起来仍然像 0。

奖励的计算流程
图 4奖励的计算流程(原文图 4)。两图共享检测器,MNN 得到候选匹配,RANSAC 估计基础矩阵并保留极线内点;内点组成稀疏奖励矩阵 ,与 的 log-prob 外和相乘后由 REINFORCE 回传。正对的内点给 ,负对的内点给 。
代码:MNN、基础矩阵与极线奖励(官方 ripe/matcher/*.py、ripe/utils/utils.py,原文节选)
    def __call__(self, pts0, pts1, inl_th):
        F, info = poselib.estimate_fundamental(
            pts0.cpu().numpy(),
            pts1.cpu().numpy(),
            {
                "max_epipolar_error": inl_th,
            },
        )

        success = F is not None
        if success:
            inliers = info.pop("inliers")
            inliers = torch.tensor(inliers, dtype=torch.bool, device=pts0.device)
        else:
            inliers = torch.zeros(pts0.shape[0], dtype=torch.bool, device=pts0.device)

        return F, inliers
            # use label information to exclude negative pairs from geometric filtering process -> enforces more descriminative descriptors
            if label is not None and label[b] == 0:
                ransac_inliers = torch.ones((idx_matches.shape[0]), device=dev).bool()
                future_results[b] = (None, ransac_inliers)
                continue
    with torch.no_grad():
        reward *= 1.0 if label else -1.0

        dense_returns = torch.zeros((len(kps1), len(kps2)), device=kps1.device)

        if filter_mode == "ignore":
            dense_returns[
                abs_idx_matches[:, 0][ransac_inliers],
                abs_idx_matches[:, 1][ransac_inliers],
            ] = reward
        elif filter_mode == "punish":
            in_padding_area = (
                padding_mask1[abs_idx_matches[:, 0]] & padding_mask2[abs_idx_matches[:, 1]]
            )  # both in the image area (not in padding area)

            dense_returns[
                abs_idx_matches[:, 0][ransac_inliers & in_padding_area],
                abs_idx_matches[:, 1][ransac_inliers & in_padding_area],
            ] = reward
            dense_returns[
                abs_idx_matches[:, 0][ransac_inliers & ~in_padding_area],
                abs_idx_matches[:, 1][ransac_inliers & ~in_padding_area],
            ] = -1.0
        else:
            raise ValueError(f"Unknown filter mode: {filter_mode}")

        if selected_only:
            dense_returns = dense_returns[selected_mask1, :][:, selected_mask2]
        if filter_mode == "ignore" and not selected_only:
            dense_returns = dense_returns[padding_mask1, :][:, padding_mask2]

        if penalty != 0.0:
            # pos. pair: small penalty for not finding a match
            # neg. pair: small reward for not finding a match
            penalty_val = penalty if label else -penalty

            dense_returns[dense_returns == 0.0] = penalty_val

        if use_whitening:
            dense_returns = (dense_returns - dense_returns.mean()) / (dense_returns.std() + 1e-6)

下面的部件把奖励矩阵做成可以玩的版本。左图 A 和图 B 各有 20 个关键点,前 9 对是真实对应,其余是随机匹配;拖动极线阈值 ,看哪些匹配能通过极线检验。右边是 的奖励矩阵,绿色是 ,红色是 ,灰色是「匹配了但没通过检验」。真实场景里 ,矩阵是 ,这里缩小到 20 只是为了看得清。

默认设置下(正对, px,离群比例 0.25),14 个候选 MNN 里有 9 个通过极线检验,矩阵里 9 个 。把 拉到 0.5,只剩 5 个;拉到 1,8 个;拉到 4 以上,9 个稳定下来,因为剩下的离群匹配离极线太远,再放宽也进不来。切到「负对:默认 RANSAC」,同样的 9 个内点变成 ;切到「负对:跳过 RANSAC」,14 个 MNN 全部变成 。这个对比就是代码与论文 Eq. 7 之间最值得注意的一处细节。

2.6 REINFORCE:把 C 个格子当成 C 条轨迹

有了奖励矩阵,接下来的问题是:奖励不可导,怎么更新检测器?RIPE 用 REINFORCE[3, 4]。把 encoder–decoder 看成策略 ,输入图像是状态 ,采样出的关键点是动作 ,策略输出的是每个格子的选择概率:

训练目标是最大化期望奖励:

REINFORCE 给出梯度估计:

这里 是外和:若 、,则 ,得到一个 矩阵。第 项的含义是「图 1 的第 个格子选了点、图 2 的第 个格子也选了点」这一对动作的 log-prob,乘以这一对的奖励 。代码里对应的是

dense_logprobs = logprobs1[b][...].view(-1, 1) + logprobs2[b][...].view(1, -1)

然后逐元素乘奖励、取均值、取负号,作为策略损失。论文把它记作 ,和式 (10) 一起优化。

为什么一次前向能提供足够多的样本?因为每张图有 个格子,每个格子独立采样一个动作,一次图像对就相当于 条轨迹、 个动作对。奖励矩阵虽然大,但只有 MNN 匹配的格子非零,实际有效的梯度项是 个;其余项乘 0,不贡献梯度。这也是为什么 RIPE 能在单张 A100 上训练,而不需要像完整 RL pipeline 那样反复采样。

但纯 REINFORCE 有一个致命问题:奖励太稀疏,而且只告诉网络「这个匹配好不好」,不告诉它「描述子应该长什么样」。如果只有策略损失,描述子没有直接监督,匹配会迅速退化,奖励也跟着消失。论文的消融很直接:去掉描述子损失( 或 ),训练失败。所以还需要下一节的辅助损失。

代码:REINFORCE 策略损失(官方 ripe/train.py,原文节选)
                current_loss_policy = (dense_rewards * dense_logprobs).view(-1)

                loss_policy_stack = (
                    current_loss_policy
                    if loss_policy_stack is None
                    else torch.hstack((loss_policy_stack, current_loss_policy))
                )

                if kp_penalty != 0.0:
                    # keypoints with low logprob are penalized
                    # as they get large negative logprob values multiplying them with the penalty will make the loss larger
                    loss_kp = (
                        logprobs1[b][mask_selection_for_matching_1]
                        * torch.full_like(
                            logprobs1[b][mask_selection_for_matching_1],
                            kp_penalty * alpha,
                        )
                    ).mean() + (
                        logprobs2[b][mask_selection_for_matching_2]
                        * torch.full_like(
                            logprobs2[b][mask_selection_for_matching_2],
                            kp_penalty * alpha,
                        )
                    ).mean()
                    loss_kp_stack = loss_kp if loss_kp_stack is None else torch.hstack((loss_kp_stack, loss_kp))

                sum_num_keypoints_1 += mask_selection_for_matching_1.sum()
                sum_num_keypoints_2 += mask_selection_for_matching_2.sum()

            loss = loss_policy_stack.mean()
            if loss_kp_stack is not None:
                loss += loss_kp_stack.mean()

            loss = -loss

            if descriptor_loss is not None:
                loss += cfg.desc_loss_weight * loss_desc_stack.mean()

2.7 描述子损失:给 RL 奖励补上稠密梯度

描述子损失是一个按图像对类型分叉的对比损失。记第 个 RANSAC 内点的匹配描述子距离为 ,它的第二近邻距离为 ,边距 :

正对时,这一项要求「匹配内点的距离」比「第二近邻的距离」至少小 :不仅要匹配上,还要比错误匹配明显更近。负对时,这一项把匹配上的描述子推开,至少推到 以外。前者提升可区分性,后者直接惩罚假阳性。

代码里还有一个细节:第二近邻距离取的是两个方向中更硬的那个。对每个内点,先在「图 1 描述子 vs 图 2 全部描述子」里找第二近邻,再在反方向找一次,取两者距离的较小值作为 。这比只看一个方向更严格,也更接近匹配时实际会遇到的竞争。

是官方配置的 pos_margin 和 neg_margin,论文正文没有强调这个数。另一个代码细节是:如果 RANSAC 内点数少于 8,损失会退化成在所有 MNN 匹配上计算(inliers = torch.ones_like(inliers)),而不是只用内点。这能在早期匹配很少时提供梯度,但也意味着少量离群匹配会进入描述子损失;论文没有讨论这个 fallback。

代码:描述子对比损失(官方 ripe/losses/contrastive_loss.py,原文节选)
    if inliers.sum() < 8:  # if there are too few inliers, calculate loss on all matches
        inliers = torch.ones_like(inliers)

    matched_inliers_descs1 = desc1[matches[:, 0][inliers]]
    matched_inliers_descs2 = desc2[matches[:, 1][inliers]]

    if logits_1 is not None and logits_2 is not None:
        matched_inliers_logits1 = logits_1[matches[:, 0][inliers]]
        matched_inliers_logits2 = logits_2[matches[:, 1][inliers]]
        logits = torch.minimum(matched_inliers_logits1, matched_inliers_logits2)
    else:
        logits = torch.ones_like(matches[:, 0][inliers])

    if label:
        snn_match_dists_1, idx1 = second_nearest_neighbor(matched_inliers_descs1, desc2)
        snn_match_dists_2, idx2 = second_nearest_neighbor(matched_inliers_descs2, desc1)

        dists = torch.hstack((snn_match_dists_1, snn_match_dists_2))
        min_dists_idx = torch.min(dists, dim=1).indices.unsqueeze(1)

        dists_hard = torch.gather(dists, 1, min_dists_idx).squeeze(-1)
        dists_pos = F.pairwise_distance(matched_inliers_descs1, matched_inliers_descs2)

        contrastive_loss = torch.clamp(pos_margin + dists_pos - dists_hard, min=0.0)

        contrastive_loss = contrastive_loss * logits

        contrastive_loss = contrastive_loss.sum() / (logits.sum() + 1e-8)  # small epsilon to avoid division by zero
    else:
        dists = F.pairwise_distance(matched_inliers_descs1, matched_inliers_descs2)
        contrastive_loss = torch.clamp(neg_margin - dists, min=0.0)

        contrastive_loss = contrastive_loss * logits

        contrastive_loss = contrastive_loss.sum() / (logits.sum() + 1e-8)  # small epsilon to avoid division by zero

2.8 正则、总损失与训练配置

除了描述子损失,RIPE 还有一项低概率关键点正则:

其中 是一个很小的负数。它的作用不是奖励匹配,而是防止网络把概率质量放到极低概率的点上: 越负,乘上负的 后损失越大。训练时 的影响在前 1/3 步内线性升起,避免一开始就把采样限制得太死。

最终损失是

官方配置 。三项各司其职:策略损失优化「选哪些点」,描述子损失优化「怎么描述」,低概率正则维持采样的多样性。

项目值说明
输入分辨率resize + pad,保持长宽比
网格 8
描述子维度256960 维 hypercolumn → 1×1 卷积
奖励 1.0每个极线内点
描述子损失权重 5.0表 1 与代码一致
正则 (表 1)/ (表 7 与代码)论文内部不一致,见下
优化器AdamW,线性衰减
训练80,000 步,batch 6,梯度累积 4bf16-mixed,单张 A100 约 3 天
数据DISK 的 MegaDepth 子集正对 / 负对 1:1

这里必须指出一处论文与代码的不一致:表 1 把 标成 ,但表 7 的消融说最优是 ,官方配置也确实是 。表 7 的标题还把 写成了「descriptor loss weight」,而正文说它是正则参数;从公式 (9) 看,标题是笔误。按表 7 和代码走, 才是实际训练用的值。

训练数据方面,正对来自 DISK 的 MegaDepth 子集:每个场景的 tuple 里有三张图,随机取两张;负对从另一个随机场景取一张图,标签为 0。正负对严格交替,所以每个 batch 里各占一半。负对不是 hard negative,只是随机另一个场景——这一点对理解后面的局限很重要。

三、实验结果

3.1 评测协议

论文用了四个基准,协议细节比数字更重要:

  • MegaDepth-1500[13, 14]:两个场景、1500 对图,挑战是大视角、光照和重复纹理。稀疏方法把长边缩到 1200,稠密方法缩到 1600;所有稀疏方法用 MNN 匹配,PoseLib 估相对位姿,取 top 2048 个点,报告 AUC@。
  • HPatches[15]:平面场景的视角/光照序列,短边缩到 480,取 top 1024 个点,用重投影误差的 AUC@1/3/5 px。
  • Aachen Day-Night v1.1[16]:6697 张参考图,1015 个查询(824 白天、191 夜间);HLoc + NetVLAD 取 top 50,PnP RANSAC,报告 0.25m/2°、0.5m/5°、5m/10° 的 AUC。
  • Boreas[17]:自动驾驶多季节数据,参考序列 + 四个查询序列(Dark、Late Autumn、Heavy Snowfall、Rain);HLoc + NetVLAD 取 top 20,报告 3cm/3°、5cm/5°、25cm/2° 的 AUC。

另外,论文用 IMC2020[18] 的一个小验证子集做超参数选择,只训练 40,000 步就停,报告 AUC@5°。这个数字不是跨方法的主基准,后面讲消融时会用到。

3.2 MegaDepth 与 HPatches:稀疏方法里的第二

先看最常被引用的 MegaDepth-1500。RIPE 的 AUC@5°/10°/20° 是 55.11/68.34/78.03,在稀疏方法里仅次于 ALIKED[19] 的 56.71/69.86/79.54,略高于 DeDoDe-B 的 55.02/67.99/77.31;DISK 是 51.83/64.49/74.3,SuperPoint 是 49.5/62.51/72.12,SiLK[20] 只有 29.83/40.59/49.98。半稠密/稠密方法更高:RoMa 68.28/80.53/88.77,Efficient LoFTR 64.63/78.03/87.21,MASt3R 22.84/35.29/47.62[21, 22, 23](MASt3R 在这个稀疏匹配协议下并不占优,和它的设计目标有关)。

HPatches 上 RIPE 是 37.93/58.93/69.20:AUC@1px 低于 DeDoDe 的 39.88 和 SiLK[20] 的 39.05,AUC@5px 略高于 SiLK 的 67.64,整体和 DeDoDe/SiLK 同一档。ALIKED[19] 在这里掉到 31.95/57.65/69.35,说明它在 MegaDepth 上的优势没有完全迁移到平面单应;SiLK 则因为训练分布接近 HPatches,表现相对好。

下面的部件把表 2 的数字做成可以切换的条形图。选数据集和指标,条形按数值排序,RIPE 用强调色标出;把鼠标停在图上可以看到每个方法的具体值(移动端可以直接点选下拉框)。

3.3 Aachen:数据多样性比网络结构更值钱

Aachen 的结果是论文最有说服力的一组,因为它直接展示了「弱监督 + 多样数据」的价值。

方法Day 0.25m/2°Day 0.5m/5°Day 5m/10°Night 0.25m/2°Night 0.5m/5°Night 5m/10°
ALIKED87.393.997.373.388.096.9
DeDoDe82.289.092.647.156.564.4
SIFT82.588.591.930.938.246.1
RIPE(仅 MegaDepth)81.689.293.152.967.579.1
RIPE + Tokyo 24/781.689.092.460.272.883.8

只看 MegaDepth 训练,RIPE 夜间 0.25m/2° 是 52.9,比 DeDoDe 的 47.1 高 5.8,比 SIFT[24] 的 30.9 高 22.0;但离 ALIKED 的 73.3 还很远。论文对此的解释很坦率:ALIKED 不只在 MegaDepth 上训练,还用了 Aachen 的训练图像和合成数据,所以它在夜间查询上不是零样本。RIPE 只用 MegaDepth 时,已经能在夜间超过 DeDoDe 和 SIFT,说明极线奖励学到的特征对光照变化有一定泛化。

加入 Tokyo 24/7[25] 后,夜间涨到 60.2/72.8/83.8,分别 +7.3/+5.3/+4.7;白天几乎不变(+0.0/-0.2/-0.7)。Tokyo 24/7 是地点识别数据集,每个地点有白天、黄昏、夜晚三个视角,图像之间只按地理位置配对,没有 3D 或深度信息——这正好是 RIPE 能吃的数据。Table 4 进一步扫了混合比例:80% MegaDepth + 20% Tokyo 最好;把 Tokyo 提高到 40%,白天掉到 71.8/81.2/87,说明 Tokyo 的视角变化太少,模型会过拟合到「只认光照变化」而牺牲视角鲁棒性。论文把原因归结为 Tokyo 的图像主要在同一视角附近变化,缺少 MegaDepth 那种大视角差。

3.4 Boreas:恶劣天气下的收益与代价

Boreas 的结果放在补充材料,但很有信息量。RIPE 仅 MegaDepth 时,四个查询序列的 3cm/3°、5cm/5°、25cm/2° 分别是:

  • Dark:8.58/27.27/88.12
  • Late Autumn:3.96/12.84/91.07
  • Heavy Snowfall:3.22/11.78/68.86
  • Rain:7.0/23.69/93.94

加入 10% ACDC[26] 后变成 9.89/28.61/88.46、4.37/12.04/91.42、3.36/10.33/69.89、11.57/29.06/94.45。最亮眼的是暴雪 3cm/3°:3.22 是表中最好的稀疏方法(ALIKED 2.37、DeDoDe 1.84、DISK 2.34、SIFT 1.24),加 ACDC 后 3.36 更高。Dark 和 Rain 的紧阈值也有提升(Dark +1.31/+1.34/+0.34,Rain +4.57/+5.37/+0.51)。

但这不是全面提升:Late Autumn 的 5cm/5° 从 12.84 降到 12.04,Heavy Snowfall 的 5cm/5° 从 11.78 降到 10.33。论文的结论是「数据多样性有帮助」,但具体到某个天气/阈值,增益并不单调。另一个不能忽略的事实是,所有方法在 3cm/3° 这种紧阈值下都只有个位数到十几个点,25cm/2° 才到 68–95;恶劣天气的视觉定位仍然很难。

3.5 消融:哪些设计真的关键

论文的消融主要在 IMC2020 验证子集上做,训练 40,000 步,指标是 AUC@5°。这个设置不是最终模型,但足以看趋势。

描述子损失权重 (表 6): 和 训练失败; 是 61.94, 是 60.46, 是 63.48, 是 60.65。最优在 5 附近,太小描述子没有直接监督,太大压过检测损失。

正则 (表 7): 还能训练,61.0; 和 失败; 是 63.48; 掉到 57.45。也就是说,这个正则不能没有,也不能太强,最优值很窄。再强调一次,表 1 写的 与表 7/代码的 不一致,实际应按后者。

Hypercolumn:补充材料说,把 hypercolumn 换成 encoder 最后一层的双线性上采样,训练直接失败。这是架构上最关键的消融之一,说明多尺度特征不是锦上添花,而是让描述子足够可区分的前提。

网格大小 和接受门:论文没有给出单独的消融。这是一个空白: 是表 1 的超参数,但读者无法从论文知道 或 会怎样,也无法知道去掉接受门会掉多少。从设计上看, 控制空间密度和每格的搜索范围,接受门控制弃权,两者都值得单独扫。

退化到极点:论文在补充材料专门分析了「所有点都预测到极点」的风险。MegaDepth 的极点通常在图像外,所以不是问题;ACDC 和 Tokyo 24/7 的极点有时在图像内,理论上可能塌缩。作者认为有两道防线:描述子损失迫使描述子可区分,网格采样强制空间均匀;训练中从未观察到塌缩。这个分析是必要的,但也只是「没观察到」,不是形式化保证。

3.6 定性结果

MegaDepth 定性对比
图 5MegaDepth-1500 上的定性对比(原文图 5 与补充图 6 拼合)。每组三行,从上到下是 RIPE、ALIKED、DeDoDe;绿线是匹配。RIPE 的点分布更均匀,DeDoDe 更容易在少数高纹理区域聚集;ALIKED 与 RIPE 的分布接近,但 RIPE 在天空、墙面等低纹理区的点更少。

定性图支持论文的核心叙事:RIPE 不是靠更多的点取胜,而是靠把点放在可匹配、可被极线几何解释的地方。DeDoDe 的聚类问题在图中很明显——它倾向在少数区域反复出点,导致匹配线扎堆;RIPE 的分格采样天然抑制了这种聚集。ALIKED 的分布也不差,但它的训练用了 pose + homography,RIPE 只用二元标签。

四、局限性

4.1 论文自己承认的

  1. 训练不稳定,超参数敏感。官方配置里学习率 0.001 的注释直接写着「somewhat unstable」;、 以及过大的 都会让训练失败。最终模型能跑,但它不是一个「随便调都能收敛」的方法。
  2. 极点塌缩的风险。奖励只看极线内点数,理论上存在把所有点放到极点附近的退化解。论文的分析是「MegaDepth 极点常在图外;ACDC/Tokyo 有时在图内,但描述子损失 + 网格采样阻止了塌缩;实验中从未观察到」。这是经验结论,不是证明。
  3. 数据多样性不是单调收益。Aachen 上 Tokyo 比例从 20% 加到 40%,白天指标明显下降;Boreas 上 ACDC 也不是每个指标都涨。作者把原因归为 Tokyo 缺少视角变化,但这说明「多喂数据」需要选对数据。
  4. 恶劣天气的紧阈值仍然很低。Boreas 3cm/3° 只有个位数到十几个点,25cm/2° 才好看;论文没有回避这一点。
  5. 计算成本。单张 A100 训练 3 天,单图推理 0.47 s(DeDoDe 0.45 s、ALIKED 0.08 s、DISK 0.17 s)。它不是面向实时的方法。
  6. 标签的定义仍有门槛。「同一场景」要求足够多的共视 3D 点,不是任意两张同地点照片;正对的选择、负对的采样仍然会影响学到的东西。

4.2 我自己的观察

  1. 没有旋转/尺度鲁棒性的评测。训练变换只有 resize + pad + 归一化,没有随机旋转、尺度或透视增强;评测也集中在 MegaDepth/HPatches 这种以直立视角为主的基准。RaCo 的结果表明,学习型检测器在 45°、135° 这类中间旋转上会明显掉点[27],而 RIPE 没有针对这一点做实验。论文的「鲁棒」主要指光照、天气、视角,不是面内旋转。
  2. 训练和推理的选点机制不一致。训练时每格 categorical 采样 + 伯努利接受门,推理时整图 NMS + top-K,按原始 logit 排序;接受概率 在推理时并没有被显式计算。这未必是 bug——logit 排序和 排序在单调性上有关——但训练分布与推理分布确实不同,论文没有分析这个 gap。
  3. 奖励不惩罚定位误差。一个匹配只要极线残差小于 1 px 就拿 ,不管关键点落在角点的哪一侧。HPatches AUC@1px 37.93 低于 DeDoDe 的 39.88,可能部分来自这里:RIPE 优化的是「能匹配」,不是「像素级准」。如果下游是三角化或 BA,亚像素定位会很重要。
  4. 负对太容易。负对从随机另一个场景取,不是 hard negative;网络很快就能学会「不同场景不要匹配」。论文没有做 hard-negative 消融,也没有分析负对难度对学到特征的影响。对真实应用来说,最难的负对往往是「同一地点、不同时间/视角但无共视」,RIPE 目前的采样没有覆盖这种。
  5. 描述子损失的 fallback 会引入离群匹配。内点少于 8 时,损失在全部 MNN 上算,而不是只用内点;早期或难对图上,这可能把错误匹配当成正样本拉近。代码注释说这是为了「too few inliers」,但没有说明阈值 8 的依据。
  6. 论文内部数字有前后不一致。除了 的 vs ,表 7 标题把 写成 descriptor loss weight;式 (10) 前的文字说「final loss for the descriptor is 」,从上下文看应是 。这些不影响方法本身,但复现时要按代码和表 7,而不是表 1。
  7. 许可证限制。官方 LICENSE 是「Software Copyright License for Academic Use of RIPE, Version 2.0」,仅限非商业学术用途;商用需要另谈。这一点比很多 MIT/Apache 的局部特征项目更严格,落地前要确认。

五、结论与展望

RIPE 的价值不在于它把 MegaDepth 刷到第一——它没有,ALIKED 仍然高 1.6 个点,RoMa/Eff. LoFTR 这些稠密方法更高。它的价值在于把监督信号压到了理论上很低的限度,并且证明了这样仍然能训练出一个有竞争力的稀疏关键点提取器。二元「是否同场景」标签 + 极线几何奖励,让训练数据从「有深度/位姿/SfM 的互联网照片」扩展到「只要知道拍摄地点或时段的任意图像对」。Aachen 夜间和 Boreas 暴雪的结果说明,这种扩展不是玩具:加入 Tokyo 24/7 或 ACDC 后,模型确实在对应的域上变好。

从方法上看,最值得带走的三个设计是:

  • 分格 categorical 采样把离散选点变成可分解的 log-prob,让 REINFORCE 的 外和成立,同时保证空间覆盖;
  • 接受门复用选中 logit,用几乎零额外参数给网络一个弃权机制;
  • 极线 RANSAC 内点数当奖励,把「几何一致性」变成不需要位姿的监督信号。

这三者缺一不可:没有分格,采样会塌缩;没有接受门,天空也会出点;没有极线奖励,就退回需要位姿的 DISK/DaD 路线。

展望上,最直接的问题是效率和鲁棒性。 的稠密 log-prob 矩阵在 时是 2400 万项,虽然只有匹配项非零,但显存和计算仍然可观;如果能只采样部分格子对,或用一个可学习的匹配器替代 MNN+RANSAC,训练成本会降很多。另一个方向是把奖励做得更细:定位精度、hard negative、旋转/尺度一致性都可以进入奖励,而不只是「是否通过 1 px 极线检验」。RIPE 的作者在后续工作 RIPE++ 中已经把「只用正对、去掉负对、加入可训练匹配器」作为方向[28],但这篇论文本身的主线仍然是「二元标签 + 极线奖励」,RIPE++ 的细节不在本文范围内。

六、上手

官方仓库提供了推理 demo、MegaDepth/HPatches 评测和训练脚本。下面命令按 README 整理,安装和评测命令折叠起来;权重在第一次运行 demo.py 时自动下载到 /tmp/ripe_weights.pth。

安装与推理 demo
git clone https://github.com/fraunhoferhhi/RIPE
cd RIPE
mamba create -f conda_env.yml
mamba activate ripe-env
python demo.py

demo.py 会读取 assets/all_souls_000013.jpg 和 assets/all_souls_000055.jpg,调用 model.detectAndCompute(image, threshold=0.5, top_k=2048),做 MNN 和 RANSAC,最后把匹配图保存为 result_ransac.png。核心接口是:

kpts, desc, score = model.detectAndCompute(image, threshold=0.5, top_k=2048)

kpts 是 坐标,desc 是 256 维 L2 归一化描述子,score 是热图分数归一化到最大值后的排序分。注意 threshold 卡的是原始热图 logit,不是接受概率 ;top_k 在 NMS 之后按原始分数截断。

复现 MegaDepth-1500 与 HPatches
git clone https://github.com/JohannesK14/glue-factory
cd glue-factory
uv sync
uv run python -m gluefactory.eval.megadepth1500 --conf ripe+NN
uv run python -m gluefactory.eval.hpatches --conf ripe+NN

README 给出的期望值是 MegaDepth-1500 的 55.11/68.34/78.03 和 HPatches 的 37.93/58.93/69.20,与表 2 一致。评测走的是作者 fork 的 Glue Factory,ripe+NN 配置里 MegaDepth 长边 1600、HPatches top 1024、PoseLib RANSAC 阈值 0.5;这和论文正文的稀疏方法协议(长边 1200、top 2048)不完全一样,复现时以仓库配置为准。

训练:MegaDepth 子集,可选 Tokyo 24/7 / ACDC
# 先在仓库根目录创建 .env,写入 OUTPUT_DIR、DATA_DIR、SLURM_JOB_ID
cd data
bash download_disk_data.sh
cd ..

python ripe/train.py --config-name train project_name=train name=reproduce wandb_mode=offline

# 加入 20% Tokyo 24/7(对应 Aachen 夜间实验)
python ripe/train.py --config-name train project_name=train name=reproduce wandb_mode=offline data=megadepth+tokyo

# 加入 10% ACDC(对应 Boreas 恶劣天气实验)
python ripe/train.py --config-name train project_name=train name=reproduce wandb_mode=offline data=megadepth+acdc

训练脚本默认 80,000 步、batch 6、梯度累积 4、bf16-mixed,单张 A100 约 3 天。data=megadepth+tokyo 和 data=megadepth+acdc 的组合权重都是 20%/80%(Tokyo/ACDC 在前,MegaDepth 在后),与论文表 4 的 80% MegaDepth + 20% Tokyo 以及补充材料的 10% ACDC 设置一致。跑之前确认 DATA_DIR 下有 disk-data,可选数据在 Tokyo_Query_V3 和 ACDC 下。

参考

  1. Künzel, Hilsmann, Eisert. RIPE: Reinforcement Learning on Unlabeled Image Pairs for Robust Keypoint Extraction. ICCV 2025. arXiv · 代码
  2. DeTone, Malisiewicz, Rabinovich. SuperPoint: Self-Supervised Interest Point Detection and Description. CVPR Workshops 2018. arXiv · 代码
  3. Tyszkiewicz, Fua, Trulls. DISK: Learning Local Features with Policy Gradient. NeurIPS 2020. arXiv · 代码
  4. Edstedt, Bökman, Wadenbäck, Felsberg. DaD: Distilled Reinforcement Learning for Diverse Keypoint Detection. arXiv 2503.07347, 2025. arXiv · 代码
  5. Edstedt, Bökman, Wadenbäck, Felsberg. DeDoDe: Detect, Don't Describe — Describe, Don't Detect for Local Feature Matching. 3DV 2024. arXiv · 代码
  6. Bhowmik, Gumhold, Rother, Brachmann. Reinforced Feature Points: Optimizing Feature Detection and Description for a High-Level Task. CVPR 2020. arXiv
  7. Simonyan, Zisserman. Very Deep Convolutional Networks for Large-Scale Image Recognition. ICLR 2015. arXiv
  8. Edstedt, Athanasiadis, Wadenbäck, Felsberg. DKM: Dense Kernelized Feature Matching for Geometry Estimation. CVPR 2023. arXiv · 代码
  9. Hariharan, Arbeláez, Girshick, Malik. Hypercolumns for Object Segmentation and Fine-Grained Localization. CVPR 2015. CVF
  10. Potje, Cadar, Araujo, Martins, Nascimento. Enhancing Deformable Local Features by Jointly Learning to Detect and Describe Keypoints. CVPR 2023. arXiv · 代码
  11. Riba, Mishkin, Ponsa, Rublee, Bradski. Kornia: an Open Source Differentiable Computer Vision Library for PyTorch. WACV 2020. arXiv · 代码
  12. Larsson, contributors. PoseLib: Minimal Solvers for Camera Pose Estimation. GitHub, 2020. GitHub
  13. Li, Snavely. MegaDepth: Learning Single-View Depth Prediction from Internet Photos. CVPR 2018. arXiv
  14. Sun, Shen, Wang, Bao, Zhou. LoFTR: Detector-Free Local Feature Matching with Transformers. CVPR 2021. arXiv · 代码
  15. Balntas, Lenc, Vedaldi, Mikolajczyk. HPatches: A Benchmark and Evaluation of Handcrafted and Learned Local Descriptors. CVPR 2017. arXiv
  16. Sattler, Maddern, Toft, Torii, Hammarstrand, Stenborg, Safari, Okutomi, Pollefeys, Sivic, Kahl, Pajdla. Benchmarking 6DOF Outdoor Visual Localization in Changing Conditions. CVPR 2018. arXiv · 基准
  17. Burnett, Yoon, Wu, Li, Zhang, Lu, Qian, Tseng, Lambert, Leung, Schoellig, Barfoot. Boreas: A Multi-Season Autonomous Driving Dataset. The International Journal of Robotics Research, 42(1–2):33–42, 2023. arXiv · 项目页
  18. Jin, Mishkin, Mishchuk, Matas, Fua, Yi, Trulls. Image Matching across Wide Baselines: From Paper to Practice. International Journal of Computer Vision, 129:517–547, 2021. arXiv · 代码
  19. Zhao, Wu, Chen, Chen, Xu, Li. ALIKED: A Lighter Keypoint and Descriptor Extraction Network via Deformable Transformation. IEEE Transactions on Instrumentation and Measurement, 2023. arXiv · 代码
  20. Gleize, Wang, Feiszli. SiLK: Simple Learned Keypoints. ICCV 2023. arXiv · 代码
  21. Edstedt, Sun, Bökman, Wadenbäck, Felsberg. RoMa: Robust Dense Feature Matching. CVPR 2024. arXiv · 代码
  22. Wang, He, Peng, Tan, Zhou. Efficient LoFTR: Semi-Dense Local Feature Matching with Sparse-Like Speed. CVPR 2024. arXiv · 代码
  23. Leroy, Cabon, Revaud. Grounding Image Matching in 3D with MASt3R. ECCV 2024. arXiv · 代码
  24. Lowe. Distinctive Image Features from Scale-Invariant Keypoints. International Journal of Computer Vision, 60(2):91–110, 2004. DOI
  25. Torii, Arandjelovic, Sivic, Okutomi, Pajdla. 24/7 Place Recognition by View Synthesis. CVPR 2015. CVF
  26. Sakaridis, Dai, Van Gool. ACDC: The Adverse Conditions Dataset with Correspondences for Semantic Driving Scene Understanding. ICCV 2021. CVF
  27. Shenoi, Lindenberger, Sarlin, Pollefeys. RaCo: Ranking and Covariance for Practical Learned Keypoints. 3DV 2026. arXiv · 代码
  28. Künzel, Eisert, Hilsmann. RIPE++: Reinforced Keypoint Learning from Positive Pairs Only. LIMIT@ECCV 2026. arXiv · 代码

Comments