UjjwalKGupta commited on
Commit
62b5810
·
verified ·
1 Parent(s): 99fbbdd

Add Legend and Histogram

Browse files
Files changed (1) hide show
  1. app.py +99 -38
app.py CHANGED
@@ -77,7 +77,7 @@ def get_gdf_from_url(url):
77
  """Downloads and reads a KML/GeoJSON from a URL."""
78
  if not url or not url.strip():
79
  return None
80
-
81
  # Handle Google Drive URLs
82
  if "drive.google.com" in url:
83
  if "/file/d/" in url:
@@ -174,32 +174,32 @@ def get_wayback_data():
174
  url = "https://wayback.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/MapServer/WMTS/1.0.0/WMTSCapabilities.xml"
175
  response = requests.get(url)
176
  response.raise_for_status() # Ensure request was successful
177
-
178
  # Parse XML
179
  root = ET.fromstring(response.content)
180
-
181
  ns = {
182
- "wmts": "https://www.opengis.net/wmts/1.0",
183
- "ows": "https://www.opengis.net/ows/1.1",
184
- "xlink": "https://www.w3.org/1999/xlink",
185
  }
186
-
187
  # Use a robust XPath to find all 'Layer' elements anywhere in the document.
188
  # This is less brittle than specifying the full path.
189
  layers = root.findall(".//wmts:Contents/wmts:Layer", ns)
190
-
191
  layer_data = []
192
  for layer in layers:
193
  title = layer.find("ows:Title", ns)
194
  identifier = layer.find("ows:Identifier", ns)
195
  resource = layer.find("wmts:ResourceURL", ns) # Tile URL template
196
-
197
  title_text = title.text if title is not None else "N/A"
198
  identifier_text = identifier.text if identifier is not None else "N/A"
199
  url_template = resource.get("template") if resource is not None else "N/A"
200
-
201
  layer_data.append({"Title": title_text, "ResourceURL_Template": url_template})
202
-
203
  wayback_df = pd.DataFrame(layer_data)
204
  wayback_df["date"] = pd.to_datetime(wayback_df["Title"].str.extract(r"(\d{4}-\d{2}-\d{2})").squeeze(), errors="coerce")
205
  wayback_df.set_index("date", inplace=True)
@@ -344,7 +344,7 @@ def process_and_display(file_obj, url_str, buffer_m, progress=gr.Progress()):
344
  .replace("{TileCol}", "{x}")
345
  )
346
  folium.TileLayer(tiles=wayback_url, attr="Esri", name=wayback_title).add_to(m)
347
-
348
  m = add_geometry_to_map(m, geometry_gdf, buffer_geometry_gdf, opacity=0.3)
349
  m.add_child(folium.LayerControl())
350
  bounds = geometry_gdf.to_crs(epsg=4326).total_bounds
