Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
giswqs
GitHub Repository: giswqs/geemap
Path: blob/master/docs/workshops/IPPN_2024.ipynb
2313 views
Kernel: geo

image

Open Source Pipeline for UAS and satellite based High Throughput Phenotyping Applications - Part 2

This notebook is designed for workshop presented at the International Plant Phenotyping Network (IPPN) conference on October 7, 2024. Click the Open in Colab button above to run this notebook interactively in the cloud. For Part 1 of the workshop, please visit this link.

Acknowledgments

This training material was partially supported by the National Agricultural Producers Data Cooperative (USDA award 2023-77039-41033; Sub-award 25-6231-0428-008).

Introduction

Recent advances in sensor technology have revolutionized the assessment of crop health by providing fine spatial and high temporal resolutions at affordable costs. As plant scientists gain access to increasingly larger volumes of Unmanned Aerial Systems (UAS) and satellite High Throughput Phenotyping (HTP) data, there is a growing need to extract biologically informative and quantitative phenotypic information from the vast amount of freely available geospatial data. However, the lack of specialized software packages tailored for processing such data makes it challenging to develop transdisciplinary research collaboration around these data. This workshop aims to bridge the gap between big data and agricultural research scientists by providing training on an open-source online platform for managing big UAS HTP data known as Data to Science. Additionally, attendees will be introduced to powerful Python packages, namely leafmap and Leafmap, designed for the seamless integration and analysis of UAS and satellite images in various agricultural applications. By participating in this workshop, attendees will acquire the skills necessary to efficiently search, visualize, and analyze geospatial data within a Jupyter environment, even with minimal coding experience. The workshop provides a hands-on learning experience through practical examples and interactive exercises, enabling participants to enhance their proficiency and gain valuable insights into leveraging geospatial data for agricultural research purposes.

Agenda

The main topics to be covered in this workshop include:

  • Create interactive maps using leafmap

  • Visualize drone imagery from D2S

  • Segment drone imagery using samgeo

  • Calculate zonal statistics from drone imagery

  • Visualize Earth Engine data

  • Create timelapse animations

Environment setup

image

Change Colab dark theme

Currently, ipywidgets does not work well with Colab dark theme. Some of the leafmap widgets may not display properly in Colab dark theme.It is recommended that you change Colab to the light theme.

image

Install packages

Uncomment the following code to install the required packages.

# %pip install -U "geemap[workshop]" leafmap d2spy mapclassify

Import libraries

Import the necessary libraries for this workshop.

import ee import leafmap import geemap from geemap import chart import matplotlib.pyplot as plt

Creating interactive maps

Let's create an interactive map using the ipyleaflet plotting backend. The leafmap.Map class inherits the ipyleaflet.Map class. Therefore, you can use the same syntax to create an interactive map as you would with ipyleaflet.Map.

m = leafmap.Map()

To display it in a Jupyter notebook, simply ask for the object representation:

m

To customize the map, you can specify various keyword arguments, such as center ([lat, lon]), zoom, width, and height. The default width is 100%, which takes up the entire cell width of the Jupyter notebook. The height argument accepts a number or a string. If a number is provided, it represents the height of the map in pixels. If a string is provided, the string must be in the format of a number followed by px, e.g., 600px.

m = leafmap.Map(center=[40, -100], zoom=4, height="600px") m

Adding basemaps

There are several ways to add basemaps to a map. You can specify the basemap to use in the basemap keyword argument when creating the map. Alternatively, you can add basemap layers to the map using the add_basemap method. leafmap has hundreds of built-in basemaps available that can be easily added to the map with only one line of code.

Create a map by specifying the basemap to use as follows. For example, the Esri.WorldImagery basemap represents the Esri world imagery basemap.

m = leafmap.Map(basemap="Esri.WorldImagery") m

You can add as many basemaps as you like to the map. For example, the following code adds the OpenTopoMap basemap to the map above:

m.add_basemap("OpenTopoMap")

You can also add an XYZ tile layer to the map.

basemap_url = "https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}" m.add_tile_layer(basemap_url, name="Hybrid", attribution="Google")

