Adding grading sweep and autokeying
This commit is contained in:
@@ -111,6 +111,29 @@ gnommo -p myproject import
|
||||
|
||||
---
|
||||
|
||||
### Stage 3: Grading
|
||||
|
||||
Iterate on keying/grading without running a full preprocess. It seeks a few
|
||||
seconds into a raw clip, runs it through the `talkinghead` filter chain, and
|
||||
writes `grade_preview.mov` (ProRes 4444 with alpha) to the project root.
|
||||
|
||||
# 1. KEY — auto, objective. Generates a candidate + preview, applies on pick.
|
||||
gnommo -p video3 grade --stage key
|
||||
open video3/grade_sweep/key_1.png # sanity-check the matte
|
||||
gnommo -p video3 grade --pick key_1 # writes it to project.json
|
||||
|
||||
# 2. DESPILL — sweep, YOU pick. 7 stills, spill_suppress 0.7–1.5.
|
||||
gnommo -p video3 grade --stage despill
|
||||
open video3/grade_sweep/ # eyeball despill_1..7.png
|
||||
gnommo -p video3 grade --pick despill_5 # apply whichever looks clean
|
||||
|
||||
# 3. GRADE — sweep, YOU pick. 6 stills, paleness 0.0–1.0.
|
||||
gnommo -p video3 grade --stage grade
|
||||
open video3/grade_sweep/ # eyeball grade_1..6.png
|
||||
gnommo -p video3 grade --pick grade_3 # apply the paleness you like
|
||||
|
||||
|
||||
|
||||
### Stage 3: Preprocess
|
||||
|
||||
Applies video filters (chroma key, scaling, etc.) to narration segments.
|
||||
@@ -167,11 +190,7 @@ some blue, saturated yellow fabric reflects almost none. `yellow_protect`
|
||||
(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
|
||||
seconds into a raw clip, runs it through the `talkinghead` filter chain, and
|
||||
writes `grade_preview.mov` (ProRes 4444 with alpha) to the project root.
|
||||
|
||||
```bash
|
||||
gnommo -p myproject grade # first raw_mov clip, 3s from 5s in
|
||||
|
||||
+26
-19
@@ -4885,43 +4885,50 @@ 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 paleness sweep 0.0–1.0 → color_grade candidates to pick."""
|
||||
base_cg = _grade_step(filters, "color_grade") or {}
|
||||
"""Deterministic 'look' sweep 0.0–1.0 → 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."""
|
||||
key = _grade_step(filters, "gnommokey")
|
||||
contrast = float(base_cg.get("contrast", 1.05))
|
||||
values = [round(i / 5, 1) for i in range(6)] # paleness 0.0 .. 1.0
|
||||
print(" Sweeping paleness 0.0 → 1.0")
|
||||
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")
|
||||
candidates = []
|
||||
for i, p in enumerate(values, 1):
|
||||
cg = {"type": "color_grade", "contrast": contrast,
|
||||
"rm": round(-0.10 * p, 3), "gm": round(0.02 * p, 3),
|
||||
"saturation": round(1.0 - 0.15 * p, 3), "brightness": round(0.05 * p, 3)}
|
||||
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 ("rm", "gm", "saturation", "brightness", "contrast")}
|
||||
params["paleness"] = p
|
||||
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]},
|
||||
})
|
||||
manifest = {
|
||||
"stage": "grade", "target_step": "color_grade",
|
||||
"source": source_name, "ss": round(ss, 2), "swept": ["paleness"],
|
||||
"recommended": "grade_3", "candidates": candidates,
|
||||
"source": source_name, "ss": round(ss, 2), "swept": ["look"],
|
||||
"recommended": "grade_4", "candidates": candidates,
|
||||
}
|
||||
mpath = _grade_write_manifest(out_dir, manifest)
|
||||
rec = manifest["recommended"]
|
||||
print(f" {'id':>10} {'paleness':>8} {'skin RGB':>17} {'warmth':>6} png")
|
||||
print(f" {'-'*10} {'-'*8} {'-'*17} {'-'*6} {'-'*3}")
|
||||
print(f" {'id':>10} {'look':>5} {'sat':>5} {'skin RGB':>17} {'warmth':>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']['paleness']:>8} "
|
||||
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 paleness you like:")
|
||||
print(f" gnommo -p {project_path.name} grade --pick grade_3")
|
||||
print(" Pick the look you like (flat → punchy):")
|
||||
print(f" gnommo -p {project_path.name} grade --pick grade_4")
|
||||
return 0
|
||||
|
||||
|
||||
@@ -4948,8 +4955,8 @@ 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
|
||||
# 'paleness' is a UI-only dial, not a real color_grade field.
|
||||
params = {k: v for k, v in cand["params"].items() if k != "paleness"}
|
||||
# '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")}
|
||||
_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()))
|
||||
|
||||
@@ -196,6 +196,12 @@ class ColorGradeConfig:
|
||||
brightness: float = 0.0 # Brightness adjustment (-1.0 to 1.0, 0 = no change)
|
||||
saturation: float = 1.0 # Saturation multiplier (0.0-3.0, 1.0 = no change)
|
||||
|
||||
# Auto-levels: a fixed histogram stretch (crush blacks, lift whites) for
|
||||
# punch, like Photoshop auto-levels. 0 = off, 1 = strong. Fixed (not
|
||||
# per-frame adaptive) so it can't flicker; it remaps a constant [lo,hi]
|
||||
# window to full range, so the keyed-out background never skews it.
|
||||
auto_levels: 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
|
||||
|
||||
@@ -1004,6 +1004,19 @@ def build_color_grade_filter(config: dict) -> str:
|
||||
# Start with format conversion to RGBA for color operations
|
||||
parts.append("format=rgba")
|
||||
|
||||
# Auto-levels: a fixed contrast/levels stretch for punch. Crushes blacks and
|
||||
# lifts whites by remapping [lo, hi] → [0, 1] equally per channel (luminance
|
||||
# levels, no colour shift). Fixed points → no per-frame flicker, and no
|
||||
# histogram is measured so the transparent green background can't skew it.
|
||||
if grade_config.auto_levels > 0:
|
||||
al = min(max(grade_config.auto_levels, 0.0), 1.0)
|
||||
lo = round(0.10 * al, 4)
|
||||
hi = round(1.0 - 0.10 * al, 4)
|
||||
parts.append(
|
||||
f"colorlevels=rimin={lo}:gimin={lo}:bimin={lo}:"
|
||||
f"rimax={hi}:gimax={hi}:bimax={hi}"
|
||||
)
|
||||
|
||||
# Color balance (only add if any value is non-zero)
|
||||
colorbalance_parts = []
|
||||
if grade_config.rs != 0:
|
||||
@@ -1085,6 +1098,7 @@ def parse_color_grade_config(config: dict) -> ColorGradeConfig:
|
||||
contrast=float(config.get("contrast", 1.0)),
|
||||
brightness=float(config.get("brightness", 0.0)),
|
||||
saturation=float(config.get("saturation", 1.0)),
|
||||
auto_levels=float(config.get("auto_levels", 0.0)),
|
||||
# Custom curves
|
||||
curves_r=config.get("curves_r", ""),
|
||||
curves_g=config.get("curves_g", ""),
|
||||
|
||||
Reference in New Issue
Block a user