#!/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, })