Community影像github.com

naokuotong-1/photo-color-grader

攝影保真調色。用於把「調色風格」變成可復用的 3D LUT 並批量應用,支援天空/人像/植被等語義分割區、批次級自動沉澱與撤銷、題材自動識別與選風格,並在沒有參考圖時用經典場景統計自查成片、識別素材是否為 log 未轉色。只做逐像素色彩映射,不重繪、不改內容、不改變幾何,適合對保真有硬要求的攝影交付。

photo-color-grader 是什麼?

此技能為一專業攝影保真調色工具,專注於將調色風格轉化為可復用的 3D LUT(色彩查找表),並支援批量應用。它透過語義分割(天空、人像、植被等區域)實現僅對特定區域的色彩映射,確保不改變圖像內容、幾何或解析度,適用於對色彩保真度有嚴格要求的攝影交付場景。技能支援兩種風格生成方式:從「原圖/修後圖」配對精確學習(fit),或從參考圖快速統計遷移(ref)。批次處理時能自動判別題材、合併相似風格,並提供完整版本控制與撤銷功能。此外,它還能無參考圖地診斷成片問題(如色偏、高光溢出),並識別 log 素材。支援平台包括 Python 環境,依賴 numpy、PIL、onnxruntime 等套件,並可處理 RAW 檔。

相容平台~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 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

攝影保真調色

何時使用

  • 用戶給一張參考圖和一張待調照片,要求「照著這張的色調調」
  • 用戶有一批已經修好的照片,想把其中的風格固化下來,套到新拍的一批
  • 用戶要「只壓天空、別動地面和臉」這類分區調整
  • 用戶要批量應用某個 Look,並要求「不要改變內容、不要重繪」
  • 用戶要驗證某個風格是否保真:有沒有斷層、色偏、把高光或暗部壓死
  • 用戶丟一張圖問「這張調得對不對」「哪裡有問題」,沒有參考圖
  • 用戶給的素材可能是 log / 未轉色(相機 raw 裡導出的 S-Log3、V-Log、HLG 幀)
  • 用戶抱怨生成式修圖「把人臉改掉了」「解析度掉了」——這類需求正是本 skill 的場景

不適用:需要改光線方向、換天空、加物體、語義重繪的任務。本 skill 只輸出色彩映射,做不到內容修改。

核心模型

  1. 一個「風格」= 基礎 .cube + 可選的分區 .cube(每個區域一個)+ 版本記錄 + 批次檔案 + 題材標籤,存放在 color-styles/<name>/
  2. 應用風格 = 逐像素查表,再按語義遮罩把分區 LUT 蓋上去。輸出與原圖尺寸嚴格一致,結構 SSIM 恆等於 1.0
  3. 風格有兩種產生方式:
    • fit:從「原圖 / 修後圖」配對學習,精確(實測 ΔE2000 0.55–0.9),需要 3 對以上
    • ref:從參考圖統計遷移,快速但是粗略(實測 ΔE2000 ≈ 4),單張圖即可,適合冷啟動
  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')"      # 16bit 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

不需要參考圖,只回答兩件事:

  • 這是什麼素材:transfer.verdict = display / ambiguous / log。log 素材直接 fit 會把 log 曲線本身學進 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] 就是在說:這張片子比風格學過的素材亮太多,套上去高光頂穿了。這種情況只有三條路——--strength 0.7 收力度、--match-key on 先把曝光對到風格學過的水平(只適合同光比的連拍)、或者接受背景變亮。不要不問就替用戶選。

--match-key off|on|auto(預設 off):先把畫面按中位亮度對到風格學過的水平,再套 LUT。對逆光片會反向傷害主體,見「保真紅線」第 14 條。

輸出格式與畫質

預設每次落兩個檔案,別只發第一個:

