gem-x-motion-capture / gem /utils /cam_utils.py
cs686's picture
Deploy GEM-X ZeroGPU motion capture
49d36c0 verified
Raw
History Blame Contribute Delete
5.3 kB
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""
Camera utility functions.
"""
import math
import torch
from gem.utils.geo_transform import convert_bbx_xys_to_lurb, cvt_to_bi01_p2d
def perspective_projection(points, K):
"""Standard pinhole camera projection.
Args:
points: (*, J, 3)
K: (*, 3, 3)
Returns:
p2d: (*, J, 2)
"""
projected = points / points[..., -1:].clamp(min=1e-6)
projected = torch.einsum("...ij,...kj->...ki", K, projected.float())
return projected[..., :2]
def compute_transl_full_cam(pred_cam, bbx_xys, K_fullimg):
"""Convert weak-perspective camera parameters to full-perspective translation.
Args:
pred_cam: (*, 3) [s, tx, ty]
bbx_xys: (*, 3) [cx, cy, size]
K_fullimg: (*, 3, 3)
Returns:
cam_t: (*, 3)
"""
s, tx, ty = pred_cam[..., 0], pred_cam[..., 1], pred_cam[..., 2]
focal_length = K_fullimg[..., 0, 0]
icx = K_fullimg[..., 0, 2]
icy = K_fullimg[..., 1, 2]
sb = s * bbx_xys[..., 2]
cx = 2 * (bbx_xys[..., 0] - icx) / (sb + 1e-9)
cy = 2 * (bbx_xys[..., 1] - icy) / (sb + 1e-9)
tz = 2 * focal_length / (sb + 1e-9)
return torch.stack([tx + cx, ty + cy, tz], dim=-1)
def get_a_pred_cam(transl, bbx_xys, K_fullimg):
"""Inverse of compute_transl_full_cam: full translation → weak-perspective params.
Args:
transl: (*, 3)
bbx_xys: (*, 3)
K_fullimg: (*, 3, 3) one extra leading dim compared to transl/bbx_xys
Returns:
pred_cam: (*, 3) [s, tx, ty]
"""
f = K_fullimg[..., 0, 0]
cx = K_fullimg[..., 0, 2]
cy = K_fullimg[..., 1, 2]
gt_s = 2 * f / (transl[..., 2] * bbx_xys[..., 2])
gt_x = transl[..., 0] - transl[..., 2] / f * (bbx_xys[..., 0] - cx)
gt_y = transl[..., 1] - transl[..., 2] / f * (bbx_xys[..., 1] - cy)
return torch.stack([gt_s, gt_x, gt_y], dim=-1)
def compute_bbox_info_bedlam(bbx_xys, K_fullimg):
"""Compute BEDLAM-style bounding box info (normalised by focal length).
Args:
bbx_xys: (*, 3) [cx, cy, size]
K_fullimg: (*, 3, 3)
Returns:
bbox_info: (*, 3)
"""
fl = K_fullimg[..., 0, 0].unsqueeze(-1)
icx = K_fullimg[..., 0, 2]
icy = K_fullimg[..., 1, 2]
cx, cy, b = bbx_xys[..., 0], bbx_xys[..., 1], bbx_xys[..., 2]
bbox_info = torch.stack([cx - icx, cy - icy, b], dim=-1)
return bbox_info / fl
def safely_render_x3d_K(x3d, K_fullimg, thr=0.3):
"""Project 3D vertices to 2D, clamping near-zero depths for safety.
Args:
x3d: (*, V, 3)
K_fullimg: (*, 3, 3)
thr: minimum depth threshold
Returns:
i_x2d: (*, V, 2)
"""
x3d = x3d.clone()
x3d[..., 2] = x3d[..., 2].clamp(min=thr)
return perspective_projection(x3d, K_fullimg)
def estimate_K(width, height):
"""Estimate default pinhole intrinsics from image dimensions.
Uses max(w, h) as focal length — a reasonable approximation when
the true focal length is unknown.
Args:
width, height: image dimensions (int or float)
Returns:
K: (3, 3) camera intrinsics tensor
"""
focal = float(max(width, height))
K = torch.eye(3)
K[0, 0] = focal
K[1, 1] = focal
K[0, 2] = float(width) / 2.0
K[1, 2] = float(height) / 2.0
return K
def resize_K(K, scale):
"""Scale camera intrinsics for a resized image.
Args:
K: (*, 3, 3) intrinsics tensor
scale: scalar scale factor
Returns:
K_new: (*, 3, 3) scaled intrinsics
"""
K_new = K.clone().float()
K_new[..., :2, :] = K_new[..., :2, :] * scale
return K_new
def create_camera_sensor(width, height, fov_deg=60):
"""Create camera intrinsics from image dimensions and field-of-view angle.
Args:
width, height: image dimensions
fov_deg: horizontal field of view in degrees
Returns:
(width, height, K) where K is (3, 3)
"""
focal = float(width) / (2.0 * math.tan(math.radians(fov_deg / 2.0)))
K = torch.eye(3)
K[0, 0] = focal
K[1, 1] = focal
K[0, 2] = float(width) / 2.0
K[1, 2] = float(height) / 2.0
return width, height, K
def get_bbx_xys_from_xyxy(xyxy, base_zoom=1.0, minimal_size=10):
"""Convert bounding box from [x1, y1, x2, y2] to [cx, cy, size].
Args:
xyxy: (..., 4) bounding box in xyxy format
base_zoom: zoom factor applied to the box size
minimal_size: minimum allowed box size
Returns:
xys: (..., 3) [cx, cy, size]
"""
x1, y1, x2, y2 = xyxy[..., 0], xyxy[..., 1], xyxy[..., 2], xyxy[..., 3]
cx = (x1 + x2) / 2
cy = (y1 + y2) / 2
size = torch.max(x2 - x1, y2 - y1) * base_zoom
size = size.clamp(min=minimal_size)
return torch.stack([cx, cy, size], dim=-1)
def project_to_bi01(points, bbx_xys, K_fullimg):
"""Project 3D points to bounding-box-normalised [0,1] 2D coordinates.
Args:
points: (*, J, 3)
bbx_xys: (*, 3)
K_fullimg: (*, 3, 3)
Returns:
p2d_bi01: (*, J, 2)
"""
p2d = perspective_projection(points, K_fullimg)
bbx_lurb = convert_bbx_xys_to_lurb(bbx_xys)
return cvt_to_bi01_p2d(p2d, bbx_lurb)