29 KiB
Camera capture measurements — towers 121 and 111
These are measured values, not estimates. They were taken to decide how auto mode can capture faster at full resolution, no binning, one quality setting, continuous acquisition. That constraint is the design decision these numbers serve. Two runs:
- 2026-09-30, tower 121: camera frame rates, streaming, auto-exposure, white balance, first CPU figures.
- 2026-10-01, towers 121 and 111: JPEG XL compression in detail (input format, effort, threads, quality setting), whether saving keeps up with the scan (pile-up), CPU heat, storage speed; and the camera tests repeated on tower 111.
Each figure applies to the tower it was measured on (see Scope).
Summary
Camera and USB
- Full resolution does not cost frame rate on tower 121. With 2×2 binning, its
standardprofile runs at the same rate as full-resolution RGB8 (4.95 fps at 250 MB/s). The camera budgets the link limit against the unbinned frame, so binning saves USB traffic but not time. - Raw Bayer is about 3× the frame rate of RGB8 over the same link on both cameras. Tower 121: BayerRG8 14.75 fps at 250 MB/s, 26.2 at 450 MB/s (RGB8 4.95 and 8.8). Tower 111: 14.2 and 20.6 (RGB8 5.9 and 9.6).
- No format stalled on either tower. Every format, at every link limit (up to 450 MB/s), streamed for 60 s with nothing lost, also while the CPU was encoding. The only losses were 2 incomplete frames out of 1,574 on tower 121 at 450 MB/s under encode load. The stalls in known-issues.md did not reproduce on either tower since the USB fixes (#19).
- The camera applies white balance to raw Bayer output exactly as to RGB8, on both cameras, so colour conversion on the tower needs no white-balance step.
Auto-exposure (tower 121; tower 111's camera saw near-darkness, see below)
- It settles faster at a higher frame rate, but not in proportion. After a large brightness step
it reached ±10% in 1.4–1.8 s at 4.95 fps, 0.7–1.2 s at 14.75 fps and 0.4–0.7 s at 26.2 fps.
Today's 1000 ms
settle_wait_msat 4.95 fps is shorter than a large step takes, which fits the badly exposed frames in known issue #16. - "Exposure and gain unchanged for N frames" is not a safe settle test. Auto-exposure moves in steps and pauses; the rule fired in 6 of 14 runs while brightness was still 17–300% off.
- From a dark step it overshoots to its maximum exposure (3.4–4.1× too bright) before coming back down.
- The camera's per-frame data (chunk mode) works on both cameras: exposure, gain and a camera timestamp on every frame.
JPEG XL compression and saving (2026-10-01; times include writing the file)
- The encoder takes ordinary images only: one channel (grey) or three (RGB). It knows nothing of Bayer patterns, so raw data is either converted to colour first or stored as a one-channel mosaic, which must be lossless.
- Converting raw to colour on the tower costs almost nothing: 5–13 ms, then the same encode time as the camera's own RGB8. The colour encode is the cost: on a detailed full frame, 0.74 s on tower 121 (16 MP) and 0.86 s on tower 111 (12 MP), with 4 threads at today's quality setting.
- Effort 1–4 cost about the same; effort 5 is 3–5× slower. Effort 3 gave the smallest files of effort 1–4.
- Production's 3 encoder threads are a poor choice. On tower 121, 3 threads was slower than 2; 4 threads was fastest on both towers.
- One save worker cannot keep up with a frame every 0.7 s: colour images piled up on both towers (9–29 behind after 2 minutes, saved up to 21 s late). Two workers with 2 threads each kept up on both. One worker handled a frame every 1.0 s on both.
- Lossless raw saving uses a tenth of the CPU (0.19–0.25 CPU-s per frame against 1.7–1.9), but files are 2–3× larger, and on tower 111 the storage is the limit: its eMMC writes at 76 MB/s (tower 121: 218 MB/s).
- Heat, in the field enclosure: flat out, the CPU levels at 75–79 °C with peaks of 88–90 °C. At one colour frame every 2 s: 53 °C on tower 121 and 63 °C on tower 111. Tower 121's throughput drifted down 13% over 10 minutes flat out; tower 111's by 4%.
Bugs found
jxl_distance = 0is not lossless in the production encoder (known issue #20): decoded pixels differ by up to 1–2 levels.- 12-bit lossless needs an explicit bit-depth setting in libjxl, or 12-bit values held in 16-bit words are rescaled. This affected only the test program; 12-bit figures before 2026-10-01 were wrong and are corrected below.
Setup
| Tower 121 | Tower 111 | |
|---|---|---|
| Computer | LattePanda IOTA: Intel N150, 4 cores / 4 threads, 0.7–3.6 GHz, 7 GB | LattePanda 3 Delta: Intel Celeron N5105, 4 cores / 4 threads, 0.8–2.9 GHz, 7.6 GB |
| OS | Ubuntu 26.04.1, kernel 7.0.0-31 | Ubuntu 22.04.5, kernel 6.8.0-138 |
| Storage | eMMC, 218 MB/s write (measured) | eMMC, 76 MB/s write (measured) |
| Camera | Alvium 1800 U-1620c, DEV_1AB22C0B7D4E, firmware 14.3.4646D35D, 5328×3040 (16.2 MP) |
Alvium 1800 U-1240c, DEV_1AB22C049CD1, firmware 13.1.794391F9, 4024×3036 (12.2 MP) |
Profile in use (standard) |
RGB8, 2×2 binning, 4.95 fps, 250 MB/s, exposure ≤ 20 ms, MinimizeBlur |
RGB8, no binning, 1.09 fps, 200 MB/s, exposure ≤ 9 s, MinimizeNoise |
| Libraries | vmbpy 1.2.0 (Python 3.14), libjxl 0.11.1, OpenCV 4.10.0 | vmbpy 1.0.5 (Python 3.10), libjxl 0.11.0, OpenCV 4.5.4 |
| Tower program | f2c61e0-dirty |
8d81e03 (same JPEG XL encoder source) |
| Enclosure | field enclosure (confirmed) | field enclosure |
| Scene | Indoors facing a window, badly out of focus (known issue #15). 2026-09-30: grey, noisy, gain at 24 dB. 2026-10-01: nearly black, a blurred blue light, exposure and gain at their limits. | Near-darkness: mean pixel value 7 of 255 at 20 ms and 27 dB. |
| USB preconditions | usbfs_memory_mb 1000, LPM off (#19) |
same |
The tower program was stopped for every test: the camera accepts one program at a time, and the CPU figures should have one cause. Nothing here is measured under the program's own load.
Method
The programs are in tools/camera-bench.
Camera (camera_bench.py, vmbpy): full sensor, binning 1. Its frame handler only records
metadata and hands the buffer back, so it cannot cause the losses it counts. On tower 111
auto-exposure was capped at 20 ms (tower 121's profile value), or its 0.7 s exposures would have
limited the frame rate instead of the link.
- Maximum frame rate per pixel format and link limit, as the camera reports it.
- Streaming 60 s at that rate, 5 buffers (as production): frames, incomplete frames, gaps in the camera's frame IDs, gaps over 1 s.
- Streaming under load: the same while encoding colour JXL in a loop.
- Auto-exposure step: settle 8 s, force brightness (exposure × gain) to 1/16 or up to 16×, hold 2 s, release, and log per-frame exposure and gain for 15 s; twice per setting.
- White balance on raw: exposure and gain fixed; red and blue
BalanceRatioat 1.0 and 2.0; compare the mean at those pixel sites in BayerRG8 and RGB8.
Compression (encode_bench.cpp), on full-resolution frames in the camera's formats. Colour
lossy encodes use the production src/camera/JpegXlEncoder.cpp unchanged. Lossless and 12-bit
encodes use a test-only encoder that sets lossless mode correctly (see #20). Every time includes
writing the file, as production does. Two input frames per tower:
- the real frame the camera saw that day (dark and blurred, so it compresses unusually easily);
- a synthetic frame at the camera's resolution: a "dead leaves" texture (the camera texture chart of ISO 19567-2) with mild sensor noise, sharp and detailed at every scale, as the realistic worst case (image).
Input kinds: rgb8 (the camera's colour), bilinear and ea (raw converted to colour on the tower,
OpenCV bilinear or edge-aware), raw8 and raw12 (the raw mosaic, lossless), colour12 (12-bit raw
converted to 16-bit colour, lossy).
- Matrix: every colour input at effort 1–5 × 1–4 threads (quality setting
distance0.8, as production); a distance sweep 0.5–3.0 with PSNR against the input; lossless raw at effort 1–3 × 1–4 threads. Median of 3 runs. - Throughput: back-to-back saves for 60 s, 1 worker × 4 threads and 2 workers × 2 threads.
- Pile-up: one frame queued every 2.0, 1.0 or 0.7 s for 2 minutes, as a scan keeps one frame per waypoint; time from queueing to file written, and the queue left at the end.
- Heat: idle baseline; 10 min of back-to-back colour saves; 5 min cool-down; 10 min at one frame every 2 s. CPU package temperature, average clock, CPU load and package power (RAPL) once a second.
- Storage:
ddof 256 MB with a final sync.
At the end of each tower's run the camera profile was reloaded, frames were confirmed to flow, and
the tower program was restarted; both came back on their standard profile.
Results: camera and USB
Maximum frame rate and streaming, full resolution
Received / incomplete / lost frames over 60 s at the maximum rate.
Tower 121 (5328×3040):
| Format | Frame | 250 MB/s: max fps | 60 s at 250 | 450 MB/s: max fps | 60 s at 450 |
|---|---|---|---|---|---|
| BayerRG8 | 16.2 MB | 14.75 | 886 / 0 / 0 | 26.17 | 1570 / 0 / 0 |
| BayerRG10p | 20.2 MB | 11.82 | not streamed | 20.95 | not streamed |
| BayerRG12p | 24.3 MB | 9.86 | 592 / 0 / 0 | 17.52 | 1053 / 0 / 0 |
| RGB8 | 48.6 MB | 4.95 | 298 / 0 / 0 | 8.79 | 527 / 0 / 0 |
RGB8, 2×2 binning (standard) |
12.1 MB | 4.95 | 50 / 0 / 0 in 10 s, 60 MB/s actual | not measured | not measured |
While encoding: BayerRG8 at 450 MB/s 1574 received, 2 incomplete; BayerRG8 at 250 and RGB8 at 250 clean.
Tower 111 (4024×3036):
| Format | Frame | 200 MB/s: max fps | 60 s at 200 | 250 MB/s: max fps | 60 s at 250 | 450 MB/s: max fps | 60 s at 450 |
|---|---|---|---|---|---|---|---|
| BayerRG8 | 12.2 MB | 12.10 | 717 / 0 / 0 | 14.25 | 854 / 0 / 0 | 20.60 | 1235 / 0 / 0 |
| BayerRG10p | 15.3 MB | 10.20 | not streamed | 12.10 | not streamed | 18.06 | not streamed |
| BayerRG12p | 18.3 MB | 8.80 | 527 / 0 / 0 | 10.52 | 632 / 0 / 0 | 16.01 | 962 / 0 / 0 |
| RGB8 | 36.7 MB | 4.84 | 290 / 0 / 0 | 5.90 | 354 / 0 / 0 | 9.61 | 575 / 0 / 0 |
While encoding: BayerRG8 at 450 and 250, and RGB8 at 250, all clean (1235, 854 and 353 frames).
In every run the measured rate was within 1.5% of the maximum and the longest gap between frames was about one frame period. Tower 111's raw rates rise less than in proportion to the link limit (20.6 fps at 450 MB/s carries 251 MB/s), so above 250 MB/s something other than the link limits that camera.
Auto-exposure step response (tower 121, BayerRG8)
Times from switching auto-exposure back on. ±10%: first frame within 10% of the pre-step brightness. Stopped changing: last change of exposure or gain in the 15 s run. 3-unchanged rule: first frame after which exposure and gain stayed the same for 3 frames.
| fps | Reaction rate | Step | ±10% | Stopped changing | 3-unchanged rule fired |
|---|---|---|---|---|---|
| 26.17 | 100 | dark ×1/16 | 0.47 s (12 frames), 0.66 s (17) | 0.89 s (23), 1.27 s (33) | 0.62 s, 0.09 s |
| 26.17 | 100 | bright ×4.1, ×3.7 | 0.54 s (14), 0.39 s (10) | 0.93 s (24), 0.93 s (24) | 0.09 s, 0.77 s |
| 14.75 | 100 | dark ×1/16 | 1.04 s (15), 0.91 s (13) | 1.17 s (17), 1.18 s (17) | 0.16 s, 0.16 s |
| 14.75 | 100 | bright ×4.0, ×3.6 | 1.18 s (17), 0.70 s (10) | 1.45 s (21), 1.38 s (20) | 0.16 s, 1.11 s |
| 4.95 | 100 | dark ×1/16 | 1.58 s (7), 1.79 s (8) | 2.19 s (10), 2.59 s (12) | 2.59 s, 3.00 s |
| 4.95 | 100 | bright ×3.8, ×3.5 | 1.38 s (6), 1.38 s (6) | 1.79 s (8), 2.59 s (12) | 2.19 s, 3.00 s |
| 14.75 | 50 | dark ×1/16 | 2.19 s (32) | 3.75 s (55) | 0.16 s |
| 14.75 | 50 | bright ×3.4 | 1.99 s (29) | 3.48 s (51) | 0.16 s |
Bold: the rule fired too early, on frames that still carried the forced values. The bright step reached only ×3.4–4.1 because gain was already at its maximum. In every run exposure and gain ended within 2% of the pre-step value. From the per-frame series:
- Once within ±10%, brightness stayed there, so that column is a real settle time.
- Old values persist for 1–5 frames after auto-exposure resumes.
- Auto-exposure moves in steps and pauses: holds of 3+ unchanged frames while still more than 10% off, 0–3 times per run at rate 100 and 5–6 at rate 50. Even skipping the first 3–4 frames, "unchanged for 3 or 4 frames" fired while 17–300% off in 6 of 14 runs.
- Overshoot from a dark step: auto-exposure first jumps to
ExposureAutoMax(20 ms) and comes back down, here 3.4–4.1× too bright.
Tower 111 saw near-darkness, so with exposure capped at 20 ms both exposure and gain sat at their limits. The bright step could not be forced at all, and the dark step only measures recovery back to the limits: 1.25 s (13 frames) at 20.6 fps, 1.56–1.65 s (10–12) at 14.2 fps, 3.6–3.8 s (12–13) at 5.9 fps, 2.43 s at reaction rate 50. Treat these as a lower bound on its settling, not a measurement of it.
White balance on raw output
Mean pixel values at those sites, exposure and gain fixed:
| Setting | 121 raw red | 121 RGB8 red | 121 raw blue | 121 RGB8 blue | 111 raw red | 111 RGB8 red | 111 raw blue | 111 RGB8 blue |
|---|---|---|---|---|---|---|---|---|
| Red ratio 1.0 | 58.3 | 57.8 | 104.3 | 102.8 | 2.55 | 6.03 | 7.76 | 17.65 |
| Red ratio 2.0 | 82.4 | 81.4 | 104.9 | 103.3 | 5.54 | 12.47 | 7.75 | 17.55 |
| Blue ratio 1.0 | 99.2 | 98.1 | 74.0 | 73.3 | 7.07 | 16.46 | 4.10 | 9.60 |
| Blue ratio 2.0 | 99.1 | 98.9 | 104.0 | 103.5 | 7.11 | 16.44 | 8.70 | 19.50 |
On both cameras the ratios change raw pixels as they change RGB8. On tower 111 doubling a ratio doubled the values (×2.07–2.17). On tower 121 it gave only ×1.41; that frame had 21% of its pixels clipped at white, which cannot get brighter, and this is the likely reason. On tower 111 the RGB8 values are about 2.3× the raw ones at the same settings; not explained.
The camera's BayerRG8 is OpenCV's COLOR_BayerBG2RGB, not BayerRG2RGB: de-Bayering with it
gives the same channel order as the camera's own RGB8.
Results: JPEG XL compression and saving
What the encoder takes
libjxl encodes ordinary images: one channel (grey) or three (RGB), 8 or 16 bits per value. Raw camera data reaches it in one of two ways:
- Converted to colour (by the camera as RGB8, or on the tower), then encoded lossy: libjxl
converts to a perceptual colour space, transforms blocks and discards what the eye is least likely
to miss.
distancesets how much (1.0 is meant to be visually lossless; production uses 0.8). - As the raw mosaic in one grey channel, which must be lossless: lossy compression would blend neighbouring pixels of different colours and ruin colour after de-Bayering. The file then needs de-Bayering after decoding.
Not tested: splitting the mosaic into four quarter-size planes (R, G, G, B) and storing them as channels, a known technique that compresses raw data better and could allow light lossy compression.
Every input at production quality (distance 0.8 or lossless), 4 threads
Time per frame including the file write, and file size. Synthetic (detailed) frame first, then the real frame.
| Input | Tower 121, 16.2 MP | Tower 111, 12.2 MP |
|---|---|---|
rgb8, camera's colour, effort 4 |
0.74 s, 7.9 MB · real 0.47–0.50 s, 1.7 MB | 0.87 s, 6.0 MB · real 0.62 s, 1.8 MB |
rgb8, effort 4, 3 threads (production today) |
0.92 s · real 0.63 s | 0.93 s · real 0.69 s |
bilinear, tower colour, effort 4 |
0.73 s, 7.0 MB · real 0.46 s, 1.2 MB | 0.84 s, 5.3 MB · real 0.49 s, 0.7 MB |
ea, tower colour, effort 4 |
0.74–0.79 s, 7.2 MB · real 0.47–0.49 s, 1.3 MB | 0.86 s, 5.4 MB · real 0.52 s, 0.8 MB |
colour12, 12-bit tower colour, effort 4 |
0.69 s, 7.0 MB · real 0.49 s, 1.2 MB | 0.94 s, 5.2 MB · real 0.59 s, 0.8 MB |
raw8 lossless, effort 1 |
0.10 s, 17.9 MB · real 0.06 s, 11.2 MB | 0.23 s, 13.5 MB · real 0.15 s, 6.8 MB |
raw12 lossless, effort 1 |
0.07 s, 26.0 MB · real 0.09 s, 19.3 MB | 0.40 s, 19.6 MB · real 0.19 s, 13.0 MB |
The conversion to colour on the tower took 5–16 ms (bilinear or edge-aware), and 23–41 ms for 12-bit. On the synthetic frame, 12-bit lossless is larger than the 24.3 MB the camera sends: sensor noise in the low bits does not compress.
Effort and threads (synthetic frame, ea, distance 0.8)
| Effort | 121: 1 / 2 / 3 / 4 threads | 121 file | 111: 1 / 2 / 3 / 4 threads | 111 file |
|---|---|---|---|---|
| 1 | 884 / 685 / 786 / 620 ms | 7.4 MB | 1257 / 849 / 731 / 730 ms | 5.6 MB |
| 2 | 882 / 717 / 792 / 587 ms | 7.4 MB | 1253 / 862 / 785 / 718 ms | 5.6 MB |
| 3 | 1027 / 788 / 890 / 645 ms | 7.0 MB | 1385 / 949 / 803 / 768 ms | 5.3 MB |
| 4 | 1070 / 839 / 907 / 742–788 ms | 7.1 MB | 1494 / 1005 / 847 / 858 ms | 5.4 MB |
| 5 | 5637 / 3500 / 2954 / 2440 ms | 8.3 MB | 7287 / 4242 / 3541 / 3161 ms | 6.3 MB |
rgb8 and bilinear follow the same pattern within about 10% (full tables:
summarize_encode.py, see Raw data).
- Effort 1–4 are within about 20% of each other; effort 5 is 3–5× slower and gave larger files on the detailed frame (smaller ones on the dark real frames). Effort 3 gave the smallest files of 1–4, at effort 4's speed or better.
- Tower 121: 3 threads is slower than 2 for every colour input and effort, on both frames (and for most lossless rows); 4 is fastest. The cause is not known. Tower 111 behaves as expected: each thread helps, though 3 and 4 are close.
- Per pixel, tower 111 is about 55% slower than tower 121.
Quality setting (effort 4, 4 threads, synthetic frame)
PSNR against the input; higher is closer. On this noisy synthetic frame PSNR counts removed noise as loss, so compare settings with each other, not with other images.
| distance | 121 rgb8 |
121 ea |
111 rgb8 |
111 ea |
|---|---|---|---|---|
| 0.5 | 0.84 s, 10.8 MB, 29.7 dB | 0.81 s, 9.8 MB, 31.7 dB | 0.92 s, 8.1 MB, 29.7 dB | 0.94 s, 7.4 MB, 31.7 dB |
| 0.8 | 0.79 s, 7.9 MB, 27.6 dB | 0.74 s, 7.2 MB, 29.4 dB | 0.85 s, 6.0 MB, 27.6 dB | 0.83 s, 5.4 MB, 29.4 dB |
| 1.0 | 0.68 s, 7.1 MB, 26.9 dB | 0.73 s, 6.4 MB, 28.7 dB | 0.80 s, 5.3 MB, 26.9 dB | 0.87 s, 4.8 MB, 28.8 dB |
| 1.5 | 0.69 s, 5.0 MB, 25.2 dB | 0.66 s, 4.5 MB, 27.0 dB | 0.68 s, 3.8 MB, 25.2 dB | 0.67 s, 3.4 MB, 27.0 dB |
| 2.0 | 0.64 s, 4.2 MB, 24.6 dB | 0.62 s, 3.8 MB, 26.2 dB | 0.65 s, 3.2 MB, 24.6 dB | 0.64 s, 2.9 MB, 26.3 dB |
| 3.0 | 0.59 s, 3.4 MB, 23.8 dB | 0.58 s, 3.1 MB, 25.4 dB | 0.62 s, 2.5 MB, 23.8 dB | 0.61 s, 2.3 MB, 25.4 dB |
From 0.5 to 3.0, files shrink about 3× and encoding gets only about 30% faster. The quality setting is a storage decision more than a speed one.
Keeping up with the scan (synthetic frame, distance 0.8, effort 4; raw8 effort 1)
Saved after: time from queueing the frame to the file being written, typical / worst. Behind: frames still queued when the 2 minutes ended.
| Input | Workers × threads | One frame every | 121: saves/s, saved after, behind | 111: saves/s, saved after, behind |
|---|---|---|---|---|
rgb8 |
1 × 4 | back to back | 1.32 /s | 1.19 /s |
rgb8 |
2 × 2 | back to back | 1.72 /s | 1.58 /s |
ea |
1 × 4 | back to back | 1.31 /s | 1.23 /s |
ea |
2 × 2 | back to back | 1.77 /s | 1.62 /s |
raw8 |
1 × 4 | back to back | 9.90 /s | 3.74 /s |
raw8 |
2 × 2 | back to back | 13.1 /s | 5.10 /s |
rgb8 |
1 × 4 | 2.0 s / 1.0 s | 0.7 / 0.8 s, none | 0.8 / 1.0 s, none |
rgb8 |
1 × 4 | 0.7 s | 3.5 / 7.4 s, 9 behind | 11.1 / 21.4 s, 29 behind |
ea |
1 × 4 | 2.0 s / 1.0 s | 0.7 / 0.8 s, none | 0.8 / 1.1 s, none |
ea |
1 × 4 | 0.7 s | 1.5 / 4.2 s, 4 behind | 8.9 / 17.0 s, 23 behind |
ea |
2 × 2 | 1.0 s | 0.8 / 0.9 s, none | 1.0 / 2.1 s, none |
ea |
2 × 2 | 0.7 s | 0.9 / 1.1 s, none | 1.2 / 1.6 s, none |
raw8 |
1 × 4 | 2.0 / 1.0 / 0.7 s | 0.1 / 0.16 s, none | 0.22 / 1.6 s, none |
ea, real frame |
1 × 4 | 1.0 s | 0.48 / 0.57 s, none | 0.48 / 0.59 s, none |
- One worker keeps up to about one colour frame per 0.8 s (121) or 0.85 s (111); faster than that, the queue grows without limit (the production queue has none).
- Two workers × 2 threads raise that by about 30%, at the cost of each image taking a little longer (about 1.1–1.2 s instead of 0.8 s), because one encode does not keep 4 cores busy: one worker × 4 threads kept the CPU 60–64% busy.
- Raw lossless keeps up easily; on tower 111 its occasional slow saves (up to 1.6 s) come from storage (see below).
Heat and power (field enclosure)
| Tower 121 | Tower 111 | |
|---|---|---|
| Idle, program stopped | 37 °C, 0.78 GHz, 2.2 W | 51 °C, 1.01 GHz, 2.9 W |
| Idle, program running (before the tests) | 39 °C | 64 °C |
One ea frame every 2 s, 10 min |
53 °C mean (peaks 83), 1.79 GHz, 5.2 W, 23% busy | 63 °C mean (peaks 84), 1.77 GHz, 5.0 W, 26% busy |
| Flat out, 10 min (1 × 4) | 75 °C mean (peaks 88), 2.82 GHz, 11.2 W | 77 °C mean (peaks 90), 2.51 GHz, 7.9 W |
| Throughput over the 10 min flat out | 92 → 80 per minute (−13%) | 76 → 73 per minute (−4%) |
| Back to idle after flat out | ~38 °C within 3–4 min | not separately measured |
| CPU-seconds per colour frame | 1.7–1.9 | 1.7–1.9 |
| CPU-seconds per raw lossless frame | 0.19–0.21 | 0.23–0.25 |
Minute by minute, flat out, the temperature levelled after 2 minutes on both towers and the clock averages stayed flat. Tower 121's throughput still fell 13%, so it slowed in a way the one-second clock average does not show. At one frame every 2 s nothing slowed on either tower: 30 frames every minute. Peaks are momentary, during each encode, and stay well below the usual 100–105 °C junction limit of these CPUs.
Storage
256 MB written with a final sync: tower 111 76 MB/s, tower 121 218 MB/s (both eMMC). A 13–18 MB raw file takes 0.17–0.24 s just to write on tower 111, which caps it at about 4 raw saves per second whatever the CPU could do. Colour files (1–8 MB) are not limited by it.
CPU cost per frame, first figures (tower 121, 2026-09-30)
Real frame of that day (grey, noisy), 3 threads unless stated.
| Step | Time | File size |
|---|---|---|
Copy a frame (as FrameReceived does for every frame) |
1.6 ms (BayerRG8), 4.2 ms (RGB8) | – |
| De-Bayer, OpenCV bilinear / edge-aware / VNG | 8.0 / 4.1 / 107 ms | – |
| Rotate 180° (RGB8) | 31 ms | – |
| Colour JXL, distance 0.8, effort 1 / 2 / 3 / 4 / 5 | 655 / 619 / 672 / 670 / 2466 ms | 2.82 / 2.82 / 2.58 / 2.65 / 2.23 MB |
| same, 4 threads | 412 / 426 / 464 / 504 / 2049 ms | same |
| same, effort 4, while streaming | 711–742 ms | – |
| Raw BayerRG8, truly lossless, effort 1 / 2 / 3 | 98 / 400 / 520 ms | 9.29 / 8.44 / 8.24 MB |
Raw BayerRG8 through production jxl_distance = 0 (not lossless), effort 1–4 |
718–789 ms | 14.9–15.1 MB |
The 12-bit figures first published in this row were wrong (see 12-bit lossless); the corrected 12-bit results are in the tables above.
jxl_distance = 0 is not lossless
JpegXlEncoder::encodeToFile sets basic_info.uses_original_profile = JXL_FALSE in every case. For
distance 0, libjxl prints Set uses_original_profile=true for lossless encoding, still returns
success, and encodes through its lossy colour space:
| Input | File | Pixel values differing | Largest difference |
|---|---|---|---|
| BayerRG8 (1 channel) | 15.1 MB | 243,403 of 16.2 M (1.5%) | 1 |
| RGB8 (3 channels) | 31.2 MB | 3,564,870 of 48.6 M (7.3%) | 2 |
config.example.ini and the encoder header both describe distance 0 as lossless. Known issue #20.
12-bit lossless needs a bit-depth setting
When 12-bit values are passed in 16-bit words, libjxl by default reads them as the full 0–65535
range and rescales, so a "lossless" encode keeps only the top bits: in a round-trip test every pixel
changed, by up to 3,839. Setting JxlEncoderSetFrameBitDepth(JXL_BIT_DEPTH_FROM_CODESTREAM) makes it
exact (0 differences). The production encoder is 8-bit only and not affected; the test program was,
and all 12-bit figures before 2026-10-01 (12-bit lossless 8.4 MB and 0.3 s, smaller than 8-bit) were
wrong. Any future 12-bit save path needs this setting.
What this means for the design
- Format. Raw Bayer gives about 3× the frames of RGB8 over the same link on both cameras. Converting it to colour on the tower costs 5–16 ms and no white-balance work, and the colour encode afterwards costs the same as for the camera's RGB8. So "raw from the camera, colour on the tower" gains frame rate (faster auto-exposure) without costing save time. Saving the mosaic losslessly instead costs a tenth of the CPU and heat, but 2–3× the storage, and is limited by tower 111's storage speed.
- Saving. One save worker handles about one colour frame per 0.8 s; the production queue has no limit, so a faster scan would fall further behind for as long as it runs. Two workers × 2 threads add about 30%. Production's 3 encoder threads should be 4 (one worker) or 2 (two workers). Effort 3 is as fast as 4 with smaller files here; effort 5 is not usable at scan pace.
- Heat. At scan paces of a frame every 1–2 s both towers ran far from throttling (53–71 °C). Flat-out encoding for 10 minutes reached 75–79 °C with momentary peaks of 90 °C and a small slowdown on tower 121. Tower 111 runs 10–14 °C hotter than tower 121 at rest and at scan pace.
- Settling. A higher frame rate settles faster (about 1.6 s at 4.95 fps to 0.5 s at 26 fps for the same ×16 step). Exposure and gain holding still is not enough to know auto-exposure has finished; the kept frame's brightness (mean against the camera's target, clipped share) has to be part of the test, with exposure and gain at a limit marking when waiting cannot help. Not yet measured.
- Link limit. 450 MB/s was clean on both towers alone and nearly clean under load. If used, the capture path should count and skip incomplete frames.
- Profile. On tower 121,
MinimizeBlurraises gain to its 24 dB maximum before exposure goes past about 5 ms. With the gimbal standing still,MinimizeNoisemay give cleaner images. Not tested.
Scope and caveats
- Two towers, two cameras, two computers. Tower 121 (IOTA, 1800 U-1620c) and tower 111 (3 Delta, 1800 U-1240c). Other Delta towers have the same board as 111 but were not measured.
- The tower program was stopped throughout. Nothing was measured under its own load (camera stream, frame copies, control loop, MQTT), nor in auto mode.
- Binning was not tested beyond tower 121's maximum frame rate and a 10 s stream with its binned profile, by design (full resolution only).
- Poor scenes on both towers. Tower 121 indoors and out of focus; tower 111 in near-darkness. The synthetic frame stands in for a sharp outdoor view in the compression tests; auto-exposure in daylight, and on tower 111 at all, is not measured.
- A step on a still camera is not a slew. Settling after real slews needs the per-waypoint log in the scan program.
- 60 s streams, 10 min heat runs. Hours-long stability and summer ambient temperatures are not covered; the ambient temperature during the runs was not recorded.
- Not measured: BayerRG10p streaming;
IntensityControllerToleranceandTarget; colour quality of the tower's de-Bayer against the camera's; the four-plane raw storage option. - vmbpy 1.2.0 cannot stream on tower 121's Python 3.14 as installed (
NameError: name 'Camera' is not definedfrom its runtime type check); the test script bypasses the check in its own process. The production program (C++) is not affected. - Per-frame data (
ChunkEnable) can only be switched per item whileChunkModeActiveis off. Chunk data adds 48 bytes per frame.
Raw data and tools
The programs and how to rerun them: tools/camera-bench. Raw results, one JSON line per measurement (including per-frame auto-exposure series and per-second heat samples):
- results-2026-09-30-tower121/ — camera tests
and first CPU figures, with
scene.jpg. - results-2026-10-01-tower121/ — compression,
pile-up, heat;
matrix12_*.jsonlare the corrected 12-bit rows;scene.jpgandsynthetic.jpg. - results-2026-10-01-tower111/ — the same, and
tower 111's camera tests (
camera_*.jsonl).
python3 tools/camera-bench/summarize_encode.py <results folder> prints every compression, pile-up
and heat table in full.