从手机拍照到卫星成像:用Python+GDAL带你入门遥感数据处理(附实战代码)
从手机拍照到卫星成像用PythonGDAL带你入门遥感数据处理附实战代码当我们用手机拍摄一张照片时其实已经在进行最简单的遥感实践——记录物体反射的可见光信息。这种日常行为与专业卫星遥感的核心原理惊人地相似都是通过传感器捕捉电磁波信息来认识世界。本文将用Python和GDAL库搭建一座桥梁让开发者能够快速上手处理真实的卫星影像数据。1. 环境配置与数据准备1.1 安装GDAL及其Python绑定GDAL(Geospatial Data Abstraction Library)是处理地理空间数据的瑞士军刀。在Python环境中我们推荐使用conda进行安装它能自动解决复杂的依赖关系conda create -n rs python3.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下载注册USGS EarthExplorer账号选择数据集Landsat Collection 2 Level-2划定感兴趣区域和时间范围下载包含所有波段的.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, axis0) # 示例加载可见光波段(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, percentile2): 将数据拉伸到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), dtypenp.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, cmapRdYlGn, vmin-0.5, vmax1) plt.colorbar(labelNDVI值) plt.title(NDVI分布图) plt.axis(off) plt.show()3.3 进阶应用植被覆盖度估算基于NDVI可进一步估算植被覆盖度(FVC)def fvc_from_ndvi(ndvi, soil_ndvi0.2, veg_ndvi0.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, epsg4326): 按地理坐标范围裁剪影像 # bounds格式(min_lon, min_lat, max_lon, max_lat) options gdal.WarpOptions( outputBoundsbounds, outputBoundsSRSfEPSG:{epsg}, resampleAlgbilinear ) gdal.Warp(output_path, input_path, optionsoptions) # 示例裁剪杭州市区范围 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, xsizearray.shape[1], ysizearray.shape[0], bands1, eTypegdal.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(xDate, yNDVI, titleNDVI时间序列变化)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(dtyperasterio.float32, count1) 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, transformsrc.transform, axax, cmapRdYlGn) plt.colorbar(axax.get_images()[0], labelNDVI值)6. 常见问题解决方案6.1 坐标系转换问题当需要将结果与其他GIS数据叠加时可能需要进行坐标系转换def reproject_raster(input_path, output_path, target_epsg): 重投影栅格数据 options gdal.WarpOptions( dstSRSfEPSG:{target_epsg}, resampleAlgbilinear ) gdal.Warp(output_path, input_path, optionsoptions) # 示例转为Web墨卡托投影(EPSG:3857) reproject_raster(NDVI.tif, NDVI_3857.tif, 3857)6.2 处理大内存影像对于超大影像可分块处理以避免内存溢出def block_process(input_path, output_path, process_func, block_size1024): 分块处理大影像 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)掌握这些核心技能后你已经可以处理大多数基础遥感分析任务。在实际项目中我通常会先对小范围测试区域验证处理流程确认无误后再扩展到全图处理这能有效避免长时间运行后才发现参数问题的情况。