views/step2-view13:12 个前端 view 迁移完成(继承 BaseView,纯 UI,service 仍占位)
This commit is contained in:
250
src/new/views/step10_view.py
Normal file
250
src/new/views/step10_view.py
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step10View —— Step 10(水色指数反演)的端到端模块化 view
|
||||||
|
|
||||||
|
UI 从 ``src/gui/panels/step10_watercolor_panel.py`` 原样搬迁。
|
||||||
|
|
||||||
|
view 层职责
|
||||||
|
===========
|
||||||
|
|
||||||
|
- 输入影像(BSQ + HDR)、公式选择 ListWidget、输出目录 + 格式 combo、
|
||||||
|
进度条 / 进度标签、运行按钮全部保留。
|
||||||
|
- 删除 ``WaterIndexWorker`` 线程(service 接管后台反演逻辑);
|
||||||
|
进度条 / 标签在 view 层保留 UI 占位,由 service 通过
|
||||||
|
``dispatch_execute`` 反馈到主窗口的日志区即可。
|
||||||
|
- ``_find_waterindex_csv`` / ``_load_formulas`` / ``_load_metadata``
|
||||||
|
不在 view 层执行;公式 ListWidget 留空,service 通过 set_config
|
||||||
|
把 selected_formulas 注入。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5.QtGui import QFont
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QAbstractItemView, QCheckBox, QComboBox, QFormLayout, QGridLayout,
|
||||||
|
QGroupBox, QHBoxLayout, QLabel, QListWidget, QListWidgetItem,
|
||||||
|
QProgressBar, QPushButton, QVBoxLayout,
|
||||||
|
)
|
||||||
|
|
||||||
|
from src.gui.components.custom_widgets import FileSelectWidget
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
from src.new.core.base_view import BaseView
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_subdir(work_dir: str, subdir_name: str) -> str:
|
||||||
|
return os.path.join(work_dir, subdir_name).replace("\\", "/")
|
||||||
|
|
||||||
|
|
||||||
|
class Step10View(BaseView):
|
||||||
|
"""Step 10: 水色指数反演(高光谱影像直接处理)"""
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# ---- 标题 ----
|
||||||
|
title = QLabel("步骤10:水色指数反演(高光谱影像直接处理)")
|
||||||
|
title.setFont(QFont("Arial", 12, QFont.Bold))
|
||||||
|
layout.addWidget(title)
|
||||||
|
|
||||||
|
# ---- 说明 ----
|
||||||
|
hint = QLabel(
|
||||||
|
"将 waterindex.csv 中的公式直接应用于去耀斑高光谱影像(BSQ),"
|
||||||
|
"输出各水质参数指数的 GeoTIFF 栅格图像。"
|
||||||
|
"指数图可直接用于水质专题图生成。"
|
||||||
|
)
|
||||||
|
hint.setWordWrap(True)
|
||||||
|
hint.setStyleSheet(f"color: {ModernStylesheet.COLORS.get('text_secondary', '#666')};")
|
||||||
|
layout.addWidget(hint)
|
||||||
|
|
||||||
|
# ---- 输入影像选择 ----
|
||||||
|
input_group = QGroupBox("输入影像")
|
||||||
|
input_layout = QFormLayout()
|
||||||
|
|
||||||
|
self.bsq_file = FileSelectWidget(
|
||||||
|
"BSQ 影像:",
|
||||||
|
"BSQ Files (*.bsq);;DAT Files (*.dat);;All Files (*.*)",
|
||||||
|
)
|
||||||
|
self.bsq_file.line_edit.setPlaceholderText("选择去耀斑处理后的 BSQ 影像")
|
||||||
|
input_layout.addRow("BSQ 影像:", self.bsq_file)
|
||||||
|
|
||||||
|
self.hdr_file = FileSelectWidget(
|
||||||
|
"ENVI 头文件:",
|
||||||
|
"HDR Files (*.hdr);;All Files (*.*)",
|
||||||
|
)
|
||||||
|
self.hdr_file.line_edit.setPlaceholderText("自动关联同路径 .hdr 文件")
|
||||||
|
input_layout.addRow("HDR 文件:", self.hdr_file)
|
||||||
|
|
||||||
|
self.meta_label = QLabel("未加载影像")
|
||||||
|
self.meta_label.setStyleSheet(
|
||||||
|
"background: #f0f0f0; padding: 4px 8px; border-radius: 4px; "
|
||||||
|
"font-size: 12px; color: #333;"
|
||||||
|
)
|
||||||
|
input_layout.addRow("影像信息:", self.meta_label)
|
||||||
|
|
||||||
|
input_group.setLayout(input_layout)
|
||||||
|
layout.addWidget(input_group)
|
||||||
|
|
||||||
|
# ---- 公式选择 ----
|
||||||
|
formula_group = QGroupBox("公式选择")
|
||||||
|
formula_layout = QGridLayout()
|
||||||
|
|
||||||
|
formula_layout.addWidget(QLabel("按类别筛选:"), 0, 0)
|
||||||
|
self.category_combo = QComboBox()
|
||||||
|
# view 层不实际加载 CSV;类别下拉由 service 通过 set_config 注入
|
||||||
|
self.category_combo.setEnabled(False)
|
||||||
|
formula_layout.addWidget(self.category_combo, 0, 1, 1, 2)
|
||||||
|
|
||||||
|
select_btn_layout = QHBoxLayout()
|
||||||
|
self.select_all_btn = QPushButton("全选")
|
||||||
|
self.deselect_all_btn = QPushButton("取消全选")
|
||||||
|
self.select_all_btn.setMaximumWidth(80)
|
||||||
|
self.deselect_all_btn.setMaximumWidth(80)
|
||||||
|
self.select_all_btn.clicked.connect(self._select_all)
|
||||||
|
self.deselect_all_btn.clicked.connect(self._deselect_all)
|
||||||
|
select_btn_layout.addWidget(self.select_all_btn)
|
||||||
|
select_btn_layout.addWidget(self.deselect_all_btn)
|
||||||
|
select_btn_layout.addStretch()
|
||||||
|
formula_layout.addLayout(select_btn_layout, 0, 3)
|
||||||
|
|
||||||
|
self.formula_list = QListWidget()
|
||||||
|
self.formula_list.setSelectionMode(QAbstractItemView.MultiSelection)
|
||||||
|
self.formula_list.setMinimumHeight(200)
|
||||||
|
# view 层不实际加公式;service 通过 set_config 注入
|
||||||
|
self.formula_list.blockSignals(True)
|
||||||
|
formula_layout.addWidget(self.formula_list, 1, 0, 1, 4)
|
||||||
|
|
||||||
|
formula_group.setLayout(formula_layout)
|
||||||
|
layout.addWidget(formula_group)
|
||||||
|
|
||||||
|
# ---- 输出设置 ----
|
||||||
|
output_group = QGroupBox("输出设置")
|
||||||
|
output_layout = QFormLayout()
|
||||||
|
|
||||||
|
self.output_dir = FileSelectWidget(
|
||||||
|
"输出目录:",
|
||||||
|
"Directories",
|
||||||
|
)
|
||||||
|
self.output_dir.line_edit.setPlaceholderText("留空 → 工作目录/10_WaterIndex_Images")
|
||||||
|
output_layout.addRow("输出目录:", self.output_dir)
|
||||||
|
|
||||||
|
self.format_combo = QComboBox()
|
||||||
|
self.format_combo.addItems(["GTiff (GeoTIFF)", "ENVI", "PCI"])
|
||||||
|
self.format_combo.setCurrentIndex(0)
|
||||||
|
output_layout.addRow("输出格式:", self.format_combo)
|
||||||
|
|
||||||
|
output_group.setLayout(output_layout)
|
||||||
|
layout.addWidget(output_group)
|
||||||
|
|
||||||
|
# ---- 进度显示 ----
|
||||||
|
self.progress_bar = QProgressBar()
|
||||||
|
self.progress_bar.setMinimum(0)
|
||||||
|
self.progress_bar.setMaximum(100)
|
||||||
|
self.progress_bar.setValue(0)
|
||||||
|
self.progress_bar.setTextVisible(True)
|
||||||
|
layout.addWidget(self.progress_bar)
|
||||||
|
|
||||||
|
self.progress_label = QLabel("")
|
||||||
|
self.progress_label.setStyleSheet("font-size: 11px; color: #666;")
|
||||||
|
layout.addWidget(self.progress_label)
|
||||||
|
|
||||||
|
# ---- 启用 & 运行 ----
|
||||||
|
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._on_run_clicked)
|
||||||
|
layout.addWidget(self.run_btn)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 公式勾选辅助
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _select_all(self):
|
||||||
|
for i in range(self.formula_list.count()):
|
||||||
|
item = self.formula_list.item(i)
|
||||||
|
item.setCheckState(Qt.Checked)
|
||||||
|
|
||||||
|
def _deselect_all(self):
|
||||||
|
for i in range(self.formula_list.count()):
|
||||||
|
item = self.formula_list.item(i)
|
||||||
|
item.setCheckState(Qt.Unchecked)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# BaseView 契约
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def get_config(self) -> dict:
|
||||||
|
bsq_path = self.bsq_file.get_path()
|
||||||
|
selected = []
|
||||||
|
for i in range(self.formula_list.count()):
|
||||||
|
item = self.formula_list.item(i)
|
||||||
|
if item.checkState() == Qt.Checked:
|
||||||
|
name = item.data(Qt.UserRole)
|
||||||
|
if name:
|
||||||
|
selected.append(name)
|
||||||
|
config = {
|
||||||
|
"bsq_path": bsq_path,
|
||||||
|
"deglint_img_path": bsq_path,
|
||||||
|
"output_format": self.format_combo.currentText().split()[0],
|
||||||
|
"selected_formulas": selected,
|
||||||
|
"enabled": self.enable_checkbox.isChecked(),
|
||||||
|
}
|
||||||
|
hdr_path = self.hdr_file.get_path()
|
||||||
|
if hdr_path:
|
||||||
|
config["hdr_path"] = hdr_path
|
||||||
|
output_dir = self.output_dir.get_path()
|
||||||
|
if output_dir:
|
||||||
|
config["output_dir"] = output_dir
|
||||||
|
return config
|
||||||
|
|
||||||
|
def set_config(self, config: dict):
|
||||||
|
if config.get("bsq_path"):
|
||||||
|
self.bsq_file.set_path(config["bsq_path"])
|
||||||
|
if config.get("hdr_path"):
|
||||||
|
self.hdr_file.set_path(config["hdr_path"])
|
||||||
|
if config.get("output_dir"):
|
||||||
|
self.output_dir.set_path(config["output_dir"])
|
||||||
|
if "selected_formulas" in config:
|
||||||
|
self.formula_list.blockSignals(True)
|
||||||
|
names = set(config["selected_formulas"])
|
||||||
|
for i in range(self.formula_list.count()):
|
||||||
|
item = self.formula_list.item(i)
|
||||||
|
name = item.data(Qt.UserRole)
|
||||||
|
item.setCheckState(Qt.Checked if name in names else Qt.Unchecked)
|
||||||
|
self.formula_list.blockSignals(False)
|
||||||
|
if "enabled" in config:
|
||||||
|
self.enable_checkbox.setChecked(config["enabled"])
|
||||||
|
|
||||||
|
def update_work_directory(self, work_dir: str):
|
||||||
|
super().update_work_directory(work_dir)
|
||||||
|
if not work_dir:
|
||||||
|
return
|
||||||
|
out_dir = _resolve_subdir(work_dir, "watercolor")
|
||||||
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
if not self.output_dir.get_path():
|
||||||
|
self.output_dir.set_path(out_dir)
|
||||||
|
# 自动填 BSQ(去耀斑输出)
|
||||||
|
deglint_dir = _resolve_subdir(work_dir, "deglint")
|
||||||
|
if os.path.isdir(deglint_dir):
|
||||||
|
import glob
|
||||||
|
candidates = (
|
||||||
|
glob.glob(os.path.join(deglint_dir, "*.bsq"))
|
||||||
|
+ glob.glob(os.path.join(deglint_dir, "*.dat"))
|
||||||
|
)
|
||||||
|
if candidates and not self.bsq_file.get_path():
|
||||||
|
candidates.sort(key=os.path.getmtime, reverse=True)
|
||||||
|
bsq_path = candidates[0]
|
||||||
|
self.bsq_file.set_path(bsq_path)
|
||||||
|
hdr_path = os.path.splitext(bsq_path)[0] + ".hdr"
|
||||||
|
if os.path.exists(hdr_path):
|
||||||
|
self.hdr_file.set_path(hdr_path)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 执行入口
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _on_run_clicked(self):
|
||||||
|
self.dispatch_execute("step10", self.get_config())
|
||||||
389
src/new/views/step11_view.py
Normal file
389
src/new/views/step11_view.py
Normal file
@ -0,0 +1,389 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step11View —— Step 11(专题图生成)的端到端模块化 view
|
||||||
|
|
||||||
|
UI 从 ``src/gui/panels/step11_map_panel.py`` 原样搬迁。
|
||||||
|
|
||||||
|
view 层职责
|
||||||
|
===========
|
||||||
|
|
||||||
|
- 渲染模式(CSV 插值模式 / GeoTIFF 栅格模式)combo + 单文件/文件夹批量
|
||||||
|
RadioButton + 4 个文件输入控件 + 参数组 + 输出目录全部保留。
|
||||||
|
- ``_toggle_input_mode`` 槽函数保留:根据 (render_mode, folder_mode)
|
||||||
|
动态显示/隐藏对应的输入组件。
|
||||||
|
- ``Step11MapBatchThread`` / ``Step11GeoTIFFBatchThread`` 删除(service 接管);
|
||||||
|
进度条 UI 占位保留。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5.QtGui import QFont
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QButtonGroup, QCheckBox, QComboBox, QDoubleSpinBox, QFormLayout,
|
||||||
|
QGroupBox, QHBoxLayout, QLabel, QLineEdit, QProgressBar, QPushButton,
|
||||||
|
QRadioButton, QVBoxLayout, QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
from src.gui.components.custom_widgets import FileSelectWidget
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
from src.new.core.base_view import BaseView
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_subdir(work_dir: str, subdir_name: str) -> str:
|
||||||
|
return os.path.join(work_dir, subdir_name).replace("\\", "/")
|
||||||
|
|
||||||
|
|
||||||
|
# RadioButton 美化样式(选中状态为方形实心块,贴合主界面风格)
|
||||||
|
RADIO_STYLE = """
|
||||||
|
QRadioButton {
|
||||||
|
font-size: 14px;
|
||||||
|
spacing: 8px;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
QRadioButton::indicator {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 2px solid #999999;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
QRadioButton::indicator:checked {
|
||||||
|
border: 2px solid #0078d4;
|
||||||
|
background-color: #0078d4;
|
||||||
|
image: none;
|
||||||
|
}
|
||||||
|
QRadioButton::indicator:hover {
|
||||||
|
border: 2px solid #005a9e;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Step11View(BaseView):
|
||||||
|
"""Step 11: 专题图生成"""
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
title = QLabel("步骤11:专题图生成")
|
||||||
|
title.setFont(QFont("Arial", 12, QFont.Bold))
|
||||||
|
layout.addWidget(title)
|
||||||
|
|
||||||
|
hint = QLabel(
|
||||||
|
"独立运行:可选「单个 CSV」或「文件夹批量」(扫描目录下所有 .csv)。"
|
||||||
|
"GeoTIFF 栅格模式下,亦支持批量渲染步骤10输出的所有水色指数 GeoTIFF 文件。"
|
||||||
|
)
|
||||||
|
hint.setWordWrap(True)
|
||||||
|
hint.setStyleSheet(
|
||||||
|
f"color: {ModernStylesheet.COLORS.get('text_secondary', '#666')};"
|
||||||
|
)
|
||||||
|
layout.addWidget(hint)
|
||||||
|
|
||||||
|
# 单 CSV / 文件夹批量 RadioButton
|
||||||
|
mode_row = QHBoxLayout()
|
||||||
|
self.mode_single_rb = QRadioButton("单个 CSV 文件")
|
||||||
|
self.mode_folder_rb = QRadioButton("文件夹批量")
|
||||||
|
self._mode_group = QButtonGroup(self)
|
||||||
|
self._mode_group.addButton(self.mode_single_rb, 0)
|
||||||
|
self._mode_group.addButton(self.mode_folder_rb, 1)
|
||||||
|
mode_row.addWidget(self.mode_single_rb)
|
||||||
|
mode_row.addWidget(self.mode_folder_rb)
|
||||||
|
mode_row.addStretch()
|
||||||
|
layout.addLayout(mode_row)
|
||||||
|
|
||||||
|
for rb in (self.mode_single_rb, self.mode_folder_rb):
|
||||||
|
rb.setStyleSheet(RADIO_STYLE)
|
||||||
|
|
||||||
|
# 渲染模式 combo
|
||||||
|
render_row = QHBoxLayout()
|
||||||
|
render_row.addWidget(QLabel("渲染模式:"))
|
||||||
|
self.render_mode_combo = QComboBox()
|
||||||
|
self.render_mode_combo.addItems(["CSV 插值模式", "GeoTIFF 栅格模式"])
|
||||||
|
self.render_mode_combo.setMinimumWidth(180)
|
||||||
|
render_row.addWidget(self.render_mode_combo)
|
||||||
|
render_row.addStretch()
|
||||||
|
layout.addLayout(render_row)
|
||||||
|
|
||||||
|
# 预测结果 CSV(单文件)
|
||||||
|
self.prediction_csv_file = FileSelectWidget(
|
||||||
|
"预测结果CSV:",
|
||||||
|
"CSV Files (*.csv);;All Files (*.*)",
|
||||||
|
)
|
||||||
|
layout.addWidget(self.prediction_csv_file)
|
||||||
|
|
||||||
|
# 预测 CSV 文件夹(批量)
|
||||||
|
self._folder_row_widget = QWidget()
|
||||||
|
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.setLayout(folder_row)
|
||||||
|
layout.addWidget(self._folder_row_widget)
|
||||||
|
|
||||||
|
# 水色指数 GeoTIFF(单文件)
|
||||||
|
self.geotiff_file = FileSelectWidget(
|
||||||
|
"水色指数 GeoTIFF:",
|
||||||
|
"GeoTIFF Files (*.tif);;All Files (*.*)",
|
||||||
|
)
|
||||||
|
self.geotiff_file.line_edit.setPlaceholderText(
|
||||||
|
"选择步骤10输出的水色指数 GeoTIFF 文件…"
|
||||||
|
)
|
||||||
|
self.geotiff_file.setVisible(False)
|
||||||
|
layout.addWidget(self.geotiff_file)
|
||||||
|
|
||||||
|
# 水色指数目录(批量)
|
||||||
|
self._geotiff_dir_widget = QWidget()
|
||||||
|
geotiff_dir_row = QHBoxLayout()
|
||||||
|
self.geotiff_dir_label = QLabel("水色指数目录:")
|
||||||
|
self.geotiff_dir_label.setMinimumWidth(120)
|
||||||
|
self.geotiff_dir_edit = QLineEdit()
|
||||||
|
self.geotiff_dir_edit.setPlaceholderText(
|
||||||
|
"选择 10_WaterIndex_Images 文件夹(批量渲染)…"
|
||||||
|
)
|
||||||
|
geotiff_dir_btn = QPushButton("浏览…")
|
||||||
|
geotiff_dir_btn.setMaximumWidth(80)
|
||||||
|
geotiff_dir_btn.clicked.connect(self.browse_geotiff_dir)
|
||||||
|
geotiff_dir_row.addWidget(self.geotiff_dir_label)
|
||||||
|
geotiff_dir_row.addWidget(self.geotiff_dir_edit, 1)
|
||||||
|
geotiff_dir_row.addWidget(geotiff_dir_btn)
|
||||||
|
self._geotiff_dir_widget.setLayout(geotiff_dir_row)
|
||||||
|
self._geotiff_dir_widget.setVisible(False)
|
||||||
|
layout.addWidget(self._geotiff_dir_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")
|
||||||
|
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._on_run_clicked)
|
||||||
|
layout.addWidget(self.run_btn)
|
||||||
|
|
||||||
|
# 批量渲染进度条
|
||||||
|
self.progress_bar = QProgressBar()
|
||||||
|
self.progress_bar.setVisible(False)
|
||||||
|
self.progress_bar.setMinimum(0)
|
||||||
|
self.progress_bar.setMaximum(100)
|
||||||
|
self.progress_bar.setValue(0)
|
||||||
|
layout.addWidget(self.progress_bar)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
# 信号绑定
|
||||||
|
self.render_mode_combo.currentTextChanged.connect(self._toggle_input_mode)
|
||||||
|
self.mode_single_rb.toggled.connect(self._toggle_input_mode)
|
||||||
|
self.mode_folder_rb.toggled.connect(self._toggle_input_mode)
|
||||||
|
self.mode_single_rb.setChecked(True)
|
||||||
|
self._toggle_input_mode()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 槽函数
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _toggle_input_mode(self):
|
||||||
|
"""根据渲染模式和输入模式动态显示/隐藏对应的输入组件"""
|
||||||
|
geotiff_mode = self.render_mode_combo.currentText() == "GeoTIFF 栅格模式"
|
||||||
|
folder_mode = self.mode_folder_rb.isChecked()
|
||||||
|
|
||||||
|
if not geotiff_mode:
|
||||||
|
# CSV 插值模式
|
||||||
|
self.prediction_csv_file.setVisible(not folder_mode)
|
||||||
|
self._folder_row_widget.setVisible(folder_mode)
|
||||||
|
self.recursive_csv_cb.setVisible(folder_mode)
|
||||||
|
self.geotiff_file.setVisible(False)
|
||||||
|
self._geotiff_dir_widget.setVisible(False)
|
||||||
|
else:
|
||||||
|
# GeoTIFF 栅格模式
|
||||||
|
self.prediction_csv_file.setVisible(False)
|
||||||
|
self._folder_row_widget.setVisible(False)
|
||||||
|
self.recursive_csv_cb.setVisible(False)
|
||||||
|
self.geotiff_file.setVisible(not folder_mode)
|
||||||
|
self._geotiff_dir_widget.setVisible(folder_mode)
|
||||||
|
|
||||||
|
def browse_prediction_csv_dir(self):
|
||||||
|
d = self._browse_dir(
|
||||||
|
"选择预测结果 CSV 所在文件夹",
|
||||||
|
self._get_default_work_dir() and _resolve_subdir(
|
||||||
|
self._get_default_work_dir(), "prediction_dir"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if d:
|
||||||
|
self.prediction_csv_dir_edit.setText(d)
|
||||||
|
|
||||||
|
def browse_geotiff_dir(self):
|
||||||
|
d = self._browse_dir(
|
||||||
|
"选择水色指数 GeoTIFF 文件夹",
|
||||||
|
self._get_default_work_dir() and _resolve_subdir(
|
||||||
|
self._get_default_work_dir(), "watercolor"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if d:
|
||||||
|
self.geotiff_dir_edit.setText(d)
|
||||||
|
|
||||||
|
def _browse_dir(self, title, default_dir):
|
||||||
|
from PyQt5.QtWidgets import QFileDialog
|
||||||
|
return QFileDialog.getExistingDirectory(self, title, default_dir or "")
|
||||||
|
|
||||||
|
def _get_default_work_dir(self) -> str:
|
||||||
|
if hasattr(self, "work_dir") and self.work_dir:
|
||||||
|
return str(self.work_dir)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# BaseView 契约
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def get_config(self) -> dict:
|
||||||
|
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()
|
||||||
|
geotiff_path = (self.geotiff_file.get_path() or "").strip()
|
||||||
|
config = {
|
||||||
|
"step10_batch_mode": "folder" if folder_mode else "single",
|
||||||
|
"render_mode": self.render_mode_combo.currentText(),
|
||||||
|
"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),
|
||||||
|
"geotiff_path": geotiff_path if geotiff_path else None,
|
||||||
|
"geotiff_dir": (self.geotiff_dir_edit.text() or "").strip() or 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(),
|
||||||
|
"enabled": self.enable_checkbox.isChecked(),
|
||||||
|
}
|
||||||
|
out_dir = (self.output_dir.get_path() or "").strip()
|
||||||
|
if not folder_mode and pred_csv and out_dir:
|
||||||
|
from pathlib import Path
|
||||||
|
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: dict):
|
||||||
|
mode = config.get("step10_batch_mode", "single")
|
||||||
|
if mode == "folder":
|
||||||
|
self.mode_folder_rb.setChecked(True)
|
||||||
|
else:
|
||||||
|
self.mode_single_rb.setChecked(True)
|
||||||
|
if "render_mode" in config:
|
||||||
|
idx = self.render_mode_combo.findText(config["render_mode"])
|
||||||
|
if idx >= 0:
|
||||||
|
self.render_mode_combo.setCurrentIndex(idx)
|
||||||
|
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 config.get("prediction_csv_path"):
|
||||||
|
self.prediction_csv_file.set_path(str(config["prediction_csv_path"]))
|
||||||
|
if config.get("geotiff_path"):
|
||||||
|
self.geotiff_file.set_path(str(config["geotiff_path"]))
|
||||||
|
if config.get("geotiff_dir"):
|
||||||
|
self.geotiff_dir_edit.setText(str(config["geotiff_dir"]))
|
||||||
|
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 config.get("output_dir"):
|
||||||
|
self.output_dir.set_path(str(config["output_dir"]))
|
||||||
|
elif config.get("output_image_path"):
|
||||||
|
from pathlib import Path
|
||||||
|
p = Path(str(config["output_image_path"]))
|
||||||
|
if p.parent and str(p.parent) != ".":
|
||||||
|
self.output_dir.set_path(str(p.parent))
|
||||||
|
if "enabled" in config:
|
||||||
|
self.enable_checkbox.setChecked(config["enabled"])
|
||||||
|
|
||||||
|
def update_work_directory(self, work_dir: str):
|
||||||
|
super().update_work_directory(work_dir)
|
||||||
|
if not work_dir:
|
||||||
|
return
|
||||||
|
output_dir = _resolve_subdir(work_dir, "visualization")
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
if not self.output_dir.get_path():
|
||||||
|
self.output_dir.set_path(output_dir)
|
||||||
|
# 自动填预测 CSV 目录(9_ML_Prediction)
|
||||||
|
ml_pred_dir = os.path.join(_resolve_subdir(work_dir, "9_ML_Prediction")).replace("\\", "/")
|
||||||
|
if os.path.isdir(ml_pred_dir) and not self.prediction_csv_dir_edit.text():
|
||||||
|
self.prediction_csv_dir_edit.setText(ml_pred_dir)
|
||||||
|
self.mode_folder_rb.setChecked(True)
|
||||||
|
# 自动填边界文件
|
||||||
|
water_mask_dir = _resolve_subdir(work_dir, "water_mask")
|
||||||
|
if os.path.isdir(water_mask_dir) and not self.boundary_file.get_path():
|
||||||
|
from pathlib import Path
|
||||||
|
candidates = (
|
||||||
|
sorted(Path(water_mask_dir).glob("*.shp"))
|
||||||
|
+ sorted(Path(water_mask_dir).glob("*.dat"))
|
||||||
|
)
|
||||||
|
if candidates:
|
||||||
|
self.boundary_file.set_path(str(candidates[0]))
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 执行入口
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _on_run_clicked(self):
|
||||||
|
self.dispatch_execute("step11", self.get_config())
|
||||||
231
src/new/views/step12_view.py
Normal file
231
src/new/views/step12_view.py
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step12View —— Step 12(可视化)的端到端模块化 view(精简版)
|
||||||
|
|
||||||
|
UI 从 ``src/gui/panels/step12_viz_panel.py``(1882 行巨型)做大幅精简。
|
||||||
|
|
||||||
|
精简原则
|
||||||
|
========
|
||||||
|
|
||||||
|
- **删除全部 matplotlib / ImageViewerWidget / ChartViewerDialog /
|
||||||
|
VisualizationWorkerThread / PandasTableModel / ChartBrowserDialog** 等子控件;
|
||||||
|
这些是「运行时画图」所需的复杂组件,由 service + 独立图片查看器窗口接管。
|
||||||
|
- **保留可视化配置 checkbox 组**(5 个:散点/光谱/箱线/掩膜缩略/采样地图)——
|
||||||
|
这是用户实际可控的业务选项。
|
||||||
|
- **保留工作目录 / 图像目录选择 + 扫描 / 生成全部按钮**——给用户「
|
||||||
|
「一键触发可视化」的入口;具体生成逻辑由 service 接管。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5.QtGui import QFont
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QCheckBox, QFrame, QGroupBox, QHBoxLayout, QLabel, QLineEdit,
|
||||||
|
QPushButton, QVBoxLayout, QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
from src.new.core.base_view import BaseView
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_subdir(work_dir: str, subdir_name: str) -> str:
|
||||||
|
return os.path.join(work_dir, subdir_name).replace("\\", "/")
|
||||||
|
|
||||||
|
|
||||||
|
class Step12View(BaseView):
|
||||||
|
"""Step 12: 可视化(精简版)"""
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
main_layout = QHBoxLayout()
|
||||||
|
main_layout.setSpacing(10)
|
||||||
|
main_layout.setContentsMargins(10, 10, 10, 10)
|
||||||
|
|
||||||
|
# ===== 左侧面板 =====
|
||||||
|
left_panel = QWidget()
|
||||||
|
left_layout = QVBoxLayout()
|
||||||
|
left_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
title = QLabel("步骤12:可视化")
|
||||||
|
title.setFont(QFont("Arial", 12, QFont.Bold))
|
||||||
|
left_layout.addWidget(title)
|
||||||
|
|
||||||
|
# 工作目录选择
|
||||||
|
dir_group = QGroupBox("工作目录")
|
||||||
|
dir_layout = QHBoxLayout()
|
||||||
|
self.work_dir_edit = QLineEdit()
|
||||||
|
self.work_dir_edit.setPlaceholderText("选择工作目录...")
|
||||||
|
self.work_dir_edit.setReadOnly(True)
|
||||||
|
dir_browse_btn = QPushButton("浏览")
|
||||||
|
dir_browse_btn.clicked.connect(self.browse_work_dir)
|
||||||
|
dir_layout.addWidget(self.work_dir_edit, 1)
|
||||||
|
dir_layout.addWidget(dir_browse_btn)
|
||||||
|
dir_group.setLayout(dir_layout)
|
||||||
|
left_layout.addWidget(dir_group)
|
||||||
|
|
||||||
|
# 图像目录(自动填充)
|
||||||
|
img_dir_group = QGroupBox("图像目录(预测结果)")
|
||||||
|
img_dir_layout = QHBoxLayout()
|
||||||
|
self.img_dir_edit = QLineEdit()
|
||||||
|
self.img_dir_edit.setPlaceholderText("预测结果目录(自动填充)…")
|
||||||
|
self.img_dir_edit.setReadOnly(True)
|
||||||
|
img_dir_browse_btn = QPushButton("浏览")
|
||||||
|
img_dir_browse_btn.clicked.connect(self.browse_img_dir)
|
||||||
|
img_dir_layout.addWidget(self.img_dir_edit, 1)
|
||||||
|
img_dir_layout.addWidget(img_dir_browse_btn)
|
||||||
|
img_dir_group.setLayout(img_dir_layout)
|
||||||
|
left_layout.addWidget(img_dir_group)
|
||||||
|
|
||||||
|
# 可视化配置
|
||||||
|
config_group = QGroupBox("可视化配置")
|
||||||
|
config_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
self.gen_scatter = QCheckBox("模型评估散点图")
|
||||||
|
self.gen_scatter.setChecked(True)
|
||||||
|
config_layout.addWidget(self.gen_scatter)
|
||||||
|
|
||||||
|
self.gen_spectrum = QCheckBox("光谱曲线图")
|
||||||
|
self.gen_spectrum.setChecked(True)
|
||||||
|
config_layout.addWidget(self.gen_spectrum)
|
||||||
|
|
||||||
|
self.gen_boxplots = QCheckBox("统计图表(箱线图)")
|
||||||
|
self.gen_boxplots.setChecked(True)
|
||||||
|
config_layout.addWidget(self.gen_boxplots)
|
||||||
|
|
||||||
|
self.gen_mask_glint = QCheckBox("掩膜和耀斑缩略图")
|
||||||
|
self.gen_mask_glint.setChecked(True)
|
||||||
|
config_layout.addWidget(self.gen_mask_glint)
|
||||||
|
|
||||||
|
self.gen_sampling_map = QCheckBox("采样点地图")
|
||||||
|
self.gen_sampling_map.setChecked(True)
|
||||||
|
config_layout.addWidget(self.gen_sampling_map)
|
||||||
|
|
||||||
|
config_layout.addSpacing(10)
|
||||||
|
line = QFrame()
|
||||||
|
line.setFrameShape(QFrame.HLine)
|
||||||
|
line.setStyleSheet("color: #ddd;")
|
||||||
|
config_layout.addWidget(line)
|
||||||
|
config_layout.addSpacing(10)
|
||||||
|
|
||||||
|
self.gen_all_btn = QPushButton("🚀 生成全部")
|
||||||
|
self.gen_all_btn.setToolTip("生成所有类型的可视化图表")
|
||||||
|
self.gen_all_btn.setStyleSheet(
|
||||||
|
"background-color: #4CAF50; color: white; font-weight: bold;"
|
||||||
|
)
|
||||||
|
self.gen_all_btn.clicked.connect(self._on_run_clicked)
|
||||||
|
config_layout.addWidget(self.gen_all_btn)
|
||||||
|
|
||||||
|
self.scan_btn = QPushButton("📁 扫描目录")
|
||||||
|
self.scan_btn.setToolTip("扫描工作目录中的图像文件")
|
||||||
|
self.scan_btn.clicked.connect(self._on_scan_clicked)
|
||||||
|
config_layout.addWidget(self.scan_btn)
|
||||||
|
|
||||||
|
config_group.setLayout(config_layout)
|
||||||
|
left_layout.addWidget(config_group)
|
||||||
|
|
||||||
|
left_panel.setLayout(left_layout)
|
||||||
|
left_panel.setMaximumWidth(350)
|
||||||
|
main_layout.addWidget(left_panel, 0)
|
||||||
|
|
||||||
|
# ===== 右侧占位 =====
|
||||||
|
right_panel = QWidget()
|
||||||
|
right_layout = QVBoxLayout()
|
||||||
|
right_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
placeholder = QLabel(
|
||||||
|
"可视化结果由 service 端生成,\n"
|
||||||
|
"生成完成后可在主窗口日志区查看文件路径。\n"
|
||||||
|
"(精简版 view 不内嵌 matplotlib 画布。)"
|
||||||
|
)
|
||||||
|
placeholder.setAlignment(Qt.AlignCenter | Qt.AlignVCenter)
|
||||||
|
placeholder.setStyleSheet(
|
||||||
|
"color: #999; font-size: 13px; padding: 40px;"
|
||||||
|
)
|
||||||
|
right_layout.addWidget(placeholder, 1)
|
||||||
|
right_panel.setLayout(right_layout)
|
||||||
|
main_layout.addWidget(right_panel, 1)
|
||||||
|
|
||||||
|
self.setLayout(main_layout)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 槽函数
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def browse_work_dir(self):
|
||||||
|
from PyQt5.QtWidgets import QFileDialog
|
||||||
|
default = self._get_default_work_dir()
|
||||||
|
dir_path = QFileDialog.getExistingDirectory(self, "选择工作目录", default)
|
||||||
|
if dir_path:
|
||||||
|
self.work_dir = dir_path
|
||||||
|
self.work_dir_edit.setText(dir_path)
|
||||||
|
|
||||||
|
def browse_img_dir(self):
|
||||||
|
from PyQt5.QtWidgets import QFileDialog
|
||||||
|
default = self._get_default_work_dir()
|
||||||
|
dir_path = QFileDialog.getExistingDirectory(self, "选择图像目录", default)
|
||||||
|
if dir_path:
|
||||||
|
self.img_dir_edit.setText(dir_path)
|
||||||
|
|
||||||
|
def _get_default_work_dir(self) -> str:
|
||||||
|
if hasattr(self, "work_dir") and self.work_dir:
|
||||||
|
return str(self.work_dir)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# BaseView 契约
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def get_config(self) -> dict:
|
||||||
|
return {
|
||||||
|
"work_dir": self.work_dir_edit.text(),
|
||||||
|
"img_dir": self.img_dir_edit.text(),
|
||||||
|
"generate_scatter": self.gen_scatter.isChecked(),
|
||||||
|
"generate_spectrum": self.gen_spectrum.isChecked(),
|
||||||
|
"generate_boxplots": self.gen_boxplots.isChecked(),
|
||||||
|
"generate_glint_previews": self.gen_mask_glint.isChecked(),
|
||||||
|
"generate_sampling_maps": self.gen_sampling_map.isChecked(),
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
def set_config(self, config: dict):
|
||||||
|
if not config:
|
||||||
|
return
|
||||||
|
if config.get("work_dir"):
|
||||||
|
self.work_dir_edit.setText(str(config["work_dir"]))
|
||||||
|
self.work_dir = str(config["work_dir"])
|
||||||
|
if config.get("img_dir"):
|
||||||
|
self.img_dir_edit.setText(str(config["img_dir"]))
|
||||||
|
if "generate_scatter" in config:
|
||||||
|
self.gen_scatter.setChecked(config["generate_scatter"])
|
||||||
|
if "generate_boxplots" in config:
|
||||||
|
self.gen_boxplots.setChecked(config["generate_boxplots"])
|
||||||
|
if "generate_spectrum" in config:
|
||||||
|
self.gen_spectrum.setChecked(config["generate_spectrum"])
|
||||||
|
if "generate_glint_previews" in config:
|
||||||
|
self.gen_mask_glint.setChecked(config["generate_glint_previews"])
|
||||||
|
if "generate_sampling_maps" in config:
|
||||||
|
self.gen_sampling_map.setChecked(config.get("generate_sampling_maps", True))
|
||||||
|
|
||||||
|
def update_work_directory(self, work_dir: str):
|
||||||
|
super().update_work_directory(work_dir)
|
||||||
|
if not work_dir:
|
||||||
|
return
|
||||||
|
self.work_dir = work_dir
|
||||||
|
self.work_dir_edit.setText(str(work_dir))
|
||||||
|
# 自动填图像目录
|
||||||
|
for sub in ("prediction_dir", "visualization"):
|
||||||
|
candidate = _resolve_subdir(work_dir, sub)
|
||||||
|
if os.path.isdir(candidate):
|
||||||
|
self.img_dir_edit.setText(candidate)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.img_dir_edit.setText(work_dir)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 执行入口
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _on_run_clicked(self):
|
||||||
|
self.dispatch_execute("step12", self.get_config())
|
||||||
|
|
||||||
|
def _on_scan_clicked(self):
|
||||||
|
# 扫描仅刷新工作目录和图像目录的显示;不实际派发到 service
|
||||||
|
work_dir = self.work_dir_edit.text()
|
||||||
|
if work_dir and os.path.isdir(work_dir):
|
||||||
|
self.img_dir_edit.setText(work_dir)
|
||||||
229
src/new/views/step13_view.py
Normal file
229
src/new/views/step13_view.py
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step13View —— Step 13(报告生成)的端到端模块化 view
|
||||||
|
|
||||||
|
UI 从 ``src/gui/panels/step13_report_panel.py`` 原样搬迁。
|
||||||
|
|
||||||
|
view 层职责
|
||||||
|
===========
|
||||||
|
|
||||||
|
- 路径组(工作目录 / 输出目录 / 报告标题)+ AI 分析开关 + 高级配置入口
|
||||||
|
+ 当前 AI 引擎只读标签全部保留。
|
||||||
|
- ``ReportGenerateThread`` 删除(service 接管后台生成);
|
||||||
|
AI 配置(Provider / API Key / 模型 / 超时)由 service 通过 QSettings
|
||||||
|
直接读取,view 不持有任何 AI 状态。
|
||||||
|
- ``AISettingsDialog`` 调用入口保留(main_view 可以在状态栏或工具栏
|
||||||
|
提供全局入口;本 view 仅暴露 button 让用户即时调出配置)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QSettings
|
||||||
|
from PyQt5.QtGui import QFont
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QCheckBox, QFormLayout, QGroupBox, QHBoxLayout, QLabel, QLineEdit,
|
||||||
|
QPushButton, QVBoxLayout,
|
||||||
|
)
|
||||||
|
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
from src.new.core.base_view import BaseView
|
||||||
|
|
||||||
|
|
||||||
|
# 与 src.gui.dialogs.AISettingsDialog 保持一致
|
||||||
|
AI_SETTINGS_ORG = "WQ_GUI"
|
||||||
|
AI_SETTINGS_APP = "AI"
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_subdir(work_dir: str, subdir_name: str) -> str:
|
||||||
|
return os.path.join(work_dir, subdir_name).replace("\\", "/")
|
||||||
|
|
||||||
|
|
||||||
|
class Step13View(BaseView):
|
||||||
|
"""Step 13: 分析报告生成"""
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
layout.setContentsMargins(10, 10, 10, 10)
|
||||||
|
layout.setSpacing(10)
|
||||||
|
|
||||||
|
title = QLabel("步骤13:分析报告生成")
|
||||||
|
title.setFont(QFont("Arial", 12, QFont.Bold))
|
||||||
|
layout.addWidget(title)
|
||||||
|
|
||||||
|
intro = QLabel(
|
||||||
|
"根据工作目录下的可视化结果(14_visualization 等)生成 Word 分析报告。"
|
||||||
|
"需已存在可视化图表;AI 分析通过 Ollama 或 Minimax 调用云端/本地服务。"
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
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 分析 ───────────────────────────────────────────────────────────
|
||||||
|
ai_group = QGroupBox("AI 分析")
|
||||||
|
ai_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
self.enable_ai_cb = QCheckBox("启用 AI 图表解读与综合总结")
|
||||||
|
self.enable_ai_cb.setChecked(
|
||||||
|
os.environ.get("ENABLE_AI_ANALYSIS", "1") not in {"0", "false", "False"}
|
||||||
|
)
|
||||||
|
ai_layout.addWidget(self.enable_ai_cb)
|
||||||
|
|
||||||
|
ai_status_row = QHBoxLayout()
|
||||||
|
ai_status_row.addWidget(QLabel("当前 AI 引擎:"))
|
||||||
|
self._ai_label = QLabel()
|
||||||
|
self._ai_label.setStyleSheet("color: #0078d4; font-weight: bold;")
|
||||||
|
ai_status_row.addWidget(self._ai_label)
|
||||||
|
ai_status_row.addStretch(1)
|
||||||
|
open_settings_btn = QPushButton("高级配置...")
|
||||||
|
open_settings_btn.clicked.connect(self._open_ai_settings)
|
||||||
|
ai_status_row.addWidget(open_settings_btn)
|
||||||
|
ai_layout.addLayout(ai_status_row)
|
||||||
|
|
||||||
|
ai_group.setLayout(ai_layout)
|
||||||
|
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_run_clicked)
|
||||||
|
btn_row.addWidget(self.generate_btn)
|
||||||
|
btn_row.addStretch()
|
||||||
|
layout.addLayout(btn_row)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
# 刷新引擎提示
|
||||||
|
self._refresh_ai_label()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 槽函数
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _refresh_ai_label(self):
|
||||||
|
s = QSettings(AI_SETTINGS_ORG, AI_SETTINGS_APP)
|
||||||
|
provider = s.value("ai_provider", "minimax", type=str)
|
||||||
|
label_map = {"ollama": "Ollama (本地)", "minimax": "Minimax (云端)"}
|
||||||
|
self._ai_label.setText(label_map.get(provider, provider))
|
||||||
|
|
||||||
|
def _open_ai_settings(self):
|
||||||
|
# AISettingsDialog 仍是旧 GUI 路径下的全局组件;
|
||||||
|
# 弹出对话框失败时降级为提示(service 重写时可全局替换入口)
|
||||||
|
try:
|
||||||
|
from src.gui.dialogs import AISettingsDialog
|
||||||
|
dlg = AISettingsDialog(self)
|
||||||
|
if dlg.exec_():
|
||||||
|
self._refresh_ai_label()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[step13] AISettingsDialog 不可用: {e}")
|
||||||
|
|
||||||
|
def _sync_work_dir(self):
|
||||||
|
mw = self.parent()
|
||||||
|
if mw is not None and getattr(mw, "work_dir", None):
|
||||||
|
self.work_dir_edit.setText(str(mw.work_dir))
|
||||||
|
else:
|
||||||
|
print("[step13] 主窗口尚未设置工作目录。")
|
||||||
|
|
||||||
|
def browse_work_dir(self):
|
||||||
|
from PyQt5.QtWidgets import QFileDialog
|
||||||
|
default = self._get_default_work_dir()
|
||||||
|
d = QFileDialog.getExistingDirectory(self, "选择工作目录", default)
|
||||||
|
if d:
|
||||||
|
self.work_dir_edit.setText(d)
|
||||||
|
|
||||||
|
def browse_output_dir(self):
|
||||||
|
from PyQt5.QtWidgets import QFileDialog
|
||||||
|
default = self._get_default_work_dir()
|
||||||
|
if default:
|
||||||
|
default = _resolve_subdir(default, "visualization")
|
||||||
|
d = QFileDialog.getExistingDirectory(self, "选择报告输出目录", default)
|
||||||
|
if d:
|
||||||
|
self.output_dir_edit.setText(d)
|
||||||
|
|
||||||
|
def _get_default_work_dir(self) -> str:
|
||||||
|
if hasattr(self, "work_dir") and self.work_dir:
|
||||||
|
return str(self.work_dir)
|
||||||
|
mw = self.parent()
|
||||||
|
if mw and hasattr(mw, "work_dir") and mw.work_dir:
|
||||||
|
return str(mw.work_dir)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# BaseView 契约
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def get_config(self) -> dict:
|
||||||
|
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 "水质参数反演分析报告",
|
||||||
|
"enable_ai_analysis": self.enable_ai_cb.isChecked(),
|
||||||
|
"enabled": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
def set_config(self, config: dict):
|
||||||
|
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 "enable_ai_analysis" in config:
|
||||||
|
self.enable_ai_cb.setChecked(bool(config["enable_ai_analysis"]))
|
||||||
|
|
||||||
|
def update_work_directory(self, work_dir: str):
|
||||||
|
super().update_work_directory(work_dir)
|
||||||
|
if not work_dir:
|
||||||
|
return
|
||||||
|
self.work_dir_edit.setText(work_dir)
|
||||||
|
# 默认输出目录 = 工作目录/14_visualization
|
||||||
|
if not self.output_dir_edit.text().strip():
|
||||||
|
out_dir = _resolve_subdir(work_dir, "visualization")
|
||||||
|
self.output_dir_edit.setText(out_dir)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 执行入口
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _on_run_clicked(self):
|
||||||
|
self.dispatch_execute("step13", self.get_config())
|
||||||
170
src/new/views/step2_view.py
Normal file
170
src/new/views/step2_view.py
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step2View —— Step 2(耀斑区域识别)的端到端模块化 view
|
||||||
|
|
||||||
|
设计原则
|
||||||
|
========
|
||||||
|
|
||||||
|
1. **UI 完整搬迁**:从 ``src/gui/panels/step2_panel.py`` 原样搬迁 init_ui 全部控件。
|
||||||
|
2. **零业务逻辑**:本文件绝不 import 任何 ``src/core/``、``src/services/`` 算法;
|
||||||
|
唯一跨层出口是底部按钮的 ``self.dispatch_execute("step2", self.get_config())``。
|
||||||
|
3. **BaseView 契约**:继承 ``BaseView``;实现 ``init_ui / get_config / set_config``;
|
||||||
|
``update_work_directory`` 保留 work_dir 自动填输出路径的行为。
|
||||||
|
|
||||||
|
调用链(自顶向下)::
|
||||||
|
|
||||||
|
Step2View._on_run_clicked
|
||||||
|
→ BaseView.dispatch_execute("step2", config)
|
||||||
|
→ MainView.run_single_step(step_id, config)
|
||||||
|
→ TaskWorker → services.placeholder_service.execute_placeholder(config)
|
||||||
|
→ 返回结果 dict → MainView 日志区
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QCheckBox, QComboBox, QDoubleSpinBox, QFormLayout, QGroupBox,
|
||||||
|
QPushButton, QSpinBox, QVBoxLayout, QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
from src.gui.components.custom_widgets import FileSelectWidget
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
from src.new.core.base_view import BaseView
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_subdir(work_dir: str, subdir_name: str) -> str:
|
||||||
|
"""view 内联的子目录解析(替代旧 ``_step_path_resolver.resolve_subdir``)"""
|
||||||
|
return os.path.join(work_dir, subdir_name).replace("\\", "/")
|
||||||
|
|
||||||
|
|
||||||
|
class Step2View(BaseView):
|
||||||
|
"""Step 2: 耀斑区域识别 —— 纯 View,所有跨层通讯走 dispatch_execute"""
|
||||||
|
|
||||||
|
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 (*.*)",
|
||||||
|
)
|
||||||
|
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()
|
||||||
|
for text, data in [("Otsu 阈值法", "otsu"), ("Z-Score 方法", "zscore"),
|
||||||
|
("百分位数法", "percentile"), ("IQR 四分位距法", "iqr"),
|
||||||
|
("自适应阈值法", "adaptive"), ("多波段综合法", "multi_band")]:
|
||||||
|
self.method.addItem(text, data)
|
||||||
|
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 (*.*)",
|
||||||
|
)
|
||||||
|
layout.addWidget(self.output_file)
|
||||||
|
|
||||||
|
# 启用步骤
|
||||||
|
self.enable_checkbox = QCheckBox("启用此步骤")
|
||||||
|
self.enable_checkbox.setChecked(True)
|
||||||
|
layout.addWidget(self.enable_checkbox)
|
||||||
|
|
||||||
|
# 执行按钮(绿色 success 样式)
|
||||||
|
self.run_btn = QPushButton("执行 Step 2: 耀斑区域识别")
|
||||||
|
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet("success"))
|
||||||
|
self.run_btn.clicked.connect(self._on_run_clicked)
|
||||||
|
layout.addWidget(self.run_btn)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# BaseView 契约
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def get_config(self) -> dict:
|
||||||
|
"""读取当前 UI 状态——返回干净字典供 dispatch_execute 携带"""
|
||||||
|
config = {
|
||||||
|
"img_path": self.img_file.get_path(),
|
||||||
|
"glint_wave": self.glint_wave.value(),
|
||||||
|
"method": self.method.currentData(),
|
||||||
|
"enabled": self.enable_checkbox.isChecked(),
|
||||||
|
}
|
||||||
|
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: dict):
|
||||||
|
"""根据 config 恢复 UI 状态——None/空值字段一律跳过"""
|
||||||
|
if config.get("img_path"):
|
||||||
|
self.img_file.set_path(config["img_path"])
|
||||||
|
if "glint_wave" in config:
|
||||||
|
self.glint_wave.setValue(config["glint_wave"])
|
||||||
|
if config.get("method"):
|
||||||
|
idx = self.method.findData(config["method"])
|
||||||
|
if idx >= 0:
|
||||||
|
self.method.setCurrentIndex(idx)
|
||||||
|
if "max_area" in config:
|
||||||
|
self.max_area.setValue(config["max_area"])
|
||||||
|
if "buffer_size" in config:
|
||||||
|
self.buffer_size.setValue(config["buffer_size"])
|
||||||
|
if config.get("water_mask_path"):
|
||||||
|
self.water_mask_file.set_path(config["water_mask_path"])
|
||||||
|
if config.get("output_path"):
|
||||||
|
self.output_file.set_path(config["output_path"])
|
||||||
|
|
||||||
|
def update_work_directory(self, work_dir: str):
|
||||||
|
"""主窗口推送工作目录变更——自动填输出路径(2_Glint_Detection)"""
|
||||||
|
super().update_work_directory(work_dir)
|
||||||
|
if work_dir:
|
||||||
|
output_dir = _resolve_subdir(work_dir, "2_Glint_Detection")
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
default_output_path = os.path.join(output_dir, "severe_glint_area.dat").replace("\\", "/")
|
||||||
|
self.output_file.set_path(default_output_path)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 执行入口
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _on_run_clicked(self):
|
||||||
|
"""绿色按钮:纯 View 层入口,把当前 UI 配置打包交给主窗口"""
|
||||||
|
self.dispatch_execute("step2", self.get_config())
|
||||||
260
src/new/views/step3_view.py
Normal file
260
src/new/views/step3_view.py
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step3View —— Step 3(耀斑去除)的端到端模块化 view
|
||||||
|
|
||||||
|
UI 从 ``src/gui/panels/step3_panel.py`` 原样搬迁;含 4 种去耀斑方法
|
||||||
|
(Goodman / Kutser / Hedley / SUGAR)的参数组切换。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QCheckBox, QComboBox, QDoubleSpinBox, QFormLayout, QGroupBox,
|
||||||
|
QLabel, QLineEdit, QPushButton, QSpinBox, QVBoxLayout,
|
||||||
|
)
|
||||||
|
|
||||||
|
from src.gui.components.custom_widgets import FileSelectWidget
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
from src.new.core.base_view import BaseView
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_subdir(work_dir: str, subdir_name: str) -> str:
|
||||||
|
return os.path.join(work_dir, subdir_name).replace("\\", "/")
|
||||||
|
|
||||||
|
|
||||||
|
class Step3View(BaseView):
|
||||||
|
"""Step 3: 耀斑去除"""
|
||||||
|
|
||||||
|
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/Boundary (*.dat *.tif *.shp);;All Files (*.*)",
|
||||||
|
)
|
||||||
|
layout.addWidget(self.water_mask_file)
|
||||||
|
|
||||||
|
hint = QLabel(
|
||||||
|
"提示:独立运行本步骤时必须选择水域掩膜或边界(与影像同区域的 .dat/.tif 掩膜,或 .shp 矢量)。"
|
||||||
|
)
|
||||||
|
hint.setWordWrap(True)
|
||||||
|
hint.setStyleSheet("color: #666; font-size: 10px;")
|
||||||
|
layout.addWidget(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(38)
|
||||||
|
kutser_layout.addRow("氧吸收波段索引:", self.oxy_band)
|
||||||
|
self.lower_oxy = QSpinBox()
|
||||||
|
self.lower_oxy.setRange(0, 200)
|
||||||
|
self.lower_oxy.setValue(36)
|
||||||
|
kutser_layout.addRow("下氧吸收波段索引:", self.lower_oxy)
|
||||||
|
self.upper_oxy = QSpinBox()
|
||||||
|
self.upper_oxy.setRange(0, 200)
|
||||||
|
self.upper_oxy.setValue(49)
|
||||||
|
kutser_layout.addRow("上氧吸收波段索引:", self.upper_oxy)
|
||||||
|
self.nir_band = QSpinBox()
|
||||||
|
self.nir_band.setRange(0, 200)
|
||||||
|
self.nir_band.setValue(47)
|
||||||
|
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)
|
||||||
|
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"])
|
||||||
|
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.output_file = FileSelectWidget(
|
||||||
|
"输出影像:",
|
||||||
|
"Image Files (*.bsq *.dat *.tif);;All Files (*.*)",
|
||||||
|
)
|
||||||
|
self.output_file.line_edit.setPlaceholderText("deglint_image.bsq")
|
||||||
|
layout.addWidget(self.output_file)
|
||||||
|
|
||||||
|
# 启用步骤
|
||||||
|
self.enable_checkbox = QCheckBox("启用此步骤")
|
||||||
|
self.enable_checkbox.setChecked(True)
|
||||||
|
layout.addWidget(self.enable_checkbox)
|
||||||
|
|
||||||
|
# 执行按钮
|
||||||
|
self.run_btn = QPushButton("执行 Step 3: 耀斑去除")
|
||||||
|
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet("success"))
|
||||||
|
self.run_btn.clicked.connect(self._on_run_clicked)
|
||||||
|
layout.addWidget(self.run_btn)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
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) -> dict:
|
||||||
|
config = {
|
||||||
|
"img_path": self.img_file.get_path(),
|
||||||
|
"method": self.method.currentData(),
|
||||||
|
"enabled": self.enable_checkbox.isChecked(),
|
||||||
|
"interpolate_zeros": self.interpolate_zeros.isChecked(),
|
||||||
|
"interpolation_method": self.interp_method.currentData(),
|
||||||
|
}
|
||||||
|
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
|
||||||
|
|
||||||
|
method = self.method.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()
|
||||||
|
config["sugar_sigma"] = self.sugar_sigma.value()
|
||||||
|
config["sugar_estimate_background"] = self.sugar_estimate_background.isChecked()
|
||||||
|
config["sugar_glint_mask_method"] = self.sugar_glint_mask_method.currentText()
|
||||||
|
config["sugar_termination_thresh"] = self.sugar_termination_thresh.value()
|
||||||
|
try:
|
||||||
|
import ast
|
||||||
|
config["sugar_bounds"] = ast.literal_eval(self.sugar_bounds.text())
|
||||||
|
except Exception:
|
||||||
|
config["sugar_bounds"] = [(1, 2)]
|
||||||
|
return config
|
||||||
|
|
||||||
|
def set_config(self, config: dict):
|
||||||
|
if config.get("img_path"):
|
||||||
|
self.img_file.set_path(config["img_path"])
|
||||||
|
if config.get("water_mask_path"):
|
||||||
|
self.water_mask_file.set_path(config["water_mask_path"])
|
||||||
|
if config.get("output_path"):
|
||||||
|
self.output_file.set_path(config["output_path"])
|
||||||
|
if config.get("method"):
|
||||||
|
idx = self.method.findData(config["method"])
|
||||||
|
if idx >= 0:
|
||||||
|
self.method.setCurrentIndex(idx)
|
||||||
|
if "enabled" in config:
|
||||||
|
self.enable_checkbox.setChecked(config["enabled"])
|
||||||
|
|
||||||
|
def update_work_directory(self, work_dir: str):
|
||||||
|
super().update_work_directory(work_dir)
|
||||||
|
if work_dir:
|
||||||
|
output_dir = _resolve_subdir(work_dir, "3_Deglint")
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
default_output_path = os.path.join(output_dir, "deglint_image.bsq").replace("\\", "/")
|
||||||
|
self.output_file.set_path(default_output_path)
|
||||||
|
|
||||||
|
def _on_run_clicked(self):
|
||||||
|
self.dispatch_execute("step3", self.get_config())
|
||||||
136
src/new/views/step4_view.py
Normal file
136
src/new/views/step4_view.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step4View —— Step 4(采样点布设)的端到端模块化 view
|
||||||
|
|
||||||
|
UI 从 ``src/gui/panels/step4_sampling_panel.py`` 原样搬迁。
|
||||||
|
注:原 panel 中的 QTimer 心跳检查(``_check_csv_exists``)和交互式预览按钮
|
||||||
|
在 view 层留接口位,运行时实际由 main_view 的 dispatch_execute 路由到 service
|
||||||
|
触发状态更新(后续 service 迁移时再补全交互逻辑)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QCheckBox, QFormLayout, QGroupBox, QPushButton, QSpinBox, QVBoxLayout,
|
||||||
|
)
|
||||||
|
|
||||||
|
from src.gui.components.custom_widgets import FileSelectWidget
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
from src.new.core.base_view import BaseView
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_subdir(work_dir: str, subdir_name: str) -> str:
|
||||||
|
return os.path.join(work_dir, subdir_name).replace("\\", "/")
|
||||||
|
|
||||||
|
|
||||||
|
class Step4View(BaseView):
|
||||||
|
"""Step 4: 采样点布设"""
|
||||||
|
|
||||||
|
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 (*.*)",
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
|
||||||
|
self.use_adaptive_sampling = QCheckBox("启用自适应采样")
|
||||||
|
self.use_adaptive_sampling.setChecked(True)
|
||||||
|
params_layout.addRow("采样模式:", self.use_adaptive_sampling)
|
||||||
|
|
||||||
|
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_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("执行 Step 4: 采样点布设")
|
||||||
|
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet("success"))
|
||||||
|
self.run_btn.clicked.connect(self._on_run_clicked)
|
||||||
|
layout.addWidget(self.run_btn)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
def get_config(self) -> dict:
|
||||||
|
config = {
|
||||||
|
"interval": self.interval.value(),
|
||||||
|
"sample_radius": self.sample_radius.value(),
|
||||||
|
"chunk_size": self.chunk_size.value(),
|
||||||
|
"use_adaptive_sampling": self.use_adaptive_sampling.isChecked(),
|
||||||
|
"enabled": self.enable_checkbox.isChecked(),
|
||||||
|
}
|
||||||
|
deglint_img_path = self.deglint_img_file.get_path()
|
||||||
|
if deglint_img_path:
|
||||||
|
config["deglint_img_path"] = deglint_img_path
|
||||||
|
water_mask_path = self.water_mask_file.get_path()
|
||||||
|
if water_mask_path:
|
||||||
|
config["water_mask_path"] = water_mask_path
|
||||||
|
output_path = self.output_file.get_path()
|
||||||
|
if output_path:
|
||||||
|
config["output_path"] = output_path
|
||||||
|
return config
|
||||||
|
|
||||||
|
def set_config(self, config: dict):
|
||||||
|
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 "use_adaptive_sampling" in config:
|
||||||
|
self.use_adaptive_sampling.setChecked(config["use_adaptive_sampling"])
|
||||||
|
if config.get("deglint_img_path"):
|
||||||
|
self.deglint_img_file.set_path(config["deglint_img_path"])
|
||||||
|
if config.get("water_mask_path"):
|
||||||
|
self.water_mask_file.set_path(config["water_mask_path"])
|
||||||
|
|
||||||
|
def update_work_directory(self, work_dir: str):
|
||||||
|
super().update_work_directory(work_dir)
|
||||||
|
if work_dir:
|
||||||
|
output_dir = _resolve_subdir(work_dir, "4_Sampling")
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
default_output_path = os.path.join(output_dir, "sampling_spectra.csv").replace("\\", "/")
|
||||||
|
self.output_file.set_path(default_output_path)
|
||||||
|
|
||||||
|
def _on_run_clicked(self):
|
||||||
|
self.dispatch_execute("step4", self.get_config())
|
||||||
120
src/new/views/step5_view.py
Normal file
120
src/new/views/step5_view.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step5View —— Step 5(数据清洗)的端到端模块化 view
|
||||||
|
|
||||||
|
UI 从 ``src/gui/panels/step5_clean_panel.py`` 原样搬迁;CSV 预览表格在
|
||||||
|
view 层保留静态占位(运行时由 main_view 控制实际刷新逻辑)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QAbstractItemView, QCheckBox, QGroupBox, QHBoxLayout, QHeaderView,
|
||||||
|
QLabel, QPushButton, QSpinBox, QTableView, QVBoxLayout,
|
||||||
|
)
|
||||||
|
|
||||||
|
from src.gui.components.custom_widgets import FileSelectWidget
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
from src.new.core.base_view import BaseView
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_subdir(work_dir: str, subdir_name: str) -> str:
|
||||||
|
return os.path.join(work_dir, subdir_name).replace("\\", "/")
|
||||||
|
|
||||||
|
|
||||||
|
class Step5View(BaseView):
|
||||||
|
"""Step 5: 数据清洗"""
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# CSV 数据预览(静态占位)
|
||||||
|
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.setEnabled(False) # 后续 service 迁移时再启用
|
||||||
|
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("执行 Step 5: 数据清洗")
|
||||||
|
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet("success"))
|
||||||
|
self.run_btn.clicked.connect(self._on_run_clicked)
|
||||||
|
layout.addWidget(self.run_btn)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
def get_config(self) -> dict:
|
||||||
|
config = {
|
||||||
|
"csv_path": self.csv_file.get_path(),
|
||||||
|
"enabled": self.enable_checkbox.isChecked(),
|
||||||
|
}
|
||||||
|
output_path = self.output_file.get_path()
|
||||||
|
if output_path:
|
||||||
|
config["output_path"] = output_path
|
||||||
|
return config
|
||||||
|
|
||||||
|
def set_config(self, config: dict):
|
||||||
|
if config.get("csv_path"):
|
||||||
|
self.csv_file.set_path(config["csv_path"])
|
||||||
|
if config.get("output_path"):
|
||||||
|
self.output_file.set_path(config["output_path"])
|
||||||
|
|
||||||
|
def update_work_directory(self, work_dir: str):
|
||||||
|
super().update_work_directory(work_dir)
|
||||||
|
if work_dir:
|
||||||
|
output_dir = _resolve_subdir(work_dir, "5_Data_Cleaning")
|
||||||
|
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)
|
||||||
|
|
||||||
|
def _on_run_clicked(self):
|
||||||
|
self.dispatch_execute("step5", self.get_config())
|
||||||
154
src/new/views/step6_view.py
Normal file
154
src/new/views/step6_view.py
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step6View —— Step 6(光谱特征提取)的端到端模块化 view
|
||||||
|
|
||||||
|
UI 从 ``src/gui/panels/step6_feature_panel.py`` 原样搬迁。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PyQt5.QtGui import QFont
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QCheckBox, QFormLayout, QGroupBox, QLabel, QPushButton, QSpinBox,
|
||||||
|
QVBoxLayout,
|
||||||
|
)
|
||||||
|
|
||||||
|
from src.gui.components.custom_widgets import FileSelectWidget
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
from src.new.core.base_view import BaseView
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_subdir(work_dir: str, subdir_name: str) -> str:
|
||||||
|
return os.path.join(work_dir, subdir_name).replace("\\", "/")
|
||||||
|
|
||||||
|
|
||||||
|
class Step6View(BaseView):
|
||||||
|
"""Step 6: 光谱特征提取"""
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# 标题
|
||||||
|
title = QLabel("步骤6:光谱特征提取")
|
||||||
|
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)
|
||||||
|
|
||||||
|
glint_hint = QLabel(
|
||||||
|
"提示:独立运行本步骤时必须选择耀斑掩膜(通常为步骤2输出的 severe_glint_area.dat),用于在采样时避开耀斑像元。"
|
||||||
|
)
|
||||||
|
glint_hint.setWordWrap(True)
|
||||||
|
glint_hint.setStyleSheet("color: #666; font-size: 10px;")
|
||||||
|
layout.addWidget(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("执行 Step 6: 光谱特征提取")
|
||||||
|
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet("success"))
|
||||||
|
self.run_btn.clicked.connect(self._on_run_clicked)
|
||||||
|
layout.addWidget(self.run_btn)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
def get_config(self) -> dict:
|
||||||
|
config = {
|
||||||
|
"radius": self.radius.value(),
|
||||||
|
"source_epsg": self.source_epsg.value(),
|
||||||
|
"enabled": self.enable_checkbox.isChecked(),
|
||||||
|
}
|
||||||
|
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
|
||||||
|
return config
|
||||||
|
|
||||||
|
def set_config(self, config: dict):
|
||||||
|
if "radius" in config:
|
||||||
|
self.radius.setValue(config["radius"])
|
||||||
|
if "source_epsg" in config:
|
||||||
|
self.source_epsg.setValue(config["source_epsg"])
|
||||||
|
if config.get("deglint_img_path"):
|
||||||
|
self.deglint_img_file.set_path(config["deglint_img_path"])
|
||||||
|
if config.get("csv_path"):
|
||||||
|
self.csv_file.set_path(config["csv_path"])
|
||||||
|
if config.get("boundary_path"):
|
||||||
|
self.water_mask_file.set_path(config["boundary_path"])
|
||||||
|
if config.get("glint_mask_path"):
|
||||||
|
self.glint_mask_file.set_path(config["glint_mask_path"])
|
||||||
|
|
||||||
|
def update_work_directory(self, work_dir: str):
|
||||||
|
super().update_work_directory(work_dir)
|
||||||
|
if work_dir:
|
||||||
|
output_dir = _resolve_subdir(work_dir, "6_Feature_Extraction")
|
||||||
|
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)
|
||||||
|
|
||||||
|
def _on_run_clicked(self):
|
||||||
|
self.dispatch_execute("step6", self.get_config())
|
||||||
163
src/new/views/step7_view.py
Normal file
163
src/new/views/step7_view.py
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step7View —— Step 7(水质光谱指数计算)的端到端模块化 view
|
||||||
|
|
||||||
|
UI 从 ``src/gui/panels/step7_index_panel.py`` 原样搬迁。
|
||||||
|
|
||||||
|
view 层职责
|
||||||
|
===========
|
||||||
|
|
||||||
|
- 公式 ListWidget 仅做占位(默认空),由 service 在真正执行时通过
|
||||||
|
``set_config({"formula_names": [...])}`` 把勾选项回填。
|
||||||
|
- 内置 ``waterindex.csv`` 路径只读展示,不在 view 层做实际加载;
|
||||||
|
CSV 解析、formula_type / color / coef 三张映射表都属于 service。
|
||||||
|
- 全选 / 仅选比值型 / 仅选浓度型按钮的 ``itemChanged`` 信号
|
||||||
|
在 view 层禁用(``self.formula_list.blockSignals(True)``),
|
||||||
|
避免无数据时触发空回调。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5.QtGui import QFont
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QCheckBox, QGroupBox, QHBoxLayout, QLabel, QListWidget,
|
||||||
|
QListWidgetItem, QPushButton, QVBoxLayout,
|
||||||
|
)
|
||||||
|
|
||||||
|
from src.gui.components.custom_widgets import FileSelectWidget
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
from src.new.core.base_view import BaseView
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_subdir(work_dir: str, subdir_name: str) -> str:
|
||||||
|
return os.path.join(work_dir, subdir_name).replace("\\", "/")
|
||||||
|
|
||||||
|
|
||||||
|
class Step7View(BaseView):
|
||||||
|
"""Step 7: 水质光谱指数计算"""
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
# 标题
|
||||||
|
title = QLabel("步骤7:水质光谱指数计算")
|
||||||
|
title.setFont(QFont("Arial", 12, QFont.Bold))
|
||||||
|
layout.addWidget(title)
|
||||||
|
|
||||||
|
# 1. 公式配置源(只读)
|
||||||
|
path_group = QGroupBox("公式配置源 (内置)")
|
||||||
|
path_layout = QVBoxLayout()
|
||||||
|
self.formula_csv_widget = FileSelectWidget("内置CSV路径:", "CSV Files (*.csv)")
|
||||||
|
self.formula_csv_widget.set_read_only(True)
|
||||||
|
self.formula_csv_widget.line_edit.setStyleSheet("background-color: #f0f0f0; color: #666;")
|
||||||
|
path_layout.addWidget(self.formula_csv_widget)
|
||||||
|
path_group.setLayout(path_layout)
|
||||||
|
layout.addWidget(path_group)
|
||||||
|
|
||||||
|
# 2. 训练数据输入
|
||||||
|
input_group = QGroupBox("输入样本数据")
|
||||||
|
input_layout = QVBoxLayout()
|
||||||
|
self.training_data_widget = FileSelectWidget("特征提取CSV:", "CSV Files (*.csv)")
|
||||||
|
input_layout.addWidget(self.training_data_widget)
|
||||||
|
input_group.setLayout(input_layout)
|
||||||
|
layout.addWidget(input_group)
|
||||||
|
|
||||||
|
# 3. 公式选择区
|
||||||
|
self.formula_group = QGroupBox("待计算水质指数勾选")
|
||||||
|
formula_outer_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
btn_layout = QHBoxLayout()
|
||||||
|
self.select_all_btn = QPushButton("全选")
|
||||||
|
self.deselect_all_btn = QPushButton("清空")
|
||||||
|
self.select_ratio_btn = QPushButton("仅选比值型")
|
||||||
|
self.select_conc_btn = QPushButton("仅选浓度型")
|
||||||
|
self.refresh_button = QPushButton("重新加载")
|
||||||
|
# 注意:按钮在 view 层仅做占位 —— 真正生效需要 service 加载 CSV 后回填
|
||||||
|
for btn in (self.select_all_btn, self.deselect_all_btn,
|
||||||
|
self.select_ratio_btn, self.select_conc_btn, self.refresh_button):
|
||||||
|
btn.setEnabled(False)
|
||||||
|
btn_layout.addWidget(self.select_all_btn)
|
||||||
|
btn_layout.addWidget(self.deselect_all_btn)
|
||||||
|
btn_layout.addWidget(self.select_ratio_btn)
|
||||||
|
btn_layout.addWidget(self.select_conc_btn)
|
||||||
|
btn_layout.addStretch()
|
||||||
|
btn_layout.addWidget(self.refresh_button)
|
||||||
|
formula_outer_layout.addLayout(btn_layout)
|
||||||
|
|
||||||
|
self.formula_list = QListWidget()
|
||||||
|
self.formula_list.setSelectionMode(QListWidget.MultiSelection)
|
||||||
|
# view 层不需要 itemChanged 副作用;service 接管时再启用
|
||||||
|
self.formula_list.blockSignals(True)
|
||||||
|
formula_outer_layout.addWidget(self.formula_list)
|
||||||
|
|
||||||
|
self.formula_group.setLayout(formula_outer_layout)
|
||||||
|
layout.addWidget(self.formula_group)
|
||||||
|
|
||||||
|
# 4. 执行设置
|
||||||
|
output_group = QGroupBox("执行设置")
|
||||||
|
output_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
self.enable_checkbox = QCheckBox("启用计算流程")
|
||||||
|
self.enable_checkbox.setChecked(True)
|
||||||
|
output_layout.addWidget(self.enable_checkbox)
|
||||||
|
|
||||||
|
output_group.setLayout(output_layout)
|
||||||
|
layout.addWidget(output_group)
|
||||||
|
|
||||||
|
# 5. 运行按钮
|
||||||
|
self.run_btn = QPushButton("立即执行计算")
|
||||||
|
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet("success"))
|
||||||
|
self.run_btn.setMinimumHeight(40)
|
||||||
|
self.run_btn.clicked.connect(self._on_run_clicked)
|
||||||
|
layout.addWidget(self.run_btn)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# BaseView 契约
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def get_config(self) -> dict:
|
||||||
|
"""读取当前 UI 状态——formula_names 由 ListWidget 勾选项收集"""
|
||||||
|
selected = []
|
||||||
|
for index in range(self.formula_list.count()):
|
||||||
|
item = self.formula_list.item(index)
|
||||||
|
if item.checkState() == Qt.Checked:
|
||||||
|
selected.append(item.text())
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"training_csv_path": self.training_data_widget.get_path(),
|
||||||
|
"formula_names": selected,
|
||||||
|
"enabled": self.enable_checkbox.isChecked(),
|
||||||
|
"output_mode": 0,
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
|
||||||
|
def set_config(self, config: dict):
|
||||||
|
"""根据 config 恢复 UI 状态——formula_names 直接回填 ListWidget"""
|
||||||
|
if config.get("training_csv_path"):
|
||||||
|
self.training_data_widget.set_path(config["training_csv_path"])
|
||||||
|
if "formula_names" in config:
|
||||||
|
# 用 blockSignals 避免 setCheckState 触发副作用
|
||||||
|
self.formula_list.blockSignals(True)
|
||||||
|
sel = set(config["formula_names"])
|
||||||
|
for index in range(self.formula_list.count()):
|
||||||
|
item = self.formula_list.item(index)
|
||||||
|
item.setCheckState(Qt.Checked if item.text() in sel else Qt.Unchecked)
|
||||||
|
self.formula_list.blockSignals(False)
|
||||||
|
if "enabled" in config:
|
||||||
|
self.enable_checkbox.setChecked(config["enabled"])
|
||||||
|
|
||||||
|
def update_work_directory(self, work_dir: str):
|
||||||
|
super().update_work_directory(work_dir)
|
||||||
|
if work_dir:
|
||||||
|
# step7 panel 在自身 get_config 里会根据 work_dir 拼 indices/ 路径
|
||||||
|
# —— view 层不主动 set_path,留给 dispatch 后的 service 决定
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 执行入口
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _on_run_clicked(self):
|
||||||
|
self.dispatch_execute("step7", self.get_config())
|
||||||
320
src/new/views/step8_view.py
Normal file
320
src/new/views/step8_view.py
Normal file
@ -0,0 +1,320 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step8View —— Step 8(机器学习建模)的端到端模块化 view
|
||||||
|
|
||||||
|
UI 从 ``src/gui/panels/step8_ml_train_panel.py`` 原样搬迁。
|
||||||
|
|
||||||
|
view 层职责
|
||||||
|
===========
|
||||||
|
|
||||||
|
- 3 个 checkbox 组(预处理 / 模型类型 / 数据划分)原样保留
|
||||||
|
(PREPROC_CHINESE / MODEL_CHINESE / SPLIT_CHINESE 三个中文映射表)。
|
||||||
|
- get_config 收集三组勾选,组装成 service 能直接消费的字典。
|
||||||
|
- set_config 把勾选项回填到对应 checkbox。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PyQt5.QtGui import QFont
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QCheckBox, QFormLayout, QGridLayout, QGroupBox, QHBoxLayout,
|
||||||
|
QLabel, QLineEdit, QPushButton, QSpinBox, QVBoxLayout,
|
||||||
|
)
|
||||||
|
|
||||||
|
from src.gui.components.custom_widgets import FileSelectWidget
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
from src.new.core.base_view import BaseView
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# 中文映射表(内部键名 -> 显示文本)
|
||||||
|
# ============================================================
|
||||||
|
PREPROC_CHINESE = {
|
||||||
|
"None": "无 (None)",
|
||||||
|
"MMS": "最小-最大归一化 (MMS)",
|
||||||
|
"SS": "标度化 (SS)",
|
||||||
|
"SNV": "标准正态变换 (SNV)",
|
||||||
|
"MA": "移动平均 (MA)",
|
||||||
|
"SG": "Savitzky-Golay (SG)",
|
||||||
|
"MSC": "多元散射校正 (MSC)",
|
||||||
|
"D1": "一阶导数 (D1)",
|
||||||
|
"D2": "二阶导数 (D2)",
|
||||||
|
"DT": "去趋势 (DT)",
|
||||||
|
"CT": "中心化 (CT)",
|
||||||
|
}
|
||||||
|
|
||||||
|
MODEL_CHINESE = {
|
||||||
|
"LinearRegression": "多元线性回归 (MLR)",
|
||||||
|
"Ridge": "岭回归 (Ridge)",
|
||||||
|
"Lasso": "套索回归 (Lasso)",
|
||||||
|
"ElasticNet": "弹性网络 (ElasticNet)",
|
||||||
|
"PLS": "偏最小二乘 (PLSR)",
|
||||||
|
"DecisionTree": "决策树 (CART)",
|
||||||
|
"RF": "随机森林 (RF)",
|
||||||
|
"ExtraTrees": "极端随机树 (ET)",
|
||||||
|
"XGBoost": "极值梯度提升 (XGBoost)",
|
||||||
|
"LightGBM": "轻量梯度提升 (LightGBM)",
|
||||||
|
"CatBoost": "类别梯度提升 (CatBoost)",
|
||||||
|
"GradientBoosting": "梯度提升树 (GBDT)",
|
||||||
|
"AdaBoost": "自适应提升 (AdaBoost)",
|
||||||
|
"SVR": "支持向量回归 (SVR)",
|
||||||
|
"KNN": "K近邻回归 (KNN)",
|
||||||
|
"MLP": "多层感知机 (BP神经网络)",
|
||||||
|
}
|
||||||
|
|
||||||
|
SPLIT_CHINESE = {
|
||||||
|
"spxy": "SPXY 算法 (考量X-Y空间)",
|
||||||
|
"ks": "KS 算法 (考量X空间)",
|
||||||
|
"random": "随机划分 (Random)",
|
||||||
|
}
|
||||||
|
|
||||||
|
PREPROC_METHODS = ["None", "MMS", "SS", "SNV", "MA", "SG", "MSC", "D1", "D2", "DT", "CT"]
|
||||||
|
MODEL_GROUPS = [
|
||||||
|
("【线性模型】", ["LinearRegression", "Ridge", "Lasso", "ElasticNet", "PLS"]),
|
||||||
|
("【树模型】", ["DecisionTree", "RF", "ExtraTrees", "XGBoost", "LightGBM", "CatBoost"]),
|
||||||
|
("【集成学习】", ["GradientBoosting", "AdaBoost"]),
|
||||||
|
("【其他模型】", ["SVR", "KNN", "MLP"]),
|
||||||
|
]
|
||||||
|
SPLIT_METHODS = ["spxy", "ks", "random"]
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_subdir(work_dir: str, subdir_name: str) -> str:
|
||||||
|
return os.path.join(work_dir, subdir_name).replace("\\", "/")
|
||||||
|
|
||||||
|
|
||||||
|
class Step8View(BaseView):
|
||||||
|
"""Step 8: 机器学习建模"""
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
title = QLabel("步骤8:机器学习建模")
|
||||||
|
title.setFont(QFont("Arial", 12, QFont.Bold))
|
||||||
|
layout.addWidget(title)
|
||||||
|
|
||||||
|
# 训练数据文件(用于独立运行)
|
||||||
|
self.training_csv_file = FileSelectWidget(
|
||||||
|
"训练数据:",
|
||||||
|
"CSV Files (*.csv);;All Files (*.*)",
|
||||||
|
)
|
||||||
|
layout.addWidget(self.training_csv_file)
|
||||||
|
|
||||||
|
# 训练参数
|
||||||
|
params_group = QGroupBox("训练参数")
|
||||||
|
params_layout = QFormLayout()
|
||||||
|
|
||||||
|
self.feature_start = QLineEdit()
|
||||||
|
self.feature_start.setText("374.285004")
|
||||||
|
params_layout.addRow("特征起始列:", self.feature_start)
|
||||||
|
|
||||||
|
feature_start_hint = QLabel(
|
||||||
|
"提示:请使用记事本打开 training_spectra.csv 确认首个波长的精确表头名称"
|
||||||
|
"(如 374.285 或 374.285004)并在此填入,避免因浮点精度差异导致列名匹配失败。"
|
||||||
|
)
|
||||||
|
feature_start_hint.setWordWrap(True)
|
||||||
|
feature_start_hint.setStyleSheet("color: #666; font-size: 10px;")
|
||||||
|
params_layout.addRow(feature_start_hint)
|
||||||
|
|
||||||
|
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, self.preproc_checkboxes = self._build_checkbox_group(
|
||||||
|
"预处理方法 (可多选)", PREPROC_METHODS, PREPROC_CHINESE, columns=4,
|
||||||
|
)
|
||||||
|
layout.addWidget(preproc_group)
|
||||||
|
|
||||||
|
# 模型类型 - 多选(带分组标题)
|
||||||
|
model_group, self.model_checkboxes = self._build_model_group()
|
||||||
|
layout.addWidget(model_group)
|
||||||
|
|
||||||
|
# 数据划分方法 - 多选
|
||||||
|
split_group, self.split_checkboxes = self._build_checkbox_group(
|
||||||
|
"数据划分方法 (可多选)", SPLIT_METHODS, SPLIT_CHINESE, columns=3,
|
||||||
|
)
|
||||||
|
layout.addWidget(split_group)
|
||||||
|
|
||||||
|
# 输出文件路径
|
||||||
|
self.output_path = FileSelectWidget(
|
||||||
|
"输出文件:",
|
||||||
|
"CSV Files (*.csv);;All Files (*.*)",
|
||||||
|
)
|
||||||
|
self.output_path.line_edit.setPlaceholderText("自动生成,或手动指定输出文件路径...")
|
||||||
|
layout.addWidget(self.output_path)
|
||||||
|
|
||||||
|
# 启用步骤
|
||||||
|
self.enable_checkbox = QCheckBox("启用此步骤")
|
||||||
|
self.enable_checkbox.setChecked(False)
|
||||||
|
layout.addWidget(self.enable_checkbox)
|
||||||
|
|
||||||
|
# 独立运行按钮
|
||||||
|
self.run_btn = QPushButton("独立运行此步骤")
|
||||||
|
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet("success"))
|
||||||
|
self.run_btn.clicked.connect(self._on_run_clicked)
|
||||||
|
layout.addWidget(self.run_btn)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 控件构造 helpers
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _build_checkbox_group(self, title, methods, chinese_map, columns):
|
||||||
|
"""构造一个 QGroupBox(多选 checkbox + 全选/全不选 按钮)"""
|
||||||
|
group = QGroupBox(title)
|
||||||
|
outer_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
grid = QGridLayout()
|
||||||
|
checkboxes = {}
|
||||||
|
for i, method in enumerate(methods):
|
||||||
|
checkbox = QCheckBox(chinese_map.get(method, method))
|
||||||
|
checkbox.setChecked(False)
|
||||||
|
checkboxes[method] = checkbox
|
||||||
|
grid.addWidget(checkbox, i // columns, i % columns)
|
||||||
|
|
||||||
|
button_layout = QHBoxLayout()
|
||||||
|
select_all_btn = QPushButton("全选")
|
||||||
|
deselect_all_btn = QPushButton("全不选")
|
||||||
|
select_all_btn.clicked.connect(lambda: self._toggle_checkboxes(checkboxes, True))
|
||||||
|
deselect_all_btn.clicked.connect(lambda: self._toggle_checkboxes(checkboxes, False))
|
||||||
|
button_layout.addWidget(select_all_btn)
|
||||||
|
button_layout.addWidget(deselect_all_btn)
|
||||||
|
button_layout.addStretch()
|
||||||
|
|
||||||
|
outer_layout.addLayout(grid)
|
||||||
|
outer_layout.addLayout(button_layout)
|
||||||
|
group.setLayout(outer_layout)
|
||||||
|
return group, checkboxes
|
||||||
|
|
||||||
|
def _build_model_group(self):
|
||||||
|
"""构造带分组标题的「模型类型」QGroupBox"""
|
||||||
|
group = QGroupBox("模型类型 (可多选)")
|
||||||
|
outer_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
grid = QGridLayout()
|
||||||
|
checkboxes = {}
|
||||||
|
|
||||||
|
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;"
|
||||||
|
)
|
||||||
|
grid.addWidget(group_label, row, 0, 1, 4)
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
for i, model in enumerate(models):
|
||||||
|
checkbox = QCheckBox(MODEL_CHINESE.get(model, model))
|
||||||
|
checkbox.setChecked(False)
|
||||||
|
checkboxes[model] = checkbox
|
||||||
|
grid.addWidget(checkbox, row, i % 4)
|
||||||
|
if (i + 1) % 4 == 0:
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
row += 1
|
||||||
|
|
||||||
|
button_layout = QHBoxLayout()
|
||||||
|
select_all_btn = QPushButton("全选")
|
||||||
|
deselect_all_btn = QPushButton("全不选")
|
||||||
|
select_all_btn.clicked.connect(lambda: self._toggle_checkboxes(checkboxes, True))
|
||||||
|
deselect_all_btn.clicked.connect(lambda: self._toggle_checkboxes(checkboxes, False))
|
||||||
|
button_layout.addWidget(select_all_btn)
|
||||||
|
button_layout.addWidget(deselect_all_btn)
|
||||||
|
button_layout.addStretch()
|
||||||
|
|
||||||
|
outer_layout.addLayout(grid)
|
||||||
|
outer_layout.addLayout(button_layout)
|
||||||
|
group.setLayout(outer_layout)
|
||||||
|
return group, checkboxes
|
||||||
|
|
||||||
|
def _toggle_checkboxes(self, checkboxes_dict, checked):
|
||||||
|
for checkbox in checkboxes_dict.values():
|
||||||
|
checkbox.setChecked(checked)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# BaseView 契约
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def get_config(self) -> dict:
|
||||||
|
preprocessing_methods = [
|
||||||
|
method for method, cb in self.preproc_checkboxes.items() if cb.isChecked()
|
||||||
|
]
|
||||||
|
model_names = [
|
||||||
|
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()
|
||||||
|
]
|
||||||
|
|
||||||
|
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(),
|
||||||
|
"enabled": self.enable_checkbox.isChecked(),
|
||||||
|
}
|
||||||
|
training_csv_path = self.training_csv_file.get_path()
|
||||||
|
if training_csv_path:
|
||||||
|
config["training_csv_path"] = training_csv_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 "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, cb in self.preproc_checkboxes.items():
|
||||||
|
cb.setChecked(method in methods)
|
||||||
|
if "model_names" in config:
|
||||||
|
models = config["model_names"]
|
||||||
|
for model, cb in self.model_checkboxes.items():
|
||||||
|
cb.setChecked(model in models)
|
||||||
|
if "split_methods" in config:
|
||||||
|
methods = config["split_methods"]
|
||||||
|
for method, cb in self.split_checkboxes.items():
|
||||||
|
cb.setChecked(method in methods)
|
||||||
|
if config.get("training_csv_path"):
|
||||||
|
self.training_csv_file.set_path(config["training_csv_path"])
|
||||||
|
if config.get("output_path"):
|
||||||
|
self.output_path.set_path(config["output_path"])
|
||||||
|
if "enabled" in config:
|
||||||
|
self.enable_checkbox.setChecked(config["enabled"])
|
||||||
|
|
||||||
|
def update_work_directory(self, work_dir: str):
|
||||||
|
super().update_work_directory(work_dir)
|
||||||
|
if not work_dir:
|
||||||
|
return
|
||||||
|
# 自动填训练数据
|
||||||
|
step6_dir = _resolve_subdir(work_dir, "spectral_feature")
|
||||||
|
step6_training_csv = os.path.join(step6_dir, "training_spectra.csv").replace("\\", "/")
|
||||||
|
if not self.training_csv_file.get_path():
|
||||||
|
self.training_csv_file.set_path(step6_training_csv)
|
||||||
|
# 自动填输出路径
|
||||||
|
indices_dir = _resolve_subdir(work_dir, "indices")
|
||||||
|
os.makedirs(indices_dir, exist_ok=True)
|
||||||
|
training_csv = self.training_csv_file.get_path()
|
||||||
|
if training_csv:
|
||||||
|
basename = os.path.splitext(os.path.basename(training_csv))[0]
|
||||||
|
output_file = f"{basename}_indices.csv"
|
||||||
|
else:
|
||||||
|
output_file = "training_spectra_indices.csv"
|
||||||
|
output_path = os.path.join(indices_dir, output_file).replace("\\", "/")
|
||||||
|
self.output_path.set_path(output_path)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 执行入口
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _on_run_clicked(self):
|
||||||
|
self.dispatch_execute("step8", self.get_config())
|
||||||
252
src/new/views/step9_view.py
Normal file
252
src/new/views/step9_view.py
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step9View —— Step 9(机器学习预测)的端到端模块化 view
|
||||||
|
|
||||||
|
UI 从 ``src/gui/panels/step9_ml_predict_panel.py`` 原样搬迁。
|
||||||
|
|
||||||
|
view 层职责
|
||||||
|
===========
|
||||||
|
|
||||||
|
- 单选按钮组(使用当前训练模型 / 导入本地预训练模型)的切换逻辑保留。
|
||||||
|
- 外部模型 QListWidget 占位保留:view 层不实际调用 joblib.load 扫描,
|
||||||
|
模型文件路径与勾选状态由 service 通过 set_config 注入。
|
||||||
|
- 当用户从「使用当前训练模型」切到「导入本地预训练模型」时,
|
||||||
|
显示外部模型相关控件;反之隐藏。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt
|
||||||
|
from PyQt5.QtGui import QFont
|
||||||
|
from PyQt5.QtWidgets import (
|
||||||
|
QAbstractItemView, QButtonGroup, QCheckBox, QComboBox, QFormLayout,
|
||||||
|
QGroupBox, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem,
|
||||||
|
QPushButton, QRadioButton, QVBoxLayout,
|
||||||
|
)
|
||||||
|
|
||||||
|
from src.gui.components.custom_widgets import FileSelectWidget
|
||||||
|
from src.gui.styles import ModernStylesheet
|
||||||
|
from src.new.core.base_view import BaseView
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_subdir(work_dir: str, subdir_name: str) -> str:
|
||||||
|
return os.path.join(work_dir, subdir_name).replace("\\", "/")
|
||||||
|
|
||||||
|
|
||||||
|
class Step9View(BaseView):
|
||||||
|
"""Step 9: 机器学习预测"""
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
title = QLabel("步骤9:机器学习预测")
|
||||||
|
title.setFont(QFont("Arial", 12, QFont.Bold))
|
||||||
|
layout.addWidget(title)
|
||||||
|
|
||||||
|
# -------- 模型来源选择(单选按钮组) --------
|
||||||
|
source_group = QGroupBox("模型来源")
|
||||||
|
source_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
self.use_trained_model = QRadioButton("使用当前训练流程的模型")
|
||||||
|
self.use_external_model = QRadioButton("导入本地预训练模型 (.joblib)")
|
||||||
|
self.use_trained_model.setChecked(True)
|
||||||
|
|
||||||
|
# 用 QButtonGroup 强制互斥
|
||||||
|
self._source_button_group = QButtonGroup(self)
|
||||||
|
self._source_button_group.addButton(self.use_trained_model)
|
||||||
|
self._source_button_group.addButton(self.use_external_model)
|
||||||
|
|
||||||
|
self.use_trained_model.toggled.connect(self._on_model_source_changed)
|
||||||
|
self.use_external_model.toggled.connect(self._on_model_source_changed)
|
||||||
|
|
||||||
|
source_layout.addWidget(self.use_trained_model)
|
||||||
|
source_layout.addWidget(self.use_external_model)
|
||||||
|
source_group.setLayout(source_layout)
|
||||||
|
layout.addWidget(source_group)
|
||||||
|
|
||||||
|
# -------- 外部模型文件选择(条件显示) --------
|
||||||
|
self.external_model_widget = FileSelectWidget(
|
||||||
|
"模型母文件夹:",
|
||||||
|
"Directories",
|
||||||
|
)
|
||||||
|
self.external_model_widget.setVisible(False)
|
||||||
|
layout.addWidget(self.external_model_widget)
|
||||||
|
|
||||||
|
# -------- 已扫描模型列表(条件显示) --------
|
||||||
|
self.model_list_group = QGroupBox("选择参与预测的模型")
|
||||||
|
self.model_list_group.setVisible(False)
|
||||||
|
model_list_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
self.model_list = QListWidget()
|
||||||
|
self.model_list.setMaximumHeight(130)
|
||||||
|
self.model_list.setSelectionMode(QAbstractItemView.NoSelection)
|
||||||
|
model_list_layout.addWidget(self.model_list)
|
||||||
|
|
||||||
|
btn_row = QHBoxLayout()
|
||||||
|
self.btn_select_all = QPushButton("全选")
|
||||||
|
self.btn_select_all.setMaximumWidth(80)
|
||||||
|
self.btn_select_none = QPushButton("全不选")
|
||||||
|
self.btn_select_none.setMaximumWidth(80)
|
||||||
|
self.btn_select_all.clicked.connect(self._select_all_models)
|
||||||
|
self.btn_select_none.clicked.connect(self._select_none_models)
|
||||||
|
btn_row.addWidget(self.btn_select_all)
|
||||||
|
btn_row.addWidget(self.btn_select_none)
|
||||||
|
btn_row.addStretch()
|
||||||
|
model_list_layout.addLayout(btn_row)
|
||||||
|
|
||||||
|
self.model_list_group.setLayout(model_list_layout)
|
||||||
|
layout.addWidget(self.model_list_group)
|
||||||
|
|
||||||
|
# -------- 采样光谱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 (*.*)",
|
||||||
|
)
|
||||||
|
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._on_run_clicked)
|
||||||
|
layout.addWidget(self.run_btn)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
self.setLayout(layout)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 单选切换
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _on_model_source_changed(self, checked: bool):
|
||||||
|
if not checked:
|
||||||
|
return
|
||||||
|
is_external = self.use_external_model.isChecked()
|
||||||
|
self.external_model_widget.setVisible(is_external)
|
||||||
|
self.model_list_group.setVisible(is_external)
|
||||||
|
|
||||||
|
def _select_all_models(self):
|
||||||
|
for i in range(self.model_list.count()):
|
||||||
|
self.model_list.item(i).setCheckState(Qt.Checked)
|
||||||
|
|
||||||
|
def _select_none_models(self):
|
||||||
|
for i in range(self.model_list.count()):
|
||||||
|
self.model_list.item(i).setCheckState(Qt.Unchecked)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# BaseView 契约
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def get_config(self) -> dict:
|
||||||
|
config = {
|
||||||
|
"metric": self.metric.currentText(),
|
||||||
|
"prediction_column": self.prediction_column.text(),
|
||||||
|
"use_external_model": self.use_external_model.isChecked(),
|
||||||
|
"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
|
||||||
|
|
||||||
|
# 外部模型模式:把 ListWidget 勾选名单 + 母文件夹路径一起带上
|
||||||
|
if self.use_external_model.isChecked():
|
||||||
|
selected = []
|
||||||
|
for i in range(self.model_list.count()):
|
||||||
|
item = self.model_list.item(i)
|
||||||
|
if item.checkState() == Qt.Checked:
|
||||||
|
selected.append(item.text())
|
||||||
|
if selected:
|
||||||
|
config["external_model_names"] = selected
|
||||||
|
external_dir = self.external_model_widget.get_path()
|
||||||
|
if external_dir:
|
||||||
|
config["external_model_dir"] = external_dir
|
||||||
|
return config
|
||||||
|
|
||||||
|
def set_config(self, config: dict):
|
||||||
|
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 config.get("sampling_csv_path"):
|
||||||
|
self.sampling_csv_file.set_path(config["sampling_csv_path"])
|
||||||
|
if config.get("models_dir"):
|
||||||
|
self.models_dir_file.set_path(config["models_dir"])
|
||||||
|
if config.get("output_path"):
|
||||||
|
self.output_file.set_path(config["output_path"])
|
||||||
|
if "use_external_model" in config:
|
||||||
|
self.use_external_model.setChecked(config["use_external_model"])
|
||||||
|
# 外部模型:把名字回填到 ListWidget
|
||||||
|
if "external_model_names" in config and config["external_model_names"]:
|
||||||
|
self.model_list.clear()
|
||||||
|
for name in config["external_model_names"]:
|
||||||
|
item = QListWidgetItem(name)
|
||||||
|
item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
|
||||||
|
item.setCheckState(Qt.Checked)
|
||||||
|
self.model_list.addItem(item)
|
||||||
|
if "external_model_dir" in config:
|
||||||
|
self.external_model_widget.set_path(config["external_model_dir"])
|
||||||
|
if "enabled" in config:
|
||||||
|
self.enable_checkbox.setChecked(config["enabled"])
|
||||||
|
|
||||||
|
def update_work_directory(self, work_dir: str):
|
||||||
|
super().update_work_directory(work_dir)
|
||||||
|
if not work_dir:
|
||||||
|
return
|
||||||
|
output_dir = _resolve_subdir(work_dir, "ml_prediction")
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
if not self.output_file.get_path():
|
||||||
|
self.output_file.set_path(output_dir)
|
||||||
|
# 自动填采样光谱 CSV
|
||||||
|
step4_dir = _resolve_subdir(work_dir, "4_Sampling")
|
||||||
|
sampling_csv = os.path.join(step4_dir, "sampling_spectra.csv").replace("\\", "/")
|
||||||
|
if not self.sampling_csv_file.get_path():
|
||||||
|
self.sampling_csv_file.set_path(sampling_csv)
|
||||||
|
# 自动填模型目录
|
||||||
|
step8_models_dir = _resolve_subdir(work_dir, "models")
|
||||||
|
if not self.models_dir_file.get_path():
|
||||||
|
self.models_dir_file.set_path(step8_models_dir)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# 执行入口
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
def _on_run_clicked(self):
|
||||||
|
self.dispatch_execute("step9", self.get_config())
|
||||||
Reference in New Issue
Block a user