我正在尝试将 3D
numpy
阵列中包含的矩形棱柱与阵列的主轴对齐。具体来说,我需要通过旋转整个阵列来完成这种对齐,而不是提取对象的坐标,然后旋转这些点(这是因为从背景中完美分割对象对于我的应用程序来说是不现实的)。
我有一种在非常特殊的情况下有效的方法,但考虑到该方法的普遍性,它似乎出人意料地敏感。简而言之,我的方法仅适用于非常特定的对象方向。
我正在寻求有关 (a) 我的方法有什么问题或 (b) 实现相同目标的不同方法的指导。
from scipy.ndimage import affine_transform
from skimage import measure
def find_orientation(prism):
# Calculate second-order moments
M = measure.moments_central(prism, order=2)
# Constructing the covariance matrix from the central moments
cov_matrix = np.array(
[
[M[2, 0, 0], M[1, 1, 0], M[1, 0, 1]],
[M[1, 1, 0], M[0, 2, 0], M[0, 1, 1]],
[M[1, 0, 1], M[0, 1, 1], M[0, 0, 2]],
]
)
# Compute eigenvectors from the covariance matrix
eigenvalues, eigenvectors = np.linalg.eig(cov_matrix)
return eigenvectors
def rotate_to_align(prism):
# Calculate orientation matrix using moments method
orientation_matrix = find_orientation(prism)
# Rotate the prism using the orientation matrix
aligned_prism = affine_transform(prism, orientation_matrix.T, order=3)
return aligned_prism
aligned_array = rotate_to_align(misaligned_array)
请参阅此笔记本了解完整详细信息(对于SO来说太长了): https://github.com/petermattia/3d-alignment/blob/main/prism_alignment.ipynb
相关但不同的问题/资源:
谢谢!
好的,这样你就有了体积,包含一个实心盒子,以某种方式定向。您正在寻找该物体的轴。
根据力矩计算轴似乎不稳定。
我建议使用对象每个面的表面法线。
您可以通过对体积的梯度进行聚类来获得这些值。这将为您提供六个稳定的表面法线簇。
配对相反的向量。合理地对它们进行排序,以减少意外的轮换。然后应用一些更多的线性代数来确保您实际上拥有彼此正交的向量。如果盒子碰巧有一点剪切或者只是由于有限算术引起的错误,则不必是这种情况。
这并不完美。插值/采样和有限算术是问题。
# imports
import numpy as np
from numpy.linalg import norm, inv
import cv2 as cv
import matplotlib.pyplot as plt
import scipy.ndimage as ndi
# first bunch of utility functions
def normalize(vec):
vec = np.asarray(vec)
return vec / norm(vec)
def translate3(tx=0, ty=0, tz=0):
T = np.eye(4)
T[:3, 3] = tx, ty, tz
return T
def rotate3_axis(axis, radians=None, degrees=None):
axis = np.asarray(axis, dtype=np.float64)
assert (radians is not None) ^ (degrees is not None)
if degrees is not None:
radians = np.deg2rad(degrees)
if radians is not None:
axis = normalize(axis) * radians
R, jac = cv.Rodrigues(axis)
T = np.eye(4)
T[:3, :3] = R
return T
def rotate3_R(R):
T = np.eye(4)
T[:3, :3] = R
return T
def rotate3(axis, **kwargs):
if isinstance(axis, np.ndarray) and axis.ndim == 2:
return rotate3_R(R=axis)
else:
return rotate3_axis(axis, **kwargs)
def scale3(s=1, sx=1, sy=1, sz=1):
return np.diag([s*sx, s*sy, s*sz, 1.0])
axis_names = ['x', 'y', 'z']
plane_names = ['yz', 'xz', 'xy']
def show(volume, gradient=False):
vmin = volume.min()
vmax = volume.max()
fig, axes = plt.subplots(figsize=(10, 10), nrows=1, ncols=3)
for k, axis in enumerate(axes):
axis_slice = np.take(volume, indices=volume.shape[k] // 2, axis=k)
axis.set_title(f"{axis_names[k]}-axis, {plane_names[k]}-plane")
axis.set_xlabel(plane_names[k][1])
axis.set_ylabel(plane_names[k][0])
axis.imshow(axis_slice, cmap="gray", vmin=vmin, vmax=vmax)
plt.tight_layout()
plt.show()
# phantom
vsize = np.array([50, 50, 50])
# box with different sizes in each dimension
box_size = np.array([40, 20, 10], dtype=int)
box_p0 = (vsize - box_size) // 2
box_p1 = box_p0 + box_size
volume = np.zeros(vsize, dtype=np.float32)
(bx0, by0, bz0), (bx1, by1, bz1) = box_p0, box_p1
volume[bx0:bx1, by0:by1, bz0:bz1] = 1
Tt = translate3(*(vsize/2))
Tr = rotate3(axis=(0,0,1), degrees=30)
T = Tt @ Tr @ inv(Tt)
warped = ndi.affine_transform(
input=volume,
matrix=T,
# offset ignored
output_shape=vsize,
order=1,
mode='nearest',
)
# per-voxel gradient of the warped volume
def gradients(vol):
gradx = np.gradient(vol, edge_order=2, axis=0)
grady = np.gradient(vol, edge_order=2, axis=1)
gradz = np.gradient(vol, edge_order=2, axis=2)
return np.stack([gradx, grady, gradz], axis=-1)
grad = gradients(warped)
mag = norm(grad, axis=-1)
# cluster the gradient vectors
# seven clusters
# one per face, and background/zero
gradvectors = grad.reshape((-1, 3))
from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters=7, random_state=0, n_init=10).fit(gradvectors)
labels = kmeans.labels_
clusters = kmeans.cluster_centers_
print("clusters (mean):")
print(clusters)
# median is not advisable. median is a single value, not a mean.
# smarter to discard outliers, then take mean
def inliers_of(target, vecs):
dists = norm(vecs - target, axis=1)
keep = (dists <= np.quantile(dists, 0.5))
return vecs[keep]
clusters = np.array([
inliers_of(cluster, gradvectors[labels == i]).mean(axis=0)
for i, cluster in enumerate(clusters)
])
print("clusters (mean of 90%):")
print(clusters)
(hist, edges) = np.histogram(labels, bins=np.arange(kmeans.n_clusters+1))
print("counts:", hist)
#bg_label = np.argmax(hist) # most common
bg_label = np.argmin(norm(clusters, axis=1)) # closest to zero
print("background label:", bg_label)
face_vecs = np.delete(clusters, bg_label, axis=0)
#face_vecs /= norm(face_vecs, axis=1, keepdims=True)
print("foreground clusters:")
print(face_vecs)
clusters (mean):
[[-0.00001 0.00004 0. ]
[-0.00007 0.00034 0.49484]
[-0.00007 0.00034 -0.49484]
[ 0.29297 0.43958 0. ]
[-0.29142 -0.4349 0. ]
[-0.4351 0.27852 0. ]
[ 0.4278 -0.28183 0. ]]
clusters (mean of 90%):
[[ 0. -0. 0. ]
[ 0. 0. 0.5 ]
[ 0. 0. -0.5 ]
[ 0.29585 0.45781 0. ]
[-0.28876 -0.4558 0. ]
[-0.44932 0.28824 0. ]
[ 0.4462 -0.27927 0. ]]
counts: [120012 1546 1546 622 632 316 326]
background label: 0
foreground clusters:
[[ 0. 0. 0.5 ]
[ 0. 0. -0.5 ]
[ 0.29585 0.45781 0. ]
[-0.28876 -0.4558 0. ]
[-0.44932 0.28824 0. ]
[ 0.4462 -0.27927 0. ]]
# it should be three pairs of face vectors, where each pair is opposite faces
# check which vectors are a pair by checking the dot product
def find_opposite_faces(face_vecs):
pairs = []
for i, vec1 in enumerate(face_vecs):
for j, vec2 in enumerate(face_vecs):
if i >= j: continue
dist = np.dot(normalize(vec1), normalize(vec2))
if dist < -0.999:
pairs.append((i, j))
print(f"({i}, {j}) dist {dist}")
return pairs
pairs = find_opposite_faces(face_vecs)
print(pairs)
(0, 1) dist -1.0
(2, 3) dist -0.9999593496322632
(4, 5) dist -0.9999381303787231
[(0, 1), (2, 3), (4, 5)]
# pick one vector per pair which should be pointing positively (dot with [1,1,1] is positive)
axis_vecs = np.array([
face_vecs[i1] if np.dot(face_vecs[i1], [1,1,1]) >= 0 else face_vecs[i2]
for (i1, i2) in pairs
])
print(axis_vecs)
[[ 0. 0. 0.5 ]
[ 0.29585 0.45781 0. ]
[ 0.4462 -0.27927 0. ]]
# vectors are normal
# build basis from the three vectors
# for least amount of "accidental" rotation, find closest to X, Y, Z axes
# 1. find the one closest to X axis, remove
# 2. find the one closest to Y axis, remove
# 3. remaining one taken for Z axis
def find_closest(vecs, target):
dists = np.dot(target, vecs.T)
return np.argmax(dists)
remaining = [0, 1, 2]
index = find_closest(axis_vecs[remaining], [1,0,0])
x_axis = axis_vecs[remaining[index]]
remaining.pop(index)
index = find_closest(axis_vecs[remaining], [0,1,0])
y_axis = axis_vecs[remaining[index]]
remaining.pop(index)
z_axis = axis_vecs[remaining[0]]
basis = np.array([x_axis, y_axis, z_axis]).T
print(basis)
[[ 0.4462 0.29585 0. ]
[-0.27927 0.45781 0. ]
[ 0. 0. 0.5 ]]
# this basis may not be orthogonal, so we need to adjust it
# Modified Gram-Schmidt process, for numerical stability
# https://en.wikipedia.org/wiki/Gram%E2%80%93Schmidt_process
def modified_gram_schmidt(basis):
"""
basis: NxN matrix of column vectors
"""
basis = np.asarray(basis, dtype=np.float64)
basis = basis.copy()
M,N = basis.shape
for i in range(N):
basis[:,i] /= norm(basis[:,i])
for j in range(i+1, N):
basis[:,j] -= np.dot(basis[:,j], basis[:,i]) * basis[:,i]
return basis
mgs_basis = modified_gram_schmidt(basis)
print(mgs_basis)
[[ 0.84766 0.53054 0. ]
[-0.53054 0.84766 0. ]
[ 0. 0. 1. ]]
# how close is this to the 30 degrees applied originally?
(rvec, jac) = cv.Rodrigues(mgs_basis)
print(np.rad2deg(norm(rvec)))
32.04207290905596
# now rotate the volume to align with the basis
Tt = translate3(*(vsize/2))
Tr = rotate3(mgs_basis)
T_unwarp = Tt @ Tr @ inv(Tt)
unwarped = ndi.affine_transform(
input=warped,
matrix=T_unwarp,
# offset ignored
output_shape=vsize,
order=1,
mode='nearest',
)