UjjwalKGupta commited on
Commit
e27e681
·
verified ·
1 Parent(s): 6a7d4fa

Update add_indices

Browse files
Files changed (1) hide show
  1. app.py +90 -60
app.py CHANGED
@@ -23,6 +23,7 @@ import xml.etree.ElementTree as ET
23
  def one_time_setup():
24
  """Initializes the Earth Engine API."""
25
  try:
 
26
  ee.Initialize()
27
  except Exception as e:
28
  try:
@@ -36,11 +37,12 @@ def one_time_setup():
36
  credentials = ee.ServiceAccountCredentials('ujjwal@ee-ujjwaliitd.iam.gserviceaccount.com', credentials_path)
37
  ee.Initialize(credentials, project='ee-ujjwaliitd')
38
  else:
 
39
  raise e
40
  except Exception as inner_e:
 
41
  print(f"Earth Engine initialization failed: {inner_e}")
42
- # In a real app, you might want to show an error to the user here
43
- # For this Gradio app, we'll let it proceed and fail gracefully later if EE is needed.
44
 
45
  def _process_spatial_data(data_bytes):
46
  """Core function to process bytes of a KML or GeoJSON file."""
@@ -78,7 +80,7 @@ def get_gdf_from_url(url):
78
  """Downloads and reads a KML/GeoJSON from a URL."""
79
  if not url or not url.strip():
80
  return None
81
-
82
  # Handle Google Drive URLs
83
  if "drive.google.com" in url:
84
  if "/file/d/" in url:
@@ -173,39 +175,44 @@ def get_wayback_data():
173
  """Fetches and parses Wayback imagery data from ArcGIS."""
174
  try:
175
  url = "https://wayback.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/MapServer/WMTS/1.0.0/WMTSCapabilities.xml"
176
- response = requests.get(url)
177
- response.raise_for_status() # Ensure request was successful
178
-
179
- # Parse XML
180
  root = ET.fromstring(response.content)
181
-
 
182
  ns = {
183
- "wmts": "https://www.opengis.net/wmts/1.0",
184
- "ows": "https://www.opengis.net/ows/1.1",
185
- "xlink": "https://www.w3.org/1999/xlink",
186
  }
187
-
188
  # Use a robust XPath to find all 'Layer' elements anywhere in the document.
189
- # This is less brittle than specifying the full path.
190
- layers = root.findall(".//wmts:Contents/wmts:Layer", ns)
191
-
192
  layer_data = []
193
  for layer in layers:
194
- title = layer.find("ows:Title", ns)
195
- identifier = layer.find("ows:Identifier", ns)
196
- resource = layer.find("wmts:ResourceURL", ns) # Tile URL template
197
-
198
- title_text = title.text if title is not None else "N/A"
199
- identifier_text = identifier.text if identifier is not None else "N/A"
200
- url_template = resource.get("template") if resource is not None else "N/A"
201
-
202
- layer_data.append({"Title": title_text, "ResourceURL_Template": url_template})
 
 
 
 
 
 
203
 
204
  wayback_df = pd.DataFrame(layer_data)
205
- wayback_df["date"] = pd.to_datetime(wayback_df["Title"].str.extract(r"(\d{4}-\d{2}-\d{2})").squeeze(), errors="coerce")
 
 
 
206
  wayback_df.set_index("date", inplace=True)
207
- return wayback_df
208
-
209
  except requests.exceptions.RequestException as e:
210
  print(f"Could not fetch Wayback data from URL: {e}")
211
  return pd.DataFrame()
@@ -216,7 +223,7 @@ def get_wayback_data():
216
  print(f"An unexpected error occurred in get_wayback_data: {e}")
217
  return pd.DataFrame() # Return empty dataframe on failure
218
 
219
- def add_indices(image, nir_band, red_band, blue_band, green_band, evi_vars):
220
  """Calculates and adds multiple vegetation indices to an Earth Engine image."""
221
  # It's safer to work with the image bands directly
222
  nir = image.select(nir_band).divide(10000)
@@ -242,16 +249,25 @@ def add_indices(image, nir_band, red_band, blue_band, green_band, evi_vars):
242
  }).rename('EVI2')
243
 
244
  # RandomForest (This part requires a pre-trained model asset in GEE)
245
- # For demonstration, we'll return a constant image if asset is not available
246
  try:
247
- table = ee.FeatureCollection('projects/in793-aq-nb-24330048/assets/cleanedVDI')
 
 
 
 
 
 
 
 
248
  classifier = ee.Classifier.smileRandomForest(50).train(
249
  features=table,
250
- classProperty='cVDI',
251
- inputProperties=['Blue', 'Red', 'NIR']
252
  )
