#!/usr/bin/env python # -*- coding: utf-8 -*- """ Step9 面板 - 水色指数反演(直接处理去耀斑 BSQ 影像) 将 waterindex.csv 中的公式直接应用于去耀斑高光谱影像, 输出各水质参数指数的 GeoTIFF 栅格图像。 """ import os import traceback from pathlib import Path from typing import Dict, List, Optional from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QFormLayout, QGroupBox, QLabel, QLineEdit, QComboBox, QCheckBox, QPushButton, QFileDialog, QMessageBox, QListWidget, QListWidgetItem, QAbstractItemView, QProgressBar, QTextEdit, QFrame, QScrollArea, QSizePolicy, ) from PyQt5.QtGui import QFont from PyQt5.QtCore import Qt, QThread, pyqtSignal from src.gui.components.custom_widgets import FileSelectWidget from src.gui.styles import ModernStylesheet class WaterIndexWorker(QThread): """后台线程:执行水色指数反演""" finished_ok = pyqtSignal(dict) failed = pyqtSignal(str) progress = pyqtSignal(str, float) # message, percent log = pyqtSignal(str) def __init__( self, bsq_path: str, hdr_path: str, output_dir: str, selected_formulas: List[str], waterindex_csv: str, water_mask_path: Optional[str] = None, work_dir: Optional[str] = None, ): super().__init__() self.bsq_path = bsq_path self.hdr_path = hdr_path self.output_dir = output_dir self.selected_formulas = selected_formulas self.waterindex_csv = waterindex_csv self.water_mask_path = water_mask_path self.work_dir = work_dir def run(self): try: from src.core.algorithms.waterindex_inversion import WaterIndexProcessor self.progress.emit("正在初始化水色指数处理器…", 2) processor = WaterIndexProcessor(self.waterindex_csv) self.progress.emit("正在读取影像元数据…", 5) # 获取影像元数据 meta = processor.get_image_metadata(self.bsq_path, self.hdr_path) if not meta: self.failed.emit("无法读取影像元数据,请检查 BSQ 和 HDR 文件是否匹配") return n_bands = meta.get('bands', 0) wv_range = meta.get('wavelength_range', '未知') self.log.emit( f"影像信息: {meta['width']}×{meta['height']} 像素, " f"{n_bands} 波段, {wv_range}" ) if self.water_mask_path: self.log.emit(f"使用水域掩膜: {self.water_mask_path}") # 使用 run_inversion 入口(含掩膜拦截链路) results = processor.run_inversion( deglint_img_path=self.bsq_path, work_dir=self.work_dir or self.output_dir, formula_csv_path=self.waterindex_csv, selected_formulas=self.selected_formulas, water_mask_path=self.water_mask_path, callback=self._on_progress, ) self.progress.emit(f"完成!共生成 {len(results)} 个指数图", 100) self.finished_ok.emit(results) except Exception as e: self.failed.emit(f"{e}\n{traceback.format_exc()}") def _on_progress(self, msg: str, pct: float): self.progress.emit(msg, pct) class Step11WaterColorPanel(QWidget): """步骤11:水色指数反演(直接处理 BSQ 影像)""" def __init__(self, parent=None): super().__init__(parent) self._worker: Optional[WaterIndexWorker] = None self._waterindex_csv = self._find_waterindex_csv() self._categories: List[str] = [] self._all_formulas: List[Dict] = [] self._formula_list_widgets: Dict[str, QListWidgetItem] = {} self.init_ui() self._load_formulas() def init_ui(self): layout = QVBoxLayout() # ---- 标题 ---- title = QLabel("步骤11:水色指数反演(高光谱影像直接处理)") title.setFont(QFont("Arial", 12, QFont.Bold)) layout.addWidget(title) # ---- 说明 ---- hint = QLabel( "将 waterindex.csv 中的公式直接应用于去耀斑高光谱影像(BSQ)," "输出各水质参数指数的 GeoTIFF 栅格图像。" "指数图可直接用于水质专题图生成。" ) hint.setWordWrap(True) hint.setStyleSheet(f"color: {ModernStylesheet.COLORS.get('text_secondary', '#666')};") layout.addWidget(hint) # ---- 输入影像选择 ---- input_group = QGroupBox("输入影像") input_layout = QFormLayout() self.bsq_file = FileSelectWidget( "去耀斑 BSQ 影像:", "BSQ Files (*.bsq);;DAT Files (*.dat);;All Files (*.*)" ) self.bsq_file.line_edit.setPlaceholderText("选择去耀斑处理后的 BSQ 影像") self.bsq_file.browse_btn.clicked.disconnect() self.bsq_file.browse_btn.clicked.connect(self._browse_bsq) input_layout.addRow("BSQ 影像:", self.bsq_file) self.hdr_file = FileSelectWidget( "ENVI 头文件:", "HDR Files (*.hdr);;All Files (*.*)" ) self.hdr_file.line_edit.setPlaceholderText("自动关联同路径 .hdr 文件") self.hdr_file.browse_btn.clicked.disconnect() self.hdr_file.browse_btn.clicked.connect(self._browse_hdr) input_layout.addRow("HDR 文件:", self.hdr_file) # 影像信息显示 self.meta_label = QLabel("未加载影像") self.meta_label.setStyleSheet( "background: #f0f0f0; padding: 4px 8px; border-radius: 4px; " "font-size: 12px; color: #333;" ) input_layout.addRow("影像信息:", self.meta_label) input_group.setLayout(input_layout) layout.addWidget(input_group) # ---- 公式选择 ---- formula_group = QGroupBox("公式选择") formula_layout = QGridLayout() # 类别过滤 formula_layout.addWidget(QLabel("按类别筛选:"), 0, 0) self.category_combo = QComboBox() self.category_combo.currentTextChanged.connect(self._on_category_changed) formula_layout.addWidget(self.category_combo, 0, 1, 1, 2) # 全选/取消全选 select_btn_layout = QHBoxLayout() self.select_all_btn = QPushButton("全选") self.select_all_btn.setMaximumWidth(80) self.select_all_btn.clicked.connect(self._select_all) select_btn_layout.addWidget(self.select_all_btn) self.deselect_all_btn = QPushButton("取消全选") self.deselect_all_btn.setMaximumWidth(80) self.deselect_all_btn.clicked.connect(self._deselect_all) select_btn_layout.addWidget(self.deselect_all_btn) select_btn_layout.addStretch() formula_layout.addLayout(select_btn_layout, 0, 3) # 公式列表 self.formula_list = QListWidget() self.formula_list.setSelectionMode(QAbstractItemView.MultiSelection) self.formula_list.setMinimumHeight(200) self.formula_list.itemChanged.connect(self._on_item_changed) formula_layout.addWidget(self.formula_list, 1, 0, 1, 4) formula_group.setLayout(formula_layout) layout.addWidget(formula_group) # ---- 输出设置 ---- output_group = QGroupBox("输出设置") output_layout = QFormLayout() self.output_dir = FileSelectWidget( "输出目录:", "Directories" ) self.output_dir.line_edit.setPlaceholderText("留空 → 工作目录/10_WaterIndex_Images") self.output_dir.browse_btn.clicked.disconnect() self.output_dir.browse_btn.clicked.connect(self._browse_output_dir) output_layout.addRow("输出目录:", self.output_dir) self.format_combo = QComboBox() self.format_combo.addItems(["GTiff (GeoTIFF)", "ENVI", "PCI"]) self.format_combo.setCurrentIndex(0) output_layout.addRow("输出格式:", self.format_combo) output_group.setLayout(output_layout) layout.addWidget(output_group) # ---- 进度显示 ---- self.progress_bar = QProgressBar() self.progress_bar.setMinimum(0) self.progress_bar.setMaximum(100) self.progress_bar.setValue(0) self.progress_bar.setTextVisible(True) layout.addWidget(self.progress_bar) self.progress_label = QLabel("") self.progress_label.setStyleSheet("font-size: 11px; color: #666;") layout.addWidget(self.progress_label) # ---- 启用 & 运行 ---- 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) layout.addStretch() self.setLayout(layout) def _find_waterindex_csv(self) -> str: """查找 waterindex.csv 路径""" candidates = [ Path(__file__).parent.parent.parent / "model" / "waterindex.csv", Path(__file__).parent.parent.parent.parent / "src" / "gui" / "model" / "waterindex.csv", ] for c in candidates: if c.exists(): return str(c) return "" def _load_formulas(self): """加载 waterindex.csv 中的公式""" if not self._waterindex_csv or not Path(self._waterindex_csv).exists(): self.meta_label.setText("⚠️ waterindex.csv 未找到") return import csv self._all_formulas = [] try: with open(self._waterindex_csv, 'r', encoding='utf-8-sig') as f: reader = csv.DictReader(f) self._all_formulas = list(reader) except Exception as e: self.meta_label.setText(f"⚠️ 加载公式失败: {e}") return # 提取所有类别 cats = set() for f in self._all_formulas: c = f.get('Category', '').strip() if c: cats.add(c) self._categories = sorted(cats) self.category_combo.clear() self.category_combo.addItem("全部") self.category_combo.addItems(self._categories) self._populate_list("全部") def _populate_list(self, category: str): """根据类别填充公式列表""" self.formula_list.clear() self._formula_list_widgets.clear() formulas_to_show = ( [f for f in self._all_formulas if f.get('Category', '') == category] if category != "全部" else self._all_formulas ) for f in formulas_to_show: name = f.get('Formula_Name', '') formula_str = f.get('Formula', '') cat = f.get('Category', '') ftype = f.get('Formula_Type', '') item = QListWidgetItem() item.setFlags(item.flags() | Qt.ItemIsUserCheckable) item.setCheckState(Qt.Checked) item.setData(Qt.UserRole, name) item.setText( f"☑ {name} [{cat}] ({ftype})\n {formula_str}" ) item.setToolTip(f"{name}\n{category}\n{formula_str}") self.formula_list.addItem(item) self._formula_list_widgets[name] = item def _on_category_changed(self, category: str): self._populate_list(category) def _select_all(self): for item in self.formula_list.selectedItems(): item.setCheckState(Qt.Checked) # 也全选当前显示的 for i in range(self.formula_list.count()): it = self.formula_list.item(i) it.setCheckState(Qt.Checked) def _deselect_all(self): for i in range(self.formula_list.count()): it = self.formula_list.item(i) it.setCheckState(Qt.Unchecked) def _on_item_changed(self, item: QListWidgetItem): pass # 可扩展:实时统计选中数量 def _browse_bsq(self): path, _ = QFileDialog.getOpenFileName( self, "选择去耀斑 BSQ 影像", "", "BSQ Files (*.bsq);;DAT Files (*.dat);;All Files (*.*)" ) if path: self.bsq_file.set_path(path) # 自动关联同路径 hdr hdr = Path(path).with_suffix('.hdr') if hdr.exists(): self.hdr_file.set_path(str(hdr)) self._load_metadata(path, str(hdr) if hdr.exists() else "") def _browse_hdr(self): path, _ = QFileDialog.getOpenFileName( self, "选择 ENVI 头文件", "", "HDR Files (*.hdr);;All Files (*.*)" ) if path: self.hdr_file.set_path(path) bsq_path = self.bsq_file.get_path() if bsq_path: self._load_metadata(bsq_path, path) def _browse_output_dir(self): d = QFileDialog.getExistingDirectory(self, "选择输出目录", "") if d: self.output_dir.set_path(d) def _load_metadata(self, bsq_path: str, hdr_path: str): """加载并显示影像元数据""" if not bsq_path or not Path(bsq_path).exists(): self.meta_label.setText("⚠️ 影像文件不存在") return if not hdr_path or not Path(hdr_path).exists(): self.meta_label.setText("⚠️ 头文件不存在") return try: from src.core.algorithms.waterindex_inversion import WaterIndexProcessor processor = WaterIndexProcessor(self._waterindex_csv) meta = processor.get_image_metadata(bsq_path, hdr_path) if meta: self.meta_label.setText( f"✅ {meta['width']}×{meta['height']} | " f"{meta['bands']} 波段 | {meta.get('wavelength_range', '未知')} | " f"驱动: {meta['driver']}" ) else: self.meta_label.setText("⚠️ 无法读取元数据") except Exception as e: self.meta_label.setText(f"⚠️ 元数据读取失败: {e}") def _get_selected_formula_names(self) -> List[str]: names = [] for i in range(self.formula_list.count()): item = self.formula_list.item(i) if item.checkState() == Qt.Checked: name = item.data(Qt.UserRole) if name: names.append(name) return names def _get_default_work_dir(self) -> str: if hasattr(self, 'work_dir') and self.work_dir: return str(self.work_dir) mw = self.window() if mw and hasattr(mw, 'work_dir') and mw.work_dir: return str(mw.work_dir) return "" def get_config(self) -> dict: bsq = self.bsq_file.get_path() return { 'bsq_path': bsq, 'hdr_path': self.hdr_file.get_path(), 'deglint_img_path': bsq, 'output_dir': self.output_dir.get_path(), 'output_format': self.format_combo.currentText().split()[0], 'selected_formulas': self._get_selected_formula_names(), } def set_config(self, config: dict): if config.get('bsq_path'): self.bsq_file.set_path(config['bsq_path']) if config.get('hdr_path'): self.hdr_file.set_path(config['hdr_path']) if config.get('output_dir'): self.output_dir.set_path(config['output_dir']) if 'selected_formulas' in config: names = set(config['selected_formulas']) for i in range(self.formula_list.count()): item = self.formula_list.item(i) name = item.data(Qt.UserRole) item.setCheckState(Qt.Checked if name in names else Qt.Unchecked) def update_from_config(self, work_dir=None, pipeline=None): if work_dir: self.work_dir = work_dir elif hasattr(self, 'work_dir') and self.work_dir: pass else: self.work_dir = None main_window = self.window() # 自动填入去耀斑影像 if main_window and hasattr(main_window, 'step3_panel'): deglint_path = main_window.step3_panel.output_file.get_path() if deglint_path and not self.bsq_file.get_path(): if not os.path.isabs(deglint_path): deglint_path = os.path.join(self.work_dir or '', deglint_path).replace('\\', '/') self.bsq_file.set_path(deglint_path) hdr = Path(deglint_path).with_suffix('.hdr') if hdr.exists(): self.hdr_file.set_path(str(hdr)) self._load_metadata(deglint_path, str(hdr)) # 自动填入输出目录 if self.work_dir: out_dir = os.path.join(self.work_dir, "10_WaterIndex_Images").replace('\\', '/') os.makedirs(out_dir, exist_ok=True) if not self.output_dir.get_path(): self.output_dir.set_path(out_dir) def run_step(self): bsq_path = self.bsq_file.get_path().strip() hdr_path = self.hdr_file.get_path().strip() output_dir = self.output_dir.get_path().strip() # 验证输入 if not bsq_path: QMessageBox.warning(self, "输入错误", "请选择去耀斑 BSQ 影像!") return if not Path(bsq_path).exists(): QMessageBox.warning(self, "输入错误", f"BSQ 影像不存在:\n{bsq_path}") return if not hdr_path: # 尝试自动查找 auto_hdr = Path(bsq_path).with_suffix('.hdr') if auto_hdr.exists(): hdr_path = str(auto_hdr) self.hdr_file.set_path(hdr_path) else: QMessageBox.warning(self, "输入错误", "请选择 ENVI 头文件!") return if not Path(hdr_path).exists(): QMessageBox.warning(self, "输入错误", f"HDR 文件不存在:\n{hdr_path}") return if not output_dir: work_dir = self._get_default_work_dir() output_dir = os.path.join(work_dir, "10_WaterIndex_Images").replace('\\', '/') os.makedirs(output_dir, exist_ok=True) self.output_dir.set_path(output_dir) selected = self._get_selected_formula_names() if not selected: QMessageBox.warning(self, "输入错误", "请至少选择一个公式!") return if self._waterindex_csv and not Path(self._waterindex_csv).exists(): QMessageBox.warning(self, "配置错误", f"waterindex.csv 不存在:\n{self._waterindex_csv}") return # ── 自动扫描工作目录下的水域掩膜文件 ──────────────────────────── work_dir = self.work_dir or str(Path(bsq_path).parent) mask_dir = os.path.join(work_dir, "1_water_mask") water_mask_path: Optional[str] = None if os.path.isdir(mask_dir): # ★★★ glob 智能扫描:取任意 .dat 或 .tif 文件 ★★★ for pattern in ("*.dat", "*.tif", "*.TIF", "*.DT"): candidates = sorted(Path(mask_dir).glob(pattern)) if candidates: water_mask_path = str(candidates[0]) break if water_mask_path: print(f"[Step8] 自动找到水域掩膜: {water_mask_path}") else: print(f"[Step8] 未找到水域掩膜,跳过陆地剔除(陆地将保留在指数图中)") # 开始后台处理 self.run_btn.setEnabled(False) self.progress_bar.setValue(0) self.progress_label.setText("") self._worker = WaterIndexWorker( bsq_path=bsq_path, hdr_path=hdr_path, output_dir=output_dir, selected_formulas=selected, waterindex_csv=self._waterindex_csv, water_mask_path=water_mask_path, work_dir=work_dir, ) self._worker.progress.connect(self._on_progress) self._worker.finished_ok.connect(self._on_finished) self._worker.failed.connect(self._on_failed) self._worker.log.connect(lambda m: self.progress_label.setText(m)) self._worker.start() def _on_progress(self, msg: str, pct: float): self.progress_bar.setValue(int(pct)) self.progress_label.setText(msg) def _on_finished(self, results: Dict[str, str]): self.run_btn.setEnabled(True) n = len(results) QMessageBox.information( self, "执行成功", f"水色指数反演完成!\n" f"共生成 {n} 个指数图(GeoTIFF)。\n\n" f"输出目录: {self.output_dir.get_path()}" ) main_window = self.window() if main_window and hasattr(main_window, 'log_message'): main_window.log_message(f"步骤8:水色指数反演完成,生成 {n} 个指数图", "info") def _on_failed(self, err: str): self.run_btn.setEnabled(True) self.progress_bar.setValue(0) QMessageBox.critical(self, "执行错误", f"水色指数反演失败:\n\n{err[:500]}") def get_output_dir(self) -> str: return self.output_dir.get_path().strip() or "" def get_output_tif_paths(self) -> List[str]: """获取输出目录下的所有 GeoTIFF 文件路径""" out_dir = self.get_output_dir() if not out_dir or not os.path.isdir(out_dir): return [] return sorted( str(p) for p in Path(out_dir).glob("*.tif") if p.is_file() )