A spherical camera rig shows up in the SDK as one device, but inside it is a ring of separate lens units, and every unit has its own intrinsic matrix. Stitching a panorama or deprojecting pixels to 3D rays needs each unit's focal length and principal point in pixels — values the factory calibration already holds. This page shows how a rig SDK exposes them per camera index, how to assemble the 3×3 K matrix for each unit, the arithmetic that checks the values against the ring geometry, and three tests that catch a wrong or stale calibration before it ruins a stitch.
One rig, N intrinsic matrices
Each lens unit obeys the same pinhole model as any machine-vision camera. The intrinsic matrix K turns a camera-frame point into a pixel:
fx and fy are the focal lengths in pixels; cx and cy locate the principal point, also in pixels. The skew term is 0 in every machine-vision API, and square pixels make fx equal fy in practice.
A 360° rig is N of these cameras fixed at set angles, with a factory calibration that stores one fx, fy, cx, cy set per unit. The SDK exposes them by unit index, unit 0 through unit N-1, never as one shared matrix. No single number describes the rig's focal length, so a call returning one rig-wide value has nothing true to return.
What the SDK hands you
Spherical-rig SDKs name the calls after what they return. One documented example from this family: ladybugGetCameraUnitFocalLength() returns the focal length of the unit you select, in pixels, and the matching image-center call in the same API returns cx and cy the same way. Both work the same whether the source is the live device or a recorded stream file: you pass the context and the unit index, and the call returns that unit's calibrated value.
Three details matter more than the names:
- Pixels, always. A returned 800.0 means 800 px. If the API also reports a focal length in millimetres, keep the two apart; only the pixel value goes into K.
- One call per unit. Loop over the unit index and store each result. Reading unit 0 and reusing it for all units is the most common way a stitch gets ghosted.
- Factory calibration. The values come off the device or the stream header, measured when the rig was calibrated. They are not estimates your code produces.
Assemble one K per unit
With the three or four scalars for unit i, build the matrix and use it in both directions:
The projection rows predict where a point at (X, Y, Z) in that unit's frame lands, in pixels. Deprojection runs them backwards: subtract the principal point, divide by the focal length, and the result is the ray direction. Keep one matrix per unit, keyed by the unit index. Averaging the units into a single K shifts every unit's principal point by several pixels, and that shift reappears as ghosting along each stitch seam.
Worked example: K for one unit of a nine-unit 40° ring
Inputs: fx = 800.0 px, fy = 800.0 px, cx = 320.0 px, cy = 240.0 px, image size 640 × 480 px — the values a standard camera-intrinsics reference uses for its 640 × 480 example. Ring geometry assumed for the check: nine units, so neighbour spacing is 360° ÷ 9 = 40°.
Rule: a unit's horizontal field of view follows from fx and the image width W, and it must exceed the ring spacing with a few degrees of overlap left for the stitcher.
43.6° covers the 40° spacing with 3.6° of overlap, so the values close a nine-unit ring. This unit's matrix is [[800, 0, 320], [0, 800, 240], [0, 0, 1]], every entry in pixels.
Decision: use it as-is for deprojection. The same arithmetic catches a wrong set. On an eight-unit ring the spacing is 45°, fx_max = 320 / tan(22.5°) = 320 / 0.414 = 773 px, and fx = 800 px would leave a 1.4° gap between neighbours — stitch seams or black wedges. An eight-unit rig reporting these values is handing you the wrong unit's calibration, so re-read per unit or recalibrate.
The test in code, with illustrative variables and the assert that proves the result:
import math
fx, fy, cx, cy = 800.0, 800.0, 320.0, 240.0 # floats, pixels, for one unit
W = 640
hfov = 2 * math.degrees(math.atan((W / 2) / fx)) # 43.6
gx_max = (W / 2) / math.tan(math.radians(20.0)) # 879.2
fx_max = (W / 2) / math.tan(math.radians(20.0))
assert hfov > 40.0 and fx < fx_max # ring closes with overlap
u = fx * (0.100 / 2.0) + cx # 360.0
assert abs(u - 360.0) <= 2.0 # 100 mm target at 2.0 m lands at col 360
Three checks before you trust the numbers
- fx against fy. Square pixels agree to within about 0.2%. A wider split means asymmetric binning or ROI readout, or a value copied from the wrong unit.
- Principal point near the middle. Factory calibrations place cx within about 5% of W/2 and cy within about 5% of H/2 — for 640 × 480 that is cx = 320 ± 16 px and cy = 240 ± 12 px. A principal point tens of pixels into a corner means a wrong unit index or a transcription slip.
- Reproject one known target. Put a target 100 mm right of the axis at Z = 2.0 m and predict the column: u = fx · (X/Z) + cx = 800 × 0.05 + 320 = 360 px. A measured centroid within ±2 px of column 360 passes; a miss of tens of pixels points at the wrong unit or at distortion not yet removed.
When to recalibrate instead
Factory values describe the optics as calibrated. Re-run a calibration, or pull fresh per-unit values from the device, when the lens-to-sensor relationship changes: a lens swap, a drop or bracket shock, or a new protective window in front of the optics. A cover window bends the path and shifts the principal point measurably, which is why assemblies that gain a window get recalibrated rather than trusted from memory. How much a stored principal point can be trusted is quantified in principal point uncertainty: when to distrust your calibration. For metrology-grade measurement, calibrate against a planar grid target yourself; for stitching and deprojection at metre-scale range, the stored values are the intended starting point.
Where the matrix goes next
K converts pixels to camera-frame rays; the rig's extrinsics — each unit's pose — then place those rays in one shared frame. The companion halves of the job are covered by multi-camera extrinsic calibration in one shared coordinate frame and multi-camera point cloud fusion with depth camera extrinsics. If you inherit a full 3×4 projection matrix instead of K, decomposing a projection matrix into camera intrinsics recovers fx, cx and cy the same way, and the same per-unit reading discipline applies to the fx and fy in a ToF camera's calibration model. When the pixels come from a depth image, which camera to deproject with decides whether the cloud comes out straight or wavy.
Common mistakes
- One averaged matrix for all units. Every unit gets a shifted principal point and every seam shows it.
- Millimetre focal lengths inside K. A data-sheet focal length in mm is not fx. fx = focal length ÷ pixel pitch, and the SDK has already done that division.
- Unit 0's cx and cy on unit 4. Each unit has its own; read them per index.
- Deprojecting before undistortion. K models the pinhole part only. If the SDK returns distortion coefficients per unit, apply them first, especially near the image edges.
- A non-zero skew term. It is 0 in these APIs; a non-zero value in your code is a transcription bug.
Building the rig yourself
If you assemble the multi-camera set-up from discrete parts instead of buying a calibrated sphere, no SDK values exist yet, and you calibrate each camera against a target. The pre-calibration estimate is fx = focal length ÷ pixel pitch, replaced by measured values once you calibrate. Choose the area scan cameras by interface and resolution, pair them with machine vision lenses whose image circle covers the sensor, or start from the 3D sensors if the job is depth rather than a stitched panorama. The camera finder filters all three groups, the camera step of the configurator narrows a shortlist, the 3D guides topic collects the calibration and integration pages, and we help plan the calibration itself — ask through the contact page.
Questions
Are fx and fy always equal?
For the square-pixel sensors in machine-vision and spherical rigs, yes in practice: stored calibrations usually agree to better than 0.2%. A wider split points at asymmetric binning or ROI readout, or at a value copied from the wrong unit. Use whatever the SDK returns rather than forcing the two equal — the calibration was measured, not assumed, and deprojection uses each row with its own focal length.
Can one averaged matrix serve all units?
No. The units sit at different angles and each lens carries its own tolerances, so the principal points differ by pixels from unit to unit. Averaging shifts every unit's principal point and shows up as ghosting along each stitch seam; worse, it hides a wrong unit index, because all units then look identical. Store one matrix per unit index and key every projection by it.
Does the SDK also provide distortion coefficients?
Most spherical-rig SDKs store a distortion model per unit next to fx, cx and cy, because wide-angle units are never perfect pinholes. The matrix covers only the pinhole part: apply the unit's distortion model before deprojecting, or accept radial error of a few pixels near the image edges. Read the coefficients from the same per-unit source as the focal length, never from a shared header.
The SDK reports focal length in millimetres — how do I get fx?
Divide by the pixel pitch: fx in pixels equals focal length in mm divided by pixel pitch in mm. An 8 mm lens on 3.45 µm pixels gives 8000 ÷ 3.45 ≈ 2319 px, which is about 2.3 px per millimetre at 1 m range. Treat that as the pre-calibration estimate only; once the device is calibrated, the SDK's stored value wins, because it already reflects the focus position.