Newer
Older
# # ls_ellipsoid and polyToParams3D functions are taken and adapted from
# http://www.juddzone.com/ALGORITHMS/least_squares_3D_ellipsoid.html
# # FindBestFittingEllipsoid3D and GetConvexHull3D are adapted from a discussion at
# https://stackoverflow.com/questions/58501545/python-fit-3d-ellipsoid-oblate-prolate-to-3d-points
from scipy.spatial import ConvexHull
from numpy.linalg import eig, inv
from brick.general.type import site_h, array_t
from brick.component.soma import soma_t
def ls_ellipsoid(xx: array_t, yy: array_t, zz: array_t) -> array_t: # finds the ellipsoid
"""
least squares fit to a 3D-ellipsoid
Ax^2 + By^2 + Cz^2 + Dxy + Exz + Fyz + Gx + Hy + Iz = 1
Note that sometimes it is expressed as a solution to
Ax^2 + By^2 + Cz^2 + 2Dxy + 2Exz + 2Fyz + 2Gx + 2Hy + 2Iz = 1
where the last six terms have a factor of 2 in them
This is in anticipation of forming a matrix with the polynomial coefficients.
Those terms with factors of 2 are all off diagonal elements. These contribute
two terms when multiplied out (symmetric) so would need to be divided by two
"""
# change xx from vector of length N to Nx1 matrix so we can use hstack
x = xx[:, np_.newaxis]
y = yy[:, np_.newaxis]
z = zz[:, np_.newaxis]
# Ax^2 + By^2 + Cz^2 + Dxy + Exz + Fyz + Gx + Hy + Iz = 1
J = np_.hstack((x * x, y * y, z * z, x * y, x * z, y * z, x, y, z))
K = np_.ones_like(x) # column of ones
# np.hstack performs a loop over all samples and creates
# a row in J for each x,y,z sample:
# J[ix,0] = x[ix]*x[ix]
# J[ix,1] = y[ix]*y[ix]
# etc.
JT = J.transpose()
JTJ = np_.dot(JT, J)
InvJTJ = np_.linalg.inv(JTJ);
ABC = np_.dot(InvJTJ, np_.dot(JT, K))
# Rearrange, move the 1 to the other side
# Ax^2 + By^2 + Cz^2 + Dxy + Exz + Fyz + Gx + Hy + Iz - 1 = 0
# or
# Ax^2 + By^2 + Cz^2 + Dxy + Exz + Fyz + Gx + Hy + Iz + J = 0
# where J = -1
ellipsoid_coef = np_.append(ABC, -1)
def polyToParams3D(ellipsoid_coef: array_t, printMe: bool = False) -> tuple:
"""
gets 3D parameters of an ellipsoid.
convert the polynomial form of the 3D-ellipsoid to parameters
center, axes, and transformation matrix
vec is the vector whose elements are the polynomial
coefficients A..J
returns (center, axes, rotation matrix)
Algebraic form: X.T * Amat * X --> polynomial form
"""
if printMe:
print('\npolynomial\n', ellipsoid_coef)
[
[ellipsoid_coef[0], ellipsoid_coef[3] / 2.0, ellipsoid_coef[4] / 2.0, ellipsoid_coef[6] / 2.0],
[ellipsoid_coef[3] / 2.0, ellipsoid_coef[1], ellipsoid_coef[5] / 2.0, ellipsoid_coef[7] / 2.0],
[ellipsoid_coef[4] / 2.0, ellipsoid_coef[5] / 2.0, ellipsoid_coef[2], ellipsoid_coef[8] / 2.0],
[ellipsoid_coef[6] / 2.0, ellipsoid_coef[7] / 2.0, ellipsoid_coef[8] / 2.0, ellipsoid_coef[9]]
])
if printMe:
print('\nAlgebraic form of polynomial\n', Amat)
# See B.Bartoni, Preprint SMU-HEP-10-14 Multi-dimensional Ellipsoidal Fitting
# equation 20 for the following method for finding the center
A3 = Amat[0:3, 0:3]
A3inv = inv(A3)
ofs = ellipsoid_coef[6:9] / 2.0
center = -np_.dot(A3inv, ofs)
if printMe:
print('\nCenter at:', center)
# Center the ellipsoid at the origin
Tofs[3, 0:3] = center
R = np_.dot(Tofs, np_.dot(Amat, Tofs.T))
if printMe:
print('\nAlgebraic form translated to center\n', R, '\n')
R3 = R[0:3, 0:3]
s1 = -R[3, 3]
R3S = R3 / s1
(el, ec) = eig(R3S)
# Sorting in descending order of the eigenvalues and eigenvectors
sorting_vec = np_.argsort(-el)
el = el[sorting_vec]
ec = ec[:, sorting_vec]
# Calculating cartesian axes
recip = 1.0 / el
axes = np_.sqrt(recip) # axes are in ascending order
if printMe:
print('\nAxes are\n', axes, '\n')
# Calculating the orientation
inverse = inv(ec) # inverse is actually the transpose here
if printMe:
print('\nRotation matrix\n', inverse)
# Calculating spherical axes
# # eigenvector 1
r1 = np_.sqrt(sum(axe**2 for axe in ec[:, 0]))
phi1 = np_.arctan2(ec[1, 0], ec[0, 0])
theta1 = np_.arccos(ec[2, 0] / r1)
spherical_axes1 = (r1, theta1, phi1)
# # eigenvector 2
r2 = np_.sqrt(sum(axe**2 for axe in ec[:, 1]))
phi2 = np_.arctan2(ec[1, 1], ec[0, 1])
theta2 = np_.arccos(ec[2, 1] / r2)
spherical_axes2 = (r2, theta2, phi2)
spherical_axes = (spherical_axes1, spherical_axes2)
if printMe:
print('\nAxes in spherical coord are\n', spherical_axes, '\n')
return center, axes, inverse, spherical_axes
def GetConvexHull3D(soma_sites: site_h) -> tuple:
soma_3D = np_.stack((soma_sites[0], soma_sites[1], soma_sites[2]), axis=-1)
hull_3D = ConvexHull(soma_3D)
volume_hull = hull_3D.volume
len_hull = len(hull_3D.vertices)
hull = np_.zeros((len_hull, 3))
for i in range(len(hull_3D.vertices)):
hull[i] = soma_3D[hull_3D.vertices[i]]
convex_hull = np_.transpose(hull)
return convex_hull, volume_hull
def FindBestFittingEllipsoid3D(soma: soma_t) -> tuple:
"""
Find the best fitting ellipsoid for the data points in 3D based on their convex hull.
Return Tuple[ellipsoid coefficients, ellipsoid center, ellipsoid axes, ellipsoid orientation]
"""
# get convex hull
convex_hull, volume_hull = GetConvexHull3D(soma.sites)
# fit ellipsoid on the convex hull
# # get ellipsoid polynomial coefficients
ellipsoid_coef = ls_ellipsoid(convex_hull[0], convex_hull[1], convex_hull[2])
# # get ellipsoid 3D parameters
center, axes, orientation, spherical_coor = polyToParams3D(ellipsoid_coef, False)
return ellipsoid_coef, center, axes, orientation, spherical_coor, convex_hull, volume_hull