Files
WQ_GUI/src/utils/extract_water_area.py
duxin 826f110894 fix: 防御性编程重构 — 栅格空间对齐 + NoData 处理 + 高危代码加固
**核心修复 (preview_generator.py):**
- 废弃 _align_mask_to_image() (numpy crop/pad 在地理空间上错误)
- 新增 _warp_mask_to_image(): 使用 gdal.Warp 将掩膜重采样到与底图
  完全一致的像素网格,处理旋转/偏移/投影差异
- 新增 _normalize_mask(nodata_value): 正确的掩膜值域自劢归一间 (0/1 vs 0/255)
- 修复 alpha = mask_data/255.0 → mask_data (掩膜是二值 0/1, 不是 0/255)
- 面积计算 valid_pixels 使用 warp 前数据排除 nodata 背景

**新增防御工具模块:**
- spatial_validator.py: SpatialAlignmentError 自定义异常 +
  validate_two_rasters() / validate_spatial_alignment() 强制空间一致性检查 +
  has_rotation() / get_pixel_resolution() 诊断工具
- nodata_handler.py: read_band_safe() / read_bands_safe() 自动 NoData→nan +
  read_band_masked() 返回 MaskedArray + create_valid_mask()

**P0 高危代码集成 (3处):**
- sampling.py: 耀斑掩膜与水体掩膜 bool 运算前验证 numpy shape 一致
- find_severe_glint_area.py: 栅格 mask 读取后验证 dims+GT+projection 对齐
- waterindex_inversion/__init__.py: mask 与 BSQ 维度验证, 不一致时优雅降级

**旋转影像兼容性修复:**
- extract_water_area.py: pixel_size = sqrt(gt[1]^2+gt[2]^2) 使用勾股定理
  正确计算旋转影像的像素分辨率
2026-07-07 09:10:40 +08:00

224 lines
8.7 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.

