Skip to content

datasets module

hydrafloods.datasets.Dataset

Base dataset class used to define an EE image collection by datetime and geographic region

A dataset wraps an ee.ImageCollection by applying the spatial and temporal filtering upon init. Provides utility functionality to make working with and managing image collections less verbose

Examples:

Create a dataset object for Sentinel-1 data over Alabama for 2019

>>> ds = Dataset(
...    region = ee.Geometry.Rectangle([
           -88.473227,
           30.223334,
           -84.88908,
           35.008028
       ]),
...    start_time = "2019-01-01",
...    end_time = "2020-01-01",
...    asset_id = "COPERNICUS/S1_GRD"
... )
>>> ds
HYDRAFloods Dataset:
{'asset_id': 'COPERNICUS/S1_GRD',
'end_time': '2020-01-01',
'name': 'Dataset',
'region': [[[...], [...], [...], [...], [...]]],
'start_time': '2019-01-01'}
Source code in hydrafloods/datasets.py
class Dataset:
    """Base dataset class used to define an EE image collection by datetime and geographic region

    A dataset wraps an ee.ImageCollection by applying the spatial and temporal filtering upon init.
    Provides utility functionality to make working with and managing image collections less verbose

    Example:
        Create a dataset object for Sentinel-1 data over Alabama for 2019
        >>> ds = Dataset(
        ...    region = ee.Geometry.Rectangle([
                   -88.473227,
                   30.223334,
                   -84.88908,
                   35.008028
               ]),
        ...    start_time = "2019-01-01",
        ...    end_time = "2020-01-01",
        ...    asset_id = "COPERNICUS/S1_GRD"
        ... )
        >>> ds
        HYDRAFloods Dataset:
        {'asset_id': 'COPERNICUS/S1_GRD',
        'end_time': '2020-01-01',
        'name': 'Dataset',
        'region': [[[...], [...], [...], [...], [...]]],
        'start_time': '2019-01-01'}
    """

    def __init__(self, region, start_time, end_time, asset_id, use_qa=False):
        """Initialize Dataset class

        args:
            region (ee.Geometry): earth engine geometry object to filter image collection by
            start_time (str | datetime.datetime): start time used to filter image collection
            end_time (str | datetime.datetime): end time used to filter image collection
            asset_id (str): asset id of earth engine collection
            use_qa (bool, optional): boolean to determine to use an internal function qa().
                Used for definining custom dataset objects

        raises:
            AttributeError: if qa() method is not defined and use_qa is True
        """

        # TODO: add exceptions to check datatypes
        self.region = region  # dtype = ee.Geometry
        self.start_time = start_time
        self.end_time = end_time
        self.asset_id = asset_id
        self.use_qa = use_qa

        # dictionary mapping of band names used to harmonized optical datasets to same names
        self.BANDREMAP = ee.Dictionary(
            {
                "landsat7": ee.List(["SR_B1", "SR_B2", "SR_B3", "SR_B4", "SR_B5", "SR_B7"]),
                "landsat8": ee.List(["SR_B2", "SR_B3", "SR_B4", "SR_B5", "SR_B6", "SR_B7"]),
                "viirs": ee.List(["M2", "M4", "I1", "I2", "I3", "M11"]),
                "sen2": ee.List(["B2", "B3", "B4", "B8", "B11", "B12"]),
                "modis": ee.List(
                    [
                        "sur_refl_b03",
                        "sur_refl_b04",
                        "sur_refl_b01",
                        "sur_refl_b02",
                        "sur_refl_b06",
                        "sur_refl_b07",
                    ]
                ),
                "new": ee.List(["blue", "green", "red", "nir", "swir1", "swir2"]),
            }
        )

        # get the image collection and filter by geographic region and date time
        imgcollection = (
            ee.ImageCollection(self.asset_id)
            .filterBounds(self.region)
            .filterDate(self.start_time, self.end_time)
        )

        # check if to apply arbitrary qa process on collection
        # qa function can be defined in custom objects extending dataset
        if self.use_qa:
            try:
                imgcollection = imgcollection.map(self.qa)
            except AttributeError:
                raise AttributeError(
                    "qa() method is not defined...please define one or set `use_qa` to False"
                )

        self.collection = imgcollection

    def __repr__(self):
        # format datetime information
        if isinstance(self.start_time, datetime.datetime):
            ststr = self.start_time.strftime("%Y-%m-%d")
        else:
            ststr = self.start_time

        if isinstance(self.end_time, datetime.datetime):
            etstr = self.end_time.strftime("%Y-%m-%d")
        else:
            etstr = self.end_time

        # create dict of dataset information
        objDict = {
            "name": self.__class__.__name__,
            "asset_id": self.asset_id,
            "start_time": ststr,
            "end_time": etstr,
            "region": self.region.coordinates().getInfo(),
        }

        # pretty format dict and return information
        strRepr = pformat(objDict, depth=3)
        return f"HYDRAFloods Dataset:\n{strRepr}"

    @property
    def collection(self):
        """image collection object property wrapped by dataset"""
        return self._collection

    @collection.setter
    def collection(self, value):
        """setter function for collection property"""
        self._collection = value
        return

    @property
    def n_images(self):
        """Number of images contained in the dataset"""
        return self.collection.size().getInfo()

    @property
    def dates(self):
        """Dates of imagery contained in the image collection"""
        eeDates = self.collection.aggregate_array("system:time_start").map(
            lambda x: ee.Date(x).format("YYYY-MM-dd HH:mm:ss.SSS")
        )
        return eeDates.getInfo()

    @staticmethod
    def from_imgcollection(img_collection):
        """Static method to convert an ee.ImageCollection object to a hf.Dataset object.
        This method will take some time as it uses computed ee Objects from the image collection
        propeties to populate the Dataset object properties (passing info from server to client)

        args:
            img_collection (ee.ImageCollection): computed ee.ImageCollection object to create a hf.Dataset

        returns:
            hf.Dataset: dataset object with property information directly from the ee.ImageCollection
        """

        # get region and date information
        region = (
            img_collection.map(geeutils.get_geoms)
            .union(maxError=100)
            .geometry(maxError=100)
        )
        # convert ee.Date info to string format
        dates = (
            img_collection.aggregate_array("system:time_start")
            .sort()
            .map(lambda x: ee.Date(x).format("YYYY-MM-dd HH:mm:ss.S"))
        )

        # pull the date info to local strings
        start_time = dates.get(0).getInfo()
        end_time = dates.get(-1).getInfo()

        # get the collection id for `.asset_id` property
        collection_id = img_collection.get("system:id").getInfo()
        if collection_id == "None":
            collection_id = "Custom ImageCollection"

        # make a dummy dataset
        dummy_ds = Dataset(
            region,
            start_time,
            end_time,
            asset_id="NOAA/VIIRS/001/VNP09GA",
            use_qa=False,
        )

        # override the dummy dataset information with the correct data fr
        dummy_ds.asset_id = collection_id
        dummy_ds.collection = img_collection

        return dummy_ds

    def qa(
        self,
    ):
        return

    def _inplace_wrapper(self, collection, inplace):
        """Private helper function to replace the collection info for a class
        typically used when other class methods have inplace
        """
        if inplace:
            self.collection = collection
            return
        else:
            outCls = self.copy()
            outCls.collection = collection
            return outCls

    def copy(self):
        """returns a deep copy of the hydrafloods dataset class"""
        return copy.deepcopy(self)

    def apply_func(self, func, inplace=False, *args, **kwargs):
        """Wrapper method to apply a function to all of the image in the dataset.
        Makes a copy of the collection and reassigns the image collection property.
        Function must accept an ee.ImageCollection and return an ee.ImageCollection

        args:
            func (object): Function to map across image collection. Function must accept ee.Image as first argument
            inplace (bool, optional): define whether to return another dataset object or update inplace. default = False
            **kwargs: arbitrary keyword to pass to `func`

        returns:
            Dataset | None: copy of class with results from `func` as image within collection property
        """

        # get a partial function to map over imagery with the keywords applied
        # expects that the first positional arg is an
        func = partial(func, **kwargs)

        out_coll = self.collection.map(func)

        return self._inplace_wrapper(out_coll, inplace)

    def apply(self, func, inplace=False, *args, **kwargs):
        """Alias for the `apply_func` method

        args:
            func (object): Function to map across image collection. Function must accept ee.Image as first argument
            inplace (bool, optional): define whether to return another dataset object or update inplace. default = False
            **kwargs: arbitrary keyword to pass to `func`

        returns:
            Dataset | None: copy of class with results from `func` as image within collection property
        """

        return self.apply_func(func, inplace, *args, *kwargs)

    def filter(self, filter, inplace=False):
        """Wrapper method for applying a filter to a datset collection.

        args:
            filter (ee.Filter): an `ee.Filter` object to apply to the dataset collection.

        returns:
            Dataset | None: returns the dataset with the filtered collection.
        """

        filtered = self.collection.filter(filter)

        return self._inplace_wrapper(filtered, inplace)

    def select(self, *args, inplace=False):
        """Wrapper method for selecting bands from dataset collection

        args:
            *args: arbitrary arguments to pass to the `ee.ImageCollection.select()` method
            inplace (bool, optional): define whether to return another dataset object or update inplace. default = False

        returns:
            Dataset | None: returns the dataset with the collection with selected bands.

        """
        selected = self.collection.select(*args)

        return self._inplace_wrapper(selected, inplace)

    def clip_to_region(self, inplace=False):
        """Clips all of the images to the geographic extent defined by region.
        Useful for setting geometries on unbounded imagery in collection (e.g. MODIS or VIIRS imagery)

        args:
            inplace (bool, optional): define whether to return another dataset object or update inplace. default = False

        returns:
            Dataset | None: returns dataset with imagery clipped to self.region or none depending on inplace
        """

        @decorators.keep_attrs
        def clip(img):
            """Closure function to perform the clipping while carrying metadata"""
            return ee.Image(img.clip(self.region))

        out_coll = self.collection.map(clip)

        return self._inplace_wrapper(out_coll, inplace)

    def merge(self, dataset, inplace=False):
        """Merge the collection of two datasets into one where self.collection will contain imagery from self and dataset arg.
        Results will be sorted by time

        args:
            dataset (Dataset): dataset object to merge
            inplace (bool, optional): define whether to return another dataset object or update inplace. default = False

        returns:
            Dataset | None: returns dataset where collection is merged imagery or none depending on inplace

        """
        merged = self.collection.merge(dataset.collection).sort("system:time_start")

        return self._inplace_wrapper(merged, inplace)

    def join(self, dataset, inplace=False):
        """Performs spatiotemporal join between self.collection and dataset.collection.
        Result will be a dataset where the collection is colocated imagery in space and time

        args:
            dataset (Dataset): dataset object to apply join with. Used as right in join operations
            inplace (bool, optional): define whether to return another dataset object or update inplace. default = False

        returns:
            Dataset | None: returns dataset where collection with joined imagery or none depending on inplace

        """

        def _merge(img):
            """Closure func to take results from the join and combine into one image with overlaping region"""
            join_coll = ee.ImageCollection.fromImages(img.get(key))

            img_geom = img.geometry(100)
            join_geom = join_coll.map(geeutils.get_geoms).union(100).geometry(100)
            overlap = img_geom.intersection(join_geom, 100)

            join_data = join_coll.mosaic()

            return img.addBands(join_data).clip(overlap)

        key = str(dataset.__class__.__name__)

        # get a time and space filter
        filter = ee.Filter.And(
            ee.Filter.maxDifference(
                **{
                    "difference": 1000 * 60 * 60 * 24,  # One day in milliseconds
                    "leftField": "system:time_start",
                    "rightField": "system:time_start",
                }
            ),
            ee.Filter.intersects(
                **{"leftField": ".geo", "rightField": ".geo", "maxError": 100}
            ),
        )
        # apply join on collections and save all results
        joined = ee.ImageCollection(
            ee.Join.saveAll(key).apply(
                primary=self.collection, secondary=dataset.collection, condition=filter
            )
        )

        # map over all filtered imagery, mosaic joined matches, and add bands to imagery
        joined = joined.map(_merge)

        return self._inplace_wrapper(joined, inplace)

    def aggregate_time(
        self,
        dates=None,
        period=1,
        period_unit="day",
        reducer="mean",
        rename=True,
        clip_to_area=False,
        inplace=False,
    ):
        """Aggregates multiple images into one based on time periods and a user defined reducer.
        Useful for mosaicing images from same date or time period.
        Expects the images in this dataset to be homogenous (same band names and types)

        args:
            dates (list[str], optional): list of dates defined as beginning time period of aggregatation. default = None,
                all available uniques dates in collection will be used
            period (int, optional): number of days to advance from dates for aggregation. default = 1
            period_unit (str, optional): time unit to advance period for aggregation. default = "day"
            reducer (str | ee.Reducer, optional): reducer to apply to images for aggregation, accepts string reducer name
                or ee.Reducer opbject, default = "mean"
            clip_to_area (bool): switch to clip imagery that has been merged to the overlaping region of imagery, default=False
            inplace (bool, optional): define whether to return another dataset object or update inplace. default = False

        returns:
            Dataset | None: returns dataset.collection with aggregated imagery or none depending on inplace

        """

        def _aggregation(d):
            """Closure function to map through days and reduce data within a given time period"""
            t1 = ee.Date(d)
            t2 = t1.advance(period, period_unit)
            img = (
                self.collection.filterDate(t1, t2)
                .reduce(reducer)
                .set("system:time_start", t1.millis())
            )
            if rename:
                img = img.rename(band_names)

            geom = (
                ee.FeatureCollection(
                    self.collection.filterDate(t1, t2).map(geeutils.get_geoms)
                )
                .union(100)
                .geometry(100)
            )
            outimg = ee.Algorithms.If(clip_to_area, img.clip(geom), img)

            return outimg

        if dates is None:
            dates = (
                self.collection.aggregate_array("system:time_start")
                .map(lambda x: ee.Date(x).format("YYYY-MM-dd"))
                .distinct()
            )
        else:
            dates = ee.List(dates)

        band_names = ee.Image(self.collection.first()).bandNames()

        out_coll = ee.ImageCollection.fromImages(dates.map(_aggregation))

        return self._inplace_wrapper(out_coll, inplace)

    @decorators.keep_attrs
    def band_pass_adjustment(self, img):
        """Method to apply linear band transformation to dataset image collection.
        Expects that dataset has properties `self.gain` and `self.bias` set

        args:
            img (ee.Image): image to apply regression on

        """
        # linear regression coefficients for adjustment
        return (
            img.multiply(self.gain)
            .add(self.bias)
            .set("system:time_start", img.get("system:time_start"))
        )

    def pipe(self, steps, inplace=False, keep_attrs=True):
        """Method to pipe imagery within dataset through multiple functions at once.
        Assumes the first argument into piped functions are and ee.Image

        args:
            steps (list | tuple): iterable of functions/steps to apply to imagery.
                list must be in the form of (func,func) or with a tuple of function/keyword ((func,kwargs),func)
            inplace (bool, optional): define whether to return another dataset object or update inplace. default = False

        returns:
            Dataset | None: returns dataset.collection with piped functions applied

        Example:
            ```python
            s1 = hf.Sentinel1(ee.Geometry.Point(105.03,11.72),"2019-10-03","2019-10-05")
            water = s1.pipe(
                (
                    hf.gamma_map, #apply speckle filter
                    (hf.egde_ostu,{'initial_threshold:-16}) # apply water mapping
                )
            )
            ```
        """

        def _piper(funcs):
            """Closure function to nest list of functions"""
            if len(funcs) > 1:
                one_shotter = funcs[0]
                for func in funcs[1:]:
                    one_shotter = pipe | one_shotter | func

            else:
                one_shotter = funcs[0]

            return one_shotter

        fs = []
        # loop through the steps and create partial funcs is kwargs are provided
        for step in steps:
            try:
                func, kwargs = step

            except TypeError:
                func = step
                kwargs = None

            if kwargs is not None:
                pfunc = partial(func, **kwargs)
            else:
                pfunc = func

            fs.append(pfunc)

        # get the piped function
        if keep_attrs:
            one_shot = decorators.keep_attrs(_piper(fs))
        else:
            one_shot = _piper(fs)

        # apply pipe to each image
        out_coll = self.collection.map(lambda img: one_shot(img))

        return self._inplace_wrapper(out_coll, inplace)

