Skip to content

Commit

Permalink
feat: maskCoverRegion computes the percentage of masked pixels inside…
Browse files Browse the repository at this point in the history
… a geometry. maskCoverRegions does the same but inside a FeatureCollection.
  • Loading branch information
fitoprincipe committed Oct 22, 2024
1 parent 86eb511 commit 379ab98
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 24 deletions.
111 changes: 98 additions & 13 deletions geetools/Image.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Toolbox for the ``ee.Image`` class."""
from __future__ import annotations

from typing import Optional

import ee
import ee_extra
import ee_extra.Algorithms.core
Expand Down Expand Up @@ -1315,11 +1317,27 @@ def distance(self, other: ee.image) -> ee.Image:

return ee.Image(distance)

def maskCover(self) -> ee.Image:
"""return an image with the mask cover ratio as an image property.
def maskCoverRegion(
self,
region: ee.Geometry,
scale: Optional[int] = None,
band: Optional[str] = None,
proxy_value: int = -999,
maxPixels: int = 1e8,
tileScale: int = 1,
) -> ee.Number:
"""Compute the coverage of masked pixels inside a Geometry.
Parameters:
region: The region to compute the mask coverage.
scale: The scale of the computation. In case you need a rough estimation use a higher scale than the original from the image.
band: The band to use. Defaults to the first band.
proxy_value: the value to use for counting the mask and avoid confusing 0s to masked values. Choose a value that is out of the range of the image values.
maxPixels: The maximum number of pixels to reduce.
tileScale: A scaling factor between 0.1 and 16 used to adjust aggregation tile size; setting a larger tileScale (e.g., 2 or 4) uses smaller tiles and may enable computations that run out of memory with the default.
Returns:
The image with the mask cover ratio as an image property.
The percentage of masked pixels within the region
Examples:
.. code-block:: python
Expand All @@ -1329,23 +1347,90 @@ def maskCover(self) -> ee.Image:
ee.Initialize()
image = ee.Image('COPERNICUS/S2_SR/20190828T151811_20190828T151809_T18GYT')
image = image.maskCover()
aoi = ee.Geometry.Point([11.880190936531116, 42.0159494554553]).buffer(2000)
image = image.maskCoverRegion(aoi)
"""
# compute the mask cover
mask, geometry = self._obj.select(0).mask(), self._obj.geometry()
cover = mask.reduceRegion(ee.Reducer.frequencyHistogram(), geometry, bestEffort=True)

band = band or 0
image = self._obj.select(band)
scale = scale or image.projection().nominalScale()
unmasked = image.unmask(proxy_value)
mask = unmasked.eq(proxy_value)
cover = mask.reduceRegion(
ee.Reducer.frequencyHistogram(),
region,
scale=scale,
bestEffort=True,
maxPixels=maxPixels,
tileScale=tileScale,
)
# The cover result is a dictionary with each band as key (in our case the first one).
# For each band key the number of 0 and 1 is stored in a dictionary.
# We need to extract the number of 1 and 0 to compute the ratio which implys lots of casting.
values = ee.Dictionary(cover.values().get(0)).values()
zeros, ones = ee.Number(values.get(0)), ee.Number(values.get(1))
ratio = zeros.divide(zeros.add(ones))
values = ee.Dictionary(cover.values().get(0))
zeros, ones = ee.Number(values.get("0", 0)), ee.Number(values.get("1", 0))
ratio = ones.divide(zeros.add(ones)).multiply(100)

# we want to display this result as a 1 digit float
return ratio

