@@ -92,6 +92,85 @@ def _xy_to_upvp(x: float, y: float) -> tuple[float, float]:
return up , vp
def _hex_to_rgb ( hex_color : str ) - > tuple [ int , int , int ] :
c = hex_color . lstrip ( " # " )
return int ( c [ 0 : 2 ] , 16 ) , int ( c [ 2 : 4 ] , 16 ) , int ( c [ 4 : 6 ] , 16 )
def _mix ( hex_a : str , hex_b : str , ratio : float ) - > str :
r1 , g1 , b1 = _hex_to_rgb ( hex_a )
r2 , g2 , b2 = _hex_to_rgb ( hex_b )
r = int ( r1 * ( 1 - ratio ) + r2 * ratio )
g = int ( g1 * ( 1 - ratio ) + g2 * ratio )
b = int ( b1 * ( 1 - ratio ) + b2 * ratio )
return f " # { r : 02x } { g : 02x } { b : 02x } "
def _is_dark_hex ( hex_color : str ) - > bool :
r , g , b = _hex_to_rgb ( hex_color )
return ( r * 299 + g * 587 + b * 114 ) / 1000 < 128
def _get_calman_palette ( ) - > dict [ str , str ] :
""" 根据当前主题生成 Calman 调试面板色板。 """
style = ttk . Style ( )
colors = style . colors
bg = colors . bg
fg = colors . fg
dark_mode = _is_dark_hex ( bg )
if dark_mode :
figure_bg = _mix ( bg , " #000000 " , 0.18 )
axes_bg = _mix ( bg , " #000000 " , 0.25 )
grid = _mix ( fg , axes_bg , 0.55 )
tree_bg = _mix ( bg , " #000000 " , 0.10 )
tree_even = _mix ( bg , " #ffffff " , 0.03 )
tree_odd = _mix ( bg , " #ffffff " , 0.07 )
heading_bg = _mix ( bg , " #ffffff " , 0.12 )
reading_bg = _mix ( bg , " #ffffff " , 0.06 )
reading_fg = _mix ( fg , " #ffffff " , 0.06 )
status_fg = _mix ( fg , bg , 0.35 )
reading_accent = colors . info
xy_series = " #d7dce4 "
d65_mark = " #ffffff "
else :
figure_bg = _mix ( bg , " #dfe7ef " , 0.45 )
axes_bg = _mix ( bg , " #eff4f9 " , 0.72 )
grid = _mix ( " #5f6f82 " , axes_bg , 0.55 )
tree_bg = " #ffffff "
tree_even = " #ffffff "
tree_odd = " #f3f7fb "
heading_bg = _mix ( colors . primary , " #ffffff " , 0.82 )
reading_bg = _mix ( bg , " #e7eef5 " , 0.58 )
reading_fg = fg
status_fg = _mix ( fg , bg , 0.25 )
reading_accent = _mix ( colors . info , " #000000 " , 0.25 )
xy_series = " #1f2a36 "
d65_mark = " #253142 "
return {
" figure_bg " : figure_bg ,
" axes_bg " : axes_bg ,
" fg " : fg ,
" grid " : grid ,
" metric_tile_bg " : _mix ( figure_bg , " #ffffff " , 0.08 if dark_mode else 0.25 ) ,
" metric_tile_fg " : reading_fg ,
" status_fg " : status_fg ,
" reading_accent " : reading_accent ,
" reading_bg " : reading_bg ,
" reading_fg " : reading_fg ,
" tree_bg " : tree_bg ,
" tree_fg " : reading_fg ,
" tree_even " : tree_even ,
" tree_odd " : tree_odd ,
" tree_heading_bg " : heading_bg ,
" tree_heading_fg " : reading_fg ,
" tree_select " : _mix ( colors . info , figure_bg , 0.35 ) ,
" xy_series " : xy_series ,
" d65_mark " : d65_mark ,
}
def _xyY_to_rgb_balance ( x : float , y : float , big_y : float ) - > tuple [ float , float , float ] :
""" 把 xyY 近似映射到 RGB 比例,并归一到平均值 100。 """
if y < = 0 or big_y < = 0 :
@@ -114,17 +193,48 @@ def _xyY_to_rgb_balance(x: float, y: float, big_y: float) -> tuple[float, float,
return ( r / avg ) * 100.0 , ( g / avg ) * 100.0 , ( b / avg ) * 100.0
def _style_axes ( ax , title : str ) - > None :
ax . set_title ( title , color = _FG , fontsize = 9 , pad = 4 )
ax . set_facecolor ( _AX_BG )
ax . grid ( True , color = _GRID , alpha = 0.35 , linewidth = 0.6 )
ax . tick_params ( colors = _FG , labelsize = 8 )
def _style_axes ( self : " PQAutomationApp " , ax , title : str ) - > None :
palette = _get_calman_palette ( )
ax . set_title ( title , color = palette [ " fg " ] , fontsize = 9 , pad = 4 )
ax . set_facecolor ( palette [ " axes_bg " ] )
ax . grid ( True , color = palette [ " grid " ] , alpha = 0.35 , linewidth = 0.6 )
ax . tick_params ( colors = palette [ " fg " ] , labelsize = 8 )
for spine in ax . spines . values ( ) :
spine . set_color ( " #8a8a8a " )
spine . set_color ( _mix ( palette [ " fg " ] , palette [ " axes_bg " ] , 0.55 ) )
def _apply_calman_tree_style ( self : " PQAutomationApp " ) - > None :
""" 应用矩阵 Treeview 的深浅色样式。 """
palette = _get_calman_palette ( )
style = ttk . Style ( )
style . configure (
" Calman.Treeview " ,
rowheight = 22 ,
font = ( " Consolas " , 9 ) ,
background = palette [ " tree_bg " ] ,
fieldbackground = palette [ " tree_bg " ] ,
foreground = palette [ " tree_fg " ] ,
)
style . map (
" Calman.Treeview " ,
background = [ ( " selected " , palette [ " tree_select " ] ) ] ,
foreground = [ ( " selected " , palette [ " tree_fg " ] ) ] ,
)
style . configure (
" Calman.Treeview.Heading " ,
font = ( " 微软雅黑 " , 9 , " bold " ) ,
background = palette [ " tree_heading_bg " ] ,
foreground = palette [ " tree_heading_fg " ] ,
)
if hasattr ( self , " calman_metric_tree " ) :
self . calman_metric_tree . configure ( style = " Calman.Treeview " )
if hasattr ( self , " calman_data_tree " ) :
self . calman_data_tree . configure ( style = " Calman.Treeview " )
def create_calman_panel ( self : " PQAutomationApp " ) - > None :
""" 创建 CALMAN 风格灰阶测试面板,注册到 panel_manager。 """
palette = _get_calman_palette ( )
self . calman_frame = ttk . Frame ( self . content_frame )
self . calman_visible = False
self . calman_levels = list ( DEFAULT_LEVELS_PCT )
@@ -152,7 +262,7 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
chart_frame . rowconfigure ( 2 , weight = 0 )
chart_frame . columnconfigure ( 0 , weight = 1 )
fig = Figure ( figsize = ( 10.5 , 3.4 ) , dpi = 90 , facecolor = _DARK_BG )
fig = Figure ( figsize = ( 10.5 , 3.4 ) , dpi = 90 , facecolor = palette [ " figure_bg " ] )
self . calman_fig = fig
self . calman_ax_de = fig . add_subplot ( 141 )
self . calman_ax_rgb_line = fig . add_subplot ( 142 )
@@ -163,7 +273,7 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
)
canvas = FigureCanvasTkAgg ( fig , master = chart_frame )
canvas_widget = canvas . get_tk_widget ( )
canvas_widget . configure ( bg = _DARK_BG , highlightthickness = 0 )
canvas_widget . configure ( bg = palette [ " figure_bg " ] , highlightthickness = 0 )
canvas_widget . grid ( row = 0 , column = 0 , sticky = tk . NSEW )
self . calman_canvas = canvas
@@ -181,11 +291,12 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
de_combo . pack ( side = tk . LEFT , padx = ( 4 , 10 ) )
self . calman_elapsed_var = tk . StringVar ( value = " Step: -- s | Total: -- s " )
ttk . Label (
self . calman_elapsed_label = ttk . Label (
control_row ,
textvariable = self . calman_elapsed_var ,
foreground = " #d0d0d0 " ,
) . pack ( side = tk . LEFT )
foreground = palette [ " status_fg " ] ,
)
self . calman_elapsed_label . pack ( side = tk . LEFT )
metrics_row = ttk . Frame ( chart_frame )
metrics_row . grid ( row = 2 , column = 0 , sticky = tk . EW , pady = ( 2 , 0 ) )
@@ -194,6 +305,7 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
self . calman_avg_cct_var = tk . StringVar ( value = " Avg CCT: -- " )
self . calman_contrast_var = tk . StringVar ( value = " Contrast Ratio: -- " )
self . calman_avg_gamma_var = tk . StringVar ( value = " Average Gamma: -- " )
self . calman_metric_labels = [ ]
for idx , v in enumerate (
(
self . calman_avg_de_var ,
@@ -202,14 +314,16 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
self . calman_avg_gamma_var ,
)
) :
tk . Label (
lbl = tk. Label (
metrics_row ,
textvariable = v ,
anchor = tk . CENTER ,
fg = " #f2f2f2 " ,
bg = " #373737 " ,
fg = palette [ " metric_tile_fg " ] ,
bg = palette [ " metric_tile_bg " ] ,
font = ( " 微软雅黑 " , 10 , " bold " ) ,
) . grid ( row = 0 , column = idx , sticky = tk . EW , padx = 2 )
)
lbl . grid ( row = 0 , column = idx , sticky = tk . EW , padx = 2 )
self . calman_metric_labels . append ( lbl )
# ---------------------------- 顶部右:按钮列 ----------------------------
btn_col = ttk . LabelFrame ( root , text = " 操作 " , padding = 6 )
@@ -246,13 +360,14 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
) . pack ( fill = tk . X , pady = 2 )
self . calman_status_var = tk . StringVar ( value = " 待机 " )
ttk . Label (
self . calman_status_label = ttk . Label (
btn_col ,
textvariable = self . calman_status_var ,
foreground = " #555 " ,
foreground = palette [ " status_fg " ] ,
wraplength = 150 ,
justify = tk . LEFT ,
) . pack ( fill = tk . X , pady = ( 8 , 0 ) )
)
self . calman_status_label . pack ( fill = tk . X , pady = ( 8 , 0 ) )
self . calman_progress_var = tk . StringVar ( value = " 0 / 0 " )
self . calman_progress = ttk . Progressbar (
@@ -269,14 +384,15 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
self . calman_reading_var = tk . StringVar (
value = " x: -- y: -- Y: -- \n CCT: -- ΔE: -- "
)
ttk . Label (
self . calman_reading_summary_label = ttk . Label (
btn_col ,
textvariable = self . calman_reading_var ,
font = ( " Consolas " , 9 ) ,
foreground = " #1f6fb2 " ,
foreground = palette [ " reading_accent " ] ,
wraplength = 160 ,
justify = tk . LEFT ,
) . pack ( fill = tk . X , pady = ( 8 , 0 ) )
)
self . calman_reading_summary_label . pack ( fill = tk . X , pady = ( 8 , 0 ) )
# ---------------------------- 中部: 灰阶色块( Target / Actual) ----------------------------
patch_outer = ttk . LabelFrame ( root , text = " 灰阶色块(点击可直接输出 Pattern) " , padding = 6 )
@@ -390,24 +506,26 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
" cd/m²: -- \n "
" ΔE2000: -- "
)
tk . Label (
self . calman_reading_detail_label = tk . Label (
left ,
textvariable = self . calman_reading_var ,
justify = tk . LEFT ,
font = ( " Consolas " , 10 ) ,
fg = " #e5e5e5 " ,
bg = " #323232 " ,
fg = palette [ " reading_fg " ] ,
bg = palette [ " reading_bg " ] ,
width = 22 ,
padx = 4 ,
pady = 4 ,
) . pack ( fill = tk . X )
)
self . calman_reading_detail_label . pack ( fill = tk . X )
xy_fig = Figure ( figsize = ( 2.6 , 2.2 ) , dpi = 90 , facecolor = _DARK_BG )
xy_fig = Figure ( figsize = ( 2.6 , 2.2 ) , dpi = 90 , facecolor = palette [ " figure_bg " ] )
self . calman_xy_fig = xy_fig
self . calman_xy_ax = xy_fig . add_subplot ( 111 )
xy_fig . subplots_adjust ( left = 0.20 , right = 0.96 , top = 0.90 , bottom = 0.18 )
xy_canvas = FigureCanvasTkAgg ( xy_fig , master = left )
xy_widget = xy_canvas . get_tk_widget ( )
xy_widget . configure ( bg = _DARK_BG , highlightthickness = 0 )
xy_widget . configure ( bg = palette [ " figure_bg " ] , highlightthickness = 0 )
xy_widget . pack ( fill = tk . BOTH , expand = True , pady = ( 6 , 0 ) )
self . calman_xy_canvas = xy_canvas
@@ -458,11 +576,7 @@ def create_calman_panel(self: "PQAutomationApp") -> None:
for widget in ( metric_tree , data_tree ) :
widget . bind ( " <MouseWheel> " , lambda e : _matrix_mousewheel ( self , e ) )
style = ttk . S tyle( )
style . configure ( " Calman.Treeview " , rowheight = 22 , font = ( " Consolas " , 9 ) )
style . configure ( " Calman.Treeview.Heading " , font = ( " 微软雅黑 " , 9 , " bold " ) )
self . calman_metric_tree . configure ( style = " Calman.Treeview " )
self . calman_data_tree . configure ( style = " Calman.Treeview " )
_apply_calman_tree_s tyle( self )
right . bind ( " <Configure> " , lambda _e : _adaptive_matrix_columns ( self ) )
@@ -796,6 +910,17 @@ def _adaptive_matrix_columns(self: "PQAutomationApp") -> None:
def _redraw_calman_charts ( self : " PQAutomationApp " ) - > None :
""" 根据 calman_results 重绘四张图和 xy 散点。 """
palette = _get_calman_palette ( )
if hasattr ( self , " calman_fig " ) :
self . calman_fig . patch . set_facecolor ( palette [ " figure_bg " ] )
if hasattr ( self , " calman_canvas " ) :
try :
self . calman_canvas . get_tk_widget ( ) . configure (
bg = palette [ " figure_bg " ] , highlightthickness = 0
)
except Exception :
pass
recs = sorted ( self . calman_results . values ( ) , key = lambda r : r [ " pct " ] )
pcts = [ r [ " pct " ] for r in recs ]
de_vals = [ r [ " de2000 " ] if r [ " de2000 " ] == r [ " de2000 " ] else 0 for r in recs ]
@@ -834,7 +959,7 @@ def _redraw_calman_charts(self: "PQAutomationApp") -> None:
# ΔE2000
a1 = self . calman_ax_de
a1 . clear ( )
_style_axes ( a1 , " DeltaE 2000 " )
_style_axes ( self , a1 , " DeltaE 2000 " )
if pcts :
a1 . bar ( pcts , de_vals , color = " #ffcf57 " , width = 3.5 )
a1 . set_xlim ( - 2 , 102 )
@@ -844,7 +969,7 @@ def _redraw_calman_charts(self: "PQAutomationApp") -> None:
# RGB Balance 线图
a2 = self . calman_ax_rgb_line
a2 . clear ( )
_style_axes ( a2 , " RGB Balance " )
_style_axes ( self , a2 , " RGB Balance " )
if rgb_pcts :
a2 . plot ( rgb_pcts , rgb_r , " - " , color = " #ff4d4d " , linewidth = 1.2 )
a2 . plot ( rgb_pcts , rgb_g , " - " , color = " #4caf50 " , linewidth = 1.2 )
@@ -857,7 +982,7 @@ def _redraw_calman_charts(self: "PQAutomationApp") -> None:
# RGB Balance 条图(用最后一个点)
a3 = self . calman_ax_rgb_bar
a3 . clear ( )
_style_axes ( a3 , " RGB Balance " )
_style_axes ( self , a3 , " RGB Balance " )
if recs :
last = recs [ - 1 ]
bars = [
@@ -875,7 +1000,7 @@ def _redraw_calman_charts(self: "PQAutomationApp") -> None:
# Gamma
a4 = self . calman_ax_gamma
a4 . clear ( )
_style_axes ( a4 , " Gamma Log/Log " )
_style_axes ( self , a4 , " Gamma Log/Log " )
if gamma_pcts :
a4 . plot ( gamma_pcts , gamma_vals , " - " , color = " #ffe24d " , linewidth = 1.3 )
a4 . axhline ( TARGET_GAMMA , color = " #9e9e9e " , lw = 0.8 , ls = " -- " )
@@ -889,21 +1014,32 @@ def _redraw_calman_charts(self: "PQAutomationApp") -> None:
def _redraw_xy_chart ( self : " PQAutomationApp " ) - > None :
palette = _get_calman_palette ( )
if hasattr ( self , " calman_xy_fig " ) :
self . calman_xy_fig . patch . set_facecolor ( palette [ " figure_bg " ] )
if hasattr ( self , " calman_xy_canvas " ) :
try :
self . calman_xy_canvas . get_tk_widget ( ) . configure (
bg = palette [ " figure_bg " ] , highlightthickness = 0
)
except Exception :
pass
ax = self . calman_xy_ax
ax . clear ( )
_style_axes ( ax , " CIE 1931 xy " )
_style_axes ( self , ax , " CIE 1931 xy " )
ax . set_xlim ( 0.29 , 0.34 )
ax . set_ylim ( 0.31 , 0.35 )
ax . plot ( [ D65_X ] , [ D65_Y ] , marker = " x " , color = " #ffffff " , markersize = 7 )
ax . plot ( [ D65_X ] , [ D65_Y ] , marker = " x " , color = palette [ " d65_mark " ] , markersize = 7 )
recs = sorted ( self . calman_results . values ( ) , key = lambda r : r [ " pct " ] )
if recs :
xs = [ r [ " x " ] for r in recs ]
ys = [ r [ " y " ] for r in recs ]
ax . plot ( xs , ys , " o- " , color = " #000000 " , linewidth = 1.0 , markersize = 3 )
ax . plot ( xs , ys , " o- " , color = palette [ " xy_series " ] , linewidth = 1.0 , markersize = 3 )
last = recs [ - 1 ]
ax . plot ( [ last [ " x " ] ] , [ last [ " y " ] ] , marker = " o " , color = " #ffcc00 " , markersize = 5 )
ax . plot ( [ last [ " x " ] , D65_X ] , [ last [ " y " ] , D65_Y ] , color = " #c7c7c7 " , linewidth = 0.8 )
ax . plot ( [ last [ " x " ] , D65_X ] , [ last [ " y " ] , D65_Y ] , color = _mix ( palette [ " fg " ] , palette [ " axes_bg " ] , 0.4 ) , linewidth = 0.8 )
self . calman_xy_canvas . draw_idle ( )
@@ -937,6 +1073,8 @@ def _update_target_strip(self: "PQAutomationApp") -> None:
def _refresh_metric_table ( self : " PQAutomationApp " ) - > None :
""" 重绘下方矩阵表。 """
_apply_calman_tree_style ( self )
palette = _get_calman_palette ( )
metrics = [
( " x CIE31 " , lambda r : _safe_float ( r . get ( " x " ) ) if r else " - " ) ,
( " y CIE31 " , lambda r : _safe_float ( r . get ( " y " ) ) if r else " - " ) ,
@@ -971,17 +1109,54 @@ def _refresh_metric_table(self: "PQAutomationApp") -> None:
self . calman_metric_tree . insert ( " " , tk . END , iid = iid , values = ( name , ) , tags = tags )
self . calman_data_tree . insert ( " " , tk . END , iid = iid , values = values , tags = tags )
self . calman_metric_tree . tag_configure ( " odd " , background = " #f5f7fa " )
self . calman_metric_tree . tag_configur e( " even " , back ground= " #ffffff " )
self . calman_data_tree . tag_configure ( " odd " , background = " #f5f7fa " )
self . calman_data _tree . tag_configure ( " even " , background = " #ffffff " )
self . calman_metric_tree . tag_configure (
" odd " , background = palett e[ " tree_odd " ] , fore ground= palette [ " tree_fg " ]
)
self . calman_metric _tree . tag_configure (
" even " , background = palette [ " tree_even " ] , foreground = palette [ " tree_fg " ]
)
self . calman_data_tree . tag_configure (
" odd " , background = palette [ " tree_odd " ] , foreground = palette [ " tree_fg " ]
)
self . calman_data_tree . tag_configure (
" even " , background = palette [ " tree_even " ] , foreground = palette [ " tree_fg " ]
)
first , last = self . calman_data_tree . yview ( )
self . calman_table_ysb . set ( first , last )
def refresh_calman_theme ( self : " PQAutomationApp " ) - > None :
""" 主题切换后刷新 Calman 灰阶调试面板的表格与图表颜色。 """
if not hasattr ( self , " calman_frame " ) :
return
palette = _get_calman_palette ( )
if hasattr ( self , " calman_elapsed_label " ) :
self . calman_elapsed_label . configure ( foreground = palette [ " status_fg " ] )
if hasattr ( self , " calman_status_label " ) :
self . calman_status_label . configure ( foreground = palette [ " status_fg " ] )
if hasattr ( self , " calman_reading_summary_label " ) :
self . calman_reading_summary_label . configure ( foreground = palette [ " reading_accent " ] )
if hasattr ( self , " calman_reading_detail_label " ) :
self . calman_reading_detail_label . configure (
fg = palette [ " reading_fg " ] ,
bg = palette [ " reading_bg " ] ,
)
for lbl in getattr ( self , " calman_metric_labels " , [ ] ) :
lbl . configure (
fg = palette [ " metric_tile_fg " ] ,
bg = palette [ " metric_tile_bg " ] ,
)
_refresh_metric_table ( self )
_redraw_calman_charts ( self )
class CalmanPanelMixin :
""" 挂载本模块的自由函数到 PQAutomationApp。 """
create_calman_panel = create_calman_panel
toggle_calman_panel = toggle_calman_panel
refresh_calman_theme = refresh_calman_theme