Sentinel-2 Cloud Masking with s2cloudless

Author(s): jdbcode

This tutorial is an introduction to masking clouds and cloud shadows in Sentinel-2 (S2) surface reflectance (SR) data using Earth Engine. Clouds are identified from the S2 cloud probability dataset (s2cloudless) and shadows are defined by cloud projection intersection with low-reflectance near-infrared (NIR) pixels.

For a similar JavaScript API script, see this Code Editor example.

Run me first

Run the following cell to initialize the Earth Engine API. The output will contain instructions on how to grant this notebook access to Earth Engine using your account.

import ee

# Trigger the authentication flow.
ee.Authenticate()

# Initialize the library.
ee.Initialize(project='my-project')

Assemble cloud mask components

This section builds an S2 SR collection and defines functions to add cloud and cloud shadow component layers to each image in the collection.

Define collection filter and cloud mask parameters

Define parameters that are used to filter the S2 image collection and determine cloud and cloud shadow identification.

Parameter Type Description
AOI ee.Geometry Area of interest
START_DATE string Image collection start date (inclusive)
END_DATE string Image collection end date (exclusive)
CLOUD_FILTER integer Maximum image cloud cover percent allowed in image collection
CLD_PRB_THRESH integer Cloud probability (%); values greater than are considered cloud
NIR_DRK_THRESH float Near-infrared reflectance; values less than are considered potential cloud shadow
CLD_PRJ_DIST float Maximum distance (km) to search for cloud shadows from cloud edges
BUFFER integer Distance (m) to dilate the edge of cloud-identified objects

The values currently set for AOI, START_DATE, END_DATE, and CLOUD_FILTER are intended to build a collection for a single S2 overpass of a region near Portland, Oregon, USA. When parameterizing and evaluating cloud masks for a new area, it is good practice to identify a single overpass date and limit the regional extent to minimize processing requirements. If you want to work with a different example, use this Earth Engine App to identify an image that includes some clouds, then replace the relevant parameter values below with those provided in the app.

AOI = ee.Geometry.Point(-122.269, 45.701)
START_DATE = '2020-06-01'
END_DATE = '2020-06-02'
CLOUD_FILTER = 60
CLD_PRB_THRESH = 50
NIR_DRK_THRESH = 0.15
CLD_PRJ_DIST = 1
BUFFER = 50

Build a Sentinel-2 collection

Sentinel-2 surface reflectance and Sentinel-2 cloud probability are two different image collections. Each collection must be filtered similarly (e.g., by date and bounds) and then the two filtered collections must be joined.

Define a function to filter the SR and s2cloudless collections according to area of interest and date parameters, then join them on the system:index property. The result is a copy of the SR collection where each image has a new 's2cloudless' property whose value is the corresponding s2cloudless image.

def get_s2_sr_cld_col(aoi, start_date, end_date):
    # Import and filter S2 SR.
    s2_sr_col = (ee.ImageCollection('COPERNICUS/S2_SR')
        .filterBounds(aoi)
        .filterDate(start_date, end_date)
        .filter(ee.Filter.lte('CLOUDY_PIXEL_PERCENTAGE', CLOUD_FILTER)))

    # Import and filter s2cloudless.
    s2_cloudless_col = (ee.ImageCollection('COPERNICUS/S2_CLOUD_PROBABILITY')
        .filterBounds(aoi)
        .filterDate(start_date, end_date))

    # Join the filtered s2cloudless collection to the SR collection by the 'system:index' property.
    return ee.ImageCollection(ee.Join.saveFirst('s2cloudless').apply(**{
        'primary': s2_sr_col,
        'secondary': s2_cloudless_col,
        'condition': ee.Filter.equals(**{
            'leftField': 'system:index',
            'rightField': 'system:index'
        })
    }))

Apply the get_s2_sr_cld_col function to build a collection according to the parameters defined above.

s2_sr_cld_col_eval = get_s2_sr_cld_col(AOI, START_DATE, END_DATE)
/tmpfs/src/tf_docs_env/lib/python3.9/site-packages/ee/deprecation.py:207: DeprecationWarning: 

Attention required for COPERNICUS/S2_SR! You are using a deprecated asset.
To make sure your code keeps working, please update it.
Learn more: https://developers.google.com/earth-engine/datasets/catalog/COPERNICUS_S2_SR

  warnings.warn(warning, category=DeprecationWarning)

