fix: PanelFactory 信号风暴修复 + 后端上帝类肢解(BaseStepHandler/调度器/Step1打样)
This commit is contained in:
20
src/core/handlers/__init__.py
Normal file
20
src/core/handlers/__init__.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
步骤处理器包
|
||||||
|
|
||||||
|
将 WaterQualityInversionPipeline 的 14 个巨型 step* 方法
|
||||||
|
拆分为独立的 Handler 类,每个 Handler 实现 BaseStepHandler 接口。
|
||||||
|
|
||||||
|
调度器(PipelineScheduler)仅维护执行上下文并根据 step_key
|
||||||
|
从注册表查找对应 Handler 执行,自身不再包含任何算法逻辑。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from src.core.handlers.base import BaseStepHandler, PipelineContext
|
||||||
|
from src.core.handlers.step1_water_mask import Step1WaterMaskHandler
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'BaseStepHandler',
|
||||||
|
'PipelineContext',
|
||||||
|
'Step1WaterMaskHandler',
|
||||||
|
]
|
||||||
277
src/core/handlers/base.py
Normal file
277
src/core/handlers/base.py
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Handler 基类与 Pipeline 执行上下文
|
||||||
|
|
||||||
|
BaseStepHandler —— 所有步骤 Handler 的抽象基类,定义统一的 execute 接口。
|
||||||
|
PipelineContext —— 在 Handler 之间传递的共享状态容器(路径、计时、回调等)。
|
||||||
|
|
||||||
|
设计原则:
|
||||||
|
- Handler 只负责"执行一个步骤的算法逻辑",不管理调度/依赖/跳过。
|
||||||
|
- Context 是 Handler 之间唯一的共享状态通道。
|
||||||
|
- 调度器(PipelineScheduler)负责遍历 config、查找 Handler、调用 execute。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Callable, Dict, List, Optional
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.filterwarnings('ignore')
|
||||||
|
|
||||||
|
|
||||||
|
class PipelineContext:
|
||||||
|
"""管道执行上下文 —— Handler 之间共享状态的唯一载体。
|
||||||
|
|
||||||
|
包含:
|
||||||
|
- 工作目录及子目录
|
||||||
|
- 中间结果路径(water_mask_path, glint_mask_path, ...)
|
||||||
|
- 步骤计时记录
|
||||||
|
- 回调函数(用于 GUI 进度通知)
|
||||||
|
- 可视化/报告生成器实例
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, work_dir: str = "./work_dir"):
|
||||||
|
self.work_dir = Path(work_dir)
|
||||||
|
self.work_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# ── 子目录 ──
|
||||||
|
self.water_mask_dir = self.work_dir / "1_water_mask"
|
||||||
|
self.glint_dir = self.work_dir / "2_Glint_Detection"
|
||||||
|
self.deglint_dir = self.work_dir / "3_deglint"
|
||||||
|
self.processed_data_dir = self.work_dir / "5_Data_Cleaning"
|
||||||
|
self.training_spectra_dir = self.work_dir / "6_Spectral_Feature_Extraction"
|
||||||
|
self.indices_dir = self.work_dir / "7_Water_Quality_Indices"
|
||||||
|
self.models_dir = self.work_dir / "8_Supervised_Model_Training"
|
||||||
|
self.non_empirical_models_dir = self.work_dir / "8_Non_Empirical_Regression"
|
||||||
|
self.custom_regression_dir = self.work_dir / "13_Custom_Regression"
|
||||||
|
self.sampling_dir = self.work_dir / "4_sampling"
|
||||||
|
self.prediction_dir = self.work_dir / "11_12_13_predictions"
|
||||||
|
self.visualization_dir = self.work_dir / "14_visualization"
|
||||||
|
self.reports_dir = self.work_dir / "reports"
|
||||||
|
|
||||||
|
for d in [self.water_mask_dir, self.glint_dir, self.deglint_dir,
|
||||||
|
self.processed_data_dir, self.training_spectra_dir,
|
||||||
|
self.indices_dir, self.models_dir, self.non_empirical_models_dir,
|
||||||
|
self.custom_regression_dir, self.sampling_dir, self.prediction_dir,
|
||||||
|
self.visualization_dir, self.reports_dir]:
|
||||||
|
d.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# ── 中间结果路径 ──
|
||||||
|
self.water_mask_path: Optional[str] = None
|
||||||
|
self.glint_mask_path: Optional[str] = None
|
||||||
|
self.interpolated_img_path: Optional[str] = None
|
||||||
|
self.deglint_img_path: Optional[str] = None
|
||||||
|
self.processed_csv_path: Optional[str] = None
|
||||||
|
self.training_csv_path: Optional[str] = None
|
||||||
|
self.indices_path: Optional[str] = None
|
||||||
|
self.custom_regression_path: Optional[str] = None
|
||||||
|
|
||||||
|
# ── 计时 ──
|
||||||
|
self.step_timings: Dict[str, dict] = {}
|
||||||
|
self.pipeline_start_time: Optional[float] = None
|
||||||
|
self.pipeline_end_time: Optional[float] = None
|
||||||
|
|
||||||
|
# ── 回调 ──
|
||||||
|
self._callback: Optional[Callable] = None
|
||||||
|
|
||||||
|
# ── 可视化组件(延迟导入避免循环依赖)──
|
||||||
|
self._visualizer = None
|
||||||
|
self._report_generator = None
|
||||||
|
self._scatter_batch = None
|
||||||
|
|
||||||
|
# ── matplotlib 中文字体 ──
|
||||||
|
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei',
|
||||||
|
'DejaVu Sans', 'Arial Unicode MS']
|
||||||
|
plt.rcParams['axes.unicode_minus'] = False
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
# 回调
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def set_callback(self, callback: Callable):
|
||||||
|
"""设置回调函数,用于向 GUI 报告进度。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
callback: 签名为 callback(step_name, status, message="")
|
||||||
|
status: 'start' | 'completed' | 'skipped' | 'error' | 'info' | 'warning'
|
||||||
|
"""
|
||||||
|
self._callback = callback
|
||||||
|
|
||||||
|
def notify(self, step_name: str, status: str, message: str = ""):
|
||||||
|
"""通知回调函数。"""
|
||||||
|
if self._callback:
|
||||||
|
try:
|
||||||
|
self._callback(step_name, status, message)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"回调函数执行失败: {e}")
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
# 计时
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def record_step_time(self, step_name: str, start_time: float, end_time: float,
|
||||||
|
status: str = "completed", error: Optional[str] = None):
|
||||||
|
elapsed = end_time - start_time
|
||||||
|
self.step_timings[step_name] = {
|
||||||
|
'start_time': datetime.fromtimestamp(start_time).strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
'end_time': datetime.fromtimestamp(end_time).strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
'elapsed_seconds': elapsed,
|
||||||
|
'elapsed_formatted': self._format_time(elapsed),
|
||||||
|
'status': status,
|
||||||
|
'error': error,
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _format_time(seconds: float) -> str:
|
||||||
|
if seconds < 60:
|
||||||
|
return f"{seconds:.2f}秒"
|
||||||
|
elif seconds < 3600:
|
||||||
|
minutes = int(seconds // 60)
|
||||||
|
secs = seconds % 60
|
||||||
|
return f"{minutes}分{secs:.2f}秒"
|
||||||
|
else:
|
||||||
|
hours = int(seconds // 3600)
|
||||||
|
minutes = int((seconds % 3600) // 60)
|
||||||
|
secs = seconds % 60
|
||||||
|
return f"{hours}小时{minutes}分{secs:.2f}秒"
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
# 可视化组件(延迟导入)
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@property
|
||||||
|
def visualizer(self):
|
||||||
|
if self._visualizer is None:
|
||||||
|
from src.postprocessing.visualization_reports import WaterQualityVisualization
|
||||||
|
self._visualizer = WaterQualityVisualization(str(self.visualization_dir))
|
||||||
|
return self._visualizer
|
||||||
|
|
||||||
|
@property
|
||||||
|
def report_generator(self):
|
||||||
|
if self._report_generator is None:
|
||||||
|
from src.postprocessing.visualization_reports import ReportGenerator
|
||||||
|
self._report_generator = ReportGenerator(str(self.reports_dir))
|
||||||
|
return self._report_generator
|
||||||
|
|
||||||
|
@property
|
||||||
|
def scatter_batch(self):
|
||||||
|
if self._scatter_batch is None:
|
||||||
|
from src.core.prediction.sctter_batch import WaterQualityScatterBatch
|
||||||
|
self._scatter_batch = WaterQualityScatterBatch()
|
||||||
|
return self._scatter_batch
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
# 步骤输出目录查找(兼容旧接口)
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
_STEP_OUTPUT_DIR_MAP: Optional[Dict[str, Path]] = None
|
||||||
|
|
||||||
|
def _ensure_step_dir_map(self) -> Dict[str, Path]:
|
||||||
|
if PipelineContext._STEP_OUTPUT_DIR_MAP is not None:
|
||||||
|
return PipelineContext._STEP_OUTPUT_DIR_MAP
|
||||||
|
wp = self.work_dir
|
||||||
|
m = {
|
||||||
|
'step1': wp / '1_water_mask',
|
||||||
|
'step2': wp / '2_Glint_Detection',
|
||||||
|
'step3': wp / '3_deglint',
|
||||||
|
'step4_sampling': wp / '4_sampling',
|
||||||
|
'step5_clean': wp / '5_Data_Cleaning',
|
||||||
|
'step6_feature': wp / '6_Spectral_Feature_Extraction',
|
||||||
|
'step7_index': wp / '7_Water_Quality_Indices',
|
||||||
|
'step8_ml_train': wp / '8_Supervised_Model_Training',
|
||||||
|
'step9_ml_predict': wp / '8_Non_Empirical_Regression',
|
||||||
|
'step10_watercolor': wp / '10_WaterIndex_Images',
|
||||||
|
'step11_map': wp / '14_visualization',
|
||||||
|
'step12_viz': wp / '14_visualization',
|
||||||
|
'step13_report': wp / '14_visualization',
|
||||||
|
'step11_predictions': wp / '11_12_13_predictions',
|
||||||
|
'step12_predictions': wp / '11_12_13_predictions',
|
||||||
|
'step13_predictions': wp / '11_12_13_predictions',
|
||||||
|
'custom_regression': wp / '13_Custom_Regression',
|
||||||
|
'prediction_dir': wp / '11_12_13_predictions',
|
||||||
|
'visualization': wp / '14_visualization',
|
||||||
|
'reports': wp / 'reports',
|
||||||
|
'step8': wp / '8_Supervised_Model_Training',
|
||||||
|
'step9': wp / '8_Non_Empirical_Regression',
|
||||||
|
'step10': wp / '10_WaterIndex_Images',
|
||||||
|
'step11': wp / '11_12_13_predictions',
|
||||||
|
'step12': wp / '13_Custom_Regression',
|
||||||
|
'step13': wp / 'reports',
|
||||||
|
'step14': wp / '14_visualization',
|
||||||
|
}
|
||||||
|
PipelineContext._STEP_OUTPUT_DIR_MAP = m
|
||||||
|
return m
|
||||||
|
|
||||||
|
def get_step_output_dir(self, step_name: str) -> Path:
|
||||||
|
mapping = self._ensure_step_dir_map()
|
||||||
|
key = (step_name or '').strip()
|
||||||
|
if key in mapping:
|
||||||
|
return mapping[key]
|
||||||
|
print(f"[PipelineContext.get_step_output_dir] 未知 step_name={key!r},回退到 work_dir")
|
||||||
|
return self.work_dir
|
||||||
|
|
||||||
|
|
||||||
|
class BaseStepHandler(ABC):
|
||||||
|
"""步骤处理器抽象基类。
|
||||||
|
|
||||||
|
所有步骤 Handler 必须实现:
|
||||||
|
- step_key: 类属性,对应 config 中的 key(如 'step1', 'step2', ...)
|
||||||
|
- execute(context, config): 执行步骤逻辑,返回结果字典
|
||||||
|
|
||||||
|
用法示例::
|
||||||
|
|
||||||
|
class Step1WaterMaskHandler(BaseStepHandler):
|
||||||
|
step_key = 'step1'
|
||||||
|
|
||||||
|
def execute(self, ctx, config):
|
||||||
|
result = WaterMaskStep.run(...)
|
||||||
|
ctx.water_mask_path = result
|
||||||
|
return {'water_mask_path': result}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 子类必须定义:对应 config 字典中的 key
|
||||||
|
step_key: str = None
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def execute(self, context: PipelineContext, config: dict) -> dict:
|
||||||
|
"""执行步骤逻辑。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: 管道执行上下文(共享状态)
|
||||||
|
config: 该步骤的配置字典(即 config[self.step_key])
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
结果字典,包含该步骤产生的输出路径等信息。
|
||||||
|
调度器会将返回值合并到全局结果中。
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: 任何异常都会由调度器捕获并记录。
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
def _resolve_path(self, explicit: Optional[str], fallback: Optional[str],
|
||||||
|
label: str = "path") -> Optional[str]:
|
||||||
|
"""解析路径:优先使用显式传入值,否则回退到上下文中的缓存值。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
explicit: 调用方显式传入的路径
|
||||||
|
fallback: 上下文中的缓存路径
|
||||||
|
label: 用于日志的标签
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
解析后的路径,若两者均为 None 则返回 None
|
||||||
|
"""
|
||||||
|
if explicit is not None:
|
||||||
|
return explicit
|
||||||
|
if fallback is not None:
|
||||||
|
return fallback
|
||||||
|
return None
|
||||||
199
src/core/handlers/pipeline_scheduler.py
Normal file
199
src/core/handlers/pipeline_scheduler.py
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
极简管道调度器
|
||||||
|
|
||||||
|
替代原 WaterQualityInversionPipeline(2598 行上帝类)的调度核心。
|
||||||
|
调度器自身不包含任何算法逻辑,仅负责:
|
||||||
|
1. 维护 PipelineContext(共享状态)
|
||||||
|
2. 根据 config key 从 Handler 注册表查找对应处理器
|
||||||
|
3. 按序调用 handler.execute(ctx, config),收集结果
|
||||||
|
4. 异常时记录错误并继续(或中止,取决于配置)
|
||||||
|
|
||||||
|
Handler 注册表是 step_key → BaseStepHandler 的映射。
|
||||||
|
新增步骤只需:写一个 Handler 类 + 在注册表中加一行。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Callable, Dict, List, Optional
|
||||||
|
|
||||||
|
from src.core.handlers.base import BaseStepHandler, PipelineContext
|
||||||
|
|
||||||
|
|
||||||
|
class PipelineScheduler:
|
||||||
|
"""极简管道调度器。
|
||||||
|
|
||||||
|
用法::
|
||||||
|
|
||||||
|
scheduler = PipelineScheduler(work_dir="./work_dir")
|
||||||
|
scheduler.register_handler(Step1WaterMaskHandler())
|
||||||
|
scheduler.register_handler(Step2GlintDetectionHandler())
|
||||||
|
# ... 注册所有步骤 ...
|
||||||
|
|
||||||
|
scheduler.set_callback(my_callback) # 可选:GUI 进度回调
|
||||||
|
|
||||||
|
result = scheduler.run_full_pipeline(config)
|
||||||
|
# result['step1'] → {'water_mask_path': ...}
|
||||||
|
# result['step2'] → {'glint_mask_path': ...}
|
||||||
|
# ...
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, work_dir: str = "./work_dir"):
|
||||||
|
self.ctx = PipelineContext(work_dir)
|
||||||
|
self._handlers: Dict[str, BaseStepHandler] = {}
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
# Handler 注册
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def register_handler(self, handler: BaseStepHandler):
|
||||||
|
"""注册一个步骤处理器。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
handler: BaseStepHandler 实例(其 step_key 类属性决定 config 中的 key)
|
||||||
|
"""
|
||||||
|
if handler.step_key is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Handler {type(handler).__name__} 未定义 step_key 类属性"
|
||||||
|
)
|
||||||
|
self._handlers[handler.step_key] = handler
|
||||||
|
|
||||||
|
def register_handlers(self, handlers: List[BaseStepHandler]):
|
||||||
|
"""批量注册步骤处理器。"""
|
||||||
|
for h in handlers:
|
||||||
|
self.register_handler(h)
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
# 回调
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def set_callback(self, callback: Callable):
|
||||||
|
"""设置 GUI 进度回调,代理到 PipelineContext。"""
|
||||||
|
self.ctx.set_callback(callback)
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
# 单步执行
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def run_step(self, step_key: str, config: dict) -> Dict[str, Any]:
|
||||||
|
"""执行单个步骤。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
step_key: 步骤 key(如 'step1', 'step2', ...)
|
||||||
|
config: 该步骤的配置字典
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
步骤执行结果字典
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
KeyError: 如果 step_key 未注册 Handler
|
||||||
|
Exception: 步骤执行中的任何异常
|
||||||
|
"""
|
||||||
|
handler = self._handlers.get(step_key)
|
||||||
|
if handler is None:
|
||||||
|
raise KeyError(
|
||||||
|
f"未注册的步骤: {step_key!r}。"
|
||||||
|
f"已注册: {list(self._handlers.keys())}"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.ctx.notify(handler.step_key, 'start')
|
||||||
|
result = handler.execute(self.ctx, config)
|
||||||
|
self.ctx.notify(handler.step_key, 'completed')
|
||||||
|
return result
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
# 全流程执行
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def run_full_pipeline(self, config: Dict[str, dict]) -> Dict[str, Any]:
|
||||||
|
"""按 config 中的 key 顺序执行全流程。
|
||||||
|
|
||||||
|
遍历 config 的顶层 key,对每个 key:
|
||||||
|
- 如果已注册 Handler → 执行并收集结果
|
||||||
|
- 如果未注册 → 跳过并通知
|
||||||
|
- 如果执行失败 → 记录错误,继续执行后续步骤(不中止)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config: 全流程配置字典,格式为 {step_key: step_config, ...}
|
||||||
|
例如: {'step1': {...}, 'step2': {...}, ...}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{
|
||||||
|
'step_results': {step_key: result_dict, ...},
|
||||||
|
'step_timings': {...},
|
||||||
|
'total_elapsed': float,
|
||||||
|
'errors': {step_key: error_message, ...},
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
self.ctx.pipeline_start_time = time.time()
|
||||||
|
|
||||||
|
step_results: Dict[str, Any] = {}
|
||||||
|
errors: Dict[str, str] = {}
|
||||||
|
|
||||||
|
# 按 config 中的顺序遍历(Python 3.7+ dict 保序)
|
||||||
|
for step_key, step_config in config.items():
|
||||||
|
handler = self._handlers.get(step_key)
|
||||||
|
|
||||||
|
if handler is None:
|
||||||
|
self.ctx.notify(step_key, 'skipped', '未注册 Handler')
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = handler.execute(self.ctx, step_config)
|
||||||
|
step_results[step_key] = result
|
||||||
|
self.ctx.notify(step_key, 'completed', str(result))
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"{type(e).__name__}: {e}"
|
||||||
|
errors[step_key] = error_msg
|
||||||
|
step_results[step_key] = {'error': error_msg}
|
||||||
|
self.ctx.notify(step_key, 'error', error_msg)
|
||||||
|
# 不中止,继续执行后续步骤
|
||||||
|
|
||||||
|
self.ctx.pipeline_end_time = time.time()
|
||||||
|
total_elapsed = self.ctx.pipeline_end_time - self.ctx.pipeline_start_time
|
||||||
|
|
||||||
|
return {
|
||||||
|
'step_results': step_results,
|
||||||
|
'step_timings': self.ctx.step_timings,
|
||||||
|
'total_elapsed': total_elapsed,
|
||||||
|
'total_elapsed_formatted': self.ctx._format_time(total_elapsed),
|
||||||
|
'errors': errors,
|
||||||
|
}
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
# 便捷属性(代理到 PipelineContext)
|
||||||
|
# ═══════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
@property
|
||||||
|
def work_dir(self) -> Path:
|
||||||
|
return self.ctx.work_dir
|
||||||
|
|
||||||
|
@property
|
||||||
|
def water_mask_path(self) -> Optional[str]:
|
||||||
|
return self.ctx.water_mask_path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def glint_mask_path(self) -> Optional[str]:
|
||||||
|
return self.ctx.glint_mask_path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def deglint_img_path(self) -> Optional[str]:
|
||||||
|
return self.ctx.deglint_img_path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def processed_csv_path(self) -> Optional[str]:
|
||||||
|
return self.ctx.processed_csv_path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def training_csv_path(self) -> Optional[str]:
|
||||||
|
return self.ctx.training_csv_path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def indices_path(self) -> Optional[str]:
|
||||||
|
return self.ctx.indices_path
|
||||||
|
|
||||||
|
def get_step_output_dir(self, step_name: str) -> Path:
|
||||||
|
return self.ctx.get_step_output_dir(step_name)
|
||||||
83
src/core/handlers/step1_water_mask.py
Normal file
83
src/core/handlers/step1_water_mask.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Step1 处理器:水域掩膜生成
|
||||||
|
|
||||||
|
将原 WaterQualityInversionPipeline.step1_generate_water_mask() 方法
|
||||||
|
剥离为独立的 Step1WaterMaskHandler。
|
||||||
|
|
||||||
|
这是 14 个步骤 Handler 的**打样模板**,其余步骤照此模式拆分:
|
||||||
|
1. 继承 BaseStepHandler,设置 step_key 类属性
|
||||||
|
2. 实现 execute(ctx, config) → 调用对应 Step 类的静态方法
|
||||||
|
3. 将输出路径写入 ctx(上下文共享)
|
||||||
|
4. 记录步骤耗时
|
||||||
|
5. 返回结果字典
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from src.core.handlers.base import BaseStepHandler, PipelineContext
|
||||||
|
from src.core.steps.water_mask_step import WaterMaskStep
|
||||||
|
|
||||||
|
|
||||||
|
class Step1WaterMaskHandler(BaseStepHandler):
|
||||||
|
"""步骤1:水域掩膜生成。
|
||||||
|
|
||||||
|
对应 config key: 'step1'
|
||||||
|
委托类: WaterMaskStep.run()
|
||||||
|
|
||||||
|
用法::
|
||||||
|
|
||||||
|
handler = Step1WaterMaskHandler()
|
||||||
|
result = handler.execute(ctx, config['step1'])
|
||||||
|
# ctx.water_mask_path 已被更新
|
||||||
|
"""
|
||||||
|
|
||||||
|
step_key = 'step1'
|
||||||
|
|
||||||
|
def execute(self, context: PipelineContext, config: dict) -> Dict[str, Any]:
|
||||||
|
"""执行水域掩膜生成。
|
||||||
|
|
||||||
|
config 可包含的键(全部透传给 WaterMaskStep.run()):
|
||||||
|
- mask_path: 水体掩膜文件路径(.shp / .dat / .tif)
|
||||||
|
- img_path: 输入影像路径(shp 栅格化或 NDWI 时需要)
|
||||||
|
- ndwi_threshold: NDWI 阈值(默认 0.4)
|
||||||
|
- use_ndwi: 是否使用 NDWI 方法(默认 False)
|
||||||
|
- generate_png: 是否生成 PNG 预览(默认 True)
|
||||||
|
- output_path: 指定输出路径(可选)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{'water_mask_path': str}
|
||||||
|
"""
|
||||||
|
step_start_time = time.time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = WaterMaskStep.run(
|
||||||
|
mask_path=config.get('mask_path'),
|
||||||
|
img_path=config.get('img_path'),
|
||||||
|
ndwi_threshold=config.get('ndwi_threshold', 0.4),
|
||||||
|
use_ndwi=config.get('use_ndwi', False),
|
||||||
|
generate_png=config.get('generate_png', True),
|
||||||
|
output_path=config.get('output_path'),
|
||||||
|
water_mask_dir=str(context.water_mask_dir),
|
||||||
|
callback=context.notify,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 将输出路径写入上下文,供后续步骤使用
|
||||||
|
context.water_mask_path = result
|
||||||
|
|
||||||
|
step_end_time = time.time()
|
||||||
|
context.record_step_time(
|
||||||
|
"步骤1: 水域掩膜生成", step_start_time, step_end_time
|
||||||
|
)
|
||||||
|
|
||||||
|
return {'water_mask_path': result}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
step_end_time = time.time()
|
||||||
|
context.record_step_time(
|
||||||
|
"步骤1: 水域掩膜生成", step_start_time, step_end_time,
|
||||||
|
status="failed", error=str(e)
|
||||||
|
)
|
||||||
|
raise
|
||||||
@ -162,14 +162,19 @@ class PanelFactory:
|
|||||||
scroll.setWidget(panel)
|
scroll.setWidget(panel)
|
||||||
scroll.setWidgetResizable(True)
|
scroll.setWidgetResizable(True)
|
||||||
|
|
||||||
# 替换占位页
|
# 替换占位页(blockSignals 阻断 removeTab/insertTab/setCurrentIndex 触发的
|
||||||
|
# currentChanged 信号风暴,防止 _on_tab_changed → _ensure_loaded 无限递归)
|
||||||
placeholder = self._placeholders.get(tab_index)
|
placeholder = self._placeholders.get(tab_index)
|
||||||
if placeholder is not None and self._tab_widget is not None:
|
if placeholder is not None and self._tab_widget is not None:
|
||||||
tab_title = self._tab_widget.tabText(tab_index)
|
tab_title = self._tab_widget.tabText(tab_index)
|
||||||
tab_icon = self._tab_widget.tabIcon(tab_index)
|
tab_icon = self._tab_widget.tabIcon(tab_index)
|
||||||
|
self._tab_widget.blockSignals(True)
|
||||||
|
try:
|
||||||
self._tab_widget.removeTab(tab_index)
|
self._tab_widget.removeTab(tab_index)
|
||||||
self._tab_widget.insertTab(tab_index, scroll, tab_icon, tab_title)
|
self._tab_widget.insertTab(tab_index, scroll, tab_icon, tab_title)
|
||||||
self._tab_widget.setCurrentIndex(tab_index)
|
self._tab_widget.setCurrentIndex(tab_index)
|
||||||
|
finally:
|
||||||
|
self._tab_widget.blockSignals(False)
|
||||||
|
|
||||||
# 注册
|
# 注册
|
||||||
self._panels[step_id] = panel
|
self._panels[step_id] = panel
|
||||||
|
|||||||
Reference in New Issue
Block a user