Python读取Landsat 8/9遥感数据并完成图像预处理及波段运算

文摘   2024-12-15 00:16   山西  

      摘要


Landsat 8和Landsat 9是美国航空航天局(NASA)和美国地质调查局(USGS)共同运营的地球观测卫星,分别于2013年和2021年发射升空。两颗卫星都搭载了操作性陆地成像仪(OLI/OLI-2)和热红外传感器(TIRS/TIRS-2),能够提供11个波段的地球观测数据,空间分辨率从15米到100米不等。它们每16天完成一次全球覆盖,广泛应用于土地利用监测、农业评估、森林管理、水资源研究等领域,是目前全球最重要的地球观测卫星系统之一。
本期文章以英国的东英格兰(East of England)地区为例,基于Python和使用两期Landsat影像,完成遥感图像的图像信息读取、辐射定标和大气校正、图像裁剪与图像拼接、波段统计、波段运算等操作。

      准备工作


1.准备数据
英国地区的Landsat 8和Landsat 9影像,获取自USGS网站,其中Landsat 8影像采集于2022年8月11日,Landsat 9影像采集于2023年9月7日,云量均小于10%,选用的是L1产品,需要经过辐射定标和大气校正。
英国东英格兰区域的行政边界,来源于英国的政府数字服务(The Government Digital Service ,GDS)。