253
- rf = image.select(['Blue', 'Red', 'NIR']).classify(classifier).multiply(0.2).add(0.1).rename('RandomForest')
254
- except Exception:
 
 
255
  rf = ee.Image.constant(0).rename('RandomForest')
256
 
257
 
@@ -273,7 +289,7 @@ def add_indices(image, nir_band, red_band, blue_band, green_band, evi_vars):
273
 
274
  # --- Gradio App Logic ---
275
 
276
- # Initialize GEE and fetch wayback data once
277
  one_time_setup()
278
  WAYBACK_DF = get_wayback_data()
279
 
@@ -319,13 +335,12 @@ def process_and_display(file_obj, url_str, buffer_m, progress=gr.Progress()):
319
 
320
  progress(0.5, desc="Generating map and stats...")
321
 
322
- # Create map - initialize without a specific location
323
  m = folium.Map()
324
-
325
  if not WAYBACK_DF.empty:
326
- # Select the last row, which is the most recent date after sorting
327
- latest_item = WAYBACK_DF.iloc[0]
328
- print(latest_item)
329
  wayback_url = (
330
  latest_item["ResourceURL_Template"]
331
  .replace("{TileMatrixSet}", "GoogleMapsCompatible")
@@ -338,13 +353,13 @@ def process_and_display(file_obj, url_str, buffer_m, progress=gr.Progress()):
338
  attr=f"Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community | Imagery Date: {latest_item.name.strftime('%Y-%m-%d')}",
339
  name="Latest Esri Satellite"
340
  ).add_to(m)
341
-
342
  # Add geometries to the map
343
  m = add_geometry_to_map(m, geometry_gdf, buffer_geometry_gdf, opacity=0.3)
344
-
345
  # Add a layer control panel
346
  m.add_child(folium.LayerControl())
347
-
348
  # Fit the map view to the bounds of the geometry
349
  bounds = geometry_gdf.to_crs(epsg=4326).total_bounds
350
  map_bounds = [[bounds[1], bounds[0]], [bounds[3], bounds[2]]] # Format: [[south, west], [north, east]]
@@ -365,11 +380,15 @@ def process_and_display(file_obj, url_str, buffer_m, progress=gr.Progress()):
365
  progress(1, desc="Done!")
366
  return m._repr_html_(), None, stats_df, geometry_json, buffer_geometry_json
367
 
 
368
  def calculate_indices(
369
  geometry_json, buffer_geometry_json, veg_indices, evi_vars, date_range,
370
  min_year, max_year, progress=gr.Progress()
371
  ):
372
  """Calculates vegetation indices based on user inputs."""
 
 
 
373
  if not all([geometry_json, buffer_geometry_json, veg_indices]):
374
  return "Please process a file and select at least one index first.", None, None, None
375
 
