623 lines
24 KiB
Plaintext
623 lines
24 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {
|
|
"tags": []
|
|
},
|
|
"source": [
|
|
"# Sentinel-2 L2A <img align=\"right\" src=\"../../resources/csiro_easi_logo.png\">\n",
|
|
"\n",
|
|
"#### Index\n",
|
|
"- [Overview](#Overview)\n",
|
|
"- [Setup (imports, defaults, dask, odc)](#Setup)\n",
|
|
"- [Example query](#Example-query)\n",
|
|
"- [Product definition](#Product-definition)\n",
|
|
"- [Quality layer](#Quality-layer)\n",
|
|
"- [Create and apply a good quality pixel mask](#Create-and-apply-a-good-quality-pixel-mask)\n",
|
|
"- [Plot and browse the data](#Plot-and-browse-the-data)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {
|
|
"jp-MarkdownHeadingCollapsed": true,
|
|
"tags": []
|
|
},
|
|
"source": [
|
|
"## Overview\n",
|
|
"\n",
|
|
"Sentinel-2 is an Earth observation mission from the EU Copernicus Programme that systematically acquires optical imagery at high spatial resolution (up to 10 m for some bands). The mission is a constellation of two identical satellites in the same orbit, 180° apart for optimal coverage and data delivery. Together, they cover all Earth's land surfaces, large islands, inland and coastal waters every 3-5 days.\n",
|
|
"\n",
|
|
"Sentinel-2A was launched on 23 June 2015 and Sentinel-2B followed on 7 March 2017.\n",
|
|
"Both of the Sentinel-2 satellites carry a wide swath high-resolution multispectral imager with 13 spectral bands.\n",
|
|
"For more information see:\n",
|
|
"- [ESA Sentinel missions](https://www.esa.int/Applications/Observing_the_Earth/Copernicus/The_Sentinel_missions)\n",
|
|
"- [Sentinel-2 technical specifications](https://sentinels.copernicus.eu/web/sentinel/technical-guides/sentinel-2-msi)\n",
|
|
"\n",
|
|
"_Selected text adapted from https://github.com/GeoscienceAustralia/dea-notebooks/blob/develop/DEA_datasets/Sentinel_2.ipynb_\n",
|
|
"\n",
|
|
"#### Data source and documentation\n",
|
|
"\n",
|
|
"ESA produces a surface reflectance \"S2 L2A\" product using their [sen2cor](https://step.esa.int/main/snap-supported-plugins/sen2cor/) software. [Element84](https://github.com/Element84/earth-search) convert these data to Cloud-Optimized Geotiff format and makes them publicly available at their [Earth Search STAC API](https://earth-search.aws.element84.com/v1) endpoint, and [AWS open data](https://registry.opendata.aws/sentinel-2-l2a-cogs/), for programmatic access.\n",
|
|
"\n",
|
|
"EASI uses its STAC indexing tools to index datasets into our ODC databases.\n",
|
|
"\n",
|
|
"| Name | Product | Source | Information | Index\n",
|
|
"|--|--|--|--|--|\n",
|
|
"| Sentinel-2 COGs | `s2_l2a` | [Earth Search STAC](https://earth-search.aws.element84.com/v1/collections/sentinel-2-l2a) | Use for global (land) surface reflectance | Select COGS via STAC and convert to ODC metadata\n",
|
|
"\n",
|
|
"#### Baseline processing and offset consideration\n",
|
|
"\n",
|
|
"ESA introduced a change to their L1C processing (processing baseline >=04.00, for data since 25 January 2022) that encodes their L1C and L2A products with an _offset_ value such that\n",
|
|
"`phyiscal_value = encoded_value * scale_factor + offset`\n",
|
|
"\n",
|
|
"Element84 additionally [may have pre-applied the offset or not](https://github.com/Element84/earth-search/issues/23#issuecomment-1834674853) to the data that we index and load. This introduces an inconsistency in the S2A series that we need to account for.\n",
|
|
"\n",
|
|
"In this notebook we [show how to](#Apply-the-correct-offset-to-the-source-data) correctly load `s2_l2a` data with the offset applied to scenes where required. We use a `load_s2l2a_with_offset` function provided in `easi_tools` that identifies which scenes within a query should and should not have the offset applied, loads the data and applies the offset, and combines the non-offsetted with the offsetted scenes into a single xarray Dataset. This method also applies the scale_factor to all data in the query."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Setup"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"#### Imports"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {
|
|
"tags": []
|
|
},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Common imports and settings\n",
|
|
"import os, sys, re\n",
|
|
"from pathlib import Path\n",
|
|
"from IPython.display import Markdown\n",
|
|
"import pandas as pd\n",
|
|
"pd.set_option(\"display.max_rows\", None)\n",
|
|
"import xarray as xr\n",
|
|
"\n",
|
|
"# Datacube\n",
|
|
"import datacube\n",
|
|
"from datacube.utils.aws import configure_s3_access\n",
|
|
"import odc.geo.xr # https://github.com/opendatacube/odc-geo\n",
|
|
"from datacube.utils import masking # https://github.com/opendatacube/datacube-core/blob/develop/datacube/utils/masking.py\n",
|
|
"from odc.algo import enum_to_bool # https://github.com/opendatacube/odc-tools/blob/develop/libs/algo/odc/algo/_masking.py\n",
|
|
"from dea_tools.plotting import display_map, rgb # https://github.com/GeoscienceAustralia/dea-notebooks/tree/develop/Tools\n",
|
|
"\n",
|
|
"# Basic plots\n",
|
|
"%matplotlib inline\n",
|
|
"# import matplotlib.pyplot as plt\n",
|
|
"# plt.rcParams['figure.figsize'] = [12, 8]\n",
|
|
"\n",
|
|
"# Holoviews\n",
|
|
"# https://holoviz.org/tutorial/Composing_Plots.html\n",
|
|
"# https://holoviews.org/user_guide/Composing_Elements.html\n",
|
|
"import hvplot.pandas\n",
|
|
"import hvplot.xarray\n",
|
|
"import panel as pn\n",
|
|
"import colorcet as cc\n",
|
|
"import cartopy.crs as ccrs\n",
|
|
"from datashader import reductions\n",
|
|
"from holoviews import opts\n",
|
|
"# hv.extension('bokeh', logo=False)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# EASI defaults\n",
|
|
"# These are convenience functions so that the notebooks in this repository work in all EASI deployments\n",
|
|
"\n",
|
|
"# The `git.Repo()` part returns the local directory that easi-notebooks has been cloned into\n",
|
|
"# If using the `easi-tools` functions from another path, replace `repo` with your local path to `easi-notebooks` directory\n",
|
|
"try:\n",
|
|
" import git\n",
|
|
" repo = git.Repo('.', search_parent_directories=True).working_tree_dir # Path to this cloned local directory\n",
|
|
"except (ImportError, git.InvalidGitRepositoryError):\n",
|
|
" repo = Path.home() / 'easi-notebooks' # Reasonable default\n",
|
|
" if not repo.is_dir():\n",
|
|
" raise RuntimeError('To use `easi-tools` please provide the local path to `https://github.com/csiro-easi/easi-notebooks`')\n",
|
|
"if repo not in sys.path:\n",
|
|
" sys.path.append(str(repo)) # Add the local path to `easi-notebooks` to python\n",
|
|
"\n",
|
|
"from easi_tools import EasiDefaults\n",
|
|
"from easi_tools import initialize_dask, xarray_object_size, mostcommon_crs, heading\n",
|
|
"from easi_tools.load_s2l2a import load_s2l2a_with_offset"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {
|
|
"tags": []
|
|
},
|
|
"source": [
|
|
"#### EASI defaults\n",
|
|
"\n",
|
|
"These default values are configured for each EASI instance. They help us to use the same training notebooks in each EASI instance. You may find some of the functions convenient for your work or you can easily override the values in your copy of this notebook."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {
|
|
"tags": []
|
|
},
|
|
"outputs": [],
|
|
"source": [
|
|
"easi = EasiDefaults()\n",
|
|
"\n",
|
|
"family = 'sentinel-2'\n",
|
|
"product = 's2_l2a' # Sentinel-2 collection 0, L2A\n",
|
|
"display(Markdown(f'Default {family} product for \"{easi.name}\": [{product}]({easi.explorer}/products/{product})'))"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"#### Dask cluster\n",
|
|
"\n",
|
|
"Using a local _Dask_ cluster is a good habit to get into. It can simplify loading and processing of data in many cases, and it provides a dashboard that shows the loading/processing progress.\n",
|
|
"\n",
|
|
"To learn more about _Dask_ see the set of [dask notebooks](https://github.com/csiro-easi/easi-notebooks/tree/main/html#dask-tutorials)."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {
|
|
"tags": []
|
|
},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Local cluster\n",
|
|
"cluster, client = initialize_dask(workers=4)\n",
|
|
"display(client)\n",
|
|
"\n",
|
|
"# Or use Dask Gateway - this may take a few minutes\n",
|
|
"# cluster, client = initialize_dask(use_gateway=True, workers=4)\n",
|
|
"# display(client)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"#### ODC database\n",
|
|
"\n",
|
|
"Connect to the ODC database. Configure the environment and low-level tools to read from AWS buckets."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"dc = datacube.Datacube()\n",
|
|
"\n",
|
|
"# Access AWS \"requester-pays\" buckets\n",
|
|
"# This is necessary for reading data from most third-party AWS S3 buckets such as for Landsat and Sentinel-2\n",
|
|
"configure_s3_access(aws_unsigned=False, requester_pays=True, client=client);"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {
|
|
"tags": []
|
|
},
|
|
"source": [
|
|
"## Example query\n",
|
|
"\n",
|
|
"Change any of the parameters in the `query` object below to adjust the location, time, projection, or spatial resolution of the returned datasets.\n",
|
|
"\n",
|
|
"Use the Explorer interface to check the temporal and spatial coverage for each product."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {
|
|
"tags": []
|
|
},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Explorer link\n",
|
|
"display(Markdown(f'See: {easi.explorer}/products/{product}'))\n",
|
|
"\n",
|
|
"# EASI defaults\n",
|
|
"display(Markdown(f'#### Location: {easi.location}'))\n",
|
|
"latitude_range = easi.latitude\n",
|
|
"longitude_range = easi.longitude\n",
|
|
"time_range = easi.time\n",
|
|
"\n",
|
|
"# Or set your own latitude / longitude\n",
|
|
"# latitude_range = (-36.3, -35.8)\n",
|
|
"# longitude_range = (146.8, 147.3)\n",
|
|
"# time_range = ('2022-01-01', '2022-03-01')\n",
|
|
"\n",
|
|
"query = {\n",
|
|
" 'product': product, # Product name\n",
|
|
" 'x': longitude_range, # \"x\" axis bounds\n",
|
|
" 'y': latitude_range, # \"y\" axis bounds\n",
|
|
" 'time': time_range, # Any parsable date strings\n",
|
|
"}\n",
|
|
"\n",
|
|
"# Convenience function to display the selected area of interest\n",
|
|
"display_map(longitude_range, latitude_range)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {
|
|
"tags": []
|
|
},
|
|
"source": [
|
|
"#### Most common CRS\n",
|
|
"\n",
|
|
"Sentinel-2 datasets are stored with different coordinate reference systems (CRS), corresponding to the multiple UTM zones that are used for S2 L1B tiling. S2 measurement bands also have different resolutions (10 m, 20 m and 60 m). As such S2 queries need to include the following two query parameters:\n",
|
|
"\n",
|
|
"* `output_crs` - This sets a consistent CRS that all Sentinel-2 data will be reprojected to, irrespective of the UTM zone the individual image is stored in.\n",
|
|
"* `resolution` - This sets the resolution that all Sentinel-2 images will be resampled to. \n",
|
|
"\n",
|
|
"Use `mostcommon_crs()` to select a CRS. Adapted from https://github.com/GeoscienceAustralia/dea-notebooks/blob/develop/Tools/dea_tools/datahandling.py"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {
|
|
"tags": []
|
|
},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Most common CRS\n",
|
|
"native_crs = mostcommon_crs(dc, query)\n",
|
|
"print(f'Most common native CRS: {native_crs}')\n",
|
|
"\n",
|
|
"# Target xarray parameters\n",
|
|
"# - Select a set of measurements to load\n",
|
|
"# - output CRS and resolution\n",
|
|
"# - Usually we group input scenes on the same day to a single time layer (groupby)\n",
|
|
"# - Select a reasonable Dask chunk size (this should be adjusted depending on the\n",
|
|
"# spatial and resolution parameters you choose\n",
|
|
"load_params = {\n",
|
|
" 'measurements': ['blue', 'red', 'green', 'nir', 'scl'], # Selected measurement or alias names\n",
|
|
" 'output_crs': native_crs, # Target EPSG code\n",
|
|
" 'resolution': (-20, 20), # Target resolution\n",
|
|
" 'group_by': 'solar_day', # Scene grouping\n",
|
|
" 'dask_chunks': {'x': 2048, 'y': 2048}, # Dask chunks\n",
|
|
"}"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {
|
|
"tags": []
|
|
},
|
|
"source": [
|
|
"#### Apply the correct _offset_ to the source data\n",
|
|
"\n",
|
|
"We provide a convenience function to load `s2_l2a` data, apply the scale (and offset if required) and return an `xarray.Dataset`. This replaces `datacube.load()` for this product."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {
|
|
"tags": []
|
|
},
|
|
"outputs": [],
|
|
"source": [
|
|
"# The usual dc.load() may give an incorrect result for this product due to inconsistent upstream handling of any offset value\n",
|
|
"# data = dc.load(query | load_params)\n",
|
|
"\n",
|
|
"# Use the replacement \"dc.load()\" function for this product\n",
|
|
"data = load_s2l2a_with_offset(dc, query | load_params) # Combine the two dicts that contain our search and load parameters\n",
|
|
"display(xarray_object_size(data))\n",
|
|
"display(data)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# When happy with the shape and size of chunks, persist() the result\n",
|
|
"data = data.persist()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {
|
|
"tags": []
|
|
},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Optional\n",
|
|
"\n",
|
|
"# Create a simple plot to verify that the data look reasonable\n",
|
|
"# This will load and create images from the data, which may take a few minutes\n",
|
|
"# Here we limit this plot to the first few time layers.\n",
|
|
"\n",
|
|
"data.isel(time=slice(0, 4)).red.plot.imshow(col=\"time\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {
|
|
"tags": []
|
|
},
|
|
"source": [
|
|
"## Product definition\n",
|
|
"\n",
|
|
"The product definition contains details on the measurements and quality layers available in the product. Datacube provides convenience functions that return this information in `pandas DataFrames`.\n",
|
|
"\n",
|
|
"Use `list_measurements` to show the details for a product, and `masking.describe_variable_flags` to show the flag definitions."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Measurement definitions for the selected product\n",
|
|
"measurement_info = dc.list_measurements().loc[query['product']]\n",
|
|
"heading(f'Measurement table for product: {query[\"product\"]}')\n",
|
|
"display(measurement_info)\n",
|
|
"\n",
|
|
"# Flag definitions\n",
|
|
"flag_name = 'scl'\n",
|
|
"heading(f'Flag definition table for flag name: {flag_name}')\n",
|
|
"display(masking.describe_variable_flags(data[flag_name]))\n",
|
|
"\n",
|
|
"flags_def = masking.describe_variable_flags(data[flag_name]).loc['qa']['values']\n",
|
|
"display(flags_def)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Quality layer\n",
|
|
"\n",
|
|
"To visualise the **SCL** layer we create a custom color map following the colors used by ESA.\n",
|
|
"\n",
|
|
"Here we use `hvplot` to create a dynamic (zoom, scroll) image with an attached histogram. This example shows how a custom color map can be used with `hvplot`, as well as the [datashader aggregator](https://datashader.org/getting_started/Pipeline.html) `reductions.mode()`."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {
|
|
"tags": []
|
|
},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Make SCL image\n",
|
|
"# https://sentiwiki.copernicus.eu/web/s2-processing\n",
|
|
"# https://www.sentinel-hub.com/faq/how-get-s2a-scene-classification-sentinel-2/\n",
|
|
"\n",
|
|
"from bokeh.models.tickers import FixedTicker\n",
|
|
"\n",
|
|
"color_def = [\n",
|
|
" (0, '#000000', 'No data'), # black\n",
|
|
" (1, '#ff0004', 'Saturated or defective'), # red\n",
|
|
" (2, '#868686', 'Dark features or shadows'), # gray\n",
|
|
" (3, '#774c0b', 'Cloud shadows'), # brown\n",
|
|
" (4, '#10d32d', 'Vegetation'), # green\n",
|
|
" (5, '#ffff53', 'Not vegetated'), # yellow\n",
|
|
" (6, '#0000ff', 'Water'), # blue\n",
|
|
" (7, '#818181', 'Unclassified'), # medium gray\n",
|
|
" (8, '#c0c0c0', 'Cloud medium probability'), # light gray\n",
|
|
" (9, '#f2f2f2', 'cloud high probability'), # very light gray\n",
|
|
" (10, '#53fff9', 'Thin cirrus'), # cyan\n",
|
|
" (11, '#ff52ff', 'Snow or ice'), # pink\n",
|
|
"]\n",
|
|
"color_val = [x[0] for x in color_def]\n",
|
|
"color_hex = [x[1] for x in color_def]\n",
|
|
"color_txt = [f'{x[0]:2d}: {x[2]}' for x in color_def]\n",
|
|
"color_lim = (min(color_val), max(color_val) + 1)\n",
|
|
"bin_edges = color_val + [max(color_val) + 1]\n",
|
|
"bin_range = (color_val[0] + 0.5, color_val[-1] + 0.5) # No idea why (0.5,11.5) works and (0,11) or (0,12) do not\n",
|
|
"\n",
|
|
"# These options manipulate the color map and colorbar to show the categories for this product\n",
|
|
"options = {\n",
|
|
" 'title': f'Flag data for: {query[\"product\"]} ({flag_name})',\n",
|
|
" 'cmap': color_hex,\n",
|
|
" 'clim': color_lim,\n",
|
|
" 'color_levels': bin_edges,\n",
|
|
" 'colorbar': True,\n",
|
|
" 'width': 800,\n",
|
|
" 'height': 450,\n",
|
|
" 'aspect': 'equal',\n",
|
|
" 'tools': ['hover'],\n",
|
|
" 'colorbar_opts': {\n",
|
|
" 'major_label_overrides': dict(zip(color_val, color_txt)),\n",
|
|
" 'major_label_text_align': 'left',\n",
|
|
" 'ticker': FixedTicker(ticks=color_val),\n",
|
|
" },\n",
|
|
"}\n",
|
|
"\n",
|
|
"# Set the dataset CRS, if using hvplot's projection and coastlines options\n",
|
|
"# plot_crs = native_crs\n",
|
|
"# if plot_crs == 'epsg:4326':\n",
|
|
"# plot_crs = ccrs.PlateCarree()\n",
|
|
"\n",
|
|
"# Native data and coastline overlay:\n",
|
|
"# - Comment `crs`, `projection`, `coastline` to plot in native_crs coords\n",
|
|
"# TODO: Update the axis labels to 'longitude', 'latitude' if `coastline` is used\n",
|
|
"\n",
|
|
"quality_plot = data[flag_name].hvplot.image(\n",
|
|
" x = 'x', y = 'y', # Dataset x,y dimension names\n",
|
|
" rasterize = True, # Use Datashader\n",
|
|
" aggregator = reductions.mode(), # Datashader selects mode value, requires 'hv.Image'\n",
|
|
" precompute = True, # Datashader precomputes what it can\n",
|
|
" # crs = plot_crs, # Datset crs\n",
|
|
" # projection = ccrs.PlateCarree(), # Output projection (ccrs.PlateCarree() when coastline=True)\n",
|
|
" # coastline = '10m', # Coastline = '10m'/'50m'/'110m'\n",
|
|
").options(opts.Image(**options)).hist(bin_range = bin_range)\n",
|
|
"\n",
|
|
"# display(quality_plot)\n",
|
|
"# Optional: Change the default time slider to a dropdown list, https://stackoverflow.com/a/54912917\n",
|
|
"fig = pn.panel(quality_plot, widgets={'time': pn.widgets.Select}) # widget_location='top_left'\n",
|
|
"fig"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Create and apply a good quality pixel mask\n",
|
|
"\n",
|
|
"Select a set of flag values that represent \"good quality\" for your application. Here we select \"vegetation\", \"not vegetated\" and \"water\"; that is we exclude clouds and low-quality features.\n",
|
|
"\n",
|
|
"The **SCL** layer uses distinct integer values to represent each class. The datacube `enum_to_bool()` function creates a boolean mask layer corresponding to a set of category values (string names).\n",
|
|
"\n",
|
|
"Recall that *scale* and *offset* (if required) have already been applied by the `load_s2l2a_with_offset()` function."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {
|
|
"tags": []
|
|
},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Create Mask layer\n",
|
|
"\n",
|
|
"good_pixel_flags = [flags_def[str(i)] for i in [4, 5, 6]]\n",
|
|
"\n",
|
|
"good_pixel_mask = enum_to_bool(data[flag_name], good_pixel_flags)\n",
|
|
"display(good_pixel_mask) # -> DataArray. Type: bool\n",
|
|
"\n",
|
|
"# Apply good pixel mask (multiple layers)\n",
|
|
"good_data = data.where(good_pixel_mask).persist()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"### Calculate NDVI"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Create an NDVI layer, as useful way to visualise the data for differences in vegetation and land cover\n",
|
|
"# ndvi = (nir - red) / (nir + red)\n",
|
|
"\n",
|
|
"ndvi = (good_data.nir - good_data.red) / (good_data.nir + good_data.red)\n",
|
|
"ndvi.persist()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {
|
|
"tags": []
|
|
},
|
|
"source": [
|
|
"## Plot and browse the data\n",
|
|
"\n",
|
|
"There are numerous tools we can use to plot and interact with the data. Here we use `hvplot` again because it works well with dask and allows us to zoom and scroll quite efficiently. `Hvplot` uses [Datashader](https://datashader.org/getting_started/Pipeline.html) to process and render only the pixels that are required for the viewport.\n",
|
|
"\n",
|
|
"Various options can be changed such as the data layer, colour map and colour range."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Generate a plot\n",
|
|
"\n",
|
|
"options = {\n",
|
|
" 'title': f'{query[\"product\"]}',\n",
|
|
" 'width': 800,\n",
|
|
" 'height': 450,\n",
|
|
" 'aspect': 'equal',\n",
|
|
" 'cmap': cc.rainbow,\n",
|
|
" 'clim': (0, 1), # Limit the color range depending on the layer_name\n",
|
|
" 'colorbar': True,\n",
|
|
" 'tools': ['hover'],\n",
|
|
"}\n",
|
|
"\n",
|
|
"# Set the dataset CRS, if using hvplot's projection and coastlines options\n",
|
|
"# plot_crs = native_crs\n",
|
|
"# if plot_crs == 'epsg:4326':\n",
|
|
"# plot_crs = ccrs.PlateCarree()\n",
|
|
"\n",
|
|
"# Native data and coastline overlay:\n",
|
|
"# - Comment `crs`, `projection`, `coastline` to plot in native_crs coords\n",
|
|
"# TODO: Update the axis labels to 'longitude', 'latitude' if `coastline` is used\n",
|
|
"\n",
|
|
"layer_plot = ndvi.hvplot.image(\n",
|
|
" x = 'x', y = 'y', # Dataset x,y dimension names\n",
|
|
" rasterize = True, # Use Datashader\n",
|
|
" aggregator = reductions.mean(), # Datashader selects mean value\n",
|
|
" precompute = True, # Datashader precomputes what it can\n",
|
|
" # crs = plot_crs, # Dataset crs\n",
|
|
" # projection = ccrs.PlateCarree(), # Output projection (use ccrs.PlateCarree() when coastline=True)\n",
|
|
" # coastline='10m', # Coastline = '10m'/'50m'/'110m'\n",
|
|
").options(opts.Image(**options)).hist(bin_range = options['clim'])\n",
|
|
"\n",
|
|
"# display(layer_plot)\n",
|
|
"# Optional: Change the default time slider to a dropdown list, https://stackoverflow.com/a/54912917\n",
|
|
"fig = pn.panel(layer_plot, widgets={'time': pn.widgets.Select}) # widget_location='top_left'\n",
|
|
"display(fig)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": []
|
|
}
|
|
],
|
|
"metadata": {
|
|
"kernelspec": {
|
|
"display_name": "Python 3 (ipykernel)",
|
|
"language": "python",
|
|
"name": "python3"
|
|
},
|
|
"language_info": {
|
|
"codemirror_mode": {
|
|
"name": "ipython",
|
|
"version": 3
|
|
},
|
|
"file_extension": ".py",
|
|
"mimetype": "text/x-python",
|
|
"name": "python",
|
|
"nbconvert_exporter": "python",
|
|
"pygments_lexer": "ipython3",
|
|
"version": "3.12.3"
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 4
|
|
}
|