Transforming Points Using Camera MatrixΒΆ

This tutorial shows how to transform points from camera space to world space using the cameraβs transformation matrix. This is useful when you need to:
Place objects relative to the cameraβs view
Visualize the camera frustum with sample points
Project points between coordinate systems
This tutorial will teach you how to:
Sample random points within a camera frustum
Transform points using homogeneous coordinates
Update scene objects in response to camera movement
Step 1: Define Frustum Sampling FunctionΒΆ
First, we create a helper function to sample random points within the camera frustum. The frustum is the visible volume between the near and far clipping planes.
near plane far plane
β β
βββββββΌββββββ βββββββΌββββββ
β Β· β Β· β β Β· β Β· β
β Β· β Β· β β Β· β Β· β
βββββββΌββββββ βββββββΌββββββ
β β
camera ββββββββββββββββββΆ Z (backward)
Points are sampled in view space where X is right, Y is up, and Z points backward.
from typing import Tuple
import numpy as np
def sample_camera_frustum_batch(
fov: float,
width: float,
height: float,
near: float,
far: float,
num_samples=1,
**_,
) -> Tuple[np.ndarray]:
"""Sample Camera Frustum
For rectangular cameras, sample a point between near and far plane, relative to camera transform
Output in view space: X right, Y up, Z backward
(Assumed to have the same intrinsics for each sample)
:param fov: vertical field-of-view, in degrees
:param width: px
:param height: px
:param near: in meters (world units)
:param far: in meters (world units)
"""
aspect = width / height
fov_rad = fov * np.pi / 180
foh_rad = np.arctan(np.tan(fov_rad / 2) * aspect) * 2
d = np.random.uniform(near, far, size=num_samples) # distance in camera coordinates
y_range = d * np.tan(fov_rad / 2)
y = np.random.uniform(-y_range, y_range, size=num_samples)
x_range = d * np.tan(foh_rad / 2)
x = np.random.uniform(-x_range, x_range, size=num_samples)
return np.stack([x, y, -d]).T
Step 2: Define Point Transformation FunctionΒΆ
To transform points from camera space to world space, we use the cameraβs 4x4 transformation matrix. We convert 3D points to homogeneous coordinates (adding a fourth component of 1), multiply by the matrix, and extract the 3D result.
def transform_points(pts, matrix):
"""Transform 3D points using a 4x4 transformation matrix.
Args:
pts: Array of shape (N, 3) containing 3D points
matrix: 4x4 transformation matrix
Returns:
Transformed points as array of shape (N, 3)
"""
# Add homogeneous coordinate (w=1) to each point
pts_homogeneous = np.hstack((pts, np.ones((len(pts), 1))))
# Apply the transformation matrix
transformed_pts = pts_homogeneous @ matrix
# Extract 3D coordinates (drop the w component)
return transformed_pts[:, :3]
Step 3: Set Up the Scene and Camera Movement HandlerΒΆ
We create a scene with a movable CameraView and register a handler to track
when the camera is moved. When the camera matrix changes, weβll update the
positions of the sample points.
import asyncio
from vuer import Vuer, VuerSession
from vuer.events import ClientEvent
from vuer.schemas import DefaultScene, CameraView, Sphere, OrbitControls
# Sample 100 random points within the camera frustum
ball_pts = sample_camera_frustum_batch(
fov=50,
width=320,
height=240,
near=0.45,
far=0.5,
num_samples=100,
)
app = Vuer()
# Initial camera transformation matrix (4x4, column-major format)
# fmt: off
matrix = np.array([
-0.9403771820302098, -0.33677144289058686, 0.04770482963301034, 0,
0.14212405695663877, -0.26162828559882034, 0.9546472608292598, 0,
-0.30901700268934784, 0.9045085048953463, 0.2938925936815643, 0,
-0.47444114213044175, 1.2453493553603068, 0.5411873913841395, 1,
]).reshape(4, 4)
# fmt: on
@app.add_handler("CAMERA_MOVE")
async def track_camera_movement(event: ClientEvent, sess: VuerSession):
"""Update the global matrix when the camera is moved."""
global matrix
# Only respond to the "ego" camera
if event.key != "ego":
return
if event.value["matrix"] is None:
return
new_matrix = np.array(event.value["matrix"]).reshape(4, 4)
if not np.allclose(new_matrix, matrix):
print("Camera matrix changed")
matrix = new_matrix
@app.spawn(start=True)
async def main(proxy):
# Set up the scene with a movable camera
proxy.set @ DefaultScene(
rawChildren=[
CameraView(
fov=50,
width=320,
height=240,
key="ego",
matrix=matrix.flatten().tolist(),
stream="ondemand",
fps=30,
near=0.4,
far=1.8,
showFrustum=True,
downsample=1,
distanceToCamera=2,
),
],
grid=False,
bgChildren=[
OrbitControls(key="OrbitControls")
],
)
last_matrix_id = None
while True:
# Only update when the matrix changes
if last_matrix_id and id(matrix) == last_matrix_id:
await asyncio.sleep(0.016)
continue
last_matrix_id = id(matrix)
# Transform sample points from camera space to world space
world_pts = transform_points(ball_pts, matrix)
print("Updating ball positions")
# Create spheres at the transformed positions
proxy.upsert @ [
Sphere(
args=[0.01],
position=world_pts[i].tolist(),
material=dict(color="red"),
materialType="phong",
key=f"ball-{i}",
)
for i in range(len(ball_pts))
]
await asyncio.sleep(0.01)