Skip to content

Commit d04b1c0

Browse files
emmaaiEmma Ai
andauthored
Fix stripe issue in landcover level34 (#175)
* set high ue but valid data as non-veg * move water_season from level1 to condition of wo frequency in level34 * fix typo * cover the edge cases of landcover level1 * make nodata condition explicit in ue * better organize the logic block of fc masking * normalize water frequency * fix typo --------- Co-authored-by: Emma Ai <emma.ai@ga.gov.au>
1 parent 7e56476 commit d04b1c0

File tree

5 files changed

+153
-190
lines changed

5 files changed

+153
-190
lines changed

odc/stats/plugins/lc_fc_wo_a0.py

Lines changed: 101 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,34 @@ def native_transform(self, xx):
5252
5. Drop the WOfS band
5353
"""
5454

55-
# clear and dry pixels not mask against bit 4: terrain high slope,
55+
# valid and dry pixels not mask against bit 4: terrain high slope,
5656
# bit 3: terrain shadow, and
5757
# bit 2: low solar angle
58-
valid = (xx["water"] & ~((1 << 4) | (1 << 3) | (1 << 2))) == 0
58+
valid = (xx["water"].data & ~((1 << 4) | (1 << 3) | (1 << 2))) == 0
5959

60-
# clear and wet pixels not mask against bit 2: low solar angle
61-
wet = (xx["water"] & ~(1 << 2)) == 128
60+
# clear wet pixels not mask against bit 2: low solar angle
61+
wet = (xx["water"].data & ~(1 << 2)) == 128
62+
63+
# clear dry pixels
64+
clear = xx["water"].data == 0
65+
66+
# get "valid" wo pixels, both dry and wet used in veg_frequency
67+
wet_valid = expr_eval(
68+
"where(a|b, a, _nan)",
69+
{"a": wet, "b": valid},
70+
name="get_valid_pixels",
71+
dtype="float32",
72+
**{"_nan": np.nan},
73+
)
74+
75+
# get "clear" wo pixels, both dry and wet used in water_frequency
76+
wet_clear = expr_eval(
77+
"where(a|b, a, _nan)",
78+
{"a": wet, "b": clear},
79+
name="get_clear_pixels",
80+
dtype="float32",
81+
**{"_nan": np.nan},
82+
)
6283

6384
# dilate both 'valid' and 'water'
6485
for key, val in self.BAD_BITS_MASK.items():
@@ -67,37 +88,64 @@ def native_transform(self, xx):
6788
raw_mask = mask_cleanup(
6889
raw_mask, mask_filters=self.cloud_filters.get(key)
6990
)
70-
valid &= ~raw_mask
71-
wet &= ~raw_mask
72-
91+
valid = expr_eval(
92+
"where(b>0, 0, a)",
93+
{"a": valid, "b": raw_mask.data},
94+
name="get_valid_pixels",
95+
dtype="uint8",
96+
)
97+
wet_clear = expr_eval(
98+
"where(b>0, _nan, a)",
99+
{"a": wet_clear, "b": raw_mask.data},
100+
name="get_clear_pixels",
101+
dtype="float32",
102+
**{"_nan": np.nan},
103+
)
104+
wet_valid = expr_eval(
105+
"where(b>0, _nan, a)",
106+
{"a": wet_valid, "b": raw_mask.data},
107+
name="get_valid_pixels",
108+
dtype="float32",
109+
**{"_nan": np.nan},
110+
)
73111
xx = xx.drop_vars(["water"])
74112

75-
# get valid wo pixels, both dry and wet
76-
data = expr_eval(
77-
"where(a|b, a, _nan)",
78-
{"a": wet.data, "b": valid.data},
79-
name="get_valid_pixels",
80-
dtype="float32",
81-
**{"_nan": np.nan},
82-
)
83-
84113
# Pick out the fc pixels that have an unmixing error of less than the threshold
85-
valid &= xx["ue"] < self.ue_threshold
114+
valid = expr_eval(
115+
"where(b<_v, a, 0)",
116+
{"a": valid, "b": xx["ue"].data},
117+
name="get_low_ue",
118+
dtype="bool",
119+
**{"_v": self.ue_threshold},
120+
)
86121
xx = xx.drop_vars(["ue"])
122+
valid = xr.DataArray(valid, dims=xx["pv"].dims, coords=xx["pv"].coords)
123+
87124
xx = keep_good_only(xx, valid, nodata=NODATA)
88125
xx = to_float(xx, dtype="float32")
89126

90-
xx["wet"] = xr.DataArray(data, dims=wet.dims, coords=wet.coords)
127+
xx["wet_valid"] = xr.DataArray(
128+
wet_valid, dims=xx["pv"].dims, coords=xx["pv"].coords
129+
)
130+
xx["wet_clear"] = xr.DataArray(
131+
wet_clear, dims=xx["pv"].dims, coords=xx["pv"].coords
132+
)
91133

92134
return xx
93135

94136
def fuser(self, xx):
95137

96-
wet = xx["wet"]
138+
wet_valid = xx["wet_valid"]
139+
wet_clear = xx["wet_clear"]
97140

98-
xx = _xr_fuse(xx.drop_vars(["wet"]), partial(_fuse_mean_np, nodata=np.nan), "")
141+
xx = _xr_fuse(
142+
xx.drop_vars(["wet_valid", "wet_clear"]),
143+
partial(_fuse_mean_np, nodata=np.nan),
144+
"",
145+
)
99146

100-
xx["wet"] = _nodata_fuser(wet, nodata=np.nan)
147+
xx["wet_valid"] = _nodata_fuser(wet_valid, nodata=np.nan)
148+
xx["wet_clear"] = _nodata_fuser(wet_clear, nodata=np.nan)
101149

102150
return xx
103151

@@ -123,7 +171,7 @@ def _veg_or_not(self, xx: xr.Dataset):
123171
# mark water freq >= 0.5 as 0
124172
data = expr_eval(
125173
"where(a>0, 0, b)",
126-
{"a": xx["wet"].data, "b": data},
174+
{"a": xx["wet_valid"].data, "b": data},
127175
name="get_veg",
128176
dtype="uint8",
129177
)
@@ -134,25 +182,25 @@ def _water_or_not(self, xx: xr.Dataset):
134182
# mark water freq > 0.5 as 1
135183
data = expr_eval(
136184
"where(a>0.5, 1, 0)",
137-
{"a": xx["wet"].data},
185+
{"a": xx["wet_clear"].data},
138186
name="get_water",
139187
dtype="uint8",
140188
)
141189

142190
# mark nans
143191
data = expr_eval(
144192
"where(a!=a, nodata, b)",
145-
{"a": xx["wet"].data, "b": data},
193+
{"a": xx["wet_clear"].data, "b": data},
146194
name="get_water",
147195
dtype="uint8",
148196
**{"nodata": int(NODATA)},
149197
)
150198
return data
151199

152-
def _max_consecutive_months(self, data, nodata):
153-
nan_mask = da.ones(data.shape[1:], chunks=data.chunks[1:], dtype="bool")
200+
def _max_consecutive_months(self, data, nodata, normalize=False):
154201
tmp = da.zeros(data.shape[1:], chunks=data.chunks[1:], dtype="uint8")
155202
max_count = da.zeros(data.shape[1:], chunks=data.chunks[1:], dtype="uint8")
203+
total = da.zeros(data.shape[1:], chunks=data.chunks[1:], dtype="uint8")
156204

157205
for t in data:
158206
# +1 if not nodata
@@ -180,23 +228,34 @@ def _max_consecutive_months(self, data, nodata):
180228
dtype="uint8",
181229
)
182230

183-
# mark nodata
184-
nan_mask = expr_eval(
185-
"where(a==nodata, b, False)",
186-
{"a": t, "b": nan_mask},
187-
name="mark_nodata",
188-
dtype="bool",
231+
# total valid
232+
total = expr_eval(
233+
"where(a==nodata, b, b+1)",
234+
{"a": t, "b": total},
235+
name="get_total_valid",
236+
dtype="uint8",
189237
**{"nodata": nodata},
190238
)
191239

192240
# mark nodata
193-
max_count = expr_eval(
194-
"where(a, nodata, b)",
195-
{"a": nan_mask, "b": max_count},
196-
name="mark_nodata",
197-
dtype="uint8",
198-
**{"nodata": int(nodata)},
199-
)
241+
if normalize:
242+
max_count = expr_eval(
243+
"where(a<=0, nodata, b/a*12)",
244+
{"a": total, "b": max_count},
245+
name="normalize_max_count",
246+
dtype="float32",
247+
**{"nodata": int(nodata)},
248+
)
249+
max_count = da.ceil(max_count).astype("uint8")
250+
else:
251+
max_count = expr_eval(
252+
"where(a<=0, nodata, b)",
253+
{"a": total, "b": max_count},
254+
name="mark_nodata",
255+
dtype="uint8",
256+
**{"nodata": int(nodata)},
257+
)
258+
200259
return max_count
201260

202261
def reduce(self, xx: xr.Dataset) -> xr.Dataset:
@@ -207,15 +266,15 @@ def reduce(self, xx: xr.Dataset) -> xr.Dataset:
207266
max_count_veg = self._max_consecutive_months(data, NODATA)
208267

209268
data = self._water_or_not(xx)
210-
max_count_water = self._max_consecutive_months(data, NODATA)
269+
max_count_water = self._max_consecutive_months(data, NODATA, normalize=True)
211270

212271
attrs = xx.attrs.copy()
213272
attrs["nodata"] = int(NODATA)
214273
data_vars = {
215-
k: xr.DataArray(v, dims=xx["wet"].dims[1:], attrs=attrs)
274+
k: xr.DataArray(v, dims=xx["pv"].dims[1:], attrs=attrs)
216275
for k, v in zip(self.measurements, [max_count_veg, max_count_water])
217276
}
218-
coords = dict((dim, xx.coords[dim]) for dim in xx["wet"].dims[1:])
277+
coords = dict((dim, xx.coords[dim]) for dim in xx["pv"].dims[1:])
219278
return xr.Dataset(data_vars=data_vars, coords=coords, attrs=xx.attrs)
220279

221280

odc/stats/plugins/lc_veg_class_a1.py

Lines changed: 17 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ def __init__(
5757
saltpan_threshold: Optional[int] = None,
5858
water_threshold: Optional[float] = None,
5959
veg_threshold: Optional[int] = None,
60-
water_seasonality_threshold: Optional[float] = None,
6160
**kwargs,
6261
):
6362
super().__init__(**kwargs)
@@ -70,9 +69,6 @@ def __init__(
7069
)
7170
self.water_threshold = water_threshold if water_threshold is not None else 0.2
7271
self.veg_threshold = veg_threshold if veg_threshold is not None else 2
73-
self.water_seasonality_threshold = (
74-
water_seasonality_threshold if water_seasonality_threshold else 0.25
75-
)
7672
self.output_classes = output_classes
7773

7874
def fuser(self, xx):
@@ -165,23 +161,26 @@ def l3_class(self, xx: xr.Dataset):
165161
},
166162
)
167163

168-
# all unmarked values (0) is terretrial veg
169-
164+
# all unmarked values (0) and 255 > veg >= 2 is terretrial veg
170165
l3_mask = expr_eval(
171-
"where(a<=0, m, a)",
172-
{"a": l3_mask},
166+
"where((a<=0)&(b>=2)&(b<nodata), m, a)",
167+
{"a": l3_mask, "b": xx["veg_frequency"].data},
173168
name="mark_veg",
174169
dtype="uint8",
175-
**{"m": self.output_classes["terrestrial_veg"]},
170+
**{
171+
"m": self.output_classes["terrestrial_veg"],
172+
"nodata": (
173+
NODATA
174+
if xx["veg_frequency"].attrs["nodata"]
175+
!= xx["veg_frequency"].attrs["nodata"]
176+
else xx["veg_frequency"].attrs["nodata"]
177+
),
178+
},
176179
)
177180

178-
# mark nodata if any source is nodata
179-
# issues:
180-
# - nodata information from non-indexed datasets missing
181-
182-
# Mask nans with NODATA
181+
# Mask nans and pixels where non of classes applicable
183182
l3_mask = expr_eval(
184-
"where((a!=a), nodata, e)",
183+
"where((a!=a)|(e<=0), nodata, e)",
185184
{
186185
"a": si5,
187186
"e": l3_mask,
@@ -191,49 +190,15 @@ def l3_class(self, xx: xr.Dataset):
191190
**{"nodata": NODATA},
192191
)
193192

194-
# Now add the water frequency
195-
# Divide water frequency into following classes:
196-
# 0 --> 0
197-
# (0,0.25] --> 1
198-
# (0.25,1] --> 2
199-
200-
water_seasonality = expr_eval(
201-
"where((a > 0) & (a <= wt), 1, a)",
202-
{"a": xx["frequency"].data},
203-
name="mark_wo_fq",
204-
dtype="float32",
205-
**{"wt": self.water_seasonality_threshold},
206-
)
207-
208-
water_seasonality = expr_eval(
209-
"where((a > wt) & (a <= 1), 2, b)",
210-
{"a": xx["frequency"].data, "b": water_seasonality},
211-
name="mark_wo_fq",
212-
dtype="float32",
213-
**{"wt": self.water_seasonality_threshold},
214-
)
215-
216-
water_seasonality = expr_eval(
217-
"where((a != a), nodata, a)",
218-
{
219-
"a": water_seasonality,
220-
},
221-
name="mark_nodata",
222-
dtype="uint8",
223-
**{"nodata": NODATA},
224-
)
225-
226-
return l3_mask, water_seasonality
193+
return l3_mask
227194

228195
def reduce(self, xx: xr.Dataset) -> xr.Dataset:
229-
l3_mask, water_seasonality = self.l3_class(xx)
196+
l3_mask = self.l3_class(xx)
230197
attrs = xx.attrs.copy()
231198
attrs["nodata"] = int(NODATA)
232199
data_vars = {
233200
k: xr.DataArray(v, dims=xx["veg_frequency"].dims[1:], attrs=attrs)
234-
for k, v in zip(
235-
self.measurements, [l3_mask.squeeze(0), water_seasonality.squeeze(0)]
236-
)
201+
for k, v in zip(self.measurements, [l3_mask.squeeze(0)])
237202
}
238203
coords = dict((dim, xx.coords[dim]) for dim in xx["veg_frequency"].dims[1:])
239204
return xr.Dataset(data_vars=data_vars, coords=coords, attrs=xx.attrs)

tests/test_landcover_plugin_a0.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -327,12 +327,12 @@ def test_native_transform(fc_wo_dataset, bits):
327327
np.array([1, 1, 3, 5, 6, 2, 6, 2, 2, 5, 6, 0, 0, 2, 3]),
328328
np.array([0, 3, 2, 1, 3, 5, 6, 1, 4, 5, 6, 0, 2, 4, 2]),
329329
)
330-
result = np.where(out_xx["wet"].data == out_xx["wet"].data)
330+
result = np.where(out_xx["wet_valid"].data == out_xx["wet_valid"].data)
331331
for a, b in zip(expected_valid, result):
332332
assert (a == b).all()
333333

334334
expected_valid = (np.array([1, 2, 3]), np.array([6, 2, 0]), np.array([6, 1, 2]))
335-
result = np.where(out_xx["wet"].data == 1)
335+
result = np.where(out_xx["wet_valid"].data == 1)
336336

337337
for a, b in zip(expected_valid, result):
338338
assert (a == b).all()
@@ -391,11 +391,11 @@ def test_water_or_not(fc_wo_dataset):
391391
xx = xx.groupby("solar_day").map(partial(StatsVegCount.fuser, None))
392392
yy = stats_veg._water_or_not(xx).compute()
393393
valid_index = (
394-
np.array([0, 0, 0, 0, 0, 1, 1, 2, 2, 2, 2, 2, 2, 2]),
395-
np.array([1, 1, 3, 5, 6, 2, 6, 0, 0, 2, 2, 3, 5, 6]),
396-
np.array([0, 3, 2, 1, 3, 5, 6, 0, 2, 1, 4, 2, 5, 6]),
394+
np.array([0, 0, 1, 1, 2, 2, 2]),
395+
np.array([3, 6, 2, 6, 0, 2, 2]),
396+
np.array([2, 3, 5, 6, 2, 1, 4]),
397397
)
398-
expected_value = np.array([0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0])
398+
expected_value = np.array([0, 0, 0, 1, 1, 1, 0])
399399
i = 0
400400
for idx in zip(*valid_index):
401401
assert yy[idx] == expected_value[i]
@@ -423,14 +423,15 @@ def test_reduce(fc_wo_dataset):
423423

424424
expected_value = np.array(
425425
[
426-
[0, 255, 1, 255, 255, 255, 255],
427-
[0, 255, 255, 0, 255, 255, 255],
428-
[255, 1, 255, 255, 0, 0, 255],
426+
[255, 255, 12, 255, 255, 255, 255],
427+
[255, 255, 255, 255, 255, 255, 255],
428+
[255, 12, 255, 255, 0, 0, 255],
429429
[255, 255, 0, 255, 255, 255, 255],
430430
[255, 255, 255, 255, 255, 255, 255],
431-
[255, 0, 255, 255, 255, 0, 255],
432-
[255, 255, 255, 0, 255, 255, 1],
433-
]
431+
[255, 255, 255, 255, 255, 255, 255],
432+
[255, 255, 255, 0, 255, 255, 12],
433+
],
434+
dtype="uint8",
434435
)
435436

436437
assert (xx.water_frequency.data == expected_value).all()

0 commit comments

Comments
 (0)