Utonia / geometry_utils.py
yujia
init utonia
d4c7a24
import numpy as np
from scipy.spatial.transform import Rotation as R
def pad_0001(Ts):
Ts = np.asarray(Ts)
if Ts.shape[-2:] == (4, 4):
return Ts
if Ts.shape[-2:] != (3, 4):
raise ValueError("Ts must have shape (..., 3, 4) or (..., 4, 4)")
pad = np.zeros((*Ts.shape[:-2], 1, 4), dtype=Ts.dtype)
pad[..., 0, 3] = 1
return np.concatenate([Ts, pad], axis=-2)
def T_to_C(T):
T = np.asarray(T)
if T.shape != (4, 4):
raise ValueError("T must be shape (4,4)")
Rm = T[:3, :3]
t = T[:3, 3]
return -Rm.T @ t
def im_distance_to_im_depth(im_dist, K):
im_dist = np.asarray(im_dist)
H, W = im_dist.shape[:2]
ys, xs = np.indices((H, W), dtype=np.float32)
fx, fy = K[0, 0], K[1, 1]
cx, cy = K[0, 2], K[1, 2]
x = (xs - cx) / max(fx, 1e-8)
y = (ys - cy) / max(fy, 1e-8)
ray_norm = np.sqrt(x * x + y * y + 1.0)
return im_dist / np.clip(ray_norm, 1e-8, None)
def im_depth_to_point_cloud(im_depth, K, T, to_image=False, ignore_invalid=False):
im_depth = np.asarray(im_depth)
H, W = im_depth.shape[:2]
ys, xs = np.indices((H, W), dtype=np.float32)
fx, fy = K[0, 0], K[1, 1]
cx, cy = K[0, 2], K[1, 2]
z = im_depth.reshape(-1)
x = (xs.reshape(-1) - cx) / max(fx, 1e-8) * z
y = (ys.reshape(-1) - cy) / max(fy, 1e-8) * z
pts_cam = np.stack([x, y, z], axis=-1)
T = np.asarray(T)
if T.shape != (4, 4):
raise ValueError("T must be shape (4,4)")
Rm = T[:3, :3]
t = T[:3, 3]
# Assume T is world->camera; invert to camera->world for point transform.
pts_world = (Rm.T @ (pts_cam - t).T).T
if ignore_invalid:
valid = np.isfinite(pts_world).all(axis=1) & (z > 0)
pts_world = pts_world[valid]
if to_image:
return pts_world.reshape(H, W, 3)
return pts_world
def rotx(x, theta=90):
"""
Rotate x by theta degrees around the x-axis
"""
theta = np.deg2rad(theta)
rot_matrix = np.array(
[
[1, 0, 0, 0],
[0, np.cos(theta), -np.sin(theta), 0],
[0, np.sin(theta), np.cos(theta), 0],
[0, 0, 0, 1],
]
)
return rot_matrix@ x
def Coord2zup(points, extrinsics, normals = None):
"""
Convert the dust3r coordinate system to the z-up coordinate system
"""
points = np.concatenate([points, np.ones([points.shape[0], 1])], axis=1).T
points = rotx(points, -90)[:3].T
if normals is not None:
normals = np.concatenate([normals, np.ones([normals.shape[0], 1])], axis=1).T
normals = rotx(normals, -90)[:3].T
normals = normals / np.linalg.norm(normals, axis=1, keepdims=True)
t = np.min(points,axis=0)
points -= t
extrinsics = rotx(extrinsics, -90)
extrinsics[:, :3, 3] -= t.T
return points, extrinsics, normals
def _ransac_plane(points, distance_threshold=0.01, ransac_n=3, num_iterations=1000):
if points.shape[0] < ransac_n:
raise ValueError("Not enough points for plane fitting.")
best_inliers = None
best_plane = None
rng = np.random.default_rng(42)
for _ in range(num_iterations):
sample_idx = rng.choice(points.shape[0], size=ransac_n, replace=False)
p0, p1, p2 = points[sample_idx]
normal = np.cross(p1 - p0, p2 - p0)
norm = np.linalg.norm(normal)
if norm < 1e-8:
continue
normal = normal / norm
d = -np.dot(normal, p0)
dist = np.abs(points @ normal + d)
inliers = np.where(dist < distance_threshold)[0]
if best_inliers is None or len(inliers) > len(best_inliers):
best_inliers = inliers
best_plane = np.array([normal[0], normal[1], normal[2], d], dtype=np.float64)
if best_inliers is None or best_plane is None:
raise ValueError("Failed to fit plane with RANSAC.")
return best_plane, best_inliers
def extract_and_align_ground_plane(points,
colors=None,
normals=None,
height_percentile=20,
ransac_distance_threshold=0.01,
ransac_n=3,
ransac_iterations=1000,
max_angle_degree=40,
max_trials=6):
points = np.asarray(points)
if points.ndim != 2 or points.shape[1] != 3:
raise ValueError("points must be shaped (N, 3)")
aligned_colors = np.asarray(colors) if colors is not None else None
aligned_normals = np.asarray(normals) if normals is not None else None
z_vals = points[:, 2]
z_thresh = np.percentile(z_vals, height_percentile)
low_indices = np.where(z_vals <= z_thresh)[0]
remaining_indices = low_indices.copy()
for trial in range(max_trials):
if len(remaining_indices) < ransac_n:
raise ValueError("Not enough points left to fit a plane.")
candidate_points = points[remaining_indices]
plane_model, inliers = _ransac_plane(
candidate_points,
distance_threshold=ransac_distance_threshold,
ransac_n=ransac_n,
num_iterations=ransac_iterations,
)
a, b, c, d = plane_model
normal = np.array([a, b, c])
normal /= np.linalg.norm(normal)
angle = np.arccos(np.clip(np.dot(normal, [0, 0, 1]), -1.0, 1.0)) * 180 / np.pi
if angle <= max_angle_degree:
inliers_global = remaining_indices[inliers]
target = np.array([0, 0, 1])
axis = np.cross(normal, target)
axis_norm = np.linalg.norm(axis)
if axis_norm < 1e-6:
rotation_matrix = np.eye(3)
else:
axis /= axis_norm
rot_angle = np.arccos(np.clip(np.dot(normal, target), -1.0, 1.0))
rotation = R.from_rotvec(axis * rot_angle)
rotation_matrix = rotation.as_matrix()
rotated_points = points @ rotation_matrix.T
ground_points_z = rotated_points[inliers_global, 2]
offset = np.mean(ground_points_z)
rotated_points[:, 2] -= offset
if aligned_normals is not None and len(aligned_normals) == len(points):
aligned_normals = aligned_normals @ rotation_matrix.T
aligned_normals = aligned_normals / np.clip(np.linalg.norm(aligned_normals, axis=-1, keepdims=True), 1e-8, None)
return rotated_points, aligned_colors, aligned_normals, inliers_global, rotation_matrix, offset
else:
rejected_indices = remaining_indices[inliers]
remaining_indices = np.setdiff1d(remaining_indices, rejected_indices)
raise ValueError("Failed to find a valid ground plane within max trials.")