周报-230825

学习内容

1.论文

2.吴恩达深度学习(124/181)


学习时间

2023.08.15 — 2023.08.24


学习笔记

论文

针对上周的两个关键问题,进行了学习与调研。

问题1:用于训练的数据集的构建过程

首先来讲一讲 3DMatch 数据集。

camera-instrinsics.txt:

在相机参数矩阵(camera-intrinsics.txt)中,3x3 矩阵表示相机的内部参数,通常称为相机内参。这些参数描述了相机的光学性能和成像特性。在3DMatch数据集中,这些参数用于将世界坐标映射到图像坐标,从而实现图像的透视投影。在矩阵中,每个参数的含义如下:

| f_x   0   c_x |
|  0   f_y  c_y |
|  0    0    1  |
  • f_xf_y焦距(focal length)在 x 和 y 方向上的缩放因子。它们用于将图像的物理坐标转换为图像坐标。焦距越大,图像中的物体看起来就越大。
  • c_xc_y主点(principal point)的坐标,表示图像平面的中心在图像坐标系中的位置。它们通常与图像的中心对齐。

综合来说,相机参数矩阵用于定义一个相机的光学特性,包括焦距和主点,从而将三维世界中的点映射到二维图像平面上的对应点。这种映射允许在图像上表示真实世界的物体,以及在图像中测量和估计物体的尺寸和位置。

seq-*:

.color.png 为 rgb 图像,.depth.png 为灰度图像。

.pose.txt:是 4x4 的矩阵

在一个4x4的矩阵中,表示相机位姿的齐次坐标变换矩阵通常可以表示为:

R11  R12  R13  T1
R21  R22  R23  T2
R31  R32  R33  T3
0    0    0    1

其中,每个元素的含义如下:

  • R11, R12, R13, R21, R22, R23, R31, R32, R33:这些元素组成了一个3x3的旋转矩阵,描述了相机的旋转。这些值表示相机坐标系在世界坐标系中的方向。例如,R11 表示相机坐标系的 x 轴在世界坐标系中的旋转。
  • T1, T2, T3:这些元素是平移向量,描述了相机在世界坐标系中的平移。它们表示相机原点从相机坐标系原点移动到世界坐标系中的位置。
  • 最后一行的元素:由于齐次坐标需要一个额外的分量来实现平移,所以最后一行的前三个元素是0,最后一个元素是1,用于保持矩阵的结构。

总之,这个4x4的齐次坐标变换矩阵将相机的旋转和平移信息结合在一起,从相机坐标系变换到世界坐标系,可以用于将相机采集的数据(比如深度图像、颜色图像等)转换到一个共同的世界坐标系中。

再来讲讲如何把 3DMatch 数据集转换为 h5 文件。

def sample_matching_pairs:

从 seq 文件夹中,先把所有的内容分别转换为矩阵,并过滤掉不合法的信息。

随机选取深度图像中的合法的一点,然后根据深度 depth 和相机参数 K,得到相机坐标,再生成一个包围这一点的正方体。

与位姿矩阵进行矩阵乘法,再加上平移量,得到世界坐标的原点(注意,旋转量和平移量都是由位姿矩阵 .pose.txt 得到的)。

根据相机参数,将顶点坐标的正方体从相机坐标投影到图像坐标系,取 x,y 的范围。

根据 x,y 的范围,分别取得局部点云与图像块添加到集合中。

在 for loop 中提取一些点,先看看平移的距离跟第一个是否够远,太近会排除。

根据位姿的逆和世界坐标原点进行矩阵乘法,加上平移量得到该点相机坐标。

相机坐标投影得到图像坐标,然后观察深度是否变化过大,过大会重新选择。

然后的步骤与第一个点一样,根据相机参数,将顶点坐标的正方体从相机坐标投影到图像坐标系,取 x,y 的范围。

根据 x,y 的范围,分别取得局部点云与图像块添加到集合中。

