Files
pqAutomationApp/app/tests/color_accuracy.py

453 lines
15 KiB
Python
Raw Normal View History

"""
色准ΔE2000 / 标准色相关纯算法
CIE / CIEDE2000 官方定义实现
标准色度来源
- 彩色块(ColorChecker): ColorChecker N Ohta 光谱积分 (CIE1931 2°, D65)
已与 Calman 彩色块 Target Y 验证一致总误差 1.34
- 灰阶: linear信号值即线性域目标相对亮度已验证对齐 Calman
- 100% 原色/混合色: 由目标色域三原色定义科学推导按色域分组
随信号格式通过 set_active_gamut() 切换默认 sRGB
统一规则
Target xy = 标准色度
Target Y = white_lv × Yr / 100
Yr 一律是"线性域相对亮度因子"(0~100) xy 同源
2026-04-20 09:41:24 +08:00
"""
import math
import numpy as np
from app.solid_color_scale import solid_color_rgb as _rgb
2026-06-12 16:46:30 +08:00
D65_X = 0.3127
D65_Y = 0.3290
# ======================================================================
# 彩色块标准 (x, y, Yr)
# 来源:直接抄自 Calman Target xyX-Rite 厂商标称值),零偏差对齐
# Yr = Calman Target Y / White Target Y(649.5768) × 100
# ======================================================================
_COLOR_PATCH_XYY = {
"dark skin": (0.4063, 0.3645, 9.91),
"light skin": (0.3780, 0.3562, 35.54),
"blue sky": (0.2489, 0.2653, 19.04),
"foliage": (0.3416, 0.4319, 13.13),
"blue flower": (0.2686, 0.2528, 23.78),
"bluish green": (0.2614, 0.3594, 42.42),
"orange": (0.5146, 0.4095, 28.59),
"purplish blue": (0.2147, 0.1891, 11.72),
"moderate red": (0.4641, 0.3122, 18.61),
"purple": (0.2882, 0.2164, 6.49),
"yellow green": (0.3774, 0.4955, 43.66),
"orange yellow": (0.4749, 0.4427, 42.91),
"blue": (0.1883, 0.1349, 6.04),
"green": (0.3049, 0.4948, 23.29),
"red": (0.5474, 0.3187, 11.57),
"yellow": (0.4477, 0.4759, 59.77),
"magenta": (0.3738, 0.2440, 18.96),
"cyan": (0.2080, 0.2688, 19.71),
}
# ======================================================================
# 100% 原色/混合色:由色域定义科学推导 (x, y, Yr),按色域分组
# 来源:各色域三原色 + 白点,线性 RGB -> XYZ -> xyY (白场Y=100)
# 注意sRGB 与 BT.709 共用同一组三原色,数值一致(做别名)
# ======================================================================
_PRIMARY_XYY_BY_GAMUT = {
"sRGB": {
"100% Red": (0.6400, 0.3300, 21.2639),
"100% Green": (0.3000, 0.6000, 71.5169),
"100% Blue": (0.1500, 0.0600, 7.2192),
"100% Cyan": (0.2246, 0.3287, 78.7361),
"100% Magenta": (0.3209, 0.1542, 28.4831),
"100% Yellow": (0.4193, 0.5053, 92.7808),
},
"DCI-P3": {
"100% Red": (0.6800, 0.3200, 20.9492),
"100% Green": (0.2650, 0.6900, 72.1595),
"100% Blue": (0.1500, 0.0600, 6.8913),
"100% Cyan": (0.2048, 0.3602, 79.0508),
"100% Magenta": (0.3424, 0.1544, 27.8405),
"100% Yellow": (0.4248, 0.5476, 93.1087),
},
"BT.2020": {
"100% Red": (0.7080, 0.2920, 26.2700),
"100% Green": (0.1700, 0.7970, 67.7998),
"100% Blue": (0.1310, 0.0460, 5.9302),
"100% Cyan": (0.1465, 0.3446, 73.7300),
"100% Magenta": (0.3682, 0.1471, 32.2002),
"100% Yellow": (0.4465, 0.5374, 94.0698),
},
}
# BT.709 共用 sRGB 三原色,做别名指向,避免重复维护
_PRIMARY_XYY_BY_GAMUT["BT.709"] = _PRIMARY_XYY_BY_GAMUT["sRGB"]
# 当前激活色域(默认 sRGB随信号格式切换调用 set_active_gamut() 修改
_ACTIVE_GAMUT = "sRGB"
def set_active_gamut(gamut_name):
"""
切换当前色域随信号格式选择调用
支持: 'sRGB' / 'BT.709' / 'DCI-P3' / 'BT.2020'
"""
global _ACTIVE_GAMUT
if gamut_name not in _PRIMARY_XYY_BY_GAMUT:
raise ValueError(
f"未知色域: {gamut_name},可选: {list(_PRIMARY_XYY_BY_GAMUT)}"
)
_ACTIVE_GAMUT = gamut_name
def get_active_gamut():
"""返回当前激活的色域名。"""
return _ACTIVE_GAMUT
def _current_primaries():
"""返回当前色域的原色表。"""
return _PRIMARY_XYY_BY_GAMUT[_ACTIVE_GAMUT]
# ======================================================================
# 测试流程用名 -> 标准来源
# 彩色块: 映射到 _COLOR_PATCH_XYY 的键
# 灰阶 : 映射到 linear 信号值Yr = 信号值×100
# 100%原色: 走当前色域的 _PRIMARY_XYY_BY_GAMUT
# ======================================================================
_PATCH_NAME_MAP = {
"Dark Skin": "dark skin",
"Light Skin": "light skin",
"Blue Sky": "blue sky",
"Foliage": "foliage",
"Blue Flower": "blue flower",
"Bluish Green": "bluish green",
"Orange": "orange",
"Purplish Blue": "purplish blue",
"Moderate Red": "moderate red",
"Purple": "purple",
"Yellow Green": "yellow green",
"Orange Yellow": "orange yellow",
"Blue (Legacy)": "blue",
"Green (Legacy)": "green",
"Red (Legacy)": "red",
"Yellow (Legacy)": "yellow",
"Magenta (Legacy)": "magenta",
"Cyan (Legacy)": "cyan",
}
# 灰阶:信号值(线性域目标相对亮度),色度恒为 D65 白点
_GRAYSCALE_SIGNAL = {
"White": 1.00,
"Gray 80": 0.80,
"Gray 65": 0.65,
"Gray 50": 0.50,
"Gray 35": 0.35,
"Black": 0.00,
2026-06-12 16:46:30 +08:00
}
2026-06-12 16:46:30 +08:00
_SDR_COLOR_PATTERNS = [
("White", *_rgb(255, 255, 255)),
("Gray 80", *_rgb(230, 230, 230)),
("Gray 65", *_rgb(209, 209, 209)),
("Gray 50", *_rgb(186, 186, 186)),
("Gray 35", *_rgb(158, 158, 158)),
("Dark Skin", *_rgb(115, 82, 66)),
("Light Skin", *_rgb(194, 150, 130)),
("Blue Sky", *_rgb(94, 122, 156)),
("Foliage", *_rgb(89, 107, 66)),
("Blue Flower", *_rgb(130, 128, 176)),
("Bluish Green", *_rgb(99, 189, 168)),
("Orange", *_rgb(217, 120, 41)),
("Purplish Blue", *_rgb(74, 92, 163)),
("Moderate Red", *_rgb(194, 84, 97)),
("Purple", *_rgb(92, 61, 107)),
("Yellow Green", *_rgb(158, 186, 64)),
("Orange Yellow", *_rgb(230, 161, 46)),
("Blue (Legacy)", *_rgb(51, 61, 150)),
("Green (Legacy)", *_rgb(71, 148, 71)),
("Red (Legacy)", *_rgb(176, 48, 59)),
("Yellow (Legacy)", *_rgb(237, 199, 33)),
("Magenta (Legacy)", *_rgb(186, 84, 145)),
("Cyan (Legacy)", *_rgb(0, 133, 163)),
("100% Red", *_rgb(255, 0, 0)),
("100% Green", *_rgb(0, 255, 0)),
("100% Blue", *_rgb(0, 0, 255)),
("100% Cyan", *_rgb(0, 255, 255)),
("100% Magenta", *_rgb(255, 0, 255)),
("100% Yellow", *_rgb(255, 255, 0)),
2026-06-12 16:46:30 +08:00
]
2026-04-20 09:41:24 +08:00
# ----------------------------------------------------------------------
# 标准 xy / Yr 解析(统一入口)
# ----------------------------------------------------------------------
2026-06-12 16:46:30 +08:00
def _resolve_reference_xy(name):
"""返回该色块的标准参考 xy。未知则回退 D65。"""
if name in _GRAYSCALE_SIGNAL:
return (D65_X, D65_Y)
if name in _PATCH_NAME_MAP:
x, y, _ = _COLOR_PATCH_XYY[_PATCH_NAME_MAP[name]]
return (x, y)
primaries = _current_primaries()
if name in primaries:
x, y, _ = primaries[name]
return (x, y)
return (D65_X, D65_Y)
def _resolve_reference_yr(name):
2026-04-20 09:41:24 +08:00
"""
返回该色块的标准相对亮度因子 Yr (0~100)
灰阶: 信号值×100 (linear)彩色/原色: 光谱或色域 Yr
无法确定返回 None
2026-06-12 16:46:30 +08:00
"""
if name in _GRAYSCALE_SIGNAL:
return _GRAYSCALE_SIGNAL[name] * 100.0
if name in _PATCH_NAME_MAP:
return _COLOR_PATCH_XYY[_PATCH_NAME_MAP[name]][2]
primaries = _current_primaries()
if name in primaries:
return primaries[name][2]
return None
2026-04-20 09:41:24 +08:00
def get_target_xyY(name, white_lv):
2026-06-12 16:46:30 +08:00
"""
返回该色块的完整 target (x, y, Y)
Target Y = white_lv × Yr / 100 统一规则
Yr 不可知时 Y 返回 None由调用方按实测处理
"""
x, y = _resolve_reference_xy(name)
Yr = _resolve_reference_yr(name)
if Yr is None or white_lv is None or white_lv <= 0:
return (x, y, None)
return (x, y, round(white_lv * Yr / 100.0, 4))
2026-04-20 09:41:24 +08:00
2026-06-12 16:46:30 +08:00
# ----------------------------------------------------------------------
# 兼容旧接口
# ----------------------------------------------------------------------
def get_accuracy_reference_y(name, white_lv):
2026-06-12 16:46:30 +08:00
"""
返回图表/表格用的参考亮度比例White=100 缩放
现基于标准 Yr 返回真实比例无标准 Yr 时回退 100
"""
Yr = _resolve_reference_yr(name)
if Yr is None or white_lv is None or white_lv <= 0:
return 100.0
return round(Yr, 4)
2026-04-20 09:41:24 +08:00
# ----------------------------------------------------------------------
# xyY -> XYZ -> LabCIE 官方定义)
# ----------------------------------------------------------------------
def _xyY_to_XYZ(x, y, Y):
"""xyY 转 XYZ。y 为 0 时返回全 0避免除零。"""
if y <= 0:
2026-06-12 16:46:30 +08:00
return 0.0, 0.0, 0.0
X = (x / y) * Y
Z = ((1.0 - x - y) / y) * Y
return X, Y, Z
2026-04-20 09:41:24 +08:00
def _XYZ_to_lab(X, Y, Z, Xn, Yn, Zn):
"""
XYZ CIE L*a*b*
Xn, Yn, Zn 为参考白点的绝对 XYZYn 通常为白场亮度 white_lv
"""
delta = 6.0 / 29.0
2026-04-20 09:41:24 +08:00
2026-06-12 16:46:30 +08:00
def f(t):
if t > delta ** 3:
return t ** (1.0 / 3.0)
return t / (3.0 * delta ** 2) + 4.0 / 29.0
xr = X / Xn if Xn != 0 else 0.0
yr = Y / Yn if Yn != 0 else 0.0
zr = Z / Zn if Zn != 0 else 0.0
2026-04-20 09:41:24 +08:00
2026-06-12 16:46:30 +08:00
fx, fy, fz = f(xr), f(yr), f(zr)
L = 116.0 * fy - 16.0
a = 500.0 * (fx - fy)
b = 200.0 * (fy - fz)
return L, a, b
def _xyY_to_lab(x, y, Y, white_x=D65_X, white_y=D65_Y, white_Y=None):
"""
xyY 直接转 Lab
白点默认 D65 色度white_Y 为白场绝对亮度用于 L 的归一化基准
white_Y None则退化为以 Y=1 归一化仅相对比较时可用
"""
if white_Y is None or white_Y <= 0:
white_Y = 1.0
X, Y3, Z = _xyY_to_XYZ(x, y, Y)
Xn, Yn, Zn = _xyY_to_XYZ(white_x, white_y, white_Y)
return _XYZ_to_lab(X, Y3, Z, Xn, Yn, Zn)
2026-04-20 09:41:24 +08:00
# ----------------------------------------------------------------------
# CIEDE2000官方公式
# ----------------------------------------------------------------------
def _delta_e_2000_from_lab(L1, a1, b1, L2, a2, b2, kL=1.0, kC=1.0, kH=1.0):
2026-04-20 09:41:24 +08:00
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)
def calc_hue(a_prime, b):
if a_prime == 0 and b == 0:
return 0.0
h = math.degrees(math.atan2(b, a_prime))
2026-04-20 09:41:24 +08:00
if h < 0:
h += 360.0
2026-04-20 09:41:24 +08:00
return h
h1_prime = calc_hue(a1_prime, b1)
h2_prime = calc_hue(a2_prime, b2)
# ΔL', ΔC'
delta_L_prime = L2 - L1
delta_C_prime = C2_prime - C1_prime
2026-04-20 09:41:24 +08:00
# Δh'(官方三分支)
if C1_prime * C2_prime == 0:
delta_h_prime = 0.0
2026-04-20 09:41:24 +08:00
else:
dh = h2_prime - h1_prime
if abs(dh) <= 180:
delta_h_prime = dh
elif dh > 180:
delta_h_prime = dh - 360.0
else:
delta_h_prime = dh + 360.0
2026-04-20 09:41:24 +08:00
delta_H_prime = (
2.0
2026-04-20 09:41:24 +08:00
* math.sqrt(C1_prime * C2_prime)
* math.sin(math.radians(delta_h_prime / 2.0))
)
# 平均值
L_bar_prime = (L1 + L2) / 2.0
C_bar_prime = (C1_prime + C2_prime) / 2.0
# H_bar'(官方分支)
if C1_prime * C2_prime == 0:
H_bar_prime = h1_prime + h2_prime
else:
dh_abs = abs(h1_prime - h2_prime)
if dh_abs <= 180:
H_bar_prime = (h1_prime + h2_prime) / 2.0
elif (h1_prime + h2_prime) < 360:
H_bar_prime = (h1_prime + h2_prime + 360.0) / 2.0
else:
H_bar_prime = (h1_prime + h2_prime - 360.0) / 2.0
2026-04-20 09:41:24 +08:00
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))
)
delta_theta = 30 * math.exp(-(((H_bar_prime - 275) / 25.0) ** 2))
2026-04-20 09:41:24 +08:00
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))
S_L = 1 + (0.015 * (L_bar_prime - 50) ** 2) / math.sqrt(
20 + (L_bar_prime - 50) ** 2
)
S_C = 1 + 0.045 * C_bar_prime
S_H = 1 + 0.015 * C_bar_prime * T
2026-04-20 09:41:24 +08:00
2026-06-12 16:46:30 +08:00
return math.sqrt(
2026-04-20 09:41:24 +08:00
(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))
)
2026-06-12 16:46:30 +08:00
def calculate_delta_e_2000(
measured_x,
measured_y,
measured_lv,
standard_x,
standard_y,
standard_lv=None,
white_lv=None,
2026-06-12 16:46:30 +08:00
):
"""
计算 ΔE 2000 色差
Args:
measured_x, measured_y: 测量的 xy 坐标
measured_lv: 测量的亮度cd/
standard_x, standard_y: 标准的 xy 坐标
standard_lv: 标准亮度cd/默认与 measured_lv 相同
white_lv: 白场亮度cd/作为 Lab L 归一化基准
默认取 measured_lv仅当不传时退化为相对比较
2026-06-12 16:46:30 +08:00
Returns:
float: ΔE 2000 色差值
"""
if standard_lv is None:
standard_lv = measured_lv
if white_lv is None:
white_lv = measured_lv
2026-06-12 16:46:30 +08:00
L1, a1, b1 = _xyY_to_lab(measured_x, measured_y, measured_lv, white_Y=white_lv)
L2, a2, b2 = _xyY_to_lab(standard_x, standard_y, standard_lv, white_Y=white_lv)
2026-06-12 16:46:30 +08:00
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
):
"""
色准测试专用 ΔE2000
标准 xy 来自光谱积分/色域定义随当前色域
目标 Y 取实测 Y同亮度下比较色度差异
L 的归一化基准使用白场亮度 white_lv
2026-06-12 16:46:30 +08:00
"""
standard_x, standard_y = _resolve_reference_xy(patch_name)
return calculate_delta_e_2000(
measured_x,
measured_y,
measured_lv,
standard_x,
standard_y,
standard_lv=measured_lv,
white_lv=white_lv,
2026-06-12 16:46:30 +08:00
)
2026-04-20 09:41:24 +08:00
def calculate_color_accuracy(measured, standard):
"""计算色差简化版xy 欧氏距离 × 1000"""
2026-04-20 09:41:24 +08:00
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
def get_accuracy_color_standards(test_type=None):
"""返回色准标准patch 名称 -> 参考 xy随当前色域"""
del test_type
return {name: _resolve_reference_xy(name) for name, _, _, _ in _SDR_COLOR_PATTERNS}