6400 lines
248 KiB
Python
6400 lines
248 KiB
Python
#!/usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
水质参数反演分析系统 - 图形用户界面
|
||
GUI for Water Quality Inversion Pipeline
|
||
"""
|
||
|
||
import os
|
||
import json
|
||
import copy
|
||
import sys
|
||
import traceback
|
||
from pathlib import Path
|
||
from datetime import datetime
|
||
from typing import Dict, Optional, List, Union
|
||
import numpy as np
|
||
import pandas as pd
|
||
import multiprocessing
|
||
from PyQt5.QtWidgets import (
|
||
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||
QPushButton, QLabel, QLineEdit, QComboBox, QCheckBox, QSpinBox,
|
||
QDoubleSpinBox, QFileDialog, QTextEdit, QProgressBar, QMessageBox,
|
||
QScrollArea, QGroupBox, QTabWidget, QSplitter, QListWidget,
|
||
QListWidgetItem, QFrame, QGridLayout, QFormLayout, QSizePolicy, QDialog,
|
||
QStackedWidget, QTableView, QHeaderView, QAbstractItemView,
|
||
QRadioButton, QButtonGroup, QToolBar, QTreeWidget, QTreeWidgetItem,
|
||
QInputDialog,
|
||
)
|
||
from PyQt5.QtCore import QThread, pyqtSignal, Qt, QTimer, QAbstractTableModel, QSize
|
||
from PyQt5.QtGui import QIcon, QFont, QTextCursor, QPalette, QColor, QPixmap
|
||
|
||
# 导入样式模块 - 兼容开发环境和 PyInstaller 打包
|
||
try:
|
||
# 开发环境或正确添加到 sys.path 时
|
||
from styles import ModernStylesheet
|
||
except ImportError:
|
||
# PyInstaller 打包后或路径不正确时
|
||
try:
|
||
from src.gui.styles import ModernStylesheet
|
||
except ImportError:
|
||
# 最终兜底:添加路径后导入
|
||
import sys
|
||
import os
|
||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||
project_root = os.path.abspath(os.path.join(current_dir, '..', '..'))
|
||
if project_root not in sys.path:
|
||
sys.path.insert(0, project_root)
|
||
from src.gui.styles import ModernStylesheet
|
||
|
||
# Matplotlib相关导入
|
||
import matplotlib
|
||
matplotlib.use('Qt5Agg')
|
||
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
||
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
|
||
from matplotlib.figure import Figure
|
||
import matplotlib.pyplot as plt
|
||
|
||
# 导入原有的pipeline类
|
||
def check_pipeline_dependencies():
|
||
"""检查pipeline模块的依赖项"""
|
||
missing_deps = []
|
||
dep_errors = {}
|
||
|
||
# 检查必需的Python包
|
||
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 = []
|
||
|
||
# 检查是否在PyInstaller环境中运行
|
||
is_frozen = getattr(sys, "frozen", False) or bool(getattr(sys, "_MEIPASS", None))
|
||
|
||
if is_frozen:
|
||
# 打包后模块在 PyInstaller 归档内,磁盘上不再有「项目根/src/core/*.py」布局;
|
||
# 切勿把 exe 所在目录(如 scripts/dist)的上级当成源码根,否则会误报「文件不存在」。
|
||
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(__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)}")
|
||
# 额外输出真实的导入失败原因(常见于 DLL 缺失,而不是包没安装)
|
||
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] 主要依赖包均已安装")
|
||
|
||
# 检查 GDAL(优先 osgeo,与运行时一致)
|
||
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")
|
||
|
||
# 检查unittest模块(PyInstaller打包时可能缺失)
|
||
try:
|
||
import unittest
|
||
error_info.append("[OK] unittest模块可用")
|
||
except ImportError:
|
||
error_info.append("[WARNING] unittest模块不可用,这可能是PyInstaller打包环境导致的")
|
||
error_info.append(" 这不会影响主要功能,但可能影响某些测试相关特性")
|
||
|
||
return error_info
|
||
|
||
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
|
||
|
||
|
||
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, {}))
|
||
|
||
# 为独立运行添加 skip_dependency_check=True
|
||
step_config['skip_dependency_check'] = True
|
||
|
||
# step9:去掉仅用于 GUI/配置保存的字段,避免传入 pipeline 报错
|
||
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)
|
||
|
||
# step5:输出路径由管线固定到工作目录,GUI 占位字段勿传入
|
||
if step_name == 'step5':
|
||
step_config.pop('output_path', None)
|
||
|
||
# 参数名映射:将GUI中的参数名映射为pipeline方法期望的参数名
|
||
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()
|
||
|
||
|
||
class ReportGenerateThread(QThread):
|
||
"""后台生成 Word 报告(避免阻塞 UI)。"""
|
||
finished_ok = pyqtSignal(str)
|
||
failed = pyqtSignal(str)
|
||
log_message = pyqtSignal(str, str)
|
||
|
||
def __init__(self, work_dir: str, output_dir: Optional[str], report_title: str, options: dict):
|
||
super().__init__()
|
||
self.work_dir = work_dir
|
||
self.output_dir = output_dir
|
||
self.report_title = report_title
|
||
self.options = options
|
||
|
||
def run(self):
|
||
import traceback
|
||
try:
|
||
from src.postprocessing.report_word import WaterQualityReportGenerator, ReportGenerationConfig
|
||
|
||
url = (self.options.get("ollama_url") or "").strip() or None
|
||
vision = (self.options.get("ollama_vision_model") or "").strip() or None
|
||
text = (self.options.get("ollama_text_model") or "").strip() or None
|
||
if self.options.get("text_same_as_vision"):
|
||
text = vision
|
||
timeout = self.options.get("ollama_timeout_s")
|
||
enable_ai = self.options.get("enable_ai_analysis")
|
||
|
||
ai_cfg = ReportGenerationConfig(
|
||
ollama_base_url=url,
|
||
ollama_vision_model=vision,
|
||
ollama_text_model=text,
|
||
ollama_timeout_s=int(timeout) if timeout is not None else None,
|
||
enable_ai_analysis=bool(enable_ai),
|
||
)
|
||
self.log_message.emit(
|
||
f"报告生成:工作目录={self.work_dir},AI={'开' if enable_ai else '关'},"
|
||
f"模型URL={url or '(环境变量 OLLAMA_URL)'}",
|
||
"info",
|
||
)
|
||
gen = WaterQualityReportGenerator(
|
||
work_dir=self.work_dir,
|
||
output_dir=self.output_dir,
|
||
ai_config=ai_cfg,
|
||
)
|
||
out_path = gen.generate_report(
|
||
work_dir=self.work_dir,
|
||
report_title=self.report_title or "水质参数反演分析报告",
|
||
)
|
||
self.finished_ok.emit(str(out_path))
|
||
except Exception as e:
|
||
self.failed.emit(f"{e}\n{traceback.format_exc()}")
|
||
|
||
|
||
class Step9BatchThread(QThread):
|
||
"""专题图:按文件夹内多个预测 CSV 批量生成分布图。"""
|
||
|
||
finished_ok = pyqtSignal(int)
|
||
failed = pyqtSignal(str)
|
||
log_message = pyqtSignal(str, str)
|
||
|
||
def __init__(self, work_dir: str, csv_paths: List[str], step9_kwargs: dict, output_dir_optional: Optional[str]):
|
||
super().__init__()
|
||
self.work_dir = work_dir
|
||
self.csv_paths = csv_paths
|
||
self.step9_kwargs = step9_kwargs
|
||
self.output_dir_optional = (output_dir_optional or "").strip() or None
|
||
|
||
def run(self):
|
||
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
|
||
pipeline = WaterQualityInversionPipeline(work_dir=self.work_dir)
|
||
n = len(self.csv_paths)
|
||
for i, csv_p in enumerate(self.csv_paths):
|
||
self.log_message.emit(f"专题图 [{i + 1}/{n}] {csv_p}", "info")
|
||
kw = {**self.step9_kwargs, "prediction_csv_path": csv_p, "skip_dependency_check": True}
|
||
if self.output_dir_optional:
|
||
stem = Path(csv_p).stem
|
||
kw["output_image_path"] = str(Path(self.output_dir_optional) / f"{stem}_distribution.png")
|
||
else:
|
||
kw["output_image_path"] = None
|
||
pipeline.step9_generate_distribution_map(**kw)
|
||
self.finished_ok.emit(n)
|
||
except Exception as e:
|
||
self.failed.emit(f"{e}\n{traceback.format_exc()}")
|
||
finally:
|
||
if mpl_prev:
|
||
try:
|
||
import matplotlib.pyplot as plt
|
||
plt.switch_backend(mpl_prev)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def _viz_training_spectra_csv_path(work_path: Path) -> Path:
|
||
"""可视化光谱/统计及模型散点图使用的训练光谱表路径(与步骤5输出一致)。"""
|
||
return work_path / "5_training_spectra" / "training_spectra.csv"
|
||
|
||
|
||
def _viz_infer_wavelength_start_column(df: pd.DataFrame) -> Union[str, int]:
|
||
"""推断光谱起始列(training_spectra 通常以波长数值为列名,未必含 UTM_Y)。"""
|
||
for i, col in enumerate(df.columns):
|
||
name = str(col).strip().lstrip("\ufeff")
|
||
try:
|
||
v = float(name)
|
||
except ValueError:
|
||
continue
|
||
if 200.0 <= v <= 3000.0:
|
||
return i
|
||
if "UTM_Y" in df.columns:
|
||
return "UTM_Y"
|
||
return 0
|
||
|
||
|
||
class VisualizationWorkerThread(QThread):
|
||
"""可视化耗时计算放入后台线程,并临时使用 Agg 后端,避免主界面未响应。"""
|
||
|
||
finished_ok = pyqtSignal(object)
|
||
failed = pyqtSignal(str)
|
||
|
||
def __init__(self, task: str, work_dir: str, extra: Optional[dict] = None):
|
||
super().__init__()
|
||
self.task = task
|
||
self.work_dir = str(work_dir)
|
||
self.extra = extra or {}
|
||
|
||
def run(self):
|
||
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:
|
||
wp = Path(self.work_dir)
|
||
if self.task == "mask_glint":
|
||
from src.postprocessing.visualization_reports import WaterQualityVisualization
|
||
viz = WaterQualityVisualization(output_dir=str(wp / "14_visualization"))
|
||
preview_paths = viz.generate_glint_deglint_previews(
|
||
work_dir=str(wp),
|
||
output_subdir="glint_deglint_previews",
|
||
)
|
||
cnt = len(preview_paths) if preview_paths else 0
|
||
self.finished_ok.emit({"task": "mask_glint", "count": cnt, "preview_paths": preview_paths})
|
||
elif self.task == "sampling_map":
|
||
hyperspectral_files = []
|
||
deglint_dir = wp / "3_deglint"
|
||
if deglint_dir.exists():
|
||
for ext in ("*.dat", "*.bsq", "*.tif", "*.tiff"):
|
||
hyperspectral_files.extend(list(deglint_dir.glob(ext)))
|
||
if not hyperspectral_files:
|
||
for ext in ("*.dat", "*.bsq", "*.tif", "*.tiff"):
|
||
hyperspectral_files.extend(list(wp.glob(f"**/{ext}")))
|
||
if not hyperspectral_files:
|
||
self.failed.emit("未找到高光谱影像文件(.dat/.bsq/.tif)。")
|
||
return
|
||
hyperspectral_path = str(hyperspectral_files[0])
|
||
csv_files = []
|
||
processed_dir = wp / "4_processed_data"
|
||
if processed_dir.exists():
|
||
csv_files = list(processed_dir.glob("*.csv"))
|
||
if not csv_files:
|
||
csv_files = (
|
||
list(wp.glob("**/*sampling*.csv"))
|
||
+ list(wp.glob("**/*point*.csv"))
|
||
+ list(wp.glob("**/*.csv"))
|
||
)
|
||
if not csv_files:
|
||
self.failed.emit("未找到采样点 CSV 文件。")
|
||
return
|
||
csv_path = str(csv_files[0])
|
||
from src.postprocessing.point_map import SamplingPointMap
|
||
map_generator = SamplingPointMap(
|
||
output_dir=str(wp / "14_visualization" / "sampling_maps"),
|
||
fast_mode=True,
|
||
)
|
||
map_path = map_generator.create_sampling_point_map(
|
||
hyperspectral_path=hyperspectral_path,
|
||
csv_path=csv_path,
|
||
point_color="red",
|
||
point_size=100,
|
||
point_alpha=0.9,
|
||
show_north_arrow=True,
|
||
show_scale_bar=True,
|
||
show_legend=True,
|
||
downsample=True,
|
||
dpi=180,
|
||
)
|
||
self.finished_ok.emit(
|
||
{
|
||
"task": "sampling_map",
|
||
"map_path": map_path,
|
||
"hyperspectral_path": hyperspectral_path,
|
||
"csv_path": csv_path,
|
||
}
|
||
)
|
||
elif self.task == "spectrum":
|
||
from src.postprocessing.visualization_reports import WaterQualityVisualization
|
||
viz = WaterQualityVisualization(output_dir=str(wp / "14_visualization"))
|
||
csv_file = self.extra.get("csv_path")
|
||
wl = self.extra.get("wavelength_start_column", "UTM_Y")
|
||
n_groups = int(self.extra.get("n_groups", 5))
|
||
param_cols = self.extra.get("param_cols") or []
|
||
if param_cols:
|
||
output_paths: List[str] = []
|
||
err_lines: List[str] = []
|
||
for param_col in param_cols:
|
||
try:
|
||
out = viz.plot_spectrum_by_parameter(
|
||
csv_path=str(csv_file),
|
||
parameter_column=param_col,
|
||
wavelength_start_column=wl,
|
||
n_groups=n_groups,
|
||
)
|
||
output_paths.append(out)
|
||
except Exception as _ex:
|
||
err_lines.append(f"{param_col}: {_ex}")
|
||
if not output_paths:
|
||
self.failed.emit(
|
||
"所有参数列的光谱图均生成失败:\n" + "\n".join(err_lines[:20])
|
||
)
|
||
return
|
||
self.finished_ok.emit(
|
||
{
|
||
"task": "spectrum",
|
||
"output_paths": output_paths,
|
||
"errors": err_lines,
|
||
}
|
||
)
|
||
else:
|
||
param_col = self.extra.get("param_col")
|
||
out = viz.plot_spectrum_by_parameter(
|
||
csv_path=str(csv_file),
|
||
parameter_column=param_col,
|
||
wavelength_start_column=wl,
|
||
n_groups=n_groups,
|
||
)
|
||
self.finished_ok.emit(
|
||
{"task": "spectrum", "output_path": out, "param_col": param_col}
|
||
)
|
||
elif self.task == "statistics":
|
||
from src.postprocessing.visualization_reports import WaterQualityVisualization
|
||
viz = WaterQualityVisualization(output_dir=str(wp / "14_visualization"))
|
||
csv_file = self.extra.get("csv_path")
|
||
param_cols = self.extra.get("param_cols") or []
|
||
output_paths = viz.plot_statistical_charts(
|
||
csv_path=str(csv_file),
|
||
parameter_columns=param_cols,
|
||
)
|
||
self.finished_ok.emit(
|
||
{"task": "statistics", "output_paths": output_paths}
|
||
)
|
||
elif self.task == "scatter":
|
||
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
|
||
|
||
training_csv_path = (self.extra.get("training_csv_path") or "").strip()
|
||
models_dir = (self.extra.get("models_dir") or "").strip()
|
||
if not training_csv_path or not Path(training_csv_path).is_file():
|
||
self.failed.emit("训练光谱 CSV 无效或不存在,请确认已选择步骤5输出的文件。")
|
||
return
|
||
if not models_dir or not Path(models_dir).is_dir():
|
||
self.failed.emit("模型目录无效或不存在,请确认步骤6已生成 7_models 下的参数子文件夹。")
|
||
return
|
||
pipeline = WaterQualityInversionPipeline(work_dir=str(wp))
|
||
scatter_paths = pipeline.generate_model_scatter_plots(
|
||
training_csv_path=training_csv_path,
|
||
models_dir=models_dir,
|
||
)
|
||
self.finished_ok.emit({"task": "scatter", "scatter_paths": scatter_paths or {}})
|
||
elif self.task == "generate_all_selected":
|
||
from src.postprocessing.visualization_reports import WaterQualityVisualization
|
||
viz = WaterQualityVisualization(output_dir=str(wp / "14_visualization"))
|
||
parts = []
|
||
if self.extra.get("gen_mask_glint"):
|
||
preview_paths = viz.generate_glint_deglint_previews(
|
||
work_dir=str(wp),
|
||
output_subdir="glint_deglint_previews",
|
||
)
|
||
parts.append(f"掩膜/耀斑预览 {len(preview_paths) if preview_paths else 0} 个")
|
||
if self.extra.get("gen_sampling_map"):
|
||
hyperspectral_files = []
|
||
deglint_dir = wp / "3_deglint"
|
||
if deglint_dir.exists():
|
||
for ext in ("*.dat", "*.bsq", "*.tif", "*.tiff"):
|
||
hyperspectral_files.extend(list(deglint_dir.glob(ext)))
|
||
if not hyperspectral_files:
|
||
for ext in ("*.dat", "*.bsq", "*.tif", "*.tiff"):
|
||
hyperspectral_files.extend(list(wp.glob(f"**/{ext}")))
|
||
if hyperspectral_files:
|
||
hyperspectral_path = str(hyperspectral_files[0])
|
||
csv_files = []
|
||
processed_dir = wp / "4_processed_data"
|
||
if processed_dir.exists():
|
||
csv_files = list(processed_dir.glob("*.csv"))
|
||
if not csv_files:
|
||
csv_files = (
|
||
list(wp.glob("**/*sampling*.csv"))
|
||
+ list(wp.glob("**/*point*.csv"))
|
||
+ list(wp.glob("**/*.csv"))
|
||
)
|
||
if csv_files:
|
||
csv_path = str(csv_files[0])
|
||
from src.postprocessing.point_map import SamplingPointMap
|
||
map_generator = SamplingPointMap(
|
||
output_dir=str(wp / "14_visualization" / "sampling_maps"),
|
||
fast_mode=True,
|
||
)
|
||
map_path = map_generator.create_sampling_point_map(
|
||
hyperspectral_path=hyperspectral_path,
|
||
csv_path=csv_path,
|
||
point_color="red",
|
||
point_size=100,
|
||
point_alpha=0.9,
|
||
show_north_arrow=True,
|
||
show_scale_bar=True,
|
||
show_legend=True,
|
||
downsample=True,
|
||
dpi=180,
|
||
)
|
||
parts.append(f"采样点图: {Path(map_path).name}")
|
||
else:
|
||
parts.append("采样点图: 跳过(无CSV)")
|
||
else:
|
||
parts.append("采样点图: 跳过(无影像)")
|
||
self.finished_ok.emit({"task": "generate_all_selected", "parts": parts})
|
||
else:
|
||
self.failed.emit(f"未知可视化任务: {self.task}")
|
||
except Exception as e:
|
||
self.failed.emit(f"{e}\n{traceback.format_exc()}")
|
||
finally:
|
||
if mpl_prev:
|
||
try:
|
||
import matplotlib.pyplot as plt
|
||
plt.switch_backend(mpl_prev)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
class FileSelectWidget(QWidget):
|
||
"""文件选择组件"""
|
||
def __init__(self, label_text, file_filter="All Files (*.*)", parent=None):
|
||
super().__init__(parent)
|
||
self.file_filter = file_filter
|
||
self.init_ui(label_text)
|
||
|
||
def init_ui(self, label_text):
|
||
layout = QHBoxLayout()
|
||
layout.setContentsMargins(0, 0, 0, 0)
|
||
|
||
self.label = QLabel(label_text)
|
||
self.label.setMinimumWidth(120)
|
||
self.line_edit = QLineEdit()
|
||
self.line_edit.setPlaceholderText("请选择文件...")
|
||
self.browse_btn = QPushButton("浏览...")
|
||
self.browse_btn.setMaximumWidth(80)
|
||
self.browse_btn.clicked.connect(self.browse_file)
|
||
|
||
layout.addWidget(self.label)
|
||
layout.addWidget(self.line_edit, 1)
|
||
layout.addWidget(self.browse_btn)
|
||
|
||
self.setLayout(layout)
|
||
|
||
def browse_file(self):
|
||
"""浏览文件"""
|
||
file_path, _ = QFileDialog.getOpenFileName(
|
||
self, "选择文件", "", self.file_filter
|
||
)
|
||
if file_path:
|
||
self.line_edit.setText(file_path)
|
||
|
||
def get_path(self):
|
||
"""获取路径"""
|
||
return self.line_edit.text()
|
||
|
||
def set_path(self, path):
|
||
"""设置路径"""
|
||
self.line_edit.setText(str(path))
|
||
|
||
|
||
class PandasTableModel(QAbstractTableModel):
|
||
"""支持DataFrame的表格模型"""
|
||
def __init__(self, data_frame: pd.DataFrame):
|
||
super().__init__()
|
||
self._data = data_frame.copy()
|
||
if self._data.empty:
|
||
self._data = pd.DataFrame()
|
||
self._data.fillna("", inplace=True)
|
||
self._columns = [str(col) for col in self._data.columns]
|
||
|
||
def rowCount(self, parent=None):
|
||
return len(self._data)
|
||
|
||
def columnCount(self, parent=None):
|
||
return len(self._columns)
|
||
|
||
def data(self, index, role=Qt.DisplayRole):
|
||
if not index.isValid() or role != Qt.DisplayRole:
|
||
return None
|
||
|
||
value = self._data.iat[index.row(), index.column()]
|
||
if pd.isna(value):
|
||
return ""
|
||
return str(value)
|
||
|
||
def headerData(self, section, orientation, role=Qt.DisplayRole):
|
||
if role != Qt.DisplayRole:
|
||
return None
|
||
if orientation == Qt.Horizontal:
|
||
if section < len(self._columns):
|
||
return self._columns[section]
|
||
return str(section)
|
||
return str(section + 1)
|
||
|
||
def flags(self, index):
|
||
if not index.isValid():
|
||
return Qt.NoItemFlags
|
||
return Qt.ItemIsEnabled | Qt.ItemIsSelectable
|
||
|
||
|
||
class Step1Panel(QWidget):
|
||
"""1. 水域掩膜生成"""
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.init_ui()
|
||
|
||
def init_ui(self):
|
||
layout = QVBoxLayout()
|
||
|
||
# 标题
|
||
|
||
|
||
# 掩膜生成方式选择
|
||
method_group = QGroupBox("掩膜生成方式")
|
||
method_layout = QVBoxLayout()
|
||
|
||
# 使用现有掩膜文件
|
||
self.use_existing_radio = QRadioButton("使用现有掩膜文件")
|
||
self.use_existing_radio.setChecked(True)
|
||
method_layout.addWidget(self.use_existing_radio)
|
||
|
||
# 使用NDWI自动生成
|
||
self.use_ndwi_radio = QRadioButton("使用NDWI自动生成")
|
||
method_layout.addWidget(self.use_ndwi_radio)
|
||
|
||
method_group.setLayout(method_layout)
|
||
layout.addWidget(method_group)
|
||
|
||
# 掩膜文件选择
|
||
self.mask_file = FileSelectWidget(
|
||
"掩膜文件:",
|
||
"Shapefiles (*.shp);;Raster Files (*.dat *.tif);;All Files (*.*)"
|
||
)
|
||
layout.addWidget(self.mask_file)
|
||
|
||
# 影像文件选择(用于shp栅格化或NDWI生成)
|
||
self.img_file = FileSelectWidget(
|
||
"参考影像:",
|
||
"Image Files (*.bsq *.dat *.tif);;All Files (*.*)"
|
||
)
|
||
layout.addWidget(self.img_file)
|
||
|
||
# NDWI参数设置
|
||
ndwi_group = QGroupBox("NDWI参数设置")
|
||
ndwi_layout = QVBoxLayout()
|
||
|
||
# NDWI阈值
|
||
threshold_layout = QHBoxLayout()
|
||
threshold_layout.addWidget(QLabel("NDWI阈值:"))
|
||
self.ndwi_threshold = QDoubleSpinBox()
|
||
self.ndwi_threshold.setRange(0.0, 1.0)
|
||
self.ndwi_threshold.setSingleStep(0.05)
|
||
self.ndwi_threshold.setValue(0.4)
|
||
self.ndwi_threshold.setDecimals(2)
|
||
threshold_layout.addWidget(self.ndwi_threshold)
|
||
threshold_layout.addStretch()
|
||
ndwi_layout.addLayout(threshold_layout)
|
||
|
||
ndwi_group.setLayout(ndwi_layout)
|
||
layout.addWidget(ndwi_group)
|
||
|
||
# 输出文件路径
|
||
self.output_file = FileSelectWidget(
|
||
"输出掩膜:",
|
||
"Mask Files (*.dat *.tif);;All Files (*.*)"
|
||
)
|
||
self.output_file.line_edit.setPlaceholderText("water_mask.dat")
|
||
layout.addWidget(self.output_file)
|
||
|
||
# 提示信息
|
||
hint = QLabel("提示: 如果掩膜文件是Shapefile(.shp),需要提供参考影像用于栅格化;如果使用NDWI自动生成,只需要提供参考影像")
|
||
hint.setStyleSheet("color: #666; font-size: 10px;")
|
||
layout.addWidget(hint)
|
||
|
||
# 启用步骤
|
||
self.enable_checkbox = QCheckBox("启用此步骤")
|
||
self.enable_checkbox.setChecked(True)
|
||
layout.addWidget(self.enable_checkbox)
|
||
|
||
# 独立运行按钮
|
||
self.run_btn = QPushButton("独立运行此步骤")
|
||
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
|
||
self.run_btn.clicked.connect(self.run_step)
|
||
layout.addWidget(self.run_btn)
|
||
|
||
# 连接信号
|
||
self.use_existing_radio.toggled.connect(self.update_ui_state)
|
||
self.use_ndwi_radio.toggled.connect(self.update_ui_state)
|
||
|
||
layout.addStretch()
|
||
self.setLayout(layout)
|
||
|
||
# 初始UI状态
|
||
self.update_ui_state()
|
||
|
||
def update_ui_state(self):
|
||
"""根据选择的掩膜生成方式更新UI状态"""
|
||
use_ndwi = self.use_ndwi_radio.isChecked()
|
||
|
||
# 掩膜文件在NDWI模式下禁用
|
||
self.mask_file.setEnabled(not use_ndwi)
|
||
|
||
# 影像文件在两种模式下都需要
|
||
self.img_file.setEnabled(True)
|
||
|
||
# NDWI参数在NDWI模式下启用
|
||
for i in range(self.layout().count()):
|
||
widget = self.layout().itemAt(i).widget()
|
||
if widget and isinstance(widget, QGroupBox) and widget.title() == "NDWI参数设置":
|
||
widget.setEnabled(use_ndwi)
|
||
break
|
||
|
||
def get_config(self):
|
||
"""获取配置"""
|
||
config = {
|
||
'mask_path': None if self.use_ndwi_radio.isChecked() else self.mask_file.get_path(),
|
||
'use_ndwi': self.use_ndwi_radio.isChecked(),
|
||
'ndwi_threshold': self.ndwi_threshold.value()
|
||
}
|
||
img_path = self.img_file.get_path()
|
||
if img_path:
|
||
config['img_path'] = img_path
|
||
output_path = self.output_file.get_path()
|
||
if output_path:
|
||
config['output_path'] = output_path
|
||
return config
|
||
|
||
def set_config(self, config):
|
||
"""设置配置"""
|
||
if 'mask_path' in config:
|
||
self.mask_file.set_path(config['mask_path'])
|
||
if 'img_path' in config:
|
||
self.img_file.set_path(config['img_path'])
|
||
if 'output_path' in config:
|
||
self.output_file.set_path(config['output_path'])
|
||
if 'use_ndwi' in config:
|
||
if config['use_ndwi']:
|
||
self.use_ndwi_radio.setChecked(True)
|
||
else:
|
||
self.use_existing_radio.setChecked(True)
|
||
if 'ndwi_threshold' in config:
|
||
self.ndwi_threshold.setValue(config['ndwi_threshold'])
|
||
|
||
self.update_ui_state()
|
||
|
||
def run_step(self):
|
||
"""独立运行步骤1"""
|
||
# 验证输入
|
||
if self.use_ndwi_radio.isChecked():
|
||
# NDWI模式:需要影像文件
|
||
img_path = self.img_file.get_path()
|
||
if not img_path:
|
||
QMessageBox.warning(self, "输入错误", "请选择参考影像文件!")
|
||
return
|
||
else:
|
||
# 现有掩膜模式:需要掩膜文件
|
||
mask_path = self.mask_file.get_path()
|
||
if not mask_path:
|
||
QMessageBox.warning(self, "输入错误", "请选择掩膜文件!")
|
||
return
|
||
|
||
# 如果是shp文件,还需要影像文件
|
||
if mask_path.lower().endswith('.shp'):
|
||
img_path = self.img_file.get_path()
|
||
if not img_path:
|
||
QMessageBox.warning(self, "输入错误", "当使用shp文件时,需要提供参考影像用于栅格化!")
|
||
return
|
||
|
||
# 获取父窗口并运行步骤
|
||
parent = self.parent()
|
||
while parent and not hasattr(parent, 'run_single_step'):
|
||
parent = parent.parent()
|
||
|
||
if parent and hasattr(parent, 'run_single_step'):
|
||
config = {'step1': self.get_config()}
|
||
parent.run_single_step("step1", config)
|
||
|
||
|
||
class Step2Panel(QWidget):
|
||
"""2. 耀斑区域识别"""
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.init_ui()
|
||
|
||
def init_ui(self):
|
||
layout = QVBoxLayout()
|
||
|
||
# 标题
|
||
|
||
|
||
# 影像文件
|
||
self.img_file = FileSelectWidget(
|
||
"影像文件:",
|
||
"Image Files (*.bsq *.dat *.tif);;All Files (*.*)"
|
||
)
|
||
layout.addWidget(self.img_file)
|
||
|
||
# 水域掩膜文件(可选,用于独立运行)
|
||
self.water_mask_file = FileSelectWidget(
|
||
"水域掩膜:",
|
||
"Mask Files (*.dat *.tif);;All Files (*.*)"
|
||
)
|
||
self.water_mask_file.label.setText("水域掩膜(可选):")
|
||
layout.addWidget(self.water_mask_file)
|
||
|
||
# 参数设置
|
||
params_group = QGroupBox("检测参数")
|
||
params_layout = QFormLayout()
|
||
|
||
# 耀斑波长
|
||
self.glint_wave = QDoubleSpinBox()
|
||
self.glint_wave.setRange(300, 1000)
|
||
self.glint_wave.setValue(750.0)
|
||
self.glint_wave.setSuffix(" nm")
|
||
params_layout.addRow("耀斑检测波长:", self.glint_wave)
|
||
|
||
# 检测方法
|
||
self.method = QComboBox()
|
||
self.method.addItems(['otsu', 'zscore', 'percentile', 'iqr', 'adaptive', 'multi_band'])
|
||
params_layout.addRow("检测方法:", self.method)
|
||
|
||
# 最大连通域面积
|
||
self.max_area = QSpinBox()
|
||
self.max_area.setRange(0, 100000)
|
||
self.max_area.setValue(50)
|
||
self.max_area.setSpecialValueText("不过滤")
|
||
params_layout.addRow("最大连通域面积:", self.max_area)
|
||
|
||
# 岸边缓冲区
|
||
self.buffer_size = QSpinBox()
|
||
self.buffer_size.setRange(0, 200)
|
||
self.buffer_size.setValue(10)
|
||
self.buffer_size.setSpecialValueText("不设置")
|
||
params_layout.addRow("岸边缓冲区大小:", self.buffer_size)
|
||
|
||
params_group.setLayout(params_layout)
|
||
layout.addWidget(params_group)
|
||
|
||
# 输出文件路径
|
||
self.output_file = FileSelectWidget(
|
||
"输出耀斑掩膜:",
|
||
"Mask Files (*.dat *.tif);;All Files (*.*)"
|
||
)
|
||
self.output_file.line_edit.setPlaceholderText("glint_mask.dat")
|
||
layout.addWidget(self.output_file)
|
||
|
||
# 启用步骤
|
||
self.enable_checkbox = QCheckBox("启用此步骤")
|
||
self.enable_checkbox.setChecked(True)
|
||
layout.addWidget(self.enable_checkbox)
|
||
|
||
# 独立运行按钮
|
||
self.run_btn = QPushButton("独立运行此步骤")
|
||
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
|
||
self.run_btn.clicked.connect(self.run_step)
|
||
layout.addWidget(self.run_btn)
|
||
|
||
layout.addStretch()
|
||
self.setLayout(layout)
|
||
|
||
def get_config(self):
|
||
"""获取配置"""
|
||
config = {
|
||
'img_path': self.img_file.get_path(),
|
||
'glint_wave': self.glint_wave.value(),
|
||
'method': self.method.currentText(),
|
||
}
|
||
if self.max_area.value() > 0:
|
||
config['max_area'] = self.max_area.value()
|
||
if self.buffer_size.value() > 0:
|
||
config['buffer_size'] = self.buffer_size.value()
|
||
# 添加水域掩膜路径(用于独立运行)
|
||
water_mask_path = self.water_mask_file.get_path()
|
||
if water_mask_path:
|
||
config['water_mask_path'] = water_mask_path
|
||
# 添加输出路径
|
||
output_path = self.output_file.get_path()
|
||
if output_path:
|
||
config['output_path'] = output_path
|
||
return config
|
||
|
||
def set_config(self, config):
|
||
"""设置配置"""
|
||
if 'img_path' in config:
|
||
self.img_file.set_path(config['img_path'])
|
||
if 'glint_wave' in config:
|
||
self.glint_wave.setValue(config['glint_wave'])
|
||
if 'method' in config:
|
||
idx = self.method.findText(config['method'])
|
||
if idx >= 0:
|
||
self.method.setCurrentIndex(idx)
|
||
if 'max_area' in config:
|
||
self.max_area.setValue(config['max_area'])
|
||
if 'buffer_size' in config:
|
||
self.buffer_size.setValue(config['buffer_size'])
|
||
if 'water_mask_path' in config:
|
||
self.water_mask_file.set_path(config['water_mask_path'])
|
||
if 'output_path' in config:
|
||
self.output_file.set_path(config['output_path'])
|
||
|
||
def run_step(self):
|
||
"""独立运行步骤2"""
|
||
# 验证输入
|
||
img_path = self.img_file.get_path()
|
||
if not img_path:
|
||
QMessageBox.warning(self, "输入错误", "请选择影像文件!")
|
||
return
|
||
|
||
# 获取主窗口并运行步骤
|
||
main_window = self.window()
|
||
if hasattr(main_window, 'run_single_step'):
|
||
config = {'step2': self.get_config()}
|
||
main_window.run_single_step('step2', config)
|
||
|
||
|
||
class Step3Panel(QWidget):
|
||
"""步骤3:耀斑去除"""
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.init_ui()
|
||
|
||
def init_ui(self):
|
||
layout = QVBoxLayout()
|
||
|
||
# 标题
|
||
|
||
|
||
# 影像文件
|
||
self.img_file = FileSelectWidget(
|
||
"影像文件:",
|
||
"Image Files (*.bsq *.dat *.tif);;All Files (*.*)"
|
||
)
|
||
layout.addWidget(self.img_file)
|
||
|
||
# 水域掩膜/边界:完整流程可由步骤1自动生成;独立单步运行时须手动指定
|
||
self.water_mask_file = FileSelectWidget(
|
||
"水域掩膜/边界:",
|
||
"Mask/Boundary (*.dat *.tif *.shp);;All Files (*.*)"
|
||
)
|
||
layout.addWidget(self.water_mask_file)
|
||
step3_mask_hint = QLabel(
|
||
"提示:独立运行本步骤时必须选择水域掩膜或边界(与影像同区域的 .dat/.tif 掩膜,或 .shp 矢量)。"
|
||
)
|
||
step3_mask_hint.setWordWrap(True)
|
||
step3_mask_hint.setStyleSheet("color: #666; font-size: 10px;")
|
||
layout.addWidget(step3_mask_hint)
|
||
|
||
# 方法选择
|
||
method_group = QGroupBox("去耀斑方法")
|
||
method_layout = QVBoxLayout()
|
||
|
||
self.method = QComboBox()
|
||
self.method.addItems(['goodman', 'kutser', 'hedley', 'sugar'])
|
||
self.method.currentTextChanged.connect(self.on_method_changed)
|
||
method_layout.addWidget(self.method)
|
||
|
||
method_group.setLayout(method_layout)
|
||
layout.addWidget(method_group)
|
||
|
||
# Goodman参数组
|
||
self.goodman_group = QGroupBox("Goodman方法参数")
|
||
goodman_layout = QFormLayout()
|
||
|
||
self.nir_lower = QSpinBox()
|
||
self.nir_lower.setRange(0, 200)
|
||
self.nir_lower.setValue(65)
|
||
goodman_layout.addRow("NIR下波段索引:", self.nir_lower)
|
||
|
||
self.nir_upper = QSpinBox()
|
||
self.nir_upper.setRange(0, 200)
|
||
self.nir_upper.setValue(91)
|
||
goodman_layout.addRow("NIR上波段索引:", self.nir_upper)
|
||
|
||
self.goodman_a = QDoubleSpinBox()
|
||
self.goodman_a.setDecimals(6)
|
||
self.goodman_a.setRange(0, 1)
|
||
self.goodman_a.setValue(0.000019)
|
||
goodman_layout.addRow("参数A:", self.goodman_a)
|
||
|
||
self.goodman_b = QDoubleSpinBox()
|
||
self.goodman_b.setDecimals(2)
|
||
self.goodman_b.setRange(0, 1)
|
||
self.goodman_b.setValue(0.1)
|
||
goodman_layout.addRow("参数B:", self.goodman_b)
|
||
|
||
self.goodman_group.setLayout(goodman_layout)
|
||
layout.addWidget(self.goodman_group)
|
||
|
||
# Kutser参数组
|
||
self.kutser_group = QGroupBox("Kutser方法参数")
|
||
kutser_layout = QFormLayout()
|
||
|
||
self.oxy_band = QSpinBox()
|
||
self.oxy_band.setRange(0, 200)
|
||
self.oxy_band.setValue(8)
|
||
kutser_layout.addRow("氧吸收波段索引:", self.oxy_band)
|
||
|
||
self.lower_oxy = QDoubleSpinBox()
|
||
self.lower_oxy.setDecimals(2)
|
||
self.lower_oxy.setRange(0, 1000)
|
||
self.lower_oxy.setValue(756.54)
|
||
kutser_layout.addRow("下氧吸收波长(nm):", self.lower_oxy)
|
||
|
||
self.upper_oxy = QDoubleSpinBox()
|
||
self.upper_oxy.setDecimals(2)
|
||
self.upper_oxy.setRange(0, 1000)
|
||
self.upper_oxy.setValue(766.54)
|
||
kutser_layout.addRow("上氧吸收波长(nm):", self.upper_oxy)
|
||
|
||
self.nir_band = QSpinBox()
|
||
self.nir_band.setRange(0, 200)
|
||
self.nir_band.setValue(65)
|
||
kutser_layout.addRow("NIR波段索引:", self.nir_band)
|
||
|
||
self.kutser_group.setLayout(kutser_layout)
|
||
self.kutser_group.setVisible(False)
|
||
layout.addWidget(self.kutser_group)
|
||
|
||
# Hedley参数组
|
||
self.hedley_group = QGroupBox("Hedley方法参数")
|
||
hedley_layout = QFormLayout()
|
||
|
||
self.hedley_nir_band = QSpinBox()
|
||
self.hedley_nir_band.setRange(0, 200)
|
||
self.hedley_nir_band.setValue(47)
|
||
hedley_layout.addRow("NIR波段索引:", self.hedley_nir_band)
|
||
|
||
self.hedley_group.setLayout(hedley_layout)
|
||
self.hedley_group.setVisible(False)
|
||
layout.addWidget(self.hedley_group)
|
||
|
||
# SUGAR参数组
|
||
self.sugar_group = QGroupBox("SUGAR方法参数")
|
||
sugar_layout = QFormLayout()
|
||
|
||
self.sugar_iter = QSpinBox()
|
||
self.sugar_iter.setRange(1, 20)
|
||
self.sugar_iter.setValue(3)
|
||
self.sugar_iter.setSpecialValueText("自动")
|
||
sugar_layout.addRow("迭代次数:", self.sugar_iter)
|
||
|
||
self.sugar_sigma = QDoubleSpinBox()
|
||
self.sugar_sigma.setDecimals(2)
|
||
self.sugar_sigma.setRange(0.1, 10)
|
||
self.sugar_sigma.setValue(1.0)
|
||
sugar_layout.addRow("LoG平滑σ:", self.sugar_sigma)
|
||
|
||
self.sugar_estimate_background = QCheckBox()
|
||
self.sugar_estimate_background.setChecked(True)
|
||
sugar_layout.addRow("估计背景光谱:", self.sugar_estimate_background)
|
||
|
||
self.sugar_glint_mask_method = QComboBox()
|
||
self.sugar_glint_mask_method.addItems(['cdf', 'otsu'])
|
||
self.sugar_glint_mask_method.setCurrentText('cdf')
|
||
sugar_layout.addRow("耀斑掩膜方法:", self.sugar_glint_mask_method)
|
||
|
||
self.sugar_termination_thresh = QDoubleSpinBox()
|
||
self.sugar_termination_thresh.setDecimals(2)
|
||
self.sugar_termination_thresh.setRange(1, 100)
|
||
self.sugar_termination_thresh.setValue(20.0)
|
||
sugar_layout.addRow("终止阈值:", self.sugar_termination_thresh)
|
||
|
||
self.sugar_bounds = QLineEdit()
|
||
self.sugar_bounds.setText("[(1, 2)]")
|
||
sugar_layout.addRow("优化边界:", self.sugar_bounds)
|
||
|
||
self.sugar_group.setLayout(sugar_layout)
|
||
self.sugar_group.setVisible(False)
|
||
layout.addWidget(self.sugar_group)
|
||
|
||
# 插值选项
|
||
interp_group = QGroupBox("0值像素插值")
|
||
interp_layout = QFormLayout()
|
||
|
||
self.interpolate_zeros = QCheckBox("启用插值")
|
||
interp_layout.addRow("", self.interpolate_zeros)
|
||
|
||
self.interp_method = QComboBox()
|
||
self.interp_method.addItems(['nearest', 'bilinear', 'spline', 'kriging'])
|
||
self.interp_method.setCurrentText('bilinear')
|
||
interp_layout.addRow("插值方法:", self.interp_method)
|
||
|
||
interp_group.setLayout(interp_layout)
|
||
layout.addWidget(interp_group)
|
||
|
||
# 输出文件路径
|
||
self.output_file = FileSelectWidget(
|
||
"输出影像:",
|
||
"Image Files (*.bsq *.dat *.tif);;All Files (*.*)"
|
||
)
|
||
self.output_file.line_edit.setPlaceholderText("deglint_image.dat")
|
||
layout.addWidget(self.output_file)
|
||
|
||
# 启用步骤
|
||
self.enable_checkbox = QCheckBox("启用此步骤")
|
||
self.enable_checkbox.setChecked(True)
|
||
layout.addWidget(self.enable_checkbox)
|
||
|
||
# 独立运行按钮
|
||
self.run_btn = QPushButton("独立运行此步骤")
|
||
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
|
||
self.run_btn.clicked.connect(self.run_step)
|
||
layout.addWidget(self.run_btn)
|
||
|
||
layout.addStretch()
|
||
self.setLayout(layout)
|
||
|
||
def on_method_changed(self, method):
|
||
"""方法改变时更新参数显示"""
|
||
self.goodman_group.setVisible(method == 'goodman')
|
||
self.kutser_group.setVisible(method == 'kutser')
|
||
self.hedley_group.setVisible(method == 'hedley')
|
||
self.sugar_group.setVisible(method == 'sugar')
|
||
|
||
def get_config(self):
|
||
"""获取配置"""
|
||
config = {
|
||
'img_path': self.img_file.get_path(),
|
||
'method': self.method.currentText(),
|
||
'enabled': self.enable_checkbox.isChecked(),
|
||
'interpolate_zeros': self.interpolate_zeros.isChecked(),
|
||
'interpolation_method': self.interp_method.currentText(),
|
||
}
|
||
water_mask_path = self.water_mask_file.get_path()
|
||
if water_mask_path:
|
||
config['water_mask'] = water_mask_path
|
||
output_path = self.output_file.get_path()
|
||
if output_path:
|
||
config['output_path'] = output_path
|
||
|
||
method = self.method.currentText()
|
||
|
||
if method == 'goodman':
|
||
config['nir_lower'] = self.nir_lower.value()
|
||
config['nir_upper'] = self.nir_upper.value()
|
||
config['goodman_A'] = self.goodman_a.value()
|
||
config['goodman_B'] = self.goodman_b.value()
|
||
|
||
elif method == 'kutser':
|
||
config['oxy_band'] = self.oxy_band.value()
|
||
config['lower_oxy'] = self.lower_oxy.value()
|
||
config['upper_oxy'] = self.upper_oxy.value()
|
||
config['nir_band'] = self.nir_band.value()
|
||
|
||
elif method == 'hedley':
|
||
config['hedley_nir_band'] = self.hedley_nir_band.value()
|
||
|
||
elif method == 'sugar':
|
||
config['sugar_iter'] = self.sugar_iter.value() if self.sugar_iter.value() > 0 else None
|
||
config['sugar_sigma'] = self.sugar_sigma.value()
|
||
config['sugar_estimate_background'] = self.sugar_estimate_background.isChecked()
|
||
config['sugar_glint_mask_method'] = self.sugar_glint_mask_method.currentText()
|
||
config['sugar_termination_thresh'] = self.sugar_termination_thresh.value()
|
||
# 解析bounds字符串
|
||
try:
|
||
import ast
|
||
config['sugar_bounds'] = ast.literal_eval(self.sugar_bounds.text())
|
||
except:
|
||
config['sugar_bounds'] = [(1, 2)] # 默认值
|
||
|
||
return config
|
||
|
||
def set_config(self, config):
|
||
"""设置配置"""
|
||
if 'img_path' in config:
|
||
self.img_file.set_path(config['img_path'])
|
||
if 'water_mask' in config:
|
||
self.water_mask_file.set_path(config['water_mask'])
|
||
if 'output_path' in config:
|
||
self.output_file.set_path(config['output_path'])
|
||
if 'method' in config:
|
||
idx = self.method.findText(config['method'])
|
||
if idx >= 0:
|
||
self.method.setCurrentIndex(idx)
|
||
if 'enabled' in config:
|
||
self.enable_checkbox.setChecked(config['enabled'])
|
||
if 'interpolate_zeros' in config:
|
||
self.interpolate_zeros.setChecked(config['interpolate_zeros'])
|
||
if 'interpolation_method' in config:
|
||
idx = self.interp_method.findText(config['interpolation_method'])
|
||
if idx >= 0:
|
||
self.interp_method.setCurrentIndex(idx)
|
||
|
||
# Goodman参数
|
||
if 'nir_lower' in config:
|
||
self.nir_lower.setValue(config['nir_lower'])
|
||
if 'nir_upper' in config:
|
||
self.nir_upper.setValue(config['nir_upper'])
|
||
if 'goodman_A' in config:
|
||
self.goodman_a.setValue(config['goodman_A'])
|
||
if 'goodman_B' in config:
|
||
self.goodman_b.setValue(config['goodman_B'])
|
||
|
||
# Kutser参数
|
||
if 'oxy_band' in config:
|
||
self.oxy_band.setValue(config['oxy_band'])
|
||
if 'lower_oxy' in config:
|
||
self.lower_oxy.setValue(config['lower_oxy'])
|
||
if 'upper_oxy' in config:
|
||
self.upper_oxy.setValue(config['upper_oxy'])
|
||
if 'nir_band' in config:
|
||
self.nir_band.setValue(config['nir_band'])
|
||
|
||
# Hedley参数
|
||
if 'hedley_nir_band' in config:
|
||
self.hedley_nir_band.setValue(config['hedley_nir_band'])
|
||
|
||
# SUGAR参数
|
||
if 'sugar_iter' in config:
|
||
self.sugar_iter.setValue(config['sugar_iter'] if config['sugar_iter'] is not None else 0)
|
||
if 'sugar_sigma' in config:
|
||
self.sugar_sigma.setValue(config['sugar_sigma'])
|
||
if 'sugar_estimate_background' in config:
|
||
self.sugar_estimate_background.setChecked(config['sugar_estimate_background'])
|
||
if 'sugar_glint_mask_method' in config:
|
||
idx = self.sugar_glint_mask_method.findText(config['sugar_glint_mask_method'])
|
||
if idx >= 0:
|
||
self.sugar_glint_mask_method.setCurrentIndex(idx)
|
||
if 'sugar_termination_thresh' in config:
|
||
self.sugar_termination_thresh.setValue(config['sugar_termination_thresh'])
|
||
if 'sugar_bounds' in config:
|
||
self.sugar_bounds.setText(str(config['sugar_bounds']))
|
||
|
||
def run_step(self):
|
||
"""独立运行步骤3"""
|
||
# 验证输入
|
||
img_path = self.img_file.get_path()
|
||
if not img_path:
|
||
QMessageBox.warning(self, "输入错误", "请选择影像文件!")
|
||
return
|
||
if self.enable_checkbox.isChecked():
|
||
water_mask_path = self.water_mask_file.get_path()
|
||
if not water_mask_path:
|
||
QMessageBox.warning(
|
||
self,
|
||
"输入错误",
|
||
"独立运行耀斑去除时,必须选择水域掩膜或边界文件。\n\n"
|
||
"请提供与当前影像空间一致的水域栅格掩膜(.dat/.tif),或水域矢量边界(.shp)。\n"
|
||
"若刚跑过完整流程,可使用步骤1生成的水域掩膜文件。",
|
||
)
|
||
return
|
||
|
||
# 获取主窗口并运行步骤
|
||
main_window = self.window()
|
||
if hasattr(main_window, 'run_single_step'):
|
||
config = {'step3': self.get_config()}
|
||
main_window.run_single_step('step3', config)
|
||
|
||
|
||
class Step4Panel(QWidget):
|
||
"""步骤4:数据预处理"""
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.init_ui()
|
||
|
||
def init_ui(self):
|
||
layout = QVBoxLayout()
|
||
|
||
# 标题
|
||
|
||
# CSV文件
|
||
self.csv_file = FileSelectWidget(
|
||
"水质参数文件:",
|
||
"CSV Files (*.csv);;All Files (*.*)"
|
||
)
|
||
layout.addWidget(self.csv_file)
|
||
|
||
hint = QLabel("提示: 处理CSV文件,筛选剔除异常值")
|
||
hint.setStyleSheet("color: #666; font-size: 10px;")
|
||
layout.addWidget(hint)
|
||
|
||
preview_group = QGroupBox("CSV数据预览")
|
||
preview_layout = QVBoxLayout()
|
||
|
||
controls_layout = QHBoxLayout()
|
||
controls_layout.addWidget(QLabel("预览行数:"))
|
||
self.preview_rows_spin = QSpinBox()
|
||
self.preview_rows_spin.setRange(1, 200)
|
||
self.preview_rows_spin.setValue(10)
|
||
controls_layout.addWidget(self.preview_rows_spin)
|
||
self.preview_btn = QPushButton("刷新预览")
|
||
self.preview_btn.clicked.connect(self.load_csv_preview)
|
||
controls_layout.addWidget(self.preview_btn)
|
||
controls_layout.addStretch()
|
||
|
||
self.preview_table = QTableView()
|
||
self.preview_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
||
self.preview_table.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||
self.preview_table.setSelectionMode(QAbstractItemView.SingleSelection)
|
||
self.preview_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||
self.preview_table.verticalHeader().setVisible(False)
|
||
self.preview_table.setMinimumHeight(200)
|
||
|
||
self.preview_status_label = QLabel("请选择CSV文件并点击刷新预览")
|
||
self.preview_status_label.setStyleSheet("color: #666; font-size: 11px;")
|
||
|
||
preview_layout.addLayout(controls_layout)
|
||
preview_layout.addWidget(self.preview_table)
|
||
preview_layout.addWidget(self.preview_status_label)
|
||
preview_group.setLayout(preview_layout)
|
||
layout.addWidget(preview_group)
|
||
|
||
# 输出文件路径
|
||
self.output_file = FileSelectWidget(
|
||
"输出处理后CSV:",
|
||
"CSV Files (*.csv);;All Files (*.*)"
|
||
)
|
||
self.output_file.line_edit.setPlaceholderText("processed_data.csv")
|
||
layout.addWidget(self.output_file)
|
||
|
||
# 启用步骤
|
||
self.enable_checkbox = QCheckBox("启用此步骤")
|
||
self.enable_checkbox.setChecked(True)
|
||
layout.addWidget(self.enable_checkbox)
|
||
|
||
# 独立运行按钮
|
||
self.run_btn = QPushButton("独立运行此步骤")
|
||
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
|
||
self.run_btn.clicked.connect(self.run_step)
|
||
layout.addWidget(self.run_btn)
|
||
|
||
layout.addStretch()
|
||
self.setLayout(layout)
|
||
self.reset_preview()
|
||
|
||
def get_config(self):
|
||
"""获取配置"""
|
||
config = {
|
||
'csv_path': self.csv_file.get_path(),
|
||
}
|
||
output_path = self.output_file.get_path()
|
||
if output_path:
|
||
config['output_path'] = output_path
|
||
return config
|
||
|
||
def set_config(self, config):
|
||
"""设置配置"""
|
||
if 'csv_path' in config:
|
||
self.csv_file.set_path(config['csv_path'])
|
||
self.load_csv_preview()
|
||
if 'output_path' in config:
|
||
self.output_file.set_path(config['output_path'])
|
||
|
||
def run_step(self):
|
||
"""独立运行步骤4"""
|
||
# 验证输入
|
||
csv_path = self.csv_file.get_path()
|
||
if not csv_path:
|
||
QMessageBox.warning(self, "输入错误", "请选择水质参数文件!")
|
||
return
|
||
|
||
# 获取主窗口并运行步骤
|
||
main_window = self.window()
|
||
if hasattr(main_window, 'run_single_step'):
|
||
config = {'step4': self.get_config()}
|
||
main_window.run_single_step('step4', config)
|
||
|
||
def reset_preview(self, message="请选择CSV文件并点击刷新预览"):
|
||
"""重置预览表格"""
|
||
empty_model = PandasTableModel(pd.DataFrame())
|
||
self.preview_table.setModel(empty_model)
|
||
self.preview_status_label.setText(message)
|
||
|
||
def load_csv_preview(self):
|
||
"""加载CSV预览数据"""
|
||
csv_path = self.csv_file.get_path()
|
||
if not csv_path:
|
||
self.reset_preview("请先选择CSV文件")
|
||
return
|
||
if not os.path.exists(csv_path):
|
||
self.reset_preview("文件不存在,请检查路径")
|
||
return
|
||
|
||
try:
|
||
rows_to_preview = max(1, self.preview_rows_spin.value())
|
||
df = pd.read_csv(csv_path, nrows=rows_to_preview)
|
||
if df.empty:
|
||
self.reset_preview("CSV文件为空")
|
||
return
|
||
|
||
model = PandasTableModel(df)
|
||
self.preview_table.setModel(model)
|
||
self.preview_status_label.setText(
|
||
f"预览 {len(df)} 行,{len(df.columns)} 列(总行数可能更多)"
|
||
)
|
||
except Exception as exc:
|
||
self.reset_preview(f"加载失败: {exc}")
|
||
|
||
|
||
class Step5Panel(QWidget):
|
||
"""步骤5:光谱提取"""
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.init_ui()
|
||
|
||
def init_ui(self):
|
||
layout = QVBoxLayout()
|
||
|
||
# 标题
|
||
title = QLabel("步骤5:训练样本光谱提取")
|
||
title.setFont(QFont("Arial", 12, QFont.Bold))
|
||
layout.addWidget(title)
|
||
|
||
# 去耀斑影像文件(用于独立运行)
|
||
self.deglint_img_file = FileSelectWidget(
|
||
"去耀斑影像:",
|
||
"Image Files (*.bsq *.dat *.tif);;All Files (*.*)"
|
||
)
|
||
layout.addWidget(self.deglint_img_file)
|
||
|
||
# 处理后的CSV文件(用于独立运行)
|
||
self.csv_file = FileSelectWidget(
|
||
"处理后CSV:",
|
||
"CSV Files (*.csv);;All Files (*.*)"
|
||
)
|
||
layout.addWidget(self.csv_file)
|
||
|
||
# 水体掩膜文件(可选,用于独立运行)
|
||
self.boundary_mask_file = FileSelectWidget(
|
||
"水体掩膜:",
|
||
"Mask Files (*.dat *.tif);;All Files (*.*)"
|
||
)
|
||
self.boundary_mask_file.line_edit.setPlaceholderText("可选,如不选择则自动生成")
|
||
layout.addWidget(self.boundary_mask_file)
|
||
|
||
self.glint_mask_file = FileSelectWidget(
|
||
"耀斑掩膜:",
|
||
"Mask Files (*.dat *.tif);;All Files (*.*)"
|
||
)
|
||
layout.addWidget(self.glint_mask_file)
|
||
step5_glint_hint = QLabel(
|
||
"提示:独立运行本步骤时必须选择耀斑掩膜(通常为步骤2输出的 severe_glint_area.dat),用于在采样时避开耀斑像元。"
|
||
)
|
||
step5_glint_hint.setWordWrap(True)
|
||
step5_glint_hint.setStyleSheet("color: #666; font-size: 10px;")
|
||
layout.addWidget(step5_glint_hint)
|
||
|
||
# 参数设置
|
||
params_group = QGroupBox("提取参数")
|
||
params_layout = QFormLayout()
|
||
|
||
self.radius = QSpinBox()
|
||
self.radius.setRange(1, 50)
|
||
self.radius.setValue(5)
|
||
params_layout.addRow("采样半径(像素):", self.radius)
|
||
|
||
self.source_epsg = QSpinBox()
|
||
self.source_epsg.setRange(1000, 99999)
|
||
self.source_epsg.setValue(4326)
|
||
params_layout.addRow("源坐标系EPSG:", self.source_epsg)
|
||
|
||
params_group.setLayout(params_layout)
|
||
layout.addWidget(params_group)
|
||
|
||
# 输出文件路径
|
||
self.output_file = FileSelectWidget(
|
||
"输出训练数据:",
|
||
"CSV Files (*.csv);;All Files (*.*)"
|
||
)
|
||
self.output_file.line_edit.setPlaceholderText("training_spectra.csv")
|
||
layout.addWidget(self.output_file)
|
||
|
||
# 启用步骤
|
||
self.enable_checkbox = QCheckBox("启用此步骤")
|
||
self.enable_checkbox.setChecked(True)
|
||
layout.addWidget(self.enable_checkbox)
|
||
|
||
# 独立运行按钮
|
||
self.run_btn = QPushButton("独立运行此步骤")
|
||
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
|
||
self.run_btn.clicked.connect(self.run_step)
|
||
layout.addWidget(self.run_btn)
|
||
|
||
layout.addStretch()
|
||
self.setLayout(layout)
|
||
|
||
def get_config(self):
|
||
"""获取配置"""
|
||
config = {
|
||
'radius': self.radius.value(),
|
||
'source_epsg': self.source_epsg.value(),
|
||
}
|
||
# 添加独立运行所需的文件路径
|
||
deglint_img_path = self.deglint_img_file.get_path()
|
||
if deglint_img_path:
|
||
config['deglint_img_path'] = deglint_img_path
|
||
csv_path = self.csv_file.get_path()
|
||
if csv_path:
|
||
config['csv_path'] = csv_path
|
||
boundary_path = self.boundary_mask_file.get_path()
|
||
if boundary_path:
|
||
config['boundary_path'] = boundary_path
|
||
glint_mask_path = self.glint_mask_file.get_path()
|
||
if glint_mask_path:
|
||
config['glint_mask_path'] = glint_mask_path
|
||
# 添加输出路径
|
||
output_path = self.output_file.get_path()
|
||
if output_path:
|
||
config['output_path'] = output_path
|
||
return config
|
||
|
||
def set_config(self, config):
|
||
"""设置配置"""
|
||
if 'radius' in config:
|
||
self.radius.setValue(config['radius'])
|
||
if 'source_epsg' in config:
|
||
self.source_epsg.setValue(config['source_epsg'])
|
||
if 'deglint_img_path' in config:
|
||
self.deglint_img_file.set_path(config['deglint_img_path'])
|
||
if 'csv_path' in config:
|
||
self.csv_file.set_path(config['csv_path'])
|
||
if 'boundary_path' in config:
|
||
self.boundary_mask_file.set_path(config['boundary_path'])
|
||
if 'glint_mask_path' in config:
|
||
self.glint_mask_file.set_path(config['glint_mask_path'])
|
||
if 'output_path' in config:
|
||
self.output_file.set_path(config['output_path'])
|
||
|
||
def run_step(self):
|
||
"""独立运行步骤5"""
|
||
# 验证输入
|
||
deglint_img_path = self.deglint_img_file.get_path()
|
||
csv_path = self.csv_file.get_path()
|
||
if not deglint_img_path:
|
||
QMessageBox.warning(self, "输入错误", "请选择去耀斑影像文件!")
|
||
return
|
||
if not csv_path:
|
||
QMessageBox.warning(self, "输入错误", "请选择处理后的CSV文件!")
|
||
return
|
||
if not self.glint_mask_file.get_path():
|
||
QMessageBox.warning(
|
||
self,
|
||
"输入错误",
|
||
"独立运行光谱特征提取时,必须选择耀斑掩膜文件。\n\n"
|
||
"请提供与去耀斑影像对应的耀斑二值掩膜(一般为步骤2输出的 severe_glint_area.dat)。",
|
||
)
|
||
return
|
||
|
||
# 获取主窗口并运行步骤
|
||
main_window = self.window()
|
||
if hasattr(main_window, 'run_single_step'):
|
||
config = {'step5': self.get_config()}
|
||
main_window.run_single_step('step5', config)
|
||
|
||
|
||
class Step5_5Panel(QWidget):
|
||
"""步骤5.5:水质指数计算"""
|
||
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.index_checkboxes: Dict[str, QCheckBox] = {}
|
||
self.csv_columns = [] # 存储CSV文件列名
|
||
self.init_ui()
|
||
|
||
def init_ui(self):
|
||
main_layout = QVBoxLayout()
|
||
|
||
# 标题
|
||
|
||
|
||
# 数据文件选择
|
||
data_group = QGroupBox("数据文件")
|
||
data_layout = QVBoxLayout()
|
||
|
||
# 训练数据CSV文件选择
|
||
self.training_data_widget = FileSelectWidget("训练数据CSV文件:", "CSV Files (*.csv)")
|
||
data_layout.addWidget(self.training_data_widget)
|
||
|
||
# 公式CSV文件选择
|
||
self.formula_csv_widget = FileSelectWidget("公式CSV文件:", "CSV Files (*.csv)")
|
||
data_layout.addWidget(self.formula_csv_widget)
|
||
|
||
# 刷新公式按钮
|
||
refresh_layout = QHBoxLayout()
|
||
self.refresh_button = QPushButton("刷新公式列表")
|
||
self.refresh_button.clicked.connect(self.refresh_formulas)
|
||
refresh_layout.addWidget(self.refresh_button)
|
||
refresh_layout.addStretch()
|
||
data_layout.addLayout(refresh_layout)
|
||
|
||
data_group.setLayout(data_layout)
|
||
main_layout.addWidget(data_group)
|
||
|
||
# 公式选择区域
|
||
self.formula_group = QGroupBox("选择要计算的公式")
|
||
formula_outer_layout = QVBoxLayout()
|
||
|
||
# 按钮控制区域
|
||
button_layout = QHBoxLayout()
|
||
self.select_all_btn = QPushButton("全选")
|
||
self.select_all_btn.clicked.connect(self.select_all_formulas)
|
||
self.deselect_all_btn = QPushButton("清空")
|
||
self.deselect_all_btn.clicked.connect(self.deselect_all_formulas)
|
||
button_layout.addWidget(self.select_all_btn)
|
||
button_layout.addWidget(self.deselect_all_btn)
|
||
button_layout.addStretch()
|
||
|
||
formula_outer_layout.addLayout(button_layout)
|
||
|
||
# 公式勾选框网格布局
|
||
self.formula_layout = QGridLayout()
|
||
formula_outer_layout.addLayout(self.formula_layout)
|
||
|
||
self.formula_group.setLayout(formula_outer_layout)
|
||
main_layout.addWidget(self.formula_group)
|
||
|
||
# 输出文件设置
|
||
output_group = QGroupBox("输出设置")
|
||
output_layout = QVBoxLayout()
|
||
|
||
output_hbox = QHBoxLayout()
|
||
output_hbox.addWidget(QLabel("输出文件名:"))
|
||
self.output_filename = QLineEdit("water_quality_indices.csv")
|
||
output_hbox.addWidget(self.output_filename)
|
||
output_layout.addLayout(output_hbox)
|
||
|
||
output_group.setLayout(output_layout)
|
||
main_layout.addWidget(output_group)
|
||
|
||
# 启用选项
|
||
self.enable_checkbox = QCheckBox("启用此步骤")
|
||
self.enable_checkbox.setChecked(True)
|
||
main_layout.addWidget(self.enable_checkbox)
|
||
|
||
# 独立运行按钮
|
||
self.run_button = QPushButton("独立运行此步骤")
|
||
self.run_button.setStyleSheet("""
|
||
QPushButton {
|
||
background-color: #4CAF50;
|
||
color: white;
|
||
padding: 8px 16px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-weight: bold;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: #45a049;
|
||
}
|
||
QPushButton:pressed {
|
||
background-color: #3e8e41;
|
||
}
|
||
""")
|
||
self.run_button.clicked.connect(self.run_step)
|
||
main_layout.addWidget(self.run_button)
|
||
|
||
# 公式编辑区域
|
||
formula_edit_group = QGroupBox("添加自定义公式")
|
||
formula_edit_layout = QFormLayout()
|
||
|
||
self.formula_name_edit = QLineEdit()
|
||
|
||
# 公式类别下拉选择框
|
||
self.formula_category_combo = QComboBox()
|
||
self.formula_category_combo.addItems([
|
||
"chlorophyll_a",
|
||
"Phycocyanin (BGA_PC)",
|
||
"Total Nitrogen (TN)",
|
||
"Total Phosphorus (TP)",
|
||
"Orthophosphate",
|
||
"COD",
|
||
"BOD",
|
||
"TOC",
|
||
"Dissolved Oxygen (DO)",
|
||
"E. coli",
|
||
"Total Coliforms",
|
||
"Turbidity",
|
||
"Total Suspended Solids (TSS)",
|
||
"Color",
|
||
"pH",
|
||
"Temperature",
|
||
"Conductivity",
|
||
"Total Dissolved Solids (TDS)"
|
||
])
|
||
self.formula_category_combo.setEditable(True) # 允许用户输入自定义类别
|
||
|
||
self.formula_expression_edit = QLineEdit()
|
||
self.formula_reference_edit = QLineEdit()
|
||
|
||
formula_edit_layout.addRow("公式名称:", self.formula_name_edit)
|
||
formula_edit_layout.addRow("公式类别:", self.formula_category_combo)
|
||
formula_edit_layout.addRow("公式表达式:", self.formula_expression_edit)
|
||
formula_edit_layout.addRow("参考文献:", self.formula_reference_edit)
|
||
|
||
add_button = QPushButton("添加公式")
|
||
add_button.clicked.connect(self.add_custom_formula)
|
||
formula_edit_layout.addRow(add_button)
|
||
|
||
formula_edit_group.setLayout(formula_edit_layout)
|
||
main_layout.addWidget(formula_edit_group)
|
||
|
||
main_layout.addStretch()
|
||
self.setLayout(main_layout)
|
||
|
||
def refresh_formulas(self):
|
||
"""刷新公式列表"""
|
||
formula_csv_path = self.formula_csv_widget.get_path()
|
||
if not formula_csv_path or not os.path.exists(formula_csv_path):
|
||
QMessageBox.warning(self, "警告", "请先选择有效的公式CSV文件")
|
||
return
|
||
|
||
try:
|
||
# 清除现有的勾选框
|
||
for checkbox in self.index_checkboxes.values():
|
||
self.formula_layout.removeWidget(checkbox)
|
||
checkbox.deleteLater()
|
||
self.index_checkboxes.clear()
|
||
|
||
# 读取公式CSV文件
|
||
df = pd.read_csv(formula_csv_path)
|
||
if df.empty or 'Formula_Name' not in df.columns:
|
||
QMessageBox.warning(self, "警告", "公式CSV文件格式不正确")
|
||
return
|
||
|
||
# 获取所有公式名称(跳过第一行)
|
||
formula_names = df['Formula_Name'].tolist()[1:]
|
||
|
||
# 创建3列布局的勾选框
|
||
row, col = 0, 0
|
||
for formula_name in formula_names:
|
||
if pd.isna(formula_name) or not formula_name.strip():
|
||
continue
|
||
|
||
checkbox = QCheckBox(formula_name.strip())
|
||
checkbox.setChecked(True)
|
||
self.index_checkboxes[formula_name.strip()] = checkbox
|
||
self.formula_layout.addWidget(checkbox, row, col)
|
||
|
||
col += 1
|
||
if col >= 3: # 每行3列
|
||
col = 0
|
||
row += 1
|
||
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "错误", f"读取公式文件失败: {str(e)}")
|
||
|
||
def add_custom_formula(self):
|
||
"""添加自定义公式到公式CSV文件"""
|
||
formula_csv_path = self.formula_csv_widget.get_path()
|
||
if not formula_csv_path:
|
||
QMessageBox.warning(self, "警告", "请先选择公式CSV文件")
|
||
return
|
||
|
||
formula_name = self.formula_name_edit.text().strip()
|
||
formula_category = self.formula_category_combo.currentText().strip()
|
||
formula_expression = self.formula_expression_edit.text().strip()
|
||
formula_reference = self.formula_reference_edit.text().strip()
|
||
|
||
if not all([formula_name, formula_category, formula_expression]):
|
||
QMessageBox.warning(self, "警告", "请填写公式名称、类别和表达式")
|
||
return
|
||
|
||
try:
|
||
# 读取现有公式文件或创建新文件
|
||
if os.path.exists(formula_csv_path):
|
||
df = pd.read_csv(formula_csv_path)
|
||
else:
|
||
df = pd.DataFrame(columns=['Formula_Name', 'Category', 'Formula', 'Reference'])
|
||
|
||
# 添加新公式
|
||
new_row = pd.DataFrame({
|
||
'Formula_Name': [formula_name],
|
||
'Category': [formula_category],
|
||
'Formula': [formula_expression],
|
||
'Reference': [formula_reference]
|
||
})
|
||
df = pd.concat([df, new_row], ignore_index=True)
|
||
|
||
# 保存文件
|
||
df.to_csv(formula_csv_path, index=False, encoding='utf-8')
|
||
|
||
# 清空输入框
|
||
self.formula_name_edit.clear()
|
||
self.formula_category_combo.setCurrentIndex(0) # 重置到第一个选项
|
||
self.formula_expression_edit.clear()
|
||
self.formula_reference_edit.clear()
|
||
|
||
# 刷新公式列表
|
||
self.refresh_formulas()
|
||
|
||
QMessageBox.information(self, "成功", "公式添加成功")
|
||
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "错误", f"添加公式失败: {str(e)}")
|
||
|
||
def get_config(self) -> Dict[str, Union[List[str], str, bool]]:
|
||
"""获取配置"""
|
||
selected = [
|
||
name for name, checkbox in self.index_checkboxes.items()
|
||
if checkbox.isChecked()
|
||
]
|
||
return {
|
||
'training_spectra_path': self.training_data_widget.get_path() or None,
|
||
'formula_csv_file': self.formula_csv_widget.get_path() or None,
|
||
'formula_names': selected,
|
||
'output_filename': self.output_filename.text().strip() or "water_quality_indices.csv",
|
||
'enabled': self.enable_checkbox.isChecked()
|
||
}
|
||
|
||
def set_config(self, config):
|
||
"""设置配置"""
|
||
if 'training_spectra_path' in config:
|
||
self.training_data_widget.set_path(config['training_spectra_path'])
|
||
|
||
if 'formula_csv_file' in config:
|
||
self.formula_csv_widget.set_path(config['formula_csv_file'])
|
||
# 设置CSV路径后自动刷新公式信息
|
||
self.refresh_formulas()
|
||
|
||
if 'formula_names' in config:
|
||
selected_formulas = set(config['formula_names'])
|
||
for name, checkbox in self.index_checkboxes.items():
|
||
checkbox.setChecked(name in selected_formulas)
|
||
|
||
if 'output_filename' in config:
|
||
self.output_filename.setText(config['output_filename'])
|
||
|
||
if 'enabled' in config:
|
||
self.enable_checkbox.setChecked(config['enabled'])
|
||
|
||
def is_enabled(self) -> bool:
|
||
return self.enable_checkbox.isChecked()
|
||
|
||
def select_all_formulas(self):
|
||
"""全选所有公式"""
|
||
for checkbox in self.index_checkboxes.values():
|
||
checkbox.setChecked(True)
|
||
|
||
def deselect_all_formulas(self):
|
||
"""清空所有公式"""
|
||
for checkbox in self.index_checkboxes.values():
|
||
checkbox.setChecked(False)
|
||
|
||
def run_step(self):
|
||
"""独立运行步骤5.5"""
|
||
# 验证输入
|
||
training_csv_path = self.training_data_widget.get_path()
|
||
formula_csv_path = self.formula_csv_widget.get_path()
|
||
|
||
if not training_csv_path:
|
||
QMessageBox.warning(self, "输入验证失败", "请选择训练数据CSV文件")
|
||
return
|
||
if not formula_csv_path:
|
||
QMessageBox.warning(self, "输入验证失败", "请选择公式CSV文件")
|
||
return
|
||
if not os.path.exists(training_csv_path):
|
||
QMessageBox.warning(self, "输入验证失败", "训练数据CSV文件不存在")
|
||
return
|
||
if not os.path.exists(formula_csv_path):
|
||
QMessageBox.warning(self, "输入验证失败", "公式CSV文件不存在")
|
||
return
|
||
|
||
# 获取配置
|
||
config = self.get_config()
|
||
|
||
# 调用GUI的run_single_step方法
|
||
parent = self.parent()
|
||
while parent and not hasattr(parent, 'run_single_step'):
|
||
parent = parent.parent()
|
||
|
||
if parent and hasattr(parent, 'run_single_step'):
|
||
parent.run_single_step('step5_5', {'step5_5': config})
|
||
else:
|
||
QMessageBox.critical(self, "错误", "无法找到父级GUI对象")
|
||
|
||
|
||
class Step6Panel(QWidget):
|
||
"""步骤6:机器学习建模"""
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.init_ui()
|
||
|
||
def init_ui(self):
|
||
layout = QVBoxLayout()
|
||
|
||
# 标题
|
||
|
||
|
||
# 训练数据文件(用于独立运行)
|
||
self.training_csv_file = FileSelectWidget(
|
||
"训练数据:",
|
||
"CSV Files (*.csv);;All Files (*.*)"
|
||
)
|
||
layout.addWidget(self.training_csv_file)
|
||
|
||
# 机器学习模型页面
|
||
self.ml_page = QWidget()
|
||
self.create_ml_page()
|
||
layout.addWidget(self.ml_page)
|
||
|
||
# 输出文件路径
|
||
self.output_dir = FileSelectWidget(
|
||
"输出模型目录:",
|
||
"Directories;;All Files (*.*)"
|
||
)
|
||
self.output_dir.line_edit.setPlaceholderText("models_output")
|
||
# 修改浏览按钮为选择目录
|
||
self.output_dir.browse_btn.clicked.disconnect()
|
||
self.output_dir.browse_btn.clicked.connect(self.browse_output_dir)
|
||
layout.addWidget(self.output_dir)
|
||
|
||
# 启用步骤
|
||
self.enable_checkbox = QCheckBox("启用此步骤")
|
||
self.enable_checkbox.setChecked(True)
|
||
layout.addWidget(self.enable_checkbox)
|
||
|
||
# 独立运行按钮
|
||
self.run_btn = QPushButton("独立运行此步骤")
|
||
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
|
||
self.run_btn.clicked.connect(self.run_step)
|
||
layout.addWidget(self.run_btn)
|
||
|
||
layout.addStretch()
|
||
self.setLayout(layout)
|
||
|
||
def create_ml_page(self):
|
||
"""创建机器学习模型页面"""
|
||
layout = QVBoxLayout()
|
||
|
||
# 参数设置
|
||
params_group = QGroupBox("训练参数")
|
||
params_layout = QFormLayout()
|
||
|
||
self.feature_start = QLineEdit()
|
||
self.feature_start.setText("374.285004")
|
||
params_layout.addRow("特征起始列:", self.feature_start)
|
||
|
||
self.cv_folds = QSpinBox()
|
||
self.cv_folds.setRange(2, 10)
|
||
self.cv_folds.setValue(3)
|
||
params_layout.addRow("交叉验证折数:", self.cv_folds)
|
||
|
||
params_group.setLayout(params_layout)
|
||
layout.addWidget(params_group)
|
||
|
||
# 预处理方法 - 多选
|
||
preproc_group = QGroupBox("预处理方法 (可多选)")
|
||
preproc_layout = QVBoxLayout()
|
||
|
||
# 创建网格布局来放置checkbox
|
||
preproc_grid = QGridLayout()
|
||
self.preproc_checkboxes = {}
|
||
preproc_methods = ['None', 'MMS', 'SS', 'SNV', 'MA', 'SG', 'MSC', 'D1', 'D2', 'DT', 'CT']
|
||
|
||
for i, method in enumerate(preproc_methods):
|
||
checkbox = QCheckBox(method)
|
||
checkbox.setChecked(True) # 默认全选
|
||
self.preproc_checkboxes[method] = checkbox
|
||
preproc_grid.addWidget(checkbox, i // 4, i % 4)
|
||
|
||
# 全选/反选按钮
|
||
button_layout = QHBoxLayout()
|
||
select_all_btn = QPushButton("全选")
|
||
deselect_all_btn = QPushButton("全不选")
|
||
select_all_btn.clicked.connect(lambda: self._toggle_checkboxes(self.preproc_checkboxes, True))
|
||
deselect_all_btn.clicked.connect(lambda: self._toggle_checkboxes(self.preproc_checkboxes, False))
|
||
button_layout.addWidget(select_all_btn)
|
||
button_layout.addWidget(deselect_all_btn)
|
||
button_layout.addStretch()
|
||
|
||
preproc_layout.addLayout(preproc_grid)
|
||
preproc_layout.addLayout(button_layout)
|
||
preproc_group.setLayout(preproc_layout)
|
||
layout.addWidget(preproc_group)
|
||
|
||
# 模型选择 - 多选
|
||
model_group = QGroupBox("模型类型 (可多选)")
|
||
model_layout = QVBoxLayout()
|
||
|
||
model_grid = QGridLayout()
|
||
self.model_checkboxes = {}
|
||
|
||
# 按照模型类型分组排序
|
||
model_groups = [
|
||
("线性模型", ['LinearRegression', 'Ridge', 'Lasso', 'ElasticNet', 'PLS']),
|
||
("树模型", ['DecisionTree', 'RF', 'ExtraTrees', 'XGBoost', 'LightGBM', 'CatBoost']),
|
||
("集成学习", ['GradientBoosting', 'AdaBoost']),
|
||
("其他模型", ['SVR', 'KNN', 'MLP'])
|
||
]
|
||
|
||
row = 0
|
||
for group_name, models in model_groups:
|
||
# 添加分组标签
|
||
group_label = QLabel(f"<b>{group_name}</b>")
|
||
group_label.setStyleSheet(f"background-color: {ModernStylesheet.COLORS['hover']}; padding: 5px; border: 1px solid {ModernStylesheet.COLORS['border_light']}; border-radius: 3px;")
|
||
model_grid.addWidget(group_label, row, 0, 1, 4) # 跨4列
|
||
row += 1
|
||
|
||
# 添加该组的模型checkbox
|
||
for i, model in enumerate(models):
|
||
checkbox = QCheckBox(model)
|
||
# 默认选择常用的4个
|
||
checkbox.setChecked(model in ['SVR', 'RF', 'Ridge', 'Lasso'])
|
||
self.model_checkboxes[model] = checkbox
|
||
model_grid.addWidget(checkbox, row, i % 4)
|
||
|
||
# 如果这一行满了,换到下一行
|
||
if (i + 1) % 4 == 0:
|
||
row += 1
|
||
|
||
# 每组结束后换行
|
||
row += 1
|
||
|
||
model_button_layout = QHBoxLayout()
|
||
model_select_all = QPushButton("全选")
|
||
model_deselect_all = QPushButton("全不选")
|
||
model_select_all.clicked.connect(lambda: self._toggle_checkboxes(self.model_checkboxes, True))
|
||
model_deselect_all.clicked.connect(lambda: self._toggle_checkboxes(self.model_checkboxes, False))
|
||
model_button_layout.addWidget(model_select_all)
|
||
model_button_layout.addWidget(model_deselect_all)
|
||
model_button_layout.addStretch()
|
||
|
||
model_layout.addLayout(model_grid)
|
||
model_layout.addLayout(model_button_layout)
|
||
model_group.setLayout(model_layout)
|
||
layout.addWidget(model_group)
|
||
|
||
# 数据划分方法 - 多选
|
||
split_group = QGroupBox("数据划分方法 (可多选)")
|
||
split_layout = QVBoxLayout()
|
||
|
||
split_grid = QGridLayout()
|
||
self.split_checkboxes = {}
|
||
split_methods = ['spxy', 'ks', 'random']
|
||
|
||
for i, method in enumerate(split_methods):
|
||
checkbox = QCheckBox(method)
|
||
checkbox.setChecked(True) # 默认全选
|
||
self.split_checkboxes[method] = checkbox
|
||
split_grid.addWidget(checkbox, 0, i)
|
||
|
||
split_button_layout = QHBoxLayout()
|
||
split_select_all = QPushButton("全选")
|
||
split_deselect_all = QPushButton("全不选")
|
||
split_select_all.clicked.connect(lambda: self._toggle_checkboxes(self.split_checkboxes, True))
|
||
split_deselect_all.clicked.connect(lambda: self._toggle_checkboxes(self.split_checkboxes, False))
|
||
split_button_layout.addWidget(split_select_all)
|
||
split_button_layout.addWidget(split_deselect_all)
|
||
split_button_layout.addStretch()
|
||
|
||
split_layout.addLayout(split_grid)
|
||
split_layout.addLayout(split_button_layout)
|
||
split_group.setLayout(split_layout)
|
||
layout.addWidget(split_group)
|
||
|
||
self.ml_page.setLayout(layout)
|
||
|
||
|
||
def _toggle_checkboxes(self, checkboxes_dict, checked):
|
||
"""统一设置checkbox状态"""
|
||
for checkbox in checkboxes_dict.values():
|
||
checkbox.setChecked(checked)
|
||
|
||
def browse_output_dir(self):
|
||
"""浏览输出目录"""
|
||
dir_path = QFileDialog.getExistingDirectory(self, "选择输出模型目录", "")
|
||
if dir_path:
|
||
self.output_dir.set_path(dir_path)
|
||
|
||
def get_config(self):
|
||
"""获取配置"""
|
||
# 获取选中的预处理方法
|
||
preprocessing_methods = [method for method, checkbox in self.preproc_checkboxes.items()
|
||
if checkbox.isChecked()]
|
||
|
||
# 获取选中的模型类型
|
||
model_names = [model for model, checkbox in self.model_checkboxes.items()
|
||
if checkbox.isChecked()]
|
||
|
||
# 获取选中的数据划分方法
|
||
split_methods = [method for method, checkbox in self.split_checkboxes.items()
|
||
if checkbox.isChecked()]
|
||
|
||
config = {
|
||
'feature_start_column': self.feature_start.text(),
|
||
'preprocessing_methods': preprocessing_methods if preprocessing_methods else ['None'],
|
||
'model_names': model_names if model_names else ['SVR'],
|
||
'split_methods': split_methods if split_methods else ['random'],
|
||
'cv_folds': self.cv_folds.value()
|
||
}
|
||
# 添加训练数据路径(用于独立运行)
|
||
training_csv_path = self.training_csv_file.get_path()
|
||
if training_csv_path:
|
||
config['training_csv_path'] = training_csv_path
|
||
# 添加输出路径
|
||
output_dir = self.output_dir.get_path()
|
||
if output_dir:
|
||
config['output_dir'] = output_dir
|
||
return config
|
||
|
||
def set_config(self, config):
|
||
"""设置配置"""
|
||
if 'feature_start_column' in config:
|
||
self.feature_start.setText(str(config['feature_start_column']))
|
||
if 'cv_folds' in config:
|
||
self.cv_folds.setValue(config['cv_folds'])
|
||
|
||
# 设置预处理方法
|
||
if 'preprocessing_methods' in config:
|
||
methods = config['preprocessing_methods']
|
||
for method, checkbox in self.preproc_checkboxes.items():
|
||
checkbox.setChecked(method in methods)
|
||
|
||
# 设置模型类型
|
||
if 'model_names' in config:
|
||
models = config['model_names']
|
||
for model, checkbox in self.model_checkboxes.items():
|
||
checkbox.setChecked(model in models)
|
||
|
||
# 设置数据划分方法
|
||
if 'split_methods' in config:
|
||
methods = config['split_methods']
|
||
for method, checkbox in self.split_checkboxes.items():
|
||
checkbox.setChecked(method in methods)
|
||
if 'training_csv_path' in config:
|
||
self.training_csv_file.set_path(config['training_csv_path'])
|
||
if 'output_dir' in config:
|
||
self.output_dir.set_path(config['output_dir'])
|
||
|
||
def run_step(self):
|
||
"""独立运行步骤6"""
|
||
# 验证输入
|
||
training_csv_path = self.training_csv_file.get_path()
|
||
if not training_csv_path:
|
||
QMessageBox.warning(self, "输入错误", "请选择训练数据CSV文件!")
|
||
return
|
||
|
||
# 获取主窗口并运行步骤
|
||
main_window = self.window()
|
||
if hasattr(main_window, 'run_single_step'):
|
||
config = {'step6': self.get_config()}
|
||
main_window.run_single_step('step6', config)
|
||
|
||
def get_training_params(self):
|
||
"""获取模型训练参数"""
|
||
return {
|
||
'pipeline_type': 'machine_learning',
|
||
'feature_start': float(self.feature_start.text()),
|
||
'cv_folds': self.cv_folds.value(),
|
||
'preprocess_methods': [method for method, cb in self.preproc_checkboxes.items() if cb.isChecked()],
|
||
'model_types': [model for model, cb in self.model_checkboxes.items() if cb.isChecked()],
|
||
'split_methods': [method for method, cb in self.split_checkboxes.items() if cb.isChecked()]
|
||
}
|
||
|
||
|
||
class Step7Panel(QWidget):
|
||
"""步骤7:采样点生成"""
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.init_ui()
|
||
|
||
def init_ui(self):
|
||
layout = QVBoxLayout()
|
||
|
||
# 标题
|
||
|
||
|
||
# 去耀斑影像文件(用于独立运行)
|
||
self.deglint_img_file = FileSelectWidget(
|
||
"去耀斑影像:",
|
||
"Image Files (*.bsq *.dat *.tif);;All Files (*.*)"
|
||
)
|
||
layout.addWidget(self.deglint_img_file)
|
||
|
||
# 水域掩膜文件(可选,用于独立运行)
|
||
self.water_mask_file = FileSelectWidget(
|
||
"水域掩膜:",
|
||
"Mask Files (*.dat *.tif);;All Files (*.*)"
|
||
)
|
||
self.water_mask_file.label.setText("水域掩膜(可选):")
|
||
layout.addWidget(self.water_mask_file)
|
||
|
||
# 参数设置
|
||
params_group = QGroupBox("采样参数")
|
||
params_layout = QFormLayout()
|
||
|
||
self.interval = QSpinBox()
|
||
self.interval.setRange(10, 500)
|
||
self.interval.setValue(50)
|
||
params_layout.addRow("采样点间隔(像素):", self.interval)
|
||
|
||
self.sample_radius = QSpinBox()
|
||
self.sample_radius.setRange(1, 50)
|
||
self.sample_radius.setValue(5)
|
||
params_layout.addRow("采样半径(像素):", self.sample_radius)
|
||
|
||
self.chunk_size = QSpinBox()
|
||
self.chunk_size.setRange(100, 10000)
|
||
self.chunk_size.setValue(1000)
|
||
params_layout.addRow("处理块大小:", self.chunk_size)
|
||
|
||
params_group.setLayout(params_layout)
|
||
layout.addWidget(params_group)
|
||
|
||
# 输出文件路径
|
||
self.output_file = FileSelectWidget(
|
||
"输出采样点:",
|
||
"CSV Files (*.csv);;All Files (*.*)"
|
||
)
|
||
self.output_file.line_edit.setPlaceholderText("sampling_points.csv")
|
||
layout.addWidget(self.output_file)
|
||
|
||
# 启用步骤
|
||
self.enable_checkbox = QCheckBox("启用此步骤")
|
||
self.enable_checkbox.setChecked(True)
|
||
layout.addWidget(self.enable_checkbox)
|
||
|
||
# 独立运行按钮
|
||
self.run_btn = QPushButton("独立运行此步骤")
|
||
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
|
||
self.run_btn.clicked.connect(self.run_step)
|
||
layout.addWidget(self.run_btn)
|
||
|
||
layout.addStretch()
|
||
self.setLayout(layout)
|
||
|
||
def get_config(self):
|
||
"""获取配置"""
|
||
config = {
|
||
'interval': self.interval.value(),
|
||
'sample_radius': self.sample_radius.value(),
|
||
'chunk_size': self.chunk_size.value(),
|
||
}
|
||
# 添加独立运行所需的文件路径
|
||
deglint_img_path = self.deglint_img_file.get_path()
|
||
if deglint_img_path:
|
||
config['deglint_img_path'] = deglint_img_path
|
||
water_mask_path = self.water_mask_file.get_path()
|
||
if water_mask_path:
|
||
config['water_mask_path'] = water_mask_path
|
||
# 添加输出路径
|
||
output_path = self.output_file.get_path()
|
||
if output_path:
|
||
config['output_path'] = output_path
|
||
return config
|
||
|
||
def set_config(self, config):
|
||
"""设置配置"""
|
||
if 'interval' in config:
|
||
self.interval.setValue(config['interval'])
|
||
if 'sample_radius' in config:
|
||
self.sample_radius.setValue(config['sample_radius'])
|
||
if 'chunk_size' in config:
|
||
self.chunk_size.setValue(config['chunk_size'])
|
||
if 'deglint_img_path' in config:
|
||
self.deglint_img_file.set_path(config['deglint_img_path'])
|
||
if 'water_mask_path' in config:
|
||
self.water_mask_file.set_path(config['water_mask_path'])
|
||
if 'output_path' in config:
|
||
self.output_file.set_path(config['output_path'])
|
||
|
||
def run_step(self):
|
||
"""独立运行步骤7"""
|
||
# 验证输入
|
||
deglint_img_path = self.deglint_img_file.get_path()
|
||
if not deglint_img_path:
|
||
QMessageBox.warning(self, "输入错误", "请选择去耀斑影像文件!")
|
||
return
|
||
|
||
# 获取主窗口并运行步骤
|
||
main_window = self.window()
|
||
if hasattr(main_window, 'run_single_step'):
|
||
config = {'step7': self.get_config()}
|
||
main_window.run_single_step('step7', config)
|
||
|
||
|
||
class Step8Panel(QWidget):
|
||
"""步骤8:机器学习预测"""
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.init_ui()
|
||
|
||
def init_ui(self):
|
||
layout = QVBoxLayout()
|
||
|
||
# 标题
|
||
|
||
|
||
# 采样光谱CSV文件(用于独立运行)
|
||
self.sampling_csv_file = FileSelectWidget(
|
||
"采样光谱CSV:",
|
||
"CSV Files (*.csv);;All Files (*.*)"
|
||
)
|
||
layout.addWidget(self.sampling_csv_file)
|
||
|
||
# 模型目录(用于独立运行)
|
||
self.models_dir_file = FileSelectWidget(
|
||
"模型目录:",
|
||
"Directories;;All Files (*.*)"
|
||
)
|
||
self.models_dir_file.label.setText("模型目录:")
|
||
# 修改浏览按钮为选择目录
|
||
self.models_dir_file.browse_btn.clicked.disconnect()
|
||
self.models_dir_file.browse_btn.clicked.connect(self.browse_models_dir)
|
||
layout.addWidget(self.models_dir_file)
|
||
|
||
# 参数设置
|
||
params_group = QGroupBox("预测参数")
|
||
params_layout = QFormLayout()
|
||
|
||
self.metric = QComboBox()
|
||
self.metric.addItems(['test_r2', 'test_rmse', 'test_mae'])
|
||
params_layout.addRow("模型选择指标:", self.metric)
|
||
|
||
self.prediction_column = QLineEdit()
|
||
self.prediction_column.setText("prediction")
|
||
params_layout.addRow("预测列名:", self.prediction_column)
|
||
|
||
params_group.setLayout(params_layout)
|
||
layout.addWidget(params_group)
|
||
|
||
# 输出路径
|
||
self.output_file = FileSelectWidget(
|
||
"输出路径:",
|
||
"CSV Files (*.csv);;All Files (*.*)"
|
||
)
|
||
layout.addWidget(self.output_file)
|
||
|
||
# 启用步骤
|
||
self.enable_checkbox = QCheckBox("启用此步骤")
|
||
self.enable_checkbox.setChecked(True)
|
||
layout.addWidget(self.enable_checkbox)
|
||
|
||
# 独立运行按钮
|
||
self.run_btn = QPushButton("独立运行此步骤")
|
||
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
|
||
self.run_btn.clicked.connect(self.run_step)
|
||
layout.addWidget(self.run_btn)
|
||
|
||
layout.addStretch()
|
||
self.setLayout(layout)
|
||
|
||
def browse_models_dir(self):
|
||
"""浏览模型目录"""
|
||
dir_path = QFileDialog.getExistingDirectory(self, "选择模型目录", "")
|
||
if dir_path:
|
||
self.models_dir_file.set_path(dir_path)
|
||
|
||
def get_config(self):
|
||
"""获取配置"""
|
||
config = {
|
||
'metric': self.metric.currentText(),
|
||
'prediction_column': self.prediction_column.text(),
|
||
}
|
||
# 添加独立运行所需的文件路径
|
||
sampling_csv_path = self.sampling_csv_file.get_path()
|
||
if sampling_csv_path:
|
||
config['sampling_csv_path'] = sampling_csv_path
|
||
models_dir = self.models_dir_file.get_path()
|
||
if models_dir:
|
||
config['models_dir'] = models_dir
|
||
# 添加输出路径
|
||
output_path = self.output_file.get_path()
|
||
if output_path:
|
||
config['output_path'] = output_path
|
||
return config
|
||
|
||
def set_config(self, config):
|
||
"""设置配置"""
|
||
if 'metric' in config:
|
||
idx = self.metric.findText(config['metric'])
|
||
if idx >= 0:
|
||
self.metric.setCurrentIndex(idx)
|
||
|
||
if 'prediction_column' in config:
|
||
self.prediction_column.setText(config['prediction_column'])
|
||
if 'sampling_csv_path' in config:
|
||
self.sampling_csv_file.set_path(config['sampling_csv_path'])
|
||
if 'models_dir' in config:
|
||
self.models_dir_file.set_path(config['models_dir'])
|
||
if 'output_path' in config:
|
||
self.output_file.set_path(config['output_path'])
|
||
|
||
def run_step(self):
|
||
"""独立运行步骤8"""
|
||
# 验证输入
|
||
sampling_csv_path = self.sampling_csv_file.get_path()
|
||
models_dir = self.models_dir_file.get_path()
|
||
if not sampling_csv_path:
|
||
QMessageBox.warning(self, "输入错误", "请选择采样光谱CSV文件!")
|
||
return
|
||
if not models_dir:
|
||
QMessageBox.warning(self, "输入错误", "请选择模型目录!")
|
||
return
|
||
|
||
# 获取主窗口并运行步骤
|
||
main_window = self.window()
|
||
if hasattr(main_window, 'run_single_step'):
|
||
config = {'step8': self.get_config()}
|
||
main_window.run_single_step('step8', config)
|
||
|
||
|
||
class Step9Panel(QWidget):
|
||
"""步骤9:分布图生成"""
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self._batch_thread = None
|
||
self.init_ui()
|
||
|
||
def init_ui(self):
|
||
layout = QVBoxLayout()
|
||
|
||
hint = QLabel(
|
||
"独立运行:可选「单个 CSV」或「文件夹批量」(扫描目录下所有 .csv)。"
|
||
"完整流程中预测 CSV 由步骤11、12、13 自动传入,无需在此选择。"
|
||
)
|
||
hint.setWordWrap(True)
|
||
hint.setStyleSheet(f"color: {ModernStylesheet.COLORS.get('text_secondary', '#666')};")
|
||
layout.addWidget(hint)
|
||
|
||
mode_row = QHBoxLayout()
|
||
self.mode_single_rb = QRadioButton("单个 CSV 文件")
|
||
self.mode_folder_rb = QRadioButton("文件夹批量")
|
||
self.mode_single_rb.setChecked(True)
|
||
self._mode_group = QButtonGroup(self)
|
||
self._mode_group.addButton(self.mode_single_rb, 0)
|
||
self._mode_group.addButton(self.mode_folder_rb, 1)
|
||
self.mode_single_rb.toggled.connect(self._on_step9_mode_changed)
|
||
self.mode_folder_rb.toggled.connect(self._on_step9_mode_changed)
|
||
mode_row.addWidget(self.mode_single_rb)
|
||
mode_row.addWidget(self.mode_folder_rb)
|
||
mode_row.addStretch()
|
||
layout.addLayout(mode_row)
|
||
|
||
self.prediction_csv_file = FileSelectWidget(
|
||
"预测结果CSV:",
|
||
"CSV Files (*.csv);;All Files (*.*)"
|
||
)
|
||
layout.addWidget(self.prediction_csv_file)
|
||
|
||
folder_row = QHBoxLayout()
|
||
self.prediction_csv_dir_label = QLabel("预测CSV目录:")
|
||
self.prediction_csv_dir_label.setMinimumWidth(120)
|
||
self.prediction_csv_dir_edit = QLineEdit()
|
||
self.prediction_csv_dir_edit.setPlaceholderText("选择含多个预测结果 CSV 的文件夹…")
|
||
pred_dir_btn = QPushButton("浏览…")
|
||
pred_dir_btn.setMaximumWidth(80)
|
||
pred_dir_btn.clicked.connect(self.browse_prediction_csv_dir)
|
||
folder_row.addWidget(self.prediction_csv_dir_label)
|
||
folder_row.addWidget(self.prediction_csv_dir_edit, 1)
|
||
folder_row.addWidget(pred_dir_btn)
|
||
self._folder_row_widget = QWidget()
|
||
self._folder_row_widget.setLayout(folder_row)
|
||
layout.addWidget(self._folder_row_widget)
|
||
|
||
self.recursive_csv_cb = QCheckBox("包含子文件夹(递归扫描 *.csv)")
|
||
layout.addWidget(self.recursive_csv_cb)
|
||
|
||
self.boundary_file = FileSelectWidget(
|
||
"边界文件:",
|
||
"Shapefiles (*.shp);;All Files (*.*)"
|
||
)
|
||
layout.addWidget(self.boundary_file)
|
||
|
||
# 参数设置
|
||
params_group = QGroupBox("生成参数")
|
||
params_layout = QFormLayout()
|
||
|
||
self.resolution = QDoubleSpinBox()
|
||
self.resolution.setRange(1, 1000)
|
||
self.resolution.setValue(30)
|
||
params_layout.addRow("分辨率(米):", self.resolution)
|
||
|
||
self.input_crs = QLineEdit()
|
||
self.input_crs.setText("EPSG:32651")
|
||
params_layout.addRow("输入坐标系:", self.input_crs)
|
||
|
||
self.output_crs = QLineEdit()
|
||
self.output_crs.setText("EPSG:4326")
|
||
params_layout.addRow("输出坐标系:", self.output_crs)
|
||
|
||
self.show_points = QCheckBox("显示采样点")
|
||
params_layout.addRow("", self.show_points)
|
||
|
||
self.use_diffusion = QCheckBox("启用距离扩散")
|
||
self.use_diffusion.setChecked(True)
|
||
params_layout.addRow("", self.use_diffusion)
|
||
|
||
params_group.setLayout(params_layout)
|
||
layout.addWidget(params_group)
|
||
|
||
# 输出目录(可选):在此目录下生成「CSV文件名_distribution.png」;留空则用工作目录/14_visualization
|
||
self.output_dir = FileSelectWidget(
|
||
"输出分布图目录:",
|
||
"Directories;;All Files (*.*)"
|
||
)
|
||
self.output_dir.line_edit.setPlaceholderText("留空→工作目录/14_visualization")
|
||
# 修改浏览按钮为选择目录
|
||
self.output_dir.browse_btn.clicked.disconnect()
|
||
self.output_dir.browse_btn.clicked.connect(self.browse_output_dir)
|
||
layout.addWidget(self.output_dir)
|
||
|
||
# 启用步骤
|
||
self.enable_checkbox = QCheckBox("启用此步骤")
|
||
self.enable_checkbox.setChecked(True)
|
||
layout.addWidget(self.enable_checkbox)
|
||
|
||
# 独立运行按钮
|
||
self.run_button = QPushButton("独立运行此步骤")
|
||
self.run_button.setStyleSheet("""
|
||
QPushButton {
|
||
background-color: #4CAF50;
|
||
color: white;
|
||
padding: 8px 16px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-weight: bold;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: #45a049;
|
||
}
|
||
QPushButton:pressed {
|
||
background-color: #3e8e41;
|
||
}
|
||
""")
|
||
self.run_button.clicked.connect(self.run_step)
|
||
layout.addWidget(self.run_button)
|
||
|
||
layout.addStretch()
|
||
self.setLayout(layout)
|
||
self._on_step9_mode_changed()
|
||
|
||
def _on_step9_mode_changed(self):
|
||
folder_mode = self.mode_folder_rb.isChecked()
|
||
self.prediction_csv_file.setEnabled(not folder_mode)
|
||
self._folder_row_widget.setEnabled(folder_mode)
|
||
self.recursive_csv_cb.setEnabled(folder_mode)
|
||
|
||
def browse_prediction_csv_dir(self):
|
||
d = QFileDialog.getExistingDirectory(self, "选择预测结果 CSV 所在文件夹")
|
||
if d:
|
||
self.prediction_csv_dir_edit.setText(d)
|
||
|
||
def _collect_csv_paths_from_folder(self) -> List[str]:
|
||
folder = (self.prediction_csv_dir_edit.text() or "").strip()
|
||
if not folder or not os.path.isdir(folder):
|
||
return []
|
||
root = Path(folder)
|
||
if self.recursive_csv_cb.isChecked():
|
||
files = sorted(root.rglob("*.csv"))
|
||
else:
|
||
files = sorted(root.glob("*.csv"))
|
||
return [str(p) for p in files if p.is_file()]
|
||
|
||
def _step9_base_pipeline_kwargs(self) -> dict:
|
||
return {
|
||
'boundary_shp_path': self.boundary_file.get_path(),
|
||
'resolution': self.resolution.value(),
|
||
'input_crs': self.input_crs.text(),
|
||
'output_crs': self.output_crs.text(),
|
||
'show_sample_points': self.show_points.isChecked(),
|
||
'use_distance_diffusion': self.use_diffusion.isChecked(),
|
||
}
|
||
|
||
def get_config(self):
|
||
"""含 GUI 专用字段 step9_batch_mode / prediction_csv_dir / recursive_csv_scan;pipeline 调用前会剔除。"""
|
||
pred_csv = (self.prediction_csv_file.get_path() or "").strip()
|
||
folder_mode = self.mode_folder_rb.isChecked()
|
||
pred_dir = (self.prediction_csv_dir_edit.text() or "").strip()
|
||
config = {
|
||
'step9_batch_mode': 'folder' if folder_mode else 'single',
|
||
'prediction_csv_dir': pred_dir if pred_dir else None,
|
||
'recursive_csv_scan': self.recursive_csv_cb.isChecked(),
|
||
'prediction_csv_path': None if folder_mode else (pred_csv if pred_csv else None),
|
||
'boundary_shp_path': self.boundary_file.get_path(),
|
||
'resolution': self.resolution.value(),
|
||
'input_crs': self.input_crs.text(),
|
||
'output_crs': self.output_crs.text(),
|
||
'show_sample_points': self.show_points.isChecked(),
|
||
'use_distance_diffusion': self.use_diffusion.isChecked(),
|
||
}
|
||
out_dir = (self.output_dir.get_path() or "").strip()
|
||
if not folder_mode and pred_csv and out_dir:
|
||
stem = Path(pred_csv).stem
|
||
config['output_image_path'] = str(Path(out_dir) / f"{stem}_distribution.png")
|
||
else:
|
||
config['output_image_path'] = None
|
||
return config
|
||
|
||
def set_config(self, config):
|
||
"""设置配置"""
|
||
mode = config.get('step9_batch_mode', 'single')
|
||
if mode == 'folder':
|
||
self.mode_folder_rb.setChecked(True)
|
||
else:
|
||
self.mode_single_rb.setChecked(True)
|
||
if config.get('prediction_csv_dir'):
|
||
self.prediction_csv_dir_edit.setText(str(config['prediction_csv_dir']))
|
||
if 'recursive_csv_scan' in config:
|
||
self.recursive_csv_cb.setChecked(bool(config['recursive_csv_scan']))
|
||
if 'prediction_csv_path' in config and config['prediction_csv_path']:
|
||
self.prediction_csv_file.set_path(str(config['prediction_csv_path']))
|
||
if 'boundary_shp_path' in config:
|
||
self.boundary_file.set_path(config['boundary_shp_path'])
|
||
if 'resolution' in config:
|
||
self.resolution.setValue(config['resolution'])
|
||
if 'input_crs' in config:
|
||
self.input_crs.setText(config['input_crs'])
|
||
if 'output_crs' in config:
|
||
self.output_crs.setText(config['output_crs'])
|
||
if 'show_sample_points' in config:
|
||
self.show_points.setChecked(config['show_sample_points'])
|
||
if 'use_distance_diffusion' in config:
|
||
self.use_diffusion.setChecked(config['use_distance_diffusion'])
|
||
if 'output_dir' in config and config['output_dir']:
|
||
self.output_dir.set_path(str(config['output_dir']))
|
||
elif config.get('output_image_path'):
|
||
p = Path(str(config['output_image_path']))
|
||
if p.parent and str(p.parent) != '.':
|
||
self.output_dir.set_path(str(p.parent))
|
||
|
||
def browse_output_dir(self):
|
||
"""浏览输出目录"""
|
||
dir_path = QFileDialog.getExistingDirectory(self, "选择输出模型目录", "")
|
||
if dir_path:
|
||
self.output_dir.set_path(dir_path)
|
||
|
||
def run_step(self):
|
||
"""独立运行步骤9(单文件走原 WorkerThread;文件夹走批量线程)"""
|
||
if self._batch_thread and self._batch_thread.isRunning():
|
||
QMessageBox.information(self, "提示", "批量任务正在运行,请稍候。")
|
||
return
|
||
|
||
boundary_shp_path = self.boundary_file.get_path()
|
||
if not boundary_shp_path:
|
||
QMessageBox.warning(self, "输入验证失败", "请选择边界文件")
|
||
return
|
||
if not os.path.exists(boundary_shp_path):
|
||
QMessageBox.warning(self, "输入验证失败", "边界文件不存在")
|
||
return
|
||
|
||
parent = self.parent()
|
||
while parent and not hasattr(parent, 'run_single_step'):
|
||
parent = parent.parent()
|
||
|
||
if not parent or not hasattr(parent, 'run_single_step'):
|
||
QMessageBox.critical(self, "错误", "无法找到父级GUI对象")
|
||
return
|
||
|
||
if self.mode_folder_rb.isChecked():
|
||
csv_list = self._collect_csv_paths_from_folder()
|
||
if not csv_list:
|
||
QMessageBox.warning(
|
||
self,
|
||
"输入验证失败",
|
||
"所选文件夹中未找到 .csv 文件,或目录无效。\n"
|
||
"可勾选「包含子文件夹」以递归扫描。",
|
||
)
|
||
return
|
||
if not PIPELINE_AVAILABLE:
|
||
QMessageBox.critical(self, "错误", "Pipeline 模块不可用,无法批量生成专题图。")
|
||
return
|
||
work_dir = getattr(parent, "work_dir", None) or "./work_dir"
|
||
work_dir = str(work_dir)
|
||
base_kw = self._step9_base_pipeline_kwargs()
|
||
out_dir_opt = (self.output_dir.get_path() or "").strip() or None
|
||
self.run_button.setEnabled(False)
|
||
self._batch_thread = Step9BatchThread(work_dir, csv_list, base_kw, out_dir_opt)
|
||
main_win = parent
|
||
|
||
def _batch_log(msg, lvl):
|
||
if hasattr(main_win, "log_message"):
|
||
main_win.log_message(msg, lvl)
|
||
|
||
self._batch_thread.log_message.connect(_batch_log, Qt.QueuedConnection)
|
||
self._batch_thread.finished_ok.connect(self._on_step9_batch_ok, Qt.QueuedConnection)
|
||
self._batch_thread.failed.connect(self._on_step9_batch_fail, Qt.QueuedConnection)
|
||
self._batch_thread.finished.connect(lambda: self.run_button.setEnabled(True), Qt.QueuedConnection)
|
||
self._batch_thread.start()
|
||
if hasattr(parent, "log_message"):
|
||
parent.log_message(f"专题图批量:共 {len(csv_list)} 个 CSV,工作目录 {work_dir}", "info")
|
||
return
|
||
|
||
prediction_csv_path = (self.prediction_csv_file.get_path() or "").strip()
|
||
if not prediction_csv_path:
|
||
QMessageBox.warning(
|
||
self,
|
||
"输入验证失败",
|
||
"请选择「预测结果 CSV」文件,或切换到「文件夹批量」。",
|
||
)
|
||
return
|
||
if not os.path.isfile(prediction_csv_path):
|
||
QMessageBox.warning(self, "输入验证失败", "预测结果 CSV 不存在或不是文件")
|
||
return
|
||
|
||
config = self.get_config()
|
||
parent.run_single_step('step9', {'step9': config})
|
||
|
||
def _on_step9_batch_ok(self, n: int):
|
||
QMessageBox.information(self, "完成", f"已批量生成 {n} 个分布图。")
|
||
parent = self.parent()
|
||
while parent and not hasattr(parent, "log_message"):
|
||
parent = parent.parent()
|
||
if parent and hasattr(parent, "log_message"):
|
||
parent.log_message(f"专题图批量完成,共 {n} 个文件。", "info")
|
||
|
||
def _on_step9_batch_fail(self, err: str):
|
||
QMessageBox.critical(self, "失败", f"批量生成中断:\n{err[:900]}")
|
||
parent = self.parent()
|
||
while parent and not hasattr(parent, "log_message"):
|
||
parent = parent.parent()
|
||
if parent and hasattr(parent, "log_message"):
|
||
parent.log_message(err, "error")
|
||
|
||
|
||
class Step8_5Panel(QWidget):
|
||
"""步骤8.5:非经验模型预测"""
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.init_ui()
|
||
|
||
def init_ui(self):
|
||
layout = QVBoxLayout()
|
||
|
||
# 标题
|
||
|
||
|
||
# 采样光谱CSV文件选择
|
||
self.sampling_csv_file = FileSelectWidget(
|
||
"采样光谱CSV:",
|
||
"CSV Files (*.csv);;All Files (*.*)"
|
||
)
|
||
layout.addWidget(self.sampling_csv_file)
|
||
|
||
# 模型目录选择
|
||
self.models_dir_file = FileSelectWidget(
|
||
"模型目录:",
|
||
"Directories;;All Files (*.*)"
|
||
)
|
||
self.models_dir_file.label.setText("模型目录:")
|
||
# 修改浏览按钮为选择目录
|
||
self.models_dir_file.browse_btn.clicked.disconnect()
|
||
self.models_dir_file.browse_btn.clicked.connect(self.browse_models_dir)
|
||
layout.addWidget(self.models_dir_file)
|
||
|
||
# 参数设置
|
||
params_group = QGroupBox("预测参数")
|
||
params_layout = QFormLayout()
|
||
|
||
# 模型选择指标
|
||
self.metric = QComboBox()
|
||
self.metric.addItems(['Average Accuracy(%)', 'Min Accuracy(%)', 'Max Accuracy(%)'])
|
||
params_layout.addRow("模型选择指标:", self.metric)
|
||
|
||
# 预测列名
|
||
self.prediction_column = QLineEdit()
|
||
self.prediction_column.setText("prediction")
|
||
params_layout.addRow("预测列名:", self.prediction_column)
|
||
|
||
params_group.setLayout(params_layout)
|
||
layout.addWidget(params_group)
|
||
|
||
# 输出路径
|
||
self.output_file = FileSelectWidget(
|
||
"输出文件夹:",
|
||
"Directories;;All Files (*.*)"
|
||
)
|
||
self.output_file.label.setText("输出文件夹:")
|
||
# 修改浏览按钮为选择目录
|
||
self.output_file.browse_btn.clicked.disconnect()
|
||
self.output_file.browse_btn.clicked.connect(self.browse_output_dir)
|
||
layout.addWidget(self.output_file)
|
||
|
||
# 启用步骤
|
||
self.enable_checkbox = QCheckBox("启用此步骤")
|
||
self.enable_checkbox.setChecked(True)
|
||
layout.addWidget(self.enable_checkbox)
|
||
|
||
# 独立运行按钮
|
||
self.run_button = QPushButton("独立运行此步骤")
|
||
self.run_button.setStyleSheet("""
|
||
QPushButton {
|
||
background-color: #4CAF50;
|
||
color: white;
|
||
padding: 8px 16px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-weight: bold;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: #45a049;
|
||
}
|
||
QPushButton:pressed {
|
||
background-color: #3e8e41;
|
||
}
|
||
""")
|
||
self.run_button.clicked.connect(self.run_step)
|
||
layout.addWidget(self.run_button)
|
||
|
||
layout.addStretch()
|
||
self.setLayout(layout)
|
||
|
||
def browse_models_dir(self):
|
||
"""浏览模型目录"""
|
||
dir_path = QFileDialog.getExistingDirectory(self, "选择模型目录", "")
|
||
if dir_path:
|
||
self.models_dir_file.set_path(dir_path)
|
||
|
||
def browse_output_dir(self):
|
||
"""浏览输出目录"""
|
||
dir_path = QFileDialog.getExistingDirectory(self, "选择输出文件夹", "")
|
||
if dir_path:
|
||
self.output_file.set_path(dir_path)
|
||
|
||
def get_config(self):
|
||
"""获取配置"""
|
||
config = {
|
||
'metric': self.metric.currentText(),
|
||
'prediction_column': self.prediction_column.text(),
|
||
'enabled': self.enable_checkbox.isChecked()
|
||
}
|
||
# 添加采样光谱CSV路径
|
||
sampling_csv_path = self.sampling_csv_file.get_path()
|
||
if sampling_csv_path:
|
||
config['sampling_csv_path'] = sampling_csv_path
|
||
# 添加模型目录路径
|
||
models_dir = self.models_dir_file.get_path()
|
||
if models_dir:
|
||
config['models_dir'] = models_dir
|
||
# 添加输出路径
|
||
output_path = self.output_file.get_path()
|
||
if output_path:
|
||
config['output_path'] = output_path
|
||
return config
|
||
|
||
def set_config(self, config):
|
||
"""设置配置"""
|
||
if 'metric' in config:
|
||
idx = self.metric.findText(config['metric'])
|
||
if idx >= 0:
|
||
self.metric.setCurrentIndex(idx)
|
||
|
||
if 'prediction_column' in config:
|
||
self.prediction_column.setText(config['prediction_column'])
|
||
|
||
if 'sampling_csv_path' in config:
|
||
self.sampling_csv_file.set_path(config['sampling_csv_path'])
|
||
|
||
if 'models_dir' in config:
|
||
self.models_dir_file.set_path(config['models_dir'])
|
||
|
||
if 'enabled' in config:
|
||
self.enable_checkbox.setChecked(config['enabled'])
|
||
|
||
def run_step(self):
|
||
"""独立运行步骤8.5"""
|
||
# 验证输入
|
||
sampling_csv_path = self.sampling_csv_file.get_path()
|
||
if not sampling_csv_path:
|
||
QMessageBox.warning(self, "输入错误", "请选择采样光谱CSV文件!")
|
||
return
|
||
|
||
# 获取配置
|
||
config = self.get_config()
|
||
|
||
# 调用GUI的run_single_step方法
|
||
parent = self.parent()
|
||
while parent and not hasattr(parent, 'run_single_step'):
|
||
parent = parent.parent()
|
||
|
||
if parent and hasattr(parent, 'run_single_step'):
|
||
parent.run_single_step('step8_5', {'step8_5': config})
|
||
else:
|
||
QMessageBox.critical(self, "错误", "无法找到父级GUI对象")
|
||
|
||
|
||
class Step8_75Panel(QWidget):
|
||
"""步骤8.75:自定义回归预测"""
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.init_ui()
|
||
|
||
def init_ui(self):
|
||
layout = QVBoxLayout()
|
||
|
||
# 标题
|
||
|
||
|
||
# 采样光谱CSV文件选择
|
||
self.sampling_csv_file = FileSelectWidget(
|
||
"采样光谱CSV:",
|
||
"CSV Files (*.csv);;All Files (*.*)"
|
||
)
|
||
layout.addWidget(self.sampling_csv_file)
|
||
|
||
# 公式CSV文件选择
|
||
self.formula_csv_file = FileSelectWidget(
|
||
"公式CSV文件:",
|
||
"CSV Files (*.csv);;All Files (*.*)"
|
||
)
|
||
layout.addWidget(self.formula_csv_file)
|
||
|
||
# 模型目录选择
|
||
self.models_dir_file = FileSelectWidget(
|
||
"模型目录:",
|
||
"Directories;;All Files (*.*)"
|
||
)
|
||
self.models_dir_file.label.setText("模型目录:")
|
||
# 修改浏览按钮为选择目录
|
||
self.models_dir_file.browse_btn.clicked.disconnect()
|
||
self.models_dir_file.browse_btn.clicked.connect(self.browse_models_dir)
|
||
layout.addWidget(self.models_dir_file)
|
||
|
||
# 参数设置
|
||
params_group = QGroupBox("预测参数")
|
||
params_layout = QFormLayout()
|
||
|
||
# 预测列名
|
||
self.prediction_column = QLineEdit()
|
||
self.prediction_column.setText("prediction")
|
||
params_layout.addRow("预测列名:", self.prediction_column)
|
||
|
||
params_group.setLayout(params_layout)
|
||
layout.addWidget(params_group)
|
||
|
||
# 启用步骤
|
||
self.enable_checkbox = QCheckBox("启用此步骤")
|
||
self.enable_checkbox.setChecked(True)
|
||
layout.addWidget(self.enable_checkbox)
|
||
|
||
# 独立运行按钮
|
||
self.run_button = QPushButton("独立运行此步骤")
|
||
self.run_button.setStyleSheet("""
|
||
QPushButton {
|
||
background-color: #4CAF50;
|
||
color: white;
|
||
padding: 8px 16px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-weight: bold;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: #45a049;
|
||
}
|
||
QPushButton:pressed {
|
||
background-color: #3e8e41;
|
||
}
|
||
""")
|
||
self.run_button.clicked.connect(self.run_step)
|
||
layout.addWidget(self.run_button)
|
||
|
||
layout.addStretch()
|
||
self.setLayout(layout)
|
||
|
||
def browse_models_dir(self):
|
||
"""浏览模型目录"""
|
||
dir_path = QFileDialog.getExistingDirectory(self, "选择模型目录", "")
|
||
if dir_path:
|
||
self.models_dir_file.set_path(dir_path)
|
||
|
||
def get_config(self):
|
||
"""获取配置"""
|
||
config = {
|
||
'prediction_column': self.prediction_column.text(),
|
||
'enabled': self.enable_checkbox.isChecked()
|
||
}
|
||
# 添加采样光谱CSV路径
|
||
sampling_csv_path = self.sampling_csv_file.get_path()
|
||
if sampling_csv_path:
|
||
config['sampling_csv_path'] = sampling_csv_path
|
||
# 添加公式CSV文件路径
|
||
formula_csv_path = self.formula_csv_file.get_path()
|
||
if formula_csv_path:
|
||
config['formula_csv_file'] = formula_csv_path
|
||
# 添加模型目录路径
|
||
models_dir = self.models_dir_file.get_path()
|
||
if models_dir:
|
||
config['custom_regression_dir'] = models_dir
|
||
return config
|
||
|
||
def set_config(self, config):
|
||
"""设置配置"""
|
||
if 'prediction_column' in config:
|
||
self.prediction_column.setText(config['prediction_column'])
|
||
|
||
if 'sampling_csv_path' in config:
|
||
self.sampling_csv_file.set_path(config['sampling_csv_path'])
|
||
|
||
if 'formula_csv_file' in config:
|
||
self.formula_csv_file.set_path(config['formula_csv_file'])
|
||
|
||
if 'custom_regression_dir' in config:
|
||
self.models_dir_file.set_path(config['custom_regression_dir'])
|
||
|
||
if 'enabled' in config:
|
||
self.enable_checkbox.setChecked(config['enabled'])
|
||
|
||
def run_step(self):
|
||
"""独立运行步骤8.75"""
|
||
# 验证输入
|
||
sampling_csv_path = self.sampling_csv_file.get_path()
|
||
if not sampling_csv_path:
|
||
QMessageBox.warning(self, "输入错误", "请选择采样光谱CSV文件!")
|
||
return
|
||
|
||
formula_csv_path = self.formula_csv_file.get_path()
|
||
if not formula_csv_path:
|
||
QMessageBox.warning(self, "输入错误", "请选择公式CSV文件!")
|
||
return
|
||
|
||
# 获取配置
|
||
config = self.get_config()
|
||
|
||
# 调用GUI的run_single_step方法
|
||
parent = self.parent()
|
||
while parent and not hasattr(parent, 'run_single_step'):
|
||
parent = parent.parent()
|
||
|
||
if parent and hasattr(parent, 'run_single_step'):
|
||
parent.run_single_step('step8_75', {'step8_75': config})
|
||
else:
|
||
QMessageBox.critical(self, "错误", "无法找到父级GUI对象")
|
||
|
||
|
||
class ChartViewerDialog(QDialog):
|
||
"""图表查看器对话框"""
|
||
def __init__(self, title="图表查看器", parent=None):
|
||
super().__init__(parent)
|
||
self.setWindowTitle(title)
|
||
self.resize(1000, 700)
|
||
self.init_ui()
|
||
|
||
def init_ui(self):
|
||
layout = QVBoxLayout()
|
||
|
||
# 创建matplotlib图形
|
||
self.figure = Figure(figsize=(10, 7))
|
||
self.canvas = FigureCanvas(self.figure)
|
||
self.canvas.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||
|
||
# 添加工具栏
|
||
self.toolbar = NavigationToolbar(self.canvas, self)
|
||
|
||
layout.addWidget(self.toolbar)
|
||
layout.addWidget(self.canvas)
|
||
|
||
# 按钮组
|
||
btn_layout = QHBoxLayout()
|
||
|
||
self.save_btn = QPushButton("保存图表")
|
||
self.save_btn.clicked.connect(self.save_chart)
|
||
btn_layout.addWidget(self.save_btn)
|
||
|
||
btn_layout.addStretch()
|
||
|
||
self.close_btn = QPushButton("关闭")
|
||
self.close_btn.clicked.connect(self.close)
|
||
btn_layout.addWidget(self.close_btn)
|
||
|
||
layout.addLayout(btn_layout)
|
||
self.setLayout(layout)
|
||
|
||
def display_image(self, image_path):
|
||
"""显示图片"""
|
||
self.figure.clear()
|
||
ax = self.figure.add_subplot(111)
|
||
|
||
try:
|
||
import matplotlib.image as mpimg
|
||
img = mpimg.imread(image_path)
|
||
ax.imshow(img)
|
||
ax.axis('off')
|
||
self.figure.tight_layout()
|
||
self.canvas.draw()
|
||
self.current_image_path = image_path
|
||
except Exception as e:
|
||
ax.text(0.5, 0.5, f'加载图片失败:\n{str(e)}',
|
||
ha='center', va='center', transform=ax.transAxes)
|
||
self.canvas.draw()
|
||
|
||
def display_custom_plot(self, plot_func):
|
||
"""显示自定义绘图函数"""
|
||
self.figure.clear()
|
||
try:
|
||
plot_func(self.figure)
|
||
self.canvas.draw()
|
||
except Exception as e:
|
||
ax = self.figure.add_subplot(111)
|
||
ax.text(0.5, 0.5, f'绘图失败:\n{str(e)}',
|
||
ha='center', va='center', transform=ax.transAxes)
|
||
self.canvas.draw()
|
||
|
||
def save_chart(self):
|
||
"""保存图表"""
|
||
file_path, _ = QFileDialog.getSaveFileName(
|
||
self, "保存图表", "",
|
||
"PNG图片 (*.png);;JPG图片 (*.jpg);;PDF文件 (*.pdf);;所有文件 (*.*)"
|
||
)
|
||
if file_path:
|
||
try:
|
||
self.figure.savefig(file_path, dpi=300, bbox_inches='tight')
|
||
QMessageBox.information(self, "成功", f"图表已保存到:\n{file_path}")
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "错误", f"保存失败:\n{str(e)}")
|
||
|
||
|
||
class ImageCategoryTree(QTreeWidget):
|
||
"""图像分类目录树 - 按类别组织图像文件"""
|
||
|
||
# 图像类别定义:(类别名称, 关键词列表, 图标)
|
||
CATEGORIES = [
|
||
("模型评估", ["scatter", "regression", "validation", "r2", "rmse"], "📊"),
|
||
("光谱分析", ["spectrum", "spectral", "band", "wavelength"], "📈"),
|
||
("统计图表", ["boxplot", "histogram", "heatmap", "statistics", "stats"], "📉"),
|
||
("处理结果", ["mask", "glint", "deglint", "preview", "overlay", "water_mask"], "🖼️"),
|
||
("采样分析", ["sampling", "flight_path", "point_map", "trajectory"], "📍"),
|
||
("其他图表", [], "📁"),
|
||
]
|
||
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.setHeaderLabel("图像目录")
|
||
self.setMaximumWidth(300)
|
||
self.setMinimumWidth(250)
|
||
self.setup_categories()
|
||
self.setStyleSheet("""
|
||
QTreeWidget {
|
||
border: 1px solid #ddd;
|
||
border-radius: 5px;
|
||
background-color: #f8f9fa;
|
||
}
|
||
QTreeWidget::item {
|
||
padding: 5px;
|
||
border-radius: 3px;
|
||
}
|
||
QTreeWidget::item:selected {
|
||
background-color: #0078D4;
|
||
color: white;
|
||
}
|
||
QTreeWidget::item:hover {
|
||
background-color: #e3f2fd;
|
||
}
|
||
""")
|
||
|
||
def setup_categories(self):
|
||
"""初始化类别节点"""
|
||
self.category_items = {}
|
||
for category_name, keywords, icon in self.CATEGORIES:
|
||
item = QTreeWidgetItem(self)
|
||
item.setText(0, f"{icon} {category_name}")
|
||
item.setData(0, Qt.UserRole, {"type": "category", "keywords": keywords, "name": category_name})
|
||
item.setExpanded(True)
|
||
self.category_items[category_name] = item
|
||
|
||
def clear_all_images(self):
|
||
"""清除所有图像项"""
|
||
for category_item in self.category_items.values():
|
||
# 删除所有子项
|
||
while category_item.childCount() > 0:
|
||
category_item.removeChild(category_item.child(0))
|
||
|
||
def add_image(self, file_path: Path, display_name: str = None):
|
||
"""添加图像到对应的类别"""
|
||
if display_name is None:
|
||
display_name = file_path.stem
|
||
|
||
# 根据文件名关键词确定类别
|
||
category = self._determine_category(file_path.name)
|
||
category_item = self.category_items.get(category, self.category_items["其他图表"])
|
||
|
||
# 创建图像项
|
||
image_item = QTreeWidgetItem(category_item)
|
||
image_item.setText(0, f" └─ {display_name}")
|
||
image_item.setData(0, Qt.UserRole, {"type": "image", "path": str(file_path)})
|
||
image_item.setToolTip(0, str(file_path))
|
||
|
||
return image_item
|
||
|
||
def _determine_category(self, filename: str) -> str:
|
||
"""根据文件名确定类别"""
|
||
filename_lower = filename.lower()
|
||
|
||
for category_name, keywords, _ in self.CATEGORIES:
|
||
if any(keyword in filename_lower for keyword in keywords):
|
||
return category_name
|
||
|
||
return "其他图表"
|
||
|
||
def scan_directory(self, work_dir: str):
|
||
"""扫描目录中的所有图像文件"""
|
||
self.clear_all_images()
|
||
|
||
work_path = Path(work_dir)
|
||
if not work_path.exists():
|
||
return
|
||
|
||
# 查找所有图像文件:14_visualization 为主,同时扫描步骤产出目录(如 1_water_mask 下的预览/叠置图)
|
||
image_extensions = ['*.png', '*.jpg', '*.jpeg', '*.tif', '*.tiff', '*.bmp']
|
||
scan_roots: List[Path] = []
|
||
_viz = work_path / "14_visualization"
|
||
if _viz.is_dir():
|
||
scan_roots.append(_viz)
|
||
_wm = work_path / "1_water_mask"
|
||
if _wm.is_dir():
|
||
scan_roots.append(_wm)
|
||
if not scan_roots:
|
||
scan_roots.append(work_path)
|
||
|
||
seen_norm: set = set()
|
||
image_files: List[Path] = []
|
||
for root in scan_roots:
|
||
for ext in image_extensions:
|
||
for p in root.glob(f"**/{ext}"):
|
||
key = os.path.normcase(os.path.normpath(str(p.resolve())))
|
||
if key in seen_norm:
|
||
continue
|
||
seen_norm.add(key)
|
||
image_files.append(p)
|
||
|
||
# 添加图像到树
|
||
for img_file in sorted(image_files):
|
||
# 跳过缩略图和临时文件
|
||
if img_file.name.startswith('.') or 'thumb' in img_file.name.lower():
|
||
continue
|
||
self.add_image(img_file)
|
||
|
||
# 更新类别项文本显示数量
|
||
for category_name, item in self.category_items.items():
|
||
count = item.childCount()
|
||
if count > 0:
|
||
for cat_name, _, icon in self.CATEGORIES:
|
||
if cat_name == category_name:
|
||
item.setText(0, f"{icon} {category_name} ({count})")
|
||
break
|
||
|
||
def get_selected_image_path(self) -> Optional[str]:
|
||
"""获取当前选中的图像路径"""
|
||
selected_item = self.currentItem()
|
||
if not selected_item:
|
||
return None
|
||
|
||
data = selected_item.data(0, Qt.UserRole)
|
||
if data and data.get("type") == "image":
|
||
return data.get("path")
|
||
return None
|
||
|
||
|
||
class ImageViewerWidget(QWidget):
|
||
"""图像查看器组件 - 支持缩放、平移"""
|
||
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.current_image_path = None
|
||
self.scale_factor = 1.0
|
||
self._update_timer = QTimer() # 防抖定时器
|
||
self._update_timer.setSingleShot(True)
|
||
self._update_timer.timeout.connect(self._do_update_display)
|
||
self._pending_scale = None # 待更新的缩放比例
|
||
self.setup_ui()
|
||
|
||
def setup_ui(self):
|
||
layout = QVBoxLayout()
|
||
layout.setContentsMargins(0, 0, 0, 0)
|
||
|
||
# 工具栏
|
||
toolbar = QHBoxLayout()
|
||
|
||
self.refresh_btn = QPushButton("🔄 刷新目录")
|
||
self.refresh_btn.setToolTip("重新扫描工作目录中的图像文件")
|
||
toolbar.addWidget(self.refresh_btn)
|
||
|
||
# 添加分隔线
|
||
separator = QFrame()
|
||
separator.setFrameShape(QFrame.VLine)
|
||
separator.setFrameShadow(QFrame.Sunken)
|
||
toolbar.addWidget(separator)
|
||
|
||
self.zoom_in_btn = QPushButton("🔍+")
|
||
self.zoom_in_btn.setToolTip("放大")
|
||
self.zoom_in_btn.setMaximumWidth(50)
|
||
toolbar.addWidget(self.zoom_in_btn)
|
||
|
||
self.zoom_out_btn = QPushButton("🔍-")
|
||
self.zoom_out_btn.setToolTip("缩小")
|
||
self.zoom_out_btn.setMaximumWidth(50)
|
||
toolbar.addWidget(self.zoom_out_btn)
|
||
|
||
self.fit_btn = QPushButton("⬜ 适应窗口")
|
||
self.fit_btn.setToolTip("适应窗口大小")
|
||
toolbar.addWidget(self.fit_btn)
|
||
|
||
self.original_btn = QPushButton("1:1 原始大小")
|
||
self.original_btn.setToolTip("原始大小")
|
||
toolbar.addWidget(self.original_btn)
|
||
|
||
toolbar.addStretch()
|
||
|
||
self.save_btn = QPushButton("💾 保存")
|
||
self.save_btn.setToolTip("保存当前图像")
|
||
toolbar.addWidget(self.save_btn)
|
||
|
||
layout.addLayout(toolbar)
|
||
|
||
# 图像显示区域 - 使用 QLabel + QScrollArea
|
||
self.scroll_area = QScrollArea()
|
||
self.scroll_area.setWidgetResizable(True)
|
||
self.scroll_area.setStyleSheet("background-color: white;")
|
||
|
||
self.image_label = QLabel()
|
||
self.image_label.setAlignment(Qt.AlignCenter)
|
||
self.image_label.setStyleSheet("background-color: white;")
|
||
|
||
self.scroll_area.setWidget(self.image_label)
|
||
layout.addWidget(self.scroll_area, 1)
|
||
|
||
# 状态栏
|
||
status_layout = QHBoxLayout()
|
||
self.status_label = QLabel("就绪")
|
||
self.status_label.setStyleSheet("color: #666; font-size: 11px;")
|
||
status_layout.addWidget(self.status_label)
|
||
status_layout.addStretch()
|
||
layout.addLayout(status_layout)
|
||
|
||
self.setLayout(layout)
|
||
|
||
# 连接信号
|
||
self.zoom_in_btn.clicked.connect(self.zoom_in)
|
||
self.zoom_out_btn.clicked.connect(self.zoom_out)
|
||
self.fit_btn.clicked.connect(self.fit_to_window)
|
||
self.original_btn.clicked.connect(self.original_size)
|
||
self.save_btn.clicked.connect(self.save_image)
|
||
|
||
def load_image(self, image_path: str):
|
||
"""加载并显示图像"""
|
||
if not image_path or not Path(image_path).exists():
|
||
self.image_label.setText("图像不存在")
|
||
self.status_label.setText("图像加载失败")
|
||
return
|
||
|
||
self.current_image_path = image_path
|
||
self.scale_factor = 1.0
|
||
|
||
# 加载图像
|
||
pixmap = QPixmap(image_path)
|
||
if pixmap.isNull():
|
||
self.image_label.setText("无法加载图像")
|
||
self.status_label.setText("图像格式不支持")
|
||
return
|
||
|
||
self.original_pixmap = pixmap
|
||
|
||
# 默认适应窗口显示
|
||
self.fit_to_window()
|
||
|
||
# 更新状态
|
||
file_info = Path(image_path).stat()
|
||
size_mb = file_info.st_size / (1024 * 1024)
|
||
self.status_label.setText(f"{pixmap.width()}x{pixmap.height()} | {size_mb:.2f} MB | {Path(image_path).name} | 适应窗口")
|
||
|
||
def update_image_display(self):
|
||
"""更新图像显示 - 使用防抖避免频繁重绘卡顿"""
|
||
# 取消之前的待执行更新,重新计时
|
||
self._update_timer.stop()
|
||
self._pending_scale = self.scale_factor
|
||
self._update_timer.start(50) # 50ms后执行实际更新
|
||
|
||
def _do_update_display(self):
|
||
"""实际执行图像更新"""
|
||
if not hasattr(self, 'original_pixmap') or self.original_pixmap.isNull():
|
||
return
|
||
|
||
if self._pending_scale is None:
|
||
return
|
||
|
||
# 根据缩放比例选择变换模式:大幅度缩放用Fast模式提升性能
|
||
if self._pending_scale > 2.0 or self._pending_scale < 0.5:
|
||
transform = Qt.FastTransformation
|
||
else:
|
||
transform = Qt.SmoothTransformation
|
||
|
||
scaled_pixmap = self.original_pixmap.scaled(
|
||
int(self.original_pixmap.width() * self._pending_scale),
|
||
int(self.original_pixmap.height() * self._pending_scale),
|
||
Qt.KeepAspectRatio,
|
||
transform
|
||
)
|
||
self.image_label.setPixmap(scaled_pixmap)
|
||
self._pending_scale = None
|
||
|
||
def wheelEvent(self, event):
|
||
"""鼠标滚轮缩放 - 实时响应"""
|
||
delta = event.angleDelta().y()
|
||
|
||
if delta > 0:
|
||
# 向上滚动 - 放大
|
||
if self.scale_factor < 5.0:
|
||
self.scale_factor = min(self.scale_factor * 1.1, 5.0)
|
||
self.update_image_display()
|
||
else:
|
||
# 向下滚动 - 缩小
|
||
if self.scale_factor > 0.1:
|
||
self.scale_factor = max(self.scale_factor / 1.1, 0.1)
|
||
self.update_image_display()
|
||
|
||
event.accept()
|
||
|
||
def zoom_in(self):
|
||
"""放大"""
|
||
if self.scale_factor < 5.0:
|
||
self.scale_factor = min(self.scale_factor * 1.25, 5.0)
|
||
self.update_image_display()
|
||
|
||
def zoom_out(self):
|
||
"""缩小"""
|
||
if self.scale_factor > 0.1:
|
||
self.scale_factor = max(self.scale_factor / 1.25, 0.1)
|
||
self.update_image_display()
|
||
|
||
def fit_to_window(self):
|
||
"""适应窗口"""
|
||
if not hasattr(self, 'original_pixmap') or self.original_pixmap.isNull():
|
||
return
|
||
|
||
# 计算适应窗口的缩放比例
|
||
view_size = self.scroll_area.viewport().size()
|
||
img_size = self.original_pixmap.size()
|
||
|
||
scale_w = view_size.width() / img_size.width()
|
||
scale_h = view_size.height() / img_size.height()
|
||
|
||
# 记录适应前的比例(用于后续恢复参考)
|
||
self._fit_scale = min(scale_w, scale_h)
|
||
self.scale_factor = self._fit_scale
|
||
|
||
self.update_image_display()
|
||
self.status_label.setText(f"适应窗口 | 缩放: {self.scale_factor:.1%}")
|
||
|
||
def original_size(self):
|
||
"""原始大小"""
|
||
self.scale_factor = 1.0
|
||
self._fit_scale = None # 清除适应记录
|
||
self.update_image_display()
|
||
self.status_label.setText("原始大小 | 缩放: 100%")
|
||
|
||
def save_image(self):
|
||
"""保存图像"""
|
||
if not self.current_image_path:
|
||
return
|
||
|
||
file_path, _ = QFileDialog.getSaveFileName(
|
||
self, "保存图像", Path(self.current_image_path).name,
|
||
"PNG图片 (*.png);;JPG图片 (*.jpg);;所有文件 (*.*)"
|
||
)
|
||
|
||
if file_path:
|
||
try:
|
||
import shutil
|
||
shutil.copy(self.current_image_path, file_path)
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "错误", f"保存失败: {e}")
|
||
|
||
|
||
class VisualizationPanel(QWidget):
|
||
"""可视化分析面板 - 重构版:左侧目录树 + 右侧图像查看器"""
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.work_dir = None
|
||
self.chart_viewer = None
|
||
self._viz_thread = None
|
||
self.init_ui()
|
||
|
||
def _viz_set_busy(self, busy: bool):
|
||
for w in (
|
||
getattr(self, "gen_all_btn", None),
|
||
getattr(self, "gen_scatter_btn", None),
|
||
getattr(self, "gen_spectrum_btn", None),
|
||
getattr(self, "gen_stats_btn", None),
|
||
getattr(self, "gen_mask_glint_btn", None),
|
||
getattr(self, "gen_sampling_map_btn", None),
|
||
):
|
||
if w is not None:
|
||
w.setEnabled(not busy)
|
||
|
||
def _start_visualization_thread(self, task: str, extra: Optional[dict] = None) -> bool:
|
||
if not self.work_dir:
|
||
QMessageBox.warning(self, "警告", "请先选择工作目录!")
|
||
return False
|
||
work_path = Path(self.work_dir)
|
||
if not work_path.exists():
|
||
QMessageBox.warning(self, "警告", "工作目录不存在!")
|
||
return False
|
||
if self._viz_thread and self._viz_thread.isRunning():
|
||
QMessageBox.information(self, "提示", "可视化任务正在运行,请稍候。")
|
||
return False
|
||
self._viz_thread = VisualizationWorkerThread(task, str(work_path), extra or {})
|
||
self._viz_thread.finished_ok.connect(self._on_visualization_worker_ok, Qt.QueuedConnection)
|
||
self._viz_thread.failed.connect(self._on_visualization_worker_fail, Qt.QueuedConnection)
|
||
self._viz_thread.finished.connect(lambda: self._viz_set_busy(False), Qt.QueuedConnection)
|
||
self._viz_set_busy(True)
|
||
self._viz_thread.start()
|
||
return True
|
||
|
||
def _spectrum_meta_param_columns(self, df: pd.DataFrame) -> List[str]:
|
||
"""光谱图可选的水质参数列(光谱波段列之前、且为数值型)。"""
|
||
wl = _viz_infer_wavelength_start_column(df)
|
||
if isinstance(wl, str):
|
||
idx = int(df.columns.get_loc(wl)) + 1
|
||
else:
|
||
idx = int(wl)
|
||
if idx <= 0 or idx >= len(df.columns):
|
||
numeric = df.select_dtypes(include=[np.number]).columns.tolist()
|
||
return [
|
||
c
|
||
for c in numeric
|
||
if not any(x in str(c).lower() for x in ("utm", "lat", "lon", "x", "y"))
|
||
]
|
||
meta = list(df.columns[:idx])
|
||
return [c for c in meta if pd.api.types.is_numeric_dtype(df[c])]
|
||
|
||
def _statistics_param_columns(self, df: pd.DataFrame) -> List[str]:
|
||
"""统计图用的参数列;若存在光谱波段,则只统计波段前的字段。"""
|
||
numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist()
|
||
wl = _viz_infer_wavelength_start_column(df)
|
||
if isinstance(wl, str):
|
||
idx = int(df.columns.get_loc(wl)) + 1
|
||
else:
|
||
idx = int(wl)
|
||
coord_kw = ("utm", "lat", "lon")
|
||
if 0 < idx < len(df.columns):
|
||
meta_set = set(df.columns[:idx])
|
||
return [
|
||
col
|
||
for col in numeric_cols
|
||
if col in meta_set and not any(x in str(col).lower() for x in coord_kw)
|
||
]
|
||
return [
|
||
col
|
||
for col in numeric_cols
|
||
if not any(x in str(col).lower() for x in coord_kw + ("x", "y"))
|
||
]
|
||
|
||
def _on_visualization_worker_ok(self, payload):
|
||
if not isinstance(payload, dict):
|
||
self.scan_work_directory()
|
||
return
|
||
t = payload.get("task")
|
||
if t == "mask_glint":
|
||
cnt = int(payload.get("count") or 0)
|
||
if cnt > 0:
|
||
QMessageBox.information(
|
||
self,
|
||
"成功",
|
||
f"掩膜和耀斑缩略图生成完成,共 {cnt} 个预览图。\n"
|
||
f"保存位置: 14_visualization/glint_deglint_previews/",
|
||
)
|
||
else:
|
||
QMessageBox.warning(
|
||
self,
|
||
"警告",
|
||
"未找到可处理的影像文件(2_glint/3_deglint 等)。",
|
||
)
|
||
elif t == "sampling_map":
|
||
map_path = payload.get("map_path")
|
||
QMessageBox.information(
|
||
self,
|
||
"成功",
|
||
"采样点地图生成完成。\n"
|
||
f"输出: {Path(map_path).name if map_path else ''}\n"
|
||
"路径: 14_visualization/sampling_maps/",
|
||
)
|
||
if map_path:
|
||
self.show_chart_viewer(map_path, "采样点分布图")
|
||
elif t == "spectrum":
|
||
multi = payload.get("output_paths")
|
||
if isinstance(multi, list) and multi:
|
||
ok_paths = [p for p in multi if p and Path(str(p)).is_file()]
|
||
errs = payload.get("errors") or []
|
||
msg = (
|
||
f"已为 {len(ok_paths)} 个水质参数生成光谱对比图。\n"
|
||
f"保存目录: 工作目录/14_visualization/"
|
||
)
|
||
if errs:
|
||
msg += f"\n\n以下列未生成或出错 ({len(errs)} 项,详见日志):\n"
|
||
msg += "\n".join(str(e) for e in errs[:8])
|
||
if len(errs) > 8:
|
||
msg += "\n..."
|
||
QMessageBox.information(self, "成功", msg)
|
||
if ok_paths:
|
||
self.show_chart_viewer(ok_paths[0], "光谱曲线对比(首张)")
|
||
else:
|
||
outp = payload.get("output_path")
|
||
param = payload.get("param_col", "")
|
||
QMessageBox.information(self, "成功", f"光谱图已生成:\n{outp}")
|
||
if outp:
|
||
self.show_chart_viewer(outp, f"{param} - 光谱曲线对比")
|
||
elif t == "statistics":
|
||
outp = payload.get("output_paths") or {}
|
||
QMessageBox.information(
|
||
self, "成功", f"统计图表已生成,共 {len(outp)} 项。"
|
||
)
|
||
if isinstance(outp, dict) and "boxplot" in outp:
|
||
self.show_chart_viewer(outp["boxplot"], "水质参数箱线图")
|
||
elif t == "scatter":
|
||
paths = payload.get("scatter_paths") or {}
|
||
ok_paths = [p for p in paths.values() if p and Path(str(p)).is_file()]
|
||
if ok_paths:
|
||
QMessageBox.information(
|
||
self,
|
||
"成功",
|
||
f"已生成 {len(ok_paths)} 个模型评估散点图。\n"
|
||
f"保存位置: 14_visualization/scatter_plots/",
|
||
)
|
||
self.show_chart_viewer(ok_paths[0], "模型评估散点图")
|
||
else:
|
||
QMessageBox.warning(
|
||
self,
|
||
"提示",
|
||
"未生成任何散点图。请确认 7_models 下已有各参数子目录及模型文件,"
|
||
"且训练 CSV 与建模时一致。",
|
||
)
|
||
elif t == "generate_all_selected":
|
||
parts = payload.get("parts") or []
|
||
QMessageBox.information(
|
||
self,
|
||
"完成",
|
||
"批量可视化已执行:\n" + "\n".join(parts) if parts else "(无选中项或已跳过)",
|
||
)
|
||
self.scan_work_directory()
|
||
|
||
def _on_visualization_worker_fail(self, err: str):
|
||
QMessageBox.critical(self, "错误", f"可视化任务失败:\n{err[:1200]}")
|
||
|
||
def init_ui(self):
|
||
"""初始化UI - 使用左右分栏布局"""
|
||
main_layout = QHBoxLayout()
|
||
main_layout.setSpacing(10)
|
||
main_layout.setContentsMargins(10, 10, 10, 10)
|
||
|
||
# ===== 左侧面板 =====
|
||
left_panel = QWidget()
|
||
left_layout = QVBoxLayout()
|
||
left_layout.setContentsMargins(0, 0, 0, 0)
|
||
|
||
# 工作目录选择
|
||
dir_group = QGroupBox("工作目录")
|
||
dir_layout = QHBoxLayout()
|
||
self.work_dir_edit = QLineEdit()
|
||
self.work_dir_edit.setPlaceholderText("选择工作目录...")
|
||
self.work_dir_edit.setReadOnly(True)
|
||
dir_browse_btn = QPushButton("浏览")
|
||
dir_browse_btn.clicked.connect(self.browse_work_dir)
|
||
dir_layout.addWidget(self.work_dir_edit, 1)
|
||
dir_layout.addWidget(dir_browse_btn)
|
||
dir_group.setLayout(dir_layout)
|
||
left_layout.addWidget(dir_group)
|
||
|
||
# 图像目录树
|
||
tree_group = QGroupBox("图像目录")
|
||
tree_layout = QVBoxLayout()
|
||
self.image_tree = ImageCategoryTree()
|
||
self.image_tree.itemClicked.connect(self.on_tree_item_clicked)
|
||
tree_layout.addWidget(self.image_tree)
|
||
|
||
# 生成按钮组
|
||
gen_btn_layout = QHBoxLayout()
|
||
self.gen_all_btn = QPushButton("🚀 生成全部")
|
||
self.gen_all_btn.setToolTip("生成所有类型的可视化图表")
|
||
self.gen_all_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;")
|
||
self.gen_all_btn.clicked.connect(self.generate_all_visualizations)
|
||
gen_btn_layout.addWidget(self.gen_all_btn)
|
||
|
||
self.scan_btn = QPushButton("📁 扫描")
|
||
self.scan_btn.setToolTip("扫描工作目录中的图像文件")
|
||
self.scan_btn.clicked.connect(self.scan_work_directory)
|
||
gen_btn_layout.addWidget(self.scan_btn)
|
||
|
||
tree_layout.addLayout(gen_btn_layout)
|
||
tree_group.setLayout(tree_layout)
|
||
left_layout.addWidget(tree_group, 1)
|
||
|
||
# 可视化配置
|
||
config_group = QGroupBox("可视化配置")
|
||
config_layout = QVBoxLayout()
|
||
|
||
self.gen_scatter = QCheckBox("模型评估散点图")
|
||
self.gen_scatter.setChecked(True)
|
||
config_layout.addWidget(self.gen_scatter)
|
||
|
||
self.gen_spectrum = QCheckBox("光谱曲线图")
|
||
self.gen_spectrum.setChecked(True)
|
||
config_layout.addWidget(self.gen_spectrum)
|
||
|
||
self.gen_boxplots = QCheckBox("统计图表")
|
||
self.gen_boxplots.setChecked(True)
|
||
config_layout.addWidget(self.gen_boxplots)
|
||
|
||
self.gen_mask_glint = QCheckBox("掩膜和耀斑缩略图")
|
||
self.gen_mask_glint.setChecked(True)
|
||
config_layout.addWidget(self.gen_mask_glint)
|
||
|
||
self.gen_sampling_map = QCheckBox("采样点地图")
|
||
self.gen_sampling_map.setChecked(True)
|
||
config_layout.addWidget(self.gen_sampling_map)
|
||
|
||
config_group.setLayout(config_layout)
|
||
left_layout.addWidget(config_group)
|
||
|
||
left_panel.setLayout(left_layout)
|
||
left_panel.setMaximumWidth(350)
|
||
main_layout.addWidget(left_panel, 0)
|
||
|
||
# ===== 右侧面板 =====
|
||
right_panel = QWidget()
|
||
right_layout = QVBoxLayout()
|
||
right_layout.setContentsMargins(0, 0, 0, 0)
|
||
|
||
# 图像查看器
|
||
self.image_viewer = ImageViewerWidget()
|
||
self.image_viewer.refresh_btn.clicked.connect(self.scan_work_directory)
|
||
right_layout.addWidget(self.image_viewer, 1)
|
||
|
||
# 生成特定图表按钮组
|
||
specific_group = QGroupBox("生成特定图表")
|
||
specific_layout = QHBoxLayout()
|
||
|
||
self.gen_scatter_btn = QPushButton("📊 散点图")
|
||
self.gen_scatter_btn.setToolTip(
|
||
"基于工作目录下 5_training_spectra/training_spectra.csv 与 7_models 生成模型评估散点图"
|
||
)
|
||
self.gen_scatter_btn.clicked.connect(lambda: self.generate_chart('scatter'))
|
||
specific_layout.addWidget(self.gen_scatter_btn)
|
||
|
||
self.gen_spectrum_btn = QPushButton("📈 光谱图")
|
||
self.gen_spectrum_btn.setToolTip(
|
||
"基于 5_training_spectra/training_spectra.csv,为每个数值型水质参数各生成一张光谱对比图(无需选择)"
|
||
)
|
||
self.gen_spectrum_btn.clicked.connect(lambda: self.generate_chart('spectrum'))
|
||
specific_layout.addWidget(self.gen_spectrum_btn)
|
||
|
||
self.gen_stats_btn = QPushButton("📉 统计图")
|
||
self.gen_stats_btn.setToolTip(
|
||
"基于工作目录下 5_training_spectra/training_spectra.csv 生成箱线图、直方图与相关性热力图"
|
||
)
|
||
self.gen_stats_btn.clicked.connect(lambda: self.generate_chart('statistics'))
|
||
specific_layout.addWidget(self.gen_stats_btn)
|
||
|
||
self.gen_mask_glint_btn = QPushButton("🖼️ 掩膜图")
|
||
self.gen_mask_glint_btn.clicked.connect(lambda: self.generate_mask_glint_previews())
|
||
specific_layout.addWidget(self.gen_mask_glint_btn)
|
||
|
||
self.gen_sampling_map_btn = QPushButton("📍 采样点图")
|
||
self.gen_sampling_map_btn.clicked.connect(lambda: self.generate_sampling_point_map())
|
||
specific_layout.addWidget(self.gen_sampling_map_btn)
|
||
|
||
specific_group.setLayout(specific_layout)
|
||
right_layout.addWidget(specific_group)
|
||
|
||
right_panel.setLayout(right_layout)
|
||
main_layout.addWidget(right_panel, 1)
|
||
|
||
self.setLayout(main_layout)
|
||
|
||
def set_work_dir(self, work_dir):
|
||
"""设置工作目录"""
|
||
self.work_dir = work_dir
|
||
self.work_dir_edit.setText(str(work_dir))
|
||
# 自动扫描目录
|
||
if work_dir:
|
||
QTimer.singleShot(100, self.scan_work_directory) # 延迟执行确保UI更新
|
||
|
||
def browse_work_dir(self):
|
||
"""浏览工作目录"""
|
||
dir_path = QFileDialog.getExistingDirectory(self, "选择工作目录")
|
||
if dir_path:
|
||
self.work_dir = dir_path
|
||
self.work_dir_edit.setText(dir_path)
|
||
# 自动扫描目录
|
||
self.scan_work_directory()
|
||
|
||
def scan_work_directory(self):
|
||
"""扫描工作目录中的图像文件"""
|
||
if not self.work_dir:
|
||
return
|
||
|
||
work_path = Path(self.work_dir)
|
||
if not work_path.exists():
|
||
return
|
||
|
||
print(f"扫描工作目录: {work_path}")
|
||
self.image_tree.scan_directory(str(work_path))
|
||
|
||
# 如果有图像,自动选择第一个
|
||
viz_dir = work_path / "14_visualization"
|
||
if viz_dir.exists():
|
||
image_files = list(viz_dir.glob("**/*.png")) + list(viz_dir.glob("**/*.jpg"))
|
||
if image_files:
|
||
self.image_viewer.load_image(str(image_files[0]))
|
||
|
||
def on_tree_item_clicked(self, item, column):
|
||
"""目录树项点击事件"""
|
||
data = item.data(0, Qt.UserRole)
|
||
if not data:
|
||
return
|
||
|
||
if data.get("type") == "image":
|
||
image_path = data.get("path")
|
||
if image_path and Path(image_path).exists():
|
||
self.image_viewer.load_image(image_path)
|
||
|
||
def generate_all_visualizations(self):
|
||
"""生成所有可视化图表(耗时任务在后台线程执行,避免界面未响应)。"""
|
||
if not self.work_dir:
|
||
QMessageBox.warning(self, "警告", "请先选择工作目录!")
|
||
return
|
||
|
||
work_path = Path(self.work_dir)
|
||
if not work_path.exists():
|
||
QMessageBox.warning(self, "警告", "工作目录不存在!")
|
||
return
|
||
|
||
reply = QMessageBox.question(
|
||
self, "确认生成",
|
||
"将按左侧勾选项在后台生成可视化(掩膜/耀斑预览、采样点图等),可能需要较长时间。\n是否继续?",
|
||
QMessageBox.Yes | QMessageBox.No
|
||
)
|
||
|
||
if reply != QMessageBox.Yes:
|
||
return
|
||
|
||
if self.gen_scatter.isChecked():
|
||
print("生成散点图...(占位,请用建模/可视化流程生成)")
|
||
if self.gen_spectrum.isChecked():
|
||
print("生成光谱图...(占位,请用下方「光谱图」按钮)")
|
||
if self.gen_boxplots.isChecked():
|
||
print("生成统计图...(占位,请用下方「统计图」按钮)")
|
||
|
||
if not self.gen_mask_glint.isChecked() and not self.gen_sampling_map.isChecked():
|
||
QMessageBox.information(
|
||
self,
|
||
"提示",
|
||
"请至少勾选「掩膜和耀斑缩略图」或「采样点地图」以执行后台批量任务。",
|
||
)
|
||
return
|
||
|
||
extra = {
|
||
"gen_mask_glint": self.gen_mask_glint.isChecked(),
|
||
"gen_sampling_map": self.gen_sampling_map.isChecked(),
|
||
}
|
||
self._start_visualization_thread("generate_all_selected", extra)
|
||
|
||
def generate_chart(self, chart_type):
|
||
"""生成图表(光谱/统计图在后台线程绘制)。"""
|
||
if not self.work_dir:
|
||
QMessageBox.warning(self, "警告", "请先选择工作目录!")
|
||
return
|
||
|
||
work_path = Path(self.work_dir)
|
||
if not work_path.exists():
|
||
QMessageBox.warning(self, "警告", "工作目录不存在!")
|
||
return
|
||
|
||
try:
|
||
training_spectra_csv = _viz_training_spectra_csv_path(work_path)
|
||
|
||
if chart_type == 'scatter':
|
||
if not training_spectra_csv.is_file():
|
||
QMessageBox.warning(
|
||
self,
|
||
"警告",
|
||
"未找到 5_training_spectra\\training_spectra.csv。\n"
|
||
"请先在工作目录中执行步骤5(光谱特征提取)生成该文件。",
|
||
)
|
||
return
|
||
training_csv = training_spectra_csv
|
||
models_dir = work_path / "7_models"
|
||
if not models_dir.is_dir() or not any(
|
||
d.is_dir() for d in models_dir.iterdir()
|
||
):
|
||
mdir = QFileDialog.getExistingDirectory(
|
||
self,
|
||
"选择模型根目录(内含各水质参数子文件夹,如 chl_a)",
|
||
str(work_path),
|
||
)
|
||
if not mdir:
|
||
return
|
||
models_dir = Path(mdir)
|
||
self._start_visualization_thread(
|
||
"scatter",
|
||
{
|
||
"training_csv_path": str(training_csv),
|
||
"models_dir": str(models_dir),
|
||
},
|
||
)
|
||
return
|
||
|
||
if chart_type == 'spectrum':
|
||
if not training_spectra_csv.is_file():
|
||
QMessageBox.warning(
|
||
self,
|
||
"警告",
|
||
"未找到 5_training_spectra\\training_spectra.csv。\n"
|
||
"光谱分析固定使用该文件,请先执行步骤5(光谱特征提取)。",
|
||
)
|
||
return
|
||
csv_file = training_spectra_csv
|
||
df = pd.read_csv(csv_file)
|
||
columns = self._spectrum_meta_param_columns(df)
|
||
if not columns:
|
||
QMessageBox.warning(
|
||
self,
|
||
"警告",
|
||
"当前 CSV 中没有可用的数值型水质参数列,无法按参数分组绘制光谱图。\n"
|
||
"请使用步骤5输出的 training_spectra.csv(含参数列+波段列)。",
|
||
)
|
||
return
|
||
wl_col = _viz_infer_wavelength_start_column(df)
|
||
self._start_visualization_thread(
|
||
"spectrum",
|
||
{
|
||
"csv_path": str(csv_file),
|
||
"param_cols": columns,
|
||
"wavelength_start_column": wl_col,
|
||
"n_groups": 5,
|
||
},
|
||
)
|
||
return
|
||
|
||
if chart_type == 'statistics':
|
||
if not training_spectra_csv.is_file():
|
||
QMessageBox.warning(
|
||
self,
|
||
"警告",
|
||
"未找到 5_training_spectra\\training_spectra.csv。\n"
|
||
"统计分析固定使用该文件,请先执行步骤5(光谱特征提取)。",
|
||
)
|
||
return
|
||
csv_file = training_spectra_csv
|
||
df = pd.read_csv(csv_file)
|
||
param_cols = self._statistics_param_columns(df)
|
||
if not param_cols:
|
||
QMessageBox.warning(self, "警告", "未找到可用的水质参数列!")
|
||
return
|
||
self._start_visualization_thread(
|
||
"statistics",
|
||
{"csv_path": str(csv_file), "param_cols": param_cols},
|
||
)
|
||
return
|
||
|
||
if chart_type == 'sampling_map':
|
||
self.generate_sampling_point_map()
|
||
return
|
||
|
||
except ImportError:
|
||
QMessageBox.critical(
|
||
self,
|
||
"错误",
|
||
"无法导入可视化模块!\n请确保 visualization_reports.py 文件存在。",
|
||
)
|
||
except Exception as e:
|
||
QMessageBox.critical(
|
||
self,
|
||
"错误",
|
||
f"生成图表时出错:\n{str(e)}\n\n{traceback.format_exc()}",
|
||
)
|
||
|
||
def generate_mask_glint_previews(self):
|
||
"""生成掩膜和耀斑缩略图(后台线程)。"""
|
||
self._start_visualization_thread("mask_glint")
|
||
|
||
def generate_sampling_point_map(self):
|
||
"""生成采样点地图(后台线程)。"""
|
||
self._start_visualization_thread("sampling_map")
|
||
|
||
def view_chart(self, chart_type):
|
||
"""查看图表"""
|
||
if not self.work_dir:
|
||
QMessageBox.warning(self, "警告", "请先选择工作目录!")
|
||
return
|
||
|
||
work_path = Path(self.work_dir)
|
||
viz_dir = work_path / "14_visualization"
|
||
viz_dir2 = work_path / "14_visualization/boxplots"
|
||
viz_dir3 = work_path / "14_visualization/scatter_plots"
|
||
if not viz_dir.exists():
|
||
QMessageBox.warning(self, "警告",
|
||
f"可视化目录不存在:\n{viz_dir}\n\n请先生成图表。")
|
||
return
|
||
|
||
# 根据类型查找图表文件
|
||
chart_files = []
|
||
if chart_type == 'scatter':
|
||
chart_files = list(viz_dir3.glob("*scatter*.png"))
|
||
elif chart_type == 'spectrum':
|
||
chart_files = list(viz_dir.glob("*spectrum*.png"))
|
||
elif chart_type == 'statistics':
|
||
chart_files = list(viz_dir2.glob("*boxplot.png")) + \
|
||
list(viz_dir.glob("*histogram.png")) + \
|
||
list(viz_dir.glob("*heatmap.png"))
|
||
elif chart_type == 'distribution':
|
||
chart_files = list(viz_dir.glob("**/*distribution.png"))
|
||
elif chart_type == 'mask_glint':
|
||
# 查找掩膜和耀斑缩略图
|
||
glint_dir = viz_dir / "glint_deglint_previews"
|
||
if glint_dir.exists():
|
||
chart_files = list(glint_dir.glob("*preview.png"))
|
||
else:
|
||
# 如果专用目录不存在,从根目录查找
|
||
chart_files = list(viz_dir.glob("*preview.png")) + \
|
||
list(viz_dir.glob("*glint*.png")) + \
|
||
list(viz_dir.glob("*mask*.png"))
|
||
elif chart_type == 'sampling_map':
|
||
# 查找采样点地图
|
||
sampling_dir = viz_dir / "sampling_maps"
|
||
if sampling_dir.exists():
|
||
chart_files = list(sampling_dir.glob("*sampling_map.png"))
|
||
else:
|
||
chart_files = list(viz_dir.glob("*sampling*.png"))
|
||
|
||
if not chart_files:
|
||
if chart_type == 'mask_glint':
|
||
QMessageBox.warning(self, "警告",
|
||
"未找到掩膜和耀斑缩略图!\n\n"
|
||
"请先点击'生成掩膜&耀斑缩略图'按钮生成预览图。\n"
|
||
"需要2_glint或3_deglint文件夹中存在影像文件。")
|
||
else:
|
||
QMessageBox.warning(self, "警告",
|
||
f"未找到{chart_type}类型的图表文件!\n\n请先生成图表。")
|
||
return
|
||
|
||
# 如果有多个文件,让用户选择
|
||
if len(chart_files) > 1:
|
||
from PyQt5.QtWidgets import QInputDialog
|
||
file_names = [f.name for f in chart_files]
|
||
file_name, ok = QInputDialog.getItem(
|
||
self, "选择图表", "请选择要查看的图表:",
|
||
file_names, 0, False
|
||
)
|
||
if ok:
|
||
selected_file = next(f for f in chart_files if f.name == file_name)
|
||
self.show_chart_viewer(str(selected_file), file_name)
|
||
else:
|
||
self.show_chart_viewer(str(chart_files[0]), chart_files[0].name)
|
||
|
||
def browse_all_charts(self):
|
||
"""浏览所有图表"""
|
||
if not self.work_dir:
|
||
QMessageBox.warning(self, "警告", "请先选择工作目录!")
|
||
return
|
||
|
||
work_path = Path(self.work_dir)
|
||
|
||
# 查找所有图表文件
|
||
chart_files = []
|
||
chart_files.extend(work_path.glob("**/*.png"))
|
||
chart_files.extend(work_path.glob("**/*.jpg"))
|
||
|
||
if not chart_files:
|
||
QMessageBox.warning(self, "警告", "未找到图表文件!")
|
||
return
|
||
|
||
# 创建图表浏览对话框
|
||
dialog = ChartBrowserDialog(chart_files, self)
|
||
dialog.exec_()
|
||
|
||
def show_chart_viewer(self, image_path, title="图表查看器"):
|
||
"""显示图表查看器"""
|
||
viewer = ChartViewerDialog(title=title, parent=self)
|
||
viewer.display_image(image_path)
|
||
viewer.exec_()
|
||
|
||
def get_config(self):
|
||
"""获取配置"""
|
||
return {
|
||
'generate_scatter': self.gen_scatter.isChecked(),
|
||
'generate_boxplots': self.gen_boxplots.isChecked(),
|
||
'generate_spectrum': self.gen_spectrum.isChecked(),
|
||
'generate_statistics': self.gen_stats_btn.isChecked(),
|
||
'generate_glint_previews': self.gen_mask_glint.isChecked(),
|
||
'generate_sampling_maps': self.gen_sampling_map.isChecked(),
|
||
'scatter_config': {
|
||
'metric': 'test_r2',
|
||
'feature_start_column': 13,
|
||
'test_size': 0.2,
|
||
'random_state': 42
|
||
},
|
||
'boxplot_config': {
|
||
'data_start_column': 4,
|
||
'save_individual': True,
|
||
'use_seaborn': True
|
||
},
|
||
'glint_preview_config': {
|
||
'work_dir': None,
|
||
'output_subdir': 'glint_deglint_previews',
|
||
'generate_glint': True,
|
||
'generate_deglint': True
|
||
}
|
||
}
|
||
|
||
def set_config(self, config):
|
||
"""设置配置"""
|
||
if 'generate_scatter' in config:
|
||
self.gen_scatter.setChecked(config['generate_scatter'])
|
||
if 'generate_boxplots' in config:
|
||
self.gen_boxplots.setChecked(config['generate_boxplots'])
|
||
if 'generate_spectrum' in config:
|
||
self.gen_spectrum.setChecked(config['generate_spectrum'])
|
||
if 'generate_statistics' in config:
|
||
self.gen_stats_btn.setChecked(config['generate_statistics'])
|
||
if 'generate_glint_previews' in config:
|
||
self.gen_mask_glint.setChecked(config['generate_glint_previews'])
|
||
if 'generate_sampling_maps' in config:
|
||
self.gen_sampling_map.setChecked(config.get('generate_sampling_maps', True))
|
||
|
||
|
||
|
||
class ReportGenerationPanel(QWidget):
|
||
"""Word 报告生成:工作目录、输出目录、Ollama URL/模型、是否启用 AI 等。"""
|
||
|
||
def __init__(self, main_window=None, parent=None):
|
||
super().__init__(parent)
|
||
self.main_window = main_window
|
||
self._report_thread = None
|
||
self.init_ui()
|
||
|
||
def init_ui(self):
|
||
layout = QVBoxLayout()
|
||
layout.setContentsMargins(10, 10, 10, 10)
|
||
layout.setSpacing(10)
|
||
|
||
intro = QLabel(
|
||
"根据工作目录下的可视化结果(14_visualization 等)生成 Word 分析报告。"
|
||
"需已存在可视化图表;AI 分析通过 Ollama /api/chat 调用本地或远程服务。"
|
||
)
|
||
intro.setWordWrap(True)
|
||
intro.setStyleSheet(f"color: {ModernStylesheet.COLORS.get('text_secondary', '#666')};")
|
||
layout.addWidget(intro)
|
||
|
||
path_group = QGroupBox("路径")
|
||
path_form = QFormLayout()
|
||
|
||
wd_row = QHBoxLayout()
|
||
self.work_dir_edit = QLineEdit()
|
||
self.work_dir_edit.setPlaceholderText("选择流程工作目录(含 14_visualization)…")
|
||
wd_browse = QPushButton("浏览…")
|
||
wd_browse.clicked.connect(self.browse_work_dir)
|
||
sync_btn = QPushButton("同步主窗口工作目录")
|
||
sync_btn.clicked.connect(self.sync_work_dir_from_main)
|
||
wd_row.addWidget(self.work_dir_edit, 1)
|
||
wd_row.addWidget(wd_browse)
|
||
wd_row.addWidget(sync_btn)
|
||
path_form.addRow("工作目录:", wd_row)
|
||
|
||
out_row = QHBoxLayout()
|
||
self.output_dir_edit = QLineEdit()
|
||
self.output_dir_edit.setPlaceholderText("留空则保存到 工作目录/14_visualization")
|
||
out_browse = QPushButton("浏览…")
|
||
out_browse.clicked.connect(self.browse_output_dir)
|
||
out_row.addWidget(self.output_dir_edit, 1)
|
||
out_row.addWidget(out_browse)
|
||
path_form.addRow("报告输出目录:", out_row)
|
||
|
||
self.report_title_edit = QLineEdit()
|
||
self.report_title_edit.setText("水质参数反演分析报告")
|
||
path_form.addRow("报告标题:", self.report_title_edit)
|
||
|
||
path_group.setLayout(path_form)
|
||
layout.addWidget(path_group)
|
||
|
||
ai_group = QGroupBox("AI 分析(Ollama)")
|
||
ai_form = QFormLayout()
|
||
|
||
self.enable_ai_cb = QCheckBox("启用 AI 图表解读与综合总结")
|
||
self.enable_ai_cb.setChecked(os.environ.get("ENABLE_AI_ANALYSIS", "1") not in {"0", "false", "False"})
|
||
ai_form.addRow(self.enable_ai_cb)
|
||
|
||
self.ollama_url_edit = QLineEdit()
|
||
self.ollama_url_edit.setText(os.environ.get("OLLAMA_URL", "http://localhost:11434").rstrip("/"))
|
||
ai_form.addRow("服务 URL:", self.ollama_url_edit)
|
||
|
||
self.vision_model_edit = QLineEdit()
|
||
self.vision_model_edit.setText(os.environ.get("OLLAMA_VISION_MODEL", "qwen3-vl:8b"))
|
||
ai_form.addRow("视觉模型:", self.vision_model_edit)
|
||
|
||
self.same_text_model_cb = QCheckBox("文本总结与视觉使用同一模型")
|
||
self.same_text_model_cb.setChecked(True)
|
||
ai_form.addRow(self.same_text_model_cb)
|
||
|
||
self.text_model_edit = QLineEdit()
|
||
self.text_model_edit.setText(os.environ.get("OLLAMA_TEXT_MODEL", self.vision_model_edit.text() or "qwen3-vl:8b"))
|
||
self.text_model_edit.setEnabled(False)
|
||
self.same_text_model_cb.toggled.connect(self._on_same_text_toggled)
|
||
self.vision_model_edit.textChanged.connect(self._sync_text_model_if_linked)
|
||
ai_form.addRow("文本模型:", self.text_model_edit)
|
||
|
||
self.timeout_spin = QSpinBox()
|
||
self.timeout_spin.setRange(30, 3600)
|
||
self.timeout_spin.setSingleStep(30)
|
||
self.timeout_spin.setValue(int(os.environ.get("OLLAMA_TIMEOUT_S", "120")))
|
||
ai_form.addRow("请求超时(秒):", self.timeout_spin)
|
||
|
||
ai_group.setLayout(ai_form)
|
||
layout.addWidget(ai_group)
|
||
|
||
btn_row = QHBoxLayout()
|
||
self.generate_btn = QPushButton("生成 Word 报告")
|
||
self.generate_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet("success"))
|
||
self.generate_btn.clicked.connect(self.on_generate_clicked)
|
||
btn_row.addWidget(self.generate_btn)
|
||
btn_row.addStretch()
|
||
layout.addLayout(btn_row)
|
||
|
||
layout.addStretch()
|
||
self.setLayout(layout)
|
||
|
||
def _on_same_text_toggled(self, checked: bool):
|
||
self.text_model_edit.setEnabled(not checked)
|
||
if checked:
|
||
self.text_model_edit.setText(self.vision_model_edit.text())
|
||
|
||
def _sync_text_model_if_linked(self, _t=None):
|
||
if self.same_text_model_cb.isChecked():
|
||
self.text_model_edit.blockSignals(True)
|
||
self.text_model_edit.setText(self.vision_model_edit.text())
|
||
self.text_model_edit.blockSignals(False)
|
||
|
||
def browse_work_dir(self):
|
||
d = QFileDialog.getExistingDirectory(self, "选择工作目录")
|
||
if d:
|
||
self.work_dir_edit.setText(d)
|
||
|
||
def browse_output_dir(self):
|
||
d = QFileDialog.getExistingDirectory(self, "选择报告输出目录")
|
||
if d:
|
||
self.output_dir_edit.setText(d)
|
||
|
||
def sync_work_dir_from_main(self):
|
||
mw = self.main_window
|
||
if mw is not None and getattr(mw, "work_dir", None):
|
||
self.work_dir_edit.setText(str(mw.work_dir))
|
||
else:
|
||
QMessageBox.information(self, "提示", "主窗口尚未设置工作目录。")
|
||
|
||
def set_work_dir(self, work_dir):
|
||
if work_dir:
|
||
self.work_dir_edit.setText(str(work_dir))
|
||
|
||
def get_config(self):
|
||
return {
|
||
"work_dir": self.work_dir_edit.text().strip() or None,
|
||
"output_dir": self.output_dir_edit.text().strip() or None,
|
||
"report_title": self.report_title_edit.text().strip() or "水质参数反演分析报告",
|
||
"ollama_url": self.ollama_url_edit.text().strip(),
|
||
"ollama_vision_model": self.vision_model_edit.text().strip(),
|
||
"ollama_text_model": self.text_model_edit.text().strip(),
|
||
"text_same_as_vision": self.same_text_model_cb.isChecked(),
|
||
"ollama_timeout_s": self.timeout_spin.value(),
|
||
"enable_ai_analysis": self.enable_ai_cb.isChecked(),
|
||
}
|
||
|
||
def set_config(self, config):
|
||
if not config:
|
||
return
|
||
if config.get("work_dir"):
|
||
self.work_dir_edit.setText(str(config["work_dir"]))
|
||
if "output_dir" in config:
|
||
self.output_dir_edit.setText(str(config["output_dir"] or ""))
|
||
if config.get("report_title"):
|
||
self.report_title_edit.setText(str(config["report_title"]))
|
||
if config.get("ollama_url"):
|
||
self.ollama_url_edit.setText(str(config["ollama_url"]))
|
||
if config.get("ollama_vision_model"):
|
||
self.vision_model_edit.setText(str(config["ollama_vision_model"]))
|
||
if "text_same_as_vision" in config:
|
||
self.same_text_model_cb.setChecked(bool(config["text_same_as_vision"]))
|
||
if config.get("ollama_text_model"):
|
||
self.text_model_edit.setText(str(config["ollama_text_model"]))
|
||
if config.get("ollama_timeout_s") is not None:
|
||
self.timeout_spin.setValue(int(config["ollama_timeout_s"]))
|
||
if "enable_ai_analysis" in config:
|
||
self.enable_ai_cb.setChecked(bool(config["enable_ai_analysis"]))
|
||
|
||
def on_generate_clicked(self):
|
||
wd = self.work_dir_edit.text().strip()
|
||
if not wd or not os.path.isdir(wd):
|
||
QMessageBox.warning(self, "提示", "请选择有效的工作目录。")
|
||
return
|
||
viz = Path(wd) / "14_visualization"
|
||
if not viz.is_dir():
|
||
QMessageBox.warning(
|
||
self,
|
||
"提示",
|
||
f"未找到可视化目录:\n{viz}\n请先完成流程或生成可视化。",
|
||
)
|
||
return
|
||
if self._report_thread and self._report_thread.isRunning():
|
||
QMessageBox.information(self, "提示", "报告正在生成中,请稍候。")
|
||
return
|
||
|
||
out = self.output_dir_edit.text().strip() or None
|
||
title = self.report_title_edit.text().strip() or "水质参数反演分析报告"
|
||
opts = {
|
||
"ollama_url": self.ollama_url_edit.text().strip(),
|
||
"ollama_vision_model": self.vision_model_edit.text().strip(),
|
||
"ollama_text_model": self.text_model_edit.text().strip(),
|
||
"text_same_as_vision": self.same_text_model_cb.isChecked(),
|
||
"ollama_timeout_s": self.timeout_spin.value(),
|
||
"enable_ai_analysis": self.enable_ai_cb.isChecked(),
|
||
}
|
||
self.generate_btn.setEnabled(False)
|
||
self._report_thread = ReportGenerateThread(wd, out, title, opts)
|
||
self._report_thread.log_message.connect(self._forward_log, Qt.QueuedConnection)
|
||
self._report_thread.finished_ok.connect(self._on_report_ok, Qt.QueuedConnection)
|
||
self._report_thread.failed.connect(self._on_report_fail, Qt.QueuedConnection)
|
||
self._report_thread.finished.connect(lambda: self.generate_btn.setEnabled(True), Qt.QueuedConnection)
|
||
self._report_thread.start()
|
||
self._forward_log("已开始生成 Word 报告…", "info")
|
||
|
||
def _forward_log(self, msg: str, level: str):
|
||
mw = self.main_window
|
||
if mw is not None and hasattr(mw, "log_message"):
|
||
mw.log_message(msg, level)
|
||
else:
|
||
print(f"[{level}] {msg}")
|
||
|
||
def _on_report_ok(self, path: str):
|
||
QMessageBox.information(self, "完成", f"报告已生成:\n{path}")
|
||
self._forward_log(f"Word 报告已保存: {path}", "info")
|
||
|
||
def _on_report_fail(self, err: str):
|
||
QMessageBox.critical(self, "失败", f"报告生成失败:\n{err[:800]}")
|
||
self._forward_log(err, "error")
|
||
|
||
|
||
class ChartBrowserDialog(QDialog):
|
||
"""图表浏览器对话框"""
|
||
def __init__(self, chart_files, parent=None):
|
||
super().__init__(parent)
|
||
self.chart_files = sorted(chart_files, key=lambda x: x.stat().st_mtime, reverse=True)
|
||
self.current_index = 0
|
||
self.setWindowTitle("图表浏览器")
|
||
self.resize(1200, 800)
|
||
self.init_ui()
|
||
self.show_chart(0)
|
||
|
||
def init_ui(self):
|
||
layout = QVBoxLayout()
|
||
|
||
# 顶部:图表列表
|
||
list_group = QGroupBox(f"图表列表 (共 {len(self.chart_files)} 个)")
|
||
list_layout = QHBoxLayout()
|
||
|
||
self.chart_list = QListWidget()
|
||
self.chart_list.setMaximumHeight(150)
|
||
for chart_file in self.chart_files:
|
||
self.chart_list.addItem(chart_file.name)
|
||
self.chart_list.currentRowChanged.connect(self.show_chart)
|
||
|
||
list_layout.addWidget(self.chart_list)
|
||
list_group.setLayout(list_layout)
|
||
layout.addWidget(list_group)
|
||
|
||
# 中间:图表显示
|
||
self.figure = Figure(figsize=(12, 8))
|
||
self.canvas = FigureCanvas(self.figure)
|
||
self.canvas.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||
|
||
self.toolbar = NavigationToolbar(self.canvas, self)
|
||
layout.addWidget(self.toolbar)
|
||
layout.addWidget(self.canvas, 1)
|
||
|
||
# 底部:控制按钮
|
||
btn_layout = QHBoxLayout()
|
||
|
||
self.prev_btn = QPushButton("◀ 上一个")
|
||
self.prev_btn.clicked.connect(self.prev_chart)
|
||
btn_layout.addWidget(self.prev_btn)
|
||
|
||
self.next_btn = QPushButton("下一个 >")
|
||
self.next_btn.clicked.connect(self.next_chart)
|
||
btn_layout.addWidget(self.next_btn)
|
||
|
||
btn_layout.addStretch()
|
||
|
||
self.save_btn = QPushButton("💾 保存当前图表")
|
||
self.save_btn.clicked.connect(self.save_current_chart)
|
||
btn_layout.addWidget(self.save_btn)
|
||
|
||
self.close_btn = QPushButton("关闭")
|
||
self.close_btn.clicked.connect(self.close)
|
||
btn_layout.addWidget(self.close_btn)
|
||
|
||
layout.addLayout(btn_layout)
|
||
self.setLayout(layout)
|
||
|
||
def show_chart(self, index):
|
||
"""显示指定索引的图表"""
|
||
if 0 <= index < len(self.chart_files):
|
||
self.current_index = index
|
||
self.chart_list.setCurrentRow(index)
|
||
|
||
chart_file = self.chart_files[index]
|
||
self.figure.clear()
|
||
ax = self.figure.add_subplot(111)
|
||
|
||
try:
|
||
import matplotlib.image as mpimg
|
||
img = mpimg.imread(str(chart_file))
|
||
ax.imshow(img)
|
||
ax.axis('off')
|
||
ax.set_title(chart_file.name, fontsize=12, pad=10)
|
||
self.figure.tight_layout()
|
||
self.canvas.draw()
|
||
except Exception as e:
|
||
ax.text(0.5, 0.5, f'加载图片失败:\n{str(e)}',
|
||
ha='center', va='center', transform=ax.transAxes)
|
||
self.canvas.draw()
|
||
|
||
# 更新按钮状态
|
||
self.prev_btn.setEnabled(index > 0)
|
||
self.next_btn.setEnabled(index < len(self.chart_files) - 1)
|
||
|
||
def prev_chart(self):
|
||
"""上一个图表"""
|
||
if self.current_index > 0:
|
||
self.show_chart(self.current_index - 1)
|
||
|
||
def next_chart(self):
|
||
"""下一个图表"""
|
||
if self.current_index < len(self.chart_files) - 1:
|
||
self.show_chart(self.current_index + 1)
|
||
|
||
def save_current_chart(self):
|
||
"""保存当前图表"""
|
||
if 0 <= self.current_index < len(self.chart_files):
|
||
current_file = self.chart_files[self.current_index]
|
||
file_path, _ = QFileDialog.getSaveFileName(
|
||
self, "保存图表", current_file.name,
|
||
"PNG图片 (*.png);;JPG图片 (*.jpg);;所有文件 (*.*)"
|
||
)
|
||
if file_path:
|
||
try:
|
||
import shutil
|
||
shutil.copy(str(current_file), file_path)
|
||
QMessageBox.information(self, "成功", f"图表已保存到:\n{file_path}")
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "错误", f"保存失败:\n{str(e)}")
|
||
|
||
|
||
class Step6_5Panel(QWidget):
|
||
"""步骤6.5:非经验统计回归建模"""
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.init_ui()
|
||
|
||
def init_ui(self):
|
||
layout = QVBoxLayout()
|
||
|
||
# 标题
|
||
|
||
|
||
# 训练数据文件(用于独立运行)
|
||
self.training_csv_file = FileSelectWidget(
|
||
"训练数据CSV:",
|
||
"CSV Files (*.csv);;All Files (*.*)"
|
||
)
|
||
layout.addWidget(self.training_csv_file)
|
||
|
||
# 参数设置
|
||
params_group = QGroupBox("模型参数")
|
||
params_layout = QFormLayout()
|
||
|
||
# 预处理方法
|
||
self.preproc_checkboxes = {}
|
||
preproc_group = QGroupBox("预处理方法 (可多选)")
|
||
preproc_layout = QVBoxLayout()
|
||
preproc_grid = QGridLayout()
|
||
preproc_methods = ['None', 'MMS', 'SS', 'SNV', 'MA', 'SG', 'MSC', 'D1', 'D2', 'DT', 'CT']
|
||
|
||
for i, method in enumerate(preproc_methods):
|
||
checkbox = QCheckBox(method)
|
||
checkbox.setChecked(True)
|
||
self.preproc_checkboxes[method] = checkbox
|
||
preproc_grid.addWidget(checkbox, i // 4, i % 4)
|
||
|
||
button_layout = QHBoxLayout()
|
||
select_all_btn = QPushButton("全选")
|
||
deselect_all_btn = QPushButton("全不选")
|
||
select_all_btn.clicked.connect(lambda: self._toggle_checkboxes(self.preproc_checkboxes, True))
|
||
deselect_all_btn.clicked.connect(lambda: self._toggle_checkboxes(self.preproc_checkboxes, False))
|
||
button_layout.addWidget(select_all_btn)
|
||
button_layout.addWidget(deselect_all_btn)
|
||
button_layout.addStretch()
|
||
|
||
preproc_layout.addLayout(preproc_grid)
|
||
preproc_layout.addLayout(button_layout)
|
||
preproc_group.setLayout(preproc_layout)
|
||
params_layout.addRow(preproc_group)
|
||
|
||
# 算法选择(可多选)
|
||
self.algorithm_inputs = {}
|
||
algorithms_widget = QWidget()
|
||
algorithms_layout = QVBoxLayout()
|
||
algorithms_layout.setContentsMargins(0, 0, 0, 0)
|
||
algorithms_layout.setSpacing(4)
|
||
|
||
algorithm_list = ['chl_a', 'nh3', 'mno4', 'tn', 'tp', 'tss']
|
||
for algorithm in algorithm_list:
|
||
row_widget = QWidget()
|
||
row_layout = QHBoxLayout()
|
||
row_layout.setContentsMargins(0, 0, 0, 0)
|
||
checkbox = QCheckBox(algorithm)
|
||
checkbox.setChecked(True)
|
||
spinbox = QSpinBox()
|
||
spinbox.setRange(0, 500)
|
||
spinbox.setValue(0)
|
||
spinbox.setMaximumWidth(90)
|
||
row_layout.addWidget(checkbox)
|
||
row_layout.addWidget(QLabel("对应值列索引:"))
|
||
row_layout.addWidget(spinbox)
|
||
row_layout.addStretch()
|
||
row_widget.setLayout(row_layout)
|
||
algorithms_layout.addWidget(row_widget)
|
||
self.algorithm_inputs[algorithm] = (checkbox, spinbox)
|
||
|
||
algorithms_widget.setLayout(algorithms_layout)
|
||
params_layout.addRow("非经验算法选择:", algorithms_widget)
|
||
|
||
# 光谱起始列
|
||
self.spectral_start_col = QSpinBox()
|
||
self.spectral_start_col.setRange(0, 100)
|
||
self.spectral_start_col.setValue(1)
|
||
params_layout.addRow("光谱起始列索引:", self.spectral_start_col)
|
||
|
||
# 窗口大小
|
||
self.window = QSpinBox()
|
||
self.window.setRange(1, 20)
|
||
self.window.setValue(5)
|
||
params_layout.addRow("窗口大小:", self.window)
|
||
|
||
params_group.setLayout(params_layout)
|
||
layout.addWidget(params_group)
|
||
|
||
# 输出文件路径
|
||
self.output_dir = FileSelectWidget(
|
||
"输出模型目录:",
|
||
"Directories;;All Files (*.*)"
|
||
)
|
||
self.output_dir.line_edit.setPlaceholderText("8_non_empirical_models")
|
||
# 修改浏览按钮为选择目录
|
||
self.output_dir.browse_btn.clicked.disconnect()
|
||
self.output_dir.browse_btn.clicked.connect(self.browse_output_dir)
|
||
layout.addWidget(self.output_dir)
|
||
|
||
# 启用步骤
|
||
self.enable_checkbox = QCheckBox("启用此步骤")
|
||
self.enable_checkbox.setChecked(True)
|
||
layout.addWidget(self.enable_checkbox)
|
||
|
||
# 独立运行按钮
|
||
self.run_button = QPushButton("独立运行此步骤")
|
||
self.run_button.setStyleSheet("""
|
||
QPushButton {
|
||
background-color: #4CAF50;
|
||
color: white;
|
||
padding: 8px 16px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-weight: bold;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: #45a049;
|
||
}
|
||
QPushButton:pressed {
|
||
background-color: #3e8e41;
|
||
}
|
||
""")
|
||
self.run_button.clicked.connect(self.run_step)
|
||
layout.addWidget(self.run_button)
|
||
|
||
layout.addStretch()
|
||
self.setLayout(layout)
|
||
|
||
def get_config(self):
|
||
"""获取配置"""
|
||
selected_algorithms = [
|
||
name for name, (checkbox, _) in self.algorithm_inputs.items()
|
||
if checkbox.isChecked()
|
||
]
|
||
if not selected_algorithms:
|
||
selected_algorithms = list(self.algorithm_inputs.keys())
|
||
|
||
value_cols = {
|
||
name: spinbox.value()
|
||
for name, (_, spinbox) in self.algorithm_inputs.items()
|
||
if name in selected_algorithms
|
||
}
|
||
|
||
preprocessing_methods = [
|
||
method for method, checkbox in self.preproc_checkboxes.items()
|
||
if checkbox.isChecked()
|
||
] or ['None']
|
||
|
||
config = {
|
||
'preprocessing_methods': preprocessing_methods,
|
||
'algorithms': selected_algorithms,
|
||
'value_cols': value_cols,
|
||
'spectral_start_col': self.spectral_start_col.value(),
|
||
'window': self.window.value(),
|
||
'enabled': self.enable_checkbox.isChecked()
|
||
}
|
||
|
||
# 添加输出路径 - 使用更简洁的方式,参照其他步骤
|
||
output_dir = self.output_dir.get_path()
|
||
if not output_dir:
|
||
# 如果output_dir为空,使用工作目录或当前目录
|
||
main_window = self.parent().window()
|
||
if hasattr(main_window, 'work_dir') and main_window.work_dir:
|
||
output_dir = str(Path(main_window.work_dir) / "8_non_empirical_models")
|
||
else:
|
||
output_dir = str(Path.cwd() / "8_non_empirical_models")
|
||
config['output_dir'] = output_dir
|
||
|
||
# 添加训练数据路径(用于独立运行)
|
||
training_csv_path = self.training_csv_file.get_path()
|
||
if training_csv_path:
|
||
config['csv_path'] = training_csv_path
|
||
|
||
return config
|
||
|
||
def set_config(self, config):
|
||
"""设置配置"""
|
||
if 'preprocessing_methods' in config:
|
||
methods = config['preprocessing_methods']
|
||
for method, checkbox in self.preproc_checkboxes.items():
|
||
checkbox.setChecked(method in methods)
|
||
|
||
if 'algorithms' in config:
|
||
algorithm_values = config['algorithms']
|
||
for algorithm, (checkbox, spinbox) in self.algorithm_inputs.items():
|
||
checkbox.setChecked(algorithm in algorithm_values)
|
||
|
||
if 'value_cols' in config:
|
||
value_cols = config['value_cols']
|
||
if isinstance(value_cols, dict):
|
||
for algorithm, (_, spinbox) in self.algorithm_inputs.items():
|
||
if algorithm in value_cols:
|
||
spinbox.setValue(value_cols[algorithm])
|
||
else:
|
||
for _, spinbox in self.algorithm_inputs.values():
|
||
spinbox.setValue(value_cols)
|
||
|
||
if 'spectral_start_col' in config:
|
||
self.spectral_start_col.setValue(config['spectral_start_col'])
|
||
|
||
if 'window' in config:
|
||
self.window.setValue(config['window'])
|
||
if 'output_dir' in config:
|
||
self.output_dir.set_path(config['output_dir'])
|
||
|
||
# 添加训练数据路径设置
|
||
if 'csv_path' in config:
|
||
self.training_csv_file.set_path(config['csv_path'])
|
||
|
||
def browse_output_dir(self):
|
||
"""浏览输出目录"""
|
||
dir_path = QFileDialog.getExistingDirectory(self, "选择输出模型目录", "")
|
||
if dir_path:
|
||
self.output_dir.set_path(dir_path)
|
||
|
||
def run_step(self):
|
||
"""独立运行步骤6.5"""
|
||
# 验证输入
|
||
training_csv_path = self.training_csv_file.get_path()
|
||
if not training_csv_path:
|
||
QMessageBox.warning(self, "输入错误", "请选择训练数据CSV文件!")
|
||
return
|
||
|
||
if not os.path.exists(training_csv_path):
|
||
QMessageBox.warning(self, "输入错误", "训练数据CSV文件不存在!")
|
||
return
|
||
|
||
# 获取配置
|
||
config = self.get_config()
|
||
|
||
# 调用GUI的run_single_step方法
|
||
parent = self.parent()
|
||
while parent and not hasattr(parent, 'run_single_step'):
|
||
parent = parent.parent()
|
||
|
||
if parent and hasattr(parent, 'run_single_step'):
|
||
parent.run_single_step('step6_5', {'step6_5': config})
|
||
else:
|
||
QMessageBox.critical(self, "错误", "无法找到父级GUI对象")
|
||
|
||
def _toggle_checkboxes(self, checkboxes_dict, checked):
|
||
"""统一设置预处理checkbox状态"""
|
||
for checkbox in checkboxes_dict.values():
|
||
checkbox.setChecked(checked)
|
||
|
||
|
||
class Step6_75Panel(QWidget):
|
||
"""步骤6.75:自定义回归分析"""
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.x_column_checkboxes: Dict[str, QCheckBox] = {}
|
||
self.y_column_checkboxes: Dict[str, QCheckBox] = {}
|
||
self.method_checkboxes: Dict[str, QCheckBox] = {}
|
||
self.csv_columns = []
|
||
self.init_ui()
|
||
|
||
def init_ui(self):
|
||
layout = QVBoxLayout()
|
||
|
||
|
||
|
||
hint = QLabel("指定自变量与因变量列,批量尝试不同回归方法")
|
||
hint.setStyleSheet("color: #666; font-size: 11px;")
|
||
layout.addWidget(hint)
|
||
|
||
# CSV文件选择
|
||
csv_group = QGroupBox("数据文件")
|
||
csv_layout = QVBoxLayout()
|
||
|
||
self.csv_file = FileSelectWidget(
|
||
"输入CSV文件:",
|
||
"CSV Files (*.csv);;All Files (*.*)"
|
||
)
|
||
self.csv_file.line_edit.textChanged.connect(self.on_csv_file_changed)
|
||
csv_layout.addWidget(self.csv_file)
|
||
|
||
self.refresh_btn = QPushButton("刷新列信息")
|
||
self.refresh_btn.clicked.connect(self.refresh_csv_columns)
|
||
csv_layout.addWidget(self.refresh_btn)
|
||
|
||
csv_group.setLayout(csv_layout)
|
||
layout.addWidget(csv_group)
|
||
|
||
# 自变量选择
|
||
x_group = QGroupBox("自变量列选择 (可多选)")
|
||
x_layout = QVBoxLayout()
|
||
|
||
# 创建滚动区域来容纳自变量选择
|
||
x_scroll = QScrollArea()
|
||
x_scroll.setWidgetResizable(True)
|
||
x_scroll.setMaximumHeight(200)
|
||
|
||
x_widget = QWidget()
|
||
self.x_columns_layout = QGridLayout()
|
||
x_widget.setLayout(self.x_columns_layout)
|
||
|
||
x_scroll.setWidget(x_widget)
|
||
x_layout.addWidget(x_scroll)
|
||
|
||
# 全选/反选按钮
|
||
x_btn_layout = QHBoxLayout()
|
||
self.x_select_all = QPushButton("全选")
|
||
self.x_deselect_all = QPushButton("全不选")
|
||
self.x_select_all.clicked.connect(lambda: self.toggle_checkboxes(self.x_column_checkboxes, True))
|
||
self.x_deselect_all.clicked.connect(lambda: self.toggle_checkboxes(self.x_column_checkboxes, False))
|
||
x_btn_layout.addWidget(self.x_select_all)
|
||
x_btn_layout.addWidget(self.x_deselect_all)
|
||
x_btn_layout.addStretch()
|
||
x_layout.addLayout(x_btn_layout)
|
||
|
||
x_group.setLayout(x_layout)
|
||
layout.addWidget(x_group)
|
||
|
||
# 因变量选择
|
||
y_group = QGroupBox("因变量列选择 (可多选)")
|
||
y_layout = QVBoxLayout()
|
||
|
||
# 创建滚动区域来容纳因变量选择
|
||
y_scroll = QScrollArea()
|
||
y_scroll.setWidgetResizable(True)
|
||
y_scroll.setMaximumHeight(150)
|
||
|
||
y_widget = QWidget()
|
||
self.y_columns_layout = QGridLayout()
|
||
y_widget.setLayout(self.y_columns_layout)
|
||
|
||
y_scroll.setWidget(y_widget)
|
||
y_layout.addWidget(y_scroll)
|
||
|
||
# 全选/反选按钮
|
||
y_btn_layout = QHBoxLayout()
|
||
self.y_select_all = QPushButton("全选")
|
||
self.y_deselect_all = QPushButton("全不选")
|
||
self.y_select_all.clicked.connect(lambda: self.toggle_checkboxes(self.y_column_checkboxes, True))
|
||
self.y_deselect_all.clicked.connect(lambda: self.toggle_checkboxes(self.y_column_checkboxes, False))
|
||
y_btn_layout.addWidget(self.y_select_all)
|
||
y_btn_layout.addWidget(self.y_deselect_all)
|
||
y_btn_layout.addStretch()
|
||
y_layout.addLayout(y_btn_layout)
|
||
|
||
y_group.setLayout(y_layout)
|
||
layout.addWidget(y_group)
|
||
|
||
# 回归方法选择
|
||
method_group = QGroupBox("回归方法选择 (可多选)")
|
||
method_layout = QVBoxLayout()
|
||
|
||
method_grid = QGridLayout()
|
||
regression_methods = [
|
||
'linear', 'exponential', 'power', 'logarithmic',
|
||
'polynomial', 'hyperbolic', 'sigmoidal'
|
||
]
|
||
|
||
for i, method in enumerate(regression_methods):
|
||
checkbox = QCheckBox(method)
|
||
# 默认选择常用的方法
|
||
if method in ['linear', 'exponential', 'power', 'logarithmic']:
|
||
checkbox.setChecked(True)
|
||
self.method_checkboxes[method] = checkbox
|
||
method_grid.addWidget(checkbox, i // 3, i % 3)
|
||
|
||
method_layout.addLayout(method_grid)
|
||
|
||
# 方法全选/反选按钮
|
||
method_btn_layout = QHBoxLayout()
|
||
self.method_select_all = QPushButton("全选")
|
||
self.method_deselect_all = QPushButton("全不选")
|
||
self.method_select_all.clicked.connect(lambda: self.toggle_checkboxes(self.method_checkboxes, True))
|
||
self.method_deselect_all.clicked.connect(lambda: self.toggle_checkboxes(self.method_checkboxes, False))
|
||
method_btn_layout.addWidget(self.method_select_all)
|
||
method_btn_layout.addWidget(self.method_deselect_all)
|
||
method_btn_layout.addStretch()
|
||
method_layout.addLayout(method_btn_layout)
|
||
|
||
method_group.setLayout(method_layout)
|
||
layout.addWidget(method_group)
|
||
|
||
# 输出目录
|
||
output_group = QGroupBox("输出设置")
|
||
output_layout = QFormLayout()
|
||
|
||
self.output_dir = QLineEdit()
|
||
self.output_dir.setText("9_custom_regression")
|
||
output_layout.addRow("输出目录名:", self.output_dir)
|
||
|
||
output_group.setLayout(output_layout)
|
||
layout.addWidget(output_group)
|
||
|
||
# 启用步骤
|
||
self.enable_checkbox = QCheckBox("启用此步骤")
|
||
self.enable_checkbox.setChecked(True)
|
||
layout.addWidget(self.enable_checkbox)
|
||
|
||
# 独立运行按钮
|
||
self.run_button = QPushButton("独立运行此步骤")
|
||
self.run_button.setStyleSheet("""
|
||
QPushButton {
|
||
background-color: #4CAF50;
|
||
color: white;
|
||
padding: 8px 16px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
font-weight: bold;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: #45a049;
|
||
}
|
||
QPushButton:pressed {
|
||
background-color: #3e8e41;
|
||
}
|
||
""")
|
||
self.run_button.clicked.connect(self.run_step)
|
||
layout.addWidget(self.run_button)
|
||
|
||
layout.addStretch()
|
||
self.setLayout(layout)
|
||
|
||
def toggle_checkboxes(self, checkboxes_dict, checked):
|
||
"""统一设置checkbox状态"""
|
||
for checkbox in checkboxes_dict.values():
|
||
checkbox.setChecked(checked)
|
||
|
||
def on_csv_file_changed(self):
|
||
"""CSV文件改变时自动刷新列信息"""
|
||
self.refresh_csv_columns()
|
||
|
||
def refresh_csv_columns(self):
|
||
"""刷新CSV文件的列信息"""
|
||
csv_path = self.csv_file.get_path()
|
||
if not csv_path or not os.path.exists(csv_path):
|
||
self.csv_columns = []
|
||
self.update_column_widgets()
|
||
return
|
||
|
||
try:
|
||
# 读取CSV文件的第一行作为列名
|
||
df = pd.read_csv(csv_path, nrows=0)
|
||
self.csv_columns = list(df.columns)
|
||
self.update_column_widgets()
|
||
except Exception as e:
|
||
self.csv_columns = []
|
||
self.update_column_widgets()
|
||
print(f"读取CSV列信息失败: {e}")
|
||
|
||
def update_column_widgets(self):
|
||
"""更新列选择组件"""
|
||
# 清空现有的自变量checkbox
|
||
for checkbox in self.x_column_checkboxes.values():
|
||
checkbox.setParent(None)
|
||
self.x_column_checkboxes.clear()
|
||
|
||
# 清空现有的因变量checkbox
|
||
for checkbox in self.y_column_checkboxes.values():
|
||
checkbox.setParent(None)
|
||
self.y_column_checkboxes.clear()
|
||
|
||
if not self.csv_columns:
|
||
return
|
||
|
||
# 添加自变量checkbox(三列排列)
|
||
for i, col in enumerate(self.csv_columns):
|
||
checkbox = QCheckBox(col)
|
||
# 默认选择一些常见的指数列
|
||
if any(keyword in col.lower() for keyword in ['index', 'ratio', 'normalized', 'nd', 'b']):
|
||
checkbox.setChecked(True)
|
||
self.x_column_checkboxes[col] = checkbox
|
||
self.x_columns_layout.addWidget(checkbox, i // 3, i % 3)
|
||
|
||
# 添加因变量checkbox(两列排列)
|
||
for i, col in enumerate(self.csv_columns):
|
||
checkbox = QCheckBox(col)
|
||
# 默认选择一些常见的水质参数列
|
||
if any(keyword in col.lower() for keyword in ['chl', 'tn', 'tp', 'turbidity', 'do', 'ph', 'conductivity']):
|
||
checkbox.setChecked(True)
|
||
self.y_column_checkboxes[col] = checkbox
|
||
self.y_columns_layout.addWidget(checkbox, i // 2, i % 2)
|
||
|
||
# 重新布局
|
||
self.x_columns_layout.update()
|
||
self.y_columns_layout.update()
|
||
|
||
def get_config(self):
|
||
# 获取选中的自变量列
|
||
selected_x_columns = [
|
||
col for col, checkbox in self.x_column_checkboxes.items()
|
||
if checkbox.isChecked()
|
||
]
|
||
|
||
# 获取选中的因变量列
|
||
selected_y_columns = [
|
||
col for col, checkbox in self.y_column_checkboxes.items()
|
||
if checkbox.isChecked()
|
||
]
|
||
|
||
# 获取选中的回归方法
|
||
selected_methods = [
|
||
method for method, checkbox in self.method_checkboxes.items()
|
||
if checkbox.isChecked()
|
||
]
|
||
if not selected_methods:
|
||
selected_methods = 'all'
|
||
|
||
return {
|
||
'csv_path': self.csv_file.get_path() or None,
|
||
'x_columns': selected_x_columns,
|
||
'y_columns': selected_y_columns,
|
||
'methods': selected_methods,
|
||
'output_dir': self.output_dir.text().strip() or None,
|
||
'enabled': self.enable_checkbox.isChecked()
|
||
}
|
||
|
||
def set_config(self, config):
|
||
if 'csv_path' in config:
|
||
self.csv_file.set_path(config['csv_path'])
|
||
# 设置CSV路径后自动刷新列信息
|
||
self.refresh_csv_columns()
|
||
|
||
if 'x_columns' in config:
|
||
selected_x = set(config['x_columns']) if isinstance(config['x_columns'], list) else set()
|
||
for col, checkbox in self.x_column_checkboxes.items():
|
||
checkbox.setChecked(col in selected_x)
|
||
|
||
if 'y_columns' in config:
|
||
selected_y = set(config['y_columns']) if isinstance(config['y_columns'], list) else set()
|
||
for col, checkbox in self.y_column_checkboxes.items():
|
||
checkbox.setChecked(col in selected_y)
|
||
|
||
if 'methods' in config:
|
||
methods = config['methods']
|
||
if isinstance(methods, list):
|
||
selected_methods = set(methods)
|
||
elif methods == 'all':
|
||
selected_methods = set(self.method_checkboxes.keys())
|
||
else:
|
||
selected_methods = set()
|
||
|
||
for method, checkbox in self.method_checkboxes.items():
|
||
checkbox.setChecked(method in selected_methods)
|
||
|
||
if 'output_dir' in config:
|
||
self.output_dir.setText(config['output_dir'] or "9_custom_regression")
|
||
if 'enabled' in config:
|
||
self.enable_checkbox.setChecked(config['enabled'])
|
||
|
||
def run_step(self):
|
||
"""独立运行步骤6.75"""
|
||
# 验证输入
|
||
csv_path = self.csv_file.get_path()
|
||
|
||
if not csv_path:
|
||
QMessageBox.warning(self, "输入验证失败", "请选择输入CSV文件")
|
||
return
|
||
if not os.path.exists(csv_path):
|
||
QMessageBox.warning(self, "输入验证失败", "输入CSV文件不存在")
|
||
return
|
||
|
||
# 检查是否有选中的自变量
|
||
selected_x_columns = [
|
||
col for col, checkbox in self.x_column_checkboxes.items()
|
||
if checkbox.isChecked()
|
||
]
|
||
if not selected_x_columns:
|
||
QMessageBox.warning(self, "输入验证失败", "请至少选择一个自变量列")
|
||
return
|
||
|
||
# 检查是否有选中的因变量
|
||
selected_y_columns = [
|
||
col for col, checkbox in self.y_column_checkboxes.items()
|
||
if checkbox.isChecked()
|
||
]
|
||
if not selected_y_columns:
|
||
QMessageBox.warning(self, "输入验证失败", "请至少选择一个因变量列")
|
||
return
|
||
|
||
# 检查是否有选中的回归方法
|
||
selected_methods = [
|
||
method for method, checkbox in self.method_checkboxes.items()
|
||
if checkbox.isChecked()
|
||
]
|
||
if not selected_methods:
|
||
QMessageBox.warning(self, "输入验证失败", "请至少选择一种回归方法")
|
||
return
|
||
|
||
# 获取配置
|
||
config = self.get_config()
|
||
|
||
# 调用GUI的run_single_step方法
|
||
parent = self.parent()
|
||
while parent and not hasattr(parent, 'run_single_step'):
|
||
parent = parent.parent()
|
||
|
||
if parent and hasattr(parent, 'run_single_step'):
|
||
parent.run_single_step('step6_75', {'step6_75': config})
|
||
else:
|
||
QMessageBox.critical(self, "错误", "无法找到父级GUI对象")
|
||
|
||
|
||
class WaterQualityGUI(QMainWindow):
|
||
"""水质参数反演分析系统主窗口"""
|
||
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.pipeline = None
|
||
self.worker = None
|
||
self.config_file = None
|
||
|
||
# 训练数据模式状态
|
||
self.has_training_data = True # 默认有训练数据
|
||
|
||
self.init_ui()
|
||
self.apply_stylesheet()
|
||
self._disable_wheel_for_all_spinboxes()
|
||
|
||
def get_icon_path(self, icon_filename):
|
||
"""
|
||
获取图标文件的完整路径
|
||
在开发环境中从../data/icons/获取,在打包后从data/icons/获取
|
||
"""
|
||
if hasattr(sys, '_MEIPASS'):
|
||
# 打包后的环境
|
||
icon_dir = os.path.join(sys._MEIPASS, 'data', 'icons')
|
||
else:
|
||
# 开发环境
|
||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||
icon_dir = os.path.join(current_dir, '..', '..', 'data', 'icons')
|
||
|
||
return os.path.join(icon_dir, icon_filename)
|
||
|
||
def _disable_wheel_for_all_spinboxes(self):
|
||
"""
|
||
遍历所有子控件,为 QSpinBox/QDoubleSpinBox/QComboBox 禁用滚轮事件
|
||
防止滚动页面时意外改变数值
|
||
"""
|
||
from PyQt5.QtCore import Qt
|
||
|
||
# 找到所有数值输入控件
|
||
for spinbox in self.findChildren(QSpinBox):
|
||
spinbox.setFocusPolicy(Qt.StrongFocus) # 只有聚焦时才响应滚轮
|
||
spinbox.wheelEvent = lambda event, sb=spinbox: None # 完全禁用滚轮
|
||
|
||
for spinbox in self.findChildren(QDoubleSpinBox):
|
||
spinbox.setFocusPolicy(Qt.StrongFocus)
|
||
spinbox.wheelEvent = lambda event, sb=spinbox: None
|
||
|
||
for combobox in self.findChildren(QComboBox):
|
||
combobox.setFocusPolicy(Qt.StrongFocus)
|
||
combobox.wheelEvent = lambda event, cb=combobox: None
|
||
|
||
def init_ui(self):
|
||
"""初始化UI"""
|
||
self.setWindowTitle("水质参数反演分析系统 v1.0")
|
||
|
||
# 获取屏幕可用区域(排除任务栏)
|
||
screen_geometry = QApplication.primaryScreen().availableGeometry()
|
||
screen_width = screen_geometry.width()
|
||
screen_height = screen_geometry.height()
|
||
|
||
# 初始尺寸:宽度固定 800,高度占满屏幕
|
||
window_width = 1200
|
||
window_height = screen_height
|
||
# 仅设置初始大小,不锁定
|
||
self.resize(window_width, window_height)
|
||
|
||
# 计算水平居中、垂直贴顶的位置
|
||
x = (screen_width - window_width) // 2
|
||
y = 0
|
||
self.move(x, y)
|
||
|
||
# 可选:设置最小尺寸,防止用户缩得太小
|
||
self.setMinimumSize(600, 400)
|
||
|
||
# 创建自定义标题栏(包含Logo和菜单栏)
|
||
self.create_title_bar()
|
||
|
||
# 创建横幅区域
|
||
self.create_banner_widget()
|
||
|
||
# 创建中央部件
|
||
central_widget = QWidget()
|
||
self.setCentralWidget(central_widget)
|
||
|
||
# 主布局
|
||
main_layout = QHBoxLayout()
|
||
|
||
# 创建左侧导航栏
|
||
self.create_navigation()
|
||
main_layout.addWidget(self.nav_widget, 1)
|
||
|
||
# 创建右侧内容区
|
||
self.create_content_area()
|
||
main_layout.addWidget(self.content_widget, 4)
|
||
|
||
central_widget.setLayout(main_layout)
|
||
|
||
# 创建状态栏
|
||
self.statusBar().showMessage("就绪")
|
||
|
||
def create_title_bar(self):
|
||
"""创建自定义标题栏(Logo和菜单栏挨着且同等宽度)"""
|
||
# 创建标题栏容器
|
||
title_widget = QWidget()
|
||
title_layout = QHBoxLayout()
|
||
title_layout.setContentsMargins(8, 4, 8, 4)
|
||
title_layout.setSpacing(0) # 让Logo和菜单栏紧挨着
|
||
|
||
# Logo部分(左侧,增加宽度)
|
||
logo_label = QLabel()
|
||
logo_label.setFixedSize(180, 48) # 增加Logo宽度,使其和菜单栏视觉平衡
|
||
logo_label.setAlignment(Qt.AlignCenter)
|
||
logo_label.setStyleSheet("""
|
||
QLabel {
|
||
background-color: #f8f9fa;
|
||
border-top-left-radius: 4px;
|
||
border-bottom-left-radius: 4px;
|
||
}
|
||
""")
|
||
|
||
# 设置Logo图片路径 - 使用相对路径(打包兼容)
|
||
from pathlib import Path
|
||
if hasattr(sys, '_MEIPASS'):
|
||
logo_path = os.path.join(sys._MEIPASS, 'data', 'icons', 'logo.png')
|
||
else:
|
||
logo_path = str(Path(__file__).parent.parent.parent / "data" / "icons" / "logo.png")
|
||
logo_pixmap = QPixmap(logo_path)
|
||
|
||
if not logo_pixmap.isNull():
|
||
# 按高度缩放图片,保持宽高比,让Logo更显眼
|
||
scaled_pixmap = logo_pixmap.scaledToHeight(38, Qt.SmoothTransformation)
|
||
logo_label.setPixmap(scaled_pixmap)
|
||
else:
|
||
# 如果图片加载失败,显示占位符
|
||
logo_label.setText("Logo")
|
||
logo_label.setStyleSheet("""
|
||
QLabel {
|
||
background-color: #f8f9fa;
|
||
color: #333;
|
||
font-size: 14px;
|
||
font-weight: bold;
|
||
border-top-left-radius: 4px;
|
||
border-bottom-left-radius: 4px;
|
||
}
|
||
""")
|
||
|
||
title_layout.addWidget(logo_label)
|
||
|
||
# 菜单栏(紧挨着Logo右侧)
|
||
menubar = self.menuBar()
|
||
menubar.setStyleSheet("""
|
||
QMenuBar {
|
||
background-color: #f8f9fa;
|
||
border: none;
|
||
padding: 4px 8px;
|
||
border-top-right-radius: 4px;
|
||
border-bottom-right-radius: 4px;
|
||
}
|
||
QMenuBar::item {
|
||
padding: 6px 12px;
|
||
background-color: transparent;
|
||
font-size: 13px;
|
||
}
|
||
QMenuBar::item:selected {
|
||
background-color: #e6f0ff;
|
||
border-radius: 3px;
|
||
}
|
||
""")
|
||
|
||
# 文件菜单
|
||
file_menu = menubar.addMenu("文件")
|
||
|
||
new_action = file_menu.addAction("新建配置")
|
||
new_action.triggered.connect(self.new_config)
|
||
|
||
open_action = file_menu.addAction("打开配置")
|
||
open_action.triggered.connect(self.load_config_dialog)
|
||
|
||
save_action = file_menu.addAction("保存配置")
|
||
save_action.triggered.connect(self.save_config_dialog)
|
||
|
||
file_menu.addSeparator()
|
||
|
||
exit_action = file_menu.addAction("退出")
|
||
exit_action.triggered.connect(self.close)
|
||
|
||
# 工具菜单
|
||
tools_menu = menubar.addMenu("工具")
|
||
|
||
work_dir_action = tools_menu.addAction("设置工作目录")
|
||
work_dir_action.triggered.connect(self.set_work_directory)
|
||
|
||
open_dir_action = tools_menu.addAction("打开工作目录")
|
||
open_dir_action.triggered.connect(self.open_work_directory)
|
||
|
||
# 在工具菜单中添加训练数据模式切换按钮
|
||
self.training_mode_action = tools_menu.addAction("有训练数据模式")
|
||
self.training_mode_action.setCheckable(True)
|
||
self.training_mode_action.setChecked(True) # 默认有训练数据模式
|
||
self.training_mode_action.triggered.connect(self.toggle_training_data_mode)
|
||
|
||
# 帮助菜单
|
||
help_menu = menubar.addMenu("帮助")
|
||
|
||
pipeline_status_action = help_menu.addAction("检查Pipeline状态")
|
||
pipeline_status_action.triggered.connect(self.show_pipeline_status)
|
||
|
||
help_menu.addSeparator()
|
||
|
||
about_action = help_menu.addAction("关于")
|
||
about_action.triggered.connect(self.show_about)
|
||
|
||
title_layout.addWidget(menubar)
|
||
title_widget.setLayout(title_layout)
|
||
|
||
# 设置整体标题栏样式
|
||
title_widget.setStyleSheet("""
|
||
QWidget {
|
||
background-color: #f8f9fa;
|
||
border-bottom: 1px solid #d0d0d0;
|
||
}
|
||
""")
|
||
|
||
# 将标题栏添加到窗口顶部
|
||
self.setMenuWidget(title_widget)
|
||
|
||
|
||
def create_banner_widget(self):
|
||
"""创建横幅区域 - 支持自适应等比缩放"""
|
||
# 创建横幅容器
|
||
banner_widget = QWidget()
|
||
banner_layout = QHBoxLayout()
|
||
banner_layout.setContentsMargins(0, 0, 0, 0)
|
||
banner_layout.setSpacing(0)
|
||
# 不设置居中对齐,让横幅填满整个容器
|
||
|
||
# 创建横幅标签 - 完全跟随窗口等比缩放,填满整个区域
|
||
self.banner_label = QLabel()
|
||
# 最小高度保证:当窗口很小时至少显示 38px 高 (200px 宽 / 5.25)
|
||
self.banner_label.setMinimumHeight(int(200 / 5.25)) # ≈ 38px
|
||
# 使用 Expanding 策略让标签填满可用空间
|
||
self.banner_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||
self.banner_label.setScaledContents(False)
|
||
# 清除 QLabel 默认的 margin 和 padding,消除右侧空白
|
||
self.banner_label.setStyleSheet("margin: 0px; padding: 0px; border: none;")
|
||
|
||
# 保存原始pixmap用于后续缩放
|
||
if hasattr(sys, '_MEIPASS'):
|
||
banner_path = os.path.join(sys._MEIPASS, 'data', 'icons', 'Mega Water 1.0.png')
|
||
else:
|
||
banner_path = str(Path(__file__).parent.parent.parent / "data" / "icons" / "Mega Water 1.0.png")
|
||
self.banner_pixmap = QPixmap(banner_path)
|
||
|
||
if not self.banner_pixmap.isNull():
|
||
# 延迟执行,确保窗口已初始化
|
||
QTimer.singleShot(50, self.update_banner_image)
|
||
else:
|
||
# 如果图片加载失败,显示占位符
|
||
self.banner_label.setText("水质参数反演分析系统")
|
||
self.banner_label.setStyleSheet("""
|
||
QLabel {
|
||
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
|
||
stop:0 #0078d4, stop:1 #00a0e9);
|
||
color: white;
|
||
font-size: 26px;
|
||
font-weight: bold;
|
||
border-bottom: 3px solid #005a9e;
|
||
}
|
||
""")
|
||
|
||
banner_layout.addWidget(self.banner_label)
|
||
banner_widget.setLayout(banner_layout)
|
||
|
||
# 将横幅添加到窗口顶部(在标题栏下方)
|
||
banner_toolbar = QToolBar()
|
||
banner_toolbar.setMovable(False)
|
||
banner_toolbar.setFloatable(False)
|
||
banner_toolbar.addWidget(banner_widget)
|
||
banner_toolbar.setContentsMargins(0, 0, 0, 0) # 清除工具栏布局的边距
|
||
banner_toolbar.setStyleSheet("""
|
||
QToolBar {
|
||
background-color: white;
|
||
border: none;
|
||
border-bottom: 1px solid #ddd;
|
||
padding: 0px;
|
||
margin: 0px;
|
||
spacing: 0px;
|
||
}
|
||
QToolBar QWidget {
|
||
margin: 0px;
|
||
padding: 0px;
|
||
}
|
||
""")
|
||
|
||
self.addToolBar(Qt.TopToolBarArea, banner_toolbar)
|
||
self.banner_widget = banner_toolbar
|
||
|
||
def create_navigation(self):
|
||
"""创建左侧导航栏"""
|
||
self.nav_widget = QWidget()
|
||
nav_layout = QVBoxLayout()
|
||
nav_layout.setContentsMargins(10, 15, 10, 15)
|
||
nav_layout.setSpacing(10)
|
||
|
||
# 标题
|
||
title = QLabel("流程步骤")
|
||
title.setFont(QFont("Arial", 13, QFont.Bold))
|
||
title.setAlignment(Qt.AlignCenter)
|
||
title.setStyleSheet(f"color: {ModernStylesheet.COLORS['text_primary']}; padding: 10px;")
|
||
nav_layout.addWidget(title)
|
||
|
||
# 步骤列表 - 分层结构
|
||
self.step_list = QListWidget()
|
||
self.step_list.setStyleSheet(ModernStylesheet.get_sidebar_stylesheet())
|
||
|
||
# 定义三阶段结构
|
||
self.process_stages = {
|
||
"阶段一:数据预处理": [
|
||
("step1", "1. 水域掩膜生成"),
|
||
("step2", "2. 耀斑区域识别"),
|
||
("step3", "3. 耀斑去除与修复"),
|
||
("step4", "4. 数据标准化处理"),
|
||
],
|
||
"阶段二:特征提取与建模": [
|
||
("step5", "5. 光谱特征提取"),
|
||
("step5_5", "6. 水质参数指数计算"),
|
||
("step6", "7. 监督学习模型训练"),
|
||
("step6_5", "8. 经验统计回归"),
|
||
("step6_75", "9. 自定义回归模型"),
|
||
],
|
||
"阶段三:应用与可视化": [
|
||
("step7", "10. 采样点布设"),
|
||
("step8", "11. 基于监督学习预测"),
|
||
("step8_5", "12. 基于统计回归预测"),
|
||
("step8_75", "13. 基于自定义回归预测"),
|
||
("step9", "14. 专题图生成"),
|
||
("step9_viz", "15. 可视化分析"),
|
||
("step_report", "16. 分析报告生成"),
|
||
]
|
||
}
|
||
|
||
# 存储步骤映射
|
||
self.step_name_map = {}
|
||
|
||
# 添加分组项到列表
|
||
for stage_idx, (stage_name, steps) in enumerate(self.process_stages.items()):
|
||
# 添加阶段标题项(可视化分组)
|
||
stage_item = QListWidgetItem(stage_name)
|
||
stage_font = QFont("Arial", 11, QFont.Bold)
|
||
stage_item.setFont(stage_font)
|
||
stage_item.setForeground(QColor(ModernStylesheet.COLORS.get('accent', '#0078D4')))
|
||
stage_item.setFlags(stage_item.flags() & ~Qt.ItemIsSelectable)
|
||
stage_item.setFlags(stage_item.flags() & ~Qt.ItemIsEnabled)
|
||
stage_item.setData(Qt.UserRole, "stage_header")
|
||
self.step_list.addItem(stage_item)
|
||
|
||
# 添加该阶段的所有步骤
|
||
for step_id, step_display in steps:
|
||
item = QListWidgetItem(f" └─ {step_display}")
|
||
item.setData(Qt.UserRole, step_id)
|
||
self.step_name_map[step_display] = step_id
|
||
|
||
# 设置步骤项的样式
|
||
step_font = QFont("Arial", 10)
|
||
item.setFont(step_font)
|
||
item.setForeground(QColor(ModernStylesheet.COLORS.get('text_secondary', '#666666')))
|
||
self.step_list.addItem(item)
|
||
|
||
# 在阶段间添加分隔符
|
||
if stage_idx < len(self.process_stages) - 1:
|
||
separator_item = QListWidgetItem("")
|
||
separator_item.setFlags(separator_item.flags() & ~Qt.ItemIsSelectable)
|
||
separator_item.setFlags(separator_item.flags() & ~Qt.ItemIsEnabled)
|
||
self.step_list.addItem(separator_item)
|
||
|
||
self.step_list.currentRowChanged.connect(self.on_step_changed)
|
||
nav_layout.addWidget(self.step_list)
|
||
|
||
# 控制按钮
|
||
btn_layout = QVBoxLayout()
|
||
btn_layout.setSpacing(8)
|
||
|
||
self.run_all_btn = QPushButton("> 运行完整流程")
|
||
self.run_all_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
|
||
self.run_all_btn.setMinimumHeight(35)
|
||
self.run_all_btn.clicked.connect(self.run_full_pipeline)
|
||
btn_layout.addWidget(self.run_all_btn)
|
||
|
||
self.stop_btn = QPushButton("⏹ 停止")
|
||
self.stop_btn.setEnabled(False)
|
||
self.stop_btn.setMinimumHeight(35)
|
||
self.stop_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('danger'))
|
||
self.stop_btn.clicked.connect(self.stop_pipeline)
|
||
btn_layout.addWidget(self.stop_btn)
|
||
|
||
nav_layout.addLayout(btn_layout)
|
||
|
||
self.nav_widget.setLayout(nav_layout)
|
||
self.nav_widget.setMaximumWidth(280)
|
||
self.nav_widget.setStyleSheet(f"background-color: {ModernStylesheet.COLORS['panel_bg']}; border-right: 1px solid {ModernStylesheet.COLORS['border_light']};")
|
||
|
||
|
||
def create_content_area(self):
|
||
"""创建右侧内容区"""
|
||
self.content_widget = QWidget()
|
||
self.content_widget.setStyleSheet(f"background-color: {ModernStylesheet.COLORS['main_bg']};")
|
||
content_layout = QVBoxLayout()
|
||
content_layout.setContentsMargins(15, 15, 15, 15)
|
||
content_layout.setSpacing(10)
|
||
|
||
# 创建步骤面板容器
|
||
self.step_stack = QTabWidget()
|
||
self.step_stack.setTabPosition(QTabWidget.North)
|
||
self.step_stack.setTabsClosable(False)
|
||
self.step_stack.setStyleSheet(ModernStylesheet.get_main_stylesheet())
|
||
|
||
# 添加各步骤面板
|
||
self.step1_panel = Step1Panel()
|
||
self.step_stack.addTab(self.create_scroll_area(self.step1_panel), QIcon(self.get_icon_path("1.png")), "水域掩膜")
|
||
|
||
self.step2_panel = Step2Panel()
|
||
self.step_stack.addTab(self.create_scroll_area(self.step2_panel), QIcon(self.get_icon_path("2.png")), "耀斑检测")
|
||
|
||
self.step3_panel = Step3Panel()
|
||
self.step_stack.addTab(self.create_scroll_area(self.step3_panel), QIcon(self.get_icon_path("3.png")), "耀斑去除")
|
||
|
||
self.step4_panel = Step4Panel()
|
||
self.step_stack.addTab(self.create_scroll_area(self.step4_panel), QIcon(self.get_icon_path("4.png")), "数据清洗")
|
||
|
||
self.step5_panel = Step5Panel()
|
||
self.step_stack.addTab(self.create_scroll_area(self.step5_panel), QIcon(self.get_icon_path("5.png")), "特征构建")
|
||
|
||
self.step5_5_panel = Step5_5Panel()
|
||
self.step_stack.addTab(self.create_scroll_area(self.step5_5_panel), QIcon(self.get_icon_path("5.png")), "水质指数")
|
||
|
||
self.step6_panel = Step6Panel()
|
||
self.step_stack.addTab(self.create_scroll_area(self.step6_panel), QIcon(self.get_icon_path("6.png")), "监督建模")
|
||
|
||
self.step6_5_panel = Step6_5Panel()
|
||
self.step_stack.addTab(self.create_scroll_area(self.step6_5_panel), QIcon(self.get_icon_path("6.png")), "回归建模")
|
||
|
||
self.step6_75_panel = Step6_75Panel()
|
||
self.step_stack.addTab(self.create_scroll_area(self.step6_75_panel), QIcon(self.get_icon_path("6.png")), "自定义回归建模")
|
||
|
||
self.step7_panel = Step7Panel()
|
||
self.step_stack.addTab(self.create_scroll_area(self.step7_panel), QIcon(self.get_icon_path("7.png")), "采样点布设")
|
||
|
||
self.step8_panel = Step8Panel()
|
||
self.step_stack.addTab(self.create_scroll_area(self.step8_panel), QIcon(self.get_icon_path("8.png")), "监督预测")
|
||
|
||
self.step8_5_panel = Step8_5Panel()
|
||
self.step_stack.addTab(self.create_scroll_area(self.step8_5_panel), QIcon(self.get_icon_path("8.png")), "回归预测")
|
||
|
||
self.step8_75_panel = Step8_75Panel()
|
||
self.step_stack.addTab(self.create_scroll_area(self.step8_75_panel), QIcon(self.get_icon_path("8.png")), "自定义回归预测")
|
||
|
||
self.step9_panel = Step9Panel()
|
||
self.step_stack.addTab(self.create_scroll_area(self.step9_panel), QIcon(self.get_icon_path("10.png")), "专题图生成")
|
||
|
||
self.viz_panel = VisualizationPanel()
|
||
self.step_stack.addTab(self.create_scroll_area(self.viz_panel), QIcon(self.get_icon_path("9.png")), "可视化")
|
||
|
||
self.report_panel = ReportGenerationPanel(main_window=self)
|
||
self.step_stack.addTab(self.create_scroll_area(self.report_panel), QIcon(self.get_icon_path("10.png")), "报告生成")
|
||
|
||
# 连接Tab切换信号,实现双向同步(必须在step_stack创建后)
|
||
self.step_stack.currentChanged.connect(self.on_tab_changed)
|
||
|
||
content_layout.addWidget(self.step_stack, 3)
|
||
|
||
# 日志区域
|
||
log_group = QGroupBox("执行日志")
|
||
log_group.setStyleSheet(f"""
|
||
QGroupBox {{
|
||
background-color: {ModernStylesheet.COLORS['panel_bg']};
|
||
border: 1px solid {ModernStylesheet.COLORS['border_light']};
|
||
border-radius: 5px;
|
||
margin-top: 8px;
|
||
padding-top: 15px;
|
||
padding-left: 9px;
|
||
padding-right: 9px;
|
||
padding-bottom: 9px;
|
||
}}
|
||
QGroupBox::title {{
|
||
subcontrol-origin: margin;
|
||
subcontrol-position: top left;
|
||
padding: 0 5px;
|
||
font-weight: bold;
|
||
color: {ModernStylesheet.COLORS['text_primary']};
|
||
}}
|
||
""")
|
||
log_layout = QVBoxLayout()
|
||
log_layout.setContentsMargins(5, 5, 5, 5)
|
||
|
||
self.log_text = QTextEdit()
|
||
self.log_text.setReadOnly(True)
|
||
self.log_text.setMaximumHeight(200)
|
||
self.log_text.setStyleSheet(f"""
|
||
QTextEdit {{
|
||
background-color: {ModernStylesheet.COLORS['panel_bg']};
|
||
color: {ModernStylesheet.COLORS['text_primary']};
|
||
border: 1px solid {ModernStylesheet.COLORS['border']};
|
||
border-radius: 4px;
|
||
padding: 5px;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 10px;
|
||
}}
|
||
""")
|
||
log_layout.addWidget(self.log_text)
|
||
|
||
log_btn_layout = QHBoxLayout()
|
||
clear_log_btn = QPushButton("清空日志")
|
||
clear_log_btn.setMaximumWidth(100)
|
||
clear_log_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('normal'))
|
||
clear_log_btn.clicked.connect(self.clear_log)
|
||
log_btn_layout.addWidget(clear_log_btn)
|
||
log_btn_layout.addStretch()
|
||
log_layout.addLayout(log_btn_layout)
|
||
|
||
log_group.setLayout(log_layout)
|
||
content_layout.addWidget(log_group, 1)
|
||
|
||
# 进度条
|
||
progress_group = QGroupBox("执行进度")
|
||
progress_group.setStyleSheet(f"""
|
||
QGroupBox {{
|
||
background-color: {ModernStylesheet.COLORS['panel_bg']};
|
||
border: 1px solid {ModernStylesheet.COLORS['border_light']};
|
||
border-radius: 5px;
|
||
margin-top: 8px;
|
||
padding-top: 10px;
|
||
padding-left: 9px;
|
||
padding-right: 9px;
|
||
padding-bottom: 9px;
|
||
}}
|
||
QGroupBox::title {{
|
||
subcontrol-origin: margin;
|
||
subcontrol-position: top left;
|
||
padding: 0 5px;
|
||
font-weight: bold;
|
||
color: {ModernStylesheet.COLORS['text_primary']};
|
||
}}
|
||
""")
|
||
progress_layout = QVBoxLayout()
|
||
progress_layout.setContentsMargins(5, 5, 5, 5)
|
||
|
||
self.progress_bar = QProgressBar()
|
||
self.progress_bar.setValue(0)
|
||
self.progress_bar.setStyleSheet(f"""
|
||
QProgressBar {{
|
||
background-color: {ModernStylesheet.COLORS['panel_bg']};
|
||
border: 1px solid {ModernStylesheet.COLORS['border']};
|
||
border-radius: 4px;
|
||
padding: 2px;
|
||
text-align: center;
|
||
height: 20px;
|
||
}}
|
||
QProgressBar::chunk {{
|
||
background-color: {ModernStylesheet.COLORS['success']};
|
||
border-radius: 3px;
|
||
}}
|
||
""")
|
||
progress_layout.addWidget(self.progress_bar)
|
||
progress_group.setLayout(progress_layout)
|
||
content_layout.addWidget(progress_group, 0)
|
||
|
||
self.content_widget.setLayout(content_layout)
|
||
|
||
# 初始化训练数据模式UI状态
|
||
self.update_ui_for_training_mode()
|
||
|
||
# 显示pipeline状态
|
||
self.show_pipeline_status_on_startup()
|
||
|
||
def create_scroll_area(self, widget):
|
||
"""创建滚动区域"""
|
||
scroll = QScrollArea()
|
||
scroll.setWidget(widget)
|
||
scroll.setWidgetResizable(True)
|
||
return scroll
|
||
|
||
def on_step_changed(self, index):
|
||
"""步骤切换 - 处理分层列表结构"""
|
||
if index < 0:
|
||
return
|
||
|
||
# 获取选中项
|
||
item = self.step_list.item(index)
|
||
if not item:
|
||
return
|
||
|
||
# 检查是否是可选中的步骤项
|
||
item_data = item.data(Qt.UserRole)
|
||
if item_data == "stage_header" or item_data is None:
|
||
# 是阶段标题或分隔符,不切换
|
||
return
|
||
|
||
# 根据步骤ID查找对应的tab索引
|
||
step_id_to_tab = {
|
||
'step1': 0,
|
||
'step2': 1,
|
||
'step3': 2,
|
||
'step4': 3,
|
||
'step5': 4,
|
||
'step5_5': 5,
|
||
'step6': 6,
|
||
'step6_5': 7,
|
||
'step6_75': 8,
|
||
'step7': 9,
|
||
'step8': 10,
|
||
'step8_5': 11,
|
||
'step8_75': 12,
|
||
'step9': 13,
|
||
'step9_viz': 14,
|
||
'step_report': 15,
|
||
}
|
||
|
||
if item_data in step_id_to_tab:
|
||
tab_index = step_id_to_tab[item_data]
|
||
self.step_stack.setCurrentIndex(tab_index)
|
||
|
||
def on_tab_changed(self, index):
|
||
"""Tab页面切换时同步更新左侧步骤列表"""
|
||
if index < 0:
|
||
return
|
||
|
||
# Tab索引到步骤ID的反向映射
|
||
tab_to_step_id = {
|
||
0: 'step1',
|
||
1: 'step2',
|
||
2: 'step3',
|
||
3: 'step4',
|
||
4: 'step5',
|
||
5: 'step5_5',
|
||
6: 'step6',
|
||
7: 'step6_5',
|
||
8: 'step6_75',
|
||
9: 'step7',
|
||
10: 'step8',
|
||
11: 'step8_5',
|
||
12: 'step8_75',
|
||
13: 'step9',
|
||
14: 'step9_viz',
|
||
15: 'step_report',
|
||
}
|
||
|
||
if index not in tab_to_step_id:
|
||
return
|
||
|
||
target_step_id = tab_to_step_id[index]
|
||
|
||
# 在step_list中查找对应的步骤项
|
||
for row in range(self.step_list.count()):
|
||
item = self.step_list.item(row)
|
||
if not item:
|
||
continue
|
||
|
||
item_data = item.data(Qt.UserRole)
|
||
if item_data == target_step_id:
|
||
# 找到对应的步骤项,设置为当前选中
|
||
self.step_list.setCurrentRow(row)
|
||
break
|
||
|
||
def apply_stylesheet(self):
|
||
"""应用样式表 - 应用现代化设计风格"""
|
||
# 应用主样式表
|
||
self.setStyleSheet(ModernStylesheet.get_main_stylesheet())
|
||
|
||
def new_config(self):
|
||
"""新建配置"""
|
||
reply = QMessageBox.question(
|
||
self, "新建配置",
|
||
"是否清空当前配置?",
|
||
QMessageBox.Yes | QMessageBox.No
|
||
)
|
||
if reply == QMessageBox.Yes:
|
||
# 重置所有面板
|
||
self.log_message("已清空配置", "info")
|
||
|
||
def load_config_dialog(self):
|
||
"""加载配置对话框"""
|
||
file_path, _ = QFileDialog.getOpenFileName(
|
||
self, "加载配置", "", "JSON Files (*.json);;All Files (*.*)"
|
||
)
|
||
if file_path:
|
||
self.load_config(file_path)
|
||
|
||
def load_config(self, file_path):
|
||
"""加载配置"""
|
||
try:
|
||
with open(file_path, 'r', encoding='utf-8') as f:
|
||
config = json.load(f)
|
||
|
||
# 应用配置到各面板
|
||
if 'step1' in config:
|
||
self.step1_panel.set_config(config['step1'])
|
||
if 'step2' in config:
|
||
self.step2_panel.set_config(config['step2'])
|
||
if 'step3' in config:
|
||
self.step3_panel.set_config(config['step3'])
|
||
if 'step4' in config:
|
||
self.step4_panel.set_config(config['step4'])
|
||
if 'step5' in config:
|
||
self.step5_panel.set_config(config['step5'])
|
||
if 'step5_5' in config:
|
||
self.step5_5_panel.set_config(config['step5_5'])
|
||
if 'step6' in config:
|
||
self.step6_panel.set_config(config['step6'])
|
||
if 'step6_5' in config:
|
||
self.step6_5_panel.set_config(config['step6_5'])
|
||
if 'step6_75' in config:
|
||
self.step6_75_panel.set_config(config['step6_75'])
|
||
if 'step7' in config:
|
||
self.step7_panel.set_config(config['step7'])
|
||
if 'step8' in config:
|
||
self.step8_panel.set_config(config['step8'])
|
||
if 'step8_5' in config:
|
||
self.step8_5_panel.set_config(config['step8_5'])
|
||
if 'step9' in config:
|
||
self.step9_panel.set_config(config['step9'])
|
||
if 'visualization' in config:
|
||
self.viz_panel.set_config(config['visualization'])
|
||
if 'report_generation' in config:
|
||
self.report_panel.set_config(config['report_generation'])
|
||
|
||
self.config_file = file_path
|
||
self.log_message(f"已加载配置: {file_path}", "info")
|
||
QMessageBox.information(self, "成功", "配置加载成功!")
|
||
except Exception as e:
|
||
self.log_message(f"加载配置失败: {str(e)}", "error")
|
||
QMessageBox.critical(self, "错误", f"加载配置失败:\n{str(e)}")
|
||
|
||
def save_config_dialog(self):
|
||
"""保存配置对话框"""
|
||
file_path, _ = QFileDialog.getSaveFileName(
|
||
self, "保存配置", "config.json", "JSON Files (*.json);;All Files (*.*)"
|
||
)
|
||
if file_path:
|
||
self.save_config(file_path)
|
||
|
||
def save_config(self, file_path):
|
||
"""保存配置"""
|
||
try:
|
||
config = self.get_current_config()
|
||
with open(file_path, 'w', encoding='utf-8') as f:
|
||
json.dump(config, f, indent=4, ensure_ascii=False)
|
||
|
||
self.config_file = file_path
|
||
self.log_message(f"已保存配置: {file_path}", "info")
|
||
QMessageBox.information(self, "成功", "配置保存成功!")
|
||
except Exception as e:
|
||
self.log_message(f"保存配置失败: {str(e)}", "error")
|
||
QMessageBox.critical(self, "错误", f"保存配置失败:\n{str(e)}")
|
||
|
||
def get_current_config(self):
|
||
"""获取当前配置"""
|
||
config = {
|
||
'step1': self.step1_panel.get_config(),
|
||
'step2': self.step2_panel.get_config(),
|
||
'step3': self.step3_panel.get_config(),
|
||
'step4': self.step4_panel.get_config(),
|
||
'step5': self.step5_panel.get_config(),
|
||
'step5_5': self.step5_5_panel.get_config(),
|
||
'step6': self.step6_panel.get_config(),
|
||
'step6_5': self.step6_5_panel.get_config(),
|
||
'step6_75': self.step6_75_panel.get_config(),
|
||
'step7': self.step7_panel.get_config(),
|
||
'step8': self.step8_panel.get_config(),
|
||
'step8_5': self.step8_5_panel.get_config(),
|
||
'step9': self.step9_panel.get_config(),
|
||
'visualization': self.viz_panel.get_config(),
|
||
'report_generation': self.report_panel.get_config(),
|
||
}
|
||
return config
|
||
|
||
def set_work_directory(self):
|
||
"""设置工作目录"""
|
||
dir_path = QFileDialog.getExistingDirectory(self, "选择工作目录")
|
||
if dir_path:
|
||
self.work_dir = dir_path
|
||
self.log_message(f"工作目录已设置: {dir_path}", "info")
|
||
self.statusBar().showMessage(f"工作目录: {dir_path}")
|
||
|
||
# 同步到可视化面板
|
||
if hasattr(self, 'viz_panel'):
|
||
self.viz_panel.set_work_dir(dir_path)
|
||
if hasattr(self, 'report_panel'):
|
||
self.report_panel.set_work_dir(dir_path)
|
||
|
||
def open_work_directory(self):
|
||
"""打开工作目录"""
|
||
work_dir = getattr(self, 'work_dir', './work_dir')
|
||
if os.path.exists(work_dir):
|
||
os.startfile(work_dir)
|
||
else:
|
||
QMessageBox.warning(self, "警告", "工作目录不存在!")
|
||
|
||
def show_pipeline_status_on_startup(self):
|
||
"""启动时显示Pipeline状态"""
|
||
if not PIPELINE_AVAILABLE:
|
||
# 如果pipeline不可用,显示警告信息
|
||
status_msg = "[WARNING] Pipeline模块无法加载\n\n"
|
||
status_msg += "系统功能将受到限制,建议检查以下问题:\n"
|
||
status_msg += "• 项目文件结构是否完整\n"
|
||
status_msg += "• Python依赖包是否已安装\n"
|
||
status_msg += "• Python版本是否兼容\n\n"
|
||
status_msg += "点击 '帮助' → '检查Pipeline状态' 查看详细信息"
|
||
|
||
QMessageBox.warning(self, "Pipeline模块警告", status_msg)
|
||
else:
|
||
# 如果pipeline可用,只在状态栏显示
|
||
self.statusBar().showMessage("Pipeline模块: 正常加载", 5000) # 显示5秒
|
||
|
||
def show_pipeline_status(self):
|
||
"""显示Pipeline状态"""
|
||
status_text = "Pipeline模块状态检查\n\n"
|
||
|
||
if PIPELINE_AVAILABLE:
|
||
status_text += "[OK] Pipeline模块状态: 正常\n\n"
|
||
status_text += "详细诊断信息:\n"
|
||
else:
|
||
status_text += "[ERROR] Pipeline模块状态: 不可用\n\n"
|
||
status_text += "详细诊断信息:\n"
|
||
|
||
for info in PIPELINE_ERROR_INFO:
|
||
status_text += info + "\n"
|
||
|
||
# 添加使用建议
|
||
status_text += "\n" + "="*50 + "\n"
|
||
if PIPELINE_AVAILABLE:
|
||
status_text += "[SUCCESS] Pipeline模块已成功加载,可以正常使用所有功能!\n"
|
||
else:
|
||
status_text += "[WARNING] Pipeline模块无法加载,功能将受到限制\n"
|
||
status_text += "建议解决方案:\n"
|
||
status_text += "1. 检查项目文件结构是否完整\n"
|
||
status_text += "2. 安装所有必需的依赖包\n"
|
||
status_text += "3. 确认Python版本兼容性\n"
|
||
status_text += "4. 查看控制台输出获取更多详细信息\n"
|
||
|
||
# 创建消息框
|
||
msg_box = QMessageBox(self)
|
||
msg_box.setWindowTitle("Pipeline状态检查")
|
||
msg_box.setText(status_text)
|
||
|
||
# 根据状态设置图标
|
||
if PIPELINE_AVAILABLE:
|
||
msg_box.setIcon(QMessageBox.Information)
|
||
else:
|
||
msg_box.setIcon(QMessageBox.Warning)
|
||
|
||
# 设置详细文本(如果有的话)
|
||
if PIPELINE_ERROR_INFO:
|
||
detailed_text = "\n".join(PIPELINE_ERROR_INFO)
|
||
msg_box.setDetailedText(detailed_text)
|
||
|
||
msg_box.setStandardButtons(QMessageBox.Ok)
|
||
msg_box.exec_()
|
||
|
||
def show_about(self):
|
||
"""显示关于对话框"""
|
||
QMessageBox.about(
|
||
self, "关于",
|
||
"水质参数反演分析系统 v1.0\n\n"
|
||
"一个完整的水质参数反演工作流程工具\n\n"
|
||
"功能包括:\n"
|
||
"- 水域掩膜生成\n"
|
||
"- 耀斑检测与去除\n"
|
||
"- 光谱提取\n"
|
||
"- 机器学习建模\n"
|
||
"- 水质参数预测\n"
|
||
"- 可视化分析\n\n"
|
||
"公司:北京依锐思遥感技术有限公司\n"
|
||
"地址:北京市海淀区清河安宁庄东路18号5号楼二层205\n"
|
||
"电话:010-51292601\n"
|
||
"邮箱:hanshanlong@iris-rs.cn\n"
|
||
)
|
||
|
||
def run_full_pipeline(self):
|
||
"""运行完整流程"""
|
||
if not PIPELINE_AVAILABLE:
|
||
QMessageBox.critical(
|
||
self, "错误",
|
||
"无法导入pipeline模块,请确保water_quality_inversion_pipeline_GUI.py文件存在!"
|
||
)
|
||
return
|
||
|
||
# 验证配置
|
||
config = self.get_current_config()
|
||
|
||
# 基本验证
|
||
if not config['step1'].get('mask_path'):
|
||
QMessageBox.warning(self, "警告", "请先配置步骤1的掩膜文件!")
|
||
# 找到第一个可选的步骤项
|
||
for i in range(self.step_list.count()):
|
||
item = self.step_list.item(i)
|
||
if item.data(Qt.UserRole) == 'step1':
|
||
self.step_list.setCurrentRow(i)
|
||
break
|
||
return
|
||
|
||
# 确认执行
|
||
reply = QMessageBox.question(
|
||
self, "确认",
|
||
"是否开始执行完整流程?\n\n这可能需要较长时间,请确保配置正确。",
|
||
QMessageBox.Yes | QMessageBox.No
|
||
)
|
||
|
||
if reply != QMessageBox.Yes:
|
||
return
|
||
|
||
# 创建pipeline实例
|
||
work_dir = getattr(self, 'work_dir', './work_dir')
|
||
self.log_message(f"初始化pipeline,工作目录: {work_dir}", "info")
|
||
|
||
# 准备实际运行配置(排除未启用的步骤)
|
||
worker_config = copy.deepcopy(config)
|
||
step5_5_cfg = worker_config.get('step5_5')
|
||
if step5_5_cfg:
|
||
enabled = step5_5_cfg.pop('enabled', True)
|
||
if not enabled:
|
||
worker_config.pop('step5_5', None)
|
||
|
||
# 工作线程内创建 Pipeline,避免主线程阻塞及 Qt5Agg 子线程绘图卡死
|
||
self.worker = WorkerThread(work_dir, worker_config, mode='full')
|
||
self.worker.log_message.connect(self.log_message, Qt.QueuedConnection)
|
||
self.worker.progress_update.connect(self.update_progress, Qt.QueuedConnection)
|
||
self.worker.finished.connect(self.on_pipeline_finished, Qt.QueuedConnection)
|
||
|
||
# 更新UI状态
|
||
self.run_all_btn.setEnabled(False)
|
||
self.stop_btn.setEnabled(True)
|
||
self.progress_bar.setValue(0)
|
||
|
||
# 启动线程
|
||
self.worker.start()
|
||
self.log_message("="*50, "info")
|
||
self.log_message("开始执行完整流程...", "info")
|
||
self.log_message("="*50, "info")
|
||
|
||
def stop_pipeline(self):
|
||
"""停止流程"""
|
||
if self.worker and self.worker.isRunning():
|
||
reply = QMessageBox.question(
|
||
self, "确认",
|
||
"是否停止当前流程?",
|
||
QMessageBox.Yes | QMessageBox.No
|
||
)
|
||
if reply == QMessageBox.Yes:
|
||
self.worker.stop()
|
||
self.log_message("用户取消执行", "warning")
|
||
self.run_all_btn.setEnabled(True)
|
||
self.stop_btn.setEnabled(False)
|
||
|
||
def on_pipeline_finished(self, success, message):
|
||
"""流程完成"""
|
||
self.run_all_btn.setEnabled(True)
|
||
self.stop_btn.setEnabled(False)
|
||
|
||
if success:
|
||
self.progress_bar.setValue(100)
|
||
self.log_message("="*50, "info")
|
||
self.log_message("流程执行完成!", "info")
|
||
self.log_message("="*50, "info")
|
||
QMessageBox.information(self, "完成", "流程执行成功!\n\n请查看工作目录中的结果文件。")
|
||
else:
|
||
self.log_message("="*50, "error")
|
||
self.log_message(f"流程执行失败: {message}", "error")
|
||
self.log_message("="*50, "error")
|
||
QMessageBox.critical(self, "失败", f"流程执行失败:\n\n{message[:200]}")
|
||
|
||
def run_single_step(self, step_name, config):
|
||
"""运行单个步骤"""
|
||
if not PIPELINE_AVAILABLE:
|
||
QMessageBox.critical(
|
||
self, "错误",
|
||
"无法导入pipeline模块,请确保water_quality_inversion_pipeline_GUI.py文件存在!"
|
||
)
|
||
return
|
||
|
||
# 创建pipeline实例
|
||
work_dir = getattr(self, 'work_dir', './work_dir')
|
||
self.log_message(f"初始化pipeline,工作目录: {work_dir}", "info")
|
||
|
||
self.worker = WorkerThread(work_dir, config, mode='single_step', step_name=step_name)
|
||
self.worker.log_message.connect(self.log_message, Qt.QueuedConnection)
|
||
self.worker.progress_update.connect(self.update_progress, Qt.QueuedConnection)
|
||
self.worker.finished.connect(self.on_pipeline_finished, Qt.QueuedConnection)
|
||
|
||
# 更新UI状态
|
||
self.run_all_btn.setEnabled(False)
|
||
self.stop_btn.setEnabled(True)
|
||
self.progress_bar.setValue(0)
|
||
|
||
# 启动线程
|
||
self.worker.start()
|
||
self.log_message("="*50, "info")
|
||
self.log_message(f"开始独立运行步骤 {step_name}...", "info")
|
||
self.log_message("="*50, "info")
|
||
|
||
def update_progress(self, percentage, message):
|
||
"""更新进度"""
|
||
self.progress_bar.setValue(percentage)
|
||
self.statusBar().showMessage(message)
|
||
|
||
def log_message(self, message, level='info'):
|
||
"""记录日志"""
|
||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||
|
||
# 设置颜色
|
||
if level == 'error':
|
||
color = 'red'
|
||
elif level == 'warning':
|
||
color = 'orange'
|
||
else:
|
||
color = 'black'
|
||
|
||
# 添加到日志
|
||
formatted_msg = f'<span style="color: {color};">[{timestamp}] {message}</span>'
|
||
self.log_text.append(formatted_msg)
|
||
|
||
# 自动滚动到底部
|
||
cursor = self.log_text.textCursor()
|
||
cursor.movePosition(QTextCursor.End)
|
||
self.log_text.setTextCursor(cursor)
|
||
|
||
def clear_log(self):
|
||
"""清空日志"""
|
||
self.log_text.clear()
|
||
self.log_message("日志已清空", "info")
|
||
|
||
def toggle_training_data_mode(self, checked):
|
||
"""切换训练数据模式"""
|
||
self.has_training_data = checked
|
||
self.update_ui_for_training_mode()
|
||
|
||
mode_text = "有训练数据" if checked else "无训练数据"
|
||
self.log_message(f"切换到{mode_text}模式", "info")
|
||
self.statusBar().showMessage(f"当前模式: {mode_text}")
|
||
|
||
# 更新按钮文本
|
||
self.training_mode_action.setText("有训练数据模式" if checked else "无训练数据模式")
|
||
|
||
def update_banner_image(self):
|
||
"""更新横幅图片 - 完全跟随窗口等比缩放,填满可用宽度"""
|
||
if not hasattr(self, 'banner_pixmap') or self.banner_pixmap.isNull():
|
||
return
|
||
|
||
# 获取可用宽度(考虑工具栏边距),跟随窗口实时变化
|
||
available_width = max(200, self.width() - 60)
|
||
|
||
# 先根据可用宽度计算目标高度(严格 5.25:1)
|
||
target_height = int(available_width / 5.25)
|
||
|
||
# 限制最小高度
|
||
if target_height < 38:
|
||
target_height = 38
|
||
available_width = int(38 * 5.25)
|
||
|
||
# 计算图片目标尺寸(保持 5.25:1 比例)
|
||
target_width = available_width
|
||
|
||
# 设置固定尺寸,确保标签严格填满整个区域
|
||
self.banner_label.setFixedSize(target_width, target_height)
|
||
|
||
# 等比缩放到目标尺寸,填满整个区域(允许轻微裁剪)
|
||
scaled_pixmap = self.banner_pixmap.scaled(
|
||
target_width,
|
||
target_height,
|
||
Qt.KeepAspectRatioByExpanding, # 保持比例,填满区域,允许裁剪超出部分
|
||
Qt.SmoothTransformation
|
||
)
|
||
|
||
self.banner_label.setPixmap(scaled_pixmap)
|
||
|
||
def resizeEvent(self, event):
|
||
"""窗口大小改变事件 - 实时更新横幅图片等比缩放"""
|
||
super().resizeEvent(event)
|
||
# 直接调用,不使用定时器延迟(或缩短到 10ms)
|
||
self.update_banner_image()
|
||
|
||
def update_ui_for_training_mode(self):
|
||
"""根据训练数据模式更新UI状态"""
|
||
# 需要禁用的步骤ID(对应无训练数据模式下需要禁用的步骤)
|
||
disabled_step_ids = ['step4', 'step5', 'step5_5', 'step6', 'step6_5', 'step6_75']
|
||
|
||
# 更新标签页的启用/禁用状态
|
||
step_id_to_tab = {
|
||
'step1': 0, 'step2': 1, 'step3': 2, 'step4': 3,
|
||
'step5': 4, 'step5_5': 5, 'step6': 6, 'step6_5': 7,
|
||
'step6_75': 8, 'step7': 9, 'step8': 10, 'step8_5': 11,
|
||
'step8_75': 12, 'step9': 13, 'step9_viz': 14
|
||
}
|
||
|
||
for step_id in disabled_step_ids:
|
||
if step_id in step_id_to_tab:
|
||
tab_index = step_id_to_tab[step_id]
|
||
if tab_index < self.step_stack.count():
|
||
self.step_stack.setTabEnabled(tab_index, self.has_training_data)
|
||
|
||
# 同时更新导航列表的启用状态
|
||
for i in range(self.step_list.count()):
|
||
item = self.step_list.item(i)
|
||
item_data = item.data(Qt.UserRole)
|
||
|
||
# 跳过阶段标题和分隔符
|
||
if item_data == "stage_header" or item_data is None:
|
||
continue
|
||
|
||
# 检查步骤是否在禁用列表中
|
||
if item_data in disabled_step_ids:
|
||
if not self.has_training_data:
|
||
item.setFlags(item.flags() & ~Qt.ItemIsEnabled)
|
||
item.setForeground(QColor(128, 128, 128)) # 灰色
|
||
else:
|
||
item.setFlags(item.flags() | Qt.ItemIsEnabled)
|
||
item.setForeground(QColor(ModernStylesheet.COLORS.get('text_secondary', '#666666'))) # 原始颜色
|
||
|
||
|
||
def main():
|
||
"""主函数"""
|
||
app = QApplication(sys.argv)
|
||
|
||
# 设置应用信息
|
||
app.setApplicationName("水质参数反演分析系统")
|
||
app.setOrganizationName("WaterQuality")
|
||
|
||
# 创建主窗口
|
||
window = WaterQualityGUI()
|
||
window.show()
|
||
|
||
sys.exit(app.exec_())
|
||
|
||
|
||
if __name__ == "__main__":
|
||
#冻结,只显示1个exe
|
||
# multiprocessing.freeze_support()
|
||
main()
|
||
|