# 生成匹配对的过程
def sample_matching_pairs(scene):
    patches = []
    # 获取深度图片集合
    frames = get_depth_frames(scene)
    # 这个 txt 是相机参数,代码的意思为将这个文件的内容转换为 np 数组,然后赋值给 K
    K = np.loadtxt(os.path.join(root, scene, "camera-intrinsics.txt"))

    # Pick a random depth frame
    path = np.random.choice(frames)
    # 相机位姿信息转换为矩阵
    T0 = np.loadtxt(path.replace(".depth.png", ".pose.txt"))
    # 将深度图像变为矩阵
    depth = np.array(Image.open(path)) * 0.001
    # 将颜色图像变为矩阵
    color = np.array(Image.open(path.replace(".depth.png", ".color.png")))
    # 过滤掉(设为 0)无效的深度信息?
    depth[depth > cutoff] = 0.0
    # 判断位姿信息中是否有为 NaN 的数据
    if np.isnan(np.sum(T0)):
        return None

    # Pick a random point P
    # u0,v0 分别为纵坐标与横坐标
    u0 = np.random.choice(depth.shape[1])
    v0 = np.random.choice(depth.shape[0])
    if depth[v0, u0] <= 0.0:
        return None

    # Compute bounding box
    # 获取深度值
    z = depth[v0, u0]
    x = (u0 - K[0, 2]) * z / K[0, 0]
    y = (v0 - K[1, 2]) * z / K[1, 1]
    # 建立针对相机的坐标
    p0 = np.array([x, y, z])
    # 由相机坐标 p0 与相机位姿信息 T0 得到世界坐标 q
    q = np.matmul(T0[0:3, 0:3], p0) + T0[0:3, 3]
    # 在相机坐标 p0 周围生成一个范围
    b = compute_bounding_box(p0)
    # 根据相机参数,将顶点坐标从相机坐标投影到图像坐标系
    b[:, 0] = np.round(b[:, 0] * K[0, 0] / b[:, 2] + K[0, 2])
    b[:, 1] = np.round(b[:, 1] * K[1, 1] / b[:, 2] + K[1, 2])

    # Get the depth patch
    # x,y 方向上投影的最小和最大像素坐标
    x = np.array([np.min(b[:, 0]), np.max(b[:, 0])], dtype=np.int32)
    y = np.array([np.min(b[:, 1]), np.max(b[:, 1])], dtype=np.int32)
    # 去除边缘值
    if np.any(x < 0) or np.any(x >= depth.shape[1]):
        return None
    if np.any(y < 0) or np.any(y >= depth.shape[0]):
        return None

    patch = {}
    # 获取该点的两个匹配块
    patch["cloud"] = extract_point_cloud(depth, color, x, y, p0, K)
    patch["color"] = extract_color_patch(color, x, y)
    patches += [patch]

    for i in range(num_samples):
        path = np.random.choice(frames)
        T1 = np.loadtxt(path.replace(".depth.png", ".pose.txt"))
        depth = np.array(Image.open(path)) * 0.001
        color = np.array(Image.open(path.replace(".depth.png", ".color.png")))
        # 计算两个相机姿态之间的平移(位置)差距
        distance = np.linalg.norm(T0[0:3, 3] - T1[0:3, 3])
        if distance < min_camera_distance:
            continue
        if np.isnan(np.sum(T1)):
            continue

        # Reproject point P into this frame
        # 相机姿态求逆
        T1 = np.linalg.inv(T1)
        # 相机位姿的逆矩阵的前 3x3 与世界坐标相乘,再加上第四列的三维向量
        p1 = np.matmul(T1[0:3, 0:3], q) + T1[0:3, 3]
        u1 = int(p1[0] * K[0, 0] / p1[2] + K[0, 2])
        v1 = int(p1[1] * K[1, 1] / p1[2] + K[1, 2])
        if u1 < 0 or u1 >= depth.shape[1]:
            continue
        if v1 < 0 or v1 >= depth.shape[0]:
            continue
        if depth[v1, u1] <= 0.0:
            continue
        # 判断深度值是否变化过大
        if abs(depth[v1, u1] - p1[2]) > threshold:
            continue

        # Compute the second bounding box
        z = depth[v1, u1]
        x = (u1 - K[0, 2]) * z / K[0, 0]
        y = (v1 - K[1, 2]) * z / K[1, 1]
        p1 = np.array([x, y, z])
        b = compute_bounding_box(p1)
        b[:, 0] = np.round(b[:, 0] * K[0, 0] / b[:, 2] + K[0, 2])
        b[:, 1] = np.round(b[:, 1] * K[1, 1] / b[:, 2] + K[1, 2])

        # Get the matching image patch
        x = np.array([np.min(b[:, 0]), np.max(b[:, 0])], dtype=np.int32)
        y = np.array([np.min(b[:, 1]), np.max(b[:, 1])], dtype=np.int32)
        if np.any(x < 0) or np.any(x >= depth.shape[1]):
            continue
        if np.any(y < 0) or np.any(y >= depth.shape[0]):
            continue

        patch = {}
        patch["cloud"] = extract_point_cloud(depth, color, x, y, p1, K)
        patch["color"] = extract_color_patch(color, x, y)
        patches += [patch]
        break

    if len(patches) <= 1:
        return None
    return patches

主函数:

有放回的抽样 seq,通过多次随机产生大数目的匹配对,然后保存到 h5 文件中。

size = 0
batch = []
while size < dataset_size:
    scene = np.random.choice(scenes)
    sample = sample_matching_pairs(scene)
    if sample is None:
        continue

    size += 1
    batch += [sample]
    print("Sample matching patches [{}/{}]".format(size, dataset_size))

    # Save batch if needed
    if len(batch) == batch_size:
        i = size // batch_size
        fname = "data/3dmatch/h5/train/{:04d}.h5".format(i)
        print("> Saving batch to {}...".format(fname))
        save_batch_h5(fname, batch)
        batch = []

问题2:评估指标

分为 4 个部分,分别是 2D,3D,2D-3D 匹配,深度估计

2D Matching precision:

假设图A与图B要进行匹配,我们先使用算法求出其中的特征描述子。然后,根据单应性矩阵,从图A映射到图B,再从图B映射到图A,去除非重复的特征描述子。

之后再对剩余的描述子进行求欧式距离,得到距离小于阈值的描述子对,这种就是匹配上了,是正确的案例。而对于没匹配上的,归作错误的案例。

再根据精度计算的公式,就能求出结果了。

图像特征点检测与匹配评价——量化_图像匹配 precision truth-rate 曲线_chauncywang_1580的博客-CSDN博客

3D registeration recall 代码

Ω ∗ \Omega_{*} Ω表示点云对中 correspondences 的数量, ( x ∗ , y ∗ ) (x_{*},y_{*}) (x,y)表示 G.T. 的 pair,T 表示基于 (i,j) 点云的变换。
E R M S E = 1 Ω ∗ ⋅ ∑ ( x ∗ , y ∗ ) ∈ Ω ∗ ∣ ∣ T i , j x ∗ − y ∗ ^ ∣ ∣ 2 E_{RMSE}=\sqrt{\frac{1}{\Omega_{*}}\cdot \sum_{(x_{*},y_{*})\in\Omega_{*}}||\hat{T_{i,j}x^{*}-y^{*}}||^{2}} ERMSE=Ω1(x,y)Ω∣∣Ti,jxy^2

"""
A collection of unrefactored functions.
"""
import os
import sys
import numpy as np
import argparse
import logging
import open3d as o3d

from lib.timer import Timer, AverageMeter

from util.misc import extract_features

