UjjwalKGupta commited on
Commit
487950f
·
verified ·
1 Parent(s): 5cabdb9

Visualize Latest NDVI on map

Browse files
Files changed (1) hide show
  1. app.py +170 -78
app.py CHANGED
@@ -19,7 +19,7 @@ import numpy as np
19
  # Enable fiona driver
20
  fiona.drvsupport.supported_drivers['LIBKML'] = 'rw'
21
 
22
- #Intialize EE library
23
  # Access secret
24
  earthengine_credentials = os.environ.get("EE_Authentication")
25
 
@@ -82,18 +82,21 @@ def validate_KML_file(gdf):
82
 
83
  return polygon_info
84
 
85
- # Calculate NDVI as Normalized Index
86
  def reduce_zonal_ndvi(image, ee_object):
87
- ndvi = image.normalizedDifference(['B8', 'B4']).rename('NDVI')
88
- image = image.addBands(ndvi)
89
- image = image.select('NDVI')
90
- reduced = image.reduceRegion(
91
- reducer=ee.Reducer.mean(),
92
- geometry=ee_object.geometry(),
93
- scale=10,
94
- maxPixels=1e12
95
- )
96
- return image.set(reduced)
 
 
 
97
 
98
  # Calculate NDVI
99
  def calculate_NDVI(image):
@@ -112,38 +115,108 @@ def get_zonal_ndviYoY(collection, ee_object):
112
  return reduced_max_ndvi.get('NDVI').getInfo()
113
 
114
  # Get Zonal NDVI
115
- def get_zonal_ndvi(collection, geom_ee_object):
116
  reduced_collection = collection.map(lambda image: reduce_zonal_ndvi(image, ee_object=geom_ee_object))
117
- stats_list = reduced_collection.aggregate_array('NDVI').getInfo()
118
  filenames = reduced_collection.aggregate_array('system:index').getInfo()
119
- dates = [f.split("_")[0].split('T')[0] for f in reduced_collection.aggregate_array('system:index').getInfo()]
120
- df = pd.DataFrame({'NDVI': stats_list, 'Date': dates, 'Imagery': filenames})
121
- return df
122
-
 
 
123
 
124
- # put title in center
125
  st.markdown("""
126
- <style>
127
- h1 {
128
- text-align: center;
129
- }
130
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  """, unsafe_allow_html=True)
132
 
133
- st.title("Mean NDVI Calculator")
134
 
135
- # get the start and end date from the user
136
- col = st.columns(2)
137
- start_date = col[0].date_input("Start Date", value=pd.to_datetime('2021-01-01'))
138
- end_date = col[1].date_input("End Date", value=pd.to_datetime('2021-01-30'))
139
- # Check if start and end dates are valid
140
- if start_date>end_date:
141
- st.write('ValueError: "Incorrect start and/or end dates."')
142
- st.stop()
 
 
 
 
 
 
143
 
144
- start_date = start_date.strftime("%Y-%m-%d")
145
- end_date = end_date.strftime("%Y-%m-%d")
 
146
 
 
 
147
 
148
  max_cloud_cover = st.number_input("Max Cloud Cover", value=20)
149
 
@@ -184,40 +257,19 @@ if uploaded_file is not None:
184
  # Add buffer of 100m to ee_object
185
  buffered_ee_object = geom_ee_object.map(lambda feature: feature.buffer(100))
186
 
