Skip to content

Commit

Permalink
EP-3251 DataCube: split mask and mask_polygon processes
Browse files Browse the repository at this point in the history
  • Loading branch information
soxofaan committed Mar 10, 2020
1 parent 91205c2 commit 38a50de
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 91 deletions.
27 changes: 0 additions & 27 deletions openeo/imagecollection.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,33 +424,6 @@ def band(self, band_name) -> 'ImageCollection':
# TODO: does this method have to be defined at the level of the ImageCollection base class? it is only implemented by the rest client
pass

def mask(self, polygon: Union[Polygon, MultiPolygon]=None, srs="EPSG:4326", rastermask: 'ImageCollection'=None,
replacement=None) -> 'ImageCollection':
"""
Mask the image collection using either a polygon or a raster mask.
All pixels outside the polygon should be set to the nodata value.
All pixels inside, or intersecting the polygon should retain their original value.
All pixels are replaced for which the corresponding pixels in the mask are non-zero (for numbers) or True
(for boolean values).
The pixel values are replaced with the value specified for replacement, which defaults to None (no data).
No data values will be left untouched by the masking operation.
TODO: Does mask by polygon imply cropping?
TODO: what about naming? Masking can also be done using a raster mask...
TODO: what will happen if the intersection between the mask and the imagecollection is empty? Raise an error?
:param polygon: A polygon, provided as a :class:`shapely.geometry.Polygon` or :class:`shapely.geometry.MultiPolygon`
:param srs: The reference system of the provided polygon, provided as an 'EPSG:XXXX' string. By default this is Lat Lon (EPSG:4326).
:param rastermask: the raster mask
:param replacement: the value to replace the masked pixels with
:raise: :class:`ValueError` if a polygon is supplied and its area is 0.
:return: A new ImageCollection, with the mask applied.
"""
pass

def merge(self, other: 'ImageCollection') -> 'ImageCollection':
"""
Merge the bands of this data cubes with the bands of another datacube. The bands of 'other' will be appended to the bands
Expand Down
124 changes: 63 additions & 61 deletions openeo/rest/datacube.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import copy
import datetime
import pathlib
import time
import typing
from typing import List, Dict, Union, Tuple
import copy

import shapely.geometry
import shapely.geometry.base
from deprecated import deprecated
from shapely.geometry import Polygon, MultiPolygon, mapping

from openeo.internal.graphbuilder import GraphBuilder
from openeo.imagecollection import ImageCollection, CollectionMetadata
from openeo.internal.graphbuilder import GraphBuilder
from openeo.job import Job
from openeo.util import get_temporal_extent, dict_no_none

Expand Down Expand Up @@ -745,74 +746,75 @@ def linear_scale_range(self, input_min, input_max, output_min, output_max) -> 'I
}
return self.graph_add_process(process_id, args)

def mask(self, polygon: Union[Polygon, MultiPolygon, str] = None, srs="EPSG:4326",
rastermask: 'ImageCollection' = None,
replacement=None) -> 'ImageCollection':
def mask(self, mask: 'DataCube' = None, replacement=None) -> 'DataCube':
"""
Mask the image collection using either a polygon or a raster mask.
Applies a mask to a raster data cube. To apply a vector mask use `mask_polygon`.
All pixels outside the polygon should be set to the nodata value.
All pixels inside, or intersecting the polygon should retain their original value.
A mask is a raster data cube for which corresponding pixels among `data` and `mask`
are compared and those pixels in `data` are replaced whose pixels in `mask` are non-zero
(for numbers) or true (for boolean values).
The pixel values are replaced with the value specified for `replacement`,
which defaults to null (no data).
All pixels are replaced for which the corresponding pixels in the mask are non-zero (for numbers) or True
(for boolean values).
:param mask: the raster mask
:param replacement: the value to replace the masked pixels with
"""
return self.graph_add_process(
process_id="mask",
args=dict_no_none(
data={'from_node': self.builder.result_node},
mask={'from_node': mask.builder.result_node},
replacement=replacement
)
)

The pixel values are replaced with the value specified for replacement, which defaults to None (no data).
No data values will be left untouched by the masking operation.
def mask_polygon(
self, mask: Union[Polygon, MultiPolygon, str, pathlib.Path] = None,
srs="EPSG:4326", replacement=None, inside: bool = None
) -> 'DataCube':
"""
Applies a polygon mask to a raster data cube. To apply a raster mask use `mask`.
# TODO: just provide a single `mask` argument and detect the type: polygon or process graph
# TODO: mask process has been split in mask/mask_polygon
# TODO: also see `mask` vs `mask_polygon` processes in https://github.com/Open-EO/openeo-processes/pull/110
All pixels for which the point at the pixel center does not intersect with any
polygon (as defined in the Simple Features standard by the OGC) are replaced.
This behaviour can be inverted by setting the parameter `inside` to true.
:param polygon: A polygon, provided as a :class:`shapely.geometry.Polygon` or :class:`shapely.geometry.MultiPolygon`, or a filename pointing to a valid vector file
The pixel values are replaced with the value specified for `replacement`,
which defaults to `no data`.
:param mask: A polygon, provided as a :class:`shapely.geometry.Polygon` or :class:`shapely.geometry.MultiPolygon`, or a filename pointing to a valid vector file
:param srs: The reference system of the provided polygon, by default this is Lat Lon (EPSG:4326).
:param rastermask: the raster mask
:param replacement: the value to replace the masked pixels with
:raise: :class:`ValueError` if a polygon is supplied and its area is 0.
:return: A new ImageCollection, with the mask applied.
"""
mask = None
new_collection = None
if polygon is not None:
if isinstance(polygon, (str, pathlib.Path)):
# TODO: default to loading file client side?
# TODO: change read_vector to load_uploaded_files https://github.com/Open-EO/openeo-processes/pull/106
new_collection = self.graph_add_process('read_vector', args={
'filename': str(polygon)
})

mask = {
'from_node': new_collection.builder.result_node
}
else:
if polygon.area == 0:
raise ValueError("Mask {m!s} has an area of {a!r}".format(m=polygon, a=polygon.area))

geojson = mapping(polygon)
geojson['crs'] = {
'type': 'name',
'properties': {
'name': srs
}
}
mask = geojson
new_collection = self
elif rastermask is not None:
mask = {'from_node': rastermask.builder.result_node}
new_collection = self
"""
if isinstance(mask, (str, pathlib.Path)):
# TODO: default to loading file client side?
# TODO: change read_vector to load_uploaded_files https://github.com/Open-EO/openeo-processes/pull/106
read_vector = self.graph_add_process(
process_id='read_vector',
args={'filename': str(mask)}
)
mask = {'from_node': read_vector.builder.result_node}
elif isinstance(mask, shapely.geometry.base.BaseGeometry):
if mask.area == 0:
raise ValueError("Mask {m!s} has an area of {a!r}".format(m=mask, a=mask.area))
mask = shapely.geometry.mapping(mask)
mask['crs'] = {
'type': 'name',
'properties': {'name': srs}
}
else:
raise AttributeError("mask process: either a polygon or a rastermask should be provided.")
# Assume mask is already a valid GeoJSON object
assert "type" in mask