from model import load_model
from util.file import ensure_dir, get_folder_list, get_file_list
from util.trajectory import read_trajectory, write_trajectory
from util.pointcloud import make_open3d_point_cloud, evaluate_feature_3dmatch
from scripts.benchmark_util import do_single_pair_matching, gen_matching_pair, gather_results

import torch

import MinkowskiEngine as ME

ch = logging.StreamHandler(sys.stdout)
logging.getLogger().setLevel(logging.INFO)
logging.basicConfig(
    format='%(asctime)s %(message)s', datefmt='%m/%d %H:%M:%S', handlers=[ch])

o3d.utility.set_verbosity_level(o3d.utility.VerbosityLevel.Error)


def extract_features_batch(model, config, source_path, target_path, voxel_size, device):

  folders = get_folder_list(source_path)
  assert len(folders) > 0, f"Could not find 3DMatch folders under {source_path}"
  logging.info(folders)
  list_file = os.path.join(target_path, "list.txt")
  f = open(list_file, "w")
  timer, tmeter = Timer(), AverageMeter()
  num_feat = 0
  model.eval()

  for fo in folders:
    if 'evaluation' in fo:
      continue
    files = get_file_list(fo, ".ply")
    fo_base = os.path.basename(fo)
    f.write("%s %d\n" % (fo_base, len(files)))
    for i, fi in enumerate(files):
      # Extract features from a file
      pcd = o3d.io.read_point_cloud(fi)
      save_fn = "%s_%03d" % (fo_base, i)
      if i % 100 == 0:
        logging.info(f"{i} / {len(files)}: {save_fn}")

      timer.tic()
      xyz_down, feature = extract_features(
          model,
          xyz=np.array(pcd.points),
          rgb=None,
          normal=None,
          voxel_size=voxel_size,
          device=device,
          skip_check=True)
      t = timer.toc()
      if i > 0:
        tmeter.update(t)
        num_feat += len(xyz_down)

      np.savez_compressed(
          os.path.join(target_path, save_fn),
          points=np.array(pcd.points),
          xyz=xyz_down,
          feature=feature.detach().cpu().numpy())
      if i % 20 == 0 and i > 0:
        logging.info(
            f'Average time: {tmeter.avg}, FPS: {num_feat / tmeter.sum}, time / feat: {tmeter.sum / num_feat}, '
        )

  f.close()


def registration(feature_path, voxel_size):
  """
  Gather .log files produced in --target folder and run this Matlab script
  https://github.com/andyzeng/3dmatch-toolbox#geometric-registration-benchmark
  (see Geometric Registration Benchmark section in
  http://3dmatch.cs.princeton.edu/)
  """
  # List file from the extract_features_batch function
  with open(os.path.join(feature_path, "list.txt")) as f:
    sets = f.readlines()
    sets = [x.strip().split() for x in sets]
  for s in sets:
    set_name = s[0]
    pts_num = int(s[1])
    matching_pairs = gen_matching_pair(pts_num)
    results = []
    for m in matching_pairs:
      results.append(do_single_pair_matching(feature_path, set_name, m, voxel_size))
    traj = gather_results(results)
    logging.info(f"Writing the trajectory to {feature_path}/{set_name}.log")
    write_trajectory(traj, "%s.log" % (os.path.join(feature_path, set_name)))


def do_single_pair_evaluation(feature_path,
                              set_name,
                              traj,
                              voxel_size,
                              tau_1=0.1,
                              tau_2=0.05,
                              num_rand_keypoints=-1):
  trans_gth = np.linalg.inv(traj.pose)
  i = traj.metadata[0]
  j = traj.metadata[1]
  name_i = "%s_%03d" % (set_name, i)
  name_j = "%s_%03d" % (set_name, j)

  # coord and feat form a sparse tensor.
  data_i = np.load(os.path.join(feature_path, name_i + ".npz"))
  coord_i, points_i, feat_i = data_i['xyz'], data_i['points'], data_i['feature']
  data_j = np.load(os.path.join(feature_path, name_j + ".npz"))
  coord_j, points_j, feat_j = data_j['xyz'], data_j['points'], data_j['feature']

  # use the keypoints in 3DMatch
  if num_rand_keypoints > 0:
    # Randomly subsample N points
    Ni, Nj = len(points_i), len(points_j)
    inds_i = np.random.choice(Ni, min(Ni, num_rand_keypoints), replace=False)
    inds_j = np.random.choice(Nj, min(Nj, num_rand_keypoints), replace=False)

    sample_i, sample_j = points_i[inds_i], points_j[inds_j]

    key_points_i = ME.utils.fnv_hash_vec(np.floor(sample_i / voxel_size))
    key_points_j = ME.utils.fnv_hash_vec(np.floor(sample_j / voxel_size))

    key_coords_i = ME.utils.fnv_hash_vec(np.floor(coord_i / voxel_size))
    key_coords_j = ME.utils.fnv_hash_vec(np.floor(coord_j / voxel_size))

    inds_i = np.where(np.isin(key_coords_i, key_points_i))[0]
    inds_j = np.where(np.isin(key_coords_j, key_points_j))[0]

    coord_i, feat_i = coord_i[inds_i], feat_i[inds_i]
    coord_j, feat_j = coord_j[inds_j], feat_j[inds_j]

  coord_i = make_open3d_point_cloud(coord_i)
  coord_j = make_open3d_point_cloud(coord_j)

  hit_ratio = evaluate_feature_3dmatch(coord_i, coord_j, feat_i, feat_j, trans_gth,
                                       tau_1)

  # logging.info(f"Hit ratio of {name_i}, {name_j}: {hit_ratio}, {hit_ratio >= tau_2}")
  if hit_ratio >= tau_2:
    return True
  else:
    return False


