KZAS99 commited on
Commit
e59b82a
·
1 Parent(s): f11a226

Initial commit

Browse files
Files changed (4) hide show
  1. .gitignore +18 -0
  2. app.py +275 -0
  3. data/best_model_c_pipeline.pkl +3 -0
  4. requirements.txt +5 -0
.gitignore ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+ *.pyc
9
+
10
+ # System
11
+ .DS_Store
12
+
13
+ # Virtual environments
14
+ .venv/
15
+ .qodo/
16
+ .gradio/
17
+
18
+ secrets/
app.py ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import json
4
+ import tempfile
5
+ from pathlib import Path
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+ import gradio as gr
10
+
11
+ # --- Earth Engine
12
+ import ee
13
+
14
+ # -------------------- AUTH GEE --------------------
15
+ # Sur Hugging Face Spaces : définir ces 2 secrets dans Settings > Repository secrets
16
+ # EE_SERVICE_ACCOUNT -> ex: gee-sa@your-project.iam.gserviceaccount.com
17
+ # EE_SERVICE_KEY -> contenu JSON de la clé (copier-coller)
18
+ SA_EMAIL = os.getenv("EE_SERVICE_ACCOUNT")
19
+ SA_KEY_JSON = os.getenv("EE_SERVICE_KEY")
20
+
21
+ def init_ee():
22
+ if SA_EMAIL and SA_KEY_JSON:
23
+ key_path = os.path.join(tempfile.gettempdir(), "ee-key.json")
24
+ if not os.path.exists(key_path):
25
+ with open(key_path, "w") as f:
26
+ f.write(SA_KEY_JSON)
27
+ creds = ee.ServiceAccountCredentials(SA_EMAIL, key_path)
28
+ ee.Initialize(creds)
29
+ else:
30
+ # Local: ee.Authenticate() une fois dans le terminal, puis:
31
+ try:
32
+ ee.Initialize()
33
+ except Exception as e:
34
+ raise RuntimeError(
35
+ "GEE non initialisé. Définis EE_SERVICE_ACCOUNT/EE_SERVICE_KEY (Space) "
36
+ "ou exécute ee.Authenticate() en local."
37
+ ) from e
38
+
39
+ init_ee()
40
+
41
+ # -------------------- Collections --------------------
42
+ S2 = ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
43
+ S1 = ee.ImageCollection("COPERNICUS/S1_GRD")
44
+ DEM = ee.Image("USGS/SRTMGL1_003")
45
+
46
+ # -------------------- Utilitaires --------------------
47
+ def days_since_utc(utc_iso: str) -> int:
48
+ t_img = time.mktime(time.strptime(utc_iso[:19], "%Y-%m-%dT%H:%M:%S"))
49
+ return int((time.time() - t_img) // 86400)
50
+
51
+ def s2_mask_clouds(img: ee.Image) -> ee.Image:
52
+ # Masque nuages QA60 (bits 10 et 11)
53
+ qa = img.select("QA60")
54
+ cloud = qa.bitwiseAnd(1 << 10).Or(qa.bitwiseAnd(1 << 11)).neq(0)
55
+ return img.updateMask(cloud.Not())
56
+
57
+ def add_s2_scale(img: ee.Image) -> ee.Image:
58
+ # S2_SR est en int16 avec scale = 1e-4
59
+ scale = 1e-4
60
+ bands = img.select(["B1","B2","B3","B4","B5","B6","B7","B8","B8A","B9","B11","B12"])
61
+ return img.addBands(bands.multiply(scale), overwrite=True)
62
+
63
+ def compute_s2_indices(img: ee.Image) -> ee.Image:
64
+ # Band aliases
65
+ B1 = img.select("B1"); B2 = img.select("B2"); B3 = img.select("B3"); B4 = img.select("B4")
66
+ B5 = img.select("B5"); B6 = img.select("B6"); B7 = img.select("B7"); B8 = img.select("B8")
67
+ B8A = img.select("B8A"); B9 = img.select("B9"); B11 = img.select("B11"); B12 = img.select("B12")
68
+
69
+ eps = 1e-6
70
+ NDVI = img.normalizedDifference(["B8","B4"]).rename("NDVI")
71
+ GNDVI = img.normalizedDifference(["B8","B3"]).rename("GNDVI")
72
+ NDWI = img.normalizedDifference(["B3","B8"]).rename("NDWI")
73
+ NDMI = img.normalizedDifference(["B8","B11"]).rename("NDMI")
74
+ NBR = img.normalizedDifference(["B8","B12"]).rename("NBR")
75
+
76
+ EVI = img.expression(
77
+ "2.5 * (NIR - RED) / (NIR + 6*RED - 7.5*BLUE + 1.0 + eps)",
78
+ {"NIR": B8, "RED": B4, "BLUE": B2, "eps": eps}
79
+ ).rename("EVI")
80
+
81
+ SAVI = img.expression(
82
+ "((NIR - RED) / (NIR + RED + L)) * (1 + L)",
83
+ {"NIR": B8, "RED": B4, "L": 0.5}
84
+ ).rename("SAVI")
85
+
86
+ MSAVI2 = img.expression(
87
+ "(2*NIR + 1 - sqrt((2*NIR + 1)^2 - 8*(NIR - RED))) / 2",
88
+ {"NIR": B8, "RED": B4}
89
+ ).rename("MSAVI2")
90
+
91
+ SIPI = img.expression(
92
+ "(NIR - BLUE) / (NIR + RED + eps)",
93
+ {"NIR": B8, "RED": B4, "BLUE": B2, "eps": eps}
94
+ ).rename("SIPI")
95
+
96
+ ARVI = img.expression(
97
+ "(NIR - (2*RED - BLUE)) / (NIR + (2*RED - BLUE) + eps)",
98
+ {"NIR": B8, "RED": B4, "BLUE": B2, "eps": eps}
99
+ ).rename("ARVI")
100
+
101
+ return img.addBands([NDVI, EVI, SAVI, NDWI, NDMI, NBR, GNDVI, MSAVI2, SIPI, ARVI])
102
+
103
+ def s1_preprocess(ic: ee.ImageCollection) -> ee.ImageCollection:
104
+ # Filtre sur mode IW, sélection VV/VH, median composite
105
+ return (ic.filter(ee.Filter.eq("instrumentMode", "IW"))
106
+ .select(["VV","VH"]))
107
+
108
+ def lonlat_to_utm_epsg(lon: float, lat: float) -> int:
109
+ zone = int((lon + 180) // 6) + 1
110
+ if lat >= 0:
111
+ return 32600 + zone # WGS84 UTM N
112
+ else:
113
+ return 32700 + zone # WGS84 UTM S
114
+
115
+ def projected_xy(lon: float, lat: float):
116
+ # Retourne easting/northing (en mètres) du centroïde en UTM
117
+ epsg = f"EPSG:{lonlat_to_utm_epsg(lon, lat)}"
118
+ # Utilise EE pour projeter sans dépendance externe
119
+ pt = ee.Geometry.Point([lon, lat])
120
+ proj = ee.Projection(epsg)
121
+ xy = ee.List(pt.transform(proj, 1).coordinates()).getInfo()
122
+ return float(xy[0]), float(xy[1])
123
+
124
+ # -------------------- Extraction features --------------------
125
+ S2_BANDS = ["B2","B3","B4","B5","B6","B7","B8","B8A","B11","B12","B1","B9"] # ordre stable (12)
126
+ S2_IDX = ["NDVI","EVI","SAVI","NDWI","NDMI","NBR","GNDVI","MSAVI2","SIPI","ARVI"]
127
+ S1_VARS = ["VV","VH","VVVHR"]
128
+ TOPO = ["elevation","slope"]
129
+ COORDS = ["latitude_proj","longitude_proj"] # en UTM (northing/easting) – noms conservés pour compat
130
+
131
+ EXPECTED_COLUMNS = S2_BANDS + S2_IDX + S1_VARS + TOPO + COORDS
132
+
133
+ def extract_from_gee(lat: float, lon: float, radius_m: int = 150):
134
+ pt = ee.Geometry.Point([float(lon), float(lat)])
135
+ roi = pt.buffer(radius_m).bounds()
136
+
137
+ # S2: 2 ans récents, nuages < 40%
138
+ end = ee.Date.now()
139
+ start = end.advance(-2, "year")
140
+ s2 = (S2.filterBounds(roi)
141
+ .filterDate(start, end)
142
+ .filter(ee.Filter.lt("CLOUDY_PIXEL_PERCENTAGE", 40))
143
+ .map(s2_mask_clouds)
144
+ .map(add_s2_scale)
145
+ .map(compute_s2_indices))
146
+
147
+ img_s2 = s2.sort("system:time_start", False).first()
148
+ if img_s2 is None:
149
+ return None, {"error": "No Sentinel-2 image available on ROI/time window."}
150
+
151
+ cloud_cover = ee.Number(img_s2.get("CLOUDY_PIXEL_PERCENTAGE")).getInfo()
152
+ acq_iso = ee.Date(img_s2.get("system:time_start")).format().getInfo()
153
+ days = days_since_utc(acq_iso)
154
+
155
+ # S1: même fenêtre – median composite
156
+ s1 = s1_preprocess(S1.filterBounds(roi).filterDate(start, end))
157
+ img_s1 = s1.median()
158
+
159
+ # DEM
160
+ elevation = DEM.select("elevation")
161
+ slope = ee.Terrain.slope(DEM).rename("slope")
162
+
163
+ # Réductions
164
+ reducer = ee.Reducer.mean()
165
+ s2_vals = img_s2.select(S2_BANDS + S2_IDX).reduceRegion(reducer=reducer, geometry=roi, scale=20, maxPixels=1e8).getInfo()
166
+ s1_vals = img_s1.select(["VV","VH"]).reduceRegion(reducer=reducer, geometry=roi, scale=20, maxPixels=1e8).getInfo()
167
+ topo_vals = elevation.addBands(slope).reduceRegion(reducer=reducer, geometry=roi, scale=30, maxPixels=1e8).getInfo()
168
+
169
+ # Compose features dict
170
+ feats = {}
171
+ # S2 bands & indices
172
+ for k in (S2_BANDS + S2_IDX):
173
+ v = s2_vals.get(k) if s2_vals else None
174
+ feats[k] = float(v) if v is not None else np.nan
175
+
176
+ # S1 VV/VH + ratio
177
+ vv = s1_vals.get("VV") if s1_vals else None
178
+ vh = s1_vals.get("VH") if s1_vals else None
179
+ vv = float(vv) if vv is not None else np.nan
180
+ vh = float(vh) if vh is not None else np.nan
181
+ feats["VV"] = vv
182
+ feats["VH"] = vh
183
+ feats["VVVHR"] = (vv / vh) if (not np.isnan(vv) and not np.isnan(vh) and vh != 0) else np.nan
184
+
185
+ # Topo
186
+ elev = topo_vals.get("elevation") if topo_vals else None
187
+ slp = topo_vals.get("slope") if topo_vals else None
188
+ feats["elevation"] = float(elev) if elev is not None else np.nan
189
+ feats["slope"] = float(slp) if slp is not None else np.nan
190
+
191
+ # Coords projetées UTM (northing/easting)
192
+ easting, northing = projected_xy(lon, lat)
193
+ feats["latitude_proj"] = northing # naming conservé
194
+ feats["longitude_proj"] = easting
195
+
196
+ # NDVI moyen pour affichage
197
+ ndvi_mean = feats["NDVI"] if "NDVI" in feats and not np.isnan(feats["NDVI"]) else None
198
+
199
+ # DataFrame ordonné (colonnes exactement comme attendues)
200
+ row = {k: feats.get(k, np.nan) for k in EXPECTED_COLUMNS}
201
+ X = pd.DataFrame([row], columns=EXPECTED_COLUMNS)
202
+
203
+ return {
204
+ "X": X,
205
+ "cloud": float(cloud_cover),
206
+ "days": int(days),
207
+ "ndvi_mean": (None if ndvi_mean is None else float(ndvi_mean))
208
+ }, None
209
+
210
+ # -------------------- Modèle (pipeline sklearn + CatBoost) --------------------
211
+ def load_pipeline():
212
+ path = Path("data/best_model_c_pipeline.pkl")
213
+ if not path.exists():
214
+ raise RuntimeError("Modèle introuvable: data/best_model_c_pipeline.pkl")
215
+ import joblib
216
+ return joblib.load(path)
217
+
218
+ PIPE = load_pipeline()
219
+
220
+ def predict_agbd(df_row: pd.DataFrame) -> float:
221
+ # Le pipeline gère prétraitements (scalers, PCA, etc.) + CatBoost
222
+ y = PIPE.predict(df_row)
223
+ return float(np.asarray(y).ravel()[0])
224
+
225
+ def carbon_from_agbd(agbd_t_ha: float, cf: float = 0.55) -> float:
226
+ return float(agbd_t_ha * cf)
227
+
228
+ # -------------------- Gradio callbacks --------------------
229
+ def run_all(location_name: str, lat: float, lon: float):
230
+ try:
231
+ data, err = extract_from_gee(lat, lon)
232
+ if err is not None or data is None:
233
+ return ("n/a", "n/a", "n/a", "n/a", "n/a")
234
+ X = data["X"]
235
+ agbd = predict_agbd(X)
236
+ carbon = carbon_from_agbd(agbd)
237
+ cloud = data["cloud"]
238
+ days = data["days"]
239
+ ndvi = data["ndvi_mean"]
240
+ return (f"{cloud:.4f} %",
241
+ f"{days} days ago",
242
+ f"{agbd:.5f} t/ha",
243
+ f"{carbon:.5f} tC/ha",
244
+ ("NDVI: {:.4f}".format(ndvi) if ndvi is not None else "NDVI: n/a"))
245
+ except Exception as e:
246
+ # Retourne 5 champs texte pour éviter les erreurs Gradio côté UI
247
+ return ("error", "", "", "", str(e))
248
+
249
+ with gr.Blocks(theme=gr.themes.Soft(), fill_height=True) as demo:
250
+ gr.Markdown("🌴 **BEEPAS (GEE + CatBoost)** — Biomass estimation 🌴")
251
+
252
+ with gr.Row():
253
+ with gr.Column(scale=1):
254
+ name = gr.Textbox(label="location_name", value="ile boulay :")
255
+ lat = gr.Number(label="lat", value=5.280498, precision=6)
256
+ lon = gr.Number(label="lon", value=-4.089883, precision=6)
257
+ with gr.Row():
258
+ clear = gr.Button("Clear")
259
+ submit = gr.Button("Submit", variant="primary")
260
+ with gr.Column(scale=1):
261
+ cloud = gr.Textbox(label="Cloud coverage")
262
+ days = gr.Textbox(label="Number of days since sensing")
263
+ agbd = gr.Textbox(label="Above ground biomass density (AGBD) t/ha")
264
+ cstock= gr.Textbox(label="Carbon stock density tC/ha")
265
+ ndvi = gr.Textbox(label="Mean NDVI")
266
+
267
+ def _on_submit(n, la, lo): return run_all(n, la, lo)
268
+ submit.click(_on_submit, [name, lat, lon], [cloud, days, agbd, cstock, ndvi])
269
+
270
+ def _on_clear():
271
+ return "", None, None, "", "", ""
272
+ clear.click(_on_clear, outputs=[name, lat, lon, cloud, days, agbd])
273
+
274
+ if __name__ == "__main__":
275
+ demo.launch()
data/best_model_c_pipeline.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5e77506950dabee1d67ddc3c2cb5919eb4c52a52a782c637aafd3c5d7286ac39
3
+ size 720690
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ catboost==1.2.8
2
+ earthengine-api==1.6.13
3
+ gradio>=5.49.1
4
+ joblib==1.5.2
5
+ pandas==2.3.3