collection property writable

image collection object property wrapped by dataset

dates property readonly

Dates of imagery contained in the image collection

n_images property readonly

Number of images contained in the dataset

__init__(self, region, start_time, end_time, asset_id, use_qa=False) special

Initialize Dataset class

Parameters:

Name Type Description Default
region ee.Geometry

earth engine geometry object to filter image collection by

required
start_time str | datetime.datetime

start time used to filter image collection

required
end_time str | datetime.datetime

end time used to filter image collection

required
asset_id str

asset id of earth engine collection

required
use_qa bool

boolean to determine to use an internal function qa(). Used for definining custom dataset objects

False

Exceptions:

Type Description
AttributeError

if qa() method is not defined and use_qa is True

Source code in hydrafloods/datasets.py
def __init__(self, region, start_time, end_time, asset_id, use_qa=False):
    """Initialize Dataset class

    args:
        region (ee.Geometry): earth engine geometry object to filter image collection by
        start_time (str | datetime.datetime): start time used to filter image collection
        end_time (str | datetime.datetime): end time used to filter image collection
        asset_id (str): asset id of earth engine collection
        use_qa (bool, optional): boolean to determine to use an internal function qa().
            Used for definining custom dataset objects

    raises:
        AttributeError: if qa() method is not defined and use_qa is True
    """

    # TODO: add exceptions to check datatypes
    self.region = region  # dtype = ee.Geometry
    self.start_time = start_time
    self.end_time = end_time
    self.asset_id = asset_id
    self.use_qa = use_qa

    # dictionary mapping of band names used to harmonized optical datasets to same names
    self.BANDREMAP = ee.Dictionary(
        {
            "landsat7": ee.List(["SR_B1", "SR_B2", "SR_B3", "SR_B4", "SR_B5", "SR_B7"]),
            "landsat8": ee.List(["SR_B2", "SR_B3", "SR_B4", "SR_B5", "SR_B6", "SR_B7"]),
            "viirs": ee.List(["M2", "M4", "I1", "I2", "I3", "M11"]),
            "sen2": ee.List(["B2", "B3", "B4", "B8", "B11", "B12"]),
            "modis": ee.List(
                [
                    "sur_refl_b03",
                    "sur_refl_b04",
                    "sur_refl_b01",
                    "sur_refl_b02",
                    "sur_refl_b06",
                    "sur_refl_b07",
                ]
            ),
            "new": ee.List(["blue", "green", "red", "nir", "swir1", "swir2"]),
        }
    )

    # get the image collection and filter by geographic region and date time
    imgcollection = (
        ee.ImageCollection(self.asset_id)
        .filterBounds(self.region)
        .filterDate(self.start_time, self.end_time)
    )

    # check if to apply arbitrary qa process on collection
    # qa function can be defined in custom objects extending dataset
    if self.use_qa:
        try:
            imgcollection = imgcollection.map(self.qa)
        except AttributeError:
            raise AttributeError(
                "qa() method is not defined...please define one or set `use_qa` to False"
            )

    self.collection = imgcollection

