Skip to content

Compiled PNG decoding for camera–LiDAR calibration

The optional images extra uses Pillow to decode larger PNG scanlines and NumPy to calculate the existing floating-point luminance. Install it from the repository checkout:

python -m pip install -e ".[images]"

Normal calibrex calibrate commands use the adapter automatically for supported images of at least 65,536 pixels. Smaller images use the native decoder. Missing Pillow, a decoder failure, or an image exceeding Pillow's configured pixel limit also uses the native decoder. Importing the reader or displaying CLI help does not import Pillow. The core remains ROS independent.

Preserved input and calibration contract

The reader still accepts only non-interlaced, 8-bit grayscale, grayscale-alpha, RGB and RGBA PNGs. Its original header, decompression, row-length and filter checks remain in effect. The adapter constructs a minimal PNG from the selected header and IDAT stream, preserving the native handling of ancillary chunks, original CRCs and trailing bytes. It leaves Pillow's global limits unchanged.

RGB luminance remains 0.299*red + 0.587*green + 0.114*blue, evaluated as three float64 multiplications and two additions. Alpha is ignored in the same way as the native reader. There is no conversion to integer grayscale, compositing or gamma adjustment. The returned immutable rows retain the original API.

The numerical solvers, train/holdout splits, known-bad probes, curvature, tracking decisions, work budgets and output schemas are unchanged. Tests cover all twenty combinations of color layout and PNG filter, byte wraparound, invisible alpha, malformed streams, optional-dependency absence, decoder failure and the original acceptance of CRC/trailing-stream variations.

Pillow is an optional external dependency under its MIT-CMU license; its codec implementation is not copied into the core.

Complete public-data comparison

The comparison uses the two authenticated A2D2 20180810_150607 front-left camera/LiDAR pairs and calibration file already used by the public examples. All five raw input hashes are retained. Both source variants use the same installed Pillow and NumPy, one BLAS thread and alternating fresh processes. Host CPU/GPU activity is unreserved.

The complete commands include startup, input reads and decoding, all solver evaluations and diagnostics, output saving, HTML/JSON report generation and bundle verification. Independent untimed workers hash every decoded float64 luminance sample from both real images. No timing warmup or failed attempt is discarded. Only the declared output paths, run ID, timestamps and source commit are normalized when comparing complete calibration results.

The indexed evidence archive contains every worker, complete result/report bundle, source capsules and manifests, input/config hashes, pixel digests and the frozen launcher/schema. Each capsule's commit is injected into source metadata because extracted source trees have no Git directory; this does not change a numerical operation.

This is execution and numerical-parity evidence. A2D2's camera-view point clouds are already registered, and the two-frame online example uses w=1; the public example's inconclusive accuracy and deployment status remain unchanged. These timings do not establish gains for other datasets or other image readers.

Three alternating pairs per command (twelve timed workers) give these medians:

Complete CLI command Native decoder Optional adapter Ratio
Levinson–Thrun online monitor 19.455 s 5.384 s 3.614x
Pandey mutual-information calibration 16.769 s 5.185 s 3.234x

All twelve complete calibration results match after the declared metadata/path normalization, and every output bundle verifies. All 4,638,720 luminance samples from the two images have identical float64 bytes. The candidate is 183fb3d1221d118a20006b615d594bbf4fdc9875; its baseline is 5478ec42493fb9495cbabba39452c70bff8890be. The measured environment uses Python 3.12.10, Pillow 12.3.0 and NumPy 2.3.4. These are CPU image-decoding gains; the solver implementations and GPU kernels are unchanged.

Reproduce and audit

python tools/download_public_dataset.py a2d2_pandey_mutual_information
python -m tools.benchmark_png_luminance \
  --baseline-commit 5478ec42493fb9495cbabba39452c70bff8890be \
  --dataset data/public/a2d2_pandey_mutual_information \
  --output-dir outputs/png-luminance-comparison --repetitions 3
python -m tools.benchmark_png_luminance --audit docs/assets/png_luminance_decoding

Run reproduction from the measured candidate revision recorded in the archive's protocol. Auditing the retained evidence requires the ordinary CPU dependencies, without Pillow, the external data or a GPU.

Array consumers

calibrex.data.read_png_luminance_array(path) returns caller-owned, C-contiguous float64 luminance with shape (height, width). It shares the existing PNG validation and optional Pillow decoder. Array consumers avoid creating Python pixel tuples and converting them back into arrays. The immutable read_png_luminance API is preserved, including its native fallback. The online A2D2 loader and KITTI online benchmark now use the array API.

A separate comparison uses all 48 already-spent public KITTI images from 2011_09_30_drive_0018_sync, with three ordered read workers per arm. Both controls already use Pillow. All 21,773,760 decoded float64 samples match bit for bit in every worker. Three alternating pairs of fresh processes give:

Scope Rows followed by array conversion Direct array reader Ratio
Complete PNG-to-array read stage 2.854628 s 0.768915 s 3.713x
Measurement process including startup and verification 4.791209 s 2.460095 s 1.948x

The read stage includes file reads, shared PNG validation, lazy Pillow import, parallel decoding and the required representation conversion. Parent timing also includes source/input verification, pixel hashing and saving evidence. Input verification warms the operating system file cache. The host is unreserved. These scopes exclude calibration and are reported separately from the earlier full CLI comparison below; the ratios must not be multiplied.

The source is frozen at 87fbc7d9 and all 401 source modules are bound by a manifest and source capsule. An earlier six-worker pilot overlapped validation tests; its original evidence and context are retained separately and are not pooled with the formal six workers. The array-reader evidence includes both cohorts without images or scans. A public A2D2 regression compares the complete calibration result against the rows control, normalizing only run/source metadata and output paths; all serialized numerical values match.

python -m tools.benchmark_png_array \
  --fixture outputs/levinson-online-screen-v1/fixture-48 \
  --output-dir outputs/png-array-comparison --workers 3 --repetitions 3
python -m tools.benchmark_png_array --audit docs/assets/png_array_decoding/formal

Reproduction requires the existing fixture and the optional images extra. The CPU-only evidence audit reads source bytes without executing the archived launcher or source capsule and needs neither the inputs nor Pillow.