Skip to content

earthlib.endmembers

Endmember spectra management tools

Spectra

Base class for endmember spectra management.

Source code in earthlib/endmembers.py
 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
 80
 81
 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
122
123
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
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
class Spectra:
    """Base class for endmember spectra management."""

    def __init__(
        self,
        data: np.ndarray | None,
        sensor: Sensor,
        metadata: pd.DataFrame | None = None,
        names: list[str] | None = None,
    ) -> None:
        """Endmember spectra initialization.

        Args:
            data: an array of spectral responses
                should be of shape (n_spectra, n_wavelengths).
                initiaalizes to zeros if None.
            sensor: the sensor object defining the instrument
                used for measurement
            names: list of names to assign to each spectrum
            metadata: dataframe containing metadata for each spectrum.
                Should have n_spectra rows.
                See earthlib.metadata.Schema for expected columns.
        """
        self.sensor = sensor.copy()
        self.metadata = metadata.copy() if metadata is not None else None

        self.data: np.ndarray
        if data is None:
            self.data = np.zeros((1, self.sensor.band_count), dtype=np.float32)
        else:
            self.data = data.copy()

        self.names: list[str]
        if names is None:
            self.names = ["spectrum_{}".format(i + 1) for i in range(len(self.data))]
        else:
            self.names = names.copy()

        # populated by earthlib.read.jfsp, which carries +/- stdev spectra
        self.spectra_stdevp: np.ndarray | None = None
        self.spectra_stdevm: np.ndarray | None = None

    def __len__(self) -> int:
        """Returns the number of spectra stored."""
        return len(self.data)

    def __getitem__(self, idx: SpectraIndex) -> "Spectra":
        """Index the spectra to return a subset.

        Supports integer, slice, integer-sequence and boolean-mask indexing.

        Args:
            idx: the index, slice, integer sequence or boolean mask to select.

        Returns:
            a new Spectra holding the selected subset.

        Example:
            ```python
            first_ten = library[:10]
            vegetation = library[library.metadata["LEVEL_2"] == "vegetation"]
            ```
        """
        positions = self._resolve_index(idx)
        data = self.data[positions]

        if self.metadata is not None:
            metadata = self.metadata.iloc[positions].reset_index(drop=True)
        else:
            metadata = None

        if self.names is not None:
            names = [self.names[i] for i in positions]
        else:
            names = None

        return Spectra(data=data, sensor=self.sensor, metadata=metadata, names=names)

    def _resolve_index(self, idx: SpectraIndex) -> np.ndarray:
        """Normalizes any supported index into an array of integer positions.

        Boolean masks are distinguished from integer sequences by dtype, so an
        integer sequence whose length happens to equal the number of spectra is
        no longer misread as a mask.
        """
        if isinstance(idx, slice):
            return np.arange(len(self.data))[idx]

        if isinstance(idx, (int, np.integer)):
            return np.array([idx], dtype=int)

        values = np.asarray(idx.values if isinstance(idx, pd.Series) else idx)

        if values.dtype == bool:
            if len(values) != len(self.data):
                raise IndexError(
                    f"Boolean mask of length {len(values)} does not match "
                    f"{len(self.data)} spectra."
                )
            return np.flatnonzero(values)

        return values.astype(int)

    def remove_water_bands(self, set_nan: bool = True) -> None:
        """Masks reflectance data from water vapor absorption bands.

        Wavelengths in the ranges of (1.35-1.46 um and 1.79-1.96 um) will be masked.
            Updates self.data in-place.

        Args:
            set_nan: set the water bands to NaN. False sets values to 0.
        """
        update_val = np.nan if set_nan else 0
        centers = np.asarray(self.sensor.band_centers)

        if self.sensor.wavelength_unit.lower() == "micrometers":
            water_vapor_bands = [[1.35, 1.46], [1.79, 1.96]]
        else:
            water_vapor_bands = [[1350.0, 1460.0], [1790.0, 1960.0]]

        # start with nir-swir1 transition
        gt = np.where(centers > water_vapor_bands[0][0])
        lt = np.where(centers < water_vapor_bands[0][1])
        nd = np.intersect1d(gt[0], lt[0])
        self.data[:, nd] = update_val

        # then swir1-swir2 transition
        gt = np.where(centers > water_vapor_bands[1][0])
        lt = np.where(centers < water_vapor_bands[1][1])
        nd = np.intersect1d(gt[0], lt[0])
        self.data[:, nd] = update_val

    def shortwave_band_idxs(self) -> np.ndarray:
        """Returns indices of the bands that encompass the shortwave range.

        This refers to the range (350 - 2500 nm).

        Returns:
            an index of bands to subset to the shortwave range.
        """
        # set range to return in nanometers
        shortwave_range = np.array([350.0, 2500.0])
        centers = np.asarray(self.sensor.band_centers)

        # normalize if wavelength units are different
        if self.sensor.wavelength_unit.lower() == "micrometers":
            shortwave_range /= 1000.0

        # find overlapping range
        gt = np.where(centers > shortwave_range[0])
        lt = np.where(centers < shortwave_range[1])
        overlap = np.intersect1d(gt[0], lt[0])

        # return output
        return overlap

    def brightness_normalize(self, inds: list[int] | None = None) -> None:
        """Brightness normalizes the spectra.

        Updates self.data, and subsets the sensor band centers and widths to the
        selected indices, in-place.

        Args:
            inds: the band indices to use for normalization. Uses all bands when
                None, or when the indices fall outside the available bands.

        Example:
            ```python
            library.brightness_normalize()
            ```
        """
        band_count = self.data.shape[-1]
        selected = list(range(band_count)) if inds is None else list(inds)

        if selected and (max(selected) >= band_count or min(selected) < 0):
            logger.warning("Invalid range set. using all spectra")
            selected = list(range(band_count))

        # normalize
        self.data = self.data[:, selected] / np.expand_dims(
            np.sqrt((self.data[:, selected] ** 2).sum(1)), 1
        )

        # subset band centers to the indices selected
        centers = np.asarray(self.sensor.band_centers)
        self.sensor.band_centers = centers[selected]

        # and fwhms, too
        if self.sensor.band_widths is not None:
            widths = np.asarray(self.sensor.band_widths)
            self.sensor.band_widths = widths[selected]

    def to_nanometers(self) -> None:
        """Converts the sensor band centers and widths to nanometers.

        Updates self.sensor.band_centers, self.sensor.band_widths and
        self.sensor.wavelength_unit in-place. Does nothing if already in
        nanometers.
        """
        if self.sensor.wavelength_unit.lower() != "micrometers":
            logger.warning(
                "Wavelength unit already in nanometers. No conversion applied."
            )
            return

        self.sensor.band_centers = np.asarray(self.sensor.band_centers) * 1000.0
        if self.sensor.band_widths is not None:
            self.sensor.band_widths = np.asarray(self.sensor.band_widths) * 1000.0
        self.sensor.wavelength_unit = "nanometers"

    def to_micrometers(self) -> None:
        """Converts the sensor band centers and widths to micrometers.

        Updates self.sensor.band_centers, self.sensor.band_widths and
        self.sensor.wavelength_unit in-place. Does nothing if already in
        micrometers.
        """
        if self.sensor.wavelength_unit.lower() != "nanometers":
            logger.warning(
                "Wavelength unit already in micrometers. No conversion applied."
            )
            return

        self.sensor.band_centers = np.asarray(self.sensor.band_centers) / 1000.0
        if self.sensor.band_widths is not None:
            self.sensor.band_widths = np.asarray(self.sensor.band_widths) / 1000.0
        self.sensor.wavelength_unit = "micrometers"

    def to_sensor(self, sensor: Sensor) -> "Spectra":
        """Resamples the spectra to a different sensor's band centers.

        Args:
            sensor: the sensor object defining the instrument
                to resample the spectra to.

        Returns:
            a new Spectra object with the resampled spectra and new sensor info.

        Example:
            ```python
            landsat_spectra = library.to_sensor(earthlib.sensors.Landsat8)
            ```
        """
        # create a band resampler for this collection
        resampler = spectral.BandResampler(
            self.sensor.band_centers,
            sensor.band_centers,
            fwhm1=self.sensor.band_widths,
            fwhm2=sensor.band_widths,
        )

        # loop through each spectrum and resample to the sensor wavelengths
        resampled = list()
        for i in range(self.data.shape[0]):
            spectrum = resampler(self.data[i, :])
            resampled.append(spectrum)

        # update the data and sensor info in place
        new_spectra = Spectra(
            data=np.array(resampled, dtype=np.float32),
            sensor=sensor.copy(),
            names=self.names.copy(),
            metadata=self.metadata.copy() if self.metadata is not None else None,
        )
        return new_spectra

    def subsample(self, n: int, by_type: str | None = None) -> "Spectra":
        """Subsamples n random spectra, with replacement.

        Args:
            n: the number of random spectra to select.
            by_type: if set, subsamples n spectra from this land cover type only.
                Uses the metadata DataFrame to filter by type.
                Get the valid type list using earthlib.list_types().

        Returns:
            subsampled Spectra data.

        Raises:
            ValueError: when by_type is set but no metadata is available.
            EndmemberError: when by_type is not a valid land cover type.

        Example:
            ```python
            vegetation = library.subsample(10, by_type="vegetation")
            ```
        """
        # pre-filter to just the spectra of the selected type
        if by_type is None:
            spectra = self.data
            names = self.names
            metadata = self.metadata

        else:
            if self.metadata is None:
                raise ValueError("Metadata is not set.")

            level = get_type_level(by_type)
            if level == 0:
                raise EndmemberError(
                    f"Invalid land cover type: {by_type}. Get valid values from earthlib.list_types()."
                )

            key = f"LEVEL_{level}"
            indices = self.metadata[key] == by_type
            spectra = self.data[indices, :]
            names = [self.names[idx] for idx in range(len(self.names)) if indices[idx]]
            metadata = self.metadata[indices].reset_index(drop=True)

        random_indices = np.random.randint(0, len(spectra), size=n)
        subsampled_spectra = spectra[random_indices, :]
        subsampled_names = [names[i] for i in random_indices]
        subsampled_metadata = (
            metadata.iloc[random_indices].reset_index(drop=True)
            if metadata is not None
            else None
        )

        endmembers = Spectra(
            data=subsampled_spectra,
            sensor=self.sensor.copy(),
            names=subsampled_names,
            metadata=subsampled_metadata,
        )

        return endmembers

    def to_sli(
        self,
        path: str,
        rows: list[int] | np.ndarray | None = None,
        bands: list[int] | np.ndarray | None = None,
    ) -> None:
        """Write the endmember spectra to an ENVI spectral library.

        Args:
            path: the output file path.
            rows: the row-wise indices of the array to write.
            bands: indices for which spectral to write

        Example:
            ```python
            library.subsample(10).to_sli("subsample.sli")
            ```
        """
        sli, hdr = read.envi_output_paths(path)

        # subset the data if specific indices are set
        spectra = self.data.copy()
        names = np.array(self.names)
        band_centers = np.asarray(self.sensor.band_centers).copy()

        if rows is not None:
            spectra = spectra[rows, :]
            names = names[rows]

        if bands is not None:
            spectra = spectra[:, bands]
            band_centers = band_centers[bands]

        # set up the metadata for the ENVI header file
        envi_metadata = {
            "samples": len(band_centers),
            "lines": len(names),
            "bands": 1,
            "data type": 4,
            "header offset": 0,
            "interleave": "bsq",
            "byte order": 0,
            "sensor type": self.sensor.name,
            "spectra names": names,
            "wavelength units": self.sensor.wavelength_unit,
            "wavelength": band_centers,
        }
        spectral.envi.write_envi_header(hdr, envi_metadata, is_library=True)

        # then write the spectral library
        with open(sli, "wb") as f:
            spectra.astype(np.float32).tofile(f)

    @classmethod
    def from_sli(
        cls,
        path: str,
        sensor: Sensor | None = None,
        metadata: pd.DataFrame | None = None,
    ) -> "Spectra":
        """Reads an ENVI spectral library file.

        Args:
            path: path to the spectral library file.
                Searches for a .hdr sidecar file.
            sensor: an earthlib.sensors.Sensor object specifying
                sensor information not included in the .hdr file.
            metadata: DataFrame containing metadata for each spectrum.

        Returns:
            Spectra containing the spectral data, sensor information, and metadata.

        Raises:
            FileNotFoundError: when no readable header accompanies the library.

        Example:
            ```python
            spectra = Spectra.from_sli("spectra.sli")
            ```
        """
        data, names, sensor = read.envi_library(path, sensor=sensor)

        return cls(data=data, sensor=sensor, names=names, metadata=metadata)

    def format_output_paths(self, path: str) -> tuple[str, str]:
        """Formats the output paths for the spectral library and header.

        Args:
            path: the base file path (with or without extension).

        Returns:
            A tuple containing the paths for the spectral library and header.
        """
        return read.envi_output_paths(path)

