Community画像github.com

naokuotong-1/photo-color-grader

写真の色味を忠実に調整するためのスキルです。調整スタイルを再利用可能な3D LUTに変換し、空・人物・植生などのセマンティック領域ごとにバッチ適用できます。また、素材がログ未変換かどうかも識別します。

photo-color-grader とは?

このスキルは、写真の色調補正を、ピクセル単位のカラーマッピングに厳密に制限して実行します。ユーザーが参照画像と補正したい画像を提供すると、そのスタイルを学習して3D LUTとして保存し、後で同じスタイルを他の画像にバッチ適用できます。特に、空、人物、植生などの領域ごとに異なる調整(例:空だけを暗くする)をセマンティックセグメンテーションを用いて実現できる点が強みです。内部ではSegFormer-B0モデルを使用し、ONNX Runtimeで動作します。スキルは、スタイルの学習(fit)、適用(apply)、検証(verify)、エクスポート(export)といった一連のワークフローをコマンドラインインターフェースで提供します。また、素材がログ形式かどうかの判定や、参照画像なしでの品質チェック機能も備えており、写真の納品において高い忠実性が求められるプロフェッショナルなワークフローに適しています。

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

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

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

写真保真調色 (Photorealistic Color Grading)

いつ使うか

  • ユーザーが参照画像と調整したい写真を1枚渡し、「この色味に合わせて」と依頼したとき
  • ユーザーがすでにレタッチ済みの写真を複数持っており、そのスタイルを固定化して、新しく撮った写真に適用したいとき
  • 「空だけ暗くして、地面や顔はそのままにして」のような領域別調整が必要なとき
  • 特定のルックをバッチ適用したいが、「内容は変えず、再描画もしないで」という要求があるとき
  • 特定のスタイルが色再現を忠実に保っているか(階調飛び、色ずれ、ハイライトやシャドウのつぶれがないか)を検証したいとき
  • ユーザーが写真を1枚渡して「この調整は正しいですか?」「どこが問題ですか?」と聞くが、参照画像がないとき
  • ユーザーが提供する素材がログ / 未変換(カメラRAWから書き出したS-Log3、V-Log、HLGフレーム)の可能性があるとき
  • ユーザーが生成系レタッチで「人物の顔が変わった」「解像度が落ちた」と不満を言っているとき——このスキルがまさにそのシナリオに該当します

適用外:光の方向の変更、空の差し替え、物体の追加、セマンティックな再描画が必要なタスク。このスキルはカラーマッピングのみを出力し、内容の変更はできません。

中核モデル

  1. 「スタイル」= ベース .cube + オプションの領域別 .cube(領域ごとに1つ)+ バージョン履歴 + バッチアーカイブ + 被写体タグ。color-styles/<name>/ に保存されます。
  2. スタイル適用 = ピクセルごとのルックアップテーブル(LUT)参照。その後、セマンティックマスクに従って領域別LUTを重ね合わせます。出力は元の画像と厳密に同じサイズで、構造的類似性(SSIM)は常に1.0です。
  3. スタイル生成方法は2種類:
    • fit:「元画像 / レタッチ後画像」のペアから学習。正確(実測ΔE2000 0.55–0.9)。3ペア以上必要。
    • ref:参照画像の統計情報を移す。高速だが粗い(実測ΔE2000 ≈ 4)。1枚の画像で可能で、コールドスタートに適しています。
  4. fit が実行されるたびに「コミット」となり、自動的に蓄積されます:どのスタイルに属するか判定 → 属するなら全履歴サンプルで再フィッティング → アーカイブ → 被写体タグを付与。モデルのトレーニングやGPUは必要ありません。

領域分割が解決する問題

グローバルLUTは「この色を何にマッピングするか」という質問にしか答えられません。同じ色が領域によって異なる結果になることが期待される場合——最も典型的なのは灰色がかった空を暗くしたいが、同じく灰色の路面はそのままにしたい——グローバルマッピングでは数学的に解がなく、最小二乗法は中間値を取るだけで、どちらにも似ません。

実測(曇りの街並み、空のみ −28% 暗くする):

空 ΔE2000空以外 ΔE2000
グローバルLUT11.270.322
グローバル + sky 領域1.500.324