Define cloud mask component functions

Cloud components

Define a function to add the s2cloudless probability layer and derived cloud mask as bands to an S2 SR image input.

def add_cloud_bands(img):
    # Get s2cloudless image, subset the probability band.
    cld_prb = ee.Image(img.get('s2cloudless')).select('probability')

    # Condition s2cloudless by the probability threshold value.
    is_cloud = cld_prb.gt(CLD_PRB_THRESH).rename('clouds')

    # Add the cloud probability layer and cloud mask as image bands.
    return img.addBands(ee.Image([cld_prb, is_cloud]))

Cloud shadow components

Define a function to add dark pixels, cloud projection, and identified shadows as bands to an S2 SR image input. Note that the image input needs to be the result of the above add_cloud_bands function because it relies on knowing which pixels are considered cloudy ('clouds' band).

def add_shadow_bands(img):
    # Identify water pixels from the SCL band.
    not_water = img.select('SCL').neq(6)

    # Identify dark NIR pixels that are not water (potential cloud shadow pixels).
    SR_BAND_SCALE = 1e4
    dark_pixels = img.select('B8').lt(NIR_DRK_THRESH*SR_BAND_SCALE).multiply(not_water).rename('dark_pixels')

    # Determine the direction to project cloud shadow from clouds (assumes UTM projection).
    shadow_azimuth = ee.Number(90).subtract(ee.Number(img.get('MEAN_SOLAR_AZIMUTH_ANGLE')));

    # Project shadows from clouds for the distance specified by the CLD_PRJ_DIST input.
    cld_proj = (img.select('clouds').directionalDistanceTransform(shadow_azimuth, CLD_PRJ_DIST*10)
        .reproject(**{'crs': img.select(0).projection(), 'scale': 100})
        .select('distance')
        .mask()
        .rename('cloud_transform'))

    # Identify the intersection of dark pixels with cloud shadow projection.
    shadows = cld_proj.multiply(dark_pixels).rename('shadows')

    # Add dark pixels, cloud projection, and identified shadows as image bands.
    return img.addBands(ee.Image([dark_pixels, cld_proj, shadows]))

Final cloud-shadow mask

Define a function to assemble all of the cloud and cloud shadow components and produce the final mask.

def add_cld_shdw_mask(img):
    # Add cloud component bands.
    img_cloud = add_cloud_bands(img)

    # Add cloud shadow component bands.
    img_cloud_shadow = add_shadow_bands(img_cloud)

    # Combine cloud and shadow mask, set cloud and shadow as value 1, else 0.
    is_cld_shdw = img_cloud_shadow.select('clouds').add(img_cloud_shadow.select('shadows')).gt(0)

    # Remove small cloud-shadow patches and dilate remaining pixels by BUFFER input.
    # 20 m scale is for speed, and assumes clouds don't require 10 m precision.
    is_cld_shdw = (is_cld_shdw.focalMin(2).focalMax(BUFFER*2/20)
        .reproject(**{'crs': img.select([0]).projection(), 'scale': 20})
        .rename('cloudmask'))

    # Add the final cloud-shadow mask to the image.
    return img_cloud_shadow.addBands(is_cld_shdw)

Visualize and evaluate cloud mask components

This section provides functions for displaying the cloud and cloud shadow components. In most cases, adding all components to images and viewing them is unnecessary. This section is included to illustrate how the cloud/cloud shadow mask is developed and demonstrate how to test and evaluate various parameters, which is helpful when defining masking variables for an unfamiliar region or time of year.

In applications outside of this tutorial, if you prefer to include only the final cloud/cloud shadow mask along with the original image bands, replace:

return img_cloud_shadow.addBands(is_cld_shdw)

with

return img.addBands(is_cld_shdw)

in the above add_cld_shdw_mask function.

Define functions to display image and mask component layers.

Folium will be used to display map layers. Import folium and define a method to display Earth Engine image tiles.

# Import the folium library.
import folium

# Define a method for displaying Earth Engine image tiles to a folium map.
def add_ee_layer(self, ee_image_object, vis_params, name, show=True, opacity=1, min_zoom=0):
    map_id_dict = ee.Image(ee_image_object).getMapId(vis_params)
    folium.raster_layers.TileLayer(
        tiles=map_id_dict['tile_fetcher'].url_format,
        attr='Map Data &copy; <a href="https://earthengine.google.com/">Google Earth Engine</a>',
        name=name,
        show=show,
        opacity=opacity,
        min_zoom=min_zoom,
        overlay=True,
        control=True
        ).add_to(self)