產物預設用途
<名>__<風格>_v<N>.jpgq98、4:4:4、保留源 ICC/EXIF/DPI交付、發人、發朋友圈
<名>__<風格>_v<N>__master.png8bit 無損、帶 ±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 走 16bit(需要 tifffile,裝不上會自動降級成 8bit PNG 並寫 master_note,不會毀掉已生成的 JPG)。批量出圖嫌佔地方用 --master none。
  • 源檔案本來就是 .png/.tif/.bmp 時不再生成母版——主產物已經無損。
  • ICC、EXIF、DPI 從源圖透傳。丟掉 ICC 最像"畫質變差":P3/AdobeRGB 的圖寫成無標籤 RGB,任何看圖軟體都按 sRGB 解釋,顏色立刻發灰發悶。
  • 抖動只在無損格式開。JPEG 的量化誤差本來就比 1 LSB 粗一個數量級,那裡抖動只是白燒碼率。

用戶說"被壓縮了",先查源圖,不要先調參數。 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         # 單風格的完整 manifest

undo 不是刪版本:它把那一批從構成裡剔除、用剩下的批次重擬合、生成一個新版本。舊版本永遠保留。

輸出判讀

所有命令都往 stdout 列印 JSON。

欄位含義判斷
quality.dE2000_mean訓練對上的平均色差(含分區)< 2 可用;2–4 勉強;> 4 說明配對不是純色彩關係
decision本次提交怎麼處理的create-new / merge-nearest / merge-into-named
auto.distance與最近風格的距離明顯小於門檻卻新建了,或者明顯大於門檻卻併入了,說明判錯了
auto.nearest最近三個風格及距離用來看「為什麼這樣判」
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單調性破壞次數預設已強制歸零;用 --no-monotonic 關掉才會出現
fit_quality.geometry_preserved幾何是否保持必須為 true
key_match.applied是否先把這張圖對到風格的曝光上預設 off;開了會給出 stops、gamma 和調整後的中位
transfer.verdict素材是顯示參考、log,還是拿不准log 必須先轉色再 fit;ambiguous 讓用戶確認來源
warnings[].metric / .note無參考自檢抓到的問題按 note 原話轉述給用戶;空陣列就是沒發現硬傷
look[原圖, 成片] 的中位亮度 / 死白比例 / 飽和度用它回答「這個風格對這張圖做了什麼」
input_guardfit 時發現原圖不是顯示參考編碼blocking: false,只是提醒,不攔
input_transferapply 時輸入裡混了非 display 的圖逐檔案給 verdict,同上

