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 geemap.foliumap as geemapfolium from streamlit_folium import st_folium # 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) # Calculate NDVI def calculate_NDVI(image): ndvi = image.normalizedDifference(['B8', 'B4']).rename('NDVI') return ndvi # Get Zonal NDVI for Year on Year Profile def get_zonal_ndviYoY(collection, ee_object): ndvi_collection = collection.map(calculate_NDVI) max_ndvi = ndvi_collection.max() reduced_max_ndvi = max_ndvi.reduceRegion( reducer=ee.Reducer.mean(), geometry=ee_object.geometry(), scale=10, maxPixels=1e12) return reduced_max_ndvi.get('NDVI').getInfo() # 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')) ####### YoY Profile ######## start_year = 2019 end_year = 2023 max_ndvi_geoms = [] max_ndvi_buffered_geoms = [] for year in range(start_year, end_year+1): # Construct start and end dates for every year start_ddmm = str(year)+pd.to_datetime(start_date).strftime("-%m-%d") end_ddmm = str(year)+pd.to_datetime(end_date).strftime("-%m-%d") # 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_ddmm, end_ddmm)).select(['B4', 'B8']) # Get Zonal NDVI based on collection and geometries (Original KML and Buffered KML) max_ndvi_geoms.append(get_zonal_ndviYoY(collection.filterBounds(geom_ee_object), geom_ee_object)) max_ndvi_buffered_geoms.append(get_zonal_ndviYoY(collection.filterBounds(buffered_ee_object), buffered_ee_object)) # Create a DataFrame for YoY profile yoy_df = pd.DataFrame({'Year': list(range(start_year, end_year+1)), 'NDVI_Inside': max_ndvi_geoms, 'NDVI_Buffer': max_ndvi_buffered_geoms}) yoy_df['Ratio'] = yoy_df['NDVI_Inside'] / yoy_df['NDVI_Buffer'] # plot the time series st.write("Year on Year Plot using Maximum NDVI Composite (computed for given duration)") st.line_chart(yoy_df[['NDVI_Inside', 'NDVI_Buffer', 'Ratio', 'Year']].set_index('Year')) # Visualize map on ESRI basemap st.write("Map Visualization") m = geemapfolium.Map(center=polygon_info['centroid'], zoom=14) # Center the map and display the image. m.addLayer(geom_ee_object, {}, 'KML Original') m.addLayer(buffered_ee_object, {}, 'KML Buffered') st_folium(m) else: st.write('ValueError: "Input must have single polygon geometry"') st.write(gdf) st.stop()