def feature_evaluation(source_path, feature_path, voxel_size, num_rand_keypoints=-1):
  with open(os.path.join(feature_path, "list.txt")) as f:
    sets = f.readlines()
    sets = [x.strip().split() for x in sets]

  assert len(
      sets
  ) > 0, "Empty list file. Makesure to run the feature extraction first with --do_extract_feature."

  tau_1 = 0.1  # 10cm
  tau_2 = 0.05  # 5% inlier
  logging.info("%f %f" % (tau_1, tau_2))
  recall = []
  for s in sets:
    set_name = s[0]
    traj = read_trajectory(os.path.join(source_path, set_name + "_gt.log"))
    assert len(traj) > 0, "Empty trajectory file"
    results = []
    for i in range(len(traj)):
      results.append(
          do_single_pair_evaluation(feature_path, set_name, traj[i], voxel_size, tau_1,
                                    tau_2, num_rand_keypoints))

    mean_recall = np.array(results).mean()
    std_recall = np.array(results).std()
    recall.append([set_name, mean_recall, std_recall])
    logging.info(f'{set_name}: {mean_recall} +- {std_recall}')
  for r in recall:
    logging.info("%s : %.4f" % (r[0], r[1]))
  scene_r = np.array([r[1] for r in recall])
  logging.info("average : %.4f +- %.4f" % (scene_r.mean(), scene_r.std()))


if __name__ == '__main__':
  parser = argparse.ArgumentParser()
  parser.add_argument(
      '--source', default=None, type=str, help='path to 3dmatch test dataset')
  parser.add_argument(
      '--source_high_res',
      default=None,
      type=str,
      help='path to high_resolution point cloud')
  parser.add_argument(
      '--target', default=None, type=str, help='path to produce generated data')
  parser.add_argument(
      '-m',
      '--model',
      default=None,
      type=str,
      help='path to latest checkpoint (default: None)')
  parser.add_argument(
      '--voxel_size',
      default=0.05,
      type=float,
      help='voxel size to preprocess point cloud')
  parser.add_argument('--extract_features', action='store_true')
  parser.add_argument('--evaluate_feature_match_recall', action='store_true')
  parser.add_argument(
      '--evaluate_registration',
      action='store_true',
      help='The target directory must contain extracted features')
  parser.add_argument('--with_cuda', action='store_true')
  parser.add_argument(
      '--num_rand_keypoints',
      type=int,
      default=5000,
      help='Number of random keypoints for each scene')

  args = parser.parse_args()

  device = torch.device('cuda' if args.with_cuda else 'cpu')

  if args.extract_features:
    assert args.model is not None
    assert args.source is not None
    assert args.target is not None

    ensure_dir(args.target)
    checkpoint = torch.load(args.model)
    config = checkpoint['config']

    num_feats = 1
    Model = load_model(config.model)
    model = Model(
        num_feats,
        config.model_n_out,
        bn_momentum=0.05,
        normalize_feature=config.normalize_feature,
        conv1_kernel_size=config.conv1_kernel_size,
        D=3)
    model.load_state_dict(checkpoint['state_dict'])
    model.eval()

    model = model.to(device)

    with torch.no_grad():
      extract_features_batch(model, config, args.source, args.target, config.voxel_size,
                             device)

  if args.evaluate_feature_match_recall:
    assert (args.target is not None)
    with torch.no_grad():
      feature_evaluation(args.source, args.target, args.voxel_size,
                         args.num_rand_keypoints)

  if args.evaluate_registration:
    assert (args.target is not None)
    with torch.no_grad():
      registration(args.target, args.voxel_size)

特征匹配评估函数 feature_evaluation 的每行代码如下所示,我将逐行解释每部分的作用以及与 Registration Recall 计算的关联:

def feature_evaluation(source_path, feature_path, voxel_size, num_rand_keypoints=-1):
  with open(os.path.join(feature_path, "list.txt")) as f:
    sets = f.readlines()
    sets = [x.strip().split() for x in sets]

  assert len(sets) > 0, "Empty list file. Makesure to run the feature extraction first with --do_extract_feature."

  tau_1 = 0.1  # 10cm
  tau_2 = 0.05  # 5% inlier
  logging.info("%f %f" % (tau_1, tau_2))
  recall = []
  for s in sets:
    set_name = s[0]
    traj = read_trajectory(os.path.join(source_path, set_name + "_gt.log"))
    assert len(traj) > 0, "Empty trajectory file"
    results = []
    for i in range(len(traj)):
      results.append(
          do_single_pair_evaluation(feature_path, set_name, traj[i], voxel_size, tau_1,
                                    tau_2, num_rand_keypoints))

    mean_recall = np.array(results).mean()
    std_recall = np.array(results).std()
    recall.append([set_name, mean_recall, std_recall])
    logging.info(f'{set_name}: {mean_recall} +- {std_recall}')
  for r in recall:
    logging.info("%s : %.4f" % (r[0], r[1]))
  scene_r = np.array([r[1] for r in recall])
  logging.info("average : %.4f +- %.4f" % (scene_r.mean(), scene_r.std()))

解释每行代码:

  1. 打开特征列表文件并解析:通过打开特征列表文件 "list.txt" 并逐行解析,将其中的信息读入到 sets 列表中。每个元素是一个包含了场景名和点云数量的子列表。
  2. 断言特征列表非空:如果 sets 列表为空,即没有读取到特征列表信息,则抛出断言错误,提醒用户需要先运行特征提取。
  3. 定义阈值 tau_1tau_2:这些阈值分别表示距离和百分比的阈值,用于判断特征匹配是否成功。
  4. 输出阈值信息:将阈值信息输出到日志。
  5. 初始化 recall 列表:用于存储每个场景的召回率。
  6. 遍历场景:对每个场景进行循环。
  7. 读取场景的Ground Truth变换:读取场景的Ground Truth变换数据,即实际的变换矩阵。
  8. 断言变换数据非空:如果读取到的变换数据为空,即没有Ground Truth变换数据,则抛出断言错误。
  9. 初始化结果列表 results:用于存储每个匹配对的评估结果。
  10. 对每个匹配对进行评估:通过遍历变换数据的每个时间步,调用 do_single_pair_evaluation 函数对该时间步的匹配对进行评估,并将结果存入 results 列表。
  11. 计算平均召回率:计算每个场景的匹配召回率的均值和标准差,并将其存入 recall 列表中。
  12. 输出场景召回率:将每个场景的匹配召回率输出到日志。
  13. 计算总体召回率:计算所有场景的召回率均值和标准差,并将其输出到日志。

