diff --git a/assets/IMG_9473.PNG b/assets/IMG_9473.PNG deleted file mode 100644 index 6b04412..0000000 Binary files a/assets/IMG_9473.PNG and /dev/null differ diff --git a/assets/gamma.ico b/assets/gamma.ico deleted file mode 100644 index 1728864..0000000 Binary files a/assets/gamma.ico and /dev/null differ diff --git a/assets/icon_check.bat b/assets/icon_check.bat deleted file mode 100644 index d27657c..0000000 --- a/assets/icon_check.bat +++ /dev/null @@ -1,93 +0,0 @@ -@echo off -setlocal ENABLEDELAYEDEXPANSION - -:: ------------------------------------------------------------ -:: Select ico file via GUI -:: ------------------------------------------------------------ -for /f "delims=" %%A in ('powershell -command ^ - "Add-Type -AssemblyName System.Windows.Forms | Out-Null; $f=New-Object Windows.Forms.OpenFileDialog; $f.Filter='Icon (*.ico)|*.ico'; if($f.ShowDialog() -eq 'OK'){Write-Output $f.FileName}"') do ( - set ICO=%%A -) - -if "%ICO%"=="" ( - echo No file selected. - pause - exit /b -) - -echo Checking icon: %ICO% -echo ----------------------------------------- - -:: ------------------------------------------------------------ -:: Extract bytes via PowerShell (safe for PNG-ICON) -:: ------------------------------------------------------------ -for /f "tokens=* delims=" %%B in ('powershell -command ^ - "[System.IO.File]::ReadAllBytes('%ICO%') -join ' '"') do ( - set RAW=%%B -) - -:: Convert byte list into array -set i=0 -for %%b in (%RAW%) do ( - set BYTE[!i!]=%%b - set /a i+=1 -) -set /a LEN=i - -:: ICONDIR.Count = bytes 4-5 -set /a COUNT=BYTE[4] + BYTE[5]*256 -echo Total frames: %COUNT% -echo. - -if %COUNT% LEQ 0 ( - echo ERROR: Invalid icon or no frames. - pause - exit /b -) - -set HAS256=0 -set FIRST256=0 - -echo Frame list: -echo ----------------------------------------- - -:: Each ICONDIRENTRY = 16 bytes starting at offset 6 -for /l %%I in (0,1,%COUNT%-1) do ( - set /a OFFSET=6 + 16 * %%I - - set W=!BYTE[%OFFSET%]! - set H=!BYTE[%OFFSET%+1]! - - if "!W!"=="0" set W=256 - if "!H!"=="0" set H=256 - - echo Frame %%I = !W! x !H! - - if "!W!"=="256" if "!H!"=="256" ( - set HAS256=1 - if %%I==0 set FIRST256=1 - ) -) - -echo. -echo ----------------------------------------- - -if %HAS256%==0 ( - echo ERROR: No 256x256 frame found. - echo This is why NSIS and Windows show a small icon. - pause - exit /b -) - -if %FIRST256%==0 ( - echo WARNING: 256x256 exists but is NOT frame 0. - echo This causes small icons in NSIS installers. - echo You should reorder frames so 256x256 is the first frame. - pause - exit /b -) - -echo SUCCESS: 256x256 frame exists AND is the first frame. -echo Icon is well-structured. -pause -exit /b \ No newline at end of file diff --git a/assets/zx_LOGO.png b/assets/zx_LOGO.png deleted file mode 100644 index 2b423c7..0000000 Binary files a/assets/zx_LOGO.png and /dev/null differ diff --git a/installer.nsi b/installer.nsi index 014c09f..43c3724 100644 --- a/installer.nsi +++ b/installer.nsi @@ -1,31 +1,17 @@ Unicode True SetCompressor /SOLID lzma +SetCompressorDictSize 64 RequestExecutionLevel user !include "MUI2.nsh" -!include "LogicLib.nsh" !define PROJECT_ROOT "." -!define DIST_ROOT "${PROJECT_ROOT}\\dist\\pqAutomationApp" +!define DIST_ROOT "${PROJECT_ROOT}\dist\pqAutomationApp" !define APP_EXE "pqAutomationApp.exe" !define APP_ID "PQAutomationApp" -; ------------------------------------------------------------ -; Detect Python from PATH -; ------------------------------------------------------------ !define PYTHON_CMD "python" -!system '"${PYTHON_CMD}" -V > python_check.txt 2>&1' -!searchparse /file python_check.txt "Python " PY_VER "" -!if "${PY_VER}" == "" - !error "Python not found. Ensure python.exe is available in PATH." -!endif -!delfile python_check.txt - -; ------------------------------------------------------------ -; Extract APP_NAME and APP_VERSION from Python code -; (App version file may contain Unicode, so we use Python to output ASCII) -; ------------------------------------------------------------ !system '"${PYTHON_CMD}" -c "import app_version; print(app_version.APP_NAME)" > app_name.txt' !system '"${PYTHON_CMD}" -c "import app_version; print(app_version.APP_VERSION)" > app_version.txt' @@ -35,20 +21,22 @@ RequestExecutionLevel user !delfile "app_name.txt" !delfile "app_version.txt" -; ------------------------------------------------------------ -; Registry Keys -; ------------------------------------------------------------ -!define UNINSTALL_REG_KEY "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${APP_ID}" -!define APP_REG_KEY "Software\\${APP_ID}" +!if /FileExists "${DIST_ROOT}\${APP_EXE}" +!else + !error "Executable not found: ${DIST_ROOT}\${APP_EXE}. Please build using PyInstaller first." +!endif + +!define UNINSTALL_REG_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_ID}" +!define APP_REG_KEY "Software\${APP_ID}" Name "${APP_NAME} ${APP_VERSION}" -OutFile "dist\\PQAutomationApp_Setup_${APP_VERSION}.exe" -InstallDir "$LOCALAPPDATA\\Programs\\${APP_ID}" +OutFile "dist\PQAutomationApp_Setup_${APP_VERSION}.exe" +InstallDir "$LOCALAPPDATA\Programs\${APP_ID}" InstallDirRegKey HKCU "${APP_REG_KEY}" "InstallDir" !define MUI_ABORTWARNING -!define MUI_ICON "assets\\pq.ico" -!define MUI_UNICON "assets\\pq.ico" +!define MUI_ICON "assets\pq.ico" +!define MUI_UNICON "assets\pq.ico" !insertmacro MUI_PAGE_WELCOME !insertmacro MUI_PAGE_DIRECTORY @@ -59,71 +47,46 @@ InstallDirRegKey HKCU "${APP_REG_KEY}" "InstallDir" !insertmacro MUI_UNPAGE_INSTFILES !insertmacro MUI_UNPAGE_FINISH -!insertmacro MUI_LANGUAGE "English" +!insertmacro MUI_LANGUAGE "SimpChinese" - -; ------------------------------------------------------------ -; Init -; ------------------------------------------------------------ -Function .onInit - IfFileExists "${DIST_ROOT}\\${APP_EXE}" +2 0 - MessageBox MB_ICONSTOP|MB_OK "Executable not found: ${DIST_ROOT}\\${APP_EXE}$\r$\nPlease build using PyInstaller first." - IfFileExists "${DIST_ROOT}\\${APP_EXE}" +2 0 - Abort -FunctionEnd - - -; ------------------------------------------------------------ -; Installation Section -; ------------------------------------------------------------ Section "Main Installation" SEC_MAIN SetOutPath "$INSTDIR" - CreateDirectory "$INSTDIR" - CreateDirectory "$INSTDIR\\internal" - CreateDirectory "$INSTDIR\\settings" - File "${DIST_ROOT}\\${APP_EXE}" + File "${DIST_ROOT}\${APP_EXE}" - SetOutPath "$INSTDIR\\internal" - File /r "${DIST_ROOT}\\internal\\*.*" + SetOutPath "$INSTDIR\internal" + File /r /x "*.pdb" /x "*.lib" /x "*.exp" /x "*.h" /x "__pycache__" /x "*.pyc" "${DIST_ROOT}\internal\*.*" - IfFileExists "${PROJECT_ROOT}\\settings\\pq_config.json" 0 +3 - SetOutPath "$INSTDIR\\settings" - File /oname=pq_config.json "${PROJECT_ROOT}\\settings\\pq_config.json" + IfFileExists "$INSTDIR\settings\pq_config.json" +3 0 + SetOutPath "$INSTDIR\settings" + File /oname=pq_config.json "${PROJECT_ROOT}\settings\pq_config.json" - WriteUninstaller "$INSTDIR\\Uninstall.exe" + WriteUninstaller "$INSTDIR\Uninstall.exe" WriteRegStr HKCU "${APP_REG_KEY}" "InstallDir" "$INSTDIR" WriteRegStr HKCU "${UNINSTALL_REG_KEY}" "DisplayName" "${APP_NAME}" WriteRegStr HKCU "${UNINSTALL_REG_KEY}" "DisplayVersion" "${APP_VERSION}" WriteRegStr HKCU "${UNINSTALL_REG_KEY}" "InstallLocation" "$INSTDIR" - WriteRegStr HKCU "${UNINSTALL_REG_KEY}" "DisplayIcon" "$INSTDIR\\${APP_EXE}" + WriteRegStr HKCU "${UNINSTALL_REG_KEY}" "DisplayIcon" "$INSTDIR\${APP_EXE}" WriteRegStr HKCU "${UNINSTALL_REG_KEY}" "Publisher" "Moka" - WriteRegStr HKCU "${UNINSTALL_REG_KEY}" "UninstallString" "$\"$INSTDIR\\Uninstall.exe$\"" + WriteRegStr HKCU "${UNINSTALL_REG_KEY}" "UninstallString" "$\"$INSTDIR\Uninstall.exe$\"" WriteRegDWORD HKCU "${UNINSTALL_REG_KEY}" "NoModify" 1 WriteRegDWORD HKCU "${UNINSTALL_REG_KEY}" "NoRepair" 1 - CreateDirectory "$SMPROGRAMS\\${APP_NAME}" - CreateShortcut "$SMPROGRAMS\\${APP_NAME}\\${APP_NAME}.lnk" "$INSTDIR\\${APP_EXE}" - CreateShortcut "$SMPROGRAMS\\${APP_NAME}\\Uninstall ${APP_NAME}.lnk" "$INSTDIR\\Uninstall.exe" - CreateShortcut "$DESKTOP\\${APP_NAME}.lnk" "$INSTDIR\\${APP_EXE}" + CreateDirectory "$SMPROGRAMS\${APP_NAME}" + CreateShortcut "$SMPROGRAMS\${APP_NAME}\${APP_NAME}.lnk" "$INSTDIR\${APP_EXE}" + CreateShortcut "$DESKTOP\${APP_NAME}.lnk" "$INSTDIR\${APP_EXE}" SectionEnd - -; ------------------------------------------------------------ -; Uninstall Section -; ------------------------------------------------------------ Section "Uninstall" - Delete "$DESKTOP\\${APP_NAME}.lnk" - Delete "$SMPROGRAMS\\${APP_NAME}\\${APP_NAME}.lnk" - Delete "$SMPROGRAMS\\${APP_NAME}\\Uninstall ${APP_NAME}.lnk" - RMDir "$SMPROGRAMS\\${APP_NAME}" + Delete "$DESKTOP\${APP_NAME}.lnk" + Delete "$SMPROGRAMS\${APP_NAME}\${APP_NAME}.lnk" + RMDir "$SMPROGRAMS\${APP_NAME}" - Delete "$INSTDIR\\${APP_EXE}" - Delete "$INSTDIR\\Uninstall.exe" - Delete "$INSTDIR\\settings\\pq_config.json" - RMDir /r "$INSTDIR\\internal" - RMDir /r "$INSTDIR\\settings" + Delete "$INSTDIR\${APP_EXE}" + Delete "$INSTDIR\Uninstall.exe" + RMDir /r "$INSTDIR\internal" + RMDir /r "$INSTDIR\settings" RMDir "$INSTDIR" DeleteRegKey HKCU "${UNINSTALL_REG_KEY}" diff --git a/pqAutomationApp.py b/pqAutomationApp.py index 2135651..e6fa2dd 100644 --- a/pqAutomationApp.py +++ b/pqAutomationApp.py @@ -6,14 +6,10 @@ import threading import time import os import datetime -import colour import json import traceback import numpy as np -import matplotlib.pyplot as plt -import matplotlib.image as mpimg import algorithm.pq_algorithm as pq_algorithm -from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from app_version import APP_NAME, APP_VERSION, get_app_title from utils.caSerail import CASerail from utils.tvSerail import tvSerial @@ -27,13 +23,11 @@ from views.collapsing_frame import CollapsingFrame # from views.pq_history_gui import PQHistoryGUI from views.pq_log_gui import PQLogGUI -from colormath.color_objects import xyYColor, LabColor -from colormath.color_conversions import convert_color -from colormath.color_diff import delta_e_cie2000 -from views.pq_debug_panel import PQDebugPanel -plt.rcParams["font.family"] = ["sans-serif"] -plt.rcParams["font.sans-serif"] = ["Microsoft YaHei"] +plt = None +mpimg = None +FigureCanvasTkAgg = None +colour = None def get_resource_path(relative_path): @@ -190,6 +184,59 @@ class PQAutomationApp: ) self.status_bar.pack(side=tk.BOTTOM, fill=tk.X) + def _ensure_matplotlib_loaded(self): + """按需加载 matplotlib,避免阻塞主界面启动。""" + global plt, mpimg, FigureCanvasTkAgg + if plt is None or mpimg is None or FigureCanvasTkAgg is None: + import matplotlib.pyplot as _plt + import matplotlib.image as _mpimg + from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg as _FigureCanvasTkAgg + + _plt.rcParams["font.family"] = ["sans-serif"] + _plt.rcParams["font.sans-serif"] = ["Microsoft YaHei"] + + plt = _plt + mpimg = _mpimg + FigureCanvasTkAgg = _FigureCanvasTkAgg + + def _ensure_colour_loaded(self): + """按需加载 colour-science,减少冷启动导入压力。""" + global colour + if colour is None: + import colour as _colour + + colour = _colour + + def _initialize_chart(self, chart_name): + """按需初始化图表。""" + if chart_name == "gamut": + self.init_gamut_chart() + elif chart_name == "gamma": + self.init_gamma_chart() + elif chart_name == "eotf": + self.init_eotf_chart() + elif chart_name == "cct": + self.init_cct_chart() + elif chart_name == "contrast": + self.init_contrast_chart() + elif chart_name == "accuracy": + self.init_accuracy_chart() + + def _ensure_chart_initialized(self, chart_name): + """确保目标图表已完成初始化。""" + if not self.chart_init_state.get(chart_name, False): + self._initialize_chart(chart_name) + self.chart_init_state[chart_name] = True + + def _warmup_remaining_charts(self): + """在界面显示后分帧预热其它图表,降低首屏阻塞。""" + for chart_name in ("gamma", "eotf", "cct", "contrast", "accuracy"): + if self.chart_init_state.get(chart_name, False): + continue + self._ensure_chart_initialized(chart_name) + self.root.after(1, self._warmup_remaining_charts) + return + def get_config_path(self): """获取配置文件的完整路径(兼容打包后的程序)""" import os @@ -227,6 +274,7 @@ class PQAutomationApp: def init_gamut_chart(self): """初始化色域图表 - 手动设置subplot位置,完全避免重叠""" + self._ensure_matplotlib_loaded() container = ttk.Frame(self.gamut_chart_frame) container.pack(expand=True, fill=tk.BOTH) @@ -263,6 +311,7 @@ class PQAutomationApp: def init_gamma_chart(self): """初始化Gamma曲线图表 - 左侧曲线 + 右侧表格(✅ 4列 + 通用说明)""" + self._ensure_matplotlib_loaded() container = ttk.Frame(self.gamma_chart_frame) container.pack(expand=True, fill=tk.BOTH) @@ -372,6 +421,7 @@ class PQAutomationApp: def init_eotf_chart(self): """初始化 EOTF 曲线图表(HDR 专用)- 左侧曲线 + 右侧表格(✅ 4列)""" + self._ensure_matplotlib_loaded() container = ttk.Frame(self.eotf_chart_frame) container.pack(expand=True, fill=tk.BOTH) @@ -477,6 +527,7 @@ class PQAutomationApp: def init_cct_chart(self): """初始化色度坐标图表 - 正向横坐标,标题居中最上方""" + self._ensure_matplotlib_loaded() container = ttk.Frame(self.cct_chart_frame) container.pack(expand=True) @@ -522,6 +573,7 @@ class PQAutomationApp: def init_contrast_chart(self): """初始化对比度图表 - 固定大小,居中显示""" + self._ensure_matplotlib_loaded() container = ttk.Frame(self.contrast_chart_frame) container.pack(expand=True) @@ -557,6 +609,7 @@ class PQAutomationApp: def init_accuracy_chart(self): """初始化色准图表 - 固定大小,居中显示""" + self._ensure_matplotlib_loaded() container = ttk.Frame(self.accuracy_chart_frame) container.pack(expand=True) @@ -2791,6 +2844,7 @@ class PQAutomationApp: def _run_custom_row_single_step(self, item_id, row_no): """后台执行客户模板单步测试""" try: + self._ensure_colour_loaded() self.root.after(0, lambda: self.status_var.set(f"单步测试第 {row_no} 行...")) self.log_gui.log(f"开始单步测试第 {row_no} 行") @@ -3387,35 +3441,43 @@ class PQAutomationApp: self.chart_notebook.add(self.contrast_chart_frame, text="对比度") self.chart_notebook.add(self.accuracy_chart_frame, text="色准") - # 初始化六个独立的图表 - self.init_gamut_chart() - self.init_gamma_chart() - self.init_eotf_chart() - self.init_cct_chart() - self.init_contrast_chart() - self.init_accuracy_chart() + # 图表初始化状态:首屏仅初始化当前可见图表,其余延后。 + self.chart_init_state = { + "gamut": False, + "gamma": False, + "eotf": False, + "cct": False, + "contrast": False, + "accuracy": False, + } + + # 首屏只初始化色域图,其他图表在切换 Tab 或空闲时初始化。 + self._ensure_chart_initialized("gamut") + self.root.after(120, self._warmup_remaining_charts) # 绑定Tab切换事件 self.chart_notebook.bind("<>", self.on_chart_tab_changed) - # ==================== ✅ 在图表下方创建单步调试面板 ==================== - self.debug_container = ttk.LabelFrame( - self.result_frame, # ← 放在 result_frame 内,图表正下方 - text="🔧 单步调试", - padding=10, - ) - # 默认不显示 - - # 创建单步调试面板实例 - self.debug_panel = PQDebugPanel(self.debug_container, self) - - self.log_gui.log("✓ 单步调试面板已创建(放在测试结果图表下方)") + # 旧版内嵌调试面板改为按需窗口,不在启动阶段创建复杂控件。 def on_chart_tab_changed(self, event): """Tab切换时的事件处理""" try: + selected_tab = self.chart_notebook.select() + tab_to_chart = { + str(self.gamut_chart_frame): "gamut", + str(self.gamma_chart_frame): "gamma", + str(self.eotf_chart_frame): "eotf", + str(self.cct_chart_frame): "cct", + str(self.contrast_chart_frame): "contrast", + str(self.accuracy_chart_frame): "accuracy", + } + chart_name = tab_to_chart.get(selected_tab) + if chart_name: + self._ensure_chart_initialized(chart_name) + self._last_tab_index = self.chart_notebook.index( - self.chart_notebook.select() + selected_tab ) except Exception as e: self.log_gui.log(f"Tab切换事件处理失败: {str(e)}") @@ -6234,6 +6296,7 @@ class PQAutomationApp: # 每完成一个 pattern,实时写入客户模板结果表。 try: + self._ensure_colour_loaded() xy = colour.XYZ_to_xy(np.array([X, Y, Z])) u_prime, v_prime, _ = colour.XYZ_to_CIE1976UCS( np.array([X, Y, Z]) @@ -7145,6 +7208,7 @@ class PQAutomationApp: def plot_gamut(self, results, coverage, test_type): """绘制色域图 - 根据用户选择的参考标准动态计算覆盖率""" + self._ensure_chart_initialized("gamut") self.gamut_ax_xy.clear() self.gamut_ax_uv.clear() @@ -7670,6 +7734,8 @@ class PQAutomationApp: def plot_gamma(self, L_bar, results_with_gamma_list, target_gamma, test_type): """绘制Gamma曲线 + 数据表格(包含实测亮度)""" + self._ensure_chart_initialized("gamma") + # ========== 1. 清空并重置左侧曲线 ========== self.gamma_ax.clear() self.gamma_ax.set_xlim(0, 105) @@ -7804,6 +7870,8 @@ class PQAutomationApp: def plot_eotf(self, L_bar, results_with_eotf_list, test_type): """绘制 EOTF 曲线 + 数据表格(HDR 专用,包含实测亮度)""" + self._ensure_chart_initialized("eotf") + # ========== 1. 清空并重置左侧曲线 ========== self.eotf_ax.clear() self.eotf_ax.set_xlim(0, 105) @@ -7985,6 +8053,8 @@ class PQAutomationApp: def plot_cct(self, test_type): """绘制 x 和 y 坐标分离图 - 每个点标注纵坐标值""" + self._ensure_chart_initialized("cct") + self.cct_fig.clear() gray_data = self.results.get_intermediate_data("shared", "gray") @@ -8301,6 +8371,8 @@ class PQAutomationApp: def plot_contrast(self, contrast_data, test_type): """绘制对比度测试结果 - 固定布局版本""" + self._ensure_chart_initialized("contrast") + # 清空并重置 self.contrast_ax.clear() self.contrast_ax.set_xlim(0, 1) @@ -8462,6 +8534,8 @@ class PQAutomationApp: def plot_accuracy(self, accuracy_data, test_type): """绘制色准测试结果 - 29色显示 - 简洁版布局(显示 Gamma)""" + self._ensure_chart_initialized("accuracy") + self.accuracy_ax.clear() self.accuracy_ax.set_xlim(0, 1) self.accuracy_ax.set_ylim(0, 1) diff --git a/pqAutomationApp.spec b/pqAutomationApp.spec index 9bacb06..256a600 100644 --- a/pqAutomationApp.spec +++ b/pqAutomationApp.spec @@ -74,8 +74,19 @@ a = Analysis( hookspath=[], hooksconfig={}, runtime_hooks=[], - excludes=['PyQt5'], + excludes=[ + 'PyQt5', + 'PyQt6', + 'PySide2', + 'PySide6', + 'cv2', + 'imageio', + 'imageio_ffmpeg', + 'IPython', + 'jedi', + ], noarchive=False, + # numpy 在运行时依赖部分 docstring,optimize=2 会移除 docstring 导致启动报错。 optimize=0, ) pyz = PYZ(a.pure) @@ -89,7 +100,8 @@ exe = EXE( debug=False, bootloader_ignore_signals=False, strip=False, - upx=True, + # 关闭 UPX:通常可减少启动时解压与杀软扫描开销,提升冷启动体感。 + upx=False, console=False, disable_windowed_traceback=False, argv_emulation=False, @@ -105,7 +117,7 @@ coll = COLLECT( a.binaries, a.datas, strip=False, - upx=True, + upx=False, upx_exclude=[], name='pqAutomationApp', )