Usable Data Map (UDM2) Cloud Detection within an AOI

This guide is a follow up to the UDM2 Cloud Detection notebook. Please refer to that notebook for further details on specifications and usage of the UDM2 asset. In this notebook, we apply cloud detection to a specific area of interest (AOI).

The udm2 asset is available for all PSScene4Band and PSOrthoTile items created after 2018-08-01. Therefore, our search should be limited to these items and this date range. In this notebook, we focus on PSOrthoTile imagery taken within the month of April, 2019.

Import dependencies


In [1]:
import datetime
import json
import os
import time

from planet import api
from planet.api import filters
import rasterio
from rasterio.plot import show

Finding clear imagery

One of the benefits of accurate and automated cloud detection is that it allows users to filter out images that don't meet a certain quality threshold. Planet's Data API allows users to search based on the value of the imagery metadata.

Planet's cloud detection algorithm classifies every pixel into one of six different categories, each of which has a corresponding metadata field that reflects the percentage of data that falls into the category.

Class Metadata field
clear clear_percent
snow snow_ice_percent
shadow shadow_percent
light haze light_haze_percent
heavy haze heavy_haze_percent
cloud cloud_percent

The UDM2 Cloud Detection notebook provides examples for how to perform searches using the Planet Python Client command-line interface.

The following example will show how use Planet's Python Client to perform a search for PSOrthoTiles that are 70-95% clear (not totally clear so the udm has some interesting content), taken within the month of April, 2019, and within an AOI.


In [2]:
# define the date range for imagery
start_date = datetime.datetime(year=2019,month=4,day=1)
stop_date = datetime.datetime(year=2019,month=5,day=1)

# define the aoi for imagery
aoi = {"geometry": {
    "type":"Polygon",
    "coordinates":
        [[
            [25.42429478260258,1.0255377823058893],
            [25.592960813580472,1.0255377823058893],
            [25.592960813580472,1.1196578801254304],
            [25.42429478260258,1.1196578801254304],
            [25.42429478260258,1.0255377823058893]
        ]]}}


item_type = 'PSOrthoTile'

In [19]:
# create an api request from the search specifications
def build_ps_request(item_type, aoi_geom, start_date, stop_date):
    query = filters.and_filter(
        filters.geom_filter(aoi_geom),
        filters.range_filter('clear_percent', gte=70),
        filters.range_filter('clear_percent', lte=95),
        filters.date_range('acquired', gt=start_date),
        filters.date_range('acquired', lt=stop_date)
    )

    return filters.build_search_request(query, [item_type])

request = build_ps_request(item_type, aoi['geometry'], start_date, stop_date)
print(request)


{'item_types': ['PSOrthoTile'], 'filter': {'type': 'AndFilter', 'config': ({'field_name': 'geometry', 'type': 'GeometryFilter', 'config': {'type': 'Polygon', 'coordinates': [[[25.42429478260258, 1.0255377823058893], [25.592960813580472, 1.0255377823058893], [25.592960813580472, 1.1196578801254304], [25.42429478260258, 1.1196578801254304], [25.42429478260258, 1.0255377823058893]]]}}, {'field_name': 'clear_percent', 'type': 'RangeFilter', 'config': {'gte': 70}}, {'field_name': 'clear_percent', 'type': 'RangeFilter', 'config': {'lte': 95}}, {'field_name': 'acquired', 'type': 'DateRangeFilter', 'config': {'gt': '2019-04-01T00:00:00Z'}}, {'field_name': 'acquired', 'type': 'DateRangeFilter', 'config': {'lt': '2019-05-01T00:00:00Z'}})}}

In [4]:
# search the API for imagery

def get_api_key():
    return os.environ['PL_API_KEY']

# quick check that key is defined
assert get_api_key(), "PL_API_KEY not defined."

def create_client():
    return api.ClientV1(api_key=get_api_key())

def search_pl_api(request, limit=500):
    client = create_client()
    result = client.quick_search(request)
    
    # note that this returns a generator
    return result.items_iter(limit=limit)

items = list(search_pl_api(request))
print(len(items))
# uncomment below to see entire metadata for a PS orthotile
# print(json.dumps(items[0], indent=4))


14

In [5]:
item_id = items[0]['id']
item_id


Out[5]:
'2310711_3539508_2019-04-25_1035'

The udm2 asset

In addition to metadata for filtering, the udm2 asset provides a pixel-by-pixel map that identifies the classification of each pixel. See the UDM2 Cloud Detection notebook for an example map.

The udm2 structure is to use a separate band for each classification type. Band 2, for example, indicates that a pixel is snowy when its value is 1, band 3 indicates shadow and so on. "

The following Python will download the data above and then display pixels that fall into a certain classifications.


In [7]:
# activate assets
client = create_client()
assets = client.get_assets_by_id(item_type, item_id).get()
client.activate(assets["analytic"])
client.activate(assets["udm2"])

# wait until activation completes
while True:
    assets = client.get_assets_by_id(item_type, item_id).get()
    if "location" in assets["analytic"] and "location" in assets["udm2"]:
        print('assets activated')
        break
    print('waiting')
    time.sleep(15)


assets activated

In [8]:
# start downloads
data_dir = 'data'
r1 = client.download(assets["analytic"], callback=api.write_to_file(data_dir))
r2 = client.download(assets["udm2"], callback=api.write_to_file(data_dir))


`background_callback` is deprecated and will be removed in 1.0, use `hooks` instead
`background_callback` is deprecated and will be removed in 1.0, use `hooks` instead

In [9]:
# wait until downloads complete
r1.wait()
r2.wait()
img_file = os.path.join(data_dir, r1.get_body().name)
udm_file = os.path.join(data_dir, r2.get_body().name)
print("image: {}".format(img_file))
print("udm2:  {}".format(udm_file))


image: data/2310711_3539508_2019-04-25_1035_BGRN_Analytic.tif
udm2:  data/2310711_3539508_2019-04-25_1035_udm2.tif

Visualize Image and UDM2


In [20]:
with rasterio.open(udm_file) as src:
    shadow_mask = src.read(3).astype(bool)
    cloud_mask = src.read(6).astype(bool)
    
show(shadow_mask, title="shadow", cmap="binary")
show(cloud_mask, title="cloud", cmap="binary")


Out[20]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f148c139a58>

In [18]:
mask = shadow_mask + cloud_mask
show(mask, title="mask", cmap="binary")


Out[18]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f148c25eb00>

In [13]:
with rasterio.open(img_file) as src:
    profile = src.profile
    img_data = src.read([3, 2, 1], masked=True) / 10000.0 # apply RGB ordering and scale down

In [14]:
show(img_data, title=item_id)


Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
Out[14]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f148c31df98>

In [15]:
img_data.mask = mask
img_data = img_data.filled(fill_value=0)

In [16]:
show(img_data, title="masked image")


Clipping input data to the valid range for imshow with RGB data ([0..1] for floats or [0..255] for integers).
Out[16]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f148c285eb8>

The image stored in img_data now has cloudy / cloud-shadowy pixels masked out and can be saved or used for analysis.