Files
pqAutomationApp/app/tests/color_accuracy.py
2026-06-12 16:46:30 +08:00

273 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""色准ΔE2000 / 标准色)相关纯算法。
Step 1 重构:从 pqAutomationApp.PQAutomationApp 中原样搬迁以下方法,
去掉 self 参数,改为模块级纯函数:
- calculate_delta_e_2000
- calculate_color_accuracy
- get_accuracy_color_standards
"""
import math
import numpy as np
D65_X = 0.3127
D65_Y = 0.3290
# Calman ColorChecker 参考 xy与 Calman dE2000 对齐;比较时使用实测 Y 作为目标 Y
_ACCURACY_REFERENCE_XY = {
"White": (0.3127, 0.3282),
"Gray 80": (0.3128, 0.3283),
"Gray 65": (0.3118, 0.3270),
"Gray 50": (0.3122, 0.3282),
"Gray 35": (0.3124, 0.3278),
"Dark Skin": (0.4042, 0.3686),
"Light Skin": (0.3774, 0.3562),
"Blue Sky": (0.2535, 0.2671),
"Foliage": (0.3379, 0.4287),
"Blue Flower": (0.2691, 0.2484),
"Bluish Green": (0.2578, 0.3544),
"Orange": (0.5047, 0.4088),
"Purplish Blue": (0.2166, 0.1857),
"Moderate Red": (0.4554, 0.3098),
"Purple": (0.2889, 0.2135),
"Yellow Green": (0.3771, 0.4937),
"Orange Yellow": (0.4578, 0.4416),
"Blue (Legacy)": (0.1851, 0.1238),
"Green (Legacy)": (0.3008, 0.4976),
"Red (Legacy)": (0.5435, 0.3200),
"Yellow (Legacy)": (0.4430, 0.4717),
"Magenta (Legacy)": (0.3735, 0.2428),
"Cyan (Legacy)": (0.2093, 0.2679),
"100% Red": (0.6424, 0.3274),
"100% Green": (0.2935, 0.6024),
"100% Blue": (0.1615, 0.0610),
"100% Cyan": (0.2302, 0.3340),
"100% Magenta": (0.3300, 0.1513),
"100% Yellow": (0.4152, 0.5047),
}
# 29 色 SDR 标准色板Legacy 色块仍保留 RGB 定义供图案发送)
_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 _resolve_reference_xy(name):
return _ACCURACY_REFERENCE_XY.get(name, (D65_X, D65_Y))
def get_accuracy_reference_y(name, white_lv):
"""
返回图表/表格用的参考亮度Calman 目标 Y 比例White=100 缩放)。
注意ΔE2000 计算使用实测 Y 作为目标 Y与 Calman 一致),此函数仅供展示。
"""
del name
if white_lv <= 0:
return 100.0
return white_lv
def get_accuracy_color_standards(test_type):
"""
获取色准测试的标准 xy 色度坐标Calman 兼容参考值)。
Args:
test_type: 测试类型 ("sdr_movie""hdr_movie")
Returns:
dict: {color_name: (x, y), ...}
"""
del test_type
return {name: _resolve_reference_xy(name) for name, _, _, _ in _SDR_COLOR_PATTERNS}
def _xyY_to_lab(x, y, Y):
if y == 0:
return 0.0, 0.0, 0.0
X = x * Y / y
Z = (1 - x - y) * Y / y
Xn, Yn, Zn = 95.047, 100.000, 108.883
def f(t):
delta = 6.0 / 29.0
if t > delta ** 3:
return t ** (1.0 / 3.0)
return t / (3 * delta ** 2) + 4.0 / 29.0
xr, yr, zr = X / Xn, Y / Yn, Z / Zn
fx, fy, fz = f(xr), f(yr), f(zr)
return 116 * fy - 16, 500 * (fx - fy), 200 * (fy - fz)
def _delta_e_2000_from_lab(L1, a1, b1, L2, a2, b2):
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
elif 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 = kC = kH = 1.0
return 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))
)
def calculate_delta_e_2000(
measured_x,
measured_y,
measured_lv,
standard_x,
standard_y,
standard_lv=None,
):
"""
计算 ΔE 2000 色差。
Args:
measured_x, measured_y: 测量的 xy 坐标
measured_lv: 测量的亮度cd/m²
standard_x, standard_y: 标准的 xy 坐标
standard_lv: 标准亮度cd/m²默认与 measured_lv 相同
Returns:
float: ΔE 2000 色差值
"""
if standard_lv is None:
standard_lv = measured_lv
L1, a1, b1 = _xyY_to_lab(measured_x, measured_y, measured_lv)
L2, a2, b2 = _xyY_to_lab(standard_x, standard_y, standard_lv)
return _delta_e_2000_from_lab(L1, a1, b1, L2, a2, b2)
def calculate_accuracy_delta_e_2000(
patch_name, measured_x, measured_y, measured_lv, white_lv
):
"""
色准测试专用 ΔE2000Calman 对齐)。
Calman 在 ColorChecker 测试中对每块使用固定参考 xy
且目标 Y 取实测 Y同亮度下比较色度差异
"""
del white_lv
standard_x, standard_y = _resolve_reference_xy(patch_name)
return calculate_delta_e_2000(
measured_x,
measured_y,
measured_lv,
standard_x,
standard_y,
measured_lv,
)
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