Files
WQ_GUI/src/gui/panels/step6_feature_panel.py
2026-06-30 14:12:12 +08:00

299 lines
12 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 -*-
"""
Step6 面板 - 光谱特征提取 (极致强迫症绝对左对齐版)
"""
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,
QLabel, QSpinBox, QPushButton, QMessageBox, QSizePolicy
)
from PyQt5.QtGui import QFont
from PyQt5.QtCore import Qt
from src.gui.components.custom_widgets import FileSelectWidget
from src.gui.styles import ModernStylesheet
class Step6FeaturePanel(QWidget):
"""步骤6:光谱特征提取"""
def __init__(self, parent=None):
super().__init__(parent)
self.init_ui()
def init_ui(self):
# 1. 注入全局样式系统
self.setStyleSheet(ModernStylesheet.get_main_stylesheet())
# 主布局:增加四周留白(24px)和模块间的呼吸间距(20px)
main_layout = QVBoxLayout()
main_layout.setContentsMargins(24, 24, 24, 24)
main_layout.setSpacing(20)
# ==========================================
# 卡片 1:输入数据
# ==========================================
input_group = QGroupBox("📁 输入数据")
input_layout = QVBoxLayout()
input_layout.setSpacing(16)
input_layout.setContentsMargins(20, 24, 20, 20)
step6_glint_hint = QLabel(
"💡 提示:独立运行本步骤时必须选择耀斑掩膜(通常为步骤2输出的 severe_glint_area.dat),用于在采样时自动避开耀斑像元。"
)
step6_glint_hint.setWordWrap(True)
step6_glint_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(step6_glint_hint)
self.deglint_img_file = FileSelectWidget(
"去耀斑影像:",
"Image Files (*.bsq *.dat *.tif);;All Files (*.*)"
)
# FileSelectWidget 内部已经处理了左侧宽度,我们这里强制设为120对齐
self.deglint_img_file.label.setMinimumWidth(120)
self.csv_file = FileSelectWidget(
"处理后CSV:",
"CSV Files (*.csv);;All Files (*.*)"
)
self.csv_file.label.setMinimumWidth(120)
self.water_mask_file = FileSelectWidget(
"水体掩膜:",
"Mask Files (*.dat *.tif);;All Files (*.*)"
)
self.water_mask_file.label.setMinimumWidth(120)
self.water_mask_file.line_edit.setPlaceholderText("可选,如不选择则自动生成")
self.glint_mask_file = FileSelectWidget(
"耀斑掩膜:",
"Mask Files (*.dat *.tif);;All Files (*.*)"
)
self.glint_mask_file.label.setMinimumWidth(120)
input_layout.addWidget(self.deglint_img_file)
input_layout.addWidget(self.csv_file)
input_layout.addWidget(self.water_mask_file)
input_layout.addWidget(self.glint_mask_file)
input_group.setLayout(input_layout)
main_layout.addWidget(input_group)
# ==========================================
# 卡片 2:提取参数 (强迫症像素级对齐)
# ==========================================
params_group = QGroupBox("⚙️ 提取参数")
params_layout = QVBoxLayout() # 废除 QFormLayout,使用可控的垂直布局
params_layout.setSpacing(16)
params_layout.setContentsMargins(20, 24, 20, 20)
self.radius = QSpinBox()
self.radius.setRange(1, 50)
self.radius.setValue(5)
self.radius.setButtonSymbols(QSpinBox.NoButtons)
self.radius.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) # 强制数字靠左
self._add_aligned_row(params_layout, "采样半径:", self.radius, "px")
self.source_epsg = QSpinBox()
self.source_epsg.setRange(1000, 99999)
self.source_epsg.setValue(4326)
self.source_epsg.setButtonSymbols(QSpinBox.NoButtons)
self.source_epsg.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) # 强制数字靠左
self._add_aligned_row(params_layout, "源坐标系 EPSG:", self.source_epsg, "")
params_group.setLayout(params_layout)
main_layout.addWidget(params_group)
# ==========================================
# 卡片 3:输出与执行
# ==========================================
output_group = QGroupBox("🚀 输出与执行")
output_layout = QVBoxLayout()
output_layout.setSpacing(16)
output_layout.setContentsMargins(20, 24, 20, 20)
self.output_file = FileSelectWidget(
"结果保存至:",
"CSV Files (*.csv);;All Files (*.*)",
mode="save"
)
self.output_file.label.setMinimumWidth(120) # 统一 120px 对齐
self.output_file.line_edit.setPlaceholderText("training_spectra.csv")
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)
def _add_aligned_row(self, parent_layout, label_text, widget, suffix_text):
"""核心对齐函数:隐形占位符实现绝对边缘对齐,且标签绝对靠左"""
row_layout = QHBoxLayout()
row_layout.setContentsMargins(0, 0, 0, 0)
row_layout.setSpacing(10)
# 1. 统一左侧标签宽度为 120px,并强制内容靠左对齐
lbl = QLabel(label_text)
lbl.setMinimumWidth(120)
lbl.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) # 强制靠左!
row_layout.addWidget(lbl)
# 2. 允许控件水平拉伸,填充满宽
widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
widget.setStyleSheet("min-height: 28px;")
row_layout.addWidget(widget)
# 3. 隐形占位符:强制挤压 40px 的空间,让右侧边界绝对整齐
suffix_lbl = QLabel(suffix_text)
suffix_lbl.setFixedWidth(40)
row_layout.addWidget(suffix_lbl)
parent_layout.addLayout(row_layout)
def get_config(self):
"""获取配置"""
config = {
'radius': self.radius.value(),
'source_epsg': self.source_epsg.value(),
}
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):
"""设置配置"""
if 'radius' in config:
self.radius.setValue(config['radius'])
if 'source_epsg' in config:
self.source_epsg.setValue(config['source_epsg'])
if 'deglint_img_path' in config:
self.deglint_img_file.set_path(config['deglint_img_path'])
if 'csv_path' in config:
self.csv_file.set_path(config['csv_path'])
if 'boundary_path' in config:
self.water_mask_file.set_path(config['boundary_path'])
if 'glint_mask_path' in config:
self.glint_mask_file.set_path(config['glint_mask_path'])
def update_from_config(self, work_dir=None, pipeline=None):
"""从全局配置/Pipeline 自动填充路径,实现上下游数据流转"""
if work_dir:
self.work_dir = work_dir
elif hasattr(self, 'work_dir') and self.work_dir:
pass
else:
self.work_dir = None
# 1. 水体掩膜:优先 pipeline,回退文件系统扫描
mask_path = None
if pipeline and hasattr(pipeline, 'water_mask_path') and pipeline.water_mask_path:
mask_path = pipeline.water_mask_path
if not mask_path and self.work_dir:
mask_path = scan_work_dir_for_input(self.work_dir, 'water_mask')
if mask_path and os.path.exists(str(mask_path)):
self.water_mask_file.set_path(str(mask_path))
# 2. 耀斑掩膜:文件系统扫描
if self.work_dir:
glint_path = scan_work_dir_for_input(self.work_dir, 'glint_mask')
if glint_path and os.path.exists(str(glint_path)):
self.glint_mask_file.set_path(str(glint_path))
# 3. 去耀斑影像:优先 pipeline.step_outputs,回退文件系统扫描
deglint_path = None
if pipeline and hasattr(pipeline, 'step_outputs'):
step3_outputs = getattr(pipeline, 'step_outputs', {}).get('step3', {})
deglint_path = (
step3_outputs.get('deglint_image')
or step3_outputs.get('output_path')
or step3_outputs.get('output_file')
or step3_outputs.get('deglint_img_path')
)
if not deglint_path and self.work_dir:
deglint_path = scan_work_dir_for_input(self.work_dir, 'deglint_image')
if deglint_path and os.path.exists(str(deglint_path)):
existing_deglint = self.deglint_img_file.get_path()
if (not existing_deglint or not existing_deglint.strip()):
self.deglint_img_file.set_path(str(deglint_path))
# 4. 处理后 CSV:文件系统扫描
if self.work_dir:
csv_path = scan_work_dir_for_input(self.work_dir, 'processed_data')
if csv_path and os.path.exists(str(csv_path)):
existing_csv = self.csv_file.get_path()
if (not existing_csv or not existing_csv.strip()):
self.csv_file.set_path(str(csv_path))
# 5. 输出路径
if self.work_dir and not self.output_file.get_path():
output_dir = resolve_subdir(self.work_dir, 'spectral_feature')
default_output_path = os.path.join(output_dir, "training_spectra.csv").replace('\\', '/')
self.output_file.set_path(default_output_path)
elif not self.work_dir:
self.output_file.set_path("")
def _on_run_single_clicked(self):
from src.gui.core.event_bus import global_event_bus
deglint_img_path = self.deglint_img_file.get_path()
csv_path = self.csv_file.get_path()
if not deglint_img_path:
QMessageBox.warning(self, "输入错误", "请选择去耀斑影像文件!")
return
if not csv_path:
QMessageBox.warning(self, "输入错误", "请选择处理后的 CSV 文件!")
return
if not self.glint_mask_file.get_path():
QMessageBox.warning(
self,
"输入错误",
"独立运行光谱特征提取时,必须选择耀斑掩膜文件。\n\n"
"请提供与去耀斑影像对应的耀斑二值掩膜(一般为步骤2输出的 severe_glint_area.dat)。",
)
return
config = {'step6_feature': self.get_config()}
global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step6_feature',
'config': config,
})