Spaces:
Sleeping
Sleeping
Update gee_utils.py
Browse files- gee_utils.py +85 -48
gee_utils.py
CHANGED
|
@@ -3,78 +3,115 @@ import os
|
|
| 3 |
import json
|
| 4 |
import base64
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
def init_gee():
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
def get_sentinel2_image(lat, lng, buffer_m, year):
|
| 15 |
-
|
| 16 |
-
point = ee.Geometry.Point([lng, lat])
|
| 17 |
region = point.buffer(buffer_m)
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
end = f"{year}-12-31"
|
| 21 |
-
|
| 22 |
-
collection = (
|
| 23 |
ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
|
| 24 |
.filterBounds(region)
|
| 25 |
-
.filterDate(
|
| 26 |
.filter(ee.Filter.lt("CLOUDY_PIXEL_PERCENTAGE", 20))
|
| 27 |
.sort("CLOUDY_PIXEL_PERCENTAGE")
|
|
|
|
|
|
|
| 28 |
)
|
| 29 |
-
|
| 30 |
-
image = collection.first().clip(region)
|
| 31 |
return image, region
|
| 32 |
|
|
|
|
| 33 |
def compute_ndvi(image):
|
| 34 |
-
|
| 35 |
-
|
| 36 |
|
| 37 |
def compute_ndwi(image):
|
| 38 |
-
|
| 39 |
-
|
| 40 |
|
| 41 |
def get_ndvi_stats(ndvi, region):
|
| 42 |
stats = ndvi.reduceRegion(
|
| 43 |
-
reducer=ee.Reducer.mean().combine(
|
| 44 |
-
|
| 45 |
-
).combine(
|
| 46 |
-
ee.Reducer.max(), sharedInputs=True
|
| 47 |
-
),
|
| 48 |
geometry=region,
|
| 49 |
scale=10,
|
| 50 |
maxPixels=1e9
|
| 51 |
).getInfo()
|
| 52 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
def classify_land(image, region):
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
ndwi = compute_ndwi(image)
|
| 58 |
-
|
| 59 |
-
vegetation = ndvi.gt(0.4)
|
| 60 |
-
agriculture = ndvi.gt(0.2).And(ndvi.lte(0.4))
|
| 61 |
-
water = ndwi.gt(0.0)
|
| 62 |
-
urban = image.select("B11").gt(2000).And(ndvi.lt(0.2))
|
| 63 |
-
bare = ndvi.lt(0.2).And(water.Not()).And(urban.Not())
|
| 64 |
-
|
| 65 |
-
classified = (
|
| 66 |
-
water.multiply(1)
|
| 67 |
-
.add(vegetation.multiply(2))
|
| 68 |
-
.add(agriculture.multiply(3))
|
| 69 |
-
.add(urban.multiply(4))
|
| 70 |
-
.add(bare.multiply(5))
|
| 71 |
-
)
|
| 72 |
-
|
| 73 |
total = region.area().getInfo()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
areas = {}
|
| 75 |
-
for name, mask in
|
| 76 |
-
px
|
| 77 |
area = px.reduceRegion(ee.Reducer.sum(), region, 10, maxPixels=1e9).getInfo()
|
| 78 |
-
|
| 79 |
-
|
|
|
|
| 80 |
return areas
|
|
|
|
| 3 |
import json
|
| 4 |
import base64
|
| 5 |
|
| 6 |
+
# ── Chemins et constantes ────────────────────────────────────────────────────
|
| 7 |
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 8 |
+
|
| 9 |
+
GEE_PROJECT = os.environ.get("GEE_PROJECT", "ee-zanakone")
|
| 10 |
+
GEE_SA_EMAIL = os.environ.get("GEE_SA_EMAIL", "pipeline-agb@ee-zanakone.iam.gserviceaccount.com")
|
| 11 |
+
GEE_KEY_PATH = os.path.join(BASE_DIR, "cle_service_gee.json")
|
| 12 |
+
|
| 13 |
+
# ── Initialisation ───────────────────────────────────────────────────────────
|
| 14 |
def init_gee():
|
| 15 |
+
"""
|
| 16 |
+
Priorité :
|
| 17 |
+
1. Fichier JSON local (développement local)
|
| 18 |
+
2. Secret HF base64 (production Hugging Face)
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
# --- 1. Fichier local présent ? ---
|
| 22 |
+
if os.path.exists(GEE_KEY_PATH):
|
| 23 |
+
print(f"🔑 Clé GEE chargée depuis fichier local : {GEE_KEY_PATH}")
|
| 24 |
+
credentials = ee.ServiceAccountCredentials(
|
| 25 |
+
email=GEE_SA_EMAIL,
|
| 26 |
+
key_file=GEE_KEY_PATH
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
# --- 2. Secret HF (base64) ---
|
| 30 |
+
else:
|
| 31 |
+
key_b64 = os.environ.get("GEE_KEY_JSON")
|
| 32 |
+
if not key_b64:
|
| 33 |
+
raise EnvironmentError(
|
| 34 |
+
"❌ Aucune clé GEE trouvée.\n"
|
| 35 |
+
f" - En local : placez 'cle_service_gee.json' dans {BASE_DIR}\n"
|
| 36 |
+
" - Sur HF : ajoutez le secret GEE_KEY_JSON (base64)"
|
| 37 |
+
)
|
| 38 |
+
print("🔑 Clé GEE chargée depuis secret Hugging Face")
|
| 39 |
+
try:
|
| 40 |
+
key_dict = json.loads(base64.b64decode(key_b64.strip()).decode("utf-8"))
|
| 41 |
+
except Exception as e:
|
| 42 |
+
raise ValueError(f"❌ Décodage base64 échoué : {e}")
|
| 43 |
|
| 44 |
+
credentials = ee.ServiceAccountCredentials(
|
| 45 |
+
email=GEE_SA_EMAIL,
|
| 46 |
+
key_data=json.dumps(key_dict)
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
# --- Initialisation avec projet ---
|
| 50 |
+
try:
|
| 51 |
+
ee.Initialize(credentials, project=GEE_PROJECT)
|
| 52 |
+
print(f"✅ GEE initialisé — projet : {GEE_PROJECT}")
|
| 53 |
+
except Exception as e:
|
| 54 |
+
raise ConnectionError(f"❌ ee.Initialize() échoué : {e}")
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
# ── Fonctions d'analyse ──────────────────────────────────────────────────────
|
| 58 |
def get_sentinel2_image(lat, lng, buffer_m, year):
|
| 59 |
+
point = ee.Geometry.Point([lng, lat])
|
|
|
|
| 60 |
region = point.buffer(buffer_m)
|
| 61 |
+
|
| 62 |
+
image = (
|
|
|
|
|
|
|
|
|
|
| 63 |
ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
|
| 64 |
.filterBounds(region)
|
| 65 |
+
.filterDate(f"{year}-01-01", f"{year}-12-31")
|
| 66 |
.filter(ee.Filter.lt("CLOUDY_PIXEL_PERCENTAGE", 20))
|
| 67 |
.sort("CLOUDY_PIXEL_PERCENTAGE")
|
| 68 |
+
.first()
|
| 69 |
+
.clip(region)
|
| 70 |
)
|
|
|
|
|
|
|
| 71 |
return image, region
|
| 72 |
|
| 73 |
+
|
| 74 |
def compute_ndvi(image):
|
| 75 |
+
return image.normalizedDifference(["B8", "B4"]).rename("NDVI")
|
| 76 |
+
|
| 77 |
|
| 78 |
def compute_ndwi(image):
|
| 79 |
+
return image.normalizedDifference(["B3", "B8"]).rename("NDWI")
|
| 80 |
+
|
| 81 |
|
| 82 |
def get_ndvi_stats(ndvi, region):
|
| 83 |
stats = ndvi.reduceRegion(
|
| 84 |
+
reducer=ee.Reducer.mean().combine(ee.Reducer.min(), sharedInputs=True)
|
| 85 |
+
.combine(ee.Reducer.max(), sharedInputs=True),
|
|
|
|
|
|
|
|
|
|
| 86 |
geometry=region,
|
| 87 |
scale=10,
|
| 88 |
maxPixels=1e9
|
| 89 |
).getInfo()
|
| 90 |
+
return {
|
| 91 |
+
"mean": round(stats.get("NDVI_mean") or 0, 3),
|
| 92 |
+
"min": round(stats.get("NDVI_min") or 0, 3),
|
| 93 |
+
"max": round(stats.get("NDVI_max") or 0, 3),
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
|
| 97 |
def classify_land(image, region):
|
| 98 |
+
ndvi = compute_ndvi(image)
|
| 99 |
+
ndwi = compute_ndwi(image)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
total = region.area().getInfo()
|
| 101 |
+
|
| 102 |
+
masks = {
|
| 103 |
+
"eau": ndwi.gt(0.0),
|
| 104 |
+
"végétation": ndvi.gt(0.4),
|
| 105 |
+
"agriculture": ndvi.gt(0.2).And(ndvi.lte(0.4)),
|
| 106 |
+
"urbain": image.select("B11").gt(2000).And(ndvi.lt(0.2)),
|
| 107 |
+
"sol nu": ndvi.lt(0.2).And(ndwi.lte(0.0)).And(image.select("B11").lte(2000)),
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
areas = {}
|
| 111 |
+
for name, mask in masks.items():
|
| 112 |
+
px = mask.multiply(ee.Image.pixelArea())
|
| 113 |
area = px.reduceRegion(ee.Reducer.sum(), region, 10, maxPixels=1e9).getInfo()
|
| 114 |
+
val = area.get("NDVI") or area.get("B11") or area.get("nd") or 0
|
| 115 |
+
areas[name] = round((val / total) * 100, 1)
|
| 116 |
+
|
| 117 |
return areas
|