Compare today's RGB8 file with the 12-bit file at the same quality setting in the 12-bit report

This commit is contained in:
pgdalmeida 2026-10-07 14:09:33 +02:00
parent a723646928
commit 1edca07293
Signed by: pedro.almeida
GPG Key ID: D4A6C394DF13F1D7
6 changed files with 122 additions and 7 deletions

View File

@ -470,5 +470,43 @@
"rms_avg": 6.01 "rms_avg": 6.01
} }
} }
],
"same_setting": [
{
"source": "camera RGB8",
"distance": 0.8,
"effort": 4,
"size_mb": 0.92,
"dark (< 300 DN)": {
"rms_px": 18.01,
"rms_avg": 2.18
},
"mid (300-1000 DN)": {
"rms_px": 21.4,
"rms_avg": 2.25
},
"bright (>= 1000 DN)": {
"rms_px": 34.3,
"rms_avg": 2.82
}
},
{
"source": "BayerRG12p",
"distance": 0.8,
"effort": 4,
"size_mb": 1.22,
"dark (< 300 DN)": {
"rms_px": 16.26,
"rms_avg": 1.22
},
"mid (300-1000 DN)": {
"rms_px": 23.57,
"rms_avg": 1.9
},
"bright (>= 1000 DN)": {
"rms_px": 42.42,
"rms_avg": 4.35
}
}
] ]
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

View File

