Skip to content

Commit f0a2f13

Browse files
authored
Merge pull request #79 from GeoscienceAustralia/pre_release_updates
Pre-publication DEA Intertidal updates
2 parents 032b2cb + bb7bef8 commit f0a2f13

16 files changed

+1566
-717
lines changed

.github/workflows/dea-intertidal-image.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ jobs:
111111
- name: Commit validation results into repository
112112
uses: stefanzweifel/git-auto-commit-action@v4
113113
if: github.event_name == 'pull_request'
114+
continue-on-error: true
114115
with:
115116
commit_message: Automatically update integration test validation results
116117
file_pattern: 'tests/validation.jpg tests/validation.csv tests/README.md'

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
!*.yaml
1010
!*.yml
1111
!*.in
12+
!*.txt
1213
!**.github/workflows
1314
!*.gitignore
1415
!*.dockerignore

Dockerfile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ RUN pip install pip-tools
2929

3030
# Pip installation
3131
RUN mkdir -p /conf
32-
COPY requirements.in /conf/
33-
RUN pip-compile --extra-index-url=https://packages.dea.ga.gov.au/ --output-file=/conf/requirements.txt /conf/requirements.in
32+
# COPY requirements.in /conf/
33+
# RUN pip-compile --extra-index-url=https://packages.dea.ga.gov.au/ --output-file=/conf/requirements.txt /conf/requirements.in
34+
COPY requirements.txt /conf/
3435
RUN pip install -r /conf/requirements.txt \
3536
&& pip install --no-cache-dir awscli
3637

intertidal/elevation.py

Lines changed: 64 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
load_data,
2323
load_topobathy_mask,
2424
load_aclum_mask,
25+
load_ocean_mask,
2526
prepare_for_export,
2627
tidal_metadata,
2728
export_dataset_metadata,
@@ -31,7 +32,7 @@
3132
round_date_strings,
3233
)
3334
from intertidal.tide_modelling import pixel_tides_ensemble
34-
from intertidal.extents import extents
35+
from intertidal.extents import extents, ocean_connection
3536
from intertidal.exposure import exposure
3637
from intertidal.tidal_bias_offset import bias_offset
3738

