eloftr特征匹配结果可视化

一、EfficientLoFTR实验结果复现

论文链接:zju3dv.github.io/efficientloftr/files/EfficientLoFTR.pdf

代码链接:zju3dv/EfficientLoFTR (github.com)

测试运行:由于weights文件中只有outdoor权重,因此以室外数据集测试为主,命令如下

bash scripts/reproduce_test/outdoor_full_auc.sh

运行结果如下:

二、评估指标解释

2.1 AUC指标

如下图所示,考虑一个分类问题,横坐标为置信度(或对应任务的误差),橙色虚线为设置的阈值大小,不同的阈值对应不同的混淆矩阵,可以得到多个混淆矩阵

TPR和FPR定义如下,每一个混淆矩阵对应一个TPR和一个FPR

ROC曲线如下图所示,曲线上每一个点对应一个混淆矩阵

TPR和FPR指标的分母是固定的,分别为数据集中正样本和负样本的数量

对于分子,希望TP更大,FP更小 即TPR更大,FPR更小

对应ROC曲线中左上角区域

量化评估:曲线下的区域面积越大越好(AUC) 

参考资料:【小萌五分钟】机器学习 | 模型评估: ROC曲线与AUC值_哔哩哔哩_bilibili

2.2 eloftr评估指标

  • auc@10: 0.7166339555133289

AUC@10: 表示在误差容忍度为10像素时的ROC曲线下面积(Area Under the Curve)。这个值越高,表示在特征点匹配任务中,误差小于10像素的情况下,模型的性能越好。

值0.7166表示在10像素误差下,模型的特征匹配性能较好。

  • auc@20: 0.8318667754522098

AUC@20: 表示在误差容忍度为20像素时的ROC曲线下面积。

值0.8318表示在20像素误差下,模型的特征匹配性能更好。

  • auc@5: 0.5551221177666241

AUC@5: 表示在误差容忍度为5像素时的ROC曲线下面积。

值0.5551表示在5像素误差下,模型的特征匹配性能相对较低。

  • num_matches:3288.1826666666666

num_matches: 表示模型在测试数据集上找到的匹配对的数量。这是一个统计指标,表示特征匹配算法找到了多少个特征点对。

  • prec@5e-04: 0.96871016885266293

Precision@5e-04: 在5e-04(0.0005)阈值下的精度。精度(Precision)表示在所有预测为正例的匹配对中,真正为正例的比例。

值0.9687表示在误差阈值为0.0005的情况下,模型的特征匹配预测非常准确。

三、匹配结果可视化

由于并未给出直观的特征匹配结果图,因此自己补充代码以便查看效果

3.1 匹配结果存储:batch变量

文件src/lightning/lightning_loftr.py

test_step函数:处理每个测试批次,进行模型前向传播,记录计算时间,并计算指标

test_epoch_end函数:在测试结束后聚合所有批次的指标,计算平均匹配时间,并记录测试结果。

中间变量batch(dict类型),其keys包括:

'bs' = {int} 1

'pair_names' = {list:2} [['xxx.jpg'],['xxx.jpg']]'

'image0' = {Tensor:(1,1,480,640)}

'image1' = {Tensor:(1,1,480,640)}

'mkpts0_f' = {Tensor:(2339,2)}

'mkpts1_f' = {Tensor:(2339,2)}

'mconf' = {Tensor:(2339,)}

其中'image0' 'image1'中的值处于0-1,应该是图像标准化结果,因此在可视化匹配结果的过程中没有使用它们,而是根据'pair_names'直接导入的原图。

