Bring Your Own COG

Sentinel Hub allows you to access your own data stored in your S3 bucket with the powerful Sentinel Hub API. Since data remains on your bucket, you keep full control over it. This functionality requires no replication of data and allows you to exercise the full power of the Sentinel Hub service including Custom algorithms. More information here!

The Sentinel Hub Dashboard has a very user-friendly “Bring your own COG” tab. If you are not going to be creating collections, adding/updating collection tiles, etc. daily, the Dashboard tool is your friend. For the rest, this tutorial is a simple walk-through on creating, updating, listing, and deleting your BYOC collections through Python using sentinelhub-py.

Some general and BYOC related functionality imports:

[1]:
# Configure plots for inline use in Jupyter Notebook
%matplotlib inline

import datetime as dt

# Utilities
import boto3
import dateutil
import geopandas as gpd
import matplotlib.pyplot as plt
import pandas as pd

# Sentinel Hub
from sentinelhub import (
    CRS,
    BBox,
    ByocCollection,
    ByocCollectionAdditionalData,
    ByocCollectionBand,
    ByocTile,
    DataCollection,
    DownloadFailedException,
    MimeType,
    SentinelHubBYOC,
    SentinelHubRequest,
    SHConfig,
    bbox_to_dimensions,
)

Prerequisites

BYOC API requires Sentinel Hub account. Please see configuration instructions how to set up your configuration.

[2]:
# Insert your credentials here in case you don't already have them in the config.toml file:
SH_CLIENT_ID = ""
SH_CLIENT_SECRET = ""

config = SHConfig()

if SH_CLIENT_ID and SH_CLIENT_SECRET:
    config.sh_client_id = SH_CLIENT_ID
    config.sh_client_secret = SH_CLIENT_SECRET

if not config.sh_client_id or not config.sh_client_secret:
    print("Warning! To use Sentinel Hub BYOC API, please provide the credentials (client ID and client secret).")

BYOC collections

SentinelHubBYOC class holds the methods for interacting with Sentinel Hub services. Let’s initialize it with our config:

[3]:
# Initialize SentinelHubBYOC class
byoc = SentinelHubBYOC(config=config)

Create new collection

The easiest way to create a collection is to use its dataclass:

[4]:
new_collection = ByocCollection(name="new collection", s3_bucket="my-s3-bucket")

The new collection is accessible on my-s3-bucket s3 bucket (please see how to configure your bucket for Sentinel Hub service here).

The call to create the collection on Sentinel Hub, will return newly created collection, which will get its own collection id:

[5]:
created_collection = byoc.create_collection(new_collection)
[6]:
print("name:", created_collection["name"])
print("id:", created_collection["id"])
name: new collection
id: b676c6c4-1133-4328-afa7-e9becc5ba590

Get a list of your collections

Now we have created a data collection named new collection, we can retrieve it with the following code.

[7]:
my_collection = byoc.get_collection(created_collection["id"])

Let’s have a look at the the collection we just created.

[8]:
print(f"name: {my_collection['name']}")
print(f"id: {my_collection['id']}")
name: new collection
id: b676c6c4-1133-4328-afa7-e9becc5ba590

In cases where you have a large amount of collections and you would only like to load collection info for a few collections, the following code would be a good option for you:

[9]:
collections_iterator = byoc.iter_collections()
[10]:
my_collection_using_next = next(collections_iterator)
print("name:", my_collection_using_next["name"])
print("id:", my_collection_using_next["id"])
name: new collection
id: b676c6c4-1133-4328-afa7-e9becc5ba590

Note: collections_iterator won’t necessarily return collections in the same order they were created. If you already have collections, the output above could show a collection other than the one we just created.

If you prefer to work with dataclasses, you can also run the following code:

my_collection = ByocCollection.from_dict(next(collections_iterator))

One can of course retrieve all of them in one go like so:

[11]:
my_collections = list(collections_iterator)

for collection in my_collections:
    print((collection["name"], collection["id"]))
('new collection', 'b676c6c4-1133-4328-afa7-e9becc5ba590')

A useful way for managing your collections is pandas.DataFrame you can create like so:

