Point Transformer V3(PTv3)【4:体素采样GridSample】

PTV3专题目录

序列化编码

降采样SerializedPooling

上采样SerializedUnpooling

背景

GridSample源码

class GridSample(object):
    def __init__(
        self,
        grid_size=0.05,
        hash_type="fnv",
        mode="train",
        return_inverse=False,
        return_grid_coord=False,
        return_min_coord=False,
        return_displacement=False,
        project_displacement=False,
    ):
        self.grid_size = grid_size
        self.hash = self.fnv_hash_vec if hash_type == "fnv" else self.ravel_hash_vec
        assert mode in ["train", "test"]
        self.mode = mode
        self.return_inverse = return_inverse
        self.return_grid_coord = return_grid_coord
        self.return_min_coord = return_min_coord
        self.return_displacement = return_displacement
        self.project_displacement = project_displacement

        self.logger = get_root_logger()

    def __call__(self, data_dict):
        assert "coord" in data_dict.keys()
        num_points = data_dict["coord"].shape[0]
        scaled_coord = data_dict["coord"] / np.array(self.grid_size)
        grid_coord = np.floor(scaled_coord).astype(int)
        min_coord = grid_coord.min(0)
        grid_coord -= min_coord
        scaled_coord -= min_coord
        min_coord = min_coord * np.array(self.grid_size)
        key = self.hash(grid_coord)
        idx_sort = np.argsort(key)
        key_sort = key[idx_sort]
        _, inverse, count = np.unique(key_sort, return_inverse=True, return_counts=True)
        if self.mode == "train":  # train mode
            idx_select = (
                np.cumsum(np.insert(count, 0, 0)[0:-1])
                + np.random.randint(0, count.max(), count.size) % count
            )
            idx_unique = idx_sort[idx_select]
            if "sampled_index" in data_dict:
                # for ScanNet data efficient, we need to make sure labeled point is sampled.
                idx_unique = np.unique(
                    np.append(idx_unique, data_dict["sampled_index"])
                )
                mask = np.zeros_like(data_dict["segment"]).astype(bool)
                mask[data_dict["sampled_index"]] = True
                data_dict["sampled_index"] = np.where(mask[idx_unique])[0]
            data_dict = index_operator(data_dict, idx_unique)
            if self.return_inverse:
                data_dict["inverse"] = np.zeros_like(inverse)
                data_dict["inverse"][idx_sort] = inverse
            if self.return_grid_coord:
                data_dict["grid_coord"] = grid_coord[idx_unique]
                if "grid_coord" not in data_dict["index_valid_keys"]:
                    data_dict["index_valid_keys"].append("grid_coord")
            if self.return_min_coord:
                data_dict["min_coord"] = min_coord.reshape([1, 3])
            if self.return_displacement:
                displacement = (
                    scaled_coord - grid_coord - 0.5
                )  # [0, 1] -> [-0.5, 0.5] displacement to center
                if self.project_displacement:
                    displacement = np.sum(
                        displacement * data_dict["normal"], axis=-1, keepdims=True
                    )
                data_dict["displacement"] = displacement[idx_unique]
                if "displacement" not in data_dict["index_valid_keys"]:
                    data_dict["index_valid_keys"].append("displacement")
            # if data_dict['coord'].shape[0] > 50000 or data_dict['coord'].shape[0] < 50:
            #     self.logger.info(f"GridSample: {num_points} points sampled to {idx_unique.shape[0]} grid cells.")
            #     print(f"GridSample: {num_points} points sampled to {idx_unique.shape[0]} grid cells.")
            return data_dict

        elif self.mode == "test":  # test mode
            data_part_list = []
            for i in range(count.max()):
                idx_select = np.cumsum(np.insert(count, 0, 0)[0:-1]) + i % count
                idx_part = idx_sort[idx_select]
                data_part = index_operator(data_dict, idx_part, duplicate=True)
                data_part["index"] = idx_part
                if self.return_inverse:
                    data_part["inverse"] = np.zeros_like(inverse)
                    data_part["inverse"][idx_sort] = inverse
                if self.return_grid_coord:
                    data_part["grid_coord"] = grid_coord[idx_part]
                    if "grid_coord" not in data_part["index_valid_keys"]:
                        data_part["index_valid_keys"].append("grid_coord")
                if self.return_min_coord:
                    data_part["min_coord"] = min_coord.reshape([1, 3])
                if self.return_displacement:
                    displacement = (
                        scaled_coord - grid_coord - 0.5
                    )  # [0, 1] -> [-0.5, 0.5] displacement to center
                    if self.project_displacement:
                        displacement = np.sum(
                            displacement * data_dict["normal"], axis=-1, keepdims=True
                        )
                    data_part["displacement"] = displacement[idx_part]
                    if "displacement" not in data_part["index_valid_keys"]:
                        data_part["index_valid_keys"].append("displacement")
                data_part_list.append(data_part)
            return data_part_list
        else:
            raise NotImplementedError

    @staticmethod
    def ravel_hash_vec(arr):
        """
        Ravel the coordinates after subtracting the min coordinates.
        """
        assert arr.ndim == 2
        arr = arr.copy()
        arr -= arr.min(0)
        arr = arr.astype(np.uint64, copy=False)
        arr_max = arr.max(0).astype(np.uint64) + 1

        keys = np.zeros(arr.shape[0], dtype=np.uint64)
        # Fortran style indexing
        for j in range(arr.shape[1] - 1):
            keys += arr[:, j]
            keys *= arr_max[j + 1]
        keys += arr[:, -1]
        return keys

    @staticmethod
    def fnv_hash_vec(arr):
        """
        FNV64-1A
        """
        assert arr.ndim == 2
        # Floor first for negative coordinates
        arr = arr.copy()
        arr = arr.astype(np.uint64, copy=False)
        hashed_arr = np.uint64(14695981039346656037) * np.ones(
            arr.shape[0], dtype=np.uint64
        )
        for j in range(arr.shape[1]):
            hashed_arr *= np.uint64(1099511628211)
            hashed_arr = np.bitwise_xor(hashed_arr, arr[:, j])
        return hashed_arr

