Étalonnage fidèle des couleurs
Quand l’utiliser
- L’utilisateur donne une image de référence et une image à étalonner, en demandant « fais-la ressembler à ça »
- L’utilisateur a un lot de photos déjà retouchées et veut figer ce style pour l’appliquer à de nouvelles photos
- L’utilisateur veut « assombrir uniquement le ciel, ne pas toucher le sol ni les visages » – un réglage par zone
- L’utilisateur veut appliquer un Look en masse et exige « ne pas modifier le contenu, ne pas redessiner »
- L’utilisateur veut vérifier la fidélité d’un style : bandes, dérive colorimétrique, hautes lumières/ombres bouchées
- L’utilisateur donne une photo et demande « est-ce que cet étalonnage est correct ? », sans image de référence
- Les fichiers sources peuvent être en log / non convertis (S-Log3, V-Log, HLG extraits du RAW)
- L’utilisateur se plaint que la retouche générative « a déformé le visage » ou « a réduit la résolution » – exactement le cas de ce skill
Ne pas utiliser : pour changer la direction de la lumière, remplacer le ciel, ajouter des objets, redessiner sémantiquement. Ce skill ne produit qu’un mapping de couleurs, pas de modification de contenu.
Modèle central
- Un « style » = un
.cubede base + des.cubeoptionnels par zone (une par région) + un historique des versions + un archivage par lot + des étiquettes de sujet, stocké danscolor-styles/<nom>/ - Appliquer un style = appliquer la LUT pixel par pixel, puis superposer les LUT zonales via un masque sémantique. La sortie a exactement la même taille que l’original, SSIM = 1.0
- Deux modes de création du style :
fit: apprentissage supervisé à partir de paires « original / retouché », précis (ΔE2000 mesuré 0,55–0,9), nécessite ≥ 3 pairesref: transfert de distribution depuis une image de référence, rapide mais approximatif (ΔE2000 ≈ 4), une seule image suffit, idéal pour démarrer
- Chaque
fitest un « commit », automatiquement consolidé : détermination du style auquel il appartient → fusion avec tous les échantillons historiques pour un nouvel apprentissage → archivage → étiquetage du sujet. Pas d’entraînement de modèle, pas besoin de GPU
Que résout la segmentation ?
Une LUT globale ne peut répondre qu’à « dans quelle couleur mapper cette couleur ». Lorsqu’une même couleur est attendue différemment selon les zones – le cas typique est un ciel grisâtre à assombrir, tandis qu’un trottoir également grisâtre doit être conservé – un mapping global est mathématiquement impossible, la méthode des moindres carrés ne donne qu’un compromis médiocre.
Mesure (rue grise sous un ciel couvert, assombrissement du ciel de −28 %) :
| Ciel ΔE2000 | Hors ciel ΔE2000 | |
|---|---|---|
| LUT globale | 11,27 | 0,322 |
| Globale + ciel | 1,50 | 0,324 |
L’erreur hors ciel reste identique, prouvant que la segmentation n’ajoute aucune pollution supplémentaire ; elle compense simplement ce que la LUT globale ne peut pas apprendre.
Dimensions de segmentation : sky / people / veg / water / ground / arch, par défaut sky,people,veg. Masques mous fournis par un SegFormer-B0 intégré (4,4 Mo).
Workflow
1. Vérifier les dépendances
python -c "import numpy, PIL; print('ok')"
python -c "import onnxruntime; print('seg ok')" # nécessaire pour segmentation et étiquettes de sujet
python -c "import tifffile; print('tif ok')" # nécessaire pour lire/écrire du TIFF 16 bits
python -c "import rawpy; print('raw ok')" # nécessaire pour lire directement les RAW, optionnel
En cas d’absence : python -m pip install numpy pillow onnxruntime tifffile rawpy. L’absence d’onnxruntime ne fait pas planter ; elle réduit le traitement à un étalonnage global pur.
2. Inspecter les fichiers (recommandé, évite des efforts inutiles)
python <SKILL_DIR>/scripts/grade.py check --in ./photos/original --person
Aucune image de référence nécessaire, il répond à deux questions :
- Quel type de fichier :
transfer.verdict=display/ambiguous/log. Un fichier log soumis directement àfitintégrerait la courbe log dans la LUT, donnant un rendu visuellement plausible mais incorrect.--personlance une segmentation, nécessaire pour juger la teinte de la peau (sans personne, pas de diagnostic). - Y a-t-il des défauts dans l’image finale : luminosité médiane, point noir, proportion de blancs/noirs bouchés, nombre de dérives colorimétriques, richesse colorimétrique. Les éléments hors limites apparaissent dans
warnings.
apply et fit exécutent automatiquement ces deux vérifications en interne. Faire check une seule fois avant la livraison en lot.
3. Soumettre un lot (action principale)
python <SKILL_DIR>/scripts/grade.py fit \
--src ./photos/original --tgt ./photos/retouched
--src/--tgt: appariement par nom de fichier ; on peut aussi utiliser--pair original retouchépour des paires uniques- Les fichiers de tailles différentes dans les répertoires ne posent pas de problème, mais chaque paire doit avoir la même taille
- Sans
--name, mode automatique : comparaison avec les styles existants ; si suffisamment proche, fusion ; sinon, création d’un nouveau style --name warm_film: mode explicite ; si le nom existe, fusion ; sinon, création--regions sky,people,veg: active la segmentation (valeur par défaut).--regions ""revient au mode purement global--auto-threshold 1.5: seuil ΔE pour considérer qu’il s’agit du même style
4. Appliquer
python <SKILL_DIR>/scripts/grade.py apply \
--style warm_film --in ./photos/new --out ./out --strength 1.0
--style auto identifie d’abord le sujet des photos d’entrée, puis sélectionne le style dont les étiquettes correspondent le mieux (le résultat explique le choix dans le champ auto).
Consultez look et warnings avant de livrer. Si look.median_luma passe de [0.195, 0.302] à [0.55, 0.95], cela signifie que l’image est bien plus lumineuse que les échantillons ayant servi au style ; l’appliquer tel quel brûlera les hautes lumières. Trois solutions : --strength 0.7 pour réduire l’intensité, --match-key on pour aligner l’exposition sur celle des échantillons (adapté uniquement aux rafales avec le même rapport de contraste), ou accepter un arrière-plan plus clair. Ne décidez pas à la place de l’utilisateur.
--match-key off|on|auto (défaut off) : aligne d’abord la luminosité médiane de l’image sur celle des échantillons du style, puis applique la LUT. Pour les contre-jours, cela nuit au sujet (voir règle de fidélité n°14).
Format et qualité de sortie
Par défaut, deux fichiers sont générés ; ne livrez pas seulement le premier :
| Produit | Défaut | Usage |
|---|---|---|
<nom>__<style>_v<N>.jpg | q98, 4:4:4, conserve ICC/EXIF/DPI | Livraison, partage, réseaux sociaux |
<nom>__<style>_v<N>__master.png | 8 bits sans perte, avec dithering ±0.5 LSB | Retouche supplémentaire, agrandissement, impression |
--quality 98: qualité JPEG. q95→q98 : PSNR 48.7→50.6 dB, taille 337→512 Ko. q92 économise la moitié du volume pour 3 dB de moins, visible sur les dégradés de ciel.--master png|tif|none:tifen 16 bits (nécessitetifffile, sinon dégradé automatique en PNG 8 bits avecmaster_note; ne détruit pas le JPG déjà généré). En lot, utilisez--master nonepour économiser de l’espace.- Pas de master si le fichier source est déjà
.png/.tif/.bmp– la sortie principale est déjà sans perte. - ICC, EXIF, DPI sont transmis depuis la source. Perdre l’ICC donne l’impression d’une « dégradation de la qualité » : un fichier P3/AdobeRGB interprété en sRGB paraît terne et délavé.
- Le dithering n’est activé que pour les formats sans perte. Le bruit de quantification du JPEG est déjà un ordre de grandeur plus grossier que 1 LSB ; y ajouter du dithering gaspille du débit.
Si l’utilisateur dit « c’est compressé », vérifiez d’abord la source, ne changez pas les paramètres. apply signale dans source_note si le JPEG source est inférieur à 0,10 octet/pixel (copie compressée par une app de messagerie, 1080p à peine 100 Ko). Aucune sortie sans perte ne peut restaurer ces détails. Deux erreurs courantes : juger la qualité via l’aperçu de l’app de messagerie (qui recompresse) ou via final_sheet.jpg (déjà réduit de moitié).
5. Vérification
python <SKILL_DIR>/scripts/grade.py verify \
--style warm_film --src ./photos/original/a.jpg --tgt ./photos/retouched/a.jpg \
--threshold 2.0
Avec --threshold, un dépassement entraîne un code de sortie 2, utile pour un pipeline CI.
6. Exporter pour d’autres logiciels
python <SKILL_DIR>/scripts/grade.py export --style warm_film --out ./for_lightroom --full
--full ajoute des instructions de montage (couche de base sur toute l’image → couches zonales avec masques mous → fondu de 1–2 % du cadre).
Un style avec segmentation ne peut pas se résumer à un seul .cube : une LUT 3D unique est une fonction de la couleur du pixel, incapable d’exprimer « même couleur différente selon la zone ».
7. Annulation et traçabilité
python <SKILL_DIR>/scripts/grade.py batches --style warm_film # voir les lots composant ce style
python <SKILL_DIR>/scripts/grade.py undo --style warm_film --batch b20261108_101500
python <SKILL_DIR>/scripts/grade.py styles # tous les styles + étiquettes + description
python <SKILL_DIR>/scripts/grade.py styles --name warm_film # manifest complet d’un style
undo ne supprime pas une version : il retire le lot concerné, ré-entraîne avec les lots restants, et crée une nouvelle version. Les anciennes versions sont conservées indéfiniment.
Interprétation des sorties
Toutes les commandes produisent du JSON sur stdout.
| Champ | Signification | Interprétation |
|---|---|---|
quality.dE2000_mean | Écart colorimétrique moyen sur les paires d’apprentissage (y compris les zones) | < 2 acceptable ; 2–4 passable ; > 4 indique que l’appariement n’est pas purement colorimétrique |
decision | Comment la soumission a été traitée | create-new / merge-nearest / merge-into-named |
auto.distance | Distance au style le plus proche | Si nettement inférieure au seuil mais création, ou nettement supérieure mais fusion, l’algorithme s’est trompé |
auto.nearest | Les trois styles les plus proches et leurs distances | Permet de comprendre le raisonnement |
learned_from | Nombre d’échantillons historiques utilisés lors de la fusion | Si seulement new_samples sans archived, c’est une première fusion |
diagnostics.coverage_pct | Proportion des points de la grille de la LUT couverts par les données | 30–50 % normal (les images naturelles n’occupent qu’une partie de l’espace colorimétrique) |
diagnostics.lut_clipped_pct_observed | Proportion des zones couvertes par les données qui sont écrasées à 0/1 | > 2 % doit alerter sur une perte potentielle des hautes lumières/ombres |
regions.active | Zones réellement actives | Vide signifie que ces zones n’apparaissent pas dans les fichiers du lot |
region_diagnostics.<r>.skipped | Raison du saut d’une zone | too few pixels normal ; no residual learned indique que la zone se comporte comme la globale |
description.summary | Description du style en une phrase | À retransmettre à l’utilisateur |
description.region_extra | Effet supplémentaire de chaque zone par rapport à la globale | Rempli seulement si segmentation active |
tags | Sujet détecté automatiquement | portrait / landscape / street / indoor / night |
diagnostics.prior_shrink | Force de la prior, 1.0 = pleine | Chaque ajustement teste 1.0 / 0.5 / 0.2 / 0.05 et choisit le meilleur ; prior_trials donne les dE |
health.monotonic_violations | Nombre de violations de monotonie | Par défaut, forcé à 0 ; apparaît seulement si --no-monotonic est utilisé |
fit_quality.geometry_preserved | La géométrie est-elle conservée | Doit être true |
key_match.applied | Si l’image a été alignée sur l’exposition du style | Par défaut off ; si activé, donne les stops, gamma et médiane ajustée |
transfer.verdict | Le fichier est-il display, log, ou incertain | log doit être converti avant fit ; ambiguous demande confirmation à l’utilisateur |
warnings[].metric / .note | Problèmes détectés par auto-vérification sans référence | Retransmettez le contenu de note ; tableau vide = aucun défaut trouvé |
look | [original, résultat] : luminosité médiane, proportion de blancs bouchés, saturation | Utilisez-le pour répondre « que fait ce style à cette image ? » |
input_guard | Lors du fit, détection que l’original n’est pas en codage display | blocking: false, simple avertissement, ne bloque pas |
input_transfer | Lors de l’application, présence de fichiers non display | Verdict par fichier, idem |
Règles de fidélité (lignes rouges)
- Le RAW est une approximation de décodage. Libraw utilise la balance des blancs de l’appareil + une matrice colorimétrique universelle, pas le DCP de l’appareil. Un même style appliqué à deux boîtiers différents dérivera (ce n’est pas non plus le rendu de Lightroom). Pour une livraison précise, demandez à l’utilisateur de fournir des TIFF 16 bits.
- L’espace de référence de la LUT est sRGB gamma 2.2. Lors de l’export d’un
.cubepour d’autres logiciels, mentionnez-le, sinon l’application cible interprétera mal les couleurs. - Un style avec segmentation ne peut pas être exprimé par un seul
.cube.vN.cuben’est que la couche globale ; la cartographie zonale est dansvN_<region>.cubeet nécessite un masque. Utilisezexport --fullpour obtenir les couches zonales et les instructions de montage. - Les pixels surexposés restent inchangés. En fin de rendu, les pixels dont le canal le plus clair ≥ 0,97 bénéficient d’une transition douce vers l’image originale : la zone surexposée n’a pas d’information de couleur, toute cartographie serait une invention. Mesure sur un disque solaire en contre-jour : il serait assombri en une tache gris-bleu par la segmentation ; la protection le ramène à une source de lumière chaude et blanche. Le coût est une légère sous-estimation des hautes lumières saturées (ΔE d’apprentissage sur mire : 1,15→1,21). Utilisez
--no-clip-guardsi vous voulez assombrir aussi les zones surexposées. - La bordure du masque mou a une zone de transition délibérée : deux mappings différents se rencontrant directement créeraient un contour visible. Le prix à payer est une possible semi-transition de quelques pixels à la jonction ciel/arbres.
- Les masques ont une résolution de seulement 128×128. Les structures plus fines qu’environ 1/128 du cadre (branches mortes, fils électriques, fines grilles) sont affectées à leur zone respective. Ces structures sont généralement des silhouettes, et leur assombrissement n’est pas perceptible ; pour une précision pixel, utilisez un outil de masquage externe.
- Les zones de faible confiance ne sont pas ajustées. Le ciel surexposé en contre-jour, ou le ciel fragmenté par des branches, ne reçoit pas de masque fiable ; ces zones reviennent à la LUT globale. Mieux vaut moins ajuster que mal ajuster.
- Les photos d’animaux peuvent être identifiées comme portrait. ADE20K classe les gros plans de chats comme
person(dans 88 % des cas). Ce skill utilise un garde-fou : si la proportion de teintes de peau dans la zonepersonest inférieure à 5 %, la zone est annulée. Les animaux très clairs peuvent encore passer. - Le mode
refne doit pas être présenté comme une « copie exacte ». Il aligne uniquement les distributions Lab ; ΔE mesuré ≈ 4. Pour une précision maximale, utilisezfit. - Les curseurs de la couche de description sont une estimation. La colonne
measuredest reproductible,estimate_lightroomne l’est pas – une LUT n’a pas de solution unique pour un curseur donné. - L’auto-vérification sans référence détecte uniquement les erreurs évidentes, elle ne juge pas si le résultat est « beau ». Elle utilise des objectifs statistiques classiques (luminosité médiane 0.45, point noir 0.02, teinte de peau 20°). Une photo de nuit doit avoir une luminosité médiane basse, un coucher de soleil naturellement chaud. Chaque critère a une tolérance ; un avertissement signifie « vérifiez », pas « c’est faux ». Deux seuils stricts : la teinte de peau n’est jugée que si la segmentation détecte une personne (sinon une scène de rue chaude confondrait 30 % de l’image avec de la peau) ; la balance des blancs n’est jugée que si l’image contient suffisamment de pixels neutres.
- La détection log se base sur la forme de l’histogramme, pas sur les métadonnées. Les images arrivent souvent au format PNG ou via le rendu de libraw, l’EXIF est perdu. Elle analyse : hausse du point noir, hautes lumières contenues, plage dynamique étroite, faible saturation. Trois concordances →
log, deux →ambiguous. Un fichier final délavé et désaturé peut être signalé commeambiguous(faux positif connu). - Le style porte une propriété d’exposition ; la LUT n’est fiable que dans la plage de luminosité qu’elle a apprise. Un look augmentant la médiane de 0.19 à 0.28, appliqué à une image de médiane 0.55, fera monter la médiane à 0.95 – ce n’est pas un bug, le look ne dispose pas de suffisamment de marge dans les hautes lumières. Mesure : l’apprentissage n’a observé que l’intervalle source 0~0.42 (p99 source 0.42 / cible p99 0.56) ; appliqué à une image de p99 0.72, la partie au-delà est extrapolée linéairement, brûlant les hautes lumières.
- Ne décidez pas à la place de l’utilisateur d’« aligner l’exposition ».
--match-keyaligne d’abord la médiane de l’image sur celle des échantillons avant d’appliquer la LUT. Mais pour un contre-jour, où le visage est à 0.27 et l’arrière-plan à 0.62, l’alignement sur la médiane de la scène entière ramène le visage à 0.08, le sujet devient noir. Par défaut, il est off ; il ne doit être activé que pour des rafales avec le même rapport de contraste mais une exposition variable. Le critère est simple : la relation de luminosité entre le sujet et l’arrière-plan de cette image est-elle la même que sur l’image de référence ? - Quand le sujet est plus sombre que l’arrière-plan dans une image, alors qu’il est le plus clair dans l’image de référence, aucun mapping monotone global ne peut satisfaire les deux. Seul un réglage local peut les satisfaire. Dans ce cas, privilégiez le sujet et acceptez un arrière-plan plus clair ; inversement, préserver l’arrière-plan écraserait le visage.
- Une dominante chaude préexistante se cumule avec un style chaud pour donner un orange rouge. Un portrait sombre avec une dominante chaude de 0.46 stop, sur lequel on applique un look chaud, voit son visage passer du brun chaud à l’orange rouge,
castmonte à 0.77, des taches apparaissent sur les joues. Ne commencez pas par neutraliser la balance des blancs – la seule « couleur neutre » de l’image est souvent un mur éclairé par la même lampe ; l’annuler verdirait le mur et jaunirait-verdirait la peau (mesuré : les deux essais 0.5/0.75 sont pires). La bonne séquence : réduisez d’abord--strength(vers 0.6). Si ce n’est pas satisfaisant, abandonnez la LUT et utilisez une simple courbe de luminance ou un gain d’exposition linéaire – n’affectez que la luminosité, pas la teinte ; la dominante chaude reste inchangée. - Chaque enregistrement JPEG détériore irréversiblement l’image. Livrez en q98 en une seule fois ; pour modifier, repartez de
__master.png, ne réitérez pas sur des JPG déjà créés. Trois passes avec le même LUT en q95 fait passer le PSNR de 48.7 à 48.6 dB, mais chaque passe ajoute des crénelages dans les dégradés lisses (ciel éclairci, flou d’arrière-plan). Pour identifier la source de la perte : si le fichier source a déjà été compressé (< 0,10 octet/pixel), la perte vient de là, pas des paramètres de codage.
Performances de base
| Scénario | Temps pour une image de 10,8 MP |
|---|---|
| Rendu purement global | 3,6 s |
| Rendu avec segmentation (2 zones actives) | 6,7 s |
| Inférence de segmentation (512 px) + suréchantillonnage | 0,7 s |
Un fit (10 paires, avec segmentation et étiquetage) | environ 12 s |
Le goulot d’étranglement est le rassemblement pixel par pixel des LUT (8 lectures aléatoires × par pixel), pas la logique algorithmique. Pour le traitement par lots, le parallélisme par fichier (multiprocess) est le plus efficace.
Tâches courantes
| Demande de l’utilisateur | Commande |
|---|---|
| « Fais ressembler mes photos à celle-ci » | ref → apply en lot |
| « Ces photos sont retouchées, applique ce style à l’avenir » | fit --src original --tgt retouché → apply |
| « J’ai retouché un autre lot, applique le même style » | fit avec --name pointant vers le même style → fusion automatique |
| « Assombris seulement le ciel, ne touche pas au sol ni aux visages » | La segmentation est activée par défaut ; vérifiez que regions.active contient sky |
| « Ce style est trop fort » | apply --strength 0.6, pas besoin de refaire fit |
| « Cette version est moins bien que la précédente » | apply --version <ancien numéro> |
| « Le lot précédent était une erreur, annule-le » | batches pour trouver le batch_id → undo |
| « Quel style utiliser pour ces photos ? » | apply --style auto, consultez auto.ranking |
| « Que fait exactement ce style ? » | styles --name X, lisez description.summary |
| « Exporte pour DaVinci Resolve » | export --full ; rappelez l’espace sRGB gamma 2.2 et les masques mous pour les LUT zonales |
| « Contre-jour / lever de soleil, ne rends pas le soleil gris » | Par défaut protégé (protection des surexpositions) ; si encore gris, vérifiez que le soleil est vraiment surexposé |
| « Cet étalonnage est-il correct ? Quels sont les problèmes ? » | check --in X --person, retransmettez warnings une par une, sans ajouter votre jugement |
| « Est-ce que ce fichier est un log ? » | check --in X, lisez transfer.verdict et transfer.signals |
| « Est-ce que l’application de ce style va brûler les hautes lumières ? » | apply donne look avec comparaison ; warnings l’indique directement |
| « Cette photo est bien plus claire que la référence, l’application donne un résultat délavé » | Vérifiez d’abord l’écart de look.median_luma ; réduisez --strength, ou pour les rafales de même contraste, utilisez --match-key on |
| « Le sujet est plus sombre que l’arrière-plan, après application le sujet devient noir » | N’activez pas --match-key ; le rapport de contraste diffère de l’image de référence, seul un réglage local peut fonctionner |
| « Cette photo est déjà chaude, après application le visage devient orange » | Réduisez --strength à 0.6 d’abord ; ne neutralisez pas la balance des blancs (cela verdirait le mur), voir la règle n°16 |
Références
references/style-schema.md: structure du répertoire des styles, champs destyle.json, sémantique des versions et des lotsreferences/color-pipeline.md: gestion des couleurs, détails algorithmiques, limites connues et suggestions de réglages