import os import ee import geemap import json import geopandas as gpd import streamlit as st import pandas as pd import geojson from shapely.geometry import Polygon, MultiPolygon, shape, Point from io import BytesIO import fiona from shapely import wkb from shapely.ops import transform import leafmap # Enable fiona driver fiona.drvsupport.supported_drivers['LIBKML'] = 'rw' #Intialize EE library # Access secret earthengine_credentials = os.environ.get("EE_Authentication") # Initialize Earth Engine with the secret credentials os.makedirs(os.path.expanduser("~/.config/earthengine/"), exist_ok=True) with open(os.path.expanduser("~/.config/earthengine/credentials"), "w") as f: f.write(earthengine_credentials) ee.Initialize(project='in793-aq-nb-24330048') # Functions def convert_to_2d_geometry(geom): if geom is None: return None elif geom.has_z: return transform(lambda x, y, z: (x, y), geom) else: return geom def validate_KML_file(gdf): if gdf.empty: return { 'corner_points': None, 'area': None, 'perimeter': None, 'is_single_polygon': False} polygon_info = {} # Check if it's a single polygon or multipolygon if isinstance(gdf.iloc[0].geometry, Polygon) and len(gdf)==1: polygon_info['is_single_polygon'] = True polygon = convert_to_2d_geometry(gdf.iloc[0].geometry) # Calculate corner points in GCS projection polygon_info['corner_points'] = [ (polygon.bounds[0], polygon.bounds[1]), (polygon.bounds[2], polygon.bounds[1]), (polygon.bounds[2], polygon.bounds[3]), (polygon.bounds[0], polygon.bounds[3]) ] # Calculate Centroids in GCS projection polygon_info['centroid'] = polygon.centroid.coords[0] # Calculate area and perimeter in EPSG:7761 projection # It is a local projection defined for Gujarat as per NNRMS polygon = gdf.to_crs(epsg=7761).geometry.iloc[0] polygon_info['area'] = polygon.area polygon_info['perimeter'] = polygon.length else: polygon_info['is_single_polygon'] = False polygon_info['corner_points'] = None polygon_info['area'] = None polygon_info['perimeter'] = None polygon_info['centroid'] = None ValueError("Input must be a single Polygon.") return polygon_info # Calculate NDVI as Normalized Index def reduce_zonal_ndvi(image, ee_object): ndvi = image.normalizedDifference(['B8', 'B4']).rename('NDVI') image = image.addBands(ndvi) image = image.select('NDVI') reduced = image.reduceRegion( reducer=ee.Reducer.mean(), geometry=ee_object.geometry(), scale=10, maxPixels=1e12 ) return image.set(reduced) # Get Zonal NDVI def get_zonal_ndvi(collection, geom_ee_object): reduced_collection = collection.map(lambda image: reduce_zonal_ndvi(image, ee_object=geom_ee_object)) stats_list = reduced_collection.aggregate_array('NDVI').getInfo() filenames = reduced_collection.aggregate_array('system:index').getInfo() dates = [f.split("_")[0].split('T')[0] for f in reduced_collection.aggregate_array('system:index').getInfo()] df = pd.DataFrame({'NDVI': stats_list, 'Date': dates, 'Imagery': filenames}) return df # put title in center st.markdown(""" """, unsafe_allow_html=True) st.title("Mean NDVI Calculator") # get the start and end date from the user col = st.columns(2) start_date = col[0].date_input("Start Date", value=pd.to_datetime('2021-01-01')) end_date = col[1].date_input("End Date", value=pd.to_datetime('2021-01-30')) # Check if start and end dates are valid if start_date>end_date: st.stop() start_date = start_date.strftime("%Y-%m-%d") end_date = end_date.strftime("%Y-%m-%d") max_cloud_cover = st.number_input("Max Cloud Cover", value=20) # Get the geojson file from the user uploaded_file = st.file_uploader("Upload KML/GeoJSON file", type=["geojson", "kml"]) if uploaded_file is not None: try: if uploaded_file.name.endswith("kml"): gdf = gpd.read_file(BytesIO(uploaded_file.read()), driver='LIBKML') elif uploaded_file.name.endswith("geojson"): gdf = gpd.read_file(uploaded_file) except Exception as e: st.write('ValueError: "Input must be a valid KML file."') st.stop() # Validate KML File polygon_info = validate_KML_file(gdf) if polygon_info["is_single_polygon"]==True: st.write("Uploaded KML file has single polygon geometry.") st.write("It has bounds as {0:.6f}, {1:.6f}, {2:.6f}, and {3:.6f}.".format( polygon_info['corner_points'][0][0], polygon_info['corner_points'][0][1], polygon_info['corner_points'][2][0], polygon_info['corner_points'][2][1] )) st.write("It has centroid at ({0:.6f}, {1:.6f}).".format(polygon_info['centroid'][0], polygon_info['centroid'][1])) st.write("It has area of {:.2f} ha.".format(polygon_info['area']/10000)) st.write("It has perimeter of {:.2f} meters.".format(polygon_info['perimeter'])) #Change geometry of polygon 3D to 2D for ee gdf.loc[0, "geometry"] = convert_to_2d_geometry(gdf.iloc[0].geometry) #Read KML file geom_ee_object = ee.FeatureCollection(json.loads(gdf.to_json())) # Add buffer of 100m to ee_object buffered_ee_object = geom_ee_object.map(lambda feature: feature.buffer(100)) # Filter data based on the date, bounds, cloud coverage and select NIR and Red Band collection = ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED").filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', max_cloud_cover)).filter(ee.Filter.date(start_date, end_date)).select(['B4', 'B8']) # Get Zonal NDVI based on collection and geometries (Original KML and Buffered KML) df_geom = get_zonal_ndvi(collection.filterBounds(geom_ee_object), geom_ee_object) df_buffered_geom = get_zonal_ndvi(collection.filterBounds(buffered_ee_object), buffered_ee_object) # Merge both Zonalstats and create resultant dataframe resultant_df = pd.merge(df_geom, df_buffered_geom, on='Date', how='inner') resultant_df = resultant_df.rename(columns={'NDVI_x': 'AvgNDVI_Inside', 'NDVI_y': 'Avg_NDVI_Buffer', 'Imagery_x': 'Imagery'}) resultant_df['Ratio'] = resultant_df['AvgNDVI_Inside'] / resultant_df['Avg_NDVI_Buffer'] resultant_df.drop(columns=['Imagery_y'], inplace=True) # Re-order the columns of the resultant dataframe resultant_df = resultant_df[['Date', 'Imagery', 'AvgNDVI_Inside', 'Avg_NDVI_Buffer', 'Ratio']] # Write the final table st.write(resultant_df) # plot the time series st.write("Time Series Plot") st.line_chart(resultant_df[['AvgNDVI_Inside', 'Avg_NDVI_Buffer', 'Date']].set_index('Date')) # Visualize map on ESRI basemap st.write("Map Visualization") # Create a Leafmap object m = leafmap.Map() # Add ESRI latest imagery basemap and polygon m.add_basemap("ESRI/WorldImagery") m.add_polygon(gdf.iloc[0].geometry, "Polygon", color="red", fill_opacity=0.5) # Display the map m.to_streamlit(height=600) else: st.write('ValueError: "Input must have single polygon geometry"') st.write(gdf) st.stop()