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
|
### Stage 3: Preprocess
|
||||||
|
|
||||||
Applies video filters (chroma key, scaling, etc.) to narration segments.
|
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`
|
(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.
|
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
|
```bash
|
||||||
gnommo -p myproject grade # first raw_mov clip, 3s from 5s in
|
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:
|
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."""
|
"""Deterministic 'look' sweep 0.0–1.0 → punchy color_grade candidates.
|
||||||
base_cg = _grade_step(filters, "color_grade") or {}
|
|
||||||
|
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")
|
key = _grade_step(filters, "gnommokey")
|
||||||
contrast = float(base_cg.get("contrast", 1.05))
|
values = [round(i / 5, 1) for i in range(6)] # look 0.0 .. 1.0
|
||||||
values = [round(i / 5, 1) for i in range(6)] # paleness 0.0 .. 1.0
|
print(" Sweeping look (auto-levels + vividness) 0.0 → 1.0")
|
||||||
print(" Sweeping paleness 0.0 → 1.0")
|
|
||||||
candidates = []
|
candidates = []
|
||||||
for i, p in enumerate(values, 1):
|
for i, p in enumerate(values, 1):
|
||||||
cg = {"type": "color_grade", "contrast": contrast,
|
cg = {
|
||||||
"rm": round(-0.10 * p, 3), "gm": round(0.02 * p, 3),
|
"type": "color_grade",
|
||||||
"saturation": round(1.0 - 0.15 * p, 3), "brightness": round(0.05 * p, 3)}
|
"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)
|
chain = _mask_and(filters, key, cg)
|
||||||
skin = _measure_skin(ref, chain)
|
skin = _measure_skin(ref, chain)
|
||||||
png = out_dir / f"grade_{i}.png"
|
png = out_dir / f"grade_{i}.png"
|
||||||
_render_preview(ref, chain, png)
|
_render_preview(ref, chain, png)
|
||||||
params = {k: cg[k] for k in ("rm", "gm", "saturation", "brightness", "contrast")}
|
params = {k: cg[k] for k in ("auto_levels", "saturation", "contrast", "brightness", "rm")}
|
||||||
params["paleness"] = p
|
params["look"] = p
|
||||||
candidates.append({
|
candidates.append({
|
||||||
"id": f"grade_{i}", "file": png.name, "params": params,
|
"id": f"grade_{i}", "file": png.name, "params": params,
|
||||||
"hint": {"skin_rgb": list(skin), "warmth": skin[0] - skin[2]},
|
"hint": {"skin_rgb": list(skin), "warmth": skin[0] - skin[2]},
|
||||||
})
|
})
|
||||||
manifest = {
|
manifest = {
|
||||||
"stage": "grade", "target_step": "color_grade",
|
"stage": "grade", "target_step": "color_grade",
|
||||||
"source": source_name, "ss": round(ss, 2), "swept": ["paleness"],
|
"source": source_name, "ss": round(ss, 2), "swept": ["look"],
|
||||||
"recommended": "grade_3", "candidates": candidates,
|
"recommended": "grade_4", "candidates": candidates,
|
||||||
}
|
}
|
||||||
mpath = _grade_write_manifest(out_dir, manifest)
|
mpath = _grade_write_manifest(out_dir, manifest)
|
||||||
rec = manifest["recommended"]
|
rec = manifest["recommended"]
|
||||||
print(f" {'id':>10} {'paleness':>8} {'skin RGB':>17} {'warmth':>6} png")
|
print(f" {'id':>10} {'look':>5} {'sat':>5} {'skin RGB':>17} {'warmth':>6} png")
|
||||||
print(f" {'-'*10} {'-'*8} {'-'*17} {'-'*6} {'-'*3}")
|
print(f" {'-'*10} {'-'*5} {'-'*5} {'-'*17} {'-'*6} {'-'*3}")
|
||||||
for c in candidates:
|
for c in candidates:
|
||||||
star = " ◀ suggested" if c["id"] == rec else ""
|
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}")
|
f"{str(tuple(c['hint']['skin_rgb'])):>17} {c['hint']['warmth']:>6} {c['file']}{star}")
|
||||||
print(f"\n Manifest: {mpath}")
|
print(f"\n Manifest: {mpath}")
|
||||||
print(" Pick the paleness you like:")
|
print(" Pick the look you like (flat → punchy):")
|
||||||
print(f" gnommo -p {project_path.name} grade --pick grade_3")
|
print(f" gnommo -p {project_path.name} grade --pick grade_4")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@@ -4948,8 +4955,8 @@ 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
|
||||||
# 'paleness' is a UI-only dial, not a real color_grade field.
|
# 'look'/'paleness' are UI-only dials, not real color_grade fields.
|
||||||
params = {k: v for k, v in cand["params"].items() if k != "paleness"}
|
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)
|
_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()))
|
||||||
|
|||||||
@@ -196,6 +196,12 @@ class ColorGradeConfig:
|
|||||||
brightness: float = 0.0 # Brightness adjustment (-1.0 to 1.0, 0 = no change)
|
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)
|
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
|
# Custom curves for lift/gamma/gain control
|
||||||
# Format: "0/0 0.5/0.56 1/1" means (input/output) control points
|
# Format: "0/0 0.5/0.56 1/1" means (input/output) control points
|
||||||
curves_r: str = "" # Red channel curve
|
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
|
# Start with format conversion to RGBA for color operations
|
||||||
parts.append("format=rgba")
|
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)
|
# Color balance (only add if any value is non-zero)
|
||||||
colorbalance_parts = []
|
colorbalance_parts = []
|
||||||
if grade_config.rs != 0:
|
if grade_config.rs != 0:
|
||||||
@@ -1085,6 +1098,7 @@ def parse_color_grade_config(config: dict) -> ColorGradeConfig:
|
|||||||
contrast=float(config.get("contrast", 1.0)),
|
contrast=float(config.get("contrast", 1.0)),
|
||||||
brightness=float(config.get("brightness", 0.0)),
|
brightness=float(config.get("brightness", 0.0)),
|
||||||
saturation=float(config.get("saturation", 1.0)),
|
saturation=float(config.get("saturation", 1.0)),
|
||||||
|
auto_levels=float(config.get("auto_levels", 0.0)),
|
||||||
# Custom curves
|
# Custom curves
|
||||||
curves_r=config.get("curves_r", ""),
|
curves_r=config.get("curves_r", ""),
|
||||||
curves_g=config.get("curves_g", ""),
|
curves_g=config.get("curves_g", ""),
|
||||||
|
|||||||
Reference in New Issue
Block a user