first commit
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
# crophealth.py
|
||||
'''
|
||||
Functions for loading and interacting with data in the crop health notebook,
|
||||
inside the Real_world_examples folder.
|
||||
'''
|
||||
|
||||
# Load modules
|
||||
|
||||
# Force GeoPandas to use Shapely instead of PyGEOS
|
||||
# In a future release, GeoPandas will switch to using Shapely by default.
|
||||
import os
|
||||
os.environ['USE_PYGEOS'] = '0'
|
||||
|
||||
from ipyleaflet import (
|
||||
Map,
|
||||
GeoJSON,
|
||||
DrawControl,
|
||||
basemaps
|
||||
)
|
||||
import datetime as dt
|
||||
import datacube
|
||||
from osgeo import ogr
|
||||
import matplotlib as mpl
|
||||
import matplotlib.pyplot as plt
|
||||
import rasterio
|
||||
from rasterio.features import geometry_mask
|
||||
import xarray as xr
|
||||
from IPython.display import display
|
||||
import warnings
|
||||
import ipywidgets as widgets
|
||||
import json
|
||||
import geopandas as gpd
|
||||
from io import BytesIO
|
||||
|
||||
# Load utility functions
|
||||
from deafrica_tools.datahandling import load_ard
|
||||
from deafrica_tools.spatial import xr_rasterize
|
||||
from deafrica_tools.bandindices import calculate_indices
|
||||
|
||||
|
||||
def load_crophealth_data(lat, lon, buffer, date):
|
||||
"""
|
||||
Loads Sentinel-2 analysis-ready data (ARD) product for the crop health
|
||||
case-study area over the last two years.
|
||||
Last modified: April 2020
|
||||
|
||||
Parameters
|
||||
----------
|
||||
lat: float
|
||||
The central latitude to analyse
|
||||
lon: float
|
||||
The central longitude to analyse
|
||||
buffer:
|
||||
The number of square degrees to load around the central latitude and longitude.
|
||||
For reasonable loading times, set this as `0.1` or lower.
|
||||
date:
|
||||
The most recent date to show data for.
|
||||
The app will automatically load all data available for the two years prior to this date.
|
||||
|
||||
Returns
|
||||
----------
|
||||
ds: xarray.Dataset
|
||||
data set containing combined, masked data
|
||||
Masked values are set to 'nan'
|
||||
"""
|
||||
|
||||
# Suppress warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
# Initialise the data cube. 'app' argument is used to identify this app
|
||||
dc = datacube.Datacube(app='Crophealth-app')
|
||||
|
||||
# Define area to load
|
||||
latitude = (lat - buffer, lat + buffer)
|
||||
longitude = (lon - buffer, lon + buffer)
|
||||
|
||||
# Specify the date range
|
||||
# Calculated as today's date, subtract 730 days to collect two years of data
|
||||
# Dates are converted to strings as required by loading function below
|
||||
end_date = dt.datetime.strptime(date, "%Y-%m-%d")
|
||||
start_date = end_date - dt.timedelta(days=730)
|
||||
|
||||
time = (start_date.strftime("%Y-%m-%d"), end_date.strftime("%Y-%m-%d"))
|
||||
|
||||
# Construct the data cube query
|
||||
products = ["s2_l2a"]
|
||||
|
||||
query = {
|
||||
'x': longitude,
|
||||
'y': latitude,
|
||||
'time': time,
|
||||
'measurements': [
|
||||
'red',
|
||||
'green',
|
||||
'blue',
|
||||
'nir',
|
||||
'swir_2'
|
||||
],
|
||||
'output_crs': 'EPSG:6933',
|
||||
'resolution': (-20, 20)
|
||||
}
|
||||
|
||||
# Load the data and mask out bad quality pixels
|
||||
ds = load_ard(dc, products=products, min_gooddata=0.5, **query)
|
||||
|
||||
# Calculate the normalised difference vegetation index (NDVI) across
|
||||
# all pixels for each image.
|
||||
# This is stored as an attribute of the data
|
||||
ds = calculate_indices(ds, index='NDVI', satellite_mission='s2')
|
||||
|
||||
# Return the data
|
||||
return(ds)
|
||||
|
||||
|
||||
def run_crophealth_app(ds, lat, lon, buffer):
|
||||
"""
|
||||
Plots an interactive map of the crop health case-study area and allows
|
||||
the user to draw polygons. This returns a plot of the average NDVI value
|
||||
in the polygon area.
|
||||
Last modified: January 2020
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ds: xarray.Dataset
|
||||
data set containing combined, masked data
|
||||
Masked values are set to 'nan'
|
||||
lat: float
|
||||
The central latitude corresponding to the area of loaded ds
|
||||
lon: float
|
||||
The central longitude corresponding to the area of loaded ds
|
||||
buffer:
|
||||
The number of square degrees to load around the central latitude and longitude.
|
||||
For reasonable loading times, set this as `0.1` or lower.
|
||||
"""
|
||||
|
||||
# Suppress warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
# Update plotting functionality through rcParams
|
||||
mpl.rcParams.update({'figure.autolayout': True})
|
||||
|
||||
# Define polygon bounds
|
||||
latitude = (lat - buffer, lat + buffer)
|
||||
longitude = (lon - buffer, lon + buffer)
|
||||
|
||||
# Define the bounding box that will be overlayed on the interactive map
|
||||
# The bounds are hard-coded to match those from the loaded data
|
||||
geom_obj = {
|
||||
"type": "Feature",
|
||||
"properties": {
|
||||
"style": {
|
||||
"stroke": True,
|
||||
"color": 'red',
|
||||
"weight": 4,
|
||||
"opacity": 0.8,
|
||||
"fill": True,
|
||||
"fillColor": False,
|
||||
"fillOpacity": 0,
|
||||
"showArea": True,
|
||||
"clickable": True
|
||||
}
|
||||
},
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[
|
||||
[
|
||||
longitude[0],
|
||||
latitude[0]
|
||||
],
|
||||
[
|
||||
longitude[1],
|
||||
latitude[0]
|
||||
],
|
||||
[
|
||||
longitude[1],
|
||||
latitude[1]
|
||||
],
|
||||
[
|
||||
longitude[0],
|
||||
latitude[1]
|
||||
],
|
||||
[
|
||||
longitude[0],
|
||||
latitude[0]
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
# Create a map geometry from the geom_obj dictionary
|
||||
# center specifies where the background map view should focus on
|
||||
# zoom specifies how zoomed in the background map should be
|
||||
loadeddata_geometry = ogr.CreateGeometryFromJson(str(geom_obj['geometry']))
|
||||
loadeddata_center = [
|
||||
loadeddata_geometry.Centroid().GetY(),
|
||||
loadeddata_geometry.Centroid().GetX()
|
||||
]
|
||||
loadeddata_zoom = 16
|
||||
|
||||
# define the study area map
|
||||
studyarea_map = Map(
|
||||
center=loadeddata_center,
|
||||
zoom=loadeddata_zoom,
|
||||
basemap=basemaps.Esri.WorldImagery
|
||||
)
|
||||
|
||||
# define the drawing controls
|
||||
studyarea_drawctrl = DrawControl(
|
||||
polygon={"shapeOptions": {"fillOpacity": 0}},
|
||||
marker={},
|
||||
circle={},
|
||||
circlemarker={},
|
||||
polyline={},
|
||||
)
|
||||
|
||||
# add drawing controls and data bound geometry to the map
|
||||
studyarea_map.add_control(studyarea_drawctrl)
|
||||
studyarea_map.add_layer(GeoJSON(data=geom_obj))
|
||||
|
||||
# Index to count drawn polygons
|
||||
polygon_number = 0
|
||||
|
||||
# Define widgets to interact with
|
||||
instruction = widgets.Output(layout={'border': '1px solid black'})
|
||||
with instruction:
|
||||
print("Draw a polygon within the red box to view a plot of "
|
||||
"average NDVI over time in that area.")
|
||||
|
||||
info = widgets.Output(layout={'border': '1px solid black'})
|
||||
with info:
|
||||
print("Plot status:")
|
||||
|
||||
fig_display = widgets.Output(layout=widgets.Layout(
|
||||
width="50%", # proportion of horizontal space taken by plot
|
||||
))
|
||||
|
||||
with fig_display:
|
||||
plt.ioff()
|
||||
fig, ax = plt.subplots(figsize=(8, 6))
|
||||
ax.set_ylim([0, 1])
|
||||
|
||||
colour_list = plt.rcParams['axes.prop_cycle'].by_key()['color']
|
||||
|
||||
# Function to execute each time something is drawn on the map
|
||||
def handle_draw(self, action, geo_json):
|
||||
nonlocal polygon_number
|
||||
|
||||
# Execute behaviour based on what the user draws
|
||||
if geo_json['geometry']['type'] == 'Polygon':
|
||||
|
||||
info.clear_output(wait=True) # wait=True reduces flicker effect
|
||||
|
||||
# Save geojson polygon to io temporary file to be rasterized later
|
||||
jsonData = json.dumps(geo_json)
|
||||
binaryData = jsonData.encode()
|
||||
io = BytesIO(binaryData)
|
||||
io.seek(0)
|
||||
|
||||
# Read the polygon as a geopandas dataframe
|
||||
gdf = gpd.read_file(io)
|
||||
gdf.crs = "EPSG:4326"
|
||||
|
||||
# Convert the drawn geometry to pixel coordinates
|
||||
xr_poly = xr_rasterize(gdf, ds.NDVI.isel(time=0), crs='EPSG:6933')
|
||||
|
||||
# Construct a mask to only select pixels within the drawn polygon
|
||||
masked_ds = ds.NDVI.where(xr_poly)
|
||||
|
||||
masked_ds_mean = masked_ds.mean(dim=['x', 'y'], skipna=True)
|
||||
colour = colour_list[polygon_number % len(colour_list)]
|
||||
|
||||
# Add a layer to the map to make the most recently drawn polygon
|
||||
# the same colour as the line on the plot
|
||||
studyarea_map.add_layer(
|
||||
GeoJSON(
|
||||
data=geo_json,
|
||||
style={
|
||||
'color': colour,
|
||||
'opacity': 1,
|
||||
'weight': 4.5,
|
||||
'fillOpacity': 0.0
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
# add new data to the plot
|
||||
xr.plot.plot(
|
||||
masked_ds_mean,
|
||||
marker='*',
|
||||
color=colour,
|
||||
ax=ax
|
||||
)
|
||||
|
||||
# reset titles back to custom
|
||||
ax.set_title("Average NDVI from Sentinel-2")
|
||||
ax.set_xlabel("Date")
|
||||
ax.set_ylabel("NDVI")
|
||||
|
||||
# refresh display
|
||||
fig_display.clear_output(wait=True) # wait=True reduces flicker effect
|
||||
with fig_display:
|
||||
display(fig)
|
||||
|
||||
with info:
|
||||
print("Plot status: polygon sucessfully added to plot.")
|
||||
|
||||
# Iterate the polygon number before drawing another polygon
|
||||
polygon_number = polygon_number + 1
|
||||
|
||||
else:
|
||||
info.clear_output(wait=True)
|
||||
with info:
|
||||
print("Plot status: this drawing tool is not currently "
|
||||
"supported. Please use the polygon tool.")
|
||||
|
||||
# call to say activate handle_draw function on draw
|
||||
studyarea_drawctrl.on_draw(handle_draw)
|
||||
|
||||
with fig_display:
|
||||
# TODO: update with user friendly something
|
||||
display(widgets.HTML(""))
|
||||
|
||||
# Construct UI:
|
||||
# +-----------------------+
|
||||
# | instruction |
|
||||
# +-----------+-----------+
|
||||
# | map | plot |
|
||||
# | | |
|
||||
# +-----------+-----------+
|
||||
# | info |
|
||||
# +-----------------------+
|
||||
ui = widgets.VBox([instruction,
|
||||
widgets.HBox([studyarea_map, fig_display]),
|
||||
info])
|
||||
display(ui)
|
||||
Reference in New Issue
Block a user