@ -824,12 +824,55 @@ consequence, on this scene the 12-bit file at distance 1.0 is 46% larger than th
frames are in any case far smaller than the 2.65 MB of the daylight frame used for the storage frames are in any case far smaller than the 2.65 MB of the daylight frame used for the storage
estimate (Section 3.12). estimate (Section 3.12).
**Today's format against `BayerRG12p` at the same setting.** To answer directly whether a reader
would see a difference between today's files and 12-bit files, both frames were saved with the
same JPEG XL setting, today's distance 0.8 and effort 4. The two frames are separate captures, so
each file is compared with its own uncompressed original rather than with the other (Table 17e,
Figures 13g and 13h).
***Table 17e.** The camera's RGB8 frame and the `BayerRG12p` frame, both saved as JPEG XL at
distance 0.8, effort 4: file size (MB) and RMS error (DN, 12-bit scale; RGB8 values × 16) against
each file's own uncompressed frame, per brightness region as in Table 17d. Per pixel: the error at
each pixel. Local average: the error after averaging over about 200 pixels.*
| File (d0.8, effort 4) | Size (MB) | Per pixel, dark / mid / bright (DN) | Local average, dark / mid / bright (DN) |
|---|---|---|---|
| Camera RGB8 (today) | 0.92 | 18.0 / 21.4 / 34.3 | 2.2 / 2.3 / 2.8 |
| `BayerRG12p`, 12-bit file | 1.22 | 16.3 / 23.6 / 42.4 | 1.2 / 1.9 / 4.4 |
![Same setting, RGB8 and BayerRG12p](images/raw12/examples/staeffelsberg/same_setting.png)
***Figure 13g.** The dark crop of Figure 13d from the camera's RGB8 frame and from the `BayerRG12p`
frame, each uncompressed and saved at distance 0.8, effort 4 (the same setting for both). Rows: as
stored (×1, as a viewer shows the linear values), brightened by 2 stops (×4) and by 4 stops (×16).
The two frames are separate captures; branches moved between them.*
![Same setting, zoomed](images/raw12/examples/staeffelsberg/same_setting_zoom.png)
***Figure 13h.** The middle 200 × 200 pixels of the two saved crops of Figure 13g at +4 stops,
enlarged 2 times (each pixel shown as 2 × 2 screen pixels): camera RGB8 (left) and `BayerRG12p`
(right), both at distance 0.8, effort 4.*
As stored, the shaded forest is almost black and the two files cannot be told apart; at +2 stops the
RGB8 file is slightly flatter. At +4 stops the difference is clear: the RGB8 file breaks the forest
into flat blocks and loses the thin branches, while the 12-bit file stays close to its uncompressed
frame (Figure 13h). In the dark region the 12-bit file keeps the local brightness 1.8 times more
accurately (1.2 against 2.2 DN) and in the mid-tones 1.2 times; in the bright region the RGB8 file
is 1.5 times more accurate (2.8 against 4.4 DN), the same pattern as in Table 17d. At the same
setting the 12-bit file is 33% larger (1.22 against 0.92 MB); at distance 1.0 it is the same size as
today's file and still free of blocks (Figure 13f d). The difference therefore shows wherever the
shadows are brightened: a viewer applying a display curve, an operator raising the brightness, or a
detection step stretching the contrast of dark terrain.
In summary, for this dark scene: (1) the four extra bits themselves change little after saving: In summary, for this dark scene: (1) the four extra bits themselves change little after saving:
1.6 against 1.8 DN of local-brightness error in the dark at distance 1.0, and 0.6 DN when 8-bit 1.6 against 1.8 DN of local-brightness error in the dark at distance 1.0, and 0.6 DN when 8-bit
values are kept losslessly; (2) saving as a 12-bit file keeps about 2.3 times more accurate shadow values are kept losslessly; (2) saving as a 12-bit file keeps about 2.3 times more accurate shadow
brightness than saving as an 8-bit file at the same distance, and visibly more shadow detail than brightness than saving as an 8-bit file at the same distance, and visibly more shadow detail than
today's RGB8 file at the same size; (3) per pixel, rounding to 8 bits raises the noise in the dark today's RGB8 file at the same size; (3) per pixel, rounding to 8 bits raises the noise in the dark
by 10–26%, which matters only for lossless or near-lossless files. by 10–26%, which matters only for lossless or near-lossless files; (4) with the same JPEG XL setting
(distance 0.8, effort 4), today's RGB8 file and the 12-bit file look the same as stored, but at
+4 stops the RGB8 shadows break into blocks while the 12-bit file keeps the forest detail and 1.8
times more accurate shadow brightness, in a 33% larger file.
#### 3.9.3 Where 12 bits matter #### 3.9.3 Where 12 bits matter
@ -959,7 +1002,7 @@ The towers have 11–22 GB of free storage, so images must leave the tower conti
| Processing | Unpack + colour conversion 38 ms (IOTA) / 50 ms (Delta); a 12-bit save 518 / 820 ms, slightly faster than today's RGB8 save (567 / 872 ms). | 3.5 | | Processing | Unpack + colour conversion 38 ms (IOTA) / 50 ms (Delta); a 12-bit save 518 / 820 ms, slightly faster than today's RGB8 save (567 / 872 ms). | 3.5 |
| Computers | The Delta takes 1.6 times as long on the identical frame. | 3.5 | | Computers | The Delta takes 1.6 times as long on the identical frame. | 3.5 |
| Quality | d1.0: 2.65 MB, SSIM 0.973. Raw planes keep sensor values much closer (47 vs 39 dB at 6 MB). | 3.7 | | Quality | d1.0: 2.65 MB, SSIM 0.973. Raw planes keep sensor values much closer (47 vs 39 dB at 6 MB). | 3.7 |
| 12 bits | In daylight, rounding to 8 bits adds at most 1.5% to the noise, and compression at d1.0 adds 13 times more error than rounding. In a dark, high-contrast scene (Stäffelsberg) rounding adds 10–26% noise in the shadows; at d1.0 an 8-bit file loses shadow detail and 2.3 times more shadow brightness than a 12-bit file, mostly because of the file's declared bit depth, not the extra bits. Today's RGB8 file shows the same loss. | 3.9 | | 12 bits | In daylight, rounding to 8 bits adds at most 1.5% to the noise, and compression at d1.0 adds 13 times more error than rounding. In a dark, high-contrast scene (Stäffelsberg) rounding adds 10–26% noise in the shadows; at d1.0 an 8-bit file loses shadow detail and 2.3 times more shadow brightness than a 12-bit file, mostly because of the file's declared bit depth, not the extra bits. Today's RGB8 file shows the same loss: saved with the same setting (d0.8 e4) as a 12-bit file, it looks the same as stored but is blocky at +4 stops, with 1.8 times the shadow-brightness error, while the 12-bit file is 33% larger. | 3.9 |
| Throughput | IOTA keeps up with a frame every 0.7 s with one worker; the Delta needs two. | 3.10 | | Throughput | IOTA keeps up with a frame every 0.7 s with one worker; the Delta needs two. | 3.10 |
| Heat | One frame every 2 s: IOTA 53 °C / 5.1 W, Delta 66 °C / 5.7 W. | 3.11 | | Heat | One frame every 2 s: IOTA 53 °C / 5.1 W, Delta 66 °C / 5.7 W. | 3.11 |
| Storage | 12-bit colour d1.0: 2.7 MB per frame, below today's 3.3–4.0 MB. | 3.12 | | Storage | 12-bit colour d1.0: 2.7 MB per frame, below today's 3.3–4.0 MB. | 3.12 |

View File