__getitem__(idx)

Index the spectra to return a subset.

Supports integer, slice, integer-sequence and boolean-mask indexing.

Parameters:

Name Type Description Default
idx SpectraIndex

the index, slice, integer sequence or boolean mask to select.

required

Returns:

Type Description
Spectra

a new Spectra holding the selected subset.

Example
first_ten = library[:10]
vegetation = library[library.metadata["LEVEL_2"] == "vegetation"]
Source code in earthlib/endmembers.py
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def __getitem__(self, idx: SpectraIndex) -> "Spectra":
    """Index the spectra to return a subset.

    Supports integer, slice, integer-sequence and boolean-mask indexing.

    Args:
        idx: the index, slice, integer sequence or boolean mask to select.

    Returns:
        a new Spectra holding the selected subset.

    Example:
        ```python
        first_ten = library[:10]
        vegetation = library[library.metadata["LEVEL_2"] == "vegetation"]
        ```
    """
    positions = self._resolve_index(idx)
    data = self.data[positions]

    if self.metadata is not None:
        metadata = self.metadata.iloc[positions].reset_index(drop=True)
    else:
        metadata = None

    if self.names is not None:
        names = [self.names[i] for i in positions]
    else:
        names = None

    return Spectra(data=data, sensor=self.sensor, metadata=metadata, names=names)

