Systematic spatial scene search¶
Conducting a systematic spatial scene search over a large region using pyeodh
- Other useful documentation: https://docs.eodatahub.org.uk/Analysts/APIs/api-examples/#spatial-search
- EODH data catalogue link: https://eodatahub.org.uk/static-apps/sg-rc-ui/prod/index.html#/
- Define a set of grid squares over the regional Area Of Interest (AOI), which will be used for a programmatic, systematic scene search in this script. You will need the coordinates of the bounding box for that grid square
- Briefly browse the EODH data catalogue to discover what data collections are available to search
📦 Import package dependencies¶
!pip install folium
Collecting folium Using cached folium-0.20.0-py2.py3-none-any.whl.metadata (4.2 kB) Collecting branca>=0.6.0 (from folium) Using cached branca-0.8.2-py3-none-any.whl.metadata (1.7 kB) Requirement already satisfied: jinja2>=2.9 in /opt/conda/lib/python3.13/site-packages (from folium) (3.1.6) Requirement already satisfied: numpy in /opt/conda/lib/python3.13/site-packages (from folium) (2.3.5) Requirement already satisfied: requests in /opt/conda/lib/python3.13/site-packages (from folium) (2.32.5) Requirement already satisfied: xyzservices in /opt/conda/lib/python3.13/site-packages (from folium) (2025.11.0) Requirement already satisfied: MarkupSafe>=2.0 in /opt/conda/lib/python3.13/site-packages (from jinja2>=2.9->folium) (3.0.3) Requirement already satisfied: charset_normalizer<4,>=2 in /opt/conda/lib/python3.13/site-packages (from requests->folium) (3.4.4) Requirement already satisfied: idna<4,>=2.5 in /opt/conda/lib/python3.13/site-packages (from requests->folium) (3.11) Requirement already satisfied: urllib3<3,>=1.21.1 in /opt/conda/lib/python3.13/site-packages (from requests->folium) (1.26.20) Requirement already satisfied: certifi>=2017.4.17 in /opt/conda/lib/python3.13/site-packages (from requests->folium) (2026.1.4) Using cached folium-0.20.0-py2.py3-none-any.whl (113 kB) Using cached branca-0.8.2-py3-none-any.whl (26 kB) Installing collected packages: branca, folium ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2/2 [folium] Successfully installed branca-0.8.2 folium-0.20.0
import requests
import pyeodh
import shapely as sh
import folium
from shapely.geometry import Point
import pandas as pd
from PIL import Image
from io import BytesIO
import matplotlib.pyplot as plt
import ipywidgets as widgets
🌐 Search area grid square - INPUT HERE¶
For a systematic search, define a set of search grid squares over the full AOI. The coordinates of each square can be input here to retreive the scenes that have footprints which overlap that grid square and match the search criteria.
# Define grid square for search using bounding box coordinates - a generic example is defined below
search_aoi = {
"type": "Polygon",
"coordinates": [
[
[47.649078, 28.966096],
[47.649078, 29.753648],
[48.613129, 29.753648],
[48.613129, 28.966096],
[47.649078, 28.966096]
]
],
}
🗺️ Show on map
coords_lonlat = search_aoi["coordinates"][0]
coords_latlon = [[lat, lon] for lon, lat in coords_lonlat]
# Calculate center
lats = [lat for lat, lon in coords_latlon]
lons = [lon for lat, lon in coords_latlon]
center = [sum(lats) / len(lats), sum(lons) / len(lons)]
m = folium.Map(location=center, zoom_start=7)
folium.Polygon(
locations=coords_latlon,
color="blue",
fill=True,
fill_opacity=0.3
).add_to(m)
m
🔗 Connect to EODH data catalogue¶
Using the pyeodh, we connect to the EODH STAC catalogues which contain the data collections we want to search.
client = pyeodh.Client(
base_url="https://eodatahub.org.uk", # you could connect to workspace by adding here: username='workspace-name', token='workspace_api_key', but this is not needed to search for scenes in the data catalogue
).get_catalog_service()
Print the list of catalogues, which contain the data collections.
# get catalogs
for c in client.get_catalogs():
print(c._pystac_object.self_href.removeprefix("https://eodatahub.org.uk/api/catalogue/stac/catalogs/"))
commercial/catalogs/airbus user/catalogs/npl/catalogs/processing-results/catalogs/qa-workflow/catalogs/airbus_phr_qa user/catalogs/tjellicoetpzuk/catalogs/processing-results/catalogs/snuggs/catalogs/catalog public/catalogs/ceda-stac-catalogue commercial user/catalogs/npl commercial/catalogs/open-cosmos commercial/catalogs/planet user/catalogs/npl/catalogs/processing-results/catalogs/qa-workflow/catalogs/planet_psscene_qa user/catalogs/npl/catalogs/processing-results
🛰️ Explore the EODH collections¶
Print a list of data collections which contain the text string 'airbus'
# get collections
for index, collect in enumerate(
[c for c in client.get_collections() if "airbus" in c.id],
start=1):
print(f"{index} -- {collect.id}")
1 -- airbus_spot_data 2 -- airbus_sar_data 3 -- airbus_pneo_data 4 -- airbus_phr_data
# get collections
for index, collect in enumerate(
[c for c in client.get_collections() if "sentinel-2-c1-l2a" in c.id],
start=1):
print(f"{index} -- {collect.id}")
Print descriptions and extents of the collections
# airbus pneo/phr
pneo_cat = client.get_catalog("commercial/catalogs/airbus").get_collection('airbus_pneo_data')
print("id: ", pneo_cat.id)
print("title: ", pneo_cat.title)
print("description: ", pneo_cat.description)
print("")
print(
"DATASET TEMPORAL EXTENT: ",
[str(d) for d in pneo_cat.extent.temporal.intervals[0]],
)
id: airbus_pneo_data title: Airbus PNEO Data description: Pléiades Neo is Airbus' industry-leading very high-resolution optical constellation of two identical satellites phased at 180° from each other. The constellation provides continuity for the Pléiades mission, with enhanced performance in terms of accuracy, reactivity and frequency. Pléiades Neo provides users with 30 cm native resolution panchromatic imagery, and six 1.2 m multispectral bands, offering the best of very high-resolution optical imagery for an unprecedented level of geospatial services. DATASET TEMPORAL EXTENT: ['2021-08-13 00:11:11.683000+00:00', '2026-06-20 02:15:16.107000+00:00']
# planet planetscope
ps_cat = client.get_catalog("commercial/catalogs/planet").get_collection('PSScene')
print("id: ", ps_cat.id)
print("title: ", ps_cat.title)
print("description: ", ps_cat.description)
print("")
print(
"DATASET TEMPORAL EXTENT: ",
[str(d) for d in ps_cat.extent.temporal.intervals[0]],
)
id: PSScene
title: PlanetScope Scene
description: PlanetScope, operated by Planet, is a constellation of approximately 130 satellites, able to image the entire land surface of the Earth every day (a daily collection capacity of 200 million km²/day). PlanetScope images are approximately 3 meters per pixel resolution. The PlanetScope satellite constellation consists of multiple launches ("flocks") of Dove satellites. PlanetScope Scenes are either Four band (RGBNIR), or Eight band (RGBNIR, red edge, green I, yellow and coastal blue), depending on which generation of satellite the product was acquired by. On-orbit capacity is constantly improving in capability and quantity, with technology improvements deployed at a rapid pace.
DATASET TEMPORAL EXTENT: ['2014-06-29 10:03:58.179416+00:00', '2026-06-26 05:26:45.858381+00:00']
# open cosmos
oc_cat = client.get_catalog("commercial/catalogs/open-cosmos").get_collection('platero-l1c-cogs')
print("id: ", oc_cat.id)
print("title: ", oc_cat.title)
print("description: ", oc_cat.description)
print("")
print(
"DATASET TEMPORAL EXTENT: ",
[str(d) for d in oc_cat.extent.temporal.intervals[0]],
)
id: platero-l1c-cogs title: Open Cosmos Platero L1C description: Platero L1C images DATASET TEMPORAL EXTENT: ['2024-02-01 11:04:49+00:00', '2026-06-18 18:52:57+00:00']
🔍 Run a data search¶
Define the criteria for the search, and count the number of scenes matching the criteria. Switch out the catalogue paths based on which catalog you want to search:
commercial/catalogs/planetcommercial/catalogs/airbuscommercial/catalogs/open-cosmosetc.
Switch out the collection id for the collection you want to search:
airbus_spot_dataairbus_sar_dataairbus_pneo_dataairbus_phr_dataPSSceneSkySatCollecthammer-l1c-cogsplatero-l1c-cogsaccenture-1-l1c-cogsmantis-l1d-cogsmenut-l1a-cogsmenut-l1b-cogsmenut-l1c-cogsetc.
# search criteria - cloud cover filter not applied, could be optionally added
# we can also search by properties, such as date range
datasearch = client.search(
collections=["airbus_pneo_data"],
catalog_paths=["commercial/catalogs/airbus"],
start_datetime="2026-02-22", # Search for scenes after 22nd Feb 2026
end_datetime="2026-06-27", # Search for scenes before 27th June 2026
intersects=search_aoi, # This is the grid square defined at the start of the script
)
# We can then count the number of items returned by the search
total_items = sum(1 for _ in datasearch)
print(f"Total items: {total_items}")
Total items: 11
📸 Fetch image thumbnails and details¶
List all scene ids returned by the search. Get the external_quicklook asset which is a low-resolution thumbnail of the full scene.
# run search to get scene ids list
# Print each item id and associated date
for i, myitem in enumerate(datasearch, start=1):
print(f"{i}: {myitem.id}: {myitem.properties}: {myitem.assets["external_quicklook"].href}")
1: ACQ_PNEO3_07301406093648: {'datetime': '2026-06-14T07:25:39.467000Z'}: https://access.foundation.api.oneatlas.airbus.com/api/v1/items/d23d07a6-58f2-4773-90e9-5e7441dbd5db/quicklook
2: ACQ_PNEO4_06925104956840: {'datetime': '2026-03-31T07:35:08.746000Z'}: https://access.foundation.api.oneatlas.airbus.com/api/v1/items/e43f40c2-31af-4bdc-94db-a431e061ad5e/quicklook
3: ACQ_PNEO4_06902904719950: {'datetime': '2026-03-16T07:45:18.593000Z'}: https://access.foundation.api.oneatlas.airbus.com/api/v1/items/afc3d723-ad72-4cf1-a8d0-ed98ac1638f9/quicklook
4: ACQ_PNEO3_06911804606013: {'datetime': '2026-03-09T08:00:10.227000Z'}: https://access.foundation.api.oneatlas.airbus.com/api/v1/items/6009410e-0146-418e-a4ed-2c078822e3ca/quicklook
5: ACQ_PNEO3_06910304589014: {'datetime': '2026-03-08T07:39:57.399000Z'}: https://access.foundation.api.oneatlas.airbus.com/api/v1/items/bea3b3cb-ea92-42a4-a2de-ee1c2cd0b26d/quicklook
6: ACQ_PNEO4_06819204483127: {'datetime': '2026-03-01T07:57:01.615000Z'}: https://access.foundation.api.oneatlas.airbus.com/api/v1/items/26b9e695-ec98-433e-908c-d0e2b553704b/quicklook
7: ACQ_PNEO4_06819204483126: {'datetime': '2026-03-01T07:56:51.090000Z'}: https://access.foundation.api.oneatlas.airbus.com/api/v1/items/30655a35-ea26-444d-9b7b-fe68d30db633/quicklook
8: ACQ_PNEO4_06819204483123: {'datetime': '2026-03-01T07:55:29.997000Z'}: https://access.foundation.api.oneatlas.airbus.com/api/v1/items/e1884eb3-36a2-478a-9b5d-9d45efa155b7/quicklook
9: ACQ_PNEO4_06816204454347: {'datetime': '2026-02-27T07:19:57.104000Z'}: https://access.foundation.api.oneatlas.airbus.com/api/v1/items/e3f75cbb-e611-432d-8f4a-0294cabef36b/quicklook
10: ACQ_PNEO4_06810304396569: {'datetime': '2026-02-23T07:41:29.187000Z'}: https://access.foundation.api.oneatlas.airbus.com/api/v1/items/a67e28d2-395b-46bc-a16e-57d9edb92c32/quicklook
11: ACQ_PNEO4_06808804382403: {'datetime': '2026-02-22T07:22:37.052000Z'}: https://access.foundation.api.oneatlas.airbus.com/api/v1/items/cb99edf5-4cc5-4017-a3cd-415c0e2ede7d/quicklook
Add the scene search information into an array
# combine as CSV and export csv
values = [[None, None, None, None]]
for i, myitem in enumerate(datasearch, start=1):
values == values.append([i, myitem.id, myitem.properties["datetime"], myitem.assets["external_quicklook"].href])
# Remove empty first row from the array
values.pop(0)
print(values)
[[1, 'ACQ_PNEO3_07301406093648', '2026-06-14T07:25:39.467000Z', 'https://access.foundation.api.oneatlas.airbus.com/api/v1/items/d23d07a6-58f2-4773-90e9-5e7441dbd5db/quicklook'], [2, 'ACQ_PNEO4_06925104956840', '2026-03-31T07:35:08.746000Z', 'https://access.foundation.api.oneatlas.airbus.com/api/v1/items/e43f40c2-31af-4bdc-94db-a431e061ad5e/quicklook'], [3, 'ACQ_PNEO4_06902904719950', '2026-03-16T07:45:18.593000Z', 'https://access.foundation.api.oneatlas.airbus.com/api/v1/items/afc3d723-ad72-4cf1-a8d0-ed98ac1638f9/quicklook'], [4, 'ACQ_PNEO3_06911804606013', '2026-03-09T08:00:10.227000Z', 'https://access.foundation.api.oneatlas.airbus.com/api/v1/items/6009410e-0146-418e-a4ed-2c078822e3ca/quicklook'], [5, 'ACQ_PNEO3_06910304589014', '2026-03-08T07:39:57.399000Z', 'https://access.foundation.api.oneatlas.airbus.com/api/v1/items/bea3b3cb-ea92-42a4-a2de-ee1c2cd0b26d/quicklook'], [6, 'ACQ_PNEO4_06819204483127', '2026-03-01T07:57:01.615000Z', 'https://access.foundation.api.oneatlas.airbus.com/api/v1/items/26b9e695-ec98-433e-908c-d0e2b553704b/quicklook'], [7, 'ACQ_PNEO4_06819204483126', '2026-03-01T07:56:51.090000Z', 'https://access.foundation.api.oneatlas.airbus.com/api/v1/items/30655a35-ea26-444d-9b7b-fe68d30db633/quicklook'], [8, 'ACQ_PNEO4_06819204483123', '2026-03-01T07:55:29.997000Z', 'https://access.foundation.api.oneatlas.airbus.com/api/v1/items/e1884eb3-36a2-478a-9b5d-9d45efa155b7/quicklook'], [9, 'ACQ_PNEO4_06816204454347', '2026-02-27T07:19:57.104000Z', 'https://access.foundation.api.oneatlas.airbus.com/api/v1/items/e3f75cbb-e611-432d-8f4a-0294cabef36b/quicklook'], [10, 'ACQ_PNEO4_06810304396569', '2026-02-23T07:41:29.187000Z', 'https://access.foundation.api.oneatlas.airbus.com/api/v1/items/a67e28d2-395b-46bc-a16e-57d9edb92c32/quicklook'], [11, 'ACQ_PNEO4_06808804382403', '2026-02-22T07:22:37.052000Z', 'https://access.foundation.api.oneatlas.airbus.com/api/v1/items/cb99edf5-4cc5-4017-a3cd-415c0e2ede7d/quicklook']]
Create a data frame from the scene search¶
id_no = [row[0] for row in values]
scene_id = [row[1] for row in values]
datetime = [row[2] for row in values]
thumbnail = [row[3] for row in values]
# Create table (DataFrame)
df = pd.DataFrame({
"ID": id_no,
"Scene ID": scene_id,
"Acquisition Datetime": datetime,
"Thumbnail": thumbnail
})
print(df)
ID Scene ID Acquisition Datetime \
0 1 ACQ_PNEO3_07301406093648 2026-06-14T07:25:39.467000Z
1 2 ACQ_PNEO4_06925104956840 2026-03-31T07:35:08.746000Z
2 3 ACQ_PNEO4_06902904719950 2026-03-16T07:45:18.593000Z
3 4 ACQ_PNEO3_06911804606013 2026-03-09T08:00:10.227000Z
4 5 ACQ_PNEO3_06910304589014 2026-03-08T07:39:57.399000Z
5 6 ACQ_PNEO4_06819204483127 2026-03-01T07:57:01.615000Z
6 7 ACQ_PNEO4_06819204483126 2026-03-01T07:56:51.090000Z
7 8 ACQ_PNEO4_06819204483123 2026-03-01T07:55:29.997000Z
8 9 ACQ_PNEO4_06816204454347 2026-02-27T07:19:57.104000Z
9 10 ACQ_PNEO4_06810304396569 2026-02-23T07:41:29.187000Z
10 11 ACQ_PNEO4_06808804382403 2026-02-22T07:22:37.052000Z
Thumbnail
0 https://access.foundation.api.oneatlas.airbus....
1 https://access.foundation.api.oneatlas.airbus....
2 https://access.foundation.api.oneatlas.airbus....
3 https://access.foundation.api.oneatlas.airbus....
4 https://access.foundation.api.oneatlas.airbus....
5 https://access.foundation.api.oneatlas.airbus....
6 https://access.foundation.api.oneatlas.airbus....
7 https://access.foundation.api.oneatlas.airbus....
8 https://access.foundation.api.oneatlas.airbus....
9 https://access.foundation.api.oneatlas.airbus....
10 https://access.foundation.api.oneatlas.airbus....
🗂️ Export a table of search results¶
Rename the CSV file by editing the text string
# Combine as CSV and export CSV to Jupyter file storage
# <--------- Check file directory on the left hand panel to find exported CSV
df.to_csv("systematic_search_results.csv", index=False)
# Right click to download the CSV file locally
- Check file directory on the left hand panel to find exported CSV
- Right click to download the CSV file locally
Manually check the thumbnails¶
def scene_thumbnail(i):
plt.imshow(Image.open(BytesIO(requests.get(thumbnail[i]).content)))
plt.title(scene_id[i])
plt.axis('off')
widgets.interact(
scene_thumbnail,
i=(0, len(urls)-1)
);
interactive(children=(IntSlider(value=5, description='i', max=10), Output()), _dom_classes=('widget-interact',…
- Open the downloaded CSV file and click each hyperlink to check the image thumbnail for visual features of interest, related to the project objectives. Or, use the scrollable image carousel widget to review the thumbnails
- Manually add a column to the spreadsheet to mark which Scene IDs are useful and of interest. You can refer to this scene ID list later
- For one grid square, run the code for all the data collections of interest
- Repeat the search process per grid square covering the AOI region