PanorAi canonical spherical geometry¶
This document is the normative geometry contract introduced in PanorAi 3.1.
The public objects and functions in panorai.geometry remain compatible
throughout the 3.x series. The packaged machine-readable summary is
panorai/geometry/geometry-v1.yaml.
Coordinate frame and units¶
Cartesian axes are
+Xright,+Yup, and+Zforward.Image origin is the top-left. Array indices and public image coordinates address pixel centres:
(x, y) = (0, 0)is the centre of the first pixel.Shapes are written
(H, W); NumPy image data isHWorHWCand Torch image data isHW,CHW, orNCHW.Angles stored by a specification are degrees. Equations on this page use radians unless they carry a degree suffix.
Cartesian vector magnitude and canonical depth are radial range in the input distance unit. No API silently interprets radial range as camera z-depth.
Equirectangular panorama (ERP)¶
For width W and height H, a pixel coordinate (x, y) maps to
lon = 2*pi*(x + 0.5)/W - pi
lat = pi/2 - pi*(y + 0.5)/H
ray = (sin(lon)*cos(lat), sin(lat), cos(lon)*cos(lat))
Longitude is normalized to [-pi, pi) and the horizontal axis is periodic.
Latitude decreases north-to-south. The four equatorial cardinals are:
periodic seam periodic seam
-Z -X +Z +X -Z
x=-0.5 W/4-0.5 W/2-0.5 3W/4-0.5 W-0.5
The north and south poles lie at y=-0.5 and y=H-0.5; they are image
boundaries, not pixel centres. Longitude is mathematically undefined at an
exact pole. rays_to_erp_pixels() returns the deterministic representative
x=W/2-0.5 for the exact vectors (0,+1,0) and (0,-1,0). It returns
horizontal coordinates in [0,W) and represents the -Z seam at x=W-0.5.
Any horizontally congruent coordinate is accepted by erp_pixels_to_rays().
Near a pole, where the horizontal components are non-zero, longitude remains
the value given by atan2(X,Z).
Zero-length or non-finite Cartesian inputs have valid=False and NaN pixel
coordinates. Their reported range is the computed norm, so the three members
of ERPPointProjection remain semantically distinct.
Rectangular gnomonic views¶
GnomonicSpec stores the centre latitude and longitude, independent
horizontal and vertical fields of view, roll, and output shape.
Specification angles must be finite real numbers, not booleans or numeric
strings. Latitude is restricted to [-90, 90] degrees. Longitude and roll are
periodic and are stored in the canonical interval [-180, 180); periodically
equivalent inputs therefore create equal immutable specifications. Each field
of view is strictly between 0 and 180 degrees. Output dimensions are
exactly two positive integers and are never truncated from floating values.
Define the unit viewing basis
forward = (sin(lon)*cos(lat), sin(lat), cos(lon)*cos(lat))
right = (cos(lon), 0, -sin(lon))
up = (-sin(lon)*sin(lat), cos(lat), -cos(lon)*sin(lat))
and tangent-plane limits
x_limit = tan(hfov/2)
y_limit = tan(vfov/2)
hfov and vfov are full boundary-to-boundary angular extents through the
view centre. Output pixel centres subdivide the tangent-plane rectangle; the
outermost pixel centres are half a pixel inside its boundaries:
x_j = (2*(j+0.5)/W - 1) * x_limit
y_i = (2*(i+0.5)/H - 1) * y_limit # positive towards image bottom
For roll=0, a ray is proportional to forward + x_j*right - y_i*up.
Positive roll is a clockwise camera rotation for an observer at the camera
looking along forward. Operationally, with roll=+90 deg, the top-centre
output sample moves toward +right, and the right-centre sample moves toward
-up:
plane_x = cos(roll)*x_j - sin(roll)*y_i
plane_y = sin(roll)*x_j + cos(roll)*y_i
ray = normalize(forward + plane_x*right - plane_y*up)
Back-projection includes the closed rectangular boundary (<= at each
tangent-plane limit) and requires a positive forward denominator. Samples
outside that region have support_mask=False.
Virtual-camera pixels, rays, and matrices¶
gnomonic_pixels_to_rays() accepts arrays whose last dimension is (x, y)
and returns unit rays in the panorama frame plus an explicit validity mask.
The closed raster footprint is [-0.5, W-0.5] x [-0.5, H-0.5].
rays_to_gnomonic_pixels() is its inverse for finite, non-zero rays that are
in front of the virtual camera and inside that footprint; it also reports the
original vector ranges. Invalid output coordinates are NaN.
The equivalent pinhole intrinsics are
fx = W / (2*tan(hfov/2)) cx = (W-1)/2
fy = H / (2*tan(vfov/2)) cy = (H-1)/2
K = [[fx, 0, cx], [0, fy, cy], [0, 0, 1]]
R_panorama_from_face has the face-camera right, raster-down, and forward
directions as its columns. Thus
ray_panorama = normalize(R_panorama_from_face @ [u, v, 1]), where
u=(x-cx)/fx and v=(y-cy)/fy. Because the image raster uses y-down while
the panorama frame uses +Y up, this direction transform is orthogonal with
determinant -1; it is not itself an SO(3) pose. The relative transform
between two face cameras is a proper rotation because the two reflections
cancel. Rig exporters must use those relative rotations and retain the
reference-face transform when results are converted back to panorama rays.
gnomonic_pixel_map() returns the exact face-to-ERP coordinate map used by
canonical sampling. Projection calls expose the same map through the optional
ProjectionResult.source_pixels_xy. GnomonicFaceGeometry groups the
resolved specification, K, direction transform, support mask, and optional
source map without inferring data validity from geometric support.
Cubemap¶
The immutable face order is front, right, back, left, up, down.
Each face basis below is (forward, right, up) in the canonical Cartesian
frame. For normalized raster coordinates u rightward and v downward, a
face ray is normalize(forward + u*right - v*up).
face |
forward |
right |
up |
|---|---|---|---|
front |
|
|
|
right |
|
|
|
back |
|
|
|
left |
|
|
|
up |
|
|
|
down |
|
|
|
The raster orientations form this net; the table below is authoritative when an edge is rotated in the net.
+--------+
| up |
+--------+--------+--------+--------+
| left | front | right | back |
+--------+--------+--------+--------+
| down |
+--------+
Edge coordinates increase top-to-bottom for left/right edges and
left-to-right for top/bottom edges. same preserves that direction and
reversed flips it.
face edge |
adjacent edge |
direction |
face edge |
adjacent edge |
direction |
|---|---|---|---|---|---|
front left |
left right |
same |
front right |
right left |
same |
front top |
up bottom |
same |
front bottom |
down top |
same |
right left |
front right |
same |
right right |
back left |
same |
right top |
up right |
reversed |
right bottom |
down right |
same |
back left |
right right |
same |
back right |
left left |
same |
back top |
up top |
reversed |
back bottom |
down bottom |
reversed |
left left |
back right |
same |
left right |
front left |
same |
left top |
up left |
same |
left bottom |
down left |
reversed |
up left |
left top |
same |
up right |
right top |
reversed |
up top |
back top |
reversed |
up bottom |
front top |
same |
down left |
left bottom |
reversed |
down right |
right bottom |
same |
down top |
front bottom |
same |
down bottom |
back bottom |
reversed |
Cubemap-to-ERP assigns a ray to the face with the largest dot product against
the face forward vector. Scores within eight float64 machine epsilons of
the maximum are treated as an exact edge or vertex tie, and the first face in
the canonical order wins. This prevents trigonometric round-off from changing
a mathematical tie. The rule is deterministic; it does not promise filtered
continuity across independently supplied face rasters. All six faces are
required and must use one backend and one spatial shape.
Sampling at boundaries¶
Nearest¶
Nearest sampling uses floor(c + 0.5). An exact half-integer therefore goes
to the more positive integer index. ERP horizontal coordinates wrap modulo
W, including after rounding; vertical ERP coordinates clamp to the first or
last row. Gnomonic and cubemap coordinates clamp to their border pixel. Thus
the exact ERP seam coordinate W-0.5 rounds and wraps to column 0.
Integer labels and boolean masks require nearest sampling and retain their
exact values. Bilinear interpolation of integer or boolean data raises
TypeError.
Bilinear¶
Bilinear weights are computed from floor(c) and the four neighbouring pixel
centres. ERP neighbours wrap horizontally and clamp vertically. Gnomonic and
cubemap neighbours clamp on both axes, which is border replication rather
than zero padding. At the ERP seam, the last and first columns are neighbours.
Geometry-v1 exposes two named invalid-data policies. invalid_policy="propagate"
is the default and uses ordinary IEEE arithmetic: if a weighted stencil
contains NaN, the result remains NaN, including when that neighbour has
zero mathematical weight. This preserves the strict 3.1 baseline.
invalid_policy="renormalize" is opt-in and requires both an explicit boolean
validity_mask and an explicit min_valid_weight in (0, 1]. For bilinear
weights w_i, mask values m_i, and samples z_i, it computes
q = sum_i(w_i * m_i)
y = sum_i(w_i * m_i * z_i) / q
Non-finite samples are excluded in addition to the caller’s mask. For
multichannel data, a neighbour is valid only when all of its channels are
finite, so validity remains one shared spatial mask. When q is below the
caller-selected threshold, the result is NaN and invalid. Otherwise the
result is normalized and valid. ProjectionResult.valid_weight exposes q,
and ProjectionResult.validity_mask exposes the thresholded result. Repeated
pixels at clamped borders keep the sum of their bilinear weights. Torch
gradients flow through valid input values only.
The normalized policy is floating-point bilinear only. Labels, boolean masks, and integer data continue to require nearest sampling. The library does not choose a universal threshold because acceptable evidence varies by modality.
Geometric support, data validity, and fill¶
ProjectionResult.support_mask describes only geometric coverage. It does
not mean that a sampled measurement is finite or scientifically valid:
a supported floating sample may be
NaN;black RGB, zero labels, and zero-valued measurements may be valid;
a non-zero fill value outside support remains unsupported;
a separate data-validity mask is authoritative; strict callers transform it with nearest as needed, while normalized bilinear callers pass it explicitly.
ERP-to-gnomonic and ERP-to-cubemap outputs are geometrically supported at every output pixel. Gnomonic-to-ERP exposes the closed rectangular footprint. A complete cubemap supports the full sphere.
Dtype, backend, and numerical precision¶
Sampling preserves the image/tensor dtype, layout, batch, channels, and Torch
device. Coordinate calculations use float64 in NumPy. Torch uses float64
grids for float64 tensors except on MPS, and float32 grids otherwise.
Torch CPU bilinear sampling promotes float16 and bfloat16 work to
float32, then casts the result back. NumPy float16 bilinear results are
also cast back to float16.
The conformance tolerances below compare the same operation and input values across supported CPU backends; they are not bounds on resampling error or on a round trip through two projections.
data / check |
absolute tolerance |
relative tolerance |
reason |
|---|---|---|---|
nearest |
exact |
exact |
index selection and values are discrete |
|
|
|
double grids and arithmetic |
|
|
|
CPU-platform grid/accumulation rounding; measured Linux maximum |
|
|
|
quantization after float32 work |
Torch |
|
|
seven-bit mantissa after float32 work |
analytic cardinal rays ( |
|
|
closed-form unit directions |
CUDA and MPS smoke checks are conditional and do not define release success. Autograd is supported for input values, not for geometric parameters.
Conformance and policy¶
The executable fixtures in tests/fixtures/geometry/v1 are the minimum
portable conformance set. Focused contract tests additionally cover the
operational rules above. The fixtures identify their frame, units, dtype,
tolerance, provenance, and derivation; expected values must not be generated
solely by the implementation under test.
The two-mode policy was authorized for 3.1 with strict propagation as the default. Conformance covers unchanged strict behavior, normalized closed-form weights, explicit thresholding, boundary rules, and NumPy/Torch parity.