__init__(data, sensor, metadata=None, names=None)

Endmember spectra initialization.

Parameters:

Name Type Description Default
data ndarray | None

an array of spectral responses should be of shape (n_spectra, n_wavelengths). initiaalizes to zeros if None.

required
sensor Sensor

the sensor object defining the instrument used for measurement

required
names list[str] | None

list of names to assign to each spectrum

None
metadata DataFrame | None

dataframe containing metadata for each spectrum. Should have n_spectra rows. See earthlib.metadata.Schema for expected columns.

None
Source code in earthlib/endmembers.py
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
def __init__(
    self,
    data: np.ndarray | None,
    sensor: Sensor,
    metadata: pd.DataFrame | None = None,
    names: list[str] | None = None,
) -> None:
    """Endmember spectra initialization.

    Args:
        data: an array of spectral responses
            should be of shape (n_spectra, n_wavelengths).
            initiaalizes to zeros if None.
        sensor: the sensor object defining the instrument
            used for measurement
        names: list of names to assign to each spectrum
        metadata: dataframe containing metadata for each spectrum.
            Should have n_spectra rows.
            See earthlib.metadata.Schema for expected columns.
    """
    self.sensor = sensor.copy()
    self.metadata = metadata.copy() if metadata is not None else None

    self.data: np.ndarray
    if data is None:
        self.data = np.zeros((1, self.sensor.band_count), dtype=np.float32)
    else:
        self.data = data.copy()

    self.names: list[str]
    if names is None:
        self.names = ["spectrum_{}".format(i + 1) for i in range(len(self.data))]
    else:
        self.names = names.copy()

    # populated by earthlib.read.jfsp, which carries +/- stdev spectra
    self.spectra_stdevp: np.ndarray | None = None
    self.spectra_stdevm: np.ndarray | None = None