process_id = 'mask'

args = {
'data': {'from_node': self.builder.result_node},
'mask': mask
}
if replacement is not None:
args['replacement'] = replacement

return new_collection.graph_add_process(process_id, args)
return self.graph_add_process(
process_id="mask",
args=dict_no_none(
data={"from_node": self.builder.result_node},
mask=mask,
replacement=replacement,
inside=inside
)
)

def merge(self, other: 'DataCube') -> 'DataCube':
# TODO: overlap_resolver parameter
Expand Down
6 changes: 3 additions & 3 deletions tests/rest/test_datacube.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def con100(requests_mock) -> Connection:
def test_mask_polygon(con100: Connection):
img = con100.load_collection("S2")
polygon = shapely.geometry.box(0, 0, 1, 1)
masked = img.mask(polygon=polygon)
masked = img.mask_polygon(mask=polygon)
assert sorted(masked.graph.keys()) == ["loadcollection1", "mask1"]
assert masked.graph["mask1"] == {
"process_id": "mask",
Expand All @@ -49,7 +49,7 @@ def test_mask_polygon(con100: Connection):

def test_mask_polygon_path(con100: Connection):
img = con100.load_collection("S2")
masked = img.mask(polygon="path/to/polygon.json")
masked = img.mask_polygon(mask="path/to/polygon.json")
assert sorted(masked.graph.keys()) == ["loadcollection1", "mask1", "readvector1"]
assert masked.graph["mask1"] == {
"process_id": "mask",
Expand All @@ -68,7 +68,7 @@ def test_mask_polygon_path(con100: Connection):
def test_mask_raster(con100: Connection):
img = con100.load_collection("S2")
mask = con100.load_collection("MASK")
masked = img.mask(rastermask=mask, replacement=102)
masked = img.mask(mask=mask, replacement=102)
assert masked.graph["mask1"] == {
"process_id": "mask",
"arguments": {
Expand Down

0 comments on commit 38a50de

Please sign in to comment.