[12]:
my_collections_df = pd.DataFrame(data=list(byoc.iter_collections()))
my_collections_df[["id", "name", "created"]].head()
[12]:
id name created
0 b676c6c4-1133-4328-afa7-e9becc5ba590 new collection 2022-07-22T08:23:00.480642Z

Update existing collection

Anything you can do on Dashboard, Bring your own COG tab, you can do programmatically as well. Below we’re going to rename the new collection collection to renamed new collection:

[13]:
my_collection["name"] = "renamed new collection"

When using next(), run the following code:

my_collection['name'] = 'renamed new collection'

When using dataclass, run the following code:

my_collection.name = 'renamed new collection'

When using list, run the following code:

collection_to_be_updated = [
    collection for collection in my_collections
    if collection['id'] == my_collection['id']
][0]
collection_to_be_updated['name'] = 'renamed new collection'

Note: While you can change other fields as well, s3_bucket cannot be changed, and the bitDepth of bands in the collection is something that is pertinent to the COGs themselves and populated during the ingestion.

To update the collection, call:

[14]:
byoc.update_collection(my_collection)
[14]:
''

Now we can see that the new collection collection has been renamed as renamed new collection.

[15]:
get_renamed_collection = byoc.get_collection(created_collection["id"])
print("name:", get_renamed_collection["name"])
print("id:", get_renamed_collection["id"])
name: renamed new collection
id: b676c6c4-1133-4328-afa7-e9becc5ba590

Delete collection

If you are the owner of the collection, you can also delete it.

Warning:

Beware! Deleting the collection will also delete all its tiles!

[16]:
byoc.delete_collection(my_collection)
[16]:
''

The collection can also be deleted via passing its id to byoc.delete_collection() as shown below:

byoc.delete_collection(my_collection['id'])

Trying to access this collection now will fail.

[ ]:
try:
    deleted_collection = byoc.get_collection(my_collection["id"])
except DownloadFailedException as e:
    print(e)

BYOC tiles (COGs in the collection)

Your data needs to be organized into collections of tiles. Each tile needs to contain a set of bands and (optionally) an acquisition date and time. Tiles with the same bands can be grouped into collections. Think of the Sentinel-2 data source as a collection of Sentinel-2 tiles.

Tiles have to be on an s3 bucket and need to be in COG format. We will not go into details about the COGification process; users can have a look at the documentation or use the BYOC tool that will take care of creating a collection and ingesting the tiles for you.

Creating a new tile (and ingesting it to collection)

When we create a new tile and add it to the collection, the ingestion process on the Sentinel Hub side will happen, checking if the tile corresponds to the COG specifications as well as if it conforms to the collection.

The simplest way to create a new tile is by using the ByocTile dataclass, which will complain if the required fields are missing.

[18]:
new_tile = ByocTile(path="2019/11/27/28V/(BAND).tif", sensing_time=dt.datetime(2019, 11, 27))

Note:

  • The most important field of the tile is its path on an s3 bucket. For example, if your band files are stored in s3://bucket-name/folder/, then set folder as the tile path. In this case, the band names will equal the file names. For example, the band B1 corresponds to the file s3://bucket-name/folder/B1.tiff. If your file names have something other than just the band name, such as a prefix, this is fine as long as the prefix is the same for all files. In this case, the path needs to include this prefix and also the band placeholder: (BAND). Adding the extension is optional. For example, this is what would happen if you would use the following path folder/tile_1_(BAND)_2019.tiff for the following files:

    • s3://bucket-name/folder/tile_1_B1_2019.tiff - the file would be used, the band name would be B1

    • s3://bucket-name/folder/tile_1_B2_2019.tiff - the file would be used, the band name would be B2

    • s3://bucket-name/folder/tile_2_B1_2019.tiff - the file would not be used

    • s3://bucket-name/folder/tile_2_B2_2019.tiff - the file would not be used

  • ByocTile takes sensing_time as optional parameters, but setting the sensing_time is highly recommended since it makes the collection “temporal” and help you search for the data with Sentinel Hub services.

  • ingestion_start is optional and it denotes the time when the ingestion of the COGs had started.

  • tile_geometry is optional as it is the bounding box of the tile and will be read from COG file.

  • cover_geometry is the geometry of where the data (within the bounding box) is and can be useful for optimized search as an optional parameter. For a good explanation of the coverGeometry please see docs.

