File size: 9,570 Bytes
46719a3
 
 
 
 
 
 
 
 
 
 
a1ed477
c66588a
e38ccc6
 
46719a3
 
 
 
 
0400d63
c0206c3
 
 
 
2276dbd
 
c0206c3
46719a3
775b8c3
2276dbd
46719a3
a1ed477
46719a3
 
 
53144e5
46719a3
 
 
882e722
46719a3
 
 
 
 
 
 
 
 
 
a1ed477
46719a3
 
882e722
46719a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8e4278b
7324e32
 
7cc54d0
7324e32
8e4278b
 
 
 
 
 
 
 
 
 
 
46719a3
 
 
 
 
 
 
 
 
7324e32
46719a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a1ed477
 
 
 
46719a3
 
 
a1ed477
46719a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
493782c
46719a3
 
 
 
 
 
 
b786efe
46719a3
b786efe
 
7d21b47
9a0e806
dbffabb
 
46719a3
dbffabb
 
46719a3
dbffabb
 
46719a3
dbffabb
acf9d02
 
46719a3
dbffabb
 
 
 
 
46719a3
dbffabb
 
46719a3
a1ed477
dbffabb
46719a3
dbffabb
 
9a0e806
a1ed477
7324e32
8e4278b
7324e32
 
 
 
 
8e4278b
 
 
 
7324e32
 
 
8e4278b
7324e32
 
 
8e4278b
 
a9d1c68
8e4278b
a9d1c68
 
25f4c1b
8e4278b
7324e32
a1ed477
 
 
e38ccc6
8cac5f3
f6c598f
 
e38ccc6
a1ed477
46719a3
 
 
 
 
 
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
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


# 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

# Calculate NDVI as Normalized Index
def reduce_zonal_ndvi(image, ee_object):
  ndvi = image.normalizedDifference(['B8', 'B4']).rename('NDVI')
  image = image.addBands(ndvi)
  image = image.select('NDVI')
  reduced = image.reduceRegion(
    reducer=ee.Reducer.mean(),
    geometry=ee_object.geometry(),
    scale=10,
    maxPixels=1e12
  )
  return image.set(reduced)

# 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()

# Get Zonal NDVI
def get_zonal_ndvi(collection, geom_ee_object):
  reduced_collection = collection.map(lambda image: reduce_zonal_ndvi(image, ee_object=geom_ee_object))
  stats_list = reduced_collection.aggregate_array('NDVI').getInfo()
  filenames = reduced_collection.aggregate_array('system:index').getInfo()
  dates = [f.split("_")[0].split('T')[0] for f in reduced_collection.aggregate_array('system:index').getInfo()]
  df = pd.DataFrame({'NDVI': stats_list, 'Date': dates, 'Imagery': filenames})
  return df


# put title in center
st.markdown("""
<style>
h1 {
    text-align: center;
}
</style>
""", unsafe_allow_html=True)

st.title("Mean NDVI Calculator")

# get the start and end date from the user
col = st.columns(2)
start_date = col[0].date_input("Start Date", value=pd.to_datetime('2021-01-01'))
end_date = col[1].date_input("End Date", value=pd.to_datetime('2021-01-30'))
# Check if start and end dates are valid
if start_date>end_date:
    st.stop()

start_date = start_date.strftime("%Y-%m-%d")
end_date = end_date.strftime("%Y-%m-%d")


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"])

if uploaded_file is not None:
    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))

        # 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_date, end_date)).select(['B4', 'B8'])

        # Get Zonal NDVI based on collection and geometries (Original KML and Buffered KML)
        df_geom = get_zonal_ndvi(collection.filterBounds(geom_ee_object), geom_ee_object)
        df_buffered_geom = get_zonal_ndvi(collection.filterBounds(buffered_ee_object), buffered_ee_object)

        # Merge both Zonalstats and create resultant dataframe
        resultant_df = pd.merge(df_geom, df_buffered_geom, on='Date', how='inner')
        resultant_df = resultant_df.rename(columns={'NDVI_x': 'AvgNDVI_Inside', 'NDVI_y': 'Avg_NDVI_Buffer', 'Imagery_x': 'Imagery'})
        resultant_df['Ratio'] = resultant_df['AvgNDVI_Inside'] / resultant_df['Avg_NDVI_Buffer']
        resultant_df.drop(columns=['Imagery_y'], inplace=True)

        # Re-order the columns of the resultant dataframe
        resultant_df = resultant_df[['Date', 'Imagery', 'AvgNDVI_Inside', 'Avg_NDVI_Buffer', 'Ratio']]

        # Write the final table
        st.write(resultant_df)
        
        # plot the time series
        st.write("Time Series Plot")
        st.line_chart(resultant_df[['AvgNDVI_Inside', 'Avg_NDVI_Buffer', 'Date']].set_index('Date'))

        ####### YoY Profile ########
        start_year = 2019
        end_year = 2023

        max_ndvi_geoms = []
        max_ndvi_buffered_geoms = []
        for year in range(start_year, end_year+1):
            
            # 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)).select(['B4', 'B8'])
        
            # Get Zonal NDVI based on collection and geometries (Original KML and Buffered KML)
            max_ndvi_geoms.append(get_zonal_ndviYoY(collection.filterBounds(geom_ee_object), geom_ee_object))
            max_ndvi_buffered_geoms.append(get_zonal_ndviYoY(collection.filterBounds(buffered_ee_object), buffered_ee_object))
        
        # Create a DataFrame for YoY profile
        yoy_df = pd.DataFrame({'Year': list(range(start_year, end_year+1)), 'NDVI_Inside': max_ndvi_geoms, 'NDVI_Buffer': max_ndvi_buffered_geoms})
        yoy_df['Ratio'] = yoy_df['NDVI_Inside'] / yoy_df['NDVI_Buffer']

        # plot the time series
        st.write("Year on Year Plot using Maximum NDVI Composite (computed for given duration)")
        st.line_chart(yoy_df[['NDVI_Inside', 'NDVI_Buffer', 'Ratio', 'Year']].set_index('Year'))
        
        # Visualize map on ESRI basemap
        st.write("Map Visualization")
        
        m = geemapfolium.Map(center=polygon_info['centroid'], zoom=14)
        # Center the map and display the image.
        m.addLayer(geom_ee_object, {}, 'KML Original')
        m.addLayer(buffered_ee_object, {}, 'KML Buffered')
        st_folium(m)
        
    
    else:
        st.write('ValueError: "Input must have single polygon geometry"')
        st.write(gdf)
        st.stop()