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: def _stage_key(project_path, filters, ref, out_dir, source_name, ss) -> int:
"""Auto matte search (objective) → one recommended candidate + manifest. """Deterministic aggressiveness sweep → 10 matte variants to pick from.
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))
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") base = _grade_step(filters, "gnommokey")
cur = dict(base) steps = [round(i / 9, 2) for i in range(10)] # aggressiveness 0.0 .. 1.0
bt, bo, bp = _eval_alpha(ref, _mask_and(filters, cur)) print(" Sweeping key aggressiveness 0.0 → 1.0 (gain 120→280, shadow_boost 0→3, clip_black 0→12)")
floor = bo * 0.98 candidates = []
print(f" Baseline: transparent {bt*100:.1f}% opaque {bo*100:.1f}% partial {bp*100:.1f}%") for i, a in enumerate(steps, 1):
params = {
g0, b0 = float(cur.get("screen_gain", 100)), float(cur.get("screen_balance", 50)) "screen_gain": int(round(120 + 160 * a)),
grids = { "shadow_boost": round(3.0 * a, 2),
"screen_gain": sorted({int(clamp(g0 * f, 80, 300)) for f in (0.7, 0.85, 1.0, 1.2, 1.4, 1.7)}), "clip_black": int(round(12 * a)),
"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], cfg = dict(base, **params)
"clip_black": [0, 2, 4, 6, 8, 10, 12], 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)
def better(cand, best): params["aggressiveness"] = a
ct, co, cp = cand candidates.append({
bt_, bo_, bp_ = best "id": f"key_{i}", "file": png.name, "params": params,
cok, bok = co >= floor, bo_ >= floor "hint": {"transparent_pct": round(t * 100, 1),
if cok != bok: "opaque_pct": round(o * 100, 1),
return cok "partial_pct": round(p * 100, 1)},
if abs(cp - bp_) > 1e-6: })
return cp < bp_ # Recommend the *gentlest* setting that has essentially cleared the background
return ct > bt_ # — the knee where transparency stops climbing. Past it, more aggression just
# erodes the subject, which is exactly what we're trying to avoid.
best = (bt, bo, bp) rec = candidates[len(candidates) // 3]["id"]
for _ in range(2): for i in range(1, len(candidates)):
for param, values in grids.items(): dt = candidates[i]["hint"]["transparent_pct"] - candidates[i - 1]["hint"]["transparent_pct"]
lb_cfg, lb = dict(cur), best if dt < 0.3:
for v in values: rec = candidates[i - 1]["id"]
cand = dict(cur) break
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)
manifest = { manifest = {
"stage": "key", "target_step": "gnommokey", "stage": "key", "target_step": "gnommokey",
"source": source_name, "ss": round(ss, 2), "swept": list(params.keys()), "source": source_name, "ss": round(ss, 2),
"recommended": "key_1", "swept": ["screen_gain", "shadow_boost", "clip_black"],
"candidates": [{ "recommended": rec, "candidates": candidates,
"id": "key_1", "file": png.name, "params": params,
"hint": {"transparent_pct": round(ft * 100, 1), "partial_pct": round(fp * 100, 1)},
}],
} }
mpath = _grade_write_manifest(out_dir, manifest) mpath = _grade_write_manifest(out_dir, manifest)
print(f"\n Selected (partial {bp*100:.1f}% → {fp*100:.1f}%, subject held at {fo*100:.1f}%):") print(f" {'id':>8} {'aggr':>5} {'gain':>4} {'shadow':>6} {'transp':>7} {'opaque':>7} {'partial':>7} png")
for k, v in params.items(): print(f" {'-'*8} {'-'*5} {'-'*4} {'-'*6} {'-'*7} {'-'*7} {'-'*7} {'-'*3}")
print(f" {k}: {base.get(k, '')}{v}") for c in candidates:
print(f" Preview: {png}") 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" 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 return 0
@@ -5040,8 +5032,9 @@ def _grade_pick(project_path, stage_hint, pick) -> int:
if cand is None: if cand is None:
print(f" ERROR: candidate '{cand_id}' not in {mpath.name}.") print(f" ERROR: candidate '{cand_id}' not in {mpath.name}.")
return 1 return 1
# 'look'/'paleness' are UI-only dials, not real color_grade fields. # '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")} 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) _apply_candidate_to_project(project_path, manifest["target_step"], params)
print(f" Applied {cand_id} → project.json ({manifest['target_step']}): " print(f" Applied {cand_id} → project.json ({manifest['target_step']}): "
+ ", ".join(f"{k}={v}" for k, v in params.items())) + ", ".join(f"{k}={v}" for k, v in params.items()))