from src.utils.util import *
from osgeo import gdal, ogr
import argparse
gdal.UseExceptions()
ogr.UseExceptions()
def xml2shp():
pass
def rasterize_envi_xml(shp_filepath):
pass
@timeit
def rasterize_shp(shp_filepath, raster_fn_out, img_path, NoData_value=None):
# ---------- 防御性处理:路径标准化 ----------
shp_filepath = os.path.abspath(shp_filepath).replace('\\', '/')
print(f"[DEBUG rasterize_shp] 标准化后的 SHP 路径: {shp_filepath}")
# 检查伴随文件完整性
shp_base = os.path.splitext(shp_filepath)[0]
for ext in ['.dbf', '.shx', '.prj']:
companion = shp_base + ext
if os.path.exists(companion):
print(f"[DEBUG rasterize_shp] 伴随文件存在: {companion}")
else:
print(f"[WARNING rasterize_shp] 伴随文件缺失: {companion}")
# 确保 GDAL/OGR 驱动已注册
gdal.AllRegister()
ogr.RegisterAll()
# 检查 ESRI Shapefile 驱动
driver = ogr.GetDriverByName("ESRI Shapefile")
if driver is None:
raise RuntimeError(
"系统中未找到 ESRI Shapefile 驱动!请检查 GDAL 是否正确安装及是否包含 Shapefile 支持。"
)
print(f"[DEBUG rasterize_shp] ESRI Shapefile 驱动: {driver.GetName()}")
# 打开参考影像获取尺寸信息
dataset = gdal.Open(img_path)
if dataset is None:
raise ValueError(f"无法打开参考影像文件: {img_path}")
im_width = dataset.RasterXSize
im_height = dataset.RasterYSize
geotransform = dataset.GetGeoTransform()
imgdata_in = dataset.GetRasterBand(1).ReadAsArray()
del dataset
# ---------- 打开 SHP 文件(双重尝试获取详细错误) ----------
source_ds = gdal.OpenEx(shp_filepath, gdal.OF_VECTOR)
if source_ds is None:
# gdal.OpenEx 失败,尝试 ogr.Open 获取更详细的错误信息
try:
ogr_ds = ogr.Open(shp_filepath)
except Exception as ogr_err:
raise RuntimeError(
f"GDAL/OGR 无法打开 SHP 文件(详细原因):\n"
f" ogr.Open 抛出异常: {str(ogr_err)}\n"
f" 文件路径: {shp_filepath}\n"
f"常见原因:\n"
f" 1. 路径包含中文/空格/特殊字符(建议复制到纯英文路径下重试)\n"
f" 2. .dbf 或 .shx 伴随文件缺失或损坏\n"
f" 3. GDAL 未注册 ESRI Shapefile 驱动\n"
f" 4. 文件被其他程序锁定"
)
if ogr_ds is None:
raise RuntimeError(
f"ogr.Open 和 gdal.OpenEx 均返回 None,无法打开 SHP 文件。\n"
f"文件路径: {shp_filepath}\n"
f"请检查:\n"
f" 1. 所有伴随文件(.dbf/.shx/.prg)是否齐全\n"
f" 2. 文件是否被其他程序占用\n"
f" 3. 路径中是否存在不支持的字符"
)
# 检查图层数量,如果有多层,指定使用第一层
layer_count = source_ds.GetLayerCount()
layer_name = None
if layer_count > 1:
print(f"警告: shapefile包含{layer_count}个图层,将使用第一个图层进行栅格化")
# 获取第一个图层
layer = source_ds.GetLayer(0)
layer_name = layer.GetName()
# 计算像素分辨率(考虑旋转参数)
# 对于旋转影像(gt[2] 或 gt[4] != 0),像素实际分辨率需用勾股定理
pixel_size_x = np.sqrt(geotransform[1]**2 + geotransform[2]**2)
pixel_size_y = np.sqrt(geotransform[4]**2 + geotransform[5]**2)
raster_fn_out_tmp = append2filename(raster_fn_out, "_tmp_delete")
# 构建栅格化参数
rasterize_kwargs = {
'format': 'envi',
'outputType': gdal.GDT_Byte,
'noData': NoData_value,
'initValues': NoData_value,
'xRes': pixel_size_x,
'yRes': pixel_size_y,
'allTouched': True,
'burnValues': 1
}
# 如果有多层,指定使用第一层
if layer_name is not None:
rasterize_kwargs['layers'] = [layer_name]
# 执行栅格化
gdal.Rasterize(raster_fn_out_tmp, source_ds, **rasterize_kwargs)
dataset_tmp = gdal.Open(raster_fn_out_tmp)
geotransform_tmp = dataset_tmp.GetGeoTransform()
inv_geotransform_tmp = gdal.InvGeoTransform(geotransform_tmp)
data_tmp = dataset_tmp.GetRasterBand(1).ReadAsArray()
del dataset_tmp
# 创建和输入影像相同行列号、相同分辨率的水域掩膜,方便后续使用
water_mask = np.zeros((im_height, im_width))
for row in range(im_height):
for column in range(im_width):
coor = gdal.ApplyGeoTransform(geotransform, column, row)
coor_pixel = gdal.ApplyGeoTransform(inv_geotransform_tmp, coor[0], coor[1])
coor_pixel = [int(num) for num in coor_pixel]
if coor_pixel[0] < 0 or coor_pixel[0] >= data_tmp.shape[1]:
continue
if coor_pixel[1] < 0 or coor_pixel[1] >= data_tmp.shape[0]:
continue
if imgdata_in[row, column] == 0: # 当shp区域比影像区域大时,略过
continue
water_mask[row, column] = data_tmp[coor_pixel[1], coor_pixel[0]]
write_bands(img_path, raster_fn_out, water_mask)
os.remove(raster_fn_out_tmp)
def calculate_NDWI(green_bandnumber, nir_bandnumber, filename):
dataset = gdal.Open(filename) # 打开文件
num_bands = dataset.RasterCount # 栅格矩阵的波段数
im_geotrans = dataset.GetGeoTransform() # 仿射矩阵
im_proj = dataset.GetProjection() # 地图投影信息
tmp = dataset.GetRasterBand(green_bandnumber + 1) # 波段计数从1开始
band_green = tmp.ReadAsArray().astype(np.int16)
tmp = dataset.GetRasterBand(nir_bandnumber + 1) # 波段计数从1开始
band_nir = tmp.ReadAsArray().astype(np.int16)
ndwi = (band_green - band_nir) / (band_green + band_nir)
del dataset
return ndwi
def extract_water(ndwi, threshold=0.3, data_ignore_value=0):
water_region = np.where(ndwi > threshold, 1, data_ignore_value)
return water_region
def ndwi(file_path, ndwi_threshold=0.4, output_path=None, data_ignore_value=0):
if output_path is None:
output_path = append2filename(file_path, "_waterarea")
dataset_in = gdal.Open(file_path)
im_width_in = dataset_in.RasterXSize # 栅格矩阵的列数
im_height_in = dataset_in.RasterYSize # 栅格矩阵的行数
num_bands_in = dataset_in.RasterCount # 栅格矩阵的波段数
geotrans_in = dataset_in.GetGeoTransform() # 仿射矩阵
proj_in = dataset_in.GetProjection() # 地图投影信息
del dataset_in
green_wave = 552.19
nir_wave = 809.2890
green_band_number = find_band_number(green_wave, file_path)
nir_band_number = find_band_number(nir_wave, file_path)
ndwi = calculate_NDWI(green_band_number, nir_band_number, file_path)
water_binary = extract_water(ndwi, threshold=ndwi_threshold) # 0.4
write_bands(file_path, output_path, water_binary)
return output_path
def main():
parser = argparse.ArgumentParser(description="此程序用于提取水域区域,输出的水域栅格和输入的影像具有相同的行列数。")
# parser.add_argument("--global_arg", type=str, help="A global argument for all modes", required=True)
# 创建子命令解析器
subparsers = parser.add_subparsers(dest="algorithm", required=True, help="Choose a mode")
rasterize_shp_ = subparsers.add_parser("rasterize_shp", help="Mode 1 description")
rasterize_shp_.add_argument('-i1', '--img_path', type=str, required=True, help='输入影像文件的路径')
rasterize_shp_.add_argument('-i2', '--shp_path', type=str, required=True, help='输入shp文件的路径')
rasterize_shp_.add_argument('-o', '--water_mask_outpath', required=True, type=str, help='输出水体掩膜文件的路径')
rasterize_shp_.set_defaults(func=rasterize_shp)
ndwi_ = subparsers.add_parser("ndwi", help="Mode 2 description")
ndwi_.add_argument('-i1', '--img_path', type=str, required=True, help='输入影像文件的路径')
ndwi_.add_argument('-i2', '--ndwi_threshold', type=float, required=True, help='输入ndwi水体阈值,大于此值的为水域')
ndwi_.add_argument('-o', '--water_mask_outpath', required=True, type=str, help='输出水体掩膜文件的路径')
ndwi_.set_defaults(func=ndwi)
# 解析参数
args = parser.parse_args()
if args.algorithm == "rasterize_shp":
args.func(args.shp_path, args.water_mask_outpath, args.img_path)
elif args.algorithm == "ndwi":
args.func(args.img_path, args.ndwi_threshold, args.water_mask_outpath)
# Press the green button in the gutter to run the script.
if __name__ == '__main__':
main()