s1-denoise 0.0.0.dev0__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.
- s1_denoise/__init__.py +18 -0
- s1_denoise/__main__.py +36 -0
- s1_denoise/auxiliary_files.py +481 -0
- s1_denoise/baq_decoder.py +203 -0
- s1_denoise/cli.py +118 -0
- s1_denoise/error_calculation.py +262 -0
- s1_denoise/jo_parser.py +295 -0
- s1_denoise/l0_noise_power_calculation.py +399 -0
- s1_denoise/logger.py +81 -0
- s1_denoise/noise_norm_factor.py +291 -0
- s1_denoise/noise_profiles.py +292 -0
- s1_denoise/output.py +551 -0
- s1_denoise/s1safe.py +1415 -0
- s1_denoise/thermal_noise.py +753 -0
- s1_denoise/utils.py +282 -0
- s1_denoise/xml_reader.py +571 -0
- s1_denoise-0.0.0.dev0.dist-info/METADATA +147 -0
- s1_denoise-0.0.0.dev0.dist-info/RECORD +21 -0
- s1_denoise-0.0.0.dev0.dist-info/WHEEL +4 -0
- s1_denoise-0.0.0.dev0.dist-info/entry_points.txt +2 -0
- s1_denoise-0.0.0.dev0.dist-info/licenses/LICENSE +21 -0
s1_denoise/output.py
ADDED
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Module for formatting and writing denoised noise profile vectors to XML output files.
|
|
6
|
+
|
|
7
|
+
Provides data classes for SLC and GRD noise range vectors, handles subswath
|
|
8
|
+
concatenation and azimuth-time interpolation for GRD products, and serialises
|
|
9
|
+
the results to noise XML files following the Sentinel-1 annotation format.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from lxml import etree
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import re
|
|
15
|
+
import numpy as np
|
|
16
|
+
import copy
|
|
17
|
+
from scipy.interpolate import interp1d
|
|
18
|
+
|
|
19
|
+
from perseo_core.timing import PreciseDateTime
|
|
20
|
+
|
|
21
|
+
from s1_denoise.utils import iso_to_datetime, get_range_boundaries, ensure_list
|
|
22
|
+
from s1_denoise.logger import denoise_logger
|
|
23
|
+
|
|
24
|
+
def get_lines_and_pixels(geolocation_info):
|
|
25
|
+
"""
|
|
26
|
+
Return the lines of the geolocation grid and the output pixel axis.
|
|
27
|
+
|
|
28
|
+
Parameters
|
|
29
|
+
----------
|
|
30
|
+
geolocation_info : dict
|
|
31
|
+
`geolocationGrid` section of the product annotation.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
unique_lines : list of int
|
|
36
|
+
Sorted unique line numbers of the geolocation grid points.
|
|
37
|
+
pixels : numpy.ndarray
|
|
38
|
+
Output pixels, every 40 samples from 0, with the last pixel of the grid appended.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
grid_points = geolocation_info['geolocationGridPointList']['geolocationGridPoint']
|
|
42
|
+
|
|
43
|
+
unique_lines = sorted({
|
|
44
|
+
int(d['line'])
|
|
45
|
+
for d in grid_points
|
|
46
|
+
if 'line' in d and str(d['line']).isdigit()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
max_pixel = max({
|
|
50
|
+
int(d['pixel'])
|
|
51
|
+
for d in grid_points
|
|
52
|
+
if 'pixel' in d and str(d['pixel']).isdigit()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
pixels = np.arange(0, max_pixel, 40)
|
|
56
|
+
if pixels[-1] != max_pixel:
|
|
57
|
+
pixels = np.append(pixels, max_pixel)
|
|
58
|
+
|
|
59
|
+
return unique_lines, pixels
|
|
60
|
+
|
|
61
|
+
class NoiseRangeVector_SLC:
|
|
62
|
+
"""
|
|
63
|
+
Noise range vector of an SLC burst, in the format of the noise annotation.
|
|
64
|
+
|
|
65
|
+
The simulated noise is divided by the sine of the incidence angle, scaled by the squared digital number
|
|
66
|
+
value and interpolated on the output pixels.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
azimuth_start_time : str
|
|
71
|
+
Azimuth start time of the burst, in ISO format.
|
|
72
|
+
line : int
|
|
73
|
+
Line number of the vector.
|
|
74
|
+
pixels : numpy.ndarray
|
|
75
|
+
Output pixel indices.
|
|
76
|
+
noise_values : numpy.ndarray
|
|
77
|
+
Simulated noise for each range sample (linear power).
|
|
78
|
+
incidence_angles : numpy.ndarray
|
|
79
|
+
Sine of the incidence angle for each range sample.
|
|
80
|
+
lut_value : float
|
|
81
|
+
Digital number calibration value from AUX_PP1.
|
|
82
|
+
|
|
83
|
+
Attributes
|
|
84
|
+
----------
|
|
85
|
+
azimuthTime : str
|
|
86
|
+
Azimuth start time of the burst.
|
|
87
|
+
line : int
|
|
88
|
+
Line number of the vector.
|
|
89
|
+
pixels : numpy.ndarray
|
|
90
|
+
Output pixel indices.
|
|
91
|
+
pixel_count : int
|
|
92
|
+
Number of output pixels.
|
|
93
|
+
noise_values : numpy.ndarray
|
|
94
|
+
Output noise values on the output pixels.
|
|
95
|
+
noiseRangeLut_count : int
|
|
96
|
+
Number of output noise values.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(self, azimuth_start_time, line, pixels, noise_values, incidence_angles, lut_value):
|
|
100
|
+
|
|
101
|
+
self.azimuthTime = azimuth_start_time
|
|
102
|
+
self.line = line
|
|
103
|
+
self.pixels = pixels
|
|
104
|
+
self.pixel_count = len(self.pixels)
|
|
105
|
+
|
|
106
|
+
noise = noise_values / incidence_angles
|
|
107
|
+
|
|
108
|
+
noise *= lut_value ** 2
|
|
109
|
+
noise = np.interp(self.pixels, np.arange(len(noise)), noise)
|
|
110
|
+
|
|
111
|
+
self.noise_values = noise
|
|
112
|
+
self.noiseRangeLut_count = len(self.noise_values)
|
|
113
|
+
|
|
114
|
+
class NoiseRangeVector_GRD:
|
|
115
|
+
"""
|
|
116
|
+
Merged cross-subswath noise range vector of a GRD azimuth line, in the format of the noise annotation.
|
|
117
|
+
|
|
118
|
+
The simulated noise is divided by the sine of the incidence angle, scaled by the squared digital number
|
|
119
|
+
value and interpolated from the sample pixel coordinates to the output pixels.
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
start_az_time : str
|
|
124
|
+
Azimuth time of the vector, in ISO format.
|
|
125
|
+
noise_values : array_like
|
|
126
|
+
Simulated noise for each sample (linear power), concatenated over the subswaths.
|
|
127
|
+
incidence_angles : array_like
|
|
128
|
+
Sine of the incidence angle for each sample.
|
|
129
|
+
line_no : int
|
|
130
|
+
Line number of the vector.
|
|
131
|
+
pixels_array : list of str
|
|
132
|
+
Output pixel indices.
|
|
133
|
+
sample_pixel_coords : array_like
|
|
134
|
+
Pixel coordinate of each noise sample.
|
|
135
|
+
lut_value : float
|
|
136
|
+
Digital number calibration value from AUX_PP1.
|
|
137
|
+
|
|
138
|
+
Attributes
|
|
139
|
+
----------
|
|
140
|
+
azimuthTime : str
|
|
141
|
+
Azimuth time of the vector.
|
|
142
|
+
line : int
|
|
143
|
+
Line number of the vector.
|
|
144
|
+
pixels : list of str
|
|
145
|
+
Output pixel indices.
|
|
146
|
+
pixel_count : int
|
|
147
|
+
Number of output pixels.
|
|
148
|
+
noise_values : numpy.ndarray
|
|
149
|
+
Output noise values on the output pixels.
|
|
150
|
+
noiseRangeLut_count : int
|
|
151
|
+
Number of output noise values.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
def __init__(self, start_az_time, noise_values, incidence_angles, line_no, pixels_array, sample_pixel_coords, lut_value):
|
|
155
|
+
|
|
156
|
+
self.azimuthTime = start_az_time
|
|
157
|
+
self.line = line_no
|
|
158
|
+
self.pixels = pixels_array
|
|
159
|
+
self.pixel_count = len(self.pixels)
|
|
160
|
+
|
|
161
|
+
noise = np.array(noise_values) / np.array(incidence_angles)
|
|
162
|
+
noise *= lut_value ** 2
|
|
163
|
+
noise = np.interp(np.array(self.pixels, dtype=float), sample_pixel_coords, noise)
|
|
164
|
+
|
|
165
|
+
self.noise_values = noise
|
|
166
|
+
self.noiseRangeLut_count = len(self.noise_values)
|
|
167
|
+
|
|
168
|
+
class OutputNoiseVectors_SLC:
|
|
169
|
+
"""
|
|
170
|
+
Output noise range vectors of an SLC channel (polarization and subswath).
|
|
171
|
+
|
|
172
|
+
Parameters
|
|
173
|
+
----------
|
|
174
|
+
adsHeader : dict
|
|
175
|
+
`adsHeader` section of the product annotation.
|
|
176
|
+
noiseRangeVectors : dict
|
|
177
|
+
Simulated noise per burst, mapping ``(burst_index, start_azimuth_time)`` to
|
|
178
|
+
``(noise, geolocation_grid, sin_incidence, dn_value)``, where `noise` is the slant range simulated noise,
|
|
179
|
+
`geolocation_grid` the `geolocationGrid` annotation section, `sin_incidence` the sine of the incidence angles
|
|
180
|
+
and `dn_value` the digital number calibration value.
|
|
181
|
+
|
|
182
|
+
Attributes
|
|
183
|
+
----------
|
|
184
|
+
ads_header : dict
|
|
185
|
+
`adsHeader` section of the product annotation.
|
|
186
|
+
noise_range_vectors : list of NoiseRangeVector_SLC
|
|
187
|
+
One noise range vector per burst, at the geolocation grid line of the burst.
|
|
188
|
+
vector_count : int
|
|
189
|
+
Number of noise range vectors.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
def __init__(self, adsHeader: dict, noiseRangeVectors):
|
|
193
|
+
|
|
194
|
+
self.ads_header = adsHeader
|
|
195
|
+
self.noise_range_vectors = [] # list
|
|
196
|
+
|
|
197
|
+
geo_grid_lines = None
|
|
198
|
+
pixels = None
|
|
199
|
+
|
|
200
|
+
for burst_values, noise_values in noiseRangeVectors.items():
|
|
201
|
+
rel_burst = burst_values[0]
|
|
202
|
+
start_az_time = burst_values[1]
|
|
203
|
+
sim_noise = noise_values[0]
|
|
204
|
+
geo_grid_info = noise_values[1]
|
|
205
|
+
incidence_angles = noise_values[2]
|
|
206
|
+
dn_value = noise_values[3]
|
|
207
|
+
|
|
208
|
+
# the geolocation grid is the same for all the bursts of the swath
|
|
209
|
+
if geo_grid_lines is None:
|
|
210
|
+
geo_grid_lines, pixels = get_lines_and_pixels(geo_grid_info)
|
|
211
|
+
|
|
212
|
+
noise_range_vec = NoiseRangeVector_SLC(start_az_time,
|
|
213
|
+
geo_grid_lines[rel_burst],
|
|
214
|
+
pixels,
|
|
215
|
+
sim_noise,
|
|
216
|
+
incidence_angles,
|
|
217
|
+
dn_value)
|
|
218
|
+
|
|
219
|
+
self.noise_range_vectors.append(noise_range_vec)
|
|
220
|
+
|
|
221
|
+
self.vector_count = len(noiseRangeVectors)
|
|
222
|
+
|
|
223
|
+
class OutputNoiseVectors_GRD:
|
|
224
|
+
"""
|
|
225
|
+
Output cross-subswath noise range vectors of a GRD channel (polarization).
|
|
226
|
+
|
|
227
|
+
The noise vectors of each subswath are extended in azimuth to the full scene extent (see `extend_noise_grd`),
|
|
228
|
+
then, for each azimuth time step, concatenated along range according to the swath merging boundaries,
|
|
229
|
+
removing the samples of the previous subswath that fall in the overlap region.
|
|
230
|
+
|
|
231
|
+
Parameters
|
|
232
|
+
----------
|
|
233
|
+
adsHeader : dict
|
|
234
|
+
`adsHeader` section of the product annotation.
|
|
235
|
+
swath_merges : list of dict
|
|
236
|
+
`swathMerge` entries of the product annotation.
|
|
237
|
+
noiseRangeVectors : dict
|
|
238
|
+
Simulated noise per block, mapping ``(block_index, subswath)`` to
|
|
239
|
+
``(azimuth_time, noise, sin_incidence)``, with the noise in ground range.
|
|
240
|
+
dn_value : float
|
|
241
|
+
Digital number calibration value from AUX_PP1.
|
|
242
|
+
tot_vec_out : int
|
|
243
|
+
Number of annotated antenna pattern records. Not used.
|
|
244
|
+
|
|
245
|
+
Attributes
|
|
246
|
+
----------
|
|
247
|
+
ads_header : dict
|
|
248
|
+
`adsHeader` section of the product annotation.
|
|
249
|
+
noise_range_vectors : list of NoiseRangeVector_GRD
|
|
250
|
+
One merged noise range vector per azimuth time step.
|
|
251
|
+
vector_count : int
|
|
252
|
+
Number of noise range vectors.
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
def __init__(self, adsHeader: dict, swath_merges, noiseRangeVectors, dn_value, tot_vec_out):
|
|
256
|
+
|
|
257
|
+
self.ads_header = adsHeader
|
|
258
|
+
self.noise_range_vectors = []
|
|
259
|
+
|
|
260
|
+
# Collect unique subswath identifiers preserving their order
|
|
261
|
+
subswaths = list(dict.fromkeys(key[1] for key in noiseRangeVectors))
|
|
262
|
+
|
|
263
|
+
# Determine the total number of azimuth lines across all swath boundaries
|
|
264
|
+
max_lines = max(
|
|
265
|
+
int(d['lastAzimuthLine'])
|
|
266
|
+
for p in swath_merges
|
|
267
|
+
for d in ensure_list(p['swathBoundsList']['swathBounds'])
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Extend each subswath's noise vectors to cover the full azimuth extent
|
|
271
|
+
# Burst vectors are re-indexed sequentially and extrapolated to fill any gaps beyond the first/last burst up to the scene boundaries
|
|
272
|
+
subswath_extended = {}
|
|
273
|
+
for subswath in subswaths:
|
|
274
|
+
burst_indices = sorted(k[0] for k in noiseRangeVectors if k[1] == subswath)
|
|
275
|
+
sw_vectors = {
|
|
276
|
+
j: noiseRangeVectors[(burst_no, subswath)]
|
|
277
|
+
for j, burst_no in enumerate(burst_indices)
|
|
278
|
+
}
|
|
279
|
+
subswath_extended[subswath] = extend_noise_grd(
|
|
280
|
+
sw_vectors, self.ads_header['startTime'], self.ads_header['stopTime'], max_lines
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Step 2: for each azimuth time step, concatenate subswaths in range.
|
|
284
|
+
# All subswaths share the same number of time steps after the extension above.
|
|
285
|
+
tot_vec = len(next(iter(subswath_extended.values())))
|
|
286
|
+
|
|
287
|
+
for i in range(tot_vec):
|
|
288
|
+
# Azimuth time and line number are taken from the first subswath (same for all)
|
|
289
|
+
az_time = subswath_extended[subswaths[0]][i][0]
|
|
290
|
+
line_no = subswath_extended[subswaths[0]][i][3]
|
|
291
|
+
|
|
292
|
+
noise = []
|
|
293
|
+
incidence_angles = []
|
|
294
|
+
sample_pixel_coords = []
|
|
295
|
+
pixels = []
|
|
296
|
+
last_prec = None
|
|
297
|
+
first_prec = None
|
|
298
|
+
|
|
299
|
+
for subswath in subswaths:
|
|
300
|
+
noise_sw = subswath_extended[subswath][i][1]
|
|
301
|
+
angles_sw = subswath_extended[subswath][i][2]
|
|
302
|
+
|
|
303
|
+
# Retrieve the pixel range covered by this subswath at the current azimuth time
|
|
304
|
+
first, last = get_range_boundaries(swath_merges=swath_merges, subswath=subswath, az_time=az_time)
|
|
305
|
+
|
|
306
|
+
# Remove samples from the previous subswath that fall in the overlap region
|
|
307
|
+
if last_prec is not None:
|
|
308
|
+
overlap_pixels = max(0, last_prec - first + 1)
|
|
309
|
+
if overlap_pixels > 0:
|
|
310
|
+
prev_pixel_width = last_prec - first_prec + 1
|
|
311
|
+
overlap_samples = round(overlap_pixels * len(noise) / prev_pixel_width)
|
|
312
|
+
noise = noise[:-overlap_samples]
|
|
313
|
+
incidence_angles = incidence_angles[:-overlap_samples]
|
|
314
|
+
sample_pixel_coords = sample_pixel_coords[:-overlap_samples]
|
|
315
|
+
|
|
316
|
+
# Append this subswath's samples, mapping them uniformly across its pixel range
|
|
317
|
+
pixels.extend([first, last])
|
|
318
|
+
n_samples = len(noise_sw)
|
|
319
|
+
sample_pixel_coords.extend(np.linspace(first, last, n_samples).tolist())
|
|
320
|
+
noise.extend(list(noise_sw))
|
|
321
|
+
incidence_angles.extend(list(angles_sw))
|
|
322
|
+
|
|
323
|
+
first_prec = first
|
|
324
|
+
last_prec = last
|
|
325
|
+
|
|
326
|
+
# Build the merged noise range vector for this azimuth line and store it
|
|
327
|
+
pixel_line = calculate_pixel_line(pixels)
|
|
328
|
+
noise_range_vec = NoiseRangeVector_GRD(az_time, noise, incidence_angles, line_no, pixel_line, sample_pixel_coords, dn_value)
|
|
329
|
+
self.noise_range_vectors.append(noise_range_vec)
|
|
330
|
+
|
|
331
|
+
self.vector_count = len(self.noise_range_vectors)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def calculate_pixel_line(pixel_bound_list):
|
|
335
|
+
"""
|
|
336
|
+
Build the output pixel axis of a merged GRD noise range vector.
|
|
337
|
+
|
|
338
|
+
Within each subswath, pixels are taken every 40 samples, always including the last pixel of the subswath; the
|
|
339
|
+
next subswath starts after the last pixel of the previous one.
|
|
340
|
+
|
|
341
|
+
Parameters
|
|
342
|
+
----------
|
|
343
|
+
pixel_bound_list : list of int
|
|
344
|
+
First and last pixel of each subswath, as ``[first_1, last_1, first_2, last_2, ...]``. Only the last pixels
|
|
345
|
+
are used.
|
|
346
|
+
|
|
347
|
+
Returns
|
|
348
|
+
-------
|
|
349
|
+
list of str
|
|
350
|
+
Output pixel indices.
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
pixels = []
|
|
354
|
+
start = 0
|
|
355
|
+
for bound in pixel_bound_list[1::2]:
|
|
356
|
+
pixels.extend(np.arange(start, bound, 40))
|
|
357
|
+
if bound not in pixels:
|
|
358
|
+
pixels.append(bound)
|
|
359
|
+
start = bound + 1
|
|
360
|
+
|
|
361
|
+
pixel_string = [str(int(x)) for x in pixels]
|
|
362
|
+
|
|
363
|
+
return pixel_string
|
|
364
|
+
|
|
365
|
+
def extend_noise_grd(input_vectors, start_time, stop_time, max_lines):
|
|
366
|
+
"""
|
|
367
|
+
Extend the noise vectors of a GRD subswath to the full azimuth extent of the scene.
|
|
368
|
+
|
|
369
|
+
The vectors are first resampled to a common length (the longest one), then linearly interpolated in azimuth on
|
|
370
|
+
a 1 s grid from `start_time` to `stop_time` (included). Values outside the input time span are held constant
|
|
371
|
+
at the first/last vector.
|
|
372
|
+
|
|
373
|
+
Parameters
|
|
374
|
+
----------
|
|
375
|
+
input_vectors : dict
|
|
376
|
+
Noise vectors of the subswath, mapping consecutive indices from 0 to
|
|
377
|
+
``(azimuth_time, noise, sin_incidence)``, with the azimuth time in ISO format.
|
|
378
|
+
start_time : str
|
|
379
|
+
Scene start time, in ISO format.
|
|
380
|
+
stop_time : str
|
|
381
|
+
Scene stop time, in ISO format.
|
|
382
|
+
max_lines : int
|
|
383
|
+
Total number of azimuth lines of the scene, used to compute the line number of each output vector.
|
|
384
|
+
|
|
385
|
+
Returns
|
|
386
|
+
-------
|
|
387
|
+
dict
|
|
388
|
+
Extended noise vectors, mapping the output index to ``(azimuth_time, noise, sin_incidence, line)``, with
|
|
389
|
+
the azimuth time as ISO string and `noise`, `sin_incidence` as numpy.ndarray.
|
|
390
|
+
"""
|
|
391
|
+
|
|
392
|
+
output_vectors = dict()
|
|
393
|
+
|
|
394
|
+
# create new azimuth time axis
|
|
395
|
+
az_times_old = [iso_to_datetime(v[0]).sec85 for v in input_vectors.values()]
|
|
396
|
+
start_sec85 = iso_to_datetime(start_time).sec85
|
|
397
|
+
stop_sec85 = iso_to_datetime(stop_time).sec85
|
|
398
|
+
az_times_new = np.arange(start_sec85, stop_sec85, 1.0)
|
|
399
|
+
|
|
400
|
+
# add the stop time as last element, unless it is already there (compare in seconds, with tolerance)
|
|
401
|
+
if az_times_new.size == 0 or not np.isclose(az_times_new[-1], stop_sec85, rtol=0.0, atol=1e-6):
|
|
402
|
+
az_times_new = np.append(az_times_new, stop_sec85)
|
|
403
|
+
|
|
404
|
+
tot_vec_out = len(az_times_new)
|
|
405
|
+
|
|
406
|
+
target_len = max(len(input_vectors[j][1]) for j in range(len(input_vectors)))
|
|
407
|
+
|
|
408
|
+
# resample noise and incidence angles of each vector to the same length,
|
|
409
|
+
# without modifying the input vectors
|
|
410
|
+
x_new = np.linspace(0, 1, target_len)
|
|
411
|
+
resampled_vectors = []
|
|
412
|
+
for j in range(len(input_vectors)):
|
|
413
|
+
vec = list(input_vectors[j]) # make a mutable copy
|
|
414
|
+
for idx in (1, 2): # noise, incidence_angles
|
|
415
|
+
y = np.array(vec[idx])
|
|
416
|
+
x = np.linspace(0, 1, len(y))
|
|
417
|
+
vec[idx] = np.interp(x_new, x, y).tolist()
|
|
418
|
+
resampled_vectors.append(vec)
|
|
419
|
+
|
|
420
|
+
noise_old = np.stack([v[1] for v in resampled_vectors])
|
|
421
|
+
angles_old = np.stack([v[2] for v in resampled_vectors])
|
|
422
|
+
|
|
423
|
+
interp_noise = interp1d(
|
|
424
|
+
az_times_old,
|
|
425
|
+
noise_old,
|
|
426
|
+
axis=0,
|
|
427
|
+
bounds_error=False,
|
|
428
|
+
fill_value=(noise_old[0], noise_old[-1])
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
interp_angles = interp1d(
|
|
432
|
+
az_times_old,
|
|
433
|
+
angles_old,
|
|
434
|
+
axis=0,
|
|
435
|
+
bounds_error=False,
|
|
436
|
+
fill_value=(angles_old[0], angles_old[-1])
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
noise_new = interp_noise(az_times_new)
|
|
440
|
+
incidence_angles_new = interp_angles(az_times_new)
|
|
441
|
+
|
|
442
|
+
delta_line = (az_times_new[-1] - az_times_new[0])/max_lines
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
for i in range(tot_vec_out):
|
|
446
|
+
|
|
447
|
+
line_number = int(np.round((az_times_new[i]-az_times_new[0])/delta_line))
|
|
448
|
+
|
|
449
|
+
output_vectors.update({
|
|
450
|
+
i: (PreciseDateTime.from_sec85(az_times_new[i]).isoformat(timespec="microseconds").rstrip("Z"), noise_new[i, :], incidence_angles_new[i, :], line_number)
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
return output_vectors
|
|
454
|
+
|
|
455
|
+
def build_filename(header):
|
|
456
|
+
"""
|
|
457
|
+
Build the output noise file name from the product header.
|
|
458
|
+
|
|
459
|
+
The name follows the Sentinel-1 annotation naming convention, with the ``denoise`` prefix:
|
|
460
|
+
``denoise-<mission>-<swath>-<type>-<pol>-<start>-<stop>-<orbit>-<datatake>-<image>.xml``.
|
|
461
|
+
|
|
462
|
+
Parameters
|
|
463
|
+
----------
|
|
464
|
+
header : dict
|
|
465
|
+
`adsHeader` section of the product annotation.
|
|
466
|
+
|
|
467
|
+
Returns
|
|
468
|
+
-------
|
|
469
|
+
str
|
|
470
|
+
Output file name.
|
|
471
|
+
"""
|
|
472
|
+
|
|
473
|
+
def clean_time(s):
|
|
474
|
+
"""Remove the fractional seconds and all ``.``, ``-`` and ``:`` characters from a timestamp."""
|
|
475
|
+
return re.sub(r"[.\-:]", "", s[:-7])
|
|
476
|
+
|
|
477
|
+
filename = (
|
|
478
|
+
f"denoise"
|
|
479
|
+
f"-{header['missionId']}-{header['swath']}-{header['productType']}-{header['polarisation']}-"
|
|
480
|
+
f"{clean_time(header['startTime'])}-{clean_time(header['stopTime'])}-" # remove all ":", "." and "-" from timestamps
|
|
481
|
+
f"{header['absoluteOrbitNumber'].zfill(6)}-" # add 0 padding
|
|
482
|
+
f"{int(header['missionDataTakeId']):06x}-" # convert mission datatake ID to hexadecimal
|
|
483
|
+
f"{header['imageNumber']}.xml"
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
return filename
|
|
487
|
+
|
|
488
|
+
def write_to_xml(noise, output_path: Path, safe_path):
|
|
489
|
+
"""
|
|
490
|
+
Write the output noise vectors to an XML noise file.
|
|
491
|
+
|
|
492
|
+
The file is written in a subfolder of `output_path` named after the input product, with the `adsHeader` and the
|
|
493
|
+
`noiseRangeVectorList`. The `noiseAzimuthVectorList` is copied unchanged from the nominal noise file of the
|
|
494
|
+
input product, if present.
|
|
495
|
+
|
|
496
|
+
Parameters
|
|
497
|
+
----------
|
|
498
|
+
noise : OutputNoiseVectors_SLC or OutputNoiseVectors_GRD
|
|
499
|
+
Output noise vectors of one channel.
|
|
500
|
+
output_path : pathlib.Path
|
|
501
|
+
Output directory.
|
|
502
|
+
safe_path : pathlib.Path
|
|
503
|
+
Path to the input SAFE product.
|
|
504
|
+
|
|
505
|
+
Returns
|
|
506
|
+
-------
|
|
507
|
+
pathlib.Path
|
|
508
|
+
Path of the written XML file.
|
|
509
|
+
"""
|
|
510
|
+
|
|
511
|
+
root = etree.Element("noise")
|
|
512
|
+
|
|
513
|
+
# adsHeader
|
|
514
|
+
header = etree.SubElement(root, "adsHeader")
|
|
515
|
+
for key, value in noise.ads_header.items():
|
|
516
|
+
child = etree.SubElement(header, key)
|
|
517
|
+
child.text = str(value)
|
|
518
|
+
|
|
519
|
+
# noiseRangeVectorList
|
|
520
|
+
vector_list = etree.SubElement(root, "noiseRangeVectorList", {"count": str(noise.vector_count)})
|
|
521
|
+
for v in noise.noise_range_vectors:
|
|
522
|
+
vec_elem = etree.SubElement(vector_list, "noiseRangeVector")
|
|
523
|
+
|
|
524
|
+
etree.SubElement(vec_elem, "azimuthTime").text = str(v.azimuthTime)
|
|
525
|
+
etree.SubElement(vec_elem, "line").text = str(v.line)
|
|
526
|
+
|
|
527
|
+
etree.SubElement(vec_elem, "pixel", {"count": str(v.pixel_count)}).text = ' '.join(map(str, v.pixels))
|
|
528
|
+
etree.SubElement(vec_elem, "noiseRangeLut", {"count": str(v.noiseRangeLut_count)}).text = " ".join(f"{x:.6e}" for x in v.noise_values)
|
|
529
|
+
|
|
530
|
+
xml_tree = etree.ElementTree(root)
|
|
531
|
+
|
|
532
|
+
output_path = output_path / Path(safe_path.stem)
|
|
533
|
+
output_path.mkdir(parents=True, exist_ok=True) # check output folder exists
|
|
534
|
+
output_filename = build_filename(noise.ads_header)
|
|
535
|
+
output_file_path = Path(str(output_path.resolve() / output_filename.lower()))
|
|
536
|
+
|
|
537
|
+
# noiseAzimuthVectorList
|
|
538
|
+
nominal_noise_file = safe_path / "annotation/calibration" / (output_file_path.stem[2:] + ".xml")
|
|
539
|
+
if nominal_noise_file.exists():
|
|
540
|
+
nominal_noise_tree = etree.parse(nominal_noise_file)
|
|
541
|
+
nominal_noise_root = nominal_noise_tree.getroot()
|
|
542
|
+
noise_az_list = nominal_noise_root.find("noiseAzimuthVectorList")
|
|
543
|
+
|
|
544
|
+
if noise_az_list is not None:
|
|
545
|
+
root.append(copy.deepcopy(noise_az_list))
|
|
546
|
+
|
|
547
|
+
etree.indent(root, space=" ")
|
|
548
|
+
xml_tree.write(output_file_path, pretty_print=True, encoding="utf-8", xml_declaration=True)
|
|
549
|
+
denoise_logger.info(f"Written output to {output_file_path.resolve()}")
|
|
550
|
+
|
|
551
|
+
return output_file_path
|