aggregate_time(self, dates=None, period=1, period_unit='day', reducer='mean', rename=True, clip_to_area=False, inplace=False)

Aggregates multiple images into one based on time periods and a user defined reducer. Useful for mosaicing images from same date or time period. Expects the images in this dataset to be homogenous (same band names and types)

Parameters:

Name Type Description Default
dates list[str]

list of dates defined as beginning time period of aggregatation. default = None, all available uniques dates in collection will be used

None
period int

number of days to advance from dates for aggregation. default = 1

1
period_unit str

time unit to advance period for aggregation. default = "day"

'day'
reducer str | ee.Reducer

reducer to apply to images for aggregation, accepts string reducer name or ee.Reducer opbject, default = "mean"

'mean'
clip_to_area bool

switch to clip imagery that has been merged to the overlaping region of imagery, default=False

False
inplace bool

define whether to return another dataset object or update inplace. default = False

False

Returns:

Type Description
Dataset | None

returns dataset.collection with aggregated imagery or none depending on inplace

Source code in hydrafloods/datasets.py
def aggregate_time(
    self,
    dates=None,
    period=1,
    period_unit="day",
    reducer="mean",
    rename=True,
    clip_to_area=False,
    inplace=False,
):
    """Aggregates multiple images into one based on time periods and a user defined reducer.
    Useful for mosaicing images from same date or time period.
    Expects the images in this dataset to be homogenous (same band names and types)

    args:
        dates (list[str], optional): list of dates defined as beginning time period of aggregatation. default = None,
            all available uniques dates in collection will be used
        period (int, optional): number of days to advance from dates for aggregation. default = 1
        period_unit (str, optional): time unit to advance period for aggregation. default = "day"
        reducer (str | ee.Reducer, optional): reducer to apply to images for aggregation, accepts string reducer name
            or ee.Reducer opbject, default = "mean"
        clip_to_area (bool): switch to clip imagery that has been merged to the overlaping region of imagery, default=False
        inplace (bool, optional): define whether to return another dataset object or update inplace. default = False

    returns:
        Dataset | None: returns dataset.collection with aggregated imagery or none depending on inplace

    """

    def _aggregation(d):
        """Closure function to map through days and reduce data within a given time period"""
        t1 = ee.Date(d)
        t2 = t1.advance(period, period_unit)
        img = (
            self.collection.filterDate(t1, t2)
            .reduce(reducer)
            .set("system:time_start", t1.millis())
        )
        if rename:
            img = img.rename(band_names)

        geom = (
            ee.FeatureCollection(
                self.collection.filterDate(t1, t2).map(geeutils.get_geoms)
            )
            .union(100)
            .geometry(100)
        )
        outimg = ee.Algorithms.If(clip_to_area, img.clip(geom), img)

        return outimg

    if dates is None:
        dates = (
            self.collection.aggregate_array("system:time_start")
            .map(lambda x: ee.Date(x).format("YYYY-MM-dd"))
            .distinct()
        )
    else:
        dates = ee.List(dates)

    band_names = ee.Image(self.collection.first()).bandNames()

    out_coll = ee.ImageCollection.fromImages(dates.map(_aggregation))

    return self._inplace_wrapper(out_coll, inplace)

apply(self, func, inplace=False, *args, **kwargs)

Alias for the apply_func method

Parameters:

Name Type Description Default
func object

Function to map across image collection. Function must accept ee.Image as first argument

required
inplace bool

define whether to return another dataset object or update inplace. default = False

False
**kwargs

arbitrary keyword to pass to func

{}

Returns:

Type Description
Dataset | None

copy of class with results from func as image within collection property

Source code in hydrafloods/datasets.py
def apply(self, func, inplace=False, *args, **kwargs):
    """Alias for the `apply_func` method

    args:
        func (object): Function to map across image collection. Function must accept ee.Image as first argument
        inplace (bool, optional): define whether to return another dataset object or update inplace. default = False
        **kwargs: arbitrary keyword to pass to `func`

    returns:
        Dataset | None: copy of class with results from `func` as image within collection property
    """

    return self.apply_func(func, inplace, *args, *kwargs)

apply_func(self, func, inplace=False, *args, **kwargs)

Wrapper method to apply a function to all of the image in the dataset. Makes a copy of the collection and reassigns the image collection property. Function must accept an ee.ImageCollection and return an ee.ImageCollection

Parameters:

Name Type Description Default
func object

Function to map across image collection. Function must accept ee.Image as first argument

required
inplace bool

define whether to return another dataset object or update inplace. default = False

False
**kwargs

arbitrary keyword to pass to func

{}

Returns:

Type Description
Dataset | None

copy of class with results from func as image within collection property

Source code in hydrafloods/datasets.py
def apply_func(self, func, inplace=False, *args, **kwargs):
    """Wrapper method to apply a function to all of the image in the dataset.
    Makes a copy of the collection and reassigns the image collection property.
    Function must accept an ee.ImageCollection and return an ee.ImageCollection

    args:
        func (object): Function to map across image collection. Function must accept ee.Image as first argument
        inplace (bool, optional): define whether to return another dataset object or update inplace. default = False
        **kwargs: arbitrary keyword to pass to `func`

    returns:
        Dataset | None: copy of class with results from `func` as image within collection property
    """

    # get a partial function to map over imagery with the keywords applied
    # expects that the first positional arg is an
    func = partial(func, **kwargs)

    out_coll = self.collection.map(func)

    return self._inplace_wrapper(out_coll, inplace)

band_pass_adjustment(self, img)

Method to apply linear band transformation to dataset image collection. Expects that dataset has properties self.gain and self.bias set

Parameters:

Name Type Description Default
img ee.Image

image to apply regression on

required
Source code in hydrafloods/datasets.py
@decorators.keep_attrs
def band_pass_adjustment(self, img):
    """Method to apply linear band transformation to dataset image collection.
    Expects that dataset has properties `self.gain` and `self.bias` set

    args:
        img (ee.Image): image to apply regression on

    """
    # linear regression coefficients for adjustment
    return (
        img.multiply(self.gain)
        .add(self.bias)
        .set("system:time_start", img.get("system:time_start"))
    )

clip_to_region(self, inplace=False)

Clips all of the images to the geographic extent defined by region. Useful for setting geometries on unbounded imagery in collection (e.g. MODIS or VIIRS imagery)

Parameters:

Name Type Description Default
inplace bool

define whether to return another dataset object or update inplace. default = False

False

Returns:

Type Description
Dataset | None

returns dataset with imagery clipped to self.region or none depending on inplace

Source code in hydrafloods/datasets.py
def clip_to_region(self, inplace=False):
    """Clips all of the images to the geographic extent defined by region.
    Useful for setting geometries on unbounded imagery in collection (e.g. MODIS or VIIRS imagery)

    args:
        inplace (bool, optional): define whether to return another dataset object or update inplace. default = False

    returns:
        Dataset | None: returns dataset with imagery clipped to self.region or none depending on inplace
    """

    @decorators.keep_attrs
    def clip(img):
        """Closure function to perform the clipping while carrying metadata"""
        return ee.Image(img.clip(self.region))

    out_coll = self.collection.map(clip)

    return self._inplace_wrapper(out_coll, inplace)

copy(self)

returns a deep copy of the hydrafloods dataset class

Source code in hydrafloods/datasets.py
def copy(self):
    """returns a deep copy of the hydrafloods dataset class"""
    return copy.deepcopy(self)

filter(self, filter, inplace=False)

Wrapper method for applying a filter to a datset collection.

Parameters:

Name Type Description Default
filter ee.Filter

an ee.Filter object to apply to the dataset collection.

required

Returns:

Type Description
Dataset | None

returns the dataset with the filtered collection.

Source code in hydrafloods/datasets.py
def filter(self, filter, inplace=False):
    """Wrapper method for applying a filter to a datset collection.

    args:
        filter (ee.Filter): an `ee.Filter` object to apply to the dataset collection.

    returns:
        Dataset | None: returns the dataset with the filtered collection.
    """

    filtered = self.collection.filter(filter)

    return self._inplace_wrapper(filtered, inplace)

from_imgcollection(img_collection) staticmethod

Static method to convert an ee.ImageCollection object to a hf.Dataset object. This method will take some time as it uses computed ee Objects from the image collection propeties to populate the Dataset object properties (passing info from server to client)

Parameters:

Name Type Description Default
img_collection ee.ImageCollection

computed ee.ImageCollection object to create a hf.Dataset

required

Returns:

Type Description
hf.Dataset

dataset object with property information directly from the ee.ImageCollection

Source code in hydrafloods/datasets.py
@staticmethod
def from_imgcollection(img_collection):
    """Static method to convert an ee.ImageCollection object to a hf.Dataset object.
    This method will take some time as it uses computed ee Objects from the image collection
    propeties to populate the Dataset object properties (passing info from server to client)

    args:
        img_collection (ee.ImageCollection): computed ee.ImageCollection object to create a hf.Dataset

    returns:
        hf.Dataset: dataset object with property information directly from the ee.ImageCollection
    """

    # get region and date information
    region = (
        img_collection.map(geeutils.get_geoms)
        .union(maxError=100)
        .geometry(maxError=100)
    )
    # convert ee.Date info to string format
    dates = (
        img_collection.aggregate_array("system:time_start")
        .sort()
        .map(lambda x: ee.Date(x).format("YYYY-MM-dd HH:mm:ss.S"))
    )

    # pull the date info to local strings
    start_time = dates.get(0).getInfo()
    end_time = dates.get(-1).getInfo()

    # get the collection id for `.asset_id` property
    collection_id = img_collection.get("system:id").getInfo()
    if collection_id == "None":
        collection_id = "Custom ImageCollection"

    # make a dummy dataset
    dummy_ds = Dataset(
        region,
        start_time,
        end_time,
        asset_id="NOAA/VIIRS/001/VNP09GA",
        use_qa=False,
    )

    # override the dummy dataset information with the correct data fr
    dummy_ds.asset_id = collection_id
    dummy_ds.collection = img_collection

    return dummy_ds

join(self, dataset, inplace=False)

Performs spatiotemporal join between self.collection and dataset.collection. Result will be a dataset where the collection is colocated imagery in space and time

Parameters:

Name Type Description Default
dataset Dataset

dataset object to apply join with. Used as right in join operations

required
inplace bool

define whether to return another dataset object or update inplace. default = False

False

Returns:

Type Description
Dataset | None

returns dataset where collection with joined imagery or none depending on inplace

Source code in hydrafloods/datasets.py
def join(self, dataset, inplace=False):
    """Performs spatiotemporal join between self.collection and dataset.collection.
    Result will be a dataset where the collection is colocated imagery in space and time

    args:
        dataset (Dataset): dataset object to apply join with. Used as right in join operations
        inplace (bool, optional): define whether to return another dataset object or update inplace. default = False

    returns:
        Dataset | None: returns dataset where collection with joined imagery or none depending on inplace

    """

    def _merge(img):
        """Closure func to take results from the join and combine into one image with overlaping region"""
        join_coll = ee.ImageCollection.fromImages(img.get(key))

        img_geom = img.geometry(100)
        join_geom = join_coll.map(geeutils.get_geoms).union(100).geometry(100)
        overlap = img_geom.intersection(join_geom, 100)

        join_data = join_coll.mosaic()

        return img.addBands(join_data).clip(overlap)

    key = str(dataset.__class__.__name__)

    # get a time and space filter
    filter = ee.Filter.And(
        ee.Filter.maxDifference(
            **{
                "difference": 1000 * 60 * 60 * 24,  # One day in milliseconds
                "leftField": "system:time_start",
                "rightField": "system:time_start",
            }
        ),
        ee.Filter.intersects(
            **{"leftField": ".geo", "rightField": ".geo", "maxError": 100}
        ),
    )
    # apply join on collections and save all results
    joined = ee.ImageCollection(
        ee.Join.saveAll(key).apply(
            primary=self.collection, secondary=dataset.collection, condition=filter
        )
    )

    # map over all filtered imagery, mosaic joined matches, and add bands to imagery
    joined = joined.map(_merge)

    return self._inplace_wrapper(joined, inplace)

merge(self, dataset, inplace=False)

Merge the collection of two datasets into one where self.collection will contain imagery from self and dataset arg. Results will be sorted by time

Parameters:

Name Type Description Default
dataset Dataset

dataset object to merge

required
inplace bool

define whether to return another dataset object or update inplace. default = False

False

Returns:

Type Description
Dataset | None

returns dataset where collection is merged imagery or none depending on inplace

Source code in hydrafloods/datasets.py
def merge(self, dataset, inplace=False):
    """Merge the collection of two datasets into one where self.collection will contain imagery from self and dataset arg.
    Results will be sorted by time

    args:
        dataset (Dataset): dataset object to merge
        inplace (bool, optional): define whether to return another dataset object or update inplace. default = False

    returns:
        Dataset | None: returns dataset where collection is merged imagery or none depending on inplace

    """
    merged = self.collection.merge(dataset.collection).sort("system:time_start")

    return self._inplace_wrapper(merged, inplace)

pipe(self, steps, inplace=False, keep_attrs=True)

Method to pipe imagery within dataset through multiple functions at once. Assumes the first argument into piped functions are and ee.Image

Parameters:

Name Type Description Default
steps list | tuple

iterable of functions/steps to apply to imagery. list must be in the form of (func,func) or with a tuple of function/keyword ((func,kwargs),func)

required
inplace bool

define whether to return another dataset object or update inplace. default = False

False

Returns:

Type Description
Dataset | None

returns dataset.collection with piped functions applied

Examples:

