步骤七页面修改

This commit is contained in:
DXC
2026-06-26 16:43:28 +08:00
parent abbe218608
commit 9ab916a61c
2 changed files with 427 additions and 2 deletions

View File

@ -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': '模块二 特征工程与数据',

View 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