Compare 8-bit and 12-bit pictures on a dark forest scene from the Stäffelsberg tower in the 12-bit report
This commit is contained in:
parent
19c5873df6
commit
a723646928
Binary file not shown.
|
After Width: | Height: | Size: 733 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 436 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
|
|
@ -0,0 +1,474 @@
|
||||||
|
{
|
||||||
|
"frame": {
|
||||||
|
"width": 5328,
|
||||||
|
"height": 3040,
|
||||||
|
"green_range_12bit": [
|
||||||
|
128,
|
||||||
|
2792
|
||||||
|
],
|
||||||
|
"share_of_frame": {
|
||||||
|
"dark (< 300 DN)": 33.2,
|
||||||
|
"mid (300-1000 DN)": 38.5,
|
||||||
|
"bright (>= 1000 DN)": 28.3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"noise_calibration": 1.089,
|
||||||
|
"noise": {
|
||||||
|
"12-bit (this frame)": [
|
||||||
|
[
|
||||||
|
156,
|
||||||
|
5.4
|
||||||
|
],
|
||||||
|
[
|
||||||
|
184,
|
||||||
|
6.2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
230,
|
||||||
|
7.9
|
||||||
|
],
|
||||||
|
[
|
||||||
|
274,
|
||||||
|
10.4
|
||||||
|
],
|
||||||
|
[
|
||||||
|
336,
|
||||||
|
14.8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
454,
|
||||||
|
11.2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
568,
|
||||||
|
12.4
|
||||||
|
],
|
||||||
|
[
|
||||||
|
738,
|
||||||
|
13.9
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1299,
|
||||||
|
16.2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1596,
|
||||||
|
18.3
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2259,
|
||||||
|
20.5
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2551,
|
||||||
|
21.6
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"8-bit, same frame (12-bit >> 4)": [
|
||||||
|
[
|
||||||
|
156,
|
||||||
|
6.4
|
||||||
|
],
|
||||||
|
[
|
||||||
|
184,
|
||||||
|
7.8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
230,
|
||||||
|
9.3
|
||||||
|
],
|
||||||
|
[
|
||||||
|
274,
|
||||||
|
11.4
|
||||||
|
],
|
||||||
|
[
|
||||||
|
336,
|
||||||
|
15.6
|
||||||
|
],
|
||||||
|
[
|
||||||
|
454,
|
||||||
|
12.3
|
||||||
|
],
|
||||||
|
[
|
||||||
|
568,
|
||||||
|
13.4
|
||||||
|
],
|
||||||
|
[
|
||||||
|
738,
|
||||||
|
14.8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1300,
|
||||||
|
16.9
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1597,
|
||||||
|
18.9
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2259,
|
||||||
|
21.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2551,
|
||||||
|
22.2
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"camera RGB8 (other capture)": [
|
||||||
|
[
|
||||||
|
155,
|
||||||
|
9.6
|
||||||
|
],
|
||||||
|
[
|
||||||
|
182,
|
||||||
|
10.5
|
||||||
|
],
|
||||||
|
[
|
||||||
|
228,
|
||||||
|
12.2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
272,
|
||||||
|
14.6
|
||||||
|
],
|
||||||
|
[
|
||||||
|
351,
|
||||||
|
15.1
|
||||||
|
],
|
||||||
|
[
|
||||||
|
428,
|
||||||
|
15.8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
602,
|
||||||
|
18.3
|
||||||
|
],
|
||||||
|
[
|
||||||
|
748,
|
||||||
|
23.1
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1272,
|
||||||
|
24.2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1681,
|
||||||
|
28.3
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2285,
|
||||||
|
32.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2442,
|
||||||
|
34.5
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"4 Oct frame, 0 dB": [
|
||||||
|
[
|
||||||
|
661,
|
||||||
|
17.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
893,
|
||||||
|
21.3
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1189,
|
||||||
|
27.8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1666,
|
||||||
|
31.6
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2177,
|
||||||
|
36.6
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2587,
|
||||||
|
37.3
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3024,
|
||||||
|
41.4
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"2 Oct frame, \u2248 15 dB": [
|
||||||
|
[
|
||||||
|
671,
|
||||||
|
32.9
|
||||||
|
],
|
||||||
|
[
|
||||||
|
860,
|
||||||
|
37.8
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1208,
|
||||||
|
46.5
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1603,
|
||||||
|
55.1
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2180,
|
||||||
|
66.0
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2601,
|
||||||
|
72.1
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3054,
|
||||||
|
77.1
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"dark_crop": {
|
||||||
|
"x": 40,
|
||||||
|
"y": 360,
|
||||||
|
"size": 400,
|
||||||
|
"mean_green_12bit": 194.1,
|
||||||
|
"green_range_12bit": [
|
||||||
|
129,
|
||||||
|
557
|
||||||
|
],
|
||||||
|
"distinct_green_levels": {
|
||||||
|
"12-bit": 272,
|
||||||
|
"8-bit same frame": 25,
|
||||||
|
"camera RGB8": 22
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mid_crop": {
|
||||||
|
"x": 3800,
|
||||||
|
"y": 1300,
|
||||||
|
"size": 400,
|
||||||
|
"mean_green_12bit": 1606.3
|
||||||
|
},
|
||||||
|
"sizes_mb": {
|
||||||
|
"12-bit d0": 38.58,
|
||||||
|
"12-bit d0.3": 3.4,
|
||||||
|
"12-bit d0.5": 2.13,
|
||||||
|
"12-bit d1.0": 0.92,
|
||||||
|
"8-bit d0": 15.53,
|
||||||
|
"8-bit d0.3": 2.51,
|
||||||
|
"8-bit d0.5": 1.54,
|
||||||
|
"8-bit d1.0": 0.63,
|
||||||
|
"8-bit values, 12-bit file d0": 39.04,
|
||||||
|
"8-bit values, 12-bit file d0.3": 3.5,
|
||||||
|
"8-bit values, 12-bit file d0.5": 2.19,
|
||||||
|
"8-bit values, 12-bit file d1.0": 0.94,
|
||||||
|
"camera RGB8 d0": 13.54,
|
||||||
|
"camera RGB8 d0.3": 2.64,
|
||||||
|
"camera RGB8 d0.5": 1.65,
|
||||||
|
"camera RGB8 d1.0": 0.66,
|
||||||
|
"camera RGB8 today e4 d0.8": 0.92
|
||||||
|
},
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"source": "12-bit",
|
||||||
|
"distance": 0,
|
||||||
|
"size_mb": 38.58,
|
||||||
|
"dark (< 300 DN)": {
|
||||||
|
"rms_px": 0.0,
|
||||||
|
"rms_avg": 0.0
|
||||||
|
},
|
||||||
|
"mid (300-1000 DN)": {
|
||||||
|
"rms_px": 0.0,
|
||||||
|
"rms_avg": 0.0
|
||||||
|
},
|
||||||
|
"bright (>= 1000 DN)": {
|
||||||
|
"rms_px": 0.0,
|
||||||
|
"rms_avg": 0.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "12-bit",
|
||||||
|
"distance": 0.3,
|
||||||
|
"size_mb": 3.4,
|
||||||
|
"dark (< 300 DN)": {
|
||||||
|
"rms_px": 12.28,
|
||||||
|
"rms_avg": 0.61
|
||||||
|
},
|
||||||
|
"mid (300-1000 DN)": {
|
||||||
|
"rms_px": 18.0,
|
||||||
|
"rms_avg": 0.94
|
||||||
|
},
|
||||||
|
"bright (>= 1000 DN)": {
|
||||||
|
"rms_px": 35.8,
|
||||||
|
"rms_avg": 2.53
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "12-bit",
|
||||||
|
"distance": 0.5,
|
||||||
|
"size_mb": 2.13,
|
||||||
|
"dark (< 300 DN)": {
|
||||||
|
"rms_px": 14.29,
|
||||||
|
"rms_avg": 0.92
|
||||||
|
},
|
||||||
|
"mid (300-1000 DN)": {
|
||||||
|
"rms_px": 20.81,
|
||||||
|
"rms_avg": 1.42
|
||||||
|
},
|
||||||
|
"bright (>= 1000 DN)": {
|
||||||
|
"rms_px": 39.92,
|
||||||
|
"rms_avg": 3.79
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "12-bit",
|
||||||
|
"distance": 1.0,
|
||||||
|
"size_mb": 0.92,
|
||||||
|
"dark (< 300 DN)": {
|
||||||
|
"rms_px": 16.77,
|
||||||
|
"rms_avg": 1.64
|
||||||
|
},
|
||||||
|
"mid (300-1000 DN)": {
|
||||||
|
"rms_px": 24.29,
|
||||||
|
"rms_avg": 2.56
|
||||||
|
},
|
||||||
|
"bright (>= 1000 DN)": {
|
||||||
|
"rms_px": 43.02,
|
||||||
|
"rms_avg": 5.98
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "8-bit",
|
||||||
|
"distance": 0,
|
||||||
|
"size_mb": 15.53,
|
||||||
|
"dark (< 300 DN)": {
|
||||||
|
"rms_px": 4.6,
|
||||||
|
"rms_avg": 0.64
|
||||||
|
},
|
||||||
|
"mid (300-1000 DN)": {
|
||||||
|
"rms_px": 4.61,
|
||||||
|
"rms_avg": 0.63
|
||||||
|
},
|
||||||
|
"bright (>= 1000 DN)": {
|
||||||
|
"rms_px": 4.61,
|
||||||
|
"rms_avg": 0.63
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "8-bit",
|
||||||
|
"distance": 0.3,
|
||||||
|
"size_mb": 2.51,
|
||||||
|
"dark (< 300 DN)": {
|
||||||
|
"rms_px": 17.35,
|
||||||
|
"rms_avg": 1.6
|
||||||
|
},
|
||||||
|
"mid (300-1000 DN)": {
|
||||||
|
"rms_px": 21.66,
|
||||||
|
"rms_avg": 1.53
|
||||||
|
},
|
||||||
|
"bright (>= 1000 DN)": {
|
||||||
|
"rms_px": 29.53,
|
||||||
|
"rms_avg": 1.59
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "8-bit",
|
||||||
|
"distance": 0.5,
|
||||||
|
"size_mb": 1.54,
|
||||||
|
"dark (< 300 DN)": {
|
||||||
|
"rms_px": 18.59,
|
||||||
|
"rms_avg": 2.18
|
||||||
|
},
|
||||||
|
"mid (300-1000 DN)": {
|
||||||
|
"rms_px": 23.9,
|
||||||
|
"rms_avg": 2.13
|
||||||
|
},
|
||||||
|
"bright (>= 1000 DN)": {
|
||||||
|
"rms_px": 34.86,
|
||||||
|
"rms_avg": 2.29
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "8-bit",
|
||||||
|
"distance": 1.0,
|
||||||
|
"size_mb": 0.63,
|
||||||
|
"dark (< 300 DN)": {
|
||||||
|
"rms_px": 20.45,
|
||||||
|
"rms_avg": 3.7
|
||||||
|
},
|
||||||
|
"mid (300-1000 DN)": {
|
||||||
|
"rms_px": 26.71,
|
||||||
|
"rms_avg": 3.44
|
||||||
|
},
|
||||||
|
"bright (>= 1000 DN)": {
|
||||||
|
"rms_px": 41.05,
|
||||||
|
"rms_avg": 3.99
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "8-bit values, 12-bit file",
|
||||||
|
"distance": 0,
|
||||||
|
"size_mb": 39.04,
|
||||||
|
"dark (< 300 DN)": {
|
||||||
|
"rms_px": 4.6,
|
||||||
|
"rms_avg": 0.64
|
||||||
|
},
|
||||||
|
"mid (300-1000 DN)": {
|
||||||
|
"rms_px": 4.61,
|
||||||
|
"rms_avg": 0.63
|
||||||
|
},
|
||||||
|
"bright (>= 1000 DN)": {
|
||||||
|
"rms_px": 4.61,
|
||||||
|
"rms_avg": 0.63
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "8-bit values, 12-bit file",
|
||||||
|
"distance": 0.3,
|
||||||
|
"size_mb": 3.5,
|
||||||
|
"dark (< 300 DN)": {
|
||||||
|
"rms_px": 12.72,
|
||||||
|
"rms_avg": 0.88
|
||||||
|
},
|
||||||
|
"mid (300-1000 DN)": {
|
||||||
|
"rms_px": 18.29,
|
||||||
|
"rms_avg": 1.14
|
||||||
|
},
|
||||||
|
"bright (>= 1000 DN)": {
|
||||||
|
"rms_px": 35.88,
|
||||||
|
"rms_avg": 2.61
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "8-bit values, 12-bit file",
|
||||||
|
"distance": 0.5,
|
||||||
|
"size_mb": 2.19,
|
||||||
|
"dark (< 300 DN)": {
|
||||||
|
"rms_px": 14.55,
|
||||||
|
"rms_avg": 1.12
|
||||||
|
},
|
||||||
|
"mid (300-1000 DN)": {
|
||||||
|
"rms_px": 20.99,
|
||||||
|
"rms_avg": 1.57
|
||||||
|
},
|
||||||
|
"bright (>= 1000 DN)": {
|
||||||
|
"rms_px": 39.96,
|
||||||
|
"rms_avg": 3.83
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"source": "8-bit values, 12-bit file",
|
||||||
|
"distance": 1.0,
|
||||||
|
"size_mb": 0.94,
|
||||||
|
"dark (< 300 DN)": {
|
||||||
|
"rms_px": 16.87,
|
||||||
|
"rms_avg": 1.76
|
||||||
|
},
|
||||||
|
"mid (300-1000 DN)": {
|
||||||
|
"rms_px": 24.36,
|
||||||
|
"rms_avg": 2.62
|
||||||
|
},
|
||||||
|
"bright (>= 1000 DN)": {
|
||||||
|
"rms_px": 43.03,
|
||||||
|
"rms_avg": 6.01
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -25,12 +25,16 @@ tests; waiting for exposure and gain to stop changing never did.
|
||||||
The extra bits themselves add less than expected. In the daylight scene measured, the sensor's own
|
The extra bits themselves add less than expected. In the daylight scene measured, the sensor's own
|
||||||
noise was 6–17 times larger than the error from rounding to 8 bits, which raised the total noise by
|
noise was 6–17 times larger than the error from rounding to 8 bits, which raised the total noise by
|
||||||
at most 1.5%; and JPEG XL compression at distance 1.0 adds about 13 times more error than that
|
at most 1.5%; and JPEG XL compression at distance 1.0 adds about 13 times more error than that
|
||||||
rounding. 12 bits are expected to make a meaningful difference only where part of the image is dark
|
rounding. A dark, high-contrast scene from the Stäffelsberg tower (shaded forest under a hazy sky)
|
||||||
while the gain is low, as in high-contrast scenes (a bright sky over shaded forest or a backlit ridge
|
was measured as well: there the noise in the dark third of the frame is below one 8-bit step, and
|
||||||
at dawn or dusk), where faint smoke over dark terrain is a small change in a dark value; and only if
|
rounding to 8 bits raises it by 10–26%, but averaged over an area the 8-bit values still keep the
|
||||||
those values are saved with little or no loss. These conditions were not measured. The 12-bit format
|
local brightness to 0.6 DN (12-bit scale). The larger difference comes from the encoder: saved at
|
||||||
costs little against 8-bit raw (same file size, about 10% more processing time), but the camera
|
distance 1.0, an 8-bit file breaks the shaded forest into flat blocks and keeps its local brightness
|
||||||
runs at 9.86 instead of 14.75 fps.
|
2.3 times less accurately than a 12-bit file, and today's RGB8 file shows the same blocks at the same
|
||||||
|
size as the 12-bit file. The same 8-bit values saved as a 12-bit file behave almost like 12-bit data,
|
||||||
|
so the gain comes mostly from the file's declared bit depth, not from the four extra bits. The
|
||||||
|
12-bit format costs little against 8-bit raw (about 10% more processing time; the same file size in
|
||||||
|
daylight, 46% larger in the dark scene), but the camera runs at 9.86 instead of 14.75 fps.
|
||||||
|
|
||||||
## Units and abbreviations
|
## Units and abbreviations
|
||||||
|
|
||||||
|
|
@ -340,6 +344,7 @@ session.*
|
||||||
| 4 Oct 16:20–16:41 | 111 | camera tests | 21 | tower 111's own camera connected but idle |
|
| 4 Oct 16:20–16:41 | 111 | camera tests | 21 | tower 111's own camera connected but idle |
|
||||||
| 4 Oct 16:41–18:19 | 111 | processing, pile-up, heat | 98 | |
|
| 4 Oct 16:41–18:19 | 111 | processing, pile-up, heat | 98 | |
|
||||||
| 4 Oct 23:03–23:32 | 121 and 111 | processing on the shared frame, both towers simultaneously | 29 | no camera needed |
|
| 4 Oct 23:03–23:32 | 121 and 111 | processing on the shared frame, both towers simultaneously | 29 | no camera needed |
|
||||||
|
| 7 Oct, morning | Stäffelsberg | one `BayerRG12p` and one RGB8 frame of a dark, high-contrast view (Section 3.9.2) | – | hazy; gain and exposure not recorded |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -617,6 +622,13 @@ Full-size `.jxl` files are kept outside the repository in `tools/camera-bench/ex
|
||||||
|
|
||||||
### 3.9 Information content of the 12-bit data
|
### 3.9 Information content of the 12-bit data
|
||||||
|
|
||||||
|
The extra four bits were examined in two scenes: the daylight scene of the main campaign (2 and
|
||||||
|
4 October, Section 3.9.1), where noise is large, and a dark, high-contrast scene photographed from
|
||||||
|
the Stäffelsberg tower on 7 October (Section 3.9.2), where large parts of the frame are dark at low
|
||||||
|
noise. Section 3.9.3 combines the two.
|
||||||
|
|
||||||
|
#### 3.9.1 Daylight scene (2 and 4 October)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
***Figure 12.** Sensor noise σ (DN, 12-bit scale) against signal level (DN, 12-bit scale), measured
|
***Figure 12.** Sensor noise σ (DN, 12-bit scale) against signal level (DN, 12-bit scale), measured
|
||||||
|
|
@ -656,45 +668,195 @@ and the dropped bits hold real scene detail.
|
||||||
has a PSNR of 36.6 dB (Table 16), an RMS error of about 61 DN, which is 13 times the rounding error;
|
has a PSNR of 36.6 dB (Table 16), an RMS error of about 61 DN, which is 13 times the rounding error;
|
||||||
even at distance 0.3 (40.3 dB) it is about 40 DN, and raw planes at distance 0.5 (47.2 dB) about
|
even at distance 0.3 (40.3 dB) it is about 40 DN, and raw planes at distance 0.5 (47.2 dB) about
|
||||||
18 DN. Much of this error is noise the encoder discards, and JPEG XL spends relatively more
|
18 DN. Much of this error is noise the encoder discards, and JPEG XL spends relatively more
|
||||||
precision on dark tones, so the error in shadows is smaller than these averages; it was not measured
|
precision on dark tones, so the error in shadows is smaller than these averages (measured on the
|
||||||
separately. Still, with lossy files the compression, not the bit depth, decides how much fine tonal
|
dark scene in Table 17d). Still, with lossy files the compression, not the bit depth, decides how
|
||||||
detail survives. Correspondingly, 12-bit and 8-bit raw give the same file size at distance 1.0
|
much fine tonal detail survives. Correspondingly, 12-bit and 8-bit raw give the same file size at
|
||||||
(2.65 and 2.66 MB, Table 14).
|
distance 1.0 on this scene (2.65 and 2.66 MB, Table 14).
|
||||||
|
|
||||||
**Where 12 bits are expected to matter.** The noise falls with the signal, while the 8-bit step
|
|
||||||
stays at 16 DN. Extrapolating Figure 12, the noise drops below one 8-bit step at about 300 DN at
|
|
||||||
0 dB gain, or 100 DN at ≈ 15 dB. The 8-bit data is linear (no gamma curve), so this region, the
|
|
||||||
darkest 2–7% of the range, holds only 6–19 of the 256 8-bit levels. Gain multiplies the noise, so at
|
|
||||||
high gain the noise exceeds one 8-bit step again almost everywhere. Table 17b combines these
|
|
||||||
observations.
|
|
||||||
|
|
||||||
***Table 17b.** Situations in which 12-bit data is expected to differ from 8-bit data. Signal in DN
|
|
||||||
on the 12-bit scale (0–4095). "Measured" refers to this campaign.*
|
|
||||||
|
|
||||||
| Situation | Signal (DN), gain (dB) | Noise against one 8-bit step | Expected effect of 12 bits | Measured |
|
|
||||||
|---|---|---|---|---|
|
|
||||||
| Daylight, mid-tones and highlights (lit terrain, sky) | 700–4095, 0–15 | 1.6–5 steps | none measurable: total noise +0.2–1.5% | yes |
|
|
||||||
| Shadows in a high-contrast scene at low gain: shaded forest under a bright sky, backlit ridges, valleys at dawn and dusk | below ≈ 300 at 0 dB, ≈ 100 at 15 dB | below 1 step | 8 bits merge neighbouring levels; 12 bits keep small changes in dark areas, such as thin smoke over dark terrain | no (extrapolated) |
|
|
||||||
| Uniformly dark scene at high gain: night, gain up to 48 dB | any, high (up to 48) | above 1 step (gain amplifies noise) | little, except in the very darkest values | no |
|
|
||||||
| Strong processing of dark areas: brightening by 3 stops (×8) or more, contrast stretching | dark areas, any | – | 12 bits delay visible banding | 2 stops (×4) only: no difference (Figure 13) |
|
|
||||||
| Saving lossy at distance 1.0 | any | compression error ≈ 61 DN ≫ rounding 4.6 DN | largely lost to compression; kept in lossless files | yes, overall PSNR only |
|
|
||||||
|
|
||||||
The case most relevant to fire detection is the second: in a scene exposed for a bright sky, smoke
|
|
||||||
rising in front of shaded forest is a small brightness increase in an already dark area, at low gain.
|
|
||||||
Whether 12 bits help there, and whether that benefit survives distance 1.0, requires a test on such a
|
|
||||||
scene (a dawn or dusk frame with dark terrain under a bright sky, compared as in Figures 12 and 13).
|
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
***Figure 13.** The same darker crop brightened by 2 stops (pixel values multiplied by 4), saved as
|
***Figure 13a.** The darkest textured crop of the daylight scene (mean ≈ 810 DN) brightened by
|
||||||
8-bit (left; the 12-bit value shifted down by 4 bits, i.e. divided by 16) and as 12-bit (right): (a,
|
2 stops (pixel values multiplied by 4), saved as 8-bit (left; the 12-bit value shifted down by
|
||||||
top) ≈ 15 dB gain, 227 distinct green levels in 8-bit against 3138 in 12-bit; (b, bottom) 0 dB gain.
|
4 bits, i.e. divided by 16) and as 12-bit (right): top, ≈ 15 dB gain, 227 distinct green levels in
|
||||||
No visible difference: the noise is larger than the extra levels.*
|
8-bit against 3138 in 12-bit; bottom, 0 dB gain. No visible difference: the noise is larger than the
|
||||||
|
extra levels.*
|
||||||
|
|
||||||
At 0 dB gain the same settings produce 25–30% smaller files and about 3 dB higher PSNR (less noise
|
At 0 dB gain the same settings produce 25–30% smaller files and about 3 dB higher PSNR (less noise
|
||||||
to encode): 12-bit colour at d1.0 is 1.9 MB, PSNR 40.0 dB, SSIM 0.987.
|
to encode): 12-bit colour at d1.0 is 1.9 MB, PSNR 40.0 dB, SSIM 0.987.
|
||||||
|
|
||||||
|
#### 3.9.2 Dark, high-contrast scene (Stäffelsberg, 7 October)
|
||||||
|
|
||||||
|
**Scene and method.** On the morning of 7 October 2026, an Alvium 1800 U-1620c (5328 × 3040) on the
|
||||||
|
Stäffelsberg tower took two frames of the same view, one after the other: hazy sky and a distant valley above a
|
||||||
|
slope of shaded forest (Figure 13b). One was taken as `BayerRG12p`, converted to colour by the camera's
|
||||||
|
viewer program and saved as a 16-bit TIFF (the 12-bit values in the top 12 bits); the other as the camera's
|
||||||
|
RGB8 (today's format), saved as PNG. Gain and exposure were not recorded with the files. The 12-bit
|
||||||
|
frame spans 128–2792 DN (green), with nothing clipped: a third of it is darker than 300 DN, the
|
||||||
|
region where Section 3.9.1 expected 12 bits to matter.
|
||||||
|
|
||||||
|
The camera's RGB8 frame is a separate capture (trees move between frames) made by the camera's own
|
||||||
|
colour processing, so it is compared by eye and by statistics only. For an exact comparison of bit
|
||||||
|
depth, an **8-bit version of the same frame** was made by keeping the top 8 of the 12 bits (the
|
||||||
|
12-bit value divided by 16, rounded down, as `BayerRG8` delivers), and compared pixel by pixel with
|
||||||
|
the 12-bit original. The 12-bit frame was converted to colour by the viewer, not by the tower's
|
||||||
|
edge-aware conversion, so its noise is not directly comparable with Figure 12 (measured on the raw
|
||||||
|
green pixels); for comparison, the October frames' lossless 12-bit colour files were measured in
|
||||||
|
the same way. Compression used libjxl 0.12.0 on the laptop with the tower's settings (effort 3;
|
||||||
|
12-bit and 8-bit input as declared by their bit depth), single runs. Script:
|
||||||
|
`tools/camera-bench/compare_8_vs_12.py`; results in
|
||||||
|
`docs/images/raw12/examples/staeffelsberg/results.json`.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
***Figure 13b.** The Stäffelsberg 12-bit frame, shown upright (the frame is rotated by 90° so that
|
||||||
|
the sky is at the top), at a quarter of full size and with a display brightness curve (sRGB) so that
|
||||||
|
the forest is visible. Blue: the dark crop of Figures 13d and 13f (shaded forest, mean 194 DN).
|
||||||
|
Red: the mid-tone crop of Figure 13e (distant valley in haze, mean 1606 DN).*
|
||||||
|
|
||||||
|
**Noise.** The noise in each colour image was measured as the standard deviation of each pixel
|
||||||
|
around its local average (a Gaussian average over about 2 pixels), in 24 × 24-pixel patches grouped
|
||||||
|
by brightness, taking the quietest 10% of each group as in Section 2.11; the result was calibrated
|
||||||
|
on generated noise of known σ.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
***Figure 13c.** Noise σ (DN, 12-bit scale) against signal (DN, 12-bit scale, green) in the colour
|
||||||
|
images: the Stäffelsberg 12-bit frame (blue), its 8-bit version (orange), the camera's RGB8 frame of
|
||||||
|
the same view (purple, another capture, multiplied by 16 to the 12-bit scale), and the October 0 dB
|
||||||
|
and ≈ 15 dB frames measured the same way (dashed). Dashed line: one 8-bit step (16 DN); dotted line:
|
||||||
|
the RMS error of rounding to 8 bits (4.6 DN). The bump at ≈ 340 DN is scene texture that remains
|
||||||
|
even in the quietest patches.*
|
||||||
|
|
||||||
|
***Table 17c.** Noise σ (DN, 12-bit scale) in the Stäffelsberg colour images at selected signal
|
||||||
|
levels (DN, 12-bit scale, green). In 8-bit steps: the 12-bit noise divided by 16; below 1, an 8-bit
|
||||||
|
value cannot record the variation the sensor delivers. Increase: how much rounding to 8 bits raises
|
||||||
|
the noise. Camera RGB8: another capture, the camera's own colour processing.*
|
||||||
|
|
||||||
|
| Signal (DN) | 12-bit noise (DN) | In 8-bit steps | 8-bit version, same frame (DN) | Increase from rounding | Camera RGB8 (DN) |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| ≈ 160 | 5.4 | 0.34 | 6.4 | +19% | 9.6 |
|
||||||
|
| ≈ 185 | 6.2 | 0.39 | 7.8 | +26% | 10.5 |
|
||||||
|
| ≈ 230 | 7.9 | 0.49 | 9.3 | +18% | 12.2 |
|
||||||
|
| ≈ 275 | 10.4 | 0.65 | 11.4 | +10% | 14.6 |
|
||||||
|
| ≈ 450 | 11.2 | 0.70 | 12.3 | +10% | 15.8 |
|
||||||
|
| ≈ 740 | 13.9 | 0.87 | 14.8 | +6% | 23.1 |
|
||||||
|
| ≈ 1300 | 16.2 | 1.01 | 16.9 | +4% | 24.2 |
|
||||||
|
| ≈ 2250 | 20.5 | 1.28 | 21.0 | +2% | 32.0 |
|
||||||
|
|
||||||
|
In this frame the noise stays below one 8-bit step up to about 1300 DN, which covers about 70% of
|
||||||
|
the frame. At ≈ 700 DN the October frames, measured the same way, have 17 DN (0 dB) and 33 DN
|
||||||
|
(≈ 15 dB) against 14 DN here, and they contain nothing darker. Rounding to 8 bits raises the noise
|
||||||
|
in the forest by 10–26%, against at most 1.5% in the daylight scene. The camera's RGB8 frame is
|
||||||
|
1.4–1.8 times noisier than the 12-bit frame at every brightness, probably from the camera's own
|
||||||
|
colour processing; this is a difference of the camera's conversion, not of bit depth.
|
||||||
|
|
||||||
|
**Levels and visibility.** The 400 × 400-pixel dark crop (green 129–557 DN) uses 272 distinct green
|
||||||
|
levels in 12-bit, 25 in its 8-bit version and 22 in the camera's RGB8.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
***Figure 13d.** The dark crop (shaded forest, mean 194 DN) at 100%, brightened by 2 stops (pixel
|
||||||
|
values ×4, top) and 4 stops (×16, bottom). Left: the camera's RGB8 frame (another capture, taken
|
||||||
|
separately; branches moved). Middle: the 8-bit version of the 12-bit frame (top 8 bits). Right:
|
||||||
|
the 12-bit frame. Not compressed.*
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
***Figure 13e.** Control: the mid-tone crop (distant valley in haze, mean 1606 DN, noise about one
|
||||||
|
8-bit step) with its contrast stretched 4 times around the crop's mean, 8-bit version (left) and
|
||||||
|
12-bit (right). Not compressed. No visible difference.*
|
||||||
|
|
||||||
|
Uncompressed, the 8-bit forest is slightly grainier than the 12-bit one at +4 stops, with no banding
|
||||||
|
(Figure 13d): even at 0.3–0.5 of a step the noise still dithers the rounding. Averaged over about
|
||||||
|
200 pixels (a Gaussian of 4 pixels), the 8-bit version differs from the 12-bit frame by 0.6 DN RMS
|
||||||
|
in every brightness range (Table 17d): the local brightness, which is what a faint, even haze such as
|
||||||
|
thin smoke changes, survives rounding to within less than a tenth of an 8-bit step.
|
||||||
|
|
||||||
|
**Compression.** Each version was saved as JPEG XL and compared with the 12-bit frame in three
|
||||||
|
brightness regions: dark (below 300 DN, 33% of the frame), mid (300–1000 DN, 39%) and bright
|
||||||
|
(1000 DN and above, 28%). Two errors are given: per pixel, which includes noise the encoder
|
||||||
|
discards; and of the local average, after averaging both images over about 200 pixels, which
|
||||||
|
measures lost scene brightness rather than lost noise. A third version tests the encoder rather than
|
||||||
|
the data: the same 8-bit values, stored as a 12-bit file (each value × 16 + 8).
|
||||||
|
|
||||||
|
***Table 17d.** File size (MB) and RMS error (DN, 12-bit scale) against the 12-bit frame, per
|
||||||
|
brightness region, for the 12-bit frame, its 8-bit version, and its 8-bit values stored in a 12-bit
|
||||||
|
file, at JPEG XL distance 0 (lossless), 0.3, 0.5 and 1.0, effort 3. Per pixel: the error at each
|
||||||
|
pixel. Local average: the error after averaging both images over about 200 pixels (Gaussian,
|
||||||
|
σ = 4 pixels). Today's file (camera RGB8, d0.8, effort 4) is 0.92 MB; the camera RGB8 frame at d1.0
|
||||||
|
is 0.66 MB (another capture, so no error can be computed).*
|
||||||
|
|
||||||
|
| File | Distance | Size (MB) | Per pixel, dark / mid / bright (DN) | Local average, dark / mid / bright (DN) |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 12-bit | lossless | 38.6 | 0 / 0 / 0 | 0 / 0 / 0 |
|
||||||
|
| 12-bit | 0.3 | 3.40 | 12.3 / 18.0 / 35.8 | 0.6 / 0.9 / 2.5 |
|
||||||
|
| 12-bit | 0.5 | 2.13 | 14.3 / 20.8 / 39.9 | 0.9 / 1.4 / 3.8 |
|
||||||
|
| **12-bit** | **1.0** | **0.92** | **16.8 / 24.3 / 43.0** | **1.6 / 2.6 / 6.0** |
|
||||||
|
| 8-bit | lossless | 15.5 | 4.6 / 4.6 / 4.6 | 0.6 / 0.6 / 0.6 |
|
||||||
|
| 8-bit | 0.3 | 2.51 | 17.4 / 21.7 / 29.5 | 1.6 / 1.5 / 1.6 |
|
||||||
|
| 8-bit | 0.5 | 1.54 | 18.6 / 23.9 / 34.9 | 2.2 / 2.1 / 2.3 |
|
||||||
|
| **8-bit** | **1.0** | **0.63** | **20.4 / 26.7 / 41.0** | **3.7 / 3.4 / 4.0** |
|
||||||
|
| 8-bit values, 12-bit file | lossless | 39.0 | 4.6 / 4.6 / 4.6 | 0.6 / 0.6 / 0.6 |
|
||||||
|
| 8-bit values, 12-bit file | 0.3 | 3.50 | 12.7 / 18.3 / 35.9 | 0.9 / 1.1 / 2.6 |
|
||||||
|
| 8-bit values, 12-bit file | 0.5 | 2.19 | 14.6 / 21.0 / 40.0 | 1.1 / 1.6 / 3.8 |
|
||||||
|
| **8-bit values, 12-bit file** | **1.0** | **0.94** | **16.9 / 24.4 / 43.0** | **1.8 / 2.6 / 6.0** |
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
***Figure 13f.** The dark crop of Figure 13d after saving, brightened by 4 stops (×16): (a) the
|
||||||
|
camera's RGB8 frame saved as today (distance 0.8, effort 4; another capture); (b) the 8-bit version
|
||||||
|
saved as an 8-bit file at distance 1.0; (c) the same 8-bit values saved as a 12-bit file at distance
|
||||||
|
1.0; (d) the 12-bit frame at distance 1.0 (the proposed setting). Effort 3 unless stated.*
|
||||||
|
|
||||||
|
At distance 1.0, the 8-bit file loses the shadow detail: the forest breaks into flat blocks
|
||||||
|
(Figure 13f b), and the local brightness in the dark region is off by 3.7 DN against 1.6 DN for the
|
||||||
|
12-bit file. Today's RGB8 file shows the same blocks (Figure 13f a), although it is as large as the
|
||||||
|
12-bit file (0.92 MB). In the bright region the order reverses (4.0 against 6.0 DN): an 8-bit file
|
||||||
|
gets about the same error at every brightness, a 12-bit file a smaller error in the dark and a larger
|
||||||
|
one in the bright.
|
||||||
|
|
||||||
|
This is a property of the encoder, not of the four extra bits: the same 8-bit values saved as a
|
||||||
|
12-bit file behave almost exactly like the 12-bit frame (1.8 against 1.6 DN in the dark, 0.94 against
|
||||||
|
0.92 MB, Figure 13f c). How the bits are spent depends on the bit depth the file declares. As a
|
||||||
|
consequence, on this scene the 12-bit file at distance 1.0 is 46% larger than the 8-bit file
|
||||||
|
(0.92 against 0.63 MB), whereas in the daylight scene the two were the same size (Table 14); dark
|
||||||
|
frames are in any case far smaller than the 2.65 MB of the daylight frame used for the storage
|
||||||
|
estimate (Section 3.12).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
#### 3.9.3 Where 12 bits matter
|
||||||
|
|
||||||
|
The noise falls with the signal, while the 8-bit step stays at 16 DN. In the daylight scene the
|
||||||
|
noise was above one 8-bit step everywhere (Figure 12); in the Stäffelsberg scene it was below one
|
||||||
|
step up to about 1300 DN, over 70% of the frame (Figure 13c). Gain multiplies the noise, so at high
|
||||||
|
gain the noise exceeds one 8-bit step again almost everywhere. Table 17b combines these
|
||||||
|
observations.
|
||||||
|
|
||||||
|
***Table 17b.** Situations in which 12-bit data differs from 8-bit data. Signal in DN on the 12-bit
|
||||||
|
scale (0–4095). "Measured" refers to this campaign.*
|
||||||
|
|
||||||
|
| Situation | Signal (DN), gain (dB) | Noise against one 8-bit step | Effect of 12 bits | Measured |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Daylight, mid-tones and highlights (lit terrain, sky) | 700–4095, 0–15 | 1.6–5 steps | none measurable: total noise +0.2–1.5% | yes (3.9.1) |
|
||||||
|
| Shadows in a high-contrast scene at low gain: shaded forest under a bright sky, backlit ridges, valleys at dawn and dusk | below ≈ 1300 in the Stäffelsberg frame | 0.3–1 step | 25 instead of 272 levels in the dark crop, +10–26% noise per pixel; local brightness kept by 8-bit values to 0.6 DN; no visible banding at +4 stops | yes (3.9.2) |
|
||||||
|
| Uniformly dark scene at high gain: night, gain up to 48 dB | any, high (up to 48) | above 1 step (gain amplifies noise) | little, except in the very darkest values | no |
|
||||||
|
| Strong processing of dark areas: brightening by 3 stops (×8) or more, contrast stretching | dark areas, any | – | 12 bits delay visible banding | +2 stops in daylight, +4 stops at Stäffelsberg: no banding, 8-bit slightly grainier (Figures 13a, 13d) |
|
||||||
|
| Saving lossy at distance 1.0 | any | compression error ≈ 17–61 DN per pixel ≫ rounding 4.6 DN | an 8-bit file loses shadow detail (blocks) and 2.3 times more local brightness in the dark than a 12-bit file; caused by the declared file bit depth, not by the missing bits | yes (3.9.2, Table 17d) |
|
||||||
|
|
||||||
|
The case most relevant to fire detection is the second: smoke rising in front of shaded forest is a
|
||||||
|
small brightness increase in an already dark area, at low gain. The Stäffelsberg frame is such a
|
||||||
|
scene. There, the proposed 12-bit file at distance 1.0 keeps the local brightness of the shaded
|
||||||
|
forest to 1.6 DN RMS and its texture visibly better than today's RGB8 file of the same size; most of
|
||||||
|
that gain comes from saving a 12-bit file, and would also be available to 8-bit data saved as a
|
||||||
|
12-bit file. Whether these differences change smoke detection was not measured.
|
||||||
|
|
||||||
### 3.10 Saving throughput and pile-up
|
### 3.10 Saving throughput and pile-up
|
||||||
|
|
||||||

|

|
||||||
|
|
@ -797,7 +959,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. 12 bits are expected to matter only in dark areas at low gain (high-contrast scenes), and only in lightly compressed files; not measured. | 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. | 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 |
|
||||||
|
|
@ -831,6 +993,10 @@ between neighbouring waypoints, which decides the real pace, remains to be measu
|
||||||
|
|
||||||
- One scene on two afternoons, at ≈ 15 dB gain in the main runs, plus one 0 dB frame. Night
|
- One scene on two afternoons, at ≈ 15 dB gain in the main runs, plus one 0 dB frame. Night
|
||||||
conditions were not measured.
|
conditions were not measured.
|
||||||
|
- The dark-scene comparison (Section 3.9.2) rests on one pair of frames, without recorded gain and
|
||||||
|
exposure; its 12-bit frame was converted to colour by the camera's viewer, not by the tower, and
|
||||||
|
compressed on a laptop (libjxl 0.12.0) rather than on a tower (0.11). Its 8-bit version is the
|
||||||
|
12-bit colour image cut to 8 bits, not a `BayerRG8` frame converted to colour.
|
||||||
- Auto-exposure steps were forced on a still camera, not caused by real slews. Real waypoint-to-
|
- Auto-exposure steps were forced on a still camera, not caused by real slews. Real waypoint-to-
|
||||||
waypoint changes are usually much smaller than ×16, so real settling should be shorter.
|
waypoint changes are usually much smaller than ×16, so real settling should be shorter.
|
||||||
- The tower program was stopped, so its own load (camera stream, control loop) is not included; the
|
- The tower program was stopped, so its own load (camera stream, control loop) is not included; the
|
||||||
|
|
@ -854,9 +1020,11 @@ between neighbouring waypoints, which decides the real pace, remains to be measu
|
||||||
4. **Keep a waypoint's frame when brightness is in the camera's target band and steady for 3
|
4. **Keep a waypoint's frame when brightness is in the camera's target band and steady for 3
|
||||||
frames**, with a time limit.
|
frames**, with a time limit.
|
||||||
5. **12-bit versus raw 8-bit is a choice, not a cost.** `BayerRG8` (14.75 fps) settles about 1 s
|
5. **12-bit versus raw 8-bit is a choice, not a cost.** `BayerRG8` (14.75 fps) settles about 1 s
|
||||||
faster in daylight and loses nothing measurable there; `BayerRG12p` is expected to help in dark areas
|
faster in daylight and loses nothing measurable there. In the dark areas of a high-contrast scene
|
||||||
of high-contrast scenes at low gain, such as shaded terrain under a bright sky at dawn or dusk
|
(Section 3.9.2), a 12-bit file at distance 1.0 keeps clearly more shadow detail than an 8-bit
|
||||||
(Section 3.9), which remains to be tested. The pixel format is part of the camera profile, so the program should accept both.
|
file, but mainly because of how the file is encoded: if `BayerRG8` is used, its colour image
|
||||||
|
should be saved as a 12-bit (or 16-bit) JPEG XL file, which keeps nearly the same shadow detail.
|
||||||
|
The pixel format is part of the camera profile, so the program should accept both.
|
||||||
6. **Store raw 12-bit planes losslessly (17–18 MB per frame) only if the detection pipeline needs
|
6. **Store raw 12-bit planes losslessly (17–18 MB per frame) only if the detection pipeline needs
|
||||||
exact sensor values**; it requires about 6 times the storage.
|
exact sensor values**; it requires about 6 times the storage.
|
||||||
|
|
||||||
|
|
@ -867,7 +1035,8 @@ between neighbouring waypoints, which decides the real pace, remains to be measu
|
||||||
1. ISO 19567-2:2019. *Photography — Digital cameras — Texture reproduction measurements — Part 2:
|
1. ISO 19567-2:2019. *Photography — Digital cameras — Texture reproduction measurements — Part 2:
|
||||||
Measurements using dead leaves pattern.* International Organization for Standardization.
|
Measurements using dead leaves pattern.* International Organization for Standardization.
|
||||||
2. ISO/IEC 18181. *Information technology — JPEG XL image coding system*; reference implementation
|
2. ISO/IEC 18181. *Information technology — JPEG XL image coding system*; reference implementation
|
||||||
libjxl, versions 0.11.0 and 0.11.1, https://github.com/libjxl/libjxl.
|
libjxl, versions 0.11.0 and 0.11.1 (towers) and 0.12.0 (dark-scene comparison, Section 3.9.2),
|
||||||
|
https://github.com/libjxl/libjxl.
|
||||||
3. OpenCV, versions 4.5.4 and 4.10: `cvtColor` colour conversions, Bayer demosaicing (bilinear and
|
3. OpenCV, versions 4.5.4 and 4.10: `cvtColor` colour conversions, Bayer demosaicing (bilinear and
|
||||||
edge-aware), https://docs.opencv.org.
|
edge-aware), https://docs.opencv.org.
|
||||||
4. EMVA. *GenICam Pixel Format Naming Convention (PFNC)* and *GenICam Standard Features Naming
|
4. EMVA. *GenICam Pixel Format Naming Convention (PFNC)* and *GenICam Standard Features Naming
|
||||||
|
|
@ -892,6 +1061,10 @@ between neighbouring waypoints, which decides the real pace, remains to be measu
|
||||||
`tools/camera-bench/examples-*`.
|
`tools/camera-bench/examples-*`.
|
||||||
- Figures: `plot_results.py`, `plot_common.py` (charts), `make_figures.py` (example crops);
|
- Figures: `plot_results.py`, `plot_common.py` (charts), `make_figures.py` (example crops);
|
||||||
quality scores: `compare_examples.py`.
|
quality scores: `compare_examples.py`.
|
||||||
|
- Dark-scene comparison (Section 3.9.2): `compare_8_vs_12.py`; figures and
|
||||||
|
[results.json](images/raw12/examples/staeffelsberg/results.json) in
|
||||||
|
`docs/images/raw12/examples/staeffelsberg/`. The two source frames (`BayerRG12p.tif`, 97 MB;
|
||||||
|
`RGB8.png`, 17 MB) are kept outside the repository, in `8bit_vs_12bit_test/` next to it.
|
||||||
|
|
||||||
## Appendix B. Processing times on each tower's own frame
|
## Appendix B. Processing times on each tower's own frame
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ This file gives an AI assistant the background needed to discuss the report
|
||||||
[raw12-pipeline-measurements.md](raw12-pipeline-measurements.md): what was asked, how the tests were
|
[raw12-pipeline-measurements.md](raw12-pipeline-measurements.md): what was asked, how the tests were
|
||||||
run, what the numbers mean, what was decided, which points have been questioned, and what is still
|
run, what the numbers mean, what was decided, which points have been questioned, and what is still
|
||||||
open. Read the report itself for the full tables and figures; this file explains around them.
|
open. Read the report itself for the full tables and figures; this file explains around them.
|
||||||
State as of 2026-10-05, branch `raw12-capture`.
|
State as of 2026-10-07, branch `raw12-capture` (dark-scene comparison added 2026-10-07).
|
||||||
|
|
||||||
## 1. The system in one paragraph
|
## 1. The system in one paragraph
|
||||||
|
|
||||||
|
|
@ -151,9 +151,26 @@ have 11–22 GB free, so images must leave the tower continuously.
|
||||||
backlit ridges under a bright sky at dawn or dusk; thin smoke over dark terrain is the relevant
|
backlit ridges under a bright sky at dawn or dusk; thin smoke over dark terrain is the relevant
|
||||||
fire-detection case. At night with high gain, noise is amplified above the step again, so little
|
fire-detection case. At night with high gain, noise is amplified above the step again, so little
|
||||||
benefit. Strong shadow lifting (≥ 3 stops) untested; 2 stops showed no difference.
|
benefit. Strong shadow lifting (≥ 3 stops) untested; 2 stops showed no difference.
|
||||||
- None of the dark cases were measured (the darkest area in the scene was ≈ 700 DN; a dusk attempt
|
- The October scene had nothing darker than ≈ 700 DN (a dusk attempt on 121 needed 24 dB and was
|
||||||
on 121 needed 24 dB and was discarded). Proposed test: a dawn/dusk frame with dark terrain under a
|
discarded). On 7 Oct the user took a dark, high-contrast pair at the Stäffelsberg tower (shaded
|
||||||
bright sky, compared as in Figures 12 and 13.
|
forest under hazy sky, same 1620c model): `BayerRG12p` converted to colour by the camera viewer and
|
||||||
|
saved as 16-bit TIFF (12-bit values << 4), and a separate RGB8 capture as PNG; files in
|
||||||
|
`../8bit_vs_12bit_test/` (outside the repo). Gain/exposure not recorded. Report Section 3.9.2,
|
||||||
|
script `tools/camera-bench/compare_8_vs_12.py`, results.json in
|
||||||
|
`docs/images/raw12/examples/staeffelsberg/`.
|
||||||
|
- Stäffelsberg findings: 33% of the frame < 300 DN; noise in the colour image (std around a local
|
||||||
|
mean, quietest 10% of 24 × 24 patches, calibrated) 5–15 DN below ≈ 1300 DN, i.e. below one 8-bit
|
||||||
|
step over ~70% of the frame. Rounding to 8 bits: +10–26% noise in the forest; dark crop 272 vs 25
|
||||||
|
green levels (camera RGB8 22); local-average error (Gaussian σ 4 px) only 0.6 DN; no visible
|
||||||
|
banding at +4 stops, 8-bit slightly grainier. Camera RGB8 1.4–1.8× noisier than the 12-bit colour
|
||||||
|
image (camera's own processing).
|
||||||
|
- Compression is where it shows: at d1.0 the 8-bit file has 3.7 DN local-average error in the dark
|
||||||
|
vs 1.6 DN for 12-bit (bright: 4.0 vs 6.0), visibly blocky shadows; today's RGB8 d0.8 e4 file
|
||||||
|
(0.92 MB) is equally blocky. The same 8-bit values stored in a 12-bit file give 1.8 DN and
|
||||||
|
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.
|
||||||
|
- 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
|
||||||
bit depth.
|
bit depth.
|
||||||
|
|
@ -178,7 +195,7 @@ N workers, bounded queue; scheduler save gate pauses the scan when the queue is
|
||||||
|
|
||||||
Not done yet: exporting the new camera profiles on a tower (the 1620c is currently on tower 111
|
Not done yet: exporting the new camera profiles on a tower (the 1620c is currently on tower 111
|
||||||
and must be moved back to 121 first); field verification with real slews; checking that downstream
|
and must be moved back to 121 first); field verification with real slews; checking that downstream
|
||||||
consumers decode 12-bit files; the dawn/dusk 12-bit test. The branch is deployed on tower 111,
|
consumers decode 12-bit files. The branch is deployed on tower 111,
|
||||||
whose boot service still passes old `--init/--start` flags and needs `scripts/install-tower`.
|
whose boot service still passes old `--init/--start` flags and needs `scripts/install-tower`.
|
||||||
|
|
||||||
## 10. Report structure (for orientation)
|
## 10. Report structure (for orientation)
|
||||||
|
|
@ -187,7 +204,8 @@ Abstract; Units and abbreviations; 1 Introduction (1.1 question, 1.2 pixel forma
|
||||||
mosaic vs raw planes, Figures 1a/1b, Table 1; 1.3 earlier work); 2 Materials and methods (2.1–2.12,
|
mosaic vs raw planes, Figures 1a/1b, Table 1; 1.3 earlier work); 2 Materials and methods (2.1–2.12,
|
||||||
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 with Tables 17a/17b, 3.10 throughput, 3.11 heat,
|
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,
|
||||||
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/`.
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ Not part of the build: CMake only compiles the tools it names.
|
||||||
| `run_common_frame.sh` | the processing tests on one shared frame (`common-frame/`, git-ignored, rebuilt from 121's lossless 12-bit example), so towers compare exactly (~25–40 min) |
|
| `run_common_frame.sh` | the processing tests on one shared frame (`common-frame/`, git-ignored, rebuilt from 121's lossless 12-bit example), so towers compare exactly (~25–40 min) |
|
||||||
| `plot_common.py` | the shared-frame tables and charts (per-step time, size per quality setting, saves per second); laptop venv |
|
| `plot_common.py` | the shared-frame tables and charts (per-step time, size per quality setting, saves per second); laptop venv |
|
||||||
| `compare_examples.py` | scores every 12-bit example against the lossless 12-bit colour image (PSNR, SSIM); laptop venv |
|
| `compare_examples.py` | scores every 12-bit example against the lossless 12-bit colour image (PSNR, SSIM); laptop venv |
|
||||||
|
| `compare_8_vs_12.py` | 8-bit against 12-bit on a dark scene: noise, levels, JPEG XL error per brightness region, crop figures (2026-10-07); laptop venv |
|
||||||
| `make_figures.py` | example-image figures (crops, difference maps, 8 vs 12-bit shadow) from the example `.jxl` files; laptop venv |
|
| `make_figures.py` | example-image figures (crops, difference maps, 8 vs 12-bit shadow) from the example `.jxl` files; laptop venv |
|
||||||
| `plot_results.py` | every chart and table of `docs/raw12-pipeline-measurements.md` from the results folders; laptop venv |
|
| `plot_results.py` | every chart and table of `docs/raw12-pipeline-measurements.md` from the results folders; laptop venv |
|
||||||
| `results-*/` | raw results of each run, one folder per tower and day |
|
| `results-*/` | raw results of each run, one folder per tower and day |
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,235 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""compare_8_vs_12.py - 8-bit against 12-bit on a dark, high-contrast scene.
|
||||||
|
|
||||||
|
~/.venvs/fgc-plots/bin/python compare_8_vs_12.py <BayerRG12p.tif> <RGB8.png> <out_dir> \
|
||||||
|
[<october lossless c12 .jxl> <label> ...]
|
||||||
|
|
||||||
|
Inputs: the camera's BayerRG12p frame converted to colour by the viewer and saved as 16-bit TIFF
|
||||||
|
(12-bit values in the top bits), and the camera's own RGB8 frame of the same view taken seconds
|
||||||
|
later (another capture: trees move, so no pixel-by-pixel comparison with it). The 8-bit version of
|
||||||
|
the same frame is the 12-bit value >> 4 (what BayerRG8 would deliver). Optional extra lossless
|
||||||
|
12-bit colour files from earlier campaigns get the same noise measurement, for comparison.
|
||||||
|
|
||||||
|
Writes to <out_dir>:
|
||||||
|
overview.jpg the frame upright, display gamma, crop boxes
|
||||||
|
noise_vs_signal.png noise against brightness, every image measured the same way
|
||||||
|
dark_crop.png dark crop, camera RGB8 / 8-bit / 12-bit, +2 and +4 stops
|
||||||
|
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)
|
||||||
|
results.json noise, levels, sizes and errors per brightness region
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import imagecodecs
|
||||||
|
import matplotlib
|
||||||
|
import matplotlib.ticker
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
matplotlib.use("Agg")
|
||||||
|
import matplotlib.pyplot as plt # noqa: E402
|
||||||
|
|
||||||
|
tif, png, out = sys.argv[1:4]
|
||||||
|
extra = list(zip(sys.argv[4::2], sys.argv[5::2]))
|
||||||
|
os.makedirs(out, exist_ok=True)
|
||||||
|
|
||||||
|
c12 = cv2.imread(tif, cv2.IMREAD_UNCHANGED)[:, :, ::-1] >> 4 # 0-4095, uint16
|
||||||
|
cam8 = cv2.imread(png, cv2.IMREAD_UNCHANGED)[:, :, ::-1].copy() # 0-255, uint8
|
||||||
|
c8 = (c12 >> 4).astype(np.uint8) # same frame, top 8 bits
|
||||||
|
c8in12 = c8.astype(np.uint16) * 16 + 8 # the same 8-bit values, declared as a 12-bit file
|
||||||
|
# (integers only: + 8 rather than + 7.5; the 0.5 is taken off again after decoding)
|
||||||
|
H, W = c12.shape[:2]
|
||||||
|
|
||||||
|
|
||||||
|
def to12(a):
|
||||||
|
"""Any decoded image on the 12-bit scale; 8-bit v stands for the middle of 16v ... 16v + 15."""
|
||||||
|
return a.astype(np.float32) * 16 + 7.5 if a.dtype == np.uint8 else a.astype(np.float32)
|
||||||
|
|
||||||
|
|
||||||
|
ref = c12.astype(np.float32)
|
||||||
|
lum_s = cv2.GaussianBlur(ref[..., 1].astype(np.float32), (0, 0), 8)
|
||||||
|
REGIONS = {"dark (< 300 DN)": lum_s < 300,
|
||||||
|
"mid (300-1000 DN)": (lum_s >= 300) & (lum_s < 1000),
|
||||||
|
"bright (>= 1000 DN)": lum_s >= 1000}
|
||||||
|
|
||||||
|
# --- noise against brightness -------------------------------------------------------------
|
||||||
|
P = 24
|
||||||
|
EDGES = np.array([100, 130, 160, 200, 250, 300, 400, 500, 700, 1000, 1400, 2000, 2400, 2800, 3300])
|
||||||
|
|
||||||
|
|
||||||
|
def noise_floor(g):
|
||||||
|
"""Per-pixel noise of a colour image's green channel, on the 12-bit scale.
|
||||||
|
|
||||||
|
Standard deviation of the residual after a Gaussian (sigma 2 px) local mean, per 24 x 24 patch;
|
||||||
|
patches grouped by brightness and the quietest 10% of each group averaged (texture only adds).
|
||||||
|
Plain standard deviation, not a median-based one, so it also works on 8-bit steps. Neighbouring
|
||||||
|
pixels of a de-Bayered image are correlated, which this measure tolerates better than
|
||||||
|
neighbour differences.
|
||||||
|
"""
|
||||||
|
g = g.astype(np.float32)
|
||||||
|
r = g - cv2.GaussianBlur(g, (0, 0), 2.0)
|
||||||
|
h, w = g.shape[0] // P * P, g.shape[1] // P * P
|
||||||
|
sd = r[:h, :w].reshape(h // P, P, w // P, P).std(axis=(1, 3)).ravel()
|
||||||
|
mn = g[:h, :w].reshape(h // P, P, w // P, P).mean(axis=(1, 3)).ravel()
|
||||||
|
rows = []
|
||||||
|
for a, b in zip(EDGES[:-1], EDGES[1:]):
|
||||||
|
m = (mn >= a) & (mn < b)
|
||||||
|
if m.sum() < 50:
|
||||||
|
continue
|
||||||
|
rows.append((float(np.median(mn[m])), float(np.sort(sd[m])[:max(1, m.sum() // 10)].mean())))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
# The residual of white noise under this high-pass is a little below its true sigma: calibrate.
|
||||||
|
wn = np.random.default_rng(0).normal(0, 10, (1200, 1200)) + 1000
|
||||||
|
CAL = 10 / noise_floor(wn)[0][1]
|
||||||
|
curves = {"12-bit (this frame)": [(s, n * CAL) for s, n in noise_floor(ref[..., 1])],
|
||||||
|
"8-bit, same frame (12-bit >> 4)": [(s, n * CAL) for s, n in noise_floor(to12(c8)[..., 1])],
|
||||||
|
"camera RGB8 (other capture)": [(s, n * CAL) for s, n in noise_floor(to12(cam8)[..., 1])]}
|
||||||
|
for path, label in extra:
|
||||||
|
a = imagecodecs.jpegxl_decode(open(path, "rb").read()).astype(np.float32)
|
||||||
|
a = a / 16 if a.max() > 4095.5 else a
|
||||||
|
curves[label] = [(s, n * CAL) for s, n in noise_floor(a[..., 1])]
|
||||||
|
|
||||||
|
fig, ax = plt.subplots(figsize=(6.6, 3.6))
|
||||||
|
style = {"12-bit (this frame)": ("tab:blue", "-", "o"),
|
||||||
|
"8-bit, same frame (12-bit >> 4)": ("tab:orange", "-", "s"),
|
||||||
|
"camera RGB8 (other capture)": ("tab:purple", "-", "^")}
|
||||||
|
other = iter([("tab:green", "--", "."), ("tab:red", "--", ".")])
|
||||||
|
for label, rows in curves.items():
|
||||||
|
c, ls, mk = style.get(label) or next(other)
|
||||||
|
ax.plot([r[0] for r in rows], [r[1] for r in rows], color=c, ls=ls, marker=mk, ms=4, label=label)
|
||||||
|
ax.axhline(16, color="k", ls="--", lw=1, label="one 8-bit step (16)")
|
||||||
|
ax.axhline(16 / np.sqrt(12), color="gray", ls=":", lw=1, label="8-bit rounding error (4.6)")
|
||||||
|
ax.set_xscale("log"); ax.set_yscale("log")
|
||||||
|
ticks = matplotlib.ticker
|
||||||
|
ax.xaxis.set_major_locator(ticks.FixedLocator([150, 200, 300, 500, 700, 1000, 1500, 2000, 3000]))
|
||||||
|
ax.yaxis.set_major_locator(ticks.FixedLocator([5, 7, 10, 15, 20, 30, 50, 70]))
|
||||||
|
for axis in (ax.xaxis, ax.yaxis):
|
||||||
|
axis.set_major_formatter(ticks.ScalarFormatter()); axis.set_minor_formatter(ticks.NullFormatter())
|
||||||
|
ax.set_xlabel("signal, 12-bit units (green)"); ax.set_ylabel("noise σ, 12-bit units")
|
||||||
|
ax.set_title("Noise vs brightness in the colour image (quietest 10% of 24×24 patches)", fontsize=10)
|
||||||
|
ax.grid(True, which="both", alpha=0.3)
|
||||||
|
ax.legend(fontsize=7, loc="upper left")
|
||||||
|
fig.tight_layout(); fig.savefig(os.path.join(out, "noise_vs_signal.png"), dpi=100); plt.close(fig)
|
||||||
|
|
||||||
|
# --- JPEG XL at the tower's settings (effort 3), 12-bit and 8-bit of the same frame ----------
|
||||||
|
DIST = [0, 0.3, 0.5, 1.0]
|
||||||
|
CS = 400
|
||||||
|
dark_xy = (40, 360) # shaded forest, about 200 DN
|
||||||
|
mid_xy = (3800, 1300) # distant valley in haze, about 1400 DN
|
||||||
|
|
||||||
|
|
||||||
|
def crop(a, xy):
|
||||||
|
x, y = xy
|
||||||
|
return a[y:y + CS, x:x + CS]
|
||||||
|
|
||||||
|
|
||||||
|
# Errors against the 12-bit frame, per region. Per pixel: everything, including discarded noise.
|
||||||
|
# Averaged: after a Gaussian average (sigma 4 px, about 200 pixels) of both images - the error in
|
||||||
|
# local brightness that remains once noise is averaged away, i.e. lost scene information such as a
|
||||||
|
# faint, even brightening.
|
||||||
|
ref_avg = cv2.GaussianBlur(ref, (0, 0), 4)
|
||||||
|
size, dark_dec, errors = {}, {}, []
|
||||||
|
SOURCES = (("12-bit", c12, 12), ("8-bit", c8, 8), ("8-bit values, 12-bit file", c8in12, 12),
|
||||||
|
("camera RGB8", cam8, 8))
|
||||||
|
for name, img, bps in SOURCES:
|
||||||
|
for d in DIST:
|
||||||
|
kw = dict(lossless=True) if d == 0 else dict(distance=d)
|
||||||
|
data = imagecodecs.jpegxl_encode(np.ascontiguousarray(img), effort=3, bitspersample=bps, **kw)
|
||||||
|
size[(name, d)] = len(data) / 1e6
|
||||||
|
print(f"{name} d{d}: {len(data) / 1e6:.2f} MB", flush=True)
|
||||||
|
if name == "camera RGB8": # another capture: sizes only
|
||||||
|
continue
|
||||||
|
a = to12(imagecodecs.jpegxl_decode(data)).astype(np.float32)
|
||||||
|
if img is c8in12:
|
||||||
|
a -= 0.5
|
||||||
|
if name == "12-bit" and d == 0:
|
||||||
|
assert np.array_equal(a, ref), "12-bit lossless must be exact"
|
||||||
|
dark_dec[(name, d)] = crop(a, dark_xy).copy()
|
||||||
|
e_px = a - ref
|
||||||
|
e_av = cv2.GaussianBlur(a, (0, 0), 4) - ref_avg
|
||||||
|
row = dict(source=name, distance=d, size_mb=round(size[(name, d)], 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))
|
||||||
|
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()
|
||||||
|
|
||||||
|
# --- crops ---------------------------------------------------------------------------------
|
||||||
|
def upright(a):
|
||||||
|
return np.ascontiguousarray(np.rot90(a, 1)) # camera on its side: sky was on the right
|
||||||
|
|
||||||
|
|
||||||
|
def view(a12, stops):
|
||||||
|
return np.clip(a12 * 2.0 ** stops / 16, 0, 255).astype(np.uint8)
|
||||||
|
|
||||||
|
|
||||||
|
def grid(tiles, col_labels, row_labels, path, titles=None):
|
||||||
|
"""Tiles in rows; labels per column and row, or a title per tile."""
|
||||||
|
nr, nc = len(tiles), len(tiles[0])
|
||||||
|
fig, axs = plt.subplots(nr, nc, figsize=(nc * 3.0, nr * 3.25), squeeze=False)
|
||||||
|
for i in range(nr):
|
||||||
|
for j in range(nc):
|
||||||
|
axs[i][j].imshow(tiles[i][j], interpolation="nearest")
|
||||||
|
axs[i][j].set_xticks([]); axs[i][j].set_yticks([])
|
||||||
|
if titles:
|
||||||
|
axs[i][j].set_title(titles[i][j], fontsize=8)
|
||||||
|
elif i == 0:
|
||||||
|
axs[i][j].set_title(col_labels[j], fontsize=8)
|
||||||
|
if j == 0 and row_labels:
|
||||||
|
axs[i][j].set_ylabel(row_labels[i], fontsize=8)
|
||||||
|
fig.tight_layout(pad=0.4); fig.savefig(path, dpi=150); plt.close(fig)
|
||||||
|
|
||||||
|
|
||||||
|
d12, d8, dc = crop(ref, dark_xy), crop(to12(c8), dark_xy), crop(to12(cam8), dark_xy)
|
||||||
|
grid([[upright(view(a, s)) for a in (dc, d8, d12)] for s in (2, 4)],
|
||||||
|
["camera RGB8 (today, other capture)", "8-bit (same frame, 12-bit >> 4)", "12-bit"],
|
||||||
|
["+2 stops (×4)", "+4 stops (×16)"], os.path.join(out, "dark_crop.png"))
|
||||||
|
keys = [[("camera RGB8 today", 0.8), ("8-bit", 1.0)], [("8-bit values, 12-bit file", 1.0), ("12-bit", 1.0)]]
|
||||||
|
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"]])
|
||||||
|
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)]],
|
||||||
|
["8-bit (same frame)", "12-bit"], ["contrast ×4 around the mean"], os.path.join(out, "mid_crop.png"))
|
||||||
|
|
||||||
|
# Overview: upright, quarter size, display gamma (sRGB) so the forest is visible.
|
||||||
|
small = cv2.resize(ref, (W // 4, H // 4), interpolation=cv2.INTER_AREA) / 4095
|
||||||
|
small = np.where(small <= 0.0031308, 12.92 * small, 1.055 * small ** (1 / 2.4) - 0.055)
|
||||||
|
ov = cv2.cvtColor((np.clip(small, 0, 1) * 255).astype(np.uint8), cv2.COLOR_RGB2BGR)
|
||||||
|
for (x, y), c in ((dark_xy, (255, 160, 0)), (mid_xy, (0, 0, 255))):
|
||||||
|
cv2.rectangle(ov, (x // 4, y // 4), ((x + CS) // 4, (y + CS) // 4), c, 2)
|
||||||
|
cv2.imwrite(os.path.join(out, "overview.jpg"), upright(ov), [cv2.IMWRITE_JPEG_QUALITY, 88])
|
||||||
|
|
||||||
|
|
||||||
|
def levels(a):
|
||||||
|
g = np.round(a[..., 1]).astype(int)
|
||||||
|
return int(len(np.unique(g)))
|
||||||
|
|
||||||
|
|
||||||
|
res = dict(
|
||||||
|
frame=dict(width=W, height=H, green_range_12bit=[int(c12[..., 1].min()), int(c12[..., 1].max())],
|
||||||
|
share_of_frame={k: round(float(m.mean()) * 100, 1) for k, m in REGIONS.items()}),
|
||||||
|
noise_calibration=round(CAL, 3),
|
||||||
|
noise={k: [[round(s), round(n, 1)] for s, n in v] for k, v in curves.items()},
|
||||||
|
dark_crop=dict(x=dark_xy[0], y=dark_xy[1], size=CS, mean_green_12bit=round(float(d12[..., 1].mean()), 1),
|
||||||
|
green_range_12bit=[int(d12[..., 1].min()), int(d12[..., 1].max())],
|
||||||
|
distinct_green_levels={"12-bit": levels(d12), "8-bit same frame": levels(d8),
|
||||||
|
"camera RGB8": levels(dc)}),
|
||||||
|
mid_crop=dict(x=mid_xy[0], y=mid_xy[1], size=CS,
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
json.dump(res, open(os.path.join(out, "results.json"), "w"), indent=1)
|
||||||
|
print(json.dumps(res, indent=1))
|
||||||
Loading…
Reference in New Issue