在特征匹配评估函数中,主要的计算是在 do_single_pair_evaluation 函数中完成的,该函数会计算匹配对的映射变换矩阵,然后根据阈值判断匹配的准确性。与 Registration Recall 的计算对应关系在于,特征匹配的准确性与Ground Truth的匹配对之间的映射关系有关。在匹配准确的情况下,认为匹配成功,从而计入召回率的计算。

下面是对 do_single_pair_evaluation 函数每行代码的详细解释,以及与 Registration Recall 的计算关系:

def do_single_pair_evaluation(feature_path,
                              set_name,
                              traj,
                              voxel_size,
                              tau_1=0.1,
                              tau_2=0.05,
                              num_rand_keypoints=-1):
    trans_gth = np.linalg.inv(traj.pose)
    i = traj.metadata[0]
    j = traj.metadata[1]
    name_i = "%s_%03d" % (set_name, i)
    name_j = "%s_%03d" % (set_name, j)

    # coord and feat form a sparse tensor.
    data_i = np.load(os.path.join(feature_path, name_i + ".npz"))
    coord_i, points_i, feat_i = data_i['xyz'], data_i['points'], data_i['feature']
    data_j = np.load(os.path.join(feature_path, name_j + ".npz"))
    coord_j, points_j, feat_j = data_j['xyz'], data_j['points'], data_j['feature']

    # use the keypoints in 3DMatch
    if num_rand_keypoints > 0:
        # Randomly subsample N points
        Ni, Nj = len(points_i), len(points_j)
        inds_i = np.random.choice(Ni, min(Ni, num_rand_keypoints), replace=False)
        inds_j = np.random.choice(Nj, min(Nj, num_rand_keypoints), replace=False)

        sample_i, sample_j = points_i[inds_i], points_j[inds_j]

        key_points_i = ME.utils.fnv_hash_vec(np.floor(sample_i / voxel_size))
        key_points_j = ME.utils.fnv_hash_vec(np.floor(sample_j / voxel_size))

        key_coords_i = ME.utils.fnv_hash_vec(np.floor(coord_i / voxel_size))
        key_coords_j = ME.utils.fnv_hash_vec(np.floor(coord_j / voxel_size))

        inds_i = np.where(np.isin(key_coords_i, key_points_i))[0]
        inds_j = np.where(np.isin(key_coords_j, key_points_j))[0]

        coord_i, feat_i = coord_i[inds_i], feat_i[inds_i]
        coord_j, feat_j = coord_j[inds_j], feat_j[inds_j]

    coord_i = make_open3d_point_cloud(coord_i)
    coord_j = make_open3d_point_cloud(coord_j)

    hit_ratio = evaluate_feature_3dmatch(coord_i, coord_j, feat_i, feat_j, trans_gth,
                                       tau_1)

    # logging.info(f"Hit ratio of {name_i}, {name_j}: {hit_ratio}, {hit_ratio >= tau_2}")
    if hit_ratio >= tau_2:
        return True
    else:
        return False

解释每行代码:

  1. 计算Ground Truth变换的逆变换:将场景的Ground Truth变换矩阵求逆,得到变换矩阵 trans_gth,用于评估预测的变换矩阵与Ground Truth之间的误差。
  2. 获取变换矩阵涉及的点云序号:获取当前匹配对中的两个点云序号。
  3. 构建点云文件名:构建每个点云的文件名。
  4. 从文件加载点云特征:从特征文件中加载坐标、点云数据和特征数据。
  5. 提取随机关键点:如果指定了随机关键点数量,则随机从点云中选择一些点作为关键点,以减少计算量。
  6. 构建关键点哈希表:使用哈希函数构建关键点和坐标的哈希表,以便快速筛选出关键点对应的坐标。
  7. 筛选出关键点对应的坐标和特征:通过哈希表的筛选,获取关键点对应的坐标和特征。
  8. 构建Open3D点云对象:将坐标转换为Open3D点云对象。
  9. 计算特征匹配的召回率:调用 evaluate_feature_3dmatch 函数,基于特征匹配评估预测的变换矩阵与Ground Truth之间的召回率,即匹配的准确性。
  10. 判断

召回率是否达到阈值 tau_2:如果召回率大于等于阈值 tau_2,则认为匹配成功,返回 True,否则返回 False

在这个函数中,关键的计算是在 evaluate_feature_3dmatch 函数中进行的,该函数会计算预测的变换矩阵与Ground Truth之间的匹配召回率,与 Registration Recall 的计算对应关系在于,它衡量了特征匹配是否在一定误差范围内准确地预测了点云之间的变换。

以下是对 evaluate_feature_3dmatch 函数每行代码的详细解释,以及与 Registration Recall 的计算关系:

