feat(step8): 新增 Step8 水色指数反演 GUI 面板 step8_waterindex_panel

This commit is contained in:
DXC
2026-06-10 17:13:37 +08:00
parent 320f2f18f2
commit 2671c0837a
3 changed files with 1307 additions and 0 deletions

View File

@ -0,0 +1,315 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Step8 面板 - QAA 物理反演(非经验模型)
"""
import os
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QGroupBox, QFormLayout, QGridLayout,
QHBoxLayout, QLabel, QLineEdit, QComboBox, QCheckBox,
QPushButton, QFileDialog, 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
from src.utils.water_owt_config import (
get_all_lake_names,
get_lake_config,
get_lambda_0,
get_default_lake,
)
from src.core.algorithms.qaa import QAABaselineSolver
class Step8QAAPanel(QWidget):
"""步骤8QAA 物理反演(非经验模型)"""
def __init__(self, parent=None):
super().__init__(parent)
self.init_ui()
def init_ui(self):
layout = QVBoxLayout()
title = QLabel("步骤8QAA 物理反演(非经验模型)")
title.setFont(QFont("Arial", 12, QFont.Bold))
layout.addWidget(title)
# 光谱 CSV 文件输入
self.spectrum_csv_file = FileSelectWidget(
"光谱 CSV 文件:",
"CSV Files (*.csv);;All Files (*.*)"
)
self.spectrum_csv_file.line_edit.setPlaceholderText(
"选择实测光谱或采样点光谱 CSV含波长列"
)
layout.addWidget(self.spectrum_csv_file)
# 水域类型选择
lake_group = QGroupBox("水域类型配置")
lake_layout = QFormLayout()
self.lake_combo = QComboBox()
lake_names = get_all_lake_names()
self.lake_combo.addItems(lake_names)
default_lake = get_default_lake()
if default_lake in lake_names:
self.lake_combo.setCurrentText(default_lake)
self.lake_combo.currentTextChanged.connect(self._on_lake_changed)
lake_layout.addRow("水域选择:", self.lake_combo)
# 参考波长显示
self.lambda_0_label = QLabel()
self.lambda_0_label.setStyleSheet(
f"color: {ModernStylesheet.COLORS['accent']}; "
f"font-weight: bold;"
)
lake_layout.addRow("参考波长 λ₀:", self.lambda_0_label)
# 算法提示
self.hint_label = QLabel()
self.hint_label.setWordWrap(True)
self.hint_label.setStyleSheet(
f"color: {ModernStylesheet.COLORS['text_secondary']}; "
"font-size: 12px;"
)
lake_layout.addRow("算法提示:", self.hint_label)
lake_group.setLayout(lake_layout)
layout.addWidget(lake_group)
# 输出路径
self.output_path = FileSelectWidget(
"输出文件:",
"CSV Files (*.csv);;All Files (*.*)",
mode="save"
)
self.output_path.line_edit.setPlaceholderText(
"自动生成到 8_QAA_Inversion或手动指定..."
)
self.output_path.browse_btn.clicked.disconnect()
self.output_path.browse_btn.clicked.connect(self.browse_output_path)
layout.addWidget(self.output_path)
# 启用步骤
self.enable_checkbox = QCheckBox("启用此步骤")
self.enable_checkbox.setChecked(False)
layout.addWidget(self.enable_checkbox)
# 独立运行按钮
self.run_btn = QPushButton("执行 QAA 反演")
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._on_lake_changed(self.lake_combo.currentText())
def _on_lake_changed(self, lake_name: str):
"""当用户切换水域时更新显示"""
cfg = get_lake_config(lake_name)
if cfg:
self.lambda_0_label.setText(
f"{cfg['lambda_0']} nm{cfg['qaa_version']}"
)
self.hint_label.setText(cfg.get('notes', ''))
else:
self.lambda_0_label.setText("")
self.hint_label.setText("")
def _get_default_work_dir(self) -> str:
"""获取 work_dir优先用 panel 自身缓存的,否则尝试从主窗口取"""
if hasattr(self, 'work_dir') and self.work_dir:
return str(self.work_dir)
mw = self.window()
if mw and hasattr(mw, 'work_dir') and mw.work_dir:
return str(mw.work_dir)
return ""
def browse_output_path(self):
"""浏览输出文件路径"""
current = self.output_path.get_path().strip()
if current:
initial_dir = os.path.dirname(current)
initial_file = os.path.basename(current)
else:
initial_dir = ""
initial_file = ""
if not initial_dir or not os.path.isdir(initial_dir):
work_dir = self._get_default_work_dir()
initial_dir = os.path.join(work_dir, "8_QAA_Inversion") if work_dir else ""
if initial_dir and not os.path.isdir(initial_dir):
os.makedirs(initial_dir, exist_ok=True)
file_path, _ = QFileDialog.getSaveFileName(
self, "保存输出文件",
os.path.join(initial_dir, initial_file) if initial_file else initial_dir,
"CSV Files (*.csv);;All Files (*.*)"
)
if file_path:
self.output_path.set_path(file_path)
def get_config(self) -> dict:
"""获取面板配置"""
config = {
'lake_name': self.lake_combo.currentText(),
'lambda_0': get_lambda_0(self.lake_combo.currentText()),
'spectrum_csv_path': self.spectrum_csv_file.get_path(),
}
output_path = self.output_path.get_path()
if output_path:
config['output_path'] = output_path
return config
def set_config(self, config: dict):
"""设置面板配置"""
if 'lake_name' in config:
lake_name = config['lake_name']
idx = self.lake_combo.findText(lake_name)
if idx >= 0:
self.lake_combo.setCurrentIndex(idx)
if 'spectrum_csv_path' in config:
self.spectrum_csv_file.set_path(config['spectrum_csv_path'])
if 'output_path' in config:
self.output_path.set_path(config['output_path'])
def update_from_config(self, work_dir=None, pipeline=None):
"""从全局配置自动填充训练数据和输出路径"""
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()
if main_window and hasattr(main_window, 'step5_panel'):
step5_output = main_window.step5_panel.output_file.get_path()
if step5_output:
if not os.path.isabs(step5_output):
step5_output = os.path.join(self.work_dir or '', step5_output).replace('\\', '/')
self.spectrum_csv_file.set_path(step5_output)
if self.work_dir:
qaa_dir = os.path.join(self.work_dir, "8_QAA_Inversion")
os.makedirs(qaa_dir, exist_ok=True)
output_path = os.path.join(qaa_dir, "a_lambda_results.csv").replace('\\', '/')
self.output_path.set_path(output_path)
else:
self.output_path.set_path("")
def run_step(self):
"""独立运行 QAA 反演"""
spectrum_path = self.spectrum_csv_file.get_path()
if not spectrum_path:
QMessageBox.warning(self, "输入错误", "请选择光谱 CSV 文件!")
return
main_window = self.window()
if hasattr(main_window, 'run_single_step'):
config = {'step8_qaa': self.get_config()}
main_window.run_single_step('step8_qaa', config)
else:
self._run_qaa_direct()
def _run_qaa_direct(self):
"""直接执行 QAA 反演(不依赖主窗口流水线)"""
spectrum_path = self.spectrum_csv_file.get_path()
output_path = self.output_path.get_path()
lake_name = self.lake_combo.currentText()
lambda_0 = get_lambda_0(lake_name)
if not output_path:
work_dir = self._get_default_work_dir()
qaa_dir = os.path.join(work_dir, "8_QAA_Inversion") if work_dir else ""
if qaa_dir and not os.path.isdir(qaa_dir):
os.makedirs(qaa_dir, exist_ok=True)
output_path = os.path.join(qaa_dir, "a_lambda_results.csv").replace('\\', '/')
try:
import numpy as np
import pandas as pd
df = pd.read_csv(spectrum_path, encoding="utf-8-sig")
col_names = df.columns.tolist()
wavelength_col_idx = None
for i, col in enumerate(col_names):
try:
float(col)
wavelength_col_idx = i
break
except (ValueError, TypeError):
pass
if wavelength_col_idx is None:
QMessageBox.warning(
self, "解析错误",
"无法从 CSV 列名中识别波长信息请确保列名包含数值型波长nm"
)
return
wavelengths = np.array(
[float(c) for c in col_names[wavelength_col_idx:]], dtype=np.float64
)
data_matrix = df.iloc[:, wavelength_col_idx:].values.astype(np.float64)
if data_matrix.ndim == 1:
data_matrix = data_matrix[np.newaxis, :]
solver = QAABaselineSolver()
raw_result = solver.run_inversion(wavelengths, data_matrix, lambda_0)
# run_inversion 返回:单样本 → dict多样本 → list[dict]
if isinstance(raw_result, list):
sample_results = raw_result
aw_0 = raw_result[0].get('aw_0', 0)
bbw_0 = raw_result[0].get('bbw_0', 0)
else:
sample_results = [raw_result]
aw_0 = raw_result.get('aw_0', 0)
bbw_0 = raw_result.get('bbw_0', 0)
rows_out = []
for i, sample_result in enumerate(sample_results):
wl_arr = wavelengths
a_arr = sample_result['a_lambda']
bb_arr = sample_result['bb_lambda']
for j, wl in enumerate(wl_arr):
rows_out.append({
'sample_id': f"sample_{i}",
'Wavelength': wl,
'a_lambda': a_arr[j],
'bb_lambda': bb_arr[j],
})
result_df = pd.DataFrame(rows_out)
os.makedirs(os.path.dirname(output_path) or '.', exist_ok=True)
result_df.to_csv(output_path, index=False, float_format='%.8f')
QMessageBox.information(
self, "执行成功",
f"QAA 反演完成!\n"
f"水域: {lake_name}\n"
f"参考波长 λ₀: {lambda_0} nm\n"
f"λ₀ 处 aw: {aw_0:.6f} m⁻¹\n"
f"λ₀ 处 bbw: {bbw_0:.6f} m⁻¹\n"
f"结果已保存到:\n{output_path}"
)
except Exception as e:
QMessageBox.critical(self, "执行错误", f"QAA 反演失败:\n{str(e)}")
def get_training_params(self) -> dict:
"""获取反演参数"""
return {
'pipeline_type': 'qaa_non_empirical',
'lake_name': self.lake_combo.currentText(),
'lambda_0': get_lambda_0(self.lake_combo.currentText()),
}