空以外の誤差は完全に一致しており、領域分割が追加のノイズをもたらさず、「学習できなかった部分」を補完していることを示しています。

領域の種類:sky / people / veg / water / ground / arch。デフォルトは sky,people,veg。内蔵のSegFormer-B0(4.4 MB)がソフトマスクを提供します。

ワークフロー

1. 依存関係の確認

python -c "import numpy, PIL; print('ok')"
python -c "import onnxruntime; print('seg ok')"   # 領域分割と被写体タグに必要
python -c "import tifffile; print('tif ok')"      # 16ビットTIFFの読み書きに必要
python -c "import rawpy; print('raw ok')"         # RAW直接読み取りに必要、オプション

不足している場合:python -m pip install numpy pillow onnxruntime tifffile rawpy。onnxruntimeがなくてもクラッシュしません。純粋なグローバルカラーグレーディングにダウングレードされます。

2. まず素材をチェックする(推奨、無駄な作業を省く)

python <SKILL_DIR>/scripts/grade.py check --in ./photos/original --person

参照画像は不要です。2つの質問に答えるだけです。

  • これは何の素材か:transfer.verdict = display / ambiguous / log。ログ素材をそのまま fit すると、ログカーブ自体をLUTに学習してしまい、見た目は妥当でも間違った結果になります。--person を指定するとセグメンテーションが実行され、肌の色相に意味が出ます(人がいない場合は判定しません)。
  • 出力画像に問題はないか:中央輝度、黒レベル、白飛び/黒つぶれの割合、色かぶりの段数、色彩の豊かさ。範囲外のものは warnings に入ります。

apply と fit は内部で自動的にこれらのチェックを実行するため、このステップはバッチ納品の前に一度実行するだけで十分です。

3. 素材のバッチを提出する(中核アクション)

python <SKILL_DIR>/scripts/grade.py fit \
  --src ./photos/original --tgt ./photos/retouched
  • --src / --tgt はファイル名のベース部分でペアリングされます。--pair 元画像 レタッチ後画像 で単一ペアを繰り返し指定することも可能です。
  • ディレクトリ内に異なるサイズの写真が混在しても問題ありませんが、各ペア内ではサイズが一致している必要があります。
  • --name を指定しない場合は自動モード:既存のスタイルと距離を比較し、十分近ければマージ、そうでなければ新規作成します。
  • --name warm_film は明示的な指定です:名前がすでに存在すればマージ、なければ新規作成します。
  • --regions sky,people,veg で領域分割を有効にします(デフォルト値)。--regions "" と指定すると純粋なグローバルに戻ります。
  • --auto-threshold 1.5 は「同じスタイル」とみなすΔEの閾値です。

4. 適用

python <SKILL_DIR>/scripts/grade.py apply \
  --style warm_film --in ./photos/new --out ./out --strength 1.0

--style auto を指定すると、まず入力写真の被写体を識別し、最もタグが一致するスタイルを選択します(結果は auto フィールドで選択理由と共に説明されます)。

納品前に look と warnings を確認してください。look.median_luma が [0.195, 0.302] から [0.55, 0.95] に変わった場合、これは「この画像はスタイルが学習した素材よりも明るすぎるため、スタイルを適用するとハイライトが飛んでしまう」ことを示しています。この場合、3つの選択肢があります。--strength 0.7 で強度を下げる、--match-key on で画像の露出をスタイルが学習したレベルに合わせる(同一光量の連続 shooting にのみ適しています)、または背景が明るくなることを許容する、です。ユーザーに確認せずに勝手に選択しないでください。

--match-key off|on|auto(デフォルト off):まず画像の中央輝度をスタイルが学習したレベルに合わせてから、LUTを適用します。逆光の写真では被写体に悪影響を与える可能性があります。「保真性の赤線」の第14条を参照してください。

出力形式と画質

デフォルトでは毎回2つのファイルが出力されます。最初のものだけを渡さないでください。