Let’s create a new collection for these tiles.

[19]:
new_collection = ByocCollection(name="byoc-s2l2a-120m-mosaic", s3_bucket="sentinel-s2-l2a-mosaic-120")
created_collection = byoc.create_collection(new_collection)
[20]:
created_tile = byoc.create_tile(created_collection, new_tile)

The response from byoc.create_tile has a valid id, and its status is set to WAITING. Checking the tile status after a while (by requesting this tile) will tell you if it has been INGESTED or if the ingestion procedure FAILED. In case of failure, additional information (with the cause of failure) will be available in the tile additional_data.

[21]:
created_tile
[21]:
{'id': '75a7bf54-2772-44b1-b87f-3f4ec96096cf',
 'created': '2023-03-16T12:56:22.205845Z',
 'ingestionStart': '2023-03-16T12:56:22.205845Z',
 'sensingTime': '2019-11-27T00:00:00Z',
 'path': '2019/11/27/28V/(BAND).tif',
 'status': 'WAITING'}

Add multiple tiles to a single collection

A data collection can for sure contain multiple tiles. It is important to know that adding multiple tiles will work only if these tiles have the same bands. Let’s add more tiles from the Sentinel-2 L2A 120m Mosaic listed on the open data registry on AWS to the collection.

We first define a function to get a list of paths for each tile:

[22]:
MY_AWS_ACCESS_KEY_ID = ""
MY_AWS_SECRET_ACCESS_KEY = ""


def list_objects_path(bucket, year_count, month_count, day_count):
    tiles_path = []
    client = boto3.client("s3", aws_access_key_id=MY_AWS_ACCESS_KEY_ID, aws_secret_access_key=MY_AWS_SECRET_ACCESS_KEY)
    result = client.list_objects(Bucket=bucket, Delimiter="/")
    for year in result.get("CommonPrefixes")[:year_count]:
        year_result = client.list_objects(Bucket=bucket, Delimiter="/", Prefix=year.get("Prefix"))
        for month in year_result.get("CommonPrefixes")[:month_count]:
            month_result = client.list_objects(Bucket=bucket, Delimiter="/", Prefix=month.get("Prefix"))
            for day in month_result.get("CommonPrefixes")[:day_count]:
                day_result = client.list_objects(Bucket=bucket, Delimiter="/", Prefix=day.get("Prefix"))
                for tile in day_result.get("CommonPrefixes"):
                    tiles_path.append(tile.get("Prefix"))  # noqa: PERF401
    return tiles_path

Next we obtain a list of paths for tiles available on s3://sentinel-s2-l2a-mosaic-120/2019/1/1/.

[23]:
tiles_path = list_objects_path(
    bucket="sentinel-s2-l2a-mosaic-120", year_count=1, month_count=1, day_count=1, config=config
)

Then we can add tiles to the collection with a for loop.

[24]:
for tile in tiles_path:
    year, month, day = tile.split("/")[:3]
    byoc_tile = ByocTile(path=f"{tile}(BAND).tif", sensing_time=dt.datetime(int(year), int(month), int(day)))
    byoc.create_tile(created_collection, byoc_tile)

Note: The tile ingesting process could take some time, please wait a few more minutes after the cell has done running before heading to the next step.

Get tiles from your collection

After byoc.create_tile being executed, we can request the tile from the collection where it is ingested. To request a specific tile you can pass a collection id and a tile id to the get_tile method.

