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/__init__.py
ADDED
dpolcat/core.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dpolcat/core - polarimetric categorization of dual-pol. synthetic aperture radar (SAR) data
|
|
3
|
+
|
|
4
|
+
Author: Luke McQuade
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from numba import njit
|
|
8
|
+
import numpy as np
|
|
9
|
+
import xarray
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def linear_to_db(x):
|
|
13
|
+
"""Convert linear backscatter to power ratio (decibels)."""
|
|
14
|
+
return 10 * np.log10(x)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# This used to do a different transform, keep this as a passthrough until the rest of the code is updated.
|
|
18
|
+
def scale_nice(x):
|
|
19
|
+
return linear_to_db(x)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def xr_scale_nice(x):
|
|
23
|
+
"""XArray, Dask-oriented, version of scaling function."""
|
|
24
|
+
return xarray.apply_ufunc(scale_nice, x, dask="parallelized", output_dtypes=[float])
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Remember to keep this updated.
|
|
28
|
+
NUM_CATEGORIES = 20
|
|
29
|
+
"""Total number of categories."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@njit
|
|
33
|
+
def categorize(vv_scaled, vh_scaled):
|
|
34
|
+
"""
|
|
35
|
+
Our dual-polarimetric categorization decision tree algorithm.
|
|
36
|
+
Inputs should be in numpy or xarray-compatible form, scaled using our custom scaling function.
|
|
37
|
+
"""
|
|
38
|
+
vv = vv_scaled
|
|
39
|
+
vh = vh_scaled
|
|
40
|
+
|
|
41
|
+
i = 0
|
|
42
|
+
if vv is None or vh is None:
|
|
43
|
+
# NoData/invalid
|
|
44
|
+
i = 0
|
|
45
|
+
# Low volume scatter, ie., predominantly surface scatterers
|
|
46
|
+
elif -np.inf <= vh < -20.0:
|
|
47
|
+
if -np.inf <= vv < -18.0:
|
|
48
|
+
i = 1
|
|
49
|
+
elif -18.0 <= vv < -16.0:
|
|
50
|
+
i = 2
|
|
51
|
+
elif -16.0 <= vv < -14.0:
|
|
52
|
+
i = 3
|
|
53
|
+
elif -14.0 <= vv < -6.0:
|
|
54
|
+
i = 4
|
|
55
|
+
elif -6.0 <= vv < 2.0:
|
|
56
|
+
i = 5
|
|
57
|
+
elif vv >= 2.0:
|
|
58
|
+
# Higher values, likely to be double-bounce (buildings) or terrain effects
|
|
59
|
+
i = 13 # [merged A]
|
|
60
|
+
elif vv < -18.0:
|
|
61
|
+
if vh < -14.0:
|
|
62
|
+
# Low specular with some cross-pol, e.g., moist soil
|
|
63
|
+
i = 6
|
|
64
|
+
elif vh >= -14.0:
|
|
65
|
+
# Predominantly polarizing surfaces (unrealistic)
|
|
66
|
+
i = 19 # [merged C]
|
|
67
|
+
# Combinations of scatter, e.g, natural volume scatterers
|
|
68
|
+
elif -18.0 <= vv < -14.0 and -22.0 <= vh < -14.0:
|
|
69
|
+
i = 7
|
|
70
|
+
elif -14.0 <= vv < -2.0 and -14.0 <= vh < -10.0:
|
|
71
|
+
i = 12
|
|
72
|
+
|
|
73
|
+
# Further mid-range categories
|
|
74
|
+
elif -18.0 <= vv < -14.0 and -14.0 <= vh < -10.0:
|
|
75
|
+
i = 19 # [merged C]
|
|
76
|
+
elif -18.0 <= vv < -6.0 and -10.0 <= vh < 2.0:
|
|
77
|
+
# Maybe double-bounce
|
|
78
|
+
i = 14
|
|
79
|
+
elif -14.0 <= vv < -6.0 and -22.0 <= vh < -17.0:
|
|
80
|
+
i = 8
|
|
81
|
+
elif -14.0 <= vv < -6.0 and -17.0 <= vh < -15.6:
|
|
82
|
+
i = 9
|
|
83
|
+
elif -14.0 <= vv < -6.0 and -15.6 <= vh < -14.0:
|
|
84
|
+
i = 10
|
|
85
|
+
elif -6.0 <= vv < 2.0 and -22.0 <= vh < -14.0:
|
|
86
|
+
i = 11
|
|
87
|
+
elif -2.0 <= vv < 2.0 and -14.0 <= vh < -10.0:
|
|
88
|
+
i = 15 # [merged B] #
|
|
89
|
+
|
|
90
|
+
# Higher values, likely to be double-bounce (buildings) or terrain effects.
|
|
91
|
+
elif -6.0 <= vv < 2.0 and -10.0 <= vh < 2.0:
|
|
92
|
+
i = 15 # [merged B]
|
|
93
|
+
elif vv >= 2.0 and -22.0 <= vh < -10.0:
|
|
94
|
+
i = 13 # [merged A]
|
|
95
|
+
elif vv >= 2.0 and -10.0 <= vh < 2.0:
|
|
96
|
+
i = 16
|
|
97
|
+
elif vh >= 2.0 and -22.0 < vv < 2.0:
|
|
98
|
+
i = 17
|
|
99
|
+
elif vv >= 2.0 and vh >= 2.0:
|
|
100
|
+
i = 18
|
|
101
|
+
|
|
102
|
+
return i
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
categorize_np = np.vectorize(categorize, otypes=[np.uint8])
|
|
106
|
+
"""Numpy vectorized version of the categorizer."""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def xr_categorize(vv_scaled, vh_scaled):
|
|
110
|
+
"""XArray, Dask-oriented, version of the categorizer."""
|
|
111
|
+
return xarray.apply_ufunc(
|
|
112
|
+
categorize_np,
|
|
113
|
+
vv_scaled,
|
|
114
|
+
vh_scaled,
|
|
115
|
+
dask="parallelized",
|
|
116
|
+
output_dtypes=[np.uint8],
|
|
117
|
+
).rename("dpolcat")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
color_list = np.array(
|
|
121
|
+
[
|
|
122
|
+
[0, 0, 0], # 0
|
|
123
|
+
[129, 244, 255], # 1
|
|
124
|
+
[199, 180, 215], # 2
|
|
125
|
+
[166, 121, 215], # 3
|
|
126
|
+
[255, 200, 128], # 4
|
|
127
|
+
[203, 73, 114], # 5
|
|
128
|
+
[222, 123, 214], # 6
|
|
129
|
+
[251, 210, 212], # 7
|
|
130
|
+
[231, 251, 187], # 8
|
|
131
|
+
[200, 187, 66], # 9
|
|
132
|
+
[157, 225, 90], # 10
|
|
133
|
+
[114, 73, 114], # 11
|
|
134
|
+
[105, 69, 42], # 12
|
|
135
|
+
[200, 10, 30], # 13
|
|
136
|
+
[255, 255, 0], # 14
|
|
137
|
+
[255, 204, 0], # 15
|
|
138
|
+
[158, 34, 108], # 16
|
|
139
|
+
[217, 0, 255], # 17
|
|
140
|
+
[252, 74, 0], # 18
|
|
141
|
+
[70, 70, 70], # 19
|
|
142
|
+
]
|
|
143
|
+
)
|
|
144
|
+
"""Suggested category colours, of the form [R,G,B (0 to 255)]"""
|
dpolcat/fast_frost.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fast Frost speckle filters using Numba.
|
|
3
|
+
|
|
4
|
+
frost_filter_fast does minimal numerical checking. It will break (silently) if there are *any* NaNs in the input.
|
|
5
|
+
|
|
6
|
+
frost_filter_nanny is NaN-aware, but this comes with a performance penalty.
|
|
7
|
+
|
|
8
|
+
Based on findpeaks.filters [0] / PyRadar [1], and the original paper [2].
|
|
9
|
+
|
|
10
|
+
Mostly generated with ChatGPT o3 (17-Jun-2025).
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
[0]: https://github.com/erdogant/findpeaks/blob/master/findpeaks/filters/frost.py
|
|
14
|
+
|
|
15
|
+
[1]: https://github.com/PyRadar/pyradar
|
|
16
|
+
|
|
17
|
+
[2]: Frost, Victor S., Josephine Abbott Stiles, K. S. Shanmugan, and Julian C. Holtzman. 'A Model for Radar Images and Its Application to Adaptive Digital Filtering of Multiplicative Noise'. IEEE Transactions on Pattern Analysis and Machine Intelligence PAMI-4, no. 2 (March 1982): 15766. https://doi.org/10.1109/TPAMI.1982.4767223.
|
|
18
|
+
|
|
19
|
+
Licence: LGPL-3.0.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
from scipy.ndimage import uniform_filter
|
|
25
|
+
from numba import njit, prange
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@njit(parallel=True, fastmath=True)
|
|
29
|
+
def _frost_core(img, factor_A, r):
|
|
30
|
+
"""Inner loop: compute weighted average for every pixel."""
|
|
31
|
+
n_rows, n_cols = img.shape
|
|
32
|
+
out = np.empty_like(img)
|
|
33
|
+
|
|
34
|
+
for i in prange(n_rows): # parallel loop
|
|
35
|
+
for j in range(n_cols):
|
|
36
|
+
# window bounds (clamped to image)
|
|
37
|
+
x0 = max(i - r, 0); x1 = min(i + r + 1, n_rows)
|
|
38
|
+
y0 = max(j - r, 0); y1 = min(j + r + 1, n_cols)
|
|
39
|
+
|
|
40
|
+
centre = img[i, j]
|
|
41
|
+
A = factor_A[i, j]
|
|
42
|
+
|
|
43
|
+
w_sum = 0.0
|
|
44
|
+
v_sum = 0.0
|
|
45
|
+
for x in range(x0, x1):
|
|
46
|
+
for y in range(y0, y1):
|
|
47
|
+
diff = abs(img[x, y] - centre)
|
|
48
|
+
w = np.exp(-A * diff)
|
|
49
|
+
w_sum += w
|
|
50
|
+
v_sum += w * img[x, y]
|
|
51
|
+
|
|
52
|
+
out[i, j] = v_sum / w_sum if w_sum > 0.0 else 0.0
|
|
53
|
+
return out
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def frost_filter_fast(img, *, damping_factor: float = 2.0,
|
|
57
|
+
win_size: int = 3, dtype=np.float32):
|
|
58
|
+
"""
|
|
59
|
+
Frost speckle filter accelerated with Numba.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
img : 2-D ndarray
|
|
64
|
+
damping_factor : float, default 2.0
|
|
65
|
+
win_size : odd int ≥ 3, default 3
|
|
66
|
+
dtype : numpy dtype, default float32
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
ndarray (same shape as *img*)
|
|
71
|
+
"""
|
|
72
|
+
if img.ndim != 2:
|
|
73
|
+
raise ValueError("Input must be a 2-D array.")
|
|
74
|
+
if win_size < 3 or win_size % 2 == 0:
|
|
75
|
+
raise ValueError("win_size must be odd and ≥ 3.")
|
|
76
|
+
|
|
77
|
+
img = np.asarray(img, dtype=dtype, order='C') # contiguous for Numba
|
|
78
|
+
r = win_size // 2
|
|
79
|
+
|
|
80
|
+
# Local mean and variance via fast box filter
|
|
81
|
+
mean = uniform_filter(img, size=win_size, mode='reflect')
|
|
82
|
+
mean_sq = uniform_filter(img * img, size=win_size, mode='reflect')
|
|
83
|
+
var = np.clip(mean_sq - mean * mean, 0, None)
|
|
84
|
+
std = np.sqrt(var)
|
|
85
|
+
|
|
86
|
+
# Frost parameters
|
|
87
|
+
sigma0 = std / np.maximum(mean, 1e-12) / np.maximum(mean, 1e-12)
|
|
88
|
+
factor_A = damping_factor * sigma0
|
|
89
|
+
|
|
90
|
+
return _frost_core(img, factor_A, r).astype(img.dtype, copy=False)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@njit(parallel=True, fastmath=True)
|
|
94
|
+
def _frost_core_nanny(img, factor_A, win_size):
|
|
95
|
+
"""Numba kernel: weight computation + pixel update (ignores NaNs)."""
|
|
96
|
+
n_rows, n_cols = img.shape
|
|
97
|
+
r = win_size // 2
|
|
98
|
+
out = np.empty_like(img)
|
|
99
|
+
|
|
100
|
+
for i in prange(n_rows):
|
|
101
|
+
for j in range(n_cols):
|
|
102
|
+
centre = img[i, j]
|
|
103
|
+
if np.isnan(centre): # centre missing → output NaN
|
|
104
|
+
out[i, j] = np.nan
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
x0 = max(i - r, 0)
|
|
108
|
+
x1 = min(i + r + 1, n_rows)
|
|
109
|
+
y0 = max(j - r, 0)
|
|
110
|
+
y1 = min(j + r + 1, n_cols)
|
|
111
|
+
|
|
112
|
+
A = factor_A[i, j]
|
|
113
|
+
w_sum = 0.0
|
|
114
|
+
v_sum = 0.0
|
|
115
|
+
|
|
116
|
+
for x in range(x0, x1):
|
|
117
|
+
for y in range(y0, y1):
|
|
118
|
+
pix = img[x, y]
|
|
119
|
+
if np.isnan(pix): # skip NaNs in the window
|
|
120
|
+
continue
|
|
121
|
+
diff = abs(pix - centre)
|
|
122
|
+
w = np.exp(-A * diff)
|
|
123
|
+
w_sum += w
|
|
124
|
+
v_sum += w * pix
|
|
125
|
+
|
|
126
|
+
out[i, j] = v_sum / w_sum if w_sum > 0 else np.nan
|
|
127
|
+
return out
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def frost_filter_nanny(img, *, damping_factor: float = 2.0, win_size: int = 3,
|
|
131
|
+
dtype=np.float32):
|
|
132
|
+
"""
|
|
133
|
+
Frost speckle filter accelerated with Numba (NaN-aware).
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
img : 2-D ndarray
|
|
138
|
+
SAR intensity image (NaNs allowed).
|
|
139
|
+
damping_factor : float, default 2.0
|
|
140
|
+
Frost damping constant.
|
|
141
|
+
win_size : int, default 3 (must be odd ≥ 3)
|
|
142
|
+
Square window size.
|
|
143
|
+
dtype : numpy dtype, default np.float32
|
|
144
|
+
Internal working precision.
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
filtered : ndarray
|
|
149
|
+
Frost-filtered image (same shape as *img*).
|
|
150
|
+
"""
|
|
151
|
+
if img.ndim != 2:
|
|
152
|
+
raise ValueError("Input must be a 2-D array.")
|
|
153
|
+
if win_size < 3 or win_size % 2 == 0:
|
|
154
|
+
raise ValueError("win_size must be odd and ≥ 3.")
|
|
155
|
+
|
|
156
|
+
img = np.asarray(img, dtype=dtype, order='C') # C-contiguous
|
|
157
|
+
|
|
158
|
+
# ---- local mean & variance, ignoring NaNs ---- #
|
|
159
|
+
nan_mask = np.isfinite(img).astype(dtype) # 1 where valid, 0 where NaN
|
|
160
|
+
img_filled = np.where(nan_mask, img, 0)
|
|
161
|
+
|
|
162
|
+
# uniform_filter returns an *average* → multiply by win_size**2 to get sums
|
|
163
|
+
size = win_size
|
|
164
|
+
win_area = float(size * size)
|
|
165
|
+
|
|
166
|
+
sum_valid = uniform_filter(img_filled, size=size, mode='reflect') * win_area
|
|
167
|
+
sum_sq_valid = uniform_filter(img_filled * img_filled, size=size, mode='reflect') * win_area
|
|
168
|
+
count_valid = uniform_filter(nan_mask, size=size, mode='reflect') * win_area
|
|
169
|
+
|
|
170
|
+
# avoid division by zero where window has no valid pixels
|
|
171
|
+
mean = np.where(count_valid > 0, sum_valid / count_valid, np.nan)
|
|
172
|
+
var = np.where(count_valid > 0, sum_sq_valid / count_valid - mean * mean, np.nan)
|
|
173
|
+
std = np.sqrt(np.maximum(var, 0))
|
|
174
|
+
|
|
175
|
+
cv = std / mean # coefficient of variation
|
|
176
|
+
sigma_zero = cv / mean # std / mean²
|
|
177
|
+
factor_A = damping_factor * sigma_zero
|
|
178
|
+
factor_A = np.where(np.isfinite(factor_A), factor_A, 0)
|
|
179
|
+
|
|
180
|
+
# ---- JIT kernel ---- #
|
|
181
|
+
filtered = _frost_core_nanny(img, factor_A.astype(dtype), win_size)
|
|
182
|
+
|
|
183
|
+
return filtered
|
|
184
|
+
|
dpolcat/modelling.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dpolcat/modelling - helper classes for semantic modelling
|
|
3
|
+
|
|
4
|
+
Author: Luke McQuade
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from enum import IntEnum
|
|
8
|
+
|
|
9
|
+
import dpolcat
|
|
10
|
+
|
|
11
|
+
# Semantic API
|
|
12
|
+
# ------------
|
|
13
|
+
# Here we aim to present the polarimetric categories in a way
|
|
14
|
+
# that they can be used to create understandable semantic models.
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Surface(IntEnum):
|
|
18
|
+
"""
|
|
19
|
+
Predominantly surface-scattering mechanics.
|
|
20
|
+
- `Smooth` (1): Very smooth surface scattering.
|
|
21
|
+
- `SlightlyRough` (2): Slightly rough surface scattering.
|
|
22
|
+
- `MediumRough` (3): Medium-rough surface scattering.
|
|
23
|
+
- `Rough` (4): Rough surface scattering.
|
|
24
|
+
- `VeryRough` (5): Very rough surface scattering.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
Smooth = 1
|
|
28
|
+
"""Very smooth surface scattering."""
|
|
29
|
+
|
|
30
|
+
SlightlyRough = 2
|
|
31
|
+
"""Slightly rough surface scattering."""
|
|
32
|
+
|
|
33
|
+
MediumRough = 3
|
|
34
|
+
"""Medium-rough surface scattering."""
|
|
35
|
+
|
|
36
|
+
Rough = 4
|
|
37
|
+
"""Rough surface scattering."""
|
|
38
|
+
|
|
39
|
+
VeryRough = 5
|
|
40
|
+
"""Very rough surface scattering."""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Volume(IntEnum):
|
|
44
|
+
"""
|
|
45
|
+
Volume-scattering mechanics.
|
|
46
|
+
- `LowCo` (6): Volume scatterer with low co-polar response.
|
|
47
|
+
- `MediumCo` (7): Volume scatterer with medium co-polar response.
|
|
48
|
+
- `Low` (8): Weakly volume-scattering object.
|
|
49
|
+
- `MediumLow` (9): Medium-weakly volume-scattering object.
|
|
50
|
+
- `Medium` (10): Medium-strength volume-scattering object.
|
|
51
|
+
- `HighCo` (11): Volume scatterer with high co-polar response.
|
|
52
|
+
- `High` (12): Highly volume-scattering object.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
LowCo = 6
|
|
56
|
+
"""Volume scatterer with low co-polar response."""
|
|
57
|
+
|
|
58
|
+
MediumCo = 7
|
|
59
|
+
"""Volume scatterer with medium co-polar response."""
|
|
60
|
+
|
|
61
|
+
Low = 8
|
|
62
|
+
"""Weakly volume-scattering object."""
|
|
63
|
+
|
|
64
|
+
MediumLow = 9
|
|
65
|
+
"""Medium-weakly volume scattering object."""
|
|
66
|
+
|
|
67
|
+
Medium = 10
|
|
68
|
+
"""Medium-strength volume scattering object."""
|
|
69
|
+
|
|
70
|
+
HighCo = 11
|
|
71
|
+
"""Volume scatterer with high co-polar response."""
|
|
72
|
+
|
|
73
|
+
High = 12
|
|
74
|
+
"""Highly volume-scattering object."""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class MultiBounce(IntEnum):
|
|
78
|
+
"""
|
|
79
|
+
Double/triple-bounce mechanics.
|
|
80
|
+
- `HighCo` (13): High-strength co-polar double/triple-bounce, with low cross-polar response.
|
|
81
|
+
- `MedHighCross` (14): Medium-strength cross-polar double/triple-bounce, with low co-polar response.
|
|
82
|
+
- `MediumHigh` (15): Medium-high co- and cross-polar double/triple-bounce response.
|
|
83
|
+
- `HighMixedCo` (16): High-strength double/triple-bounce with a mostly co-polar response.
|
|
84
|
+
- `HighMixedCross` (17): High-strength double/triple-bounce with a mostly cross-polar response.
|
|
85
|
+
- `VeryHigh` (18): Very strong co- and cross-polar double/triple-bounce response.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
HighCo = 13
|
|
89
|
+
"""High strength co-polar double/triple-bounce, with low cross-polar, response."""
|
|
90
|
+
|
|
91
|
+
MedHighCross = 14
|
|
92
|
+
"""Medium strength cross-polar double/triple-bounce, with low co-polar, response."""
|
|
93
|
+
|
|
94
|
+
MediumHigh = 15
|
|
95
|
+
"""Medium-high co- and cross-polar double/triple-bounce response."""
|
|
96
|
+
|
|
97
|
+
HighMixedCo = 16
|
|
98
|
+
"""High-strength double/triple-bounce with a mostly co-polar response."""
|
|
99
|
+
|
|
100
|
+
HighMixedCross = 17
|
|
101
|
+
"""High-strength double/triple-bounce with a mostly cross-polar response."""
|
|
102
|
+
|
|
103
|
+
VeryHigh = 18
|
|
104
|
+
"""Very strong co- and cross-polar double/triple-bounce response."""
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class Other(IntEnum):
|
|
108
|
+
"""
|
|
109
|
+
Other categories.
|
|
110
|
+
- `NoData` (0): No data.
|
|
111
|
+
- `Unknown` (19): Physically unrealistic/unknown mechanism.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
NoData = 0
|
|
115
|
+
"""No data."""
|
|
116
|
+
|
|
117
|
+
Unknown = 19
|
|
118
|
+
"""Physically unrealistic/unknown mechanism."""
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# A quick sanity check - do the definitions above line up with our expected number of categories?
|
|
122
|
+
assert set(range(dpolcat.NUM_CATEGORIES)) == set(Surface) | set(Volume) | set(
|
|
123
|
+
MultiBounce
|
|
124
|
+
) | set(Other), "Category definitions do not match the expected number."
|
dpolcat/pc_rio_driver.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Custom PlanetaryComputer RioDriver for use in odc.stac.load,
|
|
2
|
+
# which re-signs assets just before use (rather than up front).
|
|
3
|
+
# planetary_computer.sign has a token cache, so it's not overly expensive.
|
|
4
|
+
# Code via Claude Opus 5.5.
|
|
5
|
+
|
|
6
|
+
import dataclasses
|
|
7
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
8
|
+
|
|
9
|
+
import planetary_computer
|
|
10
|
+
from odc.loader._rio import RioDriver, RioReader
|
|
11
|
+
from odc.stac._mdtools import StacAuxReader
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def resign(url: str) -> str:
|
|
15
|
+
return planetary_computer.sign(urlunsplit(urlsplit(url)._replace(query="")))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PCRioReader(RioReader):
|
|
19
|
+
def read(self, cfg, dst_geobox, *, dst=None, selection=None):
|
|
20
|
+
src = dataclasses.replace(self._src, uri=resign(self._src.uri))
|
|
21
|
+
return RioReader(src, self._ctx).read(
|
|
22
|
+
cfg, dst_geobox, dst=dst, selection=selection
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PCRioDriver(RioDriver):
|
|
27
|
+
def __init__(self):
|
|
28
|
+
super().__init__(aux_reader=StacAuxReader())
|
|
29
|
+
|
|
30
|
+
def open(self, src, ctx):
|
|
31
|
+
return PCRioReader(src, ctx)
|