Spaces:
Sleeping
Sleeping
Visualize Latest NDVI on map
Browse files
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 |
-
#
|
86 |
def reduce_zonal_ndvi(image, ee_object):
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
|
|
|
|
|
|
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('
|
118 |
filenames = reduced_collection.aggregate_array('system:index').getInfo()
|
119 |
-
dates = [f.split("_")[0].split('T')[0] for f in
|
120 |
-
df = pd.DataFrame({'NDVI': stats_list, '
|
121 |
-
|
122 |
-
|
|
|
|
|
123 |
|
124 |
-
#
|
125 |
st.markdown("""
|
126 |
-
<style>
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
131 |
""", unsafe_allow_html=True)
|
132 |
|
133 |
-
st.title("
|
134 |
|
135 |
-
#
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
143 |
|
144 |
-
|
145 |
-
|
|
|
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 = ['
|
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='
|
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[['
|
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(
|
|
|
|
|
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', '
|
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(
|
265 |
|
266 |
# plot the time series
|
267 |
-
st.write("Year on Year
|
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 |
-
#
|
273 |
st.write("Map Visualization")
|
274 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
275 |
# Create Folium Map object
|
276 |
-
|
277 |
-
|
278 |
-
|
279 |
-
|
280 |
-
|
281 |
-
|
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 |
+
|