步骤一页面修改

This commit is contained in:
DXC
2026-06-26 13:07:22 +08:00
parent f63e6bce05
commit e54c2b8fe6
2 changed files with 156 additions and 200 deletions

View File

@ -1,14 +1,14 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Step1 面板 - 水域掩膜生成
Step1 面板 - 水域掩膜生成 (完美对齐无跳动重构)
"""
import os
import sys
from pathlib import Path
# 路径归一化 helper(与 pipeline.get_step_output_dir 互为表里)
# 路径归一化 helper
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
@ -16,231 +16,202 @@ from _step_path_resolver import resolve_subdir
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, QLabel,
QDoubleSpinBox, QCheckBox, QPushButton, QFormLayout, QRadioButton,
QMessageBox,
QCheckBox, QPushButton, QRadioButton, QLineEdit,
QMessageBox, QStackedWidget, 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 Step1Panel(QWidget):
"""1. 水域掩膜生成"""
def __init__(self, parent=None):
super().__init__(parent)
self.init_ui()
def init_ui(self):
layout = QVBoxLayout()
self.setStyleSheet(ModernStylesheet.get_main_stylesheet())
# 标题
main_layout = QVBoxLayout()
main_layout.setContentsMargins(24, 24, 24, 24)
main_layout.setSpacing(20)
# ==========================================
# 卡片 1:掩膜生成策略
# ==========================================
method_group = QGroupBox("📁 掩膜生成策略")
method_layout = QHBoxLayout()
method_layout.setSpacing(24)
method_layout.setContentsMargins(20, 24, 20, 20)
# 掩膜生成方式选择
method_group = QGroupBox("掩膜生成方式")
method_layout = QVBoxLayout()
# 使用现有掩膜文件
self.use_existing_radio = QRadioButton("使用现有掩膜文件")
self.use_existing_radio.setChecked(True)
# 【核心修复】:保留原生圆圈,但直接强制给予足够大(200x35)的安全盒模型,杜绝高DPI下任何形式的截断!
self.use_existing_radio.setMinimumSize(200, 35)
self.use_ndwi_radio = QRadioButton("使用 NDWI 自动生成")
self.use_ndwi_radio.setMinimumSize(200, 35)
method_layout.addWidget(self.use_existing_radio)
# 使用NDWI自动生成
self.use_ndwi_radio = QRadioButton("使用NDWI自动生成")
method_layout.addWidget(self.use_ndwi_radio)
# 应用QRadioButton样式(实心选中点)
radio_style = """
QRadioButton::indicator {
width: 16px;
height: 16px;
border-radius: 8px;
border: 2px solid #999;
}
QRadioButton::indicator:checked {
background-color: #0078D7;
border: 2px solid #0078D7;
}
QRadioButton::indicator:unchecked {
background-color: white;
border: 2px solid #999;
}
QRadioButton::indicator:hover {
border: 2px solid #0078D7;
}
"""
self.use_existing_radio.setStyleSheet(radio_style)
self.use_ndwi_radio.setStyleSheet(radio_style)
method_layout.addStretch()
method_group.setLayout(method_layout)
layout.addWidget(method_group)
main_layout.addWidget(method_group)
# 掩膜文件选择
# ==========================================
# 卡片 2:文件与参数配置 (绝对像素级对齐)
# ==========================================
config_group = QGroupBox("⚙️ 文件与参数配置")
config_layout = QVBoxLayout()
config_layout.setSpacing(16)
config_layout.setContentsMargins(20, 24, 20, 20)
hint = QLabel(
"💡 提示: 若掩膜文件是 Shapefile(.shp),需提供参考影像用于栅格化;若使用 NDWI 自动生成,只需提供参考影像即可。")
hint.setWordWrap(True)
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: 8px;
}}
""")
config_layout.addWidget(hint)
self.stack = QStackedWidget()
# 第 0 页:现有掩膜文件
self.mask_file = FileSelectWidget(
"掩膜文件:",
"现有掩膜文件:",
"Shapefiles (*.shp);;Raster Files (*.dat *.tif);;All Files (*.*)"
)
layout.addWidget(self.mask_file)
self.mask_file.label.setMinimumWidth(100)
self.stack.addWidget(self.mask_file)
# 影像文件选择(用于shp栅格化或NDWI生成)
# 第 1 页:NDWI 阈值参数
self.ndwi_widget = QWidget()
ndwi_layout = QHBoxLayout(self.ndwi_widget)
ndwi_layout.setContentsMargins(0, 0, 0, 0)
ndwi_layout.setSpacing(8)
ndwi_label = QLabel("NDWI 阈值:")
ndwi_label.setMinimumWidth(100)
ndwi_layout.addWidget(ndwi_label)
self.ndwi_threshold = QLineEdit()
validator = QDoubleValidator(0.0, 1.0, 2, self)
validator.setNotation(QDoubleValidator.StandardNotation)
self.ndwi_threshold.setValidator(validator)
self.ndwi_threshold.setText("0.40")
self.ndwi_threshold.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
ndwi_layout.addWidget(self.ndwi_threshold)
self.stack.addWidget(self.ndwi_widget)
config_layout.addWidget(self.stack)
# 参考影像
self.img_file = FileSelectWidget(
"参考影像:",
"输入参考影像:",
"Image Files (*.bsq *.dat *.tif);;All Files (*.*)"
)
layout.addWidget(self.img_file)
self.img_file.label.setMinimumWidth(100)
config_layout.addWidget(self.img_file)
# NDWI参数设置
self.ndwi_group = QGroupBox("NDWI参数设置")
ndwi_layout = QVBoxLayout()
config_group.setLayout(config_layout)
main_layout.addWidget(config_group)
# NDWI阈值
threshold_layout = QHBoxLayout()
threshold_layout.addWidget(QLabel("NDWI阈值:"))
self.ndwi_threshold = QDoubleSpinBox()
self.ndwi_threshold.setRange(0.0, 1.0)
self.ndwi_threshold.setSingleStep(0.05)
self.ndwi_threshold.setValue(0.4)
self.ndwi_threshold.setDecimals(2)
threshold_layout.addWidget(self.ndwi_threshold)
threshold_layout.addStretch()
ndwi_layout.addLayout(threshold_layout)
# ==========================================
# 卡片 3:输出与执行
# ==========================================
output_group = QGroupBox("🚀 输出与执行")
output_layout = QVBoxLayout()
output_layout.setSpacing(16)
output_layout.setContentsMargins(20, 24, 20, 20)
self.ndwi_group.setLayout(ndwi_layout)
layout.addWidget(self.ndwi_group)
# 输出文件路径(使用save模式)
self.output_file = FileSelectWidget(
"输出掩膜:",
"结果保存至:",
"Mask Files (*.dat *.tif);;All Files (*.*)",
mode="save"
)
self.output_file.label.setMinimumWidth(100)
self.output_file.line_edit.setPlaceholderText("water_mask.dat")
layout.addWidget(self.output_file)
output_layout.addWidget(self.output_file)
# 提示信息 - 专业的 Info Alert 样式
hint = QLabel("💡 提示: 如果掩膜文件是Shapefile(.shp),需要提供参考影像用于栅格化;如果使用NDWI自动生成,只需要提供参考影像")
hint.setWordWrap(True) # 允许自动换行
hint.setStyleSheet("""
QLabel {
color: #0055D4;
font-size: 13px;
font-weight: bold;
background-color: #E8F4FF;
border: 2px solid #0055D4;
border-radius: 8px;
padding: 12px 16px;
margin: 8px 0px;
}
""")
layout.addWidget(hint)
# 启用步骤
action_layout = QHBoxLayout()
self.enable_checkbox = QCheckBox("启用此步骤")
self.enable_checkbox.setChecked(True)
layout.addWidget(self.enable_checkbox)
self.enable_checkbox.setMinimumHeight(24)
action_layout.addWidget(self.enable_checkbox)
action_layout.addStretch()
# 独立运行按钮
self.run_btn = QPushButton("独立运行此步骤")
self.run_btn.setStyleSheet(ModernStylesheet.get_button_stylesheet('success'))
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)
layout.addWidget(self.run_btn)
action_layout.addWidget(self.run_btn)
output_layout.addLayout(action_layout)
output_group.setLayout(output_layout)
main_layout.addWidget(output_group)
# 连接信号
self.use_existing_radio.toggled.connect(self.update_ui_state)
self.use_ndwi_radio.toggled.connect(self.update_ui_state)
main_layout.addStretch()
self.setLayout(main_layout)
layout.addStretch()
self.setLayout(layout)
# 初始UI状态
self.update_ui_state()
def update_ui_state(self):
"""根据选择的掩膜生成方式更新UI状态(使用显示/隐藏控制)"""
use_ndwi = self.use_ndwi_radio.isChecked()
# 动态显示/隐藏组件
if use_ndwi:
# 使用NDWI模式:隐藏掩膜文件,显示NDWI参数和输出掩膜
self.mask_file.setVisible(False)
self.ndwi_group.setVisible(True)
self.output_file.setVisible(True) # 显示输出掩膜路径
# 当切换到NDWI模式时,如果工作目录已设置,自动填充输出路径
if hasattr(self, 'work_dir') and self.work_dir:
self._auto_fill_output_path()
else:
# 使用现有掩膜模式:显示掩膜文件,隐藏NDWI参数和输出掩膜
self.mask_file.setVisible(True)
self.ndwi_group.setVisible(False)
self.output_file.setVisible(False) # 隐藏输出掩膜路径
# 参考影像在两种模式下都显示
self.img_file.setVisible(True)
def update_work_directory(self, work_dir):
"""
保存工作目录引用,用于后续自动填充路径
Args:
work_dir: 工作目录路径
"""
if not work_dir:
return
# 保存工作目录引用
self.work_dir = work_dir
# 如果当前选中的是NDWI模式,立即填充输出路径
if self.use_ndwi_radio.isChecked():
self.stack.setCurrentWidget(self.ndwi_widget)
else:
self.stack.setCurrentWidget(self.mask_file)
if hasattr(self, 'work_dir') and self.work_dir:
self._auto_fill_output_path()
def update_work_directory(self, work_dir):
if not work_dir:
return
self.work_dir = work_dir
self._auto_fill_output_path()
def _auto_fill_output_path(self):
"""
自动填充输出掩膜路径(仅在NDWI模式下)
确保路径使用正斜杠,避免斜杠混用
"""
if not hasattr(self, 'work_dir') or not self.work_dir:
return
# 生成输出掩膜的完整路径(用 resolve_subdir 归一化,消除硬编码)
output_dir = resolve_subdir(self.work_dir, 'water_mask')
os.makedirs(output_dir, exist_ok=True) # 确保目录存在
# 统一使用正斜杠,避免 \ 和 / 混用
os.makedirs(output_dir, exist_ok=True)
default_output_path = os.path.join(output_dir, "water_mask_out.dat").replace('\\', '/')
self.output_file.set_path(default_output_path)
def get_config(self):
"""获取配置"""
use_ndwi = self.use_ndwi_radio.isChecked()
try:
ndwi_val = float(self.ndwi_threshold.text().strip())
except ValueError:
ndwi_val = 0.4
config = {
'mask_path': None if use_ndwi else self.mask_file.get_path(),
'use_ndwi': use_ndwi,
'ndwi_threshold': self.ndwi_threshold.value()
'ndwi_threshold': ndwi_val
}
# 参考影像路径(两种模式都可能需要)
img_path = self.img_file.get_path()
if img_path:
config['img_path'] = img_path
# 输出路径:仅在NDWI模式下有效
if use_ndwi:
output_path = self.output_file.get_path()
if output_path:
config['output_path'] = output_path
else:
# 使用现有掩膜时,不传递output_path,避免底层错误尝试保存文件
config['output_path'] = None
output_path = self.output_file.get_path()
if output_path:
config['output_path'] = output_path
return config
def set_config(self, config):
"""设置配置"""
if 'mask_path' in config:
self.mask_file.set_path(config['mask_path'])
if 'img_path' in config:
@ -253,20 +224,13 @@ class Step1Panel(QWidget):
else:
self.use_existing_radio.setChecked(True)
if 'ndwi_threshold' in config:
self.ndwi_threshold.setValue(config['ndwi_threshold'])
self.ndwi_threshold.setText(f"{config['ndwi_threshold']:.2f}")
self.update_ui_state()
def _on_run_single_clicked(self):
"""通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor)。
替代旧有的 parent 链上溯查找 run_single_step 的紧耦合方式。
PipelineExecutor 在 __init__ 中订阅 RequestRunSingleStep 事件,
收到后调用 run_single_step(step_name, config) 统一处理预检/工作目录/执行。
"""
from src.gui.core.event_bus import global_event_bus
# 验证输入(与旧 run_step 逻辑一致)
if self.use_ndwi_radio.isChecked():
img_path = self.img_file.get_path()
if not img_path:
@ -287,36 +251,4 @@ class Step1Panel(QWidget):
global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step1',
'config': config,
})
def run_step(self):
"""独立运行步骤1(旧版 parent 链上溯方式,保留兼容)。"""
# 验证输入
if self.use_ndwi_radio.isChecked():
# NDWI模式:需要影像文件
img_path = self.img_file.get_path()
if not img_path:
QMessageBox.warning(self, "输入错误", "请选择参考影像文件!")
return
else:
# 现有掩膜模式:需要掩膜文件
mask_path = self.mask_file.get_path()
if not mask_path:
QMessageBox.warning(self, "输入错误", "请选择掩膜文件!")
return
# 如果是shp文件,还需要影像文件
if mask_path.lower().endswith('.shp'):
img_path = self.img_file.get_path()
if not img_path:
QMessageBox.warning(self, "输入错误", "当使用shp文件时,需要提供参考影像用于栅格化!")
return
# 获取父窗口并运行步骤
parent = self.parent()
while parent and not hasattr(parent, 'run_single_step'):
parent = parent.parent()
if parent and hasattr(parent, 'run_single_step'):
config = {'step1': self.get_config()}
parent.run_single_step("step1", config)
})