# Add the Earth Engine layer method to folium.
folium.Map.add_ee_layer = add_ee_layer

Define a function to display all of the cloud and cloud shadow components to an interactive Folium map. The input is an image collection where each image is the result of the add_cld_shdw_mask function defined previously.

def display_cloud_layers(col):
    # Mosaic the image collection.
    img = col.mosaic()

    # Subset layers and prepare them for display.
    clouds = img.select('clouds').selfMask()
    shadows = img.select('shadows').selfMask()
    dark_pixels = img.select('dark_pixels').selfMask()
    probability = img.select('probability')
    cloudmask = img.select('cloudmask').selfMask()
    cloud_transform = img.select('cloud_transform')

    # Create a folium map object.
    center = AOI.centroid(10).coordinates().reverse().getInfo()
    m = folium.Map(location=center, zoom_start=12)

    # Add layers to the folium map.
    m.add_ee_layer(img,
                   {'bands': ['B4', 'B3', 'B2'], 'min': 0, 'max': 2500, 'gamma': 1.1},
                   'S2 image', True, 1, 9)
    m.add_ee_layer(probability,
                   {'min': 0, 'max': 100},
                   'probability (cloud)', False, 1, 9)
    m.add_ee_layer(clouds,
                   {'palette': 'e056fd'},
                   'clouds', False, 1, 9)
    m.add_ee_layer(cloud_transform,
                   {'min': 0, 'max': 1, 'palette': ['white', 'black']},
                   'cloud_transform', False, 1, 9)
    m.add_ee_layer(dark_pixels,
                   {'palette': 'orange'},
                   'dark_pixels', False, 1, 9)
    m.add_ee_layer(shadows, {'palette': 'yellow'},
                   'shadows', False, 1, 9)
    m.add_ee_layer(cloudmask, {'palette': 'orange'},
                   'cloudmask', True, 0.5, 9)

    # Add a layer control panel to the map.
    m.add_child(folium.LayerControl())

    # Display the map.
    display(m)

Display mask component layers

Map the add_cld_shdw_mask function over the collection to add mask component bands to each image, then display the results.

Give the system some time to render everything, it should take less than a minute.

s2_sr_cld_col_eval_disp = s2_sr_cld_col_eval.map(add_cld_shdw_mask)

display_cloud_layers(s2_sr_cld_col_eval_disp)

---------------------------------------------------------------------------

timeout                                   Traceback (most recent call last)

File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/urllib3/connectionpool.py:534, in HTTPConnectionPool._make_request(self, conn, method, url, body, headers, retries, timeout, chunked, response_conn, preload_content, decode_content, enforce_content_length)
    533 try:
--> 534     response = conn.getresponse()
    535 except (BaseSSLError, OSError) as e:


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/urllib3/connection.py:516, in HTTPConnection.getresponse(self)
    515 # Get the response from http.client.HTTPConnection
--> 516 httplib_response = super().getresponse()
    518 try:


File /usr/local/.pyenv/versions/3.9.5/lib/python3.9/http/client.py:1345, in HTTPConnection.getresponse(self)
   1344 try:
-> 1345     response.begin()
   1346 except ConnectionError:


File /usr/local/.pyenv/versions/3.9.5/lib/python3.9/http/client.py:307, in HTTPResponse.begin(self)
    306 while True:
--> 307     version, status, reason = self._read_status()
    308     if status != CONTINUE:


File /usr/local/.pyenv/versions/3.9.5/lib/python3.9/http/client.py:268, in HTTPResponse._read_status(self)
    267 def _read_status(self):
--> 268     line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1")
    269     if len(line) > _MAXLINE:


File /usr/local/.pyenv/versions/3.9.5/lib/python3.9/socket.py:704, in SocketIO.readinto(self, b)
    703 try:
--> 704     return self._sock.recv_into(b)
    705 except timeout:


timeout: timed out


The above exception was the direct cause of the following exception:


ReadTimeoutError                          Traceback (most recent call last)

File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/IPython/core/formatters.py:922, in IPythonDisplayFormatter.__call__(self, obj)
    920 method = get_real_method(obj, self.print_method)
    921 if method is not None:
