界面优化
This commit is contained in:
@ -555,7 +555,13 @@ class WaterQualityInference:
|
|||||||
print(f"输入数据形状: {spectra_processed.shape}")
|
print(f"输入数据形状: {spectra_processed.shape}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
predictions = model.predict(spectra_processed)
|
# 清洗 NaN / Inf,防止 SVR 等模型报错
|
||||||
|
spectra_clean = np.nan_to_num(spectra_processed, nan=0.0, posinf=0.0, neginf=0.0)
|
||||||
|
if np.any(np.isnan(spectra_clean)) or np.any(np.isinf(spectra_clean)):
|
||||||
|
print("警告: 清洗后数据中仍存在 NaN/Inf,已重置为 0")
|
||||||
|
spectra_clean = np.nan_to_num(spectra_clean, nan=0.0, posinf=0.0, neginf=0.0)
|
||||||
|
|
||||||
|
predictions = model.predict(spectra_clean)
|
||||||
print(f"预测完成,结果形状: {predictions.shape}")
|
print(f"预测完成,结果形状: {predictions.shape}")
|
||||||
print(f"预测值范围: [{np.min(predictions):.4f}, {np.max(predictions):.4f}]")
|
print(f"预测值范围: [{np.min(predictions):.4f}, {np.max(predictions):.4f}]")
|
||||||
print(f"预测值统计: 均值={np.mean(predictions):.4f}, 标准差={np.std(predictions):.4f}")
|
print(f"预测值统计: 均值={np.mean(predictions):.4f}, 标准差={np.std(predictions):.4f}")
|
||||||
|
|||||||
@ -1724,7 +1724,11 @@ class WaterQualityInversionPipeline:
|
|||||||
final_water_mask, temp_shape, geotransform, projection, img_path
|
final_water_mask, temp_shape, geotransform, projection, img_path
|
||||||
)
|
)
|
||||||
|
|
||||||
# 应用Goodman算法:直接传递文件路径,让算法类使用GDAL逐波段处理
|
# 加载影像数据(Goodman算法需要numpy数组用于后插值)
|
||||||
|
image_array, geotransform, projection = self._load_image_as_array(img_path)
|
||||||
|
print(f"影像尺寸: {image_array.shape}")
|
||||||
|
|
||||||
|
# 应用Goodman算法:传递文件路径
|
||||||
goodman = Goodman(img_path, NIR_lower=nir_lower, NIR_upper=nir_upper,
|
goodman = Goodman(img_path, NIR_lower=nir_lower, NIR_upper=nir_upper,
|
||||||
A=goodman_A, B=goodman_B, water_mask=mask_for_algorithm,
|
A=goodman_A, B=goodman_B, water_mask=mask_for_algorithm,
|
||||||
output_path=output_path) # 传递output_path,算法类会保存
|
output_path=output_path) # 传递output_path,算法类会保存
|
||||||
|
|||||||
1
src/gui/components/__init__.py
Normal file
1
src/gui/components/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# src.gui.components package
|
||||||
78
src/gui/components/custom_widgets.py
Normal file
78
src/gui/components/custom_widgets.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
自定义组件 - 文件选择控件等公共组件
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QWidget, QHBoxLayout, QLabel, QLineEdit, QPushButton, QFileDialog,
|
||||||
|
)
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
|
|
||||||
|
class FileSelectWidget(QWidget):
|
||||||
|
"""文件选择组件"""
|
||||||
|
def __init__(self, label_text, file_filter="All Files (*.*)", mode="open", parent=None):
|
||||||
|
"""
|
||||||
|
初始化文件选择组件
|
||||||
|
|
||||||
|
Args:
|
||||||
|
label_text: 标签文本
|
||||||
|
file_filter: 文件过滤器
|
||||||
|
mode: 选择模式 - "open"(打开文件) 或 "save"(保存文件)
|
||||||
|
parent: 父控件
|
||||||
|
"""
|
||||||
|
super().__init__(parent)
|
||||||
|
self.file_filter = file_filter
|
||||||
|
self.mode = mode # "open" 或 "save"
|
||||||
|
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()
|
||||||
|
placeholder = "请选择保存路径..." if self.mode == "save" else "请选择文件..."
|
||||||
|
self.line_edit.setPlaceholderText(placeholder)
|
||||||
|
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):
|
||||||
|
"""浏览文件"""
|
||||||
|
current_text = self.line_edit.text().strip()
|
||||||
|
initial_dir = ""
|
||||||
|
|
||||||
|
if current_text:
|
||||||
|
dir_path = os.path.dirname(current_text)
|
||||||
|
if dir_path and os.path.exists(dir_path):
|
||||||
|
initial_dir = dir_path
|
||||||
|
|
||||||
|
if self.mode == "save":
|
||||||
|
file_path, _ = QFileDialog.getSaveFileName(
|
||||||
|
self, "保存文件", initial_dir, self.file_filter
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
file_path, _ = QFileDialog.getOpenFileName(
|
||||||
|
self, "选择文件", initial_dir, 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))
|
||||||
1
src/gui/core/__init__.py
Normal file
1
src/gui/core/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# src.gui.core
|
||||||
327
src/gui/core/worker_thread.py
Normal file
327
src/gui/core/worker_thread.py
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
后台线程模块:Pipeline 执行线程与诊断逻辑。
|
||||||
|
"""
|
||||||
|
import traceback
|
||||||
|
from PyQt5.QtCore import QThread, pyqtSignal
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 依赖诊断
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
def check_pipeline_dependencies():
|
||||||
|
"""检查pipeline模块的依赖项"""
|
||||||
|
missing_deps = []
|
||||||
|
dep_errors = {}
|
||||||
|
|
||||||
|
required_packages = [
|
||||||
|
'numpy', 'pandas', 'scipy', 'matplotlib', 'sklearn',
|
||||||
|
'joblib', 'PIL', 'cv2', 'rasterio', 'geopandas'
|
||||||
|
]
|
||||||
|
|
||||||
|
for package in required_packages:
|
||||||
|
try:
|
||||||
|
if package == 'PIL':
|
||||||
|
import PIL
|
||||||
|
elif package == 'cv2':
|
||||||
|
import cv2
|
||||||
|
else:
|
||||||
|
__import__(package)
|
||||||
|
except Exception as e:
|
||||||
|
missing_deps.append(package)
|
||||||
|
dep_errors[package] = repr(e)
|
||||||
|
|
||||||
|
return missing_deps, dep_errors
|
||||||
|
|
||||||
|
|
||||||
|
def diagnose_pipeline_import_error():
|
||||||
|
"""诊断pipeline导入错误"""
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
error_info = []
|
||||||
|
|
||||||
|
is_frozen = getattr(sys, "frozen", False) or bool(getattr(sys, "_MEIPASS", None))
|
||||||
|
|
||||||
|
if is_frozen:
|
||||||
|
error_info.append(
|
||||||
|
"[INFO] PyInstaller 环境:Pipeline 从程序内置包加载,跳过对仓库路径 src/core/*.py 的磁盘检查"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
pipeline_file = os.path.normpath(
|
||||||
|
os.path.join(os.path.dirname(__file__), "..", "..", "core", "water_quality_inversion_pipeline_GUI.py")
|
||||||
|
)
|
||||||
|
if not os.path.exists(pipeline_file):
|
||||||
|
error_info.append(f"[ERROR] Pipeline文件不存在: {pipeline_file}")
|
||||||
|
error_info.append(
|
||||||
|
" 解决方案: 请确保项目结构完整,检查 src/core/ 下是否有 water_quality_inversion_pipeline_GUI.py"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
error_info.append(f"[OK] Pipeline文件存在: {pipeline_file}")
|
||||||
|
|
||||||
|
current_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
if current_dir not in sys.path:
|
||||||
|
sys.path.insert(0, current_dir)
|
||||||
|
error_info.append(f"[INFO] 已添加路径到sys.path: {current_dir}")
|
||||||
|
|
||||||
|
missing_deps, dep_errors = check_pipeline_dependencies()
|
||||||
|
if missing_deps:
|
||||||
|
error_info.append(f"[ERROR] 缺少必需的依赖包: {', '.join(missing_deps)}")
|
||||||
|
for pkg in missing_deps:
|
||||||
|
if pkg in dep_errors:
|
||||||
|
error_info.append(f" - {pkg} 导入失败原因: {dep_errors[pkg]}")
|
||||||
|
error_info.append(" 解决方案: 请运行以下命令安装依赖:")
|
||||||
|
error_info.append(" pip install -r requirements.txt")
|
||||||
|
error_info.append(" 或使用conda:")
|
||||||
|
error_info.append(" conda install numpy pandas scipy matplotlib scikit-learn joblib pillow opencv-python rasterio geopandas")
|
||||||
|
else:
|
||||||
|
error_info.append("[OK] 主要依赖包均已安装")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from osgeo import gdal # noqa: F401
|
||||||
|
error_info.append("[OK] GDAL (osgeo) 可用")
|
||||||
|
except ImportError:
|
||||||
|
try:
|
||||||
|
from osgeo import gdal # noqa: F401
|
||||||
|
error_info.append("[OK] GDAL 可用")
|
||||||
|
except ImportError:
|
||||||
|
error_info.append("[WARNING] GDAL/osgeo 不可用,将影响栅格与地理数据处理")
|
||||||
|
error_info.append(" 开发环境: conda install gdal")
|
||||||
|
error_info.append(" 打包环境: 请在构建所用 Conda 环境中打包,并确保 spec 已收集 Library/bin 中依赖 DLL")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import unittest
|
||||||
|
error_info.append("[OK] unittest模块可用")
|
||||||
|
except ImportError:
|
||||||
|
error_info.append("[WARNING] unittest模块不可用,这可能是PyInstaller打包环境导致的")
|
||||||
|
error_info.append(" 这不会影响主要功能,但可能影响某些测试相关特性")
|
||||||
|
|
||||||
|
return error_info
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Pipeline 可用性标志(模块级状态)
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
PIPELINE_AVAILABLE = False
|
||||||
|
PIPELINE_ERROR_INFO = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
error_info = diagnose_pipeline_import_error()
|
||||||
|
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
|
||||||
|
PIPELINE_AVAILABLE = True
|
||||||
|
print("[OK] 成功导入pipeline模块")
|
||||||
|
PIPELINE_ERROR_INFO = error_info
|
||||||
|
|
||||||
|
except ImportError as e:
|
||||||
|
PIPELINE_AVAILABLE = False
|
||||||
|
error_info = diagnose_pipeline_import_error()
|
||||||
|
|
||||||
|
print("="*60)
|
||||||
|
print("[ERROR] PIPELINE导入失败 - 详细诊断信息:")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
for info in error_info:
|
||||||
|
print(info)
|
||||||
|
|
||||||
|
print("-"*60)
|
||||||
|
print(f"原始ImportError: {str(e)}")
|
||||||
|
print("-"*60)
|
||||||
|
|
||||||
|
if "unittest" in str(e):
|
||||||
|
print("[INFO] unittest模块缺失 - 这通常在PyInstaller打包环境中发生")
|
||||||
|
print("解决方案:")
|
||||||
|
print(" 1. 这不会影响主要功能,程序仍可正常运行")
|
||||||
|
print(" 2. 如果需要修复,可以在.spec文件中添加unittest模块:")
|
||||||
|
print(" a = Analysis(..., hiddenimports=['unittest', 'unittest.mock'])")
|
||||||
|
print(" 3. 或在PyInstaller命令中添加: --hidden-import unittest")
|
||||||
|
elif "water_quality_inversion_pipeline_GUI" in str(e):
|
||||||
|
print("[INFO] 可能的解决方案:")
|
||||||
|
print(" 1. 检查src/core/water_quality_inversion_pipeline_GUI.py文件是否存在")
|
||||||
|
print(" 2. 确保Python路径设置正确")
|
||||||
|
print(" 3. 尝试重新安装依赖: pip install -r requirements.txt")
|
||||||
|
print(" 4. 检查Python版本是否兼容(推荐Python 3.8-3.11)")
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
print("\n完整错误追踪:")
|
||||||
|
traceback.print_exc()
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
PIPELINE_ERROR_INFO = error_info
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
PIPELINE_AVAILABLE = False
|
||||||
|
error_info = diagnose_pipeline_import_error()
|
||||||
|
|
||||||
|
print("="*60)
|
||||||
|
print("[ERROR] PIPELINE导入失败 - 其他错误:")
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
for info in error_info:
|
||||||
|
print(info)
|
||||||
|
|
||||||
|
print("-"*60)
|
||||||
|
print(f"原始错误: {str(e)}")
|
||||||
|
print("-"*60)
|
||||||
|
|
||||||
|
print("[INFO] 可能的解决方案:")
|
||||||
|
print(" 1. 检查Python环境和依赖包版本")
|
||||||
|
print(" 2. 尝试重新安装所有依赖")
|
||||||
|
print(" 3. 检查是否有语法错误或其他模块导入问题")
|
||||||
|
|
||||||
|
import traceback
|
||||||
|
print("\n完整错误追踪:")
|
||||||
|
traceback.print_exc()
|
||||||
|
print("="*60)
|
||||||
|
|
||||||
|
PIPELINE_ERROR_INFO = error_info
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# WorkerThread
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
class WorkerThread(QThread):
|
||||||
|
"""后台工作线程,用于执行耗时任务(在工作线程内创建 Pipeline,避免阻塞 UI)。"""
|
||||||
|
progress_update = pyqtSignal(int, str) # 进度更新信号 (percentage, message)
|
||||||
|
log_message = pyqtSignal(str, str) # 日志消息信号 (message, level: 'info'/'warning'/'error')
|
||||||
|
step_completed = pyqtSignal(str, bool, str) # 步骤完成信号 (step_name, success, message)
|
||||||
|
finished = pyqtSignal(bool, str) # 完成信号 (success, message)
|
||||||
|
|
||||||
|
def __init__(self, work_dir: str, config, mode='full', step_name=None):
|
||||||
|
super().__init__()
|
||||||
|
self.work_dir = str(work_dir)
|
||||||
|
self.config = config
|
||||||
|
self.mode = mode # 'full' 或 'single_step'
|
||||||
|
self.step_name = step_name # 单步执行时的步骤名称
|
||||||
|
self.pipeline = None
|
||||||
|
self.is_running = True
|
||||||
|
self.current_step = None
|
||||||
|
self.step_count = 0
|
||||||
|
self.total_steps = 9
|
||||||
|
|
||||||
|
def pipeline_callback(self, step_name, status, message=""):
|
||||||
|
"""Pipeline回调函数,用于接收步骤状态"""
|
||||||
|
if status == "start":
|
||||||
|
self.log_message.emit(f"[START] 开始执行: {step_name}", "info")
|
||||||
|
progress = int((self.step_count / self.total_steps) * 100)
|
||||||
|
self.progress_update.emit(progress, f"正在执行: {step_name}")
|
||||||
|
elif status == "completed":
|
||||||
|
self.step_count += 1
|
||||||
|
self.log_message.emit(f"[DONE] 完成: {step_name} {message}", "info")
|
||||||
|
self.step_completed.emit(step_name, True, message)
|
||||||
|
progress = int((self.step_count / self.total_steps) * 100)
|
||||||
|
self.progress_update.emit(progress, f"已完成: {step_name}")
|
||||||
|
elif status == "skipped":
|
||||||
|
self.step_count += 1
|
||||||
|
self.log_message.emit(f"[SKIP] 跳过: {step_name} {message}", "warning")
|
||||||
|
self.step_completed.emit(step_name, True, f"跳过: {message}")
|
||||||
|
progress = int((self.step_count / self.total_steps) * 100)
|
||||||
|
self.progress_update.emit(progress, f"已跳过: {step_name}")
|
||||||
|
elif status == "error":
|
||||||
|
self.log_message.emit(f"[ERROR] 错误: {step_name} - {message}", "error")
|
||||||
|
self.step_completed.emit(step_name, False, message)
|
||||||
|
elif status == "info":
|
||||||
|
self.log_message.emit(f" {message}", "info")
|
||||||
|
elif status == "warning":
|
||||||
|
self.log_message.emit(f" [WARNING] {message}", "warning")
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""运行 pipeline:子线程内切换 Matplotlib 为 Agg,避免 Qt5Agg 在后台线程绘图导致界面卡死。"""
|
||||||
|
mpl_prev = None
|
||||||
|
try:
|
||||||
|
import matplotlib
|
||||||
|
mpl_prev = matplotlib.get_backend()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
plt.switch_backend("Agg")
|
||||||
|
except Exception:
|
||||||
|
mpl_prev = None
|
||||||
|
try:
|
||||||
|
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
|
||||||
|
self.pipeline = WaterQualityInversionPipeline(work_dir=self.work_dir)
|
||||||
|
|
||||||
|
if self.mode == 'full':
|
||||||
|
self.log_message.emit("开始运行完整流程...", "info")
|
||||||
|
self.step_count = 0
|
||||||
|
|
||||||
|
if hasattr(self.pipeline, 'set_callback'):
|
||||||
|
self.pipeline.set_callback(self.pipeline_callback)
|
||||||
|
|
||||||
|
self.pipeline.run_full_pipeline(self.config)
|
||||||
|
|
||||||
|
self.progress_update.emit(100, "流程执行完成")
|
||||||
|
self.finished.emit(True, "完整流程执行成功!")
|
||||||
|
else:
|
||||||
|
self.log_message.emit(f"开始独立运行步骤: {self.step_name}", "info")
|
||||||
|
self.progress_update.emit(0, f"正在执行: {self.step_name}")
|
||||||
|
|
||||||
|
if hasattr(self.pipeline, 'set_callback'):
|
||||||
|
self.pipeline.set_callback(self.pipeline_callback)
|
||||||
|
|
||||||
|
self.run_single_step(self.step_name, self.config)
|
||||||
|
|
||||||
|
self.progress_update.emit(100, f"步骤 {self.step_name} 执行完成")
|
||||||
|
self.finished.emit(True, f"步骤 {self.step_name} 独立运行成功!")
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"执行失败: {str(e)}\n{traceback.format_exc()}"
|
||||||
|
self.log_message.emit(error_msg, "error")
|
||||||
|
self.finished.emit(False, error_msg)
|
||||||
|
finally:
|
||||||
|
if mpl_prev:
|
||||||
|
try:
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
plt.switch_backend(mpl_prev)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def run_single_step(self, step_name, config):
|
||||||
|
"""运行单个步骤"""
|
||||||
|
step_method_map = {
|
||||||
|
'step1': 'step1_generate_water_mask',
|
||||||
|
'step2': 'step2_find_glint_area',
|
||||||
|
'step3': 'step3_remove_glint',
|
||||||
|
'step4': 'step4_process_csv',
|
||||||
|
'step5': 'step5_extract_training_spectra',
|
||||||
|
'step5_5': 'step5_5_calculate_water_quality_indices',
|
||||||
|
'step6': 'step6_train_models',
|
||||||
|
'step6_5': 'step6_5_non_empirical_modeling',
|
||||||
|
'step6_75': 'step6_75_custom_regression',
|
||||||
|
'step7': 'step7_generate_sampling_points',
|
||||||
|
'step8': 'step8_predict_water_quality',
|
||||||
|
'step8_5': 'step8_5_predict_with_non_empirical_models',
|
||||||
|
'step8_75': 'step8_75_predict_with_custom_regression',
|
||||||
|
'step9': 'step9_generate_distribution_map'
|
||||||
|
}
|
||||||
|
|
||||||
|
if step_name not in step_method_map:
|
||||||
|
raise ValueError(f"未知的步骤名称: {step_name}")
|
||||||
|
|
||||||
|
method_name = step_method_map[step_name]
|
||||||
|
step_config = dict(config.get(step_name, {}))
|
||||||
|
|
||||||
|
step_config['skip_dependency_check'] = True
|
||||||
|
|
||||||
|
if step_name == 'step9':
|
||||||
|
step_config.pop('step9_batch_mode', None)
|
||||||
|
step_config.pop('prediction_csv_dir', None)
|
||||||
|
step_config.pop('recursive_csv_scan', None)
|
||||||
|
|
||||||
|
if step_name in ['step2', 'step3', 'step4', 'step5', 'step7', 'step8', 'step8_5', 'step8_75']:
|
||||||
|
step_config.pop('output_path', None)
|
||||||
|
|
||||||
|
if step_name == 'step8_5' and 'models_dir' in step_config:
|
||||||
|
step_config['non_empirical_models_dir'] = step_config.pop('models_dir')
|
||||||
|
|
||||||
|
method = getattr(self.pipeline, method_name)
|
||||||
|
result = method(**step_config)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""停止执行"""
|
||||||
|
self.is_running = False
|
||||||
|
self.terminate()
|
||||||
46
src/gui/model/waterindex.csv
Normal file
46
src/gui/model/waterindex.csv
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
Formula_Name,Category,Formula,Reference
|
||||||
|
BGA_Am09KBBI,Phycocyanin (BGA_PC),(w686 - w658) / (w686 + w658),"Amin, R.; Zhou, J.; Gilerson, A.; Gross, B.; Moshary, F.; Ahmed, S.; Novel optical techniques for detecting and classifying toxic dinoflagellate Karenia brevis blooms using satellite imagery, Optics Express, 2009, 17, 11, 1-13."
|
||||||
|
BGA_Be162B643sub629,Phycocyanin (BGA_PC),w644 - w629,"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 538."
|
||||||
|
BGA_Be162B700sub601,Phycocyanin (BGA_PC),w700 - w601,"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 539."
|
||||||
|
BGA_Be162BsubPhy,Phycocyanin (BGA_PC),w715 - w615,"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 540."
|
||||||
|
BGA_Be16FLHBlueRedNIR,Phycocyanin (BGA_PC),w658 - (w857 + (w458 - w857)),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 538."
|
||||||
|
BGA_Be16FLHGreenRedNIR,Phycocyanin (BGA_PC),w658 - (w857 + (w558 - w857)),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 539."
|
||||||
|
BGA_Be16FLHVioletRedNIR,Phycocyanin (BGA_PC),w658 - (w857 + (w444 - w857)),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 538."
|
||||||
|
BGA_Be16MPI,Phycocyanin (BGA_PC),(w615 - w601) - (w644 - w601),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 539."
|
||||||
|
BGA_Be16NDPhyI,Phycocyanin (BGA_PC),(w700 - w622) / (w700 + w622),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 540."
|
||||||
|
BGA_Be16NDPhyI644over615,Phycocyanin (BGA_PC),(w644 - w615) / (w644 + w615),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 541."
|
||||||
|
BGA_Be16NDPhyI644over629,Phycocyanin (BGA_PC),(w644 - w629) / (w644 + w629),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 542."
|
||||||
|
BGA_Be16Phy2BDA644over629,Phycocyanin (BGA_PC),w644 / w629,"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 545."
|
||||||
|
BGA_Da052BDA,Phycocyanin (BGA_PC),w714 / w672,"Wynne, T. T., Stumpf, R. P., Tomlinson, M. C., Warner, R. A., Tester, P. A., Dyble, J.; Relating spectral shape to cyanobacterial blooms in the Laurentian Great Lakes. Int. J. Remote Sens., 2008, 29, 3665-3672."
|
||||||
|
BGA_Go04MCI,Phycocyanin (BGA_PC),w709 - w681 - (w753 - w681),"Gower, J.F.R.; Brown,L.; Borstad, G.A.; Observation of chlorophyll fluorescence in west coast waters of Canada using the MODIS satellite sensor. Can. J. Remote Sens., 2004, 30 (1), 17闁?5."
|
||||||
|
BGA_HU103BDA,Phycocyanin (BGA_PC),(((1 / w615) - (1 / w600)) - w725),"Hunter, P.D.; Tyler, A.N.; Willby, N.J.; Gilvear, D.J.; The spatial dynamics of vertical migration by Microcystis aeruginosa in a eutrophic shallow lake: A case study using high spatial resolution time-series airborne remote sensing. Limn. Oceanogr. 2008, 53, 2391-2406"
|
||||||
|
BGA_Ku15PhyCI,Phycocyanin (BGA_PC),(-1 * (W681 - W665 - (W709 - W665))),"Kudela, R.M., Palacios, S.L., Austerberry, D.C., Accorsi, E.K., Guild, L.S.; Application of hyperspectral remote sensing to cyanobacterial blooms in inland waters, Torres-Perez, J., 2015, Remote Sens. Environ., 2015, 167, 1-10."
|
||||||
|
BGA_Ku15SLH,Phycocyanin (BGA_PC),(w715 - w658) + (w715 - w658),"Kudela, R.M., Palacios, S.L., Austerberry, D.C., Accorsi, E.K., Guild, L.S.; Application of hyperspectral remote sensing to cyanobacterial blooms in inland waters, Torres-Perez, J., 2015, Remote Sens. Environ., 2015, 167, 1-11."
|
||||||
|
BGA_MI092BDA,Phycocyanin (BGA_PC),w700 / w600,"Mishra, S.; Mishra, D.R.; Schluchter, W. M., A novel algorithm for predicting PC concentrations in cyanobacteria: A proximal hyperspectral remote sensing approach. Remote Sens., 2009, 1, 758闁?75."
|
||||||
|
BGA_MM092BDA,Phycocyanin (BGA_PC),w724 / w600,"Mishra, S.; Mishra, D.R.; Schluchter, W. M., A novel algorithm for predicting PC concentrations in cyanobacteria: A proximal hyperspectral remote sensing approach. Remote Sens., 2009, 1, 758闁?76."
|
||||||
|
BGA_MM12NDCIalt,Phycocyanin (BGA_PC),(w700 - w658) / (w700 + w658),"Mishra, S.; Mishra, D.R.; A novel remote sensing algorithm to quantify phycocyanin in cyanobacterial algal blooms, Env. Res. Lett., 2014, 9 (11), DOI:10.1088/1748-9326/9/11/114003"
|
||||||
|
BGA_MM143BDAopt,Phycocyanin (BGA_PC),((1 / w629) - (1 / w659)) * w724,"Mishra, S.; Mishra, D.R.; A novel remote sensing algorithm to quantify phycocyanin in cyanobacterial algal blooms, Env. Res. Lett., 2014, 9 (11), DOI:10.1088/1748-9326/9/11/114004"
|
||||||
|
BGA_SI052BDA,Phycocyanin (BGA_PC),w709 / w620,"Simis, S. G. H.; Peters, S.W. M.; Gons, H. J.; Remote sensing of the cyanobacteria pigment phycocyanin in turbid inland water. Limn. Oceanogr., 2005, 50, 237闁?45"
|
||||||
|
BGA_SM122BDA,Phycocyanin (BGA_PC),w709 / w600,"Mishra, S. Remote sensing of cyanobacteria in turbid productive waters, PhD Dissertation. Mississippi State University, USA. 2012."
|
||||||
|
BGA_SY002BDA,Phycocyanin (BGA_PC),w650 / w625,"Schalles, J.; Yacobi, Y. Remote detection and seasonal patterns of phycocyanin, carotenoid and chlorophyll-a pigments in eutrophic waters. Archiv fur Hydrobiologie, Special Issues Advances in Limnology, 2000, 55,153闁?68"
|
||||||
|
BGA_Wy08CI,Phycocyanin (BGA_PC),(-1 * (W686 - W672 - (W715 - W672))),"Wynne, T. T., Stumpf, R. P., Tomlinson, M. C., Warner, R. A., Tester, P. A., Dyble, J.; Relating spectral shape to cyanobacterial blooms in the Laurentian Great Lakes. Int. J. Remote Sens., 2008, 29, 3665-3672."
|
||||||
|
Chl_Al10SABI,chlorophyll_a,(w857 - w644) / (w458 + w529),"Alawadi, F. Detection of surface algal blooms using the newly developed algorithm surface algal bloom index (SABI). Proc. SPIE 2010, 7825."
|
||||||
|
Chl_Am092Bsub,chlorophyll_a,w681 - w665,"Amin, R.; Zhou, J.; Gilerson, A.; Gross, B.; Moshary, F.; Ahmed, S. Novel optical techniques for detecting and classifying toxic dinoflagellate Karenia brevis blooms using satellite imagery. Opt. Express 2009, 17, 9126闁?144."
|
||||||
|
Chl_Be16FLHblue,chlorophyll_a,w529 - (w644 + (w458 - w644)),"Beck, R.A. and 22 others; Comparison of satellite reflectance algorithms for estimating chlorophyll-a in a temperate reservoir using coincident hyperspectral aircraft imagery and dense coincident surface observations, Remote Sens. Environ., 2016, 178, 15-30."
|
||||||
|
Chl_Be16FLHviolet,chlorophyll_a,w529 - (w644 + (w429 - w644)),"Beck, R.A. and 22 others; Comparison of satellite reflectance algorithms for estimating chlorophyll-a in a temperate reservoir using coincident hyperspectral aircraft imagery and dense coincident surface observations, Remote Sens. Environ., 2016, 178, 15-30."
|
||||||
|
Chl_Be16NDTIblue,chlorophyll_a,(w658 - w458) / (w658 + w458),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 543."
|
||||||
|
Chl_Be16NDTIviolet,chlorophyll_a,(w658 - w444) / (w658 + w444),"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 544."
|
||||||
|
Chl_De933BDA,chlorophyll_a,w600 - w648 - w625,"Dekker, A.; Detection of the optical water quality parameters for eutrophic waters by high resolution remote sensing, Ph.D. thesis, 1993, Free University, Amsterdam."
|
||||||
|
Chl_Gi033BDA,chlorophyll_a,((1 / w672) - (1 / w715)) * w757,"Gitelson, A.A.; U. Gritz, and M. N. Merzlyak.; Relationships between leaf chlorophyll content and spectral reflectance and algorithms for non-destructive chlorophyll assessment in higher plant leaves. J. Plant Phys. 2003, 160, 271-282."
|
||||||
|
Chl_Kn07KIVU,chlorophyll_a,(w458 - w644) / w529,"Kneubuhler, M.; Frank T.; Kellenberger, T.W; Pasche N.; Schmid M.; Mapping chlorophyll-a in Lake Kivu with remote sensing methods. 2007, Proceedings of the Envisat Symposium 2007, Montreux, Switzerland 23闁?7 April 2007 (ESA SP-636, July 2007)."
|
||||||
|
Chl_MM12NDCI,chlorophyll_a,(w715 - w686) / (w715 + w686),"Mishra, S.; and Mishra, D.R. Normalized difference chlorophyll index: A novel model for remote estimation of chlorophyll-a concentration in turbid productive waters, Remote Sens. Environ., 2012, 117, 394-406"
|
||||||
|
Chl_Zh10FLH,chlorophyll_a,w686 - (w715 + (w672 - w751)),"Zhao, D.Z.; Xing, X.G.; Liu, Y.G.; Yang, J.H.; Wang, L. The relation of chlorophyll-a concentration with the reflectance peak near 700 nm in algae-dominated waters and sensitivity of fluorescence algorithms for detecting algal bloom. Int. J. Remote Sens. 2010, 31, 39-48"
|
||||||
|
Turb_Be16GreenPlusRedBothOverViolet,Turbidity,(w558 + w658) / w444,"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 538"
|
||||||
|
Turb_Be16RedOverViolet,Turbidity,w658 / w444,"Beck, R.; Xu, M.; Zhan, S.; Liu, H.; Johansen, R.A.; Tong, S.; Yang, B.; Shu, S.; Wu, Q.; Wang, S.; Berling, K.; Murray, A.; Emery, E.; Reif, M.; Harwood, J.; Young, J.; Martin, M.; Stillings, G.; Stumpf, R.; Su, H.; Ye, Z.; Huang, Y. Comparison of Satellite Reflectance Algorithms for Estimating Phycocyanin Values and Cyanobacterial Total Biovolume in a Temperate Reservoir Using Coincident Hyperspectral Aircraft Imagery and Dense Coincident Surface Observations. Remote Sens. 2017, 9, 539"
|
||||||
|
Turb_Bow06RedOverGreen,Turbidity,w658 / w558,"Bowers, D. G., and C. E. Binding. 2006. 闁炽儲缈籬e Optical Properties of Mineral Suspended Particles: A Review and Synthesis.闁?Estuarine Coastal and Shelf Science 67 (1闁?): 219闁?30. doi:10.1016/j.ecss.2005.11.010"
|
||||||
|
Turb_Chip09NIROverGreen,Turbidity,w857 / w558,"Chipman, J. W.; Olmanson, L.G.; Gitelson, A.A.; Remote sensing methods for lake management: A guide for resource managers and decision-makers. 2009."
|
||||||
|
Turb_Dox02NIRoverRed,Turbidity,w857 / w658,"Doxaran, D., Froidefond, J.-M.; Castaing, P. ; A reflectance band ratio used to estimate suspended matter concentrations in sediment-dominated coastal waters, Remote Sens., 2002, 23, 5079-5085"
|
||||||
|
Turb_Frohn09GreenPlusRedBothOverBlue,Turbidity,(w558 + w658) / w458,"Frohn, R. C., & Autrey, B. C. (2009). Water quality assessment in the Ohio River using new indices for turbidity and chlorophyll-a with Landsat-7 Imagery. Draft Internal Report, US Environmental Protection Agency."
|
||||||
|
Turb_Harr92NIR,Turbidity,w857,"Schiebe F.R., Harrington J.A., Ritchie J.C. Remote-Sensing of Suspended Sediments闁炽儲鏁刪e Lake Chicot, Arkansas Project. Int. J. Remote Sens. 1992;13:1487闁?509"
|
||||||
|
Turb_Lath91RedOverBlue,Turbidity,w658 / w458,"Lathrop, R. G., Jr., T. M. Lillesand, and B. S. Yandell, 1991. Testing the utility of simple multi-date Thematic Mapper calibration algorithms for monitoring turbid inland waters. International Journal of Remote Sensing"
|
||||||
|
Turb_Moore80Red,Turbidity,w658,"Moore, G.K., Satellite remote sensing of water turbidity, Hydrological Sciences, 1980, 25, 4, 407-422"
|
||||||
|
305
src/gui/panels/report_generation_panel.py
Normal file
305
src/gui/panels/report_generation_panel.py
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
ReportGenerationPanel - Word 分析报告生成面板
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt, QThread, pyqtSignal
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout,
|
||||||
|
QLabel, QCheckBox, QPushButton, QLineEdit, QSpinBox,
|
||||||
|
QMessageBox, QFileDialog,
|
||||||
|
)
|
||||||
|
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
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 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")
|
||||||
282
src/gui/panels/step1_panel.py
Normal file
282
src/gui/panels/step1_panel.py
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step1 面板 - 水域掩膜生成
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QLabel,
|
||||||
|
QDoubleSpinBox, QCheckBox, QPushButton, QFormLayout, QRadioButton,
|
||||||
|
QMessageBox,
|
||||||
|
)
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
|
# 从公共组件库导入
|
||||||
|
from src.gui.components.custom_widgets import FileSelectWidget
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 应用QRadioButton样式(实心选中点)
|
||||||
|
radio_style = """
|
||||||
|
QRadioButton::indicator {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid #999;
|
||||||
|
}
|
||||||
|
QRadioButton::indicator:checked {
|
||||||
|
background-color: #0078D7;
|
||||||
|
border: 2px solid #0078D7;
|
||||||
|
}
|
||||||
|
QRadioButton::indicator:unchecked {
|
||||||
|
background-color: white;
|
||||||
|
border: 2px solid #999;
|
||||||
|
}
|
||||||
|
QRadioButton::indicator:hover {
|
||||||
|
border: 2px solid #0078D7;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
self.use_existing_radio.setStyleSheet(radio_style)
|
||||||
|
self.use_ndwi_radio.setStyleSheet(radio_style)
|
||||||
|
|
||||||
|
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参数设置
|
||||||
|
self.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)
|
||||||
|
|
||||||
|
self.ndwi_group.setLayout(ndwi_layout)
|
||||||
|
layout.addWidget(self.ndwi_group)
|
||||||
|
|
||||||
|
# 输出文件路径(使用save模式)
|
||||||
|
self.output_file = FileSelectWidget(
|
||||||
|
"输出掩膜:",
|
||||||
|
"Mask Files (*.dat *.tif);;All Files (*.*)",
|
||||||
|
mode="save"
|
||||||
|
)
|
||||||
|
self.output_file.line_edit.setPlaceholderText("water_mask.dat")
|
||||||
|
layout.addWidget(self.output_file)
|
||||||
|
|
||||||
|
# 提示信息 - 专业的 Info Alert 样式
|
||||||
|
hint = QLabel("💡 提示: 如果掩膜文件是Shapefile(.shp),需要提供参考影像用于栅格化;如果使用NDWI自动生成,只需要提供参考影像")
|
||||||
|
hint.setWordWrap(True) # 允许自动换行
|
||||||
|
hint.setStyleSheet("""
|
||||||
|
QLabel {
|
||||||
|
color: #0055D4;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #E8F4FF;
|
||||||
|
border: 2px solid #0055D4;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin: 8px 0px;
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
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()
|
||||||
|
|
||||||
|
# 动态显示/隐藏组件
|
||||||
|
if use_ndwi:
|
||||||
|
# 使用NDWI模式:隐藏掩膜文件,显示NDWI参数和输出掩膜
|
||||||
|
self.mask_file.setVisible(False)
|
||||||
|
self.ndwi_group.setVisible(True)
|
||||||
|
self.output_file.setVisible(True) # 显示输出掩膜路径
|
||||||
|
|
||||||
|
# 当切换到NDWI模式时,如果工作目录已设置,自动填充输出路径
|
||||||
|
if hasattr(self, 'work_dir') and self.work_dir:
|
||||||
|
self._auto_fill_output_path()
|
||||||
|
else:
|
||||||
|
# 使用现有掩膜模式:显示掩膜文件,隐藏NDWI参数和输出掩膜
|
||||||
|
self.mask_file.setVisible(True)
|
||||||
|
self.ndwi_group.setVisible(False)
|
||||||
|
self.output_file.setVisible(False) # 隐藏输出掩膜路径
|
||||||
|
|
||||||
|
# 参考影像在两种模式下都显示
|
||||||
|
self.img_file.setVisible(True)
|
||||||
|
|
||||||
|
def update_work_directory(self, work_dir):
|
||||||
|
"""
|
||||||
|
保存工作目录引用,用于后续自动填充路径
|
||||||
|
|
||||||
|
Args:
|
||||||
|
work_dir: 工作目录路径
|
||||||
|
"""
|
||||||
|
if not work_dir:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 保存工作目录引用
|
||||||
|
self.work_dir = work_dir
|
||||||
|
|
||||||
|
# 如果当前选中的是NDWI模式,立即填充输出路径
|
||||||
|
if self.use_ndwi_radio.isChecked():
|
||||||
|
self._auto_fill_output_path()
|
||||||
|
|
||||||
|
def _auto_fill_output_path(self):
|
||||||
|
"""
|
||||||
|
自动填充输出掩膜路径(仅在NDWI模式下)
|
||||||
|
确保路径使用正斜杠,避免斜杠混用
|
||||||
|
"""
|
||||||
|
if not hasattr(self, 'work_dir') or not self.work_dir:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 生成输出掩膜的完整路径
|
||||||
|
output_dir = os.path.join(self.work_dir, "1_water_mask")
|
||||||
|
os.makedirs(output_dir, exist_ok=True) # 确保目录存在
|
||||||
|
|
||||||
|
# 统一使用正斜杠,避免 \ 和 / 混用
|
||||||
|
default_output_path = os.path.join(output_dir, "water_mask_out.dat").replace('\\', '/')
|
||||||
|
self.output_file.set_path(default_output_path)
|
||||||
|
|
||||||
|
def get_config(self):
|
||||||
|
"""获取配置"""
|
||||||
|
use_ndwi = self.use_ndwi_radio.isChecked()
|
||||||
|
|
||||||
|
config = {
|
||||||
|
'mask_path': None if use_ndwi else self.mask_file.get_path(),
|
||||||
|
'use_ndwi': use_ndwi,
|
||||||
|
'ndwi_threshold': self.ndwi_threshold.value()
|
||||||
|
}
|
||||||
|
|
||||||
|
# 参考影像路径(两种模式都可能需要)
|
||||||
|
img_path = self.img_file.get_path()
|
||||||
|
if img_path:
|
||||||
|
config['img_path'] = img_path
|
||||||
|
|
||||||
|
# 输出路径:仅在NDWI模式下有效
|
||||||
|
if use_ndwi:
|
||||||
|
output_path = self.output_file.get_path()
|
||||||
|
if output_path:
|
||||||
|
config['output_path'] = output_path
|
||||||
|
else:
|
||||||
|
# 使用现有掩膜时,不传递output_path,避免底层错误尝试保存文件
|
||||||
|
config['output_path'] = None
|
||||||
|
|
||||||
|
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)
|
||||||
207
src/gui/panels/step2_panel.py
Normal file
207
src/gui/panels/step2_panel.py
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step2 面板 - 耀斑区域识别
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QGroupBox, QFormLayout,
|
||||||
|
QDoubleSpinBox, QSpinBox, QComboBox, QCheckBox, QPushButton,
|
||||||
|
QMessageBox,
|
||||||
|
)
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
|
# 从公共组件库导入
|
||||||
|
from src.gui.components.custom_widgets import FileSelectWidget
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
|
||||||
|
|
||||||
|
class Step2Panel(QWidget):
|
||||||
|
"""2. 耀斑区域识别"""
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.work_dir = None
|
||||||
|
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.addItem("Otsu 阈值法", "otsu")
|
||||||
|
self.method.addItem("Z-Score 方法", "zscore")
|
||||||
|
self.method.addItem("百分位数法", "percentile")
|
||||||
|
self.method.addItem("IQR 四分位距法", "iqr")
|
||||||
|
self.method.addItem("自适应阈值法", "adaptive")
|
||||||
|
self.method.addItem("多波段综合法", "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("")
|
||||||
|
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.currentData(), # 使用 currentData() 获取英文ID
|
||||||
|
}
|
||||||
|
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.findData(config['method']) # 使用 findData()
|
||||||
|
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 update_from_config(self, work_dir=None, pipeline=None):
|
||||||
|
"""
|
||||||
|
从全局配置/Pipeline 或 Step1Panel 自动填充路径,实现上下游数据流转
|
||||||
|
|
||||||
|
Args:
|
||||||
|
work_dir: 工作目录路径
|
||||||
|
pipeline: Pipeline 实例,用于获取步骤1生成的水域掩膜路径
|
||||||
|
"""
|
||||||
|
# 保存工作目录引用
|
||||||
|
if work_dir:
|
||||||
|
self.work_dir = work_dir
|
||||||
|
elif hasattr(self, 'work_dir') and self.work_dir:
|
||||||
|
pass # 保持现有工作目录
|
||||||
|
else:
|
||||||
|
self.work_dir = None
|
||||||
|
|
||||||
|
# 1. 尝试从 Pipeline 获取
|
||||||
|
mask_path = None
|
||||||
|
if pipeline and hasattr(pipeline, 'water_mask_path') and pipeline.water_mask_path:
|
||||||
|
mask_path = pipeline.water_mask_path
|
||||||
|
|
||||||
|
# 2. 如果 Pipeline 中没有,则尝试直接从 Step1 界面读取(关键修复)
|
||||||
|
main_window = self.window()
|
||||||
|
if not mask_path and hasattr(main_window, 'step1_panel'):
|
||||||
|
if main_window.step1_panel.use_ndwi_radio.isChecked():
|
||||||
|
# NDWI模式,读取输出框的路径
|
||||||
|
mask_path = main_window.step1_panel.output_file.get_path()
|
||||||
|
else:
|
||||||
|
# 导入现有模式,读取输入框的路径
|
||||||
|
mask_path = main_window.step1_panel.mask_file.get_path()
|
||||||
|
|
||||||
|
# 填充获取到的路径
|
||||||
|
if mask_path:
|
||||||
|
self.water_mask_file.set_path(mask_path)
|
||||||
|
|
||||||
|
# 3. 自动填充输出路径(基于工作目录)
|
||||||
|
if self.work_dir:
|
||||||
|
# 生成输出耀斑掩膜的标准路径:workspace/2_glint_mask/glint_mask_out.dat
|
||||||
|
output_dir = os.path.join(self.work_dir, "2_glint_mask")
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
default_output_path = os.path.join(output_dir, "glint_mask_out.dat").replace('\\', '/')
|
||||||
|
self.output_file.set_path(default_output_path)
|
||||||
|
else:
|
||||||
|
# 没有工作目录时,清空输出路径
|
||||||
|
self.output_file.set_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)
|
||||||
450
src/gui/panels/step3_panel.py
Normal file
450
src/gui/panels/step3_panel.py
Normal file
@ -0,0 +1,450 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step3 面板 - 耀斑去除
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QGroupBox, QFormLayout,
|
||||||
|
QDoubleSpinBox, QSpinBox, QComboBox, QCheckBox, QPushButton,
|
||||||
|
QLabel, QLineEdit, QMessageBox,
|
||||||
|
)
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
|
# 从公共组件库导入
|
||||||
|
from src.gui.components.custom_widgets import FileSelectWidget
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
for text, data in [('Goodman方法', 'goodman'), ('Kutser方法', 'kutser'),
|
||||||
|
('Hedley方法', 'hedley'), ('SUGAR算法', 'sugar')]:
|
||||||
|
self.method.addItem(text, data)
|
||||||
|
self.method.currentIndexChanged.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()
|
||||||
|
for text, data in [('最近邻插值', 'nearest'), ('双线性插值', 'bilinear'),
|
||||||
|
('样条插值', 'spline'), ('克里金插值', 'kriging')]:
|
||||||
|
self.interp_method.addItem(text, data)
|
||||||
|
self.interp_method.setCurrentIndex(1) # 默认双线性插值
|
||||||
|
interp_layout.addRow("插值方法:", self.interp_method)
|
||||||
|
|
||||||
|
interp_group.setLayout(interp_layout)
|
||||||
|
layout.addWidget(interp_group)
|
||||||
|
|
||||||
|
# # 实测经纬度参考点
|
||||||
|
# self.ref_csv_file = FileSelectWidget(
|
||||||
|
# "实测经纬度CSV:",
|
||||||
|
# "CSV Files (*.csv);;All Files (*.*)"
|
||||||
|
# )
|
||||||
|
# self.ref_csv_file.line_edit.setPlaceholderText("可选:包含 Lon/Lat 列的 CSV 文件")
|
||||||
|
# layout.addWidget(self.ref_csv_file)
|
||||||
|
|
||||||
|
# 交互式预览按钮
|
||||||
|
# self.preview_btn = QPushButton("👁️ 打开交互式影像预览")
|
||||||
|
# self.preview_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('info'))
|
||||||
|
# self.preview_btn.clicked.connect(self.open_interactive_viewer)
|
||||||
|
# layout.addWidget(self.preview_btn)
|
||||||
|
|
||||||
|
# 输出文件路径
|
||||||
|
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)
|
||||||
|
# 信号连接:影像文件路径变化时动态更新波段范围
|
||||||
|
self.img_file.line_edit.textChanged.connect(self._update_band_ranges)
|
||||||
|
|
||||||
|
def open_interactive_viewer(self):
|
||||||
|
"""打开交互式影像预览"""
|
||||||
|
from src.gui.water_quality_gui import InteractiveViewerDialog
|
||||||
|
img_path = self.img_file.get_path()
|
||||||
|
if not img_path or not os.path.isfile(img_path):
|
||||||
|
QMessageBox.warning(self, "警告", "请先选择影像文件!")
|
||||||
|
return
|
||||||
|
|
||||||
|
water_mask = self.water_mask_file.get_path()
|
||||||
|
|
||||||
|
dialog = InteractiveViewerDialog(img_path, self)
|
||||||
|
if water_mask and os.path.isfile(water_mask):
|
||||||
|
dialog.load_water_mask(water_mask)
|
||||||
|
dialog.exec_()
|
||||||
|
|
||||||
|
def _update_band_ranges(self, file_path):
|
||||||
|
"""根据选择的影像动态限制波段索引的输入范围"""
|
||||||
|
from osgeo import gdal
|
||||||
|
|
||||||
|
if not file_path or not os.path.isfile(file_path):
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
dataset = gdal.Open(file_path)
|
||||||
|
if dataset is None:
|
||||||
|
return
|
||||||
|
raster_count = dataset.RasterCount
|
||||||
|
max_band = max(0, raster_count - 1)
|
||||||
|
self.nir_lower.setMaximum(max_band)
|
||||||
|
self.nir_upper.setMaximum(max_band)
|
||||||
|
self.oxy_band.setMaximum(max_band)
|
||||||
|
self.nir_band.setMaximum(max_band)
|
||||||
|
self.hedley_nir_band.setMaximum(max_band)
|
||||||
|
dataset = None
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def update_from_config(self, work_dir=None, pipeline=None):
|
||||||
|
"""
|
||||||
|
从 Step1Panel 自动填充水域掩膜路径,实现上下游数据流转
|
||||||
|
|
||||||
|
Args:
|
||||||
|
work_dir: 工作目录路径
|
||||||
|
pipeline: Pipeline 实例(未使用,保留接口兼容性)
|
||||||
|
"""
|
||||||
|
# 保存工作目录引用
|
||||||
|
if work_dir:
|
||||||
|
self.work_dir = work_dir
|
||||||
|
elif hasattr(self, 'work_dir') and self.work_dir:
|
||||||
|
pass # 保持现有工作目录
|
||||||
|
else:
|
||||||
|
self.work_dir = None
|
||||||
|
|
||||||
|
# 从 Step1 界面读取水域掩膜路径
|
||||||
|
main_window = self.window()
|
||||||
|
if hasattr(main_window, 'step1_panel'):
|
||||||
|
if main_window.step1_panel.use_ndwi_radio.isChecked():
|
||||||
|
# NDWI模式,读取输出框的路径
|
||||||
|
mask_path = main_window.step1_panel.output_file.get_path()
|
||||||
|
else:
|
||||||
|
# 导入现有模式,读取输入框的路径
|
||||||
|
mask_path = main_window.step1_panel.mask_file.get_path()
|
||||||
|
|
||||||
|
if mask_path:
|
||||||
|
self.water_mask_file.set_path(mask_path)
|
||||||
|
|
||||||
|
# 自动填充输出路径(基于工作目录)
|
||||||
|
if self.work_dir:
|
||||||
|
output_dir = os.path.join(self.work_dir, "3_deglint")
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
default_output_path = os.path.join(output_dir, "deglint_image.dat").replace('\\', '/')
|
||||||
|
self.output_file.set_path(default_output_path)
|
||||||
|
else:
|
||||||
|
self.output_file.set_path("")
|
||||||
|
|
||||||
|
def _on_method_changed(self, index):
|
||||||
|
"""方法改变时更新参数显示"""
|
||||||
|
method_id = self.method.currentData()
|
||||||
|
self.goodman_group.setVisible(method_id == 'goodman')
|
||||||
|
self.kutser_group.setVisible(method_id == 'kutser')
|
||||||
|
self.hedley_group.setVisible(method_id == 'hedley')
|
||||||
|
self.sugar_group.setVisible(method_id == 'sugar')
|
||||||
|
|
||||||
|
def get_config(self):
|
||||||
|
"""获取配置"""
|
||||||
|
config = {
|
||||||
|
'img_path': self.img_file.get_path(),
|
||||||
|
'method': self.method.currentData(), # 使用 currentData() 获取英文ID
|
||||||
|
'enabled': self.enable_checkbox.isChecked(),
|
||||||
|
'interpolate_zeros': self.interpolate_zeros.isChecked(),
|
||||||
|
'interpolation_method': self.interp_method.currentData(), # 使用 currentData()
|
||||||
|
}
|
||||||
|
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.currentData() # 使用 currentData()
|
||||||
|
|
||||||
|
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.currentData()
|
||||||
|
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 'reference_csv' in config:
|
||||||
|
self.ref_csv_file.set_path(config['reference_csv'])
|
||||||
|
if 'method' in config:
|
||||||
|
idx = self.method.findData(config['method']) # 使用 findData()
|
||||||
|
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.findData(config['interpolation_method']) # 使用 findData()
|
||||||
|
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.findData(config['sugar_glint_mask_method']) # 使用 findData()
|
||||||
|
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)
|
||||||
182
src/gui/panels/step4_panel.py
Normal file
182
src/gui/panels/step4_panel.py
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step4 面板 - 数据预处理
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QGroupBox, QHBoxLayout, QLabel,
|
||||||
|
QSpinBox, QPushButton, QCheckBox, QTableView,
|
||||||
|
QAbstractItemView, QHeaderView, QMessageBox,
|
||||||
|
)
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
|
from src.gui.components.custom_widgets import FileSelectWidget
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
|
||||||
|
|
||||||
|
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 update_from_config(self, work_dir=None, pipeline=None):
|
||||||
|
"""从全局配置自动填充输出路径
|
||||||
|
|
||||||
|
Args:
|
||||||
|
work_dir: 工作目录路径
|
||||||
|
pipeline: Pipeline 实例(未使用,保留接口兼容性)
|
||||||
|
"""
|
||||||
|
if work_dir:
|
||||||
|
self.work_dir = work_dir
|
||||||
|
elif hasattr(self, 'work_dir') and self.work_dir:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.work_dir = None
|
||||||
|
|
||||||
|
if self.work_dir:
|
||||||
|
output_dir = os.path.join(self.work_dir, "4_processed_data")
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
default_output_path = os.path.join(output_dir, "processed_data.csv").replace('\\', '/')
|
||||||
|
self.output_file.set_path(default_output_path)
|
||||||
|
else:
|
||||||
|
self.output_file.set_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文件并点击刷新预览"):
|
||||||
|
"""重置预览表格"""
|
||||||
|
from src.gui.water_quality_gui import PandasTableModel
|
||||||
|
empty_model = PandasTableModel(pd.DataFrame())
|
||||||
|
self.preview_table.setModel(empty_model)
|
||||||
|
self.preview_status_label.setText(message)
|
||||||
|
|
||||||
|
def load_csv_preview(self):
|
||||||
|
"""加载CSV预览数据"""
|
||||||
|
from src.gui.water_quality_gui import PandasTableModel
|
||||||
|
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}")
|
||||||
382
src/gui/panels/step5_5_panel.py
Normal file
382
src/gui/panels/step5_5_panel.py
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step5_5 面板 - 水质指数计算
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Union
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QGroupBox, QFormLayout, QGridLayout,
|
||||||
|
QHBoxLayout, QLabel, QLineEdit, QComboBox, QCheckBox,
|
||||||
|
QPushButton, QMessageBox,
|
||||||
|
)
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
|
from src.gui.components.custom_widgets import FileSelectWidget
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
|
||||||
|
|
||||||
|
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(ModernStylesheet.get_button_stylesheet('success'))
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 自动加载内置公式文件
|
||||||
|
formula_csv_path = (
|
||||||
|
Path(__file__).resolve().parent.parent / "model" / "waterindex.csv"
|
||||||
|
)
|
||||||
|
if formula_csv_path.is_file():
|
||||||
|
self.formula_csv_widget.set_path(str(formula_csv_path))
|
||||||
|
self.refresh_formulas()
|
||||||
|
|
||||||
|
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 update_from_config(self, work_dir=None, pipeline=None):
|
||||||
|
"""从全局配置自动填充训练数据和输出路径
|
||||||
|
|
||||||
|
Args:
|
||||||
|
work_dir: 工作目录路径
|
||||||
|
pipeline: Pipeline 实例(未使用,保留接口兼容性)
|
||||||
|
"""
|
||||||
|
if work_dir:
|
||||||
|
self.work_dir = work_dir
|
||||||
|
elif hasattr(self, 'work_dir') and self.work_dir:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.work_dir = None
|
||||||
|
|
||||||
|
# 1. 自动填入训练数据路径(从 Step5 的输出中获取)
|
||||||
|
# 优先级:直接 widget > pipeline.step_outputs 回退
|
||||||
|
main_window = self.window()
|
||||||
|
if hasattr(main_window, 'step5_panel'):
|
||||||
|
# 优先直接从 Step5 的输出 widget 读取(已运行的最新输出)
|
||||||
|
step5_output = main_window.step5_panel.output_file.get_path()
|
||||||
|
if step5_output:
|
||||||
|
self.training_data_widget.set_path(step5_output)
|
||||||
|
else:
|
||||||
|
# 退而求其次,使用 Step5 的输入 CSV
|
||||||
|
step5_csv = main_window.step5_panel.csv_file.get_path()
|
||||||
|
if step5_csv:
|
||||||
|
self.training_data_widget.set_path(step5_csv)
|
||||||
|
|
||||||
|
# 如果上述都没找到,尝试从 pipeline.step_outputs 回退
|
||||||
|
if not self.training_data_widget.get_path() and pipeline and hasattr(pipeline, 'step_outputs'):
|
||||||
|
step5_outputs = getattr(pipeline, 'step_outputs', {}).get('step5', {})
|
||||||
|
training_path = step5_outputs.get('training_spectra')
|
||||||
|
if training_path:
|
||||||
|
self.training_data_widget.set_path(training_path)
|
||||||
|
|
||||||
|
# 2. 自动填充输出文件名(通用逻辑,由 run_step 根据输入文件名动态覆盖)
|
||||||
|
# 核心方法只接受文件名,不接受完整路径。
|
||||||
|
# 保持默认值,run_step 会根据输入自动填入 _indices.csv 后缀
|
||||||
|
|
||||||
|
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:计算水质指数。
|
||||||
|
|
||||||
|
动态根据输入 CSV 文件名生成输出文件名,自动填入 output_filename 文本框。
|
||||||
|
例如:training_spectra.csv → training_spectra_indices.csv
|
||||||
|
sampling_spectra.csv → sampling_spectra_indices.csv
|
||||||
|
"""
|
||||||
|
# 验证输入
|
||||||
|
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
|
||||||
|
|
||||||
|
# 动态生成输出文件名:自动拼接 _indices 后缀
|
||||||
|
input_name = Path(training_csv_path).stem
|
||||||
|
dynamic_output = f"{input_name}_indices.csv"
|
||||||
|
self.output_filename.setText(dynamic_output)
|
||||||
|
|
||||||
|
# 获取配置
|
||||||
|
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对象")
|
||||||
218
src/gui/panels/step5_panel.py
Normal file
218
src/gui/panels/step5_panel.py
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step5 面板 - 光谱提取
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QGroupBox, QFormLayout, QLabel,
|
||||||
|
QSpinBox, QPushButton, QCheckBox, QMessageBox,
|
||||||
|
)
|
||||||
|
from PyQt5.QtGui import QFont
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
|
from src.gui.components.custom_widgets import FileSelectWidget
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
|
||||||
|
|
||||||
|
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.water_mask_file = FileSelectWidget(
|
||||||
|
"水体掩膜:",
|
||||||
|
"Mask Files (*.dat *.tif);;All Files (*.*)"
|
||||||
|
)
|
||||||
|
self.water_mask_file.line_edit.setPlaceholderText("可选,如不选择则自动生成")
|
||||||
|
layout.addWidget(self.water_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
|
||||||
|
water_mask_path = self.water_mask_file.get_path()
|
||||||
|
if water_mask_path:
|
||||||
|
config['boundary_path'] = water_mask_path
|
||||||
|
glint_mask_path = self.glint_mask_file.get_path()
|
||||||
|
if glint_mask_path:
|
||||||
|
config['glint_mask_path'] = glint_mask_path
|
||||||
|
# 注意:step5_extract_training_spectra 不接受 output_path / training_spectra_path
|
||||||
|
# 参数,输出路径由 pipeline 内部根据 training_spectra_dir 自动生成。
|
||||||
|
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.water_mask_file.set_path(config['boundary_path'])
|
||||||
|
if 'glint_mask_path' in config:
|
||||||
|
self.glint_mask_file.set_path(config['glint_mask_path'])
|
||||||
|
|
||||||
|
def update_from_config(self, work_dir=None, pipeline=None):
|
||||||
|
"""从全局配置/Pipeline 或 Step1Panel 自动填充路径,实现上下游数据流转
|
||||||
|
|
||||||
|
Args:
|
||||||
|
work_dir: 工作目录路径
|
||||||
|
pipeline: Pipeline 实例,用于获取步骤1生成的水域掩膜路径
|
||||||
|
"""
|
||||||
|
# 保存工作目录引用
|
||||||
|
if work_dir:
|
||||||
|
self.work_dir = work_dir
|
||||||
|
elif hasattr(self, 'work_dir') and self.work_dir:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.work_dir = None
|
||||||
|
|
||||||
|
# 1. 尝试从 Pipeline 获取水体掩膜路径
|
||||||
|
mask_path = None
|
||||||
|
if pipeline and hasattr(pipeline, 'water_mask_path') and pipeline.water_mask_path:
|
||||||
|
mask_path = pipeline.water_mask_path
|
||||||
|
|
||||||
|
# 2. 如果 Pipeline 中没有,则尝试直接从 Step1 界面读取
|
||||||
|
main_window = self.window()
|
||||||
|
if not mask_path and hasattr(main_window, 'step1_panel'):
|
||||||
|
if main_window.step1_panel.use_ndwi_radio.isChecked():
|
||||||
|
mask_path = main_window.step1_panel.output_file.get_path()
|
||||||
|
else:
|
||||||
|
mask_path = main_window.step1_panel.mask_file.get_path()
|
||||||
|
|
||||||
|
# 填充水体掩膜路径
|
||||||
|
if mask_path:
|
||||||
|
self.water_mask_file.set_path(mask_path)
|
||||||
|
|
||||||
|
# 3. 尝试从 Step2 界面读取耀斑掩膜路径
|
||||||
|
main_window = self.window()
|
||||||
|
if hasattr(main_window, 'step2_panel'):
|
||||||
|
glint_path = main_window.step2_panel.output_file.get_path()
|
||||||
|
if glint_path:
|
||||||
|
self.glint_mask_file.set_path(glint_path)
|
||||||
|
|
||||||
|
# 4. 自动填充输出路径(基于工作目录)
|
||||||
|
if self.work_dir:
|
||||||
|
output_dir = os.path.join(self.work_dir, "5_training_spectra")
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
default_output_path = os.path.join(output_dir, "training_spectra.csv").replace('\\', '/')
|
||||||
|
self.output_file.set_path(default_output_path)
|
||||||
|
else:
|
||||||
|
self.output_file.set_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)
|
||||||
245
src/gui/panels/step6_5_panel.py
Normal file
245
src/gui/panels/step6_5_panel.py
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step6_5 面板 - 非经验统计回归建模
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QGroupBox, QFormLayout, QGridLayout,
|
||||||
|
QHBoxLayout, QLabel, QCheckBox, QSpinBox, QPushButton,
|
||||||
|
QFileDialog, QMessageBox,
|
||||||
|
)
|
||||||
|
|
||||||
|
from src.gui.components.custom_widgets import FileSelectWidget
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
|
||||||
|
|
||||||
|
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_Regression_Modeling")
|
||||||
|
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(ModernStylesheet.get_button_stylesheet('success'))
|
||||||
|
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:
|
||||||
|
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_Regression_Modeling")
|
||||||
|
else:
|
||||||
|
output_dir = str(Path.cwd() / "8_Regression_Modeling")
|
||||||
|
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()
|
||||||
|
|
||||||
|
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)
|
||||||
327
src/gui/panels/step6_75_panel.py
Normal file
327
src/gui/panels/step6_75_panel.py
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step6_75 面板 - 自定义回归分析
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QGroupBox, QFormLayout, QGridLayout,
|
||||||
|
QHBoxLayout, QLabel, QLineEdit, QCheckBox, QPushButton,
|
||||||
|
QScrollArea, QMessageBox,
|
||||||
|
)
|
||||||
|
|
||||||
|
from src.gui.components.custom_widgets import FileSelectWidget
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
|
||||||
|
|
||||||
|
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.setMinimumHeight(250)
|
||||||
|
x_scroll.setMaximumHeight(350)
|
||||||
|
|
||||||
|
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.setMinimumHeight(200)
|
||||||
|
y_scroll.setMaximumHeight(300)
|
||||||
|
|
||||||
|
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_Modeling")
|
||||||
|
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(ModernStylesheet.get_button_stylesheet('success'))
|
||||||
|
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:
|
||||||
|
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):
|
||||||
|
"""更新列选择组件"""
|
||||||
|
for checkbox in self.x_column_checkboxes.values():
|
||||||
|
checkbox.setParent(None)
|
||||||
|
self.x_column_checkboxes.clear()
|
||||||
|
|
||||||
|
for checkbox in self.y_column_checkboxes.values():
|
||||||
|
checkbox.setParent(None)
|
||||||
|
self.y_column_checkboxes.clear()
|
||||||
|
|
||||||
|
if not self.csv_columns:
|
||||||
|
return
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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'])
|
||||||
|
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_Modeling")
|
||||||
|
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()
|
||||||
|
|
||||||
|
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对象")
|
||||||
320
src/gui/panels/step6_panel.py
Normal file
320
src/gui/panels/step6_panel.py
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step6 面板 - 机器学习建模
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QGroupBox, QFormLayout, QGridLayout,
|
||||||
|
QHBoxLayout, QLabel, QLineEdit, QSpinBox, QCheckBox,
|
||||||
|
QPushButton, QFileDialog, QMessageBox,
|
||||||
|
)
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
|
from src.gui.components.custom_widgets import FileSelectWidget
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
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']}; "
|
||||||
|
f"padding: 5px; border: 1px solid {ModernStylesheet.COLORS['border_light']}; "
|
||||||
|
f"border-radius: 3px;"
|
||||||
|
)
|
||||||
|
model_grid.addWidget(group_label, row, 0, 1, 4)
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
for i, model in enumerate(models):
|
||||||
|
checkbox = QCheckBox(model)
|
||||||
|
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
|
||||||
|
# 注意:step6_train_models 不接受 output_dir 参数,输出目录由 pipeline 内部根据 work_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 update_from_config(self, work_dir=None, pipeline=None):
|
||||||
|
"""从全局配置自动填充训练数据和输出路径
|
||||||
|
|
||||||
|
Args:
|
||||||
|
work_dir: 工作目录路径
|
||||||
|
pipeline: Pipeline 实例(未使用,保留接口兼容性)
|
||||||
|
"""
|
||||||
|
if work_dir:
|
||||||
|
self.work_dir = work_dir
|
||||||
|
elif hasattr(self, 'work_dir') and self.work_dir:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.work_dir = None
|
||||||
|
|
||||||
|
# 1. 尝试从 Step5 界面读取训练数据路径
|
||||||
|
main_window = self.window()
|
||||||
|
if hasattr(main_window, 'step5_panel'):
|
||||||
|
# 优先直接从 Step5 的输出 widget 读取
|
||||||
|
step5_output = main_window.step5_panel.output_file.get_path()
|
||||||
|
if step5_output:
|
||||||
|
self.training_csv_file.set_path(step5_output)
|
||||||
|
elif hasattr(main_window, 'step5_panel') and hasattr(main_window.step5_panel, 'get_config'):
|
||||||
|
# 回退:从 Step5 的 config 字典中查找可能的键名
|
||||||
|
step5_cfg = main_window.step5_panel.get_config()
|
||||||
|
step5_csv = (
|
||||||
|
step5_cfg.get('training_spectra_path')
|
||||||
|
or step5_cfg.get('output_file')
|
||||||
|
or step5_cfg.get('csv_path')
|
||||||
|
or step5_cfg.get('output_csv')
|
||||||
|
)
|
||||||
|
if step5_csv:
|
||||||
|
self.training_csv_file.set_path(step5_csv)
|
||||||
|
|
||||||
|
# 2. 自动填充输出目录(基于工作目录)
|
||||||
|
if self.work_dir:
|
||||||
|
output_dir = os.path.join(self.work_dir, "7_Supervised_Model_Training")
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
self.output_dir.set_path(output_dir.replace('\\', '/'))
|
||||||
|
else:
|
||||||
|
self.output_dir.set_path("")
|
||||||
|
|
||||||
|
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()]
|
||||||
|
}
|
||||||
182
src/gui/panels/step7_panel.py
Normal file
182
src/gui/panels/step7_panel.py
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step7 面板 - 采样点生成
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QGroupBox, QFormLayout,
|
||||||
|
QPushButton, QCheckBox, QSpinBox, QMessageBox,
|
||||||
|
)
|
||||||
|
|
||||||
|
from src.gui.components.custom_widgets import FileSelectWidget
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
# 注意:step7_generate_sampling_points 不接受 output_path 参数,输出路径由 pipeline 内部自动生成
|
||||||
|
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 'glint_mask_path' in config:
|
||||||
|
self.glint_mask_file.set_path(config['glint_mask_path'])
|
||||||
|
|
||||||
|
def update_from_config(self, work_dir=None, pipeline=None):
|
||||||
|
"""从全局配置自动填充去耀斑影像和掩膜路径
|
||||||
|
|
||||||
|
Args:
|
||||||
|
work_dir: 工作目录路径
|
||||||
|
pipeline: Pipeline 实例(用于从 step_outputs 获取绝对路径)
|
||||||
|
"""
|
||||||
|
if work_dir:
|
||||||
|
self.work_dir = work_dir
|
||||||
|
elif hasattr(self, 'work_dir') and self.work_dir:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.work_dir = None
|
||||||
|
|
||||||
|
main_window = self.window()
|
||||||
|
|
||||||
|
# 1. 填充去耀斑影像路径(优先从 pipeline.step_outputs 获取绝对路径)
|
||||||
|
deglint_path = None
|
||||||
|
if pipeline and hasattr(pipeline, 'step_outputs'):
|
||||||
|
step3_outputs = getattr(pipeline, 'step_outputs', {}).get('step3', {})
|
||||||
|
deglint_path = (
|
||||||
|
step3_outputs.get('deglint_image')
|
||||||
|
or step3_outputs.get('output_path')
|
||||||
|
or step3_outputs.get('output_file')
|
||||||
|
or step3_outputs.get('deglint_img_path')
|
||||||
|
)
|
||||||
|
# 回退:从 step3 面板 widget 直接读取(可能是相对路径)
|
||||||
|
if not deglint_path and hasattr(main_window, 'step3_panel'):
|
||||||
|
deglint_path = main_window.step3_panel.output_file.get_path()
|
||||||
|
|
||||||
|
if deglint_path:
|
||||||
|
self.deglint_img_file.set_path(deglint_path)
|
||||||
|
|
||||||
|
# 2. 填充水域掩膜路径(优先从 pipeline.step_outputs 获取绝对路径)
|
||||||
|
water_mask_path = None
|
||||||
|
if pipeline and hasattr(pipeline, 'step_outputs'):
|
||||||
|
step1_outputs = getattr(pipeline, 'step_outputs', {}).get('step1', {})
|
||||||
|
water_mask_path = (
|
||||||
|
step1_outputs.get('water_mask')
|
||||||
|
or step1_outputs.get('output_path')
|
||||||
|
or step1_outputs.get('output_file')
|
||||||
|
)
|
||||||
|
# 回退:从 step1 面板 widget 直接读取
|
||||||
|
if not water_mask_path and hasattr(main_window, 'step1_panel'):
|
||||||
|
water_mask_path = main_window.step1_panel.output_file.get_path()
|
||||||
|
|
||||||
|
if water_mask_path:
|
||||||
|
self.water_mask_file.set_path(water_mask_path)
|
||||||
|
|
||||||
|
# 3. 自动填充输出路径(绝对路径)
|
||||||
|
if self.work_dir:
|
||||||
|
output_path = os.path.join(self.work_dir, "10_sampling", "sampling_spectra.csv")
|
||||||
|
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||||||
|
self.output_file.set_path(output_path.replace('\\', '/'))
|
||||||
|
|
||||||
|
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)
|
||||||
143
src/gui/panels/step8_5_panel.py
Normal file
143
src/gui/panels/step8_5_panel.py
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step8_5 面板 - 非经验模型预测
|
||||||
|
"""
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QGroupBox, QFormLayout,
|
||||||
|
QPushButton, QCheckBox, QComboBox, QLineEdit, QMessageBox,
|
||||||
|
QFileDialog,
|
||||||
|
)
|
||||||
|
|
||||||
|
from src.gui.components.custom_widgets import FileSelectWidget
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
|
||||||
|
|
||||||
|
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(ModernStylesheet.get_button_stylesheet('success'))
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
|
||||||
|
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对象")
|
||||||
140
src/gui/panels/step8_75_panel.py
Normal file
140
src/gui/panels/step8_75_panel.py
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step8_75 面板 - 自定义回归预测
|
||||||
|
"""
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QGroupBox,
|
||||||
|
QPushButton, QCheckBox, QMessageBox, QFileDialog,
|
||||||
|
)
|
||||||
|
|
||||||
|
from src.gui.components.custom_widgets import FileSelectWidget
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 自定义回归模型目录选择(9_Custom_Regression_Modeling)
|
||||||
|
self.regression_models_dir = FileSelectWidget(
|
||||||
|
"回归模型目录:",
|
||||||
|
"Directories;;All Files (*.*)"
|
||||||
|
)
|
||||||
|
self.regression_models_dir.label.setText("回归模型目录:")
|
||||||
|
self.regression_models_dir.browse_btn.clicked.disconnect()
|
||||||
|
self.regression_models_dir.browse_btn.clicked.connect(self.browse_regression_models_dir)
|
||||||
|
self.regression_models_dir.set_path("9_Custom_Regression_Modeling")
|
||||||
|
layout.addWidget(self.regression_models_dir)
|
||||||
|
|
||||||
|
# 公式CSV文件选择(用于查找index_formula)
|
||||||
|
self.formula_csv_file = FileSelectWidget(
|
||||||
|
"公式CSV文件:",
|
||||||
|
"CSV Files (*.csv);;All Files (*.*)"
|
||||||
|
)
|
||||||
|
self.formula_csv_file.label.setText("公式CSV文件:")
|
||||||
|
layout.addWidget(self.formula_csv_file)
|
||||||
|
|
||||||
|
# 输出目录选择
|
||||||
|
self.output_dir_widget = FileSelectWidget(
|
||||||
|
"输出目录:",
|
||||||
|
"Directories;;All Files (*.*)"
|
||||||
|
)
|
||||||
|
self.output_dir_widget.label.setText("输出目录:")
|
||||||
|
self.output_dir_widget.browse_btn.clicked.disconnect()
|
||||||
|
self.output_dir_widget.browse_btn.clicked.connect(self.browse_output_dir)
|
||||||
|
self.output_dir_widget.line_edit.setPlaceholderText("留空使用默认prediction目录")
|
||||||
|
layout.addWidget(self.output_dir_widget)
|
||||||
|
|
||||||
|
# 启用步骤
|
||||||
|
self.enable_checkbox = QCheckBox("启用此步骤")
|
||||||
|
self.enable_checkbox.setChecked(True)
|
||||||
|
layout.addWidget(self.enable_checkbox)
|
||||||
|
|
||||||
|
# 独立运行按钮
|
||||||
|
self.run_button = QPushButton("独立运行此步骤")
|
||||||
|
self.run_button.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
|
||||||
|
self.run_button.clicked.connect(self.run_step)
|
||||||
|
layout.addWidget(self.run_button)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
def browse_regression_models_dir(self):
|
||||||
|
"""浏览回归模型目录"""
|
||||||
|
dir_path = QFileDialog.getExistingDirectory(self, "选择回归模型目录", "")
|
||||||
|
if dir_path:
|
||||||
|
self.regression_models_dir.set_path(dir_path)
|
||||||
|
|
||||||
|
def browse_output_dir(self):
|
||||||
|
"""浏览输出目录"""
|
||||||
|
dir_path = QFileDialog.getExistingDirectory(self, "选择输出目录", "")
|
||||||
|
if dir_path:
|
||||||
|
self.output_dir_widget.set_path(dir_path)
|
||||||
|
|
||||||
|
def get_config(self):
|
||||||
|
"""获取配置"""
|
||||||
|
config = {
|
||||||
|
'enabled': self.enable_checkbox.isChecked()
|
||||||
|
}
|
||||||
|
sampling_csv_path = self.sampling_csv_file.get_path()
|
||||||
|
if sampling_csv_path:
|
||||||
|
config['sampling_csv_path'] = sampling_csv_path
|
||||||
|
regression_models_dir = self.regression_models_dir.get_path()
|
||||||
|
if regression_models_dir:
|
||||||
|
config['custom_regression_dir'] = regression_models_dir
|
||||||
|
formula_csv_path = self.formula_csv_file.get_path()
|
||||||
|
if formula_csv_path:
|
||||||
|
config['formula_csv_path'] = formula_csv_path
|
||||||
|
output_dir = self.output_dir_widget.get_path()
|
||||||
|
if output_dir:
|
||||||
|
config['output_dir'] = output_dir
|
||||||
|
return config
|
||||||
|
|
||||||
|
def set_config(self, config):
|
||||||
|
"""设置配置"""
|
||||||
|
if 'sampling_csv_path' in config:
|
||||||
|
self.sampling_csv_file.set_path(config['sampling_csv_path'])
|
||||||
|
if 'custom_regression_dir' in config:
|
||||||
|
self.regression_models_dir.set_path(config['custom_regression_dir'])
|
||||||
|
if 'formula_csv_path' in config:
|
||||||
|
self.formula_csv_file.set_path(config['formula_csv_path'])
|
||||||
|
if 'output_dir' in config:
|
||||||
|
self.output_dir_widget.set_path(config['output_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
|
||||||
|
regression_models_dir = self.regression_models_dir.get_path()
|
||||||
|
if not regression_models_dir:
|
||||||
|
QMessageBox.warning(self, "输入错误", "请选择回归模型目录!")
|
||||||
|
return
|
||||||
|
|
||||||
|
config = self.get_config()
|
||||||
|
|
||||||
|
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对象")
|
||||||
131
src/gui/panels/step8_panel.py
Normal file
131
src/gui/panels/step8_panel.py
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step8 面板 - 机器学习预测
|
||||||
|
"""
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QGroupBox, QFormLayout,
|
||||||
|
QPushButton, QCheckBox, QComboBox, QLineEdit, QMessageBox,
|
||||||
|
QFileDialog,
|
||||||
|
)
|
||||||
|
|
||||||
|
from src.gui.components.custom_widgets import FileSelectWidget
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
375
src/gui/panels/step9_panel.py
Normal file
375
src/gui/panels/step9_panel.py
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step9 面板 - 分布图生成
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt, QThread, pyqtSignal
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QWidget, QVBoxLayout, QGroupBox, QFormLayout, QHBoxLayout,
|
||||||
|
QLabel, QCheckBox, QPushButton, QLineEdit, QDoubleSpinBox,
|
||||||
|
QRadioButton, QButtonGroup, QMessageBox, QFileDialog,
|
||||||
|
)
|
||||||
|
|
||||||
|
from src.gui.components.custom_widgets import FileSelectWidget
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
|
||||||
|
# Pipeline 可用性(与 core/worker_thread.py 保持一致)
|
||||||
|
try:
|
||||||
|
from src.core.water_quality_inversion_pipeline_GUI import WaterQualityInversionPipeline
|
||||||
|
PIPELINE_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
PIPELINE_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 输出目录
|
||||||
|
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(ModernStylesheet.get_button_stylesheet('success'))
|
||||||
|
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):
|
||||||
|
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"""
|
||||||
|
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")
|
||||||
1486
src/gui/panels/visualization_panel.py
Normal file
1486
src/gui/panels/visualization_panel.py
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user