基本功能

GridSample 在用于将点云数据体素化,这在处理大规模3D点云时非常常见。它通过将点云划分为规则的网格(或体素)来减少计算复杂性,并为后续的3D卷积神经网络准备数据。需要注意的是在train和test模式下有一些不一样。

GridSample 的工作原理

GridSample 的主要目标是为数据增强和网络输入做准备。它执行以下操作:

  1. 体素化 (Voxelization)

    • 它将输入的点云(包含 coordcolornormal 等属性)按照指定的 grid_size(体素大小)进行量化。每个点都被分配到一个唯一的体素中。
    • 例如,如果一个点的坐标是 (1.23, 2.34, 3.45)grid_size0.02,那么它会被量化到索引为 (61, 117, 172) 的体素中(计算方式为 floor(coord / grid_size))。
  2. 采样 (Sampling in Voxel)

    • 在训练时,为了增加数据的多样性并控制每个体素中的点数,它会在每个体素内部进行随机采样。如果一个体素包含了多个点,它会随机选择一个点来代表这个体素。这有助于防止模型对特定点的分布过拟合。
    • 在测试时,对于每个网格中的点,循环count.max()次进行采样
  3. 数据增强 (Data Augmentation)

    • GridSample 通常在一个数据增强流水线中被调用。在它执行之前,通常会应用一些增强操作,如随机旋转、缩放、翻转等。GridSample 负责将增强后的点云转换为网络可以处理的体素格式。
  4. 结果分析(train模式)

    • 输入:5个点,其中2个点在同一个体素(由 grid_size=0.02 定义),另外3个点在另一个体素。
    • 处理过程
      1. GridSample 计算出这5个点分属于2个唯一的体素。
      2. 由于是 train 模式,它会从第一个体素的2个点中随机选择1个。
      3. 然后,从第二个体素的3个点中随机选择1个。
    • 输出:最终得到2个点,每个点都是从其所在体素中随机采样出来的。coordcolorsegment 等属性也相应地被采样。