187
- # # Filter data based on the date, bounds, cloud coverage and select NIR and Red Band
188
- # 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'])
189
-
190
- # # Get Zonal NDVI based on collection and geometries (Original KML and Buffered KML)
191
- # df_geom = get_zonal_ndvi(collection.filterBounds(geom_ee_object), geom_ee_object)
192
- # df_buffered_geom = get_zonal_ndvi(collection.filterBounds(buffered_ee_object), buffered_ee_object)
193
-
194
- # # Merge both Zonalstats and create resultant dataframe
195
- # resultant_df = pd.merge(df_geom, df_buffered_geom, on='Date', how='inner')
196
- # resultant_df = resultant_df.rename(columns={'NDVI_x': 'AvgNDVI_Inside', 'NDVI_y': 'Avg_NDVI_Buffer', 'Imagery_x': 'Imagery'})
197
- # resultant_df['Ratio'] = resultant_df['AvgNDVI_Inside'] / resultant_df['Avg_NDVI_Buffer']
198
- # resultant_df.drop(columns=['Imagery_y'], inplace=True)
199
-
200
- # # Re-order the columns of the resultant dataframe
201
- # resultant_df = resultant_df[['Date', 'Imagery', 'AvgNDVI_Inside', 'Avg_NDVI_Buffer', 'Ratio']]
202
-
203
- # # Write the final table
204
- # st.write(resultant_df)
205
-
206
- # # plot the time series
207
- # st.write("Time Series Plot")
208
- # st.line_chart(resultant_df[['AvgNDVI_Inside', 'Avg_NDVI_Buffer', 'Date']].set_index('Date'))
209
-
210
  ####### YoY Profile ########
211
  start_year = 2019
212
  end_year = datetime.now().year
213
 
214
  # Create an empty resultant dataframe
215
- columns = ['Date', 'Imagery', 'AvgNDVI_Inside', 'Avg_NDVI_Buffer', 'Ratio']
216
  combined_df = pd.DataFrame(columns=columns)
217
 
218
  max_ndvi_geoms = []
219
  max_ndvi_buffered_geoms = []
220
  years=[]
 
 
221
  for year in range(start_year, end_year+1):
222
  try:
223
  # Construct start and end dates for every year
@@ -233,56 +285,96 @@ if uploaded_file is not None:
233
  years.append(str(year))
234
 
235
  # Get Zonal NDVI
236
- df_geom = get_zonal_ndvi(collection.filterBounds(geom_ee_object), geom_ee_object)
237
- df_buffered_geom = get_zonal_ndvi(collection.filterBounds(buffered_ee_object), buffered_ee_object)
 
 
 
238
 
239
  # Merge both Zonalstats and create resultant dataframe
240
- resultant_df = pd.merge(df_geom, df_buffered_geom, on='Date', how='inner')
241
- resultant_df = resultant_df.rename(columns={'NDVI_x': 'AvgNDVI_Inside', 'NDVI_y': 'Avg_NDVI_Buffer', 'Imagery_x': 'Imagery'})
242
  resultant_df['Ratio'] = resultant_df['AvgNDVI_Inside'] / resultant_df['Avg_NDVI_Buffer']
243
- resultant_df.drop(columns=['Imagery_y'], inplace=True)
244
 
245
  # Re-order the columns of the resultant dataframe
246
- resultant_df = resultant_df[['Date', 'Imagery', 'AvgNDVI_Inside', 'Avg_NDVI_Buffer', 'Ratio']]
247
 
248
  # Append to empty dataframe
249
  combined_df = pd.concat([combined_df, resultant_df], ignore_index=True)
250
  except:
251
  continue
252
 
253
-
254
  # Write the final table
255
- st.write(combined_df)
 
 
256
 
257
  # Plot the multiyear timeseries
258
  st.write("Multiyear Time Series Plot (for given duration)")
259
- st.line_chart(combined_df[['AvgNDVI_Inside', 'Avg_NDVI_Buffer', 'Date']].set_index('Date'))
260
 
261
  # Create a DataFrame for YoY profile
262
  yoy_df = pd.DataFrame({'Year': years, 'NDVI_Inside': max_ndvi_geoms, 'NDVI_Buffer': max_ndvi_buffered_geoms})
263
  yoy_df['Ratio'] = yoy_df['NDVI_Inside'] / yoy_df['NDVI_Buffer']
264
- slope, intercept = np.polyfit(yoy_df['Year'].astype(int), yoy_df['NDVI_Inside'], 1)
265
 
266
  # plot the time series