@@ -426,7 +426,7 @@ def calculate_indices(
426
  for veg_index in veg_indices:
427
  plot_cols = [veg_index, f"{veg_index}_buffer", f"{veg_index}_ratio"]
428
  existing_plot_cols = [col for col in plot_cols if col in result_df.columns]
429
-
430
  plot_df = result_df[['Year'] + existing_plot_cols].dropna()
431
 
432
  if not plot_df.empty:
@@ -442,42 +442,60 @@ def calculate_indices(
442
  traceback.print_exc()
443
  return f"An error occurred during calculation: {e}", None, None, None
444
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
  def generate_comparison_maps(geometry_json, selected_index, selected_years, evi_vars, date_start_str, date_end_str, progress=gr.Progress()):
446
- """Generates side-by-side maps for a selected index and two selected years."""
447
  if not geometry_json or not selected_index or not selected_years:
448
  return "Please process a file and select an index and years first.", "", ""
449
  if len(selected_years) != 2:
450
  return "Please select exactly two years to compare.", "", ""
451
-
452
  one_time_setup()
453
  geometry_gdf = gpd.read_file(geometry_json).to_crs(4326)
454
  ee_geometry = ee.Geometry(json.loads(geometry_gdf.to_json())['features'][0]['geometry'])
455
  bounds = geometry_gdf.total_bounds
456
  map_bounds = [[bounds[1], bounds[0]], [bounds[3], bounds[2]]]
457
-
458
  start_month, start_day = map(int, date_start_str.split('-'))
459
  end_month, end_day = map(int, date_end_str.split('-'))
460
 
461
- vis_params = {
462
- "min": 0.0, "max": 1.0,
463
- "palette": ['FFFFFF', 'CE7E45', 'DF923D', 'F1B555', 'FCD163', '99B718', '74A901', '66A000', '529400', '3E8601', '207401', '056201', '004C00', '023B01', '012E01', '011D01', '011301']
464
- }
465
-
466
  maps_html = []
467
  for i, year in enumerate(selected_years):
468
  progress((i + 1) / 2, desc=f"Generating map for {year}")
469
  start_date = f"{year}-{start_month:02d}-{start_day:02d}"
470
  end_date = f"{year}-{end_month:02d}-{end_day:02d}"
471
-
 
472
  wayback_url = None
473
  wayback_title = "Default Satellite"
474
  if not WAYBACK_DF.empty:
475
  try:
476
  target_date = datetime(int(year), start_month, 15)
477
- # Find the index of the closest date in the wayback dataframe
478
  nearest_idx = WAYBACK_DF.index.get_indexer([target_date], method='nearest')[0]
479
  wayback_item = WAYBACK_DF.iloc[nearest_idx]
480
-
481
  wayback_title = f"Esri Wayback ({wayback_item.name.strftime('%Y-%m-%d')})"
482
  wayback_url = (
483
  wayback_item["ResourceURL_Template"]
@@ -488,9 +506,9 @@ def generate_comparison_maps(geometry_json, selected_index, selected_years, evi_
488
  )
489
  except Exception as e:
490
  print(f"Could not find a suitable Wayback basemap for {year}: {e}")
491
- # Fallback to default values if any error occurs
492
  wayback_url = None
493
  wayback_title = "Default Satellite"
 
494
 
495
  collection = (
496
  ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
@@ -508,17 +526,60 @@ def generate_comparison_maps(geometry_json, selected_index, selected_years, evi_
508
  continue
509
 
510
  mosaic = collection.qualityMosaic(selected_index)
511
- clipped_image = mosaic.select(selected_index).clip(ee_geometry)
512
 
513
- # Create map and add the year-specific basemap
514
  m = gee_folium.Map(zoom_start=14)
515
  if wayback_url:
516
  m.add_tile_layer(wayback_url, name=wayback_title, attribution="Esri")
517
  else:
518
  m.add_basemap("SATELLITE")
519
 
520
- m.addLayer(clipped_image, vis_params, f"{selected_index} {year}")
521
- m.add_colorbar(vis_params=vis_params, label=f"{selected_index} Value")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
522
  folium.GeoJson(geometry_gdf, name="Geometry", style_function=lambda x: {"color": "yellow", "fillOpacity": 0, "weight": 2.5}).add_to(m)
523
  m.fit_bounds(map_bounds, padding=(10, 10))
524
  m.addLayerControl()
@@ -529,7 +590,7 @@ def generate_comparison_maps(geometry_json, selected_index, selected_years, evi_
529
 
530
  return f"Comparison generated for {selected_years[0]} and {selected_years[1]}.", maps_html[0], maps_html[1]
531
 
532
- # --- Gradio Interface ---
533
  theme = gr.themes.Soft(primary_hue="teal", secondary_hue="orange").set(
534
  background_fill_primary="white"
535
  )
@@ -556,7 +617,7 @@ with gr.Blocks(theme=theme, title="Kamlan: KML Analyzer") as demo:
556
  url_input = gr.Textbox(label="Or Provide File URL", placeholder="e.g., https://.../my_file.kml")
557
  buffer_input = gr.Number(label="Buffer (meters)", value=50)
558
  process_button = gr.Button("Process Input", variant="primary")
559
-
560
  with gr.Accordion("Advanced Settings", open=False):
561
  gr.Markdown("### Select Vegetation Indices")
562
  all_veg_indices = ["GujVDI", "NDVI", "EVI", "EVI2", "RandomForest", "CI"]
@@ -581,14 +642,14 @@ with gr.Blocks(theme=theme, title="Kamlan: KML Analyzer") as demo:
581
  max_year_input = gr.Number(label="End Year", value=today.year, precision=0)
582
 
583
  calculate_button = gr.Button("Calculate Vegetation Indices", variant="primary")
584
-
585
 
586
  with gr.Column(scale=2):
587
  gr.Markdown("## 2. Results")
588
  info_box = gr.Textbox(label="Status", interactive=False, placeholder="Status messages will appear here...")
589
  map_output = gr.HTML(label="Map View")
590
  stats_output = gr.DataFrame(label="Geometry Metrics")
591
-
592
  gr.Markdown("### Digital Elevation Model (DEM) and Slope")
593
  with gr.Row():
594
  dem_map_output = gr.HTML(label="DEM Map")
@@ -605,9 +666,9 @@ with gr.Blocks(theme=theme, title="Kamlan: KML Analyzer") as demo:
605
  with gr.Row():
606
  comparison_index_select = gr.Radio(all_veg_indices, label="Select Index for Comparison", value="NDVI")
607
  comparison_years_select = gr.CheckboxGroup(label="Select Two Years to Compare", choices=[])
608
-
609
  compare_button = gr.Button("Generate Comparison Maps", variant="secondary")
610
-
611
  with gr.Row():
612
  map_year_1_output = gr.HTML(label="Comparison Map 1")
613
  map_year_2_output = gr.HTML(label="Comparison Map 2")
@@ -640,15 +701,15 @@ with gr.Blocks(theme=theme, title="Kamlan: KML Analyzer") as demo:
640
  geometry_json, buffer_json, veg_indices,
641
  evi_vars, date_range, int(min_year), int(max_year), progress
642
  )
643
-
644
  status_message = error_msg or success_msg
645
  first_plot = plots[0] if plots else None
646
  df_display = df.round(3) if df is not None else None
647
-
648
  available_years = []
649
  if df is not None and 'Year' in df.columns:
650
  available_years = sorted(df['Year'].unique().tolist())
651
-
652
  return status_message, df_display, df, first_plot, gr.update(choices=available_years, value=[])
653
 
654
  except Exception as e:
 
77
  """Downloads and reads a KML/GeoJSON from a URL."""
78
  if not url or not url.strip():
79
  return None
80
+
81
  # Handle Google Drive URLs
82
  if "drive.google.com" in url:
83
  if "/file/d/" in url:
 
174
  url = "https://wayback.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/MapServer/WMTS/1.0.0/WMTSCapabilities.xml"
175
  response = requests.get(url)
176
  response.raise_for_status() # Ensure request was successful
177
+
178
  # Parse XML
179
  root = ET.fromstring(response.content)
180
+
181
  ns = {
182
+ "wmts": "http://www.opengis.net/wmts/1.0",
183
+ "ows": "http://www.opengis.net/ows/1.1",
184
+ "xlink": "http://www.w3.org/1999/xlink",
185
  }
186
+
187
  # Use a robust XPath to find all 'Layer' elements anywhere in the document.
188
  # This is less brittle than specifying the full path.
189
  layers = root.findall(".//wmts:Contents/wmts:Layer", ns)
190
+
191
  layer_data = []
192
  for layer in layers:
193
  title = layer.find("ows:Title", ns)
194
  identifier = layer.find("ows:Identifier", ns)
195
  resource = layer.find("wmts:ResourceURL", ns) # Tile URL template
196
+
197
  title_text = title.text if title is not None else "N/A"
198
  identifier_text = identifier.text if identifier is not None else "N/A"
199
  url_template = resource.get("template") if resource is not None else "N/A"
200
+
201
  layer_data.append({"Title": title_text, "ResourceURL_Template": url_template})
202
+
203
  wayback_df = pd.DataFrame(layer_data)
204
  wayback_df["date"] = pd.to_datetime(wayback_df["Title"].str.extract(r"(\d{4}-\d{2}-\d{2})").squeeze(), errors="coerce")
205
  wayback_df.set_index("date", inplace=True)
 
344
  .replace("{TileCol}", "{x}")
345
  )
346
  folium.TileLayer(tiles=wayback_url, attr="Esri", name=wayback_title).add_to(m)
347
+
348
  m = add_geometry_to_map(m, geometry_gdf, buffer_geometry_gdf, opacity=0.3)
349
  m.add_child(folium.LayerControl())
350
  bounds = geometry_gdf.to_crs(epsg=4326).total_bounds
 
426
  for veg_index in veg_indices:
427
  plot_cols = [veg_index, f"{veg_index}_buffer", f"{veg_index}_ratio"]
428
  existing_plot_cols = [col for col in plot_cols if col in result_df.columns]
429
+
430
  plot_df = result_df[['Year'] + existing_plot_cols].dropna()
431
 
432
  if not plot_df.empty:
 
442
  traceback.print_exc()
443
  return f"An error occurred during calculation: {e}", None, None, None
444
 
445
+ # NEW/MODIFIED FUNCTIONS START HERE
446
+
447
+ def get_histogram(index_name, image, geometry, bins):
448
+ """Calculates the histogram for an image within a given geometry using GEE."""
449
+ try:
450
+ # Request histogram data from Earth Engine
451
+ hist_info = image.reduceRegion(
452
+ reducer=ee.Reducer.fixedHistogram(min=bins[0], max=bins[-1], steps=len(bins)-1),
453
+ geometry=geometry,
454
+ scale=10, # Scale in meters appropriate for Sentinel-2
455
+ maxPixels=1e9
456
+ ).get(index_name).getInfo()
457
+
458
+ # Extract histogram counts
459
+ if hist_info:
460
+ histogram = [item[1] for item in hist_info]
461
+ return np.array(histogram), bins
462
+ else:
463
+ # Return empty histogram if no data
464
+ return np.array([0] * (len(bins) - 1)), bins
465
+ except Exception as e:
466
+ print(f"Could not compute histogram for {index_name}: {e}")
467
+ return np.array([0] * (len(bins) - 1)), bins
468
+
469
  def generate_comparison_maps(geometry_json, selected_index, selected_years, evi_vars, date_start_str, date_end_str, progress=gr.Progress()):
470
+ """Generates side-by-side maps for a selected index and two selected years with a classified legend."""
471
  if not geometry_json or not selected_index or not selected_years:
472
  return "Please process a file and select an index and years first.", "", ""
473
  if len(selected_years) != 2:
474
  return "Please select exactly two years to compare.", "", ""
475
+
476
  one_time_setup()
477
  geometry_gdf = gpd.read_file(geometry_json).to_crs(4326)
478
  ee_geometry = ee.Geometry(json.loads(geometry_gdf.to_json())['features'][0]['geometry'])
479
  bounds = geometry_gdf.total_bounds
480
  map_bounds = [[bounds[1], bounds[0]], [bounds[3], bounds[2]]]
481
+
482
  start_month, start_day = map(int, date_start_str.split('-'))
483
  end_month, end_day = map(int, date_end_str.split('-'))
484
 
 
 
 
 
 
485
  maps_html = []
486
  for i, year in enumerate(selected_years):
487
  progress((i + 1) / 2, desc=f"Generating map for {year}")
488
  start_date = f"{year}-{start_month:02d}-{start_day:02d}"
489
  end_date = f"{year}-{end_month:02d}-{end_day:02d}"
490
+
491
+ # --- Wayback Basemap Logic (unchanged) ---
492
  wayback_url = None
493
  wayback_title = "Default Satellite"
494
  if not WAYBACK_DF.empty:
495
  try:
496
  target_date = datetime(int(year), start_month, 15)
 
497
  nearest_idx = WAYBACK_DF.index.get_indexer([target_date], method='nearest')[0]
498
  wayback_item = WAYBACK_DF.iloc[nearest_idx]
 
499
  wayback_title = f"Esri Wayback ({wayback_item.name.strftime('%Y-%m-%d')})"
500
  wayback_url = (
501
  wayback_item["ResourceURL_Template"]
 
506
  )
507
  except Exception as e:
508
  print(f"Could not find a suitable Wayback basemap for {year}: {e}")
 
509
  wayback_url = None
510
  wayback_title = "Default Satellite"
511
+ # --- End Wayback Logic ---
512
 
513
  collection = (
514
  ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
 
526
  continue
527
 
528
  mosaic = collection.qualityMosaic(selected_index)
 
529
 
530
+ # Create the base map
531
  m = gee_folium.Map(zoom_start=14)
532
  if wayback_url:
533
  m.add_tile_layer(wayback_url, name=wayback_title, attribution="Esri")
534
  else:
535
  m.add_basemap("SATELLITE")
536
 
537
+ # --- START: New Legend and Layer Logic ---
538
+ if selected_index in ["NDVI", "RandomForest", "GujVDI", "CI", "EVI", "EVI2"]:
539
+ # Define bins for classification
540
+ bins = [0, 0.2, 0.4, 0.6, 0.8, 1]
541
+ histogram, bin_edges = get_histogram(selected_index, mosaic.select(selected_index), ee_geometry, bins)
542
+
543
+ total_pix = np.sum(histogram)
544
+ # Format histogram values as percentages, handle division by zero
545
+ if total_pix > 0:
546
+ formatted_histogram = [f"{h*100/total_pix:.2f}" for h in histogram]
547
+ else:
548
+ formatted_histogram = ["0.00"] * len(histogram)
549
+
550
+ # Add the classified legend to the map
551
+ m.add_legend(
552
+ title=f"{selected_index} Classification ({year})",
553
+ legend_dict={
554
+ "0-0.2: Open/Sparse Vegetation ({}%)".format(formatted_histogram[0]): "#FF0000",
555
+ "0.2-0.4: Low Vegetation ({}%)".format(formatted_histogram[1]): "#FFFF00",
556
+ "0.4-0.6: Moderate Vegetation ({}%)".format(formatted_histogram[2]): "#FFA500",
557
+ "0.6-0.8: Dense Vegetation ({}%)".format(formatted_histogram[3]): "#00FE00",
558
+ "0.8-1: Very Dense Vegetation ({}%)".format(formatted_histogram[4]): "#00A400",
559
+ },
560
+ position="bottomright",
561
+ draggable=False,
562
+ )
563
+
564
+ # Define visualization parameters for the classified layer
565
+ ind_vis_params = {
566
+ "min": 0,
567
+ "max": 1,
568
+ "palette": ["#FF0000", "#FFFF00", "#FFA500", "#00FE00", "#00A400"],
569
+ }
570
+ # Add the classified raster layer to the map
571
+ m.addLayer(mosaic.select(selected_index).clip(ee_geometry), ind_vis_params, f"{selected_index} Classified Layer ({year})")
572
+
573
+ else: # Fallback for indices without a classified legend (original behavior)
574
+ vis_params = {
575
+ "min": 0.0, "max": 1.0,
576
+ "palette": ['FFFFFF', 'CE7E45', 'DF923D', 'F1B555', 'FCD163', '99B718', '74A901', '66A000', '529400', '3E8601', '207401', '056201', '004C00', '023B01', '012E01', '011D01', '011301']
577
+ }
578
+ clipped_image = mosaic.select(selected_index).clip(ee_geometry)
579
+ m.addLayer(clipped_image, vis_params, f"{selected_index} {year}")
580
+ m.add_colorbar(vis_params=vis_params, label=f"{selected_index} Value")
581
+ # --- END: New Legend and Layer Logic ---
582
+
583
  folium.GeoJson(geometry_gdf, name="Geometry", style_function=lambda x: {"color": "yellow", "fillOpacity": 0, "weight": 2.5}).add_to(m)
584
  m.fit_bounds(map_bounds, padding=(10, 10))
585
  m.addLayerControl()
 
590
 
591
  return f"Comparison generated for {selected_years[0]} and {selected_years[1]}.", maps_html[0], maps_html[1]
592
 
593
+ # --- Gradio Interface (unchanged) ---
594
  theme = gr.themes.Soft(primary_hue="teal", secondary_hue="orange").set(
595
  background_fill_primary="white"
596
  )
 
617
  url_input = gr.Textbox(label="Or Provide File URL", placeholder="e.g., https://.../my_file.kml")
618
  buffer_input = gr.Number(label="Buffer (meters)", value=50)
619
  process_button = gr.Button("Process Input", variant="primary")
620
+
621
  with gr.Accordion("Advanced Settings", open=False):
622
  gr.Markdown("### Select Vegetation Indices")
623
  all_veg_indices = ["GujVDI", "NDVI", "EVI", "EVI2", "RandomForest", "CI"]
 
642
  max_year_input = gr.Number(label="End Year", value=today.year, precision=0)
643
 
644
  calculate_button = gr.Button("Calculate Vegetation Indices", variant="primary")
645
+
646
 
647
  with gr.Column(scale=2):
648
  gr.Markdown("## 2. Results")
649
  info_box = gr.Textbox(label="Status", interactive=False, placeholder="Status messages will appear here...")
650
  map_output = gr.HTML(label="Map View")
651
  stats_output = gr.DataFrame(label="Geometry Metrics")
652
+
653
  gr.Markdown("### Digital Elevation Model (DEM) and Slope")
654
  with gr.Row():
655
  dem_map_output = gr.HTML(label="DEM Map")
 
666
  with gr.Row():
667
  comparison_index_select = gr.Radio(all_veg_indices, label="Select Index for Comparison", value="NDVI")
668
  comparison_years_select = gr.CheckboxGroup(label="Select Two Years to Compare", choices=[])
669
+
670
  compare_button = gr.Button("Generate Comparison Maps", variant="secondary")
671
+
672
  with gr.Row():
673
  map_year_1_output = gr.HTML(label="Comparison Map 1")
674
  map_year_2_output = gr.HTML(label="Comparison Map 2")
 
701
  geometry_json, buffer_json, veg_indices,
702
  evi_vars, date_range, int(min_year), int(max_year), progress
703
  )
704
+
705
  status_message = error_msg or success_msg
706
  first_plot = plots[0] if plots else None
707
  df_display = df.round(3) if df is not None else None
708
+
709
  available_years = []
710
  if df is not None and 'Year' in df.columns:
711
  available_years = sorted(df['Year'].unique().tolist())
712
+
713
  return status_message, df_display, df, first_plot, gr.update(choices=available_years, value=[])
714
 
715
  except Exception as e: