NAME = "Victor Rong"
TITLE = "Surface Reconstruction Using Boundary Integral Equations"
# All imports
import os
os.environ["MKL_NUM_THREADS"] = "1"
os.environ["NUMEXPR_NUM_THREADS"] = "1"
os.environ["OMP_NUM_THREADS"] = "1"
import numpy as np
np.random.seed(1)
import open3d as o3d
import time
from scipy.special import sph_harm
from matplotlib import pyplot as plt
# plt.rcParams.update({'font.size': 24})
from skimage import measure
from sklearn.neighbors import NearestNeighbors
Jupyter environment detected. Enabling Open3D WebVisualizer. [Open3D INFO] WebRTC GUI backend enabled. [Open3D INFO] WebRTCWindowSystem: HTTP handshake server disabled.
class BoundingBox():
"""
Simple axis-aligned bounding box class
"""
def __init__(self, corners, boundary="[][][]"):
"""
Initializes bounding box
Corners is list of length 2 with numpy arrays of shape (3,)
Boundary is denoting closed/open intervals in each axis although it doesn't end up being relevant
"""
self.corners = corners
self.boundary = boundary
def width(self):
"""
Returns real number, the length of the longest side
"""
return np.amax(self.corners[1]-self.corners[0])
def center(self):
"""
Returns numpy array of shape (3,)
"""
return 0.5*(self.corners[0]+self.corners[1])
def meshgrid(self, x, y, z):
"""
x, y, z are integers
Returns coordinates forming regular grid on bounding box where x, y, z are the number of cells in each axis
Coordinates xyz is a numpy array of shape (x, y, z, 3)
"""
xi, yi, zi = np.meshgrid(np.linspace(self.corners[0][0], self.corners[1][0], x), np.linspace(self.corners[0][1], self.corners[1][1], y), np.linspace(self.corners[0][1], self.corners[1][1], z))
xyz = np.concatenate([np.expand_dims(xi, -1), np.expand_dims(yi, -1), np.expand_dims(zi, -1)], axis=-1)
return xyz
def halve(self, axis):
"""
Subdivides bounding box into two along axis
"""
left_corner = np.array(self.corners[1])
left_corner[axis] = (self.corners[0][axis] + self.corners[1][axis]) / 2
left_boundary = list(self.boundary)
left_boundary[2*axis+1] = ")"
left_boundary = "".join(left_boundary)
left_bbox = BoundingBox([self.corners[0], left_corner], boundary=left_boundary)
right_corner = np.array(self.corners[0])
right_corner[axis] = (self.corners[0][axis] + self.corners[1][axis]) / 2
right_boundary = list(self.boundary)
right_boundary[2*axis+0] = "["
right_boundary = "".join(right_boundary)
right_bbox = BoundingBox([right_corner, self.corners[1]], boundary=right_boundary)
return left_bbox, right_bbox
def get_filter(self, pcd):
"""
Returns numpy boolean array representing mask on points
"""
points = np.array(pcd.points)
lower_mask = np.ones(points.shape[0], dtype=bool)
for i in range(3):
if self.boundary[2*i+0] == "(":
lower_mask = lower_mask & (points[:,i] > self.corners[0][i])
else:
lower_mask = lower_mask & (points[:,i] >= self.corners[0][i])
upper_mask = np.ones(points.shape[0], dtype=bool)
for i in range(3):
if self.boundary[2*i+1] == ")":
upper_mask = upper_mask & (points[:,i] < self.corners[1][i])
else:
upper_mask = upper_mask & (points[:,i] <= self.corners[1][i])
return lower_mask & upper_mask
def filter_point_cloud(self, pcd):
"""
Returns a new point cloud formed by only taking points in the bounding box
"""
points = np.array(pcd.points)
normals = np.array(pcd.normals)
groups = np.array(pcd.groups)
ids = np.array(pcd.ids)
lower_mask = np.ones(points.shape[0], dtype=bool)
for i in range(3):
if self.boundary[2*i+0] == "(":
lower_mask = lower_mask & (points[:,i] > self.corners[0][i])
else:
lower_mask = lower_mask & (points[:,i] >= self.corners[0][i])
upper_mask = np.ones(points.shape[0], dtype=bool)
for i in range(3):
if self.boundary[2*i+1] == ")":
upper_mask = upper_mask & (points[:,i] < self.corners[1][i])
else:
upper_mask = upper_mask & (points[:,i] <= self.corners[1][i])
return PointCloud(points[lower_mask & upper_mask], normals[lower_mask & upper_mask], groups[lower_mask & upper_mask], ids[lower_mask & upper_mask])
class PointCloud():
"""
Simple point cloud class
"""
def __init__(self, points, normals, groups=None, ids=None):
"""
Initializes point cloud with oriented normals
Points and normals should be numpy arrays of shape (n, 3)
Normals doesn't actually have to be normals, it's fine to set it to np.zeros_like(points) if they're not available
Groups and ids are metadata for each point used in our Barnes-Hut implementation
"""
self.points = points
self.normals = normals
self.area = 1
self.areas = np.ones_like(points[:,0])
self.n = self.points.shape[0]
if groups is None:
self.groups = np.zeros((self.n,), dtype=np.int32)
elif isinstance(groups, int):
self.groups = groups + np.zeros((self.n,), dtype=np.int32)
else:
self.groups = groups
if ids is None:
self.ids = np.arange(0, self.n, dtype=np.int32)
else:
self.ids = ids
def __len__(self):
"""
Returns length
"""
return self.n
def compute_weights(self, area, weight_normals=True):
"""
Note that the area must be set explicitly by this function, otherwise it will be assumed to be 1 in the constructor
Computes weights used for discretizing the integration
Lazy implementation so I'm just multiplying the normals by the weights so that they can be used in the Barnes-Hut algorithm implicitly
"""
# Library implementation should be O(nlogn)
nbrs = NearestNeighbors(n_neighbors=10).fit(self.points)
self.area = area
distances, indices = nbrs.kneighbors(self.points)
self.weights = np.mean(distances, axis=-1)**2
self.weights *= (area / np.sum(self.weights))
if weight_normals:
self.normals = self.weights[:,None] * self.normals
def get_bounding_box(self):
"""
Returns bounding box slightly (1.2 x) larger than the smallest bounding box
"""
bbox = BoundingBox([np.min(self.points, axis=0), np.max(self.points, axis=0)], boundary="[][][]")
center = bbox.center()
width = bbox.width()
new_bbox = BoundingBox([center - 0.6 * width, center + 0.6 * width], boundary="[][][]")
return new_bbox
def merge(self, o):
"""
Returns a merged point cloud of self and o
"""
points = np.concatenate([self.points, o.points], axis=0)
normals = np.concatenate([self.normals, o.normals], axis=0)
groups = np.concatenate([self.groups, o.groups], axis=0)
ids = np.concatenate([self.ids, o.ids], axis=0)
return PointCloud(points, normals, groups, ids)
class Octree():
"""
Simple octree class
"""
def __init__(self, pcd, bbox=None, max_height=None, parent=None):
"""
Creates octree from a set of points
"""
self.max_height = max_height
self.parent = parent
self.children = [[[None, None], [None, None]], [[None, None], [None, None]]]
if bbox is None:
self.bbox = pcd.get_bounding_box()
else:
self.bbox = bbox
self.pcd = None
if len(pcd) > 1 and self.max_height != 0:
for i1 in [0, 1]:
for i2 in [0, 1]:
for i3 in [0, 1]:
child_bbox = self.bbox.halve(axis=0)[i1]
child_pcd = child_bbox.filter_point_cloud(pcd)
child_bbox = child_bbox.halve(axis=1)[i2]
child_pcd = child_bbox.filter_point_cloud(child_pcd)
child_bbox = child_bbox.halve(axis=2)[i3]
child_pcd = child_bbox.filter_point_cloud(child_pcd)
self.children[i1][i2][i3] = Octree(child_pcd, child_bbox, None if max_height is None else max_height-1)
else:
self.pcd = pcd
def get_centers(self):
"""
Returns centers of nodes in the octree
Used for choosing evaluation targets
"""
centers = np.array(self.bbox.center())[None,:]
for i1 in [0, 1]:
for i2 in [0, 1]:
for i3 in [0, 1]:
if self.children[i1][i2][i3] is not None:
centers = np.concatenate((centers, self.children[i1][i2][i3].get_centers()), axis=0)
return centers
def fill_grid(self, grid, indicator, targets, xyz, mask):
"""
Assuming indicator has been computed for sparse points (targets), fills denser grid (grid) using octree structure
grid is mutable numpy array of shape (m,) and used as the output
xyz is the coordinates underlying grid so it has shape (m, 3)
indicator has shape (n,)
targets has shape (n, 3)
mask is a numpy boolean array of shape (m,), just set to all True when calling
"""
target_pcd = PointCloud(targets, np.zeros_like(targets))
target_filt = self.bbox.get_filter(target_pcd)
if np.sum(target_filt) == 0:
return
val = np.mean(indicator[target_filt])
targets = targets[target_filt,:]
indicator = indicator[target_filt]
xyz_pcd = PointCloud(xyz[mask], np.zeros_like(xyz[mask]))
filt = self.bbox.get_filter(xyz_pcd)
ind = np.flatnonzero(mask)[filt]
grid[ind] = val
new_mask = np.zeros_like(mask)
new_mask[ind] = 1
# print(mask.shape, mask.sum(), new_mask.sum())
for i1 in [0, 1]:
for i2 in [0, 1]:
for i3 in [0, 1]:
if self.children[i1][i2][i3] is not None:
self.children[i1][i2][i3].fill_grid(grid, np.array(indicator), np.array(targets), xyz, np.array(new_mask))
def load_pcd(filename, N):
"""
Wrapper function for loading point cloud from mesh file with N uniform samples
"""
mesh = o3d.io.read_triangle_mesh(filename)
mesh.compute_vertex_normals()
area = mesh.get_surface_area()
pcd = mesh.sample_points_uniformly(N)
points = np.array(pcd.points)
normals = np.array(pcd.normals)
pcd = PointCloud(points, normals)
pcd.compute_weights(area, weight_normals=True)
return pcd
def load_sphere(N):
"""
Wrapper function for loading sphere point cloud with N uniform samples
Could also use Gaussian distribution trick but this mesh is accurate enough
"""
mesh = o3d.geometry.TriangleMesh.create_sphere(1.0, 100)
mesh.compute_vertex_normals()
area = mesh.get_surface_area()
pcd = mesh.sample_points_uniformly(N)
points = np.array(pcd.points)
normals = np.array(pcd.normals)
pcd = PointCloud(points, normals)
pcd.compute_weights(area, weight_normals=True)
return pcd
def load_cone(N):
"""
Wrapper function for loading cone point cloud with N uniform samples
"""
mesh = o3d.geometry.TriangleMesh.create_cone(1.0, 2.0, 100)
mesh.compute_vertex_normals()
area = mesh.get_surface_area()
pcd = mesh.sample_points_uniformly(N)
points = np.array(pcd.points)
normals = np.array(pcd.normals)
pcd = PointCloud(points, normals)
pcd.compute_weights(area, weight_normals=True)
return pcd
def load_torus(N):
"""
Wrapper function for loading torus point cloud with N uniform samples
"""
mesh = o3d.geometry.TriangleMesh.create_torus(1.0, 0.5, 100, 100)
mesh.compute_vertex_normals()
area = mesh.get_surface_area()
pcd = mesh.sample_points_uniformly(N)
points = np.array(pcd.points)
normals = np.array(pcd.normals)
pcd = PointCloud(points, normals)
pcd.compute_weights(area, weight_normals=True)
return pcd
octree_nodes = []
class FMMOctree:
"""
Octree structure
Used to prepare and apply Barnes-Hut
"""
def __init__(self, pcd, bbox=None, max_height=None, parent=None, id=0):
"""
Each node in the octree maintains a copy of the points within its bounding box
An ID system is used to maintain the parent and child relations, as well as the near neighbour and interaction lists
"""
global octree_nodes
if id == 0:
octree_nodes.clear()
octree_nodes.append(self)
self.max_height = max_height
self.parent = parent
self.id = id
self.max_id = id
self.flattened_children = []
if bbox is None:
self.bbox = pcd.get_bounding_box()
else:
self.bbox = bbox
self.pcd = pcd
if len(pcd) > 1 and self.max_height != 0:
for i1 in [0, 1]:
child_bbox1 = self.bbox.halve(axis=0)[i1]
child_pcd1 = child_bbox1.filter_point_cloud(pcd)
for i2 in [0, 1]:
child_bbox2 = child_bbox1.halve(axis=1)[i2]
child_pcd2 = child_bbox2.filter_point_cloud(child_pcd1)
for i3 in [0, 1]:
child_bbox3 = child_bbox2.halve(axis=2)[i3]
child_pcd3 = child_bbox3.filter_point_cloud(child_pcd2)
if len(child_pcd3) != 0:
child = FMMOctree(child_pcd3, child_bbox3, None if max_height is None else max_height-1, self.id, self.max_id+1)
self.max_id = max(self.max_id, child.max_id)
self.flattened_children.append(child.id)
self.neighbours = [self.id]
self.interaction_list = []
def build_bridge_neighbours(self):
"""
When building the near neighbour list, consider neighbours which have the same parent and those which don't
This function forms connections for the latter
Note that it is the parent controlling this
"""
global octree_nodes
for neighbour in self.neighbours:
if neighbour == self.id:
continue
for child in self.flattened_children:
for other_child in octree_nodes[neighbour].flattened_children:
diff = octree_nodes[other_child].bbox.center() - octree_nodes[child].bbox.center()
check = np.abs(diff) <= (octree_nodes[child].bbox.corners[1] - octree_nodes[child].bbox.corners[0]) * (1 + 1e-6)
if np.sum(check) == check.shape[0]:
octree_nodes[child].neighbours.append(other_child)
def build_child_neighbours(self):
"""
When building the near neighbour list, consider neighbours which have the same parent and those which don't
This function forms connections for the former
Note that it is the parent controlling this
"""
global octree_nodes
for i in range(len(self.flattened_children)):
for j in range(i+1, len(self.flattened_children)):
octree_nodes[self.flattened_children[i]].neighbours.append(self.flattened_children[j])
octree_nodes[self.flattened_children[j]].neighbours.append(self.flattened_children[i])
self.build_bridge_neighbours()
for child in self.flattened_children:
octree_nodes[child].build_child_neighbours()
def build_interaction_list(self):
"""
Forms the interaction list
"""
global octree_nodes
if self.parent is not None:
for parent_neighbour in octree_nodes[self.parent].neighbours:
for parent_child in octree_nodes[parent_neighbour].flattened_children:
if parent_child in self.neighbours:
continue
self.interaction_list.append(parent_child)
for child in self.flattened_children:
octree_nodes[child].build_interaction_list()
def build_moments(self, p):
"""
Computes the mean of the sources and their moments as described in the paper
N^tkm is n_moments and M^tkm is yn_moments
"""
global octree_nodes
self.sources = self.pcd.points[self.pcd.groups == 0]
self.normals = self.pcd.normals[self.pcd.groups == 0]
self.targets = self.pcd.points[self.pcd.groups == 1]
if self.sources.shape[0] != 0:
# store mean
self.mean = np.mean(self.sources, axis=0)
# note that if there's only 1 source, then diff = 0 which is annoying
diff = self.sources-self.mean[None,:]
# compute spherical coordinates
rho = np.linalg.norm(diff, axis=-1)
# mask checks for when diff == 0
mask = rho == 0
frho = np.array(rho)
frho[mask] = 1
alpha = np.arccos(diff[:,2]/frho)
beta = np.arctan2(diff[:,1], diff[:,0])
beta[beta < 0] += 2*np.pi
beta[beta > 2*np.pi] -= 2*np.pi
alpha[mask] = 0
beta[mask] = 0
self.n_moments = {}
self.yn_moments = {}
yn = np.sum(self.sources * self.normals, axis=-1)
for t in range(1, p+1):
for j in range(1, (t+1)//2+1):
n = t+1-2*j
for m in range(-n, n+1):
# compute and store O(p^3) moments
sphs = sph_harm(m, n, beta, alpha).conj()
val = sphs
if t > 1:
val *= (rho**(t-1))
self.n_moments[(t, j, m)] = np.sum(self.normals * val[:,None], axis=0)
self.yn_moments[(t, j, m)] = np.sum(yn * val, axis=0)
# recurse
for child in self.flattened_children:
octree_nodes[child].build_moments(p)
def barnes_hut_compute(self, indicator, tot, targets, ids, means, n_moments, yn_moments):
"""
Computes the multipole expansion given the precomputed moments
Note that the computations for multiple nodes may be concatenated here so that operations are parallelized better
All variable lists are cleared once the computations are done
Note that indicator is mutable and treated as the output
"""
global harmonics
# concatenate numpy arrays for parallelization
target = np.concatenate(targets, axis=0)
id = np.concatenate(ids, axis=0)
mean = np.concatenate(means, axis=0)
n_moment = {}
for d in n_moments:
n_moment[d] = np.concatenate(n_moments[d], axis=0)
yn_moment = {}
for d in yn_moments:
yn_moment[d] = np.concatenate(yn_moments[d], axis=0)
diff = target - mean
# get spherical coordinates
r = np.linalg.norm(diff, axis=-1)
theta = np.arccos(diff[:,2]/r)
phi = np.arctan2(diff[:,1], diff[:,0])
phi[phi < 0] += 2*np.pi
phi[phi > 2*np.pi] -= 2*np.pi
n_indicator = np.zeros_like(target).astype(np.complex128)
yn_indicator = np.zeros_like(target[:,0]).astype(np.complex128)
# perform O(p^3) multipole expansion computations
for d in n_moment:
t, j, m = d
n = t+1-2*j
sphl = sph_harm(m, n, phi, theta)
n_indicator -= (r**(-t-2))[:,None] * sphl[:,None] * n_moment[d]
yn_indicator -= (r**(-t-2)) * sphl * yn_moment[d]
temp_indicator = (np.sum(target * n_indicator, axis=-1) - yn_indicator).real
# insert results into indicator
for i in range(temp_indicator.shape[0]):
indicator[id[i]] += temp_indicator[i]
targets.clear()
ids.clear()
means.clear()
for d in n_moments:
n_moments[d].clear()
for d in yn_moments:
yn_moments[d].clear()
tot[0] = 0
def barnes_hut_collect(self, indicator, tot, targets, ids, means, n_moments, yn_moments):
"""
Collects the variables needed to perform multipole expansion between interaction list members
Once the length of the variable list gets large enough, the calculation is performed and the variable list cleared
Also immediately performs the near neighbour calculations
Note that indicator is mutable and treated as the output
"""
global octree_nodes
temp_indicator = np.zeros((self.targets.shape[0],))
interaction_list = list(self.interaction_list)
neighbours = list(self.neighbours)
# collect variables, stack if necessary
for source_box in interaction_list:
if octree_nodes[source_box].sources.shape[0] == 0 or self.targets.shape[0] == 0:
continue
tot[0] += self.targets.shape[0]
targets.append(self.targets)
ids.append(self.pcd.ids[self.pcd.groups == 1])
means.append(np.vstack([octree_nodes[source_box].mean]*self.targets.shape[0]))
for d in octree_nodes[source_box].n_moments.keys():
if d not in n_moments:
n_moments[d] = []
n_moments[d].append(np.vstack([octree_nodes[source_box].n_moments[d]]*self.targets.shape[0]))
if d not in yn_moments:
yn_moments[d] = []
yn_moments[d].append(np.vstack([octree_nodes[source_box].yn_moments[d]]*self.targets.shape[0]).squeeze(-1))
target_ids = self.pcd.ids[self.pcd.groups == 1]
# perform near neighbour computations
for neighbour in neighbours:
if len(octree_nodes[neighbour].flattened_children) == 0 or len(self.flattened_children) == 0:
sources = octree_nodes[neighbour].sources
normals = octree_nodes[neighbour].normals
diff = self.targets[:,None,:] - sources[None,:,:]
dis = np.linalg.norm(diff, axis=-1)
kernel = -1/4/np.pi * np.sum(diff * normals[None,:,:], axis=-1) / np.power(dis, 3)
w = octree_nodes[neighbour].bbox.width()
# kernel[dis<2*w] = 0
temp_indicator += np.sum(kernel, axis=1)
for i in range(temp_indicator.shape[0]):
indicator[target_ids[i]] += temp_indicator[i]
# if variable list is long, perform the computation
if tot[0] > 100000:
self.barnes_hut_compute(indicator, tot, targets, ids, means, n_moments, yn_moments)
# recurse
for child in self.flattened_children:
octree_nodes[child].barnes_hut_collect(indicator, tot, targets, ids, means, n_moments, yn_moments)
def psr_solve(pcd):
"""
Library implementation of Screened Poisson Surface Reconstruction
"""
mesh = o3d.geometry.TriangleMesh.create_from_point_cloud_poisson(pcd)
def fast_solve(sources, normals, targets, p=4):
"""
Evaluates the integral equation at each target point given sources and normals
sources and normals are numpy arrays of shape (N, 3)
targets is a numpy array of shape (M, 3)
Uses an O((N+M) log (N+M)) Barnes-Hut algorithm
"""
pcd = PointCloud(sources, normals, 0)
full_pcd = pcd.merge(PointCloud(targets, np.zeros_like(targets), 1))
fmm = FMMOctree(full_pcd, max_height=8)
# compute near neighbours and interaction list
# note that we do this in separate passes, although they can technically all be done in a single pass
fmm.build_child_neighbours()
fmm.build_interaction_list()
# build moments
fmm.build_moments(p)
indicator = [0 for i in range(targets.shape[0])]
# provides mutable containers
tot = [0]
targets_list = []
ids_list = []
means_list = []
n_moments_list = {}
yn_moments_list = {}
fmm.barnes_hut_collect(indicator, tot, targets_list, ids_list, means_list, n_moments_list, yn_moments_list)
# finishes off computation for remaining variables in lists
fmm.barnes_hut_compute(indicator, tot, targets_list, ids_list, means_list, n_moments_list, yn_moments_list)
indicator = np.array(indicator, dtype=np.float64)
return indicator
def slow_solve(sources, normals, targets):
"""
Evaluates the integral equation at each target point given sources and normals
sources and normals are numpy arrays of shape (N, 3)
targets is a numpy array of shape (M, 3)
Uses the O(NM) brute-force approach
"""
diff = targets[:,None,:] - sources[None,:,:]
dis = np.linalg.norm(diff, axis=-1)
kernel = -1/4/np.pi * np.sum(diff * normals[None,:,:], axis=-1) / np.power(dis, 3)
indicator = np.sum(kernel, axis=1)
return indicator
def test_sphere(n, fast=True):
"""
Tests our methods on a point cloud with n points sampled from the unit sphere
If fast is set to True, Barnes-Hut is used, else naive
Plots the indicator against the radius of the target
"""
pcd = load_sphere(n)
ot = Octree(pcd)
sources = pcd.points.astype(np.float64)
normals = pcd.normals.astype(np.float64)
targets = ot.get_centers()[:sources.shape[0],:].astype(np.float64)
indicator = fast_solve(sources, normals, targets) if fast else slow_solve(sources, normals, targets)
rad = np.linalg.norm(targets, axis=-1)
plt.figure()
plt.title(("Barnes-Hut" if fast else "Naive") + " Indicator for Unit Sphere with {:} Sources".format(sources.shape[0]))
plt.xlabel("Radius")
plt.ylabel("Indicator")
plt.scatter(rad, indicator)
plt.ylim(bottom=-1, top=2)
plt.grid()
plt.show()
test_sphere(1000, True)
test_sphere(1000, False)
def check_harmonic():
"""
Verifies the multipole expansion derived in the paper
"""
p = 50
x = np.array([[3.0, 1.0, 1.0]])
y = np.array([[1.0, 4.0, 4.1]])
rv = np.random.uniform(-1, 1, (1,3))
yb = y + rv * np.linalg.norm(x-y)/np.linalg.norm(rv) * 0.2
true_kernel = -1/4/np.pi/np.power(np.linalg.norm(x-y), 3)
dl = x-yb
ds = y-yb
print("cartesian")
print(dl)
print(ds)
r = np.linalg.norm(dl, axis=-1)
theta = np.arccos(dl[:,2]/r)
phi = np.arctan2(dl[:,1], dl[:,0])
phi[phi < 0] += 2*np.pi
phi[phi > 2*np.pi] -= 2*np.pi
rho = np.linalg.norm(ds, axis=-1)
alpha = np.arccos(ds[:,2]/rho)
beta = np.arctan2(ds[:,1], ds[:,0])
beta[beta < 0] += 2*np.pi
beta[beta > 2*np.pi] -= 2*np.pi
val = 0
u = np.sum(dl*ds, axis=-1) / (r*rho)
print("spherical coordinates")
print(r, theta, phi)
print(rho, alpha, beta)
print("spherical to cartesian")
print(r*np.cos(phi)*np.sin(theta), r*np.sin(phi)*np.sin(theta), r*np.cos(theta))
print(rho*np.cos(beta)*np.sin(alpha), rho*np.sin(beta)*np.sin(alpha), rho*np.cos(alpha))
for t in range(1, p+1):
for j in range(1, (t+1)//2+1):
n = t+1-2*j
for m in range(-n, n+1):
sphl = sph_harm(m, n, phi, theta)
sphs = sph_harm(m, n, beta, alpha).conj()
val += (r**(-t-2)) * (rho**(t-1)) * sphl * sphs
val *= -1
print("Direct computation:", true_kernel)
print("Multipole expansion with p={:}:".format(p), val.real)
check_harmonic()
cartesian [[ 2.14281373 -3.37920128 -2.23964506]] [[ 0.14281373 -0.37920128 0.86035494]] spherical coordinates [4.58548382] [2.08107526] [5.2775185] [0.95099947] [0.44015652] [5.07257672] spherical to cartesian [2.14281373] [-3.37920128] [-2.23964506] [0.14281373] [-0.37920128] [0.86035494] Direct computation: -0.0007401833172703246 Multipole expansion with p=50: [-0.00074018]
def build_mesh():
"""
Generates the indicator function from a point cloud with oriented normals
Targets are chosen to be close to the surface by picking from an octree
The evaluated points are then used to fill in a regular grid which is then fed into Marching Cubes
"""
N = 256
pcd = load_pcd("models/bunny_1k.obj", N)
#pcd = loader.load_sphere(N)
octree = Octree(pcd)
sources = pcd.points
normals = pcd.normals
targets = octree.get_centers()
indicator = fast_solve(sources, normals, targets)
# note that filling in the dense regular grid from the sparse samples is relatively fast
xyz = octree.bbox.meshgrid(32, 32, 32)
grid = np.zeros_like(xyz[:,:,:,0])
shp = xyz.shape[:-1]
xyz = np.reshape(xyz, (-1, 3))
grid = np.reshape(grid, (-1,))
mask = np.ones((grid.shape[0],), dtype=bool)
octree.fill_grid(grid, indicator, targets, xyz, mask)
grid = np.reshape(grid, shp)
verts, faces, normals, values = measure.marching_cubes(grid, 0.5)
mesh = o3d.geometry.TriangleMesh()
mesh.vertices = o3d.utility.Vector3dVector(verts)
mesh.triangles = o3d.utility.Vector3iVector(faces[:,::-1])
mesh.compute_triangle_normals()
mesh.compute_vertex_normals()
# not sure how to save, need to run this explicitly
o3d.visualization.draw_geometries([mesh])
build_mesh()
def evaluate_runtime():
"""
Times the methods
Also provides an error graph for changing N
"""
dN = range(5, 15)
Ns = []
slow_times = []
fast_times = []
psr_times = []
error = []
for i in range(len(dN)):
N = 1 << dN[i]
Ns.append(N)
pcd = load_sphere(N)
ot = Octree(pcd)
sources = pcd.points.astype(np.float64)
normals = pcd.normals.astype(np.float64)
targets = ot.get_centers()[:sources.shape[0],:].astype(np.float64)
start_time = time.time()
slow_indicator = slow_solve(sources, normals, targets)
end_time = time.time()
slow_times.append(end_time-start_time)
start_time = time.time()
fast_indicator = fast_solve(sources, normals, targets)
end_time = time.time()
fast_times.append(end_time-start_time)
onormals = normals / np.linalg.norm(normals, axis=-1)[:,None]
opcd = o3d.geometry.PointCloud()
opcd.points = o3d.utility.Vector3dVector(sources)
opcd.normals = o3d.utility.Vector3dVector(onormals)
start_time = time.time()
psr_solve(opcd)
end_time = time.time()
psr_times.append(end_time-start_time)
diff = slow_indicator - fast_indicator
error.append(np.amax(np.abs(diff)))
plt.figure()
plt.title("Runtimes for Integral Evaluation on Unit Sphere")
plt.xlabel("N")
plt.ylabel("Runtime (s)")
plt.xscale("log", base=2)
plt.yscale("log", base=2)
plt.grid()
plt.plot(Ns, slow_times, label="Naive")
plt.plot(Ns, fast_times, label="Barnes-Hut")
plt.plot(Ns, psr_times, label="Poisson Surface Reconstruction")
print("SLOW", slow_times)
print("FAST", fast_times)
print("PSR", psr_times)
plt.legend()
plt.show()
plt.figure()
plt.title("Error for Integral Evaluation on Unit Sphere")
plt.xlabel("N")
plt.ylabel("Error")
plt.xscale("log", base=2)
plt.yscale("log", base=2)
plt.grid()
plt.plot(Ns, error)
print("ERROR", error)
plt.show()
evaluate_runtime()
SLOW [0.0, 0.000997304916381836, 0.000997304916381836, 0.004986286163330078, 0.028922557830810547, 0.08577132225036621, 0.356015682220459, 1.456134557723999, 5.888250350952148, 89.46137070655823] FAST [0.15458273887634277, 0.416886568069458, 1.0083329677581787, 2.4674012660980225, 5.697758197784424, 12.26818323135376, 27.12453532218933, 60.92998456954956, 135.7829785346985, 276.152206659317] PSR [0.22142553329467773, 0.2313852310180664, 0.21439743041992188, 0.24534249305725098, 0.22140741348266602, 0.23337435722351074, 0.23935961723327637, 0.3480672836303711, 0.3949427604675293, 0.8873372077941895]
ERROR [9.344194405469519e-05, 0.0017230328717544596, 0.007677283685422087, 0.005060738895434569, 0.005542389736835407, 0.005490284755753971, 0.006314796826543234, 0.006937496000887111, 0.007228842260259993, 0.008937695447018085]
def evaluate_p():
"""
Times the methods and provides an error graph for changing p
"""
N = 64
ps = range(1, 17)
fast_times = []
error = []
for i in range(len(ps)):
pcd = load_sphere(N)
ot = Octree(pcd)
sources = pcd.points.astype(np.float64)
normals = pcd.normals.astype(np.float64)
targets = ot.get_centers()[:sources.shape[0],:].astype(np.float64)
start_time = time.time()
slow_indicator = slow_solve(sources, normals, targets)
end_time = time.time()
start_time = time.time()
fast_indicator = fast_solve(sources, normals, targets, ps[i])
end_time = time.time()
fast_times.append(end_time-start_time)
diff = slow_indicator - fast_indicator
error.append(np.amax(np.abs(diff)))
pc = [i**3 for i in ps]
plt.figure()
plt.title("Runtimes for Integral Evaluation on Unit Sphere with N = 64")
plt.xlabel("p^3")
plt.ylabel("Runtime (s)")
# plt.xscale("log", base=2)
# plt.yscale("log", base=2)
plt.grid()
plt.plot(pc, fast_times)
print("FAST", fast_times)
plt.legend()
plt.show()
plt.figure()
plt.title("Error for Integral Evaluation on Unit Sphere with N = 64")
plt.xlabel("p")
plt.ylabel("Error")
# plt.xscale("log", base=2)
plt.yscale("log", base=2)
plt.grid()
plt.plot(ps, error)
plt.legend()
plt.show()
evaluate_p()
No artists with labels found to put in legend. Note that artists whose label start with an underscore are ignored when legend() is called with no argument.
FAST [0.3779885768890381, 0.16057038307189941, 0.24135303497314453, 0.3839740753173828, 0.593442440032959, 0.8387274742126465, 1.2057747840881348, 1.696455955505371, 2.378607749938965, 3.060812473297119, 3.998304843902588, 5.156172752380371, 6.632258892059326, 8.584449529647827, 10.937072277069092, 11.603961944580078]
No artists with labels found to put in legend. Note that artists whose label start with an underscore are ignored when legend() is called with no argument.
def evaluate_p_and_N():
"""
Times the methods and provides an error graph for changing p and N
"""
dN = range(5, 10)
ps = [2, 4, 8, 16]
Ns = []
slow_times = []
fast_times = [[] for p in ps]
error = [[] for p in ps]
for i in range(len(dN)):
N = 1 << dN[i]
Ns.append(N)
pcd = load_sphere(N)
ot = Octree(pcd)
sources = pcd.points.astype(np.float64)
normals = pcd.normals.astype(np.float64)
targets = ot.get_centers()[:sources.shape[0],:].astype(np.float64)
start_time = time.time()
slow_indicator = slow_solve(sources, normals, targets)
end_time = time.time()
slow_times.append(end_time-start_time)
for j in range(len(ps)):
print(N, j)
start_time = time.time()
fast_indicator = fast_solve(sources, normals, targets, ps[j])
end_time = time.time()
fast_times[j].append(end_time-start_time)
diff = slow_indicator - fast_indicator
error[j].append(np.amax(np.abs(diff)))
plt.figure()
plt.title("Runtimes for Integral Evaluation on Unit Sphere")
plt.xlabel("N")
plt.ylabel("Runtime (s)")
plt.xscale("log", base=2)
plt.yscale("log", base=2)
plt.grid()
plt.plot(Ns, slow_times, label="Naive")
for j in range(len(ps)):
plt.plot(Ns, fast_times[j], label="Barnes-Hut (p={})".format(ps[j]))
print("SLOW", slow_times)
print("FAST", fast_times)
plt.legend()
plt.show()
plt.figure()
plt.title("Error for Integral Evaluation on Unit Sphere")
plt.xlabel("N")
plt.ylabel("Error")
plt.xscale("log", base=2)
plt.yscale("log", base=2)
plt.grid()
for j in range(len(ps)):
plt.plot(Ns, error[j], label="p={}".format(ps[j]))
plt.legend()
plt.show()
evaluate_p_and_N()
32 0 32 1 32 2 32 3 64 0 64 1 64 2 64 3 128 0 128 1 128 2 128 3 256 0 256 1 256 2 256 3 512 0 512 1 512 2 512 3 SLOW [0.0, 0.0, 0.0009984970092773438, 0.004987239837646484, 0.021942138671875] FAST [[0.07978940010070801, 0.16655516624450684, 0.3749964237213135, 0.8669297695159912, 1.968766450881958], [0.1266939640045166, 0.3839402198791504, 1.0362284183502197, 2.146228551864624, 5.111295223236084], [0.5624973773956299, 1.7653083801269531, 4.399263143539429, 10.068067789077759, 25.57215404510498], [3.4707140922546387, 11.612904787063599, 27.883418321609497, 70.079660654068, 186.57931184768677]]