从手机拍照到卫星成像:用Python+GDAL带你入门遥感数据处理(附实战代码)

当我们用手机拍摄一张照片时,其实已经在进行最简单的遥感实践——记录物体反射的可见光信息。这种日常行为与专业卫星遥感的核心原理惊人地相似:都是通过传感器捕捉电磁波信息来认识世界。本文将用Python和GDAL库搭建一座桥梁,让开发者能够快速上手处理真实的卫星影像数据。

1. 环境配置与数据准备

1.1 安装GDAL及其Python绑定

GDAL(Geospatial Data Abstraction Library)是处理地理空间数据的瑞士军刀。在Python环境中,我们推荐使用conda进行安装,它能自动解决复杂的依赖关系:

conda create -n rs python=3.8
conda activate rs
conda install -c conda-forge gdal

验证安装是否成功:

from osgeo import gdal
print(gdal.__version__)

提示:Windows用户若遇到路径问题,可将GDAL的bin目录添加到系统环境变量PATH中。

1.2 获取卫星影像数据

Landsat和Sentinel系列卫星提供了免费的开源数据。以Landsat 8为例,可以从USGS EarthExplorer下载:

  1. 注册USGS EarthExplorer账号
  2. 选择数据集"Landsat Collection 2 Level-2"
  3. 划定感兴趣区域和时间范围
  4. 下载包含所有波段的.tar.gz压缩包

典型Landsat 8文件结构:

LC08_L2SP_118038_20201020_20201105_02_T1/
├── LC08_L2SP_118038_20201020_20201105_02_T1_ANG.txt
├── LC08_L2SP_118038_20201020_20201105_02_T1_MTL.txt
├── LC08_L2SP_118038_20201020_20201105_02_T1_QA_RADSAT.tif
└── Band files (B1-B11)

2. 影像读取与基本操作

2.1 加载多波段影像

使用GDAL读取多波段TIFF文件并转换为numpy数组:

import numpy as np
from osgeo import gdal

def load_bands(band_paths):
    bands = []
    for path in band_paths:
        ds = gdal.Open(path)
        bands.append(ds.GetRasterBand(1).ReadAsArray())
        ds = None  # 显式释放资源
    return np.stack(bands, axis=0)

# 示例:加载可见光波段(B2蓝, B3绿, B4红)
band_paths = ['B2.TIF', 'B3.TIF', 'B4.TIF']
bands = load_bands(band_paths)
print(f"波段数组形状:{bands.shape}")  # 输出:(3, 行数, 列数)

2.2 生成真彩色合成图像

将红、绿、蓝波段组合显示为自然色彩:

import matplotlib.pyplot as plt

def stretch_8bit(arr, percentile=2):
    """将数据拉伸到0-255范围"""
    min_val = np.percentile(arr, percentile)
    max_val = np.percentile(arr, 100-percentile)
    return np.uint8(255 * (arr - min_val) / (max_val - min_val))

rgb = np.zeros((bands.shape[1], bands.shape[2], 3), dtype=np.uint8)
rgb[..., 0] = stretch_8bit(bands[2])  # 红波段
rgb[..., 1] = stretch_8bit(bands[1])  # 绿波段
rgb[..., 2] = stretch_8bit(bands[0])  # 蓝波段

plt.figure(figsize=(10,10))
plt.imshow(rgb)
plt.axis('off')
plt.title('真彩色合成图像')
plt.show()

注意:卫星数据通常存储为16位整数,直接显示会呈现全黑,必须进行适当拉伸。

3. 植被指数计算与分析

3.1 NDVI计算原理

归一化差异植被指数(NDVI)是最常用的植被健康度指标,计算公式为:

NDVI = (NIR - Red) / (NIR + Red)

其中:

  • NIR:近红外波段(Landsat 8的B5)
  • Red:红色波段(Landsat 8的B4)

NDVI值域为[-1,1],典型值范围:

  • 水体:<0.1
  • 裸土:0.1-0.2
  • 植被:0.2-0.8(值越高表示植被越茂盛)

3.2 Python实现NDVI计算

def calculate_ndvi(red_band, nir_band):
    """计算NDVI并处理无效值"""
    red = red_band.astype(np.float32)
    nir = nir_band.astype(np.float32)
    # 避免除以零
    denominator = (nir + red)
    denominator[denominator == 0] = 1e-10
    ndvi = (nir - red) / denominator
    # 将无效值设为-1
    ndvi[(nir == 0) & (red == 0)] = -1
    return ndvi

red = gdal.Open('B4.TIF').GetRasterBand(1).ReadAsArray()
nir = gdal.Open('B5.TIF').GetRasterBand(1).ReadAsArray()
ndvi = calculate_ndvi(red, nir)

# 可视化
plt.figure(figsize=(10,10))
plt.imshow(ndvi, cmap='RdYlGn', vmin=-0.5, vmax=1)
plt.colorbar(label='NDVI值')
plt.title('NDVI分布图')
plt.axis('off')
plt.show()

3.3 进阶应用:植被覆盖度估算

基于NDVI可进一步估算植被覆盖度(FVC):

def fvc_from_ndvi(ndvi, soil_ndvi=0.2, veg_ndvi=0.86):
    """估算植被覆盖度"""
    fvc = (ndvi - soil_ndvi) / (veg_ndvi - soil_ndvi)
    fvc = np.clip(fvc, 0, 1)  # 限制在0-1范围
    return fvc

fvc = fvc_from_ndvi(ndvi)

4. 影像裁剪与保存结果

4.1 基于地理坐标的影像裁剪

使用GDAL的Warp功能实现精确地理裁剪:

from osgeo import gdal, osr

