summaryrefslogtreecommitdiff
path: root/cnn_v3/training/pack_blender_sample.py
diff options
context:
space:
mode:
authorskal <pascal.massimino@gmail.com>2026-03-22 10:16:39 +0100
committerskal <pascal.massimino@gmail.com>2026-03-22 10:16:39 +0100
commitcdfd04179e6afd326c241e071cd082190e37b7f5 (patch)
tree52cbe16033e05f19a52bfbaf81b533afff1d202f /cnn_v3/training/pack_blender_sample.py
parent8fdc447ca7d490f2075e4f0604f6896de1bcda75 (diff)
fix(cnn_v3): native OPEN_EXR_MULTILAYER + quiet render + flexible channel names
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
Diffstat (limited to 'cnn_v3/training/pack_blender_sample.py')
-rw-r--r--cnn_v3/training/pack_blender_sample.py78
1 files changed, 45 insertions, 33 deletions
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")