重构-抽纯函数
This commit is contained in:
@@ -32,48 +32,26 @@ from colormath.color_conversions import convert_color
|
||||
from colormath.color_diff import delta_e_cie2000
|
||||
from views.pq_debug_panel import PQDebugPanel
|
||||
|
||||
# Step 0/1 重构:资源工具和纯算法已迁移到 app/ 包,这里重新导入以保持
|
||||
# 对原函数名/方法名的向后兼容(老代码内部仍用 self.calculate_* 调用)。
|
||||
from app.resources import (
|
||||
backgroud_style_set,
|
||||
get_resource_path,
|
||||
load_icon,
|
||||
)
|
||||
from app.tests.color_accuracy import (
|
||||
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,
|
||||
)
|
||||
from app.tests.eotf import calculate_pq_curve as _calc_pq_curve
|
||||
from app.tests.gamma import calculate_gamma as _calc_gamma
|
||||
from app.tests.gamut import calculate_gamut_coverage as _calc_gamut_coverage
|
||||
|
||||
plt.rcParams["font.family"] = ["sans-serif"]
|
||||
plt.rcParams["font.sans-serif"] = ["Microsoft YaHei"]
|
||||
|
||||
|
||||
def get_resource_path(relative_path):
|
||||
"""
|
||||
获取资源文件的绝对路径(兼容开发环境和打包后)
|
||||
|
||||
Args:
|
||||
relative_path: 相对路径,如 "assets/cie.png"
|
||||
|
||||
Returns:
|
||||
str: 资源文件的绝对路径
|
||||
"""
|
||||
try:
|
||||
# PyInstaller 打包后的临时文件夹路径
|
||||
base_path = sys._MEIPASS
|
||||
except AttributeError:
|
||||
# 开发环境:使用脚本所在目录
|
||||
base_path = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
return os.path.join(base_path, relative_path)
|
||||
|
||||
|
||||
def load_icon(png_path):
|
||||
"""加载并调整图标大小为64x64"""
|
||||
img = Image.open(get_resource_path(png_path))
|
||||
img = img.resize((24, 24), Image.LANCZOS)
|
||||
return ImageTk.PhotoImage(img)
|
||||
|
||||
|
||||
def backgroud_style_set():
|
||||
style = ttk.Style()
|
||||
# 移除背景色设置,使用默认背景色
|
||||
style.configure(
|
||||
"SidebarSelected.TButton",
|
||||
# anchor="w",
|
||||
padding=10,
|
||||
background="#005470",
|
||||
)
|
||||
|
||||
|
||||
class PQAutomationApp:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
@@ -6727,149 +6705,11 @@ class PQAutomationApp:
|
||||
def calculate_delta_e_2000(
|
||||
self, 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 色差值
|
||||
"""
|
||||
import math
|
||||
|
||||
# ========== 1. xy → XYZ(使用实际亮度)==========
|
||||
def xy_to_XYZ(x, y, 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))
|
||||
"""转发到 app.tests.color_accuracy.calculate_delta_e_2000(Step 1 重构)"""
|
||||
return _calc_delta_e_2000(
|
||||
measured_x, measured_y, measured_lv, standard_x, standard_y
|
||||
)
|
||||
|
||||
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 test_color_accuracy(self, test_type):
|
||||
"""测试色准 - 使用手工实现的 ΔE 2000(应用 Gamma)"""
|
||||
|
||||
@@ -7029,119 +6869,20 @@ class PQAutomationApp:
|
||||
self.log_gui.log("色准测试完成")
|
||||
|
||||
def get_accuracy_color_standards(self, test_type):
|
||||
"""
|
||||
获取色准测试的标准 xy 色度坐标(动态计算)
|
||||
|
||||
Args:
|
||||
test_type: 测试类型 ("sdr_movie" 或 "hdr_movie")
|
||||
|
||||
Returns:
|
||||
dict: {color_name: (x, y), ...}
|
||||
"""
|
||||
|
||||
# ========== RGB → xy 转换函数 ==========
|
||||
def rgb_to_xy_srgb(r, g, b):
|
||||
"""sRGB (8bit) → CIE 1931 xy"""
|
||||
# 1. 归一化到 0-1
|
||||
r, g, b = r / 255.0, g / 255.0, b / 255.0
|
||||
|
||||
# 2. sRGB Gamma 解码
|
||||
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)
|
||||
|
||||
# 3. 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
|
||||
|
||||
# 4. XYZ → xy
|
||||
total = X + Y + Z
|
||||
if total == 0:
|
||||
return 0.3127, 0.3290 # D65 白点
|
||||
|
||||
x = X / total
|
||||
y = Y / total
|
||||
return x, y
|
||||
|
||||
# ========== 你的 RGB 定义(29色)==========
|
||||
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),
|
||||
]
|
||||
|
||||
# ========== 动态计算 xy 坐标 ==========
|
||||
standards = {}
|
||||
for name, r, g, b in SDR_COLOR_PATTERNS:
|
||||
x, y = rgb_to_xy_srgb(r, g, b)
|
||||
standards[name] = (x, y)
|
||||
|
||||
return standards
|
||||
"""转发到 app.tests.color_accuracy.get_accuracy_color_standards(Step 1 重构)"""
|
||||
return _get_accuracy_color_standards(test_type)
|
||||
|
||||
def calculate_gamut_coverage(self, results):
|
||||
"""计算色域覆盖率"""
|
||||
area, coverage = pq_algorithm.calculate_gamut_coverage_DCIP3(results)
|
||||
return area, coverage
|
||||
"""转发到 app.tests.gamut.calculate_gamut_coverage(Step 1 重构)"""
|
||||
return _calc_gamut_coverage(results)
|
||||
|
||||
def calculate_gamma(self, results, max_index_fix, pattern_params=None):
|
||||
"""计算Gamma值,返回results + gamma
|
||||
|
||||
Args:
|
||||
results: 测量结果列表
|
||||
max_index_fix: 最大灰阶索引
|
||||
pattern_params: 8bit pattern参数,用于计算input_level(22293 Gamma数据对齐)
|
||||
"""
|
||||
results_with_gamma_list = pq_algorithm.calculate_gamma(
|
||||
results, max_index_fix, pattern_params
|
||||
)
|
||||
L_bar = pq_algorithm.calculate_L_bar(results)
|
||||
return results_with_gamma_list, L_bar
|
||||
"""转发到 app.tests.gamma.calculate_gamma(Step 1 重构)"""
|
||||
return _calc_gamma(results, max_index_fix, pattern_params)
|
||||
|
||||
def calculate_color_accuracy(self, measured, standard):
|
||||
"""计算色差"""
|
||||
# 使用简化的色差计算方法
|
||||
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 # 放大1000倍便于显示
|
||||
|
||||
return delta_E
|
||||
"""转发到 app.tests.color_accuracy.calculate_color_accuracy(Step 1 重构)"""
|
||||
return _calc_color_accuracy(measured, standard)
|
||||
|
||||
def plot_gamut(self, results, coverage, test_type):
|
||||
"""绘制色域图 - 根据用户选择的参考标准动态计算覆盖率"""
|
||||
@@ -7946,42 +7687,8 @@ class PQAutomationApp:
|
||||
self.log_gui.log("EOTF 曲线 + 数据表格绘制完成")
|
||||
|
||||
def calculate_pq_curve(self, gray_levels):
|
||||
"""计算 PQ (ST.2084) EOTF 理想曲线
|
||||
|
||||
Args:
|
||||
gray_levels: 灰阶百分比数组 (0-100)
|
||||
|
||||
Returns:
|
||||
L_bar: 归一化亮度数组 (0-1)
|
||||
"""
|
||||
# PQ 曲线参数(ITU-R BT.2100 标准)
|
||||
m1 = 0.1593017578125 # = 2610 / 16384
|
||||
m2 = 78.84375 # = 78.84375
|
||||
c1 = 0.8359375 # = 3424 / 4096
|
||||
c2 = 18.8515625 # = 2413 / 128
|
||||
c3 = 18.6875 # = 2392 / 128
|
||||
|
||||
L_bar = []
|
||||
for gray in gray_levels:
|
||||
# 归一化灰阶(0-1)
|
||||
V = gray / 100.0
|
||||
|
||||
if V <= 0:
|
||||
L_bar.append(0)
|
||||
else:
|
||||
# PQ 反向 EOTF 计算
|
||||
V_pow = np.power(V, 1 / m2)
|
||||
numerator = max(V_pow - c1, 0)
|
||||
denominator = c2 - c3 * V_pow
|
||||
|
||||
if denominator > 0:
|
||||
L = np.power(numerator / denominator, 1 / m1)
|
||||
else:
|
||||
L = 0
|
||||
|
||||
L_bar.append(L)
|
||||
|
||||
return np.array(L_bar)
|
||||
"""转发到 app.tests.eotf.calculate_pq_curve(Step 1 重构)"""
|
||||
return _calc_pq_curve(gray_levels)
|
||||
|
||||
def plot_cct(self, test_type):
|
||||
"""绘制 x 和 y 坐标分离图 - 每个点标注纵坐标值"""
|
||||
|
||||
Reference in New Issue
Block a user