s1 = hf.Sentinel1(ee.Geometry.Point(105.03,11.72),"2019-10-03","2019-10-05")
water = s1.pipe(
    (
        hf.gamma_map, #apply speckle filter
        (hf.egde_ostu,{'initial_threshold:-16}) # apply water mapping
    )
)
Source code in hydrafloods/datasets.py
def pipe(self, steps, inplace=False, keep_attrs=True):
    """Method to pipe imagery within dataset through multiple functions at once.
    Assumes the first argument into piped functions are and ee.Image

    args:
        steps (list | tuple): iterable of functions/steps to apply to imagery.
            list must be in the form of (func,func) or with a tuple of function/keyword ((func,kwargs),func)
        inplace (bool, optional): define whether to return another dataset object or update inplace. default = False

    returns:
        Dataset | None: returns dataset.collection with piped functions applied

    Example:
        ```python
        s1 = hf.Sentinel1(ee.Geometry.Point(105.03,11.72),"2019-10-03","2019-10-05")
        water = s1.pipe(
            (
                hf.gamma_map, #apply speckle filter
                (hf.egde_ostu,{'initial_threshold:-16}) # apply water mapping
            )
        )
        ```
    """

    def _piper(funcs):
        """Closure function to nest list of functions"""
        if len(funcs) > 1:
            one_shotter = funcs[0]
            for func in funcs[1:]:
                one_shotter = pipe | one_shotter | func

        else:
            one_shotter = funcs[0]

        return one_shotter

    fs = []
    # loop through the steps and create partial funcs is kwargs are provided
    for step in steps:
        try:
            func, kwargs = step

        except TypeError:
            func = step
            kwargs = None

        if kwargs is not None:
            pfunc = partial(func, **kwargs)
        else:
            pfunc = func

        fs.append(pfunc)

    # get the piped function
    if keep_attrs:
        one_shot = decorators.keep_attrs(_piper(fs))
    else:
        one_shot = _piper(fs)

    # apply pipe to each image
    out_coll = self.collection.map(lambda img: one_shot(img))

    return self._inplace_wrapper(out_coll, inplace)

select(self, *args, *, inplace=False)

Wrapper method for selecting bands from dataset collection

Parameters:

Name Type Description Default
*args

arbitrary arguments to pass to the ee.ImageCollection.select() method

()
inplace bool

define whether to return another dataset object or update inplace. default = False

False

Returns:

Type Description
Dataset | None

returns the dataset with the collection with selected bands.

Source code in hydrafloods/datasets.py
def select(self, *args, inplace=False):
    """Wrapper method for selecting bands from dataset collection

    args:
        *args: arbitrary arguments to pass to the `ee.ImageCollection.select()` method
        inplace (bool, optional): define whether to return another dataset object or update inplace. default = False

    returns:
        Dataset | None: returns the dataset with the collection with selected bands.

    """
    selected = self.collection.select(*args)

    return self._inplace_wrapper(selected, inplace)

hydrafloods.datasets.Sentinel1 (Dataset)

Class extending dataset for the Sentinel 1 collection This Sentinel 1 dataset is in backscatter units

Source code in hydrafloods/datasets.py
class Sentinel1(Dataset):
    """Class extending dataset for the Sentinel 1 collection
    This Sentinel 1 dataset is in backscatter units
    """

    def __init__(self, *args, asset_id="COPERNICUS/S1_GRD", use_qa=True, **kwargs):
        """Initialize Sentinel1 Dataset class

        args:
            *args: positional arguments to pass to `Dataset` (i.e. `region`, `start_time`, `end_time`)
            asset_id (str): asset id of the Sentinel 1 earth engine collection. default="COPERNICUS/S1_GRD"
            use_qa (bool, optional): boolean to determine to use a private `self.qa()` function. default=True
            **kwargs (optional): addtional arbitrary keywords to pass to `Dataset`
        """
        super(Sentinel1, self).__init__(
            *args, asset_id=asset_id, use_qa=use_qa, **kwargs
        )

        self.collection = self.collection.filter(
            ee.Filter.listContains("transmitterReceiverPolarisation", "VH")
        )

        return

    @decorators.keep_attrs
    def qa(self, img):
        """Custom QA masking method for Sentinel1 backscatter based on view angle
        Angle threshold values taken from https://doi.org/10.3390/rs13101954
        """
        angle = img.select("angle")
        angle_mask = angle.lt(45.23993).And(angle.gt(30.63993))
        return img.updateMask(angle_mask)

    def add_orbit_band(self, inplace=False):
        """Method to add orbit band from S1 image metadata
        Useful for determining if pixels are from ascending or descending orbits

        args:
            inplace (bool, optional): define whether to return another dataset object or update inplace. default = False

        returns:
            Dataset | None: returns dataset.collection where imagery has the added bands
        """

        def _add_features(img):
            """Closure function to add features as bands to the images"""
            bounds = img.geometry(100)
            orbit = ee.String(img.get("orbitProperties_pass"))
            orbit_band = ee.Algorithms.If(
                orbit.compareTo("DESCENDING"), ee.Image(1), ee.Image(0)
            )

            extraFeatures = ee.Image(orbit_band).rename("orbit")

            return img.addBands(extraFeatures.clip(bounds))

        return self.apply_func(_add_features, inplace=inplace)

    def to_db(self, inplace=False):
        """Convience method to convert units from power to db"""
        out_coll = self.collection.map(geeutils.power_to_db)

        if inplace:
            self.collection = out_coll
            return
        else:
            outCls = self.copy()
            outCls.collection = out_coll
            return outCls

    def to_power(self, inplace=False):
        """Convience method to convert units from db to power"""
        out_coll = self.collection.map(geeutils.db_to_power)

        if inplace:
            self.collection = out_coll
            return
        else:
            outCls = self.copy()
            outCls.collection = out_coll
            return outCls

__init__(self, *args, *, asset_id='COPERNICUS/S1_GRD', use_qa=True, **kwargs) special

Initialize Sentinel1 Dataset class

Parameters:

Name Type Description Default
*args

positional arguments to pass to Dataset (i.e. region, start_time, end_time)

()
asset_id str

asset id of the Sentinel 1 earth engine collection. default="COPERNICUS/S1_GRD"

'COPERNICUS/S1_GRD'
use_qa bool

boolean to determine to use a private self.qa() function. default=True

True
**kwargs optional

addtional arbitrary keywords to pass to Dataset

{}
Source code in hydrafloods/datasets.py
def __init__(self, *args, asset_id="COPERNICUS/S1_GRD", use_qa=True, **kwargs):
    """Initialize Sentinel1 Dataset class

    args:
        *args: positional arguments to pass to `Dataset` (i.e. `region`, `start_time`, `end_time`)
        asset_id (str): asset id of the Sentinel 1 earth engine collection. default="COPERNICUS/S1_GRD"
        use_qa (bool, optional): boolean to determine to use a private `self.qa()` function. default=True
        **kwargs (optional): addtional arbitrary keywords to pass to `Dataset`
    """
    super(Sentinel1, self).__init__(
        *args, asset_id=asset_id, use_qa=use_qa, **kwargs
    )

    self.collection = self.collection.filter(
        ee.Filter.listContains("transmitterReceiverPolarisation", "VH")
    )

    return

add_orbit_band(self, inplace=False)

Method to add orbit band from S1 image metadata Useful for determining if pixels are from ascending or descending orbits

Parameters:

Name Type Description Default
inplace bool

define whether to return another dataset object or update inplace. default = False

False

Returns:

Type Description
Dataset | None

returns dataset.collection where imagery has the added bands

Source code in hydrafloods/datasets.py
def add_orbit_band(self, inplace=False):
    """Method to add orbit band from S1 image metadata
    Useful for determining if pixels are from ascending or descending orbits

    args:
        inplace (bool, optional): define whether to return another dataset object or update inplace. default = False

    returns:
        Dataset | None: returns dataset.collection where imagery has the added bands
    """

    def _add_features(img):
        """Closure function to add features as bands to the images"""
        bounds = img.geometry(100)
        orbit = ee.String(img.get("orbitProperties_pass"))
        orbit_band = ee.Algorithms.If(
            orbit.compareTo("DESCENDING"), ee.Image(1), ee.Image(0)
        )

        extraFeatures = ee.Image(orbit_band).rename("orbit")

        return img.addBands(extraFeatures.clip(bounds))

    return self.apply_func(_add_features, inplace=inplace)

qa(self, img)

Custom QA masking method for Sentinel1 backscatter based on view angle Angle threshold values taken from https://doi.org/10.3390/rs13101954

Source code in hydrafloods/datasets.py
@decorators.keep_attrs
def qa(self, img):
    """Custom QA masking method for Sentinel1 backscatter based on view angle
    Angle threshold values taken from https://doi.org/10.3390/rs13101954
    """
    angle = img.select("angle")
    angle_mask = angle.lt(45.23993).And(angle.gt(30.63993))
    return img.updateMask(angle_mask)

to_db(self, inplace=False)

Convience method to convert units from power to db

Source code in hydrafloods/datasets.py
def to_db(self, inplace=False):
    """Convience method to convert units from power to db"""
    out_coll = self.collection.map(geeutils.power_to_db)

    if inplace:
        self.collection = out_coll
        return
    else:
        outCls = self.copy()
        outCls.collection = out_coll
        return outCls

to_power(self, inplace=False)

Convience method to convert units from db to power

Source code in hydrafloods/datasets.py
def to_power(self, inplace=False):
    """Convience method to convert units from db to power"""
    out_coll = self.collection.map(geeutils.db_to_power)

    if inplace:
        self.collection = out_coll
        return
    else:
        outCls = self.copy()
        outCls.collection = out_coll
        return outCls

hydrafloods.datasets.Sentinel2 (Dataset)

Source code in hydrafloods/datasets.py
class Sentinel2(Dataset):
    def __init__(
        self,
        *args,
        asset_id="COPERNICUS/S2_SR",
        use_qa=True,
        apply_band_adjustment=False,
        **kwargs,
    ):
        """Initialize Sentinel2 Dataset class

        args:
            *args: positional arguments to pass to `Dataset` (i.e. `region`, `start_time`, `end_time`)
            asset_id (str): asset id of the Sentinel2 earth engine collection. default="COPERNICUS/S2_SR"
            use_qa (bool, optional): boolean to determine to use a private `self.qa()` function. default=True
            apply_band_adjustment (bool, optional): boolean switch to apply linear band pass equation to convert values to Landsat8. default=False
            rescale (bool, optional): boolean switch to convert units from scaled int (0-10000) to float (0-1). If false values will be scaled int. default = False
            **kwargs (optional): addtional arbitrary keywords to pass to `Dataset`
        """
        super(Sentinel2, self).__init__(
            *args, asset_id=asset_id, use_qa=use_qa, **kwargs
        )

        coll = self.collection.select(
            self.BANDREMAP.get("sen2"), self.BANDREMAP.get("new")
        )

        if apply_band_adjustment:
            # band bass adjustment coefficients taken HLS project https://hls.gsfc.nasa.gov/algorithms/bandpass-adjustment/
            # slope coefficients
            self.gain = ee.Image.constant(
                [0.9778, 1.0053, 0.9765, 0.9983, 0.9987, 1.003]
            )
            # y-intercept coefficients
            self.bias = ee.Image.constant(
                [-0.00411, -0.00093, 0.00094, -0.0001, -0.0015, -0.0012]
            ).multiply(10000)
            coll = coll.map(self.band_pass_adjustment)

        self.collection = coll

        return

    @decorators.keep_attrs
    def qa(self, img):
        """Custom QA masking method for Sentinel2 surface reflectance dataset"""
        CLD_PRB_THRESH = 40
        NIR_DRK_THRESH = 0.175 * 1e4
        CLD_PRJ_DIST = 3
        BUFFER = 100
        CRS = img.select(0).projection()

        # Get s2cloudless image, subset the probability band.
        cld_prb = ee.Image(
            ee.ImageCollection("COPERNICUS/S2_CLOUD_PROBABILITY")
            .filter(ee.Filter.eq("system:index", img.get("system:index")))
            .first()
        ).select("probability")

        # Condition s2cloudless by the probability threshold value.
        is_cloud = cld_prb.gt(CLD_PRB_THRESH)

        # Identify water pixels from the SCL band, invert.
        not_water = img.select("SCL").neq(6)

        # Identify dark NIR pixels that are not water (potential cloud shadow pixels).
        dark_pixels = img.select("B8").lt(NIR_DRK_THRESH).multiply(not_water)

        # 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 = (
            is_cloud.directionalDistanceTransform(shadow_azimuth, CLD_PRJ_DIST * 10)
            .reproject(**{"crs": CRS, "scale": 120})
            .select("distance")
            .mask()
        )

        # Identify the intersection of dark pixels with cloud shadow projection.
        is_shadow = cld_proj.multiply(dark_pixels)

        # Combine cloud and shadow mask, set cloud and shadow as value 1, else 0.
        is_cld_shdw = is_cloud.add(is_shadow).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.focal_min(2)
            .focal_max(BUFFER * 2 / 20)
            .reproject(**{"crs": CRS, "scale": 60})
            .rename("cloudmask")
        )

        # Subset reflectance bands and update their masks, return the result.
        return geeutils.rescale(img).select("B.*").updateMask(is_cld_shdw.Not())

__init__(self, *args, *, asset_id='COPERNICUS/S2_SR', use_qa=True, apply_band_adjustment=False, **kwargs) special

Initialize Sentinel2 Dataset class

Parameters:

Name Type Description Default
*args

positional arguments to pass to Dataset (i.e. region, start_time, end_time)

()
asset_id str

asset id of the Sentinel2 earth engine collection. default="COPERNICUS/S2_SR"

'COPERNICUS/S2_SR'
use_qa bool

boolean to determine to use a private self.qa() function. default=True

True
apply_band_adjustment bool

boolean switch to apply linear band pass equation to convert values to Landsat8. default=False

False
rescale bool

boolean switch to convert units from scaled int (0-10000) to float (0-1). If false values will be scaled int. default = False

required
**kwargs optional

addtional arbitrary keywords to pass to Dataset