@ -170,6 +170,11 @@ have 11–22 GB free, so images must leave the tower continuously.
0.94 MB: the effect is libjxl spending bits by declared bit depth, not the four extra bits. Sizes 0.94 MB: the effect is libjxl spending bits by declared bit depth, not the four extra bits. Sizes
on this scene at d1.0: 12-bit 0.92 MB, 8-bit 0.63 MB (+46%; equal in daylight). Implication: if on this scene at d1.0: 12-bit 0.92 MB, 8-bit 0.63 MB (+46%; equal in daylight). Implication: if
BayerRG8 were ever used, save its colour as a 12/16-bit JPEG XL. BayerRG8 were ever used, save its colour as a 12/16-bit JPEG XL.
- Same setting for both (user request, Table 17e, Figures 13g/13h): camera RGB8 and BayerRG12p
both at d0.8 e4, each scored against its own uncompressed frame (separate captures). As stored
(linear, near black) no visible difference; at +4 stops RGB8 is blocky, 12-bit keeps detail.
Local-average error dark/mid/bright: RGB8 2.2/2.3/2.8, 12-bit 1.2/1.9/4.4 DN; sizes 0.92 vs
1.22 MB (+33%).
- Not yet tested: real smoke; night at high gain; the tower's own de-Bayer on a dark frame. - Not yet tested: real smoke; night at high gain; the tower's own de-Bayer on a dark frame.
- 12-bit vs 8-bit raw costs little in saving (same size, ~10% more time) but the camera runs at - 12-bit vs 8-bit raw costs little in saving (same size, ~10% more time) but the camera runs at
9.86 vs 14.75 fps, ~1 s slower settling in daylight. The main gain over today is frame rate, not 9.86 vs 14.75 fps, ~1 s slower settling in daylight. The main gain over today is frame rate, not
@ -205,7 +210,7 @@ mosaic vs raw planes, Figures 1a/1b, Table 1; 1.3 earlier work); 2 Materials and
Tables 2–8, Figure 2); 3 Results (3.1 frame rate, 3.2 auto-exposure, 3.3 keep rules, 3.4 white Tables 2–8, Figure 2); 3 Results (3.1 frame rate, 3.2 auto-exposure, 3.3 keep rules, 3.4 white
balance, 3.5 processing per step, 3.6 effort and threads, 3.7 quality setting, 3.8 example images, balance, 3.5 processing per step, 3.6 effort and threads, 3.7 quality setting, 3.8 example images,
3.9 information content of 12-bit data (3.9.1 daylight, 3.9.2 Stäffelsberg dark scene, 3.9.3 3.9 information content of 12-bit data (3.9.1 daylight, 3.9.2 Stäffelsberg dark scene, 3.9.3
summary; Figures 12, 13a–13f, Tables 17a–17d), 3.10 throughput, 3.11 heat, summary; Figures 12, 13a–13h, Tables 17a–17e), 3.10 throughput, 3.11 heat,
3.12 storage; Figures 3–17, Tables 9–20); 4 Discussion (summary Table 21, pace Table 22, 3.12 storage; Figures 3–17, Tables 9–20); 4 Discussion (summary Table 21, pace Table 22,
limitations); 5 Conclusions and six recommendations; References [1]–[7]; Appendix A raw data and limitations); 5 Conclusions and six recommendations; References [1]–[7]; Appendix A raw data and
tools; Appendix B per-tower processing times. Figures live in `docs/images/raw12/`. tools; Appendix B per-tower processing times. Figures live in `docs/images/raw12/`.

View File

