Étalonnage vidéo (analyser → corriger → looks → choisir → LUT + appliquer)
Écrire l'étalonnage depuis zéro sous forme de code, générer plusieurs looks nommés distincts sur le métrage réel, en sélectionner un via un choix humain explicite ou une décision déléguée de l'agent, puis cuire un LUT .cube portable et l'appliquer à l'intégralité du clip. Aucun pack de préréglages.
Idée centrale — corriger, puis styliser. Chaque look est base corrective + couche créative :
- base corrective rend le métrage honnête — pour les formats log/HLG plats, il s'agit de la conversion log→Rec.709 ; pour les images déjà en Rec.709, il s'agit de la neutralisation de la balance des blancs + exposition + contraste léger. Chaque look partage cette base, donc « neutre » reste pertinent et les looks diffèrent par leur intention.
- couche créative est le look empilé par-dessus (chaud / froid / teal-orange / punchy / délavé…).
La peau est le juge de qualité. Sur les personnes — surtout les peaux foncées — teal_orange et cool_desat jettent régulièrement un voile vert/gris. Jugez chaque look d'abord sur les visages ; conservez la chaleur des tons moyens, poussez la teinte dans les ombres/hauts-lumières/fond, préférez la vibrance à la saturation brute. Ne poussez pas la balance des blancs entièrement à neutre, sinon la peau devient terne.
Prérequis : ffmpeg/ffprobe dans le PATH, Python avec numpy + Pillow.
Contrat
- Chaque look =
base + couche créative; la base est partagée par tous les looks. - Tous les looks sont rendus sur la même image représentative (une comparaison juste et contrôlée), étiquetés, en pleine résolution.
- L'application complète utilise
-c:a copy(l'audio n'est jamais ré-encodé → synchronisation A/V identique) et conserve la durée, les FPS, les dimensions sources. - Sélection complète du look avant livraison. Utilisez le mode
humanlorsque l'utilisateur a demandé à choisir ; utilisez le modeagentsans pause lorsque l'utilisateur a délégué le choix ou demandé une exécution autonome. Dans les deux modes, conservez la justification dans le plan durable.
Flux de travail du protocole projet
Stockez la décision durable dans work/color-grade/grade-plan.json :
{
"schema_version": 1,
"target": "base-video",
"base": "eq=contrast=1.05",
"looks": [{"name": "clean", "chain": "null"}],
"selected_look": "clean",
"selection_mode": "agent",
"selection_rationale": "La correction neutre préserve les tons chair naturels et l'éclairage source.",
"selected_lut": "../../final/selected-color-look.cube",
"evidence_refs": ["media:source"]
}
Utilisez base-video par défaut afin que les cartes de contenu et les sous-titres ne soient pas re-colorés. composite n'est valide que lorsque vous étalonnez intentionnellement des pixels déjà composités. Lisez le fichier partagé work/understand/media.json et les preuves visuelles. Générez review/02-color-grade/choose-color-look.jpg et review/02-color-grade/skin-tone-check.jpg ; générez éventuellement la courte vidéo selected-look-preview.mp4. Enregistrez la décision et les preuves dans review/02-color-grade/selected-look.md.
Définissez selection_mode à human après un choix explicite de l'utilisateur. Définissez-le à agent lorsque l'utilisateur délègue la décision ou demande un flux de travail ininterrompu, et choisissez de manière conservatrice avec les tons chair, la rétention des hautes lumières et l'équilibre neutre comme priorités. Écrivez toujours un selection_rationale non vide ; ne faites pas de pause en mode agent.
Cuisez le LUT choisi dans final/selected-color-look.cube. Lorsque ce LUT est livré, définissez selected_lut dans le plan d'étalonnage afin que le moteur de rendu partagé consomme ce fichier exact. Un LUT cuit contenant le look complet contient déjà la base corrective ; le moteur de rendu doit appliquer lut3d seul, et non base + lut3d.
Enregistrez les révisions exactes des entrées de l'opération dans based_on, incrémentez son revision entier lorsque le plan ou l'entrée sélectionnée change, définissez la vérification de l'opération à pass uniquement après que les fichiers de révision sont complets, et contribuez les champs complets du protocole :
{
"target": {"sequence": "main", "scope": "base-video"},
"effects": {
"changes_timeline": false,
"changes_geometry": false,
"changes_video_pixels": true,
"changes_audio": false,
"adds_track": null
},
"check": {
"status": "pass",
"report": "../review/02-color-grade/selected-look.md"
},
"render": {
"kind": "video-filter",
"target": "base-video",
"plan": "color-grade/grade-plan.json"
}
}
Le moteur de rendu partagé valide que selected_look existe, applique l'étalonnage après les transformations de la timeline et avant les incrustations, et encode la vidéo finale une seule fois :
python skills/video-understand/scripts/build_render_plan.py .
python skills/video-understand/scripts/render_project.py work/render/render-plan.json
Entrées pour compatibilité autonome
- La vidéo source.
- Un fichier
looks.json(base+looksnommés) ; commencez à partir delooks.example.jsonet réajustez labaseen fonction de ce que rapporteassess.py.
Tous les scripts existants continuent d'accepter l'ancien looks.json. gradelib.load_spec() accepte également le grade-plan.json canonique, donc les commandes de planche contact, de clip, de LUT et d'application autonome restent compatibles pendant la migration.
Schéma looks.json
{
"base": "<base corrective : conversion log->Rec.709, OU WB+exposition+contraste pour les images normales>",
"looks": [
{ "name": "clean_neutral", "label": "1 · CLEAN NEUTRAL", "desc": "corrigé uniquement", "chain": "eq=contrast=1.02" },
{ "name": "teal_orange", "label": "4 · TEAL & ORANGE", "desc": "cinématographique", "chain": "colorbalance=...,eq=..." }
// le filtre ffmpeg complet d'un look = base + "," + chain. Définissez "prepend_base": false pour utiliser la chaîne seule.
]
}
Flux de travail
-
Analyser — ne jamais étalonner à l'aveugle.
python scripts/assess.py SOURCE.mp4 --out work/Rapporte log-vs-Rec.709, la dominante de balance des blancs (signalstats U/V vs 128), l'exposition (Y), choisit une image représentative (
work/assess/frame.png) et suggère des ajustements de base. Réajustez labasedelooks.jsonen conséquence (par exemple, dominante chaude → ajoutez un peu de bleu ; log → mettez la conversion log→709 dansbase).- L'image rep est choisie par la luminance médiane, pas par les visages. Sur un plan fixe d'une personne qui parle, cela donne généralement une bonne image de visage, mais sur des plans larges/plans multiples riches en B-roll, cela peut choisir une image sans visage — et alors
face_skin_check.pngest inutile (la peau est le baromètre de qualité). Forcez un moment connu avec visage avec--frame-time SECONDES. - Les statistiques WB/exposition proviennent d'une fenêtre
signalstatsd'environ 10 s à 10% de la durée — correct pour une lumière constante ; pour une lumière qui change, déplacez/allongez-la avec--win-start SECONDES/--win-dur SECONDES.
- L'image rep est choisie par la luminance médiane, pas par les visages. Sur un plan fixe d'une personne qui parle, cela donne généralement une bonne image de visage, mais sur des plans larges/plans multiples riches en B-roll, cela peut choisir une image sans visage — et alors
-
Rendre les looks pour le choix.
python scripts/render_looks.py work/assess/frame.png looks.json --out work/looksProduit
work/looks/looks_compare.png(original + chaque look, étiquetés, pleine résolution) etface_skin_check.png(bande de peau zoomée). REGARDEZ les deux. La peau est le baromètre — réajustez ou abandonnez tout look qui grise/verdise la peau, puis re-rendez. Sujet décentré ? ajoutez--face-crop W:H:X:Y. Traitez ces PNG comme des intermédiaires de travail ; publiez les copies de révision approuvées sousreview/02-color-grade/choose-color-look.jpgetreview/02-color-grade/skin-tone-check.jpg. -
(optionnel) Aperçus animés des 2–3 meilleurs pour les voir en mouvement (audio conservé) :
python scripts/make_clips.py SOURCE.mp4 looks.json --out work/clips \ --pick clean_neutral,warm_filmic,teal_orange --with-original --ss 148 --t 8 -
Sélectionner un look. Présentez la planche contact et attendez uniquement lorsque l'utilisateur a demandé à choisir. Sinon, sélectionnez-le en mode agent et continuez sans pause de décision. Écrivez
selected_look,selection_modeetselection_rationaleavant livraison. -
Après le choix — cuire le LUT et l'appliquer à l'intégralité du clip.
python scripts/bake_lut.py looks.json --name CHOSEN \ --out final/selected-color-look.cube --verify-frame work/assess/frame.png python scripts/apply_grade.py SOURCE.mp4 --looks looks.json --name CHOSEN --out out/graded.mp4 # ou appliquez le LUT livré : --lut final/selected-color-look.cubeapply_grade.pyvérifie que la durée/l'audio sont conservés, imprime le décalage WB (U/V vers 128) et génère une image de repère pour inspection.- L'application ré-encode la vidéo une fois (avec perte) ; l'audio est
-c:a copy. La qualité/le temps sont définis par--crf(par défaut 18) et--preset(par défautslow).slowest correct pour les petits/courts clips, mais sur des plans longs 1080p/4K, c'est un coût temporel important et silencieux — passez à--preset mediumpour la livraison (ouveryfastpour un aperçu rapide),slowuniquement pour le final. - Faites correspondre la vidéo livrée au LUT livré : la chaîne (
--name) et le.cube33³ cuit (--lut) peuvent différer jusqu'à ~5/255 sur les courbes raides (bake_lut.pyrapporte cemax|d|). Invisible pour la plupart des travaux, mais si vous livrez le.cubeet voulez que la vidéo rendue lui corresponde exactement, appliquez avec--lut final/selected-color-look.cube, pas--name.
- L'application ré-encode la vidéo une fois (avec perte) ; l'audio est
Notes de conception / pièges
- Étalonnez le montage NET, puis composez les incrustations PAR-DESSUS l'image étalonnée. Si vous devez étalonner une vidéo qui a déjà des sous-titres/cartes incrustés, ne faites PAS confiance à
assess.py: ses statistiques WB/exposition et le choix de l'image sont faussés par les graphiques (les bannières sombres haut/bas tirent Y vers le bas, le texte clair des sous-titres / les accents teal faussent U/V, et le choix de l'image peut tomber sur une image riche en sous-titres) — vous corrigeriez les incrustations, pas l'image. Réutilisez unebaseajustée sur l'image nette, et attendez-vous à ce que l'étalonnage teinte les incrustations, donc n'utilisez qu'un look subtil/correctif (clean_neutral) ;teal_orange/vintage_fadedrecolorera visiblement le texte/les accents. - Un look/LUT couvre tous les clips associés. Une fois que vous avez choisi un look, réutilisez le même
looks.json/.cubesur tous les clips associés (par exemple, montage net + la version avec incrustations) — ne relancez PAS la sélection en deux phases par clip. - Corriger, puis styliser. La base est la généralisation de l'étape S-Log3→Rec.709 du deck. Pour les images normales, la base est WB+exposition+contraste ; pour les images log, mettez la conversion dedans.
- La peau d'abord.
teal_orange/cool_desatsont les habituels agresseurs de peau sur les sujets plus foncés — gardez les tons moyens chauds, teintez les ombres/hauts-lumières/fond, utilisez lavibrance. Ne neutralisez pas U/V complètement. - Les étiquettes sont des PNG PIL superposés par ffmpeg — jamais
drawtext. Sur Windows, le cheminfontfilededrawtext(deux-points du lecteur) n'est pas fiable. Les polices sont définies en haut degradelib.py. - Les chemins avec deux-points du lecteur cassent les options du graphe de filtres ffmpeg qui prennent un chemin (
lut3d,metadata=print:file=). Les scripts exécutent ffmpeg aveccwd= le dossier du fichier et le référencent par son nom de base. Gardez ce modèle pour tout nouveau filtre prenant un chemin. - Les sorties atterrissent dans les répertoires
--outque vous passez (durables), pas dans un répertoire temporaire système. - La bande visage/peau utilise un recadrage fixe centré dans la moitié supérieure, pas une détection de visage. Si le sujet n'est pas à peu près centré, passez
--face-crop W:H:X:Yàrender_looks.pyou la bande de peau montrera la mauvaise région. - L'image de repère de vérification de
apply_grade.pyest à 20% de la durée — une image différente de celle sur laquelle les looks ont été choisis. Si ce point à 20% est sans visage, inspectez la peau sur une autre image (ou passez--frame-timeàassess.pypour que l'image choisie soit un moment connu avec visage). - La cuisson du LUT est exacte, car chaque filtre ici est une opération point par pixel :
bake_lut.pypousse une grille d'identité 33³ à travers la chaîne réelle et se vérifie contre elle (mean|d|devrait être bien en dessous de 1/255 ; unmax|d|de quelques niveaux n'est que de l'interpolation). - Sélection avant livraison, toujours. Le choix humain et le choix délégué à l'agent sont tous deux valides, mais le plan doit enregistrer lequel a eu lieu et pourquoi.