保真紅線

  1. RAW 是近似解碼。libraw 用相機白平衡 + 通用色彩矩陣,不用相機 DCP。同一個風格套不同機身會偏,也不是 Lightroom 的渲染結果。要精確交付就讓用戶導 16bit 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. 軟遮罩邊界有一個過渡帶,這是有意的:兩個不同映射硬碰會產生可見輪廓。代價是天空與樹梢交界處可能有十幾像素的「半調」。
  6. 遮罩只有 128×128 解析度。細於約 1/128 畫幅的結構(枯枝、電線、細欄)會被算進所在分區。這些結構通常接近剪影,被壓暗後看不出;要求像素級精確請交給外部蒙版工具。
  7. 低置信度的區域不做調整。逆光過曝到沒顏色的天空、被枝葉切碎的天空,模型給不出可靠遮罩,那部分退回全域 LUT。寧可少調,不調歪。
  8. 動物特寫可能被識別人像。ADE20K 會把特寫貓判成 person(實測佔 88% 畫面),本 skill 用「person 區域內膚色佔比」兜底,低於 5% 就歸零。極端的淺色寵物仍可能漏過。
  9. ref 模式不要宣傳成「精準複製」。它只對齊 Lab 分佈,實測 ΔE 約 4;需要精準必須走 fit。
  10. 描述層的滑塊數字是估算。measured 一欄可直接復現,estimate_lightroom 不能——LUT 到滑塊沒有唯一解。
  11. 無參考自檢只能發現「明顯的錯」,判斷不了「好不好看」。它用的是經典統計目標(中位亮度 0.45、黑位 0.02、膚色 20°),而夜拍本來就該低中位、日落本來就該偏暖,所以每項都留了寬容帶,告警的含義是「看一眼」,不是「錯了」。兩條硬門限:膚色只在分割檢測到人時才判,否則暖色街景會把 30% 畫面誤判成皮膚;白平衡只在畫面裡存在足夠中性像素時才判。
  12. log 判別看的是直方圖形狀,不是元數據。圖到這一步通常已是 PNG 或 libraw 的渲染結果,EXIF 早丟了。所以它統計黑位抬高 / 高光收著 / 動態範圍窄 / 低飽和,三項以上一致才判 log,兩項判 ambiguous。一條被壓平的褪色風格成片可能被報成 ambiguous,這是已知的假陽性方向。
  13. 風格是帶曝光屬性的,LUT 只在它學過的明暗範圍裡可信。一個把中位從 0.19 提到 0.28 的 look,套到中位 0.55 的片上,中位會衝到 0.95 —— 不是 bug,是 look 本身沒有那麼多高光餘量。實測:擬合只觀測到源圖 0~0.42 的區間,源圖 p99 0.42 / 目標 p99 0.56;套到 p99 0.72 的片上,超出部分靠線性外推,等於把高光頂穿。
  14. 不要預設替用戶「對齊曝光」。--match-key 能先把畫面中位拉到風格學過的水平再套 LUT,但對逆光片會反過來:實拍裡一張逆光人像(臉 0.27、背景 0.62)按全畫面中位對齊後,臉被壓到 0.08,主體全黑。所以它預設 off,只在「同機位同光比的連拍裡曝光不一致」時才該打開。判斷依據很簡單:這張片的主體亮度和背景亮度的關係,是否和參考片一致。
  15. 一幀裡主體比背景暗、而參考幀裡主體最亮時,任何全域單調映射都兩頭討好不了。要兩者都滿足只能做局部調整。這種情況下優先保主體,接受背景變亮;反過來保背景會把臉壓死。
  16. 素材已有的暖偏會和暖調風格疊加成橙紅。一張本身偏暖 0.46 檔的暗調人像,套上暖調 look 後 face 從暖棕變橙紅、cast 衝到 0.77,臉頰出現色斑感。這時不要先做白平衡中和——畫面裡唯一的"中性色"往往是同一盞燈照亮的牆,按它校正會把牆推綠、膚色推黃綠(實測連試 0.5/0.75 兩檔都更糟)。正確順序是:先降 --strength(0.6 附近),仍不滿意就放棄套色、改走純亮度曲線或線性曝光增益——只動明暗不動色相,暖調原樣保留。
  17. JPEG 每存一次就少一點,且不可逆。交付走 q98 一次成型;還要再改就從 __master.png 改,不要在已經存過的 JPG 上反覆疊。同一個 LUT 存三遍 q95,PSNR 從 48.7 掉到 48.6 看著不多,但每次都會在平滑漸變(被抬亮的天空、背景虛化)裡積一層塊狀台階。判定丟的是誰:源圖先被壓過(<0.10 位元組/像素)就認源頭,別在編碼參數上找原因。

性能基準

場景10.8 MP 圖耗時
純全域渲染3.6 s
分區渲染(2 個區域生效)6.7 s
分割推理(512px)+ 上取樣0.7 s
一次 fit(10 對圖,含分區與打標)約 12 s

瓶頸在逐像素 LUT gather(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
「導出給達芬奇用」export --full;提醒錨定 sRGB gamma 2.2、分區層要配軟蒙版
「逆光/日出,太陽別被壓灰」預設已做(過曝保護);若仍偏灰檢查太陽是否真的過曝
「這張調得對不對,問題在哪」check --in X --person,把 warnings 逐條轉述,別自己加判斷
「這是不是 log 素材」check --in X,看 transfer.verdict 與 transfer.signals
「這個風格套上去會不會燒高光」apply 的 look 已給前後對比,warnings 會直接點出來
「這張比參考亮太多,套完發白」先看 look.median_luma 落差;收 --strength,或同光比連拍用 --match-key on
「主體比背景暗,套完主體黑了」不要開 --match-key;這是參考片與這張片的光比不同,只能局部調整
「這張本身就偏暖,套完臉發橙」降 --strength 到 0.6 先試;別做白平衡中和(會把牆推綠),見紅線 16

參考

  • references/style-schema.md:風格目錄、style.json 欄位、版本與批次語義
  • references/color-pipeline.md:色彩管理、演算法細節、已知局限與調參建議

相關技能