{}
Source code in hydrafloods/datasets.py
def __init__(
    self,
    *args,
    asset_id="COPERNICUS/S2_SR",
    use_qa=True,
    apply_band_adjustment=False,
    **kwargs,
):
    """Initialize Sentinel2 Dataset class

    args:
        *args: positional arguments to pass to `Dataset` (i.e. `region`, `start_time`, `end_time`)
        asset_id (str): asset id of the Sentinel2 earth engine collection. default="COPERNICUS/S2_SR"
        use_qa (bool, optional): boolean to determine to use a private `self.qa()` function. default=True
        apply_band_adjustment (bool, optional): boolean switch to apply linear band pass equation to convert values to Landsat8. default=False
        rescale (bool, optional): boolean switch to convert units from scaled int (0-10000) to float (0-1). If false values will be scaled int. default = False
        **kwargs (optional): addtional arbitrary keywords to pass to `Dataset`
    """
    super(Sentinel2, self).__init__(
        *args, asset_id=asset_id, use_qa=use_qa, **kwargs
    )

    coll = self.collection.select(
        self.BANDREMAP.get("sen2"), self.BANDREMAP.get("new")
    )

    if apply_band_adjustment:
        # band bass adjustment coefficients taken HLS project https://hls.gsfc.nasa.gov/algorithms/bandpass-adjustment/
        # slope coefficients
        self.gain = ee.Image.constant(
            [0.9778, 1.0053, 0.9765, 0.9983, 0.9987, 1.003]
        )
        # y-intercept coefficients
        self.bias = ee.Image.constant(
            [-0.00411, -0.00093, 0.00094, -0.0001, -0.0015, -0.0012]
        ).multiply(10000)
        coll = coll.map(self.band_pass_adjustment)

    self.collection = coll

    return

qa(self, img)

Custom QA masking method for Sentinel2 surface reflectance dataset

Source code in hydrafloods/datasets.py
@decorators.keep_attrs
def qa(self, img):
    """Custom QA masking method for Sentinel2 surface reflectance dataset"""
    CLD_PRB_THRESH = 40
    NIR_DRK_THRESH = 0.175 * 1e4
    CLD_PRJ_DIST = 3
    BUFFER = 100
    CRS = img.select(0).projection()

    # Get s2cloudless image, subset the probability band.
    cld_prb = ee.Image(
        ee.ImageCollection("COPERNICUS/S2_CLOUD_PROBABILITY")
        .filter(ee.Filter.eq("system:index", img.get("system:index")))
        .first()
    ).select("probability")

    # Condition s2cloudless by the probability threshold value.
    is_cloud = cld_prb.gt(CLD_PRB_THRESH)

    # Identify water pixels from the SCL band, invert.
    not_water = img.select("SCL").neq(6)

    # Identify dark NIR pixels that are not water (potential cloud shadow pixels).
    dark_pixels = img.select("B8").lt(NIR_DRK_THRESH).multiply(not_water)

    # 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 = (
        is_cloud.directionalDistanceTransform(shadow_azimuth, CLD_PRJ_DIST * 10)
        .reproject(**{"crs": CRS, "scale": 120})
        .select("distance")
        .mask()
    )

    # Identify the intersection of dark pixels with cloud shadow projection.
    is_shadow = cld_proj.multiply(dark_pixels)

    # Combine cloud and shadow mask, set cloud and shadow as value 1, else 0.
    is_cld_shdw = is_cloud.add(is_shadow).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.focal_min(2)
        .focal_max(BUFFER * 2 / 20)
        .reproject(**{"crs": CRS, "scale": 60})
        .rename("cloudmask")
    )

    # Subset reflectance bands and update their masks, return the result.
    return geeutils.rescale(img).select("B.*").updateMask(is_cld_shdw.Not())

hydrafloods.datasets.Landsat8 (Dataset)

Source code in hydrafloods/datasets.py
class Landsat8(Dataset):
    def __init__(
        self,
        *args,
        asset_id="LANDSAT/LC08/C02/T1_L2",
        use_qa=True,
        **kwargs,
    ):
        """Initialize Landsat8 Dataset class
        Can theoretically be useds with any Landsat surface reflectance collection (e.g. LANDSAT/LT05/C01/T1_SR)

        args:
            *args: positional arguments to pass to `Dataset` (i.e. `region`, `start_time`, `end_time`)
            asset_id (str): asset id of the Landsat earth engine collection. default="LANDSAT/LC08/C01/T1_SR"
            use_qa (bool, optional): boolean to determine to use a private `self.qa()` function. default=True
            rescale (bool, optional): boolean switch to convert units from scaled int (0-10000) to float (0-1). If false values will be scaled int. default = False
            **kwargs (optional): addtional arbitrary keywords to pass to `Dataset`
        """
        super(Landsat8, self).__init__(
            *args, asset_id=asset_id, use_qa=use_qa, **kwargs
        )

        self.collection = self.collection.select(
            self.BANDREMAP.get("landsat8"), self.BANDREMAP.get("new")
        )

        return

    @decorators.keep_attrs
    def qa(self, img):
        """Custom QA masking method for Landsat8 surface reflectance dataset"""
        qa_band = img.select("QA_PIXEL")
        qa_flag = int('111111',2)
        sat_mask = img.select('QA_RADSAT').eq(0);
        mask = qa_band.bitwiseAnd(qa_flag).eq(0).And(sat_mask)
        return geeutils.rescale(img, scale = 0.0000275, offset = -0.2).updateMask(mask)

__init__(self, *args, *, asset_id='LANDSAT/LC08/C02/T1_L2', use_qa=True, **kwargs) special

Initialize Landsat8 Dataset class Can theoretically be useds with any Landsat surface reflectance collection (e.g. LANDSAT/LT05/C01/T1_SR)

Parameters:

Name Type Description Default
*args

positional arguments to pass to Dataset (i.e. region, start_time, end_time)

()
asset_id str

asset id of the Landsat earth engine collection. default="LANDSAT/LC08/C01/T1_SR"

'LANDSAT/LC08/C02/T1_L2'
use_qa bool

boolean to determine to use a private self.qa() function. default=True

True
rescale bool

boolean switch to convert units from scaled int (0-10000) to float (0-1). If false values will be scaled int. default = False

required
**kwargs optional

addtional arbitrary keywords to pass to Dataset

{}
Source code in hydrafloods/datasets.py
def __init__(
    self,
    *args,
    asset_id="LANDSAT/LC08/C02/T1_L2",
    use_qa=True,
    **kwargs,
):
    """Initialize Landsat8 Dataset class
    Can theoretically be useds with any Landsat surface reflectance collection (e.g. LANDSAT/LT05/C01/T1_SR)

    args:
        *args: positional arguments to pass to `Dataset` (i.e. `region`, `start_time`, `end_time`)
        asset_id (str): asset id of the Landsat earth engine collection. default="LANDSAT/LC08/C01/T1_SR"
        use_qa (bool, optional): boolean to determine to use a private `self.qa()` function. default=True
        rescale (bool, optional): boolean switch to convert units from scaled int (0-10000) to float (0-1). If false values will be scaled int. default = False
        **kwargs (optional): addtional arbitrary keywords to pass to `Dataset`
    """
    super(Landsat8, self).__init__(
        *args, asset_id=asset_id, use_qa=use_qa, **kwargs
    )

    self.collection = self.collection.select(
        self.BANDREMAP.get("landsat8"), self.BANDREMAP.get("new")
    )

    return

qa(self, img)

Custom QA masking method for Landsat8 surface reflectance dataset

Source code in hydrafloods/datasets.py
@decorators.keep_attrs
def qa(self, img):
    """Custom QA masking method for Landsat8 surface reflectance dataset"""
    qa_band = img.select("QA_PIXEL")
    qa_flag = int('111111',2)
    sat_mask = img.select('QA_RADSAT').eq(0);
    mask = qa_band.bitwiseAnd(qa_flag).eq(0).And(sat_mask)
    return geeutils.rescale(img, scale = 0.0000275, offset = -0.2).updateMask(mask)

hydrafloods.datasets.Landsat7 (Dataset)

Source code in hydrafloods/datasets.py
class Landsat7(Dataset):
    def __init__(
        self,
        *args,
        asset_id="LANDSAT/LE07/C02/T1_L2",
        use_qa=True,
        apply_band_adjustment=False,
        **kwargs,
    ):
        """Initialize Landsat7 Dataset class
        Can theoretically be useds with any Landsat surface reflectance collection (e.g. LANDSAT/LT05/C01/T1_SR)

        args:
            *args: positional arguments to pass to `Dataset` (i.e. `region`, `start_time`, `end_time`)
            asset_id (str): asset id of the Landsat7 earth engine collection. default="LANDSAT/LE07/C01/T1_SR"
            use_qa (bool, optional): boolean to determine to use a private `self.qa()` function. default=True
            apply_band_adjustment (bool, optional): boolean switch to apply linear band pass equation to convert values to Landsat8. default=False
            rescale (bool, optional): boolean switch to convert units from scaled int (0-10000) to float (0-1). If false values will be scaled int. default = False
            **kwargs (optional): addtional arbitrary keywords to pass to `Dataset`
        """
        super(Landsat7, self).__init__(
            *args, asset_id=asset_id, use_qa=use_qa, **kwargs
        )

        coll = self.collection.select(
            self.BANDREMAP.get("landsat7"), self.BANDREMAP.get("new")
        )

        if apply_band_adjustment:
            # band bass adjustment coefficients taken from Roy et al., 2016 http://dx.doi.org/10.1016/j.rse.2015.12.024
            # slope coefficients
            self.gain = ee.Image.constant(
                [0.8474, 0.8483, 0.9047, 0.8462, 0.8937, 0.9071]
            )
            # y-intercept coefficients
            self.bias = ee.Image.constant(
                [0.0003, 0.0088, 0.0061, 0.0412, 0.0254, 0.0172]
            ).multiply(10000)
            coll = coll.map(self.band_pass_adjustment)

        self.collection = coll

        return

    @decorators.keep_attrs
    def qa(self, img):
        """Custom QA masking method for Landsat7 surface reflectance dataset"""
        qa_band = img.select("QA_PIXEL")
        qa_flag = int('111111',2)
        sat_mask = img.select('QA_RADSAT').eq(0);
        mask = qa_band.bitwiseAnd(qa_flag).eq(0).And(sat_mask)
        return geeutils.rescale(img, scale = 0.0000275, offset = -0.2).updateMask(mask)