267
- st.write("Year on Year Plot using Maximum NDVI Composite (computed for given duration)")
268
  st.line_chart(yoy_df[['NDVI_Inside', 'NDVI_Buffer', 'Ratio', 'Year']].set_index('Year'))
269
  st.write("Slope (trend) and Intercept are {}, {} respectively. ".format(np.round(slope, 4), np.round(intercept, 4)))
270
 
 
 
 
 
 
 
 
 
271
 
272
- # Visualize map on ESRI basemap
273
  st.write("Map Visualization")
274
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  # Create Folium Map object
276
- m = geemapfolium.Map(center=(polygon_info['centroid'][1],polygon_info['centroid'][0]), zoom=14)
277
- # Center the map and display the image.
278
- m.add_layer(geom_ee_object, {}, 'KML Original')
279
- m.add_layer(buffered_ee_object, {}, 'KML Buffered')
280
- m.add_layer_control()
281
- st_folium(m)
282
-
283
 
284
  else:
285
  st.write('ValueError: "Input must have single polygon geometry"')
286
  st.write(gdf)
287
  st.stop()
288
 
 
 
 
19
  # Enable fiona driver
20
  fiona.drvsupport.supported_drivers['LIBKML'] = 'rw'
21
 
22
+ # Intialize EE library
23
  # Access secret
24
  earthengine_credentials = os.environ.get("EE_Authentication")
25
 
 
82
 
83
  return polygon_info
84
 
85
+ # Function to compute zonal NDVI and add it as a property to the image
86
  def reduce_zonal_ndvi(image, ee_object):
87
+ # Compute NDVI using Sentinel-2 bands (B8 - NIR, B4 - Red)
88
+ ndvi = image.normalizedDifference(['B8', 'B4']).rename('NDVI')
89
+
90
+ # Reduce the region to get the mean NDVI value for the given geometry
91
+ reduced = ndvi.reduceRegion(
92
+ reducer=ee.Reducer.mean(),
93
+ geometry=ee_object.geometry(),
94
+ scale=10,
95
+ maxPixels=1e12
96
+ )
97
+
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):
 
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:
127
+ return df
128
 
129
+ # Apply custom CSS for a visually appealing layout
130
  st.markdown("""
131
+ <style>
132
+ /* General body styling */
133
+ body {
134
+ background-color: #f9f9f9; /* Light gray background */
135
+ color: #333; /* Dark text color */
136
+ font-family: 'Arial', sans-serif; /* Clean font */
137
+ line-height: 1.6; /* Improved line spacing */
138
+ }
139
+
140
+ /* Center title */
141
+ h1 {
142
+ text-align: center;
143
+ color: #2c3e50; /* Darker blue for headings */
144
+ margin-bottom: 20px; /* Spacing below heading */
145
+ }
146
+
147
+ /* Subheading styling */
148
+ h2, h3 {
149
+ color: #2980b9; /* Blue color for subheadings */
150
+ }
151
+
152
+ /* Paragraph and list styling */
153
+ p, li {
154
+ margin-bottom: 15px; /* Spacing between paragraphs and list items */
155
+ }
156
+
157
+ /* Link styling */
158
+ a {
159
+ color: #2980b9; /* Blue links */
160
+ text-decoration: none; /* Remove underline */
161
+ }
162
+
163
+ a:hover {
164
+ text-decoration: underline; /* Underline on hover */
165
+ }
166
+
167
+ /* Button styling */
168
+ .stButton {
169
+ background-color: #2980b9; /* Blue button */
170
+ color: white; /* White text */
171
+ border: none; /* No border */
172
+ border-radius: 5px; /* Rounded corners */
173
+ padding: 10px 20px; /* Padding */
174
+ cursor: pointer; /* Pointer cursor */
175
+ }
176
+
177
+ .stButton:hover {
178
+ background-color: #1c6690; /* Darker blue on hover */
179
+ }
180
+
181
+ /* Adjust layout for printing */
182
+ @media print {
183
+ body {
184
+ background-color: white; /* White background for printing */
185
+ color: black; /* Black text for printing */
186
+ }
187
+ h1, h2, h3 {
188
+ color: black; /* Black headings for printing */
189
+ }
190
+ .stButton {
191
+ display: none; /* Hide buttons on print */
192
+ }
193
+ }
194
+ </style>
195
  """, unsafe_allow_html=True)
