I am currently evaluating my cloud shadow masking options for Sentinel-2 data. One of the options I am exploring is using an example script published by Google that projects cloud shadow locations based on the s2cloudless mask in Earth Engine, dark pixels, and the MEAN_SOLAR_AZIMUTH_ANGLE. Google has provided an example script here: https://developers.google.com/earth-engine/tutorials/community/sentinel-2-s2cloudless. When I use the code in the link for my data, the shadow masks are very inaccurate however. And I would really like to understand how I could improve this. Let's take the following image as an example:
This image is in EPSG:32632 in Earth Engine with transform [10, 0, 300000, 0, -10, 5900040]. I add the following bands to the image using the code in the link: 'probability' (cloud probabilities from s2cloudless), 'clouds' (pixels with a threshold over 30 in the 'probability' mask), 'dark_pixels' (dark pixels that are considered potential shadows based on threshold of 1500 in NIR band), 'cloud_transform' (the transform to project shadows from clouds), 'shadows' (the pixels considered shadows: overlap of 'dark_pixels' and cloud transform), 'cloudmask' (a combination of shadows and clouds as mask). The issue is that the shadows seem inaccurate. For the above image, I plotted some of the aforementioned bands, which look as follows:
where the first image are the dark pixels, the second is the cloud mask (cloud probabilities over 30), the third picture is the "cloud transform" band, and the last picture is the "shadow" band (which is the overlap of the dark pixels with the cloud transform). As you can see, the dark_pixels and cloud_mask look pretty good. The projection and final result however are completely off. When I look at the plots, it seems something is not right with the projection (third picture) as the shadow of the cloud (which is the blob in the top right) in the projection is rotated in a way that does not correspond with the truth at all. It almost looks mirrored in shape and is located too far to the right. So my guess is either something is wrong with how "shadow_azimuth" in the code below is calculated, or how "cld_proj" is defined, or both. I've been changing some of the values to see if it helps, but I do not completely understand yet what I can do here. I hope there is someone that has experience with this and can help me, or explain to me where things might be going wrong.
Interesting is that in EE's code editor and map display, this does not happen (see answers below). But when I copy the code into Python (and make it Pythonic) the cloud transform gets twisted. So there seems to be a difference between EE's code editor output, and what I get from for example ee's getDownloadUrl() function. See link here for the Python code.
def add_shadow_bands(img, NIR_DRK_THRESH=0.15, CLD_PRJ_DIST=1.5):
# 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(1).projection(), 'scale': 20})
.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]))
In case it helps, the image is part of the satellite image in Earth Engine with the following id: COPERNICUS/S2_SR/20210405T105021_20210405T105323_T32ULD.
POLYGON ((315182.01234129263 5873848.984715646, 317007.7388380545 5873848.984715646, 317007.7388380545 5875589.032606219, 315182.01234129263 5875589.032606219, 315182.01234129263 5873848.984715646))
. They're in EPSG:32632. You can read them as a shapely geometry and then use:meadow_geom_ee = ee.Geometry.Polygon(mapping(meadow_envelope_geom)['coordinates'], proj='epsg:32632', evenOdd=False)
where the meadow_envelope_geom is the shapely object.