fwt_software/tools/camera-bench/make_synthetic.py

70 lines
2.8 KiB
Python

#!/usr/bin/env python3
"""make_synthetic.py - a sharp, detailed stand-in frame for encode_bench.
The tower cameras have so far only seen blurred, dim indoor scenes, which
compress far faster and smaller than a sharp outdoor view. This writes a
"dead leaves" texture - overlapping discs with power-law radii, the texture
chart of ISO 19567-2 - with mild sensor noise, in the three formats
camera_bench.py `save` produces (RGB8, BayerRG8, BayerRG12p), so the encode
numbers can be bounded from above.
python3 make_synthetic.py <out_dir> [width] [height]
Needs numpy and OpenCV (the tower's system python3 has both).
"""
import json
import os
import sys
import cv2
import numpy as np
out = sys.argv[1]
w = int(sys.argv[2]) if len(sys.argv) > 2 else 5328
h = int(sys.argv[3]) if len(sys.argv) > 3 else 3040
rng = np.random.default_rng(20261001)
img = np.full((h, w, 3), 128, np.uint8)
rmin, rmax = 2.0, 600.0
# Enough discs to cover the frame several times over: with this radius
# distribution one disc covers ~143 px on average, the frame 16.2 M px.
n = 600000
# Radii with density ~ r^-3 (scale-invariant, as in natural images).
u = rng.random(n)
radii = 1.0 / np.sqrt(u * (1 / rmin**2 - 1 / rmax**2) + 1 / rmax**2)
for r in radii:
c = (int(rng.integers(0, w)), int(rng.integers(0, h)))
col = tuple(int(x) for x in rng.integers(10, 246, 3))
cv2.circle(img, c, int(r), col, -1, lineType=cv2.LINE_AA)
# Mild sensor noise, in 12-bit units, then the three delivered formats.
lin = img.astype(np.float32) * 16 + rng.normal(0, 24, img.shape).astype(np.float32)
lin12 = np.clip(lin, 0, 4095).astype(np.uint16)
rgb8 = (lin12 >> 4).astype(np.uint8)
# The camera's BayerRG: R at (even row, even col), B at (odd, odd).
m12 = np.empty((h, w), np.uint16)
m12[0::2, 0::2] = lin12[0::2, 0::2, 0]
m12[0::2, 1::2] = lin12[0::2, 1::2, 1]
m12[1::2, 0::2] = lin12[1::2, 0::2, 1]
m12[1::2, 1::2] = lin12[1::2, 1::2, 2]
m8 = (m12 >> 4).astype(np.uint8)
# GenICam 12p: two pixels in three bytes, little-endian.
flat = m12.reshape(-1)
p0, p1 = flat[0::2].astype(np.uint32), flat[1::2].astype(np.uint32)
packed = np.empty((p0.size, 3), np.uint8)
packed[:, 0] = p0 & 0xFF
packed[:, 1] = ((p0 >> 8) & 0x0F) | ((p1 & 0x0F) << 4)
packed[:, 2] = (p1 >> 4) & 0xFF
os.makedirs(out, exist_ok=True)
for fmt, data in (("RGB8", rgb8), ("BayerRG8", m8), ("BayerRG12p", packed)):
data.tofile(os.path.join(out, f"{fmt}_0.bin"))
with open(os.path.join(out, f"{fmt}_0.json"), "w") as f:
json.dump(dict(fmt=fmt, width=w, height=h, synthetic="dead leaves + noise"), f)
cv2.imwrite(os.path.join(out, "preview.jpg"),
cv2.cvtColor(cv2.resize(rgb8, (w // 4, h // 4), interpolation=cv2.INTER_AREA),
cv2.COLOR_RGB2BGR), [cv2.IMWRITE_JPEG_QUALITY, 85])
print(json.dumps(dict(out=out, width=w, height=h, mean=float(rgb8.mean()))))