Skip to content

earthlib.Unmix

Routines for performing spectral unmixing on earth engine images.

computeModeledSpectra(endmembers, fractions, n_bands)

Constructs a modeled spectrum for each pixel based on endmember fractions.

Parameters:

Name Type Description Default
endmembers list

a list of ee.List() items, each representing an endmember spectrum.

required
fractions Image

ee.Image output from .unmix() with the same number of bands as items in endmembers.

required
n_bands int

the number of reflectance bands used to compute the unmixing.

required

Returns:

Type Description
Image

an ee.Image with n_bands equal to the number of endmember bands.

Source code in earthlib/Unmix.py
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
def computeModeledSpectra(
    endmembers: list, fractions: ee.Image, n_bands: int
) -> ee.Image:
    """Constructs a modeled spectrum for each pixel based on endmember fractions.

    Args:
        endmembers: a list of ee.List() items, each representing an endmember spectrum.
        fractions: ee.Image output from .unmix() with the same number of bands as items in `endmembers`.
        n_bands: the number of reflectance bands used to compute the unmixing.

    Returns:
        an ee.Image with n_bands equal to the number of endmember bands.
    """
    # compute the number of endmember bands
    band_range = list(range(n_bands))
    band_names = [f"M{band:02d}" for band in band_range]

    # create a list to store each reflectance fraction
    refl_fraction_images = list()

    # loop through each endmember and mulitply the fraction estimated by the reflectance value
    for i, endmember in enumerate(endmembers):
        fraction = fractions.select([i])
        refl_fraction_list = [
            fraction.multiply(ee.Number(endmember.get(band))) for band in band_range
        ]
        refl_fraction_images.append(
            ee.ImageCollection.fromImages(refl_fraction_list)
            .toBands()
            .select(band_range, band_names)
        )

    # convert these images to an image collection and sum them together to reconstruct the spectrum
    modeled_reflectance = (
        ee.ImageCollection.fromImages(refl_fraction_images)
        .sum()
        .select(band_range, band_names)
    )

    return modeled_reflectance

computeSpectralRMSE(measured, modeled, n_bands)

Computes root mean squared error between measured and modeled spectra.

Parameters:

Name Type Description Default
measured Image

an ee.Image of measured reflectance.

required
modeled Image

an ee.Image of modeled reflectance.

required
n_bands int

the number of reflectance bands used to compute the unmixing.

required

Returns:

Name Type Description
rmse Image

a floating point ee.Image with pixel-wise RMSE values.

Source code in earthlib/Unmix.py
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
def computeSpectralRMSE(
    measured: ee.Image, modeled: ee.Image, n_bands: int
) -> ee.Image:
    """Computes root mean squared error between measured and modeled spectra.

    Args:
        measured: an ee.Image of measured reflectance.
        modeled: an ee.Image of modeled reflectance.
        n_bands: the number of reflectance bands used to compute the unmixing.

    Returns:
        rmse: a floating point ee.Image with pixel-wise RMSE values.
    """
    # harmonize band info to ensure element-wise computation
    band_range = list(range(n_bands))
    band_names = [f"M{band:02d}" for band in band_range]

    # compute rmse
    rmse = (
        measured.select(band_range, band_names)
        .subtract(modeled.select(band_range, band_names))
        .pow(2)
        .reduce(ee.Reducer.sum())
        .sqrt()
        .select([0], [RMSE])
    )

    return rmse

computeWeight(fractions, rmse_sum)

Computes the relative weight for an image's RMSE based on the sum of the global RMSE.

Parameters:

Name Type Description Default
fractions Image

a multi-band ee.Image object with an 'RMSE' band.

required
rmse_sum Image

a single-band ee.Image object with the global RMSE value.

required

Returns:

Type Description
Image

the input fractions image with a 'weight' band added.

Source code in earthlib/Unmix.py
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
def computeWeight(fractions: ee.Image, rmse_sum: ee.Image) -> ee.Image:
    """Computes the relative weight for an image's RMSE based on the sum of the global RMSE.

    Args:
        fractions: a multi-band ee.Image object with an 'RMSE' band.
        rmse_sum: a single-band ee.Image object with the global RMSE value.

    Returns:
        the input `fractions` image with a 'weight' band added.
    """
    rmse = fractions.select([RMSE])
    ratio = rmse.divide(rmse_sum).select([0], ["ratio"])
    weight = ee.Image(1).subtract(ratio).select([0], [WEIGHT])
    unweighted = fractions.addBands([weight])

    return unweighted

