界面优化
This commit is contained in:
1
src/gui/core/__init__.py
Normal file
1
src/gui/core/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# src.gui.core
|
||||
327
src/gui/core/worker_thread.py
Normal file
327
src/gui/core/worker_thread.py
Normal 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()
|
||||
Reference in New Issue
Block a user