import os import ee import json import geemap import geemap.foliumap as gee_folium import leafmap.foliumap as leaf_folium import streamlit as st from pandas import to_datetime, read_csv, merge, date_range, DateOffset from geopandas import read_file from shapely.ops import transform from functools import reduce import plotly.express as px st.set_page_config(layout="wide") ############################################ # One time setup ############################################ def initialize_ee(): credentials_path = os.path.expanduser("~/.config/earthengine/credentials") if os.path.exists(credentials_path): pass # Earth Engine credentials already exist elif "EE" in os.environ: # write the credentials to the file ee_credentials = os.environ.get("EE") os.makedirs(os.path.dirname(credentials_path), exist_ok=True) with open(credentials_path, "w") as f: f.write(ee_credentials) else: raise ValueError( f"Earth Engine credentials not found at {credentials_path} or in the environment variable 'EE'" ) ee.Initialize() if "ee_initialized" not in st.session_state: initialize_ee() st.session_state.ee_initialized = True if "wayback_mapping" not in st.session_state: with open("wayback_imagery.json") as f: st.session_state.wayback_mapping = json.load(f) ############################################ # Functions ############################################ def shape_3d_to_2d(shape): if shape.has_z: return transform(lambda x, y, z: (x, y), shape) else: return shape def preprocess_gdf(gdf): gdf = gdf.to_crs(epsg=4326) gdf = gdf[['Name', 'geometry']] gdf["geometry"] = gdf["geometry"].apply(shape_3d_to_2d) return gdf def calculate_ndvi(image, nir_band, red_band): nir = image.select(nir_band) red = image.select(red_band) ndvi = nir.subtract(red).divide(nir.add(red)).rename("NDVI") return image.addBands(ndvi) def postprocess_df(df, name): df = df.T df = df.reset_index() ndvi_df = df[df["index"].str.contains("NDVI")] ndvi_df["index"] = to_datetime(ndvi_df["index"], format="%Y-%m_NDVI") ndvi_df = ndvi_df.rename(columns={"index": "Date", 0: name}) cloud_mask_probability = df[df["index"].str.contains("MSK_CLDPRB")] cloud_mask_probability["index"] = to_datetime(cloud_mask_probability["index"], format="%Y-%m_MSK_CLDPRB") cloud_mask_probability = cloud_mask_probability.rename(columns={"index": "Date", 0: f"{name}_cloud_proba"}) # normalize cloud_mask_probability[f"{name}_cloud_proba"] = cloud_mask_probability[f"{name}_cloud_proba"] / 100 df = merge(ndvi_df, cloud_mask_probability, on="Date", how="outer") return df def write_info(info): st.write(f"{info}", unsafe_allow_html=True) ############################################ # App ############################################ # Title # make title in center st.markdown( f"""

Mean NDVI Calculator