生成物デフォルト用途
<名>__<スタイル>_v<N>.jpgq98、4:4:4、元のICC/EXIF/DPIを保持納品、共有、SNS投稿用
<名>__<スタイル>_v<N>__master.png8ビットロスレス、±0.5 LSBディザリング付きさらに編集する、拡大する、印刷する用
  • --quality 98:JPEG品質。q95→q98で実測PSNRが48.7→50.6 dB、ファイルサイズが337→512 KBに増加。q92に下げるとファイルサイズは半分になり、3 dB低下しますが、グラデーションのある空で目視で違いがわかります。
  • --master png|tif|none:tif は16ビット出力(tifffile が必要。インストールできない場合は8ビットPNGに自動ダウングレードされ、master_note に記録されます。すでに生成されたJPGは壊しません)。バッチ出力で場所を取りたくない場合は --master none を使用します。
  • ソースファイルがすでに .png/.tif/.bmp の場合は、マスターファイルは生成されません。メインの生成物はすでにロスレスだからです。
  • ICC、EXIF、DPIはソース画像から透過的に引き継がれます。ICCプロファイルを失うと、「画質が悪くなった」と最も感じられます:P3/AdobeRGBの画像にタグなしRGBとして保存されると、どの画像ビューアもsRGBとして解釈し、色がすぐにくすんで見えます。
  • ディザリングはロスレス形式でのみ有効です。JPEGの量子化誤差はそもそも1 LSBよりも一桁粗いため、JPEGにディザリングを適用しても無駄にビットレートを消費するだけです。

ユーザーが「圧縮された」と言った場合、パラメータを調整する前に、まずソース画像を確認してください。 apply は source_note でその点を指摘します:ソースJPEGが0.10 バイト/ピクセル未満の場合、それはチャットアプリなどで転送されたコピーであり(1080pでわずか100 KB強)、このステップに入る前にすでに詳細が失われています。出力をロスレスにしても取り戻せません。他によくある誤解として、チャットウィンドウのプレビュー画像で画質を判断すること(プラットフォームが再圧縮するため)や、final_sheet.jpg のような組み立てプレビューで判断すること(半分に縮小されているため)があります。

5. 検証

python <SKILL_DIR>/scripts/grade.py verify \
  --style warm_film --src ./photos/original/a.jpg --tgt ./photos/retouched/a.jpg \
  --threshold 2.0

--threshold を指定すると、しきい値を超えた場合に終了コード2で終了するため、パイプラインのゲートチェックに適しています。

6. 他の人に使えるようにエクスポート

python <SKILL_DIR>/scripts/grade.py export --style warm_film --out ./for_lightroom --full

--full を指定すると、接続方法の説明も追加で出力されます(ベースレイヤーを全面に配置 → 領域レイヤーをソフトマスクでブレンド → 画像サイズの1~2%でフェザリング)。 領域スタイルは単一の .cube だけでは表現できません:単一の3D LUTはピクセルカラーの関数であり、「同じ色が領域によって異なる処理を受ける」ことを表現できません。

7. 元に戻すと追跡

python <SKILL_DIR>/scripts/grade.py batches --style warm_film      # このスタイルがどのバッチで構成されているかを確認
python <SKILL_DIR>/scripts/grade.py undo --style warm_film --batch b20261108_101500
python <SKILL_DIR>/scripts/grade.py styles                          # 全スタイル + タグ + 一言説明
python <SKILL_DIR>/scripts/grade.py styles --name warm_film         # 単一スタイルの完全なマニフェスト

undo はバージョンを削除するのではありません:指定されたバッチを構成から除外し、残りのバッチで再フィッティングして、新しいバージョンを生成します。古いバージョンは常に保持されます。

出力の解釈

すべてのコマンドは、JSONをstdoutに出力します。