这种方法在训练时既能有效减少数据量,又能通过随机性引入噪声,起到数据增强的作用,从而提升模型的泛化能力。

具体实现(train模式)

输入示例

# 配置
config = {
    "grid_size": 0.025,  # 2.5厘米的网格大小
    "hash_type": "fnv",  # 使用FNV哈希函数
    "mode": "train",     # 训练模式
    "return_inverse": False
}

# 输入5个3D点的点云数据
data_dict = {
    "coord": np.array([
        [-0.048, -0.023,  0.015],  # P0
        [-0.045, -0.020,  0.018],  # P1
        [ 0.027,  0.031, -0.042],  # P2
        [ 0.052, -0.028,  0.035],  # P3
        [ 0.029,  0.033, -0.040]   # P4
    ])
}

逐行详细解析(包含具体计算)

  1. 检查输入数据并获取点数
assert "coord" in data_dict.keys()
num_points = data_dict["coord"].shape[0]
  • 目的:确保数据有效性并记录点数
  • 计算结果
num_points = 5  # 输入点云共有5个点
  1. 计算缩放后的坐标
scaled_coord = data_dict["coord"] / np.array(self.grid_size)
  • 目的:将物理坐标转换为网格坐标
  • 计算过程和结果
scaled_coord = [
    [-0.048/0.025, -0.023/0.025,  0.015/0.025],  # P0
    [-0.045/0.025, -0.020/0.025,  0.018/0.025],  # P1
    [ 0.027/0.025,  0.031/0.025, -0.042/0.025],  # P2
    [ 0.052/0.025, -0.028/0.025,  0.035/0.025],  # P3
    [ 0.029/0.025,  0.033/0.025, -0.040/0.025]   # P4
]
# 结果
scaled_coord = [
    [-1.92, -0.92,  0.60],  # P0
    [-1.80, -0.80,  0.72],  # P1
    [ 1.08,  1.24, -1.68],  # P2
    [ 2.08, -1.12,  1.40],  # P3
    [ 1.16,  1.32, -1.60]   # P4
]
  1. 计算网格坐标
grid_coord = np.floor(scaled_coord).astype(int)
  • 目的:确定每个点所属的网格
  • 计算过程和结果
# np.floor对每个坐标向下取整
grid_coord = [
    [-2, -1,  0],  # P0 所在网格
    [-2, -1,  0],  # P1 所在网格(与P0在同一网格)
    [ 1,  1, -2],  # P2 所在网格
    [ 2, -2,  1],  # P3 所在网格
    [ 1,  1, -2]   # P4 所在网格(与P2在同一网格)
]
  1. 计算并应用坐标偏移
min_coord = grid_coord.min(0)
grid_coord -= min_coord
scaled_coord -= min_coord
  • 目的:将坐标系移动到非负区域
  • 计算过程和结果
# 步骤1:找到每个维度的最小值
min_coord = [-2, -2, -2]  # 每个维度的最小网格坐标

# 步骤2:网格坐标平移
grid_coord = [
    [0, 1, 2],  # P0
    [0, 1, 2],  # P1
    [3, 3, 0],  # P2
    [4, 0, 3],  # P3
    [3, 3, 0]   # P4
]

# 步骤3:缩放坐标平移
scaled_coord = [
    [0.08, 1.08, 2.60],  # P0
    [0.20, 1.20, 2.72],  # P1
    [3.08, 3.24, 0.32],  # P2
    [4.08, 0.88, 3.40],  # P3
    [3.16, 3.32, 0.40]   # P4
]
  1. 计算物理空间的最小坐标
min_coord = min_coord * np.array(self.grid_size)
  • 计算结果
min_coord = [-2 * 0.025, -2 * 0.025, -2 * 0.025]
         = [-0.05, -0.05, -0.05]  # 米为单位
  1. 计算哈希值
key = self.hash(grid_coord)
  • 计算结果
