refactor(step4): 剥离 Steps 层 - step1~step3 业务逻辑下沉到独立模块

This commit is contained in:
DXC
2026-05-09 17:30:49 +08:00
parent 605ec86108
commit d0eb458392
5 changed files with 674 additions and 528 deletions

View File

@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
"""业务步骤层模块"""
from src.core.steps.water_mask_step import WaterMaskStep
from src.core.steps.glint_detection_step import GlintDetectionStep
from src.core.steps.glint_removal_step import GlintRemovalStep
__all__ = [
"WaterMaskStep",
"GlintDetectionStep",
"GlintRemovalStep",
]

View File

@ -0,0 +1,113 @@
# -*- coding: utf-8 -*-
"""
步骤2: 耀斑区域检测
支持多种检测方法: otsu, zscore, percentile, iqr, adaptive, multi_band
"""
import time
from pathlib import Path
from typing import Optional, List, Union
class GlintDetectionStep:
"""耀斑区域检测步骤"""
@staticmethod
def run(
img_path: str,
glint_wave: float = 750.0,
method: str = "otsu",
z_threshold: float = 2.5,
percentile: float = 95.0,
iqr_multiplier: float = 1.5,
window_size: int = 15,
multi_band_waves: Optional[List[float]] = None,
sub_method: str = "zscore",
weights: Optional[List[float]] = None,
max_area: Optional[int] = None,
buffer_size: Optional[int] = None,
water_mask_path: Optional[str] = None,
glint_dir: Union[str, Path] = "./2_glint",
callback: Optional[callable] = None,
) -> str:
"""
执行耀斑区域检测
Args:
img_path: 输入影像文件路径
glint_wave: 用于耀斑检测的波段波长nm
method: 检测方法 ('otsu' | 'zscore' | 'percentile' | 'iqr' | 'adaptive' | 'multi_band')
z_threshold: Z-score 方法阈值(默认 2.5
percentile: 百分位数阈值(默认 95.0
iqr_multiplier: IQR 倍数(默认 1.5
window_size: 自适应阈值窗口大小(默认 15
multi_band_waves: 多波段方法的波长列表,如 [750, 800, 850]
sub_method: 多波段方法的子方法(默认 'zscore'
weights: 多波段方法的权重列表None 表示等权重)
max_area: 最大连通域面积阈值(像素),超过则过滤
buffer_size: 岸边缓冲区大小(像素),用于去除岸边附近错误掩膜
water_mask_path: 水域掩膜文件路径dat 格式优先)
glint_dir: 工作目录
callback: 回调函数
Returns:
耀斑掩膜文件路径 (.dat)
"""
from src.utils.find_severe_glint_area import find_severe_glint_area
glint_dir = Path(glint_dir)
glint_dir.mkdir(parents=True, exist_ok=True)
def notify(status, msg=""):
if callback:
callback("步骤2", status, msg)
print("\n" + "=" * 80)
print("步骤2: 找到耀斑区域")
print("=" * 80)
step_start_time = time.time()
# 确定水体掩膜路径
if water_mask_path is not None and Path(water_mask_path).exists():
final_water_mask_path = water_mask_path
else:
final_water_mask_path = None
output_path = str(glint_dir / "severe_glint_area.dat")
# 跳过已存在的文件
if Path(output_path).exists():
print(f"检测到已存在的耀斑掩膜文件,直接使用: {output_path}")
notify("skipped", f"耀斑掩膜已设置: {output_path}")
return output_path
# 构建检测参数字典
kwargs = {
"method": method,
"z_threshold": z_threshold,
"percentile": percentile,
"iqr_multiplier": iqr_multiplier,
"window_size": window_size,
}
if method == "multi_band":
if multi_band_waves is not None:
kwargs["multi_band_waves"] = multi_band_waves
if sub_method is not None:
kwargs["sub_method"] = sub_method
if weights is not None:
kwargs["weights"] = weights
if max_area is not None:
kwargs["max_area"] = max_area
if buffer_size is not None:
kwargs["buffer_size"] = buffer_size
glint_mask_path = find_severe_glint_area(
img_path, final_water_mask_path, glint_wave, output_path, **kwargs
)
print(f"耀斑掩膜已生成: {glint_mask_path}")
print(f"使用检测方法: {method}")
notify("completed", f"耀斑掩膜已生成: {glint_mask_path}")
return glint_mask_path

View File