フィールド意味判断
quality.dE2000_meanトレーニングペアの平均色差(領域分割を含む)< 2 で使用可能;2–4 でやや不十分;> 4 はペアが純粋な色彩関係ではないことを示す
decision今回の提出がどのように処理されたかcreate-new / merge-nearest / merge-into-named
auto.distance最も近いスタイルとの距離閾値より明らかに小さいのに新規作成された、または閾値より明らかに大きいのにマージされた場合、判定を誤っている
auto.nearest最も近い3つのスタイルとその距離「なぜこの判定になったか」を確認するため
learned_fromマージ時に使用された履歴サンプルの数new_samplesのみでarchivedがない場合は、それが初回であることを示す
diagnostics.coverage_pctLUTグリッドポイントがデータでカバーされている割合30–50% が正常(自然画像は色空間の一部のみを占める)
diagnostics.lut_clipped_pct_observedデータがある領域で、0/1にクリップされた割合> 2% はハイライト/シャドウが失われる可能性があることを示す
regions.active実際に有効な領域空の場合、これらの領域が今回のバッチ素材に現れなかったことを意味する
region_diagnostics.<r>.skipped領域がスキップされた理由too few pixels は正常;no residual learned はその領域がグローバルと同様の振る舞いをすることを示す
description.summaryスタイルの一言説明ユーザー向け。そのまま伝える
description.region_extra各領域がグローバルに対して追加で何をしたか領域がある場合のみ
tags自動判定された被写体portrait / landscape / street / indoor / night
diagnostics.prior_shrink事前分布の強度、1.0は完全採用を意味するフィッティングのたびに1.0 / 0.5 / 0.2 / 0.05から最適なものを選択、prior_trials は各値のdEを示す
health.monotonic_violations単調性違反の回数デフォルトでは強制的に0に設定。--no-monotonic で無効にした場合のみ出現
fit_quality.geometry_preservedジオメトリが保持されているかtrueでなければならない
key_match.appliedまず画像の露出をスタイルに合わせたかデフォルトoff;有効にするとstops、gamma、調整後の中央値を示す
transfer.verdict素材が表示用参照、ログ、または判断不能かlog はまずトーンカーブ変換してからfitする必要がある。ambiguous はユーザーにソースを確認させる
warnings[].metric / .note参照画像なしの自己チェックで検出された問題note の内容をそのままユーザーに伝える。空の配列は問題なし
look[元画像, 出力画像] の中央輝度/白飛び割合/彩度「このスタイルがこの画像に何をしたか」を答えるために使用
input_guardfit時に元画像が表示用参照エンコーディングでないことが判明blocking: false は単なる注意喚起であり、ブロックはしない
input_transferapply時に入力にdisplayでない画像が混在ファイルごとにverdictを提供、同上

