数据获取实战:用Python搞定Sentinel-2数据
说实话,做量化分析最头疼的事之一,就是数据获取。尤其是卫星数据,看着一堆API文档,头都大了。但别担心,今天我就带你手把手搞定Sentinel-2数据的下载和预处理。
我个人习惯用两个库:eodag 和 sentinelhub。前者适合快速搜索和下载,后者适合精细控制。你想想看,一个像淘宝搜商品,一个像直接去工厂定制,各有各的好。
为什么选Sentinel-2?
Sentinel-2是欧空局的明星卫星,10米空间分辨率,5天重访周期。说白了,就是既能看清田块细节,又能频繁更新数据。我在做大豆产量预测时,就靠它捕捉到了关键生长期的植被变化。
它的波段设计也很贴心:
| 波段 | 中心波长(nm) | 分辨率(m) | 主要用途 |
|---|---|---|---|
| B2 (蓝) | 490 | 10 | 水体识别 |
| B3 (绿) | 560 | 10 | 植被健康 |
| B4 (红) | 665 | 10 | 叶绿素吸收 |
| B8 (近红外) | 842 | 10 | 植被密度 |
环境准备:先把工具装好
嗯,这里要注意。别一上来就pip install,容易踩坑。我建议用conda创建独立环境:
conda create -n sentinel python=3.9
conda activate sentinel
pip install eodag sentinelhub rasterio numpy matplotlib
我曾经在Windows上装sentinelhub时遇到SSL证书问题,折腾了半天。后来发现加个--trusted-host参数就解决了。如果你也遇到类似问题,试试:
pip install sentinelhub --trusted-host pypi.org --trusted-host files.pythonhosted.org
方法一:用eodag快速上手
eodag的好处是配置简单。你只需要注册一个EarthData账号,拿到API Key就行。我个人觉得它特别适合快速验证想法。
先配置一下:
from eodag import EODataAccessGateway
dag = EODataAccessGateway()
dag.set_preferred_provider("cop_cloud") # 选个靠谱的提供商
然后搜索数据:
# 搜索2023年7月,覆盖某个玉米产区的数据
products = dag.search(
productType="S2_MSI_L2A",
geom={"type": "Point", "coordinates": [116.4, 39.9]},
start="2023-07-01",
end="2023-07-31",
cloudCover=20 # 云量小于20%
)
print(f"找到 {len(products)} 景影像")
for p in products[:3]:
print(f"日期: {p.properties['startTimeFromAscendingNode']}, 云量: {p.properties['cloudCover']}%")
下载也很直接:
# 下载前3景
for product in products[:3]:
product.download(output_dir="./sentinel_data")
cloudCover=20,能过滤掉大部分多云影像。我在做时间序列分析时,一般要求云量低于10%,否则插值误差太大。
方法二:用sentinelhub精细控制
如果你需要更精细的控制,比如只下载某个波段,或者指定投影方式,那sentinelhub是更好的选择。它需要你注册Sentinel Hub账号,拿到Instance ID。
先初始化:
from sentinelhub import (
SHConfig, BBox, CRS, DataCollection,
SentinelHubRequest, MimeType
)
config = SHConfig()
config.sh_client_id = "你的client_id"
config.sh_client_secret = "你的client_secret"
然后定义你要下载的区域和波段:
# 定义北京周边的一个矩形区域
bbox = BBox([116.3, 39.8, 116.5, 40.0], crs=CRS.WGS84)
# 定义要下载的波段:红、绿、蓝、近红外
evalscript = """
//VERSION=3
function setup() {
return {
input: ["B02", "B03", "B04", "B08"],
output: { bands: 4 }
};
}
function evaluatePixel(sample) {
return [sample.B04, sample.B03, sample.B02, sample.B08];
}
"""
request = SentinelHubRequest(
evalscript=evalscript,
input_data=[
SentinelHubRequest.input_data(
data_collection=DataCollection.SENTINEL2_L2A,
time_interval=("2023-07-15", "2023-07-16"),
maxcc=20 # 最大云量20%
)
],
responses=[
SentinelHubRequest.output_response("default", MimeType.TIFF)
],
bbox=bbox,
size=[512, 512],
config=config
)
data = request.get_data()[0]
print(f"数据形状: {data.shape}") # (512, 512, 4)
数据预处理:去云、裁剪、重采样
下载下来的数据,说白了就是一堆原始像素。直接拿去建模?那肯定不行。我踩过的坑太多了,这里给你总结三个关键步骤。
1. 去云:别让云朵骗了你的模型
云层会严重干扰植被指数计算。Sentinel-2自带一个云掩膜波段(QA60),我们可以用它来去云:
import rasterio
import numpy as np
def remove_clouds(tiff_path, output_path):
with rasterio.open(tiff_path) as src:
# 读取所有波段
bands = src.read()
# 读取云掩膜(通常在第11或12波段)
cloud_mask = src.read(11) # QA60波段
# 云掩膜中,bit 10和bit 11分别代表云和卷云
cloud_mask = (cloud_mask & (1 << 10)) | (cloud_mask & (1 << 11))
cloud_mask = cloud_mask > 0
# 将云区域设为NaN
bands[:, cloud_mask] = np.nan
# 写入新文件
with rasterio.open(
output_path, 'w',
driver='GTiff',
height=src.height,
width=src.width,
count=src.count,
dtype=src.dtypes[0],
crs=src.crs,
transform=src.transform
) as dst:
dst.write(bands)
# 使用
remove_clouds("S2_20230715.tiff", "S2_20230715_cloudfree.tiff")
我曾经犯过一个错误:直接用插值填充云区域。结果模型把云阴影误判为水体,预测偏差很大。后来我改用时间序列上的前后影像插值,效果才好起来。
2. 裁剪:只关注你的研究区
整景影像太大了,动辄几百MB。我们只需要研究区的那一小块:
import geopandas as gpd
from shapely.geometry import box
def clip_to_aoi(tiff_path, aoi_shapefile, output_path):
# 读取研究区边界
aoi = gpd.read_file(aoi_shapefile)
with rasterio.open(tiff_path) as src:
# 裁剪
out_image, out_transform = rasterio.mask.mask(
src, aoi.geometry, crop=True
)
# 保存
with rasterio.open(
output_path, 'w',
driver='GTiff',
height=out_image.shape[1],
width=out_image.shape[2],
count=src.count,
dtype=out_image.dtype,
crs=src.crs,
transform=out_transform
) as dst:
dst.write(out_image)
# 使用
clip_to_aoi("S2_20230715_cloudfree.tiff", "farm_boundary.shp", "S2_clipped.tiff")
3. 重采样:统一分辨率
Sentinel-2的不同波段分辨率不同(10m、20m、60m)。建模时,必须统一到同一个分辨率:
def resample_to_uniform(tiff_path, target_res=10, output_path="S2_resampled.tiff"):
with rasterio.open(tiff_path) as src:
# 计算目标分辨率下的尺寸
scale = src.res[0] / target_res
new_width = int(src.width * scale)
new_height = int(src.height * scale)
# 重采样
data = src.read(
out_shape=(src.count, new_height, new_width),
resampling=rasterio.enums.Resampling.bilinear
)
# 更新变换矩阵
transform = src.transform * src.transform.scale(
(src.width / data.shape[-1]),
(src.height / data.shape[-2])
)
# 保存
with rasterio.open(
output_path, 'w',
driver='GTiff',
height=new_height,
width=new_width,
count=src.count,
dtype=data.dtype,
crs=src.crs,
transform=transform
) as dst:
dst.write(data)
# 使用
resample_to_uniform("S2_clipped.tiff", target_res=10)
你想想看,如果不做重采样,10m和60m的波段混在一起建模,模型会优先学习分辨率高的波段,导致偏差。我一般统一到10m,因为植被指数(NDVI)主要依赖10m的红和近红外波段。
完整流程:从搜索到预处理
把上面所有步骤串起来,就是一个完整的pipeline:
def sentinel_pipeline(aoi_coords, start_date, end_date, output_dir):
# 1. 搜索数据
dag = EODataAccessGateway()
products = dag.search(
productType="S2_MSI_L2A",
geom={"type": "Point", "coordinates": aoi_coords},
start=start_date,
end=end_date,
cloudCover=20
)
for product in products[:5]: # 处理前5景
# 2. 下载
product.download(output_dir=output_dir)
# 3. 去云
tiff_path = f"{output_dir}/{product.properties['title']}.tiff"
cloudfree_path = tiff_path.replace(".tiff", "_cloudfree.tiff")
remove_clouds(tiff_path, cloudfree_path)
# 4. 裁剪
clipped_path = cloudfree_path.replace("_cloudfree", "_clipped")
clip_to_aoi(cloudfree_path, "aoi.shp", clipped_path)
# 5. 重采样
resampled_path = clipped_path.replace("_clipped", "_resampled")
resample_to_uniform(clipped_path, target_res=10, output_path=resampled_path)
print(f"处理完成: {resampled_path}")
# 运行
sentinel_pipeline(
aoi_coords=[116.4, 39.9],
start_date="2023-07-01",
end_date="2023-07-31",
output_dir="./sentinel_processed"
)
本章知识体系
下面这张图,是我自己总结的Sentinel-2数据处理流程,你保存下来,以后做项目时对照着看:
嗯,到这里,数据获取和预处理的核心内容就讲完了。你可能会问:这些代码跑起来会不会很慢?说实话,如果数据量大,确实会慢。我一般用多线程下载,预处理时用rasterio的窗口读取,能快不少。
最后提醒一句:数据质量决定模型上限。预处理做得再精细都不为过。我见过太多人,数据没处理好就急着建模,结果花了几周调参,还不如别人花一天做数据清洗的效果好。