196
 
197
+ st.title("Zonal Average NDVI Trend Calculator")
198
 
199
+ # Function to create dropdowns for date input
200
+ def date_selector(label):
201
+ day = st.selectbox(f"Select {label} day", list(range(1, 32)), key=f"{label}_day")
202
+ month = st.selectbox(f"Select {label} month",
203
+ ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
204
+ key=f"{label}_month")
205
+ month = datetime.strptime(month, "%B").month
206
+ try:
207
+ # Try to create a date
208
+ datetime(year=2024, month=month, day=day) # Using a leap year for completeness
209
+ return (day, month)
210
+ except ValueError:
211
+ st.write("Invalid date and month !")
212
+ st.stop()
213
 
214
+ # Create date selectors for start date and end date
215
+ (start_day, start_month), start_year = date_selector("start"), datetime.now().year
216
+ (end_day, end_month), end_year = date_selector("end"), datetime.now().year
217
 
218
+ start_date = datetime(day=start_day, month=start_month, year=start_year)
219
+ end_date = datetime(day=end_day, month=end_month, year=end_year)
220
 
221
  max_cloud_cover = st.number_input("Max Cloud Cover", value=20)
222
 
 
257
  # Add buffer of 100m to ee_object
258
  buffered_ee_object = geom_ee_object.map(lambda feature: feature.buffer(100))
259
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  ####### YoY Profile ########
261
  start_year = 2019
262
  end_year = datetime.now().year
263
 
264
  # Create an empty resultant dataframe
265
+ columns = ['Dates', 'Imagery', 'AvgNDVI_Inside', 'Avg_NDVI_Buffer', 'Ratio', 'Id']
266
  combined_df = pd.DataFrame(columns=columns)
267
 
268
  max_ndvi_geoms = []
269
  max_ndvi_buffered_geoms = []
270
  years=[]
271
+ ndvi_collections = []
272
+ df_geoms = []
273
  for year in range(start_year, end_year+1):
274
  try:
275
  # Construct start and end dates for every year
 
285
  years.append(str(year))
286
 
287
  # Get Zonal NDVI
288
+ df_geom, ndvi_collection = get_zonal_ndvi(collection.filterBounds(geom_ee_object), geom_ee_object)
289
+ df_buffered_geom, ndvi_collection = get_zonal_ndvi(collection.filterBounds(buffered_ee_object), buffered_ee_object)
290
+ ndvi_collections.append(ndvi_collection)
291
+ df_geoms.append(df_geom)
292
+
293
 
294
  # Merge both Zonalstats and create resultant dataframe
295
+ resultant_df = pd.merge(df_geom, df_buffered_geom, on='Id', how='inner')
296
+ resultant_df = resultant_df.rename(columns={'NDVI_x': 'AvgNDVI_Inside', 'NDVI_y': 'Avg_NDVI_Buffer', 'Imagery_x': 'Imagery', 'Dates_x': 'Dates'})
297
  resultant_df['Ratio'] = resultant_df['AvgNDVI_Inside'] / resultant_df['Avg_NDVI_Buffer']
298
+ resultant_df.drop(columns=['Imagery_y', 'Dates_y'], inplace=True)
299
 
300
  # Re-order the columns of the resultant dataframe
301
+ resultant_df = resultant_df[['Dates', 'Imagery', 'AvgNDVI_Inside', 'Avg_NDVI_Buffer', 'Ratio', 'Id']]
302
 
303
  # Append to empty dataframe
304
  combined_df = pd.concat([combined_df, resultant_df], ignore_index=True)