3.2 完整代码

    def draw_matches(self,batch):
        img0_path = os.path.join('data/scannet/test/', batch['pair_names'][0][0])
        image0 = cv2.imread(img0_path)

        img1_path = os.path.join('data/scannet/test/', batch['pair_names'][1][0])
        image1 = cv2.imread(img1_path)
        mkpts0_f = batch['mkpts0_f']
        mkpts1_f = batch['mkpts1_f']
        mconf = batch['mconf']

        mkpts0_f = mkpts0_f.cpu().numpy()
        mkpts1_f = mkpts1_f.cpu().numpy()
        mconf = mconf.cpu().numpy()

        height1,width1 = image0.shape[:2]
        height2,width2 = image1.shape[:2]
        new_height = max(height1,height2)
        new_width = width1 + width2

        stitched_image = np.zeros((new_height, new_width, 3), dtype=np.uint8)
        stitched_image[:height1, :width1, :3] = image0
        stitched_image[:height2, width1:width1+width2,:3] = image1

        mkpts1_f_shifted = mkpts1_f + np.array([width1,0])

        mconf_norm = (mconf - np.min(mconf))/(np.max(mconf)-np.min(mconf))

        colors = plt.cm.jet(1-mconf_norm)
        for pt1, pt2, color in zip(mkpts0_f, mkpts1_f_shifted, colors):
            pt1 = tuple(map(int,pt1))
            pt2 = tuple(map(int,pt2))

            cv2.line(stitched_image,pt1,pt2,color[:3]*255,2)
            cv2.circle(stitched_image,pt1,5,color,-1)
            cv2.circle(stitched_image,pt2,5,color,-1)

        return stitched_image

    def save_match_result(self,batch,batch_idx):

        result_image = self.draw_matches(batch)
        output_path = os.path.join(self.dump_dir, f'match_{batch_idx}.png')
        cv2.imwrite(output_path, result_image)
        print(f"Matching result saved to {self.dump_dir}")

四、其他记录

4.1 报错

image =cv2.resize(image,resize)
cv2.error: 0pencv(4.4.0)/tmp/pip-reg-build-99ib2vsi/opencv/modules/imgproc/src/resize.cpp:3929:error:(-215:Assertion failed)!ssize.empty()in function 'resize'

原因:图像路径错误

在调用 cv2.resize 函数之前,源图像 (ssize) 为空或没有正确加载

4.2 参数配置

4.3 cv的一些基本操作

(其中操作图像需要保证其是Numpy数组,即ndarray)

  • 设置路径
# 路径拼接
img1_path = scene0707_00/color/15.jpg
img_path = os.path.join('data/test/', img1_path)

# 输出路径及文件名字设置
dump_dir = 'dump/eloftr_full_sacnnet'
output_path = os.path.join(dump_dir, f'match_{batch_idx}.png')  # 其中batch_idx为变量
  • 图像的读写
#其中image_path和output_path均需要明确文件扩展名来确定图像的格式(如'.jpg','png','.bmp'等)
# image_path = 'xxx/img.jpg'
# output_path = 'xxx/output_name.jpg'
image = cv2.imread(image_path)
cv2.imwrite(output_path, result_image)
  • 打印查看
# 打印数据x的类型
print(type(x))

# 打印dict数据的键值
print(example_dict.keys())

4.4 Tensor与Numpy

4.4.1 报错

File "EfficientLofTR-main/src/lightning/lightning loftr.py", line 138, in draw matches
stitched_image[:height1, :width1,:3]= image0/ tensor.py", line 972, in __array_.File "/home/puzek/anaconda3/envs/eloftr/lib/python3.8/site-packages/torch/
return self.numpy().astype(dtype, copy=False)
TypeError: can't convert cuda:1 device type tensor to numpy. Use Tensor.cpu() to copy the tensor to host memory first.

原因:正在尝试将一个在 GPU 上的 PyTorch tensor 直接转换为 NumPy 数组,而 PyTorch tensor 必须在 CPU 上才能进行这种转换。你需要将 tensor 从 GPU 移动到 CPU,然后再进行转换。你可以使用 Tensor.cpu() 方法将 tensor 移动到 CPU。

if isinstance(image0, torch.Tensor):
    image0 = image0.cpu().numpy()

4.4.2 Tensor与Numpy

