From 08a9a0337df75de99c18c880fe6f9fe0c00a68d5 Mon Sep 17 00:00:00 2001 From: DXC Date: Wed, 24 Jun 2026 11:43:44 +0800 Subject: [PATCH] =?UTF-8?q?refactor(step10=5Fpanel):=20=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E5=88=B0=E6=95=A3=E7=82=B9=20CSV=20=E6=A8=A1=E5=BC=8F=20UI=20+?= =?UTF-8?q?=20Worker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/panels/step10_watercolor_panel.py | 420 +++++++++------------- 1 file changed, 173 insertions(+), 247 deletions(-) diff --git a/src/gui/panels/step10_watercolor_panel.py b/src/gui/panels/step10_watercolor_panel.py index 01d0c43..1242c20 100644 --- a/src/gui/panels/step10_watercolor_panel.py +++ b/src/gui/panels/step10_watercolor_panel.py @@ -1,10 +1,17 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """ -Step10 面板 - 水色指数反演(直接处理去耀斑 BSQ 影像) +Step10 面板 - 水色指数反演(散点 CSV 模式) -将 waterindex.csv 中的公式直接应用于去耀斑高光谱影像, -输出各水质参数指数的 GeoTIFF 栅格图像。 +与 Step 9 (ML 预测) 完全对称的【散点处理模式】: + +* 输入:Step 4 输出的 ``sampling_spectra.csv``(散点+全波段光谱) +* 处理:解析 ``waterindex.csv`` 中的公式,**逐行**对每个采样点计算水色指数 +* 输出:每个公式一个 CSV,列严格为 ``longitude, latitude, ``, + 可直接喂给 Step 11 ContentMapper +* 输出目录:默认 ``{work_dir}/10_WaterIndex_CSV/`` + +注:原"读 BSQ 全图→GeoTIFF"模式已废弃(科学上误差大且与 GIS 栅格计算器重复)。 """ import os @@ -34,79 +41,66 @@ from src.gui.styles import ModernStylesheet class WaterIndexWorker(QThread): - """后台线程:执行水色指数反演""" - finished_ok = pyqtSignal(dict) - failed = pyqtSignal(str) + """后台线程:散点 CSV → 逐行公式计算 → 多 CSV 输出 + + 应用 Step 13 QThread 协议: + - 三信号 ``progress / finished / error`` 命名严格遵循约定 + (注意 ``finished`` 不能用——会覆盖 QThread 内建同名信号, + 导致 _on_finished 不被回调时按钮不会恢复) + - 进度回调两参 (msg: str, pct: float) + """ progress = pyqtSignal(str, float) # message, percent - log = pyqtSignal(str) + finished_ok = pyqtSignal(dict) # {公式名: 输出 CSV 路径} + error = pyqtSignal(str) # error message def __init__( self, - bsq_path: str, - hdr_path: str, + sampling_csv_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.sampling_csv_path = sampling_csv_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}" + from src.core.algorithms.waterindex_inversion import ( + WaterIndexCsvProcessor, ) - if self.water_mask_path: - self.log.emit(f"使用水域掩膜: {self.water_mask_path}") + self.progress.emit("正在初始化散点水色指数处理器…", 2) - # 使用 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, + processor = WaterIndexCsvProcessor(self.waterindex_csv) + + # 散点 CSV → 逐公式一个 CSV + out_files = processor.compute_indices_from_csv( + sampling_csv_path=self.sampling_csv_path, + output_dir=self.output_dir, + selected_formulas=self.selected_formulas or None, + progress_callback=lambda m, p: self.progress.emit(m, p), ) - self.progress.emit(f"完成!共生成 {len(results)} 个指数图", 100) - self.finished_ok.emit(results) + self.progress.emit( + f"完成!共生成 {len(out_files)} 个指数 CSV", 100 + ) + self.finished_ok.emit(out_files) + except FileNotFoundError as e: + self.error.emit(f"文件不存在: {e}") + except ValueError as e: + self.error.emit(f"参数错误: {e}") 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) + self.error.emit(f"{e}\n{traceback.format_exc()}") class Step10WatercolorPanel(QWidget): - """步骤10:水色指数反演(直接处理 BSQ 影像)""" + """步骤10:水色指数反演(散点 CSV 模式)""" def __init__(self, parent=None): super().__init__(parent) @@ -122,49 +116,41 @@ class Step10WatercolorPanel(QWidget): layout = QVBoxLayout() # ---- 标题 ---- - title = QLabel("步骤10:水色指数反演(高光谱影像直接处理)") + title = QLabel("步骤10:水色指数反演(散点 CSV 模式)") title.setFont(QFont("Arial", 12, QFont.Bold)) layout.addWidget(title) # ---- 说明 ---- hint = QLabel( - "将 waterindex.csv 中的公式直接应用于去耀斑高光谱影像(BSQ)," - "输出各水质参数指数的 GeoTIFF 栅格图像。" - "指数图可直接用于水质专题图生成。" + "读取 Step 4 生成的 sampling_spectra.csv 散点光谱," + "对每个采样点逐行套用 waterindex.csv 中勾选的公式," + "输出每公式一个 CSV(列:longitude, latitude, 公式值)。" + "结果可被 Step 11 直接以 ContentMapper 模式消费。" ) hint.setWordWrap(True) hint.setStyleSheet(f"color: {ModernStylesheet.COLORS.get('text_secondary', '#666')};") layout.addWidget(hint) - # ---- 输入影像选择 ---- - input_group = QGroupBox("输入影像") + # ---- 输入采样点数据 ---- + input_group = QGroupBox("输入采样点数据") input_layout = QFormLayout() - self.bsq_file = FileSelectWidget( - "去耀斑 BSQ 影像:", - "BSQ Files (*.bsq);;DAT Files (*.dat);;All Files (*.*)" + self.sampling_csv_file = FileSelectWidget( + "采样点 CSV:", + "CSV Files (*.csv);;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.sampling_csv_file.line_edit.setPlaceholderText( + "选择 Step 4 输出的 sampling_spectra.csv" ) - 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) + input_layout.addRow("采样点 CSV:", self.sampling_csv_file) - # 影像信息显示 - self.meta_label = QLabel("未加载影像") + # 数据规模提示(运行后回填,避免启动时强制 read_csv) + 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_layout.addRow("数据信息:", self.meta_label) input_group.setLayout(input_layout) layout.addWidget(input_group) @@ -211,16 +197,11 @@ class Step10WatercolorPanel(QWidget): "输出目录:", "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) + self.output_dir.line_edit.setPlaceholderText( + "留空 → 工作目录/10_WaterIndex_CSV" + ) 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) @@ -337,60 +318,24 @@ class Step10WatercolorPanel(QWidget): 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("⚠️ 影像文件不存在") + def _refresh_sampling_meta(self): + """从 sampling_csv 路径快速 peek 数据规模(不触发公式计算)""" + path = self.sampling_csv_file.get_path().strip() + if not path: + self.meta_label.setText("未加载采样点数据") return - if not hdr_path or not Path(hdr_path).exists(): - self.meta_label.setText("⚠️ 头文件不存在") + if not Path(path).exists(): + self.meta_label.setText("⚠️ 采样点 CSV 不存在") 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("⚠️ 无法读取元数据") + import pandas as pd + df = pd.read_csv(path, encoding="utf-8-sig", nrows=0) + n_cols = len(df.columns) + self.meta_label.setText( + f"✅ 已选采样点 CSV({n_cols} 列,完整列数将在运行时打印)" + ) except Exception as e: - self.meta_label.setText(f"⚠️ 元数据读取失败: {e}") + self.meta_label.setText(f"⚠️ 读取失败: {e}") def _get_selected_formula_names(self) -> List[str]: names = [] @@ -411,21 +356,20 @@ class Step10WatercolorPanel(QWidget): 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], + sampling = self.sampling_csv_file.get_path().strip() + config: Dict[str, object] = { + 'sampling_csv_path': sampling, 'selected_formulas': self._get_selected_formula_names(), } + out_dir = self.output_dir.get_path().strip() + if out_dir: + config['output_dir'] = out_dir + return config 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('sampling_csv_path'): + self.sampling_csv_file.set_path(config['sampling_csv_path']) + self._refresh_sampling_meta() if config.get('output_dir'): self.output_dir.set_path(config['output_dir']) if 'selected_formulas' in config: @@ -444,42 +388,50 @@ class Step10WatercolorPanel(QWidget): self.work_dir = None main_window = self.window() - deglint_path = None - # 1. 优先从 pipeline 的真实输出中获取 + # 1. 优先从 pipeline.step_outputs 取 Step 4 的采样点 CSV 路径 + sampling_path = None if pipeline and hasattr(pipeline, 'step_outputs'): - step3_out = pipeline.step_outputs.get('step3', {}) - deglint_path = step3_out.get('deglint_image') or step3_out.get('output_path') + step4_out = pipeline.step_outputs.get('step4_sampling', {}) + sampling_path = ( + step4_out.get('sampling_csv') + or step4_out.get('output_path') + or step4_out.get('output_file') + ) - # 2. 回退:从 step3 面板实例获取 - if not deglint_path and main_window and hasattr(main_window, 'step3_panel'): - if hasattr(main_window.step3_panel, 'output_file'): - deglint_path = main_window.step3_panel.output_file.get_path() + # 2. 回退:直接读 step4_sampling panel 的 output_file widget + if not sampling_path and main_window: + step4_widget = getattr(main_window, 'step4_sampling', None) + if step4_widget and hasattr(step4_widget, 'output_file'): + sampling_path = step4_widget.output_file.get_path() + else: + # 通过 _panel_factory 懒加载查找 + factory = getattr(main_window, '_panel_factory', None) + if factory: + step4_panel = factory.get_panel('step4_sampling') + if step4_panel and hasattr(step4_panel, 'output_file'): + sampling_path = step4_panel.output_file.get_path() - # 3. 终极回退:智能扫描 3_deglint 目录,取最新的 .bsq 或 .dat 文件 - if not deglint_path and self.work_dir: - deglint_dir = resolve_subdir(self.work_dir, 'deglint') - if os.path.isdir(deglint_dir): - import glob - candidates = glob.glob(os.path.join(deglint_dir, "*.bsq")) + glob.glob(os.path.join(deglint_dir, "*.dat")) - if candidates: - candidates.sort(key=os.path.getmtime, reverse=True) - deglint_path = candidates[0] + # 3. 终极回退:扫描 work_dir/4_sampling/sampling_spectra.csv + if not sampling_path and self.work_dir: + candidate = resolve_subdir(self.work_dir, 'sampling_csv_path') + if os.path.isfile(candidate): + sampling_path = candidate - # 填入 UI 并自动寻找对应的 hdr 文件 - if deglint_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) + # 填入 UI + if sampling_path: + if not os.path.isabs(sampling_path): + sampling_path = os.path.join( + self.work_dir or '', sampling_path + ).replace('\\', '/') + self.sampling_csv_file.set_path(sampling_path) + self._refresh_sampling_meta() - hdr_path = os.path.splitext(deglint_path)[0] + '.hdr' - if os.path.exists(hdr_path): - self.hdr_file.set_path(hdr_path) - self._load_metadata(deglint_path, hdr_path) - - # 自动填入输出目录 + # 自动填入输出目录(默认 work_dir/10_WaterIndex_CSV/) if self.work_dir: - out_dir = resolve_subdir(self.work_dir, 'watercolor') + out_dir = os.path.join( + self.work_dir, '10_WaterIndex_CSV' + ).replace('\\', '/') os.makedirs(out_dir, exist_ok=True) if not self.output_dir.get_path(): self.output_dir.set_path(out_dir) @@ -488,30 +440,20 @@ class Step10WatercolorPanel(QWidget): """通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor)。""" from src.gui.core.event_bus import global_event_bus - bsq_path = self.bsq_file.get_path().strip() - hdr_path = self.hdr_file.get_path().strip() - output_dir = self.output_dir.get_path().strip() + sampling_csv_path = self.sampling_csv_file.get_path().strip() + if not sampling_csv_path: + QMessageBox.warning(self, "输入错误", "请选择采样点 CSV!") + return + if not Path(sampling_csv_path).exists(): + QMessageBox.warning( + self, "输入错误", f"采样点 CSV 不存在:\n{sampling_csv_path}" + ) + return - 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 + output_dir = self.output_dir.get_path().strip() if not output_dir: work_dir = self._get_default_work_dir() - output_dir = resolve_subdir(work_dir, 'watercolor') + output_dir = os.path.join(work_dir, '10_WaterIndex_CSV') os.makedirs(output_dir, exist_ok=True) self.output_dir.set_path(output_dir) @@ -521,43 +463,34 @@ class Step10WatercolorPanel(QWidget): return if self._waterindex_csv and not Path(self._waterindex_csv).exists(): - QMessageBox.warning(self, "配置错误", f"waterindex.csv 不存在:\n{self._waterindex_csv}") + QMessageBox.warning( + self, "配置错误", + f"waterindex.csv 不存在:\n{self._waterindex_csv}", + ) return - config = {'step7_index': self.get_config()} + config = {'step10_watercolor': self.get_config()} global_event_bus.publish('RequestRunSingleStep', { - 'step_name': 'step7_index', + 'step_name': 'step10_watercolor', 'config': config, }) def run_step(self): """独立运行步骤10(旧版 parent 链上溯方式,保留兼容)。""" - bsq_path = self.bsq_file.get_path().strip() - hdr_path = self.hdr_file.get_path().strip() - output_dir = self.output_dir.get_path().strip() + sampling_csv_path = self.sampling_csv_file.get_path().strip() + if not sampling_csv_path: + QMessageBox.warning(self, "输入错误", "请选择采样点 CSV!") + return + if not Path(sampling_csv_path).exists(): + QMessageBox.warning( + self, "输入错误", f"采样点 CSV 不存在:\n{sampling_csv_path}" + ) + return - # 验证输入 - 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 + output_dir = self.output_dir.get_path().strip() if not output_dir: work_dir = self._get_default_work_dir() - output_dir = resolve_subdir(work_dir, 'watercolor') + output_dir = os.path.join(work_dir, '10_WaterIndex_CSV') os.makedirs(output_dir, exist_ok=True) self.output_dir.set_path(output_dir) @@ -567,25 +500,13 @@ class Step10WatercolorPanel(QWidget): return if self._waterindex_csv and not Path(self._waterindex_csv).exists(): - QMessageBox.warning(self, "配置错误", f"waterindex.csv 不存在:\n{self._waterindex_csv}") + QMessageBox.warning( + self, "配置错误", + f"waterindex.csv 不存在:\n{self._waterindex_csv}", + ) return - # ── 自动扫描工作目录下的水域掩膜文件 ──────────────────────────── - work_dir = self.work_dir or str(Path(bsq_path).parent) - mask_dir = resolve_subdir(work_dir, '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] 未找到水域掩膜,跳过陆地剔除(陆地将保留在指数图中)") + work_dir = self.work_dir or str(Path(sampling_csv_path).parent) # 开始后台处理 self.run_btn.setEnabled(False) @@ -593,18 +514,15 @@ class Step10WatercolorPanel(QWidget): self.progress_label.setText("") self._worker = WaterIndexWorker( - bsq_path=bsq_path, - hdr_path=hdr_path, + sampling_csv_path=sampling_csv_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.error.connect(self._on_error) self._worker.start() def _on_progress(self, msg: str, pct: float): @@ -614,30 +532,38 @@ class Step10WatercolorPanel(QWidget): def _on_finished(self, results: Dict[str, str]): self.run_btn.setEnabled(True) n = len(results) + names = list(results.keys())[:3] + tail = " …" if n > 3 else "" QMessageBox.information( self, "执行成功", f"水色指数反演完成!\n" - f"共生成 {n} 个指数图(GeoTIFF)。\n\n" + f"共生成 {n} 个指数 CSV(含 longitude / latitude / 公式值三列)。\n" + f"前几个: {', '.join(names)}{tail}\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") + main_window.log_message( + f"步骤10:水色指数反演完成,生成 {n} 个指数 CSV", "info" + ) - def _on_failed(self, err: str): + def _on_error(self, err: str): self.run_btn.setEnabled(True) self.progress_bar.setValue(0) - QMessageBox.critical(self, "执行错误", f"水色指数反演失败:\n\n{err[:500]}") + self.progress_label.setText("执行失败") + 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 文件路径""" + def get_output_csv_paths(self) -> List[str]: + """获取输出目录下的所有指数 CSV 文件路径(供 Step 11 ContentMapper 探测)""" 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") + str(p) for p in Path(out_dir).glob("*.csv") if p.is_file() ) \ No newline at end of file