--> 922     method()
    923     return True


Cell In[1], line 96, in __set_up_ee_live_docs.<locals>.patch_ipython_display.<locals>.<lambda>(self)
     95 def patch_ipython_display(map_class, css_class):
---> 96   map_class._ipython_display_ = lambda self: display_png(self, css_class)


Cell In[1], line 61, in __set_up_ee_live_docs.<locals>.display_png(map_class, css_class)
     58 driver = webdriver.Firefox(options=options)
     60 map_class.save(tmp_path)
---> 61 driver.get(f"file:///{tmp_path}")
     62 driver.fullscreen_window()
     63 time.sleep(map_render_delay_secs)


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/selenium/webdriver/remote/webdriver.py:454, in WebDriver.get(self, url)
    436 def get(self, url: str) -> None:
    437     """Navigate the browser to the specified URL in the current window or
    438     tab.
    439 
   (...)
    452     >>> driver.get("https://example.com")
    453     """
--> 454     self.execute(Command.GET, {"url": url})


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/selenium/webdriver/remote/webdriver.py:427, in WebDriver.execute(self, driver_command, params)
    424     elif "sessionId" not in params:
    425         params["sessionId"] = self.session_id
--> 427 response = self.command_executor.execute(driver_command, params)
    428 if response:
    429     self.error_handler.check_response(response)


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/selenium/webdriver/remote/remote_connection.py:404, in RemoteConnection.execute(self, command, params)
    402 trimmed = self._trim_large_entries(params)
    403 LOGGER.debug("%s %s %s", command_info[0], url, str(trimmed))
--> 404 return self._request(command_info[0], url, body=data)


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/selenium/webdriver/remote/remote_connection.py:428, in RemoteConnection._request(self, method, url, body)
    425     body = None
    427 if self._client_config.keep_alive:
--> 428     response = self._conn.request(method, url, body=body, headers=headers, timeout=self._client_config.timeout)
    429     statuscode = response.status
    430 else:


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/urllib3/_request_methods.py:143, in RequestMethods.request(self, method, url, body, fields, headers, json, **urlopen_kw)
    135     return self.request_encode_url(
    136         method,
    137         url,
   (...)
    140         **urlopen_kw,
    141     )
    142 else:
--> 143     return self.request_encode_body(
    144         method, url, fields=fields, headers=headers, **urlopen_kw
    145     )


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/urllib3/_request_methods.py:278, in RequestMethods.request_encode_body(self, method, url, fields, headers, encode_multipart, multipart_boundary, **urlopen_kw)
    274     extra_kw["headers"].setdefault("Content-Type", content_type)
    276 extra_kw.update(urlopen_kw)
--> 278 return self.urlopen(method, url, **extra_kw)


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/urllib3/poolmanager.py:443, in PoolManager.urlopen(self, method, url, redirect, **kw)
    441     response = conn.urlopen(method, url, **kw)
    442 else:
--> 443     response = conn.urlopen(method, u.request_uri, **kw)
    445 redirect_location = redirect and response.get_redirect_location()
    446 if not redirect_location:


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/urllib3/connectionpool.py:841, in HTTPConnectionPool.urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, preload_content, decode_content, **response_kw)
    838 elif isinstance(new_e, (OSError, HTTPException)):
    839     new_e = ProtocolError("Connection aborted.", new_e)
--> 841 retries = retries.increment(
    842     method, url, error=new_e, _pool=self, _stacktrace=sys.exc_info()[2]
    843 )
    844 retries.sleep()
    846 # Keep track of the error for the retry warning.


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/urllib3/util/retry.py:474, in Retry.increment(self, method, url, response, error, _pool, _stacktrace)
    471 elif error and self._is_read_error(error):
    472     # Read retry?
    473     if read is False or method is None or not self._is_method_retryable(method):
--> 474         raise reraise(type(error), error, _stacktrace)
    475     elif read is not None:
    476         read -= 1


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/urllib3/util/util.py:39, in reraise(tp, value, tb)
     37     if value.__traceback__ is not tb:
     38         raise value.with_traceback(tb)
---> 39     raise value
     40 finally:
     41     value = None  # type: ignore[assignment]


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/urllib3/connectionpool.py:787, in HTTPConnectionPool.urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, preload_content, decode_content, **response_kw)
    784 response_conn = conn if not release_conn else None
    786 # Make the request on the HTTPConnection object