TensorNumPy 数组都是多维数组(或张量),它们在深度学习和科学计算中非常常用。以下是 Tensor(主要指 PyTorch 的 Tensor)和 NumPy 数组之间的一些关键区别:

1. 所属库

  • Tensor:主要由深度学习框架(如 PyTorch、TensorFlow)提供。
  • NumPy 数组:由 NumPy 库提供,这是一个广泛用于科学计算的 Python 库。

2. 功能和用途

  • Tensor:设计用于深度学习,可以在 GPU 上运行,支持自动微分(autograd)功能,这对于梯度计算和反向传播至关重要。
  • NumPy 数组:设计用于一般的数值计算和科学计算,提供丰富的线性代数、傅里叶变换、统计等功能,但不具备 GPU 支持和自动微分功能。

3. 设备支持

  • Tensor:可以在 CPU 和 GPU 上运行。通过 .to(device) 方法,可以将张量在不同设备之间移动。
  • NumPy 数组:只能在 CPU 上运行。

4. 自动微分

  • Tensor:支持自动微分,这是深度学习模型训练所必需的。通过 requires_grad 属性,可以跟踪所有操作并自动计算梯度。
  • NumPy 数组:不支持自动微分。

5. 操作和兼容性

  • Tensor:支持的大部分操作与 NumPy 类似,并且 PyTorch 提供了很多与 NumPy 兼容的接口。
  • NumPy 数组:提供了丰富的科学计算函数,但没有深度学习框架的特定功能。

6. 转换

  • Tensor 转 NumPy
import torch
tensor = torch.tensor([1, 2, 3])
numpy_array = tensor.numpy()  # 注意:这个操作是共享内存的
  • NumPy 转 Tensor
import torch
import numpy as np
numpy_array = np.array([1, 2, 3])
tensor = torch.tensor(numpy_array)  # 这是一个拷贝操作
# 或者
tensor = torch.from_numpy(numpy_array)  # 这个操作是共享内存的
  • 示例代码
import torch
import numpy as np

# 创建一个 NumPy 数组
numpy_array = np.array([[1, 2, 3], [4, 5, 6]])
print("NumPy Array:")
print(numpy_array)

# 将 NumPy 数组转换为 PyTorch Tensor
tensor = torch.tensor(numpy_array)
print("\nTensor from NumPy Array:")
print(tensor)

# 将 Tensor 转换回 NumPy 数组
numpy_array_from_tensor = tensor.numpy()
print("\nNumPy Array from Tensor:")
print(numpy_array_from_tensor)

# 检查共享内存
print("\nCheck Shared Memory:")
print(f"NumPy array memory id: {id(numpy_array_from_tensor)}")
print(f"Tensor memory id: {id(tensor)}")

# 创建一个新的 PyTorch Tensor(在 GPU 上)
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
tensor_gpu = torch.tensor([[7, 8, 9], [10, 11, 12]], device=device)
print(f"\nTensor on {device}:")
print(tensor_gpu)

# 将 GPU Tensor 转换为 CPU NumPy 数组
numpy_array_from_tensor_gpu = tensor_gpu.cpu().numpy()
print("\nNumPy Array from GPU Tensor:")
print(numpy_array_from_tensor_gpu)

4.4.3 修改图像维度

如果是 PyTorch tensor,转换为numpy后通常是 (channels, height, width),需要先转换为 (height, width, channels)

# 如果通道维度在前面 (1, H, W),调整为 (H, W, 1) 并复制到 3 通道
if image0.ndim == 3 and image0.shape[0] == 1:
    image0 = np.repeat(image0.squeeze(0), 3, axis=-1)

若要将形状为 (1, 480, 640) 的图像转换为 (480, 640),你需要去掉第一个维度。可以使用 NumPy 的 squeeze() 方法来完成这项工作

if image0.ndim == 3 and image0.shape[0] == 1:
    image0 = np.squeeze(image0, axis=0)  # 去掉第一个维度

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值