Work with a spherical image object¶
Use EquirectangularImage at the boundary of an application instead of
passing anonymous arrays through every step. It owns the ERP raster and keeps
support, validity, modality and geometry available to later operations.
Open or create an ERP¶
For a real panorama, the usual entry point is one line:
from panorai import EquirectangularImage
panorama = EquirectangularImage.open("room_360.jpg")
print(panorama.size, panorama.mode, panorama.backend)
open() decodes eagerly; the returned object does not depend on the source
file remaining open. For a constant numerical input, specify height and width
without building an intermediate array:
blank = EquirectangularImage.constant(0.0, height=256, width=512)
rgb = EquirectangularImage.constant(
(0.1, 0.3, 0.6), height=256, width=512
)
constant() defaults to NumPy float32; a channel vector creates HWC data.
Use Pillow-style new(mode, (width, height), color) when image mode is the
primary concern, and fromarray() at NumPy integration boundaries. For an
executable visual example, use a deterministic synthetic ERP without
downloading an asset:
sample = EquirectangularImage.sample("spherical-grid", size=(512, 256))
The available spherical-grid, directions and checkerboard patterns use
the canonical pixel-centre lattice. They are documentation fixtures, not a
dataset or evidence of scientific accuracy.
The complete self-contained flow below is executed in CI:
from io import BytesIO
from panorai import EquirectangularImage
from panorai.geometry import GnomonicSpec
from panorai.ops.spherical import GaussianBlur
constant = EquirectangularImage.constant((0.1, 0.3, 0.6), height=32, width=64)
sphere = EquirectangularImage.sample("spherical-grid", size=(64, 32))
smaller = sphere.resize((32, 16))
turned = smaller.rotate(yaw_deg=30.0)
smooth = turned.filter(GaussianBlur(ksize=3, sigma=0.8, backend="numpy"))
view = smooth.crop(
GnomonicSpec(
center_lat_deg=10.0,
center_lon_deg=25.0,
hfov_deg=90.0,
vfov_deg=70.0,
output_shape_hw=(12, 16),
)
)
composed = smooth.paste(view)
encoded = BytesIO()
composed.save(encoded, format="PNG", value_range=(0.0, 255.0))
encoded.seek(0)
decoded = EquirectangularImage.open(encoded)
assert sphere.size == (64, 32) # transforms did not mutate the source
assert constant.image.shape == (32, 64, 3)
assert constant.image.dtype == np.float32
assert decoded.size == (32, 16)
assert view.image.shape == (12, 16, 3)
Resize and thumbnail the sphere¶
resize((width, height)) samples the pixel-centre bearings of a new canonical
ERP. Longitude wraps, support and validity are transformed separately, and
attached labels remain nearest-only. It is therefore not equivalent to calling
a generic planar resize on the storage rectangle.
thumbnail(max_size) performs the same spherical resampling but preserves the
aspect ratio, never enlarges the input and returns a new object. The original
Pillow method is intentionally not copied as an in-place mutation. If the
requested bounds would collapse either spherical dimension below two pixels,
the method fails instead of distorting the aspect ratio.
Rotate an orientation, not a canvas¶
turned = panorama.rotate(yaw_deg=30, pitch_deg=-5)
Yaw is positive from canonical +Z toward +X; pitch is positive toward
+Y; roll is positive clockwise in the image plane. Advanced callers may pass
a proper canonical 3×3 rotation matrix. A positional scalar such as
rotate(45) fails because it does not identify a 3D axis.
Use named reflections when reflection is the actual intent:
mirrored = panorama.transpose(panorama.Transpose.FLIP_LONGITUDE)
antipodal = panorama.transpose(panorama.Transpose.ANTIPODAL)
No transpose operation silently swaps ERP height and width.
Crop a tangent view¶
from panorai.geometry import GnomonicSpec
face = panorama.crop(
GnomonicSpec(
center_lat_deg=10,
center_lon_deg=25,
hfov_deg=90,
vfov_deg=70,
output_shape_hw=(256, 320),
)
)
The result is a GnomonicFace, not a smaller EquirectangularImage: it is a
pinhole chart over finite angular support and carries the exact projection
needed to lift or reproject results.
Choose direct-sphere or projected-view processing¶
Apply a registered direct operation through the object:
from panorai.ops.spherical import GaussianBlur
smooth = panorama.filter(GaussianBlur(ksize=5, sigma=1.2))
For a planar model, use views().map().reconstruct() instead. The two routes
share the same container and geometry contract; filter() does not disguise a
planar function as a spherical operation.
Composite by reprojection¶
paste(face) and composite(face) require a typed GnomonicFace. They use
its GnomonicSpec to back-project the patch rather than copying pixels into an
ERP box. blend="replace" makes face support and validity authoritative.
blend="alpha" is available for continuous fields and requires an explicit
scalar alpha; categorical alpha blending fails.
Save a viewable raster¶
smooth.save("smoothed.png", value_range=(0, 255))
Image formats do not store PanorAi support, validity, grids, units or attached
depth/labels. save() therefore encodes one selected field and does not claim
to persist the full workflow. Integer images save at their native range. A
floating image outside [0, 1] requires an explicit value_range, preventing
scientific values from being silently normalized or clipped.
Read The spherical image object for the design rationale and EquirectangularImage API for exact signatures.