Refractive Geometry
===================
This page explains the mathematical foundations of refractive ray tracing
for underwater multi-view stereo. The key challenge is that cameras are in
air while targets are underwater, with a flat water surface between them.
Light rays refract at the air-water interface according to Snell's law,
requiring a modified projection model.
Coordinate System
-----------------
AquaMVS inherits the coordinate system convention from AquaCal:
**World Frame**
The origin is at the optical center of the reference camera (whichever
camera was designated as reference during AquaCal calibration). Camera
names are hardware identifiers (e.g., ``e3v82e0``), not sequential labels.
* **+X**: Right (when facing the scene)
* **+Y**: Forward (optical axis of reference camera)
* **+Z**: Down (into the water)
* **Units**: Meters throughout
**Camera Frame**
OpenCV convention for individual cameras:
* **+X**: Right
* **+Y**: Down
* **+Z**: Forward (optical axis)
**Pixel Coordinates**
Image coordinates use ``(u, v)`` where:
* ``u``: Column (horizontal pixel index)
* ``v``: Row (vertical pixel index)
* Origin at top-left corner
**Extrinsics Convention**
Transformation from world to camera frame:
.. math::
\mathbf{p}_{\text{cam}} = \mathbf{R} \mathbf{p}_{\text{world}} + \mathbf{t}
where :math:`\mathbf{R}` is the rotation matrix and :math:`\mathbf{t}`
is the translation vector.
**Typical Geometry**
In a standard AquaMVS setup:
* Cameras are positioned near :math:`Z \approx 0`
* Water surface is at :math:`Z = z_{\text{water}} > 0` (e.g., 0.978 m)
* Underwater targets are at :math:`Z > z_{\text{water}}`
* Interface normal is :math:`\mathbf{n} = [0, 0, -1]` (points from water toward air)
Camera Model
------------
AquaMVS uses a standard pinhole camera model with radial and tangential
distortion. All images are undistorted in a preprocessing step, so the
projection model operates on rectified images.
**Intrinsic Matrix**
The intrinsic matrix :math:`\mathbf{K}` maps 3D camera coordinates to
pixel coordinates:
.. math::
\mathbf{K} = \begin{bmatrix}
f_x & 0 & c_x \\
0 & f_y & c_y \\
0 & 0 & 1
\end{bmatrix}
* :math:`f_x, f_y`: Focal lengths in pixels
* :math:`c_x, c_y`: Principal point (optical center) in pixels
**Standard Projection** (no refraction)
For a 3D point :math:`\mathbf{p}_{\text{cam}} = [X, Y, Z]^T` in camera
frame:
.. math::
\begin{bmatrix} u \\ v \\ 1 \end{bmatrix} =
\frac{1}{Z} \mathbf{K} \begin{bmatrix} X \\ Y \\ Z \end{bmatrix}
This standard model is **not valid** for underwater targets due to
refraction.
Refractive Ray Casting
-----------------------
Ray casting is the inverse operation of projection: given a pixel coordinate,
compute the 3D ray that passes through it. For refractive scenarios, this
process has four steps.
**Step 1: Pinhole Back-Projection**
Cast a ray from the camera optical center through the pixel. In camera
frame, the normalized ray direction is:
.. math::
\mathbf{d}_{\text{cam}} = \mathbf{K}^{-1} \begin{bmatrix} u \\ v \\ 1 \end{bmatrix}
Normalize to unit length: :math:`\mathbf{d}_{\text{cam}} \leftarrow \mathbf{d}_{\text{cam}} / \|\mathbf{d}_{\text{cam}}\|`
**Step 2: Transform to World Frame**
Camera optical center in world frame:
.. math::
\mathbf{C} = -\mathbf{R}^T \mathbf{t}
Ray direction in world frame:
.. math::
\mathbf{d}_{\text{air}} = \mathbf{R}^T \mathbf{d}_{\text{cam}}
**Step 3: Intersect Water Surface**
Find the point where the ray crosses the plane :math:`Z = z_{\text{water}}`.
Parametric ray equation:
.. math::
\mathbf{p}(t) = \mathbf{C} + t \mathbf{d}_{\text{air}}
At the water surface:
.. math::
C_z + t \, d_{z,\text{air}} = z_{\text{water}}
Solving for :math:`t`:
.. math::
t = \frac{z_{\text{water}} - C_z}{d_{z,\text{air}}}
The intersection point is:
.. math::
\mathbf{P} = \mathbf{C} + t \mathbf{d}_{\text{air}}
**Step 4: Apply Snell's Law**
Refract the ray direction at the interface. See the next section for
the vector form of Snell's law.
.. mermaid::
graph TD
A["Camera in Air
(optical center C)"] -->|"incident ray d_air"| B["Water Surface
z = z_water"]
B -->|"refracted ray d_water
(Snell's law)"| C["Underwater Target
(z > z_water)"]
style B fill:#4fc3f7,stroke:#0288d1,stroke-width:2px
Snell's Law: Vector Form
-------------------------
Snell's law relates the incident and refracted ray directions at an interface
between two media with different refractive indices.
**Scalar Form**
The traditional scalar formulation:
.. math::
n_1 \sin \theta_i = n_2 \sin \theta_t
where:
* :math:`n_1 = n_{\text{air}} \approx 1.0`: Refractive index of air
* :math:`n_2 = n_{\text{water}} \approx 1.333`: Refractive index of water
* :math:`\theta_i`: Angle of incidence (ray to normal)
* :math:`\theta_t`: Angle of refraction (transmitted ray to normal)
**Vector Form Derivation**
For computational implementation, we need a vector formulation. Given:
* :math:`\mathbf{d}_i`: Incident ray direction (unit vector)
* :math:`\mathbf{n}`: Interface normal (unit vector, pointing from medium 2 to medium 1)
* :math:`\eta = n_1 / n_2`: Ratio of refractive indices
The angle of incidence:
.. math::
\cos \theta_i = -\mathbf{d}_i \cdot \mathbf{n}
(Note: The negative sign accounts for :math:`\mathbf{d}_i` pointing toward
the interface while :math:`\mathbf{n}` points away from it.)
From Snell's law:
.. math::
\sin^2 \theta_t = \eta^2 (1 - \cos^2 \theta_i) = \eta^2 \sin^2 \theta_i
The refracted ray direction is:
.. math::
\mathbf{d}_t = \eta \mathbf{d}_i + \left( \eta \cos \theta_i - \sqrt{1 - \sin^2 \theta_t} \right) \mathbf{n}
**Total Internal Reflection**
If :math:`\sin^2 \theta_t > 1`, total internal reflection occurs and there
is no refracted ray. This happens when:
.. math::
\eta^2 (1 - \cos^2 \theta_i) > 1
In practice, this is rare for air-to-water transitions at typical viewing
angles but must be checked. AquaMVS returns a validity mask to flag these
cases.
**Implementation in AquaMVS**
The vector form is implemented in :py:class:`aquamvs.projection.RefractiveProjectionModel`.
For air-to-water refraction:
* :math:`\mathbf{d}_i = \mathbf{d}_{\text{air}}` (incident ray from Step 3)
* :math:`\mathbf{n} = [0, 0, -1]` (points from water toward air)
* :math:`\eta = n_{\text{air}} / n_{\text{water}} \approx 0.75`
* :math:`\mathbf{d}_t = \mathbf{d}_{\text{water}}` (refracted ray into water)
The refracted ray originates at the water surface intersection point
:math:`\mathbf{P}` and points into the water.
Depth Parameterization
-----------------------
Depth maps in AquaMVS use **ray depth** (distance along the refracted ray)
rather than world Z-coordinate. This parameterization is natural for plane
sweep stereo and simplifies depth estimation.
**Ray Depth Definition**
For a pixel at :math:`(u, v)`, ray casting returns:
* :math:`\mathbf{O}`: Ray origin (water surface intersection point)
* :math:`\mathbf{d}`: Ray direction (unit vector pointing into water)
A 3D point at ray depth :math:`d` is:
.. math::
\mathbf{p}(d) = \mathbf{O} + d \, \mathbf{d}
**World Z Conversion**
To convert ray depth to world Z-coordinate:
.. math::
Z = O_z + d \, d_z
where :math:`O_z` is the Z-component of the ray origin and :math:`d_z`
is the Z-component of the ray direction.
**Why Ray Depth?**
Ray depth is preferred over world Z for several reasons:
1. **Plane Sweep**: Depth hypotheses naturally correspond to distances
along the ray. Sweeping in world Z would require non-uniform sampling.
2. **Depth Priors**: Sparse triangulation produces 3D points. Projecting
onto the ray gives the depth directly without Z-to-depth conversion.
3. **Numerical Stability**: Ray depth is well-conditioned even for
near-vertical rays, while Z-based parameterization can be ill-conditioned.
**Depth Range Specification**
Configuration files specify depth ranges in ray depth units (meters):
.. code-block:: yaml
reconstruction:
depth_min: 0.05 # meters along ray
depth_max: 2.0 # meters along ray
These are independent of the water surface Z-coordinate or ray direction.
Connection to Code
------------------
The mathematical concepts described here are implemented in:
* :py:class:`aquamvs.projection.RefractiveProjectionModel`: Ray casting
(``cast_ray``) and refractive projection (``project``).
* :doc:`/api/calibration`: Calibration data structures providing camera
parameters (intrinsics, extrinsics, water surface position, refractive indices).
For the underlying NumPy reference implementation (used during AquaCal
calibration), see ``aquacal.core.refractive_geometry`` in the AquaCal library.
Next Steps
----------
Now that we understand how rays are cast through the refractive interface,
we can use them for depth estimation. The next section covers :doc:`dense_stereo`,
which evaluates photometric similarity at discrete depth hypotheses to build
dense depth maps.