__len__()

Returns the number of spectra stored.

Source code in earthlib/endmembers.py
61
62
63
def __len__(self) -> int:
    """Returns the number of spectra stored."""
    return len(self.data)

brightness_normalize(inds=None)

Brightness normalizes the spectra.

Updates self.data, and subsets the sensor band centers and widths to the selected indices, in-place.

Parameters:

Name Type Description Default
inds list[int] | None

the band indices to use for normalization. Uses all bands when None, or when the indices fall outside the available bands.

None
Example
library.brightness_normalize()
Source code in earthlib/endmembers.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
def brightness_normalize(self, inds: list[int] | None = None) -> None:
    """Brightness normalizes the spectra.

    Updates self.data, and subsets the sensor band centers and widths to the
    selected indices, in-place.

    Args:
        inds: the band indices to use for normalization. Uses all bands when
            None, or when the indices fall outside the available bands.

    Example:
        ```python
        library.brightness_normalize()
        ```
    """
    band_count = self.data.shape[-1]
    selected = list(range(band_count)) if inds is None else list(inds)

    if selected and (max(selected) >= band_count or min(selected) < 0):
        logger.warning("Invalid range set. using all spectra")
        selected = list(range(band_count))

    # normalize
    self.data = self.data[:, selected] / np.expand_dims(
        np.sqrt((self.data[:, selected] ** 2).sum(1)), 1
    )

    # subset band centers to the indices selected
    centers = np.asarray(self.sensor.band_centers)
    self.sensor.band_centers = centers[selected]

    # and fwhms, too
    if self.sensor.band_widths is not None:
        widths = np.asarray(self.sensor.band_widths)
        self.sensor.band_widths = widths[selected]

