进一步优化save_result(提取函数)
This commit is contained in:
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"]
|
||||||
504
app/export/excel_exporter.py
Normal file
504
app/export/excel_exporter.py
Normal file
@@ -0,0 +1,504 @@
|
|||||||
|
"""测试结果 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(" ✓ 添加色域数据")
|
||||||
|
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} 数据")
|
||||||
|
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(" ✓ 添加色度一致性数据")
|
||||||
|
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(" ✓ 添加对比度数据")
|
||||||
|
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
|
||||||
|
|
||||||
|
excel_path = os.path.join(result_dir, "测试数据.xlsx")
|
||||||
|
wb.save(excel_path)
|
||||||
|
|
||||||
|
log("✓ 已保存: 测试数据.xlsx")
|
||||||
|
log("=" * 60)
|
||||||
|
|
||||||
|
except ImportError:
|
||||||
|
log("⚠️ 未安装 openpyxl 库,跳过 Excel 导出")
|
||||||
|
log(" 安装方法: pip install openpyxl")
|
||||||
|
except Exception as e:
|
||||||
|
log(f"⚠️ Excel 导出失败: {str(e)}")
|
||||||
|
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}")
|
||||||
@@ -29,6 +29,11 @@ from colormath.color_objects import xyYColor, LabColor
|
|||||||
from colormath.color_conversions import convert_color
|
from colormath.color_conversions import convert_color
|
||||||
from colormath.color_diff import delta_e_cie2000
|
from colormath.color_diff import delta_e_cie2000
|
||||||
from app.views.pq_debug_panel import PQDebugPanel
|
from app.views.pq_debug_panel import PQDebugPanel
|
||||||
|
from app.export import (
|
||||||
|
save_result_images as _save_result_images_impl,
|
||||||
|
export_excel_report as _export_excel_report_impl,
|
||||||
|
EXCEL_EXPORT_CONFIG as _EXCEL_EXPORT_CONFIG,
|
||||||
|
)
|
||||||
|
|
||||||
# Step 0/1 重构:资源工具和纯算法已迁移到 app/ 包,这里重新导入以保持
|
# Step 0/1 重构:资源工具和纯算法已迁移到 app/ 包,这里重新导入以保持
|
||||||
# 对原函数名/方法名的向后兼容(老代码内部仍用 self.calculate_* 调用)。
|
# 对原函数名/方法名的向后兼容(老代码内部仍用 self.calculate_* 调用)。
|
||||||
@@ -2864,37 +2869,9 @@ class PQAutomationApp:
|
|||||||
# ========== 延迟1秒后执行清理 ==========
|
# ========== 延迟1秒后执行清理 ==========
|
||||||
self.root.after(1000, cleanup_and_finish)
|
self.root.after(1000, cleanup_and_finish)
|
||||||
|
|
||||||
# ==================== Excel 导出配置 ====================
|
# ==================== 保存测试结果 ====================
|
||||||
_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 save_results(self):
|
def save_results(self):
|
||||||
"""保存测试结果(图片 + Excel)"""
|
"""保存测试结果(图片 + Excel)。实现委派给 app.export。"""
|
||||||
save_dir = filedialog.askdirectory(title="选择保存测试结果的目录")
|
save_dir = filedialog.askdirectory(title="选择保存测试结果的目录")
|
||||||
if not save_dir:
|
if not save_dir:
|
||||||
return
|
return
|
||||||
@@ -2907,25 +2884,30 @@ class PQAutomationApp:
|
|||||||
|
|
||||||
current_test_type = self.test_type_var.get()
|
current_test_type = self.test_type_var.get()
|
||||||
selected_items = self.get_selected_test_items()
|
selected_items = self.get_selected_test_items()
|
||||||
|
log = self.log_gui.log
|
||||||
|
|
||||||
self.log_gui.log(f"保存测试类型: {current_test_type}")
|
log(f"保存测试类型: {current_test_type}")
|
||||||
self.log_gui.log(f"已选测试项: {selected_items}")
|
log(f"已选测试项: {selected_items}")
|
||||||
|
|
||||||
# 1) 图片
|
# 1) 图片
|
||||||
self._save_result_images(result_dir, current_test_type, selected_items)
|
_save_result_images_impl(
|
||||||
|
result_dir, current_test_type, selected_items,
|
||||||
|
lambda attr: getattr(self, attr, None),
|
||||||
|
log,
|
||||||
|
)
|
||||||
|
|
||||||
# 2) Excel
|
# 2) Excel
|
||||||
if (
|
if (current_test_type in _EXCEL_EXPORT_CONFIG
|
||||||
current_test_type in self._EXCEL_EXPORT_CONFIG
|
and hasattr(self, "results") and self.results):
|
||||||
and hasattr(self, "results")
|
_export_excel_report_impl(
|
||||||
and self.results
|
result_dir, current_test_type, selected_items,
|
||||||
):
|
self.results, log,
|
||||||
self._export_excel_report(result_dir, current_test_type, selected_items)
|
)
|
||||||
|
|
||||||
# 3) 成功提示
|
# 3) 成功提示
|
||||||
self.log_gui.log("=" * 50)
|
log("=" * 50)
|
||||||
self.log_gui.log(f"✅ 测试结果已保存到目录: {result_dir}")
|
log(f"✅ 测试结果已保存到目录: {result_dir}")
|
||||||
self.log_gui.log("=" * 50)
|
log("=" * 50)
|
||||||
messagebox.showinfo("成功", f"测试结果已保存到目录:\n{result_dir}")
|
messagebox.showinfo("成功", f"测试结果已保存到目录:\n{result_dir}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -2935,515 +2917,6 @@ class PQAutomationApp:
|
|||||||
self.log_gui.log(traceback.format_exc())
|
self.log_gui.log(traceback.format_exc())
|
||||||
messagebox.showerror("错误", f"保存测试结果失败: {str(e)}")
|
messagebox.showerror("错误", f"保存测试结果失败: {str(e)}")
|
||||||
|
|
||||||
# ==================== 图片保存 ====================
|
|
||||||
def _save_result_images(self, result_dir, current_test_type, selected_items):
|
|
||||||
"""根据测试类型和已选项保存各测试图表 PNG。"""
|
|
||||||
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),
|
|
||||||
]
|
|
||||||
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 = getattr(self, fig_attr, None)
|
|
||||||
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")
|
|
||||||
self.log_gui.log(f"✓ 已保存: {filename}")
|
|
||||||
|
|
||||||
# ==================== Excel 导出总入口 ====================
|
|
||||||
def _export_excel_report(self, result_dir, current_test_type, selected_items):
|
|
||||||
"""根据测试类型生成统一格式的 Excel 报告。"""
|
|
||||||
cfg = self._EXCEL_EXPORT_CONFIG[current_test_type]
|
|
||||||
try:
|
|
||||||
import openpyxl
|
|
||||||
from openpyxl.styles import (
|
|
||||||
Font,
|
|
||||||
Alignment,
|
|
||||||
PatternFill,
|
|
||||||
Border,
|
|
||||||
Side,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.log_gui.log("=" * 60)
|
|
||||||
self.log_gui.log(f"开始生成{cfg['log_prefix']} Excel 数据报告...")
|
|
||||||
|
|
||||||
wb = openpyxl.Workbook()
|
|
||||||
ws = wb.active
|
|
||||||
ws.title = "测试数据"
|
|
||||||
|
|
||||||
styles = self._build_excel_styles(Font, Alignment, PatternFill, Border, Side)
|
|
||||||
|
|
||||||
self._excel_write_title(ws, styles, cfg["title"])
|
|
||||||
row = self._excel_write_basic_info(ws, styles, 3, cfg["type_label"])
|
|
||||||
row += 1 # 空行
|
|
||||||
|
|
||||||
if "gamut" in selected_items:
|
|
||||||
row = self._excel_section_gamut(ws, styles, row)
|
|
||||||
if cfg["curve_type"] == "gamma" and "gamma" in selected_items:
|
|
||||||
row = self._excel_section_curve(ws, styles, row, "gamma")
|
|
||||||
if cfg["curve_type"] == "eotf" and "eotf" in selected_items:
|
|
||||||
row = self._excel_section_curve(ws, styles, row, "eotf")
|
|
||||||
if "cct" in selected_items:
|
|
||||||
row = self._excel_section_cct(ws, styles, row)
|
|
||||||
if "contrast" in selected_items:
|
|
||||||
row = self._excel_section_contrast(ws, styles, row)
|
|
||||||
if cfg["has_accuracy"] and "accuracy" in selected_items:
|
|
||||||
row = self._excel_section_accuracy(ws, styles, row)
|
|
||||||
|
|
||||||
for col, width in cfg["column_widths"].items():
|
|
||||||
ws.column_dimensions[col].width = width
|
|
||||||
|
|
||||||
excel_path = os.path.join(result_dir, "测试数据.xlsx")
|
|
||||||
wb.save(excel_path)
|
|
||||||
|
|
||||||
self.log_gui.log("✓ 已保存: 测试数据.xlsx")
|
|
||||||
self.log_gui.log("=" * 60)
|
|
||||||
|
|
||||||
except ImportError:
|
|
||||||
self.log_gui.log("⚠️ 未安装 openpyxl 库,跳过 Excel 导出")
|
|
||||||
self.log_gui.log(" 安装方法: pip install openpyxl")
|
|
||||||
except Exception as e:
|
|
||||||
self.log_gui.log(f"⚠️ Excel 导出失败: {str(e)}")
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
self.log_gui.log(traceback.format_exc())
|
|
||||||
|
|
||||||
# ==================== 样式与通用写入 ====================
|
|
||||||
@staticmethod
|
|
||||||
def _build_excel_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),
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _excel_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
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _excel_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
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _excel_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
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _excel_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
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _excel_apply_row_style(ws, row, cols, styles, label_cols=()):
|
|
||||||
"""对一整行的指定列应用数据样式;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"]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _excel_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 _excel_section_gamut(self, ws, styles, row):
|
|
||||||
rgb_data = self.results.get_intermediate_data("gamut", "rgb")
|
|
||||||
gamut_final_result = None
|
|
||||||
if "gamut" in self.results.test_items:
|
|
||||||
gamut_final_result = self.results.test_items["gamut"].final_result
|
|
||||||
|
|
||||||
if not (rgb_data and len(rgb_data) >= 3):
|
|
||||||
return row
|
|
||||||
|
|
||||||
row = self._excel_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}%"
|
|
||||||
self._excel_apply_row_style(
|
|
||||||
ws, row, ["A", "B", "C", "D"], styles, label_cols=("A", "C")
|
|
||||||
)
|
|
||||||
row += 1
|
|
||||||
|
|
||||||
row = self._excel_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"
|
|
||||||
self._excel_apply_data_row(ws, row, ["A", "B", "C", "D"], styles)
|
|
||||||
row += 1
|
|
||||||
|
|
||||||
self.log_gui.log(" ✓ 添加色域数据")
|
|
||||||
return row + 1
|
|
||||||
|
|
||||||
# ==================== 分区:Gamma / EOTF(曲线类) ====================
|
|
||||||
def _excel_section_curve(self, ws, styles, row, curve_type):
|
|
||||||
"""curve_type: 'gamma' 或 'eotf'。两者数据结构一致,仅 key/显示名不同。"""
|
|
||||||
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 = self.results.get_intermediate_data("shared", "gray")
|
|
||||||
if not gray_data:
|
|
||||||
gray_data = self.results.get_intermediate_data(curve_type, "gray")
|
|
||||||
|
|
||||||
curve_final_result = None
|
|
||||||
if curve_type in self.results.test_items:
|
|
||||||
curve_final_result = self.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 = self._excel_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}"
|
|
||||||
self._excel_apply_row_style(
|
|
||||||
ws, row, ["A", "B", "C", "D", "E", "F"],
|
|
||||||
styles, label_cols=("A", "C", "E"),
|
|
||||||
)
|
|
||||||
row += 1
|
|
||||||
|
|
||||||
# 数据表
|
|
||||||
row = self._excel_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"
|
|
||||||
self._excel_apply_data_row(
|
|
||||||
ws, row, ["A", "B", "C", "D", "E", "F"], styles
|
|
||||||
)
|
|
||||||
row += 1
|
|
||||||
|
|
||||||
self.log_gui.log(f" ✓ 添加 {label_prefix} 数据")
|
|
||||||
return row + 1
|
|
||||||
|
|
||||||
# ==================== 分区:色度一致性 ====================
|
|
||||||
def _excel_section_cct(self, ws, styles, row):
|
|
||||||
gray_data = self.results.get_intermediate_data("shared", "gray")
|
|
||||||
if not gray_data:
|
|
||||||
gray_data = self.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 = self._excel_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}"
|
|
||||||
self._excel_apply_row_style(
|
|
||||||
ws, row, ["A", "B", "C", "D"], styles, label_cols=("A", "C")
|
|
||||||
)
|
|
||||||
row += 1
|
|
||||||
|
|
||||||
row = self._excel_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"
|
|
||||||
self._excel_apply_data_row(ws, row, ["A", "B", "C", "D"], styles)
|
|
||||||
row += 1
|
|
||||||
|
|
||||||
self.log_gui.log(" ✓ 添加色度一致性数据")
|
|
||||||
return row + 1
|
|
||||||
|
|
||||||
# ==================== 分区:对比度 ====================
|
|
||||||
def _excel_section_contrast(self, ws, styles, row):
|
|
||||||
contrast_final_result = None
|
|
||||||
if "contrast" in self.results.test_items:
|
|
||||||
contrast_final_result = self.results.test_items["contrast"].final_result
|
|
||||||
if not contrast_final_result:
|
|
||||||
return row
|
|
||||||
|
|
||||||
row = self._excel_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
|
|
||||||
|
|
||||||
self.log_gui.log(" ✓ 添加对比度数据")
|
|
||||||
return row + 1
|
|
||||||
|
|
||||||
# ==================== 分区:色准(仅 SDR/HDR) ====================
|
|
||||||
def _excel_section_accuracy(self, ws, styles, row):
|
|
||||||
accuracy_final_result = None
|
|
||||||
if "accuracy" in self.results.test_items:
|
|
||||||
accuracy_final_result = self.results.test_items["accuracy"].final_result
|
|
||||||
if not accuracy_final_result:
|
|
||||||
return row
|
|
||||||
|
|
||||||
row = self._excel_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}"
|
|
||||||
self._excel_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} 个"
|
|
||||||
self._excel_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 = self._excel_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"
|
|
||||||
|
|
||||||
self._excel_apply_data_row(
|
|
||||||
ws, row, ["A", "B", "C", "D", "E", "F", "G"], styles
|
|
||||||
)
|
|
||||||
row += 1
|
|
||||||
|
|
||||||
self.log_gui.log(" ✓ 添加色准数据(含 xy 坐标和亮度)")
|
|
||||||
return row + 1
|
|
||||||
|
|
||||||
new_pq_results = _run_new_pq_results
|
new_pq_results = _run_new_pq_results
|
||||||
run_test = _run_run_test
|
run_test = _run_run_test
|
||||||
run_screen_module_test = _run_run_screen_module_test
|
run_screen_module_test = _run_run_screen_module_test
|
||||||
|
|||||||
Reference in New Issue
Block a user