From 316441ccb89c28232332664b5caef8cbcf076431 Mon Sep 17 00:00:00 2001 From: jenstandstad Date: Thu, 16 Jul 2026 13:03:06 +0200 Subject: [PATCH] Better grading of the jumpsuit --- README.md | 9 +++++++++ gnommo/models.py | 8 ++++++++ gnommo/preprocessor.py | 22 ++++++++++++++++++++-- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c4a9b6d..3a155c0 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,7 @@ Keylight-style color-difference keyer) with a `color_grade` step. Key fields: | `despill_bias` | RGB the keyed *edges* shift toward. A light neutral/skin tone reads better than cool blue. | | `despill_strength` | How hard the edge/dominant-green despill pulls toward `despill_bias`. | | `spill_suppress` | **Interior green-limiter — see below.** | +| `yellow_protect` | Shields saturated yellows/warm fabrics from `spill_suppress` (0–1). | | `edge_erode` | Shrinks the alpha edge by N passes to kill green fringe (0–5). | **`spill_suppress` — the bald-head knob.** The regular despill only acts where @@ -158,6 +159,14 @@ Green is only ever *reduced*, never boosted, so clean pixels are untouched. Start around `0.6`; go past `1.0` for heavy close-up spill. If skin tips magenta/pink, ease back down. Range is `0.0`–`2.0`. +**`yellow_protect` — keep yellows yellow.** Because `spill_suppress` caps green +*everywhere*, it also drains green from things that are legitimately yellow (a +yellow jumpsuit, warm props), turning them orange. Blue is the tell: skin keeps +some blue, saturated yellow fabric reflects almost none. `yellow_protect` +(0.0–1.0) gates the green-limiter back down where `min(r,g) − b` is high +(yellow) while leaving skin/scalp spill fully suppressed. Bump it toward `1.0` +if warm colours go orange; leave at `0` if you have no strong yellows. + #### Grade preview (`grade`) Iterate on keying/grading without running a full preprocess. It seeks a few diff --git a/gnommo/models.py b/gnommo/models.py index d4f4c99..77903d9 100644 --- a/gnommo/models.py +++ b/gnommo/models.py @@ -140,6 +140,14 @@ class GnommoKeyConfig: # close-up spill (2.0 = green can never exceed the smallest channel). spill_suppress: float = 0.0 + # Protect saturated yellows/warm fabrics from spill_suppress (0.0-1.0, 0 = off). + # spill_suppress caps green everywhere, which turns legit yellow (high r+g, + # low b) into orange. Blue is the tell: skin keeps some blue, yellow fabric + # reflects almost none. This gates the green-limiter down where + # min(r,g)-b is high (yellow) while leaving skin/scalp spill fully suppressed. + # 1.0 = full protection for strong yellows. Only matters when spill_suppress>0. + yellow_protect: float = 0.0 + # Alpha bias: influences edge treatment (RGB) # Can help with edge color contamination alpha_bias: tuple[int, int, int] = None diff --git a/gnommo/preprocessor.py b/gnommo/preprocessor.py index 62a270f..568ccd9 100644 --- a/gnommo/preprocessor.py +++ b/gnommo/preprocessor.py @@ -1215,13 +1215,30 @@ def build_gnommokey_filter(config: dict) -> str: # boosted, so non-spilled pixels are untouched. if cfg.spill_suppress > 0: t = min(max(cfg.spill_suppress, 0.0), 2.0) + yp = min(max(cfg.yellow_protect, 0.0), 1.0) + + def _protected(spill_ch: str, capped: str, warm_ch: str, cold_ch: str) -> str: + """Blend the capped spill channel back toward its original value for + saturated hues that legitimately pair the spill channel with a warm + channel while the cold channel is low (e.g. yellow = green+red, no + blue). Skin/scalp keeps some cold channel, so it stays suppressed.""" + if yp <= 0: + return capped + # "yellowness": high when spill & warm are both high but cold is low. + hue = f"max(0,min({spill_ch},{warm_ch})-{cold_ch})" + gate = f"clip(({hue}-40)/120,0,1)" + protect = f"({gate}*{yp:.3f})" + return f"({spill_ch}*{protect}+({capped})*(1-{protect}))" + if is_green_screen: ref = f"((1-{t:.3f})*max(r(X,Y),b(X,Y))+{t:.3f}*(r(X,Y)+b(X,Y))/2)" - new_g = f"min(g(X,Y),{ref})" + capped = f"min(g(X,Y),{ref})" + new_g = _protected("g(X,Y)", capped, "r(X,Y)", "b(X,Y)") parts.append(f"geq=r='r(X,Y)':g='{new_g}':b='b(X,Y)':a='alpha(X,Y)'") else: ref = f"((1-{t:.3f})*max(r(X,Y),g(X,Y))+{t:.3f}*(r(X,Y)+g(X,Y))/2)" - new_b = f"min(b(X,Y),{ref})" + capped = f"min(b(X,Y),{ref})" + new_b = _protected("b(X,Y)", capped, "r(X,Y)", "g(X,Y)") parts.append(f"geq=r='r(X,Y)':g='g(X,Y)':b='{new_b}':a='alpha(X,Y)'") # Edge-aware despill: aggressively suppress green at semi-transparent edges @@ -1311,6 +1328,7 @@ def parse_gnommokey_config(config: dict) -> GnommoKeyConfig: despill_bias=despill_bias, despill_strength=float(config.get("despill_strength", 0.5)), spill_suppress=float(config.get("spill_suppress", 0.0)), + yellow_protect=float(config.get("yellow_protect", 0.0)), alpha_bias=alpha_bias, protect_luma=int(config.get("protect_luma", -1)), shadow_boost=float(config.get("shadow_boost", 0.0)),