Compare commits
10 Commits
1aab2d2453
...
b26a3c398d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b26a3c398d | ||
|
|
4073a6e999 | ||
|
|
9a2ac69afb | ||
|
|
e27312d0a3 | ||
|
|
6cc3e55ebb | ||
|
|
6b8bfe06b9 | ||
|
|
a5595b7e60 | ||
|
|
982210a724 | ||
|
|
a2bfd6d123 | ||
|
|
b5b706ef4d |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -30,4 +30,5 @@ Thumbs.db
|
|||||||
Desktop.ini
|
Desktop.ini
|
||||||
|
|
||||||
# Local configuration overrides
|
# Local configuration overrides
|
||||||
settings/*.local.json
|
settings/*.local.json
|
||||||
|
settings/
|
||||||
@@ -40,7 +40,7 @@ def calculate_gamut_coverage(points1, points2):
|
|||||||
return intersection_area, coverage
|
return intersection_area, coverage
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ 计算色域覆盖率失败: {e}")
|
print(f"计算色域覆盖率失败: {e}")
|
||||||
return 0.0, 0.0
|
return 0.0, 0.0
|
||||||
|
|
||||||
|
|
||||||
@@ -227,7 +227,7 @@ def calculate_uv_gamut_coverage(uv_coords, reference="DCI-P3"):
|
|||||||
return coverage
|
return coverage
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"❌ 计算 u'v' 色域覆盖率失败: {str(e)}")
|
print(f"计算 u'v' 色域覆盖率失败: {str(e)}")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@@ -451,7 +451,7 @@ def calculate_cct_from_results(results):
|
|||||||
"""
|
"""
|
||||||
从测量结果提取色度坐标(xy 坐标)
|
从测量结果提取色度坐标(xy 坐标)
|
||||||
|
|
||||||
⚠️ 注意:方法名保持不变(历史原因),实际功能是提取色度坐标
|
注意:方法名保持不变(历史原因),实际功能是提取色度坐标
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
results: [[x, y, lv, ...], ...] 测量结果列表
|
results: [[x, y, lv, ...], ...] 测量结果列表
|
||||||
@@ -486,7 +486,7 @@ def calculate_cct_from_results(results):
|
|||||||
lv_values.append(0.0)
|
lv_values.append(0.0)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ 提取色度坐标失败: {str(e)}")
|
print(f"提取色度坐标失败: {str(e)}")
|
||||||
x_values.append(0.3127)
|
x_values.append(0.3127)
|
||||||
y_values.append(0.3290)
|
y_values.append(0.3290)
|
||||||
lv_values.append(0.0)
|
lv_values.append(0.0)
|
||||||
@@ -555,5 +555,5 @@ if __name__ == "__main__":
|
|||||||
print(f" 点{i+1}: Gamma={result[3]:.3f}")
|
print(f" 点{i+1}: Gamma={result[3]:.3f}")
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
print("\n" + "=" * 60)
|
||||||
print("✅ 测试完成!")
|
print("测试完成!")
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""配置文件 I/O(Step 4 重构)。
|
"""配置文件 I/O(Step 4 重构)。
|
||||||
|
|
||||||
从 pqAutomationApp.PQAutomationApp 中搬迁。每个函数第一行 `self = app`
|
从 pqAutomationApp.PQAutomationApp 中搬迁。每个函数第一行 `self = app`
|
||||||
以保留原有 `self.xxx` 属性访问不变。
|
以保留原有 `self.xxx` 属性访问不变。
|
||||||
@@ -33,19 +33,19 @@ def get_config_path(self):
|
|||||||
def load_pq_config(self):
|
def load_pq_config(self):
|
||||||
"""加载PQ配置(兼容打包后的程序)"""
|
"""加载PQ配置(兼容打包后的程序)"""
|
||||||
try:
|
try:
|
||||||
# ✅ 使用 self.config_file(已经是动态路径)
|
# 使用 self.config_file(已经是动态路径)
|
||||||
if os.path.exists(self.config_file):
|
if os.path.exists(self.config_file):
|
||||||
with open(self.config_file, "r", encoding="utf-8") as f:
|
with open(self.config_file, "r", encoding="utf-8") as f:
|
||||||
config_dict = json.load(f)
|
config_dict = json.load(f)
|
||||||
self.config.from_dict(config_dict)
|
self.config.from_dict(config_dict)
|
||||||
if hasattr(self, "log_gui"):
|
if hasattr(self, "log_gui"):
|
||||||
self.log_gui.log("✓ 配置文件加载成功")
|
self.log_gui.log("配置文件加载成功", level="success")
|
||||||
else:
|
else:
|
||||||
if hasattr(self, "log_gui"):
|
if hasattr(self, "log_gui"):
|
||||||
self.log_gui.log("⚠️ 配置文件不存在,使用默认配置")
|
self.log_gui.log("配置文件不存在,使用默认配置", level="error")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if hasattr(self, "log_gui"):
|
if hasattr(self, "log_gui"):
|
||||||
self.log_gui.log(f"⚠️ 加载配置文件失败: {str(e)},使用默认配置")
|
self.log_gui.log(f"加载配置文件失败: {str(e)},使用默认配置", level="error")
|
||||||
|
|
||||||
|
|
||||||
def save_pq_config(self):
|
def save_pq_config(self):
|
||||||
@@ -58,7 +58,7 @@ def save_pq_config(self):
|
|||||||
self.config.save_to_file(self.config_file)
|
self.config.save_to_file(self.config_file)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if hasattr(self, "log_gui"):
|
if hasattr(self, "log_gui"):
|
||||||
self.log_gui.log(f"保存配置文件失败: {str(e)}")
|
self.log_gui.log(f"保存配置文件失败: {str(e)}", level="error")
|
||||||
|
|
||||||
|
|
||||||
def clear_config_file(self):
|
def clear_config_file(self):
|
||||||
@@ -71,14 +71,14 @@ def clear_config_file(self):
|
|||||||
if os.path.exists(config_file):
|
if os.path.exists(config_file):
|
||||||
os.remove(config_file)
|
os.remove(config_file)
|
||||||
self.config_cleared = True
|
self.config_cleared = True
|
||||||
messagebox.showinfo("提示", "✓ 清理成功")
|
messagebox.showinfo("提示", "清理成功")
|
||||||
self.log_gui.log("✓ 配置文件清理成功")
|
self.log_gui.log("配置文件清理成功", level="success")
|
||||||
else:
|
else:
|
||||||
messagebox.showinfo("提示", "配置文件不存在")
|
messagebox.showinfo("提示", "配置文件不存在")
|
||||||
self.log_gui.log("⚠️ 配置文件不存在")
|
self.log_gui.log("配置文件不存在", level="error")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
messagebox.showerror("错误", "❌ 清理失败")
|
messagebox.showerror("错误", "清理失败")
|
||||||
self.log_gui.log(f"❌ 配置文件清理失败: {str(e)}")
|
self.log_gui.log(f"配置文件清理失败: {str(e)}", level="error")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class DataRangeConverter:
|
|||||||
# Full Range 不需要转换
|
# Full Range 不需要转换
|
||||||
if data_range == "Full":
|
if data_range == "Full":
|
||||||
if self.verbose:
|
if self.verbose:
|
||||||
print("✓ 使用 Full Range (0-255),无需转换")
|
print("使用 Full Range (0-255),无需转换")
|
||||||
return pattern_params
|
return pattern_params
|
||||||
|
|
||||||
# Limited Range 需要转换
|
# Limited Range 需要转换
|
||||||
@@ -157,7 +157,7 @@ class DataRangeConverter:
|
|||||||
|
|
||||||
def _print_footer(self, total_count):
|
def _print_footer(self, total_count):
|
||||||
"""打印转换尾部信息"""
|
"""打印转换尾部信息"""
|
||||||
print(f"✓ 转换完成,共 {total_count} 个图案")
|
print(f"转换完成,共 {total_count} 个图案")
|
||||||
print("=" * 80)
|
print("=" * 80)
|
||||||
|
|
||||||
def get_info(self):
|
def get_info(self):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""设备连接(UCD323 / CA410)相关逻辑(Step 4 重构)。
|
"""设备连接(UCD323 / CA410)相关逻辑(Step 4 重构)。
|
||||||
|
|
||||||
从 pqAutomationApp.PQAutomationApp 中搬迁。每个函数第一行 `self = app`
|
从 pqAutomationApp.PQAutomationApp 中搬迁。每个函数第一行 `self = app`
|
||||||
以保留原有 `self.xxx` 属性访问不变。
|
以保留原有 `self.xxx` 属性访问不变。
|
||||||
@@ -23,7 +23,7 @@ def get_available_com_ports(self):
|
|||||||
ports = serial.tools.list_ports.comports()
|
ports = serial.tools.list_ports.comports()
|
||||||
return [port.device for port in ports]
|
return [port.device for port in ports]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_gui.log(f"获取COM端口列表出错: {e}")
|
self.log_gui.log(f"获取COM端口列表出错: {e}", level="error")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ def check_port_connection(self, is_ucd=True):
|
|||||||
if not self.ucd.open(self.ucd_list_var.get()):
|
if not self.ucd.open(self.ucd_list_var.get()):
|
||||||
self.log_gui.log(
|
self.log_gui.log(
|
||||||
f"设备 {self.ucd_list_var.get()} 异常,UCD323连接失败"
|
f"设备 {self.ucd_list_var.get()} 异常,UCD323连接失败"
|
||||||
)
|
, level="error")
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
@@ -147,12 +147,12 @@ def check_port_connection(self, is_ucd=True):
|
|||||||
else:
|
else:
|
||||||
self.log_gui.log(
|
self.log_gui.log(
|
||||||
f"端口 {self.config.device_config["ca_com"]} 异常,色温仪连接失败"
|
f"端口 {self.config.device_config["ca_com"]} 异常,色温仪连接失败"
|
||||||
)
|
, level="error")
|
||||||
self.ca.close()
|
self.ca.close()
|
||||||
self.ca = None
|
self.ca = None
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_gui.log(f"端口连接失败: {e}")
|
self.log_gui.log(f"端口连接失败: {e}", level="error")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -173,7 +173,7 @@ def disconnect_com_connections(self):
|
|||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
self.ucd.status = False
|
self.ucd.status = False
|
||||||
self.log_gui.log("UCD连接已断开")
|
self.log_gui.log("UCD连接已断开", level="info")
|
||||||
|
|
||||||
# 断开CA连接
|
# 断开CA连接
|
||||||
if self.ca is not None:
|
if self.ca is not None:
|
||||||
@@ -183,7 +183,7 @@ def disconnect_com_connections(self):
|
|||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
self.ca = None
|
self.ca = None
|
||||||
self.log_gui.log("CA连接已断开")
|
self.log_gui.log("CA连接已断开", level="info")
|
||||||
|
|
||||||
# 重新启用相关控件
|
# 重新启用相关控件
|
||||||
self.enable_com_widgets()
|
self.enable_com_widgets()
|
||||||
@@ -192,7 +192,7 @@ def disconnect_com_connections(self):
|
|||||||
self.status_var.set("串口连接已断开")
|
self.status_var.set("串口连接已断开")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_gui.log(f"断开连接时发生错误: {str(e)}")
|
self.log_gui.log(f"断开连接时发生错误: {str(e)}", level="info")
|
||||||
messagebox.showerror("错误", f"断开连接失败: {str(e)}")
|
messagebox.showerror("错误", f"断开连接失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
5
app/export/__init__.py
Normal file
5
app/export/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""导出模块:测试结果图片与 Excel 报告。"""
|
||||||
|
from app.export.image_exporter import save_result_images
|
||||||
|
from app.export.excel_exporter import export_excel_report, EXCEL_EXPORT_CONFIG
|
||||||
|
|
||||||
|
__all__ = ["save_result_images", "export_excel_report", "EXCEL_EXPORT_CONFIG"]
|
||||||
506
app/export/excel_exporter.py
Normal file
506
app/export/excel_exporter.py
Normal file
@@ -0,0 +1,506 @@
|
|||||||
|
"""测试结果 Excel 导出(screen_module / sdr_movie / hdr_movie 统一实现)。"""
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
|
EXCEL_EXPORT_CONFIG = {
|
||||||
|
"screen_module": {
|
||||||
|
"title": "屏模组性能测试数据报告",
|
||||||
|
"type_label": "屏模组",
|
||||||
|
"log_prefix": "屏模组",
|
||||||
|
"curve_type": "gamma",
|
||||||
|
"has_accuracy": False,
|
||||||
|
"column_widths": {"A": 18, "B": 18, "C": 18, "D": 18, "E": 18,
|
||||||
|
"F": 15, "G": 15},
|
||||||
|
},
|
||||||
|
"sdr_movie": {
|
||||||
|
"title": "SDR Movie 性能测试数据报告",
|
||||||
|
"type_label": "SDR Movie",
|
||||||
|
"log_prefix": "SDR Movie",
|
||||||
|
"curve_type": "gamma",
|
||||||
|
"has_accuracy": True,
|
||||||
|
"column_widths": {c: 18 for c in "ABCDEFG"},
|
||||||
|
},
|
||||||
|
"hdr_movie": {
|
||||||
|
"title": "HDR Movie 性能测试数据报告",
|
||||||
|
"type_label": "HDR Movie",
|
||||||
|
"log_prefix": "HDR Movie",
|
||||||
|
"curve_type": "eotf",
|
||||||
|
"has_accuracy": True,
|
||||||
|
"column_widths": {c: 18 for c in "ABCDEFG"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 样式 ====================
|
||||||
|
def _build_styles(Font, Alignment, PatternFill, Border, Side):
|
||||||
|
thin = Side(style="thin")
|
||||||
|
return {
|
||||||
|
"title_font": Font(name="微软雅黑", size=16, bold=True, color="FFFFFF"),
|
||||||
|
"title_fill": PatternFill(start_color="4472C4", end_color="4472C4",
|
||||||
|
fill_type="solid"),
|
||||||
|
"title_alignment": Alignment(horizontal="center", vertical="center"),
|
||||||
|
"section_font": Font(name="微软雅黑", size=13, bold=True, color="FFFFFF"),
|
||||||
|
"section_fill": PatternFill(start_color="5B9BD5", end_color="5B9BD5",
|
||||||
|
fill_type="solid"),
|
||||||
|
"section_alignment": Alignment(horizontal="center", vertical="center"),
|
||||||
|
"header_font": Font(name="微软雅黑", size=10, bold=True, color="FFFFFF"),
|
||||||
|
"header_fill": PatternFill(start_color="70AD47", end_color="70AD47",
|
||||||
|
fill_type="solid"),
|
||||||
|
"header_alignment": Alignment(horizontal="center", vertical="center",
|
||||||
|
wrap_text=True),
|
||||||
|
"data_font": Font(name="微软雅黑", size=10),
|
||||||
|
"data_alignment": Alignment(horizontal="center", vertical="center"),
|
||||||
|
"label_font": Font(name="微软雅黑", size=10, bold=True),
|
||||||
|
"thin_border": Border(left=thin, right=thin, top=thin, bottom=thin),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 通用写入 ====================
|
||||||
|
def _write_title(ws, styles, title_text):
|
||||||
|
ws.merge_cells("A1:G1")
|
||||||
|
ws["A1"] = title_text
|
||||||
|
ws["A1"].font = styles["title_font"]
|
||||||
|
ws["A1"].fill = styles["title_fill"]
|
||||||
|
ws["A1"].alignment = styles["title_alignment"]
|
||||||
|
ws.row_dimensions[1].height = 35
|
||||||
|
|
||||||
|
|
||||||
|
def _write_basic_info(ws, styles, row, type_label):
|
||||||
|
ws.merge_cells(f"A{row}:B{row}")
|
||||||
|
ws[f"A{row}"] = "📋 测试基本信息"
|
||||||
|
ws[f"A{row}"].font = styles["section_font"]
|
||||||
|
ws[f"A{row}"].fill = styles["section_fill"]
|
||||||
|
ws[f"A{row}"].alignment = styles["section_alignment"]
|
||||||
|
ws.row_dimensions[row].height = 25
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
info_items = [
|
||||||
|
("测试时间", datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")),
|
||||||
|
("测试类型", type_label),
|
||||||
|
]
|
||||||
|
for label, value in info_items:
|
||||||
|
ws[f"A{row}"] = label
|
||||||
|
ws[f"B{row}"] = value
|
||||||
|
ws[f"A{row}"].font = styles["label_font"]
|
||||||
|
ws[f"B{row}"].font = styles["data_font"]
|
||||||
|
ws[f"A{row}"].border = styles["thin_border"]
|
||||||
|
ws[f"B{row}"].border = styles["thin_border"]
|
||||||
|
row += 1
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def _write_section_header(ws, styles, row, text, span="A{r}:G{r}"):
|
||||||
|
ws.merge_cells(span.format(r=row))
|
||||||
|
ws[f"A{row}"] = text
|
||||||
|
ws[f"A{row}"].font = styles["section_font"]
|
||||||
|
ws[f"A{row}"].fill = styles["section_fill"]
|
||||||
|
ws[f"A{row}"].alignment = styles["section_alignment"]
|
||||||
|
ws.row_dimensions[row].height = 25
|
||||||
|
return row + 1
|
||||||
|
|
||||||
|
|
||||||
|
def _write_headers(ws, styles, row, headers):
|
||||||
|
for col_idx, header in enumerate(headers, start=1):
|
||||||
|
cell = ws.cell(row=row, column=col_idx)
|
||||||
|
cell.value = header
|
||||||
|
cell.font = styles["header_font"]
|
||||||
|
cell.fill = styles["header_fill"]
|
||||||
|
cell.alignment = styles["header_alignment"]
|
||||||
|
cell.border = styles["thin_border"]
|
||||||
|
return row + 1
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_row_style(ws, row, cols, styles, label_cols=()):
|
||||||
|
for col in cols:
|
||||||
|
cell = ws[f"{col}{row}"]
|
||||||
|
cell.font = (styles["label_font"] if col in label_cols
|
||||||
|
else styles["data_font"])
|
||||||
|
cell.border = styles["thin_border"]
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_data_row(ws, row, cols, styles):
|
||||||
|
for col in cols:
|
||||||
|
cell = ws[f"{col}{row}"]
|
||||||
|
cell.font = styles["data_font"]
|
||||||
|
cell.alignment = styles["data_alignment"]
|
||||||
|
cell.border = styles["thin_border"]
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 分区:色域 ====================
|
||||||
|
def _section_gamut(ws, styles, row, results, log):
|
||||||
|
rgb_data = results.get_intermediate_data("gamut", "rgb")
|
||||||
|
gamut_final_result = None
|
||||||
|
if "gamut" in results.test_items:
|
||||||
|
gamut_final_result = results.test_items["gamut"].final_result
|
||||||
|
|
||||||
|
if not (rgb_data and len(rgb_data) >= 3):
|
||||||
|
return row
|
||||||
|
|
||||||
|
row = _write_section_header(ws, styles, row, "🎨 色域测试数据")
|
||||||
|
|
||||||
|
if gamut_final_result:
|
||||||
|
xy_coverage = gamut_final_result.get("coverage", 0)
|
||||||
|
uv_coverage = (
|
||||||
|
gamut_final_result.get("uv_coverage", 0)
|
||||||
|
or gamut_final_result.get("uv_space_coverage", 0)
|
||||||
|
or gamut_final_result.get("coverage_uv", 0)
|
||||||
|
or 0
|
||||||
|
)
|
||||||
|
|
||||||
|
ws[f"A{row}"] = "参考标准"
|
||||||
|
ws[f"B{row}"] = gamut_final_result.get("reference", "DCI-P3")
|
||||||
|
ws[f"A{row}"].font = styles["label_font"]
|
||||||
|
ws[f"B{row}"].font = styles["data_font"]
|
||||||
|
ws[f"A{row}"].border = styles["thin_border"]
|
||||||
|
ws[f"B{row}"].border = styles["thin_border"]
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
ws[f"A{row}"] = "XY 色域覆盖率"
|
||||||
|
ws[f"B{row}"] = f"{xy_coverage:.2f}%"
|
||||||
|
ws[f"C{row}"] = "UV 色域覆盖率"
|
||||||
|
ws[f"D{row}"] = f"{uv_coverage:.2f}%"
|
||||||
|
_apply_row_style(ws, row, ["A", "B", "C", "D"], styles,
|
||||||
|
label_cols=("A", "C"))
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
row = _write_headers(
|
||||||
|
ws, styles, row,
|
||||||
|
["点位", "x 坐标", "y 坐标", "亮度 (cd/m²)", "", "", ""],
|
||||||
|
)
|
||||||
|
|
||||||
|
rgb_labels = ["Red", "Green", "Blue"]
|
||||||
|
for i, result in enumerate(rgb_data[:3]):
|
||||||
|
x, y, lv = result[0], result[1], result[2]
|
||||||
|
ws[f"A{row}"] = rgb_labels[i]
|
||||||
|
ws[f"B{row}"] = x
|
||||||
|
ws[f"C{row}"] = y
|
||||||
|
ws[f"D{row}"] = lv
|
||||||
|
ws[f"B{row}"].number_format = "0.0000"
|
||||||
|
ws[f"C{row}"].number_format = "0.0000"
|
||||||
|
ws[f"D{row}"].number_format = "0.00"
|
||||||
|
_apply_data_row(ws, row, ["A", "B", "C", "D"], styles)
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
log(" 添加色域数据", level="info")
|
||||||
|
return row + 1
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 分区:Gamma / EOTF ====================
|
||||||
|
def _section_curve(ws, styles, row, results, log, curve_type):
|
||||||
|
if curve_type == "gamma":
|
||||||
|
section_title = "📊 Gamma 曲线数据"
|
||||||
|
label_prefix = "Gamma"
|
||||||
|
data_key = "gamma"
|
||||||
|
value_header = "Gamma 值"
|
||||||
|
else:
|
||||||
|
section_title = "📊 EOTF 曲线数据"
|
||||||
|
label_prefix = "EOTF"
|
||||||
|
data_key = "eotf"
|
||||||
|
value_header = "EOTF 值"
|
||||||
|
|
||||||
|
gray_data = results.get_intermediate_data("shared", "gray")
|
||||||
|
if not gray_data:
|
||||||
|
gray_data = results.get_intermediate_data(curve_type, "gray")
|
||||||
|
|
||||||
|
curve_final_result = None
|
||||||
|
if curve_type in results.test_items:
|
||||||
|
curve_final_result = results.test_items[curve_type].final_result
|
||||||
|
|
||||||
|
if not (gray_data and len(gray_data) > 0 and curve_final_result):
|
||||||
|
return row
|
||||||
|
|
||||||
|
value_list = curve_final_result.get(data_key, [])
|
||||||
|
L_bar_list = curve_final_result.get("L_bar", [])
|
||||||
|
|
||||||
|
row = _write_section_header(ws, styles, row, section_title)
|
||||||
|
|
||||||
|
valid_values = [
|
||||||
|
item[3]
|
||||||
|
for item in value_list
|
||||||
|
if isinstance(item, (list, tuple))
|
||||||
|
and len(item) >= 4
|
||||||
|
and 0.5 < item[3] < 5.0
|
||||||
|
]
|
||||||
|
if valid_values:
|
||||||
|
avg_v = sum(valid_values) / len(valid_values)
|
||||||
|
ws[f"A{row}"] = f"平均 {label_prefix}"
|
||||||
|
ws[f"B{row}"] = f"{avg_v:.3f}"
|
||||||
|
ws[f"C{row}"] = f"最大 {label_prefix}"
|
||||||
|
ws[f"D{row}"] = f"{max(valid_values):.3f}"
|
||||||
|
ws[f"E{row}"] = f"最小 {label_prefix}"
|
||||||
|
ws[f"F{row}"] = f"{min(valid_values):.3f}"
|
||||||
|
_apply_row_style(ws, row, ["A", "B", "C", "D", "E", "F"], styles,
|
||||||
|
label_cols=("A", "C", "E"))
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
row = _write_headers(
|
||||||
|
ws, styles, row,
|
||||||
|
["灰阶 (%)", "x 坐标", "y 坐标", "实测亮度\n(cd/m²)",
|
||||||
|
"归一化亮度\n(L_bar)", value_header, ""],
|
||||||
|
)
|
||||||
|
|
||||||
|
total_points = len(gray_data)
|
||||||
|
for i in range(total_points - 1, -1, -1):
|
||||||
|
gray_level = (100 - int(i * 100 / (total_points - 1))
|
||||||
|
if total_points > 1 else 0)
|
||||||
|
x, y, lv = gray_data[i][0], gray_data[i][1], gray_data[i][2]
|
||||||
|
L_bar_val = L_bar_list[i] if i < len(L_bar_list) else 0
|
||||||
|
|
||||||
|
val = None
|
||||||
|
if (i < len(value_list)
|
||||||
|
and isinstance(value_list[i], (list, tuple))
|
||||||
|
and len(value_list[i]) >= 4):
|
||||||
|
val = value_list[i][3]
|
||||||
|
|
||||||
|
ws[f"A{row}"] = gray_level
|
||||||
|
ws[f"B{row}"] = x
|
||||||
|
ws[f"C{row}"] = y
|
||||||
|
ws[f"D{row}"] = lv
|
||||||
|
ws[f"E{row}"] = L_bar_val
|
||||||
|
|
||||||
|
if val is not None and 0.5 < val < 5.0:
|
||||||
|
ws[f"F{row}"] = val
|
||||||
|
ws[f"F{row}"].number_format = "0.000"
|
||||||
|
else:
|
||||||
|
ws[f"F{row}"] = "N/A"
|
||||||
|
|
||||||
|
ws[f"A{row}"].number_format = "0"
|
||||||
|
ws[f"B{row}"].number_format = "0.0000"
|
||||||
|
ws[f"C{row}"].number_format = "0.0000"
|
||||||
|
ws[f"D{row}"].number_format = "0.00"
|
||||||
|
ws[f"E{row}"].number_format = "0.0000"
|
||||||
|
_apply_data_row(ws, row, ["A", "B", "C", "D", "E", "F"], styles)
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
log(f" 添加 {label_prefix} 数据", level="info")
|
||||||
|
return row + 1
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 分区:色度一致性 ====================
|
||||||
|
def _section_cct(ws, styles, row, results, log):
|
||||||
|
gray_data = results.get_intermediate_data("shared", "gray")
|
||||||
|
if not gray_data:
|
||||||
|
gray_data = results.get_intermediate_data("cct", "gray")
|
||||||
|
if not (gray_data and len(gray_data) > 1):
|
||||||
|
return row
|
||||||
|
|
||||||
|
gray_data_no_black = gray_data[:-1]
|
||||||
|
|
||||||
|
row = _write_section_header(ws, styles, row, "🌈 色度一致性数据")
|
||||||
|
|
||||||
|
x_coords = [d[0] for d in gray_data_no_black]
|
||||||
|
y_coords = [d[1] for d in gray_data_no_black]
|
||||||
|
ws[f"A{row}"] = "x 坐标范围"
|
||||||
|
ws[f"B{row}"] = f"{min(x_coords):.4f} ~ {max(x_coords):.4f}"
|
||||||
|
ws[f"C{row}"] = "y 坐标范围"
|
||||||
|
ws[f"D{row}"] = f"{min(y_coords):.4f} ~ {max(y_coords):.4f}"
|
||||||
|
_apply_row_style(ws, row, ["A", "B", "C", "D"], styles,
|
||||||
|
label_cols=("A", "C"))
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
row = _write_headers(
|
||||||
|
ws, styles, row,
|
||||||
|
["灰阶 (%)", "x 坐标", "y 坐标", "亮度 (cd/m²)", "", "", ""],
|
||||||
|
)
|
||||||
|
|
||||||
|
total_points = len(gray_data)
|
||||||
|
for i in range(len(gray_data_no_black) - 1, -1, -1):
|
||||||
|
x, y, lv = (gray_data_no_black[i][0],
|
||||||
|
gray_data_no_black[i][1],
|
||||||
|
gray_data_no_black[i][2])
|
||||||
|
gray_level = (100 - int(i * 100 / (total_points - 1))
|
||||||
|
if total_points > 1 else 0)
|
||||||
|
ws[f"A{row}"] = gray_level
|
||||||
|
ws[f"B{row}"] = x
|
||||||
|
ws[f"C{row}"] = y
|
||||||
|
ws[f"D{row}"] = lv
|
||||||
|
ws[f"A{row}"].number_format = "0"
|
||||||
|
ws[f"B{row}"].number_format = "0.0000"
|
||||||
|
ws[f"C{row}"].number_format = "0.0000"
|
||||||
|
ws[f"D{row}"].number_format = "0.00"
|
||||||
|
_apply_data_row(ws, row, ["A", "B", "C", "D"], styles)
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
log(" 添加色度一致性数据", level="info")
|
||||||
|
return row + 1
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 分区:对比度 ====================
|
||||||
|
def _section_contrast(ws, styles, row, results, log):
|
||||||
|
contrast_final_result = None
|
||||||
|
if "contrast" in results.test_items:
|
||||||
|
contrast_final_result = results.test_items["contrast"].final_result
|
||||||
|
if not contrast_final_result:
|
||||||
|
return row
|
||||||
|
|
||||||
|
row = _write_section_header(ws, styles, row, "⚫⚪ 对比度测试数据")
|
||||||
|
|
||||||
|
max_lv = contrast_final_result.get("max_luminance", 0)
|
||||||
|
min_lv = contrast_final_result.get("min_luminance", 0)
|
||||||
|
contrast_ratio = contrast_final_result.get("contrast_ratio", 0)
|
||||||
|
info_items = [
|
||||||
|
("最大亮度(白场)", f"{max_lv:.2f} cd/m²"),
|
||||||
|
("最小亮度(黑场)", f"{min_lv:.4f} cd/m²"),
|
||||||
|
("对比度", f"{contrast_ratio:.0f}:1"),
|
||||||
|
]
|
||||||
|
for label, value in info_items:
|
||||||
|
ws[f"A{row}"] = label
|
||||||
|
ws[f"B{row}"] = value
|
||||||
|
ws[f"A{row}"].font = styles["label_font"]
|
||||||
|
ws[f"B{row}"].font = styles["data_font"]
|
||||||
|
ws[f"A{row}"].border = styles["thin_border"]
|
||||||
|
ws[f"B{row}"].border = styles["thin_border"]
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
log(" 添加对比度数据", level="info")
|
||||||
|
return row + 1
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 分区:色准 ====================
|
||||||
|
def _section_accuracy(ws, styles, row, results, log):
|
||||||
|
accuracy_final_result = None
|
||||||
|
if "accuracy" in results.test_items:
|
||||||
|
accuracy_final_result = results.test_items["accuracy"].final_result
|
||||||
|
if not accuracy_final_result:
|
||||||
|
return row
|
||||||
|
|
||||||
|
row = _write_section_header(ws, styles, row, "🎯 色准测试数据")
|
||||||
|
|
||||||
|
avg_delta_e = accuracy_final_result.get("avg_delta_e", 0)
|
||||||
|
max_delta_e = accuracy_final_result.get("max_delta_e", 0)
|
||||||
|
min_delta_e = accuracy_final_result.get("min_delta_e", 0)
|
||||||
|
excellent_count = accuracy_final_result.get("excellent_count", 0)
|
||||||
|
good_count = accuracy_final_result.get("good_count", 0)
|
||||||
|
poor_count = accuracy_final_result.get("poor_count", 0)
|
||||||
|
|
||||||
|
ws[f"A{row}"] = "平均 ΔE"
|
||||||
|
ws[f"B{row}"] = f"{avg_delta_e:.2f}"
|
||||||
|
ws[f"C{row}"] = "最大 ΔE"
|
||||||
|
ws[f"D{row}"] = f"{max_delta_e:.2f}"
|
||||||
|
ws[f"E{row}"] = "最小 ΔE"
|
||||||
|
ws[f"F{row}"] = f"{min_delta_e:.2f}"
|
||||||
|
_apply_row_style(ws, row, ["A", "B", "C", "D", "E", "F"], styles,
|
||||||
|
label_cols=("A", "C", "E"))
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
ws[f"A{row}"] = "优秀 (ΔE<3)"
|
||||||
|
ws[f"B{row}"] = f"{excellent_count} 个"
|
||||||
|
ws[f"C{row}"] = "良好 (3≤ΔE<5)"
|
||||||
|
ws[f"D{row}"] = f"{good_count} 个"
|
||||||
|
ws[f"E{row}"] = "偏差 (ΔE≥5)"
|
||||||
|
ws[f"F{row}"] = f"{poor_count} 个"
|
||||||
|
_apply_row_style(ws, row, ["A", "B", "C", "D", "E", "F"], styles,
|
||||||
|
label_cols=("A", "C", "E"))
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
color_patches = accuracy_final_result.get("color_patches", [])
|
||||||
|
delta_e_values = accuracy_final_result.get("delta_e_values", [])
|
||||||
|
color_measurements = accuracy_final_result.get("color_measurements", [])
|
||||||
|
|
||||||
|
if color_patches and delta_e_values:
|
||||||
|
row = _write_headers(
|
||||||
|
ws, styles, row,
|
||||||
|
["序号", "颜色名称", "x 坐标", "y 坐标",
|
||||||
|
"亮度 (cd/m²)", "ΔE 2000", "等级"],
|
||||||
|
)
|
||||||
|
for idx, (color_name, delta_e) in enumerate(
|
||||||
|
zip(color_patches, delta_e_values), start=1):
|
||||||
|
if delta_e < 3:
|
||||||
|
grade = "优秀"
|
||||||
|
elif delta_e < 5:
|
||||||
|
grade = "良好"
|
||||||
|
else:
|
||||||
|
grade = "偏差"
|
||||||
|
|
||||||
|
x_val, y_val, lv_val = "N/A", "N/A", "N/A"
|
||||||
|
if color_measurements and idx - 1 < len(color_measurements):
|
||||||
|
m = color_measurements[idx - 1]
|
||||||
|
if len(m) >= 3:
|
||||||
|
x_val, y_val, lv_val = m[0], m[1], m[2]
|
||||||
|
|
||||||
|
ws[f"A{row}"] = idx
|
||||||
|
ws[f"B{row}"] = color_name
|
||||||
|
ws[f"C{row}"] = x_val
|
||||||
|
ws[f"D{row}"] = y_val
|
||||||
|
ws[f"E{row}"] = lv_val
|
||||||
|
ws[f"F{row}"] = delta_e
|
||||||
|
ws[f"G{row}"] = grade
|
||||||
|
|
||||||
|
ws[f"A{row}"].number_format = "0"
|
||||||
|
if isinstance(x_val, (int, float)):
|
||||||
|
ws[f"C{row}"].number_format = "0.0000"
|
||||||
|
if isinstance(y_val, (int, float)):
|
||||||
|
ws[f"D{row}"].number_format = "0.0000"
|
||||||
|
if isinstance(lv_val, (int, float)):
|
||||||
|
ws[f"E{row}"].number_format = "0.00"
|
||||||
|
ws[f"F{row}"].number_format = "0.00"
|
||||||
|
|
||||||
|
_apply_data_row(ws, row, ["A", "B", "C", "D", "E", "F", "G"], styles)
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
log(" 添加色准数据(含 xy 坐标和亮度)")
|
||||||
|
return row + 1
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 主入口 ====================
|
||||||
|
def export_excel_report(result_dir, current_test_type, selected_items,
|
||||||
|
results, log):
|
||||||
|
"""根据测试类型生成统一格式的 Excel 报告。
|
||||||
|
|
||||||
|
仅 current_test_type ∈ EXCEL_EXPORT_CONFIG 时应调用本函数;
|
||||||
|
内部吞掉 ImportError(openpyxl 未装)与其他 Exception 并通过 log 记录。
|
||||||
|
"""
|
||||||
|
if current_test_type not in EXCEL_EXPORT_CONFIG:
|
||||||
|
return
|
||||||
|
cfg = EXCEL_EXPORT_CONFIG[current_test_type]
|
||||||
|
try:
|
||||||
|
import openpyxl
|
||||||
|
from openpyxl.styles import (
|
||||||
|
Font, Alignment, PatternFill, Border, Side,
|
||||||
|
)
|
||||||
|
|
||||||
|
log("=" * 60)
|
||||||
|
log(f"开始生成{cfg['log_prefix']} Excel 数据报告...")
|
||||||
|
|
||||||
|
wb = openpyxl.Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = "测试数据"
|
||||||
|
|
||||||
|
styles = _build_styles(Font, Alignment, PatternFill, Border, Side)
|
||||||
|
|
||||||
|
_write_title(ws, styles, cfg["title"])
|
||||||
|
row = _write_basic_info(ws, styles, 3, cfg["type_label"])
|
||||||
|
row += 1 # 空行
|
||||||
|
|
||||||
|
if "gamut" in selected_items:
|
||||||
|
row = _section_gamut(ws, styles, row, results, log)
|
||||||
|
if cfg["curve_type"] == "gamma" and "gamma" in selected_items:
|
||||||
|
row = _section_curve(ws, styles, row, results, log, "gamma")
|
||||||
|
if cfg["curve_type"] == "eotf" and "eotf" in selected_items:
|
||||||
|
row = _section_curve(ws, styles, row, results, log, "eotf")
|
||||||
|
if "cct" in selected_items:
|
||||||
|
row = _section_cct(ws, styles, row, results, log)
|
||||||
|
if "contrast" in selected_items:
|
||||||
|
row = _section_contrast(ws, styles, row, results, log)
|
||||||
|
if cfg["has_accuracy"] and "accuracy" in selected_items:
|
||||||
|
row = _section_accuracy(ws, styles, row, results, log)
|
||||||
|
|
||||||
|
for col, width in cfg["column_widths"].items():
|
||||||
|
ws.column_dimensions[col].width = width
|
||||||
|
|
||||||
|
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
excel_filename = f"测试数据_{timestamp}.xlsx"
|
||||||
|
excel_path = os.path.join(result_dir, excel_filename)
|
||||||
|
wb.save(excel_path)
|
||||||
|
|
||||||
|
log(f"已保存: {excel_filename}", level="success")
|
||||||
|
log("=" * 60, level="seperator")
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
log("未安装 openpyxl 库,跳过 Excel 导出", level="error")
|
||||||
|
log(" 安装方法: pip install openpyxl")
|
||||||
|
except Exception as e:
|
||||||
|
log(f"Excel 导出失败: {str(e)}", level="error")
|
||||||
|
log(traceback.format_exc())
|
||||||
38
app/export/image_exporter.py
Normal file
38
app/export/image_exporter.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"""测试结果图表 PNG 导出。"""
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
# (item_key, fig_attr, filename, allowed_test_types_or_None, default_bbox_inches_auto)
|
||||||
|
IMAGE_SPECS = [
|
||||||
|
("gamut", "gamut_fig", "色域测试结果.png", None, True),
|
||||||
|
("gamma", "gamma_fig", "Gamma曲线测试结果.png",
|
||||||
|
{"screen_module", "sdr_movie"}, True),
|
||||||
|
("eotf", "eotf_fig", "EOTF曲线测试结果.png", {"hdr_movie"}, True),
|
||||||
|
("cct", "cct_fig", "色度一致性测试结果.png", None, True),
|
||||||
|
("contrast", "contrast_fig", "对比度测试结果.png", None, False),
|
||||||
|
("accuracy", "accuracy_fig", "色准测试结果.png",
|
||||||
|
{"sdr_movie", "hdr_movie"}, True),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def save_result_images(result_dir, current_test_type, selected_items,
|
||||||
|
figure_provider, log):
|
||||||
|
"""根据测试类型和已选项将各测试图表保存为 PNG。
|
||||||
|
|
||||||
|
figure_provider: callable(attr_name) -> matplotlib Figure 或 None
|
||||||
|
log: callable(msg)
|
||||||
|
"""
|
||||||
|
for item_key, fig_attr, filename, allowed_types, default_bbox in IMAGE_SPECS:
|
||||||
|
if item_key not in selected_items:
|
||||||
|
continue
|
||||||
|
if allowed_types is not None and current_test_type not in allowed_types:
|
||||||
|
continue
|
||||||
|
fig = figure_provider(fig_attr)
|
||||||
|
if fig is None:
|
||||||
|
continue
|
||||||
|
path = os.path.join(result_dir, filename)
|
||||||
|
if default_bbox:
|
||||||
|
fig.savefig(path, dpi=300)
|
||||||
|
else:
|
||||||
|
fig.savefig(path, dpi=300, bbox_inches="tight")
|
||||||
|
log(f"已保存: {filename}")
|
||||||
@@ -287,7 +287,7 @@ def plot_accuracy(self, accuracy_data, test_type):
|
|||||||
grade_color = "orange"
|
grade_color = "orange"
|
||||||
else:
|
else:
|
||||||
grade = "需要校准"
|
grade = "需要校准"
|
||||||
grade_icon = "✗"
|
grade_icon = "[Error]"
|
||||||
grade_color = "red"
|
grade_color = "red"
|
||||||
|
|
||||||
self.accuracy_ax.text(
|
self.accuracy_ax.text(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""CCT / 色度一致性绘制。
|
"""CCT / 色度一致性绘制。
|
||||||
|
|
||||||
Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_cct 原样搬迁。
|
Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_cct 原样搬迁。
|
||||||
"""
|
"""
|
||||||
@@ -16,7 +16,7 @@ def plot_cct(self, test_type):
|
|||||||
gray_data = self.results.get_intermediate_data("cct", "gray")
|
gray_data = self.results.get_intermediate_data("cct", "gray")
|
||||||
|
|
||||||
if not gray_data or len(gray_data) < 2:
|
if not gray_data or len(gray_data) < 2:
|
||||||
self.log_gui.log("⚠️ 无 xy 数据可用")
|
self.log_gui.log("无 xy 数据可用", level="error")
|
||||||
ax = self.cct_fig.add_subplot(111)
|
ax = self.cct_fig.add_subplot(111)
|
||||||
ax.text(
|
ax.text(
|
||||||
0.5,
|
0.5,
|
||||||
@@ -46,9 +46,9 @@ def plot_cct(self, test_type):
|
|||||||
total_points = len(gray_data)
|
total_points = len(gray_data)
|
||||||
grayscale = np.linspace(100 / total_points, 100, len(x_measured))
|
grayscale = np.linspace(100 / total_points, 100, len(x_measured))
|
||||||
|
|
||||||
self.log_gui.log(f"✓ 已移除第一个数据点,当前数据点数: {len(x_measured)}")
|
self.log_gui.log(f"已移除第一个数据点,当前数据点数: {len(x_measured)}", level="success")
|
||||||
self.log_gui.log(f" x范围: {min(x_measured):.6f} - {max(x_measured):.6f}")
|
self.log_gui.log(f" x范围: {min(x_measured):.6f} - {max(x_measured):.6f}", level="info")
|
||||||
self.log_gui.log(f" y范围: {min(y_measured):.6f} - {max(y_measured):.6f}")
|
self.log_gui.log(f" y范围: {min(y_measured):.6f} - {max(y_measured):.6f}", level="info")
|
||||||
|
|
||||||
# ========== 根据测试类型读取对应参数 ==========
|
# ========== 根据测试类型读取对应参数 ==========
|
||||||
if test_type == "sdr_movie":
|
if test_type == "sdr_movie":
|
||||||
@@ -57,50 +57,50 @@ def plot_cct(self, test_type):
|
|||||||
x_tolerance = float(self.sdr_cct_x_tolerance_var.get())
|
x_tolerance = float(self.sdr_cct_x_tolerance_var.get())
|
||||||
y_ideal = float(self.sdr_cct_y_ideal_var.get())
|
y_ideal = float(self.sdr_cct_y_ideal_var.get())
|
||||||
y_tolerance = float(self.sdr_cct_y_tolerance_var.get())
|
y_tolerance = float(self.sdr_cct_y_tolerance_var.get())
|
||||||
self.log_gui.log("✓ 使用 SDR 色度参数")
|
self.log_gui.log("使用 SDR 色度参数", level="success")
|
||||||
except:
|
except:
|
||||||
x_ideal = 0.3127
|
x_ideal = 0.3127
|
||||||
x_tolerance = 0.003
|
x_tolerance = 0.003
|
||||||
y_ideal = 0.3290
|
y_ideal = 0.3290
|
||||||
y_tolerance = 0.003
|
y_tolerance = 0.003
|
||||||
self.log_gui.log("⚠️ SDR 参数读取失败,使用默认值")
|
self.log_gui.log("SDR 参数读取失败,使用默认值", level="error")
|
||||||
elif test_type == "hdr_movie":
|
elif test_type == "hdr_movie":
|
||||||
try:
|
try:
|
||||||
x_ideal = float(self.hdr_cct_x_ideal_var.get())
|
x_ideal = float(self.hdr_cct_x_ideal_var.get())
|
||||||
x_tolerance = float(self.hdr_cct_x_tolerance_var.get())
|
x_tolerance = float(self.hdr_cct_x_tolerance_var.get())
|
||||||
y_ideal = float(self.hdr_cct_y_ideal_var.get())
|
y_ideal = float(self.hdr_cct_y_ideal_var.get())
|
||||||
y_tolerance = float(self.hdr_cct_y_tolerance_var.get())
|
y_tolerance = float(self.hdr_cct_y_tolerance_var.get())
|
||||||
self.log_gui.log("✓ 使用 HDR 色度参数")
|
self.log_gui.log("使用 HDR 色度参数", level="success")
|
||||||
except:
|
except:
|
||||||
x_ideal = 0.3127
|
x_ideal = 0.3127
|
||||||
x_tolerance = 0.003
|
x_tolerance = 0.003
|
||||||
y_ideal = 0.3290
|
y_ideal = 0.3290
|
||||||
y_tolerance = 0.003
|
y_tolerance = 0.003
|
||||||
self.log_gui.log("⚠️ HDR 参数读取失败,使用默认值")
|
self.log_gui.log("HDR 参数读取失败,使用默认值", level="error")
|
||||||
else: # screen_module
|
else: # screen_module
|
||||||
try:
|
try:
|
||||||
x_ideal = float(self.cct_x_ideal_var.get())
|
x_ideal = float(self.cct_x_ideal_var.get())
|
||||||
x_tolerance = float(self.cct_x_tolerance_var.get())
|
x_tolerance = float(self.cct_x_tolerance_var.get())
|
||||||
y_ideal = float(self.cct_y_ideal_var.get())
|
y_ideal = float(self.cct_y_ideal_var.get())
|
||||||
y_tolerance = float(self.cct_y_tolerance_var.get())
|
y_tolerance = float(self.cct_y_tolerance_var.get())
|
||||||
self.log_gui.log("✓ 使用屏模组色度参数")
|
self.log_gui.log("使用屏模组色度参数", level="success")
|
||||||
except:
|
except:
|
||||||
x_ideal = 0.306
|
x_ideal = 0.306
|
||||||
x_tolerance = 0.003
|
x_tolerance = 0.003
|
||||||
y_ideal = 0.318
|
y_ideal = 0.318
|
||||||
y_tolerance = 0.003
|
y_tolerance = 0.003
|
||||||
self.log_gui.log("⚠️ 屏模组参数读取失败,使用默认值")
|
self.log_gui.log("屏模组参数读取失败,使用默认值", level="error")
|
||||||
|
|
||||||
x_low = x_ideal - x_tolerance
|
x_low = x_ideal - x_tolerance
|
||||||
x_high = x_ideal + x_tolerance
|
x_high = x_ideal + x_tolerance
|
||||||
y_low = y_ideal - y_tolerance
|
y_low = y_ideal - y_tolerance
|
||||||
y_high = y_ideal + y_tolerance
|
y_high = y_ideal + y_tolerance
|
||||||
|
|
||||||
self.log_gui.log(f"✓ 用户设置参数:")
|
self.log_gui.log(f"用户设置参数:", level="success")
|
||||||
self.log_gui.log(f" x-ideal={x_ideal:.4f}, tolerance={x_tolerance:.4f}")
|
self.log_gui.log(f" x-ideal={x_ideal:.4f}, tolerance={x_tolerance:.4f}", level="info")
|
||||||
self.log_gui.log(f" x范围: [{x_low:.4f}, {x_high:.4f}]")
|
self.log_gui.log(f" x范围: [{x_low:.4f}, {x_high:.4f}]", level="info")
|
||||||
self.log_gui.log(f" y-ideal={y_ideal:.4f}, tolerance={y_tolerance:.4f}")
|
self.log_gui.log(f" y-ideal={y_ideal:.4f}, tolerance={y_tolerance:.4f}", level="info")
|
||||||
self.log_gui.log(f" y范围: [{y_low:.4f}, {y_high:.4f}]")
|
self.log_gui.log(f" y范围: [{y_low:.4f}, {y_high:.4f}]", level="info")
|
||||||
|
|
||||||
# 为所有测试类型创建子图
|
# 为所有测试类型创建子图
|
||||||
ax1 = self.cct_fig.add_subplot(211)
|
ax1 = self.cct_fig.add_subplot(211)
|
||||||
@@ -180,7 +180,7 @@ def plot_cct(self, test_type):
|
|||||||
x_max_data = max(x_measured)
|
x_max_data = max(x_measured)
|
||||||
data_range_x = x_max_data - x_min_data
|
data_range_x = x_max_data - x_min_data
|
||||||
|
|
||||||
self.log_gui.log(f" x数据波动: {data_range_x:.6f}")
|
self.log_gui.log(f" x数据波动: {data_range_x:.6f}", level="info")
|
||||||
|
|
||||||
range_span = x_tolerance * 2
|
range_span = x_tolerance * 2
|
||||||
margin_ratio = 0.20
|
margin_ratio = 0.20
|
||||||
@@ -190,16 +190,16 @@ def plot_cct(self, test_type):
|
|||||||
final_y_max = max(x_max_data, x_high) + extra_margin
|
final_y_max = max(x_max_data, x_high) + extra_margin
|
||||||
|
|
||||||
if x_min_data >= x_low and x_max_data <= x_high:
|
if x_min_data >= x_low and x_max_data <= x_high:
|
||||||
self.log_gui.log(f" x数据在tolerance范围内,使用tolerance范围显示")
|
self.log_gui.log(f" x数据在tolerance范围内,使用tolerance范围显示", level="info")
|
||||||
final_y_min = x_low - extra_margin
|
final_y_min = x_low - extra_margin
|
||||||
final_y_max = x_high + extra_margin
|
final_y_max = x_high + extra_margin
|
||||||
else:
|
else:
|
||||||
self.log_gui.log(f" x数据超出tolerance范围,扩展显示范围")
|
self.log_gui.log(f" x数据超出tolerance范围,扩展显示范围", level="info")
|
||||||
|
|
||||||
ax1.set_ylim(final_y_min, final_y_max)
|
ax1.set_ylim(final_y_min, final_y_max)
|
||||||
self.log_gui.log(
|
self.log_gui.log(
|
||||||
f" x轴显示范围: {final_y_min:.6f} - {final_y_max:.6f} (跨度: {final_y_max - final_y_min:.6f})"
|
f" x轴显示范围: {final_y_min:.6f} - {final_y_max:.6f} (跨度: {final_y_max - final_y_min:.6f})"
|
||||||
)
|
, level="info")
|
||||||
|
|
||||||
# ========== 下图:y coordinates ==========
|
# ========== 下图:y coordinates ==========
|
||||||
ax2.plot(
|
ax2.plot(
|
||||||
@@ -273,7 +273,7 @@ def plot_cct(self, test_type):
|
|||||||
y_max_data = max(y_measured)
|
y_max_data = max(y_measured)
|
||||||
data_range_y = y_max_data - y_min_data
|
data_range_y = y_max_data - y_min_data
|
||||||
|
|
||||||
self.log_gui.log(f" y数据波动: {data_range_y:.6f}")
|
self.log_gui.log(f" y数据波动: {data_range_y:.6f}", level="info")
|
||||||
|
|
||||||
range_span = y_tolerance * 2
|
range_span = y_tolerance * 2
|
||||||
extra_margin = range_span * margin_ratio
|
extra_margin = range_span * margin_ratio
|
||||||
@@ -282,16 +282,16 @@ def plot_cct(self, test_type):
|
|||||||
final_y_max = max(y_max_data, y_high) + extra_margin
|
final_y_max = max(y_max_data, y_high) + extra_margin
|
||||||
|
|
||||||
if y_min_data >= y_low and y_max_data <= y_high:
|
if y_min_data >= y_low and y_max_data <= y_high:
|
||||||
self.log_gui.log(f" y数据在tolerance范围内,使用tolerance范围显示")
|
self.log_gui.log(f" y数据在tolerance范围内,使用tolerance范围显示", level="info")
|
||||||
final_y_min = y_low - extra_margin
|
final_y_min = y_low - extra_margin
|
||||||
final_y_max = y_high + extra_margin
|
final_y_max = y_high + extra_margin
|
||||||
else:
|
else:
|
||||||
self.log_gui.log(f" y数据超出tolerance范围,扩展显示范围")
|
self.log_gui.log(f" y数据超出tolerance范围,扩展显示范围", level="info")
|
||||||
|
|
||||||
ax2.set_ylim(final_y_min, final_y_max)
|
ax2.set_ylim(final_y_min, final_y_max)
|
||||||
self.log_gui.log(
|
self.log_gui.log(
|
||||||
f" y轴显示范围: {final_y_min:.6f} - {final_y_max:.6f} (跨度: {final_y_max - final_y_min:.6f})"
|
f" y轴显示范围: {final_y_min:.6f} - {final_y_max:.6f} (跨度: {final_y_max - final_y_min:.6f})"
|
||||||
)
|
, level="info")
|
||||||
|
|
||||||
# ========== 总标题 - 统一格式(去掉统计信息)==========
|
# ========== 总标题 - 统一格式(去掉统计信息)==========
|
||||||
test_type_name = self.get_test_type_name(test_type)
|
test_type_name = self.get_test_type_name(test_type)
|
||||||
@@ -321,4 +321,4 @@ def plot_cct(self, test_type):
|
|||||||
self.cct_canvas.draw()
|
self.cct_canvas.draw()
|
||||||
self.chart_notebook.select(self.cct_chart_frame)
|
self.chart_notebook.select(self.cct_chart_frame)
|
||||||
|
|
||||||
self.log_gui.log("✓ xy 色度坐标图绘制完成")
|
self.log_gui.log("xy 色度坐标图绘制完成", level="success")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""EOTF 曲线绘制(HDR)。
|
"""EOTF 曲线绘制(HDR)。
|
||||||
|
|
||||||
Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_eotf 原样搬迁。
|
Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_eotf 原样搬迁。
|
||||||
"""
|
"""
|
||||||
@@ -145,4 +145,4 @@ def plot_eotf(self, L_bar, results_with_eotf_list, test_type):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self.log_gui.log("EOTF 曲线 + 数据表格绘制完成")
|
self.log_gui.log("EOTF 曲线 + 数据表格绘制完成", level="success")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Gamma 曲线绘制。
|
"""Gamma 曲线绘制。
|
||||||
|
|
||||||
Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_gamma 原样搬迁。
|
Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_gamma 原样搬迁。
|
||||||
"""
|
"""
|
||||||
@@ -139,4 +139,4 @@ def plot_gamma(self, L_bar, results_with_gamma_list, target_gamma, test_type):
|
|||||||
self.gamma_canvas.draw()
|
self.gamma_canvas.draw()
|
||||||
self.chart_notebook.select(self.gamma_chart_frame)
|
self.chart_notebook.select(self.gamma_chart_frame)
|
||||||
|
|
||||||
self.log_gui.log("Gamma曲线 + 数据表格绘制完成")
|
self.log_gui.log("Gamma曲线 + 数据表格绘制完成", level="success")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""色域图(Gamut)绘制。
|
"""色域图(Gamut)绘制。
|
||||||
|
|
||||||
Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_gamut 整体搬迁,
|
Step 2 重构:从 pqAutomationApp.PQAutomationApp.plot_gamut 整体搬迁,
|
||||||
实现与原方法完全一致;原方法仅保留为一行转发。
|
实现与原方法完全一致;原方法仅保留为一行转发。
|
||||||
@@ -30,7 +30,7 @@ def plot_gamut(self, results, coverage, test_type):
|
|||||||
UV_PIXELS_PER_U = 615.7260
|
UV_PIXELS_PER_U = 615.7260
|
||||||
UV_PIXELS_PER_V = 599.8432
|
UV_PIXELS_PER_V = 599.8432
|
||||||
|
|
||||||
# ========== ✅ 读取用户选择的参考标准 ==========
|
# ========== 读取用户选择的参考标准 ==========
|
||||||
if test_type == "screen_module":
|
if test_type == "screen_module":
|
||||||
current_ref = self.screen_gamut_ref_var.get()
|
current_ref = self.screen_gamut_ref_var.get()
|
||||||
elif test_type == "sdr_movie":
|
elif test_type == "sdr_movie":
|
||||||
@@ -40,7 +40,7 @@ def plot_gamut(self, results, coverage, test_type):
|
|||||||
else:
|
else:
|
||||||
current_ref = "DCI-P3"
|
current_ref = "DCI-P3"
|
||||||
|
|
||||||
# ========== ✅✅✅ 根据参考标准重新计算覆盖率(XY 空间)==========
|
# ========== ✅✅根据参考标准重新计算覆盖率(XY 空间)==========
|
||||||
xy_coverage = coverage # 默认使用传入的值
|
xy_coverage = coverage # 默认使用传入的值
|
||||||
uv_coverage = 0.0
|
uv_coverage = 0.0
|
||||||
|
|
||||||
@@ -67,18 +67,18 @@ def plot_gamut(self, results, coverage, test_type):
|
|||||||
xy_points
|
xy_points
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.log_gui.log(f"⚠️ 未知参考标准 '{current_ref}',使用 DCI-P3")
|
self.log_gui.log(f"未知参考标准 '{current_ref}',使用 DCI-P3", level="error")
|
||||||
_, xy_coverage = pq_algorithm.calculate_gamut_coverage_DCIP3(
|
_, xy_coverage = pq_algorithm.calculate_gamut_coverage_DCIP3(
|
||||||
xy_points
|
xy_points
|
||||||
)
|
)
|
||||||
current_ref = "DCI-P3"
|
current_ref = "DCI-P3"
|
||||||
|
|
||||||
self.log_gui.log(
|
self.log_gui.log(
|
||||||
f"✓ XY 空间覆盖率({current_ref}): {xy_coverage:.1f}%"
|
f"XY 空间覆盖率({current_ref}): {xy_coverage:.1f}%"
|
||||||
)
|
, level="success")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_gui.log(f"⚠️ 重新计算 XY 覆盖率失败: {str(e)}")
|
self.log_gui.log(f"重新计算 XY 覆盖率失败: {str(e)}", level="error")
|
||||||
xy_coverage = coverage # 回退到传入值
|
xy_coverage = coverage # 回退到传入值
|
||||||
# =================================================
|
# =================================================
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ def plot_gamut(self, results, coverage, test_type):
|
|||||||
img_xy = mpimg.imread(get_resource_path("assets/cie.png"))
|
img_xy = mpimg.imread(get_resource_path("assets/cie.png"))
|
||||||
h_xy, w_xy = img_xy.shape[:2]
|
h_xy, w_xy = img_xy.shape[:2]
|
||||||
|
|
||||||
self.log_gui.log(f"加载 XY 色域图: {w_xy}x{h_xy}")
|
self.log_gui.log(f"加载 XY 色域图: {w_xy}x{h_xy}", level="info")
|
||||||
|
|
||||||
self.gamut_ax_xy.imshow(img_xy, extent=[0, w_xy, h_xy, 0], aspect="equal")
|
self.gamut_ax_xy.imshow(img_xy, extent=[0, w_xy, h_xy, 0], aspect="equal")
|
||||||
self.gamut_ax_xy.set_xlim(0, w_xy)
|
self.gamut_ax_xy.set_xlim(0, w_xy)
|
||||||
@@ -109,7 +109,7 @@ def plot_gamut(self, results, coverage, test_type):
|
|||||||
self.log_gui.log(
|
self.log_gui.log(
|
||||||
f"测量色域: R({red_x:.4f},{red_y:.4f}) "
|
f"测量色域: R({red_x:.4f},{red_y:.4f}) "
|
||||||
f"G({green_x:.4f},{green_y:.4f}) B({blue_x:.4f},{blue_y:.4f})"
|
f"G({green_x:.4f},{green_y:.4f}) B({blue_x:.4f},{blue_y:.4f})"
|
||||||
)
|
, level="info")
|
||||||
|
|
||||||
# ========== 绘制测量三角形 ==========
|
# ========== 绘制测量三角形 ==========
|
||||||
points = [
|
points = [
|
||||||
@@ -257,7 +257,7 @@ def plot_gamut(self, results, coverage, test_type):
|
|||||||
zorder=3,
|
zorder=3,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ========== ✅ XY 覆盖率标注(使用重新计算的值)==========
|
# ========== XY 覆盖率标注(使用重新计算的值)==========
|
||||||
self.gamut_ax_xy.text(
|
self.gamut_ax_xy.text(
|
||||||
w_xy * 0.85,
|
w_xy * 0.85,
|
||||||
h_xy * 0.92,
|
h_xy * 0.92,
|
||||||
@@ -287,17 +287,17 @@ def plot_gamut(self, results, coverage, test_type):
|
|||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_gui.log(f"XY 图绘制失败: {str(e)}")
|
self.log_gui.log(f"XY 图绘制失败: {str(e)}", level="error")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
self.log_gui.log(traceback.format_exc())
|
self.log_gui.log(traceback.format_exc(), level="error")
|
||||||
|
|
||||||
# ========== 右图:CIE 1976 u'v' ==========
|
# ========== 右图:CIE 1976 u'v' ==========
|
||||||
try:
|
try:
|
||||||
img_uv = mpimg.imread(get_resource_path("assets/cie_uv.png"))
|
img_uv = mpimg.imread(get_resource_path("assets/cie_uv.png"))
|
||||||
h_uv, w_uv = img_uv.shape[:2]
|
h_uv, w_uv = img_uv.shape[:2]
|
||||||
|
|
||||||
self.log_gui.log(f"加载 UV 色域图: {w_uv}x{h_uv}")
|
self.log_gui.log(f"加载 UV 色域图: {w_uv}x{h_uv}", level="info")
|
||||||
|
|
||||||
self.gamut_ax_uv.imshow(img_uv, extent=[0, w_uv, h_uv, 0], aspect="equal")
|
self.gamut_ax_uv.imshow(img_uv, extent=[0, w_uv, h_uv, 0], aspect="equal")
|
||||||
self.gamut_ax_uv.set_xlim(0, w_uv)
|
self.gamut_ax_uv.set_xlim(0, w_uv)
|
||||||
@@ -329,18 +329,18 @@ def plot_gamut(self, results, coverage, test_type):
|
|||||||
[u, v] for u, v in [xy_to_uv(r[0], r[1]) for r in rgb_results]
|
[u, v] for u, v in [xy_to_uv(r[0], r[1]) for r in rgb_results]
|
||||||
]
|
]
|
||||||
|
|
||||||
self.log_gui.log(f"UV 坐标: {uv_coords}")
|
self.log_gui.log(f"UV 坐标: {uv_coords}", level="info")
|
||||||
|
|
||||||
# ========== ✅✅✅ 计算 u'v' 覆盖率(使用参考标准)==========
|
# ========== ✅✅计算 u'v' 覆盖率(使用参考标准)==========
|
||||||
try:
|
try:
|
||||||
uv_coverage = pq_algorithm.calculate_uv_gamut_coverage(
|
uv_coverage = pq_algorithm.calculate_uv_gamut_coverage(
|
||||||
uv_coords, reference=current_ref
|
uv_coords, reference=current_ref
|
||||||
)
|
)
|
||||||
self.log_gui.log(
|
self.log_gui.log(
|
||||||
f"✓ UV 空间覆盖率({current_ref}): {uv_coverage:.1f}%"
|
f"UV 空间覆盖率({current_ref}): {uv_coverage:.1f}%"
|
||||||
)
|
, level="success")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_gui.log(f"⚠️ 计算 UV 覆盖率失败: {str(e)}")
|
self.log_gui.log(f"计算 UV 覆盖率失败: {str(e)}", level="error")
|
||||||
uv_coverage = 0.0
|
uv_coverage = 0.0
|
||||||
# =================================================
|
# =================================================
|
||||||
|
|
||||||
@@ -491,7 +491,7 @@ def plot_gamut(self, results, coverage, test_type):
|
|||||||
zorder=3,
|
zorder=3,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ========== ✅ UV 覆盖率标注(使用动态计算的值)==========
|
# ========== UV 覆盖率标注(使用动态计算的值)==========
|
||||||
self.gamut_ax_uv.text(
|
self.gamut_ax_uv.text(
|
||||||
w_uv * 0.85,
|
w_uv * 0.85,
|
||||||
h_uv * 0.92,
|
h_uv * 0.92,
|
||||||
@@ -521,10 +521,10 @@ def plot_gamut(self, results, coverage, test_type):
|
|||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_gui.log(f"UV 图绘制失败: {str(e)}")
|
self.log_gui.log(f"UV 图绘制失败: {str(e)}", level="error")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
self.log_gui.log(traceback.format_exc())
|
self.log_gui.log(traceback.format_exc(), level="error")
|
||||||
|
|
||||||
# ========== 总标题 ==========
|
# ========== 总标题 ==========
|
||||||
test_type_name = self.get_test_type_name(test_type)
|
test_type_name = self.get_test_type_name(test_type)
|
||||||
@@ -535,4 +535,4 @@ def plot_gamut(self, results, coverage, test_type):
|
|||||||
self.gamut_canvas.draw()
|
self.gamut_canvas.draw()
|
||||||
self.chart_notebook.select(self.gamut_chart_frame)
|
self.chart_notebook.select(self.gamut_chart_frame)
|
||||||
|
|
||||||
self.log_gui.log("色域图绘制完成")
|
self.log_gui.log("色域图绘制完成", level="success")
|
||||||
|
|||||||
@@ -375,13 +375,13 @@ class PQConfig:
|
|||||||
"default_pattern_gray", self.default_pattern_gray
|
"default_pattern_gray", self.default_pattern_gray
|
||||||
)
|
)
|
||||||
|
|
||||||
# ========== ✅ 强制使用新的 29色配置 ==========
|
# ========== 强制使用新的 29色配置 ==========
|
||||||
loaded_accuracy = config_dict.get("default_pattern_accuracy", None)
|
loaded_accuracy = config_dict.get("default_pattern_accuracy", None)
|
||||||
|
|
||||||
# 检查加载的配置是否是旧的 10色
|
# 检查加载的配置是否是旧的 10色
|
||||||
if loaded_accuracy and len(loaded_accuracy.get("pattern_params", [])) != 29:
|
if loaded_accuracy and len(loaded_accuracy.get("pattern_params", [])) != 29:
|
||||||
print(
|
print(
|
||||||
f"⚠️ 检测到旧的配置({len(loaded_accuracy.get('pattern_params', []))}色),强制使用新的 29色配置"
|
f"检测到旧的配置({len(loaded_accuracy.get('pattern_params', []))}色),强制使用新的 29色配置"
|
||||||
)
|
)
|
||||||
# 使用 __init__ 中定义的新配置
|
# 使用 __init__ 中定义的新配置
|
||||||
self.default_pattern_accuracy = self.default_pattern_accuracy
|
self.default_pattern_accuracy = self.default_pattern_accuracy
|
||||||
@@ -431,7 +431,7 @@ class PQConfig:
|
|||||||
self.current_pattern = self.default_pattern_rgb
|
self.current_pattern = self.default_pattern_rgb
|
||||||
elif mode == "gray":
|
elif mode == "gray":
|
||||||
self.current_pattern = self.default_pattern_gray
|
self.current_pattern = self.default_pattern_gray
|
||||||
elif mode == "accuracy": # ✅ 色准模式(SDR 和 HDR 通用 29色)
|
elif mode == "accuracy": # 色准模式(SDR 和 HDR 通用 29色)
|
||||||
self.current_pattern = self.default_pattern_accuracy
|
self.current_pattern = self.default_pattern_accuracy
|
||||||
elif mode == "custom":
|
elif mode == "custom":
|
||||||
# self.current_pattern = self.custom_pattern
|
# self.current_pattern = self.custom_pattern
|
||||||
@@ -459,7 +459,7 @@ class PQConfig:
|
|||||||
self.custom_pattern["measurement_max_value"] = len(pattern_params) - 1
|
self.custom_pattern["measurement_max_value"] = len(pattern_params) - 1
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# ========== ✅ 获取 29色名称列表 ==========
|
# ========== 获取 29色名称列表 ==========
|
||||||
def get_accuracy_color_names(self):
|
def get_accuracy_color_names(self):
|
||||||
"""
|
"""
|
||||||
获取色准测试的 29个颜色名称(SDR 和 HDR 通用)
|
获取色准测试的 29个颜色名称(SDR 和 HDR 通用)
|
||||||
@@ -502,7 +502,7 @@ class PQConfig:
|
|||||||
"100% Yellow",
|
"100% Yellow",
|
||||||
]
|
]
|
||||||
|
|
||||||
# ========== ✅ 获取 29色的 RGB 值 ==========
|
# ========== 获取 29色的 RGB 值 ==========
|
||||||
def get_accuracy_color_rgb(self):
|
def get_accuracy_color_rgb(self):
|
||||||
"""
|
"""
|
||||||
获取色准测试的 RGB 值(用于标准值计算)
|
获取色准测试的 RGB 值(用于标准值计算)
|
||||||
@@ -579,7 +579,7 @@ class PQConfig:
|
|||||||
return config_info
|
return config_info
|
||||||
|
|
||||||
|
|
||||||
# ========== ✅ 验证代码(测试完成后可删除)==========
|
# ========== 验证代码(测试完成后可删除)==========
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
print("验证 pq_config.py 配置")
|
print("验证 pq_config.py 配置")
|
||||||
@@ -596,7 +596,7 @@ if __name__ == "__main__":
|
|||||||
print(f" measurement_max_value: {max_value}")
|
print(f" measurement_max_value: {max_value}")
|
||||||
|
|
||||||
if pattern_count == 29 and max_value == 28:
|
if pattern_count == 29 and max_value == 28:
|
||||||
print("\n✅ 配置正确(29色)")
|
print("\n配置正确(29色)")
|
||||||
|
|
||||||
# 显示前 5 个图案
|
# 显示前 5 个图案
|
||||||
print("\n前5个图案:")
|
print("\n前5个图案:")
|
||||||
@@ -619,16 +619,16 @@ if __name__ == "__main__":
|
|||||||
print(f" current_pattern 图案数量: {current_count}")
|
print(f" current_pattern 图案数量: {current_count}")
|
||||||
|
|
||||||
if current_count == 29:
|
if current_count == 29:
|
||||||
print(" ✅ set_current_pattern 工作正常")
|
print(" set_current_pattern 工作正常")
|
||||||
else:
|
else:
|
||||||
print(f" ❌ set_current_pattern 失败!只有 {current_count} 个图案")
|
print(f" set_current_pattern 失败!只有 {current_count} 个图案")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print(f"\n❌ 配置错误!")
|
print(f"\n配置错误!")
|
||||||
print(f" 期望: 29 个图案, measurement_max_value=28")
|
print(f" 期望: 29 个图案, measurement_max_value=28")
|
||||||
print(f" 实际: {pattern_count} 个图案, measurement_max_value={max_value}")
|
print(f" 实际: {pattern_count} 个图案, measurement_max_value={max_value}")
|
||||||
|
|
||||||
print("\n❌ 请检查 default_pattern_accuracy 定义!")
|
print("\n请检查 default_pattern_accuracy 定义!")
|
||||||
print(" 应该包含:")
|
print(" 应该包含:")
|
||||||
print(" - 5个灰阶")
|
print(" - 5个灰阶")
|
||||||
print(" - 18个 ColorChecker 色块")
|
print(" - 18个 ColorChecker 色块")
|
||||||
|
|||||||
@@ -414,6 +414,122 @@ class PQResult:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PQResultStore:
|
||||||
|
"""按 test_type 管理多个 PQResult 实例。
|
||||||
|
|
||||||
|
解决两个问题:
|
||||||
|
1. `self.results` 在测试流程启动前就存在(避免 AttributeError / NoneType 访问)。
|
||||||
|
2. 支持同时保留多种 test_type(screen_module / sdr_movie / hdr_movie)的历史结果,
|
||||||
|
而不是每次启动新测试就把上一次的结果覆盖掉。
|
||||||
|
|
||||||
|
使用方式:
|
||||||
|
# 主类 __init__ 中:
|
||||||
|
self.results = PQResultStore()
|
||||||
|
|
||||||
|
# 启动一次新测试时:
|
||||||
|
self.results.new("screen_module", "屏模组性能测试")
|
||||||
|
|
||||||
|
# 现有调用点(隐式访问当前活跃结果)仍然生效:
|
||||||
|
self.results.add_intermediate_data("gamut", "rgb", data)
|
||||||
|
self.results.get_intermediate_data("shared", "gray")
|
||||||
|
|
||||||
|
# 跨类型显式访问某次历史结果:
|
||||||
|
pq = self.results.get("sdr_movie")
|
||||||
|
if pq is not None:
|
||||||
|
data = pq.get_intermediate_data("gamut", "rgb")
|
||||||
|
"""
|
||||||
|
|
||||||
|
# __getattr__ 代理时,这些方法若在无活跃结果时被调用,返回 None 而不是报错,
|
||||||
|
# 以兼容"尝试读取但数据还没来"的场景。
|
||||||
|
_SAFE_GETTERS = frozenset({
|
||||||
|
"get_intermediate_data",
|
||||||
|
"has_intermediate_data",
|
||||||
|
"get_all_intermediate_data",
|
||||||
|
"get_test_summary",
|
||||||
|
"get_progress_info",
|
||||||
|
"to_dict",
|
||||||
|
})
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._results: Dict[str, PQResult] = {}
|
||||||
|
self._active_type: Optional[str] = None
|
||||||
|
|
||||||
|
# ---------- 显式 API ----------
|
||||||
|
|
||||||
|
def new(self, test_type: str, test_name: str) -> PQResult:
|
||||||
|
"""为指定 test_type 创建新的 PQResult,并置为当前活跃。
|
||||||
|
|
||||||
|
若该 test_type 已有旧结果,会被替换。
|
||||||
|
"""
|
||||||
|
result = PQResult(test_type=test_type, test_name=test_name)
|
||||||
|
self._results[test_type] = result
|
||||||
|
self._active_type = test_type
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get(self, test_type: str) -> Optional[PQResult]:
|
||||||
|
"""按 test_type 取结果;不存在则返回 None。"""
|
||||||
|
return self._results.get(test_type)
|
||||||
|
|
||||||
|
def has(self, test_type: str) -> bool:
|
||||||
|
"""判断某个 test_type 是否有结果。"""
|
||||||
|
return test_type in self._results
|
||||||
|
|
||||||
|
def set_active(self, test_type: str) -> bool:
|
||||||
|
"""切换当前活跃结果。成功返回 True。"""
|
||||||
|
if test_type in self._results:
|
||||||
|
self._active_type = test_type
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def clear(self, test_type: Optional[str] = None) -> None:
|
||||||
|
"""清空结果。不传参数清全部,否则只清指定 test_type。"""
|
||||||
|
if test_type is None:
|
||||||
|
self._results.clear()
|
||||||
|
self._active_type = None
|
||||||
|
else:
|
||||||
|
self._results.pop(test_type, None)
|
||||||
|
if self._active_type == test_type:
|
||||||
|
self._active_type = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current(self) -> Optional[PQResult]:
|
||||||
|
"""当前活跃的 PQResult,无则返回 None。"""
|
||||||
|
if self._active_type is None:
|
||||||
|
return None
|
||||||
|
return self._results.get(self._active_type)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_test_type(self) -> Optional[str]:
|
||||||
|
return self._active_type
|
||||||
|
|
||||||
|
@property
|
||||||
|
def all_types(self) -> List[str]:
|
||||||
|
return list(self._results.keys())
|
||||||
|
|
||||||
|
# ---------- 兼容性:透明代理到 current ----------
|
||||||
|
|
||||||
|
def __bool__(self) -> bool:
|
||||||
|
"""`if self.results:` 仍可用于判断当前是否有活跃结果。"""
|
||||||
|
return self.current is not None
|
||||||
|
|
||||||
|
def __getattr__(self, name: str) -> Any:
|
||||||
|
# 只有常规属性查找失败时才会走到这里
|
||||||
|
if name.startswith("_"):
|
||||||
|
raise AttributeError(name)
|
||||||
|
current = self.current
|
||||||
|
if current is not None:
|
||||||
|
return getattr(current, name)
|
||||||
|
# 无活跃结果:读类方法返回空值 stub;写类方法抛出清晰错误
|
||||||
|
if name in self._SAFE_GETTERS:
|
||||||
|
def _null_getter(*_args, **_kwargs):
|
||||||
|
return None
|
||||||
|
return _null_getter
|
||||||
|
raise AttributeError(
|
||||||
|
f"PQResultStore 当前没有活跃的 PQResult(还未调用 new());"
|
||||||
|
f"无法访问 '{name}'。如需按 test_type 取历史结果,请用 .get(test_type)。"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# 使用示例和工具函数
|
# 使用示例和工具函数
|
||||||
def create_pq_result_from_config(config: Dict[str, Any]) -> PQResult:
|
def create_pq_result_from_config(config: Dict[str, Any]) -> PQResult:
|
||||||
"""根据配置创建PQResult实例"""
|
"""根据配置创建PQResult实例"""
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
0
app/services/__init__.py
Normal file
0
app/services/__init__.py
Normal file
363
app/services/ai_image.py
Normal file
363
app/services/ai_image.py
Normal file
@@ -0,0 +1,363 @@
|
|||||||
|
"""AI 图片生成服务:后端请求 + 本地缓存管理。
|
||||||
|
|
||||||
|
API 端点待接入,当前通过 ``set_api_caller`` 注入具体实现。
|
||||||
|
缓存目录:``settings/ai_image_cache/``,每张图片有同名的 ``.json`` 侧车记录。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime as _dt
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import mimetypes
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import threading
|
||||||
|
from dataclasses import dataclass, asdict
|
||||||
|
from typing import Callable, List, Optional
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- 常量 ----------
|
||||||
|
|
||||||
|
_CACHE_DIRNAME = os.path.join("settings", "ai_image_cache")
|
||||||
|
_META_SUFFIX = ".json"
|
||||||
|
_SUPPORTED_IMG_EXT = (".png", ".jpg", ".jpeg", ".bmp", ".webp")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- 数据结构 ----------
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AIImageRecord:
|
||||||
|
"""一条缓存记录。"""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
prompt: str
|
||||||
|
image_path: str
|
||||||
|
created_at: str # ISO8601
|
||||||
|
extra: Optional[dict] = None
|
||||||
|
|
||||||
|
def to_json(self) -> str:
|
||||||
|
return json.dumps(asdict(self), ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- API 注入 ----------
|
||||||
|
|
||||||
|
# 调用签名: ``fn(prompt: str) -> (image_bytes: bytes, image_ext: str, extra: dict|None)``
|
||||||
|
# ``image_ext`` 例如 ``".png"``;``extra`` 可为 None。
|
||||||
|
_ApiCaller = Callable[[str], tuple]
|
||||||
|
|
||||||
|
_api_caller: Optional[_ApiCaller] = None
|
||||||
|
|
||||||
|
|
||||||
|
def set_api_caller(fn: Optional[_ApiCaller]) -> None:
|
||||||
|
"""注入真实的后端 API 调用函数。在 API 就绪前可保持为 None。"""
|
||||||
|
global _api_caller
|
||||||
|
_api_caller = fn
|
||||||
|
|
||||||
|
|
||||||
|
def has_api() -> bool:
|
||||||
|
return _api_caller is not None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- 缓存路径工具 ----------
|
||||||
|
|
||||||
|
|
||||||
|
def get_cache_dir(base_dir: Optional[str] = None) -> str:
|
||||||
|
"""返回缓存目录,如不存在则创建。``base_dir`` 默认使用当前工作目录。"""
|
||||||
|
root = base_dir if base_dir else os.getcwd()
|
||||||
|
path = os.path.join(root, _CACHE_DIRNAME)
|
||||||
|
os.makedirs(path, exist_ok=True)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def _make_id(prompt: str) -> str:
|
||||||
|
stamp = _dt.datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
||||||
|
digest = hashlib.md5(prompt.encode("utf-8")).hexdigest()[:8]
|
||||||
|
return f"{stamp}_{digest}"
|
||||||
|
|
||||||
|
|
||||||
|
def _meta_path_for(image_path: str) -> str:
|
||||||
|
return os.path.splitext(image_path)[0] + _META_SUFFIX
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- 读写 ----------
|
||||||
|
|
||||||
|
|
||||||
|
def list_records(base_dir: Optional[str] = None) -> List[AIImageRecord]:
|
||||||
|
"""列出缓存目录下的所有记录,按创建时间倒序(最新在前)。"""
|
||||||
|
cache_dir = get_cache_dir(base_dir)
|
||||||
|
records: List[AIImageRecord] = []
|
||||||
|
for name in os.listdir(cache_dir):
|
||||||
|
full = os.path.join(cache_dir, name)
|
||||||
|
if not (os.path.isfile(full) and name.lower().endswith(_SUPPORTED_IMG_EXT)):
|
||||||
|
continue
|
||||||
|
meta_path = _meta_path_for(full)
|
||||||
|
prompt = ""
|
||||||
|
created_at = ""
|
||||||
|
extra = None
|
||||||
|
rec_id = os.path.splitext(name)[0]
|
||||||
|
if os.path.isfile(meta_path):
|
||||||
|
try:
|
||||||
|
with open(meta_path, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
prompt = data.get("prompt", "")
|
||||||
|
created_at = data.get("created_at", "")
|
||||||
|
extra = data.get("extra")
|
||||||
|
rec_id = data.get("id", rec_id)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if not created_at:
|
||||||
|
# fallback 到文件 mtime
|
||||||
|
try:
|
||||||
|
mtime = os.path.getmtime(full)
|
||||||
|
created_at = _dt.datetime.fromtimestamp(mtime).isoformat()
|
||||||
|
except Exception:
|
||||||
|
created_at = ""
|
||||||
|
records.append(
|
||||||
|
AIImageRecord(
|
||||||
|
id=rec_id,
|
||||||
|
prompt=prompt,
|
||||||
|
image_path=full,
|
||||||
|
created_at=created_at,
|
||||||
|
extra=extra,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not records:
|
||||||
|
seeded = _seed_placeholder_record(cache_dir)
|
||||||
|
if seeded is not None:
|
||||||
|
records.append(seeded)
|
||||||
|
records.sort(key=lambda r: r.created_at, reverse=True)
|
||||||
|
return records
|
||||||
|
|
||||||
|
|
||||||
|
def _seed_placeholder_record(cache_dir: str) -> Optional[AIImageRecord]:
|
||||||
|
"""当缓存为空时,写入一张本地占位图,便于前端联调。"""
|
||||||
|
try:
|
||||||
|
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||||
|
src = os.path.join(repo_root, "assets", "entry_1.png")
|
||||||
|
if not os.path.isfile(src):
|
||||||
|
return None
|
||||||
|
|
||||||
|
rec_id = f"{_dt.datetime.now().strftime('%Y%m%d_%H%M%S')}_placeholder"
|
||||||
|
image_path = os.path.join(cache_dir, f"{rec_id}.png")
|
||||||
|
shutil.copyfile(src, image_path)
|
||||||
|
|
||||||
|
record = AIImageRecord(
|
||||||
|
id=rec_id,
|
||||||
|
prompt="本地测试占位图(后端未接入)",
|
||||||
|
image_path=image_path,
|
||||||
|
created_at=_dt.datetime.now().isoformat(timespec="seconds"),
|
||||||
|
extra={"source": "local-placeholder"},
|
||||||
|
)
|
||||||
|
with open(_meta_path_for(image_path), "w", encoding="utf-8") as f:
|
||||||
|
f.write(record.to_json())
|
||||||
|
return record
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def save_image_to_cache(
|
||||||
|
prompt: str,
|
||||||
|
image_bytes: bytes,
|
||||||
|
image_ext: str = ".png",
|
||||||
|
extra: Optional[dict] = None,
|
||||||
|
base_dir: Optional[str] = None,
|
||||||
|
) -> AIImageRecord:
|
||||||
|
"""把生成的图片字节写入缓存,返回记录。"""
|
||||||
|
if not image_ext.startswith("."):
|
||||||
|
image_ext = "." + image_ext
|
||||||
|
if image_ext.lower() not in _SUPPORTED_IMG_EXT:
|
||||||
|
image_ext = ".png"
|
||||||
|
cache_dir = get_cache_dir(base_dir)
|
||||||
|
rec_id = _make_id(prompt)
|
||||||
|
image_path = os.path.join(cache_dir, f"{rec_id}{image_ext}")
|
||||||
|
with open(image_path, "wb") as f:
|
||||||
|
f.write(image_bytes)
|
||||||
|
|
||||||
|
record = AIImageRecord(
|
||||||
|
id=rec_id,
|
||||||
|
prompt=prompt,
|
||||||
|
image_path=image_path,
|
||||||
|
created_at=_dt.datetime.now().isoformat(timespec="seconds"),
|
||||||
|
extra=extra,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with open(_meta_path_for(image_path), "w", encoding="utf-8") as f:
|
||||||
|
f.write(record.to_json())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return record
|
||||||
|
|
||||||
|
|
||||||
|
def import_image_from_url(
|
||||||
|
image_url: str,
|
||||||
|
prompt: Optional[str] = None,
|
||||||
|
extra: Optional[dict] = None,
|
||||||
|
base_dir: Optional[str] = None,
|
||||||
|
timeout: float = 20.0,
|
||||||
|
) -> AIImageRecord:
|
||||||
|
"""下载远程图片并写入缓存。"""
|
||||||
|
url = (image_url or "").strip()
|
||||||
|
if not url:
|
||||||
|
raise ValueError("图片地址不能为空")
|
||||||
|
|
||||||
|
request = Request(
|
||||||
|
url,
|
||||||
|
headers={
|
||||||
|
"User-Agent": "pqAutomationApp/1.0",
|
||||||
|
"Accept": "image/*,*/*;q=0.8",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
with urlopen(request, timeout=timeout) as response:
|
||||||
|
image_bytes = response.read()
|
||||||
|
if not image_bytes:
|
||||||
|
raise ValueError("下载结果为空")
|
||||||
|
|
||||||
|
image_ext = _guess_image_ext(
|
||||||
|
image_url=url,
|
||||||
|
content_type=response.headers.get_content_type(),
|
||||||
|
)
|
||||||
|
merged_extra = dict(extra or {})
|
||||||
|
merged_extra.update(
|
||||||
|
{
|
||||||
|
"source": "remote-url",
|
||||||
|
"source_url": url,
|
||||||
|
"content_type": response.headers.get_content_type(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
record_prompt = (prompt or _default_prompt_from_url(url)).strip()
|
||||||
|
return save_image_to_cache(
|
||||||
|
prompt=record_prompt,
|
||||||
|
image_bytes=image_bytes,
|
||||||
|
image_ext=image_ext,
|
||||||
|
extra=merged_extra,
|
||||||
|
base_dir=base_dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_record(record: AIImageRecord) -> bool:
|
||||||
|
"""删除一条缓存记录(图片 + 侧车)。返回是否成功。"""
|
||||||
|
ok = True
|
||||||
|
for p in (record.image_path, _meta_path_for(record.image_path)):
|
||||||
|
try:
|
||||||
|
if os.path.isfile(p):
|
||||||
|
os.remove(p)
|
||||||
|
except Exception:
|
||||||
|
ok = False
|
||||||
|
return ok
|
||||||
|
|
||||||
|
|
||||||
|
def export_record(record: AIImageRecord, dest_path: str) -> None:
|
||||||
|
"""把缓存中的图片另存到 ``dest_path``。"""
|
||||||
|
shutil.copyfile(record.image_path, dest_path)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- 异步请求 ----------
|
||||||
|
|
||||||
|
|
||||||
|
def request_image_async(
|
||||||
|
prompt: str,
|
||||||
|
on_success: Callable[[AIImageRecord], None],
|
||||||
|
on_error: Callable[[Exception], None],
|
||||||
|
base_dir: Optional[str] = None,
|
||||||
|
) -> threading.Thread:
|
||||||
|
"""在后台线程请求 API → 写入缓存 → 回调。
|
||||||
|
|
||||||
|
``on_success`` / ``on_error`` 会在 **工作线程** 中被调用;UI 侧若需
|
||||||
|
切回主线程,请在回调内部自行用 ``root.after(0, ...)``。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _worker():
|
||||||
|
try:
|
||||||
|
if _api_caller is None:
|
||||||
|
raise RuntimeError("AI 图片 API 尚未接入,请调用 set_api_caller 注入")
|
||||||
|
image_bytes, image_ext, extra = _normalize_api_result(_api_caller(prompt))
|
||||||
|
record = save_image_to_cache(
|
||||||
|
prompt=prompt,
|
||||||
|
image_bytes=image_bytes,
|
||||||
|
image_ext=image_ext,
|
||||||
|
extra=extra,
|
||||||
|
base_dir=base_dir,
|
||||||
|
)
|
||||||
|
on_success(record)
|
||||||
|
except Exception as exc:
|
||||||
|
on_error(exc)
|
||||||
|
|
||||||
|
t = threading.Thread(target=_worker, daemon=True)
|
||||||
|
t.start()
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
def import_image_from_url_async(
|
||||||
|
image_url: str,
|
||||||
|
on_success: Callable[[AIImageRecord], None],
|
||||||
|
on_error: Callable[[Exception], None],
|
||||||
|
prompt: Optional[str] = None,
|
||||||
|
extra: Optional[dict] = None,
|
||||||
|
base_dir: Optional[str] = None,
|
||||||
|
timeout: float = 20.0,
|
||||||
|
) -> threading.Thread:
|
||||||
|
"""在后台线程下载远程图片并写入缓存"""
|
||||||
|
|
||||||
|
def _worker():
|
||||||
|
try:
|
||||||
|
record = import_image_from_url(
|
||||||
|
image_url=image_url,
|
||||||
|
prompt=prompt,
|
||||||
|
extra=extra,
|
||||||
|
base_dir=base_dir,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
on_success(record)
|
||||||
|
except Exception as exc:
|
||||||
|
on_error(exc)
|
||||||
|
|
||||||
|
t = threading.Thread(target=_worker, daemon=True)
|
||||||
|
t.start()
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_api_result(result):
|
||||||
|
"""允许 API 返回 ``bytes`` 或 ``(bytes, ext)`` 或 ``(bytes, ext, extra)``。"""
|
||||||
|
if isinstance(result, (bytes, bytearray)):
|
||||||
|
return bytes(result), ".png", None
|
||||||
|
if isinstance(result, tuple):
|
||||||
|
if len(result) == 2:
|
||||||
|
return bytes(result[0]), str(result[1]), None
|
||||||
|
if len(result) == 3:
|
||||||
|
return bytes(result[0]), str(result[1]), result[2]
|
||||||
|
raise ValueError("API 返回格式不支持,需为 bytes 或 (bytes, ext[, extra])")
|
||||||
|
|
||||||
|
|
||||||
|
def is_remote_image_url(value: str) -> bool:
|
||||||
|
"""判断输入是否为 http/https 图片地址。"""
|
||||||
|
url = (value or "").strip()
|
||||||
|
if not url:
|
||||||
|
return False
|
||||||
|
parsed = urlparse(url)
|
||||||
|
return parsed.scheme in {"http", "https"} and bool(parsed.netloc)
|
||||||
|
|
||||||
|
|
||||||
|
def _guess_image_ext(image_url: str, content_type: Optional[str]) -> str:
|
||||||
|
if content_type:
|
||||||
|
guessed = mimetypes.guess_extension(content_type)
|
||||||
|
if guessed == ".jpe":
|
||||||
|
guessed = ".jpg"
|
||||||
|
if guessed and guessed.lower() in _SUPPORTED_IMG_EXT:
|
||||||
|
return guessed.lower()
|
||||||
|
|
||||||
|
url_path = urlparse(image_url).path
|
||||||
|
ext = os.path.splitext(url_path)[1].lower()
|
||||||
|
if ext in _SUPPORTED_IMG_EXT:
|
||||||
|
return ext
|
||||||
|
return ".png"
|
||||||
|
|
||||||
|
|
||||||
|
def _default_prompt_from_url(image_url: str) -> str:
|
||||||
|
path = urlparse(image_url).path
|
||||||
|
name = os.path.splitext(os.path.basename(path))[0].strip()
|
||||||
|
return name or "远程导入图片"
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Local Dimming 测试逻辑(应用层)。
|
"""Local Dimming 测试逻辑(应用层)。
|
||||||
|
|
||||||
整合自原 drivers/local_dimming_test.py:窗口图片生成与测试主循环
|
整合自原 drivers/local_dimming_test.py:窗口图片生成与测试主循环
|
||||||
直接落在本模块,UCD 通用操作下沉到 drivers.ucd_helpers。
|
直接落在本模块,UCD 通用操作下沉到 drivers.ucd_helpers。
|
||||||
@@ -107,51 +107,51 @@ def start_local_dimming_test(self):
|
|||||||
|
|
||||||
def worker():
|
def worker():
|
||||||
log = self.log_gui.log
|
log = self.log_gui.log
|
||||||
log("=" * 60)
|
log("=" * 60, level="separator")
|
||||||
log("开始 Local Dimming 测试")
|
log("开始 Local Dimming 测试", level="info")
|
||||||
log("=" * 60)
|
log("=" * 60, level="separator")
|
||||||
|
|
||||||
width, height = get_current_resolution(self.ucd)
|
width, height = get_current_resolution(self.ucd)
|
||||||
total = len(DEFAULT_WINDOW_PERCENTAGES)
|
total = len(DEFAULT_WINDOW_PERCENTAGES)
|
||||||
log(f" 分辨率: {width}x{height}")
|
log(f" 分辨率: {width}x{height}", level="info")
|
||||||
log(f" 测试窗口: {DEFAULT_WINDOW_PERCENTAGES}")
|
log(f" 测试窗口: {DEFAULT_WINDOW_PERCENTAGES}", level="info")
|
||||||
log(f" 等待时间: {wait_time} 秒")
|
log(f" 等待时间: {wait_time} 秒", level="info")
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
for i, percentage in enumerate(DEFAULT_WINDOW_PERCENTAGES, 1):
|
for i, percentage in enumerate(DEFAULT_WINDOW_PERCENTAGES, 1):
|
||||||
if stop_event.is_set():
|
if stop_event.is_set():
|
||||||
log("⚠️ 测试已停止")
|
log("测试已停止", level="error")
|
||||||
break
|
break
|
||||||
|
|
||||||
log(f"[{i}/{total}] 测试 {percentage}% 窗口...")
|
log(f"[{i}/{total}] 测试 {percentage}% 窗口...", level="info")
|
||||||
try:
|
try:
|
||||||
image_path = _ensure_window_image(width, height, percentage)
|
image_path = _ensure_window_image(width, height, percentage)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log(f" ❌ 图像生成失败: {e}")
|
log(f" 图像生成失败: {e}", level="error")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not send_image_pattern(self.ucd, image_path):
|
if not send_image_pattern(self.ucd, image_path):
|
||||||
log(f" ❌ {percentage}% 窗口发送失败,跳过")
|
log(f" {percentage}% 窗口发送失败,跳过", level="error")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
log(f" ⏳ 等待 {wait_time} 秒...")
|
log(f"等待 {wait_time} 秒...", level="info")
|
||||||
time.sleep(wait_time)
|
time.sleep(wait_time)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay()
|
x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log(f" ❌ 采集亮度异常: {e}")
|
log(f" 采集亮度异常: {e}", level="error")
|
||||||
continue
|
continue
|
||||||
if lv is None:
|
if lv is None:
|
||||||
log(f" ❌ {percentage}% 窗口采集失败")
|
log(f" {percentage}% 窗口采集失败", level="error")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
log(f" ✓ 采集亮度: {lv:.2f} cd/m²")
|
log(f"采集亮度: {lv:.2f} cd/m²", level="info")
|
||||||
results.append((percentage, x, y, lv, _X, _Y, _Z))
|
results.append((percentage, x, y, lv, _X, _Y, _Z))
|
||||||
|
|
||||||
log("=" * 60)
|
log("=" * 60, level="separator")
|
||||||
log(f"✅ Local Dimming 测试完成 ({len(results)}/{total})")
|
log(f"Local Dimming 测试完成 ({len(results)}/{total})", level="success")
|
||||||
log("=" * 60)
|
log("=" * 60, level="separator")
|
||||||
|
|
||||||
self.ld_test_results = results
|
self.ld_test_results = results
|
||||||
self._dispatch_ui(self.update_ld_results, results)
|
self._dispatch_ui(self.update_ld_results, results)
|
||||||
@@ -184,7 +184,7 @@ def send_ld_window(self, percentage):
|
|||||||
messagebox.showwarning("警告", "请先连接 UCD323 设备")
|
messagebox.showwarning("警告", "请先连接 UCD323 设备")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.log_gui.log(f"🔆 发送 {percentage}% 窗口...")
|
self.log_gui.log(f"🔆 发送 {percentage}% 窗口...", level="info")
|
||||||
self.current_ld_percentage = percentage
|
self.current_ld_percentage = percentage
|
||||||
|
|
||||||
def send():
|
def send():
|
||||||
@@ -192,12 +192,12 @@ def send_ld_window(self, percentage):
|
|||||||
try:
|
try:
|
||||||
image_path = _ensure_window_image(width, height, percentage)
|
image_path = _ensure_window_image(width, height, percentage)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._dispatch_ui(self.log_gui.log, f"❌ 图像生成失败: {e}")
|
self._dispatch_ui(self.log_gui.log, f"图像生成失败: {e}")
|
||||||
return
|
return
|
||||||
ok = send_image_pattern(self.ucd, image_path)
|
ok = send_image_pattern(self.ucd, image_path)
|
||||||
msg = (
|
msg = (
|
||||||
f"✅ {percentage}% 窗口已发送" if ok
|
f"{percentage}% 窗口已发送" if ok
|
||||||
else f"❌ {percentage}% 窗口发送失败"
|
else f"{percentage}% 窗口发送失败"
|
||||||
)
|
)
|
||||||
self._dispatch_ui(self.log_gui.log, msg)
|
self._dispatch_ui(self.log_gui.log, msg)
|
||||||
|
|
||||||
@@ -213,16 +213,16 @@ def measure_ld_luminance(self):
|
|||||||
messagebox.showinfo("提示", "请先发送一个窗口图案")
|
messagebox.showinfo("提示", "请先发送一个窗口图案")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.log_gui.log("📏 正在采集亮度...")
|
self.log_gui.log("📏 正在采集亮度...", level="info")
|
||||||
|
|
||||||
def measure():
|
def measure():
|
||||||
try:
|
try:
|
||||||
x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay()
|
x, y, lv, _X, _Y, _Z = self.ca.readAllDisplay()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._dispatch_ui(self.log_gui.log, f"❌ 采集异常: {str(e)}")
|
self._dispatch_ui(self.log_gui.log, f"采集异常: {str(e)}")
|
||||||
return
|
return
|
||||||
if lv is None:
|
if lv is None:
|
||||||
self._dispatch_ui(self.log_gui.log, "❌ 采集失败")
|
self._dispatch_ui(self.log_gui.log, "采集失败")
|
||||||
return
|
return
|
||||||
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
|
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
|
||||||
self._dispatch_ui(
|
self._dispatch_ui(
|
||||||
@@ -236,7 +236,7 @@ def measure_ld_luminance(self):
|
|||||||
f"{lv:.2f}", f"{x:.4f}", f"{y:.4f}", timestamp,
|
f"{lv:.2f}", f"{x:.4f}", f"{y:.4f}", timestamp,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self._dispatch_ui(self.log_gui.log, f"✅ 采集完成: {lv:.2f} cd/m²")
|
self._dispatch_ui(self.log_gui.log, f"采集完成: {lv:.2f} cd/m²")
|
||||||
|
|
||||||
threading.Thread(target=measure, daemon=True).start()
|
threading.Thread(target=measure, daemon=True).start()
|
||||||
|
|
||||||
@@ -247,7 +247,7 @@ def clear_ld_records(self):
|
|||||||
self.ld_tree.delete(item)
|
self.ld_tree.delete(item)
|
||||||
self.ld_result_label.config(text="亮度: -- cd/m² | x: -- | y: --")
|
self.ld_result_label.config(text="亮度: -- cd/m² | x: -- | y: --")
|
||||||
self.current_ld_percentage = None
|
self.current_ld_percentage = None
|
||||||
self.log_gui.log("🗑️ 测试记录已清空")
|
self.log_gui.log("测试记录已清空", level="info")
|
||||||
|
|
||||||
|
|
||||||
def save_local_dimming_results(self):
|
def save_local_dimming_results(self):
|
||||||
@@ -274,8 +274,8 @@ def save_local_dimming_results(self):
|
|||||||
writer.writerow(["窗口百分比", "亮度 (cd/m²)", "x", "y", "时间"])
|
writer.writerow(["窗口百分比", "亮度 (cd/m²)", "x", "y", "时间"])
|
||||||
for item in self.ld_tree.get_children():
|
for item in self.ld_tree.get_children():
|
||||||
writer.writerow(self.ld_tree.item(item, "values"))
|
writer.writerow(self.ld_tree.item(item, "values"))
|
||||||
self.log_gui.log(f"✓ 测试结果已保存: {save_path}")
|
self.log_gui.log(f"测试结果已保存: {save_path}", level="success")
|
||||||
messagebox.showinfo("成功", f"测试结果已保存到:\n{save_path}")
|
messagebox.showinfo("成功", f"测试结果已保存到:\n{save_path}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_gui.log(f"❌ 保存失败: {str(e)}")
|
self.log_gui.log(f"保存失败: {str(e)}", level="error")
|
||||||
messagebox.showerror("错误", f"保存失败: {str(e)}")
|
messagebox.showerror("错误", f"保存失败: {str(e)}")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""图表框架相关逻辑(Step 3 重构)。
|
"""图表框架相关逻辑(Step 3 重构)。
|
||||||
|
|
||||||
从 pqAutomationApp.PQAutomationApp 中搬迁而来。每个函数第一行 `self = app`
|
从 pqAutomationApp.PQAutomationApp 中搬迁而来。每个函数第一行 `self = app`
|
||||||
以保留原有 `self.xxx` 属性访问不变。
|
以保留原有 `self.xxx` 属性访问不变。
|
||||||
@@ -21,7 +21,7 @@ def init_gamut_chart(self):
|
|||||||
canvas_widget = self.gamut_canvas.get_tk_widget()
|
canvas_widget = self.gamut_canvas.get_tk_widget()
|
||||||
canvas_widget.pack(expand=True, fill=tk.BOTH)
|
canvas_widget.pack(expand=True, fill=tk.BOTH)
|
||||||
|
|
||||||
# ✅ 恢复原来的大尺寸:0.84 高度
|
# 恢复原来的大尺寸:0.84 高度
|
||||||
self.gamut_ax_xy = self.gamut_fig.add_axes(
|
self.gamut_ax_xy = self.gamut_fig.add_axes(
|
||||||
[0.02, 0.08, 0.46, 0.84]
|
[0.02, 0.08, 0.46, 0.84]
|
||||||
) # ← 改回 0.84
|
) # ← 改回 0.84
|
||||||
@@ -47,7 +47,7 @@ def init_gamut_chart(self):
|
|||||||
self.gamut_canvas.draw()
|
self.gamut_canvas.draw()
|
||||||
|
|
||||||
def init_gamma_chart(self):
|
def init_gamma_chart(self):
|
||||||
"""初始化Gamma曲线图表 - 左侧曲线 + 右侧表格(✅ 4列 + 通用说明)"""
|
"""初始化Gamma曲线图表 - 左侧曲线 + 右侧表格(4列 + 通用说明)"""
|
||||||
container = ttk.Frame(self.gamma_chart_frame)
|
container = ttk.Frame(self.gamma_chart_frame)
|
||||||
container.pack(expand=True, fill=tk.BOTH)
|
container.pack(expand=True, fill=tk.BOTH)
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@ def init_gamma_chart(self):
|
|||||||
self.gamma_canvas.draw()
|
self.gamma_canvas.draw()
|
||||||
|
|
||||||
def init_eotf_chart(self):
|
def init_eotf_chart(self):
|
||||||
"""初始化 EOTF 曲线图表(HDR 专用)- 左侧曲线 + 右侧表格(✅ 4列)"""
|
"""初始化 EOTF 曲线图表(HDR 专用)- 左侧曲线 + 右侧表格(4列)"""
|
||||||
container = ttk.Frame(self.eotf_chart_frame)
|
container = ttk.Frame(self.eotf_chart_frame)
|
||||||
container.pack(expand=True, fill=tk.BOTH)
|
container.pack(expand=True, fill=tk.BOTH)
|
||||||
|
|
||||||
@@ -741,7 +741,7 @@ def update_chart_tabs_state(self):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if hasattr(self, "log_gui"):
|
if hasattr(self, "log_gui"):
|
||||||
self.log_gui.log(f"更新Tab状态失败: {str(e)}")
|
self.log_gui.log(f"更新Tab状态失败: {str(e)}", level="error")
|
||||||
|
|
||||||
def create_result_chart_frame(self):
|
def create_result_chart_frame(self):
|
||||||
"""创建结果图表区域 - 6个独立Tab(Gamma 和 EOTF 分离)"""
|
"""创建结果图表区域 - 6个独立Tab(Gamma 和 EOTF 分离)"""
|
||||||
@@ -790,10 +790,10 @@ def create_result_chart_frame(self):
|
|||||||
# 绑定Tab切换事件
|
# 绑定Tab切换事件
|
||||||
self.chart_notebook.bind("<<NotebookTabChanged>>", self.on_chart_tab_changed)
|
self.chart_notebook.bind("<<NotebookTabChanged>>", self.on_chart_tab_changed)
|
||||||
|
|
||||||
# ==================== ✅ 在图表下方创建单步调试面板 ====================
|
# ==================== 在图表下方创建单步调试面板 ====================
|
||||||
self.debug_container = ttk.LabelFrame(
|
self.debug_container = ttk.LabelFrame(
|
||||||
self.result_frame, # ← 放在 result_frame 内,图表正下方
|
self.result_frame, # ← 放在 result_frame 内,图表正下方
|
||||||
text="🔧 单步调试",
|
text=" 单步调试",
|
||||||
padding=10,
|
padding=10,
|
||||||
)
|
)
|
||||||
# 默认不显示
|
# 默认不显示
|
||||||
@@ -801,8 +801,6 @@ def create_result_chart_frame(self):
|
|||||||
# 创建单步调试面板实例
|
# 创建单步调试面板实例
|
||||||
self.debug_panel = PQDebugPanel(self.debug_container, self)
|
self.debug_panel = PQDebugPanel(self.debug_container, self)
|
||||||
|
|
||||||
self.log_gui.log("✓ 单步调试面板已创建(放在测试结果图表下方)")
|
|
||||||
|
|
||||||
def on_chart_tab_changed(self, event):
|
def on_chart_tab_changed(self, event):
|
||||||
"""Tab切换时的事件处理"""
|
"""Tab切换时的事件处理"""
|
||||||
try:
|
try:
|
||||||
@@ -810,5 +808,5 @@ def on_chart_tab_changed(self, event):
|
|||||||
self.chart_notebook.select()
|
self.chart_notebook.select()
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_gui.log(f"Tab切换事件处理失败: {str(e)}")
|
self.log_gui.log(f"Tab切换事件处理失败: {str(e)}", level="error")
|
||||||
|
|
||||||
|
|||||||
561
app/views/panels/ai_image_panel.py
Normal file
561
app/views/panels/ai_image_panel.py
Normal file
@@ -0,0 +1,561 @@
|
|||||||
|
"""AI 图片对话面板:输入提示 → 生成 → 显示 → 列表切换/保存/删除。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import filedialog, messagebox, simpledialog
|
||||||
|
|
||||||
|
|
||||||
|
import ttkbootstrap as ttk
|
||||||
|
from PIL import Image, ImageTk
|
||||||
|
|
||||||
|
from app.services import ai_image as _svc
|
||||||
|
from drivers.ucd_helpers import get_current_resolution, send_image_pattern
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- 面板创建 ----------------
|
||||||
|
|
||||||
|
|
||||||
|
def create_ai_image_panel(self):
|
||||||
|
"""创建 AI 图片对话面板,并注册到面板管理。"""
|
||||||
|
frame = ttk.Frame(self.content_frame)
|
||||||
|
self.ai_image_frame = frame
|
||||||
|
|
||||||
|
# 内部状态
|
||||||
|
self.ai_image_records = [] # list[AIImageRecord]
|
||||||
|
self.ai_image_current = None # AIImageRecord | None
|
||||||
|
self.ai_image_photo = None # 防止 ImageTk.PhotoImage 被 GC
|
||||||
|
self._ai_image_requesting = False
|
||||||
|
|
||||||
|
container = ttk.Frame(frame, padding=10)
|
||||||
|
container.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# 左列:图片列表
|
||||||
|
# 使用 grid + 权重,让右侧预览区优先占据剩余空间。
|
||||||
|
container.columnconfigure(0, weight=0)
|
||||||
|
container.columnconfigure(1, weight=1)
|
||||||
|
container.rowconfigure(0, weight=1)
|
||||||
|
|
||||||
|
left = ttk.Frame(container, width=360)
|
||||||
|
left.grid(row=0, column=0, sticky=tk.NS, padx=(0, 10))
|
||||||
|
left.grid_propagate(False)
|
||||||
|
|
||||||
|
ttk.Label(left, text="历史图片", font=("微软雅黑", 10, "bold")).pack(
|
||||||
|
anchor=tk.W, pady=(0, 4)
|
||||||
|
)
|
||||||
|
|
||||||
|
list_wrap = ttk.Frame(left, padding=2)
|
||||||
|
list_wrap.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
scroll = ttk.Scrollbar(list_wrap, orient=tk.VERTICAL)
|
||||||
|
self.ai_image_listbox = tk.Listbox(
|
||||||
|
list_wrap,
|
||||||
|
width=34,
|
||||||
|
height=1, # 由 pack fill/expand 撑满,height 仅为最小保底
|
||||||
|
activestyle="none",
|
||||||
|
font=("微软雅黑", 9),
|
||||||
|
bd=1,
|
||||||
|
relief=tk.FLAT,
|
||||||
|
highlightthickness=1,
|
||||||
|
highlightbackground="#d8d8d8",
|
||||||
|
highlightcolor="#4a90e2",
|
||||||
|
selectbackground="#2b6cb0",
|
||||||
|
selectforeground="#ffffff",
|
||||||
|
yscrollcommand=scroll.set,
|
||||||
|
)
|
||||||
|
scroll.config(command=self.ai_image_listbox.yview)
|
||||||
|
self.ai_image_listbox.pack(side=tk.LEFT, fill=tk.Y)
|
||||||
|
scroll.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
self.ai_image_listbox.bind("<<ListboxSelect>>", lambda e: _on_list_select(self))
|
||||||
|
# 右键菜单:发送到 UCD / 重命名 / 另存为 / 删除
|
||||||
|
# 索引: 0=发送, 1=sep, 2=重命名, 3=另存为, 4=删除
|
||||||
|
self.ai_image_menu = tk.Menu(self.root, tearoff=0)
|
||||||
|
self.ai_image_menu.add_command(
|
||||||
|
label="发送图片",
|
||||||
|
command=lambda: _send_to_ucd(self),
|
||||||
|
)
|
||||||
|
self.ai_image_menu.add_separator()
|
||||||
|
self.ai_image_menu.add_command(
|
||||||
|
label="重命名…",
|
||||||
|
command=lambda: _rename_current(self),
|
||||||
|
)
|
||||||
|
self.ai_image_menu.add_command(
|
||||||
|
label="另存为…",
|
||||||
|
command=lambda: _save_current(self),
|
||||||
|
)
|
||||||
|
self.ai_image_menu.add_command(
|
||||||
|
label="删除",
|
||||||
|
command=lambda: _delete_current(self),
|
||||||
|
)
|
||||||
|
self.ai_image_listbox.bind(
|
||||||
|
"<Button-3>",
|
||||||
|
lambda e: _show_list_context_menu(self, e),
|
||||||
|
)
|
||||||
|
|
||||||
|
btn_row = ttk.Frame(left)
|
||||||
|
btn_row.pack(fill=tk.X, pady=(6, 0))
|
||||||
|
self.ai_image_send_ucd_btn = ttk.Button(
|
||||||
|
btn_row, text="发送", bootstyle="info-outline", width=8,
|
||||||
|
command=lambda: _send_to_ucd(self),
|
||||||
|
)
|
||||||
|
self.ai_image_send_ucd_btn.pack(side=tk.LEFT, padx=(0, 4))
|
||||||
|
ttk.Button(
|
||||||
|
btn_row, text="保存", bootstyle="success-outline", width=8,
|
||||||
|
command=lambda: _save_current(self),
|
||||||
|
).pack(side=tk.LEFT, padx=(0, 4))
|
||||||
|
ttk.Button(
|
||||||
|
btn_row, text="删除", bootstyle="danger-outline", width=8,
|
||||||
|
command=lambda: _delete_current(self),
|
||||||
|
).pack(side=tk.LEFT, padx=(0, 4))
|
||||||
|
ttk.Button(
|
||||||
|
btn_row, text="刷新", bootstyle="secondary-outline", width=8,
|
||||||
|
command=lambda: reload_ai_image_list(self),
|
||||||
|
).pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
# 右列:预览 + 输入
|
||||||
|
right = ttk.Frame(container)
|
||||||
|
right.grid(row=0, column=1, sticky=tk.NSEW)
|
||||||
|
|
||||||
|
preview_frame = ttk.LabelFrame(right, text="图片预览", padding=6)
|
||||||
|
preview_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
self.ai_image_canvas = tk.Canvas(
|
||||||
|
preview_frame, bg="#1e1e1e", highlightthickness=0
|
||||||
|
)
|
||||||
|
self.ai_image_canvas.pack(fill=tk.BOTH, expand=True)
|
||||||
|
self.ai_image_canvas.bind("<Configure>", lambda e: _redraw_preview(self))
|
||||||
|
|
||||||
|
meta_row = ttk.Frame(right)
|
||||||
|
meta_row.pack(fill=tk.X, pady=(4, 4))
|
||||||
|
self.ai_image_meta_var = tk.StringVar(value="未选择图片")
|
||||||
|
ttk.Label(
|
||||||
|
meta_row, textvariable=self.ai_image_meta_var,
|
||||||
|
foreground="#666", font=("微软雅黑", 9),
|
||||||
|
).pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
# 输入区
|
||||||
|
input_frame = ttk.LabelFrame(right, text="提示输入(Ctrl+Enter 发送)", padding=6)
|
||||||
|
input_frame.pack(fill=tk.X, pady=(4, 0))
|
||||||
|
|
||||||
|
self.ai_image_input = tk.Text(input_frame, height=3, wrap=tk.WORD)
|
||||||
|
self.ai_image_input.pack(fill=tk.X, side=tk.TOP)
|
||||||
|
self.ai_image_input.bind("<Control-Return>", lambda e: (_send_prompt(self), "break"))
|
||||||
|
|
||||||
|
send_row = ttk.Frame(input_frame)
|
||||||
|
send_row.pack(fill=tk.X, pady=(4, 0))
|
||||||
|
self.ai_image_status_var = tk.StringVar(value="就绪")
|
||||||
|
ttk.Label(
|
||||||
|
send_row, textvariable=self.ai_image_status_var,
|
||||||
|
foreground="#888", font=("微软雅黑", 9),
|
||||||
|
).pack(side=tk.LEFT)
|
||||||
|
self.ai_image_send_btn = ttk.Button(
|
||||||
|
send_row, text="发送", bootstyle="primary", width=10,
|
||||||
|
command=lambda: _send_prompt(self),
|
||||||
|
)
|
||||||
|
self.ai_image_send_btn.pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
# 注册面板
|
||||||
|
self.register_panel("ai_image", frame, None, "ai_image_visible")
|
||||||
|
self.ai_image_visible = False
|
||||||
|
|
||||||
|
# 初次加载缓存
|
||||||
|
reload_ai_image_list(self)
|
||||||
|
|
||||||
|
|
||||||
|
def toggle_ai_image_panel(self):
|
||||||
|
"""切换 AI 图片面板显隐。"""
|
||||||
|
self.show_panel("ai_image")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- 列表 / 选中 ----------------
|
||||||
|
|
||||||
|
|
||||||
|
def reload_ai_image_list(self):
|
||||||
|
"""重新扫描缓存并刷新列表。"""
|
||||||
|
self.ai_image_records = _svc.list_records()
|
||||||
|
self.ai_image_listbox.delete(0, tk.END)
|
||||||
|
for rec in self.ai_image_records:
|
||||||
|
label = _format_list_label(rec)
|
||||||
|
self.ai_image_listbox.insert(tk.END, label)
|
||||||
|
if self.ai_image_records:
|
||||||
|
self.ai_image_listbox.selection_clear(0, tk.END)
|
||||||
|
self.ai_image_listbox.selection_set(0)
|
||||||
|
self.ai_image_listbox.activate(0)
|
||||||
|
_select_record(self, self.ai_image_records[0])
|
||||||
|
else:
|
||||||
|
self.ai_image_current = None
|
||||||
|
self.ai_image_photo = None
|
||||||
|
self.ai_image_canvas.delete("all")
|
||||||
|
self.ai_image_meta_var.set("暂无缓存图片")
|
||||||
|
|
||||||
|
|
||||||
|
def _format_list_label(rec: _svc.AIImageRecord) -> str:
|
||||||
|
# 分辨率前缀:优先从 extra["size"] 取,其次从文件名猜
|
||||||
|
size_tag = ""
|
||||||
|
extra = rec.extra or {}
|
||||||
|
if isinstance(extra, dict) and extra.get("size"):
|
||||||
|
size_tag = f"[{extra['size']}] "
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
from PIL import Image as _Im
|
||||||
|
with _Im.open(rec.image_path) as _im:
|
||||||
|
size_tag = f"[{_im.width}×{_im.height}] "
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
prompt_line = (rec.prompt or "(无提示)").strip().splitlines()[0]
|
||||||
|
# 剩余可用宽度(width=34)去掉 size_tag
|
||||||
|
max_prompt = 34 - len(size_tag) - 2
|
||||||
|
if max_prompt > 4 and len(prompt_line) > max_prompt:
|
||||||
|
prompt_line = prompt_line[:max_prompt] + "…"
|
||||||
|
return f"{size_tag}{prompt_line}"
|
||||||
|
|
||||||
|
|
||||||
|
def _on_list_select(self):
|
||||||
|
sel = self.ai_image_listbox.curselection()
|
||||||
|
if not sel:
|
||||||
|
return
|
||||||
|
idx = sel[0]
|
||||||
|
if 0 <= idx < len(self.ai_image_records):
|
||||||
|
_select_record(self, self.ai_image_records[idx])
|
||||||
|
|
||||||
|
|
||||||
|
def _select_record(self, rec: _svc.AIImageRecord):
|
||||||
|
self.ai_image_current = rec
|
||||||
|
self.ai_image_meta_var.set(
|
||||||
|
f"{os.path.basename(rec.image_path)} | {rec.created_at}"
|
||||||
|
)
|
||||||
|
_redraw_preview(self)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- 预览绘制 ----------------
|
||||||
|
|
||||||
|
|
||||||
|
def _redraw_preview(self):
|
||||||
|
rec = getattr(self, "ai_image_current", None)
|
||||||
|
canvas = self.ai_image_canvas
|
||||||
|
canvas.delete("all")
|
||||||
|
if rec is None or not os.path.isfile(rec.image_path):
|
||||||
|
return
|
||||||
|
cw = canvas.winfo_width() or 1
|
||||||
|
ch = canvas.winfo_height() or 1
|
||||||
|
try:
|
||||||
|
img = Image.open(rec.image_path)
|
||||||
|
except Exception as exc:
|
||||||
|
canvas.create_text(cw // 2, ch // 2, text=f"加载失败: {exc}", fill="#f66")
|
||||||
|
return
|
||||||
|
iw, ih = img.size
|
||||||
|
scale = min(cw / iw, ch / ih, 1.0)
|
||||||
|
nw, nh = max(1, int(iw * scale)), max(1, int(ih * scale))
|
||||||
|
img_resized = img.resize((nw, nh), Image.LANCZOS)
|
||||||
|
self.ai_image_photo = ImageTk.PhotoImage(img_resized)
|
||||||
|
canvas.create_image(cw // 2, ch // 2, image=self.ai_image_photo, anchor="center")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- 发送 / 保存 / 删除 ----------------
|
||||||
|
|
||||||
|
|
||||||
|
def _send_prompt(self):
|
||||||
|
if getattr(self, "_ai_image_requesting", False):
|
||||||
|
return
|
||||||
|
prompt = self.ai_image_input.get("1.0", tk.END).strip()
|
||||||
|
if not prompt:
|
||||||
|
messagebox.showinfo("提示", "请输入内容")
|
||||||
|
return
|
||||||
|
|
||||||
|
_set_requesting(self, True)
|
||||||
|
is_remote_url = _svc.is_remote_image_url(prompt)
|
||||||
|
self.ai_image_status_var.set("下载中…" if is_remote_url else "请求中…")
|
||||||
|
|
||||||
|
def _success(record):
|
||||||
|
self.root.after(0, lambda: _on_request_done(self, record, None))
|
||||||
|
|
||||||
|
def _error(exc):
|
||||||
|
self.root.after(0, lambda: _on_request_done(self, None, exc))
|
||||||
|
|
||||||
|
if is_remote_url:
|
||||||
|
_svc.import_image_from_url_async(
|
||||||
|
prompt,
|
||||||
|
on_success=_success,
|
||||||
|
on_error=_error,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not _svc.has_api():
|
||||||
|
_set_requesting(self, False)
|
||||||
|
self.ai_image_status_var.set("就绪")
|
||||||
|
messagebox.showerror(
|
||||||
|
"API 未配置",
|
||||||
|
"AI 图片 API 尚未接入。\n"
|
||||||
|
"可直接输入图片 URL 导入,或在启动时通过 "
|
||||||
|
"app.services.ai_image.set_api_caller(...) 注入真实实现。",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
_svc.request_image_async(prompt, on_success=_success, on_error=_error)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_requesting(self, flag: bool):
|
||||||
|
self._ai_image_requesting = flag
|
||||||
|
try:
|
||||||
|
self.ai_image_send_btn.configure(state=tk.DISABLED if flag else tk.NORMAL)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _on_request_done(self, record, exc):
|
||||||
|
_set_requesting(self, False)
|
||||||
|
if exc is not None:
|
||||||
|
self.ai_image_status_var.set(f"失败: {exc}")
|
||||||
|
messagebox.showerror("生成失败", str(exc))
|
||||||
|
return
|
||||||
|
self.ai_image_status_var.set("完成")
|
||||||
|
self.ai_image_input.delete("1.0", tk.END)
|
||||||
|
reload_ai_image_list(self)
|
||||||
|
# 定位到新生成项(最新在前)
|
||||||
|
if record is not None and self.ai_image_records:
|
||||||
|
for i, r in enumerate(self.ai_image_records):
|
||||||
|
if r.id == record.id:
|
||||||
|
self.ai_image_listbox.selection_clear(0, tk.END)
|
||||||
|
self.ai_image_listbox.selection_set(i)
|
||||||
|
self.ai_image_listbox.activate(i)
|
||||||
|
_select_record(self, r)
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def _save_current(self):
|
||||||
|
rec = getattr(self, "ai_image_current", None)
|
||||||
|
if rec is None:
|
||||||
|
messagebox.showinfo("提示", "请先选择一张图片")
|
||||||
|
return
|
||||||
|
ext = os.path.splitext(rec.image_path)[1] or ".png"
|
||||||
|
dest = filedialog.asksaveasfilename(
|
||||||
|
title="另存为",
|
||||||
|
defaultextension=ext,
|
||||||
|
initialfile=os.path.basename(rec.image_path),
|
||||||
|
filetypes=[("图片", "*.png;*.jpg;*.jpeg;*.bmp;*.webp"), ("所有文件", "*.*")],
|
||||||
|
)
|
||||||
|
if not dest:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
_svc.export_record(rec, dest)
|
||||||
|
messagebox.showinfo("成功", f"已保存到:\n{dest}")
|
||||||
|
except Exception as exc:
|
||||||
|
messagebox.showerror("保存失败", str(exc))
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_current(self):
|
||||||
|
rec = getattr(self, "ai_image_current", None)
|
||||||
|
if rec is None:
|
||||||
|
messagebox.showinfo("提示", "请先选择一张图片")
|
||||||
|
return
|
||||||
|
if not messagebox.askyesno("确认删除", f"确定删除这张缓存图片吗?\n{os.path.basename(rec.image_path)}"):
|
||||||
|
return
|
||||||
|
_svc.delete_record(rec)
|
||||||
|
reload_ai_image_list(self)
|
||||||
|
|
||||||
|
|
||||||
|
def _rename_current(self):
|
||||||
|
"""弹窗让用户修改当前记录的备注名称(即列表显示中 size_tag 之后的部分)。"""
|
||||||
|
rec = getattr(self, "ai_image_current", None)
|
||||||
|
if rec is None:
|
||||||
|
messagebox.showinfo("提示", "请先选择一张图片")
|
||||||
|
return
|
||||||
|
|
||||||
|
current_name = rec.prompt or ""
|
||||||
|
new_name = simpledialog.askstring(
|
||||||
|
"重命名",
|
||||||
|
"修改备注名称(显示在分辨率标签后面):",
|
||||||
|
initialvalue=current_name,
|
||||||
|
parent=self.root,
|
||||||
|
)
|
||||||
|
if new_name is None: # 用户点了取消
|
||||||
|
return
|
||||||
|
new_name = new_name.strip()
|
||||||
|
if not new_name:
|
||||||
|
messagebox.showwarning("提示", "备注名称不能为空")
|
||||||
|
return
|
||||||
|
if new_name == current_name:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 写回 JSON 元数据
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
meta_path = os.path.splitext(rec.image_path)[0] + ".json"
|
||||||
|
meta = {}
|
||||||
|
if os.path.isfile(meta_path):
|
||||||
|
with open(meta_path, "r", encoding="utf-8") as f:
|
||||||
|
meta = json.load(f)
|
||||||
|
meta["prompt"] = new_name
|
||||||
|
with open(meta_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(meta, f, ensure_ascii=False, indent=2)
|
||||||
|
except Exception as exc:
|
||||||
|
messagebox.showerror("保存失败", f"无法更新元数据:\n{exc}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 同步内存中的记录并刷新列表
|
||||||
|
rec.prompt = new_name
|
||||||
|
reload_ai_image_list(self)
|
||||||
|
# 重新定位到刚才被重命名的图片
|
||||||
|
for i, r in enumerate(self.ai_image_records):
|
||||||
|
if r.id == rec.id:
|
||||||
|
self.ai_image_listbox.selection_clear(0, tk.END)
|
||||||
|
self.ai_image_listbox.selection_set(i)
|
||||||
|
self.ai_image_listbox.activate(i)
|
||||||
|
self.ai_image_listbox.see(i)
|
||||||
|
_select_record(self, r)
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- 发送到 UCD ----------------
|
||||||
|
|
||||||
|
|
||||||
|
def _show_list_context_menu(self, event):
|
||||||
|
"""在图片列表上显示右键菜单,并根据状态启用/禁用项。"""
|
||||||
|
try:
|
||||||
|
idx = self.ai_image_listbox.nearest(event.y)
|
||||||
|
except Exception:
|
||||||
|
idx = -1
|
||||||
|
if 0 <= idx < len(self.ai_image_records):
|
||||||
|
self.ai_image_listbox.selection_clear(0, tk.END)
|
||||||
|
self.ai_image_listbox.selection_set(idx)
|
||||||
|
self.ai_image_listbox.activate(idx)
|
||||||
|
_select_record(self, self.ai_image_records[idx])
|
||||||
|
|
||||||
|
has_selection = self.ai_image_current is not None
|
||||||
|
ucd = getattr(self, "ucd", None)
|
||||||
|
can_send = (
|
||||||
|
has_selection
|
||||||
|
and ucd is not None
|
||||||
|
and getattr(ucd, "status", False)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
self.ai_image_menu.entryconfigure(
|
||||||
|
0, state=("normal" if can_send else "disabled")
|
||||||
|
)
|
||||||
|
self.ai_image_menu.entryconfigure(
|
||||||
|
2, state=("normal" if has_selection else "disabled")
|
||||||
|
)
|
||||||
|
self.ai_image_menu.entryconfigure(
|
||||||
|
3, state=("normal" if has_selection else "disabled")
|
||||||
|
)
|
||||||
|
self.ai_image_menu.entryconfigure(
|
||||||
|
4, state=("normal" if has_selection else "disabled")
|
||||||
|
)
|
||||||
|
self.ai_image_menu.tk_popup(event.x_root, event.y_root)
|
||||||
|
finally:
|
||||||
|
self.ai_image_menu.grab_release()
|
||||||
|
|
||||||
|
|
||||||
|
def _send_to_ucd(self):
|
||||||
|
"""把当前选中的 AI 图片通过 UCD 发送到显示设备。"""
|
||||||
|
rec = getattr(self, "ai_image_current", None)
|
||||||
|
if rec is None:
|
||||||
|
messagebox.showinfo("提示", "请先选择一张图片")
|
||||||
|
return
|
||||||
|
if not os.path.isfile(rec.image_path):
|
||||||
|
messagebox.showerror("错误", f"图片文件不存在:\n{rec.image_path}")
|
||||||
|
return
|
||||||
|
ucd = getattr(self, "ucd", None)
|
||||||
|
if ucd is None or not getattr(ucd, "status", False):
|
||||||
|
messagebox.showwarning("警告", "请先连接 UCD323 设备")
|
||||||
|
return
|
||||||
|
|
||||||
|
image_path = rec.image_path
|
||||||
|
send_path = image_path
|
||||||
|
log = getattr(self, "log_gui", None)
|
||||||
|
|
||||||
|
# 发送前检查分辨率:建议与当前 UCD 输出分辨率一致。
|
||||||
|
try:
|
||||||
|
target_w, target_h = get_current_resolution(ucd)
|
||||||
|
except Exception:
|
||||||
|
target_w, target_h = 3840, 2160
|
||||||
|
try:
|
||||||
|
with Image.open(image_path) as _img:
|
||||||
|
src_w, src_h = _img.size
|
||||||
|
except Exception as exc:
|
||||||
|
messagebox.showerror("错误", f"无法读取图片尺寸:\n{exc}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if log is not None:
|
||||||
|
log.log(
|
||||||
|
f"UCD分辨率: {target_w}x{target_h} | 图片分辨率: {src_w}x{src_h}",
|
||||||
|
level="info",
|
||||||
|
)
|
||||||
|
|
||||||
|
if (src_w, src_h) != (target_w, target_h):
|
||||||
|
if not messagebox.askyesno(
|
||||||
|
"分辨率不匹配",
|
||||||
|
(
|
||||||
|
f"当前 UCD 分辨率: {target_w}x{target_h}\n"
|
||||||
|
f"图片分辨率: {src_w}x{src_h}\n\n"
|
||||||
|
"是否自动缩放后再发送?"
|
||||||
|
),
|
||||||
|
):
|
||||||
|
if log is not None:
|
||||||
|
log.log("用户取消发送:图片分辨率与 UCD 不一致", level="warning")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
send_path = _build_ucd_resized_image(image_path, target_w, target_h)
|
||||||
|
if log is not None:
|
||||||
|
log.log(
|
||||||
|
f"已生成匹配分辨率副本: {os.path.basename(send_path)}",
|
||||||
|
level="info",
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
messagebox.showerror("错误", f"自动缩放失败:\n{exc}")
|
||||||
|
return
|
||||||
|
|
||||||
|
if log is not None:
|
||||||
|
log.log(
|
||||||
|
f"发送 AI 图片到 UCD: {os.path.basename(send_path)}",
|
||||||
|
level="info",
|
||||||
|
)
|
||||||
|
if hasattr(self, "ai_image_status_var"):
|
||||||
|
self.ai_image_status_var.set("发送中…")
|
||||||
|
|
||||||
|
def _worker():
|
||||||
|
err = None
|
||||||
|
try:
|
||||||
|
ok = send_image_pattern(ucd, send_path)
|
||||||
|
except Exception as exc:
|
||||||
|
ok = False
|
||||||
|
err = str(exc)
|
||||||
|
|
||||||
|
def _done():
|
||||||
|
if ok:
|
||||||
|
if log is not None:
|
||||||
|
log.log(
|
||||||
|
f"图片已发送到 UCD: {os.path.basename(send_path)}",
|
||||||
|
level="success",
|
||||||
|
)
|
||||||
|
if hasattr(self, "ai_image_status_var"):
|
||||||
|
self.ai_image_status_var.set("已发送到 UCD")
|
||||||
|
else:
|
||||||
|
msg = f"UCD 发送失败: {err}" if err else "UCD 发送失败"
|
||||||
|
if log is not None:
|
||||||
|
log.log(msg, level="error")
|
||||||
|
if hasattr(self, "ai_image_status_var"):
|
||||||
|
self.ai_image_status_var.set("UCD 发送失败")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.root.after(0, _done)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
threading.Thread(target=_worker, daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
|
def _build_ucd_resized_image(image_path: str, target_w: int, target_h: int) -> str:
|
||||||
|
"""生成与 UCD 分辨率匹配的临时副本(缓存目录内)。"""
|
||||||
|
base_dir = os.path.dirname(image_path)
|
||||||
|
base_name = os.path.splitext(os.path.basename(image_path))[0]
|
||||||
|
out_name = f"{base_name}_{target_w}x{target_h}_ucd.png"
|
||||||
|
out_path = os.path.join(base_dir, out_name)
|
||||||
|
with Image.open(image_path) as img:
|
||||||
|
resized = img.convert("RGB").resize((target_w, target_h), Image.LANCZOS)
|
||||||
|
resized.save(out_path, format="PNG")
|
||||||
|
return out_path
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"""CCT 参数面板及其处理函数(Step 6 重构)。"""
|
"""CCT 参数面板及其处理函数(与主文件重构版本保持同步)。"""
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
@@ -10,7 +10,7 @@ import algorithm.pq_algorithm as pq_algorithm
|
|||||||
|
|
||||||
|
|
||||||
def create_cct_params_frame(self):
|
def create_cct_params_frame(self):
|
||||||
"""创建色度参数设置区域 - 屏模组、SDR、HDR 独立(✅ 增加色域参考标准选择 + 单步调试按钮)"""
|
"""创建色度参数设置区域 - 屏模组、SDR、HDR 独立(增加色域参考标准选择 + 单步调试按钮)"""
|
||||||
|
|
||||||
# ==================== 屏模组色度参数 Frame ====================
|
# ==================== 屏模组色度参数 Frame ====================
|
||||||
self.cct_params_frame = ttk.LabelFrame(
|
self.cct_params_frame = ttk.LabelFrame(
|
||||||
@@ -18,21 +18,16 @@ def create_cct_params_frame(self):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 默认值
|
# 默认值
|
||||||
self.DEFAULT_CCT_PARAMS = {
|
screen_default_cct_params = self.config.get_default_cct_params("screen_module")
|
||||||
"x_ideal": 0.3127,
|
|
||||||
"x_tolerance": 0.003,
|
|
||||||
"y_ideal": 0.3290,
|
|
||||||
"y_tolerance": 0.003,
|
|
||||||
}
|
|
||||||
|
|
||||||
# 从配置读取屏模组参数
|
# 从配置读取屏模组参数
|
||||||
saved_params = self.config.current_test_types.get("screen_module", {}).get(
|
saved_params = self.config.current_test_types.get("screen_module", {}).get(
|
||||||
"cct_params", self.DEFAULT_CCT_PARAMS.copy()
|
"cct_params", screen_default_cct_params.copy()
|
||||||
)
|
)
|
||||||
|
|
||||||
# 色域参考标准
|
# 色域参考标准
|
||||||
saved_gamut_ref = self.config.current_test_types.get("screen_module", {}).get(
|
saved_gamut_ref = self.config.current_test_types.get("screen_module", {}).get(
|
||||||
"gamut_reference", "DCI-P3"
|
"gamut_reference", self.config.get_default_gamut_reference("screen_module")
|
||||||
)
|
)
|
||||||
|
|
||||||
# 创建屏模组变量
|
# 创建屏模组变量
|
||||||
@@ -66,7 +61,7 @@ def create_cct_params_frame(self):
|
|||||||
entry.grid(row=i, column=1, sticky=tk.W, padx=5, pady=3)
|
entry.grid(row=i, column=1, sticky=tk.W, padx=5, pady=3)
|
||||||
|
|
||||||
# 绑定失去焦点事件
|
# 绑定失去焦点事件
|
||||||
default_val = self.DEFAULT_CCT_PARAMS[key]
|
default_val = screen_default_cct_params[key]
|
||||||
entry.bind(
|
entry.bind(
|
||||||
"<FocusOut>",
|
"<FocusOut>",
|
||||||
lambda e, v=var, d=default_val: self.on_cct_param_focus_out(v, d),
|
lambda e, v=var, d=default_val: self.on_cct_param_focus_out(v, d),
|
||||||
@@ -89,7 +84,7 @@ def create_cct_params_frame(self):
|
|||||||
)
|
)
|
||||||
self.screen_gamut_combo = screen_gamut_combo
|
self.screen_gamut_combo = screen_gamut_combo
|
||||||
|
|
||||||
# ==================== ✅ 单步调试按钮(右侧第二行)====================
|
# ==================== 单步调试按钮(右侧第二行)====================
|
||||||
ttk.Label(self.cct_params_frame, text="单步调试:").grid(
|
ttk.Label(self.cct_params_frame, text="单步调试:").grid(
|
||||||
row=1, column=2, sticky=tk.W, padx=(20, 5), pady=3
|
row=1, column=2, sticky=tk.W, padx=(20, 5), pady=3
|
||||||
)
|
)
|
||||||
@@ -142,21 +137,16 @@ def create_cct_params_frame(self):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# SDR 默认值
|
# SDR 默认值
|
||||||
self.SDR_DEFAULT_CCT_PARAMS = {
|
sdr_default_cct_params = self.config.get_default_cct_params("sdr_movie")
|
||||||
"x_ideal": 0.3127,
|
|
||||||
"x_tolerance": 0.003,
|
|
||||||
"y_ideal": 0.3290,
|
|
||||||
"y_tolerance": 0.003,
|
|
||||||
}
|
|
||||||
|
|
||||||
# 从配置读取 SDR 参数
|
# 从配置读取 SDR 参数
|
||||||
sdr_saved_params = self.config.current_test_types.get("sdr_movie", {}).get(
|
sdr_saved_params = self.config.current_test_types.get("sdr_movie", {}).get(
|
||||||
"cct_params", self.SDR_DEFAULT_CCT_PARAMS.copy()
|
"cct_params", sdr_default_cct_params.copy()
|
||||||
)
|
)
|
||||||
|
|
||||||
# 色域参考标准
|
# 色域参考标准
|
||||||
sdr_saved_gamut_ref = self.config.current_test_types.get("sdr_movie", {}).get(
|
sdr_saved_gamut_ref = self.config.current_test_types.get("sdr_movie", {}).get(
|
||||||
"gamut_reference", "BT.709"
|
"gamut_reference", self.config.get_default_gamut_reference("sdr_movie")
|
||||||
)
|
)
|
||||||
|
|
||||||
# 创建 SDR 变量
|
# 创建 SDR 变量
|
||||||
@@ -190,7 +180,7 @@ def create_cct_params_frame(self):
|
|||||||
entry.grid(row=i, column=1, sticky=tk.W, padx=5, pady=3)
|
entry.grid(row=i, column=1, sticky=tk.W, padx=5, pady=3)
|
||||||
|
|
||||||
# 绑定失去焦点事件
|
# 绑定失去焦点事件
|
||||||
default_val = self.SDR_DEFAULT_CCT_PARAMS[key]
|
default_val = sdr_default_cct_params[key]
|
||||||
entry.bind(
|
entry.bind(
|
||||||
"<FocusOut>",
|
"<FocusOut>",
|
||||||
lambda e, v=var, d=default_val: self.on_sdr_cct_param_focus_out(v, d),
|
lambda e, v=var, d=default_val: self.on_sdr_cct_param_focus_out(v, d),
|
||||||
@@ -211,7 +201,7 @@ def create_cct_params_frame(self):
|
|||||||
sdr_gamut_combo.bind("<<ComboboxSelected>>", self.on_sdr_gamut_ref_changed)
|
sdr_gamut_combo.bind("<<ComboboxSelected>>", self.on_sdr_gamut_ref_changed)
|
||||||
self.sdr_gamut_combo = sdr_gamut_combo
|
self.sdr_gamut_combo = sdr_gamut_combo
|
||||||
|
|
||||||
# ==================== ✅ SDR 单步调试按钮(右侧第二行)====================
|
# ==================== SDR 单步调试按钮(右侧第二行)====================
|
||||||
ttk.Label(self.sdr_cct_params_frame, text="单步调试:").grid(
|
ttk.Label(self.sdr_cct_params_frame, text="单步调试:").grid(
|
||||||
row=1, column=2, sticky=tk.W, padx=(20, 5), pady=3
|
row=1, column=2, sticky=tk.W, padx=(20, 5), pady=3
|
||||||
)
|
)
|
||||||
@@ -264,21 +254,16 @@ def create_cct_params_frame(self):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# HDR 默认值
|
# HDR 默认值
|
||||||
self.HDR_DEFAULT_CCT_PARAMS = {
|
hdr_default_cct_params = self.config.get_default_cct_params("hdr_movie")
|
||||||
"x_ideal": 0.3127,
|
|
||||||
"x_tolerance": 0.003,
|
|
||||||
"y_ideal": 0.3290,
|
|
||||||
"y_tolerance": 0.003,
|
|
||||||
}
|
|
||||||
|
|
||||||
# 从配置读取 HDR 参数
|
# 从配置读取 HDR 参数
|
||||||
hdr_saved_params = self.config.current_test_types.get("hdr_movie", {}).get(
|
hdr_saved_params = self.config.current_test_types.get("hdr_movie", {}).get(
|
||||||
"cct_params", self.HDR_DEFAULT_CCT_PARAMS.copy()
|
"cct_params", hdr_default_cct_params.copy()
|
||||||
)
|
)
|
||||||
|
|
||||||
# 色域参考标准
|
# 色域参考标准
|
||||||
hdr_saved_gamut_ref = self.config.current_test_types.get("hdr_movie", {}).get(
|
hdr_saved_gamut_ref = self.config.current_test_types.get("hdr_movie", {}).get(
|
||||||
"gamut_reference", "BT.2020"
|
"gamut_reference", self.config.get_default_gamut_reference("hdr_movie")
|
||||||
)
|
)
|
||||||
|
|
||||||
# 创建 HDR 变量
|
# 创建 HDR 变量
|
||||||
@@ -312,7 +297,7 @@ def create_cct_params_frame(self):
|
|||||||
entry.grid(row=i, column=1, sticky=tk.W, padx=5, pady=3)
|
entry.grid(row=i, column=1, sticky=tk.W, padx=5, pady=3)
|
||||||
|
|
||||||
# 绑定失去焦点事件
|
# 绑定失去焦点事件
|
||||||
default_val = self.HDR_DEFAULT_CCT_PARAMS[key]
|
default_val = hdr_default_cct_params[key]
|
||||||
entry.bind(
|
entry.bind(
|
||||||
"<FocusOut>",
|
"<FocusOut>",
|
||||||
lambda e, v=var, d=default_val: self.on_hdr_cct_param_focus_out(v, d),
|
lambda e, v=var, d=default_val: self.on_hdr_cct_param_focus_out(v, d),
|
||||||
@@ -333,7 +318,7 @@ def create_cct_params_frame(self):
|
|||||||
hdr_gamut_combo.bind("<<ComboboxSelected>>", self.on_hdr_gamut_ref_changed)
|
hdr_gamut_combo.bind("<<ComboboxSelected>>", self.on_hdr_gamut_ref_changed)
|
||||||
self.hdr_gamut_combo = hdr_gamut_combo
|
self.hdr_gamut_combo = hdr_gamut_combo
|
||||||
|
|
||||||
# ==================== ✅ HDR 单步调试按钮(右侧第二行)====================
|
# ==================== HDR 单步调试按钮(右侧第二行)====================
|
||||||
ttk.Label(self.hdr_cct_params_frame, text="单步调试:").grid(
|
ttk.Label(self.hdr_cct_params_frame, text="单步调试:").grid(
|
||||||
row=1, column=2, sticky=tk.W, padx=(20, 5), pady=3
|
row=1, column=2, sticky=tk.W, padx=(20, 5), pady=3
|
||||||
)
|
)
|
||||||
@@ -381,15 +366,68 @@ def create_cct_params_frame(self):
|
|||||||
).grid(row=5, column=0, columnspan=4, sticky=tk.W, padx=5, pady=5)
|
).grid(row=5, column=0, columnspan=4, sticky=tk.W, padx=5, pady=5)
|
||||||
|
|
||||||
|
|
||||||
def on_sdr_cct_param_focus_out(self, var, default_value):
|
def _get_cct_var_dict(self, test_type):
|
||||||
"""SDR 色度参数失去焦点时的处理"""
|
"""按测试类型返回 CCT 变量映射。"""
|
||||||
|
if test_type == "sdr_movie":
|
||||||
|
return {
|
||||||
|
"x_ideal": self.sdr_cct_x_ideal_var,
|
||||||
|
"x_tolerance": self.sdr_cct_x_tolerance_var,
|
||||||
|
"y_ideal": self.sdr_cct_y_ideal_var,
|
||||||
|
"y_tolerance": self.sdr_cct_y_tolerance_var,
|
||||||
|
}
|
||||||
|
if test_type == "hdr_movie":
|
||||||
|
return {
|
||||||
|
"x_ideal": self.hdr_cct_x_ideal_var,
|
||||||
|
"x_tolerance": self.hdr_cct_x_tolerance_var,
|
||||||
|
"y_ideal": self.hdr_cct_y_ideal_var,
|
||||||
|
"y_tolerance": self.hdr_cct_y_tolerance_var,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"x_ideal": self.cct_x_ideal_var,
|
||||||
|
"x_tolerance": self.cct_x_tolerance_var,
|
||||||
|
"y_ideal": self.cct_y_ideal_var,
|
||||||
|
"y_tolerance": self.cct_y_tolerance_var,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_cct_float(self, var, default):
|
||||||
|
"""读取并解析 CCT 输入值,失败时回落默认值。"""
|
||||||
try:
|
try:
|
||||||
value = var.get().strip()
|
value = var.get().strip()
|
||||||
|
if value == "":
|
||||||
|
return default
|
||||||
|
return float(value)
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _save_cct_params_for(self, test_type):
|
||||||
|
"""保存指定测试类型的 CCT 参数。"""
|
||||||
|
try:
|
||||||
|
default_params = self.config.get_default_cct_params(test_type)
|
||||||
|
var_dict = _get_cct_var_dict(self, test_type)
|
||||||
|
cct_params = {
|
||||||
|
key: _parse_cct_float(self, var_dict[key], default_params[key])
|
||||||
|
for key in default_params
|
||||||
|
}
|
||||||
|
|
||||||
|
if test_type not in self.config.current_test_types:
|
||||||
|
self.config.current_test_types[test_type] = {}
|
||||||
|
|
||||||
|
self.config.current_test_types[test_type]["cct_params"] = cct_params
|
||||||
|
self.save_pq_config()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_cct_focus_out(self, var, default_value, save_func, label):
|
||||||
|
"""统一处理 CCT 参数失焦校验并保存。"""
|
||||||
|
try:
|
||||||
|
value = var.get().strip()
|
||||||
if value == "":
|
if value == "":
|
||||||
var.set(str(default_value))
|
var.set(str(default_value))
|
||||||
if hasattr(self, "log_gui"):
|
if hasattr(self, "log_gui"):
|
||||||
self.log_gui.log(f"✓ SDR 参数为空,恢复默认值: {default_value}")
|
self.log_gui.log(f"{label} 参数为空,恢复默认值: {default_value}", level="success")
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
float_val = float(value)
|
float_val = float(value)
|
||||||
@@ -397,124 +435,39 @@ def on_sdr_cct_param_focus_out(self, var, default_value):
|
|||||||
var.set(str(default_value))
|
var.set(str(default_value))
|
||||||
if hasattr(self, "log_gui"):
|
if hasattr(self, "log_gui"):
|
||||||
self.log_gui.log(
|
self.log_gui.log(
|
||||||
f"⚠️ SDR 参数超出范围,恢复默认值: {default_value}"
|
f"{label} 参数超出范围,恢复默认值: {default_value}"
|
||||||
)
|
, level="error")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
var.set(str(default_value))
|
var.set(str(default_value))
|
||||||
if hasattr(self, "log_gui"):
|
if hasattr(self, "log_gui"):
|
||||||
self.log_gui.log(f"⚠️ SDR 参数无效,恢复默认值: {default_value}")
|
self.log_gui.log(
|
||||||
|
f"{label} 参数无效,恢复默认值: {default_value}"
|
||||||
|
, level="error")
|
||||||
|
|
||||||
self.save_sdr_cct_params()
|
save_func()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if hasattr(self, "log_gui"):
|
if hasattr(self, "log_gui"):
|
||||||
self.log_gui.log(f"处理 SDR 参数失败: {str(e)}")
|
self.log_gui.log(f"处理 {label} 参数失败: {str(e)}", level="error")
|
||||||
|
|
||||||
|
|
||||||
|
def on_sdr_cct_param_focus_out(self, var, default_value):
|
||||||
|
"""SDR 色度参数失去焦点时的处理。"""
|
||||||
|
_handle_cct_focus_out(self, var, default_value, self.save_sdr_cct_params, "SDR")
|
||||||
|
|
||||||
|
|
||||||
def save_sdr_cct_params(self):
|
def save_sdr_cct_params(self):
|
||||||
"""保存 SDR 色度参数"""
|
"""保存 SDR 色度参数。"""
|
||||||
try:
|
_save_cct_params_for(self, "sdr_movie")
|
||||||
|
|
||||||
def get_float(var, default):
|
|
||||||
try:
|
|
||||||
value = var.get().strip()
|
|
||||||
if value == "":
|
|
||||||
return default
|
|
||||||
return float(value)
|
|
||||||
except:
|
|
||||||
return default
|
|
||||||
|
|
||||||
sdr_cct_params = {
|
|
||||||
"x_ideal": get_float(
|
|
||||||
self.sdr_cct_x_ideal_var, self.SDR_DEFAULT_CCT_PARAMS["x_ideal"]
|
|
||||||
),
|
|
||||||
"x_tolerance": get_float(
|
|
||||||
self.sdr_cct_x_tolerance_var,
|
|
||||||
self.SDR_DEFAULT_CCT_PARAMS["x_tolerance"],
|
|
||||||
),
|
|
||||||
"y_ideal": get_float(
|
|
||||||
self.sdr_cct_y_ideal_var, self.SDR_DEFAULT_CCT_PARAMS["y_ideal"]
|
|
||||||
),
|
|
||||||
"y_tolerance": get_float(
|
|
||||||
self.sdr_cct_y_tolerance_var,
|
|
||||||
self.SDR_DEFAULT_CCT_PARAMS["y_tolerance"],
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
if "sdr_movie" not in self.config.current_test_types:
|
|
||||||
self.config.current_test_types["sdr_movie"] = {}
|
|
||||||
|
|
||||||
self.config.current_test_types["sdr_movie"]["cct_params"] = sdr_cct_params
|
|
||||||
self.save_pq_config()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def on_hdr_cct_param_focus_out(self, var, default_value):
|
def on_hdr_cct_param_focus_out(self, var, default_value):
|
||||||
"""HDR 色度参数失去焦点时的处理"""
|
"""HDR 色度参数失去焦点时的处理。"""
|
||||||
try:
|
_handle_cct_focus_out(self, var, default_value, self.save_hdr_cct_params, "HDR")
|
||||||
value = var.get().strip()
|
|
||||||
|
|
||||||
if value == "":
|
|
||||||
var.set(str(default_value))
|
|
||||||
if hasattr(self, "log_gui"):
|
|
||||||
self.log_gui.log(f"✓ HDR 参数为空,恢复默认值: {default_value}")
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
float_val = float(value)
|
|
||||||
if float_val < 0 or float_val > 1:
|
|
||||||
var.set(str(default_value))
|
|
||||||
if hasattr(self, "log_gui"):
|
|
||||||
self.log_gui.log(
|
|
||||||
f"⚠️ HDR 参数超出范围,恢复默认值: {default_value}"
|
|
||||||
)
|
|
||||||
except ValueError:
|
|
||||||
var.set(str(default_value))
|
|
||||||
if hasattr(self, "log_gui"):
|
|
||||||
self.log_gui.log(f"⚠️ HDR 参数无效,恢复默认值: {default_value}")
|
|
||||||
|
|
||||||
self.save_hdr_cct_params()
|
|
||||||
except Exception as e:
|
|
||||||
if hasattr(self, "log_gui"):
|
|
||||||
self.log_gui.log(f"处理 HDR 参数失败: {str(e)}")
|
|
||||||
|
|
||||||
|
|
||||||
def save_hdr_cct_params(self):
|
def save_hdr_cct_params(self):
|
||||||
"""保存 HDR 色度参数"""
|
"""保存 HDR 色度参数。"""
|
||||||
try:
|
_save_cct_params_for(self, "hdr_movie")
|
||||||
|
|
||||||
def get_float(var, default):
|
|
||||||
try:
|
|
||||||
value = var.get().strip()
|
|
||||||
if value == "":
|
|
||||||
return default
|
|
||||||
return float(value)
|
|
||||||
except:
|
|
||||||
return default
|
|
||||||
|
|
||||||
hdr_cct_params = {
|
|
||||||
"x_ideal": get_float(
|
|
||||||
self.hdr_cct_x_ideal_var, self.HDR_DEFAULT_CCT_PARAMS["x_ideal"]
|
|
||||||
),
|
|
||||||
"x_tolerance": get_float(
|
|
||||||
self.hdr_cct_x_tolerance_var,
|
|
||||||
self.HDR_DEFAULT_CCT_PARAMS["x_tolerance"],
|
|
||||||
),
|
|
||||||
"y_ideal": get_float(
|
|
||||||
self.hdr_cct_y_ideal_var, self.HDR_DEFAULT_CCT_PARAMS["y_ideal"]
|
|
||||||
),
|
|
||||||
"y_tolerance": get_float(
|
|
||||||
self.hdr_cct_y_tolerance_var,
|
|
||||||
self.HDR_DEFAULT_CCT_PARAMS["y_tolerance"],
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
if "hdr_movie" not in self.config.current_test_types:
|
|
||||||
self.config.current_test_types["hdr_movie"] = {}
|
|
||||||
|
|
||||||
self.config.current_test_types["hdr_movie"]["cct_params"] = hdr_cct_params
|
|
||||||
self.save_pq_config()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def recalculate_cct(self):
|
def recalculate_cct(self):
|
||||||
@@ -522,7 +475,7 @@ def recalculate_cct(self):
|
|||||||
try:
|
try:
|
||||||
# 1. 保存新参数
|
# 1. 保存新参数
|
||||||
self.save_cct_params()
|
self.save_cct_params()
|
||||||
self.log_gui.log("✓ 色度参数已更新")
|
self.log_gui.log("色度参数已更新", level="success")
|
||||||
|
|
||||||
# 2. 收起配置项
|
# 2. 收起配置项
|
||||||
if hasattr(self, "config_panel_frame"):
|
if hasattr(self, "config_panel_frame"):
|
||||||
@@ -540,7 +493,7 @@ def recalculate_cct(self):
|
|||||||
|
|
||||||
# 4. 检查是否有数据
|
# 4. 检查是否有数据
|
||||||
if not hasattr(self, "results") or not self.results:
|
if not hasattr(self, "results") or not self.results:
|
||||||
self.log_gui.log("⚠️ 没有测试数据,无法重新绘制")
|
self.log_gui.log("没有测试数据,无法重新绘制", level="error")
|
||||||
messagebox.showwarning("警告", "请先完成测试后再重新计算")
|
messagebox.showwarning("警告", "请先完成测试后再重新计算")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -550,14 +503,14 @@ def recalculate_cct(self):
|
|||||||
gray_data = self.results.get_intermediate_data("cct", "gray")
|
gray_data = self.results.get_intermediate_data("cct", "gray")
|
||||||
|
|
||||||
if not gray_data or len(gray_data) < 2:
|
if not gray_data or len(gray_data) < 2:
|
||||||
self.log_gui.log("⚠️ 没有可用的灰阶数据")
|
self.log_gui.log("没有可用的灰阶数据", level="error")
|
||||||
messagebox.showwarning("警告", "没有找到色度测试数据")
|
messagebox.showwarning("警告", "没有找到色度测试数据")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 6. 重新计算 CCT
|
# 6. 重新计算 CCT
|
||||||
self.log_gui.log("=" * 50)
|
self.log_gui.log("=" * 50, level="separator")
|
||||||
self.log_gui.log("开始重新计算色度一致性...")
|
self.log_gui.log("开始重新计算色度一致性...", level="info")
|
||||||
self.log_gui.log("=" * 50)
|
self.log_gui.log("=" * 50, level="separator")
|
||||||
|
|
||||||
cct_values = pq_algorithm.calculate_cct_from_results(gray_data)
|
cct_values = pq_algorithm.calculate_cct_from_results(gray_data)
|
||||||
|
|
||||||
@@ -568,14 +521,14 @@ def recalculate_cct(self):
|
|||||||
test_type = self.config.current_test_type
|
test_type = self.config.current_test_type
|
||||||
self.plot_cct(test_type)
|
self.plot_cct(test_type)
|
||||||
|
|
||||||
self.log_gui.log("✓ 色度图已重新绘制")
|
self.log_gui.log("色度图已重新绘制", level="success")
|
||||||
self.log_gui.log("=" * 50)
|
self.log_gui.log("=" * 50, level="separator")
|
||||||
|
|
||||||
messagebox.showinfo("成功", "色度图已根据新参数重新绘制!")
|
messagebox.showinfo("成功", "色度图已根据新参数重新绘制!")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_gui.log(f"❌ 重新计算失败: {str(e)}")
|
self.log_gui.log(f"重新计算失败: {str(e)}", level="error")
|
||||||
self.log_gui.log(traceback.format_exc())
|
self.log_gui.log(traceback.format_exc(), level="error")
|
||||||
messagebox.showerror("错误", f"重新计算失败: {str(e)}")
|
messagebox.showerror("错误", f"重新计算失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@@ -598,7 +551,7 @@ def recalculate_gamut(self):
|
|||||||
|
|
||||||
# 3. 检查是否有数据
|
# 3. 检查是否有数据
|
||||||
if not hasattr(self, "results") or not self.results:
|
if not hasattr(self, "results") or not self.results:
|
||||||
self.log_gui.log("⚠️ 没有测试数据,无法重新绘制")
|
self.log_gui.log("没有测试数据,无法重新绘制", level="error")
|
||||||
messagebox.showwarning("警告", "请先完成测试后再重新计算")
|
messagebox.showwarning("警告", "请先完成测试后再重新计算")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -606,7 +559,7 @@ def recalculate_gamut(self):
|
|||||||
rgb_data = self.results.get_intermediate_data("gamut", "rgb")
|
rgb_data = self.results.get_intermediate_data("gamut", "rgb")
|
||||||
|
|
||||||
if not rgb_data or len(rgb_data) < 3:
|
if not rgb_data or len(rgb_data) < 3:
|
||||||
self.log_gui.log("⚠️ 没有可用的色域数据")
|
self.log_gui.log("没有可用的色域数据", level="error")
|
||||||
messagebox.showwarning("警告", "没有找到色域测试数据")
|
messagebox.showwarning("警告", "没有找到色域测试数据")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -623,13 +576,14 @@ def recalculate_gamut(self):
|
|||||||
else:
|
else:
|
||||||
reference_standard = "DCI-P3"
|
reference_standard = "DCI-P3"
|
||||||
|
|
||||||
self.log_gui.log("=" * 50)
|
self.log_gui.log("=" * 50, level="separator")
|
||||||
self.log_gui.log(f"开始重新计算色域(参考标准: {reference_standard})...")
|
self.log_gui.log(f"开始重新计算色域(参考标准: {reference_standard})...", level="info")
|
||||||
self.log_gui.log("=" * 50)
|
self.log_gui.log("=" * 50, level="separator")
|
||||||
|
|
||||||
# 7. 重新计算 XY 色域覆盖率
|
# 7. 重新计算 XY 色域覆盖率
|
||||||
xy_points = [[result[0], result[1]] for result in rgb_data]
|
xy_points = [[result[0], result[1]] for result in rgb_data]
|
||||||
|
|
||||||
|
# 根据参考标准计算 XY 覆盖率
|
||||||
if reference_standard == "BT.2020":
|
if reference_standard == "BT.2020":
|
||||||
area_xy, coverage_xy = pq_algorithm.calculate_gamut_coverage_BT2020(
|
area_xy, coverage_xy = pq_algorithm.calculate_gamut_coverage_BT2020(
|
||||||
xy_points
|
xy_points
|
||||||
@@ -648,13 +602,15 @@ def recalculate_gamut(self):
|
|||||||
)
|
)
|
||||||
reference_standard = "DCI-P3"
|
reference_standard = "DCI-P3"
|
||||||
|
|
||||||
self.log_gui.log(f"✓ 参考标准: {reference_standard}")
|
self.log_gui.log(f"参考标准: {reference_standard}", level="success")
|
||||||
self.log_gui.log(f"✓ XY 色域覆盖率: {coverage_xy:.1f}%")
|
self.log_gui.log(f"XY 色域覆盖率: {coverage_xy:.1f}%", level="success")
|
||||||
|
|
||||||
# 8. 重新计算 UV 色域覆盖率
|
# ========== ✅✅8. 重新计算 UV 色域覆盖率 ==========
|
||||||
|
# 将 XY 坐标转换为 UV 坐标
|
||||||
uv_points = []
|
uv_points = []
|
||||||
for x, y in xy_points:
|
for x, y in xy_points:
|
||||||
try:
|
try:
|
||||||
|
# XY转UV公式
|
||||||
denom = -2 * x + 12 * y + 3
|
denom = -2 * x + 12 * y + 3
|
||||||
if abs(denom) < 1e-10:
|
if abs(denom) < 1e-10:
|
||||||
u, v = 0, 0
|
u, v = 0, 0
|
||||||
@@ -665,8 +621,9 @@ def recalculate_gamut(self):
|
|||||||
except ZeroDivisionError:
|
except ZeroDivisionError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
self.log_gui.log(f"✓ 转换后的 UV 点数量: {len(uv_points)}")
|
self.log_gui.log(f"转换后的 UV 点数量: {len(uv_points)}", level="success")
|
||||||
|
|
||||||
|
# 根据参考标准计算 UV 覆盖率
|
||||||
if reference_standard == "BT.2020":
|
if reference_standard == "BT.2020":
|
||||||
area_uv, coverage_uv = pq_algorithm.calculate_gamut_coverage_BT2020_uv(
|
area_uv, coverage_uv = pq_algorithm.calculate_gamut_coverage_BT2020_uv(
|
||||||
uv_points
|
uv_points
|
||||||
@@ -684,30 +641,31 @@ def recalculate_gamut(self):
|
|||||||
uv_points
|
uv_points
|
||||||
)
|
)
|
||||||
|
|
||||||
self.log_gui.log(f"✓ UV 色域覆盖率: {coverage_uv:.1f}%")
|
self.log_gui.log(f"UV 色域覆盖率: {coverage_uv:.1f}%", level="success")
|
||||||
|
# ========================================================
|
||||||
|
|
||||||
# 9. 更新结果
|
# 9. 更新结果(同时保存 XY 和 UV 覆盖率)
|
||||||
self.results.set_test_item_result(
|
self.results.set_test_item_result(
|
||||||
"gamut",
|
"gamut",
|
||||||
{
|
{
|
||||||
"area": area_xy,
|
"area": area_xy, # ← 兼容旧字段
|
||||||
"coverage": coverage_xy,
|
"coverage": coverage_xy, # ← 兼容旧字段
|
||||||
"area_xy": area_xy,
|
"area_xy": area_xy, # ← XY 面积
|
||||||
"coverage_xy": coverage_xy,
|
"coverage_xy": coverage_xy, # ← XY 覆盖率
|
||||||
"area_uv": area_uv,
|
"area_uv": area_uv, # ← UV 面积
|
||||||
"coverage_uv": coverage_uv,
|
"coverage_uv": coverage_uv, # ← UV 覆盖率
|
||||||
"uv_coverage": coverage_uv,
|
"uv_coverage": coverage_uv, # ← 兼容字段(Excel 导出用)
|
||||||
"reference": reference_standard,
|
"reference": reference_standard,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
self.log_gui.log("✓ 测试结果已更新到 results 对象")
|
self.log_gui.log("测试结果已更新到 results 对象", level="success")
|
||||||
|
|
||||||
# 10. 重新绘制色域图
|
# 10. 重新绘制色域图
|
||||||
self.plot_gamut(rgb_data, coverage_xy, test_type)
|
self.plot_gamut(rgb_data, coverage_xy, test_type)
|
||||||
|
|
||||||
self.log_gui.log("✓ 色域图已重新绘制")
|
self.log_gui.log("色域图已重新绘制", level="success")
|
||||||
self.log_gui.log("=" * 50)
|
self.log_gui.log("=" * 50, level="separator")
|
||||||
|
|
||||||
messagebox.showinfo(
|
messagebox.showinfo(
|
||||||
"成功",
|
"成功",
|
||||||
@@ -717,108 +675,19 @@ def recalculate_gamut(self):
|
|||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_gui.log(f"❌ 重新计算失败: {str(e)}")
|
self.log_gui.log(f"重新计算失败: {str(e)}", level="error")
|
||||||
self.log_gui.log(traceback.format_exc())
|
self.log_gui.log(traceback.format_exc(), level="error")
|
||||||
messagebox.showerror("错误", f"重新计算失败: {str(e)}")
|
messagebox.showerror("错误", f"重新计算失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
def on_cct_param_change(self, var, default_value):
|
|
||||||
"""色度参数改变时的处理 - 空值恢复默认"""
|
|
||||||
try:
|
|
||||||
value = var.get().strip()
|
|
||||||
|
|
||||||
if value == "":
|
|
||||||
var.set(str(default_value))
|
|
||||||
if hasattr(self, "log_gui"):
|
|
||||||
self.log_gui.log(f"输入框为空,恢复默认值: {default_value}")
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
float_val = float(value)
|
|
||||||
if float_val < 0 or float_val > 1:
|
|
||||||
var.set(str(default_value))
|
|
||||||
if hasattr(self, "log_gui"):
|
|
||||||
self.log_gui.log(
|
|
||||||
f"参数超出范围 [0, 1],恢复默认值: {default_value}"
|
|
||||||
)
|
|
||||||
except ValueError:
|
|
||||||
var.set(str(default_value))
|
|
||||||
if hasattr(self, "log_gui"):
|
|
||||||
self.log_gui.log(f"无效的参数值,恢复默认值: {default_value}")
|
|
||||||
|
|
||||||
self.save_cct_params()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
if hasattr(self, "log_gui"):
|
|
||||||
self.log_gui.log(f"处理参数变化失败: {str(e)}")
|
|
||||||
|
|
||||||
|
|
||||||
def on_cct_param_focus_out(self, var, default_value):
|
def on_cct_param_focus_out(self, var, default_value):
|
||||||
"""色度参数失去焦点时的处理 - 空值恢复默认"""
|
"""色度参数失去焦点时的处理 - 空值恢复默认"""
|
||||||
try:
|
_handle_cct_focus_out(self, var, default_value, self.save_cct_params, "屏模组")
|
||||||
value = var.get().strip()
|
|
||||||
|
|
||||||
if value == "":
|
|
||||||
var.set(str(default_value))
|
|
||||||
if hasattr(self, "log_gui"):
|
|
||||||
self.log_gui.log(f"✓ 输入框为空,恢复默认值: {default_value}")
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
float_val = float(value)
|
|
||||||
if float_val < 0 or float_val > 1:
|
|
||||||
var.set(str(default_value))
|
|
||||||
if hasattr(self, "log_gui"):
|
|
||||||
self.log_gui.log(
|
|
||||||
f"⚠️ 参数超出范围 [0, 1],恢复默认值: {default_value}"
|
|
||||||
)
|
|
||||||
except ValueError:
|
|
||||||
var.set(str(default_value))
|
|
||||||
if hasattr(self, "log_gui"):
|
|
||||||
self.log_gui.log(f"⚠️ 无效的参数值,恢复默认值: {default_value}")
|
|
||||||
|
|
||||||
self.save_cct_params()
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
if hasattr(self, "log_gui"):
|
|
||||||
self.log_gui.log(f"处理参数变化失败: {str(e)}")
|
|
||||||
|
|
||||||
|
|
||||||
def save_cct_params(self):
|
def save_cct_params(self):
|
||||||
"""保存色度参数 - 简化版"""
|
"""保存色度参数 - 简化版"""
|
||||||
try:
|
_save_cct_params_for(self, self.config.current_test_type)
|
||||||
current_type = self.config.current_test_type
|
|
||||||
|
|
||||||
def get_float(var, default):
|
|
||||||
try:
|
|
||||||
value = var.get().strip()
|
|
||||||
if value == "":
|
|
||||||
return default
|
|
||||||
return float(value)
|
|
||||||
except:
|
|
||||||
return default
|
|
||||||
|
|
||||||
cct_params = {
|
|
||||||
"x_ideal": get_float(
|
|
||||||
self.cct_x_ideal_var, self.DEFAULT_CCT_PARAMS["x_ideal"]
|
|
||||||
),
|
|
||||||
"x_tolerance": get_float(
|
|
||||||
self.cct_x_tolerance_var, self.DEFAULT_CCT_PARAMS["x_tolerance"]
|
|
||||||
),
|
|
||||||
"y_ideal": get_float(
|
|
||||||
self.cct_y_ideal_var, self.DEFAULT_CCT_PARAMS["y_ideal"]
|
|
||||||
),
|
|
||||||
"y_tolerance": get_float(
|
|
||||||
self.cct_y_tolerance_var, self.DEFAULT_CCT_PARAMS["y_tolerance"]
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
if current_type not in self.config.current_test_types:
|
|
||||||
self.config.current_test_types[current_type] = {}
|
|
||||||
|
|
||||||
self.config.current_test_types[current_type]["cct_params"] = cct_params
|
|
||||||
self.save_pq_config()
|
|
||||||
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def reload_cct_params(self):
|
def reload_cct_params(self):
|
||||||
@@ -830,7 +699,7 @@ def reload_cct_params(self):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if saved_params is None:
|
if saved_params is None:
|
||||||
saved_params = self.DEFAULT_CCT_PARAMS.copy()
|
saved_params = self.config.get_default_cct_params(current_type)
|
||||||
|
|
||||||
# 更新输入框的值
|
# 更新输入框的值
|
||||||
self.cct_x_ideal_var.set(str(saved_params["x_ideal"]))
|
self.cct_x_ideal_var.set(str(saved_params["x_ideal"]))
|
||||||
@@ -840,4 +709,72 @@ def reload_cct_params(self):
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if hasattr(self, "log_gui"):
|
if hasattr(self, "log_gui"):
|
||||||
self.log_gui.log(f"重新加载色度参数失败: {str(e)}")
|
self.log_gui.log(f"重新加载色度参数失败: {str(e)}", level="error")
|
||||||
|
|
||||||
|
|
||||||
|
def toggle_cct_params_frame(self):
|
||||||
|
"""根据测试类型和测试项的选中状态显示对应参数框"""
|
||||||
|
selected_items = self.get_selected_test_items()
|
||||||
|
current_test_type = self.config.current_test_type
|
||||||
|
|
||||||
|
# ========== 默认隐藏所有参数框 ==========
|
||||||
|
self.cct_params_frame.pack_forget()
|
||||||
|
self.sdr_cct_params_frame.pack_forget()
|
||||||
|
|
||||||
|
# HDR 色度参数框(如果存在的话)
|
||||||
|
if hasattr(self, "hdr_cct_params_frame"):
|
||||||
|
self.hdr_cct_params_frame.pack_forget()
|
||||||
|
|
||||||
|
# ========== 根据测试类型和选中项显示对应参数框 ==========
|
||||||
|
if current_test_type == "screen_module":
|
||||||
|
# 屏模组:只有色度参数
|
||||||
|
if "cct" in selected_items:
|
||||||
|
self.cct_params_frame.pack(fill=tk.X, padx=5, pady=5)
|
||||||
|
|
||||||
|
elif current_test_type == "sdr_movie":
|
||||||
|
# SDR:只有色度参数(色准不需要参数设置框)
|
||||||
|
if "cct" in selected_items:
|
||||||
|
self.sdr_cct_params_frame.pack(fill=tk.X, padx=5, pady=5)
|
||||||
|
|
||||||
|
elif current_test_type == "hdr_movie":
|
||||||
|
# HDR:只有色度参数(色准不需要参数设置框)
|
||||||
|
if "cct" in selected_items:
|
||||||
|
if hasattr(self, "hdr_cct_params_frame"):
|
||||||
|
self.hdr_cct_params_frame.pack(fill=tk.X, padx=5, pady=5)
|
||||||
|
else:
|
||||||
|
if hasattr(self, "log_gui"):
|
||||||
|
self.log_gui.log("HDR 色度参数框尚未创建", level="error")
|
||||||
|
|
||||||
|
|
||||||
|
# ---- gamut 参考标准改变回调(统一实现) ----
|
||||||
|
_GAMUT_REF_CONFIGS = {
|
||||||
|
"screen_module": {"var_attr": "screen_gamut_ref_var", "label": "屏模组"},
|
||||||
|
"sdr_movie": {"var_attr": "sdr_gamut_ref_var", "label": "SDR"},
|
||||||
|
"hdr_movie": {"var_attr": "hdr_gamut_ref_var", "label": "HDR"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _on_gamut_ref_changed(self, test_type, event=None):
|
||||||
|
cfg = _GAMUT_REF_CONFIGS[test_type]
|
||||||
|
try:
|
||||||
|
new_ref = getattr(self, cfg["var_attr"]).get()
|
||||||
|
self.log_gui.log(f"{cfg['label']} 色域参考标准已更改为: {new_ref}", level="success")
|
||||||
|
|
||||||
|
if test_type not in self.config.current_test_types:
|
||||||
|
self.config.current_test_types[test_type] = {}
|
||||||
|
self.config.current_test_types[test_type]["gamut_reference"] = new_ref
|
||||||
|
self.save_pq_config()
|
||||||
|
except Exception as e:
|
||||||
|
self.log_gui.log(f"保存 {cfg['label']} 色域参考标准失败: {str(e)}", level="error")
|
||||||
|
|
||||||
|
|
||||||
|
def on_screen_gamut_ref_changed(self, event=None):
|
||||||
|
_on_gamut_ref_changed(self, "screen_module", event)
|
||||||
|
|
||||||
|
|
||||||
|
def on_sdr_gamut_ref_changed(self, event=None):
|
||||||
|
_on_gamut_ref_changed(self, "sdr_movie", event)
|
||||||
|
|
||||||
|
|
||||||
|
def on_hdr_gamut_ref_changed(self, event=None):
|
||||||
|
_on_gamut_ref_changed(self, "hdr_movie", event)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""自定义模板结果面板(Step 6 重构)。"""
|
"""自定义模板结果面板(Step 6 重构)。"""
|
||||||
|
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
@@ -232,11 +232,11 @@ def start_custom_row_single_step(self):
|
|||||||
children = list(self.custom_result_tree.get_children())
|
children = list(self.custom_result_tree.get_children())
|
||||||
row_no = children.index(item_id) + 1 if item_id in children else 1
|
row_no = children.index(item_id) + 1 if item_id in children else 1
|
||||||
|
|
||||||
self._clear_custom_result_row(item_id, row_no)
|
_clear_custom_result_row(self, item_id, row_no)
|
||||||
|
|
||||||
threading.Thread(
|
threading.Thread(
|
||||||
target=self._run_custom_row_single_step,
|
target=_run_custom_row_single_step,
|
||||||
args=(item_id, row_no),
|
args=(self, item_id, row_no),
|
||||||
daemon=True,
|
daemon=True,
|
||||||
).start()
|
).start()
|
||||||
|
|
||||||
@@ -274,7 +274,7 @@ def _run_custom_row_single_step(self, item_id, row_no):
|
|||||||
"""后台执行客户模板单步测试"""
|
"""后台执行客户模板单步测试"""
|
||||||
try:
|
try:
|
||||||
self._dispatch_ui(self.status_var.set, f"单步测试第 {row_no} 行...")
|
self._dispatch_ui(self.status_var.set, f"单步测试第 {row_no} 行...")
|
||||||
self.log_gui.log(f"开始单步测试第 {row_no} 行")
|
self.log_gui.log(f"开始单步测试第 {row_no} 行", level="info")
|
||||||
|
|
||||||
self.config.set_current_pattern("custom")
|
self.config.set_current_pattern("custom")
|
||||||
|
|
||||||
@@ -295,7 +295,7 @@ def _run_custom_row_single_step(self, item_id, row_no):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if row_no > len(converted_params):
|
if row_no > len(converted_params):
|
||||||
self.log_gui.log(f"❌ 行号超出 pattern 范围: {row_no}/{len(converted_params)}")
|
self.log_gui.log(f"行号超出 pattern 范围: {row_no}/{len(converted_params)}", level="error")
|
||||||
self._dispatch_ui(self.status_var.set, "单步测试失败:行号超范围")
|
self._dispatch_ui(self.status_var.set, "单步测试失败:行号超范围")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -332,14 +332,14 @@ def _run_custom_row_single_step(self, item_id, row_no):
|
|||||||
}
|
}
|
||||||
|
|
||||||
self._dispatch_ui(
|
self._dispatch_ui(
|
||||||
self._update_custom_result_row, item_id, row_no, row_data
|
_update_custom_result_row, self, item_id, row_no, row_data
|
||||||
)
|
)
|
||||||
|
|
||||||
self.log_gui.log(f"✓ 第 {row_no} 行单步测试完成并已覆盖")
|
self.log_gui.log(f"第 {row_no} 行单步测试完成并已覆盖", level="success")
|
||||||
self._dispatch_ui(self.status_var.set, f"第 {row_no} 行单步测试完成")
|
self._dispatch_ui(self.status_var.set, f"第 {row_no} 行单步测试完成")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_gui.log(f"❌ 单步测试失败: {str(e)}")
|
self.log_gui.log(f"单步测试失败: {str(e)}", level="error")
|
||||||
self._dispatch_ui(self.status_var.set, "单步测试失败")
|
self._dispatch_ui(self.status_var.set, "单步测试失败")
|
||||||
|
|
||||||
|
|
||||||
@@ -423,50 +423,7 @@ def copy_custom_result_table(self):
|
|||||||
if hasattr(self, "status_var"):
|
if hasattr(self, "status_var"):
|
||||||
self.status_var.set(f"已复制 {len(items)} 行客户模板数据到剪贴板")
|
self.status_var.set(f"已复制 {len(items)} 行客户模板数据到剪贴板")
|
||||||
if hasattr(self, "log_gui"):
|
if hasattr(self, "log_gui"):
|
||||||
self.log_gui.log(f"✓ 已复制客户模板表格数据({len(items)} 行)")
|
self.log_gui.log(f"已复制客户模板表格数据({len(items)} 行)", level="success")
|
||||||
|
|
||||||
|
|
||||||
def fill_custom_result_test_data(self):
|
|
||||||
"""填充 147 行客户模板测试数据(用于界面验证)"""
|
|
||||||
if not hasattr(self, "custom_result_tree"):
|
|
||||||
return
|
|
||||||
|
|
||||||
self.clear_custom_template_results()
|
|
||||||
|
|
||||||
pattern_names = []
|
|
||||||
if hasattr(self, "config") and hasattr(self.config, "get_temp_pattern_names"):
|
|
||||||
pattern_names = self.config.get_temp_pattern_names()
|
|
||||||
|
|
||||||
total_rows = 147
|
|
||||||
for i in range(1, total_rows + 1):
|
|
||||||
ratio = (i - 1) / (total_rows - 1) if total_rows > 1 else 0
|
|
||||||
row_data = {
|
|
||||||
"pattern_name": (
|
|
||||||
pattern_names[i - 1] if i - 1 < len(pattern_names) else f"P {i}"
|
|
||||||
),
|
|
||||||
"X": 0.8 + ratio * 120,
|
|
||||||
"Y": 0.9 + ratio * 135,
|
|
||||||
"Z": 1.1 + ratio * 145,
|
|
||||||
"x": 0.24 + ratio * 0.10,
|
|
||||||
"y": 0.26 + ratio * 0.10,
|
|
||||||
"Lv": 1.0 + ratio * 500,
|
|
||||||
"u_prime": 0.16 + ratio * 0.12,
|
|
||||||
"v_prime": 0.42 + ratio * 0.08,
|
|
||||||
"Tcp": 1800 + ratio * 12000,
|
|
||||||
"duv": -0.01 + ratio * 0.03,
|
|
||||||
"lambda_d": 430 + ratio * 200,
|
|
||||||
"Pe": 10 + ratio * 90,
|
|
||||||
}
|
|
||||||
self.append_custom_template_result(i, row_data)
|
|
||||||
|
|
||||||
if hasattr(self, "chart_notebook") and hasattr(self, "custom_template_tab_frame"):
|
|
||||||
self.chart_notebook.select(self.custom_template_tab_frame)
|
|
||||||
|
|
||||||
if hasattr(self, "status_var"):
|
|
||||||
self.status_var.set("已填充 147 行客户模板测试数据")
|
|
||||||
if hasattr(self, "log_gui"):
|
|
||||||
self.log_gui.log("✓ 已填充 147 行客户模板测试数据")
|
|
||||||
|
|
||||||
|
|
||||||
def clear_custom_template_results(self):
|
def clear_custom_template_results(self):
|
||||||
"""清空客户模板结果表格"""
|
"""清空客户模板结果表格"""
|
||||||
@@ -511,7 +468,7 @@ def auto_expand_custom_result_view(self):
|
|||||||
self.root.update_idletasks()
|
self.root.update_idletasks()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if hasattr(self, "log_gui"):
|
if hasattr(self, "log_gui"):
|
||||||
self.log_gui.log(f"⚠️ 自动扩展客户模板窗口失败: {str(e)}")
|
self.log_gui.log(f"自动扩展客户模板窗口失败: {str(e)}", level="error")
|
||||||
|
|
||||||
|
|
||||||
def append_custom_template_result(self, row_no, result_data):
|
def append_custom_template_result(self, row_no, result_data):
|
||||||
@@ -605,4 +562,55 @@ def start_custom_template_test(self):
|
|||||||
self.test_thread.daemon = True
|
self.test_thread.daemon = True
|
||||||
self.test_thread.start()
|
self.test_thread.start()
|
||||||
|
|
||||||
|
def update_custom_button_visibility(self):
|
||||||
|
"""只在 SDR 测试时显示客户模版按钮"""
|
||||||
|
if not hasattr(self, "custom_btn") or not hasattr(self, "test_type_var"):
|
||||||
|
return
|
||||||
|
if self.test_type_var.get() == "sdr_movie":
|
||||||
|
if not self.custom_btn.winfo_manager():
|
||||||
|
self.custom_btn.pack(side=tk.LEFT, padx=5)
|
||||||
|
else:
|
||||||
|
if self.custom_btn.winfo_manager():
|
||||||
|
self.custom_btn.pack_forget()
|
||||||
|
|
||||||
|
|
||||||
|
# def fill_custom_result_test_data(self):
|
||||||
|
# """填充 147 行客户模板测试数据(用于界面验证)"""
|
||||||
|
# if not hasattr(self, "custom_result_tree"):
|
||||||
|
# return
|
||||||
|
|
||||||
|
# self.clear_custom_template_results()
|
||||||
|
|
||||||
|
# pattern_names = []
|
||||||
|
# if hasattr(self, "config") and hasattr(self.config, "get_temp_pattern_names"):
|
||||||
|
# pattern_names = self.config.get_temp_pattern_names()
|
||||||
|
|
||||||
|
# total_rows = 147
|
||||||
|
# for i in range(1, total_rows + 1):
|
||||||
|
# ratio = (i - 1) / (total_rows - 1) if total_rows > 1 else 0
|
||||||
|
# row_data = {
|
||||||
|
# "pattern_name": (
|
||||||
|
# pattern_names[i - 1] if i - 1 < len(pattern_names) else f"P {i}"
|
||||||
|
# ),
|
||||||
|
# "X": 0.8 + ratio * 120,
|
||||||
|
# "Y": 0.9 + ratio * 135,
|
||||||
|
# "Z": 1.1 + ratio * 145,
|
||||||
|
# "x": 0.24 + ratio * 0.10,
|
||||||
|
# "y": 0.26 + ratio * 0.10,
|
||||||
|
# "Lv": 1.0 + ratio * 500,
|
||||||
|
# "u_prime": 0.16 + ratio * 0.12,
|
||||||
|
# "v_prime": 0.42 + ratio * 0.08,
|
||||||
|
# "Tcp": 1800 + ratio * 12000,
|
||||||
|
# "duv": -0.01 + ratio * 0.03,
|
||||||
|
# "lambda_d": 430 + ratio * 200,
|
||||||
|
# "Pe": 10 + ratio * 90,
|
||||||
|
# }
|
||||||
|
# self.append_custom_template_result(i, row_data)
|
||||||
|
|
||||||
|
# if hasattr(self, "chart_notebook") and hasattr(self, "custom_template_tab_frame"):
|
||||||
|
# self.chart_notebook.select(self.custom_template_tab_frame)
|
||||||
|
|
||||||
|
# if hasattr(self, "status_var"):
|
||||||
|
# self.status_var.set("已填充 147 行客户模板测试数据")
|
||||||
|
# if hasattr(self, "log_gui"):
|
||||||
|
# self.log_gui.log("已填充 147 行客户模板测试数据", level="success")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""主布局面板创建函数(Step 6 重构)。"""
|
"""主布局面板创建函数(Step 6 重构)。"""
|
||||||
|
|
||||||
|
import re
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
import ttkbootstrap as ttk
|
import ttkbootstrap as ttk
|
||||||
|
|
||||||
@@ -404,7 +405,7 @@ def create_test_type_frame(self):
|
|||||||
fill=tk.X, padx=10, pady=10
|
fill=tk.X, padx=10, pady=10
|
||||||
)
|
)
|
||||||
|
|
||||||
# ✅ 只保留日志按钮
|
# 只保留日志按钮
|
||||||
self.log_btn = ttk.Button(
|
self.log_btn = ttk.Button(
|
||||||
self.sidebar_frame,
|
self.sidebar_frame,
|
||||||
text="测试日志",
|
text="测试日志",
|
||||||
@@ -424,12 +425,24 @@ def create_test_type_frame(self):
|
|||||||
)
|
)
|
||||||
self.local_dimming_btn.pack(fill=tk.X, padx=0, pady=1)
|
self.local_dimming_btn.pack(fill=tk.X, padx=0, pady=1)
|
||||||
|
|
||||||
# 注册面板按钮(只保留日志)
|
# AI 图片对话按钮
|
||||||
|
self.ai_image_btn = ttk.Button(
|
||||||
|
self.sidebar_frame,
|
||||||
|
text="AI 图片",
|
||||||
|
style="Sidebar.TButton",
|
||||||
|
command=self.toggle_ai_image_panel,
|
||||||
|
takefocus=False,
|
||||||
|
)
|
||||||
|
self.ai_image_btn.pack(fill=tk.X, padx=0, pady=1)
|
||||||
|
|
||||||
|
# 注册面板按钮
|
||||||
if hasattr(self, "panels"):
|
if hasattr(self, "panels"):
|
||||||
if "log" in self.panels:
|
if "log" in self.panels:
|
||||||
self.panels["log"]["button"] = self.log_btn
|
self.panels["log"]["button"] = self.log_btn
|
||||||
if "local_dimming" in self.panels:
|
if "local_dimming" in self.panels:
|
||||||
self.panels["local_dimming"]["button"] = self.local_dimming_btn
|
self.panels["local_dimming"]["button"] = self.local_dimming_btn
|
||||||
|
if "ai_image" in self.panels:
|
||||||
|
self.panels["ai_image"]["button"] = self.ai_image_btn
|
||||||
|
|
||||||
|
|
||||||
def update_config_info_display(self):
|
def update_config_info_display(self):
|
||||||
@@ -496,3 +509,97 @@ def create_operation_frame(self):
|
|||||||
self.update_custom_button_visibility()
|
self.update_custom_button_visibility()
|
||||||
|
|
||||||
|
|
||||||
|
def on_screen_module_timing_changed(self, event=None):
|
||||||
|
"""屏模组信号格式改变时的回调"""
|
||||||
|
try:
|
||||||
|
selected_timing = self.screen_module_timing_var.get()
|
||||||
|
|
||||||
|
# 记录日志
|
||||||
|
self.log_gui.log(f"屏模组信号格式已更改为: {selected_timing}", level="info")
|
||||||
|
|
||||||
|
match = re.search(r"(\d+)x(\d+)\s*@\s*(\d+)", selected_timing)
|
||||||
|
if match:
|
||||||
|
width = int(match.group(1))
|
||||||
|
height = int(match.group(2))
|
||||||
|
refresh_rate = int(match.group(3))
|
||||||
|
|
||||||
|
self.log_gui.log(f" ├─ 分辨率: {width}x{height}", level="info")
|
||||||
|
self.log_gui.log(f" └─ 刷新率: {refresh_rate}Hz", level="info")
|
||||||
|
|
||||||
|
# 根据分辨率给出提示
|
||||||
|
if width >= 3840: # 4K及以上
|
||||||
|
self.log_gui.log(" ℹ️ 检测到4K分辨率", level="info")
|
||||||
|
|
||||||
|
if refresh_rate >= 120:
|
||||||
|
self.log_gui.log(" ℹ️ 检测到高刷新率", level="info")
|
||||||
|
|
||||||
|
# 更新配置
|
||||||
|
self.config.set_current_timing(selected_timing)
|
||||||
|
|
||||||
|
# 如果正在测试,提示用户
|
||||||
|
if self.testing:
|
||||||
|
self.log_gui.log("警告: 测试进行中,信号格式更改将在下次测试时生效", level="error")
|
||||||
|
|
||||||
|
# 保存配置
|
||||||
|
self.save_pq_config()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.log_gui.log(f"屏模组信号格式更改失败: {str(e)}", level="error")
|
||||||
|
|
||||||
|
|
||||||
|
def update_test_items(self):
|
||||||
|
"""根据当前测试类型更新测试项目复选框"""
|
||||||
|
# 先隐藏所有测试项目框架
|
||||||
|
for config in self.test_items.values():
|
||||||
|
config["frame"].pack_forget()
|
||||||
|
|
||||||
|
current_test_type = self.config.current_test_type
|
||||||
|
self.test_vars = {}
|
||||||
|
|
||||||
|
if current_test_type in self.test_items:
|
||||||
|
config = self.test_items[current_test_type]
|
||||||
|
frame = config["frame"]
|
||||||
|
frame.pack(fill=tk.X, padx=5, pady=5)
|
||||||
|
|
||||||
|
# 添加测试类型标签
|
||||||
|
type_label = ttk.Label(
|
||||||
|
frame,
|
||||||
|
text=self.get_test_type_name(current_test_type),
|
||||||
|
style="primary.TLabel",
|
||||||
|
)
|
||||||
|
type_label.grid(row=0, column=0, columnspan=2, sticky=tk.W, padx=5, pady=3)
|
||||||
|
|
||||||
|
# 从配置中读取保存的选择状态
|
||||||
|
saved_test_items = self.config.current_test_types[current_test_type].get(
|
||||||
|
"test_items", []
|
||||||
|
)
|
||||||
|
|
||||||
|
# 添加复选框
|
||||||
|
for i, (text, var_name) in enumerate(config["items"]):
|
||||||
|
is_checked = var_name in saved_test_items
|
||||||
|
var = tk.BooleanVar(value=is_checked)
|
||||||
|
|
||||||
|
self.test_vars[f"{current_test_type}_{var_name}"] = var
|
||||||
|
ttk.Checkbutton(
|
||||||
|
frame,
|
||||||
|
text=text,
|
||||||
|
variable=var,
|
||||||
|
bootstyle="round-toggle",
|
||||||
|
command=self.update_config_and_tabs,
|
||||||
|
).grid(row=i // 2 + 1, column=i % 2, sticky=tk.W, padx=10, pady=5)
|
||||||
|
|
||||||
|
if hasattr(self, "chart_notebook"):
|
||||||
|
self.update_chart_tabs_state()
|
||||||
|
if hasattr(self, "cct_params_frame"):
|
||||||
|
self.toggle_cct_params_frame()
|
||||||
|
|
||||||
|
|
||||||
|
def on_test_type_change(self):
|
||||||
|
"""根据测试类型更新内容区域"""
|
||||||
|
# 更新配置信息显示
|
||||||
|
if hasattr(self, "config") and hasattr(self.config, "get_current_config"):
|
||||||
|
self.update_config_info_display()
|
||||||
|
|
||||||
|
# SDR 选中时显示客户模版按钮
|
||||||
|
self.update_custom_button_visibility()
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""侧边面板(日志 / Local Dimming / 调试)(Step 6 重构)。"""
|
"""侧边面板(日志 / Local Dimming / 调试)"""
|
||||||
|
|
||||||
import traceback
|
import traceback
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
@@ -141,7 +141,7 @@ def create_local_dimming_panel(self):
|
|||||||
|
|
||||||
self.ld_clear_btn = ttk.Button(
|
self.ld_clear_btn = ttk.Button(
|
||||||
bottom_frame,
|
bottom_frame,
|
||||||
text="🗑️ 清空记录",
|
text="清空记录",
|
||||||
command=self.clear_ld_records,
|
command=self.clear_ld_records,
|
||||||
bootstyle="danger-outline",
|
bootstyle="danger-outline",
|
||||||
width=12,
|
width=12,
|
||||||
@@ -182,231 +182,137 @@ def toggle_log_panel(self):
|
|||||||
self.show_panel("log")
|
self.show_panel("log")
|
||||||
|
|
||||||
|
|
||||||
def toggle_screen_debug_panel(self):
|
# ---- 单步调试面板(统一实现) ----
|
||||||
"""打开/关闭屏模组单步调试面板(独立窗口)"""
|
DEBUG_PANEL_CONFIGS = {
|
||||||
|
"screen_module": {
|
||||||
|
"window_attr": "debug_window",
|
||||||
|
"btn_attr": "screen_debug_btn",
|
||||||
|
"title": " 单步调试面板",
|
||||||
|
"window_log_prefix": "",
|
||||||
|
"data_log_prefix": "屏模组",
|
||||||
|
"failure_data_label": "调试数据",
|
||||||
|
# (item_key, debug_key, (category, subkey), data_label, enable_desc)
|
||||||
|
"data_items": [
|
||||||
|
("gamma", "gamma", ("shared", "gray"), "灰阶", "Gamma 单步调试"),
|
||||||
|
("gamut", "rgb", ("gamut", "rgb"), "RGB", "RGB 单步调试"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"sdr_movie": {
|
||||||
|
"window_attr": "sdr_debug_window",
|
||||||
|
"btn_attr": "sdr_debug_btn",
|
||||||
|
"title": " SDR 单步调试面板",
|
||||||
|
"window_log_prefix": "SDR ",
|
||||||
|
"data_log_prefix": "SDR",
|
||||||
|
"failure_data_label": "SDR 调试数据",
|
||||||
|
"data_items": [
|
||||||
|
("gamma", "gamma", ("shared", "gray"), "灰阶", "Gamma 单步调试"),
|
||||||
|
("accuracy", "accuracy", ("accuracy", "measured"), "色准", "色准单步调试"),
|
||||||
|
("gamut", "rgb", ("gamut", "rgb"), "RGB", "RGB 单步调试"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"hdr_movie": {
|
||||||
|
"window_attr": "hdr_debug_window",
|
||||||
|
"btn_attr": "hdr_debug_btn",
|
||||||
|
"title": " HDR 单步调试面板",
|
||||||
|
"window_log_prefix": "HDR ",
|
||||||
|
"data_log_prefix": "HDR",
|
||||||
|
"failure_data_label": "HDR 调试数据",
|
||||||
|
"data_items": [
|
||||||
|
("eotf", "eotf", ("shared", "gray"), "灰阶", "EOTF 单步调试"),
|
||||||
|
("accuracy", "accuracy", ("accuracy", "measured"), "色准", "色准单步调试"),
|
||||||
|
("gamut", "rgb", ("gamut", "rgb"), "RGB", "RGB 单步调试"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _toggle_debug_panel(self, test_type):
|
||||||
|
"""打开/关闭对应测试类型的单步调试面板(独立窗口)。"""
|
||||||
|
cfg = DEBUG_PANEL_CONFIGS[test_type]
|
||||||
|
win_attr = cfg["window_attr"]
|
||||||
|
btn = getattr(self, cfg["btn_attr"])
|
||||||
|
wlp = cfg["window_log_prefix"]
|
||||||
|
|
||||||
# 如果窗口已存在且可见,关闭它
|
# 如果窗口已存在且可见,关闭它
|
||||||
if hasattr(self, "debug_window") and self.debug_window.winfo_exists():
|
existing = getattr(self, win_attr, None)
|
||||||
self.debug_window.destroy()
|
if existing is not None and existing.winfo_exists():
|
||||||
self.screen_debug_btn.config(text="打开调试面板")
|
existing.destroy()
|
||||||
self.log_gui.log("✓ 单步调试面板已关闭")
|
btn.config(text="打开调试面板")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 创建新窗口
|
# 创建新窗口
|
||||||
self.debug_window = ttk.Toplevel(self.root)
|
win = ttk.Toplevel(self.root)
|
||||||
self.debug_window.title("🔧 单步调试面板")
|
win.title(cfg["title"])
|
||||||
self.debug_window.geometry("900x400")
|
win.geometry("900x400")
|
||||||
self.debug_window.transient(self.root)
|
win.transient(self.root)
|
||||||
|
setattr(self, win_attr, win)
|
||||||
|
|
||||||
# 创建调试面板容器
|
# 调试面板容器
|
||||||
debug_container = ttk.Frame(self.debug_window, padding=10)
|
debug_container = ttk.Frame(win, padding=10)
|
||||||
debug_container.pack(fill=tk.BOTH, expand=True) # ← 这个 pack 是对的
|
debug_container.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
# 创建调试面板实例
|
|
||||||
from app.views.pq_debug_panel import PQDebugPanel
|
|
||||||
|
|
||||||
|
# 创建调试面板实例(不要对它调用 pack)
|
||||||
debug_panel_instance = PQDebugPanel(debug_container, self)
|
debug_panel_instance = PQDebugPanel(debug_container, self)
|
||||||
# ← 这里不应该有任何 pack 调用!
|
|
||||||
|
|
||||||
self.log_gui.log("✓ 单步调试面板实例已创建")
|
|
||||||
|
|
||||||
# 重新启用调试(如果有数据)
|
# 重新启用调试(如果有数据)
|
||||||
try:
|
try:
|
||||||
test_type = self.config.current_test_type
|
|
||||||
selected_items = self.get_selected_test_items()
|
selected_items = self.get_selected_test_items()
|
||||||
|
dlp = cfg["data_log_prefix"]
|
||||||
if test_type == "screen_module":
|
# 显式按 test_type 拿历史结果,避免依赖"当前活跃"状态
|
||||||
if "gamma" in selected_items:
|
results_store = getattr(self, "results", None)
|
||||||
gray_data = self.results.get_intermediate_data("shared", "gray")
|
results_obj = results_store.get(test_type) if results_store is not None else None
|
||||||
if gray_data:
|
if results_obj is None:
|
||||||
self.log_gui.log(f" → 加载 {len(gray_data)} 个灰阶数据点")
|
self.log_gui.log(f"{dlp} 暂无 {test_type} 的测试结果,面板已打开", level="warning")
|
||||||
debug_panel_instance.enable_debug(
|
else:
|
||||||
"screen_module", "gamma", gray_data
|
for item_key, debug_key, (cat, sub), data_label, enable_desc in cfg["data_items"]:
|
||||||
)
|
if item_key not in selected_items:
|
||||||
self.log_gui.log("✓ 屏模组 Gamma 单步调试已重新启用")
|
continue
|
||||||
else:
|
data = results_obj.get_intermediate_data(cat, sub)
|
||||||
self.log_gui.log(" ✗ 没有可用的灰阶数据")
|
if not data:
|
||||||
|
if test_type == "screen_module" and item_key == "gamma":
|
||||||
if "gamut" in selected_items:
|
self.log_gui.log("没有可用的灰阶数据", level="error")
|
||||||
rgb_data = self.results.get_intermediate_data("gamut", "rgb")
|
continue
|
||||||
if rgb_data:
|
self.log_gui.log(f" → 加载 {len(data)} 个{data_label}数据点", level="info")
|
||||||
self.log_gui.log(f" → 加载 {len(rgb_data)} 个RGB数据点")
|
debug_panel_instance.enable_debug(test_type, debug_key, data)
|
||||||
debug_panel_instance.enable_debug(
|
|
||||||
"screen_module", "rgb", rgb_data
|
|
||||||
)
|
|
||||||
self.log_gui.log("✓ 屏模组 RGB 单步调试已重新启用")
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_gui.log(f"⚠️ 加载调试数据失败: {str(e)}")
|
self.log_gui.log(f"加载{cfg['failure_data_label']}失败: {str(e)}", level="error")
|
||||||
import traceback
|
self.log_gui.log(traceback.format_exc(), level="error")
|
||||||
|
|
||||||
self.log_gui.log(traceback.format_exc())
|
btn.config(text="关闭调试面板")
|
||||||
|
|
||||||
# 更新按钮文字
|
|
||||||
self.screen_debug_btn.config(text="关闭调试面板")
|
|
||||||
|
|
||||||
# 窗口关闭时的回调
|
|
||||||
def on_closing():
|
def on_closing():
|
||||||
self.screen_debug_btn.config(text="打开调试面板")
|
btn.config(text="打开调试面板")
|
||||||
self.debug_window.destroy()
|
getattr(self, win_attr).destroy()
|
||||||
self.log_gui.log("✓ 单步调试窗口已关闭")
|
|
||||||
|
|
||||||
self.debug_window.protocol("WM_DELETE_WINDOW", on_closing)
|
win.protocol("WM_DELETE_WINDOW", on_closing)
|
||||||
self.debug_window.update_idletasks()
|
win.update_idletasks()
|
||||||
|
|
||||||
self.log_gui.log("✓ 单步调试面板已打开(独立窗口)")
|
|
||||||
|
def toggle_screen_debug_panel(self):
|
||||||
|
_toggle_debug_panel(self, "screen_module")
|
||||||
|
|
||||||
|
|
||||||
def toggle_sdr_debug_panel(self):
|
def toggle_sdr_debug_panel(self):
|
||||||
"""打开/关闭 SDR 单步调试面板(独立窗口)"""
|
_toggle_debug_panel(self, "sdr_movie")
|
||||||
# 如果窗口已存在且可见,关闭它
|
|
||||||
if hasattr(self, "sdr_debug_window") and self.sdr_debug_window.winfo_exists():
|
|
||||||
self.sdr_debug_window.destroy()
|
|
||||||
self.sdr_debug_btn.config(text="打开调试面板")
|
|
||||||
self.log_gui.log("✓ SDR 单步调试面板已关闭")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 创建新窗口
|
|
||||||
self.sdr_debug_window = ttk.Toplevel(self.root)
|
|
||||||
self.sdr_debug_window.title("🔧 SDR 单步调试面板")
|
|
||||||
self.sdr_debug_window.geometry("900x400")
|
|
||||||
self.sdr_debug_window.transient(self.root)
|
|
||||||
|
|
||||||
# 创建调试面板容器
|
|
||||||
debug_container = ttk.Frame(self.sdr_debug_window, padding=10)
|
|
||||||
debug_container.pack(fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
# ✅ 创建调试面板实例(不要对它调用 pack)
|
|
||||||
from app.views.pq_debug_panel import PQDebugPanel
|
|
||||||
|
|
||||||
debug_panel_instance = PQDebugPanel(debug_container, self)
|
|
||||||
# ← 删除:debug_panel_instance.pack(...)
|
|
||||||
|
|
||||||
self.log_gui.log("✓ SDR 单步调试面板实例已创建")
|
|
||||||
|
|
||||||
# ✅ 重新启用调试(如果有数据)
|
|
||||||
try:
|
|
||||||
selected_items = self.get_selected_test_items()
|
|
||||||
|
|
||||||
if "gamma" in selected_items:
|
|
||||||
gray_data = self.results.get_intermediate_data("shared", "gray")
|
|
||||||
if gray_data:
|
|
||||||
self.log_gui.log(f" → 加载 {len(gray_data)} 个灰阶数据点")
|
|
||||||
debug_panel_instance.enable_debug("sdr_movie", "gamma", gray_data)
|
|
||||||
self.log_gui.log("✓ SDR Gamma 单步调试已重新启用")
|
|
||||||
|
|
||||||
if "accuracy" in selected_items:
|
|
||||||
accuracy_data = self.results.get_intermediate_data(
|
|
||||||
"accuracy", "measured"
|
|
||||||
)
|
|
||||||
if accuracy_data:
|
|
||||||
self.log_gui.log(f" → 加载 {len(accuracy_data)} 个色准数据点")
|
|
||||||
debug_panel_instance.enable_debug(
|
|
||||||
"sdr_movie", "accuracy", accuracy_data
|
|
||||||
)
|
|
||||||
self.log_gui.log("✓ SDR 色准单步调试已重新启用")
|
|
||||||
|
|
||||||
if "gamut" in selected_items:
|
|
||||||
rgb_data = self.results.get_intermediate_data("gamut", "rgb")
|
|
||||||
if rgb_data:
|
|
||||||
self.log_gui.log(f" → 加载 {len(rgb_data)} 个RGB数据点")
|
|
||||||
debug_panel_instance.enable_debug("sdr_movie", "rgb", rgb_data)
|
|
||||||
self.log_gui.log("✓ SDR RGB 单步调试已重新启用")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.log_gui.log(f"⚠️ 加载 SDR 调试数据失败: {str(e)}")
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
self.log_gui.log(traceback.format_exc())
|
|
||||||
|
|
||||||
# 更新按钮文字
|
|
||||||
self.sdr_debug_btn.config(text="关闭调试面板")
|
|
||||||
|
|
||||||
# 窗口关闭时的回调
|
|
||||||
def on_closing():
|
|
||||||
self.sdr_debug_btn.config(text="打开调试面板")
|
|
||||||
self.sdr_debug_window.destroy()
|
|
||||||
self.log_gui.log("✓ SDR 单步调试窗口已关闭")
|
|
||||||
|
|
||||||
self.sdr_debug_window.protocol("WM_DELETE_WINDOW", on_closing)
|
|
||||||
self.sdr_debug_window.update_idletasks()
|
|
||||||
|
|
||||||
self.log_gui.log("✓ SDR 单步调试面板已打开(独立窗口)")
|
|
||||||
|
|
||||||
|
|
||||||
def toggle_hdr_debug_panel(self):
|
def toggle_hdr_debug_panel(self):
|
||||||
"""打开/关闭 HDR 单步调试面板(独立窗口)"""
|
_toggle_debug_panel(self, "hdr_movie")
|
||||||
# 如果窗口已存在且可见,关闭它
|
|
||||||
if hasattr(self, "hdr_debug_window") and self.hdr_debug_window.winfo_exists():
|
|
||||||
self.hdr_debug_window.destroy()
|
|
||||||
self.hdr_debug_btn.config(text="打开调试面板")
|
|
||||||
self.log_gui.log("✓ HDR 单步调试面板已关闭")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 创建新窗口
|
|
||||||
self.hdr_debug_window = ttk.Toplevel(self.root)
|
|
||||||
self.hdr_debug_window.title("🔧 HDR 单步调试面板")
|
|
||||||
self.hdr_debug_window.geometry("900x400")
|
|
||||||
self.hdr_debug_window.transient(self.root)
|
|
||||||
|
|
||||||
# 创建调试面板容器
|
|
||||||
debug_container = ttk.Frame(self.hdr_debug_window, padding=10)
|
|
||||||
debug_container.pack(fill=tk.BOTH, expand=True)
|
|
||||||
|
|
||||||
# ✅ 创建调试面板实例(不要对它调用 pack)
|
|
||||||
from app.views.pq_debug_panel import PQDebugPanel
|
|
||||||
|
|
||||||
debug_panel_instance = PQDebugPanel(debug_container, self)
|
|
||||||
# ← 删除:debug_panel_instance.pack(...)
|
|
||||||
|
|
||||||
self.log_gui.log("✓ HDR 单步调试面板实例已创建")
|
|
||||||
|
|
||||||
# ✅ 重新启用调试(如果有数据)
|
|
||||||
try:
|
|
||||||
selected_items = self.get_selected_test_items()
|
|
||||||
|
|
||||||
if "eotf" in selected_items:
|
|
||||||
gray_data = self.results.get_intermediate_data("shared", "gray")
|
|
||||||
if gray_data:
|
|
||||||
self.log_gui.log(f" → 加载 {len(gray_data)} 个灰阶数据点")
|
|
||||||
debug_panel_instance.enable_debug("hdr_movie", "eotf", gray_data)
|
|
||||||
self.log_gui.log("✓ HDR EOTF 单步调试已重新启用")
|
|
||||||
|
|
||||||
if "accuracy" in selected_items:
|
|
||||||
accuracy_data = self.results.get_intermediate_data(
|
|
||||||
"accuracy", "measured"
|
|
||||||
)
|
|
||||||
if accuracy_data:
|
|
||||||
self.log_gui.log(f" → 加载 {len(accuracy_data)} 个色准数据点")
|
|
||||||
debug_panel_instance.enable_debug(
|
|
||||||
"hdr_movie", "accuracy", accuracy_data
|
|
||||||
)
|
|
||||||
self.log_gui.log("✓ HDR 色准单步调试已重新启用")
|
|
||||||
|
|
||||||
if "gamut" in selected_items:
|
|
||||||
rgb_data = self.results.get_intermediate_data("gamut", "rgb")
|
|
||||||
if rgb_data:
|
|
||||||
self.log_gui.log(f" → 加载 {len(rgb_data)} 个RGB数据点")
|
|
||||||
debug_panel_instance.enable_debug("hdr_movie", "rgb", rgb_data)
|
|
||||||
self.log_gui.log("✓ HDR RGB 单步调试已重新启用")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
self.log_gui.log(f"⚠️ 加载 HDR 调试数据失败: {str(e)}")
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
self.log_gui.log(traceback.format_exc())
|
|
||||||
|
|
||||||
# 更新按钮文字
|
|
||||||
self.hdr_debug_btn.config(text="关闭调试面板")
|
|
||||||
|
|
||||||
# 窗口关闭时的回调
|
|
||||||
def on_closing():
|
|
||||||
self.hdr_debug_btn.config(text="打开调试面板")
|
|
||||||
self.hdr_debug_window.destroy()
|
|
||||||
self.log_gui.log("✓ HDR 单步调试窗口已关闭")
|
|
||||||
|
|
||||||
self.hdr_debug_window.protocol("WM_DELETE_WINDOW", on_closing)
|
|
||||||
self.hdr_debug_window.update_idletasks()
|
|
||||||
|
|
||||||
self.log_gui.log("✓ HDR 单步调试面板已打开(独立窗口)")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def update_sidebar_selection(self):
|
||||||
|
"""更新侧边栏按钮的选中状态"""
|
||||||
|
# 重置所有按钮样式为默认
|
||||||
|
self.screen_module_btn.configure(style="Sidebar.TButton")
|
||||||
|
self.sdr_movie_btn.configure(style="Sidebar.TButton")
|
||||||
|
self.hdr_movie_btn.configure(style="Sidebar.TButton")
|
||||||
|
|
||||||
|
# 设置当前选中按钮的样式
|
||||||
|
current_type = self.test_type_var.get()
|
||||||
|
if current_type == "screen_module":
|
||||||
|
self.screen_module_btn.configure(style="SidebarSelected.TButton")
|
||||||
|
elif current_type == "sdr_movie":
|
||||||
|
self.sdr_movie_btn.configure(style="SidebarSelected.TButton")
|
||||||
|
elif current_type == "hdr_movie":
|
||||||
|
self.hdr_movie_btn.configure(style="SidebarSelected.TButton")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""
|
"""
|
||||||
PQ 单步调试面板
|
PQ 单步调试面板
|
||||||
支持屏模组、SDR、HDR 三种测试类型的单步调试功能
|
支持屏模组、SDR、HDR 三种测试类型的单步调试功能
|
||||||
"""
|
"""
|
||||||
@@ -34,7 +34,7 @@ class PQDebugPanel:
|
|||||||
# 原始测试数据(用于对比)
|
# 原始测试数据(用于对比)
|
||||||
self.original_data = {}
|
self.original_data = {}
|
||||||
|
|
||||||
# ==================== ✅ 创建主容器并自动 pack ====================
|
# ==================== 创建主容器并自动 pack ====================
|
||||||
self.main_container = ttk.Frame(parent)
|
self.main_container = ttk.Frame(parent)
|
||||||
self.main_container.pack(fill=tk.BOTH, expand=True)
|
self.main_container.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
@@ -600,13 +600,13 @@ class PQDebugPanel:
|
|||||||
|
|
||||||
if test_type == "screen_module":
|
if test_type == "screen_module":
|
||||||
self.screen_frame.pack(fill=tk.BOTH, expand=True)
|
self.screen_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
self.app.log_gui.log("✓ 显示屏模组调试面板")
|
self.app.log_gui.log("显示屏模组调试面板", level="success")
|
||||||
elif test_type == "sdr_movie":
|
elif test_type == "sdr_movie":
|
||||||
self.sdr_frame.pack(fill=tk.BOTH, expand=True)
|
self.sdr_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
self.app.log_gui.log("✓ 显示 SDR 调试面板")
|
self.app.log_gui.log("显示 SDR 调试面板", level="success")
|
||||||
elif test_type == "hdr_movie":
|
elif test_type == "hdr_movie":
|
||||||
self.hdr_frame.pack(fill=tk.BOTH, expand=True)
|
self.hdr_frame.pack(fill=tk.BOTH, expand=True)
|
||||||
self.app.log_gui.log("✓ 显示 HDR 调试面板")
|
self.app.log_gui.log("显示 HDR 调试面板", level="success")
|
||||||
|
|
||||||
# ==================== 启用/禁用控制 ====================
|
# ==================== 启用/禁用控制 ====================
|
||||||
|
|
||||||
@@ -631,39 +631,39 @@ class PQDebugPanel:
|
|||||||
if test_item == "gamma":
|
if test_item == "gamma":
|
||||||
self.screen_gray_combo.config(state="readonly")
|
self.screen_gray_combo.config(state="readonly")
|
||||||
self.screen_test_btn.config(state=tk.NORMAL)
|
self.screen_test_btn.config(state=tk.NORMAL)
|
||||||
self.app.log_gui.log("✓ 屏模组 Gamma 单步调试已启用")
|
self.app.log_gui.log("屏模组 Gamma 单步调试已启用", level="success")
|
||||||
elif test_item == "rgb":
|
elif test_item == "rgb":
|
||||||
self.screen_rgb_combo.config(state="readonly")
|
self.screen_rgb_combo.config(state="readonly")
|
||||||
self.screen_rgb_test_btn.config(state=tk.NORMAL)
|
self.screen_rgb_test_btn.config(state=tk.NORMAL)
|
||||||
self.app.log_gui.log("✓ 屏模组 RGB 单步调试已启用")
|
self.app.log_gui.log("屏模组 RGB 单步调试已启用", level="success")
|
||||||
|
|
||||||
elif test_type == "sdr_movie":
|
elif test_type == "sdr_movie":
|
||||||
if test_item == "gamma":
|
if test_item == "gamma":
|
||||||
self.sdr_gray_combo.config(state="readonly")
|
self.sdr_gray_combo.config(state="readonly")
|
||||||
self.sdr_gamma_test_btn.config(state=tk.NORMAL)
|
self.sdr_gamma_test_btn.config(state=tk.NORMAL)
|
||||||
self.app.log_gui.log("✓ SDR Gamma 单步调试已启用")
|
self.app.log_gui.log("SDR Gamma 单步调试已启用", level="success")
|
||||||
elif test_item == "accuracy":
|
elif test_item == "accuracy":
|
||||||
self.sdr_color_combo.config(state="readonly")
|
self.sdr_color_combo.config(state="readonly")
|
||||||
self.sdr_accuracy_test_btn.config(state=tk.NORMAL)
|
self.sdr_accuracy_test_btn.config(state=tk.NORMAL)
|
||||||
self.app.log_gui.log("✓ SDR 色准单步调试已启用")
|
self.app.log_gui.log("SDR 色准单步调试已启用", level="success")
|
||||||
elif test_item == "rgb":
|
elif test_item == "rgb":
|
||||||
self.sdr_rgb_combo.config(state="readonly")
|
self.sdr_rgb_combo.config(state="readonly")
|
||||||
self.sdr_rgb_test_btn.config(state=tk.NORMAL)
|
self.sdr_rgb_test_btn.config(state=tk.NORMAL)
|
||||||
self.app.log_gui.log("✓ SDR RGB 单步调试已启用")
|
self.app.log_gui.log("SDR RGB 单步调试已启用", level="success")
|
||||||
|
|
||||||
elif test_type == "hdr_movie":
|
elif test_type == "hdr_movie":
|
||||||
if test_item == "eotf":
|
if test_item == "eotf":
|
||||||
self.hdr_gray_combo.config(state="readonly")
|
self.hdr_gray_combo.config(state="readonly")
|
||||||
self.hdr_eotf_test_btn.config(state=tk.NORMAL)
|
self.hdr_eotf_test_btn.config(state=tk.NORMAL)
|
||||||
self.app.log_gui.log("✓ HDR EOTF 单步调试已启用")
|
self.app.log_gui.log("HDR EOTF 单步调试已启用", level="success")
|
||||||
elif test_item == "accuracy":
|
elif test_item == "accuracy":
|
||||||
self.hdr_color_combo.config(state="readonly")
|
self.hdr_color_combo.config(state="readonly")
|
||||||
self.hdr_accuracy_test_btn.config(state=tk.NORMAL)
|
self.hdr_accuracy_test_btn.config(state=tk.NORMAL)
|
||||||
self.app.log_gui.log("✓ HDR 色准单步调试已启用")
|
self.app.log_gui.log("HDR 色准单步调试已启用", level="success")
|
||||||
elif test_item == "rgb":
|
elif test_item == "rgb":
|
||||||
self.hdr_rgb_combo.config(state="readonly")
|
self.hdr_rgb_combo.config(state="readonly")
|
||||||
self.hdr_rgb_test_btn.config(state=tk.NORMAL)
|
self.hdr_rgb_test_btn.config(state=tk.NORMAL)
|
||||||
self.app.log_gui.log("✓ HDR RGB 单步调试已启用")
|
self.app.log_gui.log("HDR RGB 单步调试已启用", level="success")
|
||||||
|
|
||||||
def disable_all_debug(self):
|
def disable_all_debug(self):
|
||||||
"""禁用所有单步调试(新测试开始时调用)"""
|
"""禁用所有单步调试(新测试开始时调用)"""
|
||||||
@@ -759,11 +759,11 @@ class PQDebugPanel:
|
|||||||
def _run_single_step_thread(self, test_type, test_item, selected):
|
def _run_single_step_thread(self, test_type, test_item, selected):
|
||||||
"""单步测试线程"""
|
"""单步测试线程"""
|
||||||
try:
|
try:
|
||||||
self.app.log_gui.log("=" * 50)
|
self.app.log_gui.log("=" * 50, level="info")
|
||||||
self.app.log_gui.log(
|
self.app.log_gui.log(
|
||||||
f"开始单步调试: {test_type} - {test_item} - {selected}"
|
f"开始单步调试: {test_type} - {test_item} - {selected}"
|
||||||
)
|
, level="info")
|
||||||
self.app.log_gui.log("=" * 50)
|
self.app.log_gui.log("=" * 50, level="info")
|
||||||
|
|
||||||
# 禁用按钮
|
# 禁用按钮
|
||||||
self._disable_test_button(test_type, test_item)
|
self._disable_test_button(test_type, test_item)
|
||||||
@@ -796,9 +796,9 @@ class PQDebugPanel:
|
|||||||
x, y, lv, X, Y, Z = self.app.ca.readAllDisplay()
|
x, y, lv, X, Y, Z = self.app.ca.readAllDisplay()
|
||||||
|
|
||||||
self.app.log_gui.log(
|
self.app.log_gui.log(
|
||||||
f"✓ 测量完成: x={x:.4f}, y={y:.4f}, lv={lv:.2f}, "
|
f"测量完成: x={x:.4f}, y={y:.4f}, lv={lv:.2f}, "
|
||||||
f"X={X:.4f}, Y={Y:.4f}, Z={Z:.4f}"
|
f"X={X:.4f}, Y={Y:.4f}, Z={Z:.4f}"
|
||||||
)
|
, level="success")
|
||||||
|
|
||||||
# 对比数据
|
# 对比数据
|
||||||
self._compare_and_display(
|
self._compare_and_display(
|
||||||
@@ -809,10 +809,10 @@ class PQDebugPanel:
|
|||||||
self._enable_test_button(test_type, test_item)
|
self._enable_test_button(test_type, test_item)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.app.log_gui.log(f"❌ 单步测试失败: {str(e)}")
|
self.app.log_gui.log(f"单步测试失败: {str(e)}", level="error")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
self.app.log_gui.log(traceback.format_exc())
|
self.app.log_gui.log(traceback.format_exc(), level="error")
|
||||||
self._enable_test_button(test_type, test_item)
|
self._enable_test_button(test_type, test_item)
|
||||||
|
|
||||||
def _setup_signal_format(self, test_type):
|
def _setup_signal_format(self, test_type):
|
||||||
@@ -842,7 +842,7 @@ class PQDebugPanel:
|
|||||||
# 获取原始数据
|
# 获取原始数据
|
||||||
key = f"{test_type}_{test_item}"
|
key = f"{test_type}_{test_item}"
|
||||||
if key not in self.original_data:
|
if key not in self.original_data:
|
||||||
self.app.log_gui.log("⚠️ 未找到原始测试数据")
|
self.app.log_gui.log("未找到原始测试数据", level="error")
|
||||||
return
|
return
|
||||||
|
|
||||||
original_data_list = self.original_data[key]
|
original_data_list = self.original_data[key]
|
||||||
@@ -856,12 +856,12 @@ class PQDebugPanel:
|
|||||||
index = self.get_rgb_index(selected)
|
index = self.get_rgb_index(selected)
|
||||||
|
|
||||||
if index >= len(original_data_list):
|
if index >= len(original_data_list):
|
||||||
self.app.log_gui.log(f"⚠️ 索引超出范围: {index}")
|
self.app.log_gui.log(f"索引超出范围: {index}", level="error")
|
||||||
return
|
return
|
||||||
|
|
||||||
original_data = original_data_list[index]
|
original_data = original_data_list[index]
|
||||||
|
|
||||||
# ==================== ✅ 构建对比数据 ====================
|
# ==================== 构建对比数据 ====================
|
||||||
comparison = {}
|
comparison = {}
|
||||||
|
|
||||||
if test_item == "gamma":
|
if test_item == "gamma":
|
||||||
@@ -1140,57 +1140,57 @@ class PQDebugPanel:
|
|||||||
def _get_delta_e_from_results(self, test_type, color_name):
|
def _get_delta_e_from_results(self, test_type, color_name):
|
||||||
"""从原始测试结果中读取 ΔE 值"""
|
"""从原始测试结果中读取 ΔE 值"""
|
||||||
try:
|
try:
|
||||||
self.app.log_gui.log(f"[读取 ΔE] 开始:")
|
self.app.log_gui.log(f"[读取 ΔE] 开始:", level="info")
|
||||||
self.app.log_gui.log(f" test_type = {test_type}")
|
self.app.log_gui.log(f" test_type = {test_type}", level="info")
|
||||||
self.app.log_gui.log(f" color_name = {color_name}")
|
self.app.log_gui.log(f" color_name = {color_name}", level="info")
|
||||||
|
|
||||||
# ✅ 正确的访问方式:通过 test_items
|
# 正确的访问方式:通过 test_items
|
||||||
if "accuracy" not in self.app.results.test_items:
|
if "accuracy" not in self.app.results.test_items:
|
||||||
self.app.log_gui.log(" ✗ 未找到 accuracy 测试项")
|
self.app.log_gui.log("未找到 accuracy 测试项", level="error")
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
test_item = self.app.results.test_items["accuracy"]
|
test_item = self.app.results.test_items["accuracy"]
|
||||||
accuracy_result = test_item.final_result
|
accuracy_result = test_item.final_result
|
||||||
|
|
||||||
self.app.log_gui.log(f" accuracy_result = {accuracy_result is not None}")
|
self.app.log_gui.log(f" accuracy_result = {accuracy_result is not None}", level="info")
|
||||||
|
|
||||||
if not accuracy_result:
|
if not accuracy_result:
|
||||||
self.app.log_gui.log(" ✗ accuracy_result 为空")
|
self.app.log_gui.log("accuracy_result 为空", level="error")
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
# 获取色块名称列表和 ΔE 值列表
|
# 获取色块名称列表和 ΔE 值列表
|
||||||
color_patches = accuracy_result.get("color_patches", [])
|
color_patches = accuracy_result.get("color_patches", [])
|
||||||
delta_e_values = accuracy_result.get("delta_e_values", [])
|
delta_e_values = accuracy_result.get("delta_e_values", [])
|
||||||
|
|
||||||
self.app.log_gui.log(f" color_patches 数量: {len(color_patches)}")
|
self.app.log_gui.log(f" color_patches 数量: {len(color_patches)}", level="info")
|
||||||
self.app.log_gui.log(f" delta_e_values 数量: {len(delta_e_values)}")
|
self.app.log_gui.log(f" delta_e_values 数量: {len(delta_e_values)}", level="info")
|
||||||
|
|
||||||
if color_patches:
|
if color_patches:
|
||||||
self.app.log_gui.log(f" 前3个色块: {color_patches[:3]}")
|
self.app.log_gui.log(f" 前3个色块: {color_patches[:3]}", level="info")
|
||||||
if delta_e_values:
|
if delta_e_values:
|
||||||
self.app.log_gui.log(f" 前3个ΔE: {delta_e_values[:3]}")
|
self.app.log_gui.log(f" 前3个ΔE: {delta_e_values[:3]}", level="info")
|
||||||
|
|
||||||
# 查找对应色块的索引
|
# 查找对应色块的索引
|
||||||
try:
|
try:
|
||||||
index = color_patches.index(color_name)
|
index = color_patches.index(color_name)
|
||||||
delta_e = delta_e_values[index]
|
delta_e = delta_e_values[index]
|
||||||
self.app.log_gui.log(
|
self.app.log_gui.log(
|
||||||
f" ✓ 找到 {color_name}: index={index}, ΔE={delta_e:.2f}"
|
f" 找到 {color_name}: index={index}, ΔE={delta_e:.2f}"
|
||||||
)
|
, level="success")
|
||||||
return delta_e
|
return delta_e
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.app.log_gui.log(f" ✗ 未找到色块 '{color_name}'")
|
self.app.log_gui.log(f"未找到色块 '{color_name}'", level="error")
|
||||||
self.app.log_gui.log(f" 可用色块: {color_patches}")
|
self.app.log_gui.log(f" 可用色块: {color_patches}", level="info")
|
||||||
return 0.0
|
return 0.0
|
||||||
except IndexError:
|
except IndexError:
|
||||||
self.app.log_gui.log(f" ✗ 索引超出范围: {index}/{len(delta_e_values)}")
|
self.app.log_gui.log(f"索引超出范围: {index}/{len(delta_e_values)}", level="error")
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.app.log_gui.log(f"⚠️ 读取 ΔE 失败: {str(e)}")
|
self.app.log_gui.log(f"读取 ΔE 失败: {str(e)}", level="error")
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
self.app.log_gui.log(traceback.format_exc())
|
self.app.log_gui.log(traceback.format_exc(), level="error")
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
def _calculate_delta_e_for_color(
|
def _calculate_delta_e_for_color(
|
||||||
@@ -1215,5 +1215,5 @@ class PQDebugPanel:
|
|||||||
return delta_e
|
return delta_e
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.app.log_gui.log(f"⚠️ 计算 ΔE 失败: {str(e)}")
|
self.app.log_gui.log(f"计算 ΔE 失败: {str(e)}", level="error")
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|||||||
@@ -1,31 +1,160 @@
|
|||||||
|
import threading
|
||||||
|
from datetime import datetime
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
import ttkbootstrap as ttk
|
import ttkbootstrap as ttk
|
||||||
|
|
||||||
class PQLogGUI(ttk.Frame):
|
class PQLogGUI(ttk.Frame):
|
||||||
|
VALID_LEVELS = {"info", "success", "warning", "error", "debug", "separator", "blank"}
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
self._line_count = 0
|
||||||
|
self._max_lines = 1500
|
||||||
self.create_widgets()
|
self.create_widgets()
|
||||||
|
|
||||||
def create_widgets(self):
|
def create_widgets(self):
|
||||||
log_frame = ttk.LabelFrame(self, text="测试日志")
|
log_frame = ttk.LabelFrame(self, text="测试日志")
|
||||||
log_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
log_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
self.log_text = ttk.Text(log_frame, height=8, width=50)
|
toolbar = ttk.Frame(log_frame)
|
||||||
|
toolbar.pack(fill=tk.X, padx=6, pady=(6, 2))
|
||||||
|
|
||||||
|
self.log_summary_var = tk.StringVar(value="0 条日志")
|
||||||
|
ttk.Label(
|
||||||
|
toolbar,
|
||||||
|
textvariable=self.log_summary_var,
|
||||||
|
bootstyle="secondary",
|
||||||
|
).pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
ttk.Button(
|
||||||
|
toolbar,
|
||||||
|
text="清空日志",
|
||||||
|
command=self.clear_log,
|
||||||
|
bootstyle="secondary-outline",
|
||||||
|
width=10,
|
||||||
|
).pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
text_container = ttk.Frame(log_frame)
|
||||||
|
text_container.pack(fill=tk.BOTH, expand=True, padx=6, pady=(0, 6))
|
||||||
|
|
||||||
|
self.log_text = tk.Text(
|
||||||
|
text_container,
|
||||||
|
height=10,
|
||||||
|
width=50,
|
||||||
|
wrap=tk.WORD,
|
||||||
|
font=("Consolas", 10),
|
||||||
|
bg="#fbfcfe",
|
||||||
|
fg="#1f2937",
|
||||||
|
relief=tk.FLAT,
|
||||||
|
bd=0,
|
||||||
|
padx=10,
|
||||||
|
pady=8,
|
||||||
|
spacing1=2,
|
||||||
|
spacing3=2,
|
||||||
|
insertbackground="#1f2937",
|
||||||
|
)
|
||||||
self.log_text.pack(fill=tk.BOTH, expand=True, side=tk.LEFT)
|
self.log_text.pack(fill=tk.BOTH, expand=True, side=tk.LEFT)
|
||||||
|
|
||||||
log_scrollbar = ttk.Scrollbar(log_frame, command=self.log_text.yview)
|
log_scrollbar = ttk.Scrollbar(text_container, command=self.log_text.yview)
|
||||||
log_scrollbar.pack(fill=tk.Y, side=tk.RIGHT)
|
log_scrollbar.pack(fill=tk.Y, side=tk.RIGHT)
|
||||||
self.log_text.config(yscrollcommand=log_scrollbar.set)
|
self.log_text.config(yscrollcommand=log_scrollbar.set)
|
||||||
|
|
||||||
|
self._configure_tags()
|
||||||
self.log_text.config(state=tk.DISABLED)
|
self.log_text.config(state=tk.DISABLED)
|
||||||
|
|
||||||
def log(self, message):
|
def log(self, message, level="info"):
|
||||||
|
if threading.current_thread() is not threading.main_thread():
|
||||||
|
self.after(0, self.log, message, level)
|
||||||
|
return
|
||||||
|
|
||||||
|
text = "" if message is None else str(message)
|
||||||
|
normalized_level = self._normalize_level(level, text)
|
||||||
self.log_text.config(state=tk.NORMAL)
|
self.log_text.config(state=tk.NORMAL)
|
||||||
self.log_text.insert(tk.END, message + "\n")
|
self._append_message(text, normalized_level)
|
||||||
self.log_text.see(tk.END)
|
self.log_text.see(tk.END)
|
||||||
self.log_text.config(state=tk.DISABLED)
|
self.log_text.config(state=tk.DISABLED)
|
||||||
|
|
||||||
def clear_log(self):
|
def clear_log(self):
|
||||||
|
if threading.current_thread() is not threading.main_thread():
|
||||||
|
self.after(0, self.clear_log)
|
||||||
|
return
|
||||||
|
|
||||||
self.log_text.config(state=tk.NORMAL)
|
self.log_text.config(state=tk.NORMAL)
|
||||||
self.log_text.delete(1.0, tk.END)
|
self.log_text.delete(1.0, tk.END)
|
||||||
self.log_text.config(state=tk.DISABLED)
|
self.log_text.config(state=tk.DISABLED)
|
||||||
|
self._line_count = 0
|
||||||
|
self._update_summary()
|
||||||
|
|
||||||
|
def _configure_tags(self):
|
||||||
|
self.log_text.tag_configure("timestamp", foreground="#6b7280")
|
||||||
|
self.log_text.tag_configure("level_info", foreground="#2563eb")
|
||||||
|
self.log_text.tag_configure("level_success", foreground="#0f766e")
|
||||||
|
self.log_text.tag_configure("level_warning", foreground="#b45309")
|
||||||
|
self.log_text.tag_configure("level_error", foreground="#b91c1c")
|
||||||
|
self.log_text.tag_configure("level_debug", foreground="#7c3aed")
|
||||||
|
self.log_text.tag_configure("message", foreground="#1f2937")
|
||||||
|
self.log_text.tag_configure("message_success", foreground="#0f766e")
|
||||||
|
self.log_text.tag_configure("message_warning", foreground="#b45309")
|
||||||
|
self.log_text.tag_configure("message_error", foreground="#991b1b")
|
||||||
|
self.log_text.tag_configure("message_debug", foreground="#6d28d9")
|
||||||
|
self.log_text.tag_configure("separator", foreground="#94a3b8")
|
||||||
|
self.log_text.tag_configure("traceback", foreground="#7f1d1d")
|
||||||
|
self.log_text.tag_configure("blank", spacing1=4, spacing3=4)
|
||||||
|
|
||||||
|
def _append_message(self, message, level):
|
||||||
|
lines = message.splitlines() or [""]
|
||||||
|
for line in lines:
|
||||||
|
self._append_line(line, level)
|
||||||
|
self._trim_excess_lines()
|
||||||
|
self._update_summary()
|
||||||
|
|
||||||
|
def _append_line(self, line, level):
|
||||||
|
timestamp = datetime.now().strftime("%H:%M:%S")
|
||||||
|
rendered = "" if line is None else str(line).strip()
|
||||||
|
|
||||||
|
if level == "blank" or not rendered:
|
||||||
|
self.log_text.insert(tk.END, "\n", ("blank",))
|
||||||
|
self._line_count += 1
|
||||||
|
return
|
||||||
|
|
||||||
|
if level == "separator":
|
||||||
|
self.log_text.insert(tk.END, f"[{timestamp}] ", ("timestamp",))
|
||||||
|
self.log_text.insert(tk.END, "[SECTION] ", ("level_info",))
|
||||||
|
self.log_text.insert(tk.END, rendered + "\n", ("separator",))
|
||||||
|
self._line_count += 1
|
||||||
|
return
|
||||||
|
|
||||||
|
level_tag = f"level_{level}"
|
||||||
|
level_label = level.upper().ljust(7)
|
||||||
|
if level == "error" and rendered.startswith("Traceback"):
|
||||||
|
message_tag = "traceback"
|
||||||
|
elif level in {"success", "warning", "error", "debug"}:
|
||||||
|
message_tag = f"message_{level}"
|
||||||
|
else:
|
||||||
|
message_tag = "message"
|
||||||
|
|
||||||
|
self.log_text.insert(tk.END, f"[{timestamp}] ", ("timestamp",))
|
||||||
|
self.log_text.insert(tk.END, f"[{level_label}] ", (level_tag,))
|
||||||
|
self.log_text.insert(tk.END, rendered + "\n", (message_tag,))
|
||||||
|
self._line_count += 1
|
||||||
|
|
||||||
|
def _normalize_level(self, level, message):
|
||||||
|
normalized = "info" if level is None else str(level).strip().lower()
|
||||||
|
if normalized not in self.VALID_LEVELS:
|
||||||
|
normalized = "info"
|
||||||
|
|
||||||
|
if normalized == "info" and (message is None or str(message).strip() == ""):
|
||||||
|
return "blank"
|
||||||
|
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
def _trim_excess_lines(self):
|
||||||
|
overflow = self._line_count - self._max_lines
|
||||||
|
if overflow <= 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.log_text.delete("1.0", f"{overflow + 1}.0")
|
||||||
|
self._line_count = self._max_lines
|
||||||
|
|
||||||
|
def _update_summary(self):
|
||||||
|
self.log_summary_var.set(f"{self._line_count} 条日志")
|
||||||
5151
pqAutomationApp.py
5151
pqAutomationApp.py
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user