How to sepalize a process#
The wire-frame of your app is now ready but it’s an empty shell and we would like to wire it to an actual process. Sepalizing is a 2 step process that will be presented in this tutorial through the sepalization of a GEE process.
Tip
If your workflow is using CLI or is already in python (notebook) you don’t need to read the first section and can directly jump to “wire my process to a tile”
From GEE to Python#
Most of the process that requires sepalization are based on Google Earth Engine (that we will call GEE). This processes are written in Javascript in the GEE console and execute in the Javascript environment of Google (which provide several embedded tools such as a map and a plotting tool). Fortunately the Seapl framework can use the Earth Engine Python API so anything that exist in Javascript can be translated into python !
For this tutorial we will translate the following script. It analyses the cloud coverage on top of an selected AOI between 2 dates. The script provide both images and plots. It is available here.
//###################################
//## ##
//## get the parameters ##
//## ##
//###################################
var aoi = ee.FeatureCollection('users/bornToBeAlive/aoi_sandan')
var point = ee.Geometry.Point(-2.927457,6.400793)
var start_date = '2012-01-01'
var end_date = '2015-12-31'
//###################################
//## ##
//## start of the script ##
//## ##
//###################################
// Import the Landsat 8 TOA image collection.
// var l8 = ee.ImageCollection('LANDSAT/LC08/C01/T1_SR')
var l8 = ee.ImageCollection('LANDSAT/LC08/C01/T1_TOA')
.filterBounds(point)
.filterDate('2012-01-01', '2015-12-31')
.sort('CLOUD_COVER')
;
// var l7 = ee.ImageCollection('LANDSAT/LE07/C01/T1_SR')
var l7 = ee.ImageCollection('LANDSAT/LE07/C01/T1_TOA')
.filterBounds(point)
.filterDate('2012-01-01', '2015-12-31')
.sort('CLOUD_COVER')
;
//cloud
l8 = l8.map(ee.Algorithms.Landsat.simpleCloudScore)
l7 = l7.map(ee.Algorithms.Landsat.simpleCloudScore)
// add NDMI
var addNDMI_l8 = function(image) {
var ndmi = image.normalizedDifference(['B5', 'B6']).rename('NDMI');
return image.addBands(ndmi.multiply(100));
};
var addNDMI_l7 = function(image) {
var ndmi = image.normalizedDifference(['B4', 'B5']).rename('NDMI');
return image.addBands(ndmi.multiply(100));
};
l8 = l8.map(addNDMI_l8)
l7 = l7.map(addNDMI_l7)
// Get some image do display
var image_l8 = l8.first()
var image_l7 = l7.first()
//#######################################
//## ##
//## visualisation parameter ##
//## ##
//#######################################
// vis param
var rgbVis_l8 = {
bands: ['B4', 'B3', 'B2'],
min: 0.05,
max: 0.4,
gamma: 1.1
};
var rgbVis_l7 = {
bands: ['B3', 'B2', 'B1'],
min: 0.05,
max: 0.4,
gamma: 1.1
};
var ndmiParams = {
min: 0,
max: 40,
palette: ['blue', 'white', 'green']
};
var cloudParams = {
min: 20,
max: 80,
palette: ['white', 'red']
};
//################################################################
// Display the result.
Map.addLayer(image_l8.select('cloud').clip(aoi), cloudParams, 'cloud L8');
Map.addLayer(image_l7.select('cloud').clip(aoi), cloudParams, 'cloud L7');
Map.addLayer(image_l8.clip(aoi), rgbVis_l8, 'RGB image L8');
Map.addLayer(image_l7.clip(aoi), rgbVis_l7, 'RGB image L7');
Map.addLayer(image_l8.select('NDMI').clip(aoi), ndmiParams, 'NDMI image L8');
Map.addLayer(image_l7.select('NDMI').clip(aoi), ndmiParams, 'NDMI image L7');
Map.centerObject(aoi)
//###############################################################
// use the first results images to place your point on the map
// relaunch the script
// Define a region of interest as a buffer around a point.
var buffer = point.buffer(30);
Map.addLayer(point, {color: 'red'}, 'buffer')
// timeseries comparison
print(ui.Chart.image.series(l8.select(['cloud', 'NDMI']), buffer, ee.Reducer.mean(), 30));
print(ui.Chart.image.series(l7.select(['cloud', 'NDMI']), buffer, ee.Reducer.mean(), 30));
Set up#
create a test.ipynb notebook at the root of your repository. This notebook will have access to all the app component which will fasten the app wiring.
in this file create a first cell where you initialize EE API :
# test.ipynb
import ee
ee.Initialize()
Danger
If you did not authenticate to Google Earth Engine previously, some extra action will be asked in the cell output. This process need to be done at least once
Define the model#
Then you need to identify what are the input and the output of your process in order to create a model. Here we have 3 input :
AOI
start_date
end_date
point coordinates
And 2 output: - l8 ImageCollection - l7 ImageCollection
We will thus create a model
that matches our process requirements. For more information please refer to this page of the documentation.
Tip
Don’t forget to add the the file to the model package __init__.py
file
now in a second cell of our test.ipynb
we will initialize this io object with default parameters:
# test.ipynb
from component import model
process_model = model.ProcessModel()
process_model.asset = ee.FeatureCollection('users/bornToBeAlive/Juaboso_Bia_HIA')
process_model.start_date = '2012-01-01'
process_model.end_date = '2015-12-31'
Get the FeatureCollections#
Now we want to get the images collection that will be used for the rest of the process. The translation from Javascript to Python is strait forward. Keep in mind that:
Python doesn’t use
;
to end command but line breakto keep the chaining behavior and readability of ee objects use
at the end of your line
and
andor
are protected in python, useAnd
andOr
instead
Note
If you are experiencing difficulties in the translation of your code please ask questions on GIS.stackexchange using the python
and gee
keyword.
# test.ipynb
# Import the Landsat 8 TOA image collection.
l8 = ee.ImageCollection('LANDSAT/LC08/C01/T1_TOA') \
.filterBounds(aoi) \
.filterDate(process_model.start_date, process_model.end_date) \
.sort('CLOUD_COVER') \
l7 = ee.ImageCollection('LANDSAT/LE07/C01/T1_TOA') \
.filterBounds(aoi) \
.filterDate(process_model.start_date, process_model.end_date) \
.sort('CLOUD_COVER') \
# cloud
l8 = l8.map(ee.Algorithms.Landsat.simpleCloudScore)
l7 = l7.map(ee.Algorithms.Landsat.simpleCloudScore)
# add NDMI
def addNDMI_l8(image):
ndmi = image.normalizedDifference(['B5', 'B6']).rename('NDMI')
return image.addBands(ndmi.multiply(100))
def addNDMI_l7(image):
ndmi = image.normalizedDifference(['B4', 'B5']).rename('NDMI')
return image.addBands(ndmi.multiply(100))
process_io.l8 = l8.map(addNDMI_l8)
process_io.l7 = l7.map(addNDMI_l7)
display the results on a map#
to display our result we will use the SepalMap
class embedded in the sepal_ui mapping
package. It’s a wrapper of geemap Map with additional useful function. A complete description can be found here.
At the bottom of the script you see some visualization parameters. These parameters needs to be set in the parameter
component.
# component/parameter/ee_viz.py
rgbVis_l8 = {'bands': ['B4', 'B3', 'B2'], 'min': 0.05, 'max': 0.4, 'gamma': 1.1}
rgbVis_l7 = {'bands': ['B3', 'B2', 'B1'], 'min': 0.05, 'max': 0.4, 'gamma': 1.1}
ndmiParams = {'min': 0, 'max': 40, 'palette': ['blue', 'white', 'green']}
cloudParams = {'min': 20, 'max': 80, 'palette': ['white', 'red']}
Tip
The Python dictionaries keys need to be set between "
set a SepalMap object and then add all the images you like using the same method as in Javascript:
# test.ipynb
from component import parameter as cp
from sepal_ui import mapping as sm
Map = sm.SepalMap(['CartoDB.Positron'])
Map.addLayer(process_model.l8.first().select('cloud').clip(process_model.asset), cloudParams, 'cloud L8')
Map.addLayer(process_model.l7.first().select('cloud').clip(process_model.asset), cloudParams, 'cloud L7')
Map.addLayer(process_model.l8.first().clip(process_model.asset), rgbVis_l8, 'RGB image L8')
Map.addLayer(process_model.l7.first().clip(process_model.asset), rgbVis_l7, 'RGB image L7')
Map.addLayer(process_model.l8.first().select('NDMI').clip(process_model.asset), ndmiParams, 'NDMI image L8')
Map.addLayer(process_model.l7.first().select('NDMI').clip(process_model.asset), ndmiParams, 'NDMI image L7')
Map.zoom_ee_object(process_model.asset.geometry())
Create the Histogram#
GEE provide tools to directly produce graphs out of ImageCollections. In Python, the graphs will be displayed using the pyplotlib
or the bqplot
libraries.
So our work here is to extract the data from our images to reproduce the behavior of the plotting function. In this script we will translate the ui.Chart.image.series
method but it can be any other one.
Tip
You can ask help on GIS.StackExchange on the translation of the different charting methods. Some of them have already been treated:
We thus need to create a specific function that build a matplotlib
chart from ee data :
# test.ipynb import matplotlib.pyplot as plt import matplotlib.dates as mdates import pandas as pd from datetime import datetime from dateutil.relativedelta import * def create_hist(dataset, point, title): buffer = point.buffer(30) stats_image_collection = dataset.select(['cloud', 'NDMI']).map(lambda image: ee.Image(image.setMulti(image.reduceRegion( reducer = ee.Reducer.mean(), geometry = buffer, scale = 30, maxPixels = 1e9 ))) ) dates = [datetime.fromtimestamp(d//1000) for d in stats_image_collection.aggregate_array('system:time_start').getInfo()] ndmi = stats_image_collection.aggregate_array('NDMI').getInfo() cloud = stats_image_collection.aggregate_array('cloud').getInfo() if len(ndmi) == len(cloud) == len(dates): pass elif len(dates) > len(cloud) == len(ndmi): dates = dates[1:] else: raise Exception(f'The size are all different.\n dates: {len(dates)}\n ndmi: {len(ndmi)}\n cloud: {len(cloud)}') df = pd.DataFrame({'ndmi': ndmi, 'cloud': cloud}, index = dates) years = mdates.YearLocator() # every year months = mdates.MonthLocator() # every month years_fmt = mdates.DateFormatter('%b-%y') fig, ax = plt.subplots(figsize=(10,10)) df.plot(ax=ax) ax.set_title(title, fontweight="bold") # format the ticks ax.xaxis.set_major_locator(years) ax.xaxis.set_major_formatter(years_fmt) ax.xaxis.set_minor_locator(months) plt.show()
This function can then be called on each image from the process_model
:
# test.ipynb
create_hist(process_model.l8, process_model.point, 'landsat 8')
create_hist(process_model.l7, process_model.point, 'landsat 7')
All this functions are now functional. You can add them in the script component using the necessary parameters here process_model
and Map
.
wire process to a tile#
We will assume that you followed the tutorial on how to add a tile to my module and that your logic is described in the scripts package. If that’s not the case please refer to the appropriate step of the documentation.
your tile should look like this one :
# component/tile/process_tile.py
# component and widgets imports
# [...]
from component import scripts as cs
class ProcessTile(sw.Tile):
def __init__(self, model, aoi_model, m):
# Define the model and the aoi_model as class attribute so that they can be manipulated in its custom methods
self.model = model
self.aoi_model = aoi_model
semf.m = m
# the widget are defined
# [...]
# and linked to the io attributes using the model
# [...]
# construct the Tile with the widget we have initialized
super().__init__(
id_ = "process_widget", # the id will be used to make the Tile appear and disappear
title = ms.process.title, # the Title will be displayed on the top of the tile
inputs = [...] # input list
btn = sw.Btn(),
alert = sw.Alert()
)
We want to launch the process when the button is click and use all the model attributes as parameters. important things in your tile are:
set the model objects as class attributes
wire the widget to the model attributes
create a button
btn
is a Vuetify widget so it inherit some Javascripts behaviors that are describe in the ipyvuetify documentation.
here we will launch a function on every click on it:
# component/tile/process_tile.py
from sepal_ui.scripts.utils import loading_button
class ProcessTile(sw.Tile):
def __init__(self, io, aoi_io, m):
#[...]
# now that the Tile is created we can link it to a specific function
self.btn.on_event("click", self._on_run)
@loading_button()
def _on_click(self, widget, data, event):
# do stuff
return self
Some explanation on what we just coded. The on_event
method is linking the button Javascripts behavior to the python function. a complete list of Javascript’s events can be found here.
this event is linked to a callback function. This function can only have 3 arguments :
widget: the widget that thrown the event
event: the details of the event
data: the data shared on the event (none in most of the case)
As a member of the ProcessTile
class, the _on_click
method add the self argument in first position. It will allow the function to have access to all the class attribute.
A process should look like the following :
@loading_button()
def _on_click(self, widget, event, data):
# check that the input that you're gonna use are set (Not mandatory)
if not self.output.check_input(self.aoi_io.get_aoi_name()): return
if not self.output.check_input(self.io.year): return
# do stuff
return self