format_output_paths(path)

Formats the output paths for the spectral library and header.

Parameters:

Name Type Description Default
path str

the base file path (with or without extension).

required

Returns:

Type Description
tuple[str, str]

A tuple containing the paths for the spectral library and header.

Source code in earthlib/endmembers.py
430
431
432
433
434
435
436
437
438
439
def format_output_paths(self, path: str) -> tuple[str, str]:
    """Formats the output paths for the spectral library and header.

    Args:
        path: the base file path (with or without extension).

    Returns:
        A tuple containing the paths for the spectral library and header.
    """
    return read.envi_output_paths(path)

from_sli(path, sensor=None, metadata=None) classmethod

Reads an ENVI spectral library file.

Parameters:

Name Type Description Default
path str

path to the spectral library file. Searches for a .hdr sidecar file.

required
sensor Sensor | None

an earthlib.sensors.Sensor object specifying sensor information not included in the .hdr file.

None
metadata DataFrame | None

DataFrame containing metadata for each spectrum.

None

Returns:

Type Description
Spectra

Spectra containing the spectral data, sensor information, and metadata.

Raises:

Type Description
FileNotFoundError

when no readable header accompanies the library.

Example
spectra = Spectra.from_sli("spectra.sli")
Source code in earthlib/endmembers.py
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
@classmethod
def from_sli(
    cls,
    path: str,
    sensor: Sensor | None = None,
    metadata: pd.DataFrame | None = None,
) -> "Spectra":
    """Reads an ENVI spectral library file.

    Args:
        path: path to the spectral library file.
            Searches for a .hdr sidecar file.
        sensor: an earthlib.sensors.Sensor object specifying
            sensor information not included in the .hdr file.
        metadata: DataFrame containing metadata for each spectrum.

    Returns:
        Spectra containing the spectral data, sensor information, and metadata.

    Raises:
        FileNotFoundError: when no readable header accompanies the library.

    Example:
        ```python
        spectra = Spectra.from_sli("spectra.sli")
        ```
    """
    data, names, sensor = read.envi_library(path, sensor=sensor)

    return cls(data=data, sensor=sensor, names=names, metadata=metadata)

remove_water_bands(set_nan=True)

Masks reflectance data from water vapor absorption bands.

Wavelengths in the ranges of (1.35-1.46 um and 1.79-1.96 um) will be masked. Updates self.data in-place.

Parameters:

Name Type Description Default
set_nan bool

set the water bands to NaN. False sets values to 0.

True
Source code in earthlib/endmembers.py
122
123
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
def remove_water_bands(self, set_nan: bool = True) -> None:
    """Masks reflectance data from water vapor absorption bands.

    Wavelengths in the ranges of (1.35-1.46 um and 1.79-1.96 um) will be masked.
        Updates self.data in-place.

    Args:
        set_nan: set the water bands to NaN. False sets values to 0.
    """
    update_val = np.nan if set_nan else 0
    centers = np.asarray(self.sensor.band_centers)

    if self.sensor.wavelength_unit.lower() == "micrometers":
        water_vapor_bands = [[1.35, 1.46], [1.79, 1.96]]
    else:
        water_vapor_bands = [[1350.0, 1460.0], [1790.0, 1960.0]]

    # start with nir-swir1 transition
    gt = np.where(centers > water_vapor_bands[0][0])
    lt = np.where(centers < water_vapor_bands[0][1])
    nd = np.intersect1d(gt[0], lt[0])
    self.data[:, nd] = update_val

    # then swir1-swir2 transition
    gt = np.where(centers > water_vapor_bands[1][0])
    lt = np.where(centers < water_vapor_bands[1][1])
    nd = np.intersect1d(gt[0], lt[0])
    self.data[:, nd] = update_val

shortwave_band_idxs()

Returns indices of the bands that encompass the shortwave range.

This refers to the range (350 - 2500 nm).

Returns:

Type Description
ndarray

an index of bands to subset to the shortwave range.

Source code in earthlib/endmembers.py
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
def shortwave_band_idxs(self) -> np.ndarray:
    """Returns indices of the bands that encompass the shortwave range.

    This refers to the range (350 - 2500 nm).

    Returns:
        an index of bands to subset to the shortwave range.
    """
    # set range to return in nanometers
    shortwave_range = np.array([350.0, 2500.0])
    centers = np.asarray(self.sensor.band_centers)

    # normalize if wavelength units are different
    if self.sensor.wavelength_unit.lower() == "micrometers":
        shortwave_range /= 1000.0

    # find overlapping range
    gt = np.where(centers > shortwave_range[0])
    lt = np.where(centers < shortwave_range[1])
    overlap = np.intersect1d(gt[0], lt[0])

    # return output
    return overlap

subsample(n, by_type=None)

Subsamples n random spectra, with replacement.

Parameters:

Name Type Description Default
n int

the number of random spectra to select.

required
by_type str | None

if set, subsamples n spectra from this land cover type only. Uses the metadata DataFrame to filter by type. Get the valid type list using earthlib.list_types().

None

Returns:

Type Description
Spectra

subsampled Spectra data.

Raises:

Type Description
ValueError

when by_type is set but no metadata is available.

EndmemberError

when by_type is not a valid land cover type.

Example
vegetation = library.subsample(10, by_type="vegetation")
Source code in earthlib/endmembers.py
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
def subsample(self, n: int, by_type: str | None = None) -> "Spectra":
    """Subsamples n random spectra, with replacement.

    Args:
        n: the number of random spectra to select.
        by_type: if set, subsamples n spectra from this land cover type only.
            Uses the metadata DataFrame to filter by type.
            Get the valid type list using earthlib.list_types().

    Returns:
        subsampled Spectra data.

    Raises:
        ValueError: when by_type is set but no metadata is available.
        EndmemberError: when by_type is not a valid land cover type.

    Example:
        ```python
        vegetation = library.subsample(10, by_type="vegetation")
        ```
    """
    # pre-filter to just the spectra of the selected type
    if by_type is None:
        spectra = self.data
        names = self.names
        metadata = self.metadata

    else:
        if self.metadata is None:
            raise ValueError("Metadata is not set.")

        level = get_type_level(by_type)
        if level == 0:
            raise EndmemberError(
                f"Invalid land cover type: {by_type}. Get valid values from earthlib.list_types()."
            )

        key = f"LEVEL_{level}"
        indices = self.metadata[key] == by_type
        spectra = self.data[indices, :]
        names = [self.names[idx] for idx in range(len(self.names)) if indices[idx]]
        metadata = self.metadata[indices].reset_index(drop=True)

    random_indices = np.random.randint(0, len(spectra), size=n)
    subsampled_spectra = spectra[random_indices, :]
    subsampled_names = [names[i] for i in random_indices]
    subsampled_metadata = (
        metadata.iloc[random_indices].reset_index(drop=True)
        if metadata is not None
        else None
    )

    endmembers = Spectra(
        data=subsampled_spectra,
        sensor=self.sensor.copy(),
        names=subsampled_names,
        metadata=subsampled_metadata,
    )

    return endmembers

