Engine rewrite: - Remove fake Display P3 stage (sRGB->XYZ->P3 round-trip was a no-op hiding a plain saturation multiply) and redundant strength multipliers - Fix box blur: was a trailing window shifted diagonally with zero-padded borders; now centered with edge replication - Fix non-monotonic highlight soft-clip (banding above white) - White balance and tone mapping in linear light (luminance-preserving) - Smart HDR: Durand-style log-luminance base/detail local tone mapping - Color science in Oklab: hue-preserving vibrance, real skin-tone protection mask, subtle split-tone signature - Single two-scale detail stage (texture + clarity) with tanh halo suppression, replacing three overlapping sharpeners - Photon-weighted grain, natural-falloff vignette in linear light - 15 iPhone 17 Photographic Styles presets (moods + undertones + B&W) - Delete dead camera_profiles.py LUT engine: tetrahedral interpolation (industry standard) with chunked memory-safe application; document that the Apple Log LUT expects log input UI: rebuild for new params; sizing via getMinHeight/getMaxHeight + computeSize (no more guessed heights), remount safety net for workflow reload store clears, sanitize values restored by position from old saves Tests: 16-case suite covering blur centering, Oklab round-trip, styles, skin protection, tetrahedral exactness, node API Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
431 lines
18 KiB
Python
431 lines
18 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Camera Forensic Realism Engine v4 - Test Suite"""
|
|
|
|
import sys, os, io, time
|
|
|
|
if sys.platform == "win32":
|
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import numpy as np
|
|
|
|
|
|
def make_test_image(h=256, w=256):
|
|
img = np.zeros((h, w, 3), dtype=np.float32)
|
|
img[:, :, 0] = np.linspace(0.3, 0.8, w)[np.newaxis, :]
|
|
img[:, :, 1] = np.linspace(0.25, 0.65, w)[np.newaxis, :]
|
|
img[:, :, 2] = np.linspace(0.2, 0.5, w)[np.newaxis, :]
|
|
img[:h//4, :, :] = [0.7, 0.8, 0.95] # Sky highlight
|
|
img[3*h//4:, :, :] = [0.08, 0.06, 0.05] # Dark shadow
|
|
cy, cx = h // 2, w // 2
|
|
img[cy-30:cy+30, cx-30:cx+30, :] = [0.76, 0.57, 0.45] # Skin
|
|
return np.clip(img, 0.0, 1.0)
|
|
|
|
|
|
def check(out, shape, name):
|
|
ok = True
|
|
if out.shape != shape: print(f" [FAIL] {name}: shape {out.shape}"); ok = False
|
|
if out.dtype != np.float32: print(f" [FAIL] {name}: dtype {out.dtype}"); ok = False
|
|
if np.any(np.isnan(out)) or np.any(np.isinf(out)): print(f" [FAIL] {name}: NaN/Inf"); ok = False
|
|
if out.min() < 0 or out.max() > 1: print(f" [FAIL] {name}: range [{out.min():.4f},{out.max():.4f}]"); ok = False
|
|
if ok: print(f" [PASS] {name}")
|
|
return ok
|
|
|
|
|
|
def test_box_blur():
|
|
print("\n--- Test 1: Box Blur (centered, edge-safe) ---")
|
|
from forensic_engine import _box_blur
|
|
ok = True
|
|
# Constant image must stay constant (old version darkened borders)
|
|
const = np.full((64, 64), 0.5, dtype=np.float32)
|
|
r = _box_blur(const, 5)
|
|
if np.max(np.abs(r - 0.5)) > 1e-5:
|
|
print(f" [FAIL] Constant image changed (max err {np.max(np.abs(r-0.5)):.6f})"); ok = False
|
|
else:
|
|
print(" [PASS] Constant image unchanged (no border darkening)")
|
|
# Impulse response must be centered (old version shifted diagonally)
|
|
imp = np.zeros((65, 65), dtype=np.float32)
|
|
imp[32, 32] = 1.0
|
|
r = _box_blur(imp, 4)
|
|
cy, cx = np.unravel_index(np.argmax(r), r.shape)
|
|
if (cy, cx) != (32, 32):
|
|
# box is flat — check center of mass instead
|
|
ys, xs = np.nonzero(r)
|
|
cy, cx = ys.mean(), xs.mean()
|
|
if abs(cy - 32) > 0.51 or abs(cx - 32) > 0.51:
|
|
print(f" [FAIL] Blur not centered: peak at ({cy},{cx})"); ok = False
|
|
else:
|
|
print(f" [PASS] Blur centered at ({cy:.1f},{cx:.1f})")
|
|
return ok
|
|
|
|
|
|
def test_oklab_roundtrip():
|
|
print("\n--- Test 2: Oklab Round-Trip ---")
|
|
from forensic_engine import _linear_rgb_to_oklab, _oklab_to_linear_rgb
|
|
rng = np.random.default_rng(7)
|
|
rgb = rng.random((32, 32, 3)).astype(np.float32)
|
|
back = _oklab_to_linear_rgb(_linear_rgb_to_oklab(rgb))
|
|
err = np.max(np.abs(back - rgb))
|
|
if err < 1e-4:
|
|
print(f" [PASS] Round-trip max error {err:.2e}")
|
|
return True
|
|
print(f" [FAIL] Round-trip max error {err:.2e}")
|
|
return False
|
|
|
|
|
|
def test_white_balance():
|
|
print("\n--- Test 3: White Balance ---")
|
|
from forensic_engine import apply_white_balance, _luma
|
|
img = np.full((32, 32, 3), 0.25, dtype=np.float32)
|
|
r = apply_white_balance(img, 0.5, 0.0)
|
|
ok = True
|
|
if not (r[0, 0, 0] > img[0, 0, 0] and r[0, 0, 2] < img[0, 0, 2]):
|
|
print(" [FAIL] Warm temp should raise R, lower B"); ok = False
|
|
else:
|
|
print(f" [PASS] Warm WB (R: {img[0,0,0]:.3f}->{r[0,0,0]:.3f}, B: {img[0,0,2]:.3f}->{r[0,0,2]:.3f})")
|
|
lum_err = abs(float(_luma(r).mean()) - float(_luma(img).mean()))
|
|
if lum_err > 0.005:
|
|
print(f" [FAIL] Luminance not preserved (err {lum_err:.4f})"); ok = False
|
|
else:
|
|
print(f" [PASS] Luminance preserved (err {lum_err:.5f})")
|
|
return ok
|
|
|
|
|
|
def test_global_tone():
|
|
print("\n--- Test 4: Global Tone ---")
|
|
from forensic_engine import apply_global_tone
|
|
ramp = np.linspace(0, 1.4, 128, dtype=np.float32)
|
|
img = np.stack([ramp]*3, axis=-1)[np.newaxis, :, :]
|
|
r = apply_global_tone(img, exposure=0.0, contrast=0.2, shadows=0.4, highlights=0.6)
|
|
ok = True
|
|
# Shadows lifted
|
|
if r[0, 2, 0] <= img[0, 2, 0]:
|
|
print(" [FAIL] Shadows not lifted"); ok = False
|
|
else:
|
|
print(f" [PASS] Shadow lift ({img[0,2,0]:.4f} -> {r[0,2,0]:.4f})")
|
|
# Highlights compressed below input, never above 1
|
|
if r[0, -1, 0] >= img[0, -1, 0] or r.max() > 1.0 + 1e-5:
|
|
print(f" [FAIL] Highlight rolloff broken (in {img[0,-1,0]:.3f} out {r[0,-1,0]:.3f})"); ok = False
|
|
else:
|
|
print(f" [PASS] Highlight rolloff ({img[0,-1,0]:.3f} -> {r[0,-1,0]:.4f}, max {r.max():.4f})")
|
|
# Monotonic (the old soft-clip was non-monotonic above 1.0)
|
|
if np.any(np.diff(r[0, :, 0]) < -1e-6):
|
|
print(" [FAIL] Tone curve not monotonic"); ok = False
|
|
else:
|
|
print(" [PASS] Tone curve monotonic")
|
|
return ok
|
|
|
|
|
|
def test_smart_hdr():
|
|
print("\n--- Test 5: Smart HDR ---")
|
|
from forensic_engine import apply_smart_hdr, _srgb_to_linear, _luma
|
|
img = _srgb_to_linear(make_test_image())
|
|
r = apply_smart_hdr(img, 0.6)
|
|
ok = True
|
|
if np.any(np.isnan(r)) or np.any(np.isinf(r)):
|
|
print(" [FAIL] NaN/Inf"); ok = False
|
|
dark_before = float(_luma(img[200:, :, :]).mean())
|
|
dark_after = float(_luma(r[200:, :, :]).mean())
|
|
if dark_after > dark_before:
|
|
print(f" [PASS] Dark region lifted ({dark_before:.4f} -> {dark_after:.4f})")
|
|
else:
|
|
print(f" [FAIL] Dark region not lifted ({dark_before:.4f} -> {dark_after:.4f})"); ok = False
|
|
return ok
|
|
|
|
|
|
def test_color_science():
|
|
print("\n--- Test 6: Color Science (Oklab) ---")
|
|
from forensic_engine import (apply_color_science, _srgb_to_linear,
|
|
_linear_rgb_to_oklab)
|
|
ok = True
|
|
# Vibrance boosts chroma of a muted color
|
|
muted = _srgb_to_linear(np.full((16, 16, 3), [0.55, 0.5, 0.45], dtype=np.float32))
|
|
r = apply_color_science(muted, vibrance=0.8, skin_protection=0.0,
|
|
shadow_tint=0.0, highlight_warmth=0.0)
|
|
c0 = np.hypot(*_linear_rgb_to_oklab(muted)[0, 0, 1:])
|
|
c1 = np.hypot(*_linear_rgb_to_oklab(r)[0, 0, 1:])
|
|
if c1 > c0:
|
|
print(f" [PASS] Vibrance boosts muted chroma ({c0:.4f} -> {c1:.4f})")
|
|
else:
|
|
print(f" [FAIL] Vibrance had no effect ({c0:.4f} -> {c1:.4f})"); ok = False
|
|
# Skin protection attenuates the boost on skin colors
|
|
skin = _srgb_to_linear(np.full((16, 16, 3), [0.76, 0.57, 0.45], dtype=np.float32))
|
|
r_unprot = apply_color_science(skin, 0.8, 0.0, 0.0, 0.0)
|
|
r_prot = apply_color_science(skin, 0.8, 1.0, 0.0, 0.0)
|
|
d_unprot = float(np.abs(r_unprot - skin).mean())
|
|
d_prot = float(np.abs(r_prot - skin).mean())
|
|
if d_prot < d_unprot:
|
|
print(f" [PASS] Skin protection works (unprotected diff {d_unprot:.5f} > protected {d_prot:.5f})")
|
|
else:
|
|
print(f" [FAIL] Skin protection ineffective ({d_unprot:.5f} vs {d_prot:.5f})"); ok = False
|
|
# Split tone: shadows go blue (B up relative to R)
|
|
dark = _srgb_to_linear(np.full((16, 16, 3), 0.08, dtype=np.float32))
|
|
r = apply_color_science(dark, 0.0, 0.0, shadow_tint=1.0, highlight_warmth=0.0)
|
|
if r[0, 0, 2] > r[0, 0, 0]:
|
|
print(f" [PASS] Shadow tint cools shadows (R {r[0,0,0]:.4f} < B {r[0,0,2]:.4f})")
|
|
else:
|
|
print(f" [FAIL] Shadow tint missing (R {r[0,0,0]:.4f}, B {r[0,0,2]:.4f})"); ok = False
|
|
# Mono kills chroma
|
|
r = apply_color_science(skin, 0.5, 0.5, 0.0, 0.0, mono=True)
|
|
spread = float(np.max(r[0, 0]) - np.min(r[0, 0]))
|
|
if spread < 0.01:
|
|
print(f" [PASS] B&W rendering (channel spread {spread:.5f})")
|
|
else:
|
|
print(f" [FAIL] B&W rendering leaks color (spread {spread:.5f})"); ok = False
|
|
return ok
|
|
|
|
|
|
def test_detail():
|
|
print("\n--- Test 7: Detail ---")
|
|
from forensic_engine import apply_detail
|
|
img = make_test_image()
|
|
r = apply_detail(img, texture=0.8, clarity=0.4)
|
|
ok = check(r, img.shape, "Detail")
|
|
# Edge contrast across the shadow boundary should increase
|
|
edge_in = float(np.abs(np.diff(img[:, 128, 0])).max())
|
|
edge_out = float(np.abs(np.diff(r[:, 128, 0])).max())
|
|
if edge_out >= edge_in:
|
|
print(f" [PASS] Edge contrast increased ({edge_in:.4f} -> {edge_out:.4f})")
|
|
else:
|
|
print(f" [FAIL] No sharpening ({edge_in:.4f} -> {edge_out:.4f})"); ok = False
|
|
return ok
|
|
|
|
|
|
def test_optics():
|
|
print("\n--- Test 8: Optics & Sensor ---")
|
|
from forensic_engine import apply_vignette, apply_grain, _srgb_to_linear
|
|
ok = True
|
|
img = np.full((128, 128, 3), 0.5, dtype=np.float32)
|
|
v = apply_vignette(_srgb_to_linear(img), 0.6)
|
|
center = float(v[64, 64, 0]); corner = float(v[0, 0, 0])
|
|
if corner < center:
|
|
print(f" [PASS] Vignette darkens corners ({center:.4f} center vs {corner:.4f} corner)")
|
|
else:
|
|
print(f" [FAIL] Vignette broken"); ok = False
|
|
g1 = apply_grain(img, 0.5, seed=42)
|
|
g2 = apply_grain(img, 0.5, seed=42)
|
|
g3 = apply_grain(img, 0.5, seed=43)
|
|
if np.array_equal(g1, g2) and not np.array_equal(g1, g3):
|
|
print(" [PASS] Grain reproducible by seed")
|
|
else:
|
|
print(" [FAIL] Grain seeding broken"); ok = False
|
|
if np.abs(g1 - img).mean() > 1e-5:
|
|
print(f" [PASS] Grain applied (mean dev {np.abs(g1-img).mean():.5f})")
|
|
else:
|
|
print(" [FAIL] Grain had no effect"); ok = False
|
|
return ok
|
|
|
|
|
|
def test_styles():
|
|
print("\n--- Test 9: Photographic Styles ---")
|
|
from forensic_engine import PHOTOGRAPHIC_STYLES, resolve_style, process_iphone_realism
|
|
ok = True
|
|
expected = ["Standard", "Natural", "Vibrant", "Dramatic", "Amber", "Gold",
|
|
"Rose Gold", "Cool Rose", "Neutral", "Muted B&W", "Stark B&W"]
|
|
for name in expected:
|
|
if name not in PHOTOGRAPHIC_STYLES:
|
|
print(f" [FAIL] Missing style '{name}'"); ok = False
|
|
if ok:
|
|
print(f" [PASS] {len(PHOTOGRAPHIC_STYLES)} styles present")
|
|
p = resolve_style("Amber", {"wb_temperature": 0.12, "wb_tint": 0.0,
|
|
"exposure": 0.0, "contrast": 0.0, "shadows": 0.3,
|
|
"highlights": 0.5, "hdr_strength": 0.3,
|
|
"vibrance": 0.4, "skin_protection": 0.7,
|
|
"shadow_tint": 0.2, "highlight_warmth": 0.2,
|
|
"texture": 0.3, "clarity": 0.2,
|
|
"grain": 0.2, "vignette": 0.2})
|
|
if p["wb_temperature"] > 0.12:
|
|
print(f" [PASS] Amber warms temperature (0.12 -> {p['wb_temperature']:.2f})")
|
|
else:
|
|
print(" [FAIL] Amber offset not applied"); ok = False
|
|
img = make_test_image(96, 96)
|
|
std = process_iphone_realism(img, photographic_style="Standard", seed=1)
|
|
bw = process_iphone_realism(img, photographic_style="Stark B&W", seed=1)
|
|
chroma = float(np.abs(bw[:, :, 0] - bw[:, :, 1]).mean())
|
|
if not np.array_equal(std, bw) and chroma < 0.02:
|
|
print(f" [PASS] Styles change output; B&W is neutral (chroma {chroma:.4f})")
|
|
else:
|
|
print(f" [FAIL] Style differentiation broken (chroma {chroma:.4f})"); ok = False
|
|
return ok
|
|
|
|
|
|
def test_pipeline():
|
|
print("\n--- Test 10: Full Pipeline ---")
|
|
from forensic_engine import process_iphone_realism
|
|
img = make_test_image()
|
|
t0 = time.time()
|
|
r = process_iphone_realism(img, master_strength=0.85, seed=42)
|
|
elapsed = time.time() - t0
|
|
ok = check(r, img.shape, f"Pipeline ({elapsed:.2f}s)")
|
|
print(f" Total diff: {np.mean(np.abs(r-img)):.4f}")
|
|
# master_strength=0 must return the input untouched
|
|
r0 = process_iphone_realism(img, master_strength=0.0, seed=42)
|
|
if np.max(np.abs(r0 - img)) < 1e-5:
|
|
print(" [PASS] master_strength=0 is a true bypass")
|
|
else:
|
|
print(f" [FAIL] master_strength=0 changed image ({np.max(np.abs(r0-img)):.5f})"); ok = False
|
|
return ok
|
|
|
|
|
|
def test_node():
|
|
print("\n--- Test 11: ComfyUI Node ---")
|
|
try: import torch
|
|
except ImportError: print(" [SKIP] No PyTorch"); return True
|
|
from nodes import CameraForensicRealismEngine
|
|
node = CameraForensicRealismEngine()
|
|
inputs = node.INPUT_TYPES()["required"]
|
|
print(f" {len(inputs)} widgets")
|
|
for name in ["photographic_style", "vibrance", "skin_protection", "texture"]:
|
|
if name in inputs:
|
|
print(f" [PASS] '{name}' present")
|
|
else:
|
|
print(f" [FAIL] '{name}' MISSING"); return False
|
|
# Removed placebo/redundant params must be gone
|
|
for name in ["enable_p3_color", "color_saturation", "wb_strength",
|
|
"tone_strength", "sensor_strength", "fusion_strength"]:
|
|
if name in inputs:
|
|
print(f" [FAIL] Removed param '{name}' still present"); return False
|
|
print(" [PASS] Placebo/redundant params removed")
|
|
test_img = torch.rand(2, 128, 128, 3)
|
|
result = node.apply_iphone_realism(
|
|
image=test_img, photographic_style="Standard", master_strength=0.85, seed=42,
|
|
enable_white_balance=True, wb_temperature=0.12, wb_tint=0.0,
|
|
enable_tone=True, exposure=0.05, contrast=0.15, shadows=0.35, highlights=0.5,
|
|
enable_smart_hdr=True, hdr_strength=0.35,
|
|
enable_color=True, vibrance=0.4, skin_protection=0.7,
|
|
shadow_tint=0.25, highlight_warmth=0.2,
|
|
enable_detail=True, texture=0.35, clarity=0.2,
|
|
enable_optics=True, grain=0.2, vignette=0.25,
|
|
)
|
|
out = result[0]
|
|
if out.shape == test_img.shape: print(f" [PASS] Shape: {tuple(out.shape)}")
|
|
else: print(f" [FAIL] Shape"); return False
|
|
if 0 <= out.min() and out.max() <= 1: print(f" [PASS] Range: [{out.min():.4f}, {out.max():.4f}]")
|
|
else: print(f" [FAIL] Range"); return False
|
|
return True
|
|
|
|
|
|
def test_js_exists():
|
|
print("\n--- Test 12: Custom UI ---")
|
|
js_path = os.path.join(os.path.dirname(__file__), "js", "camera_forensic_ui.js")
|
|
if not os.path.exists(js_path):
|
|
print(" [FAIL] JS UI file missing")
|
|
return False
|
|
with open(js_path, encoding="utf-8") as f:
|
|
content = f.read()
|
|
ok = True
|
|
for name in ["photographic_style", "vibrance", "skin_protection"]:
|
|
if name not in content:
|
|
print(f" [FAIL] JS UI missing '{name}'"); ok = False
|
|
if "enable_p3_color" in content:
|
|
print(" [FAIL] JS UI still references removed params"); ok = False
|
|
if ok:
|
|
print(f" [PASS] JS UI in sync ({len(content)} chars)")
|
|
return ok
|
|
|
|
|
|
def _identity_lut(n=5):
|
|
ax = np.linspace(0, 1, n, dtype=np.float32)
|
|
b, g, r = np.meshgrid(ax, ax, ax, indexing='ij')
|
|
return np.stack([r, g, b], axis=-1) # indexed [b, g, r] -> (r, g, b)
|
|
|
|
|
|
def test_lut_parse():
|
|
print("\n--- Test 13: LUT Parse ---")
|
|
from lut_engine import parse_cube_file
|
|
lut_path = os.path.join(os.path.dirname(__file__), "luts", "AppleLog2_to_Rec709_33_Grid.cube")
|
|
if not os.path.exists(lut_path):
|
|
print(f" [FAIL] LUT file missing: {lut_path}"); return False
|
|
lut, dmin, dmax = parse_cube_file(lut_path)
|
|
ok = True
|
|
if lut.shape != (33, 33, 33, 3):
|
|
print(f" [FAIL] Shape: {lut.shape}"); ok = False
|
|
if np.any(np.isnan(lut)):
|
|
print(f" [FAIL] NaN in LUT"); ok = False
|
|
if ok: print(f" [PASS] LUT parsed: {lut.shape}")
|
|
return ok
|
|
|
|
|
|
def test_lut_tetrahedral():
|
|
print("\n--- Test 14: Tetrahedral LUT Interpolation ---")
|
|
from lut_engine import apply_lut_3d
|
|
dmin = np.zeros(3, dtype=np.float32)
|
|
dmax = np.ones(3, dtype=np.float32)
|
|
lut = _identity_lut(5)
|
|
rng = np.random.default_rng(11)
|
|
img = rng.random((64, 64, 3)).astype(np.float32)
|
|
r = apply_lut_3d(img, lut, dmin, dmax)
|
|
err = float(np.max(np.abs(r - img)))
|
|
# Tetrahedral interpolation on an identity lattice must be exact
|
|
if err < 1e-5:
|
|
print(f" [PASS] Identity LUT exact (max err {err:.2e})")
|
|
return True
|
|
print(f" [FAIL] Identity LUT error too large ({err:.2e})")
|
|
return False
|
|
|
|
|
|
def test_lut_strength():
|
|
print("\n--- Test 15: LUT Strength ---")
|
|
from lut_engine import parse_cube_file, apply_lut_with_strength
|
|
lut_path = os.path.join(os.path.dirname(__file__), "luts", "AppleLog2_to_Rec709_33_Grid.cube")
|
|
lut, dmin, dmax = parse_cube_file(lut_path)
|
|
img = make_test_image()
|
|
ok = True
|
|
r0 = apply_lut_with_strength(img, lut, dmin, dmax, 0.0)
|
|
if np.max(np.abs(r0 - img)) > 1e-6:
|
|
print(" [FAIL] Strength 0 changed image"); ok = False
|
|
else:
|
|
print(" [PASS] Strength 0.0 = original")
|
|
r05 = apply_lut_with_strength(img, lut, dmin, dmax, 0.5)
|
|
r10 = apply_lut_with_strength(img, lut, dmin, dmax, 1.0)
|
|
diff05 = np.mean(np.abs(r05 - img))
|
|
diff10 = np.mean(np.abs(r10 - img))
|
|
if diff05 < diff10 and diff10 > 1e-6:
|
|
print(f" [PASS] Strength scaling ({diff05:.4f} @ 0.5 < {diff10:.4f} @ 1.0)")
|
|
else:
|
|
print(" [FAIL] Strength scaling broken"); ok = False
|
|
return ok
|
|
|
|
|
|
def test_lut_nodes():
|
|
print("\n--- Test 16: LUT Nodes ---")
|
|
from nodes import LUTLoader, LUTApply
|
|
ok = True
|
|
loader_inputs = LUTLoader.INPUT_TYPES()["required"]
|
|
if "lut_name" in loader_inputs:
|
|
print(" [PASS] LUTLoader has 'lut_name' input")
|
|
else:
|
|
print(" [FAIL] LUTLoader missing 'lut_name'"); ok = False
|
|
apply_inputs = LUTApply.INPUT_TYPES()["required"]
|
|
for name in ["image", "lut_data", "strength"]:
|
|
if name in apply_inputs:
|
|
print(f" [PASS] LUTApply has '{name}' input")
|
|
else:
|
|
print(f" [FAIL] LUTApply missing '{name}'"); ok = False
|
|
return ok
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print("=" * 60)
|
|
print("Camera Forensic Realism Engine v4 - Test Suite")
|
|
print("=" * 60)
|
|
tests = [test_box_blur, test_oklab_roundtrip, test_white_balance,
|
|
test_global_tone, test_smart_hdr, test_color_science,
|
|
test_detail, test_optics, test_styles, test_pipeline,
|
|
test_node, test_js_exists,
|
|
test_lut_parse, test_lut_tetrahedral, test_lut_strength,
|
|
test_lut_nodes]
|
|
results = []
|
|
for t in tests:
|
|
try: results.append(t())
|
|
except Exception as e:
|
|
print(f" [EXCEPTION] {e}")
|
|
import traceback; traceback.print_exc()
|
|
results.append(False)
|
|
print("\n" + "=" * 60)
|
|
p = sum(1 for r in results if r)
|
|
print(f"Results: {p}/{len(results)} tests passed")
|
|
print("ALL PASSED!" if p == len(results) else "Some failed")
|
|
print("=" * 60)
|