保真性の赤線

  1. RAWは近似デコードです。librawはカメラホワイトバランスと汎用カラーマトリックスを使用し、カメラDCPは使用しません。同じスタイルを異なるボディに適用すると色がずれます。また、Lightroomのレンダリング結果とも異なります。正確な納品が必要な場合は、ユーザーに16ビットTIFFで書き出すように指示してください。
  2. LUTの基準色空間はsRGB gamma 2.2です。エクスポートした .cube を他の人が使用する際は、これを明示してください。そうしないと、相手が異なる色空間で適用して色がすべて間違ってしまいます。
  3. 領域スタイルは単一の .cube では表現できません。vN.cube はグローバルベースレイヤーのみです。領域マッピングは vN_<region>.cube にあり、マスクとの組み合わせが必要です。エクスポートする際は export --full を使用してください。領域レイヤーと接続方法の説明が同時に出力されます。
  4. 露出オーバーのピクセルはそのまま通過します。レンダリングの最後に、最も明るいチャンネルが0.97以上のピクセルに対して、元の画像にソフトに遷移します。露出オーバー領域には色情報がないため、どんなマッピングも「発明」になります。逆光の太陽のディスクは、領域分割ブロックによって灰色がかった青い斑点に圧縮されることがテストで確認されています。保護を追加することで、暖かい白色光に戻ります。代償として、最も明るい高彩度のハイライトは再マッピングされなくなります(カラーチャート実測トレーニングΔE 1.15→1.21)。露出オーバー領域も一緒に圧縮する必要がある場合は、--no-clip-guard を使用します。
  5. ソフトマスクの境界には遷移帯があります。これは意図的です。2つの異なるマッピングが硬くぶつかると、目に見える輪郭が発生します。代償として、空と木のてっぺんの境界付近で、十数ピクセルの「半ば調整された」領域が発生する可能性があります。
  6. マスクの解像度は128×128のみです。画像の約1/128より細かい構造(枯れ枝、電線、細い手すり)は、それが属する領域に含まれます。これらの構造は通常シルエットに近く、暗くしても目立ちません。ピクセルレベルの正確さが必要な場合は、外部のマスキングツールを使用してください。
  7. 信頼度の低い領域は調整しません。逆光で露出オーバーになり色のない空、葉で細かく砕かれた空など、モデルが信頼できるマスクを生成できない場合は、その部分はグローバルLUTに戻ります。少なく調整する方が、間違った調整をするよりはましです。
  8. 動物のクローズアップが人物として識別される可能性があります。ADE20Kデータセットは、猫のクローズアップをpersonと判定することがあります(テストでは画面の88%を占めました)。このスキルでは、「person領域内の肌色比率」で対処し、5%未満の場合はゼロに戻します。極端に明るい色のペットは、それでもすり抜ける可能性があります。
  9. ref モードを「精密な複製」として宣伝しないでください。Lab分布を合わせるだけであり、実測ΔEは約4です。正確さが必要な場合は fit を使用する必要があります。
  10. 記述レイヤーのスライダー数値は推定値です。measured 列は直接再現可能ですが、estimate_lightroom は再現できません。LUTからスライダーへの一意の解はありません。
  11. 参照画像なしの自己チェックは「明らかな間違い」しか検出できず、「良いか悪いか」は判断できません。これは古典的な統計目標(中央輝度0.45、黒レベル0.02、肌色相20°)を使用しています。夜景はもともと中央輝度が低く、夕日はもともと暖色であるため、各項目に許容範囲が設定されています。警告の意味は「確認してください」であり、「間違っています」ではありません。2つのハードな閾値があります:肌色はセグメンテーションで人が検出された場合にのみ判定されます。そうでなければ、暖色の街並みが画面の30%を皮膚と誤判定する可能性があります。ホワイトバランスは、画面に十分なニュートラルピクセルが存在する場合にのみ判定されます。
  12. ログの判定はヒストグラムの形状に基づき、メタデータには基づきません。この段階では、画像は通常PNGまたはlibrawのレンダリング結果であり、EXIFはすでに失われていることが多いです。そのため、黒レベルの上昇、ハイライトの抑制、ダイナミックレンジの狭さ、低彩度を統計的に評価し、3つ以上が一致した場合にのみ log と判定し、2つの場合は ambiguous と判定します。フラットで褪色したスタイルの完成写真が ambiguous と報告される可能性があります。これは既知の偽陽性の方向性です。
  13. スタイルには露出属性が含まれており、LUTは学習した明るさの範囲内でのみ信頼できます。中央値を0.19から0.28に引き上げるルックを、中央値0.55の写真に適用すると、中央値は0.95に跳ね上がります。——これはバグではなく、ルック自体にそれだけのハイライトの余裕がないからです。実測:フィッティングでは、ソース画像の0~0.42の範囲のみが観測され、ソース画像のp99は0.42、ターゲットのp99は0.56でした。これをp99 0.72の写真に適用すると、範囲外の部分は線形外挿に依存するため、ハイライトが飛んでしまいます。
  14. ユーザーに代わって「露出を合わせる」ことをデフォルトにしないでください。--match-key は、まず画像の中央値をスタイルが学習したレベルに合わせてからLUTを適用できますが、逆光の写真では逆効果になります。実例:逆光のポートレート(顔0.27、背景0.62)を全画面の中央値で合わせると、顔は0.08に圧縮され、被写体が真っ暗になります。そのため、デフォルトはoffです。--match-key は、「同じカメラ位置、同じ光量の連続 shooting で露出が一致しない場合」にのみ有効にすべきです。判断基準は単純です:この写真の被写体の明るさと背景の明るさの関係が、参照写真と一致しているかどうかです。
  15. フレーム内で被写体が背景より暗く、参照フレームでは被写体が最も明るい場合、どのようなグローバル単調マッピングも両方を満足させることはできません。両方を満たすには局所的な調整が必要です。この場合、被写体を優先し、背景が明るくなることを受け入れます。逆に背景を優先すると、顔がつぶれてしまいます。
  16. 素材がすでに持っている暖色かぶりは、暖色系スタイルと組み合わさって橙赤色になることがあります。すでに0.46段暖かい暗めのポートレートに、暖色系ルックを適用すると、顔は暖かい茶色から橙赤色に変わり、キャストが0.77に跳ね上がり、頬に色むらが発生します。この場合、まずホワイトバランスを中和しようとしてはなりません。画面内の唯一の「ニュートラルカラー」は、しばしば同じ照明で照らされた壁であり、それを基準に補正すると、壁が緑に、肌色が黄緑に傾く可能性があります(実測では0.5/0.75の2段階ともに悪化)。正しい順序は:まず --strength を下げる(0.6付近)。それでも満足できない場合は、配色を諦め、純粋な輝度カーブまたは線形露出ゲインに変更します——明暗のみを動かし、色相は動かさず、暖色系はそのまま残します。
  17. JPEGは保存するたびに劣化し、不可逆です。納品はq98で一発成型します。さらに編集が必要な場合は、__master.png から編集し、すでに保存されたJPEGに繰り返し上書きしないでください。同じLUTをq95で3回保存すると、PSNRは48.7から48.6にわずかしか低下しませんが、滑らかなグラデーション(明るくされた空、背景ボケ)に毎回ブロック状の階段状ノイズが蓄積されます。劣化の原因を特定するには:ソース画像が最初に圧縮されていた場合(<0.10 バイト/ピクセル)は、ソースが原因であると認識し、エンコードパラメータに原因を求めないでください。

