Source code for aquacal.core.interface_model
"""Refractive interface (water surface) model."""
import numpy as np
from aquacal.config.schema import Vec3
[docs]
class Interface:
"""
Planar refractive interface (air-water boundary).
The interface is a horizontal plane at a fixed Z-coordinate in the world frame.
Attributes:
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)
"""
def __init__(
self,
normal: Vec3,
camera_distances: dict[str, float],
n_air: float = 1.0,
n_water: float = 1.333,
):
"""
Initialize interface.
Args:
normal: Unit normal vector pointing from water to air (typically [0,0,-1])
camera_distances: Per-camera Z-coordinate of the water surface in world frame.
Typically the same value (water_z) for all cameras.
n_air: Refractive index of air
n_water: Refractive index of water
"""
self.normal = normal / np.linalg.norm(normal) # Ensure unit vector
self.camera_distances = camera_distances
self.n_air = n_air
self.n_water = n_water
[docs]
def get_water_z(self, camera_name: str) -> float:
"""
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.
Args:
camera_name: Name of camera
Returns:
Water surface Z-coordinate for the specified camera
Raises:
KeyError: If camera_name not in camera_distances
"""
return self.camera_distances[camera_name]
[docs]
def get_interface_point(self, camera_center: Vec3, camera_name: str) -> Vec3:
"""
Get the point on the interface directly below the camera center.
Assumes cameras look straight down (+Z direction in Z-down world frame).
Args:
camera_center: Camera center in world coordinates [x, y, z]
camera_name: Name of camera (for distance lookup)
Returns:
3D point on interface plane [x, y, z_interface]
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.
"""
z_interface = self.camera_distances[camera_name]
return np.array(
[camera_center[0], camera_center[1], z_interface], dtype=np.float64
)
@property
def n_ratio_air_to_water(self) -> float:
"""Ratio n_air / n_water for Snell's law (air to water)."""
return self.n_air / self.n_water
@property
def n_ratio_water_to_air(self) -> float:
"""Ratio n_water / n_air for Snell's law (water to air)."""
return self.n_water / self.n_air
[docs]
def ray_plane_intersection(
ray_origin: Vec3, ray_direction: Vec3, plane_point: Vec3, plane_normal: Vec3
) -> tuple[Vec3, float] | tuple[None, None]:
"""
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)
Args:
ray_origin: Origin of ray, shape (3,)
ray_direction: Direction of ray (need not be unit), shape (3,)
plane_point: Any point on the plane, shape (3,)
plane_normal: 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).
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
"""
# Compute denominator: direction · normal
denom = np.dot(ray_direction, plane_normal)
# Check if ray is parallel to plane
if abs(denom) < 1e-10:
return None, None
# Compute t: ((plane_point - origin) · normal) / (direction · normal)
t = np.dot(plane_point - ray_origin, plane_normal) / denom
# Compute intersection point
intersection = ray_origin + t * ray_direction
return intersection, t