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