I was implementing the cloud masking procedure from the s2cloudless tutorial as recommended in this answer.
From the cloud-masked image collection, I wanted to extract some spectral indices, so I followed the structure of many other scripts and mapped a function over the image collection that applies normalizeDifference
on each image.
This resulted in the following error: AttributeError: 'Image' object has no attribute 'normalizeDifference'
.
I tried to verify whether it was due to the band names, but my request s2_cl_corr.first().bandNames().getInfo()
was also answered with an error.
How can properly apply the spectral indices to the cloud-masked image collection?
AOI = ee.Geometry.Point(-122.269, 45.701)
START_DATE = '2020-06-01'
END_DATE = '2020-09-01'
CLOUD_FILTER = 60
CLD_PRB_THRESH = 35
NIR_DRK_THRESH = 0.15
CLD_PRJ_DIST = 1
BUFFER = 50
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'
})
}))
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]))
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
# (potentially 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]))
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.addBands(is_cld_shdw)
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)
def add_index_bands(img):
# regular ndvi
ndvi = img.normalizeDifference(['B8', 'B4']).rename('NDVI')
# normalized difference red edge index
ndrei = img.normalizeDifference(['B8', 'B5']).rename('NDREI')
out_bands = ee.Image([ndvi, ndrei])
return img.addBands(out_bands)
s2_cl_corr = (get_s2_sr_cld_col(AOI, START_DATE, END_DATE)
.map(apply_cld_shdw_mask)
.map(add_cld_shdw_mask))
s2_with_bands = s2_cl_corr.map(add_index_bands)