mirror of
https://github.com/Awakenchan/LanternEngine.git
synced 2026-11-12 18:08:45 -10:00
123 lines
4.9 KiB
Python
123 lines
4.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Converts the upscaler dumps (BB_DUMP_TRIGGER, BB_PRESENT_DUMP_TRIGGER) into PNGs.
|
|
|
|
nix-shell -p "python3.withPackages (p: [p.numpy p.pillow])" --run "python3 tools/dump_view.py out/dump"
|
|
|
|
Files are <dir>/fNNN_<name>_<w>x<h>_<format>.raw (vk_temporal_upscaler.cpp DumpImages):
|
|
- rgba / bgra: 8-bit color as is;
|
|
- rgba16f / rgba32f / r11g11b10f: HDR color, Reinhard-mapped and sRGB-encoded (alpha dropped);
|
|
- rg16f (motion vectors, in pixels of the render size: TAA adds them to the pixel position,
|
|
FSR gets motionVectorScale 1): hue = direction, brightness =
|
|
length (MOTION_SCALE pixels and more = full), plus a "_mag" image of the length alone;
|
|
- f32 (depth): normalized over its own range, near = bright.
|
|
Writes <same name>.png next to each file.
|
|
"""
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
from PIL import Image
|
|
|
|
MOTION_SCALE = 16.0 # pixels for full brightness
|
|
NAME = re.compile(r'f(\d+)_(\w+?)_(\d+)x(\d+)_(\w+)\.raw$')
|
|
|
|
|
|
def srgb(linear):
|
|
linear = np.clip(linear, 0.0, 1.0)
|
|
return np.where(linear <= 0.0031308, linear * 12.92, 1.055 * np.power(linear, 1 / 2.4) - 0.055)
|
|
|
|
|
|
def small_float(bits, mantissa_bits):
|
|
"""Unsigned 10/11-bit floats (5-bit exponent, no sign) of B10G11R11_UFLOAT."""
|
|
exponent = (bits >> mantissa_bits) & 31
|
|
mantissa = (bits & ((1 << mantissa_bits) - 1)).astype(np.float32)
|
|
scale = float(1 << mantissa_bits)
|
|
normal = np.ldexp(1.0 + mantissa / scale, exponent.astype(np.int32) - 15)
|
|
denormal = np.ldexp(mantissa / scale, -14)
|
|
return np.where(exponent == 0, denormal, np.where(exponent == 31, np.inf, normal)).astype(np.float32)
|
|
|
|
|
|
def hdr_to_png(rgb):
|
|
mapped = rgb / (1.0 + rgb)
|
|
return (srgb(np.maximum(mapped, 0.0)) * 255.0 + 0.5).astype(np.uint8)
|
|
|
|
|
|
def motion_to_png(uv, width, height):
|
|
px = uv[..., 0]
|
|
py = uv[..., 1]
|
|
length = np.sqrt(px * px + py * py)
|
|
valid = np.isfinite(length)
|
|
length = np.where(valid, length, 0.0)
|
|
angle = (np.arctan2(py, px) + np.pi) / (2 * np.pi) # 0..1
|
|
value = np.clip(length / MOTION_SCALE, 0.0, 1.0)
|
|
h6 = angle * 6.0
|
|
c = value
|
|
x = c * (1 - np.abs(np.mod(h6, 2) - 1))
|
|
zeros = np.zeros_like(c)
|
|
sector = np.floor(h6).astype(int) % 6
|
|
r = np.choose(sector, [c, x, zeros, zeros, x, c])
|
|
g = np.choose(sector, [x, c, c, x, zeros, zeros])
|
|
b = np.choose(sector, [zeros, zeros, x, c, c, x])
|
|
rgb = np.stack([r, g, b], axis=-1)
|
|
rgb[~valid] = (1.0, 0.0, 1.0) # NaN or infinite: magenta
|
|
mag = (np.clip(length / MOTION_SCALE, 0, 1) * 255).astype(np.uint8)
|
|
return (rgb * 255).astype(np.uint8), mag, length
|
|
|
|
|
|
def convert(path):
|
|
m = NAME.search(path.name)
|
|
if not m:
|
|
return None
|
|
_frame, name, w, h, fmt = m.groups()
|
|
w, h = int(w), int(h)
|
|
data = path.read_bytes()
|
|
out = path.with_suffix('.png')
|
|
note = ''
|
|
if fmt in ('rgba', 'bgra'):
|
|
img = np.frombuffer(data, np.uint8)[: w * h * 4].reshape(h, w, 4)[..., :3]
|
|
if fmt == 'bgra':
|
|
img = img[..., ::-1]
|
|
Image.fromarray(np.ascontiguousarray(img)).save(out)
|
|
elif fmt in ('rgba16f', 'rgba32f'):
|
|
dtype = np.float16 if fmt == 'rgba16f' else np.float32
|
|
img = np.frombuffer(data, dtype)[: w * h * 4].reshape(h, w, 4)[..., :3].astype(np.float32)
|
|
img = np.nan_to_num(img, nan=0.0, posinf=0.0, neginf=0.0)
|
|
Image.fromarray(hdr_to_png(img)).save(out)
|
|
elif fmt == 'r11g11b10f':
|
|
packed = np.frombuffer(data, np.uint32)[: w * h].reshape(h, w)
|
|
img = np.stack([small_float(packed & 0x7FF, 6), small_float((packed >> 11) & 0x7FF, 6),
|
|
small_float(packed >> 22, 5)], axis=-1)
|
|
img = np.nan_to_num(img, nan=0.0, posinf=0.0, neginf=0.0)
|
|
Image.fromarray(hdr_to_png(img)).save(out)
|
|
elif fmt == 'rg16f':
|
|
uv = np.frombuffer(data, np.float16)[: w * h * 2].reshape(h, w, 2).astype(np.float32)
|
|
rgb, mag, length = motion_to_png(uv, w, h)
|
|
Image.fromarray(rgb).save(out)
|
|
Image.fromarray(mag).save(path.with_name(path.stem + '_mag.png'))
|
|
finite = length[np.isfinite(length)]
|
|
note = (f' motion px: median {np.median(finite):.2f}, p99 {np.percentile(finite, 99):.2f}, '
|
|
f'max {finite.max():.1f}, zero {np.mean(finite < 0.01) * 100:.0f}%')
|
|
elif fmt == 'f32':
|
|
depth = np.frombuffer(data, np.float32)[: w * h].reshape(h, w)
|
|
finite = depth[np.isfinite(depth)]
|
|
lo, hi = (finite.min(), finite.max()) if finite.size else (0.0, 1.0)
|
|
norm = (depth - lo) / max(hi - lo, 1e-9)
|
|
Image.fromarray((np.clip(norm, 0, 1) * 255).astype(np.uint8)).save(out)
|
|
note = f' depth {lo:.5f}..{hi:.5f}'
|
|
else:
|
|
return None
|
|
return f'{out.name}{note}'
|
|
|
|
|
|
def main():
|
|
root = Path(sys.argv[1] if len(sys.argv) > 1 else 'out/dump')
|
|
for path in sorted(root.glob('*.raw')):
|
|
result = convert(path)
|
|
if result:
|
|
print(result)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|