Files
WQ_GUI/src/new/services/step3_service.py
DXC 6a962f5e8f feat(new-arch):主窗口全功能增强(图标系统 + 全链路参数同步 + 服务输出统一解析 + Step12 分类浏览)
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)统一视觉。
2026-06-17 13:28:58 +08:00

227 lines
8.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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,
}