to_micrometers()

Converts the sensor band centers and widths to micrometers.

Updates self.sensor.band_centers, self.sensor.band_widths and self.sensor.wavelength_unit in-place. Does nothing if already in micrometers.

Source code in earthlib/endmembers.py
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
def to_micrometers(self) -> None:
    """Converts the sensor band centers and widths to micrometers.

    Updates self.sensor.band_centers, self.sensor.band_widths and
    self.sensor.wavelength_unit in-place. Does nothing if already in
    micrometers.
    """
    if self.sensor.wavelength_unit.lower() != "nanometers":
        logger.warning(
            "Wavelength unit already in micrometers. No conversion applied."
        )
        return

    self.sensor.band_centers = np.asarray(self.sensor.band_centers) / 1000.0
    if self.sensor.band_widths is not None:
        self.sensor.band_widths = np.asarray(self.sensor.band_widths) / 1000.0
    self.sensor.wavelength_unit = "micrometers"

to_nanometers()

Converts the sensor band centers and widths to nanometers.

Updates self.sensor.band_centers, self.sensor.band_widths and self.sensor.wavelength_unit in-place. Does nothing if already in nanometers.

Source code in earthlib/endmembers.py
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
def to_nanometers(self) -> None:
    """Converts the sensor band centers and widths to nanometers.

    Updates self.sensor.band_centers, self.sensor.band_widths and
    self.sensor.wavelength_unit in-place. Does nothing if already in
    nanometers.
    """
    if self.sensor.wavelength_unit.lower() != "micrometers":
        logger.warning(
            "Wavelength unit already in nanometers. No conversion applied."
        )
        return

    self.sensor.band_centers = np.asarray(self.sensor.band_centers) * 1000.0
    if self.sensor.band_widths is not None:
        self.sensor.band_widths = np.asarray(self.sensor.band_widths) * 1000.0
    self.sensor.wavelength_unit = "nanometers"

to_sensor(sensor)

Resamples the spectra to a different sensor's band centers.

Parameters:

Name Type Description Default
sensor Sensor

the sensor object defining the instrument to resample the spectra to.

required

Returns:

Type Description
Spectra

a new Spectra object with the resampled spectra and new sensor info.

Example
landsat_spectra = library.to_sensor(earthlib.sensors.Landsat8)
Source code in earthlib/endmembers.py
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
def to_sensor(self, sensor: Sensor) -> "Spectra":
    """Resamples the spectra to a different sensor's band centers.

    Args:
        sensor: the sensor object defining the instrument
            to resample the spectra to.

    Returns:
        a new Spectra object with the resampled spectra and new sensor info.

    Example:
        ```python
        landsat_spectra = library.to_sensor(earthlib.sensors.Landsat8)
        ```
    """
    # create a band resampler for this collection
    resampler = spectral.BandResampler(
        self.sensor.band_centers,
        sensor.band_centers,
        fwhm1=self.sensor.band_widths,
        fwhm2=sensor.band_widths,
    )

    # loop through each spectrum and resample to the sensor wavelengths
    resampled = list()
    for i in range(self.data.shape[0]):
        spectrum = resampler(self.data[i, :])
        resampled.append(spectrum)

    # update the data and sensor info in place
    new_spectra = Spectra(
        data=np.array(resampled, dtype=np.float32),
        sensor=sensor.copy(),
        names=self.names.copy(),
        metadata=self.metadata.copy() if self.metadata is not None else None,
    )
    return new_spectra

to_sli(path, rows=None, bands=None)

Write the endmember spectra to an ENVI spectral library.

Parameters:

Name Type Description Default
path str

the output file path.

required
rows list[int] | ndarray | None