305
  except:
306
  continue
307
 
308
+
309
  # Write the final table
310
+ st.write("NDVI details based on Sentinel-2 Surface Reflectance Bands")
311
+ st.write(combined_df[['Dates', 'Imagery', 'AvgNDVI_Inside', 'Avg_NDVI_Buffer', 'Ratio']])
312
+
313
 
314
  # Plot the multiyear timeseries
315
  st.write("Multiyear Time Series Plot (for given duration)")
316
+ st.line_chart(combined_df[['AvgNDVI_Inside', 'Avg_NDVI_Buffer', 'Dates']].set_index('Dates'))
317
 
318
  # Create a DataFrame for YoY profile
319
  yoy_df = pd.DataFrame({'Year': years, 'NDVI_Inside': max_ndvi_geoms, 'NDVI_Buffer': max_ndvi_buffered_geoms})
320
  yoy_df['Ratio'] = yoy_df['NDVI_Inside'] / yoy_df['NDVI_Buffer']
321
+ slope, intercept = np.polyfit(list(range(1, len(years)+1)), yoy_df['NDVI_Inside'], 1)
322
 
323
  # plot the time series
324
+ st.write("Year on Year Profile using Maximum NDVI Composite (computed for given duration)")
325
  st.line_chart(yoy_df[['NDVI_Inside', 'NDVI_Buffer', 'Ratio', 'Year']].set_index('Year'))
326
  st.write("Slope (trend) and Intercept are {}, {} respectively. ".format(np.round(slope, 4), np.round(intercept, 4)))
327
 
328
+ #Get Latest NDVI Collection with completeness
329
+ ndvi_collection = None
330
+ for i in range(len(ndvi_collections)):
331
+ #Check size of NDVI collection
332
+ ndvi_collection = ndvi_collections[len(ndvi_collections)-i-1]
333
+ df_geom = df_geoms[len(ndvi_collections)-i-1]
334
+ if ndvi_collection.size().getInfo()>0:
335
+ break
336
 
337
+ #Map Visualization
338
  st.write("Map Visualization")
339
 
340
+ # Function to create the map
341
+ def create_map():
342
+ m = geemapfolium.Map(center=(polygon_info['centroid'][1],polygon_info['centroid'][0]), zoom=14) # Create a Folium map
343
+
344
+ n_layers = 4 #controls the number of images to be displayed
345
+ for i in range(min(n_layers, ndvi_collection.size().getInfo())):
346
+ ndvi_image = ee.Image(ndvi_collection.toList(ndvi_collection.size()).get(i))
347
+ date = df_geom.iloc[i]["Dates"]
348
+
349
+ # Add the image to the map as a layer
350
+ layer_name = f"Image {i+1} - {date}"
351
+ vis_params = {'min': -1, 'max': 1, 'palette': ['blue', 'white', 'green']} # Example visualization for Sentinel-2
352
+ m.add_layer(ndvi_image, vis_params, layer_name, z_index=i+10)
353
+
354
+ m.add_layer(geom_ee_object, {}, 'KML Original', z_index=1)
355
+ m.add_layer(buffered_ee_object, {}, 'KML Buffered', z_index=2)
356
+
357
+ m.add_layer_control()
358
+ return m
359
+
360
+ # Cache the map to avoid recomputation on interaction (faster)
361
+ @st.cache_resource
362
+ def get_map():
363
+ return create_map()
364
+
365
  # Create Folium Map object
366
+ if "map" not in st.session_state:
367
+ st.session_state["map"] = get_map()
368
+
369
+ # Display the map and allow interactions without triggering reruns
370
+ with st.container():
371
+ # st_folium should not cause reruns, just return user interactions
372
+ st_folium(st.session_state["map"], width=725, returned_objects=[])
373
 
374
  else:
375
  st.write('ValueError: "Input must have single polygon geometry"')
376
  st.write(gdf)
377
  st.stop()
378
 
379
+
380
+