Spaces:
Sleeping
Sleeping
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(""" | |
<style> | |
h1 { | |
text-align: center; | |
} | |
</style> | |
""", 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() | |