{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "## How to use the Radiant MLHub API to browse and download the LandCoverNet dataset\n", "\n", "Radiant MLHub Logo\n", "\n", "This Jupyter notebook, which you may copy and adapt for any use, shows basic examples of how to use the API to download labels and source imagery for the LandCoverNet dataset. Full documentation for the API is available at [docs.mlhub.earth](http://docs.mlhub.earth).\n", "\n", "We'll show you how to set up your authorization, list collection properties, and retrieve the items (the data contained within them) from those collections.\n", "\n", "Each item in our collection is explained in json format compliant with STAC label extension definition." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Dataset Citation\n", "\n", "Alemohammad S.H., Ballantyne A., Bromberg Gaber Y., Booth K., Nakanuku-Diggs L., & Miglarese A.H. (2020) \"LandCoverNet: A Global Land Cover Classification Training Dataset\", Version 1.0, Radiant MLHub. \\[Date Accessed\\] [https://doi.org/10.34911/rdnt.d2ce8i](https://doi.org/10.34911/rdnt.d2ce8i)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Authentication\n", "\n", "Access to the Radiant MLHub API requires an API key. To get your API key, go to [mlhub.earth](https://mlhub.earth/) and click the \"Sign in / Register\" button in the top right to log in. If you have not used Radiant MLHub before, you will need to sign up and create a new account; otherwise, just sign in. Once you have signed in, click on your user avatar in the top right and select the \"Settings & API keys\" from the dropdown menu.\n", "\n", "In the **API Keys** section of this page, you will be able to create new API key(s). *Do not share* your API key with others as this may pose a security risk.\n", "\n", "Next, we will create a `MLHUB_API_KEY` variable that `pystac-client` will use later use to add our API key to all requests:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdin", "output_type": "stream", "text": [ "MLHub API Key: ········\n" ] } ], "source": [ "import getpass\n", "\n", "MLHUB_API_KEY = getpass.getpass(prompt=\"MLHub API Key: \")\n", "MLHUB_ROOT_URL = \"https://api.radiant.earth/mlhub/v1\"" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "import os\n", "import requests\n", "import shutil\n", "import tempfile\n", "from pathlib import Path\n", "import itertools as it\n", "from urllib.parse import urljoin\n", "\n", "from pystac import Item\n", "from pystac.extensions.eo import EOExtension\n", "from pystac.extensions.label import LabelRelType\n", "from pystac.extensions.scientific import ScientificExtension\n", "from pystac_client import Client\n", "\n", "client = Client.open(\n", " MLHUB_ROOT_URL, parameters={\"key\": MLHUB_API_KEY}, ignore_conformance=True\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next, we will create a custom `requests.Session` instance that will automatically include our API key in requests." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "class MLHubSession(requests.Session):\n", " def __init__(self, *args, api_key=None, **kwargs):\n", " super().__init__(*args, **kwargs)\n", " self.params.update({\"key\": api_key})\n", "\n", " def request(self, method, url, *args, **kwargs):\n", " url_prefix = MLHUB_ROOT_URL.rstrip(\"/\") + \"/\"\n", " url = urljoin(url_prefix, url)\n", " return super().request(method, url, *args, **kwargs)\n", "\n", "\n", "session = MLHubSession(api_key=MLHUB_API_KEY)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Listing Collection Properties\n", "\n", "The following cell makes a request to the API for the metadata of the LandCoverNet labels collection and prints out a few important properties." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Description: LandCoverNet Africa Labels\n", "License: CC-BY-4.0\n", "DOI: 10.34911/rdnt.d2ce8i\n", "Citation: Alemohammad S.H., Ballantyne A., Bromberg Gaber Y., Booth K., Nakanuku-Diggs L., & Miglarese A.H. (2020) \"LandCoverNet: A Global Land Cover Classification Training Dataset\", Version 1.0, Radiant MLHub. [Date Accessed] https://doi.org/10.34911/rdnt.d2ce8i\n" ] } ], "source": [ "collection_id = \"ref_landcovernet_af_v1_labels\"\n", "\n", "collection = client.get_collection(collection_id)\n", "collection_sci_ext = ScientificExtension.ext(collection)\n", "print(f\"Description: {collection.description}\")\n", "print(f\"License: {collection.license}\")\n", "print(f\"DOI: {collection_sci_ext.doi}\")\n", "print(f\"Citation: {collection_sci_ext.citation}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Finding Possible Land Cover Labels\n", "\n", "Each item has a property which lists all of the possible land cover types in the dataset and which ones are present in the current item.\n", "The code below prints out those land cover types present in the dataset and we will reference these later in the notebook when we filter downloads." ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0: No Data\n", "1: Water\n", "2: Artificial Bareground\n", "3: Natural Bareground\n", "4: Permanent Snow/Ice\n", "5: Woody Vegetation\n", "6: Cultivated Vegetation\n", "7: (Semi) Natural Vegetation\n" ] } ], "source": [ "item_search = client.search(collections=[collection_id])\n", "\n", "first_item = next(item_search.get_items())\n", "labels_asset = first_item.get_assets()[\"labels\"]\n", "classification_labels = labels_asset.to_dict()[\"file:values\"]\n", "\n", "for cl in classification_labels:\n", " classification_id = cl[\"value\"][0]\n", " classification_label = cl[\"summary\"]\n", " print(f\"{classification_id}: {classification_label}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Downloading Assets\n", "\n", "For this exercise, we will find the first Item that contains labels with the `\"Woody Vegetation\"` class and download the label asset for this Item. We will then follow the link to the source imagery for these labels and download the RGB band assets for that imagery.\n", "\n", "First, we create a temporary directory into which we will download the assets." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "tmp_dir = tempfile.mkdtemp()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Downloading Labels\n", "\n", "Next, we search for Items in our collection and inspect the `\"label:overviews\"` property to find an item that contains `\"Woody Vegetation\"`." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Item ID: ref_landcovernet_af_v1_labels_38PKT_29\n", "Classification labels:\n", "- Artificial Bareground\n", "- Natural Bareground\n", "- Woody Vegetation\n", "- (Semi) Natural Vegetation\n", "Assets:\n", "- labels\n", "- source_dates\n", "- documentation\n" ] } ], "source": [ "for item in item_search.get_items():\n", " labels_count = item.properties[\"label:overviews\"][0][\"counts\"]\n", " item_labels = [lc[\"name\"] for lc in labels_count]\n", " if \"Woody Vegetation\" in item_labels:\n", " break\n", "\n", "print(f\"Item ID: {item.id}\")\n", "print(\"Classification labels:\")\n", "for label in item_labels:\n", " print(f\"- {label}\")\n", "print(\"Assets:\")\n", "for asset_key in item.assets.keys():\n", " print(f\"- {asset_key}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can see that this Item has a `\"labels\"` asset, which contains the segmentation labels for this dataset. We can download these labels using the `\"href\"` property of the asset." ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Downloading labels from https://api.radiant.earth/mlhub/v1/download/gAAAAABjkjvZqxDgYO2BPkIozpU4883Ka8zTLJY3A9tFIzPOTrI5RzX30f39lBZGrGnpypnRyayQwQSZlNXGYr-XuYq5d39xNyMZLruOZE0v9p3HXIcfMYPBKHOOATdXMF5aB10s6b-JSKkshhkgy4v8meZHuJc7YTdHGeDKIwL18Z6HwkMBpzMDNwQbvm87ikYBguUSwXJs-FfbP-ykEwrafHMJroGK2eRuMtTYVhOGKpC9XD1l-t_CZVTLrahcLXiZ4eXjcOixRb1XNSSE1JDK2t8D-MGJacrm5uhS-xFQPPEUtEtnu9U=\n" ] } ], "source": [ "labels_path = os.path.join(tmp_dir, \"labels.tif\")\n", "labels_href = item.assets[\"labels\"].href\n", "print(f\"Downloading labels from {labels_href}\")\n", "\n", "response = requests.get(labels_href, allow_redirects=True)\n", "with open(labels_path, \"wb\") as dst:\n", " dst.write(response.content)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Downloading Source Imagery\n", "\n", "Let's find the the source imagery associated with those labels by examining the Item links (source imagery links will have a `\"rel\"` type of `\"source\"`." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Source Imagery Links: 167\n", "- https://api.radiant.earth/mlhub/v1/collections/ref_landcovernet_af_v1_source_sentinel_2/items/ref_landcovernet_af_v1_source_sentinel_2_38PKT_29_20180101\n", "- https://api.radiant.earth/mlhub/v1/collections/ref_landcovernet_af_v1_source_sentinel_2/items/ref_landcovernet_af_v1_source_sentinel_2_38PKT_29_20180106\n", "- https://api.radiant.earth/mlhub/v1/collections/ref_landcovernet_af_v1_source_sentinel_2/items/ref_landcovernet_af_v1_source_sentinel_2_38PKT_29_20180111\n", "- https://api.radiant.earth/mlhub/v1/collections/ref_landcovernet_af_v1_source_sentinel_2/items/ref_landcovernet_af_v1_source_sentinel_2_38PKT_29_20180116\n", "- https://api.radiant.earth/mlhub/v1/collections/ref_landcovernet_af_v1_source_sentinel_2/items/ref_landcovernet_af_v1_source_sentinel_2_38PKT_29_20180121\n", "- https://api.radiant.earth/mlhub/v1/collections/ref_landcovernet_af_v1_source_sentinel_2/items/ref_landcovernet_af_v1_source_sentinel_2_38PKT_29_20180126\n", "- https://api.radiant.earth/mlhub/v1/collections/ref_landcovernet_af_v1_source_sentinel_2/items/ref_landcovernet_af_v1_source_sentinel_2_38PKT_29_20180131\n", "- https://api.radiant.earth/mlhub/v1/collections/ref_landcovernet_af_v1_source_sentinel_2/items/ref_landcovernet_af_v1_source_sentinel_2_38PKT_29_20180205\n", "- https://api.radiant.earth/mlhub/v1/collections/ref_landcovernet_af_v1_source_sentinel_2/items/ref_landcovernet_af_v1_source_sentinel_2_38PKT_29_20180210\n", "- https://api.radiant.earth/mlhub/v1/collections/ref_landcovernet_af_v1_source_sentinel_2/items/ref_landcovernet_af_v1_source_sentinel_2_38PKT_29_20180215\n", "...\n" ] } ], "source": [ "source_imagery_links = item.get_links(rel=LabelRelType.SOURCE)\n", "links_limit = 10\n", "print(f\"Source Imagery Links: {len(source_imagery_links)}\")\n", "for link in it.islice(source_imagery_links, links_limit):\n", " print(f\"- {link.href}\")\n", "if len(source_imagery_links) > links_limit:\n", " print(\"...\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can see that there are 167 different source images that can be associated with these labels. Let's grab the STAC Item for the first one so we can download the RGB band assets for that image. Because the Radiant MLHub API requires authentication to retrieve Items, we cannot use the standard PySTAC methods for resolving STAC Objects. Instead, we will use the custom `requests.Session` instance we created above." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Item ID: ref_landcovernet_af_v1_source_sentinel_2_38PKT_29_20180101\n", "Assets:\n", "- Asset Key: B01\n", " Bands:Coastal Aerosol\n", "- Asset Key: B02\n", " Bands:Blue\n", "- Asset Key: B03\n", " Bands:Green\n", "- Asset Key: B04\n", " Bands:Red\n", "- Asset Key: B05\n", " Bands:Vegetation Red Edge\n", "- Asset Key: B06\n", " Bands:Vegetation Red Edge\n", "- Asset Key: B07\n", " Bands:Vegetation Red Edge\n", "- Asset Key: B08\n", " Bands:NIR\n", "- Asset Key: B09\n", " Bands:Water Vapour\n", "- Asset Key: B11\n", " Bands:SWIR\n", "- Asset Key: B12\n", " Bands:SWIR\n", "- Asset Key: B8A\n", " Bands:Narrow NIR\n", "- Asset Key: CLD\n", " Bands:Cloud Mask\n", "- Asset Key: SCL\n", " Bands:Scene Classification Layer\n" ] } ], "source": [ "image_link = source_imagery_links[0]\n", "\n", "response = session.get(image_link.href)\n", "\n", "image_item = Item.from_dict(response.json())\n", "print(f\"Item ID: {image_item.id}\")\n", "print(\"Assets:\")\n", "for asset_key, asset in image_item.assets.items():\n", " print(f\"- Asset Key: {asset_key}\")\n", " asset_eo_ext = EOExtension.ext(asset)\n", " if asset_eo_ext.bands is not None:\n", " band_names = \", \".join(band.common_name for band in asset_eo_ext.bands)\n", " print(f\" Bands:{band_names}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Since we are interested in the RGB bands for this image, we will download the `\"B04\"`, `\"B03\"`, and `\"B02\"` assets." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "for asset_key in {\"B04\", \"B03\", \"B02\"}:\n", " file_path = os.path.join(tmp_dir, f\"image-{asset_key}\")\n", " asset = image_item.assets[asset_key]\n", " response = session.get(asset.href, allow_redirects=True)\n", " with open(file_path, \"wb\") as dst:\n", " dst.write(response.content)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's confirm that our downloads are all in the temporary directory." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['image-B02', 'labels.tif', 'image-B03', 'image-B04']" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "os.listdir(tmp_dir)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Download Collection Archives\n", "\n", "If you are interested in downloading all label and/or source imagery assets for this dataset, you can do so using the `/archive/{collection_id}` endpoint documented [here](https://docs.mlhub.earth/#operation/Download_Archive_archive__collection_id__get). You can see an example of using a custom `requests.Session` instance to download this archive` in the [\"Using the Radiant MLHub API\" tutorial](./using-radiant-mlhub-api.ipynb#Download-Data-Archives). Before downloading the archive, you can also use the `/archive/{collection_id}/info` endpoint to determine the size of the archive file." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'collection': 'ref_landcovernet_af_v1_labels',\n", " 'dataset': 'ref_landcovernet_af_v1',\n", " 'size': 20684609,\n", " 'types': ['labels']}" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "response = session.get(f\"/archive/{collection_id}/info\")\n", "response.json()" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "archive_path = os.path.join(tmp_dir, f\"{collection_id}.tar.gz\")\n", "response = session.get(f\"/archive/{collection_id}\", allow_redirects=True)\n", "with open(archive_path, \"wb\") as dst:\n", " dst.write(response.content)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's check that our archive file was successfully downloaded." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Archive Exists?: True\n", "Archive Size: 20684609\n" ] } ], "source": [ "archive_path_obj = Path(archive_path)\n", "print(f\"Archive Exists?: {archive_path_obj.exists()}\")\n", "print(f\"Archive Size: {archive_path_obj.stat().st_size}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "It does, and the size matches what we expected from the info endpoint! " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Clean Up\n", "\n", "Finally, we remove the temporary directory and its contents." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [], "source": [ "shutil.rmtree(tmp_dir)" ] } ], "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.10.6" } }, "nbformat": 4, "nbformat_minor": 4 }