key = [
    1234567,  # P0的哈希值
    1234567,  # P1的哈希值(与P0相同)
    8901234,  # P2的哈希值
    5678901,  # P3的哈希值
    8901234   # P4的哈希值(与P2相同)
]
  1. 获取排序索引
idx_sort = np.argsort(key)
key_sort = key[idx_sort]
  • 计算结果
# 按哈希值排序后的索引
idx_sort = [0, 1, 3, 2, 4]  # 意味着点的顺序为:P0,P1,P3,P2,P4

# 排序后的哈希值
key_sort = [1234567, 1234567, 5678901, 8901234, 8901234]
  1. 计算唯一值信息
_, inverse, count = np.unique(key_sort, return_inverse=True, return_counts=True)
  • 计算结果
_ = [1234567, 5678901, 8901234]  # 唯一的哈希值
inverse = [0, 0, 1, 2, 2]        # 每个点对应的唯一值索引
count = [2, 1, 2]                # 每个唯一值的出现次数
  1. 训练模式下的随机采样
if self.mode == "train":
    idx_select = (
        np.cumsum(np.insert(count, 0, 0)[0:-1])
        + np.random.randint(0, count.max(), count.size) % count
    )
  • 计算过程和结果
# 步骤1:插入0并计算累积和
np.insert(count, 0, 0) = [0, 2, 1, 2]
np.insert(count, 0, 0)[0:-1] = [0, 2, 1]
np.cumsum([0, 2, 1]) = [0, 2, 3]

# 步骤2:生成随机偏移
np.random.randint(0, 2, 3) = [1, 0, 1]  # 示例随机数
[1, 0, 1] % [2, 1, 2] = [1, 0, 1]

# 步骤3:计算最终索引
idx_select = [0, 2, 3] + [1, 0, 1] = [1, 2, 4]
  1. 获取最终选择的点
idx_unique = idx_sort[idx_select]
  • 计算结果
idx_sort = [0, 1, 3, 2, 4]
idx_select = [1, 2, 4]
idx_unique = [1, 3, 4]  # 最终选择了P1, P3, P4这三个点
  1. 更新数据字典
data_dict = index_operator(data_dict, idx_unique)
  • 最终结果
data_dict = {
    "coord": np.array([
        [-0.045, -0.020,  0.018],  # P1 (从第一个网格选择)
        [ 0.052, -0.028,  0.035],  # P3 (从第三个网格选择)
        [ 0.029,  0.033, -0.040]   # P4 (从第二个网格选择)
    ])
}

这样的降采样结果保证了:

  1. 每个网格中最多选择一个点
  2. 选择过程具有随机性(训练模式)
  3. 保持了点云的空间分布特征
  4. 实现了数据量的减少(从5个点降至3个点)

具体实现(test模式)

输入示例

# 配置
config = {
    "grid_size": 0.025,  # 2.5厘米的网格大小
    "hash_type": "fnv",  # 使用FNV哈希函数
    "mode": "test",      # 测试模式
    "return_inverse": False
}

# 输入点云数据(5个点)
data_dict = {
    "coord": np.array([
        [-0.048, -0.023,  0.015],  # P0
        [-0.045, -0.020,  0.018],  # P1
        [ 0.027,  0.031, -0.042],  # P2
        [ 0.052, -0.028,  0.035],  # P3
        [ 0.029,  0.033, -0.040]   # P4
    ])
}

测试模式的处理流程

  1. 前期处理与训练模式相同
# 1-7. 前期处理(与训练模式相同)
scaled_coord = data_dict["coord"] / np.array(self.grid_size)
grid_coord = np.floor(scaled_coord).astype(int)
min_coord = grid_coord.min(0)
grid_coord -= min_coord
scaled_coord -= min_coord
min_coord = min_coord * np.array(self.grid_size)
key = self.hash(grid_coord)
idx_sort = np.argsort(key)
key_sort = key[idx_sort]
_, inverse, count = np.unique(key_sort, return_inverse=True, return_counts=True)

结果与之前相同:

# 经过处理后的网格坐标
grid_coord = [
    [0, 1, 2],  # P0
    [0, 1, 2],  # P1
    [3, 3, 0],  # P2
    [4, 0, 3],  # P3
    [3, 3, 0]]  # P4

# count的值(每个唯一体素中点的数量)
count = [2, 1, 2]  # 体素A有2个点,体素B有1个点,体素C有2个点
  1. 测试模式的特殊处理
elif self.mode == "test":  # test mode
    data_part_list = []
    for i in range(count.max()):
        idx_select = np.cumsum(np.insert(count, 0, 0)[0:-1]) + i % count
        idx_part = idx_sort[idx_select]
        data_part = index_operator(data_dict, idx_part, duplicate=True)
        ...

关键区别:

  • 训练模式:每个体素随机选择一个点
  • 测试模式:依次选择每个体素中的所有点,生成多个数据部分

详细执行过程

假设 count = [2, 1, 2],则 count.max() = 2,意味着需要执行两次循环:

第一次循环 (i=0):
# 1. 计算选择索引
np.insert(count, 0, 0) = [0, 2, 1, 2]
np.insert(count, 0, 0)[0:-1] = [0, 2, 1]
np.cumsum([0, 2, 1]) = [0, 2, 3]
i % count = [0, 0, 0]
idx_select = [0, 2, 3]

# 2. 获取实际点索引
idx_part = idx_sort[idx_select] = [0, 3, 2]  # 选择P0, P3, P2

# 3. 创建第一个数据部分
data_part = {
    "coord": np.array([
        [-0.048, -0.023,  0.015],  # P0(第一个体素的第一个点)
        [ 0.052, -0.028,  0.035],  # P3(第二个体素的第一个点)
        [ 0.027,  0.031, -0.042]   # P2(第三个体素的第一个点)
    ])
}
第二次循环 (i=1):
# 1. 计算选择索引
i % count = [1, 0, 1]
idx_select = [1, 2, 4]

# 2. 获取实际点索引
idx_part = idx_sort[idx_select] = [1, 3, 4]  # 选择P1, P3, P4

# 3. 创建第二个数据部分
data_part = {
    "coord": np.array([
        [-0.045, -0.020,  0.018],  # P1(第一个体素的第二个点)
        [ 0.052, -0.028,  0.035],  # P3(第二个体素的第一个点)
        [ 0.029,  0.033, -0.040]   # P4(第三个体素的第二个点)
    ])
}

额外信息的处理

对每个 data_part,还可以根据配置添加额外信息:

if self.return_inverse:
    data_part["inverse"] = np.zeros_like(inverse)
    data_part["inverse"][idx_sort] = inverse

if self.return_grid_coord:
    data_part["grid_coord"] = grid_coord[idx_part]

if self.return_min_coord:
    data_part["min_coord"] = min_coord.reshape([1, 3])

if self.return_displacement:
    displacement = scaled_coord - grid_coord - 0.5
    if self.project_displacement:
        displacement = np.sum(
            displacement * data_dict["normal"], 
            axis=-1, 
            keepdims=True
        )
    data_part["displacement"] = displacement[idx_part]

最终返回结果

return data_part_list  # 包含两个data_part的列表

测试模式的意义

  1. 完整性

    • 训练模式:每个体素只保留一个点
    • 测试模式:保留所有点,确保不丢失信息
  2. 预测稳定性

    • 通过处理所有可能的点组合
    • 可以进行多次预测后取平均等集成操作
  3. 评估准确性

    • 能够评估模型在不同采样策略下的表现
    • 有助于理解模型的鲁棒性
  4. 应用灵活性

    • 可以根据具体应用选择使用哪些预测结果
    • 支持后处理和结果融合

这种设计使得模型在测试阶段能够充分利用所有点的信息,提高预测的可靠性和稳定性。通过生成多个数据部分,还可以进行集成学习或后处理,进一步提升模型性能。

QA

一、data_dict["inverse"][idx_sort] = inverse,这一步的原理及作用

