diff --git a/src/core/water_quality_inversion_pipeline_GUI.py b/src/core/water_quality_inversion_pipeline_GUI.py index 91e7eec..268508c 100644 --- a/src/core/water_quality_inversion_pipeline_GUI.py +++ b/src/core/water_quality_inversion_pipeline_GUI.py @@ -242,20 +242,21 @@ 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 - + 支持三种方式生成水域掩膜: 1. 基于shp文件栅格化 2. 使用现有的栅格格式掩膜文件 3. 基于NDWI从影像自动生成水体掩膜 - + 当提供img_path时,会自动生成PNG预览图,基于波长选择RGB波段: - 红波段: ~650nm - 绿波段: ~550nm - 蓝波段: ~460nm - + Args: mask_path: 水体掩膜文件路径,支持: - shp格式文件(.shp):需要提供img_path用于栅格化 @@ -265,7 +266,9 @@ class WaterQualityInversionPipeline: ndwi_threshold: NDWI阈值(当use_ndwi=True时使用) use_ndwi: 是否使用NDWI方法从影像生成水体掩膜 generate_png: 是否生成输入影像的PNG预览图(默认True) - + output_path: 指定输出掩膜文件的保存路径(可选)。如果提供,掩膜将保存到此路径; + 如果为None,则使用默认路径(self.water_mask_dir) + Returns: dat格式的水域掩膜文件路径 """ @@ -285,37 +288,44 @@ class WaterQualityInversionPipeline: raise ValueError("当use_ndwi=True时,必须提供img_path参数用于生成NDWI掩膜") if not Path(img_path).exists(): 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}") - + # 生成水域掩膜叠加图(如果不存在) overlay_path = self.water_mask_dir / "water_mask_overlay.png" if generate_png and img_path is not None and Path(img_path).exists() and not overlay_path.exists(): self._generate_water_mask_overlay(img_path, self.water_mask_path) - + return self.water_mask_path - + # 执行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}") - + # 生成水域掩膜叠加图 if generate_png: self._generate_water_mask_overlay(img_path, self.water_mask_path) - + return self.water_mask_path elif mask_path is None: @@ -331,37 +341,44 @@ class WaterQualityInversionPipeline: # 如果是shp文件,需要栅格化为dat if img_path is None: 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}") - + # 生成水域掩膜叠加图(如果不存在) overlay_path = self.water_mask_dir / "water_mask_overlay.png" if generate_png and img_path is not None and Path(img_path).exists() and not overlay_path.exists(): self._generate_water_mask_overlay(img_path, self.water_mask_path) - + return self.water_mask_path - + # 执行栅格化 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}") - + # 生成水域掩膜叠加图 if generate_png: self._generate_water_mask_overlay(img_path, self.water_mask_path) - + return self.water_mask_path else: @@ -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) diff --git a/src/gui/water_quality_gui.py b/src/gui/water_quality_gui.py index a179b14..f7d3fc9 100644 --- a/src/gui/water_quality_gui.py +++ b/src/gui/water_quality_gui.py @@ -838,34 +838,50 @@ 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): layout = QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) - + 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) - + layout.addWidget(self.label) layout.addWidget(self.line_edit, 1) layout.addWidget(self.browse_btn) - + self.setLayout(layout) - + 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) @@ -938,32 +954,55 @@ class Step1Panel(QWidget): self.use_existing_radio = QRadioButton("使用现有掩膜文件") self.use_existing_radio.setChecked(True) 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_group.setLayout(method_layout) layout.addWidget(method_group) - + # 掩膜文件选择 self.mask_file = FileSelectWidget( "掩膜文件:", "Shapefiles (*.shp);;Raster Files (*.dat *.tif);;All Files (*.*)" ) layout.addWidget(self.mask_file) - + # 影像文件选择(用于shp栅格化或NDWI生成) self.img_file = FileSelectWidget( "参考影像:", "Image Files (*.bsq *.dat *.tif);;All Files (*.*)" ) layout.addWidget(self.img_file) - + # NDWI参数设置 - ndwi_group = QGroupBox("NDWI参数设置") + self.ndwi_group = QGroupBox("NDWI参数设置") ndwi_layout = QVBoxLayout() - + # NDWI阈值 threshold_layout = QHBoxLayout() threshold_layout.addWidget(QLabel("NDWI阈值:")) @@ -975,60 +1014,91 @@ class Step1Panel(QWidget): threshold_layout.addWidget(self.ndwi_threshold) 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) - + # 启用步骤 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) - + # 连接信号 self.use_existing_radio.toggled.connect(self.update_ui_state) self.use_ndwi_radio.toggled.connect(self.update_ui_state) - + layout.addStretch() self.setLayout(layout) - + # 初始UI状态 self.update_ui_state() def update_ui_state(self): - """根据选择的掩膜生成方式更新UI状态""" + """根据选择的掩膜生成方式更新UI状态(使用显示/隐藏控制)""" use_ndwi = self.use_ndwi_radio.isChecked() + + # 动态显示/隐藏组件 + 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.setVisible(True) + self.output_file.setVisible(True) + + def update_work_directory(self, work_dir): + """ + 接收主窗口传来的工作目录,自动填充输出路径 - # 掩膜文件在NDWI模式下禁用 - self.mask_file.setEnabled(not use_ndwi) + Args: + work_dir: 工作目录路径 + """ + if not work_dir: + return - # 影像文件在两种模式下都需要 - self.img_file.setEnabled(True) + # 自动生成输出掩膜的完整路径 + output_dir = os.path.join(work_dir, "1_water_mask") + os.makedirs(output_dir, exist_ok=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 + 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,20 +5463,25 @@ class WaterQualityGUI(QMainWindow): self.pipeline = None self.worker = None self.config_file = None - + self.work_dir = None # 工作目录 + # 训练数据模式状态 self.has_training_data = True # 默认有训练数据 - + # 步骤输出路径记录 self.step_outputs = {} # 记录每个步骤的输出路径 - + # 定义步骤依赖关系和标准输出路径 self._init_step_dependencies() - + self.init_ui() self.apply_stylesheet() self._disable_wheel_for_all_spinboxes() + # 延迟调用工作目录选择对话框,确保主界面已完全渲染 + # 100ms 延迟足以让 GUI 事件循环启动并显示主窗口 + QTimer.singleShot(100, self.init_workspace) + def _init_step_dependencies(self): """初始化步骤依赖关系和标准输出路径""" # 定义每个步骤的标准输出路径模式(相对于工作目录) @@ -5549,6 +5624,59 @@ class WaterQualityGUI(QMainWindow): for combobox in self.findChildren(QComboBox): 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"""