修改SDR色准dE计算方式

This commit is contained in:
xinzhu.yin
2026-06-12 16:46:30 +08:00
parent 46a97d6ae7
commit c8ad244c45
4 changed files with 177 additions and 155 deletions

View File

@@ -989,8 +989,7 @@ def test_color_accuracy(self: "PQAutomationApp", test_type):
self.log_gui.log(f"计算色准ΔE 2000Gamma {target_gamma}...", level="info")
self.log_gui.log("=" * 50, level="separator")
# 获取标准 xy 坐标
standards = self.get_accuracy_color_standards(test_type)
white_lv = measured_data_list[0][2]
delta_e_values = []
color_patches = []
@@ -1000,14 +999,12 @@ def test_color_accuracy(self: "PQAutomationApp", test_type):
measured_y = measured_data[1]
measured_lv = measured_data[2]
standard_x, standard_y = standards.get(name, (0.3127, 0.3290))
delta_e = self.calculate_delta_e_2000(
delta_e = self.calculate_accuracy_delta_e_2000(
name,
measured_x,
measured_y,
measured_lv,
standard_x,
standard_y,
white_lv,
)
delta_e_values.append(delta_e)

View File

@@ -11,68 +11,126 @@ import math
import numpy as np
D65_X = 0.3127
D65_Y = 0.3290
def calculate_delta_e_2000(
measured_x, measured_y, measured_lv, standard_x, standard_y
):
# 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):
"""
计算 ΔE 2000 色差(修正版)
返回图表/表格用的参考亮度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:
measured_x, measured_y: 测量的 xy 坐标
measured_lv: 测量的亮度cd/m²
standard_x, standard_y: 标准的 xy 坐标
test_type: 测试类型 ("sdr_movie""hdr_movie")
Returns:
float: ΔE 2000 色差值
dict: {color_name: (x, y), ...}
"""
del test_type
return {name: _resolve_reference_xy(name) for name, _, _, _ in _SDR_COLOR_PATTERNS}
# ========== 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)
def _xyY_to_lab(x, y, Y):
if y == 0:
return 0.0, 0.0, 0.0
# 修复:标准值使用相同的参考亮度(只比较色度差异)
X2, Y2, Z2 = xy_to_XYZ(standard_x, standard_y, measured_lv)
X = x * Y / y
Z = (1 - x - y) * Y / y
Xn, Yn, Zn = 95.047, 100.000, 108.883
# ========== 2. XYZ → LabD65 白点)==========
def XYZ_to_Lab(X, Y, Z):
# D65 白点
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 = X / Xn
yr = Y / Yn
zr = Z / Zn
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)
# 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 公式 ==========
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)
@@ -111,13 +169,12 @@ def calculate_delta_e_2000(
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:
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
H_bar_prime = (h1_prime + h2_prime - 360) / 2.0
delta_L_prime = L2 - L1
delta_C_prime = C2_prime - C1_prime
@@ -144,18 +201,63 @@ def calculate_delta_e_2000(
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
kL = kC = kH = 1.0
delta_E = math.sqrt(
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))
)
return delta_E
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):
@@ -168,81 +270,3 @@ def calculate_color_accuracy(measured, standard):
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 → XYZD65 白点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

View File

@@ -1175,18 +1175,17 @@ class PQDebugPanel:
):
"""计算单个色块的 ΔE 2000"""
try:
# 获取标准 xy 坐标
test_type = self.current_test_type
standards = self.app.get_accuracy_color_standards(test_type)
white_lv = measured_lv
measured = self.app.results.get_intermediate_data("accuracy", "measured")
if measured and len(measured) > 0 and measured[0][2]:
white_lv = measured[0][2]
if color_name not in standards:
return 0.0
standard_x, standard_y = standards[color_name]
# 调用主程序的 ΔE 计算方法
delta_e = self.app.calculate_delta_e_2000(
measured_x, measured_y, measured_lv, standard_x, standard_y
delta_e = self.app.calculate_accuracy_delta_e_2000(
color_name,
measured_x,
measured_y,
measured_lv,
white_lv,
)
return delta_e

View File

@@ -44,6 +44,7 @@ from app.resources import (
load_icon,
)
from app.tests.color_accuracy import (
calculate_accuracy_delta_e_2000 as _calc_accuracy_delta_e_2000,
calculate_color_accuracy as _calc_color_accuracy,
calculate_delta_e_2000 as _calc_delta_e_2000,
get_accuracy_color_standards as _get_accuracy_color_standards,
@@ -740,6 +741,7 @@ class PQAutomationApp(
# 纯算法函数:作为 staticmethod 保留在主类(不依赖 self且 calculate_xxx
# 的命名空间由历史代码以 self.calculate_xxx 调用)。
calculate_delta_e_2000 = staticmethod(_calc_delta_e_2000)
calculate_accuracy_delta_e_2000 = staticmethod(_calc_accuracy_delta_e_2000)
get_accuracy_color_standards = staticmethod(_get_accuracy_color_standards)
calculate_gamut_coverage = staticmethod(_calc_gamut_coverage)
calculate_gamma = staticmethod(_calc_gamma)