CommunityImagegithub.com

naokuotong-1/photo-color-grader

Étalonnage fidèle des couleurs. Transformez un « style d’étalonnage » en LUT 3D réutilisable et appliquez-le en lot, avec segmentation sémantique (ciel, portrait, végétation…), cumul automatique et annulation de lots, reconnaissance automatique du sujet et sélection de style, et auto-vérification sans image de référence basée sur des statistiques classiques. Opérations pixel par pixel uniquement, pas de redessin ni de modification géométrique, idéal pour les livraisons photo exigeant une haute fidélité.

Qu'est-ce que photo-color-grader ?

Ce skill transforme un style d’étalonnage en LUT 3D réutilisable, appliquée en lot via un pipeline de correction couleur pixel par pixel. Il supporte la segmentation sémantique (ciel, personnes, végétation) pour des ajustements localisés, l’auto-reconnaissance du sujet (portrait, paysage, rue, intérieur, nuit) et l’auto-détection des métadonnées (log/display). Les styles peuvent être générés par apprentissage supervisé (fit, ΔE~0,55–0,9) ou transfert de distribution (ref, ΔE~4). Il offre un auto-diagnostic sans référence, une gestion complète des versions, l’annulation de lots, l’export au format .cube avec instructions de montage pour les LUT partitionnées, et des alertes sur les écueils (ICC perdu, surexposition, JPEG dégradé). Fonctionne avec Python, numpy, PIL, onnxruntime, tifffile. Idéal pour les photographes exigeant une fidélité absolue, mais ne modifie ni le contenu ni la géométrie de l’image.

Compatible avec~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/naokuotong-1/photo-color-grader/tree/HEAD

Installed? Explore more Image skills: steipete/songsee, affaan-m/frontend-design, mattpocock/improve-codebase-architecture · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

É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

  1. Un « style » = un .cube de base + des .cube optionnels par zone (une par région) + un historique des versions + un archivage par lot + des étiquettes de sujet, stocké dans color-styles/<nom>/
  2. 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
  3. 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 paires
    • ref : transfert de distribution depuis une image de référence, rapide mais approximatif (ΔE2000 ≈ 4), une seule image suffit, idéal pour démarrer
  4. Chaque fit est 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 ΔE2000Hors ciel ΔE2000
LUT globale11,270,322
Globale + ciel1,500,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 à fit intégrerait la courbe log dans la LUT, donnant un rendu visuellement plausible mais incorrect. --person lance 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 :

ProduitDéfautUsage
<nom>__<style>_v<N>.jpgq98, 4:4:4, conserve ICC/EXIF/DPILivraison, partage, réseaux sociaux
<nom>__<style>_v<N>__master.png8 bits sans perte, avec dithering ±0.5 LSBRetouche 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 : tif en 16 bits (nécessite tifffile, sinon dégradé automatique en PNG 8 bits avec master_note ; ne détruit pas le JPG déjà généré). En lot, utilisez --master none pour é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.

ChampSignificationInterpré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
decisionComment la soumission a été traitéecreate-new / merge-nearest / merge-into-named
auto.distanceDistance au style le plus procheSi nettement inférieure au seuil mais création, ou nettement supérieure mais fusion, l’algorithme s’est trompé
auto.nearestLes trois styles les plus proches et leurs distancesPermet de comprendre le raisonnement
learned_fromNombre d’échantillons historiques utilisés lors de la fusionSi seulement new_samples sans archived, c’est une première fusion
diagnostics.coverage_pctProportion des points de la grille de la LUT couverts par les données30–50 % normal (les images naturelles n’occupent qu’une partie de l’espace colorimétrique)
diagnostics.lut_clipped_pct_observedProportion 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.activeZones réellement activesVide signifie que ces zones n’apparaissent pas dans les fichiers du lot
region_diagnostics.<r>.skippedRaison du saut d’une zonetoo few pixels normal ; no residual learned indique que la zone se comporte comme la globale
description.summaryDescription du style en une phraseÀ retransmettre à l’utilisateur
description.region_extraEffet supplémentaire de chaque zone par rapport à la globaleRempli seulement si segmentation active
tagsSujet détecté automatiquementportrait / landscape / street / indoor / night
diagnostics.prior_shrinkForce de la prior, 1.0 = pleineChaque ajustement teste 1.0 / 0.5 / 0.2 / 0.05 et choisit le meilleur ; prior_trials donne les dE
health.monotonic_violationsNombre de violations de monotoniePar défaut, forcé à 0 ; apparaît seulement si --no-monotonic est utilisé
fit_quality.geometry_preservedLa géométrie est-elle conservéeDoit être true
key_match.appliedSi l’image a été alignée sur l’exposition du stylePar défaut off ; si activé, donne les stops, gamma et médiane ajustée
transfer.verdictLe fichier est-il display, log, ou incertainlog doit être converti avant fit ; ambiguous demande confirmation à l’utilisateur
warnings[].metric / .noteProblèmes détectés par auto-vérification sans référenceRetransmettez le contenu de note ; tableau vide = aucun défaut trouvé
look[original, résultat] : luminosité médiane, proportion de blancs bouchés, saturationUtilisez-le pour répondre « que fait ce style à cette image ? »
input_guardLors du fit, détection que l’original n’est pas en codage displayblocking: false, simple avertissement, ne bloque pas
input_transferLors de l’application, présence de fichiers non displayVerdict par fichier, idem

Règles de fidélité (lignes rouges)

  1. 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.
  2. L’espace de référence de la LUT est sRGB gamma 2.2. Lors de l’export d’un .cube pour d’autres logiciels, mentionnez-le, sinon l’application cible interprétera mal les couleurs.
  3. Un style avec segmentation ne peut pas être exprimé par un seul .cube. vN.cube n’est que la couche globale ; la cartographie zonale est dans vN_<region>.cube et nécessite un masque. Utilisez export --full pour obtenir les couches zonales et les instructions de montage.
  4. 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-guard si vous voulez assombrir aussi les zones surexposées.
  5. 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.
  6. 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.
  7. 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.
  8. 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 zone person est inférieure à 5 %, la zone est annulée. Les animaux très clairs peuvent encore passer.
  9. Le mode ref ne doit pas être présenté comme une « copie exacte ». Il aligne uniquement les distributions Lab ; ΔE mesuré ≈ 4. Pour une précision maximale, utilisez fit.
  10. Les curseurs de la couche de description sont une estimation. La colonne measured est reproductible, estimate_lightroom ne l’est pas – une LUT n’a pas de solution unique pour un curseur donné.
  11. 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.
  12. 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é comme ambiguous (faux positif connu).
  13. 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.
  14. Ne décidez pas à la place de l’utilisateur d’« aligner l’exposition ». --match-key aligne 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 ?
  15. 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.
  16. 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, cast monte à 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.
  17. 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énarioTemps pour une image de 10,8 MP
Rendu purement global3,6 s
Rendu avec segmentation (2 zones actives)6,7 s
Inférence de segmentation (512 px) + suréchantillonnage0,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’utilisateurCommande
« 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 de style.json, sémantique des versions et des lots
  • references/color-pipeline.md : gestion des couleurs, détails algorithmiques, limites connues et suggestions de réglages

Skills associés