重构-抽纯函数

This commit is contained in:
xinzhu.yin
2026-04-20 09:41:24 +08:00
parent 90d7d8e3f0
commit 22c46632ac
8 changed files with 400 additions and 322 deletions

0
app/__init__.py Normal file
View File

55
app/resources.py Normal file
View File

@@ -0,0 +1,55 @@
"""资源与全局样式工具。
Step 0 重构:将原先散落在 pqAutomationApp.py 顶部的
get_resource_path / load_icon / backgroud_style_set 三个辅助函数
迁移到独立模块,保持行为完全一致。
"""
import os
import sys
import ttkbootstrap as ttk
from PIL import Image, ImageTk
# 项目根目录app/ 的上一级)。开发环境下用它作为资源查找基准,
# 从而兼容从项目根目录启动的 pqAutomationApp.py。
_PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def get_resource_path(relative_path):
"""
获取资源文件的绝对路径(兼容开发环境和打包后)
Args:
relative_path: 相对路径,如 "assets/cie.png"
Returns:
str: 资源文件的绝对路径
"""
try:
# PyInstaller 打包后的临时文件夹路径
base_path = sys._MEIPASS
except AttributeError:
# 开发环境:使用项目根目录
base_path = _PROJECT_ROOT
return os.path.join(base_path, relative_path)
def load_icon(png_path):
"""加载并调整图标大小为 24x24原注释写 64x64实际 resize 为 24x24保持原行为"""
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",
)

0
app/tests/__init__.py Normal file
View File

248
app/tests/color_accuracy.py Normal file
View File

@@ -0,0 +1,248 @@
"""色准Δ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 → LabD65 白点)==========
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 → 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

40
app/tests/eotf.py Normal file
View File

@@ -0,0 +1,40 @@
"""EOTFPQ / ST.2084)相关纯算法。"""
import numpy as np
def calculate_pq_curve(gray_levels):
"""计算 PQ (ST.2084) EOTF 理想曲线。
Args:
gray_levels: 灰阶百分比数组 (0-100)
Returns:
numpy.ndarray: 归一化亮度数组 (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:
V = gray / 100.0
if V <= 0:
L_bar.append(0)
else:
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)

19
app/tests/gamma.py Normal file
View File

@@ -0,0 +1,19 @@
"""Gamma 相关纯算法。"""
import algorithm.pq_algorithm as pq_algorithm
def calculate_gamma(results, max_index_fix, pattern_params=None):
"""计算 Gamma 值,返回 (results_with_gamma_list, L_bar)。
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

9
app/tests/gamut.py Normal file
View File

@@ -0,0 +1,9 @@
"""色域Gamut相关纯算法。"""
import algorithm.pq_algorithm as pq_algorithm
def calculate_gamut_coverage(results):
"""计算色域覆盖率DCI-P3"""
area, coverage = pq_algorithm.calculate_gamut_coverage_DCIP3(results)
return area, coverage