Spaces:
Sleeping
Sleeping
Transparent Geometry, Add Colormap, Visualize MaxNDVI
Browse files
app.py
CHANGED
@@ -15,6 +15,7 @@ import geemap.foliumap as geemapfolium
|
|
15 |
from streamlit_folium import st_folium
|
16 |
from datetime import datetime
|
17 |
import numpy as np
|
|
|
18 |
|
19 |
# Enable fiona driver
|
20 |
fiona.drvsupport.supported_drivers['LIBKML'] = 'rw'
|
@@ -98,6 +99,23 @@ def reduce_zonal_ndvi(image, ee_object):
|
|
98 |
# Set the reduced NDVI mean as a property on the image
|
99 |
return ndvi.set('NDVI_mean', reduced.get('NDVI'))
|
100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
101 |
# Calculate NDVI
|
102 |
def calculate_NDVI(image):
|
103 |
ndvi = image.normalizedDifference(['B8', 'B4']).rename('NDVI')
|
@@ -112,15 +130,19 @@ def get_zonal_ndviYoY(collection, ee_object):
|
|
112 |
geometry=ee_object.geometry(),
|
113 |
scale=10,
|
114 |
maxPixels=1e12)
|
115 |
-
return reduced_max_ndvi.get('NDVI').getInfo()
|
|
|
|
|
116 |
|
117 |
# Get Zonal NDVI
|
118 |
def get_zonal_ndvi(collection, geom_ee_object, return_ndvi=True):
|
119 |
reduced_collection = collection.map(lambda image: reduce_zonal_ndvi(image, ee_object=geom_ee_object))
|
|
|
120 |
stats_list = reduced_collection.aggregate_array('NDVI_mean').getInfo()
|
121 |
filenames = reduced_collection.aggregate_array('system:index').getInfo()
|
|
|
122 |
dates = [f.split("_")[0].split('T')[0] for f in filenames]
|
123 |
-
df = pd.DataFrame({'NDVI': stats_list, 'Dates': dates, 'Imagery': filenames, 'Id': filenames})
|
124 |
if return_ndvi==True:
|
125 |
return df, reduced_collection
|
126 |
else:
|
@@ -199,10 +221,14 @@ st.title("Zonal Average NDVI Trend Calculator")
|
|
199 |
input_container = st.container()
|
200 |
# Function to create dropdowns for date input
|
201 |
def date_selector(label):
|
202 |
-
|
203 |
-
|
204 |
-
|
205 |
-
|
|
|
|
|
|
|
|
|
206 |
month = datetime.strptime(month, "%B").month
|
207 |
try:
|
208 |
# Try to create a date
|
@@ -265,14 +291,16 @@ if uploaded_file is not None and submit_button:
|
|
265 |
end_year = datetime.now().year
|
266 |
|
267 |
# Create an empty resultant dataframe
|
268 |
-
columns = ['Dates', 'Imagery', 'AvgNDVI_Inside', 'Avg_NDVI_Buffer', 'Ratio', 'Id']
|
269 |
combined_df = pd.DataFrame(columns=columns)
|
270 |
|
|
|
271 |
max_ndvi_geoms = []
|
272 |
max_ndvi_buffered_geoms = []
|
273 |
years=[]
|
274 |
ndvi_collections = []
|
275 |
df_geoms = []
|
|
|
276 |
for year in range(start_year, end_year+1):
|
277 |
try:
|
278 |
# Construct start and end dates for every year
|
@@ -280,95 +308,122 @@ if uploaded_file is not None and submit_button:
|
|
280 |
end_ddmm = str(year)+pd.to_datetime(end_date).strftime("-%m-%d")
|
281 |
|
282 |
# Filter data based on the date, bounds, cloud coverage and select NIR and Red Band
|
283 |
-
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))
|
284 |
|
285 |
# Get Zonal Max composite NDVI based on collection and geometries (Original KML and Buffered KML)
|
286 |
-
|
287 |
-
|
|
|
|
|
|
|
288 |
years.append(str(year))
|
289 |
|
290 |
# Get Zonal NDVI
|
291 |
-
df_geom, ndvi_collection = get_zonal_ndvi(collection.filterBounds(geom_ee_object), geom_ee_object)
|
292 |
df_buffered_geom, ndvi_collection = get_zonal_ndvi(collection.filterBounds(buffered_ee_object), buffered_ee_object)
|
293 |
ndvi_collections.append(ndvi_collection)
|
294 |
df_geoms.append(df_geom)
|
295 |
|
296 |
|
297 |
-
# Merge both Zonalstats and create resultant dataframe
|
298 |
resultant_df = pd.merge(df_geom, df_buffered_geom, on='Id', how='inner')
|
299 |
-
resultant_df = resultant_df.rename(columns={'NDVI_x': 'AvgNDVI_Inside', 'NDVI_y': 'Avg_NDVI_Buffer', 'Imagery_x': 'Imagery', 'Dates_x': 'Dates'
|
|
|
300 |
resultant_df['Ratio'] = resultant_df['AvgNDVI_Inside'] / resultant_df['Avg_NDVI_Buffer']
|
301 |
resultant_df.drop(columns=['Imagery_y', 'Dates_y'], inplace=True)
|
302 |
|
303 |
# Re-order the columns of the resultant dataframe
|
304 |
-
resultant_df = resultant_df[
|
305 |
|
306 |
# Append to empty dataframe
|
307 |
combined_df = pd.concat([combined_df, resultant_df], ignore_index=True)
|
308 |
-
except:
|
309 |
continue
|
310 |
|
311 |
-
|
312 |
-
|
313 |
-
|
314 |
-
|
315 |
-
|
316 |
|
317 |
-
# Plot the multiyear timeseries
|
318 |
-
st.write("Multiyear Time Series Plot (for given duration)")
|
319 |
-
st.line_chart(combined_df[['AvgNDVI_Inside', 'Avg_NDVI_Buffer', 'Dates']].set_index('Dates'))
|
320 |
-
|
321 |
-
# Create a DataFrame for YoY profile
|
322 |
-
yoy_df = pd.DataFrame({'Year': years, 'NDVI_Inside': max_ndvi_geoms, 'NDVI_Buffer': max_ndvi_buffered_geoms})
|
323 |
-
yoy_df['Ratio'] = yoy_df['NDVI_Inside'] / yoy_df['NDVI_Buffer']
|
324 |
-
slope, intercept = np.polyfit(list(range(1, len(years)+1)), yoy_df['NDVI_Inside'], 1)
|
325 |
-
|
326 |
-
# plot the time series
|
327 |
-
st.write("Year on Year Profile using Maximum NDVI Composite (computed for given duration)")
|
328 |
-
st.line_chart(yoy_df[['NDVI_Inside', 'NDVI_Buffer', 'Ratio', 'Year']].set_index('Year'))
|
329 |
-
st.write("Slope (trend) and Intercept are {}, {} respectively. ".format(np.round(slope, 4), np.round(intercept, 4)))
|
330 |
-
|
331 |
-
#Get Latest NDVI Collection with completeness
|
332 |
-
ndvi_collection = None
|
333 |
-
for i in range(len(ndvi_collections)):
|
334 |
-
#Check size of NDVI collection
|
335 |
-
ndvi_collection = ndvi_collections[len(ndvi_collections)-i-1]
|
336 |
-
df_geom = df_geoms[len(ndvi_collections)-i-1]
|
337 |
-
if ndvi_collection.size().getInfo()>0:
|
338 |
-
break
|
339 |
-
|
340 |
-
#Map Visualization
|
341 |
-
st.write("Map Visualization")
|
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 |
else:
|
|
|
372 |
st.write('ValueError: "Input must have single polygon geometry"')
|
373 |
st.write(gdf)
|
374 |
st.stop()
|
|
|
15 |
from streamlit_folium import st_folium
|
16 |
from datetime import datetime
|
17 |
import numpy as np
|
18 |
+
import branca.colormap as cm
|
19 |
|
20 |
# Enable fiona driver
|
21 |
fiona.drvsupport.supported_drivers['LIBKML'] = 'rw'
|
|
|
99 |
# Set the reduced NDVI mean as a property on the image
|
100 |
return ndvi.set('NDVI_mean', reduced.get('NDVI'))
|
101 |
|
102 |
+
# Function to compute cloud probability and add it as a property to the image
|
103 |
+
def reduce_zonal_cloud_probability(image, ee_object):
|
104 |
+
# Compute cloud probability using the SCL band (Scene Classification Layer) in Sentinel-2
|
105 |
+
cloud_probability = image.select('MSK_CLDPRB').rename('cloud_probability')
|
106 |
+
|
107 |
+
# Reduce the region to get the mean cloud probability value for the given geometry
|
108 |
+
reduced = cloud_probability.reduceRegion(
|
109 |
+
reducer=ee.Reducer.mean(),
|
110 |
+
geometry=ee_object.geometry(),
|
111 |
+
scale=10,
|
112 |
+
maxPixels=1e12
|
113 |
+
)
|
114 |
+
|
115 |
+
# Set the reduced cloud probability mean as a property on the image
|
116 |
+
return image.set('cloud_probability_mean', reduced.get('cloud_probability'))
|
117 |
+
|
118 |
+
|
119 |
# Calculate NDVI
|
120 |
def calculate_NDVI(image):
|
121 |
ndvi = image.normalizedDifference(['B8', 'B4']).rename('NDVI')
|
|
|
130 |
geometry=ee_object.geometry(),
|
131 |
scale=10,
|
132 |
maxPixels=1e12)
|
133 |
+
return reduced_max_ndvi.get('NDVI').getInfo(), max_ndvi
|
134 |
+
|
135 |
+
|
136 |
|
137 |
# Get Zonal NDVI
|
138 |
def get_zonal_ndvi(collection, geom_ee_object, return_ndvi=True):
|
139 |
reduced_collection = collection.map(lambda image: reduce_zonal_ndvi(image, ee_object=geom_ee_object))
|
140 |
+
cloud_prob_collection = collection.map(lambda image: reduce_zonal_cloud_probability(image, ee_object=geom_ee_object))
|
141 |
stats_list = reduced_collection.aggregate_array('NDVI_mean').getInfo()
|
142 |
filenames = reduced_collection.aggregate_array('system:index').getInfo()
|
143 |
+
cloud_probabilities = cloud_prob_collection.aggregate_array('cloud_probability_mean').getInfo()
|
144 |
dates = [f.split("_")[0].split('T')[0] for f in filenames]
|
145 |
+
df = pd.DataFrame({'NDVI': stats_list, 'Dates': dates, 'Imagery': filenames, 'Id': filenames, 'CLDPRB': cloud_probabilities})
|
146 |
if return_ndvi==True:
|
147 |
return df, reduced_collection
|
148 |
else:
|
|
|
221 |
input_container = st.container()
|
222 |
# Function to create dropdowns for date input
|
223 |
def date_selector(label):
|
224 |
+
day_options = list(range(1, 32))
|
225 |
+
if label=='start':
|
226 |
+
day = st.selectbox(f"Select {label} day", day_options, key=f"{label}_day", index=0)
|
227 |
+
else:
|
228 |
+
day = st.selectbox(f"Select {label} day", day_options, key=f"{label}_day", index=14)
|
229 |
+
|
230 |
+
month_options = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
|
231 |
+
month = st.selectbox(f"Select {label} month", month_options, key=f"{label}_month",index=11)
|
232 |
month = datetime.strptime(month, "%B").month
|
233 |
try:
|
234 |
# Try to create a date
|
|
|
291 |
end_year = datetime.now().year
|
292 |
|
293 |
# Create an empty resultant dataframe
|
294 |
+
columns = ['Dates', 'Imagery', 'AvgNDVI_Inside', 'CLDPRB', 'Avg_NDVI_Buffer', 'CLDPRB_Buffer', 'Ratio', 'Id' ]
|
295 |
combined_df = pd.DataFrame(columns=columns)
|
296 |
|
297 |
+
# Create empty lists of parameters
|
298 |
max_ndvi_geoms = []
|
299 |
max_ndvi_buffered_geoms = []
|
300 |
years=[]
|
301 |
ndvi_collections = []
|
302 |
df_geoms = []
|
303 |
+
max_ndvis = []
|
304 |
for year in range(start_year, end_year+1):
|
305 |
try:
|
306 |
# Construct start and end dates for every year
|
|
|
308 |
end_ddmm = str(year)+pd.to_datetime(end_date).strftime("-%m-%d")
|
309 |
|
310 |
# Filter data based on the date, bounds, cloud coverage and select NIR and Red Band
|
311 |
+
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))
|
312 |
|
313 |
# Get Zonal Max composite NDVI based on collection and geometries (Original KML and Buffered KML)
|
314 |
+
max_ndvi_geom, max_ndvi = get_zonal_ndviYoY(collection.filterBounds(geom_ee_object), geom_ee_object) # max_NDVI is image common to both
|
315 |
+
max_ndvi_geoms.append(max_ndvi_geom)
|
316 |
+
max_ndvi_geom, max_ndvi = get_zonal_ndviYoY(collection.filterBounds(buffered_ee_object), buffered_ee_object)
|
317 |
+
max_ndvi_buffered_geoms.append(max_ndvi_geom)
|
318 |
+
max_ndvis.append(max_ndvi)
|
319 |
years.append(str(year))
|
320 |
|
321 |
# Get Zonal NDVI
|
322 |
+
df_geom, ndvi_collection = get_zonal_ndvi(collection.filterBounds(geom_ee_object), geom_ee_object) # ndvi collection is common to both
|
323 |
df_buffered_geom, ndvi_collection = get_zonal_ndvi(collection.filterBounds(buffered_ee_object), buffered_ee_object)
|
324 |
ndvi_collections.append(ndvi_collection)
|
325 |
df_geoms.append(df_geom)
|
326 |
|
327 |
|
328 |
+
# Merge both Zonalstats on ID and create resultant dataframe
|
329 |
resultant_df = pd.merge(df_geom, df_buffered_geom, on='Id', how='inner')
|
330 |
+
resultant_df = resultant_df.rename(columns={'NDVI_x': 'AvgNDVI_Inside', 'NDVI_y': 'Avg_NDVI_Buffer', 'Imagery_x': 'Imagery', 'Dates_x': 'Dates',
|
331 |
+
'CLDPRB_x': 'CLDPRB', 'CLDPRB_y': 'CLDPRB_Buffer'})
|
332 |
resultant_df['Ratio'] = resultant_df['AvgNDVI_Inside'] / resultant_df['Avg_NDVI_Buffer']
|
333 |
resultant_df.drop(columns=['Imagery_y', 'Dates_y'], inplace=True)
|
334 |
|
335 |
# Re-order the columns of the resultant dataframe
|
336 |
+
resultant_df = resultant_df[columns]
|
337 |
|
338 |
# Append to empty dataframe
|
339 |
combined_df = pd.concat([combined_df, resultant_df], ignore_index=True)
|
340 |
+
except Exception as e:
|
341 |
continue
|
342 |
|
343 |
+
if len(combined_df)>1:
|
344 |
+
# Write the final table
|
345 |
+
st.write("NDVI details based on Sentinel-2 Surface Reflectance Bands")
|
346 |
+
st.write(combined_df[columns[:-1]])
|
|
|
347 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
348 |
|
349 |
+
# Plot the multiyear timeseries
|
350 |
+
st.write("Multiyear Time Series Plot (for given duration)")
|
351 |
+
st.line_chart(combined_df[['AvgNDVI_Inside', 'Avg_NDVI_Buffer', 'Dates']].set_index('Dates'))
|
352 |
|
353 |
+
# Create a DataFrame for YoY profile
|
354 |
+
yoy_df = pd.DataFrame({'Year': years, 'NDVI_Inside': max_ndvi_geoms, 'NDVI_Buffer': max_ndvi_buffered_geoms})
|
355 |
+
yoy_df['Ratio'] = yoy_df['NDVI_Inside'] / yoy_df['NDVI_Buffer']
|
356 |
+
slope, intercept = np.polyfit(list(range(1, len(years)+1)), yoy_df['NDVI_Inside'], 1)
|
357 |
+
|
358 |
+
# plot the time series
|
359 |
+
st.write("Year on Year Profile using Maximum NDVI Composite (computed for given duration)")
|
360 |
+
st.line_chart(yoy_df[['NDVI_Inside', 'NDVI_Buffer', 'Ratio', 'Year']].set_index('Year'))
|
361 |
+
st.write("Slope (trend) and Intercept are {}, {} respectively. ".format(np.round(slope, 4), np.round(intercept, 4)))
|
362 |
+
|
363 |
+
#Get Latest NDVI Collection with completeness
|
364 |
+
ndvi_collection = None
|
365 |
+
for i in range(len(ndvi_collections)):
|
366 |
+
#Check size of NDVI collection
|
367 |
+
ndvi_collection = ndvi_collections[len(ndvi_collections)-i-1]
|
368 |
+
df_geom = df_geoms[len(ndvi_collections)-i-1]
|
369 |
+
if ndvi_collection.size().getInfo()>0:
|
370 |
+
break
|
371 |
|
372 |
+
#Map Visualization
|
373 |
+
st.write("Map Visualization")
|
374 |
+
|
375 |
+
# Function to create the map
|
376 |
+
def create_map():
|
377 |
+
m = geemapfolium.Map(center=(polygon_info['centroid'][1],polygon_info['centroid'][0]), zoom=14) # Create a Folium map
|
378 |
+
|
379 |
+
vis_params = {'min': -1, 'max': 1, 'palette': ['blue', 'white', 'green']} # Example visualization for Sentinel-2
|
380 |
+
|
381 |
+
# Create a colormap and name it as NDVI
|
382 |
+
colormap = cm.LinearColormap(
|
383 |
+
colors=vis_params['palette'],
|
384 |
+
vmin=vis_params['min'],
|
385 |
+
vmax=vis_params['max']
|
386 |
+
)
|
387 |
+
colormap.caption = 'NDVI'
|
388 |
+
|
389 |
+
n_layers = 4 #controls the number of images to be displayed
|
390 |
+
for i in range(min(n_layers, ndvi_collection.size().getInfo())):
|
391 |
+
ndvi_image = ee.Image(ndvi_collection.toList(ndvi_collection.size()).get(i))
|
392 |
+
date = df_geom.iloc[i]["Dates"]
|
393 |
+
|
394 |
+
# Add the image to the map as a layer
|
395 |
+
layer_name = f"Sentinel-2 NDVI - {date}"
|
396 |
+
m.add_layer(ndvi_image, vis_params, layer_name, z_index=i+10, opacity=0.5)
|
397 |
+
|
398 |
+
for i in range(len(max_ndvis)):
|
399 |
+
layer_name = f"Sentinel-2 MaxNDVI-{years[i]}"
|
400 |
+
m.add_layer(max_ndvis[i], vis_params, layer_name, z_index=i+20, opacity=0.5)
|
401 |
+
|
402 |
+
# Add the colormap to the map
|
403 |
+
m.add_child(colormap)
|
404 |
+
|
405 |
+
geom_vis_params = {'color': '000000', 'pointSize': 3,'pointShape': 'circle','width': 2,'lineType': 'solid','fillColor': '00000000'}
|
406 |
+
buffer_vis_params = {'color': 'FF0000', 'pointSize': 3,'pointShape': 'circle','width': 2,'lineType': 'solid','fillColor': '00000000'}
|
407 |
+
|
408 |
+
m.add_layer(geom_ee_object.style(**geom_vis_params), {}, 'KML Original', z_index=1, opacity=1)
|
409 |
+
m.add_layer(buffered_ee_object.style(**buffer_vis_params), {}, 'KML Buffered', z_index=2, opacity=1)
|
410 |
+
|
411 |
+
m.add_layer_control()
|
412 |
+
return m
|
413 |
+
|
414 |
+
# Create Folium Map object and store it in streamlit session
|
415 |
+
if "map" not in st.session_state or submit_button:
|
416 |
+
st.session_state["map"] = create_map()
|
417 |
+
|
418 |
+
# Display the map and allow interactions without triggering reruns
|
419 |
+
with st.container():
|
420 |
+
st_folium(st.session_state["map"], width=725, returned_objects=[])
|
421 |
+
else:
|
422 |
+
# Failed to find any Sentinel-2 Image in given period
|
423 |
+
st.write("No Sentinel-2 Imagery found for the given period.")
|
424 |
+
st.stop()
|
425 |
else:
|
426 |
+
# Failed to have single polygon geometry
|
427 |
st.write('ValueError: "Input must have single polygon geometry"')
|
428 |
st.write(gdf)
|
429 |
st.stop()
|