为了理解这行代码,我们需要先回顾一下它之前的几个关键变量:

  1. coord: 原始输入点云的坐标,形状为 (N, 3),其中 N 是总点数。
  2. key: 根据每个点的网格坐标计算出的哈希值(或索引值),形状为 (N,)key[i] 代表第 i 个点所在的网格的唯一标识。
  3. idx_sort: 对 key 数组进行排序后得到的索引数组,形状为 (N,)。它记录了原始点在排序后的新位置。例如,key[idx_sort] 会得到一个排好序的 key 数组。
  4. key_sort: key[idx_sort] 的结果,即排好序的哈希值数组。
  5. inverse: np.unique(key_sort, return_inverse=True) 的第二个返回值。它的作用是,对于 key_sort 中的每一个元素,inverse 给出了它在 unique_keys(唯一哈希值数组)中的索引。简单来说,inverse 将每个排好序的点映射到了一个唯一的体素ID(从 0 到 M-1,M是体素总数)。

代码原理:一步步解析

data_dict["inverse"][idx_sort] = inverse 这行代码是一个非常巧妙的 “逆向排序” 或 “恢复顺序” 的操作。

  • data_dict["inverse"]: 在这行代码之前,它被初始化为一个全零数组,长度与原始点云数量 N 相同。
  • inverse: 这个数组的顺序是根据排序后的点key_sort)来确定的。inverse[j] 对应的是排序后第 j 个点的体素ID。
  • idx_sort: 这个数组存储了从排序后位置原始位置的映射。idx_sort[j] 是排序后第 j 个点在原始点云中的索引

现在,我们把它们组合起来看 data_dict["inverse"][idx_sort] = inverse

这个操作相当于说:
"对于从 j = 0N-1 的每一个 j

  1. 找到排序后第 j 个点在原始数组中的索引,即 p = idx_sort[j]
  2. 找到排序后第 j 个点的体素ID,即 v = inverse[j]
  3. 将这个体素ID v 赋值给 data_dict["inverse"] 中索引为 p 的位置。即 data_dict["inverse"][p] = v。"

最终结果是:
经过这个操作,data_dict["inverse"] 数组就建立了一个从原始点云索引到其所属体素ID的直接映射。也就是说,data_dict["inverse"][i] 的值就是原始第 i 个点所在的体素的唯一ID。

举个例子:

原始索引coordkeyidx_sortkey_sortinverse (体素ID)
0[1.1, 2.3, 3.4]1002500
1[5.6, 6.7, 7.8]20001001
2[0.1, 0.2, 0.3]5031001
3[1.2, 2.4, 3.5]10012002
  • idx_sort = [2, 0, 3, 1] (因为 key50 最小在索引2,100 在索引0和3,200 最大在索引1)
  • inverse = [0, 1, 1, 2] (体素ID:50->0, 100->1, 200->2)

执行 data_dict["inverse"][idx_sort] = inverse,即 data_dict["inverse"][[2, 0, 3, 1]] = [0, 1, 1, 2]

  • data_dict["inverse"][2] = 0
  • data_dict["inverse"][0] = 1
  • data_dict["inverse"][3] = 1
  • data_dict["inverse"][1] = 2

最终得到的 data_dict["inverse"][1, 2, 0, 1]
我们可以验证一下:

  • 原始点0的key是100,体素ID是1。
  • 原始点1的key是200,体素ID是2。
  • 原始点2的key是50,体素ID是0。
  • 原始点3的key是100,体素ID是1。
    结果完全正确!

核心作用

