Skip to content

Commit 479dc32

Browse files
committed
First draft of top level masking functions for bitmask and enumerated masks
1 parent a00c882 commit 479dc32

File tree

3 files changed

+187
-0
lines changed

3 files changed

+187
-0
lines changed

odc/geo/_xr_interop.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@
4848
resolve_fill_value,
4949
resolve_nodata,
5050
)
51+
from .masking import (
52+
bits_to_bool,
53+
enum_to_bool,
54+
scale_and_offset,
55+
scale_and_offset_dataset,
56+
)
5157
from .overlap import compute_output_geobox
5258
from .roi import roi_is_empty
5359
from .types import Nodata, Resolution, SomeNodata, SomeResolution, SomeShape, xy_
@@ -1053,6 +1059,12 @@ def nodata(self, value: Nodata):
10531059

10541060
colorize = _wrap_op(colorize)
10551061

1062+
scale_and_offset = _wrap_op(scale_and_offset)
1063+
1064+
bits_to_bool = _wrap_op(bits_to_bool)
1065+
1066+
enum_to_bool = _wrap_op(enum_to_bool)
1067+
10561068
if have.rasterio:
10571069
write_cog = _wrap_op(write_cog)
10581070
to_cog = _wrap_op(to_cog)
@@ -1093,6 +1105,8 @@ def to_rgba(
10931105
) -> xarray.DataArray:
10941106
return to_rgba(self._xx, bands=bands, vmin=vmin, vmax=vmax)
10951107

1108+
scale_and_offset = _wrap_op(scale_and_offset_dataset)
1109+
10961110

10971111
ODCExtensionDs.to_rgba.__doc__ = to_rgba.__doc__
10981112

odc/geo/masking.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# This file is part of the Open Data Cube, see https://opendatacube.org for more information
2+
#
3+
# Copyright (c) 2015-2020 ODC Contributors
4+
# SPDX-License-Identifier: Apache-2.0
5+
"""
6+
Functions around supporting cloud masking.
7+
"""
8+
9+
from xarray import DataArray, Dataset
10+
11+
12+
def bits_to_bool(
13+
xx: DataArray, bits: list[int] | None, bitflags: int | None, invert: bool = False
14+
) -> DataArray:
15+
"""
16+
Convert integer array into boolean array using bitmasks.
17+
18+
:param xx: DataArray with integer values
19+
:param bits: List of bit positions to convert to a bitflag mask (e.g. [0, 1, 2] -> 0b111)
20+
:param bitflags: Integer value with bits set that will be used to extract the boolean mask (e.g. 0b00011000)
21+
:param invert: Invert the mask
22+
:return: DataArray with boolean values
23+
"""
24+
assert not (
25+
bits is None and bitflags is None
26+
), "Either bits or bitflags must be provided"
27+
assert not (
28+
bits is not None and bitflags is not None
29+
), "Only one of bits or bitflags can be provided"
30+
31+
if bitflags is None:
32+
bitflags = 0
33+
34+
if bits is not None:
35+
for b in bits:
36+
bitflags |= 1 << b
37+
38+
mask = (xx & bitflags) != 0
39+
40+
if invert:
41+
mask = ~mask
42+
43+
return mask
44+
45+
46+
def enum_to_bool(xx: DataArray, values: list, invert: bool = False) -> DataArray:
47+
"""
48+
Convert array into boolean array using a list of invalid values.
49+
50+
:param xx: DataArray with integer values
51+
:param values: List of valid values to convert to a boolean mask
52+
:param invert: Invert the mask
53+
:return: DataArray with boolean values
54+
"""
55+
56+
mask = xx.isin(values)
57+
58+
if invert:
59+
mask = ~mask
60+
61+
return mask
62+
63+
64+
def scale_and_offset(
65+
xx: DataArray,
66+
scale: float | None,
67+
offset: float | None,
68+
ignore_missing: bool = False,
69+
) -> DataArray:
70+
"""
71+
Apply scale and offset to the DataArray. Leave scale and offset blank to use
72+
the values from the DataArray's attrs.
73+
74+
:param xx: DataArray with integer values
75+
:param scale: Scale factor
76+
:param offset: Offset
77+
:return: DataArray with scaled and offset values
78+
"""
79+
80+
# Scales and offsets is used by GDAL.
81+
if scale is None:
82+
scale = xx.attrs.get("scales")
83+
84+
if offset is None:
85+
offset = xx.attrs.get("offsets")
86+
87+
# Catch the case where one is provided and not the other...
88+
if scale is None and offset is not None:
89+
scale = 1.0
90+
91+
if offset is None and scale is not None:
92+
offset = 0.0
93+
94+
if scale is not None and offset is not None:
95+
xx = xx * scale + offset
96+
else:
97+
if not ignore_missing:
98+
raise ValueError(
99+
"Scale and offset not provided and not found in attrs.scales and attrs.offset"
100+
)
101+
102+
return xx
103+
104+
105+
def scale_and_offset_dataset(
106+
xx: Dataset, scale: float | None, offset: float | None
107+
) -> Dataset:
108+
"""
109+
Apply scale and offset to the Dataset. Leave scale and offset blank to use
110+
the values from each DataArray's attrs.
111+
112+
:param xx: Dataset with integer values
113+
:param scale: Scale factor
114+
:param offset: Offset
115+
:return: Dataset with scaled and offset values
116+
"""
117+
118+
for var in xx.data_vars:
119+
xx[var] = scale_and_offset(xx[var], scale, offset, ignore_missing=True)
120+
121+
return xx

tests/test_masking.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from odc.geo.masking import bits_to_bool, enum_to_bool, scale_and_offset
2+
3+
from xarray import DataArray
4+
5+
# Top left is cloud, top right is cloud shadow
6+
# Bottom left is both cloud and cloud shadow, bottom right is neither
7+
xx_bits = DataArray(
8+
[[0b00010000, 0b00001000], [0b00011000, 0b00000000]], dims=("y", "x")
9+
)
10+
11+
# Set up a 2x2 8 bit integer DataArray with some
12+
# values set to 3 (shadow), 9 (high confidence cloud).
13+
xx_values = DataArray([[3, 9], [3, 0]], dims=("y", "x"))
14+
15+
16+
# Test bits_to_bool
17+
def test_bits_to_bool():
18+
# Test with bits
19+
mask = bits_to_bool(xx_bits, bits=[4, 3], bitflags=None)
20+
assert mask.equals(DataArray([[True, True], [True, False]], dims=("y", "x")))
21+
22+
# Test with bitflags
23+
mask = bits_to_bool(xx_bits, bits=None, bitflags=0b00011000)
24+
assert mask.equals(DataArray([[True, True], [True, False]], dims=("y", "x")))
25+
26+
# Test with invert
27+
mask = bits_to_bool(xx_bits, bits=[4, 3], bitflags=None, invert=True)
28+
assert mask.equals(DataArray([[False, False], [False, True]], dims=("y", "x")))
29+
30+
mask = bits_to_bool(xx_bits, bits=None, bitflags=0b00010000, invert=True)
31+
assert mask.equals(DataArray([[False, True], [False, True]], dims=("y", "x")))
32+
33+
34+
# Test enum_to_bool
35+
def test_enum_to_bool():
36+
mask = enum_to_bool(xx_values, values=[3, 9])
37+
assert mask.equals(DataArray([[True, True], [True, False]], dims=("y", "x")))
38+
39+
mask = enum_to_bool(xx_values, values=[3, 9], invert=True)
40+
assert mask.equals(DataArray([[False, False], [False, True]], dims=("y", "x")))
41+
42+
43+
# Test apply_scale_and_offset
44+
def test_scale_and_offset():
45+
mask = scale_and_offset(xx_values, scale=1.0, offset=0.0)
46+
assert mask.equals(DataArray([[3, 9], [3, 0]], dims=("y", "x")))
47+
48+
mask = scale_and_offset(xx_values, scale=None, offset=None, ignore_missing=True)
49+
assert mask.equals(DataArray([[3, 9], [3, 0]], dims=("y", "x")))
50+
51+
mask = scale_and_offset(xx_values, scale=2.0, offset=1.0)
52+
assert mask.equals(DataArray([[7, 19], [7, 1]], dims=("y", "x")))

0 commit comments

Comments
 (0)