467 lines
19 KiB
Python
467 lines
19 KiB
Python
#!/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,
|
||
}) |