2.配置环境
本代码使用的Python库包括:rasterio用于处理地理空间栅格数据;numpy用于科学计算和数组操作;matplotlib用于数据可视化;matplotlib.colors提供颜色映射功能;rasterio.warp用于空间坐标转换;pyproj处理地理坐标系统;os用于文件系统操作。这些库是地理空间数据处理和可视化常用的基础环境,下面提供了环境的配置方法。
import rasterioimport numpy as npimport matplotlib.pyplot as pltfrom matplotlib.colors import LinearSegmentedColormapfrom rasterio.warp import transform_boundsfrom pyproj import CRSimport os
# 环境配置方法:'''1. 使用conda安装:conda install -c conda-forge rasterioconda install numpy matplotlibconda install -c conda-forge pyproj
2. 使用pip安装:pip install rasteriopip install numpypip install matplotlibpip install pyproj'''

      处理步骤


1.读取数据
读取数据的信息可以分为两部分,第一部分是读取数据的元数据文件(MTL文件),获取数据的一些基本的产品信息。MTL(Metadata) 文件是以 .txt 格式存储的元数据文件,包含了影像的重要信息,这些信息尤其是辐射定标参数和成像条件等在进行辐射定标和大气校正时十分重要。
def extract_important_info(info):    """提取重要的MTL信息"""    important_fields = {        'DATE_ACQUIRED': '获取日期',        'SCENE_CENTER_TIME': '场景中心时间',        'CLOUD_COVER': '云覆盖率',        'SUN_ELEVATION': '太阳高度角',        'SPACECRAFT_ID': '卫星ID',        'WRS_PATH': 'WRS路径',        'WRS_ROW': 'WRS行'    }        result = {}    for eng_key, ch_key in important_fields.items():        for key in info:            if eng_key in key:                result[ch_key] = info[key]                break    return result
此外还可以通过直接读取影像,来获取图像形状、波段数量、数据类型、坐标系和分辨率等,这些信息在Python处理遥感图像的过程中是非常重要的参数。
def read_image_info(folder_path):    """读取影像的基本信息"""    try:        # 查找tif文件        tif_files = [f for f in os.listdir(folder_path) if f.endswith('.TIF') and not f.endswith('_QA.TIF')]                if not tif_files:            print("未找到TIF影像文件")            return            # 读取第一个找到的TIF文件        tif_path = os.path.join(folder_path, tif_files[0])        with rasterio.open(tif_path) as src:            print("\n影像基本信息:")            print("文件名:", tif_files[0])            print("图像的宽度 (Width):", src.width)            print("图像的高度 (Height):", src.height)            print("波段数 (Number of bands):", src.count)            print("坐标参考系 (CRS):", src.crs)            print("分辨率 (Resolution):", src.res)            print("边界 (Bounds):", src.bounds)    except Exception as e:        print(f"读取影像信息时出错: {str(e)}")
读取的结果如下(这里只展示一景影像)


2.辐射定标与大气校正
这里,辐射定标采用了线性变换方法,使用MTL文件中提供的辐射定标系数(ML和AL),将DN值转换为TOA辐射亮度,公式为:TOA辐射亮度 = ML × DN + AL。大气校正则采用了简化的(Dark Object Subtraction,DOS)方法,通过考虑日地距离(d)、太阳高度角(θse)和大气层顶太阳辐照度(ESUN)来计算地表反射率,计算公式为:地表反射率 = π × (TOA辐射亮度 × d²) / (ESUN × cos(θse))。这是一种相对简单的大气校正方法,适用于快速处理,但精度可能不如FLAASH等专业大气校正软件。
def process_folder(input_folder, output_folder, esun_values):    """处理单个文件夹的数据"""    # 查找MTL文件    mtl_files = [f for f in os.listdir(input_folder) if f.endswith('_MTL.txt')]    if not mtl_files:        print(f"未在 {input_folder} 中找到MTL文件")        return
   # 读取MTL信息    mtl_path = os.path.join(input_folder, mtl_files[0])    mtl_info = read_mtl_info(mtl_path)        if not mtl_info:        return
   # 计算日地距离    date_acquired = datetime.strptime(mtl_info['DATE_ACQUIRED'], '%Y-%m-%d')    doy = date_acquired.timetuple().tm_yday    d = 1 - 0.01672 * np.cos(0.9856 * (doy - 4))
   # 获取太阳高度角    sun_elevation = float(mtl_info['SUN_ELEVATION'])
   # 处理每个波段    tif_files = [f for f in os.listdir(input_folder)                 if f.endswith('.TIF') and 'B' in f                 and not f.endswith('_QA.TIF')]
   for tif_file in tif_files:        try:            band_number = int(tif_file.split('_B')[1].split('.')[0])                        # 获取定标参数            ml_key = f'RADIANCE_MULT_BAND_{band_number}'            al_key = f'RADIANCE_ADD_BAND_{band_number}'                        if ml_key not in mtl_info or al_key not in mtl_info:                print(f"波段 {band_number} 缺少定标参数")                continue
           ml = float(mtl_info[ml_key])            al = float(mtl_info[al_key])
           # 处理影像            input_path = os.path.join(input_folder, tif_file)            output_path = os.path.join(output_folder, f'SR_B{band_number}.tif')                        process_band(input_path, output_path, ml, al, d,                        esun_values[band_number], sun_elevation)                        print(f"完成波段 {band_number} 处理: {tif_file}")
       except Exception as e:            print(f"处理波段 {tif_file} 时出错: {str(e)}")
校正结果如下:


3.图像裁剪和图像拼接
在GIS软件中加载原始的波段数据(B5),叠加英国东英格兰区域的范围,可以查看影像的覆盖范围。
原始的数据很大,我们只需要将研究区作为我们的实验范围,使用以下代码将数据裁剪到英国东英格兰区域。
def clip_with_shapefile(input_raster, output_raster, shapefile):    """使用shapefile裁剪栅格数据"""    try:        # 读取shapefile        gdf = gpd.read_file(shapefile)        with rasterio.open(input_raster) as src:            if gdf.crs != src.crs:                gdf = gdf.to_crs(src.crs)            # 获取研究区的几何形状            shapes = [feature['geometry'] for _, feature in gdf.iterrows()]            # 执行裁剪            out_image, out_transform = mask(src, shapes, crop=True)            out_meta = src.meta.copy()            out_meta.update({                "driver": "GTiff",                "height": out_image.shape[1],                "width": out_image.shape[2],                "transform": out_transform            })            # 保存裁剪结果            with rasterio.open(output_raster, "w", **out_meta) as dest:                dest.write(out_image)           print(f"裁剪完成: {output_raster}")        return True            except Exception as e:        print(f"裁剪过程出错: {str(e)}")        return False
裁剪后的影像如下(WRS是Landsat对卫星影像进行编目和定位的全球参考系统):

WRS:201024

WRS:201023

由于Landsat的一幅影像不能将整个区域完全覆盖,所以需要使用多幅影像进行拼接,形成一个完整的研究区影像。
for band, files in band_groups.items():        print(f"\n处理波段: {band}")        print(f"发现 {len(files)} 个文件")                try:            src_files = []            for path in files:                src = rasterio.open(path)                src_files.append(src)                        # 拼接影像            mosaic, out_trans = merge(src_files)                        out_meta = src_files[0].meta.copy()
           out_meta.update({                "driver": "GTiff",                "height": mosaic.shape[1],                "width": mosaic.shape[2],                "transform": out_trans            })
           output_filename = f"mosaic_{satellite}_{band}.TIF"            output_path = os.path.join(output_folder, output_filename)
处理好的结果如下:


4.波段统计
Landsat 8\9总共有11个波段,这里选取了波段2到波段7(蓝光到短波红外)这些光谱波段,对提取的区域的波段数据进行统计。
    band_files = {}    for i in range(2, 8):        band_file = os.path.join(satellite_folder, f"mosaic_{os.path.basename(satellite_folder)}_B{i}.TIF")        if os.path.exists(band_file):            band_files[f"B{i}"] = band_file        if not band_files:        print(f"在 {satellite_folder} 中未找到波段文件")        return        # 统计信息    print(f"\n{os.path.basename(satellite_folder)} 波段统计信息:")    stats_data = []    headers = ["波段", "最小值", "最大值", "平均值"]
处理的结果如下:


得到的结果如下:
使用影像数据的不同波段制作直方图。
    for idx, (band_name, file_path) in enumerate(band_files.items()):        row = idx // 3        col = idx % 3                with rasterio.open(file_path) as src:            band_data = src.read(1, masked=True)            valid_data = band_data.compressed()                        data_range = np.percentile(valid_data, [2, 98])            bins = np.linspace(data_range[0], data_range[1], n_bins)                        cmap = LinearSegmentedColormap.from_list('custom', colors, N=n_bins)            colors_list = [cmap(i) for i in np.linspace(0, 1, n_bins)]                        n, bins, patches = axs[row, col].hist(valid_data, bins=bins,                                                  edgecolor='black',                                                  linewidth=1)                        bin_centers = 0.5 * (bins[:-1] + bins[1:])            col_val = np.linspace(0, 1, len(bin_centers))            for c, p in zip(col_val, patches):                plt.setp(p, 'facecolor', cmap(c))                        axs[row, col].set_title(f'{band_name} 直方图', fontsize=12, fontweight='bold')            axs[row, col].set_xlabel('反射率值', fontsize=10)            axs[row, col].set_ylabel('像元数量', fontsize=10)
统计的直方图如下:
5.图像合成和图像拉伸
首先我们可以读取6个波段,然后分别展示,为了突出图像的更多细节,这里使用了2个标准差进行图像拉伸。
    # 绘制每个波段    for idx, (band_name, file_path) in enumerate(band_files.items()):        row = idx // 3        col = idx % 3                with rasterio.open(file_path) as src:            band = src.read(1, masked=True)            band = np.ma.masked_invalid(band)                        # 将UTM坐标转换为经纬度            bounds = src.bounds            left, bottom, right, top = transform_bounds(src.crs, dst_crs,                                                       bounds.left, bounds.bottom,                                                       bounds.right, bounds.top)                        # 标准差拉伸            stretched_band = standard_deviation_stretch(band)                        # 绘制图像            im = axs[row, col].imshow(stretched_band, cmap=cmap,                                     extent=[left, right, bottom, top])            axs[row, col].set_title(f'{band_name}', fontsize=12, fontweight='bold', pad=10)            
处理的结果如下:
接下来我们读取多波段,并使用真彩色(RGB)、不同的假彩色组合进行波段展示,对比不同时间东英格兰地区的地表景观变化。
    # 1. 真彩色合成 (4,3,2)    rgb_natural = create_composite_image(band_files['B4'],                                        band_files['B3'],                                        band_files['B2'])    axs[0, 0].imshow(rgb_natural, extent=[left, right, bottom, top])    axs[0, 0].set_title('真彩色合成 (R:B4 G:B3 B:B2)', fontsize=12, fontweight='bold', pad=10)        # 2. 标准假彩色 (5,4,3)    rgb_false = create_composite_image(band_files['B5'],                                      band_files['B4'],                                      band_files['B3'])    axs[0, 1].imshow(rgb_false, extent=[left, right, bottom, top])    axs[0, 1].set_title('标准假彩色 (R:B5 G:B4 B:B3)', fontsize=12, fontweight='bold', pad=10)        # 3. 城市假彩色 (7,6,4)    rgb_urban = create_composite_image(band_files['B7'],                                      band_files['B6'],                                      band_files['B4'])    axs[1, 0].imshow(rgb_urban, extent=[left, right, bottom, top])    axs[1, 0].set_title('城市假彩色 (R:B7 G:B6 B:B4)', fontsize=12, fontweight='bold', pad=10)        # 4. 农业假彩色 (6,5,4)    rgb_agriculture = create_composite_image(band_files['B6'],                                           band_files['B5'],                                           band_files['B4'])    axs[1, 1].imshow(rgb_agriculture, extent=[left, right, bottom, top])    axs[1, 1].set_title('农业假彩色 (R:B6 G:B5 B:B4)', fontsize=12, fontweight='bold', pad=10)


6.波段运算
通过波段计算我们可以得到一些信息,这里计算一些常用指数,包括: NDVI(植被)、NDBI(建筑)、EVI (增强型植被指数)、SAVI(土壤调节植被)。
# NDVI (归一化植被指数)    ndvi = (nir - red) / (nir + red)        # NDBI (归一化建筑指数)    ndbi = (swir1 - nir) / (swir1 + nir)        # EVI (增强型植被指数)    G = 2.5  # 增益系数    C1 = 6   # 气溶胶阻抗系数1    C2 = 7.5 # 气溶胶阻抗系数2    L_evi = 1  # EVI的土壤调节因子    evi = G * (nir - red) / (nir + C1 * red - C2 * blue + L_evi)        # SAVI (土壤调节植被指数)    savi = ((nir - red) * (1 + L)) / (nir + red + L)

      结果分析


得到的结果可以保存为tif文件,然后在GIS或者其他绘图、统计分析软件中加载,进行后续分析。比如利用阈值分割提取植被、城市,或者将这些变量加入到机器学习的模型训练中作为特征变量参与分类和预测,也可以计算多期数据进行长时间的趋势检验。
这些在本公众号的后续更新中都会有涉及,感兴趣的朋友可以关注本公众号哦!有课程、研究和项目需要也可随时联系博主,期待与您的合作!

参考教程

1.Landsat 8 Data Users Handbook | U.S. Geological Survey

2.Rasterio: access to geospatial raster data

3.NumPy 教程| 菜鸟教程

本期编辑|努力学习遥感知识的Min

校稿|忱喵喵

  PyGeoSense

一起用遥感探索蓝星风景

有需求或者合作欢迎随时联系

遥感小屋
分享遥感相关文章、代码,大家一起交流,互帮互助
 最新文章