|
|
""" Utility functions for Google Earth Engine data extraction and processing. """ |
|
|
from datetime import datetime, timedelta |
|
|
import json |
|
|
import os |
|
|
import tempfile |
|
|
import time |
|
|
|
|
|
import ee |
|
|
import geopandas as gpd |
|
|
from shapely.geometry import Point |
|
|
import pandas as pd |
|
|
|
|
|
from indices import add_s2_indices, add_s1_indices |
|
|
from variables import s2_bands, s1_bands |
|
|
|
|
|
|
|
|
def check_inside_civ(lat: float, lon: float): |
|
|
""" Check if the given latitude and longitude are inside Côte d'Ivoire. """ |
|
|
civ = gpd.read_file("data/CIV_0.json") |
|
|
point = Point(lon, lat) |
|
|
return civ.contains(point).any() |
|
|
|
|
|
|
|
|
def initialize_ee(): |
|
|
""" Initialize Google Earth Engine """ |
|
|
sa_email = os.getenv("EE_SERVICE_ACCOUNT") |
|
|
sa_key_json = os.getenv("EE_SERVICE_KEY") |
|
|
|
|
|
try: |
|
|
|
|
|
if sa_email and sa_key_json: |
|
|
key_path = os.path.join(tempfile.gettempdir(), "ee-key.json") |
|
|
with open(key_path, "w", encoding="utf-8") as f: |
|
|
f.write(sa_key_json) |
|
|
creds = ee.ServiceAccountCredentials(sa_email, key_path) |
|
|
ee.Initialize(creds) |
|
|
print(f"[INFO] GEE initialized with service account {sa_email}") |
|
|
return |
|
|
|
|
|
|
|
|
local_key = "secrets/gcp-sa-key.json" |
|
|
if os.path.exists(local_key): |
|
|
with open(local_key, "r", encoding="utf-8") as f: |
|
|
key_data = json.load(f) |
|
|
creds = ee.ServiceAccountCredentials(key_data["client_email"], local_key) |
|
|
ee.Initialize(creds) |
|
|
print(f"[INFO] GEE initialized from {local_key}") |
|
|
return |
|
|
|
|
|
|
|
|
ee.Initialize() |
|
|
print("[INFO] GEE initialized") |
|
|
|
|
|
except Exception as e: |
|
|
raise RuntimeError(f"GEE initialization failed : {e}") from e |
|
|
|
|
|
def mask_s2_clouds(image): |
|
|
""" Mask clouds and cirrus in Sentinel-2 images. """ |
|
|
qa = image.select('QA60') |
|
|
|
|
|
|
|
|
cloud_bit_mask = 1 << 10 |
|
|
cirrus_bit_mask = 1 << 11 |
|
|
|
|
|
|
|
|
mask = ( |
|
|
qa.bitwiseAnd(cloud_bit_mask) |
|
|
.eq(0) |
|
|
.And(qa.bitwiseAnd(cirrus_bit_mask).eq(0)) |
|
|
) |
|
|
|
|
|
masked = image.updateMask(mask).divide(10000) |
|
|
masked = masked.copyProperties( |
|
|
source=image, |
|
|
properties=[ |
|
|
"system:time_start", |
|
|
"system:time_end", |
|
|
"CLOUDY_PIXEL_PERCENTAGE", |
|
|
"SPACECRAFT_NAME" |
|
|
] |
|
|
) |
|
|
|
|
|
return masked |
|
|
|
|
|
def mask_edge(image): |
|
|
""" Mask pixels at the edge in Sentinel-1 images. """ |
|
|
edge = image.lt(-30.0) |
|
|
masked_image = image.mask().And(edge.Not()) |
|
|
|
|
|
return image.updateMask(masked_image) |
|
|
|
|
|
def days_since_utc(utc_iso: str) -> int: |
|
|
""" Compute the number of days since the image was taken. """ |
|
|
t_img = time.mktime(time.strptime(utc_iso[:19], "%Y-%m-%dT%H:%M:%S")) |
|
|
return int((time.time() - t_img) // 86400) |
|
|
|
|
|
def lonlat_to_utm_epsg(lon: float, lat: float) -> int: |
|
|
""" Convert longitude and latitude to UTM EPSG code. """ |
|
|
zone = int((lon + 180) // 6) + 1 |
|
|
if lat >= 0: |
|
|
return 32600 + zone |
|
|
return 32700 + zone |
|
|
|
|
|
def projected_xy(lon: float, lat: float): |
|
|
""" Convert lon/lat to projected coordinates (easting, northing). """ |
|
|
epsg = f"EPSG:{lonlat_to_utm_epsg(lon, lat)}" |
|
|
pt = ee.Geometry.Point([lon, lat]) |
|
|
proj = ee.Projection(epsg) |
|
|
xy = ee.List(pt.transform(proj, 1).coordinates()).getInfo() |
|
|
return float(xy[0]), float(xy[1]) |
|
|
|
|
|
|
|
|
def extract_from_gee(lat: float, lon: float, radius_m: int = 30): |
|
|
""" Extract data from GEE for given lat/lon. """ |
|
|
pt = ee.Geometry.Point([float(lon), float(lat)]) |
|
|
roi = pt.buffer(radius_m).bounds() |
|
|
|
|
|
end_date = datetime.now() |
|
|
start_date = end_date - timedelta(days=31) |
|
|
start = start_date.strftime('%Y-%m-%d') |
|
|
end = end_date.strftime('%Y-%m-%d') |
|
|
|
|
|
S2 = ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED") |
|
|
S1 = ee.ImageCollection("COPERNICUS/S1_GRD") |
|
|
DEM = ee.Image("USGS/SRTMGL1_003") |
|
|
TIME_KEY = "system:time_start" |
|
|
|
|
|
s2 = (S2.filterBounds(roi) |
|
|
.filterDate(start, end) |
|
|
.filter(ee.Filter.lt("CLOUDY_PIXEL_PERCENTAGE", 40)) |
|
|
.map(mask_s2_clouds) |
|
|
.select(s2_bands)) |
|
|
|
|
|
img_s2 = s2.sort(TIME_KEY, False).first() |
|
|
if img_s2 is None: |
|
|
return None, {"error": "No Sentinel-2 image available on ROI/time window."} |
|
|
|
|
|
cloud_cover = ee.Number(img_s2.get("CLOUDY_PIXEL_PERCENTAGE")).getInfo() |
|
|
acq_iso_s2 = ee.Date(img_s2.get(TIME_KEY)).format().getInfo() |
|
|
days_s2 = days_since_utc(acq_iso_s2) |
|
|
|
|
|
s1 = (S1.filterBounds(roi) |
|
|
.filterDate(start, end) |
|
|
.filter(ee.Filter.eq('instrumentMode', 'IW')) |
|
|
.filter(ee.Filter.listContains('transmitterReceiverPolarisation', 'VV')) |
|
|
.filter(ee.Filter.listContains('transmitterReceiverPolarisation', 'VH')) |
|
|
.map(mask_edge) |
|
|
.select(s1_bands)) |
|
|
|
|
|
img_s1 = s1.sort(TIME_KEY, False).first() |
|
|
acq_iso_s1 = ee.Date(img_s1.get(TIME_KEY)).format().getInfo() |
|
|
days_s1 = days_since_utc(acq_iso_s1) |
|
|
|
|
|
if days_s1 > days_s2: |
|
|
estimation_date = acq_iso_s2 |
|
|
else: |
|
|
estimation_date = acq_iso_s1 |
|
|
|
|
|
|
|
|
s1_proj = img_s1.projection() |
|
|
elevation = DEM.reproject(s1_proj).clip(roi) |
|
|
|
|
|
s1_s2_dem_image = ( |
|
|
img_s1 |
|
|
.addBands(img_s2) |
|
|
.addBands(elevation) |
|
|
) |
|
|
|
|
|
|
|
|
image_dict = s1_s2_dem_image.reduceRegion( |
|
|
reducer=ee.Reducer.mean(), |
|
|
geometry=roi, |
|
|
scale=10, |
|
|
maxPixels=1e8 |
|
|
).getInfo() |
|
|
|
|
|
if image_dict: |
|
|
init_dict = {k: image_dict.get(k) for k in (s2_bands + s1_bands + ['elevation'])} |
|
|
data = pd.DataFrame([init_dict]) |
|
|
data["lon"] = lon |
|
|
data["lat"] = lat |
|
|
easting, northing = projected_xy(lon, lat) |
|
|
data["latitude_proj"] = northing |
|
|
data["longitude_proj"] = easting |
|
|
data = add_s2_indices(data) |
|
|
data = add_s1_indices(data) |
|
|
ndvi_mean = data["NDVI"].values[0] if "NDVI" in data else None |
|
|
else: |
|
|
data = None |
|
|
ndvi_mean = None |
|
|
|
|
|
|
|
|
return { |
|
|
"X": data, |
|
|
"cloud": float(cloud_cover), |
|
|
"estimation_date": estimation_date.split("T")[0], |
|
|
"ndvi_mean": ndvi_mean |
|
|
}, None |
|
|
|