From cdfd04179e6afd326c241e071cd082190e37b7f5 Mon Sep 17 00:00:00 2001 From: skal Date: Sun, 22 Mar 2026 10:16:39 +0100 Subject: fix(cnn_v3): native OPEN_EXR_MULTILAYER + quiet render + flexible channel names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit blender_export.py: - Replace broken compositor FileOutput approach with native OPEN_EXR_MULTILAYER render output; all enabled passes included automatically, no socket wiring needed - Suppress Fra:/Mem: render spam via os.dup2 fd redirect; per-frame progress printed to stderr via render_post handler pack_blender_sample.py: - get_pass_r: try .R/.X/.Y/.Z/'' suffixes + aliases param for Depth→Z fallback - combined_rgba loaded once via ("Combined","Image") loop; shared by transp+target - Remove unused sys import HOW_TO_CNN.md: update channel table to native EXR naming (Depth.Z, IndexOB.X, Shadow.X), fix example command, note Shadow defaults to 255 when absent handoff(Gemini): blender pipeline now produces correct multilayer EXR with all G-buffer passes; pack script handles native channel naming --- cnn_v3/training/pack_blender_sample.py | 78 ++++++++++++++++++++-------------- 1 file changed, 45 insertions(+), 33 deletions(-) (limited to 'cnn_v3/training/pack_blender_sample.py') diff --git a/cnn_v3/training/pack_blender_sample.py b/cnn_v3/training/pack_blender_sample.py index 84344c1..03d540c 100644 --- a/cnn_v3/training/pack_blender_sample.py +++ b/cnn_v3/training/pack_blender_sample.py @@ -9,7 +9,7 @@ Output files: normal.png — RG uint8 (octahedral-encoded world normal in [0,1]) depth.png — R uint16 (1/(z+1) normalized to [0,1], 16-bit PNG) matid.png — R uint8 (IndexOB / 255) - shadow.png — R uint8 (1 - shadow_catcher, so 255 = fully lit) + shadow.png — R uint8 (255 = fully lit; defaults to 255 if pass absent) transp.png — R uint8 (alpha from Combined pass, 0=opaque) target.png — RGBA uint8 (Combined beauty pass) @@ -27,7 +27,6 @@ Dependencies: import argparse import os -import sys import numpy as np from PIL import Image @@ -128,13 +127,21 @@ def get_pass_rgba(channels: dict, prefix: str) -> np.ndarray: return np.concatenate([rgb, a[..., np.newaxis]], axis=-1) -def get_pass_r(channels: dict, prefix: str, default: float = 0.0) -> np.ndarray: - """Extract a single-channel pass.""" - ch = channels.get(f"{prefix}.R", channels.get(prefix, None)) - if ch is None: - h, w = next(iter(channels.values())).shape[:2] - return np.full((h, w), default, dtype=np.float32) - return ch.astype(np.float32) +def get_pass_r(channels: dict, prefix: str, default: float = 0.0, + aliases: tuple = ()) -> np.ndarray: + """Extract a single-channel pass. + + Tries multiple channel suffixes (.R, .X, .Y, .Z, none) and optional prefix + aliases to handle both compositor FileOutput naming and native OPEN_EXR_MULTILAYER + naming (e.g. 'Depth.Z' vs 'Z', 'Shadow.X' vs 'Shadow.R'). + """ + for p in (prefix,) + tuple(aliases): + for suffix in ('.R', '.X', '.Y', '.Z', ''): + ch = channels.get(f"{p}{suffix}" if suffix else p) + if ch is not None: + return ch.astype(np.float32) + h, w = next(iter(channels.values())).shape[:2] + return np.full((h, w), default, dtype=np.float32) def get_pass_xyz(channels: dict, prefix: str) -> np.ndarray: @@ -155,6 +162,10 @@ def pack_blender_sample(exr_path: str, output_dir: str) -> None: print(f"[pack_blender_sample] Loading {exr_path} …") channels, width, height = load_exr(exr_path) + # Native OPEN_EXR_MULTILAYER prefixes channels with the view-layer name, + # e.g. "RenderLayer.DiffCol.R". Strip it so all lookups use 2-part names. + if any(k.count(".") >= 2 for k in channels): + channels = {".".join(k.split(".")[1:]): v for k, v in channels.items()} print(f" Dimensions: {width}×{height}") print(f" Channels: {sorted(channels.keys())}") @@ -191,8 +202,10 @@ def pack_blender_sample(exr_path: str, output_dir: str) -> None: os.path.join(output_dir, "normal.png") ) - # ---- depth (Z pass → 1/(z+1), stored as 16-bit PNG) ---- - z_raw = get_pass_r(channels, "Z", default=0.0) + # ---- depth (Z/Depth pass → 1/(z+1), stored as 16-bit PNG) ---- + # Native OPEN_EXR_MULTILAYER stores depth as "Depth.Z"; compositor FileOutput + # uses slot name "Z" (channel may be .R or .Z). + z_raw = get_pass_r(channels, "Depth", default=0.0, aliases=("Z",)) # 1/z style: 1/(z + 1) maps z=0→1.0, z=∞→0.0 depth_norm = 1.0 / (np.clip(z_raw, 0.0, None) + 1.0) depth_norm = np.clip(depth_norm, 0.0, 1.0) @@ -209,40 +222,39 @@ def pack_blender_sample(exr_path: str, output_dir: str) -> None: os.path.join(output_dir, "matid.png") ) - # ---- shadow (Shadow pass → invert: 1=fully lit, stored u8) ---- - # Blender Shadow pass: 1=lit, 0=shadowed. We keep that convention - # (shadow=1 means fully lit), so just convert directly. + # ---- shadow (Shadow pass → 1=fully lit, stored u8; defaults to 255 if absent) ---- shadow_raw = get_pass_r(channels, "Shadow", default=1.0) shadow_u8 = (np.clip(shadow_raw, 0.0, 1.0) * 255.0).astype(np.uint8) Image.fromarray(shadow_u8, mode="L").save( os.path.join(output_dir, "shadow.png") ) - # ---- transp (Alpha from Combined pass → u8, 0=opaque) ---- - # Blender alpha: 1=opaque, 0=transparent. - # CNN convention: transp=0 means opaque, transp=1 means transparent. - # So transp = 1 - alpha. - try: - combined_rgba = get_pass_rgba(channels, "Combined") - alpha = combined_rgba[..., 3] - except KeyError: - alpha = np.ones((height, width), dtype=np.float32) - transp = 1.0 - np.clip(alpha, 0.0, 1.0) - transp_u8 = (transp * 255.0).astype(np.uint8) + # ---- load beauty (Combined or Image pass) once; used for transp + target ---- + # Native OPEN_EXR_MULTILAYER stores beauty as "Combined"; some scenes use "Image". + combined_rgba = None + for _beauty in ("Combined", "Image"): + try: + combined_rgba = get_pass_rgba(channels, _beauty) + break + except KeyError: + pass + if combined_rgba is None: + print(" WARNING: Combined/Image pass not found; transp/target will be zeros.") + + # ---- transp (Combined alpha → u8, CNN convention: 0=opaque) ---- + alpha = combined_rgba[..., 3] if combined_rgba is not None \ + else np.ones((height, width), dtype=np.float32) + transp_u8 = ((1.0 - np.clip(alpha, 0.0, 1.0)) * 255.0).astype(np.uint8) Image.fromarray(transp_u8, mode="L").save( os.path.join(output_dir, "transp.png") ) - # ---- target (Combined beauty pass → RGBA uint8, gamma-correct) ---- - try: - combined_rgba = get_pass_rgba(channels, "Combined") - # Convert linear → sRGB for display (RGB channels only) + # ---- target (Combined beauty → RGBA uint8, gamma-correct) ---- + if combined_rgba is not None: c_rgb = np.power(np.clip(combined_rgba[..., :3], 0, 1), 1.0 / 2.2) c_alpha = combined_rgba[..., 3:4] - target_lin = np.concatenate([c_rgb, c_alpha], axis=-1) - target_u8 = (np.clip(target_lin, 0, 1) * 255.0).astype(np.uint8) - except KeyError: - print(" WARNING: Combined pass not found; target will be zeros.") + target_u8 = (np.clip(np.concatenate([c_rgb, c_alpha], axis=-1), 0, 1) * 255.0).astype(np.uint8) + else: target_u8 = np.zeros((height, width, 4), dtype=np.uint8) Image.fromarray(target_u8, mode="RGBA").save( os.path.join(output_dir, "target.png") -- cgit v1.2.3