Files
WQ_GUI/src/gui/panels/step3_panel.py
2026-06-30 11:32:40 +08:00

467 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Step3 面板 - 耀斑去除 (已移除“启用此步骤”)
"""
import os
from pathlib import Path
from src.gui.panels._step_path_resolver import resolve_subdir, scan_work_dir_for_input
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout,
QSpinBox, QComboBox, QCheckBox, QPushButton,
QLabel, QLineEdit, QMessageBox, QSizePolicy
)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QDoubleValidator
from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet
class Step3Panel(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.init_ui()
def init_ui(self):
self.setStyleSheet(ModernStylesheet.get_main_stylesheet())
main_layout = QVBoxLayout()
main_layout.setContentsMargins(24, 24, 24, 24)
main_layout.setSpacing(20)
input_group = QGroupBox("📁 输入数据")
input_layout = QVBoxLayout()
input_layout.setSpacing(16)
input_layout.setContentsMargins(20, 24, 20, 20)
self.img_file = FileSelectWidget(
"原始影像:",
"Image Files (*.bsq *.dat *.tif);;All Files (*.*)"
)
self.img_file.label.setMinimumWidth(120)
self.water_mask_file = FileSelectWidget(
"水域掩膜/边界:",
"Mask/Boundary (*.dat *.tif *.shp);;All Files (*.*)"
)
self.water_mask_file.label.setMinimumWidth(120)
step3_mask_hint = QLabel(
"💡 提示:独立运行本步骤时必须选择水域掩膜或边界(与影像同区域的 .dat/.tif 掩膜,或 .shp 矢量)。"
)
step3_mask_hint.setWordWrap(True)
step3_mask_hint.setStyleSheet(f"""
QLabel {{
color: {ModernStylesheet.COLORS['primary']};
background-color: {ModernStylesheet.COLORS['selected']};
border: 1px solid {ModernStylesheet.COLORS['border_light']};
border-radius: 6px;
padding: 10px 14px;
margin-bottom: 4px;
}}
""")
input_layout.addWidget(step3_mask_hint)
input_layout.addWidget(self.img_file)
input_layout.addWidget(self.water_mask_file)
input_group.setLayout(input_layout)
main_layout.addWidget(input_group)
params_group = QGroupBox("⚙️ 去耀斑算法设置")
params_layout = QVBoxLayout()
params_layout.setSpacing(16)
params_layout.setContentsMargins(20, 24, 20, 20)
method_row = QHBoxLayout()
method_row.setContentsMargins(0, 0, 0, 0)
method_label = QLabel("去耀斑方法:")
method_label.setMinimumWidth(120)
self.method = QComboBox()
for text, data in [('Goodman 方法', 'goodman'), ('Kutser 方法', 'kutser'),
('Hedley 方法', 'hedley'), ('SUGAR 算法', 'sugar')]:
self.method.addItem(text, data)
self.method.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self.method.currentIndexChanged.connect(self._on_method_changed)
method_row.addWidget(method_label)
method_row.addWidget(self.method)
params_layout.addLayout(method_row)
# --- Goodman ---
self.goodman_widget = QWidget()
goodman_layout = QFormLayout(self.goodman_widget)
goodman_layout.setContentsMargins(0, 0, 0, 0)
self.nir_lower = QSpinBox()
self.nir_lower.setRange(0, 200)
self.nir_lower.setValue(65)
self.nir_lower.setButtonSymbols(QSpinBox.NoButtons)
self._add_row_with_fixed_label(goodman_layout, "NIR下波段索引:", self.nir_lower)
self.nir_upper = QSpinBox()
self.nir_upper.setRange(0, 200)
self.nir_upper.setValue(91)
self.nir_upper.setButtonSymbols(QSpinBox.NoButtons)
self._add_row_with_fixed_label(goodman_layout, "NIR上波段索引:", self.nir_upper)
self.goodman_a = QLineEdit("0.000019")
self.goodman_a.setValidator(QDoubleValidator(0.0, 1.0, 6, self))
self._add_row_with_fixed_label(goodman_layout, "参数 A:", self.goodman_a)
self.goodman_b = QLineEdit("0.10")
self.goodman_b.setValidator(QDoubleValidator(0.0, 1.0, 2, self))
self._add_row_with_fixed_label(goodman_layout, "参数 B:", self.goodman_b)
params_layout.addWidget(self.goodman_widget)
# --- Kutser ---
self.kutser_widget = QWidget()
kutser_layout = QFormLayout(self.kutser_widget)
kutser_layout.setContentsMargins(0, 0, 0, 0)
self.oxy_band = QSpinBox()
self.oxy_band.setRange(0, 200)
self.oxy_band.setValue(38)
self.oxy_band.setButtonSymbols(QSpinBox.NoButtons)
self._add_row_with_fixed_label(kutser_layout, "氧吸收波段索引:", self.oxy_band)
self.lower_oxy = QSpinBox()
self.lower_oxy.setRange(0, 200)
self.lower_oxy.setValue(36)
self.lower_oxy.setButtonSymbols(QSpinBox.NoButtons)
self._add_row_with_fixed_label(kutser_layout, "下氧吸收波段索引:", self.lower_oxy)
self.upper_oxy = QSpinBox()
self.upper_oxy.setRange(0, 200)
self.upper_oxy.setValue(49)
self.upper_oxy.setButtonSymbols(QSpinBox.NoButtons)
self._add_row_with_fixed_label(kutser_layout, "上氧吸收波段索引:", self.upper_oxy)
self.nir_band = QSpinBox()
self.nir_band.setRange(0, 200)
self.nir_band.setValue(47)
self.nir_band.setButtonSymbols(QSpinBox.NoButtons)
self._add_row_with_fixed_label(kutser_layout, "NIR波段索引:", self.nir_band)
self.kutser_widget.setVisible(False)
params_layout.addWidget(self.kutser_widget)
# --- Hedley ---
self.hedley_widget = QWidget()
hedley_layout = QFormLayout(self.hedley_widget)
hedley_layout.setContentsMargins(0, 0, 0, 0)
self.hedley_nir_band = QSpinBox()
self.hedley_nir_band.setRange(0, 200)
self.hedley_nir_band.setValue(47)
self.hedley_nir_band.setButtonSymbols(QSpinBox.NoButtons)
self._add_row_with_fixed_label(hedley_layout, "NIR波段索引:", self.hedley_nir_band)
self.hedley_widget.setVisible(False)
params_layout.addWidget(self.hedley_widget)
# --- SUGAR ---
self.sugar_widget = QWidget()
sugar_layout = QFormLayout(self.sugar_widget)
sugar_layout.setContentsMargins(0, 0, 0, 0)
self.sugar_iter = QSpinBox()
self.sugar_iter.setRange(1, 20)
self.sugar_iter.setValue(3)
self.sugar_iter.setButtonSymbols(QSpinBox.NoButtons)
self._add_row_with_fixed_label(sugar_layout, "迭代次数:", self.sugar_iter)
self.sugar_sigma = QLineEdit("1.00")
self.sugar_sigma.setValidator(QDoubleValidator(0.1, 10.0, 2, self))
self._add_row_with_fixed_label(sugar_layout, "LoG 平滑 σ:", self.sugar_sigma)
self.sugar_estimate_background = QCheckBox()
self.sugar_estimate_background.setChecked(True)
self._add_row_with_fixed_label(sugar_layout, "估计背景光谱:", self.sugar_estimate_background)
self.sugar_glint_mask_method = QComboBox()
self.sugar_glint_mask_method.addItems(['cdf', 'otsu'])
self.sugar_glint_mask_method.setCurrentText('cdf')
self.sugar_glint_mask_method.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
self._add_row_with_fixed_label(sugar_layout, "耀斑掩膜方法:", self.sugar_glint_mask_method)
self.sugar_termination_thresh = QLineEdit("20.00")
self.sugar_termination_thresh.setValidator(QDoubleValidator(1.0, 100.0, 2, self))
self._add_row_with_fixed_label(sugar_layout, "终止阈值:", self.sugar_termination_thresh)
self.sugar_bounds = QLineEdit("[(1, 2)]")
self._add_row_with_fixed_label(sugar_layout, "优化边界:", self.sugar_bounds)
self.sugar_widget.setVisible(False)
params_layout.addWidget(self.sugar_widget)
# --- 通用参数 ---
interp_row = QHBoxLayout()
interp_row.setContentsMargins(0, 8, 0, 0)
self.interpolate_zeros = QCheckBox("启用 0 值像素插值")
self.interpolate_zeros.setMinimumWidth(120)
self.interp_method = QComboBox()
for text, data in [('最近邻插值', 'nearest'), ('双线性插值', 'bilinear'),
('样条插值', 'spline'), ('克里金插值', 'kriging')]:
self.interp_method.addItem(text, data)
self.interp_method.setCurrentIndex(1)
self.interp_method.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
interp_row.addWidget(self.interpolate_zeros)
interp_row.addWidget(self.interp_method)
params_layout.addLayout(interp_row)
params_group.setLayout(params_layout)
main_layout.addWidget(params_group)
output_group = QGroupBox("🚀 输出与执行")
output_layout = QVBoxLayout()
output_layout.setSpacing(16)
output_layout.setContentsMargins(20, 24, 20, 20)
self.output_file = FileSelectWidget(
"结果保存至:",
"Image Files (*.bsq *.dat *.tif);;All Files (*.*)",
mode="save"
)
self.output_file.label.setMinimumWidth(120)
self.output_file.line_edit.setPlaceholderText("deglint_image.bsq")
output_layout.addWidget(self.output_file)
action_layout = QHBoxLayout()
action_layout.addStretch()
self.run_btn = QPushButton("独立运行步骤")
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('primary'))
self.run_btn.setMinimumWidth(140)
self.run_btn.clicked.connect(self._on_run_single_clicked)
action_layout.addWidget(self.run_btn)
output_layout.addLayout(action_layout)
output_group.setLayout(output_layout)
main_layout.addWidget(output_group)
main_layout.addStretch()
self.setLayout(main_layout)
self.img_file.line_edit.textChanged.connect(self._update_band_ranges)
def _add_row_with_fixed_label(self, form_layout, label_text, widget):
lbl = QLabel(label_text)
lbl.setMinimumWidth(120)
row_layout = QHBoxLayout()
row_layout.setContentsMargins(0, 0, 0, 0)
row_layout.addWidget(lbl)
if hasattr(widget, 'setSizePolicy'):
widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
row_layout.addWidget(widget)
form_layout.addRow(row_layout)
def open_interactive_viewer(self):
from src.gui.components.chart_dialogs import InteractiveViewerDialog
img_path = self.img_file.get_path()
if not img_path or not os.path.isfile(img_path):
QMessageBox.warning(self, "警告", "请先选择影像文件!")
return
water_mask = self.water_mask_file.get_path()
dialog = InteractiveViewerDialog(img_path, self)
if water_mask and os.path.isfile(water_mask):
dialog.load_water_mask(water_mask)
dialog.exec_()
def _update_band_ranges(self, file_path):
from osgeo import gdal
if not file_path or not os.path.isfile(file_path):
return
try:
dataset = gdal.Open(file_path)
if dataset is None:
return
raster_count = dataset.RasterCount
max_band = max(0, raster_count - 1)
self.nir_lower.setMaximum(max_band)
self.nir_upper.setMaximum(max_band)
self.oxy_band.setMaximum(max_band)
self.nir_band.setMaximum(max_band)
self.hedley_nir_band.setMaximum(max_band)
dataset = None
except Exception:
pass
def update_from_config(self, work_dir=None, pipeline=None):
if work_dir:
self.work_dir = work_dir
elif hasattr(self, 'work_dir') and self.work_dir:
pass
else:
self.work_dir = None
# ── 水域掩膜输入 ──
# 优先:pipeline context
mask_path = None
if pipeline and hasattr(pipeline, 'step_outputs'):
step1_out = pipeline.step_outputs.get('step1', {})
mask_path = step1_out.get('water_mask') or step1_out.get('output_path')
# 回退:文件系统扫描 1_water_mask/
if not mask_path or not os.path.exists(mask_path):
mask_path = scan_work_dir_for_input(self.work_dir, 'water_mask')
if mask_path and os.path.exists(mask_path):
if not os.path.isabs(mask_path):
mask_path = os.path.join(self.work_dir or '', mask_path).replace('\\', '/')
self.water_mask_file.set_path(mask_path)
# ── 输出路径 ──
if self.work_dir:
if not self.output_file.get_path():
output_dir = resolve_subdir(self.work_dir, 'deglint')
default_output_path = os.path.join(output_dir, "deglint_image.bsq").replace('\\', '/')
self.output_file.set_path(default_output_path)
else:
self.output_file.set_path("")
def _on_method_changed(self, index):
method_id = self.method.currentData()
self.goodman_widget.setVisible(method_id == 'goodman')
self.kutser_widget.setVisible(method_id == 'kutser')
self.hedley_widget.setVisible(method_id == 'hedley')
self.sugar_widget.setVisible(method_id == 'sugar')
def _safe_float(self, line_edit, default_val=0.0):
try:
return float(line_edit.text().strip())
except ValueError:
return default_val
def get_config(self):
config = {
'img_path': self.img_file.get_path(),
'method': self.method.currentData(),
'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._safe_float(self.goodman_a, 0.000019)
config['goodman_B'] = self._safe_float(self.goodman_b, 0.1)
elif method == 'kutser':
config['oxy_band'] = self.oxy_band.value()
config['lower_oxy'] = self.lower_oxy.value()
config['upper_oxy'] = self.upper_oxy.value()
config['nir_band'] = self.nir_band.value()
elif method == 'hedley':
config['hedley_nir_band'] = self.hedley_nir_band.value()
elif method == 'sugar':
config['sugar_iter'] = self.sugar_iter.value() if self.sugar_iter.value() > 0 else None
config['sugar_sigma'] = self._safe_float(self.sugar_sigma, 1.0)
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._safe_float(self.sugar_termination_thresh, 20.0)
try:
import ast
config['sugar_bounds'] = ast.literal_eval(self.sugar_bounds.text())
except:
config['sugar_bounds'] = [(1, 2)]
return config
def set_config(self, config):
if 'img_path' in config:
self.img_file.set_path(config['img_path'])
if 'water_mask_path' in config:
self.water_mask_file.set_path(config['water_mask_path'])
if 'output_path' in config:
self.output_file.set_path(config['output_path'])
if 'method' in config:
idx = self.method.findData(config['method'])
if idx >= 0:
self.method.setCurrentIndex(idx)
if 'interpolate_zeros' in config:
self.interpolate_zeros.setChecked(config['interpolate_zeros'])
if 'interpolation_method' in config:
idx = self.interp_method.findData(config['interpolation_method'])
if idx >= 0:
self.interp_method.setCurrentIndex(idx)
if 'nir_lower' in config:
self.nir_lower.setValue(config['nir_lower'])
if 'nir_upper' in config:
self.nir_upper.setValue(config['nir_upper'])
if 'goodman_A' in config:
self.goodman_a.setText(f"{config['goodman_A']:.6f}")
if 'goodman_B' in config:
self.goodman_b.setText(f"{config['goodman_B']:.2f}")
if 'oxy_band' in config:
self.oxy_band.setValue(config['oxy_band'])
if 'lower_oxy' in config:
self.lower_oxy.setValue(config['lower_oxy'])
if 'upper_oxy' in config:
self.upper_oxy.setValue(config['upper_oxy'])
if 'nir_band' in config:
self.nir_band.setValue(config['nir_band'])
if 'hedley_nir_band' in config:
self.hedley_nir_band.setValue(config['hedley_nir_band'])
if 'sugar_iter' in config:
self.sugar_iter.setValue(config['sugar_iter'] if config['sugar_iter'] is not None else 0)
if 'sugar_sigma' in config:
self.sugar_sigma.setText(f"{config['sugar_sigma']:.2f}")
if 'sugar_estimate_background' in config:
self.sugar_estimate_background.setChecked(config['sugar_estimate_background'])
if 'sugar_glint_mask_method' in config:
idx = self.sugar_glint_mask_method.findData(config['sugar_glint_mask_method'])
if idx >= 0:
self.sugar_glint_mask_method.setCurrentIndex(idx)
if 'sugar_termination_thresh' in config:
self.sugar_termination_thresh.setText(f"{config['sugar_termination_thresh']:.2f}")
if 'sugar_bounds' in config:
self.sugar_bounds.setText(str(config['sugar_bounds']))
def _on_run_single_clicked(self):
from src.gui.core.event_bus import global_event_bus
img_path = self.img_file.get_path()
if not img_path:
QMessageBox.warning(self, "输入错误", "请选择影像文件!")
return
water_mask_path = self.water_mask_file.get_path()
if not water_mask_path:
QMessageBox.warning(
self,
"输入错误",
"独立运行耀斑去除时,必须选择水域掩膜或边界文件。\n\n"
"请提供与当前影像空间一致的水域栅格掩膜(.dat/.tif),或水域矢量边界(.shp)。\n"
"若刚跑过完整流程,可使用步骤1生成的水域掩膜文件。",
)
return
config = {'step3': self.get_config()}
global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step3',
'config': config,
})