KPConv-PyTorch/kernels/kernel_points.py

563 lines
18 KiB
Python
Raw Normal View History

2020-03-31 19:42:35 +00:00
#
#
# 0=================================0
# | Kernel Point Convolutions |
# 0=================================0
#
#
# ----------------------------------------------------------------------------------------------------------------------
#
# Functions handling the disposition of kernel points.
#
# ----------------------------------------------------------------------------------------------------------------------
#
# Hugues THOMAS - 11/06/2018
#
# ------------------------------------------------------------------------------------------
#
# Imports and global variables
# \**********************************/
#
# Import numpy package and name it "np"
import numpy as np
import matplotlib.pyplot as plt
from os import makedirs
from os.path import join, exists
from utils.ply import read_ply, write_ply
from utils.config import bcolors
# ------------------------------------------------------------------------------------------
#
# Functions
# \***************/
#
#
2023-05-15 15:18:10 +00:00
2020-03-31 19:42:35 +00:00
def create_3D_rotations(axis, angle):
"""
Create rotation matrices from a list of axes and angles. Code from wikipedia on quaternions
:param axis: float32[N, 3]
:param angle: float32[N,]
:return: float32[N, 3, 3]
"""
t1 = np.cos(angle)
t2 = 1 - t1
t3 = axis[:, 0] * axis[:, 0]
t6 = t2 * axis[:, 0]
t7 = t6 * axis[:, 1]
t8 = np.sin(angle)
t9 = t8 * axis[:, 2]
t11 = t6 * axis[:, 2]
t12 = t8 * axis[:, 1]
t15 = axis[:, 1] * axis[:, 1]
t19 = t2 * axis[:, 1] * axis[:, 2]
t20 = t8 * axis[:, 0]
t24 = axis[:, 2] * axis[:, 2]
2023-05-15 15:18:10 +00:00
R = np.stack(
[
t1 + t2 * t3,
t7 - t9,
t11 + t12,
t7 + t9,
t1 + t2 * t15,
t19 - t20,
t11 - t12,
t19 + t20,
t1 + t2 * t24,
],
axis=1,
)
2020-03-31 19:42:35 +00:00
return np.reshape(R, (-1, 3, 3))
2020-04-27 22:01:40 +00:00
2023-05-15 15:18:10 +00:00
def spherical_Lloyd(
radius,
num_cells,
dimension=3,
fixed="center",
approximation="monte-carlo",
approx_n=5000,
max_iter=500,
momentum=0.9,
verbose=0,
):
2020-03-31 19:42:35 +00:00
"""
Creation of kernel point via Lloyd algorithm. We use an approximation of the algorithm, and compute the Voronoi
cell centers with discretization of space. The exact formula is not trivial with part of the sphere as sides.
:param radius: Radius of the kernels
:param num_cells: Number of cell (kernel points) in the Voronoi diagram.
:param dimension: dimension of the space
:param fixed: fix position of certain kernel points ('none', 'center' or 'verticals')
:param approximation: Approximation method for Lloyd's algorithm ('discretization', 'monte-carlo')
:param approx_n: Number of point used for approximation.
:param max_iter: Maximum nu;ber of iteration for the algorithm.
:param momentum: Momentum of the low pass filter smoothing kernel point positions
:param verbose: display option
:return: points [num_kernels, num_points, dimension]
"""
#######################
# Parameters definition
#######################
# Radius used for optimization (points are rescaled afterwards)
radius0 = 1.0
#######################
# Kernel initialization
#######################
# Random kernel points (Uniform distribution in a sphere)
kernel_points = np.zeros((0, dimension))
while kernel_points.shape[0] < num_cells:
new_points = np.random.rand(num_cells, dimension) * 2 * radius0 - radius0
kernel_points = np.vstack((kernel_points, new_points))
d2 = np.sum(np.power(kernel_points, 2), axis=1)
2023-05-15 15:18:10 +00:00
kernel_points = kernel_points[
np.logical_and(d2 < radius0**2, (0.9 * radius0) ** 2 < d2), :
]
2020-03-31 19:42:35 +00:00
kernel_points = kernel_points[:num_cells, :].reshape((num_cells, -1))
# Optional fixing
2023-05-15 15:18:10 +00:00
if fixed == "center":
2020-03-31 19:42:35 +00:00
kernel_points[0, :] *= 0
2023-05-15 15:18:10 +00:00
if fixed == "verticals":
2020-03-31 19:42:35 +00:00
kernel_points[:3, :] *= 0
kernel_points[1, -1] += 2 * radius0 / 3
kernel_points[2, -1] -= 2 * radius0 / 3
##############################
# Approximation initialization
##############################
# Initialize figure
if verbose > 1:
fig = plt.figure()
# Initialize discretization in this method is chosen
2023-05-15 15:18:10 +00:00
if approximation == "discretization":
side_n = int(np.floor(approx_n ** (1.0 / dimension)))
2020-03-31 19:42:35 +00:00
dl = 2 * radius0 / side_n
2023-05-15 15:18:10 +00:00
coords = np.arange(-radius0 + dl / 2, radius0, dl)
2020-03-31 19:42:35 +00:00
if dimension == 2:
x, y = np.meshgrid(coords, coords)
X = np.vstack((np.ravel(x), np.ravel(y))).T
elif dimension == 3:
x, y, z = np.meshgrid(coords, coords, coords)
X = np.vstack((np.ravel(x), np.ravel(y), np.ravel(z))).T
elif dimension == 4:
x, y, z, t = np.meshgrid(coords, coords, coords, coords)
X = np.vstack((np.ravel(x), np.ravel(y), np.ravel(z), np.ravel(t))).T
else:
2023-05-15 15:18:10 +00:00
raise ValueError("Unsupported dimension (max is 4)")
elif approximation == "monte-carlo":
2020-03-31 19:42:35 +00:00
X = np.zeros((0, dimension))
else:
2023-05-15 15:18:10 +00:00
raise ValueError(
'Wrong approximation method chosen: "{:s}"'.format(approximation)
)
2020-03-31 19:42:35 +00:00
# Only points inside the sphere are used
d2 = np.sum(np.power(X, 2), axis=1)
X = X[d2 < radius0 * radius0, :]
#####################
# Kernel optimization
#####################
# Warning if at least one kernel point has no cell
warning = False
# moving vectors of kernel points saved to detect convergence
max_moves = np.zeros((0,))
for iter in range(max_iter):
# In the case of monte-carlo, renew the sampled points
2023-05-15 15:18:10 +00:00
if approximation == "monte-carlo":
2020-03-31 19:42:35 +00:00
X = np.random.rand(approx_n, dimension) * 2 * radius0 - radius0
d2 = np.sum(np.power(X, 2), axis=1)
X = X[d2 < radius0 * radius0, :]
# Get the distances matrix [n_approx, K, dim]
differences = np.expand_dims(X, 1) - kernel_points
sq_distances = np.sum(np.square(differences), axis=2)
# Compute cell centers
cell_inds = np.argmin(sq_distances, axis=1)
centers = []
for c in range(num_cells):
2023-05-15 15:18:10 +00:00
bool_c = cell_inds == c
2020-03-31 19:42:35 +00:00
num_c = np.sum(bool_c.astype(np.int32))
if num_c > 0:
centers.append(np.sum(X[bool_c, :], axis=0) / num_c)
else:
warning = True
centers.append(kernel_points[c])
# Update kernel points with low pass filter to smooth mote carlo
centers = np.vstack(centers)
moves = (1 - momentum) * (centers - kernel_points)
kernel_points += moves
# Check moves for convergence
max_moves = np.append(max_moves, np.max(np.linalg.norm(moves, axis=1)))
# Optional fixing
2023-05-15 15:18:10 +00:00
if fixed == "center":
2020-03-31 19:42:35 +00:00
kernel_points[0, :] *= 0
2023-05-15 15:18:10 +00:00
if fixed == "verticals":
2020-03-31 19:42:35 +00:00
kernel_points[0, :] *= 0
kernel_points[:3, :-1] *= 0
if verbose:
2023-05-15 15:18:10 +00:00
print(
"iter {:5d} / max move = {:f}".format(
iter, np.max(np.linalg.norm(moves, axis=1))
)
)
2020-03-31 19:42:35 +00:00
if warning:
2023-05-15 15:18:10 +00:00
print(
"{:}WARNING: at least one point has no cell{:}".format(
bcolors.WARNING, bcolors.ENDC
)
)
2020-03-31 19:42:35 +00:00
if verbose > 1:
plt.clf()
2023-05-15 15:18:10 +00:00
plt.scatter(
X[:, 0],
X[:, 1],
c=cell_inds,
s=20.0,
marker=".",
cmap=plt.get_cmap("tab20"),
)
# plt.scatter(kernel_points[:, 0], kernel_points[:, 1], c=np.arange(num_cells), s=100.0,
2020-03-31 19:42:35 +00:00
# marker='+', cmap=plt.get_cmap('tab20'))
2023-05-15 15:18:10 +00:00
plt.plot(kernel_points[:, 0], kernel_points[:, 1], "k+")
circle = plt.Circle((0, 0), radius0, color="r", fill=False)
2020-03-31 19:42:35 +00:00
fig.axes[0].add_artist(circle)
fig.axes[0].set_xlim((-radius0 * 1.1, radius0 * 1.1))
fig.axes[0].set_ylim((-radius0 * 1.1, radius0 * 1.1))
2023-05-15 15:18:10 +00:00
fig.axes[0].set_aspect("equal")
2020-03-31 19:42:35 +00:00
plt.draw()
plt.pause(0.001)
plt.show(block=False)
###################
# User verification
###################
# Show the convergence to ask user if this kernel is correct
if verbose:
if dimension == 2:
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=[10.4, 4.8])
ax1.plot(max_moves)
2023-05-15 15:18:10 +00:00
ax2.scatter(
X[:, 0],
X[:, 1],
c=cell_inds,
s=20.0,
marker=".",
cmap=plt.get_cmap("tab20"),
)
2020-03-31 19:42:35 +00:00
# plt.scatter(kernel_points[:, 0], kernel_points[:, 1], c=np.arange(num_cells), s=100.0,
# marker='+', cmap=plt.get_cmap('tab20'))
2023-05-15 15:18:10 +00:00
ax2.plot(kernel_points[:, 0], kernel_points[:, 1], "k+")
circle = plt.Circle((0, 0), radius0, color="r", fill=False)
2020-03-31 19:42:35 +00:00
ax2.add_artist(circle)
ax2.set_xlim((-radius0 * 1.1, radius0 * 1.1))
ax2.set_ylim((-radius0 * 1.1, radius0 * 1.1))
2023-05-15 15:18:10 +00:00
ax2.set_aspect("equal")
plt.title("Check if kernel is correct.")
2020-03-31 19:42:35 +00:00
plt.draw()
plt.show()
if dimension > 2:
plt.figure()
plt.plot(max_moves)
2023-05-15 15:18:10 +00:00
plt.title("Check if kernel is correct.")
2020-03-31 19:42:35 +00:00
plt.show()
# Rescale kernels with real radius
return kernel_points * radius
2023-05-15 15:18:10 +00:00
def kernel_point_optimization_debug(
radius,
num_points,
num_kernels=1,
dimension=3,
fixed="center",
ratio=0.66,
verbose=0,
):
2020-03-31 19:42:35 +00:00
"""
Creation of kernel point via optimization of potentials.
:param radius: Radius of the kernels
:param num_points: points composing kernels
:param num_kernels: number of wanted kernels
:param dimension: dimension of the space
:param fixed: fix position of certain kernel points ('none', 'center' or 'verticals')
:param ratio: ratio of the radius where you want the kernels points to be placed
:param verbose: display option
:return: points [num_kernels, num_points, dimension]
"""
#######################
# Parameters definition
#######################
# Radius used for optimization (points are rescaled afterwards)
radius0 = 1
diameter0 = 2
# Factor multiplicating gradients for moving points (~learning rate)
moving_factor = 1e-2
continuous_moving_decay = 0.9995
# Gradient threshold to stop optimization
thresh = 1e-5
# Gradient clipping value
clip = 0.05 * radius0
#######################
# Kernel initialization
#######################
# Random kernel points
2023-05-15 15:18:10 +00:00
kernel_points = (
np.random.rand(num_kernels * num_points - 1, dimension) * diameter0 - radius0
)
while kernel_points.shape[0] < num_kernels * num_points:
new_points = (
np.random.rand(num_kernels * num_points - 1, dimension) * diameter0
- radius0
)
2020-03-31 19:42:35 +00:00
kernel_points = np.vstack((kernel_points, new_points))
d2 = np.sum(np.power(kernel_points, 2), axis=1)
kernel_points = kernel_points[d2 < 0.5 * radius0 * radius0, :]
2023-05-15 15:18:10 +00:00
kernel_points = kernel_points[: num_kernels * num_points, :].reshape(
(num_kernels, num_points, -1)
)
2020-03-31 19:42:35 +00:00
# Optionnal fixing
2023-05-15 15:18:10 +00:00
if fixed == "center":
2020-03-31 19:42:35 +00:00
kernel_points[:, 0, :] *= 0
2023-05-15 15:18:10 +00:00
if fixed == "verticals":
2020-03-31 19:42:35 +00:00
kernel_points[:, :3, :] *= 0
kernel_points[:, 1, -1] += 2 * radius0 / 3
kernel_points[:, 2, -1] -= 2 * radius0 / 3
#####################
# Kernel optimization
#####################
# Initialize figure
2023-05-15 15:18:10 +00:00
if verbose > 1:
2020-03-31 19:42:35 +00:00
fig = plt.figure()
saved_gradient_norms = np.zeros((10000, num_kernels))
old_gradient_norms = np.zeros((num_kernels, num_points))
2020-12-14 16:46:05 +00:00
step = -1
while step < 10000:
# Increment
step += 1
2020-03-31 19:42:35 +00:00
# Compute gradients
# *****************
# Derivative of the sum of potentials of all points
A = np.expand_dims(kernel_points, axis=2)
B = np.expand_dims(kernel_points, axis=1)
interd2 = np.sum(np.power(A - B, 2), axis=-1)
2023-05-15 15:18:10 +00:00
inter_grads = (A - B) / (np.power(np.expand_dims(interd2, -1), 3 / 2) + 1e-6)
2020-03-31 19:42:35 +00:00
inter_grads = np.sum(inter_grads, axis=1)
# Derivative of the radius potential
2023-05-15 15:18:10 +00:00
circle_grads = 10 * kernel_points
2020-03-31 19:42:35 +00:00
# All gradients
gradients = inter_grads + circle_grads
2023-05-15 15:18:10 +00:00
if fixed == "verticals":
2020-03-31 19:42:35 +00:00
gradients[:, 1:3, :-1] = 0
# Stop condition
# **************
# Compute norm of gradients
gradients_norms = np.sqrt(np.sum(np.power(gradients, 2), axis=-1))
2020-12-14 16:46:05 +00:00
saved_gradient_norms[step, :] = np.max(gradients_norms, axis=1)
2020-03-31 19:42:35 +00:00
# Stop if all moving points are gradients fixed (low gradients diff)
2023-05-15 15:18:10 +00:00
if (
fixed == "center"
and np.max(np.abs(old_gradient_norms[:, 1:] - gradients_norms[:, 1:]))
< thresh
):
2020-03-31 19:42:35 +00:00
break
2023-05-15 15:18:10 +00:00
elif (
fixed == "verticals"
and np.max(np.abs(old_gradient_norms[:, 3:] - gradients_norms[:, 3:]))
< thresh
):
2020-03-31 19:42:35 +00:00
break
elif np.max(np.abs(old_gradient_norms - gradients_norms)) < thresh:
break
old_gradient_norms = gradients_norms
# Move points
# ***********
# Clip gradient to get moving dists
moving_dists = np.minimum(moving_factor * gradients_norms, clip)
# Fix central point
2023-05-15 15:18:10 +00:00
if fixed == "center":
2020-03-31 19:42:35 +00:00
moving_dists[:, 0] = 0
2023-05-15 15:18:10 +00:00
if fixed == "verticals":
2020-03-31 19:42:35 +00:00
moving_dists[:, 0] = 0
# Move points
2023-05-15 15:18:10 +00:00
kernel_points -= (
np.expand_dims(moving_dists, -1)
* gradients
/ np.expand_dims(gradients_norms + 1e-6, -1)
)
2020-03-31 19:42:35 +00:00
if verbose:
2023-05-15 15:18:10 +00:00
print(
"step {:5d} / max grad = {:f}".format(
step, np.max(gradients_norms[:, 3:])
)
)
2020-03-31 19:42:35 +00:00
if verbose > 1:
plt.clf()
2023-05-15 15:18:10 +00:00
plt.plot(kernel_points[0, :, 0], kernel_points[0, :, 1], ".")
circle = plt.Circle((0, 0), radius, color="r", fill=False)
2020-03-31 19:42:35 +00:00
fig.axes[0].add_artist(circle)
2023-05-15 15:18:10 +00:00
fig.axes[0].set_xlim((-radius * 1.1, radius * 1.1))
fig.axes[0].set_ylim((-radius * 1.1, radius * 1.1))
fig.axes[0].set_aspect("equal")
2020-03-31 19:42:35 +00:00
plt.draw()
plt.pause(0.001)
plt.show(block=False)
print(moving_factor)
# moving factor decay
moving_factor *= continuous_moving_decay
2020-12-14 16:46:05 +00:00
# Remove unused lines in the saved gradients
if step < 10000:
2023-05-15 15:18:10 +00:00
saved_gradient_norms = saved_gradient_norms[: step + 1, :]
2020-12-14 16:46:05 +00:00
2020-03-31 19:42:35 +00:00
# Rescale radius to fit the wanted ratio of radius
r = np.sqrt(np.sum(np.power(kernel_points, 2), axis=-1))
kernel_points *= ratio / np.mean(r[:, 1:])
# Rescale kernels with real radius
return kernel_points * radius, saved_gradient_norms
def load_kernels(radius, num_kpoints, dimension, fixed, lloyd=False):
# Kernel directory
2023-05-15 15:18:10 +00:00
kernel_dir = "kernels/dispositions"
2020-03-31 19:42:35 +00:00
if not exists(kernel_dir):
makedirs(kernel_dir)
# To many points switch to Lloyds
if num_kpoints > 30:
lloyd = True
# Kernel_file
2023-05-15 15:18:10 +00:00
kernel_file = join(
kernel_dir, "k_{:03d}_{:s}_{:d}D.ply".format(num_kpoints, fixed, dimension)
)
2020-03-31 19:42:35 +00:00
# Check if already done
if not exists(kernel_file):
if lloyd:
# Create kernels
2023-05-15 15:18:10 +00:00
kernel_points = spherical_Lloyd(
1.0, num_kpoints, dimension=dimension, fixed=fixed, verbose=0
)
2020-03-31 19:42:35 +00:00
else:
# Create kernels
2023-05-15 15:18:10 +00:00
kernel_points, grad_norms = kernel_point_optimization_debug(
1.0,
num_kpoints,
num_kernels=100,
dimension=dimension,
fixed=fixed,
verbose=0,
)
2020-03-31 19:42:35 +00:00
# Find best candidate
best_k = np.argmin(grad_norms[-1, :])
# Save points
kernel_points = kernel_points[best_k, :, :]
2023-05-15 15:18:10 +00:00
write_ply(kernel_file, kernel_points, ["x", "y", "z"])
2020-03-31 19:42:35 +00:00
else:
data = read_ply(kernel_file)
2023-05-15 15:18:10 +00:00
kernel_points = np.vstack((data["x"], data["y"], data["z"])).T
2020-03-31 19:42:35 +00:00
# Random roations for the kernel
# N.B. 4D random rotations not supported yet
R = np.eye(dimension)
theta = np.random.rand() * 2 * np.pi
if dimension == 2:
2023-05-15 15:18:10 +00:00
if fixed != "vertical":
2020-03-31 19:42:35 +00:00
c, s = np.cos(theta), np.sin(theta)
R = np.array([[c, -s], [s, c]], dtype=np.float32)
elif dimension == 3:
2023-05-15 15:18:10 +00:00
if fixed != "vertical":
2020-03-31 19:42:35 +00:00
c, s = np.cos(theta), np.sin(theta)
R = np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]], dtype=np.float32)
else:
phi = (np.random.rand() - 0.5) * np.pi
# Create the first vector in carthesian coordinates
2023-05-15 15:18:10 +00:00
u = np.array(
[np.cos(theta) * np.cos(phi), np.sin(theta) * np.cos(phi), np.sin(phi)]
)
2020-03-31 19:42:35 +00:00
# Choose a random rotation angle
alpha = np.random.rand() * 2 * np.pi
# Create the rotation matrix with this vector and angle
2023-05-15 15:18:10 +00:00
R = create_3D_rotations(np.reshape(u, (1, -1)), np.reshape(alpha, (1, -1)))[
0
]
2020-03-31 19:42:35 +00:00
R = R.astype(np.float32)
# Add a small noise
2023-05-15 15:18:10 +00:00
kernel_points = kernel_points + np.random.normal(
scale=0.01, size=kernel_points.shape
)
2020-03-31 19:42:35 +00:00
# Scale kernels
kernel_points = radius * kernel_points
# Rotate kernels
kernel_points = np.matmul(kernel_points, R)
2023-05-15 15:18:10 +00:00
return kernel_points.astype(np.float32)