[25]:
tile = byoc.get_tile(collection=created_collection["id"], tile=created_tile["id"])
[26]:
tile
[26]:
{'id': '75a7bf54-2772-44b1-b87f-3f4ec96096cf',
 'created': '2023-03-16T12:56:22.205845Z',
 'ingestionStart': '2023-03-16T12:56:22.205845Z',
 'sensingTime': '2019-11-27T00:00:00Z',
 'coverGeometry': {'type': 'Polygon',
  'crs': {'type': 'name',
   'properties': {'name': 'urn:ogc:def:crs:EPSG::32628'}},
  'coordinates': [[[299999.9980359557, 6899880.000729979],
    [600120.0000155062, 6899880.00067825],
    [600120.0000231846, 7100040.000720726],
    [299999.9970628682, 7100040.000814738],
    [299999.9980359557, 6899880.000729979]]]},
 'tileGeometry': {'type': 'Polygon',
  'crs': {'type': 'name',
   'properties': {'name': 'urn:ogc:def:crs:EPSG::32628'}},
  'coordinates': [[[300000.0, 7100040.0],
    [600120.0, 7100040.0],
    [600120.0, 6899880.0],
    [300000.0, 6899880.0],
    [300000.0, 7100040.0]]]},
 'path': '2019/11/27/28V/(BAND).tif',
 'status': 'INGESTED',
 'additionalData': {'hasAddedPoints': True,
  'filesMetadata': {'B02': {'headerSize': 1328,
    'etag': '"838a04acfa29e10b3b0a972f9ff2f658"'},
   'B03': {'headerSize': 1328, 'etag': '"bc692959f00b3186efbab64853f14bbe"'},
   'B04': {'headerSize': 1328, 'etag': '"26de702b115d976797cdfd00ad6f7f10"'},
   'B08': {'headerSize': 1328, 'etag': '"8c638bf4ad45d2bf606f458bdc9cc165"'},
   'B11': {'headerSize': 1328, 'etag': '"1179bd6c9750c4acc998346864587fe6"'},
   'B12': {'headerSize': 1328, 'etag': '"826091544f541a597651007676765a73"'},
   'dataMask': {'headerSize': 1328,
    'etag': '"183b03ed56250b2624322e5179db585d"'}},
  'minMetersPerPixel': 120.0,
  'maxMetersPerPixel': 960.0}}

You can of course retrieve all tiles into a list.

[27]:
tiles = list(byoc.iter_tiles(created_collection))

In cases where you have a large collection with a lot of tiles and you would only like to load tile info for a few tiles, the following code using next() would be a good option for you:

tile = next(byoc.iter_tiles(created_collection))

To convert it to ByocTile dataclass using the code below:

tile = ByocTile.from_dict(next(byoc.iter_tiles(created_collection)))

Let’s take a look at the keys of the first dictionary, which contains the info of the first tile, in the returned list.

[28]:
list(tiles[0].keys())
[28]:
['id',
 'created',
 'ingestionStart',
 'sensingTime',
 'coverGeometry',
 'tileGeometry',
 'path',
 'status',
 'additionalData']

To check if there’s any tile failed to be ingested, run the code below:

[29]:
tiles_failed_to_be_ingested = [tile["path"] for tile in tiles if tile["status"] == "FAILED"]
tiles_failed_to_be_ingested
[29]:
[]

Visualize the tiles in your collection

Using ByocTile dataclass, which will properly parse tile geometries, date-time strings, etc., one can create a geopandas.GeoDataFrame.

Note: the geometries can be in different coordinate reference systems, so a transform to a common CRS might be needed.

[30]:
tile_iterator = byoc.iter_tiles(created_collection)
[31]:
tiles_for_visualized = [ByocTile.from_dict(next(tile_iterator)) for _ in range(100)]

