Spaces:
Running
Running
File size: 17,962 Bytes
46719a3 a1ed477 c66588a e38ccc6 61d57e7 1777b8d 44aadb9 46719a3 487950f c0206c3 2276dbd c0206c3 46719a3 775b8c3 2276dbd 46719a3 a1ed477 46719a3 53144e5 46719a3 882e722 46719a3 a1ed477 46719a3 882e722 46719a3 487950f 46719a3 487950f 46719a3 44aadb9 8e4278b 7324e32 7cc54d0 7324e32 8e4278b 44aadb9 8e4278b 46719a3 487950f 46719a3 44aadb9 487950f 46719a3 44aadb9 487950f 44aadb9 487950f 7324e32 487950f 46719a3 487950f ec19192 487950f 46719a3 30cf8c5 46719a3 ec19192 487950f 44aadb9 487950f a1ed477 ec19192 46719a3 ec19192 a1ed477 ec19192 46719a3 ec19192 46719a3 ec19192 46719a3 493782c 46719a3 b786efe 46719a3 b786efe 7d21b47 9a0e806 dbffabb 46719a3 dbffabb 46719a3 7324e32 8e4278b 1777b8d 7324e32 0faf3a8 44aadb9 0faf3a8 44aadb9 7324e32 1777b8d 487950f 44aadb9 7324e32 1777b8d 8e4278b 1777b8d 44aadb9 8e4278b 1777b8d 44aadb9 03d4ea9 1777b8d 44aadb9 487950f 1777b8d 44aadb9 487950f 44aadb9 1777b8d 487950f 0faf3a8 1777b8d 44aadb9 0faf3a8 1777b8d 44aadb9 1777b8d 0faf3a8 44aadb9 01a7e0d 7638c84 44aadb9 487950f 44aadb9 487950f 44aadb9 30cf8c5 44aadb9 46719a3 44aadb9 46719a3 abbf4aa |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 |
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
from datetime import datetime
import numpy as np
import branca.colormap as cm
# 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
# Function to compute zonal NDVI and add it as a property to the image
def reduce_zonal_ndvi(image, ee_object):
# Compute NDVI using Sentinel-2 bands (B8 - NIR, B4 - Red)
ndvi = image.normalizedDifference(['B8', 'B4']).rename('NDVI')
# Reduce the region to get the mean NDVI value for the given geometry
reduced = ndvi.reduceRegion(
reducer=ee.Reducer.mean(),
geometry=ee_object.geometry(),
scale=10,
maxPixels=1e12
)
# Set the reduced NDVI mean as a property on the image
return ndvi.set('NDVI_mean', reduced.get('NDVI'))
# Function to compute cloud probability and add it as a property to the image
def reduce_zonal_cloud_probability(image, ee_object):
# Compute cloud probability using the SCL band (Scene Classification Layer) in Sentinel-2
cloud_probability = image.select('MSK_CLDPRB').rename('cloud_probability')
# Reduce the region to get the mean cloud probability value for the given geometry
reduced = cloud_probability.reduceRegion(
reducer=ee.Reducer.mean(),
geometry=ee_object.geometry(),
scale=10,
maxPixels=1e12
)
# Set the reduced cloud probability mean as a property on the image
return image.set('cloud_probability_mean', reduced.get('cloud_probability'))
# 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(), max_ndvi
# Get Zonal NDVI
def get_zonal_ndvi(collection, geom_ee_object, return_ndvi=True):
reduced_collection = collection.map(lambda image: reduce_zonal_ndvi(image, ee_object=geom_ee_object))
cloud_prob_collection = collection.map(lambda image: reduce_zonal_cloud_probability(image, ee_object=geom_ee_object))
stats_list = reduced_collection.aggregate_array('NDVI_mean').getInfo()
filenames = reduced_collection.aggregate_array('system:index').getInfo()
cloud_probabilities = cloud_prob_collection.aggregate_array('cloud_probability_mean').getInfo()
dates = [f.split("_")[0].split('T')[0] for f in filenames]
df = pd.DataFrame({'NDVI': stats_list, 'Dates': dates, 'Imagery': filenames, 'Id': filenames, 'CLDPRB': cloud_probabilities})
if return_ndvi==True:
return df, reduced_collection
else:
return df
# Apply custom CSS for a visually appealing layout
st.markdown("""
<style>
/* General body styling */
body {
background-color: #f9f9f9; /* Light gray background */
color: #333; /* Dark text color */
font-family: 'Arial', sans-serif; /* Clean font */
line-height: 1.6; /* Improved line spacing */
}
/* Center title */
h1 {
text-align: center;
color: #FFFFFF; /* Darker blue for headings */
margin-bottom: 20px; /* Spacing below heading */
}
/* Subheading styling */
h2, h3 {
color: #2980b9; /* Blue color for subheadings */
}
/* Paragraph and list styling */
p, li {
margin-bottom: 15px; /* Spacing between paragraphs and list items */
}
/* Link styling */
a {
color: #2980b9; /* Blue links */
text-decoration: none; /* Remove underline */
}
a:hover {
text-decoration: underline; /* Underline on hover */
}
/* Button styling */
.stButton {
background-color: #2980b9; /* Blue button */
color: white; /* White text */
border: none; /* No border */
border-radius: 5px; /* Rounded corners */
padding: 10px 20px; /* Padding */
cursor: pointer; /* Pointer cursor */
}
.stButton:hover {
background-color: #1c6690; /* Darker blue on hover */
}
/* Adjust layout for printing */
@media print {
body {
background-color: white; /* White background for printing */
color: black; /* Black text for printing */
}
h1, h2, h3 {
color: black; /* Black headings for printing */
}
.stButton {
display: none; /* Hide buttons on print */
}
}
</style>
""", unsafe_allow_html=True)
st.title("Zonal Average NDVI Trend Analyser")
input_container = st.container()
# Function to create dropdowns for date input
def date_selector(label):
day_options = list(range(1, 32))
if label=='start':
day = st.selectbox(f"Select {label} day", day_options, key=f"{label}_day", index=0)
else:
day = st.selectbox(f"Select {label} day", day_options, key=f"{label}_day", index=14)
month_options = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
month = st.selectbox(f"Select {label} month", month_options, key=f"{label}_month",index=11)
month = datetime.strptime(month, "%B").month
try:
# Try to create a date
datetime(year=2024, month=month, day=day) # Using a leap year for completeness
return (day, month)
except ValueError:
st.write("Invalid date and month !")
st.stop()
with input_container.form(key='input_form'):
# Create date selectors for start date and end date
(start_day, start_month), start_year = date_selector("start"), datetime.now().year
(end_day, end_month), end_year = date_selector("end"), datetime.now().year
start_date = datetime(day=start_day, month=start_month, year=start_year)
end_date = datetime(day=end_day, month=end_month, year=end_year)
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"])
submit_button = st.form_submit_button(label='Submit')
if uploaded_file is not None and submit_button:
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))
####### YoY Profile ########
start_year = 2019
end_year = datetime.now().year
# Create an empty resultant dataframe
columns = ['Dates', 'Imagery', 'AvgNDVI_Inside', 'CLDPRB', 'Avg_NDVI_Buffer', 'CLDPRB_Buffer', 'Ratio', 'Id' ]
combined_df = pd.DataFrame(columns=columns)
# Create empty lists of parameters
max_ndvi_geoms = []
max_ndvi_buffered_geoms = []
years=[]
ndvi_collections = []
df_geoms = []
max_ndvis = []
for year in range(start_year, end_year+1):
try:
# 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))
# Get Zonal Max composite NDVI based on collection and geometries (Original KML and Buffered KML)
max_ndvi_geom, max_ndvi = get_zonal_ndviYoY(collection.filterBounds(geom_ee_object), geom_ee_object) # max_NDVI is image common to both
max_ndvi_geoms.append(max_ndvi_geom)
max_ndvi_geom, max_ndvi = get_zonal_ndviYoY(collection.filterBounds(buffered_ee_object), buffered_ee_object)
max_ndvi_buffered_geoms.append(max_ndvi_geom)
max_ndvis.append(max_ndvi)
years.append(str(year))
# Get Zonal NDVI
df_geom, ndvi_collection = get_zonal_ndvi(collection.filterBounds(geom_ee_object), geom_ee_object) # ndvi collection is common to both
df_buffered_geom, ndvi_collection = get_zonal_ndvi(collection.filterBounds(buffered_ee_object), buffered_ee_object)
ndvi_collections.append(ndvi_collection)
df_geoms.append(df_geom)
# Merge both Zonalstats on ID and create resultant dataframe
resultant_df = pd.merge(df_geom, df_buffered_geom, on='Id', how='inner')
resultant_df = resultant_df.rename(columns={'NDVI_x': 'AvgNDVI_Inside', 'NDVI_y': 'Avg_NDVI_Buffer', 'Imagery_x': 'Imagery', 'Dates_x': 'Dates',
'CLDPRB_x': 'CLDPRB', 'CLDPRB_y': 'CLDPRB_Buffer'})
resultant_df['Ratio'] = resultant_df['AvgNDVI_Inside'] / resultant_df['Avg_NDVI_Buffer']
resultant_df.drop(columns=['Imagery_y', 'Dates_y'], inplace=True)
# Re-order the columns of the resultant dataframe
resultant_df = resultant_df[columns]
# Append to empty dataframe
combined_df = pd.concat([combined_df, resultant_df], ignore_index=True)
except Exception as e:
continue
if len(combined_df)>1:
# Write the final table
st.write("NDVI details based on Sentinel-2 Surface Reflectance Bands")
st.write(combined_df[columns[:-1]])
# Plot the multiyear timeseries
st.write("Multiyear Time Series Plot (for given duration)")
st.line_chart(combined_df[['AvgNDVI_Inside', 'Avg_NDVI_Buffer', 'Dates']].set_index('Dates'))
# Create a DataFrame for YoY profile
yoy_df = pd.DataFrame({'Year': years, 'NDVI_Inside': max_ndvi_geoms, 'NDVI_Buffer': max_ndvi_buffered_geoms})
yoy_df['Ratio'] = yoy_df['NDVI_Inside'] / yoy_df['NDVI_Buffer']
slope, intercept = np.polyfit(list(range(1, len(years)+1)), yoy_df['NDVI_Inside'], 1)
# plot the time series
st.write("Year on Year Profile using Maximum NDVI Composite (computed for given duration)")
st.line_chart(yoy_df[['NDVI_Inside', 'NDVI_Buffer', 'Ratio', 'Year']].set_index('Year'))
st.write("Slope (trend) and Intercept are {}, {} respectively. ".format(np.round(slope, 4), np.round(intercept, 4)))
#Get Latest NDVI Collection with completeness
ndvi_collection = None
for i in range(len(ndvi_collections)):
#Check size of NDVI collection
ndvi_collection = ndvi_collections[len(ndvi_collections)-i-1]
df_geom = df_geoms[len(ndvi_collections)-i-1]
if ndvi_collection.size().getInfo()>0:
break
#Map Visualization
st.write("Map Visualization")
# Function to create the map
def create_map():
m = geemapfolium.Map(center=(polygon_info['centroid'][1],polygon_info['centroid'][0]), zoom=14) # Create a Folium map
vis_params = {'min': -1, 'max': 1, 'palette': ['blue', 'white', 'green']} # Example visualization for Sentinel-2
# Create a colormap and name it as NDVI
colormap = cm.LinearColormap(
colors=vis_params['palette'],
vmin=vis_params['min'],
vmax=vis_params['max']
)
colormap.caption = 'NDVI'
n_layers = 4 #controls the number of images to be displayed
for i in range(min(n_layers, ndvi_collection.size().getInfo())):
ndvi_image = ee.Image(ndvi_collection.toList(ndvi_collection.size()).get(i))
date = df_geom.iloc[i]["Dates"]
# Add the image to the map as a layer
layer_name = f"Sentinel-2 NDVI - {date}"
m.add_layer(ndvi_image, vis_params, layer_name, z_index=i+10, opacity=0.5)
for i in range(len(max_ndvis)):
layer_name = f"Sentinel-2 MaxNDVI-{years[i]}"
m.add_layer(max_ndvis[i], vis_params, layer_name, z_index=i+20, opacity=0.5)
# Add the colormap to the map
m.add_child(colormap)
geom_vis_params = {'color': '000000', 'pointSize': 3,'pointShape': 'circle','width': 2,'lineType': 'solid','fillColor': '00000000'}
buffer_vis_params = {'color': 'FF0000', 'pointSize': 3,'pointShape': 'circle','width': 2,'lineType': 'solid','fillColor': '00000000'}
m.add_layer(geom_ee_object.style(**geom_vis_params), {}, 'KML Original', z_index=1, opacity=1)
m.add_layer(buffered_ee_object.style(**buffer_vis_params), {}, 'KML Buffered', z_index=2, opacity=1)
m.add_layer_control()
return m
# Create Folium Map object and store it in streamlit session
if "map" not in st.session_state or submit_button:
st.session_state["map"] = create_map()
# Display the map and allow interactions without triggering reruns
with st.container():
st_folium(st.session_state["map"], width=725, returned_objects=[])
st.stop()
else:
# Failed to find any Sentinel-2 Image in given period
st.write("No Sentinel-2 Imagery found for the given period.")
st.stop()
else:
# Failed to have single polygon geometry
st.write('ValueError: "Input must have single polygon geometry"')
st.write(gdf)
st.stop()
# Cut the "infinite" html page to the content
st.stop() |