refactor(step10_panel): 切换到散点 CSV 模式 UI + Worker

This commit is contained in:
DXC
2026-06-24 11:43:44 +08:00
parent b0ace0bde8
commit 08a9a0337d

View File

@ -1,10 +1,17 @@
#!/usr/bin/env python #!/usr/bin/env python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Step10 面板 - 水色指数反演(直接处理去耀斑 BSQ 影像 Step10 面板 - 水色指数反演(散点 CSV 模式
将 waterindex.csv 中的公式直接应用于去耀斑高光谱影像, 与 Step 9 (ML 预测) 完全对称的【散点处理模式】:
输出各水质参数指数的 GeoTIFF 栅格图像。
* 输入Step 4 输出的 ``sampling_spectra.csv``(散点+全波段光谱)
* 处理:解析 ``waterindex.csv`` 中的公式,**逐行**对每个采样点计算水色指数
* 输出:每个公式一个 CSV列严格为 ``longitude, latitude, <formula_name>``
可直接喂给 Step 11 ContentMapper
* 输出目录:默认 ``{work_dir}/10_WaterIndex_CSV/``
注:原"读 BSQ 全图→GeoTIFF"模式已废弃(科学上误差大且与 GIS 栅格计算器重复)。
""" """
import os import os
@ -34,79 +41,66 @@ from src.gui.styles import ModernStylesheet
class WaterIndexWorker(QThread): class WaterIndexWorker(QThread):
"""后台线程:执行水色指数反演""" """后台线程:散点 CSV → 逐行公式计算 → 多 CSV 输出
finished_ok = pyqtSignal(dict)
failed = pyqtSignal(str) 应用 Step 13 QThread 协议:
- 三信号 ``progress / finished / error`` 命名严格遵循约定
(注意 ``finished`` 不能用——会覆盖 QThread 内建同名信号,
导致 _on_finished 不被回调时按钮不会恢复)
- 进度回调两参 (msg: str, pct: float)
"""
progress = pyqtSignal(str, float) # message, percent progress = pyqtSignal(str, float) # message, percent
log = pyqtSignal(str) finished_ok = pyqtSignal(dict) # {公式名: 输出 CSV 路径}
error = pyqtSignal(str) # error message
def __init__( def __init__(
self, self,
bsq_path: str, sampling_csv_path: str,
hdr_path: str,
output_dir: str, output_dir: str,
selected_formulas: List[str], selected_formulas: List[str],
waterindex_csv: str, waterindex_csv: str,
water_mask_path: Optional[str] = None,
work_dir: Optional[str] = None, work_dir: Optional[str] = None,
): ):
super().__init__() super().__init__()
self.bsq_path = bsq_path self.sampling_csv_path = sampling_csv_path
self.hdr_path = hdr_path
self.output_dir = output_dir self.output_dir = output_dir
self.selected_formulas = selected_formulas self.selected_formulas = selected_formulas
self.waterindex_csv = waterindex_csv self.waterindex_csv = waterindex_csv
self.water_mask_path = water_mask_path
self.work_dir = work_dir self.work_dir = work_dir
def run(self): def run(self):
try: try:
from src.core.algorithms.waterindex_inversion import WaterIndexProcessor from src.core.algorithms.waterindex_inversion import (
WaterIndexCsvProcessor,
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.progress.emit("正在初始化散点水色指数处理器…", 2)
self.log.emit(f"使用水域掩膜: {self.water_mask_path}")
# 使用 run_inversion 入口(含掩膜拦截链路) processor = WaterIndexCsvProcessor(self.waterindex_csv)
results = processor.run_inversion(
deglint_img_path=self.bsq_path, # 散点 CSV → 逐公式一个 CSV
work_dir=self.work_dir or self.output_dir, out_files = processor.compute_indices_from_csv(
formula_csv_path=self.waterindex_csv, sampling_csv_path=self.sampling_csv_path,
selected_formulas=self.selected_formulas, output_dir=self.output_dir,
water_mask_path=self.water_mask_path, selected_formulas=self.selected_formulas or None,
callback=self._on_progress, progress_callback=lambda m, p: self.progress.emit(m, p),
) )
self.progress.emit(f"完成!共生成 {len(results)} 个指数图", 100) self.progress.emit(
self.finished_ok.emit(results) 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: except Exception as e:
self.failed.emit(f"{e}\n{traceback.format_exc()}") self.error.emit(f"{e}\n{traceback.format_exc()}")
def _on_progress(self, msg: str, pct: float):
self.progress.emit(msg, pct)
class Step10WatercolorPanel(QWidget): class Step10WatercolorPanel(QWidget):
"""步骤10水色指数反演直接处理 BSQ 影像""" """步骤10水色指数反演散点 CSV 模式"""
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
@ -122,49 +116,41 @@ class Step10WatercolorPanel(QWidget):
layout = QVBoxLayout() layout = QVBoxLayout()
# ---- 标题 ---- # ---- 标题 ----
title = QLabel("步骤10水色指数反演高光谱影像直接处理") title = QLabel("步骤10水色指数反演散点 CSV 模式")
title.setFont(QFont("Arial", 12, QFont.Bold)) title.setFont(QFont("Arial", 12, QFont.Bold))
layout.addWidget(title) layout.addWidget(title)
# ---- 说明 ---- # ---- 说明 ----
hint = QLabel( hint = QLabel(
"将 waterindex.csv 中的公式直接应用于去耀斑高光谱影像BSQ" "读取 Step 4 生成的 sampling_spectra.csv 散点光谱"
"输出各水质参数指数的 GeoTIFF 栅格图像。" "对每个采样点逐行套用 waterindex.csv 中勾选的公式,"
"指数图可直接用于水质专题图生成" "输出每公式一个 CSVlongitude, latitude, 公式值)"
"结果可被 Step 11 直接以 ContentMapper 模式消费。"
) )
hint.setWordWrap(True) hint.setWordWrap(True)
hint.setStyleSheet(f"color: {ModernStylesheet.COLORS.get('text_secondary', '#666')};") hint.setStyleSheet(f"color: {ModernStylesheet.COLORS.get('text_secondary', '#666')};")
layout.addWidget(hint) layout.addWidget(hint)
# ---- 输入影像选择 ---- # ---- 输入采样点数据 ----
input_group = QGroupBox("输入影像") input_group = QGroupBox("输入采样点数据")
input_layout = QFormLayout() input_layout = QFormLayout()
self.bsq_file = FileSelectWidget( self.sampling_csv_file = FileSelectWidget(
"去耀斑 BSQ 影像:", "采样点 CSV:",
"BSQ Files (*.bsq);;DAT Files (*.dat);;All Files (*.*)" "CSV Files (*.csv);;All Files (*.*)"
) )
self.bsq_file.line_edit.setPlaceholderText("选择去耀斑处理后的 BSQ 影像") self.sampling_csv_file.line_edit.setPlaceholderText(
self.bsq_file.browse_btn.clicked.disconnect() "选择 Step 4 输出的 sampling_spectra.csv"
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 文件") input_layout.addRow("采样点 CSV:", self.sampling_csv_file)
self.hdr_file.browse_btn.clicked.disconnect()
self.hdr_file.browse_btn.clicked.connect(self._browse_hdr)
input_layout.addRow("HDR 文件:", self.hdr_file)
# 影像信息显示 # 数据规模提示(运行后回填,避免启动时强制 read_csv
self.meta_label = QLabel("未加载影像") self.meta_label = QLabel("未加载采样点数据")
self.meta_label.setStyleSheet( self.meta_label.setStyleSheet(
"background: #f0f0f0; padding: 4px 8px; border-radius: 4px; " "background: #f0f0f0; padding: 4px 8px; border-radius: 4px; "
"font-size: 12px; color: #333;" "font-size: 12px; color: #333;"
) )
input_layout.addRow("影像信息:", self.meta_label) input_layout.addRow("数据信息:", self.meta_label)
input_group.setLayout(input_layout) input_group.setLayout(input_layout)
layout.addWidget(input_group) layout.addWidget(input_group)
@ -211,16 +197,11 @@ class Step10WatercolorPanel(QWidget):
"输出目录:", "输出目录:",
"Directories" "Directories"
) )
self.output_dir.line_edit.setPlaceholderText("留空 → 工作目录/10_WaterIndex_Images") self.output_dir.line_edit.setPlaceholderText(
self.output_dir.browse_btn.clicked.disconnect() "留空 → 工作目录/10_WaterIndex_CSV"
self.output_dir.browse_btn.clicked.connect(self._browse_output_dir) )
output_layout.addRow("输出目录:", self.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) output_group.setLayout(output_layout)
layout.addWidget(output_group) layout.addWidget(output_group)
@ -337,60 +318,24 @@ class Step10WatercolorPanel(QWidget):
def _on_item_changed(self, item: QListWidgetItem): def _on_item_changed(self, item: QListWidgetItem):
pass # 可扩展:实时统计选中数量 pass # 可扩展:实时统计选中数量
def _browse_bsq(self): def _refresh_sampling_meta(self):
path, _ = QFileDialog.getOpenFileName( """从 sampling_csv 路径快速 peek 数据规模(不触发公式计算)"""
self, "选择去耀斑 BSQ 影像", path = self.sampling_csv_file.get_path().strip()
"", if not path:
"BSQ Files (*.bsq);;DAT Files (*.dat);;All Files (*.*)" self.meta_label.setText("未加载采样点数据")
)
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 return
if not hdr_path or not Path(hdr_path).exists(): if not Path(path).exists():
self.meta_label.setText("⚠️ 头文件不存在") self.meta_label.setText("⚠️ 采样点 CSV 不存在")
return return
try: try:
from src.core.algorithms.waterindex_inversion import WaterIndexProcessor import pandas as pd
processor = WaterIndexProcessor(self._waterindex_csv) df = pd.read_csv(path, encoding="utf-8-sig", nrows=0)
meta = processor.get_image_metadata(bsq_path, hdr_path) n_cols = len(df.columns)
if meta:
self.meta_label.setText( self.meta_label.setText(
f"{meta['width']}×{meta['height']} | " f"已选采样点 CSV{n_cols} 列,完整列数将在运行时打印)"
f"{meta['bands']} 波段 | {meta.get('wavelength_range', '未知')} | "
f"驱动: {meta['driver']}"
) )
else:
self.meta_label.setText("⚠️ 无法读取元数据")
except Exception as e: except Exception as e:
self.meta_label.setText(f"⚠️ 元数据读取失败: {e}") self.meta_label.setText(f"⚠️ 读取失败: {e}")
def _get_selected_formula_names(self) -> List[str]: def _get_selected_formula_names(self) -> List[str]:
names = [] names = []
@ -411,21 +356,20 @@ class Step10WatercolorPanel(QWidget):
return "" return ""
def get_config(self) -> dict: def get_config(self) -> dict:
bsq = self.bsq_file.get_path() sampling = self.sampling_csv_file.get_path().strip()
return { config: Dict[str, object] = {
'bsq_path': bsq, 'sampling_csv_path': sampling,
'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(), '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): def set_config(self, config: dict):
if config.get('bsq_path'): if config.get('sampling_csv_path'):
self.bsq_file.set_path(config['bsq_path']) self.sampling_csv_file.set_path(config['sampling_csv_path'])
if config.get('hdr_path'): self._refresh_sampling_meta()
self.hdr_file.set_path(config['hdr_path'])
if config.get('output_dir'): if config.get('output_dir'):
self.output_dir.set_path(config['output_dir']) self.output_dir.set_path(config['output_dir'])
if 'selected_formulas' in config: if 'selected_formulas' in config:
@ -444,42 +388,50 @@ class Step10WatercolorPanel(QWidget):
self.work_dir = None self.work_dir = None
main_window = self.window() 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'): if pipeline and hasattr(pipeline, 'step_outputs'):
step3_out = pipeline.step_outputs.get('step3', {}) step4_out = pipeline.step_outputs.get('step4_sampling', {})
deglint_path = step3_out.get('deglint_image') or step3_out.get('output_path') sampling_path = (
step4_out.get('sampling_csv')
or step4_out.get('output_path')
or step4_out.get('output_file')
)
# 2. 回退: step3 面板实例获取 # 2. 回退:直接读 step4_sampling panel 的 output_file widget
if not deglint_path and main_window and hasattr(main_window, 'step3_panel'): if not sampling_path and main_window:
if hasattr(main_window.step3_panel, 'output_file'): step4_widget = getattr(main_window, 'step4_sampling', None)
deglint_path = main_window.step3_panel.output_file.get_path() 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 文件 # 3. 终极回退:扫描 work_dir/4_sampling/sampling_spectra.csv
if not deglint_path and self.work_dir: if not sampling_path and self.work_dir:
deglint_dir = resolve_subdir(self.work_dir, 'deglint') candidate = resolve_subdir(self.work_dir, 'sampling_csv_path')
if os.path.isdir(deglint_dir): if os.path.isfile(candidate):
import glob sampling_path = candidate
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]
# 填入 UI 并自动寻找对应的 hdr 文件 # 填入 UI
if deglint_path: if sampling_path:
if not os.path.isabs(deglint_path): if not os.path.isabs(sampling_path):
deglint_path = os.path.join(self.work_dir or '', deglint_path).replace('\\', '/') sampling_path = os.path.join(
self.bsq_file.set_path(deglint_path) 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' # 自动填入输出目录(默认 work_dir/10_WaterIndex_CSV/
if os.path.exists(hdr_path):
self.hdr_file.set_path(hdr_path)
self._load_metadata(deglint_path, hdr_path)
# 自动填入输出目录
if self.work_dir: 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) os.makedirs(out_dir, exist_ok=True)
if not self.output_dir.get_path(): if not self.output_dir.get_path():
self.output_dir.set_path(out_dir) self.output_dir.set_path(out_dir)
@ -488,30 +440,20 @@ class Step10WatercolorPanel(QWidget):
"""通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor""" """通过 EventBus 发布单步执行请求(解耦面板与 PipelineExecutor"""
from src.gui.core.event_bus import global_event_bus from src.gui.core.event_bus import global_event_bus
bsq_path = self.bsq_file.get_path().strip() sampling_csv_path = self.sampling_csv_file.get_path().strip()
hdr_path = self.hdr_file.get_path().strip() if not sampling_csv_path:
output_dir = self.output_dir.get_path().strip() 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: output_dir = self.output_dir.get_path().strip()
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: if not output_dir:
work_dir = self._get_default_work_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) os.makedirs(output_dir, exist_ok=True)
self.output_dir.set_path(output_dir) self.output_dir.set_path(output_dir)
@ -521,43 +463,34 @@ class Step10WatercolorPanel(QWidget):
return return
if self._waterindex_csv and not Path(self._waterindex_csv).exists(): 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 return
config = {'step7_index': self.get_config()} config = {'step10_watercolor': self.get_config()}
global_event_bus.publish('RequestRunSingleStep', { global_event_bus.publish('RequestRunSingleStep', {
'step_name': 'step7_index', 'step_name': 'step10_watercolor',
'config': config, 'config': config,
}) })
def run_step(self): def run_step(self):
"""独立运行步骤10旧版 parent 链上溯方式,保留兼容)。""" """独立运行步骤10旧版 parent 链上溯方式,保留兼容)。"""
bsq_path = self.bsq_file.get_path().strip() sampling_csv_path = self.sampling_csv_file.get_path().strip()
hdr_path = self.hdr_file.get_path().strip() if not sampling_csv_path:
output_dir = self.output_dir.get_path().strip() QMessageBox.warning(self, "输入错误", "请选择采样点 CSV")
return
if not Path(sampling_csv_path).exists():
QMessageBox.warning(
self, "输入错误", f"采样点 CSV 不存在:\n{sampling_csv_path}"
)
return
# 验证输入 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: if not output_dir:
work_dir = self._get_default_work_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) os.makedirs(output_dir, exist_ok=True)
self.output_dir.set_path(output_dir) self.output_dir.set_path(output_dir)
@ -567,25 +500,13 @@ class Step10WatercolorPanel(QWidget):
return return
if self._waterindex_csv and not Path(self._waterindex_csv).exists(): 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 return
# ── 自动扫描工作目录下的水域掩膜文件 ──────────────────────────── work_dir = self.work_dir or str(Path(sampling_csv_path).parent)
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] 未找到水域掩膜,跳过陆地剔除(陆地将保留在指数图中)")
# 开始后台处理 # 开始后台处理
self.run_btn.setEnabled(False) self.run_btn.setEnabled(False)
@ -593,18 +514,15 @@ class Step10WatercolorPanel(QWidget):
self.progress_label.setText("") self.progress_label.setText("")
self._worker = WaterIndexWorker( self._worker = WaterIndexWorker(
bsq_path=bsq_path, sampling_csv_path=sampling_csv_path,
hdr_path=hdr_path,
output_dir=output_dir, output_dir=output_dir,
selected_formulas=selected, selected_formulas=selected,
waterindex_csv=self._waterindex_csv, waterindex_csv=self._waterindex_csv,
water_mask_path=water_mask_path,
work_dir=work_dir, work_dir=work_dir,
) )
self._worker.progress.connect(self._on_progress) self._worker.progress.connect(self._on_progress)
self._worker.finished_ok.connect(self._on_finished) self._worker.finished_ok.connect(self._on_finished)
self._worker.failed.connect(self._on_failed) self._worker.error.connect(self._on_error)
self._worker.log.connect(lambda m: self.progress_label.setText(m))
self._worker.start() self._worker.start()
def _on_progress(self, msg: str, pct: float): def _on_progress(self, msg: str, pct: float):
@ -614,30 +532,38 @@ class Step10WatercolorPanel(QWidget):
def _on_finished(self, results: Dict[str, str]): def _on_finished(self, results: Dict[str, str]):
self.run_btn.setEnabled(True) self.run_btn.setEnabled(True)
n = len(results) n = len(results)
names = list(results.keys())[:3]
tail = "" if n > 3 else ""
QMessageBox.information( QMessageBox.information(
self, "执行成功", self, "执行成功",
f"水色指数反演完成!\n" 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()}" f"输出目录: {self.output_dir.get_path()}"
) )
main_window = self.window() main_window = self.window()
if main_window and hasattr(main_window, 'log_message'): 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.run_btn.setEnabled(True)
self.progress_bar.setValue(0) 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: def get_output_dir(self) -> str:
return self.output_dir.get_path().strip() or "" return self.output_dir.get_path().strip() or ""
def get_output_tif_paths(self) -> List[str]: def get_output_csv_paths(self) -> List[str]:
"""获取输出目录下的所有 GeoTIFF 文件路径""" """获取输出目录下的所有指数 CSV 文件路径(供 Step 11 ContentMapper 探测)"""
out_dir = self.get_output_dir() out_dir = self.get_output_dir()
if not out_dir or not os.path.isdir(out_dir): if not out_dir or not os.path.isdir(out_dir):
return [] return []
return sorted( 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() if p.is_file()
) )