refactor(pipeline): 路径直接传输 — 统一 ctx 字段名/panel key/step 形参名
This commit is contained in:
206
.qwen/skills/wq_gui_data_flow/SKILL.md
Normal file
206
.qwen/skills/wq_gui_data_flow/SKILL.md
Normal file
@ -0,0 +1,206 @@
|
||||
---
|
||||
name: WQ_GUI 数据流转架构
|
||||
description: WQ_GUI ProjectSession 事件总线驱动的步骤间数据传递机制(完整重构版)
|
||||
source: auto-skill
|
||||
extracted_at: '2026-05-28T09:07:34.967Z'
|
||||
---
|
||||
|
||||
# WQ_GUI 数据流转架构
|
||||
|
||||
## 核心结论
|
||||
|
||||
整个系统是**基于文件路径驱动**的管道,所有数据存储在本地磁盘。重构后通过 `ProjectSession` 事件总线实现 Panel 间完全解耦。
|
||||
|
||||
---
|
||||
|
||||
## 1. 旧架构(旧代码中已删除)
|
||||
|
||||
主窗口通过 `self.step_outputs` 字典 + `step_dependencies` 配置 + `auto_populate_*` 系列方法管理步骤间路径填充。存在高度耦合问题:
|
||||
|
||||
```python
|
||||
# 已废弃并删除
|
||||
self.step_outputs = {}
|
||||
self._init_step_dependencies()
|
||||
self.update_step_outputs(step_name, work_path)
|
||||
self.auto_populate_dependent_steps(completed_step)
|
||||
self.auto_populate_step_inputs(step_id)
|
||||
self.find_step_output(work_path, step_id, output_type)
|
||||
self.add_auto_fill_buttons_to_panels()
|
||||
self.scan_work_directory_for_files(work_path)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 新架构:ProjectSession 事件总线
|
||||
|
||||
### Session 核心 API(`src/core/project_session.py`)
|
||||
|
||||
```python
|
||||
class ProjectSession(QObject):
|
||||
path_updated = pyqtSignal(str, str, str) # step, out_type, path
|
||||
step_outputs_ready = pyqtSignal(str, str) # step, out_type
|
||||
|
||||
def update_output(step, out_type, path):
|
||||
"""Panel 完成后广播输出路径"""
|
||||
|
||||
def update_outputs(step, {out_type: path, ...}):
|
||||
"""Panel 完成后批量广播多个输出路径"""
|
||||
|
||||
def get_output(step, out_type):
|
||||
"""Panel 可主动查询上游路径(用于自动填充)"""
|
||||
|
||||
def get_step_outputs(step):
|
||||
"""返回该 step 的全部输出字典"""
|
||||
|
||||
def scan_work_directory():
|
||||
"""主窗口 on_step_completed 末尾调用,扫描并广播所有已知路径"""
|
||||
```
|
||||
|
||||
### Panel 重构模板
|
||||
|
||||
```python
|
||||
class StepXPanel(QWidget):
|
||||
def __init__(self, session=None, parent=None):
|
||||
super().__init__(parent)
|
||||
self.session = session
|
||||
self.work_dir = None
|
||||
self.init_ui()
|
||||
self._bind_session_signals()
|
||||
|
||||
def _bind_session_signals(self):
|
||||
if not self.session:
|
||||
return
|
||||
self.session.path_updated.connect(
|
||||
self._on_session_path_updated, Qt.QueuedConnection
|
||||
)
|
||||
|
||||
@pyqtSlot(str, str, str)
|
||||
def _on_session_path_updated(self, step_name, output_type, path):
|
||||
print(f"[StepX Debug] 收到广播: step={step_name}, type={output_type}, path={path}")
|
||||
if step_name == 'step1':
|
||||
if output_type == 'reference_img':
|
||||
if not self.img_file.get_path().strip():
|
||||
self.img_file.set_path(path)
|
||||
print(f"[StepX] 自动填充参考影像: {path}")
|
||||
elif output_type == 'water_mask':
|
||||
if not self.water_mask_file.get_path().strip():
|
||||
self.water_mask_file.set_path(path)
|
||||
print(f"[StepX] 自动填充水域掩膜: {path}")
|
||||
# ...
|
||||
|
||||
def on_step_finished(self, success, message):
|
||||
"""由主窗口 on_step_completed 通过 getattr 动态调用"""
|
||||
if not success:
|
||||
return
|
||||
if self.session:
|
||||
outputs = {}
|
||||
path = self.output_widget.get_path().strip()
|
||||
if path:
|
||||
outputs['output_type'] = path
|
||||
if outputs:
|
||||
self.session.update_outputs('stepX', outputs)
|
||||
```
|
||||
|
||||
### 主窗口两处改动
|
||||
|
||||
```python
|
||||
# 1. __init__ 中注入 session(所有 Panel 统一注入)
|
||||
self.step1_panel = Step1Panel(session=self.session)
|
||||
self.step2_panel = Step2Panel(session=self.session)
|
||||
self.step3_panel = Step3Panel(session=self.session)
|
||||
self.step4_panel = Step4Panel(session=self.session)
|
||||
self.step5_panel = Step5Panel(session=self.session)
|
||||
self.step5_5_panel = Step5_5Panel(session=self.session)
|
||||
self.step6_panel = Step6Panel(session=self.session)
|
||||
self.step6_5_panel = Step6_5Panel(session=self.session)
|
||||
self.step6_75_panel = Step6_75Panel(session=self.session)
|
||||
self.step7_panel = Step7Panel(session=self.session)
|
||||
self.step8_panel = Step8Panel(session=self.session)
|
||||
self.step8_5_panel = Step8_5Panel(session=self.session)
|
||||
self.step8_75_panel = Step8_75Panel(session=self.session)
|
||||
self.step9_panel = Step9Panel(session=self.session)
|
||||
|
||||
# 2. on_step_completed(通用动态获取,无需维护字典)
|
||||
def on_step_completed(self, step_name, success, message):
|
||||
if not success:
|
||||
return
|
||||
if hasattr(self, 'session') and self.session:
|
||||
self.session.scan_work_directory()
|
||||
|
||||
panel = getattr(self, f"{step_name}_panel", None)
|
||||
if panel and hasattr(panel, 'on_step_finished'):
|
||||
panel.on_step_finished(success, message)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 全链路事件流
|
||||
|
||||
### step1 → step2 / step3 路径(通过 Shapefile 栅格化产物)
|
||||
|
||||
| 场景 | 广播的 water_mask 路径 |
|
||||
|------|----------------------|
|
||||
| NDWI 模式 | `output_file` 用户指定路径 |
|
||||
| Shapefile 模式 | `{work_dir}/1_water_mask/water_mask_from_shp.dat`(优先)<br>若文件不存在则 fallback 回 `mask_file.get_path()` |
|
||||
|
||||
```
|
||||
step1 完成
|
||||
→ step1_panel.on_step_finished()
|
||||
→ session.update_outputs('step1', {
|
||||
'reference_img': img_path,
|
||||
'water_mask': mask_path # 可能是 .dat 或 .shp(见上表)
|
||||
})
|
||||
→ step2_panel._on_session_path_updated()
|
||||
→ step3_panel._on_session_path_updated()
|
||||
```
|
||||
|
||||
### step3 → step5 / step7;step5 → 下游训练
|
||||
|
||||
```
|
||||
step3.deglint_image ──┬─→ step5.deglint_image(填充 img_file)
|
||||
└─→ step7.deglint_image(填充 img_file)
|
||||
|
||||
step5.training_spectra ──┬─→ step5_5.index_features
|
||||
├─→ step6.models_dir ──→ step8.predictions
|
||||
├─→ step6_5.models_dir ──→ step8_5.predictions
|
||||
└─→ step6_75.models_dir ──→ step8_75.predictions
|
||||
|
||||
step7.sampling_points ──┬─→ step8
|
||||
├─→ step8_5
|
||||
└─→ step8_75
|
||||
|
||||
step8/8_5/8_75.predictions ──→ step9.distribution_map
|
||||
```
|
||||
|
||||
### 各 Panel 监听/发布对照表(完整版)
|
||||
|
||||
| Panel | 监听 | 发布 |
|
||||
|-------|------|------|
|
||||
| step1 | — | `reference_img`, `water_mask` |
|
||||
| step2 | `step1.reference_img`, `step1.water_mask` | `glint_mask` |
|
||||
| step3 | `step1.reference_img`, `step1.water_mask`, `step2.glint_mask` | `deglint_image` |
|
||||
| step4 | — | `processed_data` |
|
||||
| step5 | `step3.deglint_image`, `step4.processed_data`, `step2.glint_mask` | `training_spectra` |
|
||||
| step5_5 | `step5.training_spectra` | `index_features` |
|
||||
| step6 | `step5.training_spectra` | `models_dir` |
|
||||
| step6_5 | `step5.training_spectra` | `models_dir` |
|
||||
| step6_75 | `step5.training_spectra` | `models_dir` |
|
||||
| step7 | `step3.deglint_image`, `step1.water_mask`, `step2.glint_mask` | `sampling_points` |
|
||||
| step8 | `step7.sampling_points`, `step6.models_dir` | `predictions` |
|
||||
| step8_5 | `step7.sampling_points`, `step6_5.models_dir` | `predictions` |
|
||||
| step8_75 | `step7.sampling_points`, `step6_75.models_dir` | `predictions` |
|
||||
| step9 | `step8.predictions`, `step8_5.predictions`, `step8_75.predictions` | `distribution_map` |
|
||||
|
||||
---
|
||||
|
||||
## 4. 关键约束
|
||||
|
||||
- `__init__` 参数 `session=None`(向后兼容,主窗口可继续不传)
|
||||
- 所有 Panel 的 `init_ui / get_config / set_config / update_from_config` 完整保留
|
||||
- 删除所有 `self.window().stepX_panel` 跨界访问
|
||||
- 使用 `self.session.get_output()` 替代直接读取其他 panel 的 widget
|
||||
- 监听使用 `Qt.QueuedConnection` 确保跨线程安全
|
||||
- 仅在 field 为空时自动填充(`not widget.get_path().strip()`)
|
||||
- `update_from_config` 中优先从 Session 获取路径,再用 Session 广播
|
||||
- 主窗口 `on_step_completed` 中使用 `getattr(self, f"{step_name}_panel", None)` 实现通用动态获取,无需维护硬编码字典
|
||||
- `step1` Shapefile 模式下,**不能**直接广播 `.shp` 输入文件,必须拼接 `{work_dir}/1_water_mask/water_mask_from_shp.dat` 作为产物路径
|
||||
Reference in New Issue
Block a user