@@ -381,7 +400,7 @@ def calculate_indices(
381
  # Convert to EE geometry
382
  ee_geometry = ee.Geometry(json.loads(geometry_gdf.to_crs(4326).to_json())['features'][0]['geometry'])
383
  buffer_ee_geometry = ee.Geometry(json.loads(buffer_geometry_gdf.to_crs(4326).to_json())['features'][0]['geometry'])
384
-
385
  # Date ranges
386
  start_day, start_month = date_range[0].day, date_range[0].month
387
  end_day, end_month = date_range[1].day, date_range[1].month
@@ -394,10 +413,10 @@ def calculate_indices(
394
  collection = (
395
  ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
396
  .select(
397
- ["B2", "B3", "B4", "B8", "MSK_CLDPRB"],
398
- ["Blue", "Green", "Red", "NIR", "MSK_CLDPRB"]
399
  )
400
- .map(lambda img: add_indices(img, 'NIR', 'Red', 'Blue', 'Green', evi_vars))
401
  )
402
 
403
  result_rows = []
@@ -411,11 +430,21 @@ def calculate_indices(
411
  row = {'daterange': f"{start_date}_to_{end_date}"}
412
  for veg_index in veg_indices:
413
  mosaic = filtered_collection.qualityMosaic(veg_index)
414
- mean_val = mosaic.reduceRegion(reducer=ee.Reducer.mean(), geometry=ee_geometry, scale=10, maxPixels=1e9).get(veg_index).getInfo()
415
- buffer_mean_val = mosaic.reduceRegion(reducer=ee.Reducer.mean(), geometry=buffer_ee_geometry, scale=10, maxPixels=1e9).get(veg_index).getInfo()
416
- row[veg_index] = mean_val
417
- row[f"{veg_index}_buffer"] = buffer_mean_val
418
- row[f"{veg_index}_ratio"] = (mean_val / buffer_mean_val) if buffer_mean_val else 0
 
 
 
 
 
 
 
 
 
 
419
  result_rows.append(row)
420
 
421
  if not result_rows:
@@ -427,14 +456,17 @@ def calculate_indices(
427
  # Create plots
428
  plots = []
429
  for veg_index in veg_indices:
430
- plot_df = result_df[[veg_index, f"{veg_index}_buffer", f"{veg_index}_ratio"]]
431
- fig = px.line(plot_df, x=plot_df.index, y=plot_df.columns, markers=True, title=f"{veg_index} Time Series")
432
- fig.update_layout(xaxis_title="Year", yaxis_title="Index Value")
433
- plots.append(fig)
 
434
 
435
  return None, result_df, plots, "Calculation complete."
436
 
437
  except Exception as e:
 
 
438
  return f"An error occurred during calculation: {e}", None, None, None
439
 
440
 
@@ -498,7 +530,7 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Kamlan: KML Analyzer") as demo:
498
 
499
 
500
  # --- Event Handlers ---
501
-
502
  def process_on_load(request: gr.Request):
503
  """Checks for a 'file_url' query parameter when the app loads and populates the URL input field."""
504
  file_url = request.query_params.get("file_url")
@@ -542,7 +574,6 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Kamlan: KML Analyzer") as demo:
542
  # Catch any other unexpected errors during the process
543
  return f"An error occurred in the calculation wrapper: {e}", None, None
544
 
545
- # **FIX**: Cleaned up the outputs to have a direct one-to-one mapping.
546
  calculate_button.click(
547
  fn=calculate_wrapper,
548
  inputs=[
@@ -554,7 +585,6 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Kamlan: KML Analyzer") as demo:
554
  outputs=[results_info_box, timeseries_table, plot_output]
555
  )
556
 
557
-
558
  gr.HTML("""
559
  <div style="text-align: center; margin-top: 20px;">
560
  <p>Developed by <a href="https://sustainability-lab.github.io/">Sustainability Lab</a>, <a href="https://www.iitgn.ac.in/">IIT Gandhinagar</a></p>
@@ -563,4 +593,4 @@ with gr.Blocks(theme=gr.themes.Soft(), title="Kamlan: KML Analyzer") as demo:
563
  """)
564
 
565
  if __name__ == "__main__":
566
- demo.launch(debug=True)
 
23
  def one_time_setup():
24
  """Initializes the Earth Engine API."""
25
  try:
26
+ # Attempt to initialize with default credentials
27
  ee.Initialize()
28
  except Exception as e:
29
  try:
 
37
  credentials = ee.ServiceAccountCredentials('ujjwal@ee-ujjwaliitd.iam.gserviceaccount.com', credentials_path)
38
  ee.Initialize(credentials, project='ee-ujjwaliitd')
39
  else:
40
+ # If no credentials are found, re-raise the original exception
41
  raise e
42
  except Exception as inner_e:
43
+ # If the fallback also fails, print the error
44
  print(f"Earth Engine initialization failed: {inner_e}")
45
+
 
46
 
47
  def _process_spatial_data(data_bytes):
48
  """Core function to process bytes of a KML or GeoJSON file."""
 
80
  """Downloads and reads a KML/GeoJSON from a URL."""
81
  if not url or not url.strip():
82
  return None
83
+
84
  # Handle Google Drive URLs
85
  if "drive.google.com" in url:
86
  if "/file/d/" in url:
 
175
  """Fetches and parses Wayback imagery data from ArcGIS."""
176
  try:
177
  url = "https://wayback.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/MapServer/WMTS/1.0.0/WMTSCapabilities.xml"
178
+ response = requests.get(url, timeout=30)
179
+ response.raise_for_status()
 
 
180
  root = ET.fromstring(response.content)
181
+
182
+ # Define the namespaces found in the XML document
183
  ns = {
184
+ "wmts": "http://www.opengis.net/wmts/1.0",
185
+ "ows": "http://www.opengis.net/ows/1.1",
 
186
  }
187
+
188
  # Use a robust XPath to find all 'Layer' elements anywhere in the document.
189
+ layers = root.findall(".//wmts:Layer", ns)
190
+
 
191
  layer_data = []
192
  for layer in layers:
193
+ title_element = layer.find("ows:Title", ns)
194
+ resource_element = layer.find("wmts:ResourceURL", ns)
195
+
196
+ # Ensure elements and their attributes exist before trying to access them
197
+ if title_element is not None and resource_element is not None and title_element.text is not None:
198
+ template = resource_element.get("template")
199
+ if template:
200
+ layer_data.append({
201
+ "Title": title_element.text,
202
+ "ResourceURL_Template": template
203
+ })
204
+
205
+ if not layer_data:
206
+ print("Warning: No valid layers with titles and resource URLs found in Wayback XML response.")
207
+ return pd.DataFrame()
208
 
209
  wayback_df = pd.DataFrame(layer_data)
210
+ # Extract date and handle potential parsing errors by coercing them to NaT
211
+ wayback_df["date"] = pd.to_datetime(wayback_df["Title"].str.extract(r"(\d{4}-\d{2}-\d{2})")[0], errors="coerce")
212
+ # Drop rows where date could not be parsed
213
+ wayback_df.dropna(subset=['date'], inplace=True)
214
  wayback_df.set_index("date", inplace=True)
215
+ return wayback_df.sort_index()
 
216
  except requests.exceptions.RequestException as e:
217
  print(f"Could not fetch Wayback data from URL: {e}")
218
  return pd.DataFrame()
 
223
  print(f"An unexpected error occurred in get_wayback_data: {e}")
224
  return pd.DataFrame() # Return empty dataframe on failure
225
 
226
+ def add_indices(image, nir_band, red_band, blue_band, green_band, swir1_band, swir2_band, evi_vars):
227
  """Calculates and adds multiple vegetation indices to an Earth Engine image."""
228
  # It's safer to work with the image bands directly
229
  nir = image.select(nir_band).divide(10000)
 
249
  }).rename('EVI2')
250
 
251
  # RandomForest (This part requires a pre-trained model asset in GEE)
 
252
  try:
253
+ # FIX: Select and rename bands from the training data to match what the classifier expects.
254
+ table = ee.FeatureCollection('projects/in793-aq-nb-24330048/assets/cleanedVDI').select(
255
+ ["B2", "B4", "B8", "cVDI"],
256
+ ["Blue", "Red", "NIR", 'cVDI']
257
+ )
258
+
259
+ bands = ['Blue', 'Red', 'NIR']
260
+ label = 'cVDI'
261
+
262
  classifier = ee.Classifier.smileRandomForest(50).train(
263
  features=table,
264
+ classProperty=label,
265
+ inputProperties=bands,
266
  )
267
+ # Classify the image. The image already has bands named 'Blue', 'Red', 'NIR' from the previous select.
268
+ rf = image.classify(classifier).multiply(ee.Number(0.2)).add(ee.Number(0.1)).rename('RandomForest')
269
+ except Exception as e:
270
+ print(f"Random Forest calculation failed: {e}") # More specific error message
271
  rf = ee.Image.constant(0).rename('RandomForest')
272
 
273
 
 
289
 
290
  # --- Gradio App Logic ---
291
 
292
+ # Initialize GEE and fetch wayback data once at the start
293
  one_time_setup()
294
  WAYBACK_DF = get_wayback_data()
295
 
 
335
 
336
  progress(0.5, desc="Generating map and stats...")
337
 
338
+ # Create map
339
  m = folium.Map()
340
+
341
  if not WAYBACK_DF.empty:
342
+ # The DataFrame is already sorted by date, so the last item is the latest.
343
+ latest_item = WAYBACK_DF.iloc[-1]
 
344
  wayback_url = (
345
  latest_item["ResourceURL_Template"]
346
  .replace("{TileMatrixSet}", "GoogleMapsCompatible")
 
353
  attr=f"Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community | Imagery Date: {latest_item.name.strftime('%Y-%m-%d')}",
354
  name="Latest Esri Satellite"
355
  ).add_to(m)
356
+
357
  # Add geometries to the map
358
  m = add_geometry_to_map(m, geometry_gdf, buffer_geometry_gdf, opacity=0.3)
359
+
360
  # Add a layer control panel
361
  m.add_child(folium.LayerControl())
362
+
363
  # Fit the map view to the bounds of the geometry
364
  bounds = geometry_gdf.to_crs(epsg=4326).total_bounds
365
  map_bounds = [[bounds[1], bounds[0]], [bounds[3], bounds[2]]] # Format: [[south, west], [north, east]]
 
380
  progress(1, desc="Done!")
381
  return m._repr_html_(), None, stats_df, geometry_json, buffer_geometry_json
382
 
383
+
384
  def calculate_indices(
385
  geometry_json, buffer_geometry_json, veg_indices, evi_vars, date_range,
386
  min_year, max_year, progress=gr.Progress()
387
  ):
388
  """Calculates vegetation indices based on user inputs."""
389
+ # **FIX**: Ensure GEE is initialized before making any calls.
390
+ one_time_setup()
391
+
392
  if not all([geometry_json, buffer_geometry_json, veg_indices]):
393
  return "Please process a file and select at least one index first.", None, None, None
394
 
 
400
  # Convert to EE geometry
401
  ee_geometry = ee.Geometry(json.loads(geometry_gdf.to_crs(4326).to_json())['features'][0]['geometry'])
402
  buffer_ee_geometry = ee.Geometry(json.loads(buffer_geometry_gdf.to_crs(4326).to_json())['features'][0]['geometry'])
403
+
404
  # Date ranges
405
  start_day, start_month = date_range[0].day, date_range[0].month
406
  end_day, end_month = date_range[1].day, date_range[1].month
 
413
  collection = (
414
  ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
415
  .select(
416
+ ["B2", "B3", "B4", "B8", "B11", "B12", "MSK_CLDPRB"],
417
+ ["Blue", "Green", "Red", "NIR", "SWIR1", "SWIR2", "MSK_CLDPRB"]
418
  )
419
+ .map(lambda img: add_indices(img, 'NIR', 'Red', 'Blue', 'Green', "SWIR1", "SWIR2", evi_vars))
420
  )
421
 
422
  result_rows = []
 
430
  row = {'daterange': f"{start_date}_to_{end_date}"}
431
  for veg_index in veg_indices:
432
  mosaic = filtered_collection.qualityMosaic(veg_index)
433
+
434
+ mean_dict = mosaic.reduceRegion(reducer=ee.Reducer.mean(), geometry=ee_geometry, scale=10, maxPixels=1e9).getInfo()
435
+ mean_val = mean_dict.get(veg_index) if mean_dict else None
436
+
437
+ buffer_mean_dict = mosaic.reduceRegion(reducer=ee.Reducer.mean(), geometry=buffer_ee_geometry, scale=10, maxPixels=1e9).getInfo()
438
+ buffer_mean_val = buffer_mean_dict.get(veg_index) if buffer_mean_dict else None
439
+
440
+ row[veg_index] = mean_val if mean_val is not None else np.nan
441
+ row[f"{veg_index}_buffer"] = buffer_mean_val if buffer_mean_val is not None else np.nan
442
+
443
+ if mean_val is not None and buffer_mean_val is not None and buffer_mean_val != 0:
444
+ row[f"{veg_index}_ratio"] = mean_val / buffer_mean_val
445
+ else:
446
+ row[f"{veg_index}_ratio"] = np.nan
447
+
448
  result_rows.append(row)
449
 
450
  if not result_rows:
 
456
  # Create plots
457
  plots = []
458
  for veg_index in veg_indices:
459
+ plot_df = result_df[[veg_index, f"{veg_index}_buffer", f"{veg_index}_ratio"]].dropna()
460
+ if not plot_df.empty:
461
+ fig = px.line(plot_df, x=plot_df.index, y=plot_df.columns, markers=True, title=f"{veg_index} Time Series")
462
+ fig.update_layout(xaxis_title="Year", yaxis_title="Index Value")
463
+ plots.append(fig)
464
 
465
  return None, result_df, plots, "Calculation complete."
466
 
467
  except Exception as e:
468
+ import traceback
469
+ traceback.print_exc()
470
  return f"An error occurred during calculation: {e}", None, None, None
471
 
472
 
 
530
 
531
 
532
  # --- Event Handlers ---
533
+
534
  def process_on_load(request: gr.Request):
535
  """Checks for a 'file_url' query parameter when the app loads and populates the URL input field."""
536
  file_url = request.query_params.get("file_url")
 
574
  # Catch any other unexpected errors during the process
575
  return f"An error occurred in the calculation wrapper: {e}", None, None
576
 
 
577
  calculate_button.click(
578
  fn=calculate_wrapper,
579
  inputs=[
 
585
  outputs=[results_info_box, timeseries_table, plot_output]
586
  )
587
 
 
588
  gr.HTML("""
589
  <div style="text-align: center; margin-top: 20px;">
590
  <p>Developed by <a href="https://sustainability-lab.github.io/">Sustainability Lab</a>, <a href="https://www.iitgn.ac.in/">IIT Gandhinagar</a></p>
 
593
  """)
594
 
595
  if __name__ == "__main__":
596
+ demo.launch(debug=True)