def evaluate_feature_3dmatch(coord_i, coord_j, feat_i, feat_j, trans_gth, tau_1=0.1):
    num_corr = 0
    num_corr_gth = 0
    trans_pred = np.eye(4)

    # Calculate the transformation matrix from the matched feature correspondences
    corr_inds = np.argmin(
        scipy.spatial.distance.cdist(feat_i, feat_j, metric='euclidean'),
        axis=1)
    num_corr = len(corr_inds)

    corr_pts_i = coord_i[corr_inds]
    corr_pts_j = coord_j

    trans_pred = trans_pred_registration(
        corr_pts_i, corr_pts_j)  # Calculate predicted transformation matrix

    # Calculate the transformation error between predicted and ground truth matrices
    trans_err = np.linalg.inv(trans_gth) @ trans_pred

    # Calculate the transformation error magnitude
    trans_err_magnitude = np.sqrt(
        trans_err[0, 3]**2 + trans_err[1, 3]**2 + trans_err[2, 3]**2)

    # Calculate the percentage of inliers (matched points) within the specified error threshold
    if trans_err_magnitude <= tau_1:
        num_corr_gth = num_corr

    hit_ratio = num_corr_gth / float(len(corr_inds))
    return hit_ratio

解释每行代码:

  1. 初始化计数变量:初始化变量 num_corr 用于记录匹配点对的数量,num_corr_gth 用于记录匹配点对数量中在Ground Truth误差范围内的点对数量,trans_pred 用于存储预测的变换矩阵。
  2. 计算特征匹配的索引:通过计算特征之间的欧式距离,获取最匹配的点对的索引。
  3. 获取匹配点对的坐标:从对应索引中获取匹配点对的坐标。
  4. 预测变换矩阵:通过匹配点对的坐标,计算预测的变换矩阵。
  5. 计算变换矩阵的误差:计算预测的变换矩阵与Ground Truth之间的误差矩阵。
  6. 计算误差的幅值:计算误差矩阵的平移部分的幅值,表示匹配点对的预测变换误差的大小。
  7. 计算在阈值范围内的匹配率:如果预测的变换误差幅值小于等于阈值 tau_1,则将匹配点对数量记录在 num_corr_gth 中。
  8. 计算召回率:计算匹配点对数量中在Ground Truth误差范围内的点对占总匹配点对数量的比例,作为匹配的召回率。

这个函数的关键是在计算匹配点对的预测变换矩阵,以及在指定误差范围内匹配的召回率。这个召回率的计算与 Registration Recall 的概念相符,衡量了预测的匹配是否在一定误差范围内准确。

2D-3D

在 Torri 的 《24/7 place recognition by view synthesis》一文中,在 Experiments 章节的第三段中,提到了召回率的计算。

Evaluation metric. The query place is deemed correctly recognized if at least one of the top N retrieved database images is within d = 25 meters from the ground truth position of the query. This is a common place recognition metric used e.g. in [8, 35, 40]. The percentage of correctly recognized queries (Recall) is then plotted for different values of N .

在 Arandjelovic 的《NetVLAD: CNN architecture for weakly supervised place recognition》中,也在 Experiments 章节的 6.1 中,提到了召回率的计算方式。

Evaluation metric. We follow the standard place recognition evaluation procedure [5], [6], [8], [9], [10]. The query image is deemed correctly localized if at least one of the top N retrieved database images is within d = 25 meters from the ground truth position of the query. The percentage of correctly recognized queries (Recall) is then plotted for different values of N . For Tokyo 24/7 we follow [10] and perform spatial non-maximal suppression on ranked database images before evaluation.

关于具体的代码,我在本段中找了相关的论文进行泛读,结果是目前还没有找到代码,不过分析了没找到的原因:

首先看论文原文:
2D-3D place recognition
We further evaluated our local cross-domain descriptor with 2D-to-3D place recognition. Unlike previous works in singledomain place recognition (Torii et al. 2015; Arandjelovic et al. 2016), our task is to find the corresponding 3D geometry submap in the database given a query 2D image. We only assume that raw geometries are given, without additional associated image descriptors and/or camera information as often seen in the camera localization problem (Zeisl, Sattler, and Pollefeys 2015; Sattler et al . 2015). With increasing availability of 3D data, such 2D-3D place recognition becomes practical as it allows using Internet photos to localize a place in 3D. To the best of our knowledge, there has been no previous report about solving this cross-domain problem.
Here we again use the SceneNN dataset (Hua et al . 2016).Following the split from (Hua, Tran, and Yeung 2018), we use20 scenes for evaluation. To generate geometry submaps, we integrate every 100 consecutive RGB-D frames. In total, our database is consisted of 1, 191 submaps from various lighting conditions and settings such as office, kitchen, bedroom, etc. The query images are taken directly from the RGB frames, such that every submap has at least one associated image.
We cast this 2D-3D place recognition problem as an retrieval task. Inspired by the approach from DenseVLAD (Torii et al . 2015), for each 3D submap, we sample descriptors on a regular voxel grid. These descriptors are then aggregate into a single compact VLAD descriptor (J ́egou et al. 2010), using a dictionary of size 64. To extract the descriptor for an image, we follow the same process, but on a 2D grid. The query descriptor are matched with the database to retrieve the final results.
We follow the standard place recognition evaluation procedure (Torii et al. 2015; Arandjelovic et al . 2016). The query image is deemed to be correctly localized if at least one of the top N retrieved database submaps is within d = 0.5 mand θ = 30◦ from the ground-truth pose of the query.
We plot the fraction of correct queries (recall@N) for different value of N , as shown in Figure 4. Representative top-3 retrieval results are shown in Figure 5. It can be seen that our local cross-domain descriptor are highly effective in this 2D-to-3D retrieval task.

1.这篇文章对于 2D-3D 位置识别方法比较新颖。这就导致了很难在前面的论文中找到评估的源代码。

2.这周还没有对 SceneNN 数据集进行具体的了解。

3.评估指标在论文中说的非常详细。

深度预测

来自论文 FCRN,以下是进行的操作

