界面优化

This commit is contained in:
DXC
2026-05-07 14:23:58 +08:00
parent dc33ee260d
commit c12b9d8d8a
24 changed files with 6090 additions and 4638 deletions

1
src/gui/core/__init__.py Normal file
View File

@ -0,0 +1 @@
# src.gui.core

View File

@ -0,0 +1,327 @@
# -*- coding: utf-8 -*-
"""
后台线程模块Pipeline 执行线程与诊断逻辑。
"""
import traceback
from PyQt5.QtCore import QThread, pyqtSignal
# =============================================================================
# 依赖诊断
# =============================================================================
def check_pipeline_dependencies():
"""检查pipeline模块的依赖项"""
missing_deps = []
dep_errors = {}
required_packages = [
'numpy', 'pandas', 'scipy', 'matplotlib', 'sklearn',
'joblib', 'PIL', 'cv2', 'rasterio', 'geopandas'
]
for package in required_packages:
try:
if package == 'PIL':
import PIL
elif package == 'cv2':
import cv2
else:
__import__(package)
except Exception as e:
missing_deps.append(package)
dep_errors[package] = repr(e)
return missing_deps, dep_errors
def diagnose_pipeline_import_error():
"""诊断pipeline导入错误"""
import sys
import os
error_info = []
is_frozen = getattr(sys, "frozen", False) or bool(getattr(sys, "_MEIPASS", None))
if is_frozen:
error_info.append(
"[INFO] PyInstaller 环境Pipeline 从程序内置包加载,跳过对仓库路径 src/core/*.py 的磁盘检查"
)
else:
pipeline_file = os.path.normpath(
os.path.join(os.path.dirname(__file__), "..", "..", "core", "water_quality_inversion_pipeline_GUI.py")
)
if not os.path.exists(pipeline_file):
error_info.append(f"[ERROR] Pipeline文件不存在: {pipeline_file}")
error_info.append(
" 解决方案: 请确保项目结构完整,检查 src/core/ 下是否有 water_quality_inversion_pipeline_GUI.py"
)
else:
error_info.append(f"[OK] Pipeline文件存在: {pipeline_file}")
current_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
if current_dir not in sys.path:
sys.path.insert(0, current_dir)
error_info.append(f"[INFO] 已添加路径到sys.path: {current_dir}")
missing_deps, dep_errors = check_pipeline_dependencies()
if missing_deps:
error_info.append(f"[ERROR] 缺少必需的依赖包: {', '.join(missing_deps)}")
for pkg in missing_deps:
if pkg in dep_errors:
error_info.append(f" - {pkg} 导入失败原因: {dep_errors[pkg]}")
error_info.append(" 解决方案: 请运行以下命令安装依赖:")
error_info.append(" pip install -r requirements.txt")
error_info.append(" 或使用conda:")
error_info.append(" conda install numpy pandas scipy matplotlib scikit-learn joblib pillow opencv-python rasterio geopandas")
else:
error_info.append("[OK] 主要依赖包均已安装")
try:
from osgeo import gdal # noqa: F401
error_info.append("[OK] GDAL (osgeo) 可用")
except ImportError:
try:
from osgeo import gdal # noqa: F401
error_info.append("[OK] GDAL 可用")
except ImportError:
error_info.append("[WARNING] GDAL/osgeo 不可用,将影响栅格与地理数据处理")
error_info.append(" 开发环境: conda install gdal")
error_info.append(" 打包环境: 请在构建所用 Conda 环境中打包,并确保 spec 已收集 Library/bin 中依赖 DLL")
try:
import unittest
error_info.append("[OK] unittest模块可用")
except ImportError:
error_info.append("[WARNING] unittest模块不可用这可能是PyInstaller打包环境导致的")
error_info.append(" 这不会影响主要功能,但可能影响某些测试相关特性")
return error_info
# =============================================================================
# Pipeline 可用性标志(模块级状态)
# =============================================================================
PIPELINE_AVAILABLE = False
PIPELINE_ERROR_INFO = []
try:
error_info = diagnose_pipeline_import_error()
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
PIPELINE_AVAILABLE = True
print("[OK] 成功导入pipeline模块")
PIPELINE_ERROR_INFO = error_info
except ImportError as e:
PIPELINE_AVAILABLE = False
error_info = diagnose_pipeline_import_error()
print("="*60)
print("[ERROR] PIPELINE导入失败 - 详细诊断信息:")
print("="*60)
for info in error_info:
print(info)
print("-"*60)
print(f"原始ImportError: {str(e)}")
print("-"*60)
if "unittest" in str(e):
print("[INFO] unittest模块缺失 - 这通常在PyInstaller打包环境中发生")
print("解决方案:")
print(" 1. 这不会影响主要功能,程序仍可正常运行")
print(" 2. 如果需要修复,可以在.spec文件中添加unittest模块:")
print(" a = Analysis(..., hiddenimports=['unittest', 'unittest.mock'])")
print(" 3. 或在PyInstaller命令中添加: --hidden-import unittest")
elif "water_quality_inversion_pipeline_GUI" in str(e):
print("[INFO] 可能的解决方案:")
print(" 1. 检查src/core/water_quality_inversion_pipeline_GUI.py文件是否存在")
print(" 2. 确保Python路径设置正确")
print(" 3. 尝试重新安装依赖: pip install -r requirements.txt")
print(" 4. 检查Python版本是否兼容推荐Python 3.8-3.11")
import traceback
print("\n完整错误追踪:")
traceback.print_exc()
print("="*60)
PIPELINE_ERROR_INFO = error_info
except Exception as e:
PIPELINE_AVAILABLE = False
error_info = diagnose_pipeline_import_error()
print("="*60)
print("[ERROR] PIPELINE导入失败 - 其他错误:")
print("="*60)
for info in error_info:
print(info)
print("-"*60)
print(f"原始错误: {str(e)}")
print("-"*60)
print("[INFO] 可能的解决方案:")
print(" 1. 检查Python环境和依赖包版本")
print(" 2. 尝试重新安装所有依赖")
print(" 3. 检查是否有语法错误或其他模块导入问题")
import traceback
print("\n完整错误追踪:")
traceback.print_exc()
print("="*60)
PIPELINE_ERROR_INFO = error_info
# =============================================================================
# WorkerThread
# =============================================================================
class WorkerThread(QThread):
"""后台工作线程,用于执行耗时任务(在工作线程内创建 Pipeline避免阻塞 UI"""
progress_update = pyqtSignal(int, str) # 进度更新信号 (percentage, message)
log_message = pyqtSignal(str, str) # 日志消息信号 (message, level: 'info'/'warning'/'error')
step_completed = pyqtSignal(str, bool, str) # 步骤完成信号 (step_name, success, message)
finished = pyqtSignal(bool, str) # 完成信号 (success, message)
def __init__(self, work_dir: str, config, mode='full', step_name=None):
super().__init__()
self.work_dir = str(work_dir)
self.config = config
self.mode = mode # 'full' 或 'single_step'
self.step_name = step_name # 单步执行时的步骤名称
self.pipeline = None
self.is_running = True
self.current_step = None
self.step_count = 0
self.total_steps = 9
def pipeline_callback(self, step_name, status, message=""):
"""Pipeline回调函数用于接收步骤状态"""
if status == "start":
self.log_message.emit(f"[START] 开始执行: {step_name}", "info")
progress = int((self.step_count / self.total_steps) * 100)
self.progress_update.emit(progress, f"正在执行: {step_name}")
elif status == "completed":
self.step_count += 1
self.log_message.emit(f"[DONE] 完成: {step_name} {message}", "info")
self.step_completed.emit(step_name, True, message)
progress = int((self.step_count / self.total_steps) * 100)
self.progress_update.emit(progress, f"已完成: {step_name}")
elif status == "skipped":
self.step_count += 1
self.log_message.emit(f"[SKIP] 跳过: {step_name} {message}", "warning")
self.step_completed.emit(step_name, True, f"跳过: {message}")
progress = int((self.step_count / self.total_steps) * 100)
self.progress_update.emit(progress, f"已跳过: {step_name}")
elif status == "error":
self.log_message.emit(f"[ERROR] 错误: {step_name} - {message}", "error")
self.step_completed.emit(step_name, False, message)
elif status == "info":
self.log_message.emit(f" {message}", "info")
elif status == "warning":
self.log_message.emit(f" [WARNING] {message}", "warning")
def run(self):
"""运行 pipeline子线程内切换 Matplotlib 为 Agg避免 Qt5Agg 在后台线程绘图导致界面卡死。"""
mpl_prev = None
try:
import matplotlib
mpl_prev = matplotlib.get_backend()
except Exception:
pass
try:
import matplotlib.pyplot as plt
plt.switch_backend("Agg")
except Exception:
mpl_prev = None
try:
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
self.pipeline = WaterQualityInversionPipeline(work_dir=self.work_dir)
if self.mode == 'full':
self.log_message.emit("开始运行完整流程...", "info")
self.step_count = 0
if hasattr(self.pipeline, 'set_callback'):
self.pipeline.set_callback(self.pipeline_callback)
self.pipeline.run_full_pipeline(self.config)
self.progress_update.emit(100, "流程执行完成")
self.finished.emit(True, "完整流程执行成功!")
else:
self.log_message.emit(f"开始独立运行步骤: {self.step_name}", "info")
self.progress_update.emit(0, f"正在执行: {self.step_name}")
if hasattr(self.pipeline, 'set_callback'):
self.pipeline.set_callback(self.pipeline_callback)
self.run_single_step(self.step_name, self.config)
self.progress_update.emit(100, f"步骤 {self.step_name} 执行完成")
self.finished.emit(True, f"步骤 {self.step_name} 独立运行成功!")
except Exception as e:
error_msg = f"执行失败: {str(e)}\n{traceback.format_exc()}"
self.log_message.emit(error_msg, "error")
self.finished.emit(False, error_msg)
finally:
if mpl_prev:
try:
import matplotlib.pyplot as plt
plt.switch_backend(mpl_prev)
except Exception:
pass
def run_single_step(self, step_name, config):
"""运行单个步骤"""
step_method_map = {
'step1': 'step1_generate_water_mask',
'step2': 'step2_find_glint_area',
'step3': 'step3_remove_glint',
'step4': 'step4_process_csv',
'step5': 'step5_extract_training_spectra',
'step5_5': 'step5_5_calculate_water_quality_indices',
'step6': 'step6_train_models',
'step6_5': 'step6_5_non_empirical_modeling',
'step6_75': 'step6_75_custom_regression',
'step7': 'step7_generate_sampling_points',
'step8': 'step8_predict_water_quality',
'step8_5': 'step8_5_predict_with_non_empirical_models',
'step8_75': 'step8_75_predict_with_custom_regression',
'step9': 'step9_generate_distribution_map'
}
if step_name not in step_method_map:
raise ValueError(f"未知的步骤名称: {step_name}")
method_name = step_method_map[step_name]
step_config = dict(config.get(step_name, {}))
step_config['skip_dependency_check'] = True
if step_name == 'step9':
step_config.pop('step9_batch_mode', None)
step_config.pop('prediction_csv_dir', None)
step_config.pop('recursive_csv_scan', None)
if step_name in ['step2', 'step3', 'step4', 'step5', 'step7', 'step8', 'step8_5', 'step8_75']:
step_config.pop('output_path', None)
if step_name == 'step8_5' and 'models_dir' in step_config:
step_config['non_empirical_models_dir'] = step_config.pop('models_dir')
method = getattr(self.pipeline, method_name)
result = method(**step_config)
return result
def stop(self):
"""停止执行"""
self.is_running = False
self.terminate()