summaryrefslogtreecommitdiff
path: root/cnn_v3
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
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')
-rw-r--r--cnn_v3/docs/HOW_TO_CNN.md48
-rw-r--r--cnn_v3/training/blender_export.py220
-rw-r--r--cnn_v3/training/pack_blender_sample.py78
3 files changed, 121 insertions, 225 deletions
diff --git a/cnn_v3/docs/HOW_TO_CNN.md b/cnn_v3/docs/HOW_TO_CNN.md
index 3013e1c..69bbf6f 100644
--- a/cnn_v3/docs/HOW_TO_CNN.md
+++ b/cnn_v3/docs/HOW_TO_CNN.md
@@ -138,49 +138,53 @@ Produces all 20 feature channels including normals, depth, mat IDs, and shadow.
#### Blender requirements
-- Blender 3.x+ or 5.x+, Cycles render engine (5.x API differences handled automatically)
-- Object indices set: *Properties → Object → Relations → Object Index* must be > 0
+- **Blender 4.5 LTS** recommended (`blender4` alias); 5.x also works
+- Cycles render engine (set automatically by the script)
+- Object indices set: *Properties → Object → Relations → Object Index* > 0
for objects you want tracked in `matid` (IndexOB pass)
#### Step 1 — Render EXRs
```bash
-blender -b scene.blend -P cnn_v3/training/blender_export.py -- \
- --output /tmp/renders/ \
+cd cnn_v3/training
+blender4 -b input_3d/scene.blend -P blender_export.py -- \
+ --output tmp/renders/frames \
--width 640 --height 360 \
- --start-frame 1 --end-frame 200
+ --start-frame 1 --end-frame 200 \
+ --view-layer RenderLayer
```
-The `--` separator is **required**; arguments after it are passed to the Python script,
-not to Blender. `--output` is the base directory for the compositor File Output node;
-Blender appends the slot name and frame number automatically (e.g. `Combined0001.exr`).
-`//` is a Blender shorthand for the directory containing the `.blend` file.
+The `--` separator is **required**. `blender_export.py` uses native
+`OPEN_EXR_MULTILAYER` render output — all enabled passes are written
+automatically. One file per frame: `{output}/0001.exr`, `0002.exr`, …
+Render progress (`Fra:/Mem:` spam) is suppressed; per-frame status goes to stderr.
**Available flags:**
| Flag | Default | Notes |
|------|---------|-------|
-| `--output PATH` | `//renders/` | Base output directory; `//` = blend file directory |
+| `--output PATH` | `//renders/` | Output directory; `//` = blend file directory |
| `--width N` | 640 | Render resolution |
| `--height N` | 360 | Render resolution |
| `--start-frame N` | scene start | First frame |
| `--end-frame N` | scene end | Last frame |
| `--view-layer NAME` | first layer | View layer name; pass `?` to list available layers |
-**Render pass → CNN channel mapping:**
+**Render pass → EXR channel → CNN file:**
-| Blender pass | EXR channels | CNN use |
-|-------------|-------------|---------|
-| Combined | `.R .G .B .A` | `target.png` (beauty, sRGB-converted) |
-| DiffCol | `.R .G .B` | `albedo.png` (linear → sRGB gamma 2.2) |
-| Normal | `.X .Y .Z` | `normal.png` (world-space, oct-encoded to RG) |
-| Z | `.R` | `depth.png` (mapped as 1/(z+1) → uint16) |
-| IndexOB | `.R` | `matid.png` (object index, clamped uint8) |
-| Shadow | `.R` | `shadow.png` (255 = lit, 0 = shadowed) |
-| Combined alpha | `.A` | `transp.png` (inverted: 0 = opaque) |
+| Blender pass | Native EXR channel | CNN file |
+|-------------|--------------------|---------|
+| Combined | `Combined.R/G/B/A` | `target.png` (beauty, linear→sRGB) |
+| DiffCol | `DiffCol.R/G/B` | `albedo.png` (linear→sRGB γ2.2) |
+| Normal | `Normal.X/Y/Z` | `normal.png` (oct-encoded RG) |
+| Depth | `Depth.Z` | `depth.png` (1/(z+1) → uint16) |
+| IndexOB | `IndexOB.X` | `matid.png` (object index, uint8) |
+| Shadow | `Shadow.X` | `shadow.png` (255=lit; defaults to 255 if absent) |
+| Combined alpha | `Combined.A` | `transp.png` (1−alpha, 0=opaque) |
-**Pitfall:** Blender `Normal` pass uses `.X .Y .Z` channel names in the EXR, not `.R .G .B`.
-`pack_blender_sample.py` handles both naming conventions automatically.
+**Note on Shadow pass:** Blender's Cycles Shadow pass may be absent for scenes
+without shadow-casting lights or catcher objects; `pack_blender_sample.py` defaults
+to 1.0 (fully lit) when the channel is missing.
#### Step 2 — Pack EXRs into sample directories
diff --git a/cnn_v3/training/blender_export.py b/cnn_v3/training/blender_export.py
index daa5588..5ad7273 100644
--- a/cnn_v3/training/blender_export.py
+++ b/cnn_v3/training/blender_export.py
@@ -1,48 +1,41 @@
"""
Blender export script for CNN v3 G-buffer training data.
-Configures render passes and a compositor File Output node,
-then renders the current scene to a multi-layer EXR.
+Enables render passes and uses native OPEN_EXR_MULTILAYER output.
Usage (headless):
blender -b scene.blend -P blender_export.py -- --output /tmp/renders/
- # List available view layers in the blend file:
+ # List available view layers:
blender -b scene.blend -P blender_export.py -- --view-layer ?
- # Use a specific view layer:
- blender -b scene.blend -P blender_export.py -- --output /tmp/renders/ --view-layer "MyLayer"
-
---output is the base directory for the compositor File Output node. Blender appends
-the frame number to produce ####.exr files (e.g. /tmp/renders/0001.exr).
-Use // as a prefix to resolve relative to the .blend file directory.
-
-G-buffer pass mapping:
- Combined → training target RGBA (beauty)
- DiffCol → albedo.rgb (pre-lighting material color)
- Normal → normal.xy (world-space, oct-encode in pack_blender_sample.py)
- Z → depth (view-space distance, normalize in pack step)
- IndexOB → mat_id (object index, u8 / 255)
- Shadow → shadow (1 = fully lit)
- Alpha → transp. (0=opaque, 1=clear/transparent)
-
-Blender version notes:
- ≤4.x Works: scene.node_tree compositor routes all per-pass render data.
- 5.0.x BROKEN: the new post-process compositor (compositing_node_group) only
- receives the Combined image; DiffCol/Normal/Z/IndexOB/Shadow are empty.
- Use Blender 4.5 LTS until this is fixed upstream.
- 5.x+ The compositing_node_group path is kept ready; once Blender routes
- per-pass buffers to CompositorNodeRLayers in the new compositor,
- this script will work without changes.
+Output: one multilayer EXR per frame at {output}/0001.exr, 0002.exr, ...
+Passes: Combined (beauty), DiffCol (albedo), Normal, Depth, IndexOB, Shadow.
"""
+import contextlib
import os
import sys
import argparse
-import shutil
import bpy
+@contextlib.contextmanager
+def _suppress_blender_stdout():
+ """Redirect C-level stdout to /dev/null to suppress Fra:/Mem: render spam."""
+ sys.stdout.flush()
+ saved = os.dup(1)
+ null = os.open('/dev/null', os.O_WRONLY)
+ os.dup2(null, 1)
+ os.close(null)
+ try:
+ yield
+ finally:
+ sys.stdout.flush()
+ os.dup2(saved, 1)
+ os.close(saved)
+
+
def parse_args():
# Blender passes its own argv; our args follow '--'.
argv = sys.argv
@@ -52,8 +45,7 @@ def parse_args():
)
parser.add_argument(
"--output", default="//renders/",
- help="Base output directory for compositor File Output node. "
- "Blender appends frame number (e.g. 0001.exr). "
+ help="Output directory; frames are written as 0001.exr, 0002.exr, … "
"Use // for blend file directory. Default: //renders/",
)
parser.add_argument("--width", type=int, default=640, help="Render width in pixels (default: 640)")
@@ -98,39 +90,6 @@ def _resolve_view_layer(scene, name):
_die(f"view layer '{name}' not found. Available: {available}")
-def _get_compositor_tree(scene):
- """Return the compositor node tree, creating it if necessary.
-
- Blender ≤4.x: scene.node_tree (activated via scene.use_nodes = True).
- Blender 5.0+: scene.compositing_node_group. NOTE: as of 5.0.x the new
- post-process compositor does not route per-pass render data to
- CompositorNodeRLayers — only the Combined image flows through. The code
- below is correct and will work automatically once Blender fixes this.
- """
- if _is_blender5():
- if scene.compositing_node_group is None:
- scene.compositing_node_group = bpy.data.node_groups.new(
- name="Compositor", type="CompositorNodeTree"
- )
- return scene.compositing_node_group
- # Blender ≤4: use legacy scene.node_tree.
- if hasattr(scene, "use_nodes"):
- scene.use_nodes = True
- if hasattr(scene, "node_tree"):
- return scene.node_tree
- candidates = [a for a in dir(scene) if "node" in a.lower() or "compos" in a.lower()]
- _die("cannot find compositor node tree on scene.", candidates)
-
-
-def _set_output_path(node, path):
- """Set the output path on a CompositorNodeOutputFile (API differs by version)."""
- for attr in ("base_path", "directory"):
- if hasattr(node, attr):
- setattr(node, attr, path)
- return
- candidates = [a for a in dir(node) if "path" in a.lower() or "dir" in a.lower() or "file" in a.lower()]
- _die("cannot find path attribute on CompositorNodeOutputFile.", candidates)
-
def configure_scene(args):
"""Configure render settings and enable G-buffer passes."""
@@ -139,9 +98,7 @@ def configure_scene(args):
major, minor = _blender_version()
print(f"[blender_export] Blender {major}.{minor}")
if _is_blender5():
- print(" WARNING: Blender 5.x detected. Per-pass compositor routing is broken "
- "in 5.0.x — only Combined will appear in the output EXR.")
- print(" Use Blender 4.5 LTS for training data generation.")
+ print(" NOTE: Blender 5.x — using native OPEN_EXR_MULTILAYER output.")
scene.render.engine = "CYCLES"
scene.render.resolution_x = args.width
@@ -161,125 +118,48 @@ def configure_scene(args):
vl.use_pass_shadow = True # shadow
# Alpha is available via the combined pass alpha channel.
- # Ensure the compositor runs during render (needed for File Output node).
- if hasattr(scene.render, "use_compositing"):
- scene.render.use_compositing = True
+ # Use native multilayer EXR output — all enabled passes are included
+ # automatically. This is more reliable than the compositor FileOutput
+ # approach (which requires per-pass socket wiring that varies by Blender
+ # version and may silently omit passes).
+ scene.render.image_settings.file_format = "OPEN_EXR_MULTILAYER"
+ scene.render.image_settings.exr_codec = "ZIP"
+ out_abs = os.path.abspath(args.output)
+ os.makedirs(out_abs, exist_ok=True)
+ scene.render.filepath = out_abs + os.sep # trailing sep → {out}/0001.exr
- # Redirect the render engine's own output to a discard directory; the real
- # output comes from the compositor File Output node written to args.output.
- discard_dir = os.path.join(os.path.abspath(args.output), "tmp")
- os.makedirs(discard_dir, exist_ok=True)
- scene.render.filepath = discard_dir + os.sep
+ # Disable compositor post-processing so we capture raw G-buffer data.
+ if hasattr(scene.render, "use_compositing"):
+ scene.render.use_compositing = False
print(f"[blender_export] Render passes configured on ViewLayer '{vl.name}'.")
print(f" Resolution: {args.width}x{args.height}")
print(f" Frames: {scene.frame_start} – {scene.frame_end}")
- return discard_dir
-
+ print(f" Output: {out_abs}/")
-# CompositorNodeRLayers output socket names (queried from Blender 5.0.1 console):
-# ng = bpy.data.node_groups.new("T", "CompositorNodeTree")
-# rl = ng.nodes.new("CompositorNodeRLayers")
-# print([s.name for s in rl.outputs])
-# bpy.data.node_groups.remove(ng)
-#
-# ['Image', 'Alpha', 'Depth', 'Normal', 'UV', 'Vector', 'Position',
-# 'Deprecated', 'Deprecated',
-# 'Shadow', 'Ambient Occlusion',
-# 'Deprecated', 'Deprecated', 'Deprecated',
-# 'Object Index', 'Material Index', 'Mist', 'Emission', 'Environment',
-# 'Diffuse Direct', 'Diffuse Indirect', 'Diffuse Color',
-# 'Glossy Direct', 'Glossy Indirect', 'Glossy Color',
-# 'Transmission Direct', 'Transmission Indirect', 'Transmission Color',
-# 'Subsurface Direct', 'Subsurface Indirect', 'Subsurface Color']
-#
-# Table: (RenderLayer socket name, EXR layer name, Blender 5+ slot type)
-PASS_SOCKETS = [
- ("Image", "Combined", "RGBA"), # beauty / target
- ("Diffuse Color","DiffCol", "RGBA"), # albedo
- ("Normal", "Normal", "VECTOR"), # world normals
- ("Depth", "Z", "FLOAT"), # view-space depth
- ("Object Index", "IndexOB", "FLOAT"), # object index
- ("Shadow", "Shadow", "FLOAT"), # shadow
- ("Alpha", "Alpha", "FLOAT"), # transparency
-]
-# Fallback socket name aliases for older Blender versions.
-_SOCKET_ALIASES = {
- "Shadow": ["Shadow Catcher", "ShadowCatcher"],
- "Diffuse Color": ["DiffCol", "Diffuse"],
- "Depth": ["Z"],
- "Object Index": ["IndexOB"],
- "Alpha": ["Alpha"],
-}
-
-
-def configure_compositor(args):
- """Build the compositor node graph: RenderLayers → FileOutput (OPEN_EXR_MULTILAYER)."""
- scene = bpy.context.scene
- vl = _resolve_view_layer(scene, args.view_layer)
- tree = _get_compositor_tree(scene)
- tree.nodes.clear()
-
- rl_node = tree.nodes.new("CompositorNodeRLayers")
- rl_node.location = (0, 0)
- # Bind to the correct scene + view layer so per-pass outputs are populated.
- if hasattr(rl_node, "scene"):
- rl_node.scene = scene
- if hasattr(rl_node, "layer"):
- rl_node.layer = vl.name
-
- out_node = tree.nodes.new("CompositorNodeOutputFile")
- out_node.location = (600, 0)
- out_node.format.file_format = "OPEN_EXR_MULTILAYER"
- out_node.format.exr_codec = "ZIP"
- _set_output_path(out_node, args.output)
- if hasattr(out_node, "file_name"):
- out_node.file_name = "" # Blender 5: clear prefix so output is just ####.exr
-
- # Populate output slots.
- # file_output_items (Blender 5+) starts empty; add all slots.
- # file_slots (Blender ≤4) has one default slot; rename first, then append.
- slots = getattr(out_node, "file_output_items", None) \
- or getattr(out_node, "file_slots", None)
- if slots is None:
- candidates = [a for a in dir(out_node) if not a.startswith("__")]
- _die("cannot find slots attribute on CompositorNodeOutputFile.", candidates)
-
- if hasattr(out_node, "file_output_items"):
- for _, layer_name, sock_type in PASS_SOCKETS:
- slots.new(sock_type, layer_name)
- else:
- _, first_layer, _ = PASS_SOCKETS[0]
- slots[0].path = first_layer
- for _, layer_name, _ in PASS_SOCKETS[1:]:
- slots.new(layer_name)
+def main():
+ args = parse_args()
+ configure_scene(args)
- # Link each RenderLayer output socket to the corresponding FileOutput slot.
- for socket_name, layer_name, _ in PASS_SOCKETS:
- candidates = [socket_name] + _SOCKET_ALIASES.get(socket_name, [])
- src = next((s for s in rl_node.outputs if s.name in candidates), None)
- dst = out_node.inputs.get(layer_name)
- if src and dst:
- tree.links.new(src, dst)
- else:
- print(f"[blender_export] WARNING: cannot link '{socket_name}' → '{layer_name}'. Skipping.")
+ # Render with render-progress output suppressed. Per-frame status is
+ # printed via a render_post handler to stderr (always visible).
+ out_abs = os.path.abspath(args.output)
- print(f"[blender_export] Compositor configured. Output → {args.output}")
- print(" Layers: " + ", ".join(ln for _, ln, _ in PASS_SOCKETS))
+ def _on_render_post(scene, depsgraph=None):
+ print(f"[blender_export] Frame {scene.frame_current} done.", file=sys.stderr, flush=True)
+ bpy.app.handlers.render_post.append(_on_render_post)
+ try:
+ with _suppress_blender_stdout():
+ bpy.ops.render.render(animation=True)
+ finally:
+ bpy.app.handlers.render_post.remove(_on_render_post)
-def main():
- args = parse_args()
- discard_dir = configure_scene(args)
- configure_compositor(args)
- bpy.ops.render.render(animation=True)
- shutil.rmtree(discard_dir, ignore_errors=True)
print("[blender_export] Render complete.")
# Print the next-step pack command.
pack_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pack_blender_sample.py")
- out_abs = os.path.abspath(args.output)
print("\n[blender_export] Next step — pack EXRs into sample directories:")
print(f" for exr in {out_abs}/*.exr; do")
print(f' name=$(basename "${{exr%.exr}}")')
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")