Copernicus Data Space Ecosystem (CDSE) là nền tảng cloud mới thay thế cho Copernicus Open Access Hub, cung cấp truy cập miễn phí đến toàn bộ dữ liệu Sentinel và các sản phẩm Copernicus khác.
29.1. Mục tiêu học tập¶
Sau khi hoàn thành bài học này, bạn sẽ có thể:
Thiết lập xác thực với CDSE S3 credentials
Tìm kiếm và truy cập dữ liệu Sentinel qua STAC API
Tải dữ liệu trực tiếp từ cloud storage với odc-stac
Lọc và xử lý dữ liệu ảnh vệ tinh theo khu vực quan tâm
Làm việc với xarray cho phân tích dữ liệu time-series
29.2. Thiết lập xác thực CDSE¶
Bước 1: Tạo tài khoản tại Copernicus Data Space nếu chưa có.
Bước 2: Đăng nhập và tạo S3 credentials từ S3 Key Manager.
Bước 3: Copy Access Key và Secret Key để sử dụng trong code.
Bạn có thể làm theo 1 trong 2 cách sau để authenticate và truy cập vào CDSE.
Cách 1: Có thể thiết lập environment variables với conda:
conda env config vars set GDAL_HTTP_TCP_KEEPALIVE=YES \
AWS_S3_ENDPOINT=eodata.dataspace.copernicus.eu \
AWS_ACCESS_KEY_ID=your_access_key \
AWS_SECRET_ACCESS_KEY=your_secret_key \
AWS_HTTPS=YES \
AWS_VIRTUAL_HOSTING=FALSE \
GDAL_HTTP_UNSAFESSL=YESCách 2: Authenticate trực tiếp trong notebook như sau.
# Thay thế bằng keys thực tế của bạn
import os
access_key = 'your_actual_access_key' # Thay bằng Access Key từ CDSE
secret_key = 'your_actual_secret_key' # Thay bằng Secret Key từ CDSE
# Cấu hình environment variables cho GDAL và AWS S3
os.environ["GDAL_HTTP_TCP_KEEPALIVE"] = "YES"
os.environ["AWS_S3_ENDPOINT"] = "eodata.dataspace.copernicus.eu"
os.environ["AWS_HTTPS"] = "YES"
os.environ["AWS_VIRTUAL_HOSTING"] = "FALSE"
os.environ["GDAL_HTTP_UNSAFESSL"] = "YES"
# Uncomment và sử dụng keys của bạn
# os.environ["AWS_ACCESS_KEY_ID"] = access_key
# os.environ["AWS_SECRET_ACCESS_KEY"] = secret_key29.3. Kết nối và khám phá STAC Catalog¶
29.3.1. Kết nối STAC Catalog¶
from pystac_client import Client# Connect to the Copernicus Data Space STAC catalog
catalog = Client.open("https://catalogue.dataspace.copernicus.eu/stac")
# you can see all collections in the catalog
collections = catalog.get_all_collections()
collections_ids = [col.id for col in collections]
print(collections_ids[10:15]) # print some collection ids['cop-dem-glo-90-dged-cog', 'sentinel-3-olci-2-wrr-nrt', 'sentinel-2-l2a', 'sentinel-1-slc', 'sentinel-2-gri-l1c-gcp']29.3.2. Tìm kiếm dữ liệu Sentinel-2¶
# define a bounding box or area of interest
bbox = [11.439263980173, 47.81384831137465, 11.475186504136818, 47.83147903246192] # this location is in Germany
# search for items in sentinel-2-l2a collection
items = catalog.search(
collections=["sentinel-2-l2a"],
bbox=bbox,
datetime="2023-05-01/2023-09-30",
query={"eo:cloud_cover": {"lt": 20}},
).item_collection()
print(f"Found {len(items)} items")Found 31 items29.3.3. Xem thông tin thuộc tính của bức ảnh¶
# get the first image id as example
# print the properties of the first item
list(items)[:1][0].properties # you can use this info to filter queries{'gsd': 10,
'created': '2025-03-21T17:41:10.043000Z',
'updated': '2025-04-21T00:16:33.282435Z',
'datetime': '2023-09-28T10:17:19.024Z',
'platform': 'sentinel-2b',
'grid:code': 'MGRS-32UPU',
'published': '2025-04-21T00:16:33.282435Z',
'statistics': {'water': 1.483443,
'nodata': 1.7e-05,
'dark_area': 0.003301,
'vegetation': 80.213308,
'thin_cirrus': 0.805717,
'cloud_shadow': 0.0,
'unclassified': 0.000269,
'not_vegetated': 17.480843,
'high_proba_clouds': 0.002183,
'medium_proba_clouds': 0.010677,
'saturated_defective': 0.0},
'instruments': ['msi'],
'auth:schemes': {'s3': {'type': 's3'},
'oidc': {'type': 'openIdConnect',
'openIdConnectUrl': 'https://identity.dataspace.copernicus.eu/auth/realms/CDSE/.well-known/openid-configuration'}},
'end_datetime': '2023-09-28T10:17:19.024Z',
'product:type': 'S2MSI2A',
'view:azimuth': 269.6725837191976,
'constellation': 'sentinel-2',
'eo:snow_cover': 0.000255,
'eo:cloud_cover': 0.818577,
'start_datetime': '2023-09-28T10:17:19.024Z',
'sat:orbit_state': 'descending',
'storage:schemes': {'cdse-s3': {'type': 'custom-s3',
'title': 'Copernicus Data Space Ecosystem S3',
'platform': 'https://eodata.dataspace.copernicus.eu',
'description': 'This endpoint provides access to EO data which is stored on the object storage of both CloudFerro Cloud and OpenTelekom Cloud (OTC). See the [documentation](https://documentation.dataspace.copernicus.eu/APIs/S3.html) for more information, including how to get credentials.',
'requester_pays': False},
'creodias-s3': {'type': 'custom-s3',
'title': 'CREODIAS S3',
'platform': 'https://eodata.cloudferro.com',
'description': 'Comprehensive Earth Observation Data (EODATA) archive offered by CREODIAS as a commercial part of CDSE, designed to provide users with access to a vast repository of satellite data without predefined quota limits.',
'requester_pays': True}},
'eopf:datatake_id': 'GS2B_20230928T101719_034268_N05.10',
'processing:level': 'L2',
'view:sun_azimuth': 167.450831069454,
'eopf:datastrip_id': 'S2B_OPER_MSI_L2A_DS_S2RP_20241026T142623_S20230928T102123_N05.10',
'processing:version': '05.10',
'product:timeliness': 'PT24H',
'sat:absolute_orbit': 34268,
'sat:relative_orbit': 65,
'view:sun_elevation': 39.0424772062148,
'processing:datetime': '2024-10-26T14:26:23.000000Z',
'processing:facility': 'ESA',
'eopf:instrument_mode': 'INS-NOBS',
'eopf:origin_datetime': '2025-03-21T17:41:10.043000Z',
'view:incidence_angle': 4.842546212382268,
'product:timeliness_category': 'NRT',
'sat:platform_international_designator': '2017-013A'}29.4. Tải dữ liệu với odc-stac¶
29.4.1. Đọc tất cả dữ liệu theo AOI và datetime¶
from odc.stac import stac_load
# read the image data from the STAC items
data = stac_load(
list(items),
chunks={"x": 1024, "y": 1024},
resolution=10,
bbox=bbox, # you can also use selected band names here. here we load all bands
) # always return xarray.Dataset
data29.4.2. Đọc dữ liệu theo image id¶
img_id = list(items)[:1][0].id
search = catalog.search(collections=["sentinel-2-l2a"], ids=[img_id])
items = search.item_collection()
data = stac_load(
list(items),
chunks={"x": 1024, "y": 1024},
resolution=10
) # always return xarray.Dataset
data29.7. Xử lý và trực quan hóa dữ liệu¶
29.7.1. Trích xuất và xử lý band đơn lẻ¶
# Trích xuất band Blue (B02) với độ phân giải 10m
blue = data['B02_10m'].squeeze() # Loại bỏ dimensions đơn lẻ
blue = blue.compute() # Tải dữ liệu vào memory
print(f"🔵 Band Blue (B02):")
print(f" Kích thước: {blue.shape}")
print(f" Kiểu dữ liệu: {blue.dtype}")
print(f" Giá trị min: {blue.min().values}")
print(f" Giá trị max: {blue.max().values}")
print(f" Giá trị trung bình: {blue.mean().values:.2f}")29.7.2. Trực quan hóa dữ liệu¶
# Tạo figure với subplots
fig, axes = plt.subplots(1, 2, figsize=(15, 6))
# Plot 1: Band Blue
im1 = axes[0].imshow(blue, cmap='Blues', vmin=0, vmax=4000)
axes[0].set_title('Sentinel-2 Band Blue (B02)', fontsize=12, fontweight='bold')
axes[0].set_xlabel('Pixel X')
axes[0].set_ylabel('Pixel Y')
plt.colorbar(im1, ax=axes[0], label='Reflectance')
# Plot 2: Histogram
blue_flat = blue.values.flatten()
blue_flat = blue_flat[~np.isnan(blue_flat)] # Loại bỏ NaN values
axes[1].hist(blue_flat, bins=50, alpha=0.7, color='blue', edgecolor='black')
axes[1].set_title('Histogram - Band Blue Distribution', fontsize=12, fontweight='bold')
axes[1].set_xlabel('Reflectance Value')
axes[1].set_ylabel('Frequency')
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
print("📊 Đã hoàn thành trực quan hóa band Blue!")29.7.3. Xử lý nhiều band và tính chỉ số thực vật¶
# Trích xuất các band cần thiết cho tính NDVI
red = data['B04_10m'].squeeze().compute() # Band Red
nir = data['B08_10m'].squeeze().compute() # Band NIR
# Tính NDVI (Normalized Difference Vegetation Index)
# NDVI = (NIR - Red) / (NIR + Red)
ndvi = (nir - red) / (nir + red)
print(f"🌱 NDVI Statistics:")
print(f" Min: {ndvi.min().values:.3f}")
print(f" Max: {ndvi.max().values:.3f}")
print(f" Mean: {ndvi.mean().values:.3f}")
print(f" Std: {ndvi.std().values:.3f}")
# Trực quan hóa NDVI
fig, ax = plt.subplots(figsize=(10, 8))
im = ax.imshow(ndvi, cmap='RdYlGn', vmin=-0.5, vmax=1.0)
ax.set_title('NDVI - Chỉ số thực vật chuẩn hóa', fontsize=14, fontweight='bold')
ax.set_xlabel('Pixel X')
ax.set_ylabel('Pixel Y')
cbar = plt.colorbar(im, ax=ax, label='NDVI Value')
# Thêm legend cho NDVI
ax.text(0.02, 0.98, 'NDVI Interpretation:\n• < 0: Water/Snow\n• 0-0.2: Bare soil\n• 0.2-0.5: Low vegetation\n• > 0.5: Dense vegetation',
transform=ax.transAxes, fontsize=10, verticalalignment='top',
bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
plt.tight_layout()
plt.show()
print("🎯 Đã tính và trực quan hóa NDVI thành công!")29.7.4. Tạo composite RGB¶
# Trích xuất các band RGB
green = data['B03_10m'].squeeze().compute() # Band Green
# red và blue đã có từ trước
# Chuẩn hóa giá trị về 0-1 cho hiển thị RGB
def normalize_band(band, percentile_range=(2, 98)):
"""Chuẩn hóa band sử dụng percentile stretch"""
p_low, p_high = np.percentile(band.values[~np.isnan(band.values)], percentile_range)
band_norm = (band - p_low) / (p_high - p_low)
return np.clip(band_norm, 0, 1)
# Chuẩn hóa các band
red_norm = normalize_band(red)
green_norm = normalize_band(green)
blue_norm = normalize_band(blue)
# Tạo RGB composite
rgb = np.stack([red_norm, green_norm, blue_norm], axis=-1)
# Hiển thị RGB composite
fig, axes = plt.subplots(1, 2, figsize=(16, 8))
# Plot RGB
axes[0].imshow(rgb)
axes[0].set_title('Sentinel-2 True Color (RGB)', fontsize=12, fontweight='bold')
axes[0].set_xlabel('Pixel X')
axes[0].set_ylabel('Pixel Y')
# Plot NDVI để so sánh
im2 = axes[1].imshow(ndvi, cmap='RdYlGn', vmin=-0.5, vmax=1.0)
axes[1].set_title('NDVI Comparison', fontsize=12, fontweight='bold')
axes[1].set_xlabel('Pixel X')
axes[1].set_ylabel('Pixel Y')
plt.colorbar(im2, ax=axes[1], label='NDVI Value')
plt.tight_layout()
plt.show()
print("🌈 Đã tạo và hiển thị RGB composite thành công!")29.8. Tóm tắt¶
Trong bài học này, chúng ta đã học cách:
✅ Thiết lập xác thực với CDSE S3 credentials
✅ Kết nối và khám phá STAC Catalog của Copernicus Data Space
✅ Tìm kiếm dữ liệu Sentinel-2 theo khu vực và thời gian
✅ Tải dữ liệu trực tiếp từ cloud với odc-stac và chunking
✅ Xử lý và trực quan hóa các band ảnh vệ tinh
✅ Tính toán chỉ số NDVI từ dữ liệu Sentinel-2
✅ Tạo RGB composite cho hiển thị màu tự nhiên
Ưu điểm của CDSE:¶
🚀 Truy cập nhanh chóng đến dữ liệu Copernicus
☁️ Không cần download, xử lý trực tiếp từ cloud
🔄 Cập nhật dữ liệu real-time
💾 Tiết kiệm băng thông và storage
🎯 STAC API chuẩn hóa cho interoperability
Ứng dụng thực tế:¶
🌾 Monitoring nông nghiệp và crop health
🏙️ Theo dõi phát triển đô thị
🌊 Giám sát tài nguyên nước
🌳 Phân tích biến đổi rừng
🌡️ Nghiên cứu biến đổi khí hậu
29.5.2. Tìm kiếm ảnh Sentinel-2 Level-2A¶
# Tìm kiếm ảnh trong collection sentinel-2-l2a
items = catalog.search(
collections=["sentinel-2-l2a"],
bbox=bbox,
datetime="2023-05-01/2023-09-30", # Thời gian từ tháng 5 đến tháng 9/2023
query={"eo:cloud_cover": {"lt": 20}}, # Độ phủ mây < 20%
).item_collection()
print(f"🔍 Đã tìm kiếm xong với các tiêu chí:")
print(f" - Collection: sentinel-2-l2a")
print(f" - Thời gian: 2023-05-01 đến 2023-09-30")
print(f" - Độ phủ mây: < 20%")access_key = 'example_access_key' # Please replace with your actual access key
secret_key = 'example_secret_key' # Please replace with your actual secret key
os.environ["GDAL_HTTP_TCP_KEEPALIVE"] = "YES"
os.environ["AWS_S3_ENDPOINT"] = "eodata.dataspace.copernicus.eu"
os.environ["AWS_HTTPS"] = "YES"
os.environ["AWS_VIRTUAL_HOSTING"] = "FALSE"
os.environ["GDAL_HTTP_UNSAFESSL"] = "YES"
# os.environ["AWS_ACCESS_KEY_ID"] = access_key
# os.environ["AWS_SECRET_ACCESS_KEY"] = secret_key# Connect to the Copernicus Data Space STAC catalog
catalog = Client.open("https://catalogue.dataspace.copernicus.eu/stac")
# you can see all collections in the catalog
collections = catalog.get_all_collections()
collections_ids = [col.id for col in collections]
print(collections_ids) # print all collection ids['sentinel-3-sl-2-aod-nrt', 'sentinel-1-global-mosaics', 'sentinel-3-olci-2-wfr-nrt', 'cop-dem-glo-30-dged-cog', 'sentinel-1-slc-wv', 'sentinel-2-gri-l1c', 'sentinel-2-l1c', 'sentinel-2-global-mosaics', 'sentinel-3-sl-2-wst-nrt', 'sentinel-3-olci-1-efr-nrt', 'cop-dem-glo-90-dged-cog', 'sentinel-3-olci-2-wrr-nrt', 'sentinel-2-l2a', 'sentinel-1-slc', 'sentinel-2-gri-l1c-gcp', 'sentinel-3-sl-2-lst-ntc', 'sentinel-3-olci-1-efr-ntc', 'sentinel-3-olci-2-wfr-ntc', 'sentinel-3-olci-2-lfr-nrt', 'sentinel-3-sl-2-wst-ntc', 'sentinel-3-olci-1-err-nrt', 'sentinel-3-olci-2-lfr-ntc', 'sentinel-3-sl-2-frp-ntc', 'sentinel-3-olci-1-err-ntc', 'sentinel-3-sl-1-rbt-ntc', 'sentinel-3-olci-2-lrr-ntc', 'sentinel-3-sl-2-lst-nrt', 'sentinel-3-sl-1-rbt-nrt', 'sentinel-3-olci-2-lrr-nrt', 'sentinel-3-sl-2-frp-nrt', 'sentinel-6-p4-1b-nrt', 'sentinel-5p-l1-ra-bd2-offl', 'sentinel-3-sr-1-sra-a-nrt', 'sentinel-6-p4-1b-ntc', 'sentinel-5p-l1-ra-bd2-nrti', 'sentinel-3-sr-1-sra-a-stc', 'sentinel-6-p4-1b-stc', 'sentinel-5p-l1-ra-bd1-rpro', 'sentinel-3-sr-1-sra-a-ntc', 'sentinel-6-p4-2-nrt', 'sentinel-5p-l1-ra-bd5-nrti', 'sentinel-1-grd', 'sentinel-6-p4-2-ntc', 'sentinel-5p-l1-ra-bd6-offl', 'sentinel-6-p4-2-stc', 'ccm-dem', 'sentinel-5p-l1-ra-bd4-nrti', 'sentinel-3-olci-2-wrr-ntc', 'sentinel-3-sr-1-sra-stc', 'ccm-sar', 'sentinel-5p-l1-ra-bd1-nrti', 'sentinel-3-sr-1-sra-ntc', 'ccm-optical', 'sentinel-5p-l1-ra-bd3-nrti', 'sentinel-3-sr-1-sra-nrt', 'sentinel-5p-l1-ra-bd6-rpro', 'sentinel-3-sr-2-lan-hy-nrt', 'opengeohub-landsat-bimonthly-mosaic-v1.0.1', 'sentinel-5p-l1-ra-bd5-rpro', 'sentinel-3-sr-2-lan-hy-stc', 'sentinel-6-amr-c-nrt', 'sentinel-5p-l1-ra-bd8-rpro', 'sentinel-3-sr-2-lan-hy-ntc', 'sentinel-6-amr-c-ntc', 'sentinel-5p-l1-ra-bd3-rpro', 'sentinel-3-sr-2-lan-li-ntc', 'sentinel-6-amr-c-stc', 'sentinel-5p-l1-ra-bd7-offl', 'sentinel-3-sr-2-lan-li-stc', 'sentinel-5p-l1-ra-bd6-nrti', 'sentinel-3-sr-2-lan-li-nrt', 'sentinel-5p-l1-ra-bd5-offl', 'sentinel-3-sr-2-lan-si-stc', 'sentinel-5p-l1-ra-bd8-offl', 'sentinel-3-sr-2-lan-si-nrt', 'sentinel-5p-l1-ra-bd2-rpro', 'sentinel-3-sr-2-lan-si-ntc', 'sentinel-5p-l1-ra-bd4-offl', 'sentinel-3-sr-2-lan-nrt', 'sentinel-5p-l1-ra-bd7-rpro', 'sentinel-3-sr-2-lan-stc', 'sentinel-5p-l1-ra-bd8-nrti', 'sentinel-3-sr-2-lan-ntc', 'sentinel-5p-l1-ra-bd4-rpro', 'sentinel-3-sr-2-wat-stc', 'sentinel-5p-l1-ra-bd7-nrti', 'sentinel-3-sr-2-wat-nrt', 'sentinel-5p-l1-ra-bd3-offl', 'sentinel-3-sr-2-wat-ntc', 'sentinel-5p-l1-ra-bd1-offl', 'sentinel-3-syn-2-aod-ntc', 'sentinel-5p-l2-aer-ai-nrti', 'sentinel-3-syn-2-syn-stc', 'sentinel-5p-l2-aer-ai-offl', 'sentinel-3-syn-2-syn-ntc', 'sentinel-5p-l2-aer-ai-rpro', 'sentinel-3-syn-2-v10-ntc', 'sentinel-5p-l2-aer-lh-nrti', 'sentinel-3-syn-2-v10-stc', 'sentinel-5p-l2-aer-lh-offl', 'sentinel-3-syn-2-vg1-ntc', 'sentinel-5p-l2-aer-lh-rpro', 'sentinel-3-syn-2-vg1-stc', 'sentinel-5p-l2-ch4-offl', 'sentinel-3-syn-2-vgp-stc', 'sentinel-5p-l2-ch4-rpro', 'sentinel-3-syn-2-vgp-ntc', 'sentinel-5p-l2-cloud-nrti', 'sentinel-5p-l2-cloud-offl', 'sentinel-5p-l2-cloud-rpro', 'sentinel-5p-l2-co-rpro', 'sentinel-5p-l2-co-nrti', 'sentinel-5p-l2-co-offl', 'sentinel-5p-l2-hcho-nrti', 'sentinel-5p-l2-hcho-rpro', 'sentinel-5p-l2-hcho-offl', 'sentinel-5p-l2-no2-offl', 'sentinel-5p-l2-no2-rpro', 'sentinel-5p-l2-no2-nrti', 'sentinel-5p-l2-np-bd3-rpro', 'sentinel-5p-l2-np-bd3-offl', 'sentinel-5p-l2-np-bd6-rpro', 'sentinel-5p-l2-np-bd6-offl', 'sentinel-5p-l2-np-bd7-rpro', 'sentinel-5p-l2-np-bd7-offl', 'sentinel-5p-l2-o3-pr-nrti', 'sentinel-5p-l2-o3-pr-rpro', 'sentinel-5p-l2-o3-pr-offl', 'sentinel-5p-l2-o3-tcl-offl', 'sentinel-5p-l2-o3-tcl-nrti', 'sentinel-5p-l2-o3-tcl-rpro', 'sentinel-5p-l2-o3-rpro', 'sentinel-5p-l2-o3-nrti', 'sentinel-5p-l2-o3-offl', 'sentinel-5p-l2-so2-offl', 'sentinel-5p-l2-so2-nrti', 'sentinel-5p-l2-so2-rpro']# define a bounding box or area of interest
bbox = [11.439263980173, 47.81384831137465, 11.475186504136818, 47.83147903246192] # this location is in Germany
# search for items in sentinel-2-l2a collection
items = catalog.search(
collections=["sentinel-2-l2a"],
bbox=bbox,
datetime="2023-05-01/2023-09-30",
query={"eo:cloud_cover": {"lt": 20}},
).item_collection()# check how many items found
print(f'Number of items found: {len(items)}')
# get the first image id as example
items = list(items)[:1]
# print the properties of the first item
items[0].properties # you can use this info to filter queries# read the image data from the STAC items
data = stac_load(
items,
chunks={"x": 1024, "y": 1024},
resolution=10,
bbox=bbox, # you can also use selected band names here. here we load all bands
) # always return xarray.Dataset
data# load B02_10m band only
blue = data['B02_10m'].squeeze()
blue = blue.compute() # load data into memory