fractionalCover(img, endmembers, endmember_names, n_bands=None, shade_normalize=True)

Computes the percent cover of each endmember spectra.

Parameters:

Name Type Description Default
img Image

the ee.Image to unmix.

required
endmembers list

lists of ee.List objects, each element corresponding to a subType.

required
endmember_names list

list of names for each endmember. must match the number of lists passed.

required
n_bands int

number of reflectance bands used for unmixing.

None
shade_normalize bool

flag to apply shade normalization during unmixing.

True

Returns:

Name Type Description
unmixed Image

a 3-band image file in order of (soil-veg-impervious).

Source code in earthlib/Unmix.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def fractionalCover(
    img: ee.Image,
    endmembers: list,
    endmember_names: list,
    n_bands: int = None,
    shade_normalize: bool = True,
) -> ee.Image:
    """Computes the percent cover of each endmember spectra.

    Args:
        img: the ee.Image to unmix.
        endmembers: lists of ee.List objects, each element corresponding to a subType.
        endmember_names: list of names for each endmember. must match the number of lists passed.
        n_bands: number of reflectance bands used for unmixing.
        shade_normalize: flag to apply shade normalization during unmixing.

    Returns:
        unmixed: a 3-band image file in order of (soil-veg-impervious).
    """
    if n_bands is None:
        n_bands = len(list(img.bandNames().getInfo()))
    n_classes = len(endmembers)
    band_numbers = list(range(n_classes))
    shade = ee.List([0] * n_bands)

    # create a list of images to append and later convert to an image collection
    unmixed = list()

    # loop through each iteration and unmix each
    for spectra in zip(*endmembers):

        if shade_normalize:
            spectra += (shade,)

        unmixed_iter = img.unmix(spectra, True, True)

        # run the forward model to evaluate the fractional cover fit
        modeled_reflectance = computeModeledSpectra(spectra, unmixed_iter, n_bands)
        rmse = computeSpectralRMSE(img, modeled_reflectance, n_bands)

        # normalize by the observed shade fraction
        if shade_normalize:
            shade_fraction = unmixed_iter.select([n_classes]).subtract(1).abs()
            unmixed_iter = unmixed_iter.divide(shade_fraction)

        # rename the bands and append an rmse band
        unmixed.append(
            unmixed_iter.select(band_numbers, endmember_names).addBands(rmse)
        )

    # use the sum of rmse to weight each estimate
    rmse_sum = ee.Image(
        ee.ImageCollection.fromImages(unmixed).select([RMSE]).sum().select([0], ["SUM"])
    )
    unscaled = [computeWeight(fractions, rmse_sum) for fractions in unmixed]

    # use these weights to scale each unmixing estimate
    weight_sum = ee.Image(
        ee.ImageCollection.fromImages(unscaled).select([WEIGHT]).sum()
    )
    scaled = [
        weightedAverage(fractions, weight_sum, endmember_names)
        for fractions in unscaled
    ]

    # reduce it to a single image and return
    unmixed = ee.ImageCollection.fromImages(scaled).sum().toFloat()

    return unmixed

weightedAverage(fractions, weight_sum, band_names)

Computes an RMSE-weighted fractional cover image.

Parameters:

Name Type Description Default
fractions Image

a multi-band ee.Image object with a 'weight' band.

required
weight_sum Image

a single-band ee.Image object with the global weight sum.

required
band_names list

list of band names to apply the weighted average to

required

Returns:

Type Description
Image

a scaled fractional cover image.

Source code in earthlib/Unmix.py
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
def weightedAverage(
    fractions: ee.Image, weight_sum: ee.Image, band_names: list
) -> ee.Image:
    """Computes an RMSE-weighted fractional cover image.

    Args:
        fractions: a multi-band ee.Image object with a 'weight' band.
        weight_sum: a single-band ee.Image object with the global weight sum.
        band_names: list of band names to apply the weighted average to

    Returns:
        a scaled fractional cover image.
    """
    # harmonize band info
    band_range = list(range(len(band_names)))

    scaler = fractions.select([WEIGHT]).divide(weight_sum)
    weighted = fractions.select(band_range, band_names).multiply(scaler)

    return weighted