def clip_by_coordinates(input_path, output_path, bounds, epsg=4326):
    """按地理坐标范围裁剪影像"""
    # bounds格式:(min_lon, min_lat, max_lon, max_lat)
    options = gdal.WarpOptions(
        outputBounds=bounds,
        outputBoundsSRS=f'EPSG:{epsg}',
        resampleAlg='bilinear'
    )
    gdal.Warp(output_path, input_path, options=options)

# 示例:裁剪杭州市区范围
hangzhou_bounds = (120.0, 30.1, 120.3, 30.4)
clip_by_coordinates('B4.TIF', 'B4_clip.tif', hangzhou_bounds)

4.2 保存计算结果为GeoTIFF

将NDVI结果保存为具有地理参考的TIFF文件:

def save_geotiff(output_path, array, template_path):
    """使用模板文件的投影和转换信息保存数组"""
    template_ds = gdal.Open(template_path)
    driver = gdal.GetDriverByName('GTiff')
    out_ds = driver.Create(
        output_path,
        xsize=array.shape[1],
        ysize=array.shape[0],
        bands=1,
        eType=gdal.GDT_Float32
    )
    out_ds.SetGeoTransform(template_ds.GetGeoTransform())
    out_ds.SetProjection(template_ds.GetProjection())
    out_ds.GetRasterBand(1).WriteArray(array)
    out_ds.FlushCache()
    out_ds = None

save_geotiff('NDVI.tif', ndvi, 'B4.TIF')

5. 进阶处理技巧

5.1 批量处理多时相数据

分析同一区域不同时间的植被变化:

import glob
import pandas as pd

def batch_ndvi_analysis(data_folder):
    """批量处理多时相NDVI"""
    dates = []
    mean_ndvis = []
    
    for scene in glob.glob(f'{data_folder}/LC08_*'):
        date = scene.split('_')[3][:8]  # 从文件名提取日期
        red_path = f'{scene}/B4.TIF'
        nir_path = f'{scene}/B5.TIF'
        
        red = gdal.Open(red_path).GetRasterBand(1).ReadAsArray()
        nir = gdal.Open(nir_path).GetRasterBand(1).ReadAsArray()
        ndvi = calculate_ndvi(red, nir)
        
        # 排除无效值(-1)后计算均值
        valid_ndvi = ndvi[ndvi != -1]
        if len(valid_ndvi) > 0:
            dates.append(pd.to_datetime(date, format='%Y%m%d'))
            mean_ndvis.append(np.mean(valid_ndvi))
    
    return pd.DataFrame({'Date': dates, 'NDVI': mean_ndvis}).sort_values('Date')

ndvi_series = batch_ndvi_analysis('Landsat_data')
ndvi_series.plot(x='Date', y='NDVI', title='NDVI时间序列变化')

5.2 使用Rasterio简化操作

Rasterio是基于GDAL的更友好Python接口:

import rasterio
from rasterio.plot import show

# 读取波段
with rasterio.open('B4.TIF') as src:
    red = src.read(1)
    profile = src.profile  # 保存元数据

# 修改元数据后保存NDVI
profile.update(dtype=rasterio.float32, count=1)
with rasterio.open('NDVI_rasterio.tif', 'w', **profile) as dst:
    dst.write(ndvi.astype(rasterio.float32), 1)

# 交互式显示
fig, ax = plt.subplots(figsize=(10,10))
show(ndvi, transform=src.transform, ax=ax, cmap='RdYlGn')
plt.colorbar(ax=ax.get_images()[0], label='NDVI值')

6. 常见问题解决方案

6.1 坐标系转换问题

当需要将结果与其他GIS数据叠加时,可能需要进行坐标系转换:

def reproject_raster(input_path, output_path, target_epsg):
    """重投影栅格数据"""
    options = gdal.WarpOptions(
        dstSRS=f'EPSG:{target_epsg}',
        resampleAlg='bilinear'
    )
    gdal.Warp(output_path, input_path, options=options)

# 示例:转为Web墨卡托投影(EPSG:3857)
reproject_raster('NDVI.tif', 'NDVI_3857.tif', 3857)

6.2 处理大内存影像

对于超大影像,可分块处理以避免内存溢出:

def block_process(input_path, output_path, process_func, block_size=1024):
    """分块处理大影像"""
    src_ds = gdal.Open(input_path)
    cols = src_ds.RasterXSize
    rows = src_ds.RasterYSize
    
    driver = gdal.GetDriverByName('GTiff')
    out_ds = driver.Create(
        output_path,
        cols,
        rows,
        1,
        gdal.GDT_Float32
    )
    out_ds.SetGeoTransform(src_ds.GetGeoTransform())
    out_ds.SetProjection(src_ds.GetProjection())
    
    for i in range(0, rows, block_size):
        for j in range(0, cols, block_size):
            # 计算当前块的实际大小
            actual_block_x = min(block_size, cols - j)
            actual_block_y = min(block_size, rows - i)
            
            # 读取块数据
            block = src_ds.GetRasterBand(1).ReadAsArray(
                j, i, actual_block_x, actual_block_y
            )
            
            # 处理并写入结果
            processed_block = process_func(block)
            out_ds.GetRasterBand(1).WriteArray(
                processed_block, j, i
            )
    
    out_ds.FlushCache()
    src_ds = None
    out_ds = None

# 使用示例:分块计算NDVI
def ndvi_process(block):
    """假设block包含红和近红外波段交替的行"""
    red = block[::2]
    nir = block[1::2]
    return calculate_ndvi(red, nir)

block_process('combined_bands.tif', 'ndvi_blocks.tif', ndvi_process)

掌握这些核心技能后,你已经可以处理大多数基础遥感分析任务。在实际项目中,我通常会先对小范围测试区域验证处理流程,确认无误后再扩展到全图处理,这能有效避免长时间运行后才发现参数问题的情况。

更多推荐