__init__(self, *args, *, asset_id='LANDSAT/LE07/C02/T1_L2', use_qa=True, apply_band_adjustment=False, **kwargs) special

Initialize Landsat7 Dataset class Can theoretically be useds with any Landsat surface reflectance collection (e.g. LANDSAT/LT05/C01/T1_SR)

Parameters:

Name Type Description Default
*args

positional arguments to pass to Dataset (i.e. region, start_time, end_time)

()
asset_id str

asset id of the Landsat7 earth engine collection. default="LANDSAT/LE07/C01/T1_SR"

'LANDSAT/LE07/C02/T1_L2'
use_qa bool

boolean to determine to use a private self.qa() function. default=True

True
apply_band_adjustment bool

boolean switch to apply linear band pass equation to convert values to Landsat8. default=False

False
rescale bool

boolean switch to convert units from scaled int (0-10000) to float (0-1). If false values will be scaled int. default = False

required
**kwargs optional

addtional arbitrary keywords to pass to Dataset

{}
Source code in hydrafloods/datasets.py
def __init__(
    self,
    *args,
    asset_id="LANDSAT/LE07/C02/T1_L2",
    use_qa=True,
    apply_band_adjustment=False,
    **kwargs,
):
    """Initialize Landsat7 Dataset class
    Can theoretically be useds with any Landsat surface reflectance collection (e.g. LANDSAT/LT05/C01/T1_SR)

    args:
        *args: positional arguments to pass to `Dataset` (i.e. `region`, `start_time`, `end_time`)
        asset_id (str): asset id of the Landsat7 earth engine collection. default="LANDSAT/LE07/C01/T1_SR"
        use_qa (bool, optional): boolean to determine to use a private `self.qa()` function. default=True
        apply_band_adjustment (bool, optional): boolean switch to apply linear band pass equation to convert values to Landsat8. default=False
        rescale (bool, optional): boolean switch to convert units from scaled int (0-10000) to float (0-1). If false values will be scaled int. default = False
        **kwargs (optional): addtional arbitrary keywords to pass to `Dataset`
    """
    super(Landsat7, self).__init__(
        *args, asset_id=asset_id, use_qa=use_qa, **kwargs
    )

    coll = self.collection.select(
        self.BANDREMAP.get("landsat7"), self.BANDREMAP.get("new")
    )

    if apply_band_adjustment:
        # band bass adjustment coefficients taken from Roy et al., 2016 http://dx.doi.org/10.1016/j.rse.2015.12.024
        # slope coefficients
        self.gain = ee.Image.constant(
            [0.8474, 0.8483, 0.9047, 0.8462, 0.8937, 0.9071]
        )
        # y-intercept coefficients
        self.bias = ee.Image.constant(
            [0.0003, 0.0088, 0.0061, 0.0412, 0.0254, 0.0172]
        ).multiply(10000)
        coll = coll.map(self.band_pass_adjustment)

    self.collection = coll

    return

qa(self, img)

Custom QA masking method for Landsat7 surface reflectance dataset

Source code in hydrafloods/datasets.py
@decorators.keep_attrs
def qa(self, img):
    """Custom QA masking method for Landsat7 surface reflectance dataset"""
    qa_band = img.select("QA_PIXEL")
    qa_flag = int('111111',2)
    sat_mask = img.select('QA_RADSAT').eq(0);
    mask = qa_band.bitwiseAnd(qa_flag).eq(0).And(sat_mask)
    return geeutils.rescale(img, scale = 0.0000275, offset = -0.2).updateMask(mask)

hydrafloods.datasets.Viirs (Dataset)

Source code in hydrafloods/datasets.py
class Viirs(Dataset):
    def __init__(
        self,
        *args,
        asset_id="NOAA/VIIRS/001/VNP09GA",
        use_qa=True,
        apply_band_adjustment=False,
        **kwargs,
    ):
        """Initialize VIIRS Dataset class

        args:
            *args: positional arguments to pass to `Dataset` (i.e. `region`, `start_time`, `end_time`)
            asset_id (str): asset id of the VIIRS earth engine collection. default="NOAA/VIIRS/001/VNP09GA"
            use_qa (bool, optional): boolean to determine to use a private `self.qa()` function. default=True
            apply_band_adjustment (bool, optional): boolean switch to apply linear band pass equation to convert values to Landsat8. default=False
            rescale (bool, optional): boolean switch to convert units from scaled int (0-10000) to float (0-1). If false values will be scaled int. default = False
            **kwargs (optional): addtional arbitrary keywords to pass to `Dataset`
        """
        super(Viirs, self).__init__(*args, asset_id=asset_id, use_qa=use_qa, **kwargs)

        # get the bands and rename to common optical names
        coll = self.collection.select(
            self.BANDREMAP.get("viirs"), self.BANDREMAP.get("new")
        )

        if apply_band_adjustment:
            # band bass adjustment coefficients taken calculated from https://code.earthengine.google.com/876f53861690e483fb3e3439a3571f27
            # slope coefficients
            self.gain = ee.Image.constant(
                [0.68328, 0.66604, 0.78901, 0.95324, 0.98593, 0.88941]
            )
            # y-intercept coefficients
            self.bias = ee.Image.constant(
                [0.016728, 0.030814, 0.023199, 0.036571, 0.026923, 0.021615]
            ).multiply(10000)
            coll = coll.map(self.band_pass_adjustment)

        self.collection = coll

        return

    @decorators.keep_attrs
    def qa(self, img):
        """Custom QA masking method for VIIRS VNP09GA dataset"""
        cloudMask = geeutils.extract_bits(
            img.select("QF1"), 2, end=3, new_name="cloud_qa"
        ).lt(1)
        shadowMask = geeutils.extract_bits(
            img.select("QF2"), 3, new_name="shadow_qa"
        ).Not()
        snowMask = geeutils.extract_bits(img.select("QF2"), 5, new_name="snow_qa").Not()
        sensorZenith = img.select("SensorZenith").abs().lt(6000)

        env_mask = cloudMask.And(shadowMask).And(sensorZenith)

        # internal pixel quality masks
        pixal_quality_3 = img.select("QF3")
        pixal_quality_4 = img.select("QF4")
        m2_qual = geeutils.extract_bits(pixal_quality_3, 1, new_name="m2_quality").eq(0)
        m4_qual = geeutils.extract_bits(pixal_quality_3, 3, new_name="m4_quality").eq(0)
        m11_qual = geeutils.extract_bits(pixal_quality_4, 0, new_name="m11_quality").eq(0)
        i1_qual = geeutils.extract_bits(pixal_quality_4, 1, new_name="i1_quality").eq(0)
        i2_qual = geeutils.extract_bits(pixal_quality_4, 2, new_name="i2_quality").eq(0)
        i3_qual = geeutils.extract_bits(pixal_quality_4, 3, new_name="i3_quality").eq(0)

        qual_mask = m2_qual.And(m4_qual).And(m11_qual).And(i1_qual).And(i2_qual).And(i3_qual)

        mask = env_mask.And(qual_mask)

        return geeutils.rescale(img).updateMask(mask)

__init__(self, *args, *, asset_id='NOAA/VIIRS/001/VNP09GA', use_qa=True, apply_band_adjustment=False, **kwargs) special

Initialize VIIRS Dataset class

Parameters:

Name Type Description Default
*args

positional arguments to pass to Dataset (i.e. region, start_time, end_time)

()
asset_id str

asset id of the VIIRS earth engine collection. default="NOAA/VIIRS/001/VNP09GA"

'NOAA/VIIRS/001/VNP09GA'
use_qa bool

boolean to determine to use a private self.qa() function. default=True

True
apply_band_adjustment bool

boolean switch to apply linear band pass equation to convert values to Landsat8. default=False

False
rescale bool

boolean switch to convert units from scaled int (0-10000) to float (0-1). If false values will be scaled int. default = False

required
**kwargs optional

addtional arbitrary keywords to pass to Dataset