--> 787 response = self._make_request(
    788     conn,
    789     method,
    790     url,
    791     timeout=timeout_obj,
    792     body=body,
    793     headers=headers,
    794     chunked=chunked,
    795     retries=retries,
    796     response_conn=response_conn,
    797     preload_content=preload_content,
    798     decode_content=decode_content,
    799     **response_kw,
    800 )
    802 # Everything went great!
    803 clean_exit = True


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/urllib3/connectionpool.py:536, in HTTPConnectionPool._make_request(self, conn, method, url, body, headers, retries, timeout, chunked, response_conn, preload_content, decode_content, enforce_content_length)
    534     response = conn.getresponse()
    535 except (BaseSSLError, OSError) as e:
--> 536     self._raise_timeout(err=e, url=url, timeout_value=read_timeout)
    537     raise
    539 # Set properties that are used by the pooling layer.


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/urllib3/connectionpool.py:367, in HTTPConnectionPool._raise_timeout(self, err, url, timeout_value)
    364 """Is the error actually a timeout? Will raise a ReadTimeout or pass"""
    366 if isinstance(err, SocketTimeout):
--> 367     raise ReadTimeoutError(
    368         self, url, f"Read timed out. (read timeout={timeout_value})"
    369     ) from err
    371 # See the above comment about EAGAIN in Python 3.
    372 if hasattr(err, "errno") and err.errno in _blocking_errnos:


ReadTimeoutError: HTTPConnectionPool(host='localhost', port=38699): Read timed out. (read timeout=120)

Evaluate mask component layers

In the above map, use the layer control panel in the upper right corner to toggle layers on and off; layer names are the same as band names, for easy code referral. Note that the layers have a minimum zoom level of 9 to avoid resource issues that can occur when visualizing layers that depend on the ee.Image.reproject function (used during cloud shadow project and mask dilation).

Try changing the above CLD_PRB_THRESH, NIR_DRK_THRESH, CLD_PRJ_DIST, and BUFFER input variables and rerunning the previous cell to see how the results change. Find a good set of values for a given overpass and then try the procedure with a new overpass with different cloud conditions (this S2 SR image browser app is handy for quickly identifying images and determining image collection filter criteria). Try to identify a set of parameter values that balances cloud/cloud shadow commission and omission error for a range of cloud types. In the next section, we'll use the values to actually apply the mask to generate a cloud-free composite for 2020.

Apply cloud and cloud shadow mask

In this section we'll generate a cloud-free composite for the same region as above that represents mean reflectance for July and August, 2020.

Define collection filter and cloud mask parameters

We'll redefine the parameters to be a little more aggressive, i.e. decrease the cloud probability threshold, increase the cloud projection distance, and increase the buffer. These changes will increase cloud commission error (mask out some clear pixels), but since we will be compositing images from three months, there should be plenty of observations to complete the mosaic.

AOI = ee.Geometry.Point(-122.269, 45.701)
START_DATE = '2020-06-01'
END_DATE = '2020-09-01'
CLOUD_FILTER = 60
CLD_PRB_THRESH = 40
NIR_DRK_THRESH = 0.15
CLD_PRJ_DIST = 2
BUFFER = 100

Build a Sentinel-2 collection

Reassemble the S2-cloudless collection since the collection filter parameters have changed.

s2_sr_cld_col = get_s2_sr_cld_col(AOI, START_DATE, END_DATE)

Define cloud mask application function

Define a function to apply the cloud mask to each image in the collection.

def apply_cld_shdw_mask(img):
    # Subset the cloudmask band and invert it so clouds/shadow are 0, else 1.
    not_cld_shdw = img.select('cloudmask').Not()

    # Subset reflectance bands and update their masks, return the result.
    return img.select('B.*').updateMask(not_cld_shdw)

Process the collection

Add cloud and cloud shadow component bands to each image and then apply the mask to each image. Reduce the collection by median (in your application, you might consider using medoid reduction to build a composite from actual data values, instead of per-band statistics).

s2_sr_median = (s2_sr_cld_col.map(add_cld_shdw_mask)
                             .map(apply_cld_shdw_mask)
                             .median())

Display the cloud-free composite

