diff --git a/docs/images/raw12/examples/staeffelsberg/results.json b/docs/images/raw12/examples/staeffelsberg/results.json index 2d8cf08..ca471d0 100644 --- a/docs/images/raw12/examples/staeffelsberg/results.json +++ b/docs/images/raw12/examples/staeffelsberg/results.json @@ -470,5 +470,43 @@ "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 + } + } ] } \ No newline at end of file diff --git a/docs/images/raw12/examples/staeffelsberg/same_setting.png b/docs/images/raw12/examples/staeffelsberg/same_setting.png new file mode 100644 index 0000000..3f2a1dd Binary files /dev/null and b/docs/images/raw12/examples/staeffelsberg/same_setting.png differ diff --git a/docs/images/raw12/examples/staeffelsberg/same_setting_zoom.png b/docs/images/raw12/examples/staeffelsberg/same_setting_zoom.png new file mode 100644 index 0000000..2fb2c52 Binary files /dev/null and b/docs/images/raw12/examples/staeffelsberg/same_setting_zoom.png differ diff --git a/docs/raw12-pipeline-measurements.md b/docs/raw12-pipeline-measurements.md index f1e2c69..29bc2e6 100644 --- a/docs/raw12-pipeline-measurements.md +++ b/docs/raw12-pipeline-measurements.md @@ -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 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: 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 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 -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 @@ -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 | | 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 | -| 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 | | 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 | diff --git a/docs/raw12-pipeline-measurements_llm_context.md b/docs/raw12-pipeline-measurements_llm_context.md index 4eec8b9..e3a39a3 100644 --- a/docs/raw12-pipeline-measurements_llm_context.md +++ b/docs/raw12-pipeline-measurements_llm_context.md @@ -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 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. +- 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. - 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 @@ -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 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 -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, 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/`. diff --git a/tools/camera-bench/compare_8_vs_12.py b/tools/camera-bench/compare_8_vs_12.py index e6b9b38..d59a477 100644 --- a/tools/camera-bench/compare_8_vs_12.py +++ b/tools/camera-bench/compare_8_vs_12.py @@ -17,6 +17,9 @@ Writes to : 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 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 """ import json @@ -158,10 +161,25 @@ for name, img, bps in SOURCES: rms_avg=round(float(np.sqrt((e_av[m] ** 2).mean())), 2)) errors.append(row) del a, e_px, e_av -# Today's save: the camera's RGB8 at distance 0.8, effort 4. -data = imagecodecs.jpegxl_encode(cam8, effort=4, distance=0.8, bitspersample=8) -size[("camera RGB8 today e4", 0.8)] = len(data) / 1e6 -dark_dec[("camera RGB8 today", 0.8)] = crop(to12(imagecodecs.jpegxl_decode(data)), dark_xy).copy() +# Both formats with today's setting (distance 0.8, effort 4). The two frames are separate captures, +# so each is scored against its own uncompressed original. +same_setting, same_crops = [], {} +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 --------------------------------------------------------------------------------- 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"), 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"]]) +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 grid([[upright(np.clip((crop(a, mid_xy) - mid_mean) * 4 / 16 + 128, 0, 255).astype(np.uint8)) for a in (to12(c8), ref)]], @@ -230,6 +258,7 @@ res = dict( 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()}, errors=errors, + same_setting=same_setting, ) json.dump(res, open(os.path.join(out, "results.json"), "w"), indent=1) print(json.dumps(res, indent=1))