重构-抽纯函数

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

View File

@@ -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 → 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))
"""转发到 app.tests.color_accuracy.calculate_delta_e_2000Step 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 → 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
# 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_standardsStep 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_coverageStep 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_level22293 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_gammaStep 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_accuracyStep 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_curveStep 1 重构)"""
return _calc_pq_curve(gray_levels)
def plot_cct(self, test_type):
"""绘制 x 和 y 坐标分离图 - 每个点标注纵坐标值"""