tiles_gdf = gpd.GeoDataFrame(
    tiles_for_visualized,
    geometry=[t.cover_geometry.transform(CRS.WGS84).geometry for t in tiles_for_visualized],
    crs="epsg:4326",
)
[32]:
tiles_gdf.head()
[32]:
path status tile_id tile_geometry cover_geometry created sensing_time ingestion_start additional_data other_data geometry
0 2019/1/1/15X/(BAND).tif INGESTED 004d2bcf-c414-40f7-a9eb-e26d63b8e5d8 Geometry(POLYGON ((399960 8100000, 700080 8100... Geometry(POLYGON ((399959.99978184653 7999920.... 2023-03-16 12:56:39.547622+00:00 2019-01-01 00:00:00+00:00 2023-03-16 12:56:39.547622+00:00 {'hasAddedPoints': True, 'filesMetadata': {'B0... {} POLYGON ((-95.91354 72.07680, -87.18720 72.011...
1 2019/1/1/50T/(BAND).tif INGESTED 01c99620-7971-4a30-b974-892803ec8c5e Geometry(POLYGON ((199980 5300040, 800100 5300... Geometry(POLYGON ((199979.99898144224 4399920.... 2023-03-16 12:57:22.328714+00:00 2019-01-01 00:00:00+00:00 2023-03-16 12:57:22.328714+00:00 {'hasAddedPoints': True, 'filesMetadata': {'B0... {} POLYGON ((113.50108 39.69643, 120.49985 39.696...
2 2019/1/1/21H/(BAND).tif INGESTED 01ee0e98-1e15-40e4-ac80-25bfd29be80b Geometry(POLYGON ((199980 6500020, 800100 6500... Geometry(POLYGON ((199979.99898124964 5599899.... 2023-03-16 12:56:47.542399+00:00 2019-01-01 00:00:00+00:00 2023-03-16 12:56:47.542399+00:00 {'hasAddedPoints': True, 'filesMetadata': {'B0... {} POLYGON ((-60.49900 -39.69805, -53.50006 -39.6...
3 2019/1/1/50V/(BAND).tif INGESTED 02952525-edea-432f-87f4-8740c0472877 Geometry(POLYGON ((300000 7100040, 700080 7100... Geometry(POLYGON ((299999.99941996665 6199920.... 2023-03-16 12:57:22.551168+00:00 2019-01-01 00:00:00+00:00 2023-03-16 12:57:22.551168+00:00 {'hasAddedPoints': True, 'filesMetadata': {'B0... {} POLYGON ((113.80060 55.90309, 120.20068 55.903...
4 2019/1/1/34N/(BAND).tif INGESTED 03803c3d-5d36-4f9a-916f-c3f1d3d8de21 Geometry(POLYGON ((199980 900000, 900060 90000... Geometry(POLYGON ((199980.00000812783 -119.999... 2023-03-16 12:57:00.186216+00:00 2019-01-01 00:00:00+00:00 2023-03-16 12:57:00.186216+00:00 {'hasAddedPoints': True, 'filesMetadata': {'B0... {} POLYGON ((18.30480 -0.00108, 24.59287 -0.00108...
[33]:
fig, ax = plt.subplots(figsize=(17, 8))
tiles_gdf.plot(ax=ax);
../_images/examples_byoc_request_72_0.png

In the above example, the ingested tiles are 100 tiles from the Sentinel-2 L2A 120m Mosaic which contains 19869 tiles around the globe, hence the tiles are so sparse in the image above.

Updating and deleting a tile

Updating and deleting a tile follow the same logic as updating/deleting a collection.

  • To update a tile:

[34]:
tile["sensingTime"] = "2021-06-29T18:02:34"
byoc.update_tile(created_collection, tile)
[34]:
''

After updating we can see that the sensingTime has been changed.

[35]:
byoc.get_tile(collection=created_collection["id"], tile=created_tile["id"])["sensingTime"]
[35]:
'2021-06-29T18:02:34.000'
  • To delete a tile:

[36]:
byoc.delete_tile(created_collection, tile)
[36]:
''

Now the tile is gone forever.

[37]:
tiles = list(byoc.iter_tiles(created_collection))
[tile for tile in tiles if tile["id"] == created_tile["id"]]
[37]:
[]

Retrieve data from collection

Once we have a collection created and its tiles ingested, we can retrieve the data from said collection. We will be using ProcessAPI for this.

[38]:
data_collection = DataCollection.define_byoc(created_collection["id"])

Alternatively using dataclass:

data_collection = my_collection_dataclass.to_data_collection()
[39]:
tile_time = dateutil.parser.parse(tiles[0]["sensingTime"])

If using dataclass run:

tile_time = tile_dataclass.sensing_time

Below we’re going to request a false color image of Caspian Sea.

[40]:
caspian_sea_bbox = BBox((49.9604, 44.7176, 51.0481, 45.2324), crs=CRS.WGS84)
[41]:
false_color_evalscript = """
//VERSION=3
function setup() {
  return {
    input: ["B08","B04","B03", "dataMask"],
    output: { bands: 4 },
  };
}

var f = 2.5/10000;
function evaluatePixel(sample) {
  return [f*sample.B08, f*sample.B04, f*sample.B03, sample.dataMask];
}
"""

request = SentinelHubRequest(
    evalscript=false_color_evalscript,
    input_data=[SentinelHubRequest.input_data(data_collection=data_collection, time_interval=tile_time)],
    responses=[SentinelHubRequest.output_response("default", MimeType.PNG)],
    bbox=caspian_sea_bbox,
    size=bbox_to_dimensions(caspian_sea_bbox, 100),
    config=config,
)
[42]:
data = request.get_data()[0]
[43]:
fig, ax = plt.subplots(figsize=(15, 10))

ax.imshow(data)
ax.set_title(tile_time.date().isoformat(), fontsize=10)

plt.tight_layout()
../_images/examples_byoc_request_92_0.png

Configure collections

In the above case, bands are not defined before the tile ingestion process. BYOC service then automatically configures bands based on the first ingested tile and names the band after the filename unless a band placeholder is defined (see creating a new tile section). If the first ingested tile is a multi-band TIFF, the band index in 1-based numbering is also added at the end, e.g., the bands in a multi-band file named RGB.tiff would be named RGB_1, RGB_2, etc.

The automatic configuration generally configures all bands in a TIFF; however, BYOC service also allows user to configure bands manually. This can be useful when users have a multi-band TIFF but are only interested in ingesting part of the bands. Instead of trying to separate a multi-band TIFF into several single-band TIFFs, one can configure bands manually and ingest those specific bands only.

Automatic band configuration

Let’s create a new collection without bands configuration.

[7]:
byoc_auto_config = ByocCollection(name="byoc-auto-config", s3_bucket="byoc-config-demo")
byoc_auto_config_collection = byoc.create_collection(byoc_auto_config)

At this step, there is no band configured and we can check the configuration status using the following code.

[8]:
byoc_auto_config_collection = byoc.get_collection(byoc_auto_config_collection["id"])
byoc_auto_config_collection["isConfigured"]
[8]:
False

Now let’s try to ingest a true color TIFF which has red, green, and blue band.

[9]:
example_tile = ByocTile(path="(BAND)_COG.tiff", sensing_time=dt.datetime(2022, 12, 1))
added_tile = byoc.create_tile(byoc_auto_config_collection, example_tile)

added_tile
[9]:
{'id': '15659300-cf50-43da-afa7-ea4e72ed133c',
 'created': '2023-01-16T14:00:09.079499Z',
 'ingestionStart': '2023-01-16T14:00:09.079499Z',
 'sensingTime': '2022-12-01T00:00:00Z',
 'path': '(BAND)_COG.tiff',
 'status': 'WAITING'}
[10]:
added_tile_info = byoc.get_tile(collection=byoc_auto_config_collection["id"], tile=added_tile["id"])
added_tile_info["status"]
[10]:
'WAITING'

After the tile being ingested successfully, we can see that the configuration status changes to True and the band information is stored in the additionalData dictionary.

[14]:
byoc_auto_config_collection = byoc.get_collection(byoc_auto_config_collection["id"])
byoc_auto_config_collection["isConfigured"]
[14]:
True
[15]:
byoc_auto_config_collection["additionalData"]
[15]:
{'bands': {'RGB_1': {'bitDepth': 8,
   'source': 'RGB',
   'bandIndex': 1,
   'sampleFormat': 'UINT'},
  'RGB_2': {'bitDepth': 8,
   'source': 'RGB',
   'bandIndex': 2,
   'sampleFormat': 'UINT'},
  'RGB_3': {'bitDepth': 8,
   'source': 'RGB',
   'bandIndex': 3,
   'sampleFormat': 'UINT'}}}

Manual band configuration

Now let’s try to create a pre-configured BYOC collection. In this example, the red, green, and blue band all need to be configured so that the true color TIFF can be fully ingested.

[16]:
band_config = ByocCollectionAdditionalData(
    bands={
        # source should be the string inside (BAND) placeholder
        "R": ByocCollectionBand(source="RGB", band_index=1, bit_depth=8, sample_format="UINT"),
        "G": ByocCollectionBand(source="RGB", band_index=2, bit_depth=8, sample_format="UINT"),
        "B": ByocCollectionBand(source="RGB", band_index=3, bit_depth=8, sample_format="UINT"),
    }
)
byoc_manual_config = ByocCollection(
    name="byoc-manual-config", s3_bucket="byoc-config-demo", additional_data=band_config
)

byoc_manual_config_collection = byoc.create_collection(byoc_manual_config)

By checking the configuration status and additional data, we can see that the data collection is configured and the band information is stored in the additionalData dictionary before any tiles being ingested.

[17]:
byoc_manual_config_collection["isConfigured"]
[17]:
True
[18]:
byoc_manual_config_collection["additionalData"]
[18]:
{'bands': {'R': {'bitDepth': 8,
   'source': 'RGB',
   'bandIndex': 1,
   'sampleFormat': 'UINT'},
  'G': {'bitDepth': 8,
   'source': 'RGB',
   'bandIndex': 2,
   'sampleFormat': 'UINT'},
  'B': {'bitDepth': 8,
   'source': 'RGB',
   'bandIndex': 3,
   'sampleFormat': 'UINT'}}}

Then, we can ingest tiles as shown above.

[19]:
added_tile = byoc.create_tile(byoc_manual_config_collection, example_tile)
added_tile
[19]:
{'id': '6071cb25-472e-4dd6-8919-ebd1fdeedc11',
 'created': '2023-01-16T14:07:03.313843Z',
 'ingestionStart': '2023-01-16T14:07:03.313843Z',
 'sensingTime': '2022-12-01T00:00:00Z',
 'path': '(BAND)_COG.tiff',
 'status': 'WAITING'}
[20]:
added_tile_info = byoc.get_tile(collection=byoc_manual_config_collection["id"], tile=added_tile["id"])
added_tile_info["status"]
[20]:
'INGESTED'

Ingest a specific band of the TIFF

One can also ingest a specific band from a multi-band TIFF. In this example, we will demonstrate how to only ingest green band from a true color TIFF.

Let’s start from creating a BYOC collection with green band being configured only.

[21]:
specific_band_config = ByocCollectionAdditionalData(
    bands={
        # here we specify the second band which is the green band to be ingested
        "G": ByocCollectionBand(source="RGB", band_index=2, bit_depth=8, sample_format="UINT")
    }
)
byoc_demo_manual_specific_band = ByocCollection(
    name="byoc-specific-band-config", s3_bucket="byoc-config-demo", additional_data=specific_band_config
)

byoc_specific_band_config_collection = byoc.create_collection(byoc_demo_manual_specific_band)

This time we can see that the collection is configured and there is only one band in the additional data dictionary, meaning that BYOC service will only ingest the specified band that is pre-defined for the collection from the example multi-band TIFF.

[22]:
byoc_specific_band_config_collection["isConfigured"]
[22]:
True
[23]:
byoc_specific_band_config_collection["additionalData"]
[23]:
{'bands': {'G': {'bitDepth': 8,
   'source': 'RGB',
   'bandIndex': 2,
   'sampleFormat': 'UINT'}}}

We can now use the same code to ingest the tile to the collection, but only the configured band will be ingested by the BYOC service.

[24]:
added_tile = byoc.create_tile(byoc_specific_band_config_collection, example_tile)
added_tile
[24]:
{'id': '7c518995-1c05-4f7c-883a-0630232ecb82',
 'created': '2023-01-16T14:08:01.831044Z',
 'ingestionStart': '2023-01-16T14:08:01.831044Z',
 'sensingTime': '2022-12-01T00:00:00Z',
 'path': '(BAND)_COG.tiff',
 'status': 'WAITING'}
[25]:
added_tile_info = byoc.get_tile(collection=byoc_specific_band_config_collection["id"], tile=added_tile["id"])
added_tile_info["status"]
[25]:
'INGESTED'