2026-04-08 15:25:08 +08:00
|
|
|
|
import numpy as np
|
2026-04-09 17:25:52 +08:00
|
|
|
|
import sys
|
2026-04-08 15:25:08 +08:00
|
|
|
|
# import preprocessing
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
from osgeo import gdal
|
|
|
|
|
|
GDAL_AVAILABLE = True
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
|
GDAL_AVAILABLE = False
|
|
|
|
|
|
print("警告: GDAL未安装,将使用numpy处理模式")
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
from tqdm import tqdm
|
|
|
|
|
|
TQDM_AVAILABLE = True
|
|
|
|
|
|
except ImportError:
|
|
|
|
|
|
TQDM_AVAILABLE = False
|
|
|
|
|
|
# 如果tqdm不可用,定义一个简单的包装器
|
2026-04-09 17:25:52 +08:00
|
|
|
|
def tqdm(iterable, desc=None, total=None, disable=None):
|
2026-04-08 15:25:08 +08:00
|
|
|
|
return iterable
|
|
|
|
|
|
|
2026-04-09 17:25:52 +08:00
|
|
|
|
# 检测是否在 PyInstaller 打包环境(无控制台)
|
|
|
|
|
|
_is_frozen_gui = getattr(sys, "frozen", False) and (not hasattr(sys, 'stdout') or sys.stdout is None)
|
|
|
|
|
|
|
2026-04-08 15:25:08 +08:00
|
|
|
|
class Goodman:
|
|
|
|
|
|
def __init__(self, im_aligned, NIR_lower = 25, NIR_upper = 37, A = 0.000019, B = 0.1,
|
|
|
|
|
|
use_gdal=True, chunk_size=None, water_mask=None, output_path=None):
|
|
|
|
|
|
"""
|
|
|
|
|
|
:param im_aligned (np.ndarray or str): band aligned and calibrated & corrected reflectance image
|
|
|
|
|
|
可以是numpy数组或GDAL可读取的文件路径
|
|
|
|
|
|
:param NIR_lower (int): band index which corresponds to 641.93nm, closest band to 640nm
|
|
|
|
|
|
:param NIR_upper (int): band index which corresponds to 751.49nm, closest band to 750nm
|
|
|
|
|
|
:param A (float): the values in Goodman et al's paper, using AVIRIS reflectance (rather than radiance) data
|
|
|
|
|
|
:param B (float): the values in Goodman et al's paper, using AVIRIS reflectance (rather than radiance) data
|
|
|
|
|
|
see Goodman et al, which corrects each pixel independently. The NIR radiance is subtracted from the radiance at each wavelength,
|
|
|
|
|
|
but a wavelength-independent offset is also added.
|
|
|
|
|
|
it is not clear how A and B were chosen, but an optimization for a case where in situ data is
|
|
|
|
|
|
available would enable values to be found
|
|
|
|
|
|
:param use_gdal (bool): 是否使用GDAL加速处理(需要GDAL可用且输入为文件路径或大数组)
|
|
|
|
|
|
:param chunk_size (int): 已废弃,不再使用分块处理,改为逐波段处理
|
|
|
|
|
|
:param water_mask (np.ndarray or str or None): 水域掩膜,1表示水域,0表示非水域
|
|
|
|
|
|
可以是numpy数组、栅格文件路径(.dat/.tif)或shapefile路径(.shp)
|
|
|
|
|
|
如果为None,则处理全图
|
|
|
|
|
|
:param output_path (str or None): 输出文件路径,如果提供则保存校正后的图像
|
|
|
|
|
|
如果为None,则不保存
|
|
|
|
|
|
"""
|
|
|
|
|
|
self.im_aligned = im_aligned
|
|
|
|
|
|
self.NIR_lower = NIR_lower
|
|
|
|
|
|
self.NIR_upper = NIR_upper
|
|
|
|
|
|
self.A = A
|
|
|
|
|
|
self.B = B
|
|
|
|
|
|
self.use_gdal = use_gdal and GDAL_AVAILABLE
|
|
|
|
|
|
self.chunk_size = chunk_size
|
|
|
|
|
|
self.is_file_path = isinstance(im_aligned, str)
|
|
|
|
|
|
self.output_path = output_path
|
|
|
|
|
|
|
|
|
|
|
|
# 获取图像信息(需要在加载掩膜之前获取尺寸)
|
|
|
|
|
|
if self.is_file_path:
|
|
|
|
|
|
if not self.use_gdal:
|
|
|
|
|
|
raise ValueError("输入为文件路径时,必须安装GDAL")
|
|
|
|
|
|
self.dataset = gdal.Open(im_aligned, gdal.GA_ReadOnly)
|
|
|
|
|
|
if self.dataset is None:
|
|
|
|
|
|
raise ValueError(f"无法打开影像文件: {im_aligned}")
|
|
|
|
|
|
self.height = self.dataset.RasterYSize
|
|
|
|
|
|
self.width = self.dataset.RasterXSize
|
|
|
|
|
|
self.n_bands = self.dataset.RasterCount
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.dataset = None
|
|
|
|
|
|
self.height = im_aligned.shape[0]
|
|
|
|
|
|
self.width = im_aligned.shape[1]
|
|
|
|
|
|
self.n_bands = im_aligned.shape[-1]
|
|
|
|
|
|
|
|
|
|
|
|
# 加载水域掩膜(在获取图像尺寸之后)
|
|
|
|
|
|
self.water_mask = self._load_water_mask(water_mask)
|
|
|
|
|
|
|
|
|
|
|
|
def _load_water_mask(self, water_mask):
|
|
|
|
|
|
"""
|
|
|
|
|
|
加载水域掩膜
|
|
|
|
|
|
|
|
|
|
|
|
:param water_mask: 可以是None、numpy数组、文件路径(.dat/.tif)或shapefile路径(.shp)
|
|
|
|
|
|
:return: numpy数组或None,1表示水域,0表示非水域
|
|
|
|
|
|
"""
|
|
|
|
|
|
if water_mask is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
# 如果已经是numpy数组
|
|
|
|
|
|
if isinstance(water_mask, np.ndarray):
|
|
|
|
|
|
if water_mask.shape[:2] != (self.height, self.width):
|
|
|
|
|
|
raise ValueError(f"掩膜尺寸 {water_mask.shape[:2]} 与图像尺寸 {(self.height, self.width)} 不匹配")
|
|
|
|
|
|
return (water_mask > 0).astype(np.uint8) # 确保是0/1掩膜
|
|
|
|
|
|
|
|
|
|
|
|
# 如果是文件路径
|
|
|
|
|
|
if isinstance(water_mask, str):
|
|
|
|
|
|
if not GDAL_AVAILABLE:
|
|
|
|
|
|
raise ValueError("使用文件路径作为掩膜时,必须安装GDAL")
|
|
|
|
|
|
|
|
|
|
|
|
# 检查是否为shapefile
|
|
|
|
|
|
if water_mask.lower().endswith('.shp'):
|
|
|
|
|
|
# 从shp文件创建掩膜
|
|
|
|
|
|
if self.is_file_path:
|
|
|
|
|
|
ref_path = self.im_aligned
|
|
|
|
|
|
else:
|
|
|
|
|
|
raise ValueError("输入为numpy数组时,无法从shp文件创建掩膜(需要参考栅格)")
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
from osgeo import ogr
|
|
|
|
|
|
ref_dataset = gdal.Open(ref_path, gdal.GA_ReadOnly)
|
|
|
|
|
|
if ref_dataset is None:
|
|
|
|
|
|
raise ValueError(f"无法打开参考栅格文件: {ref_path}")
|
|
|
|
|
|
|
|
|
|
|
|
geotransform = ref_dataset.GetGeoTransform()
|
|
|
|
|
|
projection = ref_dataset.GetProjection()
|
|
|
|
|
|
width = ref_dataset.RasterXSize
|
|
|
|
|
|
height = ref_dataset.RasterYSize
|
|
|
|
|
|
|
|
|
|
|
|
# 创建内存中的栅格数据集
|
|
|
|
|
|
mem_driver = gdal.GetDriverByName('MEM')
|
|
|
|
|
|
mask_dataset = mem_driver.Create('', width, height, 1, gdal.GDT_Byte)
|
|
|
|
|
|
mask_dataset.SetGeoTransform(geotransform)
|
|
|
|
|
|
mask_dataset.SetProjection(projection)
|
|
|
|
|
|
|
|
|
|
|
|
mask_band = mask_dataset.GetRasterBand(1)
|
|
|
|
|
|
mask_band.Fill(0)
|
|
|
|
|
|
|
|
|
|
|
|
# 打开shp文件
|
|
|
|
|
|
shp_dataset = ogr.Open(water_mask)
|
|
|
|
|
|
if shp_dataset is None:
|
|
|
|
|
|
raise ValueError(f"无法打开shp文件: {water_mask}")
|
|
|
|
|
|
|
|
|
|
|
|
layer = shp_dataset.GetLayer()
|
|
|
|
|
|
gdal.RasterizeLayer(mask_dataset, [1], layer, burn_values=[1])
|
|
|
|
|
|
|
|
|
|
|
|
water_mask_array = mask_band.ReadAsArray()
|
|
|
|
|
|
|
|
|
|
|
|
ref_dataset = None
|
|
|
|
|
|
mask_dataset = None
|
|
|
|
|
|
shp_dataset = None
|
|
|
|
|
|
|
|
|
|
|
|
return (water_mask_array > 0).astype(np.uint8)
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
raise ValueError(f"从shp文件创建掩膜时出错: {e}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 栅格文件
|
|
|
|
|
|
mask_dataset = gdal.Open(water_mask, gdal.GA_ReadOnly)
|
|
|
|
|
|
if mask_dataset is None:
|
|
|
|
|
|
raise ValueError(f"无法打开掩膜文件: {water_mask}")
|
|
|
|
|
|
|
|
|
|
|
|
mask_array = mask_dataset.GetRasterBand(1).ReadAsArray()
|
|
|
|
|
|
mask_dataset = None
|
|
|
|
|
|
|
|
|
|
|
|
if mask_array.shape != (self.height, self.width):
|
|
|
|
|
|
raise ValueError(f"掩膜尺寸 {mask_array.shape} 与图像尺寸 {(self.height, self.width)} 不匹配")
|
|
|
|
|
|
|
|
|
|
|
|
return (mask_array > 0).astype(np.uint8)
|
|
|
|
|
|
|
|
|
|
|
|
raise ValueError(f"不支持的掩膜类型: {type(water_mask)}")
|
|
|
|
|
|
|
|
|
|
|
|
def _get_corrected_bands_numpy(self):
|
|
|
|
|
|
"""
|
|
|
|
|
|
使用numpy处理(用于小图像或GDAL不可用时)
|
|
|
|
|
|
|
|
|
|
|
|
注意:由于输入已经是numpy数组,数据已在内存中。
|
|
|
|
|
|
此方法通过逐波段处理,避免同时创建多个校正后的波段数组。
|
|
|
|
|
|
内存峰值 = 原始数组 + NIR波段(2个) + 当前处理的波段(1个)
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 预提取重复使用的NIR波段,避免在循环中重复访问
|
|
|
|
|
|
# 这些波段会一直保存在内存中,因为它们需要用于所有波段的校正
|
|
|
|
|
|
R_640 = self.im_aligned[:,:,self.NIR_lower]
|
|
|
|
|
|
R_750 = self.im_aligned[:,:,self.NIR_upper]
|
|
|
|
|
|
# 预计算常量部分
|
|
|
|
|
|
diff_640_750 = R_640 - R_750
|
|
|
|
|
|
corrected_bands = []
|
|
|
|
|
|
|
|
|
|
|
|
# 获取水域掩膜(如果存在)
|
|
|
|
|
|
water_mask_bool = self.water_mask.astype(bool) if self.water_mask is not None else None
|
|
|
|
|
|
|
|
|
|
|
|
# 逐波段处理:每次只处理一个波段,处理完后立即添加到结果列表
|
2026-04-09 17:25:52 +08:00
|
|
|
|
for i in tqdm(range(self.n_bands), desc="处理波段 (numpy)", total=self.n_bands, disable=_is_frozen_gui):
|
2026-04-08 15:25:08 +08:00
|
|
|
|
# 获取当前波段(这是数组视图,不是复制)
|
|
|
|
|
|
R = self.im_aligned[:,:,i]
|
|
|
|
|
|
# 优化计算:减少中间数组创建
|
|
|
|
|
|
corrected_band = R - R_750 + self.A + self.B * diff_640_750
|
|
|
|
|
|
# 使用np.maximum原地操作,将负值设为0
|
|
|
|
|
|
np.maximum(corrected_band, 0, out=corrected_band)
|
|
|
|
|
|
|
|
|
|
|
|
# 如果存在水域掩膜,只对水域区域应用校正
|
|
|
|
|
|
if water_mask_bool is not None:
|
|
|
|
|
|
corrected_band = np.where(water_mask_bool, corrected_band, R)
|
|
|
|
|
|
|
|
|
|
|
|
# 立即添加到结果列表(corrected_band会保留在列表中)
|
|
|
|
|
|
corrected_bands.append(corrected_band)
|
|
|
|
|
|
return corrected_bands
|
|
|
|
|
|
|
2026-07-07 14:10:47 +08:00
|
|
|
|
def _get_corrected_bands_gdal(self, out_dataset=None):
|
2026-04-08 15:25:08 +08:00
|
|
|
|
"""
|
2026-07-07 14:10:47 +08:00
|
|
|
|
使用GDAL逐波段处理。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
out_dataset: 若提供,每处理完一个波段立即写入此数据集(流式模式);
|
|
|
|
|
|
若为 None,累积到列表中返回(传统模式,大图像慎用)。
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
若 out_dataset 为 None,返回波段列表;
|
|
|
|
|
|
若 out_dataset 不为 None,返回 None(波段已直接写入输出文件)
|
2026-04-08 15:25:08 +08:00
|
|
|
|
"""
|
|
|
|
|
|
# 获取NIR波段对象(用于所有波段的校正)
|
2026-07-07 14:10:47 +08:00
|
|
|
|
band_640 = self.dataset.GetRasterBand(self.NIR_lower + 1)
|
2026-04-08 15:25:08 +08:00
|
|
|
|
band_750 = self.dataset.GetRasterBand(self.NIR_upper + 1)
|
2026-07-07 14:10:47 +08:00
|
|
|
|
|
|
|
|
|
|
# 读取NIR波段(全程保留在内存中)
|
2026-04-08 15:25:08 +08:00
|
|
|
|
R_640 = band_640.ReadAsArray().astype(np.float32)
|
|
|
|
|
|
R_750 = band_750.ReadAsArray().astype(np.float32)
|
|
|
|
|
|
diff_640_750 = R_640 - R_750
|
2026-07-07 14:10:47 +08:00
|
|
|
|
del R_640 # 释放不再需要的 R_640
|
|
|
|
|
|
|
2026-04-08 15:25:08 +08:00
|
|
|
|
# 获取水域掩膜
|
|
|
|
|
|
water_mask_bool = self.water_mask.astype(bool) if self.water_mask is not None else None
|
2026-07-07 14:10:47 +08:00
|
|
|
|
|
|
|
|
|
|
# 输出模式
|
|
|
|
|
|
streaming = (out_dataset is not None)
|
|
|
|
|
|
corrected_bands = [] if not streaming else None
|
|
|
|
|
|
|
|
|
|
|
|
# 逐波段处理
|
|
|
|
|
|
for i in tqdm(range(self.n_bands), desc="处理波段 (GDAL)",
|
|
|
|
|
|
total=self.n_bands, disable=_is_frozen_gui):
|
|
|
|
|
|
# 读取当前波段
|
2026-04-08 15:25:08 +08:00
|
|
|
|
current_band = self.dataset.GetRasterBand(i + 1)
|
|
|
|
|
|
R = current_band.ReadAsArray().astype(np.float32)
|
2026-07-07 14:10:47 +08:00
|
|
|
|
|
|
|
|
|
|
# 校正计算(原地操作以减少临时分配)
|
|
|
|
|
|
corrected = R - R_750
|
|
|
|
|
|
corrected += self.A
|
|
|
|
|
|
corrected += self.B * diff_640_750
|
|
|
|
|
|
np.maximum(corrected, 0, out=corrected)
|
|
|
|
|
|
|
|
|
|
|
|
# 水域掩膜:只在有水的地方用校正值,陆地保持原值
|
2026-04-08 15:25:08 +08:00
|
|
|
|
if water_mask_bool is not None:
|
2026-07-07 14:10:47 +08:00
|
|
|
|
corrected = np.where(water_mask_bool, corrected, R)
|
|
|
|
|
|
|
|
|
|
|
|
if streaming:
|
|
|
|
|
|
# 流式模式:立即写入磁盘并释放
|
|
|
|
|
|
out_band = out_dataset.GetRasterBand(i + 1)
|
|
|
|
|
|
out_band.WriteArray(corrected)
|
|
|
|
|
|
out_band.FlushCache()
|
|
|
|
|
|
del corrected, R
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 传统模式:累积到列表
|
|
|
|
|
|
corrected_bands.append(corrected)
|
|
|
|
|
|
del R
|
|
|
|
|
|
|
|
|
|
|
|
# 清理
|
|
|
|
|
|
del R_750, diff_640_750
|
|
|
|
|
|
|
2026-04-08 15:25:08 +08:00
|
|
|
|
return corrected_bands
|
|
|
|
|
|
|
|
|
|
|
|
def _get_corrected_bands_gdal_mem(self):
|
|
|
|
|
|
"""使用GDAL内存驱动处理numpy数组,逐波段处理"""
|
|
|
|
|
|
# 创建内存数据集
|
|
|
|
|
|
driver = gdal.GetDriverByName('MEM')
|
|
|
|
|
|
mem_dataset = driver.Create('', self.width, self.height, self.n_bands, gdal.GDT_Float32)
|
|
|
|
|
|
|
|
|
|
|
|
# 将numpy数组写入内存数据集(显示进度)
|
2026-04-09 17:25:52 +08:00
|
|
|
|
for i in tqdm(range(self.n_bands), desc="加载波段到内存", total=self.n_bands, disable=_is_frozen_gui):
|
2026-04-08 15:25:08 +08:00
|
|
|
|
band = mem_dataset.GetRasterBand(i + 1)
|
|
|
|
|
|
band.WriteArray(self.im_aligned[:,:,i])
|
|
|
|
|
|
band.FlushCache()
|
|
|
|
|
|
|
|
|
|
|
|
# 临时保存原始dataset引用
|
|
|
|
|
|
original_dataset = self.dataset
|
|
|
|
|
|
self.dataset = mem_dataset
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 使用逐波段处理方法
|
|
|
|
|
|
result = self._get_corrected_bands_gdal()
|
|
|
|
|
|
finally:
|
|
|
|
|
|
# 恢复原始dataset
|
|
|
|
|
|
self.dataset = original_dataset
|
|
|
|
|
|
mem_dataset = None
|
|
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
def _save_corrected_bands(self, corrected_bands):
|
|
|
|
|
|
"""
|
|
|
|
|
|
保存校正后的波段到文件(BSQ格式,ENVI格式)
|
|
|
|
|
|
|
|
|
|
|
|
注意:为了节省内存,直接逐波段写入,不先堆叠成完整数组
|
|
|
|
|
|
|
|
|
|
|
|
:param corrected_bands: 校正后的波段列表
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not GDAL_AVAILABLE:
|
|
|
|
|
|
raise ImportError("GDAL未安装,无法保存影像文件")
|
|
|
|
|
|
|
|
|
|
|
|
if self.output_path is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
|
# 确保输出目录存在
|
|
|
|
|
|
output_dir = os.path.dirname(self.output_path)
|
|
|
|
|
|
if output_dir and not os.path.exists(output_dir):
|
|
|
|
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
# 从第一个波段获取尺寸信息(避免堆叠所有波段)
|
|
|
|
|
|
if not corrected_bands:
|
|
|
|
|
|
raise ValueError("校正后的波段列表为空")
|
|
|
|
|
|
first_band = corrected_bands[0]
|
|
|
|
|
|
height, width = first_band.shape
|
|
|
|
|
|
n_bands = len(corrected_bands)
|
|
|
|
|
|
|
|
|
|
|
|
# 获取地理变换和投影信息
|
|
|
|
|
|
if self.is_file_path and self.dataset is not None:
|
|
|
|
|
|
geotransform = self.dataset.GetGeoTransform()
|
|
|
|
|
|
projection = self.dataset.GetProjection()
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 如果没有地理信息,使用默认值
|
|
|
|
|
|
geotransform = (0, 1, 0, 0, 0, -1)
|
|
|
|
|
|
projection = ""
|
|
|
|
|
|
|
|
|
|
|
|
# 强制使用ENVI格式(BSQ格式),确保文件扩展名为.bsq
|
|
|
|
|
|
base_path, ext = os.path.splitext(self.output_path)
|
|
|
|
|
|
# 如果扩展名不是.bsq,使用基础路径添加.bsq
|
|
|
|
|
|
if ext.lower() != '.bsq':
|
|
|
|
|
|
bsq_path = base_path + '.bsq'
|
|
|
|
|
|
else:
|
|
|
|
|
|
bsq_path = self.output_path
|
|
|
|
|
|
|
|
|
|
|
|
# 使用ENVI驱动(默认就是BSQ格式)
|
|
|
|
|
|
driver = gdal.GetDriverByName('ENVI')
|
|
|
|
|
|
if driver is None:
|
|
|
|
|
|
raise ValueError("无法创建ENVI格式文件,ENVI驱动不可用")
|
|
|
|
|
|
|
|
|
|
|
|
# 创建ENVI格式数据集(会自动生成.hdr文件)
|
|
|
|
|
|
dataset = driver.Create(bsq_path, width, height, n_bands, gdal.GDT_Float32)
|
|
|
|
|
|
if dataset is None:
|
|
|
|
|
|
raise ValueError(f"无法创建输出文件: {bsq_path}")
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 设置地理变换和投影
|
|
|
|
|
|
if geotransform:
|
|
|
|
|
|
dataset.SetGeoTransform(geotransform)
|
|
|
|
|
|
if projection:
|
|
|
|
|
|
dataset.SetProjection(projection)
|
|
|
|
|
|
|
|
|
|
|
|
# 直接逐波段写入(不先堆叠,节省内存)
|
2026-04-09 17:25:52 +08:00
|
|
|
|
for i in tqdm(range(n_bands), desc="保存波段", total=n_bands, disable=_is_frozen_gui):
|
2026-04-08 15:25:08 +08:00
|
|
|
|
band = dataset.GetRasterBand(i + 1)
|
|
|
|
|
|
# 直接从列表中获取波段并写入,避免创建完整数组
|
|
|
|
|
|
band.WriteArray(corrected_bands[i])
|
|
|
|
|
|
band.FlushCache()
|
|
|
|
|
|
finally:
|
|
|
|
|
|
dataset = None
|
|
|
|
|
|
|
|
|
|
|
|
# 检查.hdr文件是否已创建
|
|
|
|
|
|
hdr_path = bsq_path + '.hdr'
|
|
|
|
|
|
if os.path.exists(hdr_path):
|
|
|
|
|
|
print(f"校正后的图像已保存至: {bsq_path} (BSQ格式)")
|
|
|
|
|
|
print(f"头文件已保存至: {hdr_path}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"校正后的图像已保存至: {bsq_path} (BSQ格式)")
|
|
|
|
|
|
print(f"警告: 未检测到.hdr文件,但GDAL应该已自动创建")
|
|
|
|
|
|
|
|
|
|
|
|
def get_corrected_bands(self):
|
|
|
|
|
|
"""
|
2026-07-07 14:10:47 +08:00
|
|
|
|
获取校正后的波段(自动选择最优处理模式)
|
|
|
|
|
|
|
|
|
|
|
|
内存优化: 当 output_path 已设置时,使用流式模式逐波段直接写入磁盘,
|
|
|
|
|
|
避免在内存中累积全部波段(对 6522×13215×150 的大影像可节省 ~46 GB)。
|
|
|
|
|
|
|
|
|
|
|
|
:return: 校正后的波段列表(流式模式返回 None,波段已在输出文件中)
|
2026-04-08 15:25:08 +08:00
|
|
|
|
"""
|
2026-07-07 14:10:47 +08:00
|
|
|
|
# ── 流式模式:output_path 已设置 → 逐波段处理+立即写入 → 零累积 ──
|
|
|
|
|
|
if self.output_path is not None:
|
|
|
|
|
|
return self._get_corrected_bands_streaming()
|
|
|
|
|
|
|
|
|
|
|
|
# ── 传统模式:output_path 为空 → 返回波段列表 ──
|
2026-04-08 15:25:08 +08:00
|
|
|
|
if self.is_file_path:
|
|
|
|
|
|
if self.use_gdal:
|
2026-07-07 14:10:47 +08:00
|
|
|
|
return self._get_corrected_bands_gdal(out_dataset=None)
|
2026-04-08 15:25:08 +08:00
|
|
|
|
else:
|
|
|
|
|
|
raise ValueError("输入为文件路径时,必须安装GDAL")
|
|
|
|
|
|
else:
|
|
|
|
|
|
if self.use_gdal and self.height * self.width * self.n_bands > 100000000:
|
2026-07-07 14:10:47 +08:00
|
|
|
|
return self._get_corrected_bands_gdal_mem()
|
2026-04-08 15:25:08 +08:00
|
|
|
|
else:
|
2026-07-07 14:10:47 +08:00
|
|
|
|
return self._get_corrected_bands_numpy()
|
2026-04-08 15:25:08 +08:00
|
|
|
|
|
2026-07-07 14:10:47 +08:00
|
|
|
|
def _get_corrected_bands_streaming(self):
|
|
|
|
|
|
"""流式处理:逐波段校正并直接写入输出文件,不累积内存
|
|
|
|
|
|
|
|
|
|
|
|
适用于大尺度影像(如 6522×13215×150)。
|
|
|
|
|
|
内存峰值 ≈ 3 个全波段数组(NIR×2 + 当前波段),而非全部 150 个波段。
|
|
|
|
|
|
|
|
|
|
|
|
:return: None(波段已在输出文件中)
|
|
|
|
|
|
"""
|
|
|
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
|
|
# ── 创建输出文件 ──
|
|
|
|
|
|
base_path, ext = os.path.splitext(self.output_path)
|
|
|
|
|
|
bsq_path = base_path + '.bsq' if ext.lower() != '.bsq' else self.output_path
|
|
|
|
|
|
output_dir = os.path.dirname(bsq_path)
|
|
|
|
|
|
if output_dir and not os.path.exists(output_dir):
|
|
|
|
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
driver = gdal.GetDriverByName('ENVI')
|
|
|
|
|
|
out_ds = driver.Create(
|
|
|
|
|
|
bsq_path, self.width, self.height, self.n_bands, gdal.GDT_Float32
|
|
|
|
|
|
)
|
|
|
|
|
|
if out_ds is None:
|
|
|
|
|
|
raise ValueError(f"无法创建输出文件: {bsq_path}")
|
|
|
|
|
|
|
|
|
|
|
|
# ── 设置地理参考 ──
|
|
|
|
|
|
if self.is_file_path and self.dataset is not None:
|
|
|
|
|
|
out_ds.SetGeoTransform(self.dataset.GetGeoTransform())
|
|
|
|
|
|
out_ds.SetProjection(self.dataset.GetProjection())
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
# 逐波段处理 + 立即写入(波段在 _get_corrected_bands_gdal 的循环中
|
|
|
|
|
|
# 由 WriteArray 写入 → FlushCache → del,不会累积)
|
|
|
|
|
|
self._get_corrected_bands_gdal(out_dataset=out_ds)
|
|
|
|
|
|
finally:
|
|
|
|
|
|
out_ds = None # 关闭文件,确保数据落盘
|
|
|
|
|
|
|
|
|
|
|
|
# ── 日志 ──
|
|
|
|
|
|
hdr_path = bsq_path + '.hdr'
|
|
|
|
|
|
if os.path.exists(hdr_path):
|
|
|
|
|
|
print(f"校正后的图像已保存至: {bsq_path} (BSQ格式, 流式写入)")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"校正后的图像已保存至: {bsq_path} (BSQ格式)")
|
|
|
|
|
|
print("警告: 未检测到.hdr文件,但GDAL应该已自动创建")
|
|
|
|
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
2026-04-08 15:25:08 +08:00
|
|
|
|
def __del__(self):
|
|
|
|
|
|
"""清理资源"""
|
|
|
|
|
|
if self.dataset is not None and self.is_file_path:
|
|
|
|
|
|
self.dataset = None
|