import enum
import dask.array as da
import numpy as np
import xarray as xr
[docs]class QABits(enum.Enum):
"""QA bits.
Reference:
https://www.usgs.gov/landsat-missions/landsat-project-documents
"""
landsat_c2_l2 = {
'fill': 0,
'dilated_cloud': 1,
'cirrus': 2,
'cloud': 3,
'cloud_shadow': 4,
'snow': 5,
'clear': 6,
'water': 7,
}
[docs]class QAMasker(object):
"""A class for masking bit-packed quality flags.
Args:
qa (DataArray): The band quality array.
sensor (str): The sensor name. Choices are ['ard', 'hls', 'l8-pre', 'l8-c1', 'l-c1', 'modis', 's2a', 's2c'].
Codes:
'ard':
`USGS Landsat Analysis Ready Data <https://www.usgs.gov/land-resources/nli/landsat/us-landsat-analysis-ready-data?qt-science_support_page_related_con=0#qt`-science_support_page_related_con>`_
'hls':
`NASA Harmonized Landsat Sentinel <https://hls.gsfc.nasa.gov/>`_
'l-c1':
Landsat Collection 1 L4-5 and L7
'l8-c1':
Landsat Collection 1 L8
's2a':
Sentinel 2A (surface reflectance)
's2c':
Sentinel 2C (top of atmosphere)
mask_items (str list): A list of items to mask.
modis_qa_position (Optional[int]): The MODIS QA band position. Default is 1.
modis_quality (Optional[int]): The MODIS quality level. Default is 2.
confidence_level (Optional[str]): The confidence level. Choices are ['notdet', 'no', 'maybe', 'yes'].
References:
Landsat Collection 1:
https://landsat.usgs.gov/collectionqualityband
Examples:
>>> import geowombat as gw
>>> from geowombat.radiometry import QAMasker
>>>
>>> # Get the MODIS cloud mask.
>>> with gw.open('qa.tif') as qa:
>>> mask = QAMasker(qs, 'modis').to_mask()
>>>
>>> # NASA HLS
>>> with gw.open('qa.tif') as qa:
>>> mask = QAMasker(qs, 'hls', ['cloud']).to_mask()
"""
def __init__(
self,
qa,
sensor,
mask_items=None,
modis_qa_band=1,
modis_quality=2,
confidence_level='yes',
):
self.qa = qa
self.sensor = sensor
self.modis_qa_band = modis_qa_band
self.modis_quality = modis_quality
self.mask_items = mask_items
self.confidence_level = confidence_level
self._set_dicts()
[docs] def to_mask(self):
"""Converts QA bit-packed data to an integer mask.
Returns:
``xarray.DataArray``:
0: clear,
1: water,
2: shadow,
3: snow or ice,
4: cloud,
5: cirrus cloud,
6: adjacent cloud,
7: saturated,
8: dropped,
9: terrain occluded,
255: fill
"""
if self.sensor == 'MODIS':
mask = self._get_modis_qa_mask()
else:
mask = da.zeros(
(self.qa.gw.nrows, self.qa.gw.ncols),
chunks=(self.qa.gw.row_chunks, self.qa.gw.col_chunks),
dtype='uint8',
)
for mask_item in self.mask_items:
if mask_item in self.qa_flags[self.sensor]:
if 'conf' in mask_item:
# Has high confidence that
# this condition was met.
mask_value = self.conf_dict[self.confidence_level]
else:
mask_value = 1
mask = da.where(
self._get_qa_mask(mask_item) >= mask_value,
self.fmask_dict[mask_item],
mask,
)
if self.qa.gw.has_time:
mask = xr.DataArray(
mask,
dims=('time', 'y', 'x'),
coords={'time': self.qa.time, 'y': self.qa.y, 'x': self.qa.x},
attrs=self.qa.attrs,
)
else:
mask = xr.DataArray(
mask,
dims=('y', 'x'),
coords={'y': self.qa.y, 'x': self.qa.x},
attrs=self.qa.attrs,
)
mask = mask.expand_dims(dim='band')
mask = mask.assign_coords(band=['mask'])
for k, v in self.fmask_dict.items():
mask.attrs[k] = v
return mask
def _set_dicts(self):
self.fmask_dict = dict(
clear=0,
water=1,
shadow=2,
shadowconf=2,
snow=3,
snowice=3,
snowiceconf=3,
cloud=4,
cloudconf=4,
cirrus=5,
cirrusconf=5,
adjacent=6,
saturated=7,
dropped=8,
terrain=0,
fill=255,
)
self.conf_dict = dict(notdet=0, no=1, maybe=2, yes=3)
self.qa_flags = {
'hls': {
'cirrus': (0, 0),
'cloud': (1, 1),
'adjacent': (2, 2),
'shadow': (3, 3),
'snowice': (4, 4),
'water': (5, 5),
},
'l8-pre': {
'cirrus': (13, 12),
'snowice': (11, 10),
'water': (5, 4),
'fill': (0, 0),
'dropped': (1, 1),
'terrain': (2, 2),
'shadow': (7, 6),
'vegconf': (9, 8),
'snowiceconf': (11, 10),
'cirrusconf': (13, 12),
'cloudconf': (15, 14),
},
'l8-c1': {
'cirrusconf': (12, 11),
'snowiceconf': (10, 9),
'shadowconf': (8, 7),
'cloudconf': (6, 5),
'cloud': (4, 4),
'saturated': (3, 2),
'terrain': (1, 1),
'fill': (0, 0),
},
'l-c1': {
'fill': (0, 0),
'dropped': (1, 1),
'saturated': (3, 2),
'cloud': (4, 4),
'cloudconf': (6, 5),
'shadowconf': (8, 7),
'snowice': (10, 9),
},
'ard': {
'fill': (0, 0),
'clear': (1, 1),
'water': (2, 2),
'shadow': (3, 3),
'snow': (4, 4),
'cloud': (5, 5),
},
'modis': {
'cloud': (0, 0),
'daynight': (3, 3),
'sunglint': (4, 4),
'snowice': (5, 5),
'landwater': (7, 6),
},
's2a': {'cloud': (10, 10), 'cirrus': (11, 11)},
's2c': {'cloud': (10, 10), 'cirrus': (11, 11)},
}
self.modis_bit_shifts = {1: 0, 2: 4, 3: 8, 4: 12, 5: 16, 6: 20, 7: 24}
def _qa_bits(self, mask_item):
"""
Args:
mask_item (str)
For confidence bits:
0 = not determined
1 = no
2 = maybe
3 = yes
"""
bit_location = self.qa_flags[self.sensor][mask_item]
self.b1 = bit_location[0]
self.b2 = bit_location[1]
def _get_modis_qa_mask(self):
"""
Reference:
https://github.com/haoliangyu/pymasker/blob/master/pymasker.py
"""
# `modis_mask`
# 0: best quality
# 1: good quality
# 4: fill value
#
# `output`
# 0: good data = clear
# 255: bad data = fill
return np.where(
np.uint8(self.qa >> self.modis_bit_shifts[self.modis_qa_band] & 4)
<= self.modis_quality,
self.fmask_dict['clear'],
self.fmask_dict['fill'],
)
def _get_qa_mask(self, mask_item):
"""
Args:
mask_item (str)
Reference:
https://github.com/mapbox/landsat8-qa/blob/master/landsat8_qa/qa.py
"""
self._qa_bits(mask_item)
width_int = int((self.b1 - self.b2 + 1) * '1', 2)
return ((self.qa.data.squeeze() >> self.b2) & width_int).astype(
'uint8'
)