814 lines
28 KiB
Python
814 lines
28 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
高光谱反射率校正工具
|
||
|
||
功能:
|
||
- 读取ENVI ASCII Plot File格式的反射率校正文件
|
||
- 读取航带文件夹中的高光谱数据文件 (.bil/.bip/.bsq等ENVI格式)
|
||
注意:spectral库通过读取对应的.hdr头文件来访问数据
|
||
- 根据波长匹配进行反射率校正(航带数据 / 校正值)
|
||
- 保存校正后的反射率文件
|
||
|
||
依赖:
|
||
- numpy
|
||
- GDAL - 用于读取和保存ENVI格式高光谱数据
|
||
- pathlib
|
||
|
||
使用方法:
|
||
python redlence.py <航带文件夹路径> <校正文件路径> [输出文件夹路径]
|
||
|
||
文件要求:
|
||
- 航带文件夹中应包含 .bil/.bip/.bsq 文件及其对应的 .hdr 头文件
|
||
- 校正文件为ENVI ASCII Plot File格式,包含波长和校正值两列数据
|
||
"""
|
||
|
||
import numpy as np
|
||
import os
|
||
import sys
|
||
from pathlib import Path
|
||
from typing import Tuple, List, Dict, Optional
|
||
import argparse
|
||
|
||
# 可选:GDAL库用于读取和保存ENVI格式文件
|
||
try:
|
||
from osgeo import gdal
|
||
# GDAL性能优化配置
|
||
|
||
GDAL_AVAILABLE = True
|
||
except ImportError:
|
||
GDAL_AVAILABLE = False
|
||
print("警告: GDAL不可用,请安装GDAL")
|
||
|
||
|
||
def parse_correction_file(correction_file: str) -> Tuple[np.ndarray, np.ndarray]:
|
||
"""
|
||
解析ENVI ASCII Plot File格式的反射率校正文件(优化版本)
|
||
|
||
参数:
|
||
-----------
|
||
correction_file : str
|
||
校正文件路径
|
||
|
||
返回:
|
||
-----------
|
||
wavelengths : np.ndarray
|
||
波长数组 (nm)
|
||
corrections : np.ndarray
|
||
校正值数组
|
||
"""
|
||
try:
|
||
# 使用numpy的loadtxt加速读取(跳过标题行)
|
||
# 首先找到数据开始的行号
|
||
with open(correction_file, 'r', encoding='utf-8') as f:
|
||
lines = f.readlines()
|
||
|
||
# 找到数据开始的行索引
|
||
data_start_idx = 0
|
||
for i, line in enumerate(lines):
|
||
line = line.strip()
|
||
if not line or line.startswith(';'):
|
||
continue
|
||
if 'ENVI ASCII Plot File' in line or 'Column 1:' in line or 'Column 2:' in line:
|
||
continue
|
||
# 找到第一个可能是数据行的行
|
||
try:
|
||
parts = line.split()
|
||
if len(parts) >= 2:
|
||
float(parts[0]), float(parts[1])
|
||
data_start_idx = i
|
||
break
|
||
except (ValueError, IndexError):
|
||
continue
|
||
|
||
if data_start_idx == 0 and not lines:
|
||
raise ValueError("校正文件中未找到有效的数据行")
|
||
|
||
# 使用numpy loadtxt从数据开始行读取
|
||
data = np.loadtxt(correction_file, skiprows=data_start_idx, dtype=float)
|
||
|
||
if data.ndim == 1:
|
||
# 如果只有一行数据
|
||
wavelengths = np.array([data[0]])
|
||
corrections = np.array([data[1]])
|
||
else:
|
||
wavelengths = data[:, 0]
|
||
corrections = data[:, 1]
|
||
|
||
print(f"✅ 成功解析校正文件: {correction_file}")
|
||
print(f" 数据点数: {len(wavelengths)}")
|
||
print(f" 波长范围: {wavelengths.min():.1f} - {wavelengths.max():.1f} nm")
|
||
print(f" 校正值范围: {corrections.min():.6f} - {corrections.max():.6f}")
|
||
|
||
return wavelengths, corrections
|
||
|
||
except Exception as e:
|
||
# 如果numpy loadtxt失败,回退到原始方法
|
||
print(f"⚠️ numpy加速读取失败,回退到逐行读取: {e}")
|
||
return parse_correction_file_fallback(correction_file)
|
||
|
||
|
||
def parse_correction_file_fallback(correction_file: str) -> Tuple[np.ndarray, np.ndarray]:
|
||
"""
|
||
回退方法:使用原始的逐行解析方式
|
||
"""
|
||
wavelengths = []
|
||
corrections = []
|
||
|
||
try:
|
||
with open(correction_file, 'r', encoding='utf-8') as f:
|
||
lines = f.readlines()
|
||
|
||
# 跳过标题行,找到数据开始
|
||
for line in lines:
|
||
line = line.strip()
|
||
|
||
# 跳过空行和注释
|
||
if not line or line.startswith(';'):
|
||
continue
|
||
|
||
# 跳过标题
|
||
if 'ENVI ASCII Plot File' in line or 'Column 1:' in line or 'Column 2:' in line:
|
||
continue
|
||
|
||
# 尝试解析数据行(波长 校正值)
|
||
try:
|
||
parts = line.split()
|
||
if len(parts) >= 2:
|
||
wavelength = float(parts[0])
|
||
correction = float(parts[1])
|
||
wavelengths.append(wavelength)
|
||
corrections.append(correction)
|
||
except (ValueError, IndexError):
|
||
# 如果这一行不是数据,可能是其他格式,跳过
|
||
continue
|
||
|
||
if not wavelengths:
|
||
raise ValueError("校正文件中未找到有效的数据行")
|
||
|
||
wavelengths = np.array(wavelengths, dtype=float)
|
||
corrections = np.array(corrections, dtype=float)
|
||
|
||
return wavelengths, corrections
|
||
|
||
except Exception as e:
|
||
raise RuntimeError(f"解析校正文件失败: {correction_file}, 错误: {e}")
|
||
|
||
|
||
def find_hyperspectral_files(folder_path: str) -> List[str]:
|
||
"""
|
||
查找文件夹中的高光谱数据文件
|
||
|
||
支持的格式:.bil, .bip, .bsq等ENVI格式文件
|
||
注意:GDAL可以直接读取ENVI格式文件,会自动查找对应的.hdr头文件
|
||
|
||
参数:
|
||
-----------
|
||
folder_path : str
|
||
文件夹路径
|
||
|
||
返回:
|
||
-----------
|
||
file_list : List[str]
|
||
高光谱数据文件路径列表 (.bil/.bip/.bsq文件)
|
||
"""
|
||
folder = Path(folder_path)
|
||
if not folder.exists():
|
||
raise FileNotFoundError(f"文件夹不存在: {folder_path}")
|
||
|
||
# 支持的ENVI格式扩展名
|
||
supported_extensions = ['.bil', '.bip', '.bsq','.dat']
|
||
|
||
hyperspectral_files = []
|
||
for ext in supported_extensions:
|
||
files = list(folder.glob(f'*{ext}'))
|
||
hyperspectral_files.extend([str(f) for f in files])
|
||
|
||
# 去重(防止同一个文件被多次识别)
|
||
hyperspectral_files = list(set(hyperspectral_files))
|
||
|
||
if not hyperspectral_files:
|
||
print(f"警告: 在文件夹 {folder_path} 中未找到高光谱数据文件")
|
||
print("支持的格式: .bil, .bip, .bsq")
|
||
|
||
return sorted(hyperspectral_files)
|
||
|
||
|
||
def load_hyperspectral_data(file_path: str) -> Tuple[np.ndarray, Dict]:
|
||
"""
|
||
使用GDAL读取高光谱数据文件 - 流式版本
|
||
|
||
不加载整个立方体,只返回数据集句柄和元数据
|
||
|
||
参数:
|
||
-----------
|
||
file_path : str
|
||
高光谱数据文件路径 (.bil/.bip/.bsq)
|
||
|
||
返回:
|
||
-----------
|
||
dataset : gdal.Dataset
|
||
GDAL数据集对象(用于流式读取)
|
||
metadata : dict
|
||
元数据信息
|
||
"""
|
||
if not GDAL_AVAILABLE:
|
||
raise RuntimeError("需要GDAL库来读取高光谱文件,请安装GDAL")
|
||
|
||
file_path = Path(file_path)
|
||
|
||
try:
|
||
# 使用GDAL打开ENVI文件
|
||
dataset = gdal.Open(str(file_path), gdal.GA_ReadOnly)
|
||
if dataset is None:
|
||
raise RuntimeError(f"无法打开文件: {file_path}")
|
||
|
||
# 获取基本信息
|
||
lines = dataset.RasterYSize
|
||
samples = dataset.RasterXSize
|
||
bands = dataset.RasterCount
|
||
|
||
# 查找对应的HDR文件
|
||
hdr_file = None
|
||
hdr_candidates = [
|
||
file_path.with_suffix('.hdr'), # datafile.hdr
|
||
file_path.with_suffix(file_path.suffix + '.hdr'), # datafile.ext.hdr
|
||
file_path.parent / f"{file_path.name}.hdr", # datafile.ext.hdr (另一种写法)
|
||
]
|
||
|
||
for candidate in hdr_candidates:
|
||
if candidate.exists():
|
||
hdr_file = candidate
|
||
break
|
||
|
||
# 尝试从HDR文件中提取波长信息
|
||
wavelengths = None
|
||
if hdr_file and hdr_file.exists():
|
||
try:
|
||
# 读取HDR文件内容
|
||
with open(str(hdr_file), 'r', encoding='utf-8', errors='ignore') as f:
|
||
hdr_content = f.read()
|
||
|
||
# 提取波长信息
|
||
import re
|
||
wavelength_match = re.search(r'wavelength\s*=\s*\{([^}]+)\}', hdr_content, re.IGNORECASE)
|
||
if wavelength_match:
|
||
wavelength_str = wavelength_match.group(1)
|
||
# 解析波长值
|
||
wavelength_values = []
|
||
for val in wavelength_str.split(','):
|
||
val = val.strip()
|
||
try:
|
||
wavelength_values.append(float(val))
|
||
except ValueError:
|
||
continue
|
||
if wavelength_values:
|
||
wavelengths = np.array(wavelength_values, dtype=float)
|
||
|
||
except Exception as e:
|
||
print(f"⚠️ 无法从HDR文件读取波长信息: {e}")
|
||
|
||
# 构建元数据
|
||
metadata = {
|
||
'file_path': str(file_path), # 原始数据文件路径
|
||
'hdr_file': str(hdr_file) if hdr_file else None, # 对应的hdr文件路径
|
||
'lines': lines,
|
||
'samples': samples,
|
||
'bands': bands,
|
||
'wavelengths': wavelengths,
|
||
'data_type': 'float32', # 流式读取时的数据类型
|
||
'interleave': 'unknown' # GDAL不直接提供interleave信息
|
||
}
|
||
|
||
print(f"✅ 成功打开高光谱文件: {Path(file_path).name}")
|
||
if hdr_file:
|
||
print(f" HDR文件: {Path(hdr_file).name}")
|
||
print(f" 数据尺寸: {metadata['lines']} x {metadata['samples']} x {metadata['bands']}")
|
||
print(f" 数据类型: {metadata['data_type']} (流式)")
|
||
|
||
if metadata['wavelengths'] is not None:
|
||
print(f" 波长范围: {metadata['wavelengths'][0]:.1f} - {metadata['wavelengths'][-1]:.1f} nm")
|
||
|
||
return dataset, metadata
|
||
|
||
except Exception as e:
|
||
raise RuntimeError(f"读取高光谱文件失败: {file_path}, 错误: {e}")
|
||
|
||
|
||
def interpolate_corrections(wavelengths_data: np.ndarray, wavelengths_corr: np.ndarray,
|
||
corrections: np.ndarray) -> np.ndarray:
|
||
"""
|
||
将校正值插值到数据波长上
|
||
|
||
参数:
|
||
-----------
|
||
wavelengths_data : np.ndarray
|
||
数据文件的波长数组
|
||
wavelengths_corr : np.ndarray
|
||
校正文件的波长数组
|
||
corrections : np.ndarray
|
||
校正值数组
|
||
|
||
返回:
|
||
-----------
|
||
interpolated_corrections : np.ndarray
|
||
插值后的校正值数组
|
||
"""
|
||
if wavelengths_data is None:
|
||
raise ValueError("数据文件缺少波长信息,无法进行校正")
|
||
|
||
try:
|
||
# 使用线性插值
|
||
from scipy.interpolate import interp1d
|
||
interp_func = interp1d(wavelengths_corr, corrections,
|
||
kind='linear', bounds_error=False,
|
||
fill_value='extrapolate')
|
||
interpolated = interp_func(wavelengths_data)
|
||
|
||
print(f"✅ 成功插值校正值到数据波长")
|
||
print(f" 数据波段数: {len(wavelengths_data)}")
|
||
print(f" 校正数据点数: {len(wavelengths_corr)}")
|
||
print(f" 插值范围: {interpolated.min():.3f} - {interpolated.max():.3f}")
|
||
|
||
return interpolated
|
||
|
||
except ImportError:
|
||
# 如果没有scipy,使用numpy的interp
|
||
print("警告: scipy不可用,使用numpy进行线性插值")
|
||
interpolated = np.interp(wavelengths_data, wavelengths_corr, corrections)
|
||
|
||
print(f"✅ 使用numpy插值校正值到数据波长")
|
||
print(f" 插值范围: {interpolated.min():.3f} - {interpolated.max():.3f}")
|
||
|
||
return interpolated
|
||
|
||
|
||
def apply_reflectance_correction_streaming(input_dataset, output_dataset, corrections: np.ndarray, block_size: int = 1024):
|
||
"""
|
||
应用反射率校正 - 流式版本
|
||
|
||
按块读取→校正→写入,避免加载整个立方体
|
||
|
||
参数:
|
||
-----------
|
||
input_dataset : gdal.Dataset
|
||
输入数据集
|
||
output_dataset : gdal.Dataset
|
||
输出数据集
|
||
corrections : np.ndarray
|
||
校正值数组 (bands,)
|
||
block_size : int
|
||
块大小(行数)
|
||
"""
|
||
lines = input_dataset.RasterYSize
|
||
samples = input_dataset.RasterXSize
|
||
bands = input_dataset.RasterCount
|
||
|
||
if bands != len(corrections):
|
||
raise ValueError(f"数据波段数 ({bands}) 与校正值数量 ({len(corrections)}) 不匹配")
|
||
|
||
print("🔢 正在应用反射率校正(流式处理,向量化加速)...")
|
||
|
||
# 用“乘倒数”替代逐元素除法,并在无效校正值(0/NaN/Inf)处直接置零
|
||
corrections = np.asarray(corrections, dtype=np.float32)
|
||
scale = np.zeros((bands,), dtype=np.float32)
|
||
valid = np.isfinite(corrections) & (corrections != 0)
|
||
scale[valid] = np.float32(10000.0) / corrections[valid]
|
||
scale_3d = scale[:, None, None]
|
||
|
||
# 提前缓存输出波段对象,避免循环内重复 GetRasterBand
|
||
output_bands = [output_dataset.GetRasterBand(i + 1) for i in range(bands)]
|
||
|
||
total_blocks = (lines + block_size - 1) // block_size
|
||
|
||
# 按块处理:每个块一次性读全波段并向量化计算
|
||
for block_idx, y_start in enumerate(range(0, lines, block_size), start=1):
|
||
y_end = min(y_start + block_size, lines)
|
||
actual_block_size = y_end - y_start
|
||
|
||
# 低频打印进度,避免大量I/O拖慢处理
|
||
if block_idx == 1 or block_idx == total_blocks or block_idx % 10 == 0:
|
||
print(f" 处理块 {block_idx}/{total_blocks}: 行 {y_start}-{y_end-1} ({actual_block_size} 行)")
|
||
|
||
block = input_dataset.ReadAsArray(0, y_start, samples, actual_block_size)
|
||
if block is None:
|
||
raise RuntimeError(f"读取数据块失败: y_start={y_start}, block_size={actual_block_size}")
|
||
|
||
# 统一维度为 (bands, block_y, samples)
|
||
if block.ndim == 2:
|
||
block = block[np.newaxis, :, :]
|
||
|
||
block_f = block.astype(np.float32, copy=False)
|
||
np.multiply(block_f, scale_3d, out=block_f, casting='unsafe')
|
||
np.clip(block_f, 0, 65535, out=block_f)
|
||
block_u16 = block_f.astype(np.uint16, copy=False)
|
||
|
||
# GDAL按波段写出
|
||
for band_idx in range(bands):
|
||
output_bands[band_idx].WriteArray(block_u16[band_idx, :, :], 0, y_start)
|
||
|
||
print("✅ 成功应用反射率校正(流式处理,向量化加速)")
|
||
|
||
|
||
def save_corrected_data_streaming(input_dataset, corrections: np.ndarray, output_file: str,
|
||
wavelengths: Optional[np.ndarray] = None, source_hdr: Optional[str] = None):
|
||
"""
|
||
保存校正后的反射率数据为ENVI格式 - 流式版本
|
||
|
||
参数:
|
||
-----------
|
||
input_dataset : gdal.Dataset
|
||
输入数据集
|
||
corrections : np.ndarray
|
||
校正值数组
|
||
output_file : str
|
||
输出文件路径(不含扩展名)
|
||
wavelengths : np.ndarray, optional
|
||
波长信息
|
||
source_hdr : str, optional
|
||
源HDR文件路径,用于复制HDR内容
|
||
"""
|
||
lines = input_dataset.RasterYSize
|
||
samples = input_dataset.RasterXSize
|
||
bands = input_dataset.RasterCount
|
||
|
||
# 确保输出目录存在
|
||
output_path = Path(output_file)
|
||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
||
# 输出文件路径
|
||
bil_file = str(output_path.with_suffix('.dat'))
|
||
hdr_file = str(output_path.with_suffix('.hdr'))
|
||
|
||
try:
|
||
# 创建输出数据集
|
||
driver = gdal.GetDriverByName('ENVI')
|
||
output_dataset = driver.Create(bil_file, samples, lines, bands, gdal.GDT_UInt16,
|
||
options=['INTERLEAVE=BSQ'])
|
||
|
||
if output_dataset is None:
|
||
raise RuntimeError(f"无法创建ENVI数据集: {bil_file}")
|
||
|
||
# 设置NoData值
|
||
for band_idx in range(bands):
|
||
output_band = output_dataset.GetRasterBand(band_idx + 1)
|
||
output_band.SetNoDataValue(0)
|
||
|
||
# 应用流式校正和保存
|
||
apply_reflectance_correction_streaming(input_dataset, output_dataset, corrections)
|
||
|
||
# 关闭输出数据集
|
||
output_dataset = None
|
||
|
||
# 处理HDR文件
|
||
if source_hdr and Path(source_hdr).exists():
|
||
import shutil
|
||
shutil.copy2(source_hdr, hdr_file)
|
||
print(f"✅ 已复制源HDR文件: {Path(source_hdr).name}")
|
||
else:
|
||
# 创建HDR头文件
|
||
create_envi_header(hdr_file, lines, samples, bands, wavelengths, None)
|
||
|
||
print(f"✅ 成功保存校正结果:")
|
||
print(f" 数据文件: {bil_file}")
|
||
print(f" 头文件: {hdr_file}")
|
||
print(f" 数据尺寸: {lines} x {samples} x {bands}")
|
||
print(f" 数据类型: uint16 (反射率x10000)")
|
||
print(f" 处理方式: 流式处理")
|
||
|
||
except Exception as e:
|
||
raise RuntimeError(f"保存文件失败: {output_file}, 错误: {e}")
|
||
|
||
|
||
def save_with_gdal(data: np.ndarray, bil_file: str, wavelengths: Optional[np.ndarray] = None,
|
||
source_file: Optional[str] = None, source_hdr: Optional[str] = None):
|
||
"""
|
||
使用GDAL保存ENVI格式文件
|
||
"""
|
||
lines, samples, bands = data.shape
|
||
hdr_file = bil_file.replace('.dat', '.hdr')
|
||
|
||
# 创建GDAL驱动
|
||
driver = gdal.GetDriverByName('ENVI')
|
||
|
||
# 创建数据集 - 使用uint16格式优化性能和文件大小
|
||
dataset = driver.Create(bil_file, samples, lines, bands, gdal.GDT_UInt16,
|
||
options=['INTERLEAVE=BSQ'])
|
||
|
||
if dataset is None:
|
||
raise RuntimeError(f"无法创建ENVI数据集: {bil_file}")
|
||
|
||
try:
|
||
# 设置元数据
|
||
metadata = dataset.GetMetadata()
|
||
metadata['DESCRIPTION'] = 'Reflectance corrected hyperspectral data using Python'
|
||
metadata['SENSOR_TYPE'] = 'Hyperspectral'
|
||
metadata['DATA_UNITS'] = 'Reflectance'
|
||
metadata['PROCESSING_ALGORITHM'] = 'Reflectance Correction'
|
||
metadata['CREATION_DATE'] = str(np.datetime64('now'))
|
||
|
||
if source_file:
|
||
metadata['SOURCE_FILE'] = Path(source_file).name
|
||
|
||
# 添加波长信息到元数据
|
||
if wavelengths is not None and len(wavelengths) == bands:
|
||
metadata['wavelength_units'] = 'nm'
|
||
for i, wl in enumerate(wavelengths):
|
||
metadata[f'wavelength_{i+1}'] = str(wl)
|
||
|
||
dataset.SetMetadata(metadata)
|
||
|
||
# 写入数据 - 优化版本:转换为uint16格式
|
||
print(f"💾 正在写入 {bands} 个波段的数据...")
|
||
|
||
# 将反射率转换为uint16(乘以10000以保留4位小数精度,裁剪到有效范围)
|
||
data_uint16 = np.clip(data * 10000, 0, 65535).astype(np.uint16, copy=False)
|
||
|
||
for band_idx in range(bands):
|
||
band = dataset.GetRasterBand(band_idx + 1)
|
||
band_data = data_uint16[:, :, band_idx]
|
||
band.WriteArray(band_data)
|
||
band.SetNoDataValue(0) # uint16的NoData值为0
|
||
|
||
# 简化:只在必要时设置波段描述(可选优化:完全移除以提升速度)
|
||
# if wavelengths is not None and band_idx < len(wavelengths):
|
||
# band.SetDescription(f'{wavelengths[band_idx]:.1f} nm')
|
||
|
||
# 每处理100个波段显示一次进度(减少打印频率)
|
||
if bands >= 100 and (band_idx + 1) % 100 == 0:
|
||
print(f" 已写入 {band_idx + 1}/{bands} 个波段")
|
||
|
||
print(f"✅ 数据写入完成 ({bands} 个波段)")
|
||
|
||
# 创建HDR头文件(GDAL会自动创建基本的HDR,但我们需要添加更多信息)
|
||
create_envi_header(hdr_file, lines, samples, bands, wavelengths, source_file)
|
||
|
||
finally:
|
||
# 关闭数据集
|
||
dataset = None
|
||
|
||
|
||
def save_with_numpy(data: np.ndarray, bil_file: str, hdr_file: str,
|
||
wavelengths: Optional[np.ndarray] = None, source_file: Optional[str] = None, source_hdr: Optional[str] = None):
|
||
"""
|
||
使用numpy保存ENVI格式文件(GDAL不可用时的回退方案,优化版本)
|
||
"""
|
||
lines, samples, bands = data.shape
|
||
|
||
print(f"💾 正在保存 {bands} 个波段的数据...")
|
||
|
||
# 保存二进制数据 - uint16格式:预先转换数据类型
|
||
with open(bil_file, 'wb') as f:
|
||
# 将反射率转换为uint16(乘以10000以保留4位小数精度,裁剪到有效范围)
|
||
data_to_save = np.clip(data * 10000, 0, 65535).astype(np.uint16, copy=False)
|
||
data_to_save.tofile(f)
|
||
|
||
print(f"✅ 数据文件写入完成")
|
||
|
||
# 如果有源HDR文件,直接复制
|
||
if source_hdr and Path(source_hdr).exists():
|
||
import shutil
|
||
shutil.copy2(source_hdr, hdr_file)
|
||
print(f"✅ 已复制源HDR文件: {Path(source_hdr).name}")
|
||
else:
|
||
# 创建HDR头文件
|
||
create_envi_header(hdr_file, lines, samples, bands, wavelengths, source_file)
|
||
|
||
|
||
def create_envi_header(hdr_file: str, lines: int, samples: int, bands: int,
|
||
wavelengths: Optional[np.ndarray] = None, source_file: Optional[str] = None):
|
||
"""
|
||
创建ENVI格式的HDR头文件
|
||
"""
|
||
with open(hdr_file, 'w', encoding='utf-8') as f:
|
||
f.write("ENVI\n")
|
||
f.write("description = {\n")
|
||
f.write(" Reflectance corrected hyperspectral data\n")
|
||
f.write(" Processed with Python reflectance correction}\n")
|
||
f.write(f"samples = {samples}\n")
|
||
f.write(f"lines = {lines}\n")
|
||
f.write(f"bands = {bands}\n")
|
||
f.write("header offset = 0\n")
|
||
f.write("file type = ENVI Standard\n")
|
||
f.write("data type = 12\n") # uint16
|
||
f.write("interleave = bsq\n")
|
||
f.write("sensor type = Hyperspectral\n")
|
||
f.write("byte order = 0\n") # little-endian
|
||
f.write("reflectance scale factor = 10000\n") # 反射率缩放因子
|
||
|
||
# 添加波长信息
|
||
if wavelengths is not None and len(wavelengths) == bands:
|
||
f.write("wavelength units = nm\n")
|
||
f.write("wavelength = {\n")
|
||
for i, wl in enumerate(wavelengths):
|
||
f.write(f" {wl}")
|
||
if i < len(wavelengths) - 1:
|
||
f.write(",")
|
||
if (i + 1) % 10 == 0: # 每10个波长换行
|
||
f.write("\n")
|
||
f.write("}\n")
|
||
|
||
# 添加波段名称
|
||
f.write("band names = {\n")
|
||
for i, wl in enumerate(wavelengths):
|
||
f.write(f" {wl:.1f} nm")
|
||
if i < len(wavelengths) - 1:
|
||
f.write(",")
|
||
if (i + 1) % 8 == 0: # 每8个波段名称换行
|
||
f.write("\n")
|
||
f.write("}\n")
|
||
|
||
|
||
|
||
|
||
def process_single_file(hyp_file: str, wavelengths_corr: np.ndarray, corrections: np.ndarray,
|
||
output_dir: str) -> bool:
|
||
"""
|
||
处理单个高光谱文件
|
||
|
||
参数:
|
||
-----------
|
||
hyp_file : str
|
||
高光谱文件路径
|
||
wavelengths_corr : np.ndarray
|
||
校正文件的波长
|
||
corrections : np.ndarray
|
||
校正值
|
||
output_dir : str
|
||
输出目录
|
||
|
||
返回:
|
||
-----------
|
||
success : bool
|
||
处理是否成功
|
||
"""
|
||
try:
|
||
print(f"\n🔄 处理文件: {Path(hyp_file).name}")
|
||
|
||
# 读取高光谱数据(流式)
|
||
input_dataset, metadata = load_hyperspectral_data(hyp_file)
|
||
|
||
try:
|
||
# 获取数据波长
|
||
wavelengths_data = metadata.get('wavelengths')
|
||
if wavelengths_data is None:
|
||
print(f"⚠️ 跳过文件 {Path(hyp_file).name}: 缺少波长信息")
|
||
return False
|
||
|
||
# 判断数据波长和校正波长是否一致
|
||
wavelengths_data = np.array(wavelengths_data)
|
||
if len(wavelengths_data) == len(wavelengths_corr) and np.allclose(wavelengths_data, wavelengths_corr, rtol=1e-6):
|
||
# 波长完全一致,直接使用校正值
|
||
print("✅ 数据波长与校正波长完全一致,无需插值")
|
||
interpolated_corrections = corrections
|
||
else:
|
||
# 波长不一致,需要插值
|
||
print("🔄 数据波长与校正波长不一致,进行插值")
|
||
interpolated_corrections = interpolate_corrections(
|
||
wavelengths_data, wavelengths_corr, corrections
|
||
)
|
||
|
||
# 生成输出文件名
|
||
input_name = Path(hyp_file).stem
|
||
output_file = Path(output_dir) / f"{input_name}_reflectance"
|
||
|
||
# 流式保存结果(包含校正)
|
||
save_corrected_data_streaming(input_dataset, interpolated_corrections, str(output_file),
|
||
wavelengths_data, metadata.get('hdr_file'))
|
||
|
||
finally:
|
||
# 确保关闭输入数据集
|
||
input_dataset = None
|
||
|
||
print(f"✅ 成功处理文件: {Path(hyp_file).name}")
|
||
return True
|
||
|
||
except Exception as e:
|
||
print(f"❌ 处理文件失败: {Path(hyp_file).name}, 错误: {e}")
|
||
return False
|
||
|
||
|
||
def batch_process(hyperspectral_dir: str, correction_file: str, output_dir: str) -> Dict[str, int]:
|
||
"""
|
||
批量处理文件夹中的所有高光谱文件
|
||
|
||
参数:
|
||
-----------
|
||
hyperspectral_dir : str
|
||
高光谱文件文件夹
|
||
correction_file : str
|
||
校正文件路径
|
||
output_dir : str
|
||
输出目录
|
||
|
||
返回:
|
||
-----------
|
||
results : dict
|
||
处理结果统计
|
||
"""
|
||
print("=" * 60)
|
||
print("🏁 开始高光谱反射率校正批量处理")
|
||
print("=" * 60)
|
||
|
||
# 检查依赖
|
||
if not GDAL_AVAILABLE:
|
||
print("❌ 错误: 需要安装GDAL库")
|
||
return {'total': 0, 'success': 0, 'failed': 0}
|
||
|
||
# 确保输出目录存在
|
||
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
||
|
||
try:
|
||
# 解析校正文件
|
||
print("📖 解析校正文件...")
|
||
wavelengths_corr, corrections = parse_correction_file(correction_file)
|
||
|
||
# 查找高光谱文件
|
||
print(f"\n📂 查找高光谱文件...")
|
||
hyp_files = find_hyperspectral_files(hyperspectral_dir)
|
||
|
||
if not hyp_files:
|
||
print("❌ 未找到高光谱文件,处理终止")
|
||
return {'total': 0, 'success': 0, 'failed': 0}
|
||
|
||
print(f"找到 {len(hyp_files)} 个高光谱数据文件:")
|
||
for f in hyp_files:
|
||
print(f" - {Path(f).name} (需要对应的.hdr头文件)")
|
||
|
||
# 处理每个文件
|
||
results = {'total': len(hyp_files), 'success': 0, 'failed': 0}
|
||
|
||
for hyp_file in hyp_files:
|
||
success = process_single_file(hyp_file, wavelengths_corr, corrections, output_dir)
|
||
if success:
|
||
results['success'] += 1
|
||
else:
|
||
results['failed'] += 1
|
||
|
||
# 输出总结
|
||
print("\n" + "=" * 60)
|
||
print("📊 处理完成总结:")
|
||
print(f" 总文件数: {results['total']}")
|
||
print(f" 成功处理: {results['success']}")
|
||
print(f" 处理失败: {results['failed']}")
|
||
print(f" 输出目录: {output_dir}")
|
||
print("=" * 60)
|
||
|
||
return results
|
||
|
||
except Exception as e:
|
||
print(f"❌ 批量处理失败: {e}")
|
||
return {'total': 0, 'success': 0, 'failed': 0}
|
||
|
||
|
||
def main():
|
||
"""
|
||
主函数 - 命令行接口
|
||
"""
|
||
parser = argparse.ArgumentParser(
|
||
description='高光谱反射率校正工具',
|
||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||
epilog="""
|
||
文件要求:
|
||
- 航带文件夹中应包含ENVI格式的高光谱数据文件(.bil/.bip/.bsq)和对应的头文件(.hdr)
|
||
- 校正文件应为ENVI ASCII Plot File格式,包含波长和校正值两列数据
|
||
|
||
使用示例:
|
||
python redlence.py /path/to/hyperspectral/folder /path/to/correction.txt
|
||
python redlence.py /path/to/hyperspectral/folder /path/to/correction.txt /path/to/output/folder
|
||
"""
|
||
)
|
||
|
||
parser.add_argument('hyperspectral_dir', help='包含高光谱文件的文件夹路径')
|
||
parser.add_argument('correction_file', help='反射率校正文件路径 (ENVI ASCII Plot File格式)')
|
||
parser.add_argument('output_dir', nargs='?', default=None,
|
||
help='输出目录路径 (可选,默认在输入文件夹下创建output文件夹)')
|
||
|
||
args = parser.parse_args()
|
||
|
||
# 设置默认输出目录
|
||
if args.output_dir is None:
|
||
args.output_dir = str(Path(args.hyperspectral_dir) / 'reflectance_output')
|
||
|
||
# 检查输入文件存在
|
||
if not Path(args.correction_file).exists():
|
||
print(f"❌ 校正文件不存在: {args.correction_file}")
|
||
return 1
|
||
|
||
if not Path(args.hyperspectral_dir).exists():
|
||
print(f"❌ 高光谱文件夹不存在: {args.hyperspectral_dir}")
|
||
return 1
|
||
|
||
# 执行批量处理
|
||
results = batch_process(args.hyperspectral_dir, args.correction_file, args.output_dir)
|
||
|
||
# 返回适当的退出码
|
||
if results['success'] > 0:
|
||
print("✅ 处理完成!")
|
||
return 0
|
||
else:
|
||
print("❌ 没有成功处理任何文件")
|
||
return 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
exit(main()) |