You can also change basemaps interactively using the basemap GUI.

m = leafmap.Map() m.add_basemap_gui() m

Visualizing Drone Imagery from D2S

The Data to Science (D2S) platform (https://ps2.d2s.org) hosts a large collection of drone imagery that can be accessed through the D2S API (https://py.d2s.org). To visualize drone imagery from D2S, you need to sign up for a free account on the D2S platform and obtain an API key.

Login to D2S

Login and connect to your D2S workspace in one go using the d2spy.

from d2spy.workspace import Workspace # Replace with URL to a D2S instance d2s_url = "https://ps2.d2s.org" # Login and connect to workspace with your email address workspace = Workspace.connect(d2s_url, "[email protected]")
# Check for API key api_key = workspace.api_key if not api_key: print( "No API key. Please request one from the D2S profile page and re-run this cell." )
import os from datetime import date os.environ["D2S_API_KEY"] = api_key os.environ["TITILER_ENDPOINT"] = "https://tt.d2s.org"

Choose a project to work with

The Workspace get_projects method will retrieve a collection of the projects your account can currently access on the D2S instance.

# Get list of all your projects projects = workspace.get_projects() for project in projects: print(project)

The projects variable is a ProjectCollection. The collection can be filtered by either the project descriptions or titles using the methods filter_by_title or filter_by_name.

# Example of creating new collection of only projects with the keyword "Corn" in the title filtered_projects = projects.filter_by_title("Corn") print(filtered_projects)

Now you can choose a specific project to work with. In this case, the filtered projects returned only one project, so we will use that project.

project = filtered_projects[0]

Get the project boundary

get_project_boundary method of the Project class will retrieve a GeoJSON object of the project boundary.

# Get project boundary as Python dictionary in GeoJSON structure project_boundary = project.get_project_boundary() project_boundary

Get project flights

The Project get_flights method will retrieve a list of flights associated with the project.

# Get list of all flights for a project flights = project.get_flights() # Print first flight object (if one exists) for flight in flights: print(flight)

Now, we can choose a flight from the filtered flight. Let's choose the flight on June 8, 2023.

flight = flights[0] flight

Get data products

The Flight get_data_products method will retrieve a list of data products associated with the flight.

# Get list of data products from a flight data_products = flight.get_data_products() for data_product in data_products: print(data_product)

The data_products variable is a DataProductCollection. The collection can be filtered by data type using the method filter_by_data_type. This method will return all data products that match the requested data type.

# Example of creating new collection of data products with the "ortho" data type ortho_data_products = data_products.filter_by_data_type("ortho") print(ortho_data_products)

Visualize ortho imagery

Now we can grab the ortho URL to display it using leafmap.

m = leafmap.Map() m.add_basemap("HYBRID", show=False) ortho_data = ortho_data_products[0] ortho_url_202306 = ortho_data.url ortho_url_202306 = leafmap.d2s_tile(ortho_url_202306) m.add_cog_layer(ortho_url_202306, name="Ortho Imagery 202306") m

image

Add the project boundary to the map.

m.add_geojson(project_boundary, layer_name="Project Boundary", info_mode=None)

Add grid boundaries to the map.

map_layers = project.get_map_layers() grid_boundaries = map_layers[0]
m.add_geojson(grid_boundaries, layer_name="Grid Boundaries")

Visualizing Earth Engine data

Authenticate and initialize Earth Engine

You will need to create a Google Cloud Project and enable the Earth Engine API for the project. You can find detailed instructions here.

Login to Earth Engine JavaScript Code Editor at https://code.earthengine.google.com and click on th profile icon at the top right. Remember the project ID listed in the dialog that appears. Uncomment the following code block and replace YOUR_PROJECT_ID with your project ID.

# os.environ["EE_PROJECT_ID"] = "YOUR-PROJECT-ID"

Then, run the code block to authenticate and initialize the Earth Engine Python API.

geemap.ee_initialize(project=None)

Create an interactive map

m = geemap.Map(center=[40.565282, -86.959159], zoom=16) m.add_basemap("Esri.WorldImagery") m._toolbar.toggle_layers(False) m

Visualize project boundary

project_boundary_ee = ee.FeatureCollection(geemap.geojson_to_ee(project_boundary)) style = {"color": "yellow", "fillColor": "#00000000"} m.add_layer(project_boundary_ee.style(**style), {}, name="Project Boundary")

Visualize grid boundaries

grid_boundaries_ee = ee.FeatureCollection(geemap.geojson_to_ee(grid_boundaries)) style = {"color": "red", "fillColor": "#00000000"} m.add_layer(grid_boundaries_ee.style(**style), {}, name="Tree Boundaries")

Create NAIP timeseries

naip_timeseries = geemap.naip_timeseries(project_boundary_ee) naip_timeseries
m.add_time_slider( naip_timeseries, vis_params={"bands": ["R", "G", "B"]}, layer_name="NAIP" )

Create Sentinel-2 timeseries

start_date = "2023-01-01" end_date = "2023-12-31" collection = ee.ImageCollection("COPERNICUS/S2_HARMONIZED") images = ( collection.filterBounds(project_boundary_ee) .filterDate(start_date, end_date) .filterMetadata("CLOUDY_PIXEL_PERCENTAGE", "less_than", 1) .select(["B2", "B3", "B4", "B8"]) .map( lambda img: img.divide(10000) .clipToCollection(project_boundary_ee) .copyProperties(img, img.propertyNames()) ) ) images.size()
dates = ee.List(geemap.image_dates(images)).distinct() dates

Create NDVI timeseries

def create_timeseries(date): date = ee.Date(date) image = images.filterDate(date, date.advance(1, "day")).mosaic() image = image.addBands(image.normalizedDifference(["B8", "B4"]).rename("NDVI")) return image.set("system:time_start", date.millis()) s2_timeseries = ee.ImageCollection(dates.map(create_timeseries))
s2_timeseries
m = geemap.Map(center=[40.565282, -86.959159], zoom=16) m.add_basemap("Esri.WorldImagery") m._toolbar.toggle_layers(False) m

image

vis_params = {"min": 0, "max": 0.3, "bands": ["B4", "B3", "B2"]} m.add_time_slider(s2_timeseries, vis_params=vis_params, layer_name="Sentinel-2")
grid_boundaries_ee = ee.FeatureCollection(geemap.geojson_to_ee(grid_boundaries)) style = {"color": "red", "fillColor": "#00000000"} m.add_layer(grid_boundaries_ee.style(**style), {}, name="Tree Boundaries")
grid_df = geemap.ee_to_df(grid_boundaries_ee) grid_df
ndvi_ts = s2_timeseries.select("NDVI").toBands() ndvi_ts = ndvi_ts.select(ndvi_ts.bandNames(), dates) ndvi_ts

Analyzing Earth Engine data

Calculate zonal statistics

stats = geemap.zonal_stats( ndvi_ts, grid_boundaries_ee, scale=10, stat_type="mean", verbose=False, return_fc=True, ) gdf = geemap.ee_to_gdf(stats) gdf.head()
gdf.explore()

Plot NDVI graphs

fig = chart.image_series( s2_timeseries, region=grid_boundaries_ee, reducer=ee.Reducer.mean(), scale=10, x_property="system:time_start", chart_type="LineChart", x_cols="date", y_cols=["NDVI"], legend_location="right", ) fig

image

# Extract columns with dates (assuming they're strings that look like '2023-01-10') date_columns = gdf.columns[gdf.columns.str.contains(r"\d{4}-\d{2}-\d{2}")] # Extract the relevant date columns and transpose the dataframe for plotting data_to_plot = gdf[date_columns].T # Plot each row as a separate line plt.figure(figsize=(15, 6)) for idx in range(data_to_plot.shape[1]): plt.plot(data_to_plot.index, data_to_plot.iloc[:, idx], label=f"Row {idx+1}") plt.xlabel("Dates") plt.ylabel("NDVI") plt.title("Normalized Difference Vegetation Index (NDVI) Time Series") plt.xticks(rotation=45) # plt.legend(loc='upper right', bbox_to_anchor=(1.15, 1)) plt.tight_layout() plt.show()

image