这段代码是用来使用预训练的 FCRN(Fully Convolutional Regression Network)模型来预测输入图像的深度信息。以下是代码的解读:

  1. 导入必要的库:

    import argparse
    import os
    import numpy as np
    import tensorflow as tf
    from matplotlib import pyplot as plt
    from PIL import Image
    import models
    
  2. 定义预测函数 predict,接受预训练模型的路径和输入图像路径作为参数:

    def predict(model_data_path, image_path):
    
  3. 设置默认的输入图像尺寸、通道数、批大小:

        height = 228
        width = 304
        channels = 3
        batch_size = 1
    
  4. 读取输入图像,将其调整为指定的尺寸,转换为浮点型数组并扩展维度以适应网络的输入格式:

        img = Image.open(image_path)
        img = img.resize([width, height], Image.ANTIALIAS)
        img = np.array(img).astype('float32')
        img = np.expand_dims(np.asarray(img), axis=0)
    
  5. 创建 TensorFlow 占位符,用于输入图像数据:

        input_node = tf.placeholder(tf.float32, shape=(None, height, width, channels))
    
  6. 构建 FCRN 网络,这里使用了一个 ResNet50UpProj 模型,通过传入占位符和其他参数来构建网络:

        net = models.ResNet50UpProj({'data': input_node}, batch_size, 1, False)
    
  7. 创建 TensorFlow 会话:

        with tf.Session() as sess:
    
  8. 加载预训练的模型参数:

            print('Loading the model')
            saver = tf.train.Saver()
            saver.restore(sess, model_data_path)
    
  9. 使用网络对输入图像进行深度预测:

            pred = sess.run(net.get_output(), feed_dict={input_node: img})
    
  10. 绘制深度预测结果,使用 Matplotlib 将预测结果可视化:

           fig = plt.figure()
           ii = plt.imshow(pred[0, :, :, 0], interpolation='nearest')
           fig.colorbar(ii)
           plt.show()
  1. 返回预测的深度图像数据:
           return pred
  1. 主函数 main
   def main():
       parser = argparse.ArgumentParser()
       parser.add_argument('model_path', help='Converted parameters for the model')
       parser.add_argument('image_paths', help='Directory of images to predict')
       args = parser.parse_args()

       pred = predict(args.model_path, args.image_paths)

       os._exit(0)

   if __name__ == '__main__':
       main()

在主函数中,通过命令行参数传入预训练模型的路径和输入图像的路径,然后调用 predict 函数进行深度预测并进行可视化。整个代码的目的是使用预训练的 FCRN 模型对输入图像进行深度预测,并将预测结果进行可视化展示。


深度学习

1.2 边缘检测示例

卷积运算:是卷积神经网络的重要组成部分,计算方式如下:

让 3*3 的卷积核(kernel,也叫过滤器 filter)在图像灰度矩阵上移动,贴合着图像,然后逐元素相乘后相加得到结果。

使用右侧这个卷积核可以标清楚区域的边界,左边和右边可以分别得到纵向的结果。如第二张图。

在这里插入图片描述

在这里插入图片描述

1.3 更多边缘检测的内容(More edge detection)

卷积核的数字组合是多种多样的,比如 Sobel filter: [ 1 0 − 1 2 0 − 2 1 0 − 1 ] \begin{bmatrix} 1 & 0 & -1\\2&0&-2\\1&0&-1 \end{bmatrix} 121000121 ,Scher filter: [ 3 0 − 3 10 0 − 10 3 0 − 3 ] \begin{bmatrix} 3 & 0 & -3\\10&0&-10\\3&0&-3 \end{bmatrix} 31030003103 ,此外,还可以通过反向传播来训练边缘检测卷积核,都能够达到不错的效果。

1.4 Padding

在 1.2 节中,我们提到了卷积计算,使用 3x3 的卷积核对 6x6 图像进行运算,最终会得到一个 4x4 的输出图像。如果我们一直进行计算,图像会越来越小,更何况深度网络有很多层。因此,为了解决这个问题,我们可以填充边界,假设输入图像为 nxn,卷积核为 fxf,那么输出维度就是 ( n − f + 1 ) ⋅ ( n − f + 1 ) (n - f + 1)\cdot(n - f + 1) (nf+1)(nf+1),因此,为了保持原有的图像大小,我们可以对边缘进行填充,填充的大小为 p 时,输出大小为 ( n + 2 p − f + 1 ) ⋅ ( n + 2 p − f + 1 ) (n + 2p -f + 1)\cdot(n + 2p -f + 1) (n+2pf+1)(n+2pf+1),我们把这种操作叫做 Same Convolution。对于之前讲的不填充的做法叫做 Valid Convolution

1.5 卷积步长(Strided convolutions)

步幅是卷积核每次移动的距离。

比如 7x7 的图像,使用 3x3 卷积核,最终输出结果为 3x3 的图像。

输出的维度如下:其中 n 表示输入图像的大小,p 表示图像进行填充时的长度,f 表示卷积核的大小,s 表示卷积步长。(向下取整)
n + 2 p − f s + 1 \frac{n+2p-f}{s}+1 sn+2pf+1
双重镜像操作(以 /为轴翻转)在深度学习中可省略。

1.6 三维卷积(Convolutions over volumes)

当需要训练的不是像素灰度图时,比如使用 RGB 图像时,有三个颜色通道,那么就有三层,我们这时使用 fxfx3 的卷积核来进行卷积。比如,输入是一个 6x6x3 的图像,卷积核为 3x3x3,输出的维度为 4x4x1 。计算方式和之前的卷积类似。

我们可以使用不同的卷积核进行处理,结果按照顺序排列,让输出维度的第三位增加,达到识别水平、垂直特征的输出。

某些文献中,会把通道说成深度,注意不要和神经网络的深度搞混。

1.7 单层卷积网络(One layer of a convolutional network)

