249 lines
6.9 KiB
Python
249 lines
6.9 KiB
Python
"""色准(ΔE2000 / 标准色)相关纯算法。
|
||
|
||
Step 1 重构:从 pqAutomationApp.PQAutomationApp 中原样搬迁以下方法,
|
||
去掉 self 参数,改为模块级纯函数:
|
||
- calculate_delta_e_2000
|
||
- calculate_color_accuracy
|
||
- get_accuracy_color_standards
|
||
"""
|
||
|
||
import math
|
||
|
||
import numpy as np
|
||
|
||
|
||
def calculate_delta_e_2000(
|
||
measured_x, measured_y, measured_lv, standard_x, standard_y
|
||
):
|
||
"""
|
||
计算 ΔE 2000 色差(修正版)
|
||
|
||
Args:
|
||
measured_x, measured_y: 测量的 xy 坐标
|
||
measured_lv: 测量的亮度(cd/m²)
|
||
standard_x, standard_y: 标准的 xy 坐标
|
||
|
||
Returns:
|
||
float: ΔE 2000 色差值
|
||
"""
|
||
|
||
# ========== 1. xy → XYZ(使用实际亮度)==========
|
||
def xy_to_XYZ(x, y, Y):
|
||
if y == 0:
|
||
return 0, 0, 0
|
||
X = x * Y / y
|
||
Z = (1 - x - y) * Y / y
|
||
return X, Y, Z
|
||
|
||
# 修复:使用实际测量的亮度
|
||
X1, Y1, Z1 = xy_to_XYZ(measured_x, measured_y, measured_lv)
|
||
|
||
# 修复:标准值使用相同的参考亮度(只比较色度差异)
|
||
X2, Y2, Z2 = xy_to_XYZ(standard_x, standard_y, measured_lv)
|
||
|
||
# ========== 2. XYZ → Lab(D65 白点)==========
|
||
def XYZ_to_Lab(X, Y, Z):
|
||
# D65 白点
|
||
Xn, Yn, Zn = 95.047, 100.000, 108.883
|
||
|
||
# 归一化
|
||
xr = X / Xn
|
||
yr = Y / Yn
|
||
zr = Z / Zn
|
||
|
||
# f(t) 函数
|
||
def f(t):
|
||
delta = 6.0 / 29.0
|
||
if t > delta ** 3:
|
||
return t ** (1.0 / 3.0)
|
||
else:
|
||
return t / (3 * delta ** 2) + 4.0 / 29.0
|
||
|
||
fx = f(xr)
|
||
fy = f(yr)
|
||
fz = f(zr)
|
||
|
||
L = 116 * fy - 16
|
||
a = 500 * (fx - fy)
|
||
b = 200 * (fy - fz)
|
||
|
||
return L, a, b
|
||
|
||
L1, a1, b1 = XYZ_to_Lab(X1, Y1, Z1)
|
||
L2, a2, b2 = XYZ_to_Lab(X2, Y2, Z2)
|
||
|
||
# ========== 3. ΔE 2000 公式 ==========
|
||
L_bar = (L1 + L2) / 2.0
|
||
C1 = math.sqrt(a1 ** 2 + b1 ** 2)
|
||
C2 = math.sqrt(a2 ** 2 + b2 ** 2)
|
||
C_bar = (C1 + C2) / 2.0
|
||
|
||
G = 0.5 * (1 - math.sqrt(C_bar ** 7 / (C_bar ** 7 + 25 ** 7)))
|
||
|
||
a1_prime = a1 * (1 + G)
|
||
a2_prime = a2 * (1 + G)
|
||
|
||
C1_prime = math.sqrt(a1_prime ** 2 + b1 ** 2)
|
||
C2_prime = math.sqrt(a2_prime ** 2 + b2 ** 2)
|
||
C_bar_prime = (C1_prime + C2_prime) / 2.0
|
||
|
||
def calc_hue(a_prime, b):
|
||
if a_prime == 0 and b == 0:
|
||
return 0
|
||
h = math.atan2(b, a_prime) * 180 / math.pi
|
||
if h < 0:
|
||
h += 360
|
||
return h
|
||
|
||
h1_prime = calc_hue(a1_prime, b1)
|
||
h2_prime = calc_hue(a2_prime, b2)
|
||
|
||
if C1_prime == 0 or C2_prime == 0:
|
||
delta_h_prime = 0
|
||
else:
|
||
delta_h = h2_prime - h1_prime
|
||
if abs(delta_h) <= 180:
|
||
delta_h_prime = delta_h
|
||
elif delta_h > 180:
|
||
delta_h_prime = delta_h - 360
|
||
else:
|
||
delta_h_prime = delta_h + 360
|
||
|
||
if C1_prime == 0 or C2_prime == 0:
|
||
H_bar_prime = h1_prime + h2_prime
|
||
else:
|
||
if abs(h1_prime - h2_prime) <= 180:
|
||
H_bar_prime = (h1_prime + h2_prime) / 2.0
|
||
elif h1_prime + h2_prime < 360:
|
||
H_bar_prime = (h1_prime + h2_prime + 360) / 2.0
|
||
else:
|
||
H_bar_prime = (h1_prime + h2_prime - 360) / 2.0
|
||
|
||
delta_L_prime = L2 - L1
|
||
delta_C_prime = C2_prime - C1_prime
|
||
delta_H_prime = (
|
||
2
|
||
* math.sqrt(C1_prime * C2_prime)
|
||
* math.sin(math.radians(delta_h_prime / 2.0))
|
||
)
|
||
|
||
S_L = 1 + (0.015 * (L_bar - 50) ** 2) / math.sqrt(20 + (L_bar - 50) ** 2)
|
||
S_C = 1 + 0.045 * C_bar_prime
|
||
|
||
T = (
|
||
1
|
||
- 0.17 * math.cos(math.radians(H_bar_prime - 30))
|
||
+ 0.24 * math.cos(math.radians(2 * H_bar_prime))
|
||
+ 0.32 * math.cos(math.radians(3 * H_bar_prime + 6))
|
||
- 0.20 * math.cos(math.radians(4 * H_bar_prime - 63))
|
||
)
|
||
|
||
S_H = 1 + 0.015 * C_bar_prime * T
|
||
|
||
delta_theta = 30 * math.exp(-(((H_bar_prime - 275) / 25) ** 2))
|
||
R_C = 2 * math.sqrt(C_bar_prime ** 7 / (C_bar_prime ** 7 + 25 ** 7))
|
||
R_T = -R_C * math.sin(math.radians(2 * delta_theta))
|
||
|
||
kL = 1.0
|
||
kC = 1.0
|
||
kH = 1.0
|
||
|
||
delta_E = math.sqrt(
|
||
(delta_L_prime / (kL * S_L)) ** 2
|
||
+ (delta_C_prime / (kC * S_C)) ** 2
|
||
+ (delta_H_prime / (kH * S_H)) ** 2
|
||
+ R_T * (delta_C_prime / (kC * S_C)) * (delta_H_prime / (kH * S_H))
|
||
)
|
||
|
||
return delta_E
|
||
|
||
|
||
def calculate_color_accuracy(measured, standard):
|
||
"""计算色差(简化版,欧氏距离 × 1000)"""
|
||
delta_E = {}
|
||
|
||
for color in measured.keys():
|
||
dx = measured[color][0] - standard[color][0]
|
||
dy = measured[color][1] - standard[color][1]
|
||
delta_E[color] = np.sqrt(dx * dx + dy * dy) * 1000
|
||
|
||
return delta_E
|
||
|
||
|
||
# 29 色 SDR 标准色板(保持与原实现一致)
|
||
_SDR_COLOR_PATTERNS = [
|
||
("White", 255, 255, 255),
|
||
("Gray 80", 230, 230, 230),
|
||
("Gray 65", 209, 209, 209),
|
||
("Gray 50", 186, 186, 186),
|
||
("Gray 35", 158, 158, 158),
|
||
("Dark Skin", 115, 82, 66),
|
||
("Light Skin", 194, 150, 130),
|
||
("Blue Sky", 94, 122, 156),
|
||
("Foliage", 89, 107, 66),
|
||
("Blue Flower", 130, 128, 176),
|
||
("Bluish Green", 99, 189, 168),
|
||
("Orange", 217, 120, 41),
|
||
("Purplish Blue", 74, 92, 163),
|
||
("Moderate Red", 194, 84, 97),
|
||
("Purple", 92, 61, 107),
|
||
("Yellow Green", 158, 186, 64),
|
||
("Orange Yellow", 230, 161, 46),
|
||
("Blue (Legacy)", 51, 61, 150),
|
||
("Green (Legacy)", 71, 148, 71),
|
||
("Red (Legacy)", 176, 48, 59),
|
||
("Yellow (Legacy)", 237, 199, 33),
|
||
("Magenta (Legacy)", 186, 84, 145),
|
||
("Cyan (Legacy)", 0, 133, 163),
|
||
("100% Red", 255, 0, 0),
|
||
("100% Green", 0, 255, 0),
|
||
("100% Blue", 0, 0, 255),
|
||
("100% Cyan", 0, 255, 255),
|
||
("100% Magenta", 255, 0, 255),
|
||
("100% Yellow", 255, 255, 0),
|
||
]
|
||
|
||
|
||
def _rgb_to_xy_srgb(r, g, b):
|
||
"""sRGB (8bit) → CIE 1931 xy"""
|
||
r, g, b = r / 255.0, g / 255.0, b / 255.0
|
||
|
||
def gamma_decode(c):
|
||
if c <= 0.04045:
|
||
return c / 12.92
|
||
else:
|
||
return ((c + 0.055) / 1.055) ** 2.4
|
||
|
||
r_linear = gamma_decode(r)
|
||
g_linear = gamma_decode(g)
|
||
b_linear = gamma_decode(b)
|
||
|
||
# sRGB → XYZ(D65 白点,IEC 61966-2-1)
|
||
X = r_linear * 0.4124564 + g_linear * 0.3575761 + b_linear * 0.1804375
|
||
Y = r_linear * 0.2126729 + g_linear * 0.7151522 + b_linear * 0.0721750
|
||
Z = r_linear * 0.0193339 + g_linear * 0.1191920 + b_linear * 0.9503041
|
||
|
||
total = X + Y + Z
|
||
if total == 0:
|
||
return 0.3127, 0.3290 # D65 白点
|
||
|
||
return X / total, Y / total
|
||
|
||
|
||
def get_accuracy_color_standards(test_type):
|
||
"""
|
||
获取色准测试的标准 xy 色度坐标(动态计算)
|
||
|
||
Args:
|
||
test_type: 测试类型 ("sdr_movie" 或 "hdr_movie")
|
||
|
||
Returns:
|
||
dict: {color_name: (x, y), ...}
|
||
"""
|
||
# 注意:原实现对 sdr/hdr 使用同一张色板,这里保持原行为。
|
||
del test_type # 参数保留以兼容调用方签名
|
||
standards = {}
|
||
for name, r, g, b in _SDR_COLOR_PATTERNS:
|
||
standards[name] = _rgb_to_xy_srgb(r, g, b)
|
||
return standards
|