Files
WQ_GUI/src/gui/panels/step3_panel.py

453 lines
18 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 PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QGroupBox, QFormLayout,
QDoubleSpinBox, QSpinBox, QComboBox, QCheckBox, QPushButton,
QLabel, QLineEdit, QMessageBox,
)
from PyQt5.QtCore import Qt
# 从公共组件库导入
from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet
class Step3Panel(QWidget):
"""步骤3耀斑去除"""
def __init__(self, parent=None):
super().__init__(parent)
self.init_ui()
def init_ui(self):
layout = QVBoxLayout()
# 标题
# 影像文件
self.img_file = FileSelectWidget(
"影像文件:",
"Image Files (*.bsq *.dat *.tif);;All Files (*.*)"
)
layout.addWidget(self.img_file)
# 水域掩膜/边界完整流程可由步骤1自动生成独立单步运行时须手动指定
self.water_mask_file = FileSelectWidget(
"水域掩膜/边界:",
"Mask/Boundary (*.dat *.tif *.shp);;All Files (*.*)"
)
layout.addWidget(self.water_mask_file)
step3_mask_hint = QLabel(
"提示:独立运行本步骤时必须选择水域掩膜或边界(与影像同区域的 .dat/.tif 掩膜,或 .shp 矢量)。"
)
step3_mask_hint.setWordWrap(True)
step3_mask_hint.setStyleSheet("color: #666; font-size: 10px;")
layout.addWidget(step3_mask_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(8)
kutser_layout.addRow("氧吸收波段索引:", self.oxy_band)
self.lower_oxy = QDoubleSpinBox()
self.lower_oxy.setDecimals(2)
self.lower_oxy.setRange(0, 1000)
self.lower_oxy.setValue(756.54)
kutser_layout.addRow("下氧吸收波长(nm):", self.lower_oxy)
self.upper_oxy = QDoubleSpinBox()
self.upper_oxy.setDecimals(2)
self.upper_oxy.setRange(0, 1000)
self.upper_oxy.setValue(766.54)
kutser_layout.addRow("上氧吸收波长(nm):", self.upper_oxy)
self.nir_band = QSpinBox()
self.nir_band.setRange(0, 200)
self.nir_band.setValue(65)
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)
self.sugar_iter.setSpecialValueText("自动")
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'])
self.sugar_glint_mask_method.setCurrentText('cdf')
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.ref_csv_file = FileSelectWidget(
# "实测经纬度CSV:",
# "CSV Files (*.csv);;All Files (*.*)"
# )
# self.ref_csv_file.line_edit.setPlaceholderText("可选:包含 Lon/Lat 列的 CSV 文件")
# layout.addWidget(self.ref_csv_file)
# 交互式预览按钮
# self.preview_btn = QPushButton("👁️ 打开交互式影像预览")
# self.preview_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('info'))
# self.preview_btn.clicked.connect(self.open_interactive_viewer)
# layout.addWidget(self.preview_btn)
# 输出文件路径
self.output_file = FileSelectWidget(
"输出影像:",
"Image Files (*.bsq *.dat *.tif);;All Files (*.*)"
)
self.output_file.line_edit.setPlaceholderText("deglint_image.dat")
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.run_step)
layout.addWidget(self.run_btn)
layout.addStretch()
self.setLayout(layout)
# 信号连接:影像文件路径变化时动态更新波段范围
self.img_file.line_edit.textChanged.connect(self._update_band_ranges)
def open_interactive_viewer(self):
"""打开交互式影像预览"""
from src.gui.water_quality_gui 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):
"""
从 Step1Panel 自动填充水域掩膜路径,实现上下游数据流转
Args:
work_dir: 工作目录路径
pipeline: Pipeline 实例(未使用,保留接口兼容性)
"""
# 保存工作目录引用
if work_dir:
self.work_dir = work_dir
elif hasattr(self, 'work_dir') and self.work_dir:
pass # 保持现有工作目录
else:
self.work_dir = None
# 从 Step1 界面读取水域掩膜路径
main_window = self.window()
if hasattr(main_window, 'step1_panel'):
if main_window.step1_panel.use_ndwi_radio.isChecked():
# NDWI模式读取输出框的路径
mask_path = main_window.step1_panel.output_file.get_path()
else:
# 导入现有模式,读取输入框的路径
mask_path = main_window.step1_panel.mask_file.get_path()
if mask_path:
# 若为相对路径,使用 work_dir 合成为绝对路径
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:
output_dir = os.path.join(self.work_dir, "3_deglint")
os.makedirs(output_dir, exist_ok=True)
default_output_path = os.path.join(output_dir, "deglint_image.dat").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_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):
"""获取配置"""
config = {
'img_path': self.img_file.get_path(),
'method': self.method.currentData(), # 使用 currentData() 获取英文ID
'enabled': self.enable_checkbox.isChecked(),
'interpolate_zeros': self.interpolate_zeros.isChecked(),
'interpolation_method': self.interp_method.currentData(), # 使用 currentData()
}
water_mask_path = self.water_mask_file.get_path()
if water_mask_path:
config['water_mask'] = water_mask_path
output_path = self.output_file.get_path()
if output_path:
config['output_path'] = output_path
method = self.method.currentData() # 使用 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() if self.sugar_iter.value() > 0 else None
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.currentData()
config['sugar_termination_thresh'] = self.sugar_termination_thresh.value()
# 解析bounds字符串
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' in config:
self.water_mask_file.set_path(config['water_mask'])
if 'output_path' in config:
self.output_file.set_path(config['output_path'])
if 'reference_csv' in config:
self.ref_csv_file.set_path(config['reference_csv'])
if 'method' in config:
idx = self.method.findData(config['method']) # 使用 findData()
if idx >= 0:
self.method.setCurrentIndex(idx)
if 'enabled' in config:
self.enable_checkbox.setChecked(config['enabled'])
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']) # 使用 findData()
if idx >= 0:
self.interp_method.setCurrentIndex(idx)
# Goodman参数
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.setValue(config['goodman_A'])
if 'goodman_B' in config:
self.goodman_b.setValue(config['goodman_B'])
# Kutser参数
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'])
# Hedley参数
if 'hedley_nir_band' in config:
self.hedley_nir_band.setValue(config['hedley_nir_band'])
# SUGAR参数
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.setValue(config['sugar_sigma'])
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']) # 使用 findData()
if idx >= 0:
self.sugar_glint_mask_method.setCurrentIndex(idx)
if 'sugar_termination_thresh' in config:
self.sugar_termination_thresh.setValue(config['sugar_termination_thresh'])
if 'sugar_bounds' in config:
self.sugar_bounds.setText(str(config['sugar_bounds']))
def run_step(self):
"""独立运行步骤3"""
# 验证输入
img_path = self.img_file.get_path()
if not img_path:
QMessageBox.warning(self, "输入错误", "请选择影像文件!")
return
if self.enable_checkbox.isChecked():
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
# 获取主窗口并运行步骤
main_window = self.window()
if hasattr(main_window, 'run_single_step'):
config = {'step3': self.get_config()}
main_window.run_single_step('step3', config)