@ -0,0 +1,314 @@
# -*- coding: utf-8 -*-
"""
步骤3: 去除耀斑
支持多种方法: subtract_nir, regression_slope, oxygen_absorption, kutser, goodman, hedley, sugar
每种方法都会:
1. 准备水域掩膜(支持 shp 自动转 dat
2. 调用对应的算法类执行处理
3. 复制 hdr 文件到输出影像
"""
import os
import time
from pathlib import Path
from typing import Optional, List, Union, Callable
import numpy as np
class GlintRemovalStep:
"""去除耀斑步骤"""
@staticmethod
def run(
img_path: str,
method: str = "subtract_nir",
start_wave: Optional[float] = None,
end_wave: Optional[float] = None,
json_path: Optional[str] = None,
left_shoulder_wave: Optional[float] = None,
valley_wave: Optional[float] = None,
right_shoulder_wave: Optional[float] = None,
water_mask: Optional[Union[str, np.ndarray]] = None,
interpolated_img_path: Optional[str] = None,
interpolate_zeros: bool = False,
interpolation_method: str = "nearest",
enabled: bool = True,
# Kutser 参数
kutser_shp_path: Optional[str] = None,
oxy_band: int = 38,
lower_oxy: int = 36,
upper_oxy: int = 49,
nir_band: int = 47,
# Goodman 参数
nir_lower: int = 25,
nir_upper: int = 37,
goodman_A: float = 0.000019,
goodman_B: float = 0.1,
# Hedley 参数
hedley_shp_path: Optional[str] = None,
hedley_nir_band: int = 47,
# SUGAR 参数
sugar_bounds: Optional[List[tuple]] = None,
sugar_sigma: float = 1.0,
sugar_estimate_background: bool = True,
sugar_glint_mask_method: str = "cdf",
sugar_iter: Optional[int] = 3,
sugar_termination_thresh: float = 20.0,
# 内部工具函数
_get_image_geo_info=None,
_load_image_as_array=None,
_save_bands_as_image=None,
_copy_hdr_info=None,
_prepare_water_mask_for_algorithm=None,
_interpolate_zero_pixels_batch=None,
deglint_dir: Union[str, Path] = "./3_deglint",
water_mask_dir: Union[str, Path] = "./1_water_mask",
callback: Optional[Callable] = None,
) -> str:
"""
执行去除耀斑处理
Args:
img_path: 输入影像文件路径
method: 去耀斑方法
...(其余参数同主类 step3_remove_glint
Returns:
去除耀斑后的影像文件路径
"""
from src.core.glint_removal.Kutser import Kutser
from src.core.glint_removal.Goodman import Goodman
from src.core.glint_removal.Hedley import Hedley
from src.core.glint_removal.SUGAR import SUGAR, correction_iterative
from src.core.utils.gdal_helper import (
get_image_geo_info as _default_get_geo,
load_image_as_array as _default_load,
save_bands_as_image as _default_save_bands,
copy_hdr_info as _default_copy_hdr,
)
from src.core.utils.mask_converter import (
prepare_water_mask_for_algorithm as _default_prepare,
)
# 使用提供的函数或默认函数
if _get_image_geo_info is None:
_get_image_geo_info = _default_get_geo
if _load_image_as_array is None:
_load_image_as_array = _default_load
if _save_bands_as_image is None:
_save_bands_as_image = _default_save_bands
if _copy_hdr_info is None:
_copy_hdr_info = _default_copy_hdr
if _prepare_water_mask_for_algorithm is None:
_prepare_water_mask_for_algorithm = _default_prepare
deglint_dir = Path(deglint_dir)
deglint_dir.mkdir(parents=True, exist_ok=True)
def notify(status, msg=""):
if callback:
callback("步骤3", status, msg)
print("\n" + "=" * 80)
print("步骤3: 去除耀斑")
print("=" * 80)
step_start_time = time.time()
# 方法名标准化
raw_method = str(method).lower()
if "kutser" in raw_method:
method = "kutser"
elif "goodman" in raw_method:
method = "goodman"
elif "hedley" in raw_method:
method = "hedley"
elif "sugar" in raw_method:
method = "sugar"
# 如果未启用,直接返回原始影像
if not enabled:
print("已设置跳过去除耀斑enabled=False将直接使用原始影像。")
notify("skipped", "跳过去耀斑,使用原始影像")
return img_path
# ---- 确定水域掩膜 ----
final_water_mask = water_mask
if final_water_mask is not None and str(final_water_mask).lower().endswith(".shp"):
# shp 自动替换为 dat
dat_mask = str(Path(water_mask_dir) / "water_mask_from_shp.dat")
if Path(dat_mask).exists():
print(f"检测到输入掩膜为 .shp自动替换为栅格掩膜: {dat_mask}")
final_water_mask = dat_mask
if final_water_mask is None:
dat_mask_default = str(Path(water_mask_dir) / "water_mask_from_shp.dat")
if Path(dat_mask_default).exists():
final_water_mask = dat_mask_default
print(f"使用步骤1生成的水域掩膜: {final_water_mask}")
# ---- 步骤3.1: 0值像素插值 ----
if interpolate_zeros:
print("\n" + "-" * 80)
print("步骤3.1: 对0值像素进行插值")
print("-" * 80)
interp_start_time = time.time()
if _interpolate_zero_pixels_batch is None:
from src.core.algorithms.interpolation.interpolator import (
interpolate_zero_pixels_batch as _interp_batch,
)
_interpolate_zero_pixels_batch = _interp_batch
interp_result, _ = _interpolate_zero_pixels_batch(
img_path=img_path,
interpolation_method=interpolation_method,
output_path=None,
water_mask=final_water_mask,
deglint_dir=str(deglint_dir),
callback_progress=lambda msg: print(f" {msg}"),
)
img_path = interp_result
interp_end_time = time.time()
print(f"插值完成,使用插值后的影像: {img_path}")
# ---- 获取影像信息 ----
geotransform, projection, width, height, n_bands = _get_image_geo_info(img_path)
print(f"影像尺寸: {width} x {height} x {n_bands}")
mask_for_algorithm = _prepare_water_mask_for_algorithm(
final_water_mask, (height, width), geotransform, projection, img_path
)
# ==================== Kutser ====================
if method == "kutser":
print(f"使用方法: Kutser (氧吸收波段={oxy_band}, NIR波段={nir_band})")
output_path = str(deglint_dir / "deglint_kutser.bsq")
if Path(output_path).exists():
print(f"检测到已存在的去耀斑影像文件,直接使用: {output_path}")
notify("skipped", f"去耀斑影像已设置: {output_path}")
return output_path
kutser = Kutser(
img_path,
shp_path=None,
oxy_band=oxy_band,
lower_oxy=lower_oxy,
upper_oxy=upper_oxy,
NIR_band=nir_band,
water_mask=mask_for_algorithm,
output_path=output_path,
)
kutser.get_corrected_bands()
if Path(output_path).exists():
_copy_hdr_info(img_path, output_path)
notify("completed", f"去耀斑影像已生成: {output_path}")
return output_path
raise RuntimeError(f"Kutser算法未生成输出文件: {output_path}")
# ==================== Goodman ====================
elif method == "goodman":
print(f"使用方法: Goodman (NIR波段范围: {nir_lower}-{nir_upper})")
output_path = str(deglint_dir / "deglint_goodman.bsq")
if Path(output_path).exists():
print(f"检测到已存在的去耀斑影像文件,直接使用: {output_path}")
notify("skipped", f"去耀斑影像已设置: {output_path}")
return output_path
goodman = Goodman(
img_path,
NIR_lower=nir_lower,
NIR_upper=nir_upper,
A=goodman_A,
B=goodman_B,
water_mask=mask_for_algorithm,
output_path=output_path,
)
corrected_bands = goodman.get_corrected_bands()
if not Path(output_path).exists():
_save_bands_as_image(corrected_bands, output_path, geotransform, projection)
_copy_hdr_info(img_path, output_path)
else:
_copy_hdr_info(img_path, output_path)
del corrected_bands
notify("completed", f"去耀斑影像已生成: {output_path}")
return output_path
# ==================== Hedley ====================
elif method == "hedley":
print(f"使用方法: Hedley (NIR波段={hedley_nir_band})")
output_path = str(deglint_dir / "deglint_hedley.bsq")
if Path(output_path).exists():
print(f"检测到已存在的去耀斑影像文件,直接使用: {output_path}")
notify("skipped", f"去耀斑影像已设置: {output_path}")
return output_path
hedley = Hedley(
img_path,
shp_path=None,
NIR_band=hedley_nir_band,
water_mask=mask_for_algorithm,
output_path=output_path,
)
hedley.get_corrected_bands()
if Path(output_path).exists():
_copy_hdr_info(img_path, output_path)
notify("completed", f"去耀斑影像已生成: {output_path}")
return output_path
raise RuntimeError(f"Hedley算法未生成输出文件: {output_path}")
# ==================== SUGAR ====================
elif method == "sugar":
# 方法名标准化
glint_method_raw = str(sugar_glint_mask_method).lower()
if "cdf" in glint_method_raw or "累积" in glint_method_raw:
sugar_glint_mask_method_fixed = "cdf"
elif "otsu" in glint_method_raw or "大津" in glint_method_raw:
sugar_glint_mask_method_fixed = "otsu"
else:
sugar_glint_mask_method_fixed = "cdf"
print(
f"使用方法: SUGAR (迭代次数={sugar_iter}, 掩膜方法={sugar_glint_mask_method_fixed})"
)
output_path = str(deglint_dir / "deglint_sugar.bsq")
if Path(output_path).exists():
print(f"检测到已存在的去耀斑影像文件,直接使用: {output_path}")
notify("skipped", f"去耀斑影像已设置: {output_path}")
return output_path
if sugar_bounds is None:
sugar_bounds = [(1, 2)]
correction_iterative(
img_path,
iter=sugar_iter,
bounds=sugar_bounds,
estimate_background=sugar_estimate_background,
glint_mask_method=sugar_glint_mask_method_fixed,
termination_thresh=sugar_termination_thresh,
water_mask=mask_for_algorithm,
output_path=output_path,
)
if Path(output_path).exists():
_copy_hdr_info(img_path, output_path)
notify("completed", f"去耀斑影像已生成: {output_path}")
return output_path
raise RuntimeError(f"SUGAR算法未生成输出文件: {output_path}")
else:
raise ValueError(
f"不支持的方法: {method}。支持的方法: kutser, goodman, hedley, sugar"
)

