changh95's picture
tt-model push diffusion-planner-p150 (container)
4d9b003 verified
Raw History Blame Contribute Delete
8.45 kB
# SPDX-License-Identifier: Apache-2.0
"""C10 geometry (sweep and ego-motion parts): rigid transforms in float64, Autoware's float32 sweep transform, and
the yaw / quaternion conventions of the LiDAR detectors.
Naming: ``T_a_from_b`` maps points of frame ``b`` into frame ``a`` (4x4, metres, row-major, translation in the last
column), so ``p_a = T_a_from_b @ p_b`` and ``T_a_from_c = T_a_from_b @ T_b_from_c``.
Two precisions on purpose:
- **float64** for composing poses (``compose``, ``invert_rigid``): what tf2 does, and what every port uses unless its
Autoware node does otherwise (S:streampetr:386; S:pointpainting:212-215; S:bevformer:437).
- **float32 as Autoware's lidar_centerpoint does it**: the node casts each TF to ``Eigen::Affine3f``, inverts and
multiplies in float32 (``pointcloud_densification.cpp:47-51,82-86``; ``voxel_generator.cpp:63``) and the CUDA
kernel ``generateSweepPoints_kernel`` applies the float32 affine as ``m0*x + m4*y + m8*z + m12`` (column-major,
``preprocess_kernel.cu:58-90``). :func:`invert_affine_f32` and :func:`transform_points_f32` reproduce that
arithmetic in numpy float32 without fused multiply-adds, so they agree with the GPU to the last bit except where
nvcc contracts a multiply-add (an FMA rounds once): a sub-micrometre difference, irrelevant to 0.32 m pillars.
Yaw conventions (S:centerpoint:305-307): the deployed CenterPoint ONNX regresses the "mmdet3d" yaw; Autoware
publishes ``yaw_ros = -yaw_net - pi/2`` counter-clockwise from +x of ``base_link`` (``ros_utils.cpp:55-60``).
numpy only; no side effects on import.
"""
from __future__ import annotations
import math
from typing import Any, Sequence
import numpy as np
from .io import parse_transform
__all__ = [
"as_transform",
"compose",
"invert_rigid",
"invert_affine_f32",
"compose_f32",
"transform_points",
"transform_points_f32",
"rotation_from_rpy",
"transform_from_xyz_rpy",
"quaternion_wxyz_from_yaw",
"yaw_from_quaternion_wxyz",
"yaw_from_rotation",
"wrap_angle",
"mmdet_yaw_to_ros",
"ros_yaw_to_mmdet",
]
# ------------------------------------------------------------------------------------------------- transforms
def as_transform(obj: Any, *, field: str = "transform") -> np.ndarray:
"""Any spelling :func:`ttaw.io.parse_transform` accepts (4x4 / 3x4 array, ``{matrix}``,
``{translation, rotation_wxyz | rotation_xyzw | rotation}``, ``{x, y, z, roll, pitch, yaw}``) -> a validated
(4, 4) float64 rigid transform. A malformed value raises :class:`ttaw.io.InputError`."""
return parse_transform(obj, field=field)
def compose(*transforms: np.ndarray) -> np.ndarray:
"""``T_0 @ T_1 @ ... @ T_n`` in float64 (``compose(T_a_from_b, T_b_from_c) == T_a_from_c``)."""
if not transforms:
return np.eye(4)
out = np.asarray(transforms[0], dtype=np.float64)
for t in transforms[1:]:
out = out @ np.asarray(t, dtype=np.float64)
return out
def invert_rigid(T: np.ndarray) -> np.ndarray:
"""Exact inverse of a rigid transform in float64: ``[R^T, -R^T t]``."""
T = np.asarray(T, dtype=np.float64)
out = np.eye(4)
r = T[:3, :3]
out[:3, :3] = r.T
out[:3, 3] = -(r.T @ T[:3, 3])
return out
def invert_affine_f32(T: np.ndarray) -> np.ndarray:
"""``Eigen::Affine3f::inverse()`` (TransformTraits ``Affine``) in float32: the 3x3 linear part inverted through
its cofactors and the column-0 determinant (Eigen's ``compute_inverse<..., 3>``), translation ``-L^-1 t``,
every operation rounded to float32. Used by the Autoware densification emulation
(``pointcloud_densification.cpp:84``)."""
m = np.asarray(T, dtype=np.float32)
a = m[:3, :3]
f = np.float32
def cofactor(i: int, j: int) -> np.float32: # Eigen cofactor_3x3<i, j>
i1, i2, j1, j2 = (i + 1) % 3, (i + 2) % 3, (j + 1) % 3, (j + 2) % 3
return f(f(a[i1, j1] * a[i2, j2]) - f(a[i1, j2] * a[i2, j1]))
det = f(f(cofactor(0, 0) * a[0, 0]) + f(cofactor(1, 0) * a[1, 0])) + f(cofactor(2, 0) * a[2, 0])
if det == 0 or not np.isfinite(det):
raise ValueError("singular affine transform")
inv_det = f(f(1.0) / det)
inv = np.empty((3, 3), np.float32)
for i in range(3):
for j in range(3):
inv[i, j] = f(cofactor(j, i) * inv_det) # adjugate / det
out = np.eye(4, dtype=np.float32)
out[:3, :3] = inv
t = m[:3, 3]
for i in range(3):
out[i, 3] = -(f(f(inv[i, 0] * t[0]) + f(inv[i, 1] * t[1])) + f(inv[i, 2] * t[2]))
return out
def compose_f32(A: np.ndarray, B: np.ndarray) -> np.ndarray:
"""``A @ B`` of two float32 affine matrices with float32 rounding after every product and sum, summed in index
order (Eigen's lazy product of two ``Affine3f``)."""
a = np.asarray(A, dtype=np.float32)
b = np.asarray(B, dtype=np.float32)
out = np.zeros((4, 4), np.float32)
for k in range(4):
out = (out + a[:, k:k + 1] * b[k:k + 1, :]).astype(np.float32)
return out
def transform_points(points: np.ndarray, T: np.ndarray) -> np.ndarray:
"""``T @ [x, y, z, 1]`` for (N, >=3) points in float64 (extra columns are passed through unchanged)."""
p = np.asarray(points, dtype=np.float64)
T = np.asarray(T, dtype=np.float64)
out = p.copy()
out[:, :3] = p[:, :3] @ T[:3, :3].T + T[:3, 3]
return out
def transform_points_f32(xyz: np.ndarray, T: np.ndarray) -> np.ndarray:
"""Autoware ``generateSweepPoints_kernel`` (``preprocess_kernel.cu:58-90``): the affine is cast to float32 and
each output coordinate is ``((m[r,0]*x + m[r,1]*y) + m[r,2]*z) + m[r,3]`` in float32, left to right (no FMA).
``xyz``: (N, >=3); returns (N, 3) float32."""
p = np.asarray(xyz, dtype=np.float32)
m = np.asarray(T, dtype=np.float32)
x, y, z = p[:, 0], p[:, 1], p[:, 2]
out = np.empty((len(p), 3), np.float32)
for r in range(3):
out[:, r] = ((m[r, 0] * x + m[r, 1] * y) + m[r, 2] * z) + m[r, 3]
return out
# ----------------------------------------------------------------------------------------------- rotations
def rotation_from_rpy(roll: float, pitch: float, yaw: float) -> np.ndarray:
"""tf2 ``setRPY``: ``R = Rz(yaw) @ Ry(pitch) @ Rx(roll)`` (float64)."""
cr, sr, cp, sp, cy, sy = (math.cos(roll), math.sin(roll), math.cos(pitch), math.sin(pitch), math.cos(yaw),
math.sin(yaw))
return np.array([[cy * cp, cy * sp * sr - sy * cr, cy * sp * cr + sy * sr],
[sy * cp, sy * sp * sr + cy * cr, sy * sp * cr - cy * sr],
[-sp, cp * sr, cp * cr]], dtype=np.float64)
def transform_from_xyz_rpy(x: float, y: float, z: float, roll: float = 0.0, pitch: float = 0.0,
yaw: float = 0.0) -> np.ndarray:
"""A (4, 4) float64 transform from a translation and tf2 RPY angles (Autoware's calibration yaml spelling)."""
out = np.eye(4)
out[:3, :3] = rotation_from_rpy(roll, pitch, yaw)
out[:3, 3] = (x, y, z)
return out
def quaternion_wxyz_from_yaw(yaw: float) -> np.ndarray:
"""``autoware_utils::create_quaternion_from_yaw``: rotation about +z, as (w, x, y, z) float64."""
return np.array([math.cos(yaw / 2.0), 0.0, 0.0, math.sin(yaw / 2.0)], dtype=np.float64)
def yaw_from_quaternion_wxyz(q: Sequence[float]) -> float:
"""``tf2::getYaw`` of a (w, x, y, z) quaternion."""
w, x, y, z = (float(v) for v in q)
return math.atan2(2.0 * (w * z + x * y), 1.0 - 2.0 * (y * y + z * z))
def yaw_from_rotation(r: np.ndarray) -> float:
"""Heading of a rotation matrix (rotation of +x about +z, ``atan2(R[1,0], R[0,0])``)."""
r = np.asarray(r, dtype=np.float64)
return math.atan2(r[1, 0], r[0, 0])
def wrap_angle(a: Any) -> Any:
"""Wrap angles to [-pi, pi)."""
return (np.asarray(a, dtype=np.float64) + math.pi) % (2.0 * math.pi) - math.pi
def mmdet_yaw_to_ros(yaw_net: Any) -> np.ndarray:
"""``ros_utils.cpp:55``: ``const float yaw = -box3d.yaw - pi / 2`` (float input, double arithmetic, float
result). Returns float32 radians, not wrapped (as Autoware)."""
y = np.asarray(yaw_net, dtype=np.float32).astype(np.float64)
return (-y - math.pi / 2.0).astype(np.float32)
def ros_yaw_to_mmdet(yaw_ros: Any) -> np.ndarray:
"""Inverse of :func:`mmdet_yaw_to_ros` (float64): ``yaw_net = -yaw_ros - pi/2``."""
return -np.asarray(yaw_ros, dtype=np.float64) - math.pi / 2.0