@@ -82,7 +83,7 @@ def ds_to_flat(
8283
If True, remove any seasonal signal from the tide height data
8384
by subtracting monthly mean tide height from each value. This
8485
can reduce false tide correlations in regions where tide heights
85-
correlate with seasonal changes in surface water. Note that
86+
correlate with seasonal changes in surface water. Note that
8687
seasonally corrected tides are only used to identify potentially
8788
tide influenced pixels - not for elevation modelling itself.
8889
valid_mask : xr.DataArray, optional
@@ -131,15 +132,15 @@ def ds_to_flat(
131132
# correlation. This prevents small changes in NDWI beneath the water
132133
# surface from producing correlations with tide height.
133134
wet_dry = flat_ds[index] > ndwi_thresh
134-
135+
135136
# Use either tides directly or correct to remove seasonal signal
136137
if correct_seasonality:
137138
print("Removing seasonal signal before calculating tide correlations")
138-
gb = flat_ds.tide_m.groupby('time.month')
139-
tide_array = (gb - gb.mean())
139+
gb = flat_ds.tide_m.groupby("time.month")
140+
tide_array = gb - gb.mean()
140141
else:
141-
tide_array = flat_ds.tide_m
142-
142+
tide_array = flat_ds.tide_m
143+
143144
if corr_method == "pearson":
144145
corr = xr.corr(wet_dry, tide_array, dim="time").rename("qa_ndwi_corr")
145146
elif corr_method == "spearman":
@@ -558,10 +559,11 @@ def pixel_uncertainty(
558559
max_q=0.75,
559560
):
560561
"""
561-
Calculate uncertainty bounds around a modelled elevation based on
562-
observations that were misclassified by a given NDWI threshold.
562+
Calculate one-sided uncertainty bounds around a modelled elevation
563+
based on observations that were misclassified by a given NDWI
564+
threshold.
563565
564-
The function identifies observations that were misclassified by the
566+
Uncertainty is based observations that were misclassified by the
565567
modelled elevation, i.e., wet observations (NDWI > threshold) at
566568
lower tide heights than the modelled elevation, or dry observations
567569
(NDWI < threshold) at higher tide heights than the modelled
@@ -603,7 +605,8 @@ def pixel_uncertainty(
603605
-------
604606
dem_flat_low, dem_flat_high, dem_flat_uncertainty : xarray.DataArray
605607
The lower and upper uncertainty bounds around the modelled
606-
elevation, and the summary uncertainty range between them.
608+
elevation, and the summary uncertainty range between them
609+
(expressed as one-sided uncertainty).
607610
misclassified_sum : xarray.DataArray
608611
The sum of individual satellite observations misclassified by
609612
the modelled elevation and NDWI threshold.
@@ -666,8 +669,9 @@ def pixel_uncertainty(
666669
dem_flat_low = np.minimum(uncertainty_low, flat_dem.elevation)
667670
dem_flat_high = np.maximum(uncertainty_high, flat_dem.elevation)
668671

669-
# Subtract low from high DEM to summarise uncertainy range
670-
dem_flat_uncertainty = dem_flat_high - dem_flat_low
672+
# Subtract low from high DEM to summarise uncertainty range
673+
# (and divide by two to give one-sided uncertainty)
674+
dem_flat_uncertainty = (dem_flat_high - dem_flat_low) / 2.0
671675

672676
return (
673677
dem_flat_low,
@@ -763,6 +767,7 @@ def clean_edge_pixels(ds):
763767
def elevation(
764768
satellite_ds,
765769
valid_mask=None,
770+
ocean_mask=None,
766771
ndwi_thresh=0.1,
767772
min_freq=0.01,
768773
max_freq=0.99,
@@ -791,6 +796,12 @@ def elevation(
791796
this could be a mask generated from a topo-bathy DEM, used to
792797
limit the analysis to likely intertidal pixels. Default is None,
793798
which will not apply a mask.
799+
ocean_mask : xr.DataArray, optional
800+
An optional mask identifying ocean pixels within the analysis
801+
area, with the same spatial dimensions as `satellite_ds`.
802+
If provided, this will be used to restrict the analysis to pixels
803+
that are directly connected to ocean waters. Defaults is None,
804+
which will not apply a mask.
794805
ndwi_thresh : float, optional
795806
A threshold value for the normalized difference water index
796807
(NDWI) above which pixels are considered water, by default 0.1.
@@ -950,6 +961,16 @@ def elevation(
950961
elevation_bands = [d for d in ds.data_vars if "elevation" in d]
951962
ds[elevation_bands] = clean_edge_pixels(ds[elevation_bands])
952963

964+
# Mask out any non-ocean connected elevation pixels.
965+
# `~(ds.qa_ndwi_freq < min_freq)` ensures that nodata pixels are
966+
# treated as wet
967+
if ocean_mask is not None:
968+
log.info(f"{run_id}: Restricting outputs to ocean-connected waters")
969+
ocean_connected_mask = ocean_connection(
970+
~(ds.qa_ndwi_freq < min_freq), ocean_mask
971+
)
972+
ds[elevation_bands] = ds[elevation_bands].where(ocean_connected_mask)
973+
953974
# Return output data and tide height array
954975
log.info(f"{run_id}: Successfully completed intertidal elevation modelling")
955976
return ds, tide_m
@@ -1067,6 +1088,18 @@ def elevation(
10671088
help="Proportion of the tide range to use for each window radius "
10681089
"in the per-pixel rolling median calculation, by default 0.15.",
10691090
)
1091+
@click.option(
1092+
"--correct_seasonality/--no-correct_seasonality",
1093+
is_flag=True,
1094+
default=False,
1095+
help="If True, remove any seasonal signal from the tide height data "
1096+
"by subtracting monthly mean tide height from each value prior to "
1097+
"correlation calculations. This can reduce false tide correlations "
1098+
"in regions where tide heights correlate with seasonal changes in "
1099+
"surface water. Note that seasonally corrected tides are only used "
1100+
"to identify potentially tide influenced pixels - not for elevation "
1101+
"modelling itself.",
1102+
)
10701103
@click.option(
10711104
"--tide_model",
10721105
type=str,
@@ -1126,6 +1159,7 @@ def intertidal_cli(
11261159
min_correlation,
11271160
windows_n,
11281161
window_prop_tide,
1162+
correct_seasonality,
11291163
tide_model,
11301164
tide_model_dir,
11311165
modelled_freq,
@@ -1175,11 +1209,12 @@ def intertidal_cli(
11751209
)
11761210
satellite_ds.load()
11771211

1178-
# Load topobathy mask from GA's AusBathyTopo 250m 2023 Grid
1212+
# Load topobathy mask from GA's AusBathyTopo 250m 2023 Grid,
1213+
# urban land use class mask from ABARES CLUM, and ocean mask
1214+
# from geodata_coast_100k
11791215
topobathy_mask = load_topobathy_mask(dc, satellite_ds.odc.geobox.compat)
1180-
1181-
# Load urban land use class mask from ABARES CLUM
11821216
reclassified_aclum = load_aclum_mask(dc, satellite_ds.odc.geobox.compat)
1217+
ocean_mask = load_ocean_mask(dc, satellite_ds.odc.geobox.compat)
11831218

11841219
# Also load ancillary dataset IDs to use in metadata
11851220
# (both layers are continental continental products with only
@@ -1193,30 +1228,31 @@ def intertidal_cli(
11931228
ds, tide_m = elevation(
11941229
satellite_ds,
11951230
valid_mask=topobathy_mask,
1231+
ocean_mask=ocean_mask,
11961232
ndwi_thresh=ndwi_thresh,
11971233
min_freq=min_freq,
11981234
max_freq=max_freq,
11991235
min_correlation=min_correlation,
12001236
windows_n=windows_n,
12011237
window_prop_tide=window_prop_tide,
1202-
correct_seasonality=True,
1238+
correct_seasonality=correct_seasonality,
12031239
tide_model=tide_model,
12041240
tide_model_dir=tide_model_dir,
12051241
run_id=run_id,
12061242
log=log,
12071243
)
12081244

1209-
# Calculate extents
1210-
log.info(f"{run_id}: Calculating Intertidal Extents")
1211-
ds["extents"] = extents(
1212-
dem=ds.elevation,
1213-
freq=ds.qa_ndwi_freq,
1214-
corr=ds.qa_ndwi_corr,
1215-
reclassified_aclum=reclassified_aclum,
1216-
min_freq=min_freq,
1217-
max_freq=max_freq,
1218-
min_correlation=min_correlation,
1219-
)
1245+
# # Calculate extents (to be included in next version)
1246+
# log.info(f"{run_id}: Calculating Intertidal Extents")
1247+
# ds["extents"] = extents(
1248+
# dem=ds.elevation,
1249+
# freq=ds.qa_ndwi_freq,
1250+
# corr=ds.qa_ndwi_corr,
1251+
# reclassified_aclum=reclassified_aclum,
1252+
# min_freq=min_freq,
1253+
# max_freq=max_freq,
1254+
# min_correlation=min_correlation,
1255+
# )
12201256

12211257
if exposure_offsets:
12221258
log.info(f"{run_id}: Calculating Intertidal Exposure")
@@ -1251,7 +1287,6 @@ def intertidal_cli(
12511287
) = bias_offset(
12521288
tide_m=tide_m,
12531289
tide_cq=tide_cq,
1254-
extents=ds.extents,
12551290
lot_hot=True,
12561291
lat_hat=True,
12571292
)

intertidal/extents.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,3 +246,103 @@ def extents(
246246
extents = extents.combine_first(0)
247247

248248
return extents
249+
250+
251+
def ocean_connection(water, ocean_da, connectivity=2):
252+
"""
253+
Identifies areas of water pixels that are adjacent to or directly
254+
connected to intertidal pixels.
255+
256+
Parameters:
257+
-----------
258+
water : xarray.DataArray
259+
An array containing True for water pixels.
260+
ocean_da : xarray.DataArray
261+
An array containing True for ocean pixels.
262+
connectivity : integer, optional
263+
An integer passed to the 'connectivity' parameter of the
264+
`skimage.measure.label` function.
265+
266+
Returns:
267+
--------
268+
ocean_connection : xarray.DataArray
269+
An array containing the a mask consisting of identified
270+
ocean-connected pixels as True.
271+
"""
272+
273+
# First, break `water` array into unique, discrete
274+
# regions/blobs.
275+
blobs = xr.apply_ufunc(label, water, 0, False, connectivity)
276+
277+
# For each unique region/blob, use region properties to determine
278+
# whether it overlaps with a feature from `intertidal`. If
279+
# it does, then it is considered to be adjacent or directly connected
280+
# to intertidal pixels
281+
ocean_connection = blobs.isin(
282+
[i.label for i in regionprops(blobs.values, ocean_da.values) if i.max_intensity]
283+
)
284+
285+
return ocean_connection
286+
287+
288+
289+
# from rasterio.features import sieve
290+
291+
292+
# def extents_ocean_masking(
293+
# dem,
294+
# freq,
295+
# corr,
296+
# ocean_mask,
297+
# urban_mask,
298+
# min_freq=0.01,
299+
# max_freq=0.99,
300+
# mostly_dry_freq=0.5,
301+
# min_correlation=0.15,
302+
# ):
303+
# """
304+
# Experimental ocean masking extents code
305+
# """
306+
# # Set NaN values (i.e. pixels masked out over deep water) in frequency to 1
307+
# freq = freq.fillna(1)
308+
309+
# # Identify broad classes based on wetness frequency
310+
# intermittent = (freq >= min_freq) & (freq <= max_freq) # wet and dynamic
311+
# wet_all = freq >= min_freq # all occasionally wet pixels incl. intertidal
312+
# mostly_dry = freq < mostly_dry_freq # dry for majority of the timeseries
313+
314+
# # Classify 'wet_all' pixels into 'wet_ocean' and 'wet_inland' based
315+
# # on connectivity to ocean pixels, and mask out `wet_inland` pixels
316+
# # identified as intensive urban use
317+
# wet_ocean = ocean_connection(wet_all, (ocean_mask | (corr >= 0.5)))
318+
# wet_inland = wet_all & ~wet_ocean & ~urban_mask
319+
320+
# # Distinguish mostly dry intermittent inland from other wet inland
321+
# wet_inland_intermittent = wet_inland & mostly_dry
322+
323+
# # Separate all intertidal from high confidence intertidal pixels
324+
# intertidal = intermittent & (corr >= min_correlation)
325+
# intertidal_hc = dem.notnull() & wet_ocean
326+
327+
# # Identify intertidal fringe pixels (e.g. non-tidally correlated
328+
# # ocean pixels that appear in close proximity to the intertidal zone
329+
# # that are dry for at least half the timeseries.
330+
# intertidal_dilated = mask_cleanup(mask=intertidal, mask_filters=[("dilation", 3)])
331+
# intertidal_fringe = intertidal_dilated & wet_ocean & mostly_dry
332+
333+
# # Combine all layers
334+
# extents = odc.geo.xr.xr_zeros(dem.odc.geobox).astype(np.uint8)
335+
# extents.values[wet_ocean.values] = 3
336+
# extents.values[wet_inland.values] = 2
337+
# extents.values[wet_inland_intermittent.values] = 1
338+
# extents.values[intertidal_fringe.values] = 0
339+
# extents.values[intertidal.values] = 4
340+
341+
# # Reduce noise by sieving all classes except high confidence intertidal.
342+
# # This merges small areas of isolated pixels with their most common neighbour
343+
# extents.values[:] = sieve(extents, 3, connectivity=4)
344+
345+
# # Finally add intertidal high confidence extents over the top
346+
# extents.values[intertidal_hc.values] = 5
347+
348+
# return extents

0 commit comments

Comments
 (0)