Display the results. Be patient while the map renders, it may take a minute; ee.Image.reproject is forcing computations to happen at 100 and 20 m scales (i.e. it is not relying on appropriate pyramid level scales for analysis). The issues with ee.Image.reproject being resource-intensive in this case are mostly confined to interactive map viewing. Batch image exports and table reduction exports where the scale parameter is set to typical Sentinel-2 scales (10-60 m) are less affected.

# Create a folium map object.
center = AOI.centroid(10).coordinates().reverse().getInfo()
m = folium.Map(location=center, zoom_start=12)

# Add layers to the folium map.
m.add_ee_layer(s2_sr_median,
                {'bands': ['B4', 'B3', 'B2'], 'min': 0, 'max': 2500, 'gamma': 1.1},
                'S2 cloud-free mosaic', True, 1, 9)

# Add a layer control panel to the map.
m.add_child(folium.LayerControl())

# Display the map.
display(m)

---------------------------------------------------------------------------

timeout                                   Traceback (most recent call last)

File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/urllib3/connectionpool.py:534, in HTTPConnectionPool._make_request(self, conn, method, url, body, headers, retries, timeout, chunked, response_conn, preload_content, decode_content, enforce_content_length)
    533 try:
--> 534     response = conn.getresponse()
    535 except (BaseSSLError, OSError) as e:


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/urllib3/connection.py:516, in HTTPConnection.getresponse(self)
    515 # Get the response from http.client.HTTPConnection
--> 516 httplib_response = super().getresponse()
    518 try:


File /usr/local/.pyenv/versions/3.9.5/lib/python3.9/http/client.py:1345, in HTTPConnection.getresponse(self)
   1344 try:
-> 1345     response.begin()
   1346 except ConnectionError:


File /usr/local/.pyenv/versions/3.9.5/lib/python3.9/http/client.py:307, in HTTPResponse.begin(self)
    306 while True:
--> 307     version, status, reason = self._read_status()
    308     if status != CONTINUE:


File /usr/local/.pyenv/versions/3.9.5/lib/python3.9/http/client.py:268, in HTTPResponse._read_status(self)
    267 def _read_status(self):
--> 268     line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1")
    269     if len(line) > _MAXLINE:


File /usr/local/.pyenv/versions/3.9.5/lib/python3.9/socket.py:704, in SocketIO.readinto(self, b)
    703 try:
--> 704     return self._sock.recv_into(b)
    705 except timeout:


timeout: timed out


The above exception was the direct cause of the following exception:


ReadTimeoutError                          Traceback (most recent call last)

File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/IPython/core/formatters.py:922, in IPythonDisplayFormatter.__call__(self, obj)
    920 method = get_real_method(obj, self.print_method)
    921 if method is not None:
--> 922     method()
    923     return True


Cell In[1], line 96, in __set_up_ee_live_docs.<locals>.patch_ipython_display.<locals>.<lambda>(self)
     95 def patch_ipython_display(map_class, css_class):
---> 96   map_class._ipython_display_ = lambda self: display_png(self, css_class)


Cell In[1], line 61, in __set_up_ee_live_docs.<locals>.display_png(map_class, css_class)
     58 driver = webdriver.Firefox(options=options)
     60 map_class.save(tmp_path)
---> 61 driver.get(f"file:///{tmp_path}")
     62 driver.fullscreen_window()
     63 time.sleep(map_render_delay_secs)


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/selenium/webdriver/remote/webdriver.py:454, in WebDriver.get(self, url)
    436 def get(self, url: str) -> None:
    437     """Navigate the browser to the specified URL in the current window or
    438     tab.
    439 
   (...)
    452     >>> driver.get("https://example.com")
    453     """
--> 454     self.execute(Command.GET, {"url": url})


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/selenium/webdriver/remote/webdriver.py:427, in WebDriver.execute(self, driver_command, params)
    424     elif "sessionId" not in params:
    425         params["sessionId"] = self.session_id
--> 427 response = self.command_executor.execute(driver_command, params)
    428 if response:
    429     self.error_handler.check_response(response)


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/selenium/webdriver/remote/remote_connection.py:404, in RemoteConnection.execute(self, command, params)
    402 trimmed = self._trim_large_entries(params)
    403 LOGGER.debug("%s %s %s", command_info[0], url, str(trimmed))
