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
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Decode Sentinel-1 plain BAQ NOISE-packet samples (ICD "Data Format C", 3/4/5-bit), which ``sentinel1decoder`` cannot
|
|
6
|
+
decode.
|
|
7
|
+
|
|
8
|
+
The user data field is made of four 16-bit-aligned sections (IE, IO, QE, QO) of `num_quads` sign-magnitude
|
|
9
|
+
`code_bits`-wide codes (MSB first); QE alone has an extra 8-bit Threshold Index (THIDX) field every 128 samples.
|
|
10
|
+
The raw quantizer code is returned, not the NRL/sigma-corrected amplitude: this is valid only while THIDX stays below
|
|
11
|
+
the ICD reconstruction threshold, which is checked explicitly (see `QE_THIDX_THRESHOLD`).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
from s1_denoise.logger import denoise_logger
|
|
18
|
+
|
|
19
|
+
QE_BLOCK_LEN = 128
|
|
20
|
+
QE_HEADER_BITS = 8
|
|
21
|
+
|
|
22
|
+
# THIDX threshold per BAQ mode below which the ICD-decoded sample value equals the raw
|
|
23
|
+
# quantizer code (ICD section 9.1.2.2); at or above it, NRL/sigma-factor reconstruction
|
|
24
|
+
# would be needed instead. Matches AUX_INS's thresholdLutList (thidxThreshold).
|
|
25
|
+
QE_THIDX_THRESHOLD = {3: 3, 4: 5, 5: 10}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _section_align(bit_pos: int) -> int:
|
|
29
|
+
"""
|
|
30
|
+
Round a bit position up to the next 16-bit boundary, where IE/IO/QE/QO sections start.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
bit_pos : int
|
|
35
|
+
Bit position.
|
|
36
|
+
|
|
37
|
+
Returns
|
|
38
|
+
-------
|
|
39
|
+
int
|
|
40
|
+
Aligned bit position.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
rem = bit_pos % 16
|
|
44
|
+
|
|
45
|
+
return bit_pos if rem == 0 else bit_pos + (16 - rem)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _decode_plain_section(bits: np.ndarray, offset: int, nbits: int, count: int):
|
|
49
|
+
"""
|
|
50
|
+
Decode a contiguous run of `count` sign-magnitude `nbits`-wide codes, without headers.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
bits : numpy.ndarray
|
|
55
|
+
Unpacked user data bits (0/1).
|
|
56
|
+
offset : int
|
|
57
|
+
Bit position of the first code.
|
|
58
|
+
nbits : int
|
|
59
|
+
Code width in bits, sign bit included.
|
|
60
|
+
count : int
|
|
61
|
+
Number of codes to decode.
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
values : numpy.ndarray
|
|
66
|
+
Signed integer quantizer codes.
|
|
67
|
+
offset : int
|
|
68
|
+
Bit position following the last decoded code.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
total = nbits * count
|
|
72
|
+
chunk = bits[offset : offset + total].reshape(count, nbits)
|
|
73
|
+
weights = 1 << np.arange(nbits - 1, -1, -1)
|
|
74
|
+
codes = chunk @ weights
|
|
75
|
+
sign = (codes >> (nbits - 1)) & 1
|
|
76
|
+
mag = codes & ((1 << (nbits - 1)) - 1)
|
|
77
|
+
values = np.where(sign == 1, -mag, mag)
|
|
78
|
+
|
|
79
|
+
return values, offset + total
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _decode_qe_section(bits: np.ndarray, offset: int, nbits: int, count: int):
|
|
83
|
+
"""
|
|
84
|
+
Decode the QE section, skipping the 8-bit THIDX field before every 128-sample block.
|
|
85
|
+
|
|
86
|
+
Each THIDX is checked to stay below the ICD reconstruction threshold, where the decoded value is the raw quantizer
|
|
87
|
+
code (see `QE_THIDX_THRESHOLD`).
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
bits : numpy.ndarray
|
|
92
|
+
Unpacked user data bits (0/1).
|
|
93
|
+
offset : int
|
|
94
|
+
Bit position of the first THIDX field.
|
|
95
|
+
nbits : int
|
|
96
|
+
Code width in bits, sign bit included.
|
|
97
|
+
count : int
|
|
98
|
+
Number of codes to decode.
|
|
99
|
+
|
|
100
|
+
Returns
|
|
101
|
+
-------
|
|
102
|
+
values : numpy.ndarray
|
|
103
|
+
Signed integer quantizer codes.
|
|
104
|
+
offset : int
|
|
105
|
+
Bit position following the last decoded code.
|
|
106
|
+
|
|
107
|
+
Raises
|
|
108
|
+
------
|
|
109
|
+
SystemExit
|
|
110
|
+
If a THIDX is at or above the reconstruction threshold (exit code 128).
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
threshold = QE_THIDX_THRESHOLD[nbits]
|
|
114
|
+
values = np.zeros(count, dtype=int)
|
|
115
|
+
pos = offset
|
|
116
|
+
idx = 0
|
|
117
|
+
|
|
118
|
+
while idx < count:
|
|
119
|
+
n = min(QE_BLOCK_LEN, count - idx)
|
|
120
|
+
|
|
121
|
+
thidx = 0
|
|
122
|
+
for bit in bits[pos : pos + QE_HEADER_BITS]:
|
|
123
|
+
thidx = (thidx << 1) | int(bit)
|
|
124
|
+
if thidx >= threshold:
|
|
125
|
+
denoise_logger.error(
|
|
126
|
+
f"QE block THIDX={thidx} is at or above the ICD reconstruction threshold ({threshold}) for BAQ {nbits}-bit mode; the raw quantizer code no longer equals the decoded sample value (NRL/sigma-factor reconstruction would be needed instead). Aborting rather than returning silently wrong noise power values."
|
|
127
|
+
)
|
|
128
|
+
sys.exit(128)
|
|
129
|
+
|
|
130
|
+
pos += QE_HEADER_BITS
|
|
131
|
+
block_values, pos = _decode_plain_section(bits, pos, nbits, n)
|
|
132
|
+
values[idx : idx + n] = block_values
|
|
133
|
+
idx += n
|
|
134
|
+
|
|
135
|
+
return values, pos
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def decode_baq_packet(raw_user_data: bytes, num_quads: int, code_bits: int = 5):
|
|
139
|
+
"""
|
|
140
|
+
Decode the user data field of one plain BAQ packet.
|
|
141
|
+
|
|
142
|
+
Parameters
|
|
143
|
+
----------
|
|
144
|
+
raw_user_data : bytes
|
|
145
|
+
Packet user data field.
|
|
146
|
+
num_quads : int
|
|
147
|
+
Number of quads, i.e. of codes in each section.
|
|
148
|
+
code_bits : int, optional
|
|
149
|
+
BAQ code width in bits. Only 5 (BAQ 5-bit) is supported. Default is 5.
|
|
150
|
+
|
|
151
|
+
Returns
|
|
152
|
+
-------
|
|
153
|
+
IE, IO, QE, QO : numpy.ndarray
|
|
154
|
+
Quantizer codes of the even/odd I and even/odd Q samples, one array per section.
|
|
155
|
+
|
|
156
|
+
Raises
|
|
157
|
+
------
|
|
158
|
+
SystemExit
|
|
159
|
+
If `code_bits` is not 5, or a QE THIDX is at or above the reconstruction threshold (exit code 128).
|
|
160
|
+
"""
|
|
161
|
+
|
|
162
|
+
if code_bits != 5:
|
|
163
|
+
denoise_logger.error(
|
|
164
|
+
f"BAQ {code_bits}-bit decoding is not validated against real data yet (only 5-bit noise packets have been verified); refusing to decode silently to avoid producing unverified noise power values."
|
|
165
|
+
)
|
|
166
|
+
sys.exit(128)
|
|
167
|
+
|
|
168
|
+
bits = np.unpackbits(np.frombuffer(raw_user_data, dtype=np.uint8)).astype(np.int64)
|
|
169
|
+
|
|
170
|
+
IE, pos = _decode_plain_section(bits, 0, code_bits, num_quads)
|
|
171
|
+
pos = _section_align(pos)
|
|
172
|
+
IO, pos = _decode_plain_section(bits, pos, code_bits, num_quads)
|
|
173
|
+
pos = _section_align(pos)
|
|
174
|
+
QE, pos = _decode_qe_section(bits, pos, code_bits, num_quads)
|
|
175
|
+
pos = _section_align(pos)
|
|
176
|
+
QO, _ = _decode_plain_section(bits, pos, code_bits, num_quads)
|
|
177
|
+
|
|
178
|
+
return IE, IO, QE, QO
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def quads_to_complex(IE: np.ndarray, IO: np.ndarray, QE: np.ndarray, QO: np.ndarray) -> np.ndarray:
|
|
182
|
+
"""
|
|
183
|
+
Interleave the decoded sections into complex samples in the standard order.
|
|
184
|
+
|
|
185
|
+
The output is ``IE[0] + 1j*QE[0], IO[0] + 1j*QO[0], IE[1] + 1j*QE[1], ...``.
|
|
186
|
+
|
|
187
|
+
Parameters
|
|
188
|
+
----------
|
|
189
|
+
IE, IO, QE, QO : numpy.ndarray
|
|
190
|
+
Quantizer codes of the even/odd I and even/odd Q samples, as returned by `decode_baq_packet`.
|
|
191
|
+
|
|
192
|
+
Returns
|
|
193
|
+
-------
|
|
194
|
+
numpy.ndarray
|
|
195
|
+
Complex64 samples, of length ``2 * len(IE)``.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
num_quads = len(IE)
|
|
199
|
+
out = np.empty(num_quads * 2, dtype=np.complex64)
|
|
200
|
+
out[0::2] = IE + 1j * QE
|
|
201
|
+
out[1::2] = IO + 1j * QO
|
|
202
|
+
|
|
203
|
+
return out
|
s1_denoise/cli.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
CLI module for the denoising processor, built with Click.
|
|
6
|
+
|
|
7
|
+
Defines the ``denoise_cli`` command, which reads a Job Order file to locate input L1 SAFE products and auxiliary
|
|
8
|
+
files, generates noise profiles, writes output XML files, and, when the nominal noise annotations are available
|
|
9
|
+
in the input product, computes the mean and maximum absolute error against them.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import click
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
from perseo_core.timing import PreciseDateTime
|
|
17
|
+
|
|
18
|
+
from s1_denoise.logger import denoise_logger
|
|
19
|
+
from s1_denoise.noise_profiles import generate_noise_profiles, print_exec_time
|
|
20
|
+
from s1_denoise import __version__
|
|
21
|
+
from s1_denoise.jo_parser import JobOrder, find_l0n
|
|
22
|
+
from s1_denoise.auxiliary_files import AuxiliaryFiles
|
|
23
|
+
from s1_denoise.output import write_to_xml
|
|
24
|
+
from s1_denoise.error_calculation import calculate_error
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@click.command()
|
|
28
|
+
@click.version_option(__version__, prog_name="denoise")
|
|
29
|
+
@click.argument("job_order_path",
|
|
30
|
+
type=click.Path(exists=True, file_okay=True, dir_okay=False, path_type=Path)
|
|
31
|
+
)
|
|
32
|
+
def denoise_cli(job_order_path):
|
|
33
|
+
"""
|
|
34
|
+
Generate noise profiles starting from L1 SAFE files.
|
|
35
|
+
\f
|
|
36
|
+
For each L1 product listed in the Job Order, the associated L0N product is looked up, noise vectors are
|
|
37
|
+
reconstructed and written as XML files in the output directory. If the product contains the corresponding
|
|
38
|
+
nominal noise annotation, the mean and maximum absolute errors (dB) are logged.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
job_order_path : pathlib.Path
|
|
43
|
+
Path to the Job Order XML file.
|
|
44
|
+
|
|
45
|
+
Raises
|
|
46
|
+
------
|
|
47
|
+
SystemExit
|
|
48
|
+
With code 128 on any unexpected error; ``SystemExit`` raised during processing (e.g. by Job Order
|
|
49
|
+
validation) is propagated. The exit code is always logged before leaving.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
return_code = 0
|
|
53
|
+
try:
|
|
54
|
+
jo = JobOrder(job_order_path)
|
|
55
|
+
|
|
56
|
+
# set log level
|
|
57
|
+
denoise_logger.setLevel(jo.log_level)
|
|
58
|
+
denoise_logger.debug("Starting noise processor...")
|
|
59
|
+
|
|
60
|
+
# read auxiliary files
|
|
61
|
+
aux_files = AuxiliaryFiles(mission_id = jo.mission_id,
|
|
62
|
+
pp1_path = jo.aux_pp1_filepath,
|
|
63
|
+
cal_path = jo.aux_cal_filepath,
|
|
64
|
+
ins_path = jo.aux_ins_filepath)
|
|
65
|
+
|
|
66
|
+
# read file to process from Job Order
|
|
67
|
+
product_path_list = [
|
|
68
|
+
Path(file_name)
|
|
69
|
+
for file_names in jo.input_files_to_process
|
|
70
|
+
for file_name in file_names['List_of_File_Names']['File_Name']
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
out_filepath_list = []
|
|
74
|
+
|
|
75
|
+
start_time = PreciseDateTime.now()
|
|
76
|
+
|
|
77
|
+
for current_prod in product_path_list:
|
|
78
|
+
|
|
79
|
+
# respective L0N product
|
|
80
|
+
associated_l0n = find_l0n(l1a=current_prod, l0n_list=jo.input_l0n_files)
|
|
81
|
+
|
|
82
|
+
# denoise product
|
|
83
|
+
noise_vectors_list = generate_noise_profiles(prod=current_prod, l0n_file=associated_l0n, aux_files=aux_files)
|
|
84
|
+
|
|
85
|
+
# generate output files
|
|
86
|
+
for elem in noise_vectors_list:
|
|
87
|
+
|
|
88
|
+
out_filepath = write_to_xml(noise=elem, output_path=jo.output_path, safe_path=current_prod)
|
|
89
|
+
out_filepath_list.append(out_filepath)
|
|
90
|
+
|
|
91
|
+
# if nominal noise files exist, calculate MAE
|
|
92
|
+
nominal_noise = current_prod / "annotation/calibration" / (out_filepath.stem[2:] + ".xml")
|
|
93
|
+
if nominal_noise.exists():
|
|
94
|
+
|
|
95
|
+
mae_on_denoise, max_err_on_denoise = calculate_error(nominal_noise_filepath=nominal_noise,
|
|
96
|
+
denoise_filepath=out_filepath)
|
|
97
|
+
|
|
98
|
+
if abs(mae_on_denoise) < 1.0:
|
|
99
|
+
denoise_logger.info(f"Mean absolute error on denoising: {mae_on_denoise:.2e} dB")
|
|
100
|
+
else:
|
|
101
|
+
denoise_logger.warning(f"High mean absolute error on denoising: {mae_on_denoise:.2e} dB")
|
|
102
|
+
|
|
103
|
+
denoise_logger.info(f"Maximum absolute error on denoising: {max_err_on_denoise:.2e} dB")
|
|
104
|
+
|
|
105
|
+
denoise_logger.info("Denoising process terminated successfully")
|
|
106
|
+
|
|
107
|
+
print_exec_time(elapsed_seconds=PreciseDateTime.now() - start_time)
|
|
108
|
+
|
|
109
|
+
except SystemExit as e:
|
|
110
|
+
return_code = e.code
|
|
111
|
+
raise
|
|
112
|
+
except Exception:
|
|
113
|
+
# any unhandled error stops the processor with exit code 128
|
|
114
|
+
denoise_logger.exception("Unexpected error. Aborting...")
|
|
115
|
+
return_code = 128
|
|
116
|
+
sys.exit(128)
|
|
117
|
+
finally:
|
|
118
|
+
denoise_logger.info(f"Exit code: {return_code}")
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Compute the error between nominal and reconstructed Sentinel-1 noise vectors.
|
|
6
|
+
|
|
7
|
+
Provides utilities to read noise XML files, interpolate the noise range vectors onto a common range/azimuth grid, and
|
|
8
|
+
compute the mean and maximum absolute differences in dB between the two, after normalisation by the digital number
|
|
9
|
+
scaling factor of the product.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import xml.etree.ElementTree as ET
|
|
14
|
+
import numpy as np
|
|
15
|
+
from scipy.interpolate import interp1d, RegularGridInterpolator
|
|
16
|
+
|
|
17
|
+
from perseo_core.timing import PreciseDateTime
|
|
18
|
+
|
|
19
|
+
from s1_denoise.utils import linear_to_db
|
|
20
|
+
|
|
21
|
+
DN_VALUES = {
|
|
22
|
+
("IW", "SLC"): 237,
|
|
23
|
+
("IW", "GRD"): 474,
|
|
24
|
+
("EW", "SLC"): 543,
|
|
25
|
+
("EW", "GRD"): 1087
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def read_noise_xml(path):
|
|
29
|
+
"""
|
|
30
|
+
Read the noise range vectors from a Sentinel-1 noise XML file (GRD or SLC).
|
|
31
|
+
|
|
32
|
+
An XML namespace, if present, is handled.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
path : str or pathlib.Path
|
|
37
|
+
Path of the noise XML file.
|
|
38
|
+
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
times : numpy.ndarray
|
|
42
|
+
Azimuth times of the noise range vectors, as PreciseDateTime objects.
|
|
43
|
+
pixels_list : list of numpy.ndarray
|
|
44
|
+
Range pixel indices of each vector.
|
|
45
|
+
noise_list : list of numpy.ndarray
|
|
46
|
+
Noise range LUT values of each vector (linear scale).
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
tree = ET.parse(path)
|
|
50
|
+
root = tree.getroot()
|
|
51
|
+
|
|
52
|
+
# Detect namespace (if present)
|
|
53
|
+
if "}" in root.tag:
|
|
54
|
+
ns = {"ns": root.tag.split("}")[0].strip("{")}
|
|
55
|
+
search_path = ".//ns:noiseRangeVector"
|
|
56
|
+
else:
|
|
57
|
+
ns = {}
|
|
58
|
+
search_path = ".//noiseRangeVector"
|
|
59
|
+
|
|
60
|
+
times = []
|
|
61
|
+
pixels_list = []
|
|
62
|
+
noise_list = []
|
|
63
|
+
|
|
64
|
+
for vec in root.findall(search_path, ns):
|
|
65
|
+
|
|
66
|
+
def find(tag):
|
|
67
|
+
"""Find a child tag of the current noise range vector, honouring the XML namespace."""
|
|
68
|
+
if ns:
|
|
69
|
+
return vec.find(f"ns:{tag}", ns)
|
|
70
|
+
return vec.find(tag)
|
|
71
|
+
|
|
72
|
+
az_time = PreciseDateTime.fromisoformat(find("azimuthTime").text)
|
|
73
|
+
pixels = np.array(list(map(float, find("pixel").text.split())))
|
|
74
|
+
noise = np.array(list(map(float, find("noiseRangeLut").text.split())))
|
|
75
|
+
|
|
76
|
+
times.append(az_time)
|
|
77
|
+
pixels_list.append(pixels)
|
|
78
|
+
noise_list.append(noise)
|
|
79
|
+
|
|
80
|
+
return np.array(times), pixels_list, noise_list
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def interpolate_noise(pixels_list, noise_list, common_pixels):
|
|
84
|
+
"""
|
|
85
|
+
Linearly interpolate each noise range vector onto a common range pixel axis.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
pixels_list : list of numpy.ndarray
|
|
90
|
+
Range pixel indices of each vector.
|
|
91
|
+
noise_list : list of numpy.ndarray
|
|
92
|
+
Noise values of each vector.
|
|
93
|
+
common_pixels : numpy.ndarray
|
|
94
|
+
Common range pixel axis.
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
numpy.ndarray
|
|
99
|
+
Interpolated noise, shape ``(n_vectors, len(common_pixels))``; NaN outside each vector's pixel span.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
interp_noises = []
|
|
103
|
+
for pixels, noise in zip(pixels_list, noise_list):
|
|
104
|
+
f = interp1d(
|
|
105
|
+
pixels,
|
|
106
|
+
noise,
|
|
107
|
+
kind="linear",
|
|
108
|
+
bounds_error=False,
|
|
109
|
+
fill_value=np.nan,
|
|
110
|
+
)
|
|
111
|
+
interp_noises.append(f(common_pixels))
|
|
112
|
+
return np.array(interp_noises)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def build_common_grid(times1, pixels_list1, times2, pixels_list2):
|
|
116
|
+
"""
|
|
117
|
+
Compute a common range pixel and azimuth time grid for two sets of noise range vectors.
|
|
118
|
+
|
|
119
|
+
Both axes have 1500 points spanning the overlap of the two sets. Azimuth times are expressed in seconds relative to
|
|
120
|
+
the first vector of each set.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
times1, times2 : numpy.ndarray
|
|
125
|
+
Azimuth times of the two sets, as PreciseDateTime objects.
|
|
126
|
+
pixels_list1, pixels_list2 : list of numpy.ndarray
|
|
127
|
+
Range pixel indices of each vector of the two sets.
|
|
128
|
+
|
|
129
|
+
Returns
|
|
130
|
+
-------
|
|
131
|
+
common_pixels : numpy.ndarray
|
|
132
|
+
Common range pixel axis.
|
|
133
|
+
times1_sec : numpy.ndarray
|
|
134
|
+
Azimuth times of the first set [s], relative to its first vector.
|
|
135
|
+
times2_sec : numpy.ndarray
|
|
136
|
+
Azimuth times of the second set [s], relative to its first vector.
|
|
137
|
+
common_times_sec : numpy.ndarray
|
|
138
|
+
Common azimuth time axis [s].
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
# Common pixel axis
|
|
142
|
+
min_pixel = max(min(p[0] for p in pixels_list1),
|
|
143
|
+
min(p[0] for p in pixels_list2))
|
|
144
|
+
max_pixel = min(max(p[-1] for p in pixels_list1),
|
|
145
|
+
max(p[-1] for p in pixels_list2))
|
|
146
|
+
|
|
147
|
+
common_pixels = np.linspace(min_pixel, max_pixel, 1500)
|
|
148
|
+
|
|
149
|
+
# Convert azimuth time to seconds
|
|
150
|
+
times1_sec = np.array([(t - times1[0]) for t in times1])
|
|
151
|
+
times2_sec = np.array([(t - times2[0]) for t in times2])
|
|
152
|
+
|
|
153
|
+
common_times_sec = np.linspace(
|
|
154
|
+
max(times1_sec[0], times2_sec[0]),
|
|
155
|
+
min(times1_sec[-1], times2_sec[-1]),
|
|
156
|
+
1500,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return common_pixels, times1_sec, times2_sec, common_times_sec
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def compare_noise(noise_file, denoise_file, dn):
|
|
163
|
+
"""
|
|
164
|
+
Compare nominal and reconstructed noise vectors on a common grid.
|
|
165
|
+
|
|
166
|
+
Both files are interpolated onto the grid of `build_common_grid` and normalised by ``dn**2``; zero values are set to
|
|
167
|
+
NaN and values are clipped at 1e-12 before conversion to dB.
|
|
168
|
+
|
|
169
|
+
Parameters
|
|
170
|
+
----------
|
|
171
|
+
noise_file : str or pathlib.Path
|
|
172
|
+
Nominal noise XML file.
|
|
173
|
+
denoise_file : str or pathlib.Path
|
|
174
|
+
Reconstructed noise XML file.
|
|
175
|
+
dn : float
|
|
176
|
+
Digital number scaling factor of the product (see `DN_VALUES`).
|
|
177
|
+
|
|
178
|
+
Returns
|
|
179
|
+
-------
|
|
180
|
+
common_pixels : numpy.ndarray
|
|
181
|
+
Common range pixel axis.
|
|
182
|
+
common_times_sec : numpy.ndarray
|
|
183
|
+
Common azimuth time axis [s].
|
|
184
|
+
diff_db : numpy.ndarray
|
|
185
|
+
Reconstructed minus nominal noise [dB], shape ``(len(common_times_sec), len(common_pixels))``.
|
|
186
|
+
f1 : scipy.interpolate.RegularGridInterpolator
|
|
187
|
+
Interpolator of the nominal noise over (azimuth time [s], range pixel), not normalised by `dn`.
|
|
188
|
+
f2 : scipy.interpolate.RegularGridInterpolator
|
|
189
|
+
Interpolator of the reconstructed noise over (azimuth time [s], range pixel), not normalised by `dn`.
|
|
190
|
+
mae : float
|
|
191
|
+
Mean absolute difference [dB], ignoring NaN.
|
|
192
|
+
max_err : float
|
|
193
|
+
Maximum absolute difference [dB], ignoring NaN.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
times1, pixels_list1, noise_list = read_noise_xml(noise_file)
|
|
197
|
+
times2, pixels_list2, denoise_list = read_noise_xml(denoise_file)
|
|
198
|
+
|
|
199
|
+
common_pixels, times1_sec, times2_sec, common_times_sec = build_common_grid(times1, pixels_list1, times2, pixels_list2)
|
|
200
|
+
|
|
201
|
+
interp_noise = interpolate_noise(pixels_list1, noise_list, common_pixels)
|
|
202
|
+
interp_denoise = interpolate_noise(pixels_list2, denoise_list, common_pixels)
|
|
203
|
+
|
|
204
|
+
f1 = RegularGridInterpolator(
|
|
205
|
+
(times1_sec, common_pixels),
|
|
206
|
+
interp_noise,
|
|
207
|
+
bounds_error=False,
|
|
208
|
+
fill_value=np.nan,
|
|
209
|
+
)
|
|
210
|
+
f2 = RegularGridInterpolator(
|
|
211
|
+
(times2_sec, common_pixels),
|
|
212
|
+
interp_denoise,
|
|
213
|
+
bounds_error=False,
|
|
214
|
+
fill_value=np.nan,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
T, P = np.meshgrid(common_times_sec, common_pixels, indexing="ij")
|
|
218
|
+
|
|
219
|
+
noise = f1(np.stack([T, P], axis=-1)) / (dn**2)
|
|
220
|
+
denoise = f2(np.stack([T, P], axis=-1)) / (dn**2)
|
|
221
|
+
|
|
222
|
+
noise[noise == 0] = np.nan
|
|
223
|
+
denoise[denoise == 0] = np.nan
|
|
224
|
+
|
|
225
|
+
diff_db = linear_to_db(np.maximum(np.abs(denoise), 1e-12)) - linear_to_db(np.maximum(np.abs(noise), 1e-12))
|
|
226
|
+
|
|
227
|
+
mae = np.nanmean(np.abs(diff_db))
|
|
228
|
+
max_err = np.nanmax(np.abs(diff_db))
|
|
229
|
+
|
|
230
|
+
return common_pixels, common_times_sec, diff_db, f1, f2, mae, max_err
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def calculate_error(nominal_noise_filepath, denoise_filepath):
|
|
234
|
+
"""
|
|
235
|
+
Compute the mean and maximum absolute error between a nominal and a reconstructed noise XML file.
|
|
236
|
+
|
|
237
|
+
The acquisition mode and product type, used to select the digital number in `DN_VALUES`, are derived from the name
|
|
238
|
+
of the SAFE product containing the nominal noise file (three directory levels up).
|
|
239
|
+
|
|
240
|
+
Parameters
|
|
241
|
+
----------
|
|
242
|
+
nominal_noise_filepath : pathlib.Path
|
|
243
|
+
Nominal noise XML file, inside a SAFE product.
|
|
244
|
+
denoise_filepath : pathlib.Path
|
|
245
|
+
Reconstructed noise XML file.
|
|
246
|
+
|
|
247
|
+
Returns
|
|
248
|
+
-------
|
|
249
|
+
mean_error : float
|
|
250
|
+
Mean absolute difference [dB].
|
|
251
|
+
max_error : float
|
|
252
|
+
Maximum absolute difference [dB].
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
product_name = nominal_noise_filepath.parent.parent.parent.stem
|
|
256
|
+
mode = product_name[4:6]
|
|
257
|
+
prod_type = product_name[7:10]
|
|
258
|
+
dn = DN_VALUES.get((mode, prod_type))
|
|
259
|
+
|
|
260
|
+
common_pixels, common_times_sec, diff_db, f1, f2, mean_error, max_error = compare_noise(nominal_noise_filepath, denoise_filepath, dn)
|
|
261
|
+
|
|
262
|
+
return mean_error, max_error
|