The Planetary Computer's Landsat dataset represents a global archive of Level-1 and Level-2 data from from Landsat Collection 2. The dataset is grouped into two STAC Collections:
landsat-c2-l2
for Level-2 datalandsat-c2-l1
for Level-1 dataThis notebook demonstrates the use of the Planetary Computer STAC API to query for Landsat data. We will start with an example using Level-2 data and follow with a similar example using Level-1 data.
This notebook works with or without an API key, but you will be given more permissive access to the data with an API key.
PC_SDK_SUBSCRIPTION_KEY
or use pc.settings.set_subscription_key(<YOUR API Key>)
.import pystac_client
import planetary_computer
import odc.stac
import matplotlib.pyplot as plt
from pystac.extensions.eo import EOExtension as eo
The datasets hosted by the Planetary Computer are available from Azure Blob Storage. We'll use pystac-client to search the Planetary Computer's STAC API for the subset of the data that we care about, and then we'll load the data directly from Azure Blob Storage. We'll specify a modifier
so that we can access the data stored in the Planetary Computer's private Blob Storage Containers. See Reading from the STAC API and Using tokens for data access for more.
catalog = pystac_client.Client.open(
"https://planetarycomputer.microsoft.com/api/stac/v1",
modifier=planetary_computer.sign_inplace,
)
This area is in Redmond, WA, USA, near Microsoft's main campus. We'll search all of 2021, limiting the results to those less than 10% cloudy.
bbox_of_interest = [-122.2751, 47.5469, -121.9613, 47.7458]
time_of_interest = "2021-01-01/2021-12-31"
search = catalog.search(
collections=["landsat-c2-l2"],
bbox=bbox_of_interest,
datetime=time_of_interest,
query={"eo:cloud_cover": {"lt": 10}},
)
items = search.item_collection()
print(f"Returned {len(items)} Items")
Returned 13 Items
Let's find the least cloudy of the bunch.
selected_item = min(items, key=lambda item: eo.ext(item).cloud_cover)
print(
f"Choosing {selected_item.id} from {selected_item.datetime.date()}"
+ f" with {selected_item.properties['eo:cloud_cover']}% cloud cover"
)
Choosing LC08_L2SP_046027_20210725_02_T1 from 2021-07-25 with 0.38% cloud cover
In addition to numerous metadata assets, each Electro-Optical (EO) band is a separate asset.
max_key_length = len(max(selected_item.assets, key=len))
for key, asset in selected_item.assets.items():
print(f"{key.rjust(max_key_length)}: {asset.title}")
qa: Surface Temperature Quality Assessment Band ang: Angle Coefficients File red: Red Band blue: Blue Band drad: Downwelled Radiance Band emis: Emissivity Band emsd: Emissivity Standard Deviation Band trad: Thermal Radiance Band urad: Upwelled Radiance Band atran: Atmospheric Transmittance Band cdist: Cloud Distance Band green: Green Band nir08: Near Infrared Band 0.8 lwir11: Surface Temperature Band swir16: Short-wave Infrared Band 1.6 swir22: Short-wave Infrared Band 2.2 coastal: Coastal/Aerosol Band mtl.txt: Product Metadata File (txt) mtl.xml: Product Metadata File (xml) mtl.json: Product Metadata File (json) qa_pixel: Pixel Quality Assessment Band qa_radsat: Radiometric Saturation and Terrain Occlusion Quality Assessment Band qa_aerosol: Aerosol Quality Assessment Band tilejson: TileJSON with default rendering rendered_preview: Rendered preview
We'll start by loading the red, green, and blue bands for our area of interest into an xarray dataset using odc-stac
. We will also load the nir08 band for use in computing an NDVI value in a later example. Note that we pass the sign
function from the planetary-computer package so that odc-stac
can supply the required Shared Access Signature tokens that are necessary to download the asset data.
bands_of_interest = ["nir08", "red", "green", "blue", "qa_pixel", "lwir11"]
data = odc.stac.stac_load(
[selected_item], bands=bands_of_interest, bbox=bbox_of_interest
).isel(time=0)
data
<xarray.Dataset> Dimensions: (y: 747, x: 795) Coordinates: * y (y) float64 5.289e+06 5.289e+06 ... 5.266e+06 5.266e+06 * x (x) float64 5.543e+05 5.544e+05 ... 5.781e+05 5.781e+05 spatial_ref int32 32610 time datetime64[ns] 2021-07-25T18:55:39.475647 Data variables: nir08 (y, x) uint16 7191 7187 7216 7237 ... 19509 21532 20871 20917 red (y, x) uint16 7172 7158 7193 7219 7246 ... 8067 8171 9372 8634 green (y, x) uint16 7498 7489 7518 7541 7570 ... 8706 8771 9580 9062 blue (y, x) uint16 7337 7343 7370 7433 7467 ... 8011 8068 8616 8252 qa_pixel (y, x) uint16 21952 21952 21952 21952 ... 21824 21824 21824 lwir11 (y, x) uint16 43477 43466 43464 43468 ... 44336 44431 44355
Now we'll convert our xarray Dataset to a DataArray and plot the RGB image. We set robust=True
in imshow
to avoid manual computation of the color limits (vmin and vmax) that is necessary for data not scaled to between 0-1 while also eliminating extreme values that can cause a washed out image.
fig, ax = plt.subplots(figsize=(10, 10))
data[["red", "green", "blue"]].to_array().plot.imshow(robust=True, ax=ax)
ax.set_title("Natural Color, Redmond, WA");
Landsat has several bands, and with them we can go beyond rendering natural color imagery; for example, the following code computes a Normalized Difference Vegetation Index (NDVI) using the near-infrared and red bands. Note that we convert the red and near infrared bands to a data type that can contain negative values; if this is not done, negative NDVI values will be incorrectly stored.
red = data["red"].astype("float")
nir = data["nir08"].astype("float")
ndvi = (nir - red) / (nir + red)
fig, ax = plt.subplots(figsize=(14, 10))
ndvi.plot.imshow(ax=ax, cmap="viridis")
ax.set_title("NDVI, Redmond, WA");
Landsat Collection 2 Level-2 includes assets from several different Platforms.
catalog.get_collection("landsat-c2-l2").summaries.to_dict()["platform"]
['landsat-4', 'landsat-5', 'landsat-7', 'landsat-8', 'landsat-9']
You might want to limit your search to a specific platform or platforms, to avoid the Landsat 7 Scan Line Corrector failure, for example. Use the "platform": {"in": ... }
query to select specific platforms.
search = catalog.search(
collections=["landsat-c2-l2"],
bbox=bbox_of_interest,
datetime=time_of_interest,
query={
"eo:cloud_cover": {"lt": 10},
"platform": {"in": ["landsat-8", "landsat-9"]},
},
)
items = search.get_all_items()
Landsat Collection 2 Level 2 includes measures of Surface Temperature. We'll look at the surface temperature band TIRS_B10
under the key lwir11
. The raw values are rescaled, so you should scale and offset the data before interpreting it. Use the metadata in the asset's raster_bands
to find the scale and offset values:
band_info = selected_item.assets["lwir11"].extra_fields["raster:bands"][0]
band_info
{'unit': 'kelvin', 'scale': 0.00341802, 'nodata': 0, 'offset': 149.0, 'data_type': 'uint16', 'spatial_resolution': 30}
To go from the raw values to Kelvin, we apply those values:
temperature = data["lwir11"].astype(float)
temperature *= band_info["scale"]
temperature += band_info["offset"]
temperature[:5, :5]
<xarray.DataArray 'lwir11' (y: 5, x: 5)> array([[297.60525554, 297.56765732, 297.56082128, 297.57449336, 297.58474742], [297.54031316, 297.46853474, 297.43435454, 297.44802662, 297.4958789 ], [297.50271494, 297.4275185 , 297.35232206, 297.342068 , 297.41384642], [297.49929692, 297.41384642, 297.342068 , 297.32839592, 297.37966622], [297.52664108, 297.46853474, 297.41726444, 297.41384642, 297.45144464]]) Coordinates: * y (y) float64 5.289e+06 5.289e+06 5.289e+06 5.288e+06 5.288e+06 * x (x) float64 5.543e+05 5.544e+05 5.544e+05 5.544e+05 5.544e+05 spatial_ref int32 32610 time datetime64[ns] 2021-07-25T18:55:39.475647 Attributes: nodata: 0
To convert from Kelvin to degrees, subtract 273.15.
celsius = temperature - 273.15
celsius.plot(cmap="magma", size=10);
Thus far we have worked with Landsat Collection 2 Level-2 data, which is processed to a consistent set of surface reflectance and surface temperature science products.
The Planetary Computer also hosts Collection 2 Level-1 data (top of atmosphere values) acquired by the Multispectral Scanner System (MSS) onboard Landsat 1 through 5. These data do not include a blue band, so a natural color image is not possible. We will plot a color infrared image from the nir08, red, and green bands instead.
As before, we use pystac-client to search over the landsat-c2-l1
collection. We'll use the same area of interest and return only those with less than 10% cloud cover.
search = catalog.search(
collections=["landsat-c2-l1"],
bbox=bbox_of_interest,
query={"eo:cloud_cover": {"lt": 10}},
)
items = search.item_collection()
print(f"Returned {len(items)} Items")
Returned 272 Items
Choose the least cloudy Item.
selected_item = min(items, key=lambda item: eo.ext(item).cloud_cover)
print(
f"Choosing {selected_item.id} from {selected_item.datetime.date()}"
+ f" with {selected_item.properties['eo:cloud_cover']}% cloud cover"
)
Choosing LM05_L1TP_046027_20121004_02_T2 from 2012-10-04 with 0.0% cloud cover
MSS data has four EO bands assets and a number of metadata files and bands.
max_key_length = len(max(selected_item.assets, key=len))
for key, asset in selected_item.assets.items():
print(f"{key.rjust(max_key_length)}: {asset.title}")
red: Red Band green: Green Band nir08: Near Infrared Band 0.8 nir09: Near Infrared Band 0.9 mtl.txt: Product Metadata File (txt) mtl.xml: Product Metadata File (xml) mtl.json: Product Metadata File (json) qa_pixel: Pixel Quality Assessment Band qa_radsat: Radiometric Saturation and Dropped Pixel Quality Assessment Band tilejson: TileJSON with default rendering rendered_preview: Rendered preview
We'll use odc-stac
to load only the EO bands and area we are interested in.
bands_of_interest = ["nir08", "red", "green"]
data = odc.stac.stac_load(
[selected_item], bands=bands_of_interest, bbox=bbox_of_interest
).isel(time=0)
cir = data.to_array()
fig, ax = plt.subplots(figsize=(10, 10))
cir.plot.imshow(robust=True, ax=ax)
ax.set_title("Color Infrared, Redmond, WA");