1. main_view.py:图标系统 + 全链路参数自动传导
- 新增 _res() 解析项目根的相对路径,PyInstaller 打包后兼容 sys._MEIPASS。
- 新增 QListWidgetItem / QMessageBox 导入,左侧导航列表支持右键菜单 + 错误弹窗。
- ROUTES 12 条全部新增 icon 字段("1.png" 等),侧边栏显示业务图标。
- 新增 step_outputs 缓存机制:每个 step 完成后把 output_path 写入 self.step_outputs。
- 新增 _sync_dependencies() 同步函数 + _safe_set_config() 包装器,
按依赖图把上游产物推给下游 view:
step1 → step6 water_mask_path
step3 → step4 / step6 / step10 deglint_img_path / bsq_path
step4 → step9 sampling_csv_path
step5 → step6 csv_path
step6 → step7 / step8 training_csv_path
step8 → step9 models_dir(父目录)
step9 → step11 prediction_csv_dir / prediction_csv_path(双推)
step10 → step11 geotiff_dir / geotiff_path(双推)
2. services/step1-13:统一输出解析器集成
- 新增 src/new/services/_output_resolver.py,提供 resolve_output_dir /
copy_to_user_path / get_user_output_path / is_user_specified 四个共享工具。
- 每个 service 把原有的私有 _resolve_xxx_dir 改为调用 resolve_output_dir,
强制执行"用户优先"规则(用户指定 output_path 时用其父目录,否则用 work_dir/<subdir>)。
- 用户指定文件名 vs 底层硬编码文件名的"事后劫持"通过 copy_to_user_path 完成
(覆盖 step2、step4、step7、step8 等底层 step 不接受 output_path 关键字的步骤)。
3. views/step12_view.py:恢复 ImageCategoryTree + ImageViewerWidget 高级组件
- 删掉精简版占位 Label,挂回旧版的 ImageCategoryTree(按"模型评估/光谱分析/
统计图表/处理结果/含量分布图"五类自动归类工作目录下的图像文件)。
- 挂回 ImageViewerWidget(滚轮缩放 0.1x-5x + 50ms 防抖 + FastTransformation/
SmoothTransformation 智能切换 + Ctrl+Wheel + 工具栏)。
- 扫描按钮接通 image_tree.scan_directory(),选中节点即时加载到 image_viewer。
- 按钮样式切换为 ModernStylesheet(success/primary)统一视觉。
227 lines
8.5 KiB
Python
227 lines
8.5 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
Step3 后端计算服务(耀斑去除)
|
||
====================================
|
||
|
||
纯计算函数——绝对不引用 PyQt、绝对不引用 main_view、绝对不读写全局变量。它只:
|
||
|
||
1. 从 ``config`` 字典读取参数;
|
||
2. 调用旧版 ``GlintRemovalStep.run`` 执行去耀斑(4 种方法:Goodman/Kutser/Hedley/SUGAR);
|
||
3. 返回结果字典 ``{status, output_path, message, mode}``。
|
||
|
||
调用入口(由 main_view 在后台 QThread 中调用):
|
||
|
||
execute_step3({
|
||
"img_path": "D:/ref.bsq", # 输入影像(去耀斑前)
|
||
"method": "goodman", # goodman/kutser/hedley/sugar
|
||
"enabled": True,
|
||
"interpolate_zeros": False, # 是否先做 0 值像素插值
|
||
"interpolation_method": "bilinear",
|
||
"water_mask_path": "D:/mask.dat", # 水域掩膜(可选)
|
||
"output_path": "D:/deglint_image.bsq",
|
||
# 方法专属参数(按 method 任选一组)
|
||
"nir_lower": 65, "nir_upper": 91, "goodman_A": 1.9e-5, "goodman_B": 0.1, # goodman
|
||
"oxy_band": 38, "lower_oxy": 36, "upper_oxy": 49, "nir_band": 47, # kutser
|
||
"hedley_nir_band": 47, # hedley
|
||
"sugar_iter": 3, "sugar_sigma": 1.0, "sugar_estimate_background": True,
|
||
"sugar_glint_mask_method": "cdf", "sugar_termination_thresh": 20.0,
|
||
"sugar_bounds": [(1, 2)], # sugar
|
||
"work_dir": "D:/workspace", # 工作目录(main_view 注入)
|
||
})
|
||
|
||
返回字典字段:
|
||
|
||
* ``status`` : "completed" | "skipped" | "error"
|
||
* ``output_path`` : 生成的 .bsq 去耀斑影像路径(失败时为 None)
|
||
* ``message`` : 人类可读说明
|
||
* ``mode`` : 实际调用的去耀斑方法名,便于 UI 提示
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
from typing import Any, Dict
|
||
|
||
from src.core.steps.glint_removal_step import GlintRemovalStep
|
||
from src.new.services._output_resolver import (
|
||
copy_to_user_path,
|
||
get_user_output_path,
|
||
is_user_specified,
|
||
resolve_output_dir,
|
||
)
|
||
|
||
|
||
def _resolve_dirs(config: Dict[str, Any], work_dir: str) -> tuple[Path, Path]:
|
||
"""根据 output_path / work_dir 计算 (deglint_dir, water_mask_dir)
|
||
|
||
使用共享解析器强制执行"用户优先"规则——用户指定 output_path 时用其父目录,
|
||
否则用 work_dir/3_Deglint 默认。
|
||
"""
|
||
deglint_dir, _source = resolve_output_dir(
|
||
config, work_dir, "3_Deglint", "output_path", "output_dir"
|
||
)
|
||
water_mask_dir = Path(work_dir) / "1_water_mask"
|
||
return deglint_dir, water_mask_dir
|
||
|
||
|
||
def _normalize_method(method: str) -> str:
|
||
"""方法名标准化(与 GlintRemovalStep.run 内部规则保持一致)"""
|
||
raw = str(method).lower()
|
||
if "kutser" in raw:
|
||
return "kutser"
|
||
if "goodman" in raw:
|
||
return "goodman"
|
||
if "hedley" in raw:
|
||
return "hedley"
|
||
if "sugar" in raw:
|
||
return "sugar"
|
||
return raw
|
||
|
||
|
||
def _build_method_kwargs(method: str, config: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""按 method 从 config 中抽取对应的方法专属参数"""
|
||
if method == "goodman":
|
||
return {
|
||
"nir_lower": int(config.get("nir_lower", 65)),
|
||
"nir_upper": int(config.get("nir_upper", 91)),
|
||
"goodman_A": float(config.get("goodman_A", 0.000019)),
|
||
"goodman_B": float(config.get("goodman_B", 0.1)),
|
||
}
|
||
if method == "kutser":
|
||
return {
|
||
"oxy_band": int(config.get("oxy_band", 38)),
|
||
"lower_oxy": int(config.get("lower_oxy", 36)),
|
||
"upper_oxy": int(config.get("upper_oxy", 49)),
|
||
"nir_band": int(config.get("nir_band", 47)),
|
||
}
|
||
if method == "hedley":
|
||
return {
|
||
"hedley_nir_band": int(config.get("hedley_nir_band", 47)),
|
||
}
|
||
if method == "sugar":
|
||
bounds = config.get("sugar_bounds")
|
||
if bounds is None or not isinstance(bounds, (list, tuple)):
|
||
bounds = [(1, 2)]
|
||
return {
|
||
"sugar_iter": int(config.get("sugar_iter", 3)),
|
||
"sugar_sigma": float(config.get("sugar_sigma", 1.0)),
|
||
"sugar_estimate_background": bool(config.get("sugar_estimate_background", True)),
|
||
"sugar_glint_mask_method": str(config.get("sugar_glint_mask_method", "cdf")),
|
||
"sugar_termination_thresh": float(config.get("sugar_termination_thresh", 20.0)),
|
||
"sugar_bounds": bounds,
|
||
}
|
||
return {}
|
||
|
||
|
||
def execute_step3(config: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""Step 3 后端计算入口——纯函数
|
||
|
||
Args:
|
||
config: 由前端 view.get_config() 序列化、再经 main_view 注入 work_dir 的字典
|
||
|
||
Returns:
|
||
标准结果字典 ``{status, output_path, message, mode}``
|
||
"""
|
||
# ---------- 入参规整 ----------
|
||
img_path = config.get("img_path")
|
||
raw_method = config.get("method", "goodman")
|
||
method = _normalize_method(raw_method)
|
||
enabled = bool(config.get("enabled", True))
|
||
interpolate_zeros = bool(config.get("interpolate_zeros", False))
|
||
interpolation_method = str(config.get("interpolation_method", "bilinear"))
|
||
water_mask_path = config.get("water_mask_path")
|
||
output_path = config.get("output_path")
|
||
work_dir = config.get("work_dir") or "."
|
||
|
||
deglint_dir, water_mask_dir = _resolve_dirs(config, work_dir)
|
||
|
||
# ---------- 提前失败检查 ----------
|
||
if not enabled:
|
||
return {
|
||
"status": "skipped",
|
||
"output_path": img_path,
|
||
"message": "用户禁用此步骤(enabled=False),保留原始影像",
|
||
"mode": method,
|
||
}
|
||
if not img_path:
|
||
return {
|
||
"status": "error",
|
||
"output_path": None,
|
||
"message": "未提供输入影像路径(img_path)",
|
||
"mode": method,
|
||
}
|
||
if not Path(img_path).exists():
|
||
return {
|
||
"status": "error",
|
||
"output_path": None,
|
||
"message": f"输入影像不存在: {img_path}",
|
||
"mode": method,
|
||
}
|
||
|
||
# ---------- 构建底层 kwargs ----------
|
||
# 注意:GlintRemovalStep.run 不接受 output_path 关键字——它只接收 deglint_dir;
|
||
# 用户指定的文件名将通过下文的 copy_to_user_path 事后劫持拷贝。
|
||
method_kwargs = _build_method_kwargs(method, config)
|
||
kwargs: Dict[str, Any] = {
|
||
"img_path": img_path,
|
||
"method": method,
|
||
"water_mask": water_mask_path,
|
||
"interpolate_zeros": interpolate_zeros,
|
||
"interpolation_method": interpolation_method,
|
||
"deglint_dir": deglint_dir,
|
||
"water_mask_dir": water_mask_dir,
|
||
"callback": None, # 日志由 main_view 统一接管
|
||
}
|
||
kwargs.update(method_kwargs)
|
||
|
||
# ---------- 执行(包一层 try/except 把异常转 dict,避免炸线程) ----------
|
||
try:
|
||
result_path = GlintRemovalStep.run(**kwargs)
|
||
except FileNotFoundError as e:
|
||
return {
|
||
"status": "error",
|
||
"output_path": None,
|
||
"message": f"文件不存在: {e}",
|
||
"mode": method,
|
||
}
|
||
except ValueError as e:
|
||
return {
|
||
"status": "error",
|
||
"output_path": None,
|
||
"message": f"参数错误: {e}",
|
||
"mode": method,
|
||
}
|
||
except Exception as e: # noqa: BLE001 —— service 层兜底捕获所有
|
||
return {
|
||
"status": "error",
|
||
"output_path": None,
|
||
"message": f"{type(e).__name__}: {e}",
|
||
"mode": method,
|
||
}
|
||
|
||
# ---------- 成功路径 ----------
|
||
p = Path(result_path)
|
||
if not p.exists():
|
||
return {
|
||
"status": "error",
|
||
"output_path": None,
|
||
"message": f"GlintRemovalStep.run 未生成文件: {result_path}",
|
||
"mode": method,
|
||
}
|
||
|
||
# ---------- 事后劫持:用户指定文件名 vs 底层硬编码文件名 ----------
|
||
# 旧版 GlintRemovalStep.run 只接受 deglint_dir 不接受确切文件名;
|
||
# 用户浏览指定的 .bsq 文件名被底层忽略(同时 .hdr 头文件也按硬编码名生成)。
|
||
# 这里事后把 result_path 拷贝/重命名到 user_path,copy_to_user_path
|
||
# 会自动处理 .hdr / .HDR 伴随文件。
|
||
user_path = config.get("output_path")
|
||
if user_path:
|
||
result_path = copy_to_user_path(result_path, user_path)
|
||
p = Path(result_path)
|
||
|
||
return {
|
||
"status": "completed",
|
||
"output_path": str(p).replace("\\", "/"),
|
||
"message": f"去耀斑影像已生成: {p.name}",
|
||
"mode": method,
|
||
} |