View File

@ -0,0 +1,148 @@
# -*- coding: utf-8 -*-
"""
步骤1: 水域掩膜生成
支持三种方式:
1. 基于 shp 文件栅格化
2. 使用现有栅格格式掩膜文件 (.dat/.tif)
3. 基于 NDWI 从影像自动生成水体掩膜
"""
import os
import time
from pathlib import Path
from typing import Optional, List, Callable, Union
import numpy as np
class WaterMaskStep:
"""水域掩膜生成步骤"""
@staticmethod
def run(
mask_path: Optional[str] = None,
img_path: Optional[str] = None,
ndwi_threshold: float = 0.4,
use_ndwi: bool = False,
generate_png: bool = True,
output_path: Optional[str] = None,
water_mask_dir: Union[str, Path] = "./1_water_mask",
callback: Optional[Callable] = None,
) -> str:
"""
执行水域掩膜生成
Args:
mask_path: 水体掩膜文件路径,支持 .shp需 img_path或 .dat/.tif直接使用
img_path: 输入影像文件路径(当 mask_path 为 shp 或 use_ndwi=True 时必须提供)
ndwi_threshold: NDWI 阈值use_ndwi=True 时使用)
use_ndwi: 是否使用 NDWI 方法从影像生成水体掩膜
generate_png: 是否生成 PNG 预览图(默认 True
output_path: 指定输出掩膜文件的保存路径(可选)
water_mask_dir: 工作目录
callback: 回调函数,签名为 callback(step, status, message)
Returns:
dat 格式的水域掩膜文件路径
"""
from src.utils.extract_water_area import rasterize_shp, ndwi
from src.core.utils.preview_generator import (
generate_image_preview,
generate_water_mask_overlay,
)
water_mask_dir = Path(water_mask_dir)
water_mask_dir.mkdir(parents=True, exist_ok=True)
def notify(status, msg=""):
if callback:
callback("步骤1", status, msg)
print("\n" + "=" * 80)
print("步骤1: 生成或设置水域mask")
print("=" * 80)
step_start_time = time.time()
# 生成影像预览图
if generate_png and img_path is not None and Path(img_path).exists():
preview_path = str(water_mask_dir / "hsi_preview.png")
generate_image_preview(
img_path=img_path,
output_path=preview_path,
title="影像预览: RGB波段(基于波长)"
)
# ---- NDWI 方法 ----
if use_ndwi:
if img_path is None:
raise ValueError("当 use_ndwi=True 时,必须提供 img_path 参数")
if not Path(img_path).exists():
raise ValueError(f"影像文件不存在: {img_path}")
print(f"使用NDWI方法从影像生成水体掩膜阈值={ndwi_threshold}...")
ndwi_output_path = output_path or str(water_mask_dir / "water_mask_from_ndwi.dat")
os.makedirs(Path(ndwi_output_path).parent, exist_ok=True)
if Path(ndwi_output_path).exists():
print(f"检测到已存在的NDWI掩膜文件直接使用: {ndwi_output_path}")
notify("skipped", f"水域掩膜已设置: {ndwi_output_path}")
return ndwi_output_path
ndwi(img_path, ndwi_threshold, ndwi_output_path)
if generate_png:
overlay_path = water_mask_dir / "water_mask_overlay.png"
generate_water_mask_overlay(
img_path=img_path, mask_path=ndwi_output_path, output_path=str(overlay_path)
)
notify("completed", f"NDWI水体掩膜已生成: {ndwi_output_path}")
return ndwi_output_path
# ---- 必须提供 mask_path ----
if mask_path is None:
raise ValueError("必须提供 mask_path 参数或设置 use_ndwi=True")
if not Path(mask_path).exists():
raise ValueError(f"文件不存在: {mask_path}")
file_ext = Path(mask_path).suffix.lower()
# ---- SHP 栅格化 ----
if file_ext == ".shp":
if img_path is None:
raise ValueError("当 mask_path 为 shp 格式时,必须提供 img_path 参数")
print(f"检测到shp格式的水体掩膜正在转换为dat格式...")
shp_output_path = output_path or str(water_mask_dir / "water_mask_from_shp.dat")
os.makedirs(Path(shp_output_path).parent, exist_ok=True)
if Path(shp_output_path).exists():
print(f"检测到已存在的栅格化掩膜文件,直接使用: {shp_output_path}")
notify("skipped", f"水域掩膜已设置: {shp_output_path}")
if generate_png:
overlay_path = water_mask_dir / "water_mask_overlay.png"
if not overlay_path.exists():
generate_water_mask_overlay(img_path, shp_output_path, str(overlay_path))
return shp_output_path
safe_mask_path = os.path.abspath(mask_path).replace("\\", "/")
rasterize_shp(safe_mask_path, shp_output_path, img_path)
if generate_png:
overlay_path = water_mask_dir / "water_mask_overlay.png"
generate_water_mask_overlay(img_path, shp_output_path, str(overlay_path))
notify("completed", f"dat格式水域掩膜已生成: {shp_output_path}")
return shp_output_path
# ---- 栅格格式直接使用 ----
print(f"检测到栅格格式的水体掩膜,直接使用: {mask_path}")
if generate_png and img_path is not None and Path(img_path).exists():
overlay_path = water_mask_dir / "water_mask_overlay.png"
generate_water_mask_overlay(img_path, mask_path, str(overlay_path))
notify("completed", f"水域掩膜已设置: {mask_path}")
return mask_path

