feat: 水质分析系统用户体验核心升级

This commit is contained in:
DXC
2026-05-06 11:33:35 +08:00
parent 69ce95cda4
commit 71e3aaa8cd
2 changed files with 223 additions and 78 deletions

View File

@ -242,7 +242,8 @@ class WaterQualityInversionPipeline:
ndwi_threshold: float = 0.4,
use_ndwi: bool = False,
skip_dependency_check: bool = False,
generate_png: bool = True) -> str:
generate_png: bool = True,
output_path: Optional[str] = None) -> str:
"""
步骤1: 生成或设置水域mask
@ -265,6 +266,8 @@ class WaterQualityInversionPipeline:
ndwi_threshold: NDWI阈值当use_ndwi=True时使用
use_ndwi: 是否使用NDWI方法从影像生成水体掩膜
generate_png: 是否生成输入影像的PNG预览图默认True
output_path: 指定输出掩膜文件的保存路径(可选)。如果提供,掩膜将保存到此路径;
如果为None则使用默认路径self.water_mask_dir
Returns:
dat格式的水域掩膜文件路径
@ -287,12 +290,19 @@ class WaterQualityInversionPipeline:
raise ValueError(f"影像文件不存在: {img_path}")
print(f"使用NDWI方法从影像生成水体掩膜阈值={ndwi_threshold}...")
output_path = str(self.water_mask_dir / "water_mask_from_ndwi.dat")
# 使用用户指定的输出路径,或使用默认路径
if output_path:
ndwi_output_path = output_path
# 确保输出目录存在
os.makedirs(Path(output_path).parent, exist_ok=True)
else:
ndwi_output_path = str(self.water_mask_dir / "water_mask_from_ndwi.dat")
# 检查文件是否已存在,避免重复生成
if Path(output_path).exists():
print(f"检测到已存在的NDWI掩膜文件直接使用: {output_path}")
self.water_mask_path = output_path
if Path(ndwi_output_path).exists():
print(f"检测到已存在的NDWI掩膜文件直接使用: {ndwi_output_path}")
self.water_mask_path = ndwi_output_path
step_end_time = time.time()
self._record_step_time("步骤1: 生成水域mask", step_start_time, step_end_time, status="skipped")
print(f"水域掩膜已设置: {self.water_mask_path}")
@ -306,8 +316,8 @@ class WaterQualityInversionPipeline:
# 执行NDWI水体提取
from src.utils.extract_water_area import ndwi
ndwi(img_path, ndwi_threshold, output_path)
self.water_mask_path = output_path
ndwi(img_path, ndwi_threshold, ndwi_output_path)
self.water_mask_path = ndwi_output_path
step_end_time = time.time()
self._record_step_time("步骤1: 生成水域mask", step_start_time, step_end_time)
print(f"已生成NDWI水体掩膜: {self.water_mask_path}")
@ -333,12 +343,19 @@ class WaterQualityInversionPipeline:
raise ValueError("当mask_path为shp格式时必须提供img_path参数用于栅格化")
print(f"检测到shp格式的水体掩膜正在转换为dat格式...")
output_path = str(self.water_mask_dir / "water_mask_from_shp.dat")
# 使用用户指定的输出路径,或使用默认路径
if output_path:
shp_output_path = output_path
# 确保输出目录存在
os.makedirs(Path(output_path).parent, exist_ok=True)
else:
shp_output_path = str(self.water_mask_dir / "water_mask_from_shp.dat")
# 检查文件是否已存在,避免重复栅格化
if Path(output_path).exists():
print(f"检测到已存在的栅格化掩膜文件,直接使用: {output_path}")
self.water_mask_path = output_path
if Path(shp_output_path).exists():
print(f"检测到已存在的栅格化掩膜文件,直接使用: {shp_output_path}")
self.water_mask_path = shp_output_path
step_end_time = time.time()
self._record_step_time("步骤1: 生成水域mask", step_start_time, step_end_time, status="skipped")
print(f"水域掩膜已设置: {self.water_mask_path}")
@ -352,8 +369,8 @@ class WaterQualityInversionPipeline:
# 执行栅格化
from src.utils.extract_water_area import rasterize_shp
rasterize_shp(mask_path, output_path, img_path)
self.water_mask_path = output_path
rasterize_shp(mask_path, shp_output_path, img_path)
self.water_mask_path = shp_output_path
step_end_time = time.time()
self._record_step_time("步骤1: 生成水域mask", step_start_time, step_end_time)
print(f"已生成dat格式的水域掩膜: {self.water_mask_path}")
@ -483,13 +500,13 @@ class WaterQualityInversionPipeline:
fontsize=12, fontweight='bold')
ax.axis('off')
# 添加比例尺信息
# 添加比例尺信息(白色文字,黑色背景下清晰可见)
geo_transform = dataset.GetGeoTransform()
if geo_transform:
pixel_size_x = abs(geo_transform[1])
pixel_size_y = abs(geo_transform[5])
scale_text = f"分辨率: {pixel_size_x:.2f}m x {pixel_size_y:.2f}m | 尺寸: {width} x {height}"
fig.text(0.5, 0.02, scale_text, ha='center', fontsize=10, style='italic')
fig.text(0.5, 0.02, scale_text, ha='center', fontsize=10, style='italic', color='white')
plt.tight_layout()
plt.savefig(png_path, dpi=150, bbox_inches='tight', pad_inches=0.1)

View File

@ -838,9 +838,19 @@ class VisualizationWorkerThread(QThread):
class FileSelectWidget(QWidget):
"""文件选择组件"""
def __init__(self, label_text, file_filter="All Files (*.*)", parent=None):
def __init__(self, label_text, file_filter="All Files (*.*)", mode="open", parent=None):
"""
初始化文件选择组件
Args:
label_text: 标签文本
file_filter: 文件过滤器
mode: 选择模式 - "open"(打开文件) 或 "save"(保存文件)
parent: 父控件
"""
super().__init__(parent)
self.file_filter = file_filter
self.mode = mode # "open" 或 "save"
self.init_ui(label_text)
def init_ui(self, label_text):
@ -850,7 +860,8 @@ class FileSelectWidget(QWidget):
self.label = QLabel(label_text)
self.label.setMinimumWidth(120)
self.line_edit = QLineEdit()
self.line_edit.setPlaceholderText("请选择文件...")
placeholder = "请选择保存路径..." if self.mode == "save" else "请选择文件..."
self.line_edit.setPlaceholderText(placeholder)
self.browse_btn = QPushButton("浏览...")
self.browse_btn.setMaximumWidth(80)
self.browse_btn.clicked.connect(self.browse_file)
@ -863,9 +874,14 @@ class FileSelectWidget(QWidget):
def browse_file(self):
"""浏览文件"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择文件", "", self.file_filter
)
if self.mode == "save":
file_path, _ = QFileDialog.getSaveFileName(
self, "保存文件", "", self.file_filter
)
else:
file_path, _ = QFileDialog.getOpenFileName(
self, "选择文件", "", self.file_filter
)
if file_path:
self.line_edit.setText(file_path)
@ -943,6 +959,29 @@ class Step1Panel(QWidget):
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_group.setLayout(method_layout)
layout.addWidget(method_group)
@ -961,7 +1000,7 @@ class Step1Panel(QWidget):
layout.addWidget(self.img_file)
# NDWI参数设置
ndwi_group = QGroupBox("NDWI参数设置")
self.ndwi_group = QGroupBox("NDWI参数设置")
ndwi_layout = QVBoxLayout()
# NDWI阈值
@ -976,20 +1015,33 @@ class Step1Panel(QWidget):
threshold_layout.addStretch()
ndwi_layout.addLayout(threshold_layout)
ndwi_group.setLayout(ndwi_layout)
layout.addWidget(ndwi_group)
self.ndwi_group.setLayout(ndwi_layout)
layout.addWidget(self.ndwi_group)
# 输出文件路径
# 输出文件路径使用save模式
self.output_file = FileSelectWidget(
"输出掩膜:",
"Mask Files (*.dat *.tif);;All Files (*.*)"
"Mask Files (*.dat *.tif);;All Files (*.*)",
mode="save"
)
self.output_file.line_edit.setPlaceholderText("water_mask.dat")
layout.addWidget(self.output_file)
# 提示信息
hint = QLabel("提示: 如果掩膜文件是Shapefile(.shp)需要提供参考影像用于栅格化如果使用NDWI自动生成只需要提供参考影像")
hint.setStyleSheet("color: #666; font-size: 10px;")
# 提示信息 - 专业的 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)
# 启用步骤
@ -1014,21 +1066,39 @@ class Step1Panel(QWidget):
self.update_ui_state()
def update_ui_state(self):
"""根据选择的掩膜生成方式更新UI状态"""
"""根据选择的掩膜生成方式更新UI状态(使用显示/隐藏控制)"""
use_ndwi = self.use_ndwi_radio.isChecked()
# 掩膜文件在NDWI模式下禁用
self.mask_file.setEnabled(not use_ndwi)
# 动态显示/隐藏组件
if use_ndwi:
# 使用NDWI模式隐藏掩膜文件显示NDWI参数
self.mask_file.setVisible(False)
self.ndwi_group.setVisible(True)
else:
# 使用现有掩膜模式显示掩膜文件隐藏NDWI参数
self.mask_file.setVisible(True)
self.ndwi_group.setVisible(False)
# 影像文件在两种模式下都需要
self.img_file.setEnabled(True)
# 参考影像和输出掩膜在两种模式下都显示
self.img_file.setVisible(True)
self.output_file.setVisible(True)
# NDWI参数在NDWI模式下启用
for i in range(self.layout().count()):
widget = self.layout().itemAt(i).widget()
if widget and isinstance(widget, QGroupBox) and widget.title() == "NDWI参数设置":
widget.setEnabled(use_ndwi)
break
def update_work_directory(self, work_dir):
"""
接收主窗口传来的工作目录,自动填充输出路径
Args:
work_dir: 工作目录路径
"""
if not work_dir:
return
# 自动生成输出掩膜的完整路径
output_dir = os.path.join(work_dir, "1_water_mask")
os.makedirs(output_dir, exist_ok=True) # 确保目录存在
default_output_path = os.path.join(output_dir, "water_mask_out.dat")
self.output_file.set_path(default_output_path)
def get_config(self):
"""获取配置"""
@ -5393,6 +5463,7 @@ class WaterQualityGUI(QMainWindow):
self.pipeline = None
self.worker = None
self.config_file = None
self.work_dir = None # 工作目录
# 训练数据模式状态
self.has_training_data = True # 默认有训练数据
@ -5407,6 +5478,10 @@ class WaterQualityGUI(QMainWindow):
self.apply_stylesheet()
self._disable_wheel_for_all_spinboxes()
# 延迟调用工作目录选择对话框,确保主界面已完全渲染
# 100ms 延迟足以让 GUI 事件循环启动并显示主窗口
QTimer.singleShot(100, self.init_workspace)
def _init_step_dependencies(self):
"""初始化步骤依赖关系和标准输出路径"""
# 定义每个步骤的标准输出路径模式(相对于工作目录)
@ -5550,6 +5625,59 @@ class WaterQualityGUI(QMainWindow):
combobox.setFocusPolicy(Qt.StrongFocus)
combobox.wheelEvent = lambda event, cb=combobox: None
def init_workspace(self):
"""
初始化工作空间:弹出对话框选择工作目录
此方法通过 QTimer 延迟调用,确保主界面已完全渲染后再弹出对话框
如果用户取消或关闭,则退出程序
"""
from PyQt5.QtWidgets import QMessageBox
msg_box = QMessageBox()
msg_box.setIcon(QMessageBox.Information)
msg_box.setWindowTitle("选择工作目录")
msg_box.setText("欢迎使用水质参数反演分析系统!\n\n请选择工作目录来保存所有分析结果。")
msg_box.setInformativeText("工作目录将用于存储:\n• 水域掩膜文件\n• 耀斑检测结果\n• 模型训练数据\n• 预测结果与分布图\n\n点击'确定'选择目录")
msg_box.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
msg_box.setDefaultButton(QMessageBox.Ok)
result = msg_box.exec_()
if result == QMessageBox.Cancel:
QMessageBox.warning(None, "取消操作", "未选择工作目录,程序将退出。")
sys.exit(0)
# 弹出目录选择对话框
work_dir = QFileDialog.getExistingDirectory(
self, # 使用 self 作为父窗口,而不是 None
"选择工作目录",
"",
QFileDialog.ShowDirsOnly | QFileDialog.DontResolveSymlinks
)
if not work_dir:
QMessageBox.critical(self, "错误", "必须选择工作目录才能使用系统!\n程序即将退出。")
sys.exit(0)
self.work_dir = work_dir
print(f"✓ 已选择工作目录: {self.work_dir}")
# 选择完成后,自动填充输出路径
self._auto_fill_output_paths()
def _auto_fill_output_paths(self):
"""
根据工作目录自动填充各步骤的输出路径
"""
if not self.work_dir:
return
# Step1: 输出掩膜路径
if hasattr(self, 'step1_panel') and hasattr(self.step1_panel, 'output_file'):
default_mask_path = os.path.join(self.work_dir, "1_water_mask", "water_mask_out.dat")
self.step1_panel.output_file.set_path(default_mask_path)
self.step1_panel.update_work_directory(self.work_dir)
def init_ui(self):
"""初始化UI"""
self.setWindowTitle("水质参数反演分析系统 v1.0")