这个 inverse 映射是连接原始密集点云稀疏体素网格的关键桥梁,主要有两个核心作用:

  1. 特征上采样(Upsampling)
    在点云分割任务中,网络(特别是稀疏卷积网络)通常在体素级别进行计算和预测。当网络输出每个体素的预测结果(例如,这个体素属于“桌子”类)后,我们需要将这个预测结果传递回原始的、更密集的点云上。
    有了 inverse 映射,这个过程就变得非常简单。假设 voxel_predictions 是一个包含了每个体素预测结果的数组,那么 voxel_predictions[data_dict["inverse"]] 就可以直接将每个原始点映射到其对应的预测结果。

  2. 特征聚合(Downsampling/Pooling)
    虽然在这个 GridSample 实现中,它主要用于上采样,但在其他场景下,这个映射同样可以用于将点云特征聚合到体素上。例如,你可以使用 torch_scatter 库,依据 inverse 提供的分组信息,将同一个体素内的所有点的颜色或法向量等特征进行平均或最大池化,从而为每个体素生成一个聚合后的特征向量。

总结来说,data_dict["inverse"][idx_sort] = inverse 这一步的根本目的就是为了高效地生成一个查找表(data_dict["inverse"]),这个表记录了每个原始点属于哪个体素。这个查找表是后续将稀疏体素空间的处理结果映射回原始密集点云空间的关键。

针对点云分割中从体素类别推广到点云类别

比如上面四个点,三个体素,知道了0、1、3三个点的类别后(即知道了体素类别),如何获取所有点的类别呢?
这正是 inverse 映射的核心应用场景。

假设我们已经通过某种方式(比如网络预测)知道了每个体素的类别,现在需要将这个类别信息传递给原始点云中的每一个点

继续使用上面的例子:

  • 原始点云:4个点 (索引 0, 1, 2, 3)
  • 体素:3个 (ID 0, 1, 2)
  • data_dict["inverse"] (我们已经计算出): [1, 2, 0, 1]
    • 这表示:点0在体素1,点1在体素2,点2在体素0,点3在体素1。

这里需要稍微精确一下:在实际流程中,我们是通过对每个体素进行预测来得到其类别的。GridSample 在训练时会从每个体素中采样一个代表点(idx_unique),网络会对这些代表点进行预测,其预测结果就作为该体素的类别

假设网络预测后,我们得到了每个体素的类别:

  • 体素 0 的类别是 L0 (比如: “桌子”)
  • 体素 1 的类别是 L1 (比如: “椅子”)
  • 体素 2 的类别是 L2 (比如: “地板”)

我们可以创建一个体素类别查找表 voxel_labels
voxel_labels = [L0, L1, L2]


如何获取所有点的类别?

答案非常简单,只需要一步索引操作:

all_point_labels = voxel_labels[data_dict["inverse"]]

让我们来分解这个操作:

  1. data_dict["inverse"][1, 2, 0, 1]
  2. voxel_labels[L0, L1, L2]
  3. 执行 voxel_labels[[1, 2, 0, 1]]
    • voxel_labels 的第1个元素 -> L1
    • voxel_labels 的第2个元素 -> L2
    • voxel_labels 的第0个元素 -> L0
    • voxel_labels 的第1个元素 -> L1

所以,all_point_labels 的结果是 [L1, L2, L0, L1]
这一步和我们之前在point.feat[inverse]的实现,其实就是根据inverse把特征进行复制操作吧中的原理是一样的,这里面的核心是需要理解numpy数组操作:c=a[b],这部分的解释我们放在第二个QA里说明下


结果解读

我们得到的 all_point_labels 数组的长度和原始点云一样,并且每个位置对应一个点的类别:

  • 原始点 0 的类别是 L1 (“椅子”)
  • 原始点 1 的类别是 L2 (“地板”)
  • 原始点 2 的类别是 L0 (“桌子”)
  • 原始点 3 的类别是 L1 (“椅子”)

这和我们最初的设定是完全一致的:

  • 点0和点3都在体素1内,所以它们都获得了体素1的类别 L1
  • 点1在体素2内,获得了类别 L2
  • 点2在体素0内,获得了类别 L0

总结:
一旦你有了 (1) 体素类别查找表(2) inverse 映射,你就可以通过一次简单的数组索引,非常高效地将稀疏的体素预测结果“广播”或“上采样”到密集的原始点云中的每一个点上,从而完成分割任务。

二、numpy数组中voxel_labels[data_dict["inverse"]]这个操作的原理

