步骤七页面修改
This commit is contained in:
@ -18,7 +18,7 @@ from src.gui.panels.step3_panel import Step3Panel
|
||||
from src.gui.panels.step4_sampling_panel import Step4SamplingPanel
|
||||
from src.gui.panels.step5_clean_panel import Step5CleanPanel
|
||||
from src.gui.panels.step6_feature_panel import Step6FeaturePanel
|
||||
from src.new.views.step7_view import Step7View
|
||||
from src.gui.panels.step7_inversion_panel import Step7InversionPanel
|
||||
from src.gui.panels.step8_ml_train_panel import Step8MlTrainPanel
|
||||
from src.gui.panels.step9_ml_predict_panel import Step9MlPredictPanel
|
||||
from src.gui.panels.step10_watercolor_panel import Step10WatercolorPanel
|
||||
@ -128,7 +128,7 @@ PANEL_REGISTRY = [
|
||||
},
|
||||
{
|
||||
'step_id': 'step7_index',
|
||||
'class_ref': Step7View,
|
||||
'class_ref': Step7InversionPanel,
|
||||
'title': '水质光谱指数计算',
|
||||
'icon': '7.png',
|
||||
'stage': '模块二 特征工程与数据',
|
||||
|
||||
425
src/gui/panels/step7_inversion_panel.py
Normal file
425
src/gui/panels/step7_inversion_panel.py
Normal file
@ -0,0 +1,425 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Step7 视图 - 水质光谱指数计算 (完美对齐卡片化重构版)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import pandas as pd
|
||||
from pathlib import Path
|
||||
|
||||
from PyQt5.QtWidgets import (
|
||||
QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout,
|
||||
QLabel, QPushButton, QMessageBox, QListWidget,
|
||||
QListWidgetItem, QSizePolicy, QWidget
|
||||
)
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
from src.gui.core.event_bus import global_event_bus
|
||||
|
||||
from src.gui.components.custom_widgets import FileSelectWidget
|
||||
from src.gui.styles import ModernStylesheet
|
||||
|
||||
|
||||
class Step7InversionPanel(QWidget):
|
||||
"""步骤7:水质光谱指数计算"""
|
||||
|
||||
# --- 保持原有的类级别映射表不变 ---
|
||||
_formula_type_map = {
|
||||
'NDWI': 'ratio', 'MNDWI': 'ratio', 'AWEI': 'ratio', 'AWEInsh': 'ratio',
|
||||
'NDTI': 'ratio', 'WRI': 'ratio', 'NDSS': 'ratio', 'NDSSI': 'ratio',
|
||||
'NDVI': 'ratio', 'EVI': 'ratio', 'SABI': 'ratio', 'WAVI': 'ratio',
|
||||
'FAI': 'ratio', 'FAI2': 'ratio', 'FAI3': 'ratio', 'FAI4': 'ratio',
|
||||
'KIV': 'ratio', 'MCI': 'ratio', 'NDBI': 'ratio', 'CYA': 'ratio',
|
||||
|
||||
'Chla_3B': 'concentration', 'Chla_2B': 'concentration', 'Chla_2B_sim': 'concentration',
|
||||
'Chla_4B': 'concentration', 'Chla_NDCI': 'concentration', 'Chla_NDI': 'concentration',
|
||||
'Chla_Peak': 'concentration', 'Chla_SABG': 'concentration', 'Chla_MCI': 'concentration',
|
||||
'Chla_FAI': 'concentration',
|
||||
'TP_1': 'concentration', 'TP_2': 'concentration', 'TP_3': 'concentration',
|
||||
'TP_4': 'concentration', 'TP_5': 'concentration', 'TP_6': 'concentration',
|
||||
'TN_1': 'concentration', 'TN_2': 'concentration', 'TN_3': 'concentration',
|
||||
'TN_4': 'concentration',
|
||||
'NH3_1': 'concentration', 'NH3_2': 'concentration', 'NH3_3': 'concentration',
|
||||
'NH3_4': 'concentration',
|
||||
'COD_1': 'concentration', 'COD_2': 'concentration', 'COD_3': 'concentration',
|
||||
'COD_4': 'concentration'
|
||||
}
|
||||
|
||||
_formula_color_map = {
|
||||
'ratio': ModernStylesheet.COLORS['panel_bg'], # 比值型用白色底
|
||||
'concentration': ModernStylesheet.COLORS['primary_light'] # 浓度型用浅蓝底
|
||||
}
|
||||
|
||||
_formula_coef_map = {
|
||||
'Chla_3B': [25.0456, 116.8924],
|
||||
'Chla_2B': [5.8753, 56.4025],
|
||||
'Chla_2B_sim': [38.16, 21.36],
|
||||
'Chla_4B': [27.79, 137.9],
|
||||
'Chla_NDCI': [14.039, 86.115, 194.325],
|
||||
'Chla_NDI': [1.321, 23.94],
|
||||
'Chla_Peak': [3.614, 5.097],
|
||||
'Chla_SABG': [10.2, 5.6],
|
||||
'Chla_MCI': [0.5, 0.2],
|
||||
'Chla_FAI': [0.8, 0.1],
|
||||
|
||||
'TP_1': [0.1, 0.05],
|
||||
'TP_2': [0.2, 0.1],
|
||||
'TP_3': [0.15, 0.08],
|
||||
'TP_4': [0.12, 0.06],
|
||||
'TP_5': [0.18, 0.09],
|
||||
'TP_6': [0.25, 0.12],
|
||||
|
||||
'TN_1': [0.5, 0.2],
|
||||
'TN_2': [0.6, 0.3],
|
||||
'TN_3': [0.45, 0.15],
|
||||
'TN_4': [0.55, 0.25],
|
||||
|
||||
'NH3_1': [0.05, 0.02],
|
||||
'NH3_2': [0.06, 0.03],
|
||||
'NH3_3': [0.04, 0.015],
|
||||
'NH3_4': [0.07, 0.035],
|
||||
|
||||
'COD_1': [10.0, 5.0],
|
||||
'COD_2': [12.0, 6.0],
|
||||
'COD_3': [8.5, 4.0],
|
||||
'COD_4': [11.0, 5.5],
|
||||
}
|
||||
|
||||
def __init__(self, parent=None):
|
||||
self._formula_df = None
|
||||
super().__init__(parent)
|
||||
self.init_ui()
|
||||
|
||||
def init_ui(self):
|
||||
# 注入全局样式
|
||||
self.setStyleSheet(ModernStylesheet.get_main_stylesheet())
|
||||
|
||||
# 主布局:增加四周留白(24px)和呼吸间距(20px)
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(24, 24, 24, 24)
|
||||
main_layout.setSpacing(20)
|
||||
|
||||
# ==========================================
|
||||
# 卡片 1:输入数据配置
|
||||
# ==========================================
|
||||
input_group = QGroupBox("📁 输入数据")
|
||||
input_layout = QVBoxLayout()
|
||||
input_layout.setSpacing(16)
|
||||
input_layout.setContentsMargins(20, 24, 20, 20)
|
||||
|
||||
step7_hint = QLabel("💡 提示: 本步骤将根据内置公式库,为训练集样本自动计算常见的水质光谱指数。")
|
||||
step7_hint.setWordWrap(True)
|
||||
step7_hint.setStyleSheet(f"""
|
||||
QLabel {{
|
||||
color: {ModernStylesheet.COLORS['primary']};
|
||||
background-color: {ModernStylesheet.COLORS['selected']};
|
||||
border: 1px solid {ModernStylesheet.COLORS['border_light']};
|
||||
border-radius: 6px;
|
||||
padding: 10px 14px;
|
||||
margin-bottom: 4px;
|
||||
}}
|
||||
""")
|
||||
input_layout.addWidget(step7_hint)
|
||||
|
||||
self.formula_file = FileSelectWidget(
|
||||
"内置公式库源:",
|
||||
"CSV Files (*.csv);;All Files (*.*)"
|
||||
)
|
||||
self.formula_file.line_edit.setReadOnly(True)
|
||||
self.formula_file.label.setMinimumWidth(120)
|
||||
|
||||
# 智能查找自带的公式库文件(兼容开发 + PyInstaller 打包)
|
||||
builtin_csv = self._resolve_builtin_csv()
|
||||
if builtin_csv:
|
||||
self.formula_file.set_path(builtin_csv)
|
||||
|
||||
self.training_data_widget = FileSelectWidget(
|
||||
"输入样本数据:",
|
||||
"CSV Files (*.csv);;All Files (*.*)"
|
||||
)
|
||||
self.training_data_widget.label.setMinimumWidth(120)
|
||||
|
||||
input_layout.addWidget(self.formula_file)
|
||||
input_layout.addWidget(self.training_data_widget)
|
||||
input_group.setLayout(input_layout)
|
||||
main_layout.addWidget(input_group)
|
||||
|
||||
# ==========================================
|
||||
# 卡片 2:指数计算配置
|
||||
# ==========================================
|
||||
params_group = QGroupBox("⚙️ 指数计算配置")
|
||||
params_layout = QVBoxLayout()
|
||||
params_layout.setSpacing(16)
|
||||
params_layout.setContentsMargins(20, 24, 20, 20)
|
||||
|
||||
# 辅助布局:公式选择列表
|
||||
list_row = QHBoxLayout()
|
||||
list_row.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
list_label = QLabel("待计算的水质指数:")
|
||||
list_label.setMinimumWidth(120)
|
||||
list_label.setAlignment(Qt.AlignTop | Qt.AlignLeft)
|
||||
|
||||
right_panel = QWidget()
|
||||
right_layout = QVBoxLayout(right_panel)
|
||||
right_layout.setContentsMargins(0, 0, 0, 0)
|
||||
right_layout.setSpacing(8)
|
||||
|
||||
# 按钮工具栏
|
||||
btn_layout = QHBoxLayout()
|
||||
btn_layout.setContentsMargins(0, 0, 0, 0)
|
||||
btn_layout.setSpacing(10) # 👉 [新增] 增加按钮之间的水平间距,不再挤在一起
|
||||
|
||||
self.btn_select_all = QPushButton("全选")
|
||||
self.btn_select_none = QPushButton("清空")
|
||||
self.btn_select_ratio = QPushButton("仅比值型")
|
||||
self.btn_select_conc = QPushButton("仅浓度型")
|
||||
self.btn_reload = QPushButton("重新加载库")
|
||||
|
||||
for btn in [self.btn_select_all, self.btn_select_none,
|
||||
self.btn_select_ratio, self.btn_select_conc, self.btn_reload]:
|
||||
|
||||
base_style = ModernStylesheet.get_button_stylesheet('normal')
|
||||
# 👉 [修改] 追加左右内边距 (padding-left/right),保证无论文字长短都不会被截断
|
||||
btn.setStyleSheet(base_style + "\nQPushButton { padding-left: 14px; padding-right: 14px; }")
|
||||
|
||||
# 👉 [修改] 高度从 28 稍微调高到 30,让中文字体上下有更好的呼吸感
|
||||
btn.setFixedHeight(30)
|
||||
|
||||
# 👉 [新增] 设置尺寸策略:高度固定,宽度根据文字内容自动撑开
|
||||
btn.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
|
||||
|
||||
btn_layout.addWidget(btn)
|
||||
|
||||
btn_layout.addStretch()
|
||||
right_layout.addLayout(btn_layout)
|
||||
|
||||
# 列表框 (去掉 setSelectionMode,改为默认行为)
|
||||
self.formula_list = QListWidget()
|
||||
self.formula_list.setMinimumHeight(150)
|
||||
# 用样式表去掉默认的边框和丑陋的焦点框,增加 hover 和复选框大小调整
|
||||
self.formula_list.setStyleSheet(f"""
|
||||
QListWidget {{
|
||||
border: 1px solid {ModernStylesheet.COLORS['border']};
|
||||
border-radius: {ModernStylesheet.VARS['radius_md']};
|
||||
background-color: {ModernStylesheet.COLORS['panel_bg']};
|
||||
outline: none;
|
||||
}}
|
||||
QListWidget::item {{
|
||||
padding: 4px 8px; /* 稍微减小上下间距,给复选框留空间 */
|
||||
border-bottom: 1px solid {ModernStylesheet.COLORS['border_light']};
|
||||
}}
|
||||
QListWidget::item:hover {{
|
||||
background-color: {ModernStylesheet.COLORS['hover']};
|
||||
}}
|
||||
/* 放大复选框并拉开间距 */
|
||||
QListWidget::indicator {{
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 6px;
|
||||
}}
|
||||
""")
|
||||
right_layout.addWidget(self.formula_list)
|
||||
|
||||
list_row.addWidget(list_label)
|
||||
list_row.addWidget(right_panel)
|
||||
params_layout.addLayout(list_row)
|
||||
|
||||
params_group.setLayout(params_layout)
|
||||
main_layout.addWidget(params_group)
|
||||
|
||||
# ==========================================
|
||||
# 卡片 3:输出与执行
|
||||
# ==========================================
|
||||
output_group = QGroupBox("🚀 输出与执行")
|
||||
output_layout = QVBoxLayout()
|
||||
output_layout.setSpacing(16)
|
||||
output_layout.setContentsMargins(20, 24, 20, 20)
|
||||
|
||||
# 这里不需要输出文件选择框,因为服务层会直接覆盖原 CSV,但保留执行按钮
|
||||
action_layout = QHBoxLayout()
|
||||
action_layout.addStretch()
|
||||
|
||||
self.run_btn = QPushButton("独立运行步骤")
|
||||
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('primary'))
|
||||
self.run_btn.setMinimumWidth(140)
|
||||
self.run_btn.clicked.connect(self._on_run_single_clicked)
|
||||
action_layout.addWidget(self.run_btn)
|
||||
|
||||
output_layout.addLayout(action_layout)
|
||||
output_group.setLayout(output_layout)
|
||||
main_layout.addWidget(output_group)
|
||||
|
||||
main_layout.addStretch()
|
||||
|
||||
# 事件绑定
|
||||
self.btn_select_all.clicked.connect(self._select_all_formulas)
|
||||
self.btn_select_none.clicked.connect(self._clear_formula_selection)
|
||||
self.btn_select_ratio.clicked.connect(self._select_ratio_formulas)
|
||||
self.btn_select_conc.clicked.connect(self._select_conc_formulas)
|
||||
self.btn_reload.clicked.connect(self._load_formulas_from_csv)
|
||||
|
||||
# 初始加载
|
||||
self._load_formulas_from_csv()
|
||||
|
||||
def _resolve_builtin_csv(self):
|
||||
"""探测内置 waterindex.csv 的实际位置。
|
||||
|
||||
按优先级探测 4 个候选路径(兼容开发 + PyInstaller 打包):
|
||||
1. sys._MEIPASS/_internal/model/ (PyInstaller onedir 带 _MEIPASS)
|
||||
2. sys._MEIPASS/model/ (PyInstaller onefile 或 onedir)
|
||||
3. <panel_dir>/../model/ (开发模式真实位置:src/gui/model/)
|
||||
4. <panel_dir>/../../model/ (项目根 model/,兼容旧布局)
|
||||
"""
|
||||
panel_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
candidates = [
|
||||
os.path.join(panel_dir, '..', 'model', 'waterindex.csv'),
|
||||
os.path.join(panel_dir, '..', '..', 'model', 'waterindex.csv'),
|
||||
]
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
meipass = sys._MEIPASS
|
||||
candidates.insert(0, os.path.join(meipass, '_internal', 'model', 'waterindex.csv'))
|
||||
candidates.insert(0, os.path.join(meipass, 'model', 'waterindex.csv'))
|
||||
for p in candidates:
|
||||
norm = os.path.normpath(p).replace('\\', '/')
|
||||
if os.path.exists(norm):
|
||||
return norm
|
||||
return None
|
||||
|
||||
def get_config(self) -> dict:
|
||||
"""收集当前界面的配置字典"""
|
||||
# 只筛选出打勾状态为 Checked 的项
|
||||
selected_names = [
|
||||
self.formula_list.item(i).text()
|
||||
for i in range(self.formula_list.count())
|
||||
if self.formula_list.item(i).checkState() == Qt.Checked
|
||||
]
|
||||
|
||||
config = {
|
||||
'training_csv_path': self.training_data_widget.get_path(),
|
||||
'formula_csv_file': self.formula_file.get_path(),
|
||||
'formula_names': selected_names,
|
||||
'enabled': True # 默认启用
|
||||
}
|
||||
return config
|
||||
|
||||
def set_config(self, config: dict):
|
||||
"""根据传入字典恢复界面状态"""
|
||||
if 'training_csv_path' in config:
|
||||
self.training_data_widget.set_path(config['training_csv_path'])
|
||||
if 'formula_csv_file' in config:
|
||||
self.formula_file.set_path(config['formula_csv_file'])
|
||||
|
||||
# 恢复勾选状态
|
||||
if 'formula_names' in config:
|
||||
target_names = set(config['formula_names'])
|
||||
self.formula_list.blockSignals(True)
|
||||
for i in range(self.formula_list.count()):
|
||||
item = self.formula_list.item(i)
|
||||
state = Qt.Checked if item.text() in target_names else Qt.Unchecked
|
||||
item.setCheckState(state)
|
||||
self.formula_list.blockSignals(False)
|
||||
|
||||
def _load_formulas_from_csv(self):
|
||||
"""解析公式 CSV 文件并填充列表框"""
|
||||
csv_path = self.formula_file.get_path()
|
||||
if not csv_path or not os.path.exists(csv_path):
|
||||
QMessageBox.warning(self, "警告", "未找到内置的水质指数公式文件!")
|
||||
return
|
||||
|
||||
try:
|
||||
df = pd.read_csv(csv_path, encoding='utf-8')
|
||||
self._formula_df = df
|
||||
self.formula_list.blockSignals(True)
|
||||
self.formula_list.clear()
|
||||
|
||||
target_col = None
|
||||
for col in ['Index_Name', 'Formula_Name']:
|
||||
if col in df.columns:
|
||||
target_col = col
|
||||
break
|
||||
|
||||
if not target_col:
|
||||
QMessageBox.warning(self, "错误", f"CSV 文件格式不正确,列名应为 'Index_Name' 或 'Formula_Name'。当前列为: {list(df.columns)}")
|
||||
self.formula_list.blockSignals(False)
|
||||
return
|
||||
|
||||
for idx_name in df[target_col]:
|
||||
item = QListWidgetItem(idx_name)
|
||||
|
||||
# 开启复选框功能,并设置为默认选中
|
||||
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
|
||||
item.setCheckState(Qt.Checked)
|
||||
|
||||
ftype = self._formula_type_map.get(idx_name, 'ratio')
|
||||
# 简化底色判断,保留类型的色彩提示
|
||||
bg_color = Qt.transparent if ftype == 'ratio' else Qt.GlobalColor.cyan
|
||||
item.setBackground(bg_color)
|
||||
|
||||
self.formula_list.addItem(item)
|
||||
|
||||
self.formula_list.blockSignals(False)
|
||||
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "加载错误", f"加载公式文件时发生错误:\n{str(e)}")
|
||||
|
||||
def _select_all_formulas(self):
|
||||
for i in range(self.formula_list.count()):
|
||||
self.formula_list.item(i).setCheckState(Qt.Checked)
|
||||
|
||||
def _clear_formula_selection(self):
|
||||
for i in range(self.formula_list.count()):
|
||||
self.formula_list.item(i).setCheckState(Qt.Unchecked)
|
||||
|
||||
def _select_ratio_formulas(self):
|
||||
for i in range(self.formula_list.count()):
|
||||
item = self.formula_list.item(i)
|
||||
ftype = self._formula_type_map.get(item.text(), 'ratio')
|
||||
item.setCheckState(Qt.Checked if ftype == 'ratio' else Qt.Unchecked)
|
||||
|
||||
def _select_conc_formulas(self):
|
||||
for i in range(self.formula_list.count()):
|
||||
item = self.formula_list.item(i)
|
||||
ftype = self._formula_type_map.get(item.text(), 'ratio')
|
||||
item.setCheckState(Qt.Checked if ftype == 'concentration' else Qt.Unchecked)
|
||||
|
||||
def _on_run_single_clicked(self):
|
||||
"""点击独立运行按钮:包装参数并通过 EventBus 派发任务"""
|
||||
training_path = self.training_data_widget.get_path()
|
||||
if not training_path:
|
||||
QMessageBox.warning(self, "输入错误", "请先选择输入的样本数据 (CSV)!")
|
||||
return
|
||||
|
||||
# 检查是否有任何一项处于打勾状态
|
||||
has_checked = any(
|
||||
self.formula_list.item(i).checkState() == Qt.Checked
|
||||
for i in range(self.formula_list.count())
|
||||
)
|
||||
|
||||
if not has_checked:
|
||||
QMessageBox.warning(self, "输入错误", "请至少勾选一种待计算的水质指数!")
|
||||
return
|
||||
|
||||
config = self.get_config()
|
||||
# 兼容旧架构的数据流,外层包装为 {'step_id': config}
|
||||
payload = {
|
||||
'step_name': 'step7_index',
|
||||
'config': {'step7_index': config}
|
||||
}
|
||||
global_event_bus.publish('RequestRunSingleStep', payload)
|
||||
|
||||
def update_from_config(self, work_dir=None, pipeline=None):
|
||||
"""从全局配置/Pipeline 同步工作目录。
|
||||
|
||||
step6 的训练数据已由 PANEL_REGISTRY 的 dependencies 自动通过 set_config
|
||||
注入到 self.training_data_widget;此处仅缓存 work_dir,
|
||||
不重复拉取,避免与 panel_factory 注入路径冲突。
|
||||
"""
|
||||
if work_dir:
|
||||
self.work_dir = work_dir
|
||||
elif hasattr(self, 'work_dir') and self.work_dir:
|
||||
pass
|
||||
else:
|
||||
self.work_dir = None
|
||||
Reference in New Issue
Block a user