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/logger.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Logging configuration module for the denoising processor.
|
|
6
|
+
|
|
7
|
+
Sets up a named logger with colorized console output and plain-text file output, using a structured log format
|
|
8
|
+
that includes timestamp, hostname, processor name, version, and process ID. Exposes a module-level
|
|
9
|
+
``denoise_logger`` instance for use across the package.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
import colorlog
|
|
14
|
+
import socket
|
|
15
|
+
|
|
16
|
+
from s1_denoise import __version__, PROCESSOR_NAME
|
|
17
|
+
|
|
18
|
+
def setup_logger():
|
|
19
|
+
"""
|
|
20
|
+
Set up the ``denoise`` logger with colorized console output and file output.
|
|
21
|
+
|
|
22
|
+
Handlers are attached only if the logger has none yet, so repeated calls do not duplicate output. Log levels
|
|
23
|
+
are renamed to short tags (``[D]``, ``[I]``, ``[W]``, ``[E]``, ``[P]``), records are written both to the console
|
|
24
|
+
(colorized) and to ``denoise.log`` in the current working directory, and the level is set to ``INFO``.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
logging.Logger
|
|
29
|
+
The configured ``denoise`` logger.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
# Check if the logger has already handlers attached
|
|
33
|
+
logger = logging.getLogger("denoise")
|
|
34
|
+
|
|
35
|
+
# Only set up logging if no handlers are already attached
|
|
36
|
+
if not logger.hasHandlers():
|
|
37
|
+
|
|
38
|
+
logging.addLevelName(logging.NOTSET, "")
|
|
39
|
+
logging.addLevelName(logging.DEBUG, "[D]")
|
|
40
|
+
logging.addLevelName(logging.INFO, "[I]")
|
|
41
|
+
logging.addLevelName(logging.WARNING, "[W]")
|
|
42
|
+
logging.addLevelName(logging.ERROR, "[E]")
|
|
43
|
+
logging.addLevelName(logging.CRITICAL, "[P]")
|
|
44
|
+
|
|
45
|
+
log_colors = {
|
|
46
|
+
'': 'green',
|
|
47
|
+
'[D]': 'cyan',
|
|
48
|
+
# '[I]': 'white',
|
|
49
|
+
'[W]': 'yellow',
|
|
50
|
+
'[E]': 'red',
|
|
51
|
+
'[P]': 'bold_red',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
HOSTNAME=socket.gethostname()
|
|
55
|
+
|
|
56
|
+
LOG_STRING = f'%(asctime)s.%(msecs)03d000 {HOSTNAME} {PROCESSOR_NAME} {__version__} [%(process)d] %(levelname)s %(message)s'
|
|
57
|
+
COLORED_LOG_STRING = "%(log_color)s" + LOG_STRING
|
|
58
|
+
DATE_FMT = "%Y-%m-%dT%H:%M:%S"
|
|
59
|
+
|
|
60
|
+
# Create a colored formatter
|
|
61
|
+
formatter = colorlog.ColoredFormatter(COLORED_LOG_STRING, log_colors=log_colors, datefmt=DATE_FMT)
|
|
62
|
+
|
|
63
|
+
# Create console handler for colored logs
|
|
64
|
+
console_handler = logging.StreamHandler()
|
|
65
|
+
console_handler.setFormatter(formatter)
|
|
66
|
+
|
|
67
|
+
# Create file handler to log to "denoise.log"
|
|
68
|
+
file_handler = logging.FileHandler("denoise.log")
|
|
69
|
+
file_handler.setFormatter(logging.Formatter(LOG_STRING, datefmt=DATE_FMT))
|
|
70
|
+
|
|
71
|
+
# Create logger and set level
|
|
72
|
+
logger = logging.getLogger("denoise")
|
|
73
|
+
|
|
74
|
+
logger.addHandler(console_handler)
|
|
75
|
+
logger.addHandler(file_handler)
|
|
76
|
+
|
|
77
|
+
logger.setLevel(logging.INFO)
|
|
78
|
+
|
|
79
|
+
return logger
|
|
80
|
+
|
|
81
|
+
denoise_logger = setup_logger()
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Utility module providing signal processing helpers for noise profile generation.
|
|
6
|
+
|
|
7
|
+
Includes functions for FFT size selection, Hamming window computation, look
|
|
8
|
+
start index and overlap calculation, and window normalisation factor estimation.
|
|
9
|
+
|
|
10
|
+
IPF noise normalization factor
|
|
11
|
+
------------------------------
|
|
12
|
+
During L1 post-processing (LBP), each look is extracted with a Hamming window
|
|
13
|
+
and, in azimuth, possibly corrected for the azimuth antenna pattern (AAP). The
|
|
14
|
+
signal is then scaled by the window normalization factor so that its power is
|
|
15
|
+
preserved:
|
|
16
|
+
|
|
17
|
+
windowNorm = sum(w^2) / sum(AAP^2 over the processing bandwidth)
|
|
18
|
+
|
|
19
|
+
Thermal noise has a flat spectrum, so this normalization changes its power
|
|
20
|
+
differently. Since the AAP correction (division by AAP) also boosts the noise
|
|
21
|
+
at the spectrum edges, its processing gain is:
|
|
22
|
+
|
|
23
|
+
noiseProcGain = sum(w^2 / AAP^2) / bwInSamples
|
|
24
|
+
|
|
25
|
+
The noise normalization factor is the ratio between the two, averaged over the
|
|
26
|
+
looks:
|
|
27
|
+
|
|
28
|
+
noiseNormFactor = mean_looks(noiseProcGain / windowNorm)
|
|
29
|
+
|
|
30
|
+
It is computed in azimuth and in range (flat spectrum, so the range factor is
|
|
31
|
+
~1) and the final value is their product. The IPF multiplies the noise vectors
|
|
32
|
+
by this factor when noiseNormalizationFlag is set, and annotates its average
|
|
33
|
+
over the blocks of each swath in noiseNormalizationFactorList (1.0 if the flag
|
|
34
|
+
is not set). If the AAP correction is not applied, AAP is not divided out:
|
|
35
|
+
sum(w^2) is used in noiseProcGain and sum((AAP * w)^2) per look in windowNorm.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
import numpy as np
|
|
39
|
+
import math
|
|
40
|
+
|
|
41
|
+
def find_fft_size(desired_fft_size):
|
|
42
|
+
"""
|
|
43
|
+
Return the smallest valid IPF FFT length not lower than the desired size.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
desired_fft_size : int
|
|
48
|
+
Minimum required FFT length [samples].
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
int
|
|
53
|
+
Smallest FFT length in the list of lengths supported by the IPF that is >= `desired_fft_size`.
|
|
54
|
+
|
|
55
|
+
Raises
|
|
56
|
+
------
|
|
57
|
+
ValueError
|
|
58
|
+
If `desired_fft_size` exceeds the largest supported FFT length.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
FFT_VALID_LENGTHS = [ 240, 256, 288, 320, 384, 480, 512, 576, 640, 720, 768, 960, 1024, 1152, 1300, 1400, 1584, 1800, 1950, 2080, 2464, 2560, 2880, 3072, 3564, 3750, 3822, 4096, 4400, 5000, 5760, 6144, 7840, 8100, 8400, 8640, 8736, 8820, 9072, 9216, 11520, 12288, 15360, 15552, 16000, 16384, 17280, 18000, 18480, 19008, 19440, 19500, 20000, 20580, 22400, 23760, 24576, 25920, 27648, 28800, 30720, 31104, 32000, 32768, 34560, 36000, 36864, 38400, 40000, 40960, 41472, 43200, 46080, 48000, 49152, 51200, 51840, 54000, 55296, 57600, 60000, 61440, 62208, 64000, 64800, 65536, 69120, 72000, 73728, 76800, 77760, 80000, 81920, 82944, 86400, 92160, 93312, 96000, 98304 ]
|
|
62
|
+
|
|
63
|
+
for size in FFT_VALID_LENGTHS:
|
|
64
|
+
|
|
65
|
+
if desired_fft_size <= size:
|
|
66
|
+
return size
|
|
67
|
+
|
|
68
|
+
raise ValueError(f"No valid FFT size found for the desired FFT size {desired_fft_size}.")
|
|
69
|
+
|
|
70
|
+
def update_window_noise_norm(data_spectra, window, look_start_indices, in_line_len, correct_spectra=True):
|
|
71
|
+
"""
|
|
72
|
+
Compute the per-look sum of the squared window divided by the squared spectrum (noise processing gain numerator).
|
|
73
|
+
|
|
74
|
+
For each look, the window is aligned to the FFT grid at the look start index (wrapping around the grid) and
|
|
75
|
+
``sum((window / data_spectra)**2)`` is computed. If the spectra are not corrected (or not given), the sum of the
|
|
76
|
+
squared window is used for each look, as in the IPF.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
data_spectra : numpy.ndarray or None
|
|
81
|
+
Azimuth antenna pattern on the FFT grid, in linear amplitude, with at least `in_line_len` samples.
|
|
82
|
+
window : numpy.ndarray
|
|
83
|
+
Look window (Hamming) of length not greater than `in_line_len`.
|
|
84
|
+
look_start_indices : array_like of int
|
|
85
|
+
Start index of each look on the FFT grid, lower than `in_line_len`.
|
|
86
|
+
in_line_len : int
|
|
87
|
+
FFT length [samples].
|
|
88
|
+
correct_spectra : bool, optional
|
|
89
|
+
Whether the azimuth antenna pattern correction is applied (division by the AAP). Default is True.
|
|
90
|
+
|
|
91
|
+
Returns
|
|
92
|
+
-------
|
|
93
|
+
numpy.ndarray
|
|
94
|
+
Sum of squared (window / AAP) for each look, or of squared window if the correction is not applied.
|
|
95
|
+
|
|
96
|
+
Raises
|
|
97
|
+
------
|
|
98
|
+
ValueError
|
|
99
|
+
If a look start index is not lower than `in_line_len`, if the window is longer than `in_line_len`, or if
|
|
100
|
+
`data_spectra` is shorter than `in_line_len`.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
window = np.asarray(window, dtype=np.float64)
|
|
104
|
+
look_start_indices = np.asarray(look_start_indices, dtype=np.int64)
|
|
105
|
+
|
|
106
|
+
window_len = window.size
|
|
107
|
+
window_sq = window ** 2
|
|
108
|
+
|
|
109
|
+
if data_spectra is None or not correct_spectra:
|
|
110
|
+
return np.full(look_start_indices.size, np.sum(window_sq))
|
|
111
|
+
|
|
112
|
+
data_spectra = np.asarray(data_spectra, dtype=np.float64)
|
|
113
|
+
|
|
114
|
+
if np.any(look_start_indices >= in_line_len):
|
|
115
|
+
raise ValueError(f"look_start_indices ({look_start_indices}) must be less than in_line_len ({in_line_len})")
|
|
116
|
+
if window_len > in_line_len:
|
|
117
|
+
raise ValueError(f"window length ({window_len}) must be <= in_line_len ({in_line_len})")
|
|
118
|
+
if data_spectra.size < in_line_len:
|
|
119
|
+
raise ValueError(f"data_spectra is shorter ({data_spectra.size}) than in_line_len ({in_line_len})")
|
|
120
|
+
|
|
121
|
+
base_idx = np.arange(window_len, dtype=np.int64)
|
|
122
|
+
|
|
123
|
+
idx = (look_start_indices[:, None] + base_idx) % in_line_len
|
|
124
|
+
segments = data_spectra[idx]
|
|
125
|
+
|
|
126
|
+
sum_window_sq_noise = np.sum(window_sq / (segments**2), axis=1)
|
|
127
|
+
|
|
128
|
+
return sum_window_sq_noise
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def calculate_window_norm_factor(data_spectra, bw_in_samples, in_line_len, window, look_start_indices, correct_spectra=True):
|
|
132
|
+
"""
|
|
133
|
+
Compute the window normalization factor of each look (IPF ``LBP_GenerateLook::calcNormFactor``).
|
|
134
|
+
|
|
135
|
+
The factor is the sum of the squared window divided by the sum of the squared spectrum over the processing
|
|
136
|
+
bandwidth, centered on the zero frequency. If the spectra are not corrected, the spectrum remaining at the end
|
|
137
|
+
of processing is included in the sum of the squared window, separately for each look.
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
data_spectra : numpy.ndarray or None
|
|
142
|
+
Azimuth antenna pattern on the FFT grid, in linear amplitude. If None, a flat unit spectrum is assumed.
|
|
143
|
+
bw_in_samples : int
|
|
144
|
+
Processing bandwidth [samples].
|
|
145
|
+
in_line_len : int
|
|
146
|
+
FFT length [samples].
|
|
147
|
+
window : numpy.ndarray
|
|
148
|
+
Look window (Hamming).
|
|
149
|
+
look_start_indices : array_like of int
|
|
150
|
+
Start index of each look on the FFT grid.
|
|
151
|
+
correct_spectra : bool, optional
|
|
152
|
+
Whether the azimuth antenna pattern correction is applied (division by the AAP). Default is True.
|
|
153
|
+
|
|
154
|
+
Returns
|
|
155
|
+
-------
|
|
156
|
+
numpy.ndarray
|
|
157
|
+
Window normalization factor for each look.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
window = np.asarray(window, dtype=np.float64)
|
|
161
|
+
look_start_indices = np.asarray(look_start_indices, dtype=np.int64)
|
|
162
|
+
|
|
163
|
+
if data_spectra is not None:
|
|
164
|
+
data_spectra = np.array(data_spectra, dtype=np.float32)
|
|
165
|
+
|
|
166
|
+
start_index = math.floor((-1.0 * bw_in_samples + 1) / 2)
|
|
167
|
+
start_index %= in_line_len
|
|
168
|
+
|
|
169
|
+
indices = (start_index + np.arange(bw_in_samples)) % in_line_len
|
|
170
|
+
|
|
171
|
+
selected = np.take(data_spectra, indices, mode="wrap")
|
|
172
|
+
|
|
173
|
+
sum_adjustment_sq = np.sum(selected ** 2)
|
|
174
|
+
|
|
175
|
+
else:
|
|
176
|
+
sum_adjustment_sq = bw_in_samples
|
|
177
|
+
|
|
178
|
+
if data_spectra is not None and not correct_spectra:
|
|
179
|
+
idx = (look_start_indices[:, None] + np.arange(window.size)) % in_line_len
|
|
180
|
+
sum_window_sq = np.sum((data_spectra[idx] * window) ** 2, axis=1)
|
|
181
|
+
else:
|
|
182
|
+
sum_window_sq = np.full(look_start_indices.size, np.sum(window ** 2))
|
|
183
|
+
|
|
184
|
+
window_norm_factor = sum_window_sq / sum_adjustment_sq
|
|
185
|
+
|
|
186
|
+
return window_norm_factor
|
|
187
|
+
|
|
188
|
+
def hamming(nsize, alpha):
|
|
189
|
+
"""
|
|
190
|
+
Compute a generalized Hamming window as in the IPF.
|
|
191
|
+
|
|
192
|
+
The window is ``alpha + (1 - alpha) * cos(2 * pi * (n - centre) / (nsize - 1))`` for ``n = 1, ..., nsize``,
|
|
193
|
+
with ``centre = (nsize + 1) / 2``.
|
|
194
|
+
|
|
195
|
+
Parameters
|
|
196
|
+
----------
|
|
197
|
+
nsize : int
|
|
198
|
+
Window length [samples].
|
|
199
|
+
alpha : float
|
|
200
|
+
Window coefficient. Values >= 1 give a flat (rectangular) window.
|
|
201
|
+
|
|
202
|
+
Returns
|
|
203
|
+
-------
|
|
204
|
+
numpy.ndarray or None
|
|
205
|
+
Window samples, or None if `alpha` is negative.
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
whamm = np.zeros(nsize, dtype=np.float64)
|
|
209
|
+
|
|
210
|
+
# alpha >= 1.0 -> flat window
|
|
211
|
+
if alpha >= 1.0:
|
|
212
|
+
whamm[:] = 1.0
|
|
213
|
+
return whamm
|
|
214
|
+
|
|
215
|
+
if alpha < 0.0:
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
# Constants
|
|
219
|
+
TWO_PI = 2.0 * math.pi
|
|
220
|
+
centre = float(nsize + 1) / 2.0 # central index
|
|
221
|
+
fsize = float(nsize - 1)
|
|
222
|
+
|
|
223
|
+
# Hamming (sample indices from 1 to nsize)
|
|
224
|
+
arg = TWO_PI * (np.arange(1, nsize + 1, dtype=np.float64) - centre) / fsize
|
|
225
|
+
whamm = alpha + (1.0 - alpha) * np.cos(arg)
|
|
226
|
+
|
|
227
|
+
return whamm
|
|
228
|
+
|
|
229
|
+
def get_look_overlap_samples(window_len, bw_in_samples, number_of_looks = 1):
|
|
230
|
+
"""
|
|
231
|
+
Compute the look overlap in frequency samples, as in the IPF (``LBP_GenerateLook::initBlock``).
|
|
232
|
+
|
|
233
|
+
Parameters
|
|
234
|
+
----------
|
|
235
|
+
window_len : int
|
|
236
|
+
Look length [samples].
|
|
237
|
+
bw_in_samples : int
|
|
238
|
+
Processing bandwidth [samples].
|
|
239
|
+
number_of_looks : int, optional
|
|
240
|
+
Number of looks. Default is 1.
|
|
241
|
+
|
|
242
|
+
Returns
|
|
243
|
+
-------
|
|
244
|
+
int
|
|
245
|
+
Overlap between consecutive looks [samples], ``ceil((L * window_len - bw_in_samples) / (L - 1))`` for
|
|
246
|
+
``L > 1`` looks, 0 for single-look processing.
|
|
247
|
+
"""
|
|
248
|
+
|
|
249
|
+
if number_of_looks <= 1:
|
|
250
|
+
return 0
|
|
251
|
+
|
|
252
|
+
extra_samples = window_len * number_of_looks - bw_in_samples
|
|
253
|
+
|
|
254
|
+
return int(math.ceil(extra_samples / (number_of_looks - 1.0)))
|
|
255
|
+
|
|
256
|
+
def get_start_indices(look_overlap, window_len, in_line_len, number_of_looks = 1):
|
|
257
|
+
"""
|
|
258
|
+
Compute the start index of each look on the FFT grid.
|
|
259
|
+
|
|
260
|
+
The looks are centered on the zero frequency and spaced by ``window_len - look_overlap`` samples; indices are
|
|
261
|
+
wrapped modulo `in_line_len`.
|
|
262
|
+
|
|
263
|
+
Parameters
|
|
264
|
+
----------
|
|
265
|
+
look_overlap : int
|
|
266
|
+
Overlap between consecutive looks [samples].
|
|
267
|
+
window_len : int
|
|
268
|
+
Look length [samples].
|
|
269
|
+
in_line_len : int
|
|
270
|
+
FFT length [samples].
|
|
271
|
+
number_of_looks : int, optional
|
|
272
|
+
Number of looks. Default is 1.
|
|
273
|
+
|
|
274
|
+
Returns
|
|
275
|
+
-------
|
|
276
|
+
list of int
|
|
277
|
+
Start index of each look on the FFT grid.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
extracted_freq_samples = window_len + (window_len - look_overlap) * (number_of_looks - 1)
|
|
281
|
+
start_offset = int(np.floor((1.0 - extracted_freq_samples) / 2))
|
|
282
|
+
|
|
283
|
+
look_start_indices = [0] * number_of_looks
|
|
284
|
+
look_end_indices = [0] * number_of_looks
|
|
285
|
+
|
|
286
|
+
for lookIdx in range(number_of_looks):
|
|
287
|
+
look_start_indices[lookIdx] = int((start_offset + (window_len - look_overlap) * lookIdx) % in_line_len)
|
|
288
|
+
|
|
289
|
+
look_end_indices[lookIdx] = int((look_start_indices[lookIdx] + window_len - 1) % in_line_len)
|
|
290
|
+
|
|
291
|
+
return look_start_indices
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Core module orchestrating the generation of simulated thermal noise profiles for
|
|
6
|
+
Sentinel-1 SLC and GRD products.
|
|
7
|
+
|
|
8
|
+
Iterates over all polarisation/subswath channels of a SAFE product, applies
|
|
9
|
+
thermal noise modelling via the ThermalNoise class, and returns structured output
|
|
10
|
+
noise vectors. Optionally incorporates noise power correction derived from the
|
|
11
|
+
associated L0N file.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
from perseo_core.timing import PreciseDateTime
|
|
19
|
+
|
|
20
|
+
# Local imports
|
|
21
|
+
from s1_denoise.logger import denoise_logger
|
|
22
|
+
from s1_denoise.s1safe import S1Safe
|
|
23
|
+
from s1_denoise.xml_reader import create_slant_range_times, read_metadata_from_safe_annotation
|
|
24
|
+
from s1_denoise.thermal_noise import ThermalNoise
|
|
25
|
+
from s1_denoise.auxiliary_files import AuxiliaryFiles
|
|
26
|
+
from s1_denoise.output import OutputNoiseVectors_SLC, OutputNoiseVectors_GRD
|
|
27
|
+
from s1_denoise.l0_noise_power_calculation import get_noise_pwr_from_l0n
|
|
28
|
+
from s1_denoise.utils import ensure_list, slant2ground
|
|
29
|
+
|
|
30
|
+
def print_exec_time(elapsed_seconds: float) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Log the execution time in a human-readable format.
|
|
33
|
+
|
|
34
|
+
The time is split into hours, minutes and seconds and logged at debug level.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
elapsed_seconds : float
|
|
39
|
+
Elapsed time [s].
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
hours = int(elapsed_seconds // 3600)
|
|
43
|
+
remaining = elapsed_seconds % 3600
|
|
44
|
+
minutes = int(remaining // 60)
|
|
45
|
+
seconds = remaining % 60
|
|
46
|
+
|
|
47
|
+
time_parts = []
|
|
48
|
+
if hours > 0:
|
|
49
|
+
time_parts.append(f"{hours} hour{'s' if hours != 1 else ''}")
|
|
50
|
+
if minutes > 0:
|
|
51
|
+
time_parts.append(f"{minutes} minute{'s' if minutes != 1 else ''}")
|
|
52
|
+
if seconds > 0 or not time_parts:
|
|
53
|
+
time_parts.append(f"{seconds:.3f} seconds")
|
|
54
|
+
|
|
55
|
+
denoise_logger.debug("Execution time: %s", ", ".join(time_parts))
|
|
56
|
+
|
|
57
|
+
def slc_processing(safe, aux_files, dn_value, noise_power_correction):
|
|
58
|
+
"""
|
|
59
|
+
Generate the simulated noise range vectors of an SLC product.
|
|
60
|
+
|
|
61
|
+
For each polarization/subswath channel, one noise vector is computed per burst, at the burst center azimuth
|
|
62
|
+
time, on the slant range axis of the swath.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
safe : S1Safe
|
|
67
|
+
Input SLC SAFE product.
|
|
68
|
+
aux_files : AuxiliaryFiles
|
|
69
|
+
Auxiliary files used for processing.
|
|
70
|
+
dn_value : float
|
|
71
|
+
Digital number calibration value from AUX_PP1, used to scale the output noise values.
|
|
72
|
+
noise_power_correction : dict or None
|
|
73
|
+
Noise power extracted from the L0N product, mapping ``(swath, polarization)`` to
|
|
74
|
+
``[azimuth_times, noise_powers]``, or None to use the noise power annotated in the product.
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
list of OutputNoiseVectors_SLC
|
|
79
|
+
Output noise vectors, one element per channel.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
simulated_noises = []
|
|
83
|
+
|
|
84
|
+
# loop on annotation files
|
|
85
|
+
for pol, subswath in safe._channels:
|
|
86
|
+
|
|
87
|
+
channel = safe.channel(pol, subswath)
|
|
88
|
+
denoise_logger.info(f"Processing bursts from polarization {pol}, subswath {subswath}")
|
|
89
|
+
|
|
90
|
+
metadata_dict = read_metadata_from_safe_annotation(channel.annotation)
|
|
91
|
+
ads_header = metadata_dict['product']['adsHeader']
|
|
92
|
+
lines_per_burst = int(metadata_dict['product']['swathTiming']['linesPerBurst'])
|
|
93
|
+
az_dt = float(metadata_dict['product']['imageAnnotation']['imageInformation']['azimuthTimeInterval'])
|
|
94
|
+
|
|
95
|
+
thermal_noise = ThermalNoise(metadata_dict=metadata_dict,
|
|
96
|
+
channel=channel,
|
|
97
|
+
aux_info=aux_files,
|
|
98
|
+
product_id=safe.path.stem[4:13],
|
|
99
|
+
noise_power_from_l0=noise_power_correction)
|
|
100
|
+
|
|
101
|
+
sim_noises = {}
|
|
102
|
+
geo_grid_info = metadata_dict['product']['geolocationGrid']
|
|
103
|
+
|
|
104
|
+
# slant range times are the same for all the bursts of the swath
|
|
105
|
+
range_t = create_slant_range_times(metadata_dict)
|
|
106
|
+
thermal_noise.set_range_values(range_t)
|
|
107
|
+
|
|
108
|
+
denoise_logger.info(f"Generating noise profiles for subswath {subswath} ({pol})")
|
|
109
|
+
|
|
110
|
+
for rel_burst in range(channel.num_bursts):
|
|
111
|
+
|
|
112
|
+
denoise_logger.debug(f"Generating noise profiles for burst {rel_burst+1} ({pol}, {subswath})")
|
|
113
|
+
|
|
114
|
+
az_t0 = PreciseDateTime.from_utc_string((channel.annotation.find('antennaPattern/antennaPatternList/antennaPattern[{}]/azimuthTime'.format(rel_burst+1)).text))
|
|
115
|
+
|
|
116
|
+
az_central_time = az_t0 + az_dt * lines_per_burst/2
|
|
117
|
+
|
|
118
|
+
# create simulated noise
|
|
119
|
+
thermal_noise.rel_burst = rel_burst
|
|
120
|
+
thermal_noise.get_shrink_factor()
|
|
121
|
+
|
|
122
|
+
sim_noise, incidence_angles = thermal_noise.compute_simulated_noise(az_central_time)
|
|
123
|
+
|
|
124
|
+
start_az_time = az_t0.isoformat(sep="T", timespec="microseconds").rstrip("Z")
|
|
125
|
+
|
|
126
|
+
sim_noises.update({
|
|
127
|
+
(rel_burst, start_az_time): (sim_noise, geo_grid_info, incidence_angles, dn_value)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
# Append it
|
|
131
|
+
simulated_noises.append(OutputNoiseVectors_SLC(adsHeader=ads_header, noiseRangeVectors=sim_noises))
|
|
132
|
+
|
|
133
|
+
return simulated_noises
|
|
134
|
+
|
|
135
|
+
def grd_processing(safe, aux_files, dn_value, noise_power_correction):
|
|
136
|
+
"""
|
|
137
|
+
Generate the simulated noise range vectors of a GRD product.
|
|
138
|
+
|
|
139
|
+
For each polarization, the subswaths merged in the annotation are processed separately: one noise vector is
|
|
140
|
+
computed in slant range for each antenna pattern record (block) of the subswath and converted to ground range.
|
|
141
|
+
The subswaths are then merged into cross-subswath noise vectors by `OutputNoiseVectors_GRD`.
|
|
142
|
+
|
|
143
|
+
Parameters
|
|
144
|
+
----------
|
|
145
|
+
safe : S1Safe
|
|
146
|
+
Input GRD SAFE product.
|
|
147
|
+
aux_files : AuxiliaryFiles
|
|
148
|
+
Auxiliary files used for processing.
|
|
149
|
+
dn_value : float
|
|
150
|
+
Digital number calibration value from AUX_PP1, used to scale the output noise values.
|
|
151
|
+
noise_power_correction : dict or None
|
|
152
|
+
Noise power extracted from the L0N product, mapping ``(swath, polarization)`` to
|
|
153
|
+
``[azimuth_times, noise_powers]``, or None to use the noise power annotated in the product.
|
|
154
|
+
|
|
155
|
+
Returns
|
|
156
|
+
-------
|
|
157
|
+
list of OutputNoiseVectors_GRD
|
|
158
|
+
Output noise vectors, one element per polarization.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
simulated_noises = []
|
|
162
|
+
|
|
163
|
+
for pol, swath in safe._channels:
|
|
164
|
+
|
|
165
|
+
channel = safe.channel(pol, swath)
|
|
166
|
+
denoise_logger.info(f"Processing bursts from polarization {pol}, subswath {swath}")
|
|
167
|
+
|
|
168
|
+
metadata_dict = read_metadata_from_safe_annotation(channel.annotation)
|
|
169
|
+
ads_header = metadata_dict['product']['adsHeader']
|
|
170
|
+
|
|
171
|
+
count_noise_list = int(channel.annotation.find('generalAnnotation/noiseList').attrib['count'])
|
|
172
|
+
antenna_patterns = channel.annotation.findall('antennaPattern/antennaPatternList/antennaPattern')
|
|
173
|
+
tot_noise_vectors_out = int(channel.annotation.find('antennaPattern/antennaPatternList').attrib['count'])
|
|
174
|
+
swath_merging_info = ensure_list(metadata_dict['product']['swathMerging']['swathMergeList']['swathMerge'])
|
|
175
|
+
|
|
176
|
+
# for slant to range conversion
|
|
177
|
+
raw_cc = metadata_dict['product']['coordinateConversion']['coordinateConversionList']['coordinateConversion']
|
|
178
|
+
coordinate_conversions = []
|
|
179
|
+
for cc in raw_cc:
|
|
180
|
+
coordinate_conversions.append({
|
|
181
|
+
"azimuthTime": cc["azimuthTime"],
|
|
182
|
+
"sr0": float(cc["sr0"]),
|
|
183
|
+
"gr0": float(cc["gr0"]), # adjust if needed
|
|
184
|
+
"srgrCoeffs": [float(x) for x in cc["srgrCoefficients"]["#text"].split()]
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
# separate noise by subswath
|
|
188
|
+
noise_subswaths = []
|
|
189
|
+
for idx in range(1, count_noise_list+1):
|
|
190
|
+
tmp_subswath = channel.annotation.find('generalAnnotation/noiseList/noise[{}]/swath'.format(idx)).text
|
|
191
|
+
if tmp_subswath not in noise_subswaths:
|
|
192
|
+
noise_subswaths.append(tmp_subswath)
|
|
193
|
+
|
|
194
|
+
sim_noises = {}
|
|
195
|
+
|
|
196
|
+
for subswath in noise_subswaths:
|
|
197
|
+
|
|
198
|
+
thermal_noise = ThermalNoise(metadata_dict=metadata_dict,
|
|
199
|
+
channel=channel,
|
|
200
|
+
aux_info=aux_files,
|
|
201
|
+
product_id=safe.path.stem[4:13],
|
|
202
|
+
noise_power_from_l0=noise_power_correction,
|
|
203
|
+
subswath=subswath)
|
|
204
|
+
|
|
205
|
+
subswath_antenna_patterns = [
|
|
206
|
+
ap for ap in antenna_patterns
|
|
207
|
+
if ap.findtext('swath') == subswath
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
denoise_logger.info(f"Generating noise profiles for subswath {subswath} ({pol})")
|
|
211
|
+
|
|
212
|
+
for index in range(len(subswath_antenna_patterns)):
|
|
213
|
+
|
|
214
|
+
denoise_logger.debug(f"Generating noise profiles for block {index+1} ({pol}, {subswath})")
|
|
215
|
+
|
|
216
|
+
az_t0 = PreciseDateTime.from_utc_string(subswath_antenna_patterns[index].findtext('azimuthTime'))
|
|
217
|
+
|
|
218
|
+
range_t = create_slant_range_times(metadata_dict, subswath=subswath, az_t0=az_t0)
|
|
219
|
+
|
|
220
|
+
# create simulated noise
|
|
221
|
+
thermal_noise.set_range_values(range_t)
|
|
222
|
+
thermal_noise.rel_burst = index
|
|
223
|
+
thermal_noise.get_shrink_factor()
|
|
224
|
+
|
|
225
|
+
# slant range simulated noise
|
|
226
|
+
sim_noise, incidence_angles = thermal_noise.compute_simulated_noise(az_t0)
|
|
227
|
+
|
|
228
|
+
# convert from slant range to ground range using actual SR distances
|
|
229
|
+
az_t0_string = az_t0.isoformat(sep="T", timespec="microseconds").rstrip("Z")
|
|
230
|
+
sim_noise, incidence_angles = slant2ground(
|
|
231
|
+
az_t0_string, sim_noise, incidence_angles,
|
|
232
|
+
coordinate_conversions, thermal_noise.range_distances
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
sim_noises.update({
|
|
236
|
+
(index, subswath): (az_t0_string, sim_noise, incidence_angles)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
# append it
|
|
240
|
+
simulated_noises.append(OutputNoiseVectors_GRD(adsHeader=ads_header,
|
|
241
|
+
swath_merges=swath_merging_info,
|
|
242
|
+
noiseRangeVectors=sim_noises,
|
|
243
|
+
dn_value=dn_value,
|
|
244
|
+
tot_vec_out=tot_noise_vectors_out))
|
|
245
|
+
|
|
246
|
+
return simulated_noises
|
|
247
|
+
|
|
248
|
+
def generate_noise_profiles(prod: Path, aux_files: AuxiliaryFiles, l0n_file: Path = None):
|
|
249
|
+
"""
|
|
250
|
+
Generate the simulated noise range vectors of a Sentinel-1 L1 product.
|
|
251
|
+
|
|
252
|
+
Dispatches to `slc_processing` or `grd_processing` depending on the product type. If an L0N product is given,
|
|
253
|
+
the noise power is extracted from it instead of being read from the product annotation.
|
|
254
|
+
|
|
255
|
+
Parameters
|
|
256
|
+
----------
|
|
257
|
+
prod : pathlib.Path
|
|
258
|
+
Path to the input L1 SAFE product (SLC or GRD).
|
|
259
|
+
aux_files : AuxiliaryFiles
|
|
260
|
+
Auxiliary files used for processing.
|
|
261
|
+
l0n_file : pathlib.Path, optional
|
|
262
|
+
Path to the associated L0N SAFE product. If None (default), the annotated noise power is used.
|
|
263
|
+
|
|
264
|
+
Returns
|
|
265
|
+
-------
|
|
266
|
+
list of OutputNoiseVectors_SLC or list of OutputNoiseVectors_GRD
|
|
267
|
+
Output noise vectors, one element per output noise file.
|
|
268
|
+
|
|
269
|
+
Notes
|
|
270
|
+
-----
|
|
271
|
+
The process exits with code 128 if the product type is neither SLC nor GRD.
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
safe = S1Safe(prod)
|
|
275
|
+
dn_value = aux_files.get_dn_value(safe.mode, safe.product_type)
|
|
276
|
+
|
|
277
|
+
denoise_logger.info(f"Denoising product: {prod.resolve()}")
|
|
278
|
+
|
|
279
|
+
if l0n_file is not None:
|
|
280
|
+
noise_power_correction = get_noise_pwr_from_l0n(l0n_path = l0n_file, aux_ins_path = aux_files.ins_filepath)
|
|
281
|
+
else:
|
|
282
|
+
noise_power_correction = None
|
|
283
|
+
|
|
284
|
+
if safe.product_type == "SLC":
|
|
285
|
+
simulated_noises = slc_processing(safe, aux_files, dn_value, noise_power_correction)
|
|
286
|
+
elif safe.product_type.startswith("GRD"):
|
|
287
|
+
simulated_noises = grd_processing(safe, aux_files, dn_value, noise_power_correction)
|
|
288
|
+
else:
|
|
289
|
+
denoise_logger.error(f"Unknown product type: {safe.product_type}")
|
|
290
|
+
sys.exit(128)
|
|
291
|
+
|
|
292
|
+
return simulated_noises
|