这在 NumPy 中被称为高级索引 (Advanced Indexing)花式索引 (Fancy Indexing),是 NumPy 强大功能的核心之一。

基本原理

当你用一个数组(或列表)去索引另一个 NumPy 数组时,NumPy 不会像切片那样返回一个连续的块,而是会根据索引数组中的每一个元素,从被索引的数组中挑选出相应的元素,然后将这些挑选出的元素组成一个新的数组返回。新数组的形状与索引数组的形状相同。

我们把它拆解成两个部分:

  1. 数据源数组 (被索引的数组)voxel_labels
  2. 索引数组data_dict["inverse"]
操作过程详解

假设我们有:

  • voxel_labels = np.array(['桌子', '椅子', '地板'])
    • 这是一个长度为 3 的数组。索引 0 对应 ‘桌子’,索引 1 对应 ‘椅子’,索引 2 对应 ‘地板’。
  • data_dict["inverse"] = np.array([1, 2, 0, 1])
    • 这是一个长度为 4 的数组,它的是用来从 voxel_labels 中取元素的索引。

当 NumPy 执行 voxel_labels[data_dict["inverse"]] 时,它会执行以下步骤:

  1. 创建一个新数组:NumPy 准备创建一个新的数组,其形状与索引数组 data_dict["inverse"] 相同。在这里,就是一个长度为 4 的新数组。

  2. 遍历索引数组:NumPy 会遍历 data_dict["inverse"] 中的每一个元素。

  3. 查找并填充

    • 对于 data_dict["inverse"] 的第 0 个元素,值是 1。NumPy 就会去 voxel_labels 中查找索引为 1 的元素,即 '椅子'。然后将 '椅子' 放入新数组的第 0 个位置。
      • 新数组:['椅子', ?, ?, ?]
    • 对于 data_dict["inverse"] 的第 1 个元素,值是 2。NumPy 就会去 voxel_labels 中查找索引为 2 的元素,即 '地板'。然后将 '地板' 放入新数组的第 1 个位置。
      • 新数组:['椅子', '地板', ?, ?]
    • 对于 data_dict["inverse"] 的第 2 个元素,值是 0。NumPy 就会去 voxel_labels 中查找索引为 0 的元素,即 '桌子'。然后将 '桌子' 放入新数组的第 2 个位置。
      • 新数组:['椅子', '地板', '桌子', ?]
    • 对于 data_dict["inverse"] 的第 3 个元素,值是 1。NumPy 再次去 voxel_labels 中查找索引为 1 的元素,即 '椅子'。然后将 '椅子' 放入新数组的第 3 个位置。
      • 新数组:['椅子', '地板', '桌子', '椅子']
  4. 返回新数组:遍历完成后,NumPy 返回这个构建好的新数组 ['椅子', '地板', '桌子', '椅子']

核心思想与应用

这个操作的本质是一个高效的批量查找和映射过程。

  • data_dict["inverse"] 提供了一个**“转换规则”“映射关系”**。
  • voxel_labels 提供了一个**“值字典”“查找表”**。

操作 voxel_labels[data_dict["inverse"]] 的语义可以理解为:“按照 data_dict["inverse"] 提供的规则,去 voxel_labels 这个字典里查找对应的值,然后把查到的值组合成一个新数组。”

在我们的点云场景中:

  • data_dict["inverse"] 的索引代表原始点的ID,值代表该点所属的体素ID
  • voxel_labels 的索引代表体素ID,值代表该体素的类别标签

所以,这个操作完美地实现了“为每个原始点查找其所在体素的类别标签”这一任务。

为什么这么做?
  • 高效:这种索引方式是在 NumPy 的底层用 C 或 Fortran 实现的,速度极快,远胜于使用 Python 的 for 循环来逐个查找。
  • 简洁:一行代码就能完成复杂的映射逻辑,让代码更易读、更易维护。

这就是 NumPy 高级索引的强大之处,它允许我们用数据本身来驱动计算和转换,是数据处理和科学计算中的一个基本且非常重要的技巧。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Garfield2005

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值