前端

螢幕截圖色彩並非 sRGB,而您的 UI 將會顯現這一點

從 macOS 螢幕截圖上吸取的十六進位色碼,是採用顯示器描述檔,而非 sRGB。將它貼到程式碼中,顏色就會產生色偏。以下是來回轉換的證明。

本文由 AI 模型從英文原文翻譯而來,用字可能與原文有所出入。 閱讀英文原文

如果你用滴管從螢幕截圖中取樣顏色,並將其十六進位碼貼到樣式表中來匹配顏色,結果可能會出現明顯的色差。這個數字並沒有錯,它只是在錯誤的空間中被測量。在廣色域顯示器上,螢幕截圖是使用顯示器設定檔進行編碼,而你的 CSS、主題資訊清單和設計權杖則被解讀為 sRGB。相同的數字,不同的顏色。

我在將一個 UI 元素與另一個看起來已經正確的元素進行匹配時遇到了這個問題。我取樣到的值是 #6DCEBD。而實際產生該像素的值是 #28D0BC。這兩者並不相近。

滴管工具實際提供的資訊

在廣色域螢幕上的螢幕截圖,並非 sRGB 值的傾印。合成器會渲染至顯示器的色彩空間,而螢幕截圖檔案則帶有一個描述該色彩空間的 ICC 設定檔。當您開啟該檔案並讀取一個像素時,您會得到一個以該設定檔表示的數值。

您的程式碼則以不同的方式被讀取。CSS 十六進位碼、主題資訊清單,以及大多數的設計權杖格式,根據定義都是 sRGB。在處理管線中,沒有任何環節會為您進行轉換。因此,這個往返過程包含兩次轉換,而其中只有一次對您是可見的。

為何取樣的十六進位碼無法往返 1 渲染 2 取樣 3 貼回 sRGB 來源 40,208,188 螢幕上 109,206,189 取樣的十六進位碼 6DCEBD 再次渲染 138,204,190 // 步驟 2 讀取帶有色彩設定檔的數值,步驟 3 將其視為 sRGB

可茲證明的往返過程

請勿僅憑信念就接受此理論。您可以用幾行程式碼驗證整個流程,因為 您已經知道一些基本事實:您自己的程式碼所宣告的任何顏色,根據定義,就是一個 sRGB 值。在螢幕截圖中找到它,然後轉換回來。

from PIL import Image, ImageCms
import io

im = Image.open("screenshot.png")
profile = ImageCms.ImageCmsProfile(io.BytesIO(im.info["icc_profile"]))
to_srgb = ImageCms.buildTransformFromOpenProfiles(
    profile, ImageCms.createProfile("sRGB"), "RGB", "RGB", renderingIntent=0
)
converted = ImageCms.applyTransform(im.convert("RGB"), to_srgb)

三個宣告的顏色,先原始取樣再轉換:

宣告值 (sRGB) 檔案中的原始像素 轉換後
10, 8, 23 10, 8, 22 10, 8, 23
25, 24, 36 25, 24, 35 25, 24, 36
163, 18, 98 147, 33, 96 164, 18, 99

每個宣告值都在一個單位內返回。管線已確認。現在,對您想匹配的顏色執行正向運算:

sRGB  40, 208, 188   becomes on screen   109, 206, 189
sRGB 109, 206, 189   becomes on screen   138, 204, 190

第一行是答案。第二行是如果你將取樣的十六進位碼直接貼到你的程式碼中會發生的情況:你會得到第三種顏色,它足夠接近,看起來像是刻意為之,但又錯得夠離譜,以至於在你原本要匹配的元素旁邊顯得混濁。

為何深色會隱藏問題

再看一下表格。前兩列移動了一個單位。第三列的紅色移動了 16,綠色移動了 15。這種不對稱性就是這個錯誤能在隨意檢查中倖存的原因。

在接近中性軸和黑色的地方,色彩空間之間的轉換幾乎是恆等轉換。深色背景、邊框和陰影的來回轉換幾乎都完美無瑕。如果你抽查螢幕截圖,並將其與背景顏色比對,看到相符,你就會斷定螢幕截圖是 sRGB 色彩空間,然後就不再深究。

差異存在於飽和度存在的地方。強調色、品牌色調、狀態指示器和語法高亮,正是那些偏移最大、也正是你最可能用肉眼比對的數值。你執行的檢查之所以通過,是因為你在色域中錯誤不存在的部分執行了它。