パフォーマンスベースライン

シナリオ10.8 MP画像の処理時間
純粋なグローバルレンダリング3.6 秒
領域レンダリング(2領域有効)6.7 秒
セグメンテーション推論(512px)+ アップサンプリング0.7 秒
1回の fit(10ペア、領域分割とタグ付け含む)約12 秒

ボトルネックはピクセル単位のLUT gather(1ピクセルあたり8回のランダム読み取り)であり、アルゴリズム上の分岐ではありません。バッチ処理の際は、マルチプロセスで画像ごとに並列化するのが最も効率的です。

よくあるタスク

ユーザーの発言対処法
「この写真の色味に、あの写真たちを合わせて」ref でスタイル生成 → apply でバッチ適用
「これらはレタッチ済みです。今後はこのスタイルでお願いします」fit --src 元画像 --tgt レタッチ後 → apply
「またレタッチしました。このスタイルでお願いします」fit 時に --name で同じスタイルを指定。自動マージ&再フィッティング
「空だけ暗くして、地面と顔はそのままに」デフォルトで領域分割は有効。regions.active に sky が含まれていることを確認
「このスタイルは濃すぎる」apply --strength 0.6。再フィッティングは不要
「このバージョンは前のバージョンより良くない」apply --version <古いバージョン番号>
「前のバッチは間違っていたので、取り消してください」batches で batch_id を確認 → undo
「この写真にはどのスタイルを使うべきですか」apply --style auto を実行し、auto.ranking を確認
「このスタイルは一体何を調整していますか」styles --name X を実行し、description.summary を読む
「DaVinci Resolveで使えるようにエクスポートして」export --full。基準色空間がsRGB gamma 2.2であること、領域レイヤーにはソフトマスクが必要であることを伝える
「逆光/日の出の場合、太陽が灰色にならないように」デフォルトで対応済み(露出オーバー保護)。それでも灰色がかる場合は、太陽が本当に露出オーバーか確認
「この写真、調整は合ってますか?問題はどこですか?」check --in X --person を実行し、warnings を1つずつ伝える。自分で判断を追加しない
「これはログ素材ですか?」check --in X を実行し、transfer.verdict と transfer.signals を確認
「このスタイルを適用するとハイライトが飛びませんか?」apply の look ですでに前後比較が提供されている。warnings は直接指摘する
「この写真は参考写真よりずっと明るく、スタイルを適用すると白くなります」まず look.median_luma の差を確認。--strength を下げるか、同一光量の連続 shooting なら --match-key on を使用
「被写体が背景より暗く、スタイルを適用すると被写体が黒くなりました」--match-key を有効にしない。これは参照写真とこの写真の光量バランスが異なるためで、局所的な調整が必要
「この写真自体が暖かみがあり、スタイルを適用すると顔がオレンジ色になりました」--strength を0.6に下げて試す。ホワイトバランスの中和は行わない(壁が緑になる可能性があるため)。赤線16を参照

関連スキル