dpolcat 0.2.0.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dpolcat/__init__.py +2 -0
- dpolcat/core.py +144 -0
- dpolcat/fast_frost.py +184 -0
- dpolcat/modelling.py +124 -0
- dpolcat/pc_rio_driver.py +31 -0
- dpolcat/processor.py +777 -0
- dpolcat/utils.py +412 -0
- dpolcat-0.2.0.dev1.dist-info/METADATA +202 -0
- dpolcat-0.2.0.dev1.dist-info/RECORD +11 -0
- dpolcat-0.2.0.dev1.dist-info/WHEEL +4 -0
- dpolcat-0.2.0.dev1.dist-info/licenses/LICENSE +201 -0
dpolcat/utils.py
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import marimo
|
|
2
|
+
|
|
3
|
+
__generated_with = "0.25.1"
|
|
4
|
+
app = marimo.App(width="medium")
|
|
5
|
+
|
|
6
|
+
with app.setup:
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
from collections.abc import Iterable
|
|
9
|
+
from functools import partial
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import tomllib
|
|
12
|
+
from typing import Literal
|
|
13
|
+
import xml.etree.ElementTree as ET
|
|
14
|
+
|
|
15
|
+
import bokeh.models
|
|
16
|
+
import dask
|
|
17
|
+
# from dask.diagnostics import ProgressBar
|
|
18
|
+
import geopandas as gpd
|
|
19
|
+
import numpy as np
|
|
20
|
+
import odc.stac
|
|
21
|
+
# from osgeo import gdal
|
|
22
|
+
import pandas as pd
|
|
23
|
+
import pystac
|
|
24
|
+
import pystac_client
|
|
25
|
+
# import planetary_computer
|
|
26
|
+
import shapely.geometry as sg
|
|
27
|
+
import xarray
|
|
28
|
+
import holoviews as hv
|
|
29
|
+
import hvplot.pandas
|
|
30
|
+
import hvplot.xarray
|
|
31
|
+
import rioxarray
|
|
32
|
+
import zarr
|
|
33
|
+
|
|
34
|
+
import dpolcat.core as dpolcat
|
|
35
|
+
import dpolcat.fast_frost as ff
|
|
36
|
+
from dpolcat.pc_rio_driver import PCRioDriver
|
|
37
|
+
|
|
38
|
+
# gdal.UseExceptions()
|
|
39
|
+
|
|
40
|
+
# hv.extension("bokeh")
|
|
41
|
+
|
|
42
|
+
# Want total coverage, with some wiggle room due to reprojection
|
|
43
|
+
MIN_COVERAGE_FRACTION = 0.99
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@app.cell
|
|
47
|
+
def _():
|
|
48
|
+
import marimo as mo
|
|
49
|
+
|
|
50
|
+
return (mo,)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.cell(hide_code=True)
|
|
54
|
+
def _(mo):
|
|
55
|
+
mo.md(r"""
|
|
56
|
+
Luke McQuade, October 2026
|
|
57
|
+
|
|
58
|
+
# `dpolcat utils`
|
|
59
|
+
|
|
60
|
+
Utility/helper functions etc. for working with *dpolcat* in various settings.
|
|
61
|
+
|
|
62
|
+
/// admonition | Info
|
|
63
|
+
|
|
64
|
+
This notebook is designed around the concept of [marimo reusable functions](https://docs.marimo.io/guides/reusing_functions/). Be careful changing things!
|
|
65
|
+
|
|
66
|
+
///
|
|
67
|
+
""")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@app.cell(hide_code=True)
|
|
72
|
+
def _(mo):
|
|
73
|
+
mo.md(r"""
|
|
74
|
+
## Search
|
|
75
|
+
|
|
76
|
+
Finding Sentinel-1 STAC items that work well together for a *dpolcat* time series is a little more involved than for optical imagery. For consistent comparisons over time, especially in hilly/mountainous areas of interest (AoIs), the sensing geometry should be as similar as possible. I.e., items should should have the same orbit direction and relative orbit number. However, items covering the AoI with the same `datatake_id` (continuous take over a day's orbit), can be safely combined (which are then referred to as 'scenes'). These are the rules governing the search functions here.
|
|
77
|
+
""")
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@app.function
|
|
82
|
+
def find_stac_items(
|
|
83
|
+
aoi: gpd.GeoDataFrame, date_range: str
|
|
84
|
+
) -> gpd.GeoDataFrame:
|
|
85
|
+
"""
|
|
86
|
+
Find Sentinel-1 RTC scenes from the Microsoft Planetary Computer catalog intersecting the `aoi`.
|
|
87
|
+
- `aoi` is a single-row GeoDataFrame.
|
|
88
|
+
- `date_range` is a string with format `{from_date}/{to_date}`, e.g., "2020-01-01/2020-03-31".
|
|
89
|
+
|
|
90
|
+
Returns a GeoDataFrame representing the STAC items, ordered by datetime. The original pystac.Item is stashed in the _item field.
|
|
91
|
+
"""
|
|
92
|
+
STAC_COLLECTION = "sentinel-1-rtc"
|
|
93
|
+
|
|
94
|
+
catalog = pystac_client.Client.open(
|
|
95
|
+
"https://planetarycomputer.microsoft.com/api/stac/v1",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if len(aoi.geometry) != 1:
|
|
99
|
+
raise ValueError(
|
|
100
|
+
"Expected only a single geometry for the given AoI."
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
search = catalog.search(
|
|
104
|
+
collections=[STAC_COLLECTION],
|
|
105
|
+
datetime=date_range,
|
|
106
|
+
intersects=aoi.to_crs("EPSG:4326").geometry[0],
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
items = search.item_collection()
|
|
110
|
+
|
|
111
|
+
items_gdf = gpd.GeoDataFrame.from_features(items, crs="EPSG:4326")
|
|
112
|
+
|
|
113
|
+
# Add the item ID, which isn't included in the above.
|
|
114
|
+
items_gdf["id"] = [item.id for item in items]
|
|
115
|
+
|
|
116
|
+
# datetime is originally a string, so convert it.
|
|
117
|
+
items_gdf["datetime"] = pd.to_datetime(items_gdf["datetime"])
|
|
118
|
+
|
|
119
|
+
# Stash the original item (a shallow copy) for later.
|
|
120
|
+
items_gdf["_item"] = items
|
|
121
|
+
|
|
122
|
+
return items_gdf.sort_values("datetime")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@app.function
|
|
126
|
+
def calc_coverages(
|
|
127
|
+
items_gdf: gpd.GeoDataFrame, aoi: gpd.GeoDataFrame
|
|
128
|
+
) -> pd.DataFrame:
|
|
129
|
+
"""
|
|
130
|
+
Calculate full vs partial scene coverages by orbit for the given AoI, grouped by `datatake_id`, i.e.,
|
|
131
|
+
approx. same time.
|
|
132
|
+
- `items_gdf` is a GeoDataFrame representing the STAC items.
|
|
133
|
+
- `aoi` is a single-row GeoDataFrame.
|
|
134
|
+
|
|
135
|
+
Returns a DataFrame containing the orbit, and number of `full_scenes` and `partial_scenes` covered,
|
|
136
|
+
ordered by number of `full_scenes` (highest first).
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
# Reproject to UTM for area calculation, which is planar.
|
|
140
|
+
utm_crs = items_gdf.estimate_utm_crs()
|
|
141
|
+
items_gdf = items_gdf.to_crs(utm_crs)
|
|
142
|
+
aoi = aoi.to_crs(utm_crs)
|
|
143
|
+
|
|
144
|
+
intersection = items_gdf.dissolve(
|
|
145
|
+
by=["sat:orbit_state", "sat:relative_orbit", "s1:datatake_id"]
|
|
146
|
+
).intersection(aoi.geometry[0])
|
|
147
|
+
overlap_fraction = (intersection.area / aoi.area[0]).rename(
|
|
148
|
+
"overlap_fraction"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
full_scenes_count = (
|
|
152
|
+
overlap_fraction[overlap_fraction >= MIN_COVERAGE_FRACTION]
|
|
153
|
+
.groupby(["sat:orbit_state", "sat:relative_orbit"])
|
|
154
|
+
.count()
|
|
155
|
+
)
|
|
156
|
+
partial_scenes_count = (
|
|
157
|
+
overlap_fraction[overlap_fraction < MIN_COVERAGE_FRACTION]
|
|
158
|
+
.groupby(["sat:orbit_state", "sat:relative_orbit"])
|
|
159
|
+
.count()
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
df_coverages = pd.DataFrame(
|
|
163
|
+
{
|
|
164
|
+
"full_scenes": full_scenes_count,
|
|
165
|
+
"partial_scenes": partial_scenes_count,
|
|
166
|
+
}
|
|
167
|
+
).fillna(0)
|
|
168
|
+
|
|
169
|
+
return df_coverages.sort_values("full_scenes", ascending=False)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@app.function
|
|
173
|
+
def select_by_orbit(
|
|
174
|
+
items_gdf,
|
|
175
|
+
orbit_direction: Literal["ascending", "descending"],
|
|
176
|
+
relative_orbit: int,
|
|
177
|
+
) -> Iterable[pystac.Item]:
|
|
178
|
+
"""
|
|
179
|
+
Selects the STAC items for the given orbit direction and relative orbit.
|
|
180
|
+
- `items_gdf` is a GeoDataFrame representing the source STAC items.
|
|
181
|
+
- `orbit_direction` is either "ascending" or "descending".
|
|
182
|
+
- `relative_orbit` is the relative orbit number.
|
|
183
|
+
Returns a filtered collection of `pystac.Item`s.
|
|
184
|
+
"""
|
|
185
|
+
return items_gdf.loc[
|
|
186
|
+
(items_gdf["sat:orbit_state"] == orbit_direction)
|
|
187
|
+
& (items_gdf["sat:relative_orbit"] == relative_orbit)
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@app.cell(hide_code=True)
|
|
192
|
+
def _(mo):
|
|
193
|
+
mo.md(r"""
|
|
194
|
+
### Test/demo
|
|
195
|
+
""")
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@app.cell
|
|
200
|
+
def _():
|
|
201
|
+
test_aoi = gpd.read_file("gis/aois/salzburg_south_4326.gpkg")
|
|
202
|
+
test_date_range = "2025-03-01/2025-03-31"
|
|
203
|
+
return test_aoi, test_date_range
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@app.cell
|
|
207
|
+
def _(test_aoi, test_date_range):
|
|
208
|
+
test_items = find_stac_items(aoi=test_aoi, date_range=test_date_range)
|
|
209
|
+
return (test_items,)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@app.cell
|
|
213
|
+
def _(test_items):
|
|
214
|
+
test_items
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@app.cell
|
|
219
|
+
def _(test_aoi, test_items):
|
|
220
|
+
test_scene_coverages = calc_coverages(test_items, aoi=test_aoi)
|
|
221
|
+
test_scene_coverages
|
|
222
|
+
return (test_scene_coverages,)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@app.cell(hide_code=True)
|
|
226
|
+
def _(mo):
|
|
227
|
+
mo.md(r"""
|
|
228
|
+
Find the orbit with the highest number of fully-covering scenes.
|
|
229
|
+
""")
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@app.cell
|
|
234
|
+
def _(test_scene_coverages):
|
|
235
|
+
_best_match = test_scene_coverages.reset_index().iloc[0]
|
|
236
|
+
test_best_match = _best_match["sat:orbit_state"], _best_match["sat:relative_orbit"]
|
|
237
|
+
test_best_match
|
|
238
|
+
return (test_best_match,)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@app.cell
|
|
242
|
+
def _(test_best_match, test_items):
|
|
243
|
+
test_sel_items = select_by_orbit(test_items,*test_best_match)
|
|
244
|
+
test_sel_items["_item"].values
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@app.cell(hide_code=True)
|
|
249
|
+
def _(mo):
|
|
250
|
+
mo.md(r"""
|
|
251
|
+
## Analysis
|
|
252
|
+
""")
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@app.function
|
|
257
|
+
def frost_xr(
|
|
258
|
+
da: xarray.DataArray, nanny=True, damping_factor=2.0, win_size=5
|
|
259
|
+
) -> xarray.DataArray:
|
|
260
|
+
"""Apply 2D (y, x) Frost filter to 3D (y, x, time) xarray DataArray; See underlying filter function for damping_factor and win_size."""
|
|
261
|
+
# Mostly Claude (Opus 5.5)
|
|
262
|
+
f = ff.frost_filter_nanny if nanny else ff.frost_filter_fast
|
|
263
|
+
g = np.vectorize(
|
|
264
|
+
partial(f, damping_factor=damping_factor, win_size=win_size),
|
|
265
|
+
signature="(m,n)->(m,n)",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# In case da holds a materialized np array, operate directly...
|
|
269
|
+
if da.chunks is None:
|
|
270
|
+
return da.copy(data=g(da.data))
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# ...otherwise, set up parallel calc with overlapping windows
|
|
274
|
+
# (because it's a neighbourhood operation).
|
|
275
|
+
h = win_size // 2
|
|
276
|
+
data = da.data.map_overlap(
|
|
277
|
+
g,
|
|
278
|
+
depth=(0, h, h),
|
|
279
|
+
boundary="none",
|
|
280
|
+
meta=np.array((), dtype=da.dtype),
|
|
281
|
+
)
|
|
282
|
+
return da.copy(data=data)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@app.function
|
|
286
|
+
def normalize_db(arr, min=-30, max=10):
|
|
287
|
+
"""Shift, scale and clip the input array in dB to the range 0 to 1"""
|
|
288
|
+
# E.g. range -30 to +10 -> 0.0 to 1.0
|
|
289
|
+
|
|
290
|
+
return ((arr - min) / (max - min)).clip(0.0, 1.0)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@app.cell(hide_code=True)
|
|
294
|
+
def _(mo):
|
|
295
|
+
mo.md(r"""
|
|
296
|
+
## Visualization
|
|
297
|
+
""")
|
|
298
|
+
return
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@app.function
|
|
302
|
+
def make_cmap():
|
|
303
|
+
"""Make a HoloViews-compatible categorical colormap (string key is needed)."""
|
|
304
|
+
return {
|
|
305
|
+
str(i): list(dpolcat.color_list[i])
|
|
306
|
+
for i in range(len(dpolcat.color_list))
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
@app.function
|
|
311
|
+
def get_hex_colors(cat_indexes = range(dpolcat.NUM_CATEGORIES)):
|
|
312
|
+
"""Make a hex-string-format colour list for the given categories."""
|
|
313
|
+
return [
|
|
314
|
+
f"#{r:02x}{g:02x}{b:02x}"
|
|
315
|
+
for r, g, b in dpolcat.color_list[cat_indexes]
|
|
316
|
+
]
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@app.function
|
|
320
|
+
def make_legend(size=350):
|
|
321
|
+
"""Create a legend (in the form of a Holoviews plot)."""
|
|
322
|
+
n_steps = 100
|
|
323
|
+
steps = np.linspace(-30.5, 10.5, n_steps)
|
|
324
|
+
vv_ramp = np.vstack([steps] * n_steps)
|
|
325
|
+
vh_ramp = vv_ramp.T
|
|
326
|
+
|
|
327
|
+
cat_values = xarray.DataArray(
|
|
328
|
+
dpolcat.categorize_np(vv_ramp, vh_ramp),
|
|
329
|
+
coords=[steps, steps],
|
|
330
|
+
dims=["VH", "VV"],
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
label_data = []
|
|
334
|
+
for i in range(1, dpolcat.NUM_CATEGORIES):
|
|
335
|
+
box = cat_values.where(cat_values == i, drop=True)
|
|
336
|
+
center_x = float(box["VV"].mean())
|
|
337
|
+
center_y = float(box["VH"].mean())
|
|
338
|
+
label_data.append((center_x, center_y, str(i)))
|
|
339
|
+
|
|
340
|
+
labels = hv.Labels(
|
|
341
|
+
label_data, kdims=["VV", "VH"], vdims=["text"]
|
|
342
|
+
).opts(text_color="black")
|
|
343
|
+
|
|
344
|
+
return (
|
|
345
|
+
cat_values.astype("str")
|
|
346
|
+
.hvplot(cmap=make_cmap())
|
|
347
|
+
.opts(
|
|
348
|
+
width=size,
|
|
349
|
+
height=size,
|
|
350
|
+
title="NoData (0) is black",
|
|
351
|
+
xlabel="VV (dB)",
|
|
352
|
+
ylabel="VH (dB)",
|
|
353
|
+
)
|
|
354
|
+
* labels
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
@app.cell(hide_code=True)
|
|
359
|
+
def _(mo):
|
|
360
|
+
mo.md(r"""
|
|
361
|
+
### Test/demo
|
|
362
|
+
""")
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@app.cell
|
|
367
|
+
def _():
|
|
368
|
+
make_legend()
|
|
369
|
+
return
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
@app.cell
|
|
373
|
+
def _():
|
|
374
|
+
_n = dpolcat.NUM_CATEGORIES
|
|
375
|
+
_color = get_hex_colors(range(_n))
|
|
376
|
+
pd.DataFrame(
|
|
377
|
+
{
|
|
378
|
+
"category": range(_n),
|
|
379
|
+
"count": [10] * _n,
|
|
380
|
+
"color": _color,
|
|
381
|
+
}
|
|
382
|
+
).hvplot.bar(x="category", y="count", color="color")
|
|
383
|
+
return
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@app.cell(hide_code=True)
|
|
387
|
+
def _(mo):
|
|
388
|
+
mo.md(r"""
|
|
389
|
+
### QGIS Colour Map
|
|
390
|
+
|
|
391
|
+
Create a QGIS Color Map file that can be used for dpolcat layer symbology.
|
|
392
|
+
""")
|
|
393
|
+
return
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@app.function
|
|
397
|
+
def write_qgis_colormap():
|
|
398
|
+
lines = []
|
|
399
|
+
|
|
400
|
+
for i, c in enumerate(dpolcat.color_list):
|
|
401
|
+
label = i
|
|
402
|
+
a = 255
|
|
403
|
+
r, g, b = c
|
|
404
|
+
lines.append(f"{i} {r} {g} {b} {a} {label}\n")
|
|
405
|
+
|
|
406
|
+
f = open("dpolcat-qgis-cmap.clr", "w")
|
|
407
|
+
f.writelines(lines)
|
|
408
|
+
f.close()
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
if __name__ == "__main__":
|
|
412
|
+
app.run()
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: dpolcat
|
|
3
|
+
Version: 0.2.0.dev1
|
|
4
|
+
Summary: Semantic categorization of dual-polarimetric synthetic aperture radar (SAR) imagery.
|
|
5
|
+
Project-URL: Homepage, https://github.com/lukesdm/dpolcat
|
|
6
|
+
Project-URL: Issues, https://github.com/lukesdm/dpolcat/issues
|
|
7
|
+
Author-email: Luke McQuade <2824165+lukesdm@users.noreply.github.com>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Requires-Dist: geopandas<2,>=1.2.0
|
|
12
|
+
Requires-Dist: marimo<0.26,>=0.25.1
|
|
13
|
+
Requires-Dist: numba<0.69,>=0.68.0
|
|
14
|
+
Requires-Dist: odc-stac<0.6,>=0.5.3
|
|
15
|
+
Requires-Dist: pandas<4,>=3.0.6
|
|
16
|
+
Requires-Dist: planetary-computer<2,>=1.0.0
|
|
17
|
+
Requires-Dist: pystac-client<0.10,>=0.9.0
|
|
18
|
+
Requires-Dist: rioxarray<0.24,>=0.23.0
|
|
19
|
+
Requires-Dist: xarray<2027,>=2026.9.0
|
|
20
|
+
Provides-Extra: gdal
|
|
21
|
+
Requires-Dist: gdal<4,>=3.13.3; extra == 'gdal'
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# dpolcat
|
|
25
|
+
|
|
26
|
+
*dpolcat* is a tool for the semantic categorization of dual-polarimetric synthetic aperture radar (SAR) imagery. (Specifically, Sentinel-1 with VV and VH polarizations at present.)
|
|
27
|
+
|
|
28
|
+
It is inspired by Dr. Andrea Baraldi's [Satellite Image Automatic Mapper™](http://siam.andreabaraldi.com/) (SIAM™) concept of *spectral categories* for optical data, the [Sen2Cube](https://www.sen2cube.at/) project, and Luuk van Der Meer's [Semantique](https://github.com/zgis/semantique).
|
|
29
|
+
|
|
30
|
+
Status: This is a research project with occasional updates.
|
|
31
|
+
|
|
32
|
+
## Overview
|
|
33
|
+
|
|
34
|
+
Different objects on the earth's surface scatter the microwave signals that radar satellites emit and receive, in different ways.
|
|
35
|
+
|
|
36
|
+

|
|
37
|
+
|
|
38
|
+
We bin the backscatter received in the same- (VV) and cross-polarized (VH) signals into distinct *polarimetric categories* that give a hint at what's on the ground.
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
TODO: 👆 update this (new categories).
|
|
43
|
+
|
|
44
|
+
We can then make inferences based on how these categories are distributed in an area, or change over time.
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+

|
|
48
|
+
|
|
49
|
+
TODO: 👆 update this (colour scheme, result output).
|
|
50
|
+
|
|
51
|
+
[*]: [Copernicus EMSR517](https://emergency.copernicus.eu/mapping/ems-product-component/EMSR517_AOI06_DEL_MONIT01_r1_RTP03/1)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
This is implemented in a Python library that performs scaling, speckle-filtering, and then categorization, using a decision-tree algorithm.
|
|
55
|
+
|
|
56
|
+
.
|
|
57
|
+
|
|
58
|
+
TODO: 👆 scaler should be linear to dB; add speckle filter.
|
|
59
|
+
|
|
60
|
+
It uses the [Numba](https://numba.pydata.org/) just-in-time compiler to increase performance, and [Dask](https://www.dask.org) for parallelization.
|
|
61
|
+
|
|
62
|
+
### Colour palette
|
|
63
|
+
|
|
64
|
+
A note on the colour palette. The choices **loosely** follow these rules:
|
|
65
|
+
- Natural phenomena map to pale colours.
|
|
66
|
+
- Man-made phenonema map to bright colours.
|
|
67
|
+
|
|
68
|
+
Sometimes the colours resemble certain land cover types. This may not be coincidence. BUT, there's no one-to-one mapping of land cover type to category/colour.
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
### Category numbering
|
|
72
|
+
|
|
73
|
+
The numbering of the categories, i.e., their indices, has a property with an interesting side effect: they loosely increase in order of magnitude of the VV+VH vector they represent. This means modelling and visualization can sometimes use the category index alone as an input. E.g.,
|
|
74
|
+
|
|
75
|
+
TODO: B/W & viridis map of Graz + caption.
|
|
76
|
+
|
|
77
|
+
## Environment
|
|
78
|
+
|
|
79
|
+
Launch marimo notebooks via either:
|
|
80
|
+
- Pixi - run `pixi run marimo edit` (a browser opens automatically).
|
|
81
|
+
- Docker - use locally with, e.g., `docker compose up` and browse to localhost:8080. ⚠️ This opens a notebook server without authentication, make sure to be behind a properly configured firewall.
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
## Contents
|
|
85
|
+
|
|
86
|
+
'🟢' indicates the file can be run as a marimo notebook.
|
|
87
|
+
|
|
88
|
+
#### 📄 `dpolcat.py`
|
|
89
|
+
|
|
90
|
+
The *dpolcat* module, containing the main algorithms/functions for performing polarimetric categorization.
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
#### 📄🟢 `dpolcat.processor.py`
|
|
94
|
+
|
|
95
|
+
A parameterized notebook containing a batch processing workflow.
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
#### 📄 `dpolcat_modelling.py`
|
|
99
|
+
|
|
100
|
+
Helpers for creating semantic models with categories.
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
#### 📄🟢 `dpolcat.utils.py`
|
|
104
|
+
|
|
105
|
+
Contains reusable utility/helper functions etc. for working with *dpolcat* in various settings.
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
#### 📄🟢 `demo_01_intro.py`
|
|
109
|
+
|
|
110
|
+
An introduction to *dpolcat*.
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
#### 📄🟢 `demo_02_flood.py`
|
|
114
|
+
|
|
115
|
+
Demonstration of a simple flood modelling and mapping workflow using *dpolcat*.
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
#### 📄🟢 `demo_03_batch_processing.py`
|
|
119
|
+
|
|
120
|
+
A batch processing/datacube-writing demonstration.
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
#### 📄🟢 `demo_04_urban.py`
|
|
124
|
+
|
|
125
|
+
Demonstration of some simple urban change modelling and mapping.
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
#### 📄🟢 `gis/inverness_*.py`
|
|
129
|
+
|
|
130
|
+
Exploratory analysis of SAR signals and dpolcat processing in a study area of Inverness, Scotland.
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
#### 📄 `pc_rio_driver.py`
|
|
134
|
+
|
|
135
|
+
A utility class for [odc-stac](https://odc-stac.readthedocs.io/en/latest/intro.html) which takes care of Planetary Computer asset signing.
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
#### 📄 `fast_frost.py`
|
|
139
|
+
|
|
140
|
+
A numba-based implementation of a Frost speckle filter.
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
#### 📁 `gis/duisburg`
|
|
144
|
+
|
|
145
|
+
Supplementary folder for the demo notebook's flood mapping example, containing a QGIS project and associated data for accuracy assessment.
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
#### 📁 `gis/graz`
|
|
149
|
+
|
|
150
|
+
Supplementary folder inc. QGIS project for the Graz-related demos.
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
#### 📁 `archive`
|
|
154
|
+
|
|
155
|
+
Some things based on earlier versions, which may be useful for additional context.
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
#### 📄 `media/EARSeL2024-Slides.pdf`
|
|
164
|
+
|
|
165
|
+
Slides presented at the 43rd EARSeL Symposium in Manchester, UK, 17-June-2024. (Based on an older version.)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
#### 📄 `media/poster.pdf`
|
|
169
|
+
|
|
170
|
+
A simple poster about the project presented internally (based on an even older version).
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
## Credits
|
|
174
|
+
|
|
175
|
+
Imagery: Contains modified Copernicus Sentinel data, processed by ESA and others.
|
|
176
|
+
|
|
177
|
+
Flood reference: [Copernicus Emergency Mapping EMSR517](https://emergency.copernicus.eu/mapping/ems-product-component/EMSR517_AOI06_DEL_MONIT01_r1_RTP03/1)
|
|
178
|
+
|
|
179
|
+
Graz aerial imagery: TODO: add source.
|
|
180
|
+
|
|
181
|
+
## Authors
|
|
182
|
+
|
|
183
|
+
Created by Luke McQuade at [Z_GIS](https://www.plus.ac.at/geoinformatik/?lang=en), originally as part of the the Applied Geoinformatics MSc programme.
|
|
184
|
+
|
|
185
|
+
## Acknowledgements
|
|
186
|
+
|
|
187
|
+
We would like to greatly thank the members of the [EO Analytics Group](https://www.plus.ac.at/geoinformatik/research/research-areas/eo-analytics/?lang=en) and Dr. Zhara Dabiri of the [Risk, Hazard and Climate Lab](https://www.plus.ac.at/geoinformatik/research/research-areas/risk-hazard-climate/?lang=en) for their ideas and support, and Assoc. Prof. Hermann Klug and Dr. Bernhard Zagel for their instruction and continuous feedback.
|
|
188
|
+
|
|
189
|
+
Further developments have been part of my masters thesis, supervised by Prof. Dr. Dirk Tiede, Dr. Martin Sudmanns, and Dr. Zhara Dabiri.
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
## References
|
|
193
|
+
|
|
194
|
+
Augustin, H., Sudmanns, M., Tiede, D., Lang, S., & Baraldi, A. (2019). Semantic Earth observation data cubes. Data, 4(3), 102.
|
|
195
|
+
DOI: 10.3390/data4030102
|
|
196
|
+
|
|
197
|
+
Baraldi, A., Humber, M.L., Tiede, D., Lang, S. (2018). GEO-CEOS stage 4 validation of the Satellite Image Automatic Mapper lightweight computer program for ESA Earth observation level 2 product generation – Part 2: Validation. Cogent Geosci. 4, 1–52.
|
|
198
|
+
DOI: 10.1080/23312041.2018.1467254
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
Meyer, F. (2019), Ch. 2, The SAR Handbook: Comprehensive Methodologies for Forest Monitoring and Biomass Estimation.
|
|
202
|
+
DOI: 10.25966/nr2c-s697
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
dpolcat/__init__.py,sha256=3JcOfymPgsGgWUAvwMAroqk1JdKGC99cnE77qqYQCfQ,92
|
|
2
|
+
dpolcat/core.py,sha256=NzNtjK28KYITnIyFEFRpt37lRrkfsYD1ZDFlLtt6Gm4,4059
|
|
3
|
+
dpolcat/fast_frost.py,sha256=tIzWu7w-Fv95DV-nFQU8oQoorepk44HKNyYTyte3bVw,6441
|
|
4
|
+
dpolcat/modelling.py,sha256=A-yhtbhIL8sRx-nBRvbTU6xW52J8FCmhTEUMIlz29GM,3853
|
|
5
|
+
dpolcat/pc_rio_driver.py,sha256=o8XdBI-q4HU8OX6fFpe90B45NgH8rKhlgkKmyA_a-EM,1021
|
|
6
|
+
dpolcat/processor.py,sha256=RIQ-beRJQcfdZZJcbgPTRjLYuVY-m4zWpJL9GxV3Kww,22119
|
|
7
|
+
dpolcat/utils.py,sha256=U6JwCAjHqtZUI4Qw9ijFBlwMxyceRXBwcAK2XMroDMQ,11244
|
|
8
|
+
dpolcat-0.2.0.dev1.dist-info/METADATA,sha256=6yu5q1fnND-1ZdUjC2yLgi-5Lr4AejZjqeNvUK6xiwE,7239
|
|
9
|
+
dpolcat-0.2.0.dev1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
10
|
+
dpolcat-0.2.0.dev1.dist-info/licenses/LICENSE,sha256=fkPhb5zI5mg5fHiiRNNb-HVxPHsFF0tsk9a_eFtMm2o,11541
|
|
11
|
+
dpolcat-0.2.0.dev1.dist-info/RECORD,,
|