@ -17,6 +17,9 @@ Writes to <out_dir>:
dark_compressed.png dark crop after JPEG XL, +4 stops: today's camera RGB8 file (d0.8 e4), dark_compressed.png dark crop after JPEG XL, +4 stops: today's camera RGB8 file (d0.8 e4),
and at d1.0 the 8-bit file, the 8-bit values in a 12-bit file, 12-bit and at d1.0 the 8-bit file, the 8-bit values in a 12-bit file, 12-bit
mid_crop.png mid-tone crop, 8-bit / 12-bit, contrast x4 around its mean (control) mid_crop.png mid-tone crop, 8-bit / 12-bit, contrast x4 around its mean (control)
same_setting.png dark crop, camera RGB8 and 12-bit, uncompressed and both at d0.8 e4,
shown x1, +2 and +4 stops
same_setting_zoom.png the middle 200 x 200 pixels of the two d0.8 e4 crops at +4 stops, 2x
results.json noise, levels, sizes and errors per brightness region results.json noise, levels, sizes and errors per brightness region
""" """
import json import json
@ -158,10 +161,25 @@ for name, img, bps in SOURCES:
rms_avg=round(float(np.sqrt((e_av[m] ** 2).mean())), 2)) rms_avg=round(float(np.sqrt((e_av[m] ** 2).mean())), 2))
errors.append(row) errors.append(row)
del a, e_px, e_av del a, e_px, e_av
# Today's save: the camera's RGB8 at distance 0.8, effort 4. # Both formats with today's setting (distance 0.8, effort 4). The two frames are separate captures,
data = imagecodecs.jpegxl_encode(cam8, effort=4, distance=0.8, bitspersample=8) # so each is scored against its own uncompressed original.
size[("camera RGB8 today e4", 0.8)] = len(data) / 1e6 same_setting, same_crops = [], {}
dark_dec[("camera RGB8 today", 0.8)] = crop(to12(imagecodecs.jpegxl_decode(data)), dark_xy).copy() for name, img, bps in (("camera RGB8", cam8, 8), ("BayerRG12p", c12, 12)):
data = imagecodecs.jpegxl_encode(np.ascontiguousarray(img), effort=4, distance=0.8, bitspersample=bps)
own = to12(img)
a = to12(imagecodecs.jpegxl_decode(data))
same_crops[name] = (crop(own, dark_xy).copy(), crop(a, dark_xy).copy())
e_px = a - own
e_av = cv2.GaussianBlur(a, (0, 0), 4) - cv2.GaussianBlur(own, (0, 0), 4)
row = dict(source=name, distance=0.8, effort=4, size_mb=round(len(data) / 1e6, 2))
for rn, m in REGIONS.items():
row[rn] = dict(rms_px=round(float(np.sqrt((e_px[m] ** 2).mean())), 2),
rms_avg=round(float(np.sqrt((e_av[m] ** 2).mean())), 2))
same_setting.append(row)
print(json.dumps(row), flush=True)
del own, a, e_px, e_av
size[("camera RGB8 today e4", 0.8)] = same_setting[0]["size_mb"]
dark_dec[("camera RGB8 today", 0.8)] = same_crops["camera RGB8"][1]
# --- crops --------------------------------------------------------------------------------- # --- crops ---------------------------------------------------------------------------------
def upright(a): def upright(a):
@ -198,6 +216,16 @@ grid([[upright(view(dark_dec[k], 4)) for k in row] for row in keys], None, None,
os.path.join(out, "dark_compressed.png"), os.path.join(out, "dark_compressed.png"),
titles=[["(a) camera RGB8 file, d0.8 e4 (today)", "(b) 8-bit file, d1.0"], titles=[["(a) camera RGB8 file, d0.8 e4 (today)", "(b) 8-bit file, d1.0"],
["(c) 8-bit values in a 12-bit file, d1.0", "(d) 12-bit file, d1.0"]]) ["(c) 8-bit values in a 12-bit file, d1.0", "(d) 12-bit file, d1.0"]])
tiles = [same_crops["camera RGB8"][0], same_crops["camera RGB8"][1],
same_crops["BayerRG12p"][0], same_crops["BayerRG12p"][1]]
grid([[upright(view(a, s)) for a in tiles] for s in (0, 2, 4)],
["camera RGB8, uncompressed", "camera RGB8, d0.8 e4", "BayerRG12p, uncompressed", "BayerRG12p, d0.8 e4"],
["as stored (×1)", "+2 stops (×4)", "+4 stops (×16)"], os.path.join(out, "same_setting.png"))
z = slice(100, 300)
grid([[np.kron(upright(view(same_crops[n][1], 4))[z, z], np.ones((2, 2, 1), np.uint8))
for n in ("camera RGB8", "BayerRG12p")]],
["camera RGB8, d0.8 e4", "BayerRG12p, d0.8 e4"], ["+4 stops (×16), 2× zoom"],
os.path.join(out, "same_setting_zoom.png"))
mid_mean = crop(ref, mid_xy).mean(axis=(0, 1)) # per channel mid_mean = crop(ref, mid_xy).mean(axis=(0, 1)) # per channel
grid([[upright(np.clip((crop(a, mid_xy) - mid_mean) * 4 / 16 + 128, 0, 255).astype(np.uint8)) grid([[upright(np.clip((crop(a, mid_xy) - mid_mean) * 4 / 16 + 128, 0, 255).astype(np.uint8))
for a in (to12(c8), ref)]], for a in (to12(c8), ref)]],
@ -230,6 +258,7 @@ res = dict(
mean_green_12bit=round(float(crop(ref, mid_xy)[..., 1].mean()), 1)), mean_green_12bit=round(float(crop(ref, mid_xy)[..., 1].mean()), 1)),
sizes_mb={f"{n} d{d}": round(v, 2) for (n, d), v in size.items()}, sizes_mb={f"{n} d{d}": round(v, 2) for (n, d), v in size.items()},
errors=errors, errors=errors,
same_setting=same_setting,
) )
json.dump(res, open(os.path.join(out, "results.json"), "w"), indent=1) json.dump(res, open(os.path.join(out, "results.json"), "w"), indent=1)
print(json.dumps(res, indent=1)) print(json.dumps(res, indent=1))