the row-wise indices of the array to write.

None
bands list[int] | ndarray | None

indices for which spectral to write

None
Example
library.subsample(10).to_sli("subsample.sli")
Source code in earthlib/endmembers.py
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
def to_sli(
    self,
    path: str,
    rows: list[int] | np.ndarray | None = None,
    bands: list[int] | np.ndarray | None = None,
) -> None:
    """Write the endmember spectra to an ENVI spectral library.

    Args:
        path: the output file path.
        rows: the row-wise indices of the array to write.
        bands: indices for which spectral to write

    Example:
        ```python
        library.subsample(10).to_sli("subsample.sli")
        ```
    """
    sli, hdr = read.envi_output_paths(path)

    # subset the data if specific indices are set
    spectra = self.data.copy()
    names = np.array(self.names)
    band_centers = np.asarray(self.sensor.band_centers).copy()

    if rows is not None:
        spectra = spectra[rows, :]
        names = names[rows]

    if bands is not None:
        spectra = spectra[:, bands]
        band_centers = band_centers[bands]

    # set up the metadata for the ENVI header file
    envi_metadata = {
        "samples": len(band_centers),
        "lines": len(names),
        "bands": 1,
        "data type": 4,
        "header offset": 0,
        "interleave": "bsq",
        "byte order": 0,
        "sensor type": self.sensor.name,
        "spectra names": names,
        "wavelength units": self.sensor.wavelength_unit,
        "wavelength": band_centers,
    }
    spectral.envi.write_envi_header(hdr, envi_metadata, is_library=True)

    # then write the spectral library
    with open(sli, "wb") as f:
        spectra.astype(np.float32).tofile(f)

getTypeLevel(Type)

Deprecated alias for get_type_level.

Source code in earthlib/endmembers.py
494
495
496
497
498
499
500
501
def getTypeLevel(Type: str) -> int:
    """Deprecated alias for `get_type_level`."""
    warnings.warn(
        "getTypeLevel() is deprecated and will be removed in v2.0.0. Use get_type_level().",
        DeprecationWarning,
        stacklevel=2,
    )
    return get_type_level(Type)

get_type_level(type_name)

Checks whether a spectral data type is available in the endmember library.

Parameters:

Name Type Description Default
type_name str

the type of spectra to select.

required

Returns:

Name Type Description
level int

the metadata "level" of the group for subsetting. returns 0 if not found.

Example
level = get_type_level("vegetation")
Source code in earthlib/endmembers.py
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
def get_type_level(type_name: str) -> int:
    """Checks whether a spectral data type is available in the endmember library.

    Args:
        type_name: the type of spectra to select.

    Returns:
        level: the metadata "level" of the group for subsetting. returns 0 if not found.

    Example:
        ```python
        level = get_type_level("vegetation")
        ```
    """
    for i in range(4):
        level = i + 1
        available_types = list_types(level=level)
        if type_name in available_types:
            return level

    return 0

listTypes(level=2)

Deprecated alias for list_types.

Source code in earthlib/endmembers.py
484
485
486
487
488
489
490
491
def listTypes(level: int = 2) -> list:
    """Deprecated alias for `list_types`."""
    warnings.warn(
        "listTypes() is deprecated and will be removed in v2.0.0. Use list_types().",
        DeprecationWarning,
        stacklevel=2,
    )
    return list_types(level=level)

list_types(level=2)

Returns a list of the spectral classification types.

Parameters:

Name Type Description Default
level int

the level of spectral classification specificity to return. Supports integers 1-4.

2

Returns:

Name Type Description
classes list

a list of spectral data types referenced throughout this package.

Example
cover_types = list_types(level=2)
Source code in earthlib/endmembers.py
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
def list_types(level: int = 2) -> list:
    """Returns a list of the spectral classification types.

    Args:
        level: the level of spectral classification specificity to return. Supports integers 1-4.

    Returns:
        classes: a list of spectral data types referenced throughout this package.

    Example:
        ```python
        cover_types = list_types(level=2)
        ```
    """
    key = f"LEVEL_{level}"
    types = list(metadata[key].unique())
    return types