1

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)

2 Answers 2

2

You simply misspelled the function's name. It's normalizedDifference

0

I think you have used normalizeDifference when it should be normalizedDifference:

https://developers.google.com/earth-engine/apidocs/ee-image-normalizeddifference

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.