Sweep possible now for key

This commit is contained in:
2026-07-23 14:56:10 +02:00
parent 715a36cf6e
commit a3919f595a
+53 -60
View File
@@ -4847,71 +4847,63 @@ def _grade_write_manifest(out_dir: Path, manifest: dict) -> Path:
def _stage_key(project_path, filters, ref, out_dir, source_name, ss) -> int:
"""Auto matte search (objective) → one recommended candidate + manifest.
Coordinate descent over the matte fields, minimising 'partial' (unresolved
green fringe) subject to keeping the subject opaque — a candidate that drops
the opaque fraction below baseline (over-keying eats the subject) is rejected.
"""
def clamp(v, lo, hi):
return max(lo, min(hi, v))
"""Deterministic aggressiveness sweep → 10 matte variants to pick from.
An objective search always prefers the most aggressive key (lowest fringe),
which erodes edges and eats low-saturation subject pixels (e.g. eyes go
transparent/magenta). So instead of auto-picking, this ramps the three
eroding matte knobs together — screen_gain, shadow_boost, clip_black — from
gentle (subject fully intact, maybe faint background residue) to aggressive
(background gone but subject eroding), and lets the eye judge. The opaque
column falls as the key eats the subject; pick the balance before that."""
base = _grade_step(filters, "gnommokey")
cur = dict(base)
bt, bo, bp = _eval_alpha(ref, _mask_and(filters, cur))
floor = bo * 0.98
print(f" Baseline: transparent {bt*100:.1f}% opaque {bo*100:.1f}% partial {bp*100:.1f}%")
g0, b0 = float(cur.get("screen_gain", 100)), float(cur.get("screen_balance", 50))
grids = {
"screen_gain": sorted({int(clamp(g0 * f, 80, 300)) for f in (0.7, 0.85, 1.0, 1.2, 1.4, 1.7)}),
"screen_balance": sorted({int(clamp(b0 + d, 0, 100)) for d in (-20, -10, 0, 10, 20)}),
"shadow_boost": [0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0],
"clip_black": [0, 2, 4, 6, 8, 10, 12],
}
def better(cand, best):
ct, co, cp = cand
bt_, bo_, bp_ = best
cok, bok = co >= floor, bo_ >= floor
if cok != bok:
return cok
if abs(cp - bp_) > 1e-6:
return cp < bp_
return ct > bt_
best = (bt, bo, bp)
for _ in range(2):
for param, values in grids.items():
lb_cfg, lb = dict(cur), best
for v in values:
cand = dict(cur)
cand[param] = v
s = _eval_alpha(ref, _mask_and(filters, cand))
if better(s, lb):
lb, lb_cfg = s, cand
cur, best = lb_cfg, lb
ft, fo, fp = best
params = {k: cur[k] for k in ("screen_gain", "screen_balance", "shadow_boost", "clip_black") if k in cur}
png = out_dir / "key_1.png"
_render_preview(ref, _mask_and(filters, cur), png)
steps = [round(i / 9, 2) for i in range(10)] # aggressiveness 0.0 .. 1.0
print(" Sweeping key aggressiveness 0.0 → 1.0 (gain 120→280, shadow_boost 0→3, clip_black 0→12)")
candidates = []
for i, a in enumerate(steps, 1):
params = {
"screen_gain": int(round(120 + 160 * a)),
"shadow_boost": round(3.0 * a, 2),
"clip_black": int(round(12 * a)),
}
cfg = dict(base, **params)
t, o, p = _eval_alpha(ref, _mask_and(filters, cfg))
png = out_dir / f"key_{i}.png"
_render_preview(ref, _mask_and(filters, cfg), png)
params["aggressiveness"] = a
candidates.append({
"id": f"key_{i}", "file": png.name, "params": params,
"hint": {"transparent_pct": round(t * 100, 1),
"opaque_pct": round(o * 100, 1),
"partial_pct": round(p * 100, 1)},
})
# Recommend the *gentlest* setting that has essentially cleared the background
# — the knee where transparency stops climbing. Past it, more aggression just
# erodes the subject, which is exactly what we're trying to avoid.
rec = candidates[len(candidates) // 3]["id"]
for i in range(1, len(candidates)):
dt = candidates[i]["hint"]["transparent_pct"] - candidates[i - 1]["hint"]["transparent_pct"]
if dt < 0.3:
rec = candidates[i - 1]["id"]
break
manifest = {
"stage": "key", "target_step": "gnommokey",
"source": source_name, "ss": round(ss, 2), "swept": list(params.keys()),
"recommended": "key_1",
"candidates": [{
"id": "key_1", "file": png.name, "params": params,
"hint": {"transparent_pct": round(ft * 100, 1), "partial_pct": round(fp * 100, 1)},
}],
"source": source_name, "ss": round(ss, 2),
"swept": ["screen_gain", "shadow_boost", "clip_black"],
"recommended": rec, "candidates": candidates,
}
mpath = _grade_write_manifest(out_dir, manifest)
print(f"\n Selected (partial {bp*100:.1f}% → {fp*100:.1f}%, subject held at {fo*100:.1f}%):")
for k, v in params.items():
print(f" {k}: {base.get(k, '')}{v}")
print(f" Preview: {png}")
print(f" {'id':>8} {'aggr':>5} {'gain':>4} {'shadow':>6} {'transp':>7} {'opaque':>7} {'partial':>7} png")
print(f" {'-'*8} {'-'*5} {'-'*4} {'-'*6} {'-'*7} {'-'*7} {'-'*7} {'-'*3}")
for c in candidates:
star = " ◀ suggested" if c["id"] == rec else ""
pr, h = c["params"], c["hint"]
print(f" {c['id']:>8} {pr['aggressiveness']:>5} {pr['screen_gain']:>4} {pr['shadow_boost']:>6} "
f"{h['transparent_pct']:>6}% {h['opaque_pct']:>6}% {h['partial_pct']:>6}% {c['file']}{star}")
print("\n opaque falls as the key eats the subject (eroded edges, keyed eyes) — pick just before that.")
print(f" Manifest: {mpath}")
print(f" Apply: gnommo -p {project_path.name} grade --pick key_1")
print(" Pick the gentlest variant that clears the background without eroding the head/eyes:")
print(f" gnommo -p {project_path.name} grade --pick key_4")
return 0
@@ -5040,8 +5032,9 @@ def _grade_pick(project_path, stage_hint, pick) -> int:
if cand is None:
print(f" ERROR: candidate '{cand_id}' not in {mpath.name}.")
return 1
# 'look'/'paleness' are UI-only dials, not real color_grade fields.
params = {k: v for k, v in cand["params"].items() if k not in ("look", "paleness")}
# 'look'/'paleness'/'aggressiveness' are UI-only dials, not real fields.
params = {k: v for k, v in cand["params"].items()
if k not in ("look", "paleness", "aggressiveness")}
_apply_candidate_to_project(project_path, manifest["target_step"], params)
print(f" Applied {cand_id} → project.json ({manifest['target_step']}): "
+ ", ".join(f"{k}={v}" for k, v in params.items()))