twanghcmut's picture
download
raw
6.86 kB
"""Pinhole camera: projection/unprojection paired with an explicit resolution.
The h5 ``intrinsic`` is valid at the scene-flow annotation resolution, which is not
generally the mp4 frame resolution. :meth:`Camera.rescaled` is the single
grep-able choke point for resolution changes -- :meth:`Camera.project` and
:meth:`Camera.unproject` never take a resolution argument, so it is structurally
impossible to project at the wrong scale without going through ``rescaled`` first.
"""
from __future__ import annotations
import logging
import warnings
import numpy as np
from fpgm.geometry.transforms import assert_valid_se3, invert_se3, transform_points
from fpgm.types import CameraIntrinsics
logger = logging.getLogger(__name__)
class Camera:
"""A pinhole camera: intrinsics plus a world-to-camera SE3 extrinsic."""
def __init__(self, intrinsics: CameraIntrinsics, world_to_cam: np.ndarray) -> None:
"""Build a camera.
Args:
intrinsics: Pinhole intrinsics, valid at ``intrinsics.width/height``.
world_to_cam: ``(4, 4)`` SE3 matrix mapping world points to camera frame.
Raises:
GeometryError: if ``world_to_cam`` is not a valid SE3 matrix -- this
catches an accidental cam2world/world2cam swap at construction time
rather than letting it manifest as silently-wrong depths later.
"""
world_to_cam = np.asarray(world_to_cam, dtype=np.float64)
assert_valid_se3(world_to_cam)
self.K = intrinsics
self._world_to_cam = world_to_cam
self._cam_to_world = invert_se3(world_to_cam)
@classmethod
def from_pointworld(
cls, intrinsic_3x3: np.ndarray, extrinsic_4x4: np.ndarray, width: int, height: int
) -> "Camera":
"""Build a :class:`Camera` from raw PointWorld-DROID h5 arrays.
Args:
intrinsic_3x3: ``(3, 3)`` K matrix, valid at ``(width, height)``.
extrinsic_4x4: ``(4, 4)`` world-to-camera SE3 matrix (PointWorld stores
``extrinsic = inv(cam2world)``).
width: Annotation-resolution pixel width the intrinsic is valid at.
height: Annotation-resolution pixel height the intrinsic is valid at.
"""
intrinsics = CameraIntrinsics.from_matrix(intrinsic_3x3, width=width, height=height)
return cls(intrinsics, extrinsic_4x4)
def rescaled(self, width: int, height: int) -> "Camera":
"""Return a new :class:`Camera` with intrinsics rescaled to ``(width, height)``.
The extrinsic is unaffected -- only pixel-space quantities change with
resolution. This is the only sanctioned way to change the resolution a
camera operates at.
"""
return Camera(self.K.scaled(width, height), self._world_to_cam)
def world_to_cam(self, points_world: np.ndarray) -> np.ndarray:
"""Transform points ``(..., 3)`` from world frame to camera frame."""
return transform_points(self._world_to_cam, points_world)
def cam_to_world(self, points_cam: np.ndarray) -> np.ndarray:
"""Transform points ``(..., 3)`` from camera frame to world frame."""
return transform_points(self._cam_to_world, points_cam)
def project_cam(self, points_cam: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
"""Project camera-frame points ``(..., 3)`` to pixels, intrinsic only.
Returns:
A tuple ``(uv, depth)`` of shapes ``(..., 2)`` and ``(...,)``. Points
behind the camera (``depth <= 0``) are returned as ordinary data for the
caller to mask -- this method never raises on them, since a track that
temporarily goes behind the camera plane is a normal occurrence, not an
error.
"""
points_cam = np.asarray(points_cam, dtype=np.float64)
z = points_cam[..., 2]
safe_z = np.where(z == 0.0, np.finfo(np.float64).eps, z)
u = self.K.fx * points_cam[..., 0] / safe_z + self.K.cx
v = self.K.fy * points_cam[..., 1] / safe_z + self.K.cy
uv = np.stack([u, v], axis=-1)
return uv, z
def project(self, points_world: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
"""Project world-frame points ``(..., 3)`` to pixels via the extrinsic.
Returns:
A tuple ``(uv, depth)`` of shapes ``(..., 2)`` and ``(...,)``, where
``depth`` is ``Zc`` (the camera-frame Z coordinate). See
:meth:`project_cam` for the behind-camera convention.
"""
return self.project_cam(self.world_to_cam(points_world))
def unproject_to_cam(self, uv: np.ndarray, depth: np.ndarray) -> np.ndarray:
"""Back-project pixels ``(..., 2)`` + depth ``(...,)`` to camera-frame points."""
uv = np.asarray(uv, dtype=np.float64)
depth = np.asarray(depth, dtype=np.float64)
x = (uv[..., 0] - self.K.cx) * depth / self.K.fx
y = (uv[..., 1] - self.K.cy) * depth / self.K.fy
return np.stack([x, y, depth], axis=-1)
def unproject(self, uv: np.ndarray, depth: np.ndarray) -> np.ndarray:
"""Back-project pixels ``(..., 2)`` + depth ``(...,)`` to world-frame points."""
return self.cam_to_world(self.unproject_to_cam(uv, depth))
def sanity_check_resolution(self, rgb_shape: tuple[int, ...]) -> None:
"""Warn loudly if ``rgb_shape`` disagrees with the principal point / claimed size.
A well-formed pinhole intrinsic has its principal point near the image
centre, i.e. ``2*cx ~= width`` and ``2*cy ~= height``. If that disagrees
badly with ``rgb_shape``, the resolution this intrinsic is assumed to be
valid at is likely wrong -- e.g. it was read at mp4 resolution instead of
annotation resolution, or vice versa.
Args:
rgb_shape: ``(H, W, ...)`` shape of an actual image array to check against.
"""
img_h, img_w = rgb_shape[0], rgb_shape[1]
if img_w != self.K.width or img_h != self.K.height:
warnings.warn(
f"Camera resolution ({self.K.width}x{self.K.height}) does not match "
f"image shape ({img_w}x{img_h}); did you forget to call .rescaled()?",
stacklevel=2,
)
implied_w, implied_h = 2.0 * self.K.cx, 2.0 * self.K.cy
rel_err_w = abs(implied_w - self.K.width) / max(self.K.width, 1)
rel_err_h = abs(implied_h - self.K.height) / max(self.K.height, 1)
if rel_err_w > 0.25 or rel_err_h > 0.25:
warnings.warn(
f"Principal point (2*cx={implied_w:.1f}, 2*cy={implied_h:.1f}) disagrees "
f"badly with claimed resolution ({self.K.width}x{self.K.height}); the "
"resolution this intrinsic is assumed valid at may be wrong.",
stacklevel=2,
)

Xet Storage Details

Size:
6.86 kB
·
Xet hash:
e793458e27d9d522b673ef0b8275dbdba451f938157b75d30a45c1cf19fd757f

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.