def maskCoverRegions(
self,
collection: ee.FeatureCollection,
scale: Optional[int] = None,
band: Optional[str] = None,
proxy_value: int = -999,
column_name: str = "mask_cover",
tileScale: int = 1,
) -> ee.FeatureCollection:
"""Compute the coverage of masked pixels inside a Geometry.
Parameters:
collection: The collection to compute the mask coverage (in each Feature).
scale: The scale of the computation. In case you need a rough estimation use a higher scale than the original from the image.
band: The band to use. Defaults to the first band.
proxy_value: the value to use for counting the mask and avoid confusing 0s to masked values. Choose a value that is out of the range of the image values.
column_name: name of the column that will hold the value.
tileScale: A scaling factor between 0.1 and 16 used to adjust aggregation tile size; setting a larger tileScale (e.g., 2 or 4) uses smaller tiles and may enable computations that run out of memory with the default.
Returns:
The passed table with the new column containing the percentage of masked pixels within the region
Examples:
.. code-block:: python
import ee, geetools
ee.Initialize()
image = ee.Image('COPERNICUS/S2_SR/20190828T151811_20190828T151809_T18GYT')
reg = ee.Geometry.Point([11.880190936531116, 42.0159494554553]).buffer(2000)
aoi = ee.FeatureCollection([ee.Feature(reg)])
image = image.maskCoverRegions(aoi)
"""
# compute the mask cover
band = band or 0
properties = collection.propertyNames() # original properties
image = self._obj.select(band)
scale = scale or image.projection().nominalScale()
unmasked = image.unmask(proxy_value)
mask = unmasked.eq(proxy_value)
column = "_geetools_histo_"
cover = mask.reduceRegions(
collection=collection,
reducer=ee.Reducer.frequencyHistogram().setOutputs([column]),
scale=scale,
tileScale=tileScale,
)

# we want to display this resutl as a 1 digit float
ratio = ratio.multiply(1000).toInt().divide(10)
def compute_percentage(feat):
"""function to map over the resulting table and compute the percentage from the reducer's output."""
histo = ee.Dictionary(feat.get(column))
zeros, ones = ee.Number(histo.get("0", 0)), ee.Number(histo.get("1", 0))
ratio = ones.divide(zeros.add(ones)).multiply(100)
return feat.select(properties).set(column_name, ratio)

return ee.Image(self._obj.set("mask_cover", ratio))
return cover.map(compute_percentage)

def plot(
self,
Expand Down
43 changes: 32 additions & 11 deletions tests/test_Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,20 +570,41 @@ def other(self):


class TestMaskCover:
"""Test the ``maskCover`` method."""

def test_mask_cover(self):
image = self.image.geetools.maskCover()
assert isclose(image.get("mask_cover").getInfo(), 99.2)

def test_deprecated_mask_cover(self):
with pytest.deprecated_call():
image = geetools.algorithms.maskCover(self.image)
assert isclose(image.get("mask_cover").getInfo(), 99.2)
"""Test the ``maskCoverRegion`` method."""

def test_mask_cover_region(self):
aoi = ee.Geometry.Point([12.210900891755129, 41.928551351175386]).buffer(2200)
ratio = self.image.geetools.maskCoverRegion(aoi, scale=10)
assert isclose(ratio.getInfo(), 9.99, abs_tol=0.01)

def test_mask_cover_region_zero(self):
aoi = ee.Geometry.Point([11.880190936531116, 42.0159494554553]).buffer(1000)
ratio = self.image.geetools.maskCoverRegion(aoi, scale=10)
assert isclose(ratio.getInfo(), 0)

def test_mask_cover_regions(self):
geom = ee.Geometry.Point([12.210900891755129, 41.928551351175386]).buffer(2200)
aoi = ee.FeatureCollection([ee.Feature(geom, {"test_property": 1})])
result = self.image.geetools.maskCoverRegions(aoi, scale=10)
feat = ee.Feature(result.first())
ratio = feat.getInfo()["properties"]["mask_cover"]
# ratio = ee.Number(feat.get('mask_cover'))
# the last line should work, but it doesn't, I don't know why
assert isclose(ratio, 9.99, abs_tol=0.01)

def test_mask_cover_regions_zero(self):
geom = ee.Geometry.Point([11.880190936531116, 42.0159494554553]).buffer(1000)
aoi = ee.FeatureCollection([ee.Feature(geom, {"test_property": 1})])
result = self.image.geetools.maskCoverRegions(aoi, scale=10)
feat = ee.Feature(result.first())
ratio = feat.getInfo()["properties"]["mask_cover"]
# ratio = ee.Number(feat.get('mask_cover'))
# the last line should work, but it doesn't, I don't know why
assert isclose(ratio, 0)

@property
def image(self):
image_id = "COPERNICUS/S2_SR_HARMONIZED/20210105T100319_20210105T100317_T32TQM"
image_id = "COPERNICUS/S2_SR_HARMONIZED/20180401T100019_20180401T100022_T32TQM"
image = ee.Image(image_id)
qa = image.select("QA60")
cloudBitMask, cirrusBitMask = 1 << 10, 1 << 11
Expand Down

0 comments on commit 379ab98

Please sign in to comment.