View File

@ -71,6 +71,10 @@ from src.core.utils.preview_generator import (
)
# 导入算法层模块
from src.core.algorithms.interpolation.interpolator import interpolate_zero_pixels_batch as _interpolate_zero_pixels_batch
# 导入业务步骤模块
from src.core.steps.water_mask_step import WaterMaskStep
from src.core.steps.glint_detection_step import GlintDetectionStep
from src.core.steps.glint_removal_step import GlintRemovalStep
# 导入新的耀斑去除算法
from src.core.glint_removal.Kutser import Kutser
from src.core.glint_removal.Goodman import Goodman
@ -260,164 +264,29 @@ class WaterQualityInversionPipeline:
skip_dependency_check: bool = False,
generate_png: bool = True,
output_path: Optional[str] = None) -> str:
"""
步骤1: 生成或设置水域mask
支持三种方式生成水域掩膜:
1. 基于shp文件栅格化
2. 使用现有的栅格格式掩膜文件
3. 基于NDWI从影像自动生成水体掩膜
当提供img_path时会自动生成PNG预览图基于波长选择RGB波段
- 红波段: ~650nm
- 绿波段: ~550nm
- 蓝波段: ~460nm
Args:
mask_path: 水体掩膜文件路径,支持:
- shp格式文件.shp需要提供img_path用于栅格化
- dat格式文件.dat/.tif等栅格格式直接使用不需要img_path
- None当use_ndwi=True时从影像生成NDWI掩膜
img_path: 输入影像文件路径当mask_path为shp格式或use_ndwi=True时必须提供
ndwi_threshold: NDWI阈值当use_ndwi=True时使用
use_ndwi: 是否使用NDWI方法从影像生成水体掩膜
generate_png: 是否生成输入影像的PNG预览图默认True
output_path: 指定输出掩膜文件的保存路径(可选)。如果提供,掩膜将保存到此路径;
如果为None则使用默认路径self.water_mask_dir
Returns:
dat格式的水域掩膜文件路径
"""
print("\n" + "="*80)
print("步骤1: 生成或设置水域mask")
print("="*80)
"""步骤1: 生成或设置水域maskFacade"""
step_start_time = time.time()
try:
# 如果提供了img_path且开启生成PNG功能生成影像预览图
if generate_png and img_path is not None and Path(img_path).exists():
self._generate_image_preview(img_path)
if use_ndwi:
# 使用NDWI方法从影像生成水体掩膜
if img_path is None:
raise ValueError("当use_ndwi=True时必须提供img_path参数用于生成NDWI掩膜")
if not Path(img_path).exists():
raise ValueError(f"影像文件不存在: {img_path}")
print(f"使用NDWI方法从影像生成水体掩膜阈值={ndwi_threshold}...")
# 使用用户指定的输出路径,或使用默认路径
if output_path:
ndwi_output_path = output_path
# 确保输出目录存在
os.makedirs(Path(output_path).parent, exist_ok=True)
else:
ndwi_output_path = str(self.water_mask_dir / "water_mask_from_ndwi.dat")
# 检查文件是否已存在,避免重复生成
if Path(ndwi_output_path).exists():
print(f"检测到已存在的NDWI掩膜文件直接使用: {ndwi_output_path}")
self.water_mask_path = ndwi_output_path
step_end_time = time.time()
self._record_step_time("步骤1: 生成水域mask", step_start_time, step_end_time, status="skipped")
print(f"水域掩膜已设置: {self.water_mask_path}")
# 生成水域掩膜叠加图(如果不存在)
overlay_path = self.water_mask_dir / "water_mask_overlay.png"
if generate_png and img_path is not None and Path(img_path).exists() and not overlay_path.exists():
self._generate_water_mask_overlay(img_path, self.water_mask_path)
return self.water_mask_path
# 执行NDWI水体提取
from src.utils.extract_water_area import ndwi
ndwi(img_path, ndwi_threshold, ndwi_output_path)
self.water_mask_path = ndwi_output_path
step_end_time = time.time()
self._record_step_time("步骤1: 生成水域mask", step_start_time, step_end_time)
print(f"已生成NDWI水体掩膜: {self.water_mask_path}")
# 生成水域掩膜叠加图
if generate_png:
self._generate_water_mask_overlay(img_path, self.water_mask_path)
return self.water_mask_path
elif mask_path is None:
raise ValueError("必须提供mask_path参数或设置use_ndwi=True")
if not Path(mask_path).exists():
raise ValueError(f"文件不存在: {mask_path}")
# 检查文件扩展名判断是shp文件还是dat文件
file_ext = Path(mask_path).suffix.lower()
if file_ext == '.shp':
# 如果是shp文件需要栅格化为dat
if img_path is None:
raise ValueError("当mask_path为shp格式时必须提供img_path参数用于栅格化")
print(f"检测到shp格式的水体掩膜正在转换为dat格式...")
# 使用用户指定的输出路径,或使用默认路径
if output_path:
shp_output_path = output_path
# 确保输出目录存在
os.makedirs(Path(output_path).parent, exist_ok=True)
else:
shp_output_path = str(self.water_mask_dir / "water_mask_from_shp.dat")
# 检查文件是否已存在,避免重复栅格化
if Path(shp_output_path).exists():
print(f"检测到已存在的栅格化掩膜文件,直接使用: {shp_output_path}")
self.water_mask_path = shp_output_path
step_end_time = time.time()
self._record_step_time("步骤1: 生成水域mask", step_start_time, step_end_time, status="skipped")
print(f"水域掩膜已设置: {self.water_mask_path}")
# 生成水域掩膜叠加图(如果不存在)
overlay_path = self.water_mask_dir / "water_mask_overlay.png"
if generate_png and img_path is not None and Path(img_path).exists() and not overlay_path.exists():
self._generate_water_mask_overlay(img_path, self.water_mask_path)
return self.water_mask_path
# 执行栅格化
from src.utils.extract_water_area import rasterize_shp
safe_mask_path = os.path.abspath(mask_path).replace('\\', '/')
rasterize_shp(safe_mask_path, shp_output_path, img_path)
self.water_mask_path = shp_output_path
step_end_time = time.time()
self._record_step_time("步骤1: 生成水域mask", step_start_time, step_end_time)
print(f"已生成dat格式的水域掩膜: {self.water_mask_path}")
# 生成水域掩膜叠加图
if generate_png:
self._generate_water_mask_overlay(img_path, self.water_mask_path)
return self.water_mask_path
else:
# 如果是dat或其他栅格格式直接使用
print(f"检测到栅格格式的水体掩膜,直接使用: {mask_path}")
self.water_mask_path = mask_path
step_end_time = time.time()
self._record_step_time("步骤1: 生成水域mask", step_start_time, step_end_time)
print(f"水域掩膜已设置: {self.water_mask_path} (dat格式)")
# 生成水域掩膜叠加图
if generate_png and img_path is not None and Path(img_path).exists():
self._generate_water_mask_overlay(img_path, self.water_mask_path)
return self.water_mask_path
result = WaterMaskStep.run(
mask_path=mask_path,
img_path=img_path,
ndwi_threshold=ndwi_threshold,
use_ndwi=use_ndwi,
generate_png=generate_png,
output_path=output_path,
water_mask_dir=str(self.water_mask_dir),
callback=self._notify,
)
self.water_mask_path = result
step_end_time = time.time()
self._record_step_time("步骤1: 生成水域mask", step_start_time, step_end_time)
return result
except Exception as e:
step_end_time = time.time()
self._record_step_time("步骤1: 生成水域mask", step_start_time, step_end_time,
self._record_step_time("步骤1: 生成水域mask", step_start_time, step_end_time,
status="failed", error=str(e))
raise
def _generate_image_preview(self, img_path: str, bands: Optional[List[int]] = None) -> str:
"""生成影像预览图,转发至工具模块"""
output_path = str(self.water_mask_dir / f"hsi_preview.png")
@ -520,106 +389,36 @@ class WaterQualityInversionPipeline:
buffer_size: Optional[int] = None,
water_mask_path: Optional[str] = None,
skip_dependency_check: bool = False) -> str:
"""
步骤2: 找到耀斑区域
Args:
img_path: 输入影像文件路径
glint_wave: 用于提取耀斑严重区域的波段波长(单波段方法使用)
method: 检测方法,可选:
- 'otsu': Otsu阈值分割默认
- 'zscore': Z-score统计方法
- 'percentile': 百分位数阈值方法
- 'iqr': IQR异常值检测
- 'adaptive': 自适应阈值方法
- 'multi_band': 多波段融合方法
z_threshold: Z-score方法的阈值默认2.5
percentile: 百分位数阈值默认95.0
iqr_multiplier: IQR倍数默认1.5
window_size: 自适应阈值窗口大小默认15
multi_band_waves: 多波段方法的波长列表,如[750, 800, 850]
sub_method: 多波段方法的子方法('zscore', 'percentile', 'otsu'),默认'zscore'
weights: 多波段方法的权重列表如果为None则使用等权重
max_area: 最大连通域面积阈值(像素数),超过此面积的连通域将被过滤掉,
用于去除岸边、浅水、水华等大面积区域默认None表示不过滤
buffer_size: 岸边缓冲区大小(像素数),用于去除岸边附近的错误耀斑掩膜
默认None表示不进行岸边缓冲区去除设置为正整数时启用
Returns:
耀斑掩膜文件路径
"""
print("\n" + "="*80)
print("步骤2: 找到耀斑区域")
print("="*80)
"""步骤2: 找到耀斑区域Facade"""
step_start_time = time.time()
try:
# 使用dat格式的水体掩膜
if water_mask_path is not None:
# 优先使用传入的参数
final_water_mask_path = water_mask_path
elif self.water_mask_path is not None:
# 其次使用步骤1生成的水体掩膜
final_water_mask_path = self.water_mask_path
else:
# 如果没有水体掩膜根据skip_dependency_check决定行为
if skip_dependency_check:
print("警告: 未找到水体掩膜,将对全图进行耀斑检测")
final_water_mask_path = None
else:
raise ValueError("请先执行步骤1: 生成水域mask或提供water_mask_path参数或设置skip_dependency_check=True")
output_path = str(self.glint_dir / "severe_glint_area.dat")
# 检查文件是否已存在
if Path(output_path).exists():
print(f"检测到已存在的耀斑掩膜文件,直接使用: {output_path}")
self.glint_mask_path = output_path
step_end_time = time.time()
self._record_step_time("步骤2: 找到耀斑区域", step_start_time, step_end_time, status="skipped")
print(f"耀斑掩膜已设置: {self.glint_mask_path}")
return self.glint_mask_path
# 构建参数字典
kwargs = {
'method': method,
'z_threshold': z_threshold,
'percentile': percentile,
'iqr_multiplier': iqr_multiplier,
'window_size': window_size,
}
# 如果是多波段方法,添加相关参数
if method == 'multi_band':
if multi_band_waves is not None:
kwargs['multi_band_waves'] = multi_band_waves
if sub_method is not None:
kwargs['sub_method'] = sub_method
if weights is not None:
kwargs['weights'] = weights
# 添加连通域面积过滤和岸边缓冲区参数
if max_area is not None:
kwargs['max_area'] = max_area
if buffer_size is not None:
kwargs['buffer_size'] = buffer_size
# 传递dat格式的水体掩膜文件路径
self.glint_mask_path = find_severe_glint_area(
img_path, final_water_mask_path, glint_wave, output_path, **kwargs
result = GlintDetectionStep.run(
img_path=img_path,
glint_wave=glint_wave,
method=method,
z_threshold=z_threshold,
percentile=percentile,
iqr_multiplier=iqr_multiplier,
window_size=window_size,
multi_band_waves=multi_band_waves,
sub_method=sub_method,
weights=weights,
max_area=max_area,
buffer_size=buffer_size,
water_mask_path=water_mask_path,
glint_dir=str(self.glint_dir),
callback=self._notify,
)
self.glint_mask_path = result
step_end_time = time.time()
self._record_step_time("步骤2: 找到耀斑区域", step_start_time, step_end_time)
print(f"耀斑掩膜已生成: {self.glint_mask_path}")
print(f"使用检测方法: {method}")
return self.glint_mask_path
return result
except Exception as e:
step_end_time = time.time()
self._record_step_time("步骤2: 找到耀斑区域", step_start_time, step_end_time,
self._record_step_time("步骤2: 找到耀斑区域", step_start_time, step_end_time,
status="failed", error=str(e))
raise
def _get_image_geo_info(self, img_path: str) -> tuple:
"""获取影像地理信息,转发至工具模块"""
return _get_image_geo_info(img_path)
@ -708,28 +507,21 @@ class WaterQualityInversionPipeline:
left_shoulder_wave: Optional[float] = None,
valley_wave: Optional[float] = None,
right_shoulder_wave: Optional[float] = None,
# 水域掩膜参数
water_mask: Optional[Union[str, np.ndarray]] = None,
# 0值像素插值参数
interpolate_zeros: bool = False,
interpolation_method: str = 'nearest',
# 是否执行去除耀斑
enabled: bool = True,
# Kutser方法参数
kutser_shp_path: Optional[str] = None,
oxy_band: int = 38,
lower_oxy: int = 36,
upper_oxy: int = 49,
nir_band: int = 47,
# Goodman方法参数
nir_lower: int = 25,
nir_upper: int = 37,
goodman_A: float = 0.000019,
goodman_B: float = 0.1,
# Hedley方法参数
hedley_shp_path: Optional[str] = None,
hedley_nir_band: int = 47,
# SUGAR方法参数
sugar_bounds: Optional[List[tuple]] = None,
sugar_sigma: float = 1.0,
sugar_estimate_background: bool = True,
@ -737,292 +529,59 @@ class WaterQualityInversionPipeline:
sugar_iter: Optional[int] = 3,
sugar_termination_thresh: float = 20.0,
skip_dependency_check: bool = False) -> str:
"""
步骤3: 去除耀斑
Args:
img_path: 输入影像文件路径
method: 去耀斑方法,支持:
- "subtract_nir": 减去NIR方法
- "regression_slope": 回归斜率方法
- "oxygen_absorption": 氧吸收谷方法
- "kutser": Kutser方法基于氧吸收特征
- "goodman": Goodman方法
- "hedley": Hedley方法基于NIR相关性
- "sugar": SUGAR方法迭代去耀斑
start_wave: 起始波长subtract_nir和regression_slope方法需要
end_wave: 结束波长subtract_nir和regression_slope方法需要
json_path: ROI JSON文件路径regression_slope方法需要
left_shoulder_wave: 左肩波长oxygen_absorption方法需要
valley_wave: 谷值波长oxygen_absorption方法需要
right_shoulder_wave: 右肩波长oxygen_absorption方法需要
water_mask: 水域掩膜,可以是:
- None: 自动使用步骤1生成的水域掩膜如果存在
- numpy数组: 直接使用数组作为掩膜
- 文件路径: 栅格文件路径(.dat/.tif)或shapefile路径(.shp)
如果为None且步骤1未生成掩膜则处理全图
interpolate_zeros: 是否对0值像素进行插值默认False
interpolation_method: 插值方法,支持:
- 'nearest': 邻近插值(最快)
- 'bilinear': 双线性插值
- 'spline': 样条插值RBF
- 'kriging': 克里金插值(最慢但最准确)
# Kutser方法参数
kutser_shp_path: 深水区域shp文件路径可选已废弃请使用water_mask
oxy_band: 氧吸收波段索引默认38对应760.6nm
lower_oxy: 氧吸收下波段索引默认36对应742.39nm
upper_oxy: 氧吸收上波段索引默认49对应860.48nm
nir_band: NIR波段索引默认47对应842.36nm
# Goodman方法参数
nir_lower: NIR下波段索引默认25对应641.93nm
nir_upper: NIR上波段索引默认37对应751.49nm
goodman_A: Goodman参数A默认0.000019
goodman_B: Goodman参数B默认0.1
# Hedley方法参数
hedley_shp_path: 深水区域shp文件路径可选已废弃请使用water_mask
hedley_nir_band: NIR波段索引默认47对应842.36nm
# SUGAR方法参数
sugar_bounds: 优化边界列表,如[(1,2)]默认None使用[(1,2)]
sugar_sigma: LoG平滑sigma默认1.0
sugar_estimate_background: 是否估计背景光谱默认True
sugar_glint_mask_method: 耀斑掩膜方法,"cdf""otsu"(默认"cdf"
sugar_iter: 迭代次数None表示自动终止默认3
sugar_termination_thresh: 终止阈值默认20.0
Returns:
去除耀斑后的影像文件路径
"""
print("\n" + "="*80)
print("步骤3: 去除耀斑")
print("="*80)
"""步骤3: 去除耀斑Facade"""
step_start_time = time.time()
try:
# 兼容中文和各种格式
raw_method = str(method).lower()
if 'kutser' in raw_method:
method = 'kutser'
elif 'goodman' in raw_method:
method = 'goodman'
elif 'hedley' in raw_method:
method = 'hedley'
elif 'sugar' in raw_method:
method = 'sugar'
# 其余方法subtract_nir, regression_slope, oxygen_absorption保持原值
# 如果未启用,直接跳过处理并把原始影像路径作为后续流程输入
if not enabled:
print("已设置跳过去除耀斑enabled=False将直接使用原始影像。")
self.deglint_img_path = img_path
step_end_time = time.time()
self._record_step_time("步骤3: 去除耀斑", step_start_time, step_end_time, status="skipped")
return self.deglint_img_path
# 确定使用的水域掩膜
final_water_mask = water_mask
# 【新增智能转换逻辑】:如果 GUI 传入的是 .shp且第一步已经生成了 .dat 掩膜,强制替换为栅格 .dat
if final_water_mask is not None and str(final_water_mask).lower().endswith('.shp'):
if self.water_mask_path is not None and Path(self.water_mask_path).exists():
print(f"检测到输入掩膜为 .shp 矢量文件自动替换为步骤1生成的栅格掩膜: {self.water_mask_path}")
final_water_mask = self.water_mask_path
# 优先级处理
if final_water_mask is None:
if self.water_mask_path is not None:
final_water_mask = self.water_mask_path
print(f"使用步骤1生成的水域掩膜: {final_water_mask}")
else:
print("未提供水域掩膜,将处理全图")
final_water_mask = None
# 步骤3.1: 对0值像素进行插值如果启用
if interpolate_zeros:
print("\n" + "-"*80)
print("步骤3.1: 对0值像素进行插值")
print("-"*80)
interp_start_time = time.time()
try:
# 准备水域掩膜用于插值
interp_water_mask = final_water_mask
if interp_water_mask is None and self.water_mask_path:
interp_water_mask = self.water_mask_path
# 执行插值
interpolated_img = self._interpolate_zero_pixels(
img_path=img_path,
interpolation_method=interpolation_method,
water_mask=interp_water_mask
)
# 使用插值后的影像作为后续处理的输入
img_path = interpolated_img
interp_end_time = time.time()
self._record_step_time("步骤3.1: 0值像素插值", interp_start_time, interp_end_time)
print(f"插值完成,使用插值后的影像: {img_path}")
except Exception as e:
print(f"警告: 0值像素插值失败: {e},将使用原始影像继续处理")
interp_end_time = time.time()
self._record_step_time("步骤3.1: 0值像素插值", interp_start_time, interp_end_time,
status="failed", error=str(e))
if method == "kutser":
print(f"使用方法: Kutser (氧吸收波段={oxy_band}, NIR波段={nir_band})")
output_path = str(self.deglint_dir / "deglint_kutser.bsq")
bsq_path = output_path if output_path.endswith('.bsq') else output_path.replace('.dat', '.bsq').replace(
'.tif', '.bsq')
if Path(bsq_path).exists() or Path(output_path).exists():
existing_path = bsq_path if Path(bsq_path).exists() else output_path
print(f"检测到已存在的去耀斑影像文件,直接使用: {existing_path}")
self.deglint_img_path = existing_path
self._record_step_time("步骤3: 去除耀斑", step_start_time, time.time(), status="skipped")
print(f"去耀斑影像已设置: {self.deglint_img_path}")
return self.deglint_img_path
geotransform, projection, width, height, n_bands = self._get_image_geo_info(img_path)
print(f"影像尺寸: {width} x {height} x {n_bands}")
mask_for_algorithm = self._prepare_water_mask_for_algorithm(
final_water_mask, (height, width), geotransform, projection, img_path
)
kutser = Kutser(img_path, shp_path=None, oxy_band=oxy_band,
lower_oxy=lower_oxy, upper_oxy=upper_oxy,
NIR_band=nir_band, water_mask=mask_for_algorithm,
output_path=output_path)
kutser.get_corrected_bands()
if Path(bsq_path).exists() or Path(output_path).exists():
self.deglint_img_path = bsq_path if Path(bsq_path).exists() else output_path
self._copy_hdr_info(img_path, self.deglint_img_path)
else:
raise RuntimeError(f"Kutser算法未生成输出文件: {bsq_path}")
elif method == "goodman":
print(f"使用方法: Goodman (NIR波段范围: {nir_lower}-{nir_upper})")
output_path = str(self.deglint_dir / "deglint_goodman.bsq")
bsq_path = output_path if output_path.endswith('.bsq') else output_path.replace('.dat', '.bsq').replace(
'.tif', '.bsq')
if Path(bsq_path).exists() or Path(output_path).exists():
existing_path = bsq_path if Path(bsq_path).exists() else output_path
print(f"检测到已存在的去耀斑影像文件,直接使用: {existing_path}")
self.deglint_img_path = existing_path
self._record_step_time("步骤3: 去除耀斑", step_start_time, time.time(), status="skipped")
return self.deglint_img_path
geotransform, projection, width, height, n_bands = self._get_image_geo_info(img_path)
mask_for_algorithm = self._prepare_water_mask_for_algorithm(
final_water_mask, (height, width), geotransform, projection, img_path
)
image_array, geotransform, projection = self._load_image_as_array(img_path)
goodman = Goodman(img_path, NIR_lower=nir_lower, NIR_upper=nir_upper,
A=goodman_A, B=goodman_B, water_mask=mask_for_algorithm,
output_path=output_path)
corrected_bands = goodman.get_corrected_bands()
if not Path(bsq_path).exists() and not Path(output_path).exists():
self._save_bands_as_image(corrected_bands, output_path, geotransform, projection)
self.deglint_img_path = output_path
self._copy_hdr_info(img_path, output_path)
else:
self.deglint_img_path = bsq_path if Path(bsq_path).exists() else output_path
self._copy_hdr_info(img_path, self.deglint_img_path)
del corrected_bands
elif method == "hedley":
print(f"使用方法: Hedley (NIR波段={hedley_nir_band})")
output_path = str(self.deglint_dir / "deglint_hedley.bsq")
bsq_path = output_path if output_path.endswith('.bsq') else output_path.replace('.dat', '.bsq').replace(
'.tif', '.bsq')
if Path(bsq_path).exists() or Path(output_path).exists():
existing_path = bsq_path if Path(bsq_path).exists() else output_path
print(f"检测到已存在的去耀斑影像文件,直接使用: {existing_path}")
self.deglint_img_path = existing_path
self._record_step_time("步骤3: 去除耀斑", step_start_time, time.time(), status="skipped")
return self.deglint_img_path
geotransform, projection, width, height, n_bands = self._get_image_geo_info(img_path)
mask_for_algorithm = self._prepare_water_mask_for_algorithm(
final_water_mask, (height, width), geotransform, projection, img_path
)
hedley = Hedley(img_path, shp_path=None, NIR_band=hedley_nir_band,
water_mask=mask_for_algorithm, output_path=output_path)
hedley.get_corrected_bands()
if Path(bsq_path).exists() or Path(output_path).exists():
self.deglint_img_path = bsq_path if Path(bsq_path).exists() else output_path
self._copy_hdr_info(img_path, self.deglint_img_path)
else:
raise RuntimeError(f"Hedley算法未生成输出文件: {bsq_path}")
elif method == "sugar":
sugar_glint_mask_method_raw = str(sugar_glint_mask_method).lower()
if 'cdf' in sugar_glint_mask_method_raw or '累积' in sugar_glint_mask_method:
sugar_glint_mask_method_fixed = 'cdf'
elif 'otsu' in sugar_glint_mask_method_raw or '大津' in sugar_glint_mask_method:
sugar_glint_mask_method_fixed = 'otsu'
else:
sugar_glint_mask_method_fixed = 'cdf'
print(f"使用方法: SUGAR (迭代次数={sugar_iter}, 掩膜方法={sugar_glint_mask_method_fixed})")
output_path = str(self.deglint_dir / "deglint_sugar.bsq")
if Path(output_path).exists():
print(f"检测到已存在的去耀斑影像文件,直接使用: {output_path}")
self.deglint_img_path = output_path
self._record_step_time("步骤3: 去除耀斑", step_start_time, time.time(), status="skipped")
return self.deglint_img_path
geotransform, projection, width, height, n_bands = self._get_image_geo_info(img_path)
# 修复BUG必须传入 (height, width)
mask_for_algorithm = self._prepare_water_mask_for_algorithm(
final_water_mask, (height, width), geotransform, projection, img_path
)
if sugar_bounds is None:
sugar_bounds = [(1, 2)]
if sugar_iter is None:
correction_iterative(
img_path, iter=None, bounds=sugar_bounds,
estimate_background=sugar_estimate_background,
glint_mask_method=sugar_glint_mask_method_fixed,
termination_thresh=sugar_termination_thresh,
water_mask=mask_for_algorithm,
output_path=output_path
)
else:
correction_iterative(
img_path, iter=sugar_iter, bounds=sugar_bounds,
estimate_background=sugar_estimate_background,
glint_mask_method=sugar_glint_mask_method_fixed,
water_mask=mask_for_algorithm,
output_path=output_path
)
bsq_path = output_path if output_path.endswith('.bsq') else output_path.replace('.dat', '.bsq').replace(
'.tif', '.bsq')
if Path(bsq_path).exists() or Path(output_path).exists():
self.deglint_img_path = bsq_path if Path(bsq_path).exists() else output_path
self._copy_hdr_info(img_path, self.deglint_img_path)
else:
raise RuntimeError(f"SUGAR算法未生成输出文件: {bsq_path}")
else:
raise ValueError(f"不支持的方法: {method}。支持的方法: kutser, goodman, hedley, sugar")
result = GlintRemovalStep.run(
img_path=img_path,
method=method,
start_wave=start_wave,
end_wave=end_wave,
json_path=json_path,
left_shoulder_wave=left_shoulder_wave,
valley_wave=valley_wave,
right_shoulder_wave=right_shoulder_wave,
water_mask=water_mask,
interpolate_zeros=interpolate_zeros,
interpolation_method=interpolation_method,
enabled=enabled,
kutser_shp_path=kutser_shp_path,
oxy_band=oxy_band,
lower_oxy=lower_oxy,
upper_oxy=upper_oxy,
nir_band=nir_band,
nir_lower=nir_lower,
nir_upper=nir_upper,
goodman_A=goodman_A,
goodman_B=goodman_B,
hedley_shp_path=hedley_shp_path,
hedley_nir_band=hedley_nir_band,
sugar_bounds=sugar_bounds,
sugar_sigma=sugar_sigma,
sugar_estimate_background=sugar_estimate_background,
sugar_glint_mask_method=sugar_glint_mask_method,
sugar_iter=sugar_iter,
sugar_termination_thresh=sugar_termination_thresh,
_get_image_geo_info=self._get_image_geo_info,
_load_image_as_array=self._load_image_as_array,
_save_bands_as_image=self._save_bands_as_image,
_copy_hdr_info=self._copy_hdr_info,
_prepare_water_mask_for_algorithm=self._prepare_water_mask_for_algorithm,
_interpolate_zero_pixels_batch=_interpolate_zero_pixels_batch,
deglint_dir=str(self.deglint_dir),
water_mask_dir=str(self.water_mask_dir),
callback=self._notify,
)
self.deglint_img_path = result
step_end_time = time.time()
self._record_step_time("步骤3: 去除耀斑", step_start_time, step_end_time)
print(f"去耀斑影像已生成: {self.deglint_img_path}")
return self.deglint_img_path
return result
except Exception as e:
step_end_time = time.time()
self._record_step_time("步骤3: 去除耀斑", step_start_time, step_end_time,
status="failed", error=str(e))
status="failed", error=str(e))
raise
def step4_process_csv(self, csv_path: str, skip_dependency_check: bool = False) -> str:
"""
步骤4: 对csv文件进行处理筛选剔除异常值