""", unsafe_allow_html=True, ) # Input: Date and Cloud Cover col = st.columns(2) start_date = col[0].date_input("Start Date", value=to_datetime("2021-01-01")) end_date = col[1].date_input("End Date", value=to_datetime("2021-07-31")) start_date = start_date.strftime("%Y-%m") end_date = end_date.strftime("%Y-%m") # max_cloud_cover = st.number_input("Max Cloud Cover (in percentage)", value=5) # Input: GeoJSON/KML file uploaded_file = st.file_uploader("Upload KML/GeoJSON file", type=["geojson", "kml"]) if uploaded_file is None: st.stop() file_name = uploaded_file.name gdf = read_file(uploaded_file) gdf = preprocess_gdf(gdf) selected_shape = st.selectbox("Select the geometry", gdf.Name.values) if selected_shape is None: st.stop() selected_shape = gdf[gdf.Name == selected_shape] ee_object = geemap.gdf_to_ee(selected_shape) write_info(f"Type of Geometry: {selected_shape.geometry.type.values[0]}") st.write("Select the satellite sources:") satellites = { "LANDSAT/LC08/C02/T1_TOA": { "selected": st.checkbox("LANDSAT/LC08/C02/T1_TOA", value=True), "nir_band": "B5", "red_band": "B4", "scale": 30, }, "COPERNICUS/S2_SR_HARMONIZED": { "selected": st.checkbox("COPERNICUS/S2_SR_HARMONIZED", value=True), "nir_band": "B8", "red_band": "B4", "scale": 10, }, } submit = st.button("Submit", use_container_width=True) if submit: if not any(satellites.values()): st.error("Please select at least one satellite source") st.stop() # Create month range dates = date_range(start_date, end_date, freq="MS").strftime("%Y-%m-%d").tolist() write_info( f"Start Date (inclusive): {start_date}, End Date (exclusive): {end_date}" ) df_list = [] collections = {} for satellite, attrs in satellites.items(): if not attrs["selected"]: continue collection = ee.ImageCollection(satellite) collection = collection.filterBounds(ee_object) if satellite == "COPERNICUS/S2_SR_HARMONIZED": collection = collection.select([attrs["red_band"], attrs["nir_band"], "MSK_CLDPRB"]) else: collection = collection.select([attrs["red_band"], attrs["nir_band"]]) # collection = collection.filter(ee.Filter.lt(attrs["cloud_cover_var"], max_cloud_cover)) collection = collection.filterDate(start_date, end_date) collection = collection.map( lambda image: calculate_ndvi( image, nir_band=attrs["nir_band"], red_band=attrs["red_band"] ) ) write_info(f"Number of images in {satellite}: {collection.size().getInfo()}") progress_bar = st.progress(0) def monthly_quality_mosaic(start, end, i): progress_bar.progress((i + 1) / (len(dates) - 1)) collection_filtered = collection.filterDate(start, end) size = collection_filtered.size().getInfo() if size == 0: return None mosaic = collection_filtered.qualityMosaic("NDVI") month = to_datetime(start).strftime("%Y-%m") print(f"Processing {month} with {size} images") return mosaic.set("system:index", f"{month}") collection = [monthly_quality_mosaic(start, end, i) for i, (start, end) in enumerate(zip(dates[:-1], dates[1:]))] collection = list(filter(None, collection)) collection = ee.ImageCollection(collection) collections[satellite] = collection save_name = satellite.replace("/", "_") geemap.zonal_stats( collection,#.select(["NDVI"]), ee_object, f"/tmp/{save_name}.csv", stat_type="mean", scale=attrs["scale"], ) df = read_csv(f"/tmp/{save_name}.csv") df = postprocess_df(df, name=satellite) df_list.append(df) df = reduce(lambda left, right: merge(left, right, on="Date", how="outer"), df_list) df = df.sort_values("Date") # drop rows with all NaN values df = df.dropna(how="all") # drop columns with all NaN values df = df.dropna(axis=1, how="all") st.session_state.df = df st.session_state.collections = collections if "df" in st.session_state: df = st.session_state.df collections = st.session_state.collections st.write(df.applymap(lambda x: f"{x:.2f}" if isinstance(x, float) else x)) fig = px.line(df, x="Date", y=df.columns[1:], title='Mean NDVI', markers=True) fig.update_yaxes(range=[0, 1]) st.plotly_chart(fig) st.subheader("Visual Inspection") cols = st.columns(2) with cols[0]: start_date = st.selectbox("Start Date", df.Date, index=0) start_date_index = df[df.Date == start_date].index[0].item() with cols[1]: end_date = st.selectbox("End Date", df.Date, index=len(df.Date) - 1) end_date_index = df[df.Date == end_date].index[0].item() for imagery in satellites: collection = collections[imagery] m_list = [] for col, date in zip(cols, [start_date, end_date]): date_index = df[df.Date == date].index[0].item() image = ee.Image(collections[imagery].toList(collection.size()).get(date_index)) layer = gee_folium.ee_tile_layer(image, {"bands": ["NDVI"], "min": 0, "max": 1}, f"{imagery}_{date}") with col: m = leaf_folium.Map() m.add_layer(layer) m.add_gdf(selected_shape, layer_name="Selected Geometry") st.write(f"{imagery} - {date}") m.to_streamlit() for col, date in zip(cols, [start_date, end_date]): esri_date = min(st.session_state.wayback_mapping.keys(), key=lambda x: abs(to_datetime(x) - date)) with col: m = leaf_folium.Map() m.add_tile_layer(st.session_state.wayback_mapping[esri_date], name=f"Esri Wayback Imagery - {date}", attribution="Esri") m.add_gdf(selected_shape, layer_name="Selected Geometry") st.write(f"Esri Wayback Imagery - {esri_date} (Closest to {date})") m.to_streamlit()