{}
Source code in hydrafloods/datasets.py
def __init__(
    self,
    *args,
    asset_id="NOAA/VIIRS/001/VNP09GA",
    use_qa=True,
    apply_band_adjustment=False,
    **kwargs,
):
    """Initialize VIIRS Dataset class

    args:
        *args: positional arguments to pass to `Dataset` (i.e. `region`, `start_time`, `end_time`)
        asset_id (str): asset id of the VIIRS earth engine collection. default="NOAA/VIIRS/001/VNP09GA"
        use_qa (bool, optional): boolean to determine to use a private `self.qa()` function. default=True
        apply_band_adjustment (bool, optional): boolean switch to apply linear band pass equation to convert values to Landsat8. default=False
        rescale (bool, optional): boolean switch to convert units from scaled int (0-10000) to float (0-1). If false values will be scaled int. default = False
        **kwargs (optional): addtional arbitrary keywords to pass to `Dataset`
    """
    super(Viirs, self).__init__(*args, asset_id=asset_id, use_qa=use_qa, **kwargs)

    # get the bands and rename to common optical names
    coll = self.collection.select(
        self.BANDREMAP.get("viirs"), self.BANDREMAP.get("new")
    )

    if apply_band_adjustment:
        # band bass adjustment coefficients taken calculated from https://code.earthengine.google.com/876f53861690e483fb3e3439a3571f27
        # slope coefficients
        self.gain = ee.Image.constant(
            [0.68328, 0.66604, 0.78901, 0.95324, 0.98593, 0.88941]
        )
        # y-intercept coefficients
        self.bias = ee.Image.constant(
            [0.016728, 0.030814, 0.023199, 0.036571, 0.026923, 0.021615]
        ).multiply(10000)
        coll = coll.map(self.band_pass_adjustment)

    self.collection = coll

    return

qa(self, img)

Custom QA masking method for VIIRS VNP09GA dataset

Source code in hydrafloods/datasets.py
@decorators.keep_attrs
def qa(self, img):
    """Custom QA masking method for VIIRS VNP09GA dataset"""
    cloudMask = geeutils.extract_bits(
        img.select("QF1"), 2, end=3, new_name="cloud_qa"
    ).lt(1)
    shadowMask = geeutils.extract_bits(
        img.select("QF2"), 3, new_name="shadow_qa"
    ).Not()
    snowMask = geeutils.extract_bits(img.select("QF2"), 5, new_name="snow_qa").Not()
    sensorZenith = img.select("SensorZenith").abs().lt(6000)

    env_mask = cloudMask.And(shadowMask).And(sensorZenith)

    # internal pixel quality masks
    pixal_quality_3 = img.select("QF3")
    pixal_quality_4 = img.select("QF4")
    m2_qual = geeutils.extract_bits(pixal_quality_3, 1, new_name="m2_quality").eq(0)
    m4_qual = geeutils.extract_bits(pixal_quality_3, 3, new_name="m4_quality").eq(0)
    m11_qual = geeutils.extract_bits(pixal_quality_4, 0, new_name="m11_quality").eq(0)
    i1_qual = geeutils.extract_bits(pixal_quality_4, 1, new_name="i1_quality").eq(0)
    i2_qual = geeutils.extract_bits(pixal_quality_4, 2, new_name="i2_quality").eq(0)
    i3_qual = geeutils.extract_bits(pixal_quality_4, 3, new_name="i3_quality").eq(0)

    qual_mask = m2_qual.And(m4_qual).And(m11_qual).And(i1_qual).And(i2_qual).And(i3_qual)

    mask = env_mask.And(qual_mask)

    return geeutils.rescale(img).updateMask(mask)

hydrafloods.datasets.Modis (Dataset)

Source code in hydrafloods/datasets.py
class Modis(Dataset):
    def __init__(
        self, *args, asset_id="MODIS/006/MOD09GA", use_qa=True, **kwargs
    ):
        """Initialize MODIS Dataset class
        Can be used with MOD09GA and MYD09GA

        args:
            *args: positional arguments to pass to `Dataset` (i.e. `region`, `start_time`, `end_time`)
            asset_id (str): asset id of the MODIS earth engine collection. default="MODIS/006/MOD09GA"
            use_qa (bool, optional): boolean to determine to use a private `self.qa()` function. default=True
            rescale (bool, optional): boolean switch to convert units from scaled int (0-10000) to float (0-1). If false values will be scaled int. default = False
            **kwargs (optional): addtional arbitrary keywords to pass to `Dataset`
        """
        super(Modis, self).__init__(*args, asset_id=asset_id, use_qa=use_qa, **kwargs)

        self.collection = self.collection.select(
            self.BANDREMAP.get("modis"), self.BANDREMAP.get("new")
        )

        return

    @decorators.keep_attrs
    def qa(self, img):
        """Custom QA masking method for MODIS MXD09GA dataset"""
        # internal env masks
        qa = img.select("state_1km")
        cloudMask = geeutils.extract_bits(qa, 10, end=11, new_name="cloud_qa").lt(1)
        shadowMask = geeutils.extract_bits(qa, 2, new_name="shadow_qa").Not()
        snowMask = geeutils.extract_bits(qa, 12, new_name="snow_qa").Not()
        cloudAdjMask = geeutils.extract_bits(qa, 13, new_name="cloud_adjacency_qa").Not()
        sensorZenith = img.select("SensorZenith").abs().lt(6000)
        env_mask = cloudMask.And(shadowMask).And(snowMask).And(sensorZenith).And(cloudAdjMask)

        # internal pixel quality masks
        pixal_quality = img.select("QC_500m")
        b1_qual = geeutils.extract_bits(pixal_quality, 2, end=5, new_name="b1_quality").eq(0)
        b2_qual = geeutils.extract_bits(pixal_quality, 6, end=9, new_name="b1_quality").eq(0)
        b3_qual = geeutils.extract_bits(pixal_quality, 10, end=13, new_name="b1_quality").eq(0)
        b4_qual = geeutils.extract_bits(pixal_quality, 14, end=17, new_name="b1_quality").eq(0)
        b6_qual = geeutils.extract_bits(pixal_quality, 22, end=25, new_name="b1_quality").eq(0)
        b7_qual = geeutils.extract_bits(pixal_quality, 26, end=29, new_name="b1_quality").eq(0)

        qual_mask = b1_qual.And(b2_qual).And(b3_qual).And(b4_qual).And(b6_qual).And(b7_qual)

        mask = env_mask.And(qual_mask)
        return geeutils.rescale(img).updateMask(mask)

__init__(self, *args, *, asset_id='MODIS/006/MOD09GA', use_qa=True, **kwargs) special

Initialize MODIS Dataset class Can be used with MOD09GA and MYD09GA

Parameters:

Name Type Description Default
*args

positional arguments to pass to Dataset (i.e. region, start_time, end_time)

()
asset_id str

asset id of the MODIS earth engine collection. default="MODIS/006/MOD09GA"

'MODIS/006/MOD09GA'
use_qa bool

boolean to determine to use a private self.qa() function. default=True

True
rescale bool

boolean switch to convert units from scaled int (0-10000) to float (0-1). If false values will be scaled int. default = False

required
**kwargs optional

addtional arbitrary keywords to pass to Dataset

{}
Source code in hydrafloods/datasets.py
def __init__(
    self, *args, asset_id="MODIS/006/MOD09GA", use_qa=True, **kwargs
):
    """Initialize MODIS Dataset class
    Can be used with MOD09GA and MYD09GA

    args:
        *args: positional arguments to pass to `Dataset` (i.e. `region`, `start_time`, `end_time`)
        asset_id (str): asset id of the MODIS earth engine collection. default="MODIS/006/MOD09GA"
        use_qa (bool, optional): boolean to determine to use a private `self.qa()` function. default=True
        rescale (bool, optional): boolean switch to convert units from scaled int (0-10000) to float (0-1). If false values will be scaled int. default = False
        **kwargs (optional): addtional arbitrary keywords to pass to `Dataset`
    """
    super(Modis, self).__init__(*args, asset_id=asset_id, use_qa=use_qa, **kwargs)

    self.collection = self.collection.select(
        self.BANDREMAP.get("modis"), self.BANDREMAP.get("new")
    )

    return

qa(self, img)

Custom QA masking method for MODIS MXD09GA dataset

Source code in hydrafloods/datasets.py
@decorators.keep_attrs
def qa(self, img):
    """Custom QA masking method for MODIS MXD09GA dataset"""
    # internal env masks
    qa = img.select("state_1km")
    cloudMask = geeutils.extract_bits(qa, 10, end=11, new_name="cloud_qa").lt(1)
    shadowMask = geeutils.extract_bits(qa, 2, new_name="shadow_qa").Not()
    snowMask = geeutils.extract_bits(qa, 12, new_name="snow_qa").Not()
    cloudAdjMask = geeutils.extract_bits(qa, 13, new_name="cloud_adjacency_qa").Not()
    sensorZenith = img.select("SensorZenith").abs().lt(6000)
    env_mask = cloudMask.And(shadowMask).And(snowMask).And(sensorZenith).And(cloudAdjMask)

    # internal pixel quality masks
    pixal_quality = img.select("QC_500m")
    b1_qual = geeutils.extract_bits(pixal_quality, 2, end=5, new_name="b1_quality").eq(0)
    b2_qual = geeutils.extract_bits(pixal_quality, 6, end=9, new_name="b1_quality").eq(0)
    b3_qual = geeutils.extract_bits(pixal_quality, 10, end=13, new_name="b1_quality").eq(0)
    b4_qual = geeutils.extract_bits(pixal_quality, 14, end=17, new_name="b1_quality").eq(0)
    b6_qual = geeutils.extract_bits(pixal_quality, 22, end=25, new_name="b1_quality").eq(0)
    b7_qual = geeutils.extract_bits(pixal_quality, 26, end=29, new_name="b1_quality").eq(0)

    qual_mask = b1_qual.And(b2_qual).And(b3_qual).And(b4_qual).And(b6_qual).And(b7_qual)

    mask = env_mask.And(qual_mask)
    return geeutils.rescale(img).updateMask(mask)