192 lines
6.0 KiB
Python
192 lines
6.0 KiB
Python
"""
|
|
nsphere.py
|
|
--------------
|
|
|
|
Functions for fitting and minimizing nspheres:
|
|
circles, spheres, hyperspheres, etc.
|
|
"""
|
|
import numpy as np
|
|
|
|
from . import util
|
|
from . import convex
|
|
|
|
from .constants import log, tol
|
|
|
|
try:
|
|
# scipy is a soft dependency
|
|
from scipy import spatial
|
|
from scipy.optimize import leastsq
|
|
except BaseException as E:
|
|
# raise the exception when someone tries to use it
|
|
from . import exceptions
|
|
leastsq = exceptions.closure(E)
|
|
spatial = exceptions.ExceptionModule(E)
|
|
|
|
try:
|
|
import psutil
|
|
|
|
def _MAX_MEMORY():
|
|
# if we have psutil check actual free memory when called
|
|
return psutil.virtual_memory().free / 2.0
|
|
except ImportError:
|
|
def _MAX_MEMORY():
|
|
# use a hardcoded best guess estimate
|
|
return 1e9
|
|
|
|
|
|
def minimum_nsphere(obj):
|
|
"""
|
|
Compute the minimum n- sphere for a mesh or a set of points.
|
|
|
|
Uses the fact that the minimum n- sphere will be centered at one of
|
|
the vertices of the furthest site voronoi diagram, which is n*log(n)
|
|
but should be pretty fast due to using the scipy/qhull implementations
|
|
of convex hulls and voronoi diagrams.
|
|
|
|
Parameters
|
|
----------
|
|
obj : (n, d) float or trimesh.Trimesh
|
|
Points or mesh to find minimum bounidng nsphere
|
|
|
|
Returns
|
|
----------
|
|
center : (d,) float
|
|
Center of fitted n- sphere
|
|
radius : float
|
|
Radius of fitted n-sphere
|
|
"""
|
|
# reduce the input points or mesh to the vertices of the convex hull
|
|
# since we are computing the furthest site voronoi diagram this reduces
|
|
# the input complexity substantially and returns the same value
|
|
points = convex.hull_points(obj)
|
|
|
|
# we are scaling the mesh to a unit cube
|
|
# this used to pass qhull_options 'QbB' to Voronoi however this had a bug somewhere
|
|
# to avoid this we scale to a unit cube ourselves inside this function
|
|
points_origin = points.min(axis=0)
|
|
points_scale = points.ptp(axis=0).min()
|
|
points = (points - points_origin) / points_scale
|
|
|
|
# if all of the points are on an n-sphere already the voronoi
|
|
# method will fail so we check a least squares fit before
|
|
# bothering to compute the voronoi diagram
|
|
fit_C, fit_R, fit_E = fit_nsphere(points)
|
|
# return fit radius and center to global scale
|
|
fit_R = (((points - fit_C)**2).sum(axis=1).max() ** .5) * points_scale
|
|
fit_C = (fit_C * points_scale) + points_origin
|
|
|
|
if fit_E < 1e-6:
|
|
log.debug('Points were on an n-sphere, returning fit')
|
|
return fit_C, fit_R
|
|
|
|
# calculate a furthest site voronoi diagram
|
|
# this will fail if the points are ALL on the surface of
|
|
# the n-sphere but hopefully the least squares check caught those cases
|
|
# , qhull_options='QbB Pp')
|
|
voronoi = spatial.Voronoi(points, furthest_site=True)
|
|
|
|
# find the maximum radius^2 point for each of the voronoi vertices
|
|
# this is worst case quite expensive but we have taken
|
|
# convex hull to reduce n for this operation
|
|
# we are doing comparisons on the radius squared then rooting once
|
|
try:
|
|
# cdist is massivly faster than looping or tiling methods
|
|
# although it does create a very large intermediate array
|
|
# first, get an order of magnitude memory size estimate
|
|
# a float64 would be 8 bytes per entry plus overhead
|
|
memory_estimate = len(voronoi.vertices) * len(points) * 9
|
|
if memory_estimate > _MAX_MEMORY():
|
|
raise MemoryError
|
|
radii_2 = spatial.distance.cdist(
|
|
voronoi.vertices, points,
|
|
metric='sqeuclidean').max(axis=1)
|
|
except MemoryError:
|
|
# log the MemoryError
|
|
log.warning('MemoryError: falling back to slower check!')
|
|
# fall back to a potentially very slow list comprehension
|
|
radii_2 = np.array([((points - v) ** 2).sum(axis=1).max()
|
|
for v in voronoi.vertices])
|
|
|
|
# we want the smallest sphere so take the min of the radii
|
|
radii_idx = radii_2.argmin()
|
|
|
|
# return voronoi radius and center to global scale
|
|
radius_v = np.sqrt(radii_2[radii_idx]) * points_scale
|
|
center_v = (voronoi.vertices[radii_idx] *
|
|
points_scale) + points_origin
|
|
|
|
if radius_v > fit_R:
|
|
return fit_C, fit_R
|
|
|
|
return center_v, radius_v
|
|
|
|
|
|
def fit_nsphere(points, prior=None):
|
|
"""
|
|
Fit an n-sphere to a set of points using least squares.
|
|
|
|
Parameters
|
|
------------
|
|
points : (n, d) float
|
|
Points in space
|
|
prior : (d,) float
|
|
Best guess for center of nsphere
|
|
|
|
Returns
|
|
---------
|
|
center : (d,) float
|
|
Location of center
|
|
radius : float
|
|
Mean radius across circle
|
|
error : float
|
|
Peak to peak value of deviation from mean radius
|
|
"""
|
|
# make sure points are numpy array
|
|
points = np.asanyarray(points, dtype=np.float64)
|
|
# create ones so we can dot instead of using slower sum
|
|
ones = np.ones(points.shape[1])
|
|
|
|
def residuals(center):
|
|
# do the axis sum with a dot
|
|
# this gets called a LOT so worth optimizing
|
|
radii_sq = np.dot((points - center) ** 2, ones)
|
|
# residuals are difference between mean
|
|
# use our sum mean vs .mean() as it is slightly faster
|
|
return radii_sq - (radii_sq.sum() / len(radii_sq))
|
|
|
|
if prior is None:
|
|
guess = points.mean(axis=0)
|
|
else:
|
|
guess = np.asanyarray(prior)
|
|
|
|
center_result, return_code = leastsq(residuals,
|
|
guess,
|
|
xtol=1e-8)
|
|
|
|
if not (return_code in [1, 2, 3, 4]):
|
|
raise ValueError('Least square fit failed!')
|
|
|
|
radii = util.row_norm(points - center_result)
|
|
radius = radii.mean()
|
|
error = radii.ptp()
|
|
return center_result, radius, error
|
|
|
|
|
|
def is_nsphere(points):
|
|
"""
|
|
Check if a list of points is an nsphere.
|
|
|
|
Parameters
|
|
-----------
|
|
points : (n, dimension) float
|
|
Points in space
|
|
|
|
Returns
|
|
-----------
|
|
check : bool
|
|
True if input points are on an nsphere
|
|
"""
|
|
center, radius, error = fit_nsphere(points)
|
|
check = error < tol.merge
|
|
return check
|