介绍了球面体素化的过程,包括重要的类和方法,如ConvertToSphericalVoxel和spherical_voxel_optimized,详细解释了参数及其作用。球面体素化通过将点云转换为球面坐标系,利用自适应采样权重来平衡不同纬度区域的点密度,从而有效捕捉几何特征。文中还提到C++绑定的s
参考链接:
代码组成:
ConvertToSphericalVoxel类:最高接口,实例化一个converter类,调用convert转换局部点云
↓
spherical_voxel_optimized方法:在convert中调用,实现转换,先转换到球面坐标系,然后进行体素化
↓
spherical_voxel.compute方法:最终实现体素化,用pybind绑定C++代码最终实现
from utils import geometry as ug
class ConvertToSphericalVoxel():
"""
Convert point cloud to spherical voxel [beta = 2 * bandwidth, alfa = 2 * bandwidth, num_radial_division].
Alfa in [0, 2pi], Beta in [0, pi]
"""
def __init__(self, bandwidth, radius_support, num_radial_division, num_points, random_sampling):
self.bandwidth = bandwidth
self.radius_support = radius_support
self.num_radial_division = num_radial_division
self.num_points = num_points
self.random_sampling = random_sampling
def __call__(self, point_cloud):
features, pts_normed = ug.spherical_voxel_optimized(points=point_cloud,
size_bandwidth=self.bandwidth,
size_radial_divisions=self.num_radial_division,
radius_support=self.radius_support,
do_random_sampling=self.random_sampling,
num_random_points=self.num_points)
return features, pts_normed
……
True
使得在局部区域内的点采样更加多样化,避免由于局部密度过高或过低而导致的信息丢失。随机采样可以让网络更具鲁棒性,适应不同点云的分布。
def spherical_voxel_optimized(points: np.ndarray, size_bandwidth: int, size_radial_divisions: int,
radius_support: float, do_random_sampling: bool, num_random_points: int) \
-> Tuple[np.ndarray, np.ndarray]:
"""Compute spherical voxel using the C++ code.
Compute Spherical Voxel signal as defined in:
Pointwise Rotation-Invariant Network withAdaptive Sampling and 3D Spherical Voxel Convolution.
Yang You, Yujing Lou, Qi Liu, Yu-Wing Tai, Weiming Wang, Lizhuang Ma and Cewu Lu.
AAAI 2020.
:param points: the points to convert.
:param size_bandwidth: alpha and beta bandwidth.
:param size_radial_divisions: the number of bins along radial dimension.
:param radius_support: the radius used to compute the points in the support.
:param do_random_sampling: if true a subset of random points will be used to compute the spherical voxel.
:param num_random_points: the number of points to keep if do_random_sampling is true.
:return: A tuple containing:
The spherical voxel, shape(size_radial_divisions, 2 * size_bandwidth, 2 * size_bandwidth).
The points used to compute the signal normalized according the the farthest point.
"""
if do_random_sampling:
min_limit = 1 if points.shape[0] > 1 else 0
indices_random = np.random.randint(min_limit, points.shape[0], num_random_points)
points = points[indices_random]
pts_norm = np.linalg.norm(points, axis=1)
# Scale points to fit unit sphere
pts_normed = points / pts_norm[:, None]
pts_normed = np.clip(pts_normed, -1, 1)
pts_s2_coord = S2.change_coordinates(pts_normed, p_from='C', p_to='S')
# Convert to spherical voxel indices
pts_s2_coord[:, 0] *= 2 * size_bandwidth / np.pi # [0, pi]
pts_s2_coord[:, 1] *= size_bandwidth / np.pi # raw 2*size_bandwidth/2*np.pi
pts_s2_coord[:, 1][pts_s2_coord[:, 1] < 0] += 2 * size_bandwidth
# Adaptive sampling factor sin{pi*[(1/2,..., 2*size_bandwidth+1/2)/(2*size_bandwidth)]}
# 能更好的聚合点云信息,但是也会导致更多的形变,有得必有失
daas_weights = np.sin(np.pi * (2 * np.arange(2 * size_bandwidth) + 1) / 4 / size_bandwidth).astype(np.float32)
voxel = np.asarray(sv.compute(pts_on_s2=pts_s2_coord,
pts_norm=pts_norm,
size_bandwidth=size_bandwidth,
size_radial_divisions=size_radial_divisions,
radius_support=radius_support,
daas_weights=daas_weights))
pts_normed = points / np.max(pts_norm)
return voxel.astype(np.float32), pts_normed.astype(np.float32)
pts_norm
是local patch的点云径向距离,所以
local patch输入的时候最好经过对于关键点的中心化操作
,不然径向距离会是关于坐标系原点的。
S2.change_coordinates
用于将点云从笛卡尔坐标系转换成球面坐标系,球面坐标系解释见WIKI,简单来说就是两个坐标,维度角度坐标\beta,和经度角度坐标\alpha
β
表示)不同区域的面积差异,不同区域的点密度会有所不同。例如,在球面的极地区域(纬度接近
0
或
π
的区域),同样的角度变化可能覆盖的球面面积较小,而在赤道区域,面积较大。为了避免在这些区域中出现过度或不足的采样,自适应采样权重用于平衡不同纬度区域的影响。
sv.compute
用于体素转换。
该函数是用pybind绑定的C++方法,文件为
spherical_voxel.cc
,代码解释如下:
const float interval = radius_support / (size_radial_divisions);
std::vector > > > > grids;
std::vector > > features;
grids.resize(size_radial_divisions);
features.resize(size_radial_divisions);
for (auto &beta: grids) {
beta.resize(2 * size_bandwidth);
for (auto &alpha: beta) {
alpha.resize(2 * size_bandwidth);
}
}
for (auto &beta: features) {
beta.resize(2 * size_bandwidth);
for (auto &alpha: beta) {
alpha.resize(2 * size_bandwidth, 0);
}
}
// mapping the points to the voxel grid
for (size_t i = 0; i < pts_on_s2.size(); i++) {
int r_idx = int(pts_norm[i] / interval);
// except for the points radius larger than radius_support
if (r_idx > size_radial_divisions - 1) r_idx = size_radial_divisions - 1;
int beta_idx = int(pts_on_s2[i][0] + 0.5f);
if (beta_idx > 2 * size_bandwidth - 1) beta_idx = 2 * size_bandwidth - 1;
int alpha_idx = int(pts_on_s2[i][1] + 0.5f);
if (alpha_idx > 2 * size_bandwidth - 1) alpha_idx = 2 * size_bandwidth - 1;
grids[r_idx][beta_idx][alpha_idx].emplace_back(std::vector{pts_norm[i], pts_on_s2[i][0], pts_on_s2[i][1]});
}
这里会遍历每个点,计算每个点的径向体素所用
r_idx
,纬度体素索引
beta_idx
,经度体素索引
alpha_idx
,然后push到对应的体素里面。
首先计算每个体素的经度左右特征计算边界
left
、
right
(也就是说每个体素的特征计算并不仅仅只考虑本体素内部,还有一些可能出现的相邻体素),这里计算左右边界就用到自适应权重,维度高的,左右边界会宽一些。
之后根据左右边界访问对应体素,并取出体素中所有点,基于径向距离确定点是否靠近本体素中心,越靠近该点的特征权重越大([0, 1])。
然后考虑径向相邻体素内部的点,用于本体素的特征计算,因为从径向考虑,点分布相对连续,需要补充这样的信息。
最后计算本体素的特征(密度特征(加过权的点个数))
// compute the feature of each voxel
for (size_t i = 0; i < size_radial_divisions; i++) {
for (size_t j = 0; j < 2 * size_bandwidth; j++) {
for (size_t k = 0; k < 2 * size_bandwidth; k++) {
const float left = std::max(0.f, k - 0.5f / daas_weights[j]);
const float right = std::min(2.f * size_bandwidth, k + 0.5f / daas_weights[j]);
float sum = 0.f;
int cnt = 0;
for (int m = int(left + 0.5f); m < int(right + 0.5f); m++) {
for (int n = 0; n < grids[i][j][m].size(); n++) {
if (grids[i][j][m][n][2] > left && grids[i][j][m][n][2] < right) {
sum += 1.f - std::abs(grids[i][j][m][n][0] / interval - (i + 1)); // radial feature weight
cnt++;
}
}
// 在实际情况中,点云数据可能分布在两个相邻的径向分割之间,
// 尤其是当点的径向距离位于两个径向分割的边界附近时。
// 为了防止因单纯考虑当前径向分割而导致信息的丢失,
// 代码会查找相邻径向分割中满足条件的点,并将它们的贡献也加到当前体素单元的特征值中。
if (i < size_radial_divisions - 1) {
for (int n = 0; n < grids[i + 1][j][m].size(); n++) {
if (grids[i + 1][j][m][n][2] > left && grids[i + 1][j][m][n][2] < right) {
sum += 1.f - std::abs(grids[i + 1][j][m][n][0] / interval - (i + 1));
cnt++;
}
}
}
}
// 与径向分割不同,纬度分割(即 beta 方向)代表的是球面坐标中的角度,
// 分割的区域代表不同的“环”或“带”。
// 在这种情况下,每个纬度分割对应的球面区域是明确的,
// 且这些分割区域之间没有交叉,因此点不会“跨越”到另一个纬度分割。
if (cnt > 0) {
features[i][j][k] = sum / cnt;
}
}
}
}