feat: 波长偏移修正 + 项目架构文档
- BandMathCalculator 支持 wavelength_offset 参数,公式波长统一加减偏移后匹配传感器波段 - WaterQualityIndexCalculator 全链传递偏移量 (band_math → calculate_one → calculate_many) - WaterIndexCsvProcessor / Step7Handler / DataPreparationStep 传播偏移参数 - Step7/Step10 面板新增 QDoubleSpinBox 波长偏移控件 (±200nm, 默认0) - 偏移控件去除单位后缀,避免编辑时需手动移动光标 - 新增 ARCHITECTURE.md 完整项目架构文档
This commit is contained in:
452
ARCHITECTURE.md
Normal file
452
ARCHITECTURE.md
Normal file
@ -0,0 +1,452 @@
|
||||
# WQ_GUI 水质遥感分析系统 — 架构文档
|
||||
|
||||
> 生成日期:2026-06-30 | 分支:Mega2.2 | 步骤数:13
|
||||
|
||||
---
|
||||
|
||||
## 一、项目总览
|
||||
|
||||
```
|
||||
src/
|
||||
├── gui/
|
||||
│ ├── water_quality_gui.py # V1 入口
|
||||
│ ├── water_quality_gui_v2.py # V2 入口(当前主力)
|
||||
│ ├── styles.py # 全局样式 ModernStylesheet
|
||||
│ ├── core/
|
||||
│ │ ├── panel_registry.py # 步骤注册表
|
||||
│ │ ├── panel_factory.py # 惰性加载面板工厂
|
||||
│ │ ├── pipeline_executor.py # 流水线调度
|
||||
│ │ ├── event_bus.py # 全局事件总线
|
||||
│ │ ├── worker_thread.py # 工作线程
|
||||
│ │ ├── dependency_subscriber.py # 步骤间依赖自动注入
|
||||
│ │ └── workspace_initializer.py # 工作区初始化
|
||||
│ ├── panels/
|
||||
│ │ ├── _step_path_resolver.py # 路径解析(文件系统扫描)
|
||||
│ │ ├── step1_panel.py ~ step13_report_panel.py # 13个步骤面板
|
||||
│ │ └── ...
|
||||
│ └── components/
|
||||
│ ├── custom_widgets.py # FileSelectWidget / DirSelectWidget
|
||||
│ ├── data_models.py # PandasTableModel
|
||||
│ └── chart_dialogs.py # InteractiveViewerDialog 等
|
||||
├── core/ # 后端处理引擎
|
||||
│ ├── handlers/ # 步骤处理器(14个)
|
||||
│ ├── steps/ # 步骤算法实现
|
||||
│ ├── algorithms/ # 核心算法
|
||||
│ └── pipeline/ # 流水线调度器
|
||||
├── postprocessing/ # 后处理(可视化、地图、报告)
|
||||
└── new/ # 新架构(部分迁移中)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、核心架构机制
|
||||
|
||||
### 2.1 事件总线 (EventBus)
|
||||
|
||||
全局单例 `global_event_bus`,解耦面板之间及面板与调度器的通信:
|
||||
|
||||
| 事件名 | 发布者 | 订阅者 | 用途 |
|
||||
|---|---|---|---|
|
||||
| `RequestRunSingleStep` | 各面板 `run_btn` | PipelineExecutor | 单步执行 |
|
||||
| `OutputUpdated` | 面板输出变化时 | 依赖订阅者 | 自动传播输出路径 |
|
||||
| `PipelineStarted/Finished/Stopped` | PipelineExecutor | 全局 | 流水线生命周期 |
|
||||
| `StepCompleted` | WorkerThread | 日志/UI | 单步完成通知 |
|
||||
| `LogMessage` | 各处 | LogManager | 日志写入 |
|
||||
| `ProgressUpdate` | WorkerThread | 进度条 | 进度更新 |
|
||||
| `NavigateToTab` | 各处 | QTabWidget | 标签切换 |
|
||||
| `WorkspaceChanged` | WorkspaceManager | 全局面板 | 工作目录变更 |
|
||||
|
||||
### 2.2 面板工厂 (PanelFactory)
|
||||
|
||||
惰性加载机制:启动时仅创建占位 Tab,首次切换到某 Tab 时才实例化对应 Panel。
|
||||
|
||||
核心方法:
|
||||
- `get_panel(step_id)` → 返回面板实例(已缓存则直接返回)
|
||||
- `_ensure_loaded(tab_index)` → 实例化、包装 QScrollArea、替换占位 Tab
|
||||
- `_replay_state_to_panel(panel)` → 新加载的面板回放上游已产生的输出状态
|
||||
|
||||
### 2.3 依赖注入系统
|
||||
|
||||
`PANEL_REGISTRY` 中每个步骤定义 `dependencies`:
|
||||
|
||||
```python
|
||||
dependencies = {
|
||||
'target_attr': ('upstream_step_id', 'output_type', 'source_attr')
|
||||
}
|
||||
```
|
||||
|
||||
`DependencySubscriber` 监听 `OutputUpdated` 事件,自动将上游输出路径填入下游面板的对应输入控件。
|
||||
|
||||
`_step_path_resolver.py` 则通过文件系统扫描 + 目录映射表实现回退路径发现。
|
||||
|
||||
### 2.4 执行流程
|
||||
|
||||
```
|
||||
用户点击 "独立运行步骤"
|
||||
→ Panel._on_run_single_clicked()
|
||||
→ EventBus.publish('RequestRunSingleStep', {step_name, config})
|
||||
→ PipelineExecutor._on_request_run_single_step()
|
||||
→ WorkerThread(QThread) 启动
|
||||
→ PipelineScheduler.run_step(step_key, config)
|
||||
→ BaseStepHandler.execute(ctx, config)
|
||||
→ 实际算法逻辑
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、13 个步骤详解
|
||||
|
||||
### 模块一:影像预处理(Steps 1-3)
|
||||
|
||||
---
|
||||
|
||||
#### Step 1 — 水域掩膜生成
|
||||
|
||||
| 项目 | 详情 |
|
||||
|---|---|
|
||||
| **面板文件** | `src/gui/panels/step1_panel.py` |
|
||||
| **面板类** | `Step1Panel(QWidget)` |
|
||||
| **后端处理器** | `src/core/handlers/step1_water_mask.py` — `Step1WaterMaskHandler` |
|
||||
| **后端算法** | `src/core/steps/water_mask_step.py` — `WaterMaskStep` |
|
||||
| **UI 输入** | `mask_file`(FileSelectWidget) / NDWI 阈值模式:`ndwi_threshold`(QLineEdit, 0.0-1.0), `img_file`(FileSelectWidget) |
|
||||
| **UI 输出** | `output_file`(FileSelectWidget, mode=save), `run_btn` |
|
||||
| **上游依赖** | 无 |
|
||||
| **产出类型** | `reference_img`, `water_mask` |
|
||||
| **关键方法** | `init_ui()` → `update_ui_state()` 切换掩膜/NDWI 策略;`get_config()` 返回 `{mask_path, use_ndwi, ndwi_threshold, img_path, output_path}` |
|
||||
| **目录映射** | `→ 1_water_mask` |
|
||||
|
||||
#### Step 2 — 耀斑检测
|
||||
|
||||
| 项目 | 详情 |
|
||||
|---|---|
|
||||
| **面板文件** | `src/gui/panels/step2_panel.py` |
|
||||
| **面板类** | `Step2Panel(QWidget)` |
|
||||
| **后端处理器** | `src/core/handlers/step2_glint_detection.py` — `Step2GlintDetectionHandler` |
|
||||
| **后端算法** | `src/core/steps/glint_detection_step.py` — `GlintDetectionStep` |
|
||||
| **UI 输入** | `img_file`(FileSelectWidget), `water_mask_file`(FileSelectWidget) |
|
||||
| **UI 参数** | `glint_wave`(QDoubleSpinBox, 300-1000nm), `method`(QComboBox: Otsu/Z-Score/百分位数/IQR/自适应/多波段综合), `max_area`(QSpinBox), `buffer_size`(QSpinBox) |
|
||||
| **UI 输出** | `output_file`(FileSelectWidget, mode=save), `run_btn` |
|
||||
| **上游依赖** | `img_file` ← step1.reference_img; `water_mask_file` ← step1.water_mask |
|
||||
| **产出类型** | `glint_mask` |
|
||||
| **目录映射** | `→ 2_Glint_Detection` |
|
||||
|
||||
#### Step 3 — 去耀斑
|
||||
|
||||
| 项目 | 详情 |
|
||||
|---|---|
|
||||
| **面板文件** | `src/gui/panels/step3_panel.py` |
|
||||
| **面板类** | `Step3Panel(QWidget)` |
|
||||
| **后端处理器** | `src/core/handlers/step3_glint_removal.py` — `Step3GlintRemovalHandler` |
|
||||
| **后端算法** | `src/core/steps/glint_removal_step.py` — `GlintRemovalStep` |
|
||||
| **UI 输入** | `img_file`(FileSelectWidget), `water_mask_file`(FileSelectWidget) |
|
||||
| **UI 参数** | `method`(QComboBox: Goodman/Kutser/Hedley/SUGAR), 4 个 StackedWidget 参数页 + `interpolate_zeros`(QCheckBox), `interp_method`(QComboBox) |
|
||||
| **UI 输出** | `output_file`(FileSelectWidget), `run_btn` |
|
||||
| **上游依赖** | `img_file` ← step1.reference_img; `water_mask_file` ← step1.water_mask |
|
||||
| **产出类型** | `deglint_image` |
|
||||
| **特殊机制** | `img_file.textChanged` → 自动调用 `_update_band_ranges()` 检测波段范围;`method.currentIndexChanged` → 切换参数 StackedWidget 页面 |
|
||||
| **目录映射** | `→ 3_deglint` |
|
||||
|
||||
---
|
||||
|
||||
### 模块二:特征工程与数据(Steps 4-7)
|
||||
|
||||
---
|
||||
|
||||
#### Step 4 — 采样点布局与交互式探索
|
||||
|
||||
| 项目 | 详情 |
|
||||
|---|---|
|
||||
| **面板文件** | `src/gui/panels/step4_sampling_panel.py` |
|
||||
| **面板类** | `Step4SamplingPanel(QWidget)` |
|
||||
| **后端处理器** | `src/core/handlers/step4_sampling.py` — `Step4SamplingHandler` |
|
||||
| **UI 输入** | `deglint_img_file`(FileSelectWidget), `water_mask_file`(FileSelectWidget) |
|
||||
| **UI 参数** | `interval`(QSpinBox, 10-500px), `sample_radius`(QSpinBox, 1-50px), `chunk_size`(QSpinBox, 100-10000px), `use_adaptive_sampling`(QCheckBox) |
|
||||
| **UI 输出** | `output_file`(FileSelectWidget), `refresh_btn`, `run_btn` |
|
||||
| **可视化** | Matplotlib 嵌入:`FigureCanvasQTAgg` + 隐藏 `NavigationToolbar2QT`,左右分栏(散点图 + 光谱曲线),自定义工具栏按钮 |
|
||||
| **交互** | `_on_hover()` 鼠标变小手 + 悬停标注;`_on_click()` 点击高亮 + 光谱绘制 + 再次点击取消选中;三按钮互斥:👆 点选探针 / ✋ 拖拽漫游 / 🔍 框选放大 |
|
||||
| **上游依赖** | `deglint_img_file` ← step3.deglint_image; `water_mask_file` ← step1.water_mask |
|
||||
| **产出类型** | `sampling_points` |
|
||||
| **特殊机制** | `_status_timer` (5000ms) 自动检测 CSV 并重渲染;`draw_idle()` 防 UI 卡死;rcParams 中文字体双重保障 |
|
||||
| **目录映射** | `→ 4_sampling` |
|
||||
|
||||
#### Step 5 — 数据清洗
|
||||
|
||||
| 项目 | 详情 |
|
||||
|---|---|
|
||||
| **面板文件** | `src/gui/panels/step5_clean_panel.py` |
|
||||
| **面板类** | `Step5CleanPanel(QWidget)` |
|
||||
| **后端处理器** | `src/core/handlers/step5_process_csv.py` — `Step5ProcessCsvHandler` |
|
||||
| **UI 输入** | `csv_file`(FileSelectWidget) |
|
||||
| **UI 参数** | `preview_rows_spin`(QSpinBox), `preview_table`(QTableView + PandasTableModel), `preview_status_label`(QLabel) |
|
||||
| **UI 输出** | `output_file`(FileSelectWidget, mode=save), `run_btn` |
|
||||
| **上游依赖** | 无(独立输入源) |
|
||||
| **产出类型** | `processed_data` |
|
||||
| **目录映射** | `→ 5_Data_Cleaning` |
|
||||
|
||||
#### Step 6 — 光谱特征提取
|
||||
|
||||
| 项目 | 详情 |
|
||||
|---|---|
|
||||
| **面板文件** | `src/gui/panels/step6_feature_panel.py` |
|
||||
| **面板类** | `Step6FeaturePanel(QWidget)` |
|
||||
| **后端处理器** | `src/core/handlers/step6_extract_spectra.py` — `Step6ExtractSpectraHandler` |
|
||||
| **UI 输入** | `deglint_img_file`(FileSelectWidget), `csv_file`(FileSelectWidget), `water_mask_file`(FileSelectWidget), `glint_mask_file`(FileSelectWidget) |
|
||||
| **UI 参数** | `radius`(QSpinBox, 1-50px), `source_epsg`(QSpinBox, default 4326) |
|
||||
| **UI 输出** | `output_file`(FileSelectWidget), `run_btn` |
|
||||
| **上游依赖** | `deglint_img_file` ← step3; `csv_file` ← step5; `water_mask_file` ← step1; `glint_mask_file` ← step2 |
|
||||
| **产出类型** | `output_file` |
|
||||
| **目录映射** | `→ 6_Spectral_Feature_Extraction` |
|
||||
|
||||
#### Step 7 — 水质指数计算
|
||||
|
||||
| 项目 | 详情 |
|
||||
|---|---|
|
||||
| **面板文件** | `src/gui/panels/step7_inversion_panel.py` |
|
||||
| **面板类** | `Step7InversionPanel(QWidget)` |
|
||||
| **后端处理器** | `src/core/handlers/step7_calc_indices.py` — `Step7CalcIndicesHandler` |
|
||||
| **UI 输入** | `formula_file`(FileSelectWidget, 只读), `training_data_widget`(FileSelectWidget) |
|
||||
| **UI 参数** | `category_combo`(QComboBox 按类别筛选), `formula_list`(NoScrollPassListWidget, checkable), 选择按钮:全选/清空/比值/浓度/重载 |
|
||||
| **UI 输出** | `output_file`(FileSelectWidget), `run_btn` |
|
||||
| **上游依赖** | `training_data_widget` ← step6_feature.output_file |
|
||||
| **产出类型** | `training_spectra_indices` |
|
||||
| **特殊机制** | `_load_formulas_from_csv()` 从内置公式库加载;`_update_formula_count()` 统计选中数 |
|
||||
| **目录映射** | `→ 7_Water_Quality_Indices` |
|
||||
|
||||
---
|
||||
|
||||
### 模块三:模型训练与反演(Steps 8-10)
|
||||
|
||||
---
|
||||
|
||||
#### Step 8 — 机器学习建模
|
||||
|
||||
| 项目 | 详情 |
|
||||
|---|---|
|
||||
| **面板文件** | `src/gui/panels/step8_ml_train_panel.py` |
|
||||
| **面板类** | `Step8MlTrainPanel(QWidget)` |
|
||||
| **后端处理器** | `src/core/handlers/step8_ml_train.py` — `Step8MlTrainHandler` |
|
||||
| **后端算法** | `src/core/steps/modeling_step.py` — `ModelingStep` |
|
||||
| **UI 输入** | `training_csv_file`(FileSelectWidget) |
|
||||
| **UI 参数** | `feature_start`(QComboBox), `cv_folds`(QSpinBox 2-10), 预处理 11 项 CheckBox(None/MMS/SS/SNV/MA/SG/MSC/D1/D2/DT/CT), 模型 15 项 CheckBox(LR/Ridge/Lasso/ElasticNet/PLS/DT/RF/ExtraTrees/XGBoost/LightGBM/CatBoost/GBDT/AdaBoost/SVR/KNN/MLP), 数据划分 3 项 CheckBox(SPXY/KS/Random) |
|
||||
| **UI 输出** | `output_path`(FileSelectWidget, 目录模式), `run_btn` |
|
||||
| **上游依赖** | `training_csv_file` ← step7_index.training_spectra_indices |
|
||||
| **产出类型** | `output_path` |
|
||||
| **目录映射** | `→ 8_Supervised_Model_Training` |
|
||||
|
||||
#### Step 9 — 机器学习预测
|
||||
|
||||
| 项目 | 详情 |
|
||||
|---|---|
|
||||
| **面板文件** | `src/gui/panels/step9_ml_predict_panel.py` |
|
||||
| **面板类** | `Step9MlPredictPanel(QWidget)` |
|
||||
| **后端处理器** | `src/core/handlers/step9_ml_predict.py` — `Step9MlPredictHandler` |
|
||||
| **后端算法** | `src/core/steps/prediction_step.py` — `PredictionStep` |
|
||||
| **UI 输入** | `sampling_csv_file`(FileSelectWidget), `models_dir_file`(FileSelectWidget, 目录) |
|
||||
| **UI 参数** | 模型来源:`use_trained_model`/`use_external_model`(QRadioButton), 外部模型 `model_list`(QListWidget, checkable), `metric`(QComboBox: R²/RMSE/MAE), `prediction_column`(QLineEdit) |
|
||||
| **UI 输出** | `output_file`(FileSelectWidget, 目录), `run_btn` |
|
||||
| **上游依赖** | `models_dir_file` ← step8_ml_train.output_path |
|
||||
| **产出类型** | `output_file` |
|
||||
| **目录映射** | `→ 9_ML_Prediction` |
|
||||
|
||||
#### Step 10 — 水色指数反演
|
||||
|
||||
| 项目 | 详情 |
|
||||
|---|---|
|
||||
| **面板文件** | `src/gui/panels/step10_watercolor_panel.py` |
|
||||
| **面板类** | `Step10WatercolorPanel(QWidget)` |
|
||||
| **后端处理器** | `src/core/handlers/step10_qaa_inversion.py` — `Step10QaaInversionHandler` |
|
||||
| **后端算法** | `src/core/algorithms/waterindex_inversion.py` — `WaterIndexCsvProcessor` |
|
||||
| **UI 输入** | `formula_file`(FileSelectWidget, 只读), `sampling_csv_file`(FileSelectWidget) |
|
||||
| **UI 参数** | `category_combo`(QComboBox 按水质类别筛选), `formula_list`(NoScrollPassListWidget, checkable), 全选/清空/比值/浓度按钮 |
|
||||
| **UI 输出** | `output_dir`(FileSelectWidget, 目录), `progress_bar`(QProgressBar), `progress_label`(QLabel), `run_btn` |
|
||||
| **上游依赖** | `sampling_csv_file` ← step4_sampling.sampling_points |
|
||||
| **产出类型** | `output_dir` |
|
||||
| **特殊机制** | 内嵌 `WaterIndexWorker(QThread)` 后台处理 CSV |
|
||||
| **目录映射** | `→ 10_WaterIndex_CSV` |
|
||||
|
||||
---
|
||||
|
||||
### 模块四:制图与成果汇编(Steps 11-13)
|
||||
|
||||
---
|
||||
|
||||
#### Step 11 — 专题图生成
|
||||
|
||||
| 项目 | 详情 |
|
||||
|---|---|
|
||||
| **面板文件** | `src/gui/panels/step11_map_panel.py` |
|
||||
| **面板类** | `Step11MapPanel(QWidget)` |
|
||||
| **后端处理器** | `src/core/handlers/step11_concentration.py` — `Step11ConcentrationHandler` |
|
||||
| **后端算法** | CSV 模式: `src/core/steps/mapping_step.py` — `MappingStep.generate_distribution_map()`;GeoTIFF 模式: `src/postprocessing/map.py` — `ContentMapper.visualize_raster()` |
|
||||
| **UI 输入** | `render_mode_combo`(QComboBox: CSV插值/GeoTIFF栅格), `batch_mode_combo`(QComboBox: 单个文件/文件夹批量), `prediction_csv_file`(FileSelectWidget), `geotiff_file`(FileSelectWidget), `boundary_file`(FileSelectWidget: *.shp) |
|
||||
| **UI 参数** | `resolution`(QDoubleSpinBox, 1-1000), `input_crs`/`output_crs`(QLineEdit, EPSG:32651), `show_points`(QCheckBox), `use_diffusion`(QCheckBox) |
|
||||
| **UI 输出** | `output_dir`(FileSelectWidget), `progress_bar`(QProgressBar), `run_button` |
|
||||
| **上游依赖** | `prediction_csv_dir_edit` ← step9; `geotiff_dir_edit` ← step10; `boundary_file` ← step1.water_mask |
|
||||
| **特殊机制** | 两个后台线程:`Step11MapBatchThread`(CSV) / `Step11GeoTIFFBatchThread`(GeoTIFF) |
|
||||
| **目录映射** | `→ 14_visualization` |
|
||||
|
||||
#### Step 12 — 可视化
|
||||
|
||||
| 项目 | 详情 |
|
||||
|---|---|
|
||||
| **面板文件** | `src/gui/panels/step12_viz_panel.py` |
|
||||
| **面板类** | `Step12VizPanel(QWidget)` |
|
||||
| **后端算法** | `src/postprocessing/visualization_reports.py` — `WaterQualityVisualization`; `src/core/visualization/scatter_plot.py`; `src/postprocessing/point_map.py` — `SamplingPointMap` |
|
||||
| **UI 输入** | `work_dir_edit`(QLineEdit), `img_dir_edit`(QLineEdit) |
|
||||
| **UI 参数** | 6 个 QCheckBox: `gen_scatter`(模型评估散点图), `gen_spectrum`(光谱曲线), `gen_boxplots`(统计图), `gen_mask_glint`(掩膜缩略图), `gen_sampling_map`(采样点地图), `gen_distribution_map`(空间分布图) |
|
||||
| **UI 输出** | `gen_all_btn`(QPushButton), `scan_btn`(QPushButton), `ImageCategoryTree`(QTreeWidget), `ImageViewerWidget`(带缩放/平移/保存), 筛选: `view_mode_cb` + `chart_filter_cb` |
|
||||
| **上游依赖** | 无(文件系统自动扫描) |
|
||||
| **产出类型** | 多种图表 PNG |
|
||||
| **特殊机制** | `VisualizationWorkerThread(QThread)` 后台批量生成;`ImageCategoryTree` 三视图模式(按水质参数/图表类型/物理文件夹) |
|
||||
|
||||
#### Step 13 — 报告生成
|
||||
|
||||
| 项目 | 详情 |
|
||||
|---|---|
|
||||
| **面板文件** | `src/gui/panels/step13_report_panel.py` |
|
||||
| **面板类** | `Step13ReportPanel(QWidget)` |
|
||||
| **后端算法** | `src/postprocessing/report_word.py` — `WaterQualityReportGenerator` + `ReportGenerationConfig` |
|
||||
| **UI 输入** | `work_dir_edit`(QLineEdit, 只读), `output_dir_edit`(QLineEdit + 浏览) |
|
||||
| **UI 参数** | `report_title_edit`(QLineEdit), `enable_ai_cb`(QCheckBox), AI 设置按钮 → `AISettingsDialog` |
|
||||
| **UI 输出** | `progress_label`(QLabel), `progress_bar`(QProgressBar), `generate_btn`(QPushButton) |
|
||||
| **上游依赖** | 无(但需要 `main_window` 在构造时注入) |
|
||||
| **产出类型** | Word 报告 (.docx) |
|
||||
| **特殊机制** | `ReportWorkerThread(QThread)` 后台生成;AI 分析可选(需配置 API Key) |
|
||||
| **目录映射** | `→ reports` |
|
||||
|
||||
---
|
||||
|
||||
## 四、步骤间数据流转图
|
||||
|
||||
```
|
||||
Step1 水域掩膜 ─── img_file ───────────────┬──► Step2 耀斑检测 ── glint_mask ──► Step6
|
||||
│ │
|
||||
├── water_mask ──────────────────────┼──► Step3 去耀斑 ─── deglint ────► Step4
|
||||
│ │ │ │
|
||||
│ │ └───────────────────────────┤
|
||||
│ │ │
|
||||
│ ├──► Step4 采样 ←────────────────────┘
|
||||
│ ├──► Step6 特征提取
|
||||
│ ├──► Step11 专题图 ───► Step12
|
||||
│ └──► Step12 可视化 Step13
|
||||
│
|
||||
└──► Step11 专题图(边界)
|
||||
|
||||
Step4 采样 ── sampling_csv ──► Step10 水色指数 ── output_dir ──► Step11(GeoTIFF)
|
||||
|
||||
Step5 清洗 ── csv ──► Step6 特征提取 ── output ──► Step7 指数计算
|
||||
│
|
||||
▼
|
||||
Step8 机器学习建模
|
||||
│
|
||||
▼
|
||||
Step9 机器学习预测
|
||||
│
|
||||
▼
|
||||
Step11 专题图(CSV)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、自定义组件
|
||||
|
||||
### 5.1 文件选择控件
|
||||
|
||||
**`FileSelectWidget`** (`src/gui/components/custom_widgets.py`)
|
||||
|
||||
| 模式 | 行为 |
|
||||
|---|---|
|
||||
| `mode="open"` | `QFileDialog.getOpenFileName()` — 选择已存在文件 |
|
||||
| `mode="save"` | `QFileDialog.getSaveFileName()` — 选择保存路径 |
|
||||
| `mode="dir"` | `QFileDialog.getExistingDirectory()` — 选择目录 |
|
||||
|
||||
关键方法:`get_path()`, `set_path(path)`, `set_read_only(bool)`, `line_edit.textChanged` 信号
|
||||
|
||||
### 5.2 路径解析器
|
||||
|
||||
**`_step_path_resolver.py`** — 所有 13 个 Panel 统一通过 `from src.gui.panels._step_path_resolver import ...` 导入
|
||||
|
||||
| 函数 | 用途 |
|
||||
|---|---|
|
||||
| `resolve_subdir(work_dir, subdir_key)` | 步骤名 → 实际子目录路径 |
|
||||
| `scan_work_dir_for_input(work_dir, output_type)` | 文件系统扫描发现上游输出 |
|
||||
| `resolve_step_widget(main_window, step_key, widget_attr)` | 通过 panel_factory 定位上游控件 |
|
||||
| `get_step_output_path(main_window, step_key, work_dir, widget_attr, fallback_key)` | 获取上游输出路径(含回退) |
|
||||
|
||||
### 5.3 全局样式
|
||||
|
||||
**`ModernStylesheet`** (`src/gui/styles.py`) — 提供 `get_button_stylesheet('normal'|'primary')`
|
||||
|
||||
---
|
||||
|
||||
## 六、关键设计模式
|
||||
|
||||
| 模式 | 位置 | 说明 |
|
||||
|---|---|---|
|
||||
| **惰性加载** | PanelFactory | Tab 切换时才实例化 Panel,降低启动时间 |
|
||||
| **事件总线** | EventBus | Panel ↔ PipelineExecutor 解耦通信 |
|
||||
| **依赖注入** | DependencySubscriber + PANEL_REGISTRY | 上游输出自动填入下游输入 |
|
||||
| **文件系统回退** | _step_path_resolver | 当依赖注入失败时,扫描实际文件系统 |
|
||||
| **独立运行** | 每个 Panel 的 `run_btn` | 不依赖全流水线,可单独执行 |
|
||||
| **后台线程** | WorkerThread / 各 Panel 内嵌 QThread | 长时间计算不阻塞 GUI |
|
||||
| **交互模式** | Step4 三按钮互斥 | 点选探针 / 拖拽漫游 / 框选放大 |
|
||||
|
||||
---
|
||||
|
||||
## 七、文件清单
|
||||
|
||||
### 前端面板(13 个)
|
||||
|
||||
| 文件 | 类 | Step ID |
|
||||
|---|---|---|
|
||||
| `step1_panel.py` | `Step1Panel` | `step1` |
|
||||
| `step2_panel.py` | `Step2Panel` | `step2` |
|
||||
| `step3_panel.py` | `Step3Panel` | `step3` |
|
||||
| `step4_sampling_panel.py` | `Step4SamplingPanel` | `step4_sampling` |
|
||||
| `step5_clean_panel.py` | `Step5CleanPanel` | `step5_clean` |
|
||||
| `step6_feature_panel.py` | `Step6FeaturePanel` | `step6_feature` |
|
||||
| `step7_inversion_panel.py` | `Step7InversionPanel` | `step7_index` |
|
||||
| `step8_ml_train_panel.py` | `Step8MlTrainPanel` | `step8_ml_train` |
|
||||
| `step9_ml_predict_panel.py` | `Step9MlPredictPanel` | `step9_ml_predict` |
|
||||
| `step10_watercolor_panel.py` | `Step10WatercolorPanel` | `step10_watercolor` |
|
||||
| `step11_map_panel.py` | `Step11MapPanel` | `step11_map` |
|
||||
| `step12_viz_panel.py` | `Step12VizPanel` | `step12_viz` |
|
||||
| `step13_report_panel.py` | `Step13ReportPanel` | `step13_report` |
|
||||
|
||||
### 后端核心(14 个 Handler)
|
||||
|
||||
| 文件 | 类 | Step Key |
|
||||
|---|---|---|
|
||||
| `handlers/step1_water_mask.py` | `Step1WaterMaskHandler` | `step1` |
|
||||
| `handlers/step2_glint_detection.py` | `Step2GlintDetectionHandler` | `step2` |
|
||||
| `handlers/step3_glint_removal.py` | `Step3GlintRemovalHandler` | `step3` |
|
||||
| `handlers/step4_sampling.py` | `Step4SamplingHandler` | `step4_sampling` |
|
||||
| `handlers/step5_process_csv.py` | `Step5ProcessCsvHandler` | `step5_clean` |
|
||||
| `handlers/step6_extract_spectra.py` | `Step6ExtractSpectraHandler` | `step6_feature` |
|
||||
| `handlers/step7_calc_indices.py` | `Step7CalcIndicesHandler` | `step7_index` |
|
||||
| `handlers/step8_ml_train.py` | `Step8MlTrainHandler` | `step8_ml_train` |
|
||||
| `handlers/step9_ml_predict.py` | `Step9MlPredictHandler` | `step9_ml_predict` |
|
||||
| `handlers/step10_qaa_inversion.py` | `Step10QaaInversionHandler` | `step10_qaa_inversion` |
|
||||
| `handlers/step11_concentration.py` | `Step11ConcentrationHandler` | `step11_concentration` |
|
||||
| `handlers/step12_kriging.py` | `Step12KrigingHandler` | `step12_kriging` |
|
||||
| `handlers/step13_visualization.py` | `Step13VisualizationHandler` | `step13_visualization` |
|
||||
| `handlers/step14_report.py` | `Step14ReportHandler` | `step14_report` |
|
||||
|
||||
> 注:Handler 注册有 14 个(step1-14),对应 Panel 有 13 个(step1-13)。Step12 的 Handler 处理克里金插值,Step13 的 Handler 处理可视化,Step14 的 Handler 处理报告。
|
||||
|
||||
---
|
||||
|
||||
## 八、审计修复记录(2026-06-30)
|
||||
|
||||
本轮已修复的全部问题:
|
||||
|
||||
| # | 严重度 | 文件 | 问题 | 修复 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | 🔴 | `step11_map_panel.py:652` | `mapper` 在 import 之前引用 | 移动 import 至引用上方 |
|
||||
| 2 | 🟡 | `step12_viz_panel.py:1719` | `get_panel('step1_mask')` key 不存在 | 改为 `'step1'` |
|
||||
| 3 | 🟡 | `step3_panel.py:384` | `currentData()` 返回 None | 改为 `currentText()` |
|
||||
| 4 | 🟡 | `step5_clean_panel.py:228/253` | PandasTableModel 从 v1 入口导入 | 改为 `src.gui.components.data_models` |
|
||||
| 5 | 🟡 | `step3_panel.py:271` | InteractiveViewerDialog 同上 | 改为 `src.gui.components.chart_dialogs` |
|
||||
| 6 | 🟢 | `step7_inversion_panel.py:9` | `import csv` 未使用 | 移除 |
|
||||
| 7 | 🟢 | `step5_clean_panel.py:167` | 死方法 `_add_row_with_fixed_label` | 标注废弃 |
|
||||
| 8 | 🟢 | 全部 12 个 Panel | `_step_path_resolver` 导入风格不一致 | 统一为 `from src.gui.panels._step_path_resolver import ...`,移除 `_HERE`/`sys.path` hack |
|
||||
@ -114,6 +114,7 @@ class WaterIndexCsvProcessor:
|
||||
output_dir: str,
|
||||
selected_formulas: Optional[List[str]] = None,
|
||||
progress_callback: Optional[Callable[[str, float], None]] = None,
|
||||
wavelength_offset: float = 0.0,
|
||||
) -> Dict[str, str]:
|
||||
"""
|
||||
散点 CSV → 按指数拆分的多个 CSV。
|
||||
@ -128,6 +129,8 @@ class WaterIndexCsvProcessor:
|
||||
要计算的公式名列表;None 或空列表 = 全部公式
|
||||
progress_callback : callable, optional
|
||||
进度回调 ``(msg: str, pct: float)``
|
||||
wavelength_offset : float
|
||||
波长偏移修正量(nm),公式波长统一加上此值后再匹配波段
|
||||
|
||||
Returns
|
||||
-------
|
||||
@ -192,7 +195,7 @@ class WaterIndexCsvProcessor:
|
||||
notify(f"开始逐行计算 {len(targets)} 个公式…", 25)
|
||||
spectra_df = df[wl_cols]
|
||||
try:
|
||||
results_df = calc.calculate_many(targets, spectra_df)
|
||||
results_df = calc.calculate_many(targets, spectra_df, wavelength_offset=wavelength_offset)
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"公式计算失败: {e}")
|
||||
|
||||
|
||||
@ -38,6 +38,7 @@ class Step7CalcIndicesHandler(BaseStepHandler):
|
||||
output_file=config.get('output_file'),
|
||||
enabled=config.get('enabled', True),
|
||||
output_dir=str(context.indices_dir),
|
||||
wavelength_offset=float(config.get('wavelength_offset', 0)),
|
||||
)
|
||||
|
||||
context.indices_path = result
|
||||
|
||||
@ -133,6 +133,7 @@ class DataPreparationStep:
|
||||
enabled: bool = True,
|
||||
output_dir: Union[str, Path] = "./7_Water_Quality_Indices",
|
||||
callback: Optional[Callable] = None,
|
||||
wavelength_offset: float = 0.0,
|
||||
) -> Optional[str]:
|
||||
"""根据训练光谱计算水质光谱指数(使用 band_math 方法)"""
|
||||
output_dir = Path(output_dir)
|
||||
@ -170,7 +171,7 @@ class DataPreparationStep:
|
||||
|
||||
from src.utils.band_math import BandMathCalculator
|
||||
|
||||
calculator = BandMathCalculator(training_csv_path)
|
||||
calculator = BandMathCalculator(training_csv_path, wavelength_offset=wavelength_offset)
|
||||
result_df = calculator.process_formulas_from_csv(
|
||||
formula_csv_file=formula_csv_file,
|
||||
formula_names=formula_names,
|
||||
|
||||
@ -26,7 +26,7 @@ from PyQt5.QtWidgets import (
|
||||
QGroupBox, QLabel, QLineEdit, QComboBox, QCheckBox, QPushButton,
|
||||
QFileDialog, QMessageBox, QListWidget, QListWidgetItem,
|
||||
QAbstractItemView, QProgressBar, QTextEdit, QFrame,
|
||||
QScrollArea, QSizePolicy,
|
||||
QScrollArea, QSizePolicy, QDoubleSpinBox,
|
||||
)
|
||||
from PyQt5.QtGui import QFont
|
||||
from PyQt5.QtCore import Qt, QThread, pyqtSignal
|
||||
@ -72,6 +72,7 @@ class WaterIndexWorker(QThread):
|
||||
selected_formulas: List[str],
|
||||
waterindex_csv: str,
|
||||
work_dir: Optional[str] = None,
|
||||
wavelength_offset: float = 0.0,
|
||||
):
|
||||
super().__init__()
|
||||
self.sampling_csv_path = sampling_csv_path
|
||||
@ -79,6 +80,7 @@ class WaterIndexWorker(QThread):
|
||||
self.selected_formulas = selected_formulas
|
||||
self.waterindex_csv = waterindex_csv
|
||||
self.work_dir = work_dir
|
||||
self.wavelength_offset = float(wavelength_offset)
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
@ -96,6 +98,7 @@ class WaterIndexWorker(QThread):
|
||||
output_dir=self.output_dir,
|
||||
selected_formulas=self.selected_formulas or None,
|
||||
progress_callback=lambda m, p: self.progress.emit(m, p),
|
||||
wavelength_offset=self.wavelength_offset,
|
||||
)
|
||||
|
||||
self.progress.emit(
|
||||
@ -166,7 +169,23 @@ class Step10WatercolorPanel(QWidget):
|
||||
self.sampling_csv_file.label.setMinimumWidth(100)
|
||||
input_layout.addWidget(self.sampling_csv_file)
|
||||
|
||||
# 注意:彻底去掉了 self.meta_label 及其相关的布局代码
|
||||
# ── 波长偏移修正 ──
|
||||
offset_layout = QHBoxLayout()
|
||||
offset_label = QLabel("波长偏移 (nm):")
|
||||
offset_label.setMinimumWidth(120)
|
||||
self.wavelength_offset_spin = QDoubleSpinBox()
|
||||
self.wavelength_offset_spin.setRange(-200.0, 200.0)
|
||||
self.wavelength_offset_spin.setValue(0.0)
|
||||
self.wavelength_offset_spin.setDecimals(1)
|
||||
self.wavelength_offset_spin.setSuffix("")
|
||||
self.wavelength_offset_spin.setToolTip(
|
||||
"传感器波长系统偏移修正量。正数=公式波长加偏移(如+100则w450→找≈w550的波段);"
|
||||
"负数=公式波长减偏移。默认0表示不做修正。"
|
||||
)
|
||||
offset_layout.addWidget(offset_label)
|
||||
offset_layout.addWidget(self.wavelength_offset_spin)
|
||||
offset_layout.addStretch()
|
||||
input_layout.addLayout(offset_layout)
|
||||
|
||||
input_group.setLayout(input_layout)
|
||||
layout.addWidget(input_group)
|
||||
@ -653,6 +672,7 @@ class Step10WatercolorPanel(QWidget):
|
||||
selected_formulas=selected,
|
||||
waterindex_csv=self._waterindex_csv,
|
||||
work_dir=work_dir,
|
||||
wavelength_offset=self.wavelength_offset_spin.value(),
|
||||
)
|
||||
self._worker.progress.connect(self._on_progress)
|
||||
self._worker.finished_ok.connect(self._on_finished)
|
||||
|
||||
@ -14,6 +14,7 @@ from PyQt5.QtWidgets import (
|
||||
QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout,
|
||||
QLabel, QPushButton, QMessageBox, QListWidget,
|
||||
QListWidgetItem, QSizePolicy, QWidget, QComboBox,
|
||||
QDoubleSpinBox,
|
||||
)
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QColor
|
||||
@ -238,6 +239,30 @@ class Step7InversionPanel(QWidget):
|
||||
params_group.setLayout(params_layout)
|
||||
main_layout.addWidget(params_group)
|
||||
|
||||
# ==========================================
|
||||
# 波长偏移修正
|
||||
# ==========================================
|
||||
offset_group = QGroupBox("🔧 波长偏移修正")
|
||||
offset_layout = QHBoxLayout()
|
||||
offset_layout.setContentsMargins(20, 16, 20, 16)
|
||||
offset_label = QLabel("统一偏移量 (nm):")
|
||||
offset_label.setMinimumWidth(120)
|
||||
self.wavelength_offset_spin = QDoubleSpinBox()
|
||||
self.wavelength_offset_spin.setRange(-200.0, 200.0)
|
||||
self.wavelength_offset_spin.setValue(0.0)
|
||||
self.wavelength_offset_spin.setDecimals(1)
|
||||
self.wavelength_offset_spin.setSuffix("")
|
||||
self.wavelength_offset_spin.setToolTip(
|
||||
"传感器波长系统偏移修正。公式中的目标波长统一加上此值后再匹配传感器波段。\n"
|
||||
"例如:偏移 +100 意味着公式中的 w450 实际去找传感器 ~550nm 的波段。\n"
|
||||
"默认 0 不做修正。适用于传感器标定漂移或不同传感器间的波段对齐。"
|
||||
)
|
||||
offset_layout.addWidget(offset_label)
|
||||
offset_layout.addWidget(self.wavelength_offset_spin)
|
||||
offset_layout.addStretch()
|
||||
offset_group.setLayout(offset_layout)
|
||||
main_layout.addWidget(offset_group)
|
||||
|
||||
# ==========================================
|
||||
# 卡片 3:输出与执行
|
||||
# ==========================================
|
||||
@ -309,7 +334,8 @@ class Step7InversionPanel(QWidget):
|
||||
'training_csv_path': self.training_data_widget.get_path(),
|
||||
'formula_csv_file': self.formula_file.get_path(),
|
||||
'formula_names': selected_names,
|
||||
'enabled': True
|
||||
'enabled': True,
|
||||
'wavelength_offset': self.wavelength_offset_spin.value(),
|
||||
}
|
||||
output_path = self.output_file.get_path()
|
||||
if output_path:
|
||||
@ -337,6 +363,12 @@ class Step7InversionPanel(QWidget):
|
||||
if 'output_path' in config:
|
||||
self.output_file.set_path(config['output_path'])
|
||||
|
||||
if 'wavelength_offset' in config:
|
||||
try:
|
||||
self.wavelength_offset_spin.setValue(float(config['wavelength_offset']))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
def _load_formulas_from_csv(self):
|
||||
"""解析公式 CSV 文件并填充列表框"""
|
||||
csv_path = self.formula_file.get_path()
|
||||
|
||||
@ -4,13 +4,16 @@ import re
|
||||
|
||||
|
||||
class BandMathCalculator:
|
||||
def __init__(self, csv_file):
|
||||
def __init__(self, csv_file, wavelength_offset=0.0):
|
||||
"""
|
||||
初始化计算器
|
||||
csv_file: 包含光谱反射率的CSV文件路径
|
||||
wavelength_offset: 波长偏移修正量(nm),公式中的目标波长会统一加上此偏移后再匹配最近的传感器波段。
|
||||
例如 offset=100 意味着公式中的 w450 实际会去找传感器波段 ~550nm。
|
||||
"""
|
||||
self.df = pd.read_csv(csv_file)
|
||||
self.wavelengths = self._extract_wavelengths()
|
||||
self.wavelength_offset = float(wavelength_offset)
|
||||
|
||||
def _extract_wavelengths(self):
|
||||
"""从列名中提取波长信息"""
|
||||
@ -25,19 +28,25 @@ class BandMathCalculator:
|
||||
return wavelengths
|
||||
|
||||
def _find_closest_wavelength(self, target_wavelength):
|
||||
"""找到最接近目标波长的列索引"""
|
||||
"""找到最接近目标波长的列索引(自动应用波长偏移修正)"""
|
||||
# 应用波长偏移修正
|
||||
adjusted_target = target_wavelength + self.wavelength_offset
|
||||
valid_indices = [i for i, wl in enumerate(self.wavelengths) if wl is not None]
|
||||
if not valid_indices:
|
||||
raise ValueError("未找到有效的波长列")
|
||||
|
||||
# 计算与目标波长的差值
|
||||
differences = [abs(self.wavelengths[i] - target_wavelength) for i in valid_indices]
|
||||
differences = [abs(self.wavelengths[i] - adjusted_target) for i in valid_indices]
|
||||
min_diff_index = np.argmin(differences)
|
||||
closest_index = valid_indices[min_diff_index]
|
||||
closest_wavelength = self.wavelengths[closest_index]
|
||||
|
||||
print(
|
||||
f"目标波长 {target_wavelength}nm -> 最接近波长 {closest_wavelength}nm (列: {self.df.columns[closest_index]})")
|
||||
if abs(self.wavelength_offset) > 0.01:
|
||||
print(
|
||||
f"公式波长 {target_wavelength}nm + 偏移 {self.wavelength_offset}nm → 目标 {adjusted_target}nm → 最接近波段 {closest_wavelength}nm (列: {self.df.columns[closest_index]})")
|
||||
else:
|
||||
print(
|
||||
f"目标波长 {target_wavelength}nm -> 最接近波长 {closest_wavelength}nm (列: {self.df.columns[closest_index]})")
|
||||
return closest_index
|
||||
|
||||
def _parse_expression(self, expression):
|
||||
|
||||
@ -90,13 +90,14 @@ class WaterQualityIndexCalculator:
|
||||
parts = [float(x.strip()) for x in s.split(",")]
|
||||
return np.array(parts)
|
||||
|
||||
def _band_math_all_rows(self, df: pd.DataFrame, expression: str) -> pd.Series:
|
||||
def _band_math_all_rows(self, df: pd.DataFrame, expression: str, wavelength_offset: float = 0.0) -> pd.Series:
|
||||
"""
|
||||
使用 BandMathCalculator 的公式计算引擎,在整个 DataFrame 上批量求值。
|
||||
|
||||
Args:
|
||||
df: 输入光谱数据(列名为 wNNN 格式)
|
||||
expression: 波段计算表达式,如 "(w715 - w686) / (w715 + w686)"
|
||||
wavelength_offset: 波长偏移修正量(nm)
|
||||
|
||||
Returns:
|
||||
pd.Series,与 df 等长的计算结果
|
||||
@ -104,6 +105,7 @@ class WaterQualityIndexCalculator:
|
||||
calc = BandMathCalculator.__new__(BandMathCalculator)
|
||||
calc.df = df.copy()
|
||||
calc.wavelengths = calc._extract_wavelengths()
|
||||
calc.wavelength_offset = float(wavelength_offset)
|
||||
|
||||
variables = calc._parse_expression(expression)
|
||||
results = []
|
||||
@ -126,13 +128,14 @@ class WaterQualityIndexCalculator:
|
||||
|
||||
return pd.Series(results, index=df.index, name=expression)
|
||||
|
||||
def calculate_one(self, name: str, df: pd.DataFrame) -> pd.Series:
|
||||
def calculate_one(self, name: str, df: pd.DataFrame, wavelength_offset: float = 0.0) -> pd.Series:
|
||||
"""
|
||||
计算单个水质指数。
|
||||
|
||||
Args:
|
||||
name: 公式名称(对应 Formula_Name)
|
||||
df: 光谱反射率 DataFrame
|
||||
wavelength_offset: 波长偏移修正量(nm)
|
||||
|
||||
Returns:
|
||||
pd.Series,计算结果
|
||||
@ -145,7 +148,7 @@ class WaterQualityIndexCalculator:
|
||||
ftype = cfg["type"]
|
||||
coeff_str = cfg["coeff"]
|
||||
|
||||
raw = self._band_math_all_rows(df, expr)
|
||||
raw = self._band_math_all_rows(df, expr, wavelength_offset=wavelength_offset)
|
||||
|
||||
if ftype == "concentration":
|
||||
coeff = self._parse_coeff(coeff_str)
|
||||
@ -155,13 +158,14 @@ class WaterQualityIndexCalculator:
|
||||
raw.name = name
|
||||
return raw
|
||||
|
||||
def calculate_many(self, names: List[str], df: pd.DataFrame) -> pd.DataFrame:
|
||||
def calculate_many(self, names: List[str], df: pd.DataFrame, wavelength_offset: float = 0.0) -> pd.DataFrame:
|
||||
"""
|
||||
批量计算多个水质指数。
|
||||
|
||||
Args:
|
||||
names: 公式名称列表
|
||||
df: 光谱反射率 DataFrame
|
||||
wavelength_offset: 波长偏移修正量(nm)
|
||||
|
||||
Returns:
|
||||
pd.DataFrame,每列对应一个公式的计算结果
|
||||
@ -169,7 +173,7 @@ class WaterQualityIndexCalculator:
|
||||
results = {}
|
||||
for name in names:
|
||||
try:
|
||||
results[name] = self.calculate_one(name, df)
|
||||
results[name] = self.calculate_one(name, df, wavelength_offset=wavelength_offset)
|
||||
except Exception as e:
|
||||
print(f"⚠️ 计算 {name} 失败: {e}")
|
||||
results[name] = pd.Series(np.nan, index=df.index, name=name)
|
||||
|
||||
Reference in New Issue
Block a user