--> 404 return self._request(command_info[0], url, body=data)


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/selenium/webdriver/remote/remote_connection.py:428, in RemoteConnection._request(self, method, url, body)
    425     body = None
    427 if self._client_config.keep_alive:
--> 428     response = self._conn.request(method, url, body=body, headers=headers, timeout=self._client_config.timeout)
    429     statuscode = response.status
    430 else:


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/urllib3/_request_methods.py:143, in RequestMethods.request(self, method, url, body, fields, headers, json, **urlopen_kw)
    135     return self.request_encode_url(
    136         method,
    137         url,
   (...)
    140         **urlopen_kw,
    141     )
    142 else:
--> 143     return self.request_encode_body(
    144         method, url, fields=fields, headers=headers, **urlopen_kw
    145     )


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/urllib3/_request_methods.py:278, in RequestMethods.request_encode_body(self, method, url, fields, headers, encode_multipart, multipart_boundary, **urlopen_kw)
    274     extra_kw["headers"].setdefault("Content-Type", content_type)
    276 extra_kw.update(urlopen_kw)
--> 278 return self.urlopen(method, url, **extra_kw)


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/urllib3/poolmanager.py:443, in PoolManager.urlopen(self, method, url, redirect, **kw)
    441     response = conn.urlopen(method, url, **kw)
    442 else:
--> 443     response = conn.urlopen(method, u.request_uri, **kw)
    445 redirect_location = redirect and response.get_redirect_location()
    446 if not redirect_location:


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/urllib3/connectionpool.py:841, in HTTPConnectionPool.urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, preload_content, decode_content, **response_kw)
    838 elif isinstance(new_e, (OSError, HTTPException)):
    839     new_e = ProtocolError("Connection aborted.", new_e)
--> 841 retries = retries.increment(
    842     method, url, error=new_e, _pool=self, _stacktrace=sys.exc_info()[2]
    843 )
    844 retries.sleep()
    846 # Keep track of the error for the retry warning.


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/urllib3/util/retry.py:474, in Retry.increment(self, method, url, response, error, _pool, _stacktrace)
    471 elif error and self._is_read_error(error):
    472     # Read retry?
    473     if read is False or method is None or not self._is_method_retryable(method):
--> 474         raise reraise(type(error), error, _stacktrace)
    475     elif read is not None:
    476         read -= 1


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/urllib3/util/util.py:39, in reraise(tp, value, tb)
     37     if value.__traceback__ is not tb:
     38         raise value.with_traceback(tb)
---> 39     raise value
     40 finally:
     41     value = None  # type: ignore[assignment]


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/urllib3/connectionpool.py:787, in HTTPConnectionPool.urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, preload_content, decode_content, **response_kw)
    784 response_conn = conn if not release_conn else None
    786 # Make the request on the HTTPConnection object
--> 787 response = self._make_request(
    788     conn,
    789     method,
    790     url,
    791     timeout=timeout_obj,
    792     body=body,
    793     headers=headers,
    794     chunked=chunked,
    795     retries=retries,
    796     response_conn=response_conn,
    797     preload_content=preload_content,
    798     decode_content=decode_content,
    799     **response_kw,
    800 )
    802 # Everything went great!
    803 clean_exit = True


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/urllib3/connectionpool.py:536, in HTTPConnectionPool._make_request(self, conn, method, url, body, headers, retries, timeout, chunked, response_conn, preload_content, decode_content, enforce_content_length)
    534     response = conn.getresponse()
    535 except (BaseSSLError, OSError) as e:
--> 536     self._raise_timeout(err=e, url=url, timeout_value=read_timeout)
    537     raise
    539 # Set properties that are used by the pooling layer.


File /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/urllib3/connectionpool.py:367, in HTTPConnectionPool._raise_timeout(self, err, url, timeout_value)
    364 """Is the error actually a timeout? Will raise a ReadTimeout or pass"""
    366 if isinstance(err, SocketTimeout):
--> 367     raise ReadTimeoutError(
    368         self, url, f"Read timed out. (read timeout={timeout_value})"
    369     ) from err
    371 # See the above comment about EAGAIN in Python 3.
    372 if hasattr(err, "errno") and err.errno in _blocking_errnos:


ReadTimeoutError: HTTPConnectionPool(host='localhost', port=35813): Read timed out. (read timeout=120)

Hopefully you now have a good sense for Sentinel-2 cloud masking in the cloud 😉 with Earth Engine.