Core Geometry¶
Note
For theory behind these functions, see Refractive Geometry and Coordinate Conventions.
The core package provides refractive ray tracing, camera models, and geometric primitives.
Refractive Projection¶
- aquacal.core.refractive_geometry.refractive_project(camera, interface, point_3d, max_iterations=10, tolerance=1e-09)[source]¶
Project 3D underwater point to 2D pixel through refractive interface.
Auto-selects the fastest algorithm: - Flat interface (normal ≈ [0,0,-1]): Newton-Raphson (2-4 iterations, ~50x faster) - General interface: Brent-search fallback
This is the forward projection used for computing reprojection error.
- Parameters:
camera (Camera) – Camera object
interface (Interface) – Interface object
point_3d (ndarray[tuple[int, ...], dtype[float64]]) – 3D point in water (world coordinates, Z > interface_z)
max_iterations (int) – Maximum Newton iterations for flat interface (default 10)
tolerance (float) – Convergence tolerance for flat interface (default 1e-9 meters)
- Returns:
2D pixel coordinates, or None if projection fails.
- Return type:
Example
>>> import numpy as np >>> from aquacal.core.camera import Camera >>> from aquacal.core.interface_model import Interface >>> # Assuming camera and interface are set up >>> point_3d = np.array([0.5, 0.3, 0.8]) # Underwater point >>> pixel = refractive_project(camera, interface, point_3d) >>> if pixel is not None: >>> print(f"Projected to pixel: {pixel}")
Note
For a detailed explanation of the refractive geometry model, see the Refractive Geometry guide.
Notes
Returns None if: point above interface, TIR, optimization fails, or refracted ray doesn’t reach camera
- aquacal.core.refractive_geometry.refractive_project_batch(camera, interface, points_3d, max_iterations=10, tolerance=1e-09)[source]¶
Project multiple 3D underwater points to 2D pixels (vectorized).
Currently only supports flat interfaces (normal ≈ [0,0,-1]). Uses vectorized Newton-Raphson for fast batch projection.
- Parameters:
- Returns:
Array of shape (N, 2) with pixel coordinates. Invalid projections have NaN values.
- Raises:
ValueError – If interface normal is not horizontal [0, 0, -1]
- Return type:
Example
>>> import numpy as np >>> points = np.array([[0.5, 0.3, 0.8], [0.2, 0.4, 0.9], [0.1, 0.2, 1.0]]) >>> pixels = refractive_project_batch(camera, interface, points) >>> valid_pixels = pixels[~np.isnan(pixels).any(axis=1)]
Note
For a detailed explanation of the refractive geometry model, see the Refractive Geometry guide.
Notes
Batch Brent-search is not implemented. For non-flat interfaces, use refractive_project() in a loop instead.
Snell’s Law and Ray Tracing¶
- aquacal.core.refractive_geometry.snells_law_3d(incident_direction, surface_normal, n_ratio)[source]¶
Apply Snell’s law in 3D to compute refracted ray direction.
- Parameters:
incident_direction (ndarray[tuple[int, ...], dtype[float64]]) – Unit vector of incoming ray (toward interface)
surface_normal (ndarray[tuple[int, ...], dtype[float64]]) – Unit normal of interface (always pass interface.normal, which points from water toward air [0,0,-1])
n_ratio (float) – Ratio n1/n2 where ray goes from medium 1 to medium 2
- Returns:
Unit vector of refracted ray direction, or None if total internal reflection.
- Return type:
Example
>>> import numpy as np >>> incident = np.array([0.0, 0.1, -1.0]) # Ray going down into water >>> normal = np.array([0.0, 0.0, -1.0]) # Points up from water to air >>> refracted = snells_law_3d(incident, normal, n_ratio=0.75) # Air to water >>> print(f"Refracted: {refracted}")
Note
For a detailed explanation of the refractive geometry model, see the Refractive Geometry guide.
Notes
Function handles normal orientation internally based on ray direction
For air-to-water: n_ratio = n_air / n_water ~= 0.75
For water-to-air: n_ratio = n_water / n_air ~= 1.33
TIR only possible when going from denser to less dense medium
- aquacal.core.refractive_geometry.trace_ray_air_to_water(camera, interface, pixel)[source]¶
Trace ray from camera through air-water interface.
- Parameters:
- Returns:
intersection_point: where ray hits interface (world coords)
refracted_direction: unit direction of ray in water (points +Z, into water)
Returns (None, None) if ray doesn’t hit interface or TIR occurs.
- Return type:
Tuple of (intersection_point, refracted_direction)
- aquacal.core.refractive_geometry.refractive_back_project(camera, interface, pixel)[source]¶
Back-project pixel to ray in water.
This is a convenience wrapper around trace_ray_air_to_water, providing an API consistent with Camera.pixel_to_ray_world.
- Parameters:
- Returns:
ray_origin: point on interface where ray enters water
ray_direction: unit direction of ray in water
Returns (None, None) if back-projection fails.
- Return type:
Tuple of (ray_origin, ray_direction)
Camera Models¶
- class aquacal.core.camera.Camera(name, intrinsics, extrinsics)[source]¶
Bases:
objectCamera model combining intrinsics and extrinsics.
Handles standard pinhole projection with distortion, but NOT refraction. For refractive projection, use refractive_geometry module.
- Parameters:
name (str)
intrinsics (CameraIntrinsics)
extrinsics (CameraExtrinsics)
- name¶
Camera identifier string
- intrinsics¶
CameraIntrinsics dataclass
- extrinsics¶
CameraExtrinsics dataclass
Example
>>> from aquacal.core.camera import Camera >>> from aquacal.config.schema import CameraIntrinsics, CameraExtrinsics >>> import numpy as np >>> # Create camera with intrinsics and extrinsics >>> camera = Camera("cam1", intrinsics, extrinsics) >>> point_3d = np.array([1.0, 0.5, 2.0]) >>> pixel = camera.project(point_3d)
Note
For coordinate system conventions, see the Coordinate Conventions guide.
- property C: ndarray[tuple[int, ...], dtype[float64]]¶
Camera center in world coordinates. Delegates to extrinsics.C.
- property P: ndarray[tuple[int, ...], dtype[float64]]¶
3x4 projection matrix (without distortion).
P = K @ [R | t]
Note: This is the ideal pinhole projection. For projection with distortion, use the project() method.
- world_to_camera(point_world)[source]¶
Transform point from world to camera coordinates.
- Parameters:
point_world (ndarray[tuple[int, ...], dtype[float64]]) – 3D point in world frame, shape (3,)
- Returns:
3D point in camera frame, shape (3,)
- Return type:
Formula: p_cam = R @ p_world + t
- project(point_world, apply_distortion=True)[source]¶
Project 3D world point to 2D pixel coordinates.
- Parameters:
- Returns:
2D pixel coordinates shape (2,), or None if point is behind camera.
- Return type:
Notes
Returns None if point’s Z coordinate in camera frame is ≤ 0
Uses cv2.projectPoints when apply_distortion=True
Uses direct K @ (p_cam / p_cam[2]) when apply_distortion=False
- pixel_to_ray(pixel, undistort=True)[source]¶
Back-project pixel to unit ray in camera frame.
- Parameters:
- Returns:
Unit direction vector in camera frame (Z forward), shape (3,)
- Return type:
Notes
Principal point maps to ray [0, 0, 1]
Uses cv2.undistortPoints when undistort=True
Board Geometry¶
ChArUco board geometry and utilities.
- class aquacal.core.board.BoardGeometry(config)[source]
Bases:
objectChArUco board 3D geometry.
The board frame has origin at the top-left corner (when viewed from front), with X pointing right, Y pointing down, and Z pointing into the board (away from viewer). This matches OpenCV 4.6+ CharucoBoard convention.
- Parameters:
config (BoardConfig)
- config
Board configuration
- corner_positions
Dict mapping corner_id to 3D position in board frame
- num_corners
Total number of interior corners
- property corner_positions: dict[int, ndarray[tuple[int, ...], dtype[float64]]]
Get 3D positions of all corners in board frame.
- Returns:
Dict mapping corner_id (int) to position (3,) in meters
Example
>>> board = BoardGeometry(config) >>> pos = board.corner_positions[0] >>> pos.shape (3,)
- property num_corners: int
Get total number of interior corners.
- Returns:
Number of corners = (squares_x - 1) * (squares_y - 1)
- get_opencv_board()[source]
Get OpenCV CharucoBoard object for detection.
- Returns:
OpenCV CharucoBoard instance
- Return type:
CharucoBoard
- transform_corners(rvec, tvec)[source]
Transform all corners from board frame to world frame.
- Parameters:
- Returns:
Dict mapping corner_id to 3D position in world frame
- Return type:
Example
>>> board = BoardGeometry(config) >>> # Identity transform >>> world_pts = board.transform_corners(np.zeros(3), np.zeros(3)) >>> np.allclose(world_pts[0], board.corner_positions[0]) True
- get_corner_array(corner_ids)[source]
Get 3D positions for specific corners as array.
- Parameters:
corner_ids (ndarray[tuple[int, ...], dtype[int32]]) – Array of corner IDs to retrieve
- Returns:
Array of shape (N, 3) with 3D positions in board frame
- Return type:
Example
>>> board = BoardGeometry(config) >>> pts = board.get_corner_array(np.array([0, 1, 2])) >>> pts.shape (3, 3)
Interface Model¶
Refractive interface (water surface) model.
- class aquacal.core.interface_model.Interface(normal, camera_distances, n_air=1.0, n_water=1.333)[source]¶
Bases:
objectPlanar refractive interface (air-water boundary).
The interface is a horizontal plane at a fixed Z-coordinate in the world frame.
- Parameters:
- normal¶
Unit normal vector pointing from water toward air [0, 0, -1]
- camera_distances¶
Per-camera Z-coordinate of the water surface in world frame. After optimization this is the same value (water_z) for all cameras. The physical camera-to-water gap is computed internally by projection functions as
water_z - C_z.
- n_air¶
Refractive index of air (default 1.0)
- n_water¶
Refractive index of water (default 1.333)
- get_water_z(camera_name)[source]¶
Get the water surface Z-coordinate for a specific camera.
This is the Z-coordinate of the interface plane in world frame. The physical camera-to-water gap is
z_interface - C_z, computed internally by the projection functions.
- get_interface_point(camera_center, camera_name)[source]¶
Get the point on the interface directly below the camera center.
Assumes cameras look straight down (+Z direction in Z-down world frame).
- Parameters:
- Returns:
3D point on interface plane [x, y, z_interface]
- Return type:
Note
Only uses camera_center[0] and camera_center[1] (XY position). The Z-coordinate is determined by the camera’s interface distance, not by camera_center[2]. This is intentional — the interface is at a fixed world Z position.
- aquacal.core.interface_model.ray_plane_intersection(ray_origin, ray_direction, plane_point, plane_normal)[source]¶
Compute intersection of ray with plane.
Uses the parametric ray equation: P = origin + t * direction And plane equation: (P - plane_point) · plane_normal = 0
Solving: t = ((plane_point - origin) · normal) / (direction · normal)
- Parameters:
ray_origin (ndarray[tuple[int, ...], dtype[float64]]) – Origin of ray, shape (3,)
ray_direction (ndarray[tuple[int, ...], dtype[float64]]) – Direction of ray (need not be unit), shape (3,)
plane_point (ndarray[tuple[int, ...], dtype[float64]]) – Any point on the plane, shape (3,)
plane_normal (ndarray[tuple[int, ...], dtype[float64]]) – Normal vector of plane (need not be unit), shape (3,)
- Returns:
Tuple of (intersection_point, t) where intersection = origin + t * direction. Returns (None, None) if ray is parallel to plane (direction · normal ≈ 0).
- Return type:
tuple[ndarray[tuple[int, …], dtype[float64]], float] | tuple[None, None]
Notes
Returns intersection for ANY t value, including negative (behind ray origin)
Caller should check t > 0 if only forward intersections are desired
Uses tolerance of 1e-10 for parallel check