每一个卷积核充当了之前神经网络里的 w,就相当于一个 w 的集合。以下是一些参数维度的定义:

nh 和 nw 分别表示长和宽,m 表示一个 batch 内样本的数量。

在这里插入图片描述

1.8 简单卷积网络示例(A simple Convolution network example)

含有多层的卷积的例子。对照一下维度等参数。

在卷积的过程和这个例子中,长和宽在不断减小,图像的 channels 在不断的增加。

在这里插入图片描述

1.9 池化层(Pooling Layers)

移动方式与卷积类似,但是计算方式不是。

最大池化(Max Pooling)指的是每次找到区域内元素的最大值,并保留。

平均池化,就是求区域内的平均值。

最大池化只是在计算神经网络某一层的静态属性。

1.10 卷积神经网络示例(Convolutional neural network example)

池化层不单独占一层,它一般与前一个进行的卷积组成同层。因为池化层中的计算没有权重,不能称之为“学习”,但是,在某些资料中可能会把池化层单独设为一层。

全连接层:如 1.8 中的网络结构示例,在最后会把卷积起来的元素平摊成一个向量,然后使用该向量进行计算(类似前面所讲的神经网络),得到一个新的向量,把该层称之为全连接层。

可以根据数据的维度获得 activation size,在计算的过程中,activation size 的下降不能过快,否则会影响神经网络的性能。

在这里插入图片描述

1.11 为什么使用卷积?(Why Convolutions?)

主要优势:

  • 参数共享:特征检测器对于图片中的任何部分具有通用性。识别图片的任何一个位置都能达到可观的效果。
  • 稀疏连接:卷积计算的结果中的每一个元素只受它附近输入元素的影响。

可以预防过拟合,减小训练集。

卷积也是要迭代优化的,可以采用梯度下降等方式训练网络。

2.2 经典网络(Classic networks)

介绍了 LeNet-5,AlexNet,VGGNet,如下图。

我认为这些论文是需要去阅读的。顺序为 Alex – VGG – LeNet。

LeNet-5 每个过滤器的 channels 与它上一层的 channels 相同,因为当时计算机的运行速度非常慢,为了减少计算量与参数,该网络使用了非常复杂的计算方式。该网络有 6 万个参数。

在这里插入图片描述

AlexNet 比 LeNet-5 要大的多。该网络在当时论文中写到,把网络布置在了两个 GPU 上进行训练,通过某种方式用于两个 GPU 之间的交流。该网络还提出了 LRN 层,具体做法是选择一个位置,穿过整个 channels,进行归一化处理,当然这种方法的效果不大,因此没有广泛运用。**在这篇论文之后,学者们相信深度网络可以作用于计算机视觉,并开始了不断的探索。**该网络有约 600 万个参数。

在这里插入图片描述

VGG 网络十分规整,**Conv 采用 same 模式,channels 加倍,每次 Pool 长宽减半。**约 1.38 亿个参数。还有一个 VGG-19,跟 VGG-16 的性能差不多。

在这里插入图片描述

2.3 残差网络(Residual Networks(ResNets))

残差网络中,把某一层的输出值 a,直接加到后一层的激活函数中(即 a [ l + 2 ] = g ( z [ l + 2 ] + a [ l ] ) a^{[l+2]}=g(z^{[l+2]}+a[l]) a[l+2]=g(z[l+2]+a[l])),这样做可以防止错误率随着长时间的训练而上升(第二章的 1.8 中,在描述 early stopping 里),如下图:
在这里插入图片描述

2.4 残差网络为什么有用?(Why ResNets work?)

很多时候,残差网络不会对真正的输出结果产生坏的影响。我们观察表达式,可以看到在最后一层过激活函数的时候,仍然有 al 的出现。如果 zl+1 为 0,那么也不会对结果产生任何影响。

此外,维度不同的时候,需要在 a 前面乘一个矩阵调整维度,这个矩阵不需要我们自己来写(它是网络通过学习得到的)。

2.5 网络中的网络以及 1x1 卷积(Network in Network and 1x1 Convolutions)

我认为,本质上,它可以用来压缩或者增加 channels,可以横向对比 Pooling,Pooling 是一种压缩长宽的方式。

2.6 Inception 网络(Inception Network)

Inception 网络的作用就是帮你决定,过滤器的大小与方式。

同时,可以使用 1x1 卷积,来进行网络缩放从而减少运算次数。

第二张图为 googleNet,是用来致敬 LeNet 的。注意到其中很多层都是 Inception Network 的变体。中间突出来的分支用于计算与调和结果,具有防止过拟合的效果。

在这里插入图片描述

在这里插入图片描述

3.1 目标定位(Object localization)

现在不仅仅要识别图像里的是什么东西了,我们还需要用方框把目标定位起来。

这就需要调整标签 y,这是一个例子:

  • 是否有对象,pc = 0 or 1
  • 对象的高,宽,中心点(假设左上为 (0, 0))
  • 对象是个什么,就有几个值

然后是 loss 的定义,可以对不同的值使用不同的 loss,比如对于 pc 使用 logistic regression 的 loss,对象的数值(高、宽)使用方差等等。

3.2 特征点检测(Landmark detection)

比如要识别某些特征点时,该怎么做?

要让标签 y 中有这些输出,需要进行标注,把 x,y 值记录下来。

3.3 目标检测(Object detection)

当图片中含有多个目标时,如果我们想都检测出来,我们就要进行滑动窗口进行卷积,使用一个小于图片的规模,提取图片中的一小块,然后像卷积一样,使用步幅进行移动。

这样计算成本可能会很大,但是已经有了很好的解决方式,如下图。

我们建立了对于小规模块的网络以后,可以直接把网络参数用于大的图像,然后返回多个结果,每一列对应一块的结果。

在这里插入图片描述


  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值