Adding yellow tint control
This commit is contained in:
+54
-33
@@ -4757,6 +4757,21 @@ def _measure_skin(ref: Path, filters: "list[dict]") -> "tuple[int, int, int]":
|
||||
return (sr // n, sg // n, sb // n) if n else (0, 0, 0)
|
||||
|
||||
|
||||
def _measure_suit(ref: Path, filters: "list[dict]") -> "tuple[int, int, int]":
|
||||
"""Mean (R,G,B) of yellow-costume pixels (opaque, high R&G, low B). R−G is
|
||||
the 'orange-ness': ~0 = pure yellow, larger = more orange."""
|
||||
vf = _grade_video_filter(filters)
|
||||
cmd = ["ffmpeg", "-v", "error", "-i", str(ref), "-frames:v", "1",
|
||||
"-vf", f"{vf},format=rgba", "-f", "rawvideo", "-pix_fmt", "rgba", "-"]
|
||||
raw = subprocess.run(cmd, capture_output=True).stdout
|
||||
sr = sg = sb = n = 0
|
||||
for i in range(0, len(raw) - 3, 4 * 7):
|
||||
r, g, b, a = raw[i], raw[i + 1], raw[i + 2], raw[i + 3]
|
||||
if a > 200 and r > 120 and g > 90 and b < 90 and r > b and g > b:
|
||||
sr += r; sg += g; sb += b; n += 1
|
||||
return (sr // n, sg // n, sb // n) if n else (0, 0, 0)
|
||||
|
||||
|
||||
def _render_preview(ref: Path, filters: "list[dict]", out_png: Path) -> None:
|
||||
"""Save the filtered still composited over magenta (to judge the matte)."""
|
||||
vf = _grade_video_filter(filters)
|
||||
@@ -4885,50 +4900,56 @@ def _stage_despill(project_path, filters, ref, out_dir, source_name, ss) -> int:
|
||||
|
||||
|
||||
def _stage_grade(project_path, filters, ref, out_dir, source_name, ss) -> int:
|
||||
"""Deterministic 'look' sweep 0.0–1.0 → punchy color_grade candidates.
|
||||
"""Deterministic look × yellow-tint grid → punchy color_grade candidates.
|
||||
|
||||
The dial ramps a Photoshop-auto-levels-style contrast/levels stretch plus
|
||||
vividness (saturation) and a slight pale lean, from flat (0) to strong (1),
|
||||
so the candidates differ boldly instead of by a hair."""
|
||||
Two axes: 'look' ramps the auto-levels/vividness punch (flat → strong), and
|
||||
'yellow_tint' hue-selectively pulls the yellow costume back from the orange
|
||||
that punch introduces (negative = greener/preserve, 0 = as-is). Skin (reds)
|
||||
is untouched by the tint, so you can crank vibrance and keep the suit yellow."""
|
||||
key = _grade_step(filters, "gnommokey")
|
||||
values = [round(i / 5, 1) for i in range(6)] # look 0.0 .. 1.0
|
||||
print(" Sweeping look (auto-levels + vividness) 0.0 → 1.0")
|
||||
looks = [0.4, 0.7, 1.0]
|
||||
tints = [0.0, -0.4, -0.8] # 0 = as-is, negative = pull yellows back from orange
|
||||
print(" Grid: look {0.4, 0.7, 1.0} × yellow_tint {0.0, -0.4, -0.8}")
|
||||
candidates = []
|
||||
for i, p in enumerate(values, 1):
|
||||
cg = {
|
||||
"type": "color_grade",
|
||||
"auto_levels": round(p, 3), # levels/contrast punch
|
||||
"saturation": round(1.0 + 0.5 * p, 3), # 1.0 → 1.5 vivid
|
||||
"contrast": round(1.0 + 0.15 * p, 3), # gentle, colorlevels adds the rest
|
||||
"brightness": round(0.02 * p, 3),
|
||||
"rm": round(-0.04 * p, 3), # a touch paler/cooler
|
||||
}
|
||||
chain = _mask_and(filters, key, cg)
|
||||
skin = _measure_skin(ref, chain)
|
||||
png = out_dir / f"grade_{i}.png"
|
||||
_render_preview(ref, chain, png)
|
||||
params = {k: cg[k] for k in ("auto_levels", "saturation", "contrast", "brightness", "rm")}
|
||||
params["look"] = p
|
||||
candidates.append({
|
||||
"id": f"grade_{i}", "file": png.name, "params": params,
|
||||
"hint": {"skin_rgb": list(skin), "warmth": skin[0] - skin[2]},
|
||||
})
|
||||
i = 0
|
||||
for p in looks:
|
||||
for yt in tints:
|
||||
i += 1
|
||||
cg = {
|
||||
"type": "color_grade",
|
||||
"auto_levels": round(p, 3),
|
||||
"saturation": round(1.0 + 0.5 * p, 3),
|
||||
"contrast": round(1.0 + 0.15 * p, 3),
|
||||
"brightness": round(0.02 * p, 3),
|
||||
"yellow_tint": yt,
|
||||
}
|
||||
chain = _mask_and(filters, key, cg)
|
||||
suit = _measure_suit(ref, chain)
|
||||
png = out_dir / f"grade_{i}.png"
|
||||
_render_preview(ref, chain, png)
|
||||
params = {k: cg[k] for k in ("auto_levels", "saturation", "contrast", "brightness", "yellow_tint")}
|
||||
params["look"] = p
|
||||
candidates.append({
|
||||
"id": f"grade_{i}", "file": png.name, "params": params,
|
||||
"hint": {"suit_rgb": list(suit), "orange": suit[0] - suit[1]},
|
||||
})
|
||||
manifest = {
|
||||
"stage": "grade", "target_step": "color_grade",
|
||||
"source": source_name, "ss": round(ss, 2), "swept": ["look"],
|
||||
"recommended": "grade_4", "candidates": candidates,
|
||||
"source": source_name, "ss": round(ss, 2), "swept": ["look", "yellow_tint"],
|
||||
"recommended": "grade_8", "candidates": candidates, # look 1.0, tint -0.4
|
||||
}
|
||||
mpath = _grade_write_manifest(out_dir, manifest)
|
||||
rec = manifest["recommended"]
|
||||
print(f" {'id':>10} {'look':>5} {'sat':>5} {'skin RGB':>17} {'warmth':>6} png")
|
||||
print(f" {'id':>10} {'look':>5} {'tint':>5} {'suit RGB':>17} {'orange':>6} png")
|
||||
print(f" {'-'*10} {'-'*5} {'-'*5} {'-'*17} {'-'*6} {'-'*3}")
|
||||
for c in candidates:
|
||||
star = " ◀ suggested" if c["id"] == rec else ""
|
||||
print(f" {c['id']:>10} {c['params']['look']:>5} {c['params']['saturation']:>5} "
|
||||
f"{str(tuple(c['hint']['skin_rgb'])):>17} {c['hint']['warmth']:>6} {c['file']}{star}")
|
||||
print(f"\n Manifest: {mpath}")
|
||||
print(" Pick the look you like (flat → punchy):")
|
||||
print(f" gnommo -p {project_path.name} grade --pick grade_4")
|
||||
print(f" {c['id']:>10} {c['params']['look']:>5} {c['params']['yellow_tint']:>5} "
|
||||
f"{str(tuple(c['hint']['suit_rgb'])):>17} {c['hint']['orange']:>6} {c['file']}{star}")
|
||||
print("\n orange = suit R−G: lower is more yellow, higher is more orange")
|
||||
print(f" Manifest: {mpath}")
|
||||
print(" Pick punch (look) + a tint that keeps the suit yellow:")
|
||||
print(f" gnommo -p {project_path.name} grade --pick grade_8")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -202,6 +202,12 @@ class ColorGradeConfig:
|
||||
# window to full range, so the keyed-out background never skews it.
|
||||
auto_levels: float = 0.0
|
||||
|
||||
# Yellow tint: hue-selective nudge of ONLY the yellow range (leaves reds/skin
|
||||
# alone), to counter the orange shift auto-levels/saturation gives a yellow
|
||||
# costume. <0 pulls yellows back toward green/pure yellow (preserve), >0
|
||||
# pushes them warmer/orange. Range roughly -1.0..1.0. 0 = off.
|
||||
yellow_tint: float = 0.0
|
||||
|
||||
# Custom curves for lift/gamma/gain control
|
||||
# Format: "0/0 0.5/0.56 1/1" means (input/output) control points
|
||||
curves_r: str = "" # Red channel curve
|
||||
|
||||
@@ -1057,6 +1057,16 @@ def build_color_grade_filter(config: dict) -> str:
|
||||
if eq_parts:
|
||||
parts.append(f"eq={':'.join(eq_parts)}")
|
||||
|
||||
# Yellow tint: hue-selective correction of the yellow range only (skin, in
|
||||
# the reds, is untouched). In CMYK terms a yellow's "orange-ness" is its
|
||||
# magenta content, so we push magenta up (warmer/orange) or down (greener,
|
||||
# preserving the costume yellow) proportional to yellow_tint.
|
||||
if grade_config.yellow_tint != 0:
|
||||
yt = max(-1.0, min(1.0, grade_config.yellow_tint))
|
||||
magenta = round(0.6 * yt, 3) # red content of the yellow range
|
||||
yellow = round(0.2 * yt, 3) # a little saturation follow so it reads
|
||||
parts.append(f"selectivecolor=yellows=0 {magenta} {yellow} 0")
|
||||
|
||||
# Custom curves (if specified)
|
||||
custom_curves = []
|
||||
if grade_config.curves_r:
|
||||
@@ -1099,6 +1109,7 @@ def parse_color_grade_config(config: dict) -> ColorGradeConfig:
|
||||
brightness=float(config.get("brightness", 0.0)),
|
||||
saturation=float(config.get("saturation", 1.0)),
|
||||
auto_levels=float(config.get("auto_levels", 0.0)),
|
||||
yellow_tint=float(config.get("yellow_tint", 0.0)),
|
||||
# Custom curves
|
||||
curves_r=config.get("curves_r", ""),
|
||||
curves_g=config.get("curves_g", ""),
|
||||
|
||||
Reference in New Issue
Block a user