還有一個形式相同的第二個陷阱。如果一個工具在儲存時移除或忽略 ICC 設定檔,數值會維持原樣,但其意義卻遺失了。透過聊天應用程式貼上、調整大小或重新編碼的螢幕截圖,可能會在沒有任何設定檔的情況下送達。到那時,檔案中沒有任何東西能告訴你你處於哪個色彩空間,而唯一誠實的答案是,你無法僅從檔案本身得知。

兩種修正方法

明確轉換。 如果你需要一個絕對值,請執行上述的轉換並使用轉換後的數字。當顏色必須寫入樣式表、資訊清單或其他人會讀取的權杖檔案時,這是正確的選擇。保留原始螢幕截圖及其色彩描述檔,以便日後可以重複推導過程。

或在單一影像內比較。 如果你的實際目標是「讓元素 A 與元素 B 的顏色相同」,你根本不需要絕對值。擷取一張同時包含兩者的螢幕截圖,用相同的工具和相同的程式碼路徑提取兩個像素,然後將它們相互比較。無論檔案處於哪個色彩空間,兩個樣本都在其中。即使你從未得知 sRGB 值,這個比較也是有效的。

第二種方法更為穩健,我現在會優先採用。在引發此事的案例中,最終的檢查正是如此:從一次擷取中取樣參考元素和目標元素,並比較色相和飽和度。色相相差 0.0 度,飽和度相差 0.003。不需要任何轉換即可確認匹配。

有兩個細節使得影像內比較變得可靠:

  • 取樣字符內部,而非邊緣。 反鋸齒文字在邊界處會與背景融合。取該區域中飽和度最高的像素,而不是取平均值,否則你測量到的會是混合色而非原始顏色。
  • 注意停用狀態。 工具列中一個灰化的圖示,即使其顏色與啟用狀態的顏色相差甚遠,仍可能通過飽和度過濾器。應刻意挑選你的參考元素,而不是掃描整個區域並相信最常見的值。

這種情況會在哪裡出現

此模式並非特定於單一平台或單一檔案格式。它出現在任何將渲染後的影像視為真實來源,而其數值後續將在固定空間中解譯的場合。

  • 將 UI 元素與廣色域機器匯出的設計稿顏色進行匹配。
  • 透過取樣一個你無法直接讀取的現有檔案,來建立主題或外觀檔案。
  • 編寫視覺迴歸測試,將擷取的像素與寫死的預期值進行比較。
  • 從相片或影片影格中提取調色盤,以用於 CSS。
  • 使用螢幕截圖回報顏色錯誤,而讀者在取樣你的影像後,得到一個你們雙方都未曾渲染出的數值。

視覺迴歸測試值得特別提出警告。一個在筆記型電腦上擷取畫面並與 sRGB 常數進行斷言的測試套件,其通過或失敗將取決於執行它的機器。如果 CI 執行器與開發者機器的色彩描述檔不同,失敗看起來會像是偶發性問題,並在重試後被忽略。

常見問答

這也會影響 Windows 和 Linux 嗎? 這個機制是通用的,但會不會遇到這個問題取決於顯示器和擷取工具。當擷取的影像被編碼為廣色域空間而非 sRGB 時,問題就會出現。使用標準色域顯示器擷取至 sRGB 可以乾淨地來回轉換,這就是為什麼在廣色域面板在筆記型電腦上普及之前,這個問題很少見。

我可以直接移除色彩描述檔嗎? 不行。移除色彩描述檔並不會轉換任何東西。它只會移除轉換所需的資訊,留下一堆其意義現在無從得知的數字。如果一個檔案沒有色彩描述檔,你無法僅從像素值中回復其原始的色彩空間。

我的設計工具顯示的十六進位碼和我的螢幕截圖不同。哪一個才是對的? 如果沒有標明色彩空間,那麼兩個都不對。你應該詢問每個數字是在哪個色彩空間中。設計工具通常會顯示 sRGB 值,但透過廣色域顯示器來預覽它們,這正是螢幕上的像素和回報的十六進位碼不一致的原因。

這和 gamma 或位元深度是同一回事嗎? 不是。Gamma 編碼和位元深度是不同的問題,它們同樣也可能扭曲天真的像素運算。這個特定的問題是關於數字所表示的色彩空間,即使在整個過程中正確處理了 gamma 並使用每通道 8 位元,這個問題仍然存在。

我要如何防止這種情況再次發生? 在數值的旁邊寫下其色彩空間。在十六進位常數旁加上一個註解說明是 sRGB,或是在檔名中記錄樣本的來源,這些做法幾乎沒有成本,卻能消除導致這個錯誤發生的模糊性。保留帶有色彩描述檔的原始擷取影像,這樣任何從中衍生的數值都可以被重新計算,而不是重新猜測。