Sweep possible now for key
This commit is contained in:
+53
-60
@@ -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()))
|
||||||
|
|||||||
Reference in New Issue
Block a user