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/s1safe.py ADDED
@@ -0,0 +1,1415 @@
1
+ # SPDX-FileCopyrightText: Aresys S.r.l. <info@aresys.it>
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """
5
+ Module providing classes and utilities for reading and handling Sentinel-1 SAFE products.
6
+
7
+ Defines S1Safe, S1Channel, and S1Roi as the primary abstractions for accessing
8
+ product metadata, annotation, noise, calibration, RFI, and measurement files across
9
+ TopSAR acquisition modes (IW, EW). Also provides functions for noise map
10
+ computation, calibration vector interpolation, and SAFE filename parsing.
11
+ """
12
+
13
+ from pathlib import Path
14
+ from datetime import datetime
15
+ import os
16
+ import xml.etree.ElementTree as ET
17
+ from PIL import Image
18
+ import numpy as np
19
+ import rasterio
20
+ from rasterio.windows import Window
21
+
22
+ from perseo_core.timing import PreciseDateTime
23
+
24
+ _subswaths = {'EW': ('EW1', 'EW2', 'EW3', 'EW4', 'EW5'),
25
+ 'IW': ('IW1', 'IW2', 'IW3'),
26
+ 'WV': ('WV1', 'WV2')}
27
+
28
+ _pols = {'DV': ('VV', 'VH'),
29
+ 'DH': ('HH', 'HV'),
30
+ 'SH': ('HH',),
31
+ 'SV': ('VV',)}
32
+
33
+ calibration_types = ('dn', 'betaNought', 'gamma', 'sigmaNought')
34
+
35
+ def ch_idx(pol, swath_id):
36
+ """
37
+ Return the key identifying a channel in the channel dictionary of ``S1Safe``.
38
+
39
+ Parameters
40
+ ----------
41
+ pol : str
42
+ Polarization (e.g. ``VV``).
43
+ swath_id : str
44
+ Swath identifier (e.g. ``IW1``).
45
+
46
+ Returns
47
+ -------
48
+ tuple of str
49
+ The ``(pol, swath_id)`` pair.
50
+ """
51
+ return pol, swath_id
52
+
53
+ def read_calibration_vectors(calibration_et_root, calibration):
54
+ """
55
+ Read the calibration vectors of a given type from a calibration annotation.
56
+
57
+ Parameters
58
+ ----------
59
+ calibration_et_root : xml.etree.ElementTree.ElementTree or xml.etree.ElementTree.Element
60
+ Parsed calibration annotation XML.
61
+ calibration : str
62
+ Calibration type, one of ``dn``, ``betaNought``, ``gamma``, ``sigmaNought``.
63
+
64
+ Returns
65
+ -------
66
+ list of dict
67
+ One dictionary per ``calibrationVector`` with keys ``azimuthTime`` (PreciseDateTime), ``line`` (int),
68
+ ``pixel`` (numpy.ndarray of int) and ``<calibration>`` (numpy.ndarray of float, calibration LUT values).
69
+
70
+ Raises
71
+ ------
72
+ ValueError
73
+ If ``calibration`` is not a supported calibration type.
74
+ """
75
+ if calibration not in calibration_types:
76
+ raise ValueError('Calibration type must be one of the following:' + ', '.join(calibration_types))
77
+ cal_vector_list = list()
78
+ for cal_vector in calibration_et_root.findall('calibrationVectorList/calibrationVector'):
79
+ cal_vector_dict = dict()
80
+ cal_vector_dict['azimuthTime'] = PreciseDateTime.fromisoformat(cal_vector.find('azimuthTime').text)
81
+ cal_vector_dict['line'] = int(cal_vector.find('line').text)
82
+ cal_vector_dict['pixel'] = np.fromstring(cal_vector.find('pixel').text, sep=' ', dtype=int)
83
+ cal_vector_dict[calibration] = np.fromstring(cal_vector.find(calibration).text, sep=' ', dtype=float)
84
+ cal_vector_list.append(cal_vector_dict)
85
+ return cal_vector_list
86
+
87
+
88
+ def calibrate(annotation_et_root, calibration_et_root, data, roi, calibration=None):
89
+ """
90
+ Apply radiometric calibration to data over a region of interest.
91
+
92
+ The calibration LUT is interpolated linearly in range on the ROI pixels and linearly in azimuth time between
93
+ the two calibration vectors bracketing each line (extrapolating outside the annotated vectors). The output
94
+ is ``|data|**2 / lut**2``.
95
+
96
+ Parameters
97
+ ----------
98
+ annotation_et_root : xml.etree.ElementTree.ElementTree or xml.etree.ElementTree.Element
99
+ Parsed product annotation XML, used to build the azimuth time axis.
100
+ calibration_et_root : xml.etree.ElementTree.ElementTree or xml.etree.ElementTree.Element
101
+ Parsed calibration annotation XML.
102
+ data : numpy.ndarray
103
+ Data over the ROI, with shape ``(roi.num_azi_lines, roi.num_range_samples)``.
104
+ roi : S1Roi
105
+ Region of interest covered by ``data``.
106
+ calibration : str, optional
107
+ Calibration type, one of ``dn``, ``betaNought``, ``gamma``, ``sigmaNought``. Default is ``None``, in which
108
+ case ``data`` is returned unchanged.
109
+
110
+ Returns
111
+ -------
112
+ numpy.ndarray
113
+ Calibrated data in linear power units, or ``data`` itself if ``calibration`` is ``None``.
114
+
115
+ Raises
116
+ ------
117
+ ValueError
118
+ If ``calibration`` is not a supported calibration type.
119
+ """
120
+ if calibration is None:
121
+ return data
122
+ # Build the azimuth time axis
123
+ cal_vectors = read_calibration_vectors(calibration_et_root, calibration)
124
+ start_line_time = PreciseDateTime.fromisoformat(annotation_et_root.find('imageAnnotation/imageInformation/productFirstLineUtcTime').text)
125
+ line_time_step = float(annotation_et_root.find('imageAnnotation/imageInformation/azimuthTimeInterval').text)
126
+ azi_time_axis = np.array([start_line_time + ii * line_time_step for ii in range(roi.azi_start_idx, roi.azi_start_idx + roi.num_azi_lines)])
127
+
128
+ # Resample all the calibration vectors on the same range axis
129
+ rng_pixel_axis = np.arange(roi.range_start_idx, roi.range_start_idx+roi.num_range_samples)
130
+ for cal_vec in cal_vectors:
131
+ cal_vec[calibration] = np.interp(rng_pixel_axis, cal_vec['pixel'], cal_vec[calibration])
132
+
133
+ # For each azimuth time, linearly interpolate the vectors before and after it.
134
+ cal_mat = np.zeros((roi.num_azi_lines, roi.num_range_samples))
135
+ for azi_idx, azi_time in enumerate(azi_time_axis):
136
+ # Search an azimuth time lower than the current line being processed
137
+ cal_vec_idx = 1
138
+ while (cal_vec_idx < len(cal_vectors) - 1) and (cal_vectors[cal_vec_idx]['azimuthTime'] <= azi_time):
139
+ cal_vec_idx += 1
140
+ # Linear interpolation of the calibration vector
141
+ cal_mat[azi_idx, :] = (cal_vectors[cal_vec_idx - 1][calibration] +
142
+ (cal_vectors[cal_vec_idx][calibration] - cal_vectors[cal_vec_idx - 1][calibration]) /
143
+ (cal_vectors[cal_vec_idx]['azimuthTime'] - cal_vectors[cal_vec_idx - 1]['azimuthTime']) *
144
+ (azi_time - cal_vectors[cal_vec_idx - 1]['azimuthTime']))
145
+ return np.abs(data)**2 / cal_mat**2
146
+
147
+
148
+ def find_azi_block_from_line(azi_noise_vec_list, line_idx):
149
+ """
150
+ Return the index of the azimuth noise vector whose line interval contains a given line.
151
+
152
+ Parameters
153
+ ----------
154
+ azi_noise_vec_list : list of dict
155
+ Azimuth noise vectors of one swath, as returned by ``read_azimuth_noise_vectors``.
156
+ line_idx : int
157
+ Azimuth line index.
158
+
159
+ Returns
160
+ -------
161
+ int or None
162
+ Index of the first vector with ``firstAzimuthLine <= line_idx <= lastAzimuthLine``, or ``None`` if none.
163
+ """
164
+ for vec_idx, azi_noise_vec in enumerate(azi_noise_vec_list):
165
+ if (azi_noise_vec['firstAzimuthLine'] <= line_idx) and (azi_noise_vec['lastAzimuthLine'] >= line_idx):
166
+ return vec_idx
167
+ return None
168
+
169
+
170
+ def read_range_noise_vectors(noise_et_root):
171
+ """
172
+ Read the range noise vectors from a noise annotation.
173
+
174
+ Parameters
175
+ ----------
176
+ noise_et_root : xml.etree.ElementTree.ElementTree or xml.etree.ElementTree.Element
177
+ Parsed noise annotation XML.
178
+
179
+ Returns
180
+ -------
181
+ list of dict
182
+ One dictionary per ``noiseRangeVector`` with keys ``azimuthTime`` (PreciseDateTime), ``line`` (int),
183
+ ``pixel`` (numpy.ndarray of int) and ``noiseRangeLut`` (numpy.ndarray of float, linear power).
184
+ """
185
+ noise_rng_vec_list = list()
186
+ for noise_rng_vec in noise_et_root.findall('noiseRangeVectorList/noiseRangeVector'):
187
+ noise_rng_vec_dict = dict()
188
+ noise_rng_vec_dict['azimuthTime'] = PreciseDateTime.fromisoformat(noise_rng_vec.find('azimuthTime').text)
189
+ noise_rng_vec_dict['line'] = int(noise_rng_vec.find('line').text)
190
+ noise_rng_vec_dict['pixel'] = np.fromstring(noise_rng_vec.find('pixel').text, sep=' ', dtype=int)
191
+ noise_rng_vec_dict['noiseRangeLut'] = np.fromstring(noise_rng_vec.find('noiseRangeLut').text, sep=' ',
192
+ dtype=float)
193
+ noise_rng_vec_list.append(noise_rng_vec_dict)
194
+ return noise_rng_vec_list
195
+
196
+
197
+ def read_azimuth_noise_vectors(noise_et_root):
198
+ """
199
+ Read the azimuth noise vectors from a noise annotation, grouped by swath.
200
+
201
+ Parameters
202
+ ----------
203
+ noise_et_root : xml.etree.ElementTree.ElementTree or xml.etree.ElementTree.Element
204
+ Parsed noise annotation XML.
205
+
206
+ Returns
207
+ -------
208
+ dict of str to list of dict
209
+ Mapping from swath identifier to its azimuth noise vectors. Each vector has keys ``swath``,
210
+ ``firstAzimuthLine``, ``firstRangeSample``, ``lastAzimuthLine``, ``lastRangeSample`` (int), ``line``
211
+ (numpy.ndarray of int) and ``noiseAzimuthLut`` (numpy.ndarray of float, linear). Empty if the annotation
212
+ has no azimuth noise vectors.
213
+ """
214
+ noise_azi_vec_dict = dict()
215
+ for noise_azi_vec in noise_et_root.findall('noiseAzimuthVectorList/noiseAzimuthVector'):
216
+ noise_azi_vec_curr_swath = dict()
217
+ curr_swath = noise_azi_vec.find('swath').text
218
+ noise_azi_vec_curr_swath['swath'] = curr_swath
219
+ noise_azi_vec_curr_swath['firstAzimuthLine'] = int(noise_azi_vec.find('firstAzimuthLine').text)
220
+ noise_azi_vec_curr_swath['firstRangeSample'] = int(noise_azi_vec.find('firstRangeSample').text)
221
+ noise_azi_vec_curr_swath['lastAzimuthLine'] = int(noise_azi_vec.find('lastAzimuthLine').text)
222
+ noise_azi_vec_curr_swath['lastRangeSample'] = int(noise_azi_vec.find('lastRangeSample').text)
223
+ noise_azi_vec_curr_swath['line'] = np.fromstring(noise_azi_vec.find('line').text, sep=' ', dtype=int)
224
+ noise_azi_vec_curr_swath['noiseAzimuthLut'] = np.fromstring(noise_azi_vec.find('noiseAzimuthLut').text,
225
+ sep=' ', dtype=float)
226
+ if curr_swath not in noise_azi_vec_dict:
227
+ noise_azi_vec_dict[curr_swath] = [noise_azi_vec_curr_swath]
228
+ else:
229
+ noise_azi_vec_dict[curr_swath].append(noise_azi_vec_curr_swath)
230
+ return noise_azi_vec_dict
231
+
232
+
233
+ def compute_noise_map(noise_et_root, data_size, roi):
234
+ """
235
+ Compute a 2D noise map over a region of interest from the annotated noise vectors.
236
+
237
+ Range LUTs are interpolated to every range sample and linearly in azimuth between the range vectors bracketing
238
+ each line (restricted to the vectors falling in the current azimuth block, or in the whole swath as fallback).
239
+ The result is multiplied by the azimuth LUT interpolated on the lines of each azimuth block. If the annotation
240
+ has no azimuth vectors, a unit azimuth LUT over the whole image is used. Pixels not covered by any azimuth
241
+ block are left to zero.
242
+
243
+ Parameters
244
+ ----------
245
+ noise_et_root : xml.etree.ElementTree.ElementTree or xml.etree.ElementTree.Element
246
+ Parsed noise annotation XML.
247
+ data_size : tuple of int
248
+ Image size as ``(num_azimuth_lines, num_range_samples)``.
249
+ roi : tuple of int
250
+ Region of interest as ``(start_sample, start_line, width, height)``, i.e. ``S1Roi.as_tuple()``.
251
+
252
+ Returns
253
+ -------
254
+ numpy.ndarray
255
+ Noise map of shape ``(height, width)``, square root of the interpolated noise power LUT product (linear
256
+ amplitude scale).
257
+
258
+ Raises
259
+ ------
260
+ ValueError
261
+ If the ROI exceeds the image size, or if no range noise vector is found for a swath.
262
+ """
263
+
264
+ n_az, n_rg = data_size
265
+ start_sample, start_line, width, height = roi
266
+ end_sample = start_sample + width - 1
267
+ end_line = start_line + height - 1
268
+
269
+ if end_sample >= n_rg or end_line >= n_az:
270
+ raise ValueError("ROI outside of bounds")
271
+
272
+ range_noise = read_range_noise_vectors(noise_et_root)
273
+ azimuth_noise = read_azimuth_noise_vectors(noise_et_root)
274
+
275
+ # Handle missing azimuth noise
276
+ if not azimuth_noise:
277
+ swath = noise_et_root.find("adsHeader/swath").text
278
+ azimuth_noise[swath] = [{
279
+ "swath": swath,
280
+ "firstAzimuthLine": 0,
281
+ "lastAzimuthLine": n_az - 1,
282
+ "firstRangeSample": 0,
283
+ "lastRangeSample": n_rg - 1,
284
+ "line": np.array([0, n_az - 1]),
285
+ "noiseAzimuthLut": np.array([1.0, 1.0]),
286
+ }]
287
+
288
+ # Precompute swath limits
289
+ min_line = {k: min(v["firstAzimuthLine"] for v in lst) for k, lst in azimuth_noise.items()}
290
+ max_line = {k: max(v["lastAzimuthLine"] for v in lst) for k, lst in azimuth_noise.items()}
291
+
292
+ # Pre-interpolate range LUTs to full pixel resolution
293
+ pixels = np.arange(n_rg)
294
+ for rvec in range_noise:
295
+ if rvec:
296
+ rvec["noiseRangeLut"] = np.interp(pixels, rvec["pixel"], rvec["noiseRangeLut"])
297
+
298
+ # Pre-cache line values for range noise vectors
299
+ line_arr_rng = np.array([rvec["line"] for rvec in range_noise])
300
+ noise_map = np.zeros((height, width), dtype=float)
301
+
302
+ for swath_name, azi_list in azimuth_noise.items():
303
+ for azi_vec in azi_list:
304
+ first_line, last_line = azi_vec["firstAzimuthLine"], azi_vec["lastAzimuthLine"]
305
+
306
+ # Lines overlapping ROI
307
+ valid_lines = np.arange(max(first_line, start_line), min(last_line, end_line) + 1)
308
+ if valid_lines.size == 0:
309
+ continue
310
+
311
+ # Interpolate azimuth LUT over valid lines
312
+ line_arr = azi_vec["line"]
313
+ azi_lut = azi_vec["noiseAzimuthLut"]
314
+ azi_interp = np.interp(valid_lines, line_arr, azi_lut) if len(line_arr) > 1 else np.full(valid_lines.shape, azi_lut[0])
315
+
316
+ # Valid range samples
317
+ valid_rg = np.arange(max(azi_vec["firstRangeSample"], start_sample),
318
+ min(azi_vec["lastRangeSample"], end_sample) + 1)
319
+ if valid_rg.size == 0:
320
+ continue
321
+
322
+ # Find range noise vectors once per azimuth segment
323
+ in_swath = (line_arr_rng >= first_line) & (line_arr_rng <= last_line)
324
+ valid_idx = np.where(in_swath)[0]
325
+
326
+ if valid_idx.size == 0:
327
+ # fallback to sub-swath range
328
+ in_subswath = (line_arr_rng >= min_line[swath_name]) & (line_arr_rng <= max_line[swath_name])
329
+ valid_idx = np.where(in_subswath)[0]
330
+ if valid_idx.size == 0:
331
+ raise ValueError(f"{swath_name}: no valid range vectors")
332
+
333
+ # Process each azimuth line
334
+ for i, line in enumerate(valid_lines):
335
+ if valid_idx.size == 1:
336
+ rng_lut = range_noise[valid_idx[0]]["noiseRangeLut"][valid_rg]
337
+ else:
338
+ # find bracket vectors
339
+ idx = np.searchsorted(line_arr_rng[valid_idx], line) - 1
340
+ idx = np.clip(idx, 0, len(valid_idx) - 2)
341
+
342
+ vec1 = range_noise[valid_idx[idx]]
343
+ vec2 = range_noise[valid_idx[idx + 1]]
344
+ lut1, lut2 = vec1["noiseRangeLut"][valid_rg], vec2["noiseRangeLut"][valid_rg]
345
+ w = (line - vec1["line"]) / (vec2["line"] - vec1["line"])
346
+ rng_lut = lut1 + w * (lut2 - lut1)
347
+
348
+ noise_map[line - start_line, valid_rg - start_sample] = rng_lut * azi_interp[i]
349
+
350
+ return np.sqrt(noise_map)
351
+
352
+
353
+ def subswaths_from_mode(mode_name):
354
+ """
355
+ Return the swath identifiers of an acquisition mode.
356
+
357
+ Parameters
358
+ ----------
359
+ mode_name : str
360
+ Acquisition mode (``IW``, ``EW``, ``WV`` or a stripmap mode ``S1``..``S6``).
361
+
362
+ Returns
363
+ -------
364
+ tuple of str
365
+ Swath identifiers (e.g. ``('IW1', 'IW2', 'IW3')``); for stripmap modes, the mode itself.
366
+
367
+ Raises
368
+ ------
369
+ KeyError
370
+ If the mode is not supported.
371
+ """
372
+ if is_stripmap_mode(mode_name):
373
+ return (mode_name,)
374
+ return _subswaths[mode_name]
375
+
376
+
377
+ def mode_from_safe_name(safe_name):
378
+ """
379
+ Return the acquisition mode from a SAFE product name.
380
+
381
+ Parameters
382
+ ----------
383
+ safe_name : str
384
+ SAFE product name (e.g. ``S1A_IW_SLC__1SDV_...``).
385
+
386
+ Returns
387
+ -------
388
+ str
389
+ Two-character acquisition mode (e.g. ``IW``).
390
+ """
391
+ return safe_name[4:6]
392
+
393
+
394
+ def product_type_from_safe_name(safe_name):
395
+ """
396
+ Return the product type from a SAFE product name.
397
+
398
+ Parameters
399
+ ----------
400
+ safe_name : str
401
+ SAFE product name.
402
+
403
+ Returns
404
+ -------
405
+ str
406
+ Product type with underscores removed (e.g. ``SLC``, ``GRDH``).
407
+ """
408
+ tmp = safe_name[6:11]
409
+ tmp = tmp.replace('_', '')
410
+ return tmp
411
+
412
+
413
+ def polarizations_from_safe_name(safe_name):
414
+ """
415
+ Return the polarizations from a SAFE product name.
416
+
417
+ Parameters
418
+ ----------
419
+ safe_name : str
420
+ SAFE product name.
421
+
422
+ Returns
423
+ -------
424
+ tuple of str
425
+ Polarizations of the product (e.g. ``('VV', 'VH')`` for ``DV``).
426
+
427
+ Raises
428
+ ------
429
+ KeyError
430
+ If the polarization code is not supported.
431
+ """
432
+ return _pols[safe_name[14:16]]
433
+
434
+
435
+ def start_time_str_from_safe_name(safe_name):
436
+ """
437
+ Return the sensing start time string from a SAFE product name.
438
+
439
+ Parameters
440
+ ----------
441
+ safe_name : str
442
+ SAFE product name.
443
+
444
+ Returns
445
+ -------
446
+ str
447
+ Start time in the format ``YYYYMMDDTHHMMSS``.
448
+ """
449
+ return safe_name[17:32]
450
+
451
+
452
+ def stop_time_str_from_safe_name(safe_name):
453
+ """
454
+ Return the sensing stop time string from a SAFE product name.
455
+
456
+ Parameters
457
+ ----------
458
+ safe_name : str
459
+ SAFE product name.
460
+
461
+ Returns
462
+ -------
463
+ str
464
+ Stop time in the format ``YYYYMMDDTHHMMSS``.
465
+ """
466
+ return safe_name[33:48]
467
+
468
+
469
+ def is_topsar_mode(mode_name):
470
+ """
471
+ Check whether an acquisition mode is a TOPSAR mode.
472
+
473
+ Parameters
474
+ ----------
475
+ mode_name : str
476
+ Acquisition mode.
477
+
478
+ Returns
479
+ -------
480
+ bool
481
+ ``True`` for ``IW`` and ``EW``, ``False`` otherwise.
482
+ """
483
+ if mode_name == 'EW' or mode_name == 'IW':
484
+ return True
485
+ return False
486
+
487
+
488
+ def is_topsar(safe_name):
489
+ """
490
+ Check whether a SAFE product was acquired in a TOPSAR mode (IW or EW).
491
+
492
+ Parameters
493
+ ----------
494
+ safe_name : str
495
+ SAFE product name.
496
+
497
+ Returns
498
+ -------
499
+ bool
500
+ ``True`` if the product is IW or EW.
501
+ """
502
+ mode = mode_from_safe_name(safe_name)
503
+ return is_topsar_mode(mode)
504
+
505
+
506
+ def is_stripmap_mode(mode_name):
507
+ """
508
+ Check whether an acquisition mode is a stripmap mode.
509
+
510
+ Parameters
511
+ ----------
512
+ mode_name : str
513
+ Acquisition mode.
514
+
515
+ Returns
516
+ -------
517
+ bool
518
+ ``True`` if the mode starts with ``S`` (``S1``..``S6``).
519
+ """
520
+ if mode_name[:1] == 'S':
521
+ return True
522
+ return False
523
+
524
+
525
+ def is_stripmap(safe_name):
526
+ """
527
+ Check whether a SAFE product was acquired in a stripmap mode.
528
+
529
+ Parameters
530
+ ----------
531
+ safe_name : str
532
+ SAFE product name.
533
+
534
+ Returns
535
+ -------
536
+ bool
537
+ ``True`` if the product is a stripmap product.
538
+ """
539
+ mode = mode_from_safe_name(safe_name)
540
+ return is_stripmap_mode(mode)
541
+
542
+
543
+ def is_wave(safe_name):
544
+ """
545
+ Check whether a SAFE product was acquired in wave mode.
546
+
547
+ Parameters
548
+ ----------
549
+ safe_name : str
550
+ SAFE product name.
551
+
552
+ Returns
553
+ -------
554
+ bool or None
555
+ ``True`` if the mode is ``WV``, ``None`` otherwise.
556
+ """
557
+ mode = mode_from_safe_name(safe_name)
558
+ if mode == 'WV':
559
+ return True
560
+
561
+
562
+ def is_grd(safe_name):
563
+ """
564
+ Check whether a SAFE product is a GRD product.
565
+
566
+ Parameters
567
+ ----------
568
+ safe_name : str
569
+ SAFE product name.
570
+
571
+ Returns
572
+ -------
573
+ bool
574
+ ``True`` if the product type is ``GR`` followed by two characters (e.g. ``GRDH``, ``GRDM``).
575
+ """
576
+ prod = product_type_from_safe_name(safe_name)
577
+ if prod[:-2] == 'GR':
578
+ return True
579
+ return False
580
+
581
+
582
+ def measurement_file_from_safe_path(safe_path, pol, swath, img_num=None):
583
+ """
584
+ Return the measurement TIFF file of a channel inside a SAFE product.
585
+
586
+ Parameters
587
+ ----------
588
+ safe_path : str or pathlib.Path
589
+ Path to the SAFE product directory.
590
+ pol : str
591
+ Polarization (e.g. ``VV``, ``VH``).
592
+ swath : str
593
+ Swath identifier (e.g. ``IW1``, ``EW3``, ``WV1``).
594
+ img_num : str, optional
595
+ Image number, used only for wave mode swaths (``WV1``, ``WV2``). Default is ``None``.
596
+
597
+ Returns
598
+ -------
599
+ pathlib.Path
600
+ Path to the first matching file in ``measurement``.
601
+
602
+ Raises
603
+ ------
604
+ StopIteration
605
+ If no matching file is found (e.g. for annotation-only products).
606
+ """
607
+ safe_path = Path(safe_path)
608
+ if (swath in ['WV1', 'WV2']) and (img_num is not None):
609
+ return next(safe_path.glob('measurement/s1*-{}-*-{}-*-{}.tiff'.format(swath.lower(), pol.lower(), img_num)))
610
+ else:
611
+ return next(safe_path.glob('measurement/s1*-{}-*-{}-*.tiff'.format(swath.lower(), pol.lower())))
612
+
613
+
614
+ def calibration_file_from_safe_path(safe_path, pol, swath, img_num=None):
615
+ """
616
+ Return the calibration annotation file of a channel inside a SAFE product.
617
+
618
+ The file is searched in ``annotation/calibration`` by swath, product type and polarization. Product type ``SL1`` is
619
+ searched as ``SLC``.
620
+
621
+ Parameters
622
+ ----------
623
+ safe_path : str or pathlib.Path
624
+ Path to the SAFE product directory.
625
+ pol : str
626
+ Polarization (e.g. ``VV``, ``VH``).
627
+ swath : str
628
+ Swath identifier (e.g. ``IW1``, ``EW3``, ``WV1``).
629
+ img_num : str, optional
630
+ Image number, used only for wave mode swaths (``WV1``, ``WV2``). Default is ``None``.
631
+
632
+ Returns
633
+ -------
634
+ pathlib.Path
635
+ Path to the first matching calibration annotation file.
636
+
637
+ Raises
638
+ ------
639
+ StopIteration
640
+ If no matching file is found.
641
+ """
642
+ safe_path = Path(safe_path)
643
+ safe_name = safe_path.name
644
+ prod = product_type_from_safe_name(safe_name)
645
+ if prod == 'SL1':
646
+ prod = 'SLC'
647
+
648
+ if (swath in ['WV1', 'WV2']) and (img_num is not None):
649
+ return next(safe_path.glob('annotation/calibration/calibration*-{}-{}-{}-*-{}.xml'.format(swath.lower(), prod[:3].lower(), pol.lower(), img_num)))
650
+ else:
651
+ return next(safe_path.glob('annotation/calibration/calibration*-{}-{}-{}-*.xml'.format(swath.lower(), prod[:3].lower(), pol.lower())))
652
+
653
+
654
+ def noise_file_from_safe_path(safe_path, pol, swath, img_num=None):
655
+ """
656
+ Return the noise annotation file of a channel inside a SAFE product.
657
+
658
+ The file is searched in ``annotation/calibration`` by swath, product type and polarization. Product type ``SL1`` is
659
+ searched as ``SLC``.
660
+
661
+ Parameters
662
+ ----------
663
+ safe_path : str or pathlib.Path
664
+ Path to the SAFE product directory.
665
+ pol : str
666
+ Polarization (e.g. ``VV``, ``VH``).
667
+ swath : str
668
+ Swath identifier (e.g. ``IW1``, ``EW3``, ``WV1``).
669
+ img_num : str, optional
670
+ Image number, used only for wave mode swaths (``WV1``, ``WV2``). Default is ``None``.
671
+
672
+ Returns
673
+ -------
674
+ pathlib.Path
675
+ Path to the first matching noise annotation file.
676
+
677
+ Raises
678
+ ------
679
+ StopIteration
680
+ If no matching file is found.
681
+ """
682
+ safe_path = Path(safe_path)
683
+ safe_name = safe_path.name
684
+ prod = product_type_from_safe_name(safe_name)
685
+ if prod == 'SL1':
686
+ prod = 'SLC'
687
+
688
+ if (swath in ['WV1', 'WV2']) and (img_num is not None):
689
+ return next(safe_path.glob('annotation/calibration/noise*-{}-{}-{}-*-{}.xml'.format(swath.lower(), prod[:3].lower(), pol.lower(), img_num)))
690
+ else:
691
+ return next(safe_path.glob('annotation/calibration/noise*-{}-{}-{}-*.xml'.format(swath.lower(), prod[:3].lower(), pol.lower())))
692
+
693
+
694
+ def rfi_file_from_safe_path(safe_path, pol, swath, img_num=None):
695
+ """
696
+ Return the RFI annotation file of a channel inside a SAFE product.
697
+
698
+ The file is searched in ``annotation/rfi`` by swath, product type and polarization. Product type ``SL1`` is
699
+ searched as ``SLC``.
700
+
701
+ Parameters
702
+ ----------
703
+ safe_path : str or pathlib.Path
704
+ Path to the SAFE product directory.
705
+ pol : str
706
+ Polarization (e.g. ``VV``, ``VH``).
707
+ swath : str
708
+ Swath identifier (e.g. ``IW1``, ``EW3``, ``WV1``).
709
+ img_num : str, optional
710
+ Image number, used only for wave mode swaths (``WV1``, ``WV2``). Default is ``None``.
711
+
712
+ Returns
713
+ -------
714
+ pathlib.Path
715
+ Path to the first matching RFI annotation file.
716
+
717
+ Raises
718
+ ------
719
+ StopIteration
720
+ If no matching file is found.
721
+ """
722
+ safe_path = Path(safe_path)
723
+ safe_name = safe_path.name
724
+ prod = product_type_from_safe_name(safe_name)
725
+ if prod == 'SL1':
726
+ prod = 'SLC'
727
+
728
+ if (swath in ['WV1', 'WV2']) and (img_num is not None):
729
+ return next(safe_path.glob('annotation/rfi/rfi*-{}-{}-{}-*-{}.xml'.format(swath.lower(), prod[:3].lower(), pol.lower(), img_num)))
730
+ else:
731
+ return next(safe_path.glob('annotation/rfi/rfi*-{}-{}-{}-*.xml'.format(swath.lower(), prod[:3].lower(), pol.lower())))
732
+
733
+
734
+ def annotation_file_from_safe_path(safe_path, pol, swath, img_num=None):
735
+ """
736
+ Return the product annotation file of a channel inside a SAFE product.
737
+
738
+ The file is searched in ``annotation`` by swath, product type and polarization. Product type ``SL1`` is
739
+ searched as ``SLC``.
740
+
741
+ Parameters
742
+ ----------
743
+ safe_path : str or pathlib.Path
744
+ Path to the SAFE product directory.
745
+ pol : str
746
+ Polarization (e.g. ``VV``, ``VH``).
747
+ swath : str
748
+ Swath identifier (e.g. ``IW1``, ``EW3``, ``WV1``).
749
+ img_num : str, optional
750
+ Image number, used only for wave mode swaths (``WV1``, ``WV2``). Default is ``None``.
751
+
752
+ Returns
753
+ -------
754
+ pathlib.Path
755
+ Path to the first matching product annotation file.
756
+
757
+ Raises
758
+ ------
759
+ StopIteration
760
+ If no matching file is found.
761
+ """
762
+ safe_path = Path(safe_path)
763
+ safe_name = safe_path.name
764
+ prod = product_type_from_safe_name(safe_name)
765
+ if prod == 'SL1':
766
+ prod = 'SLC'
767
+
768
+ if (swath in ['WV1', 'WV2']) and (img_num is not None):
769
+ return next(safe_path.glob('annotation/*-{}-{}-{}*{}.xml'.format(swath.lower(), prod[:3].lower(), pol.lower(), img_num)))
770
+ else:
771
+ return next(safe_path.glob('annotation/*-{}-{}-{}-*.xml'.format(swath.lower(), prod[:3].lower(), pol.lower())))
772
+
773
+
774
+ def string_to_datetime(safe_datetime):
775
+ """
776
+ Convert a SAFE datetime string to a datetime object.
777
+
778
+ Parameters
779
+ ----------
780
+ safe_datetime : str
781
+ Datetime string in the format ``YYYYMMDDTHHMMSS`` (e.g. ``'20211112T230710'``).
782
+
783
+ Returns
784
+ -------
785
+ datetime.datetime
786
+ The corresponding naive datetime.
787
+ """
788
+ dt = datetime(year=int(safe_datetime[:4]),
789
+ month=int(safe_datetime[4:6]),
790
+ day=int(safe_datetime[6:8]),
791
+ hour=int(safe_datetime[9:11]),
792
+ minute=int(safe_datetime[11:13]),
793
+ second=int(safe_datetime[13:15]))
794
+ return dt
795
+
796
+
797
+ def datetime_to_string(dt):
798
+ """
799
+ Convert a datetime object to a SAFE datetime string.
800
+
801
+ Parameters
802
+ ----------
803
+ dt : datetime.datetime
804
+ Datetime to convert.
805
+
806
+ Returns
807
+ -------
808
+ str
809
+ String ``YYYYMMDDTHHMMSS`` built from the datetime fields.
810
+
811
+ Notes
812
+ -----
813
+ Fields are not zero-padded, so the result matches the SAFE format only when month, day, hour, minute and
814
+ second have two digits.
815
+ """
816
+ safe_datetime_str = '{}{}{}T{}{}{}'.format(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second)
817
+ return safe_datetime_str
818
+
819
+
820
+ def time_from_safe_name(safe_name):
821
+ """
822
+ Return the sensing start and stop times from a SAFE product name.
823
+
824
+ Parameters
825
+ ----------
826
+ safe_name : str
827
+ SAFE product name.
828
+
829
+ Returns
830
+ -------
831
+ time_start : datetime.datetime
832
+ Sensing start time.
833
+ time_stop : datetime.datetime
834
+ Sensing stop time.
835
+ """
836
+ time_start = string_to_datetime(start_time_str_from_safe_name(safe_name))
837
+ time_stop = string_to_datetime(stop_time_str_from_safe_name(safe_name))
838
+ return time_start, time_stop
839
+
840
+
841
+ def start_time_str_from_orbit_name(orbit_name):
842
+ """
843
+ Return the validity start time string from an orbit file name.
844
+
845
+ Parameters
846
+ ----------
847
+ orbit_name : str
848
+ Orbit file name (e.g. ``S1A_OPER_AUX_POEORB_OPOD_<creation>_V<start>_<stop>.EOF``).
849
+
850
+ Returns
851
+ -------
852
+ str
853
+ Validity start time in the format ``YYYYMMDDTHHMMSS``.
854
+ """
855
+ return orbit_name[42:57]
856
+
857
+
858
+ def stop_time_str_from_orbit_name(orbit_name):
859
+ """
860
+ Return the validity stop time string from an orbit file name.
861
+
862
+ Parameters
863
+ ----------
864
+ orbit_name : str
865
+ Orbit file name (e.g. ``S1A_OPER_AUX_POEORB_OPOD_<creation>_V<start>_<stop>.EOF``).
866
+
867
+ Returns
868
+ -------
869
+ str
870
+ Validity stop time in the format ``YYYYMMDDTHHMMSS``.
871
+ """
872
+ return orbit_name[58:73]
873
+
874
+
875
+ def get_time_from_orbit_name(orbit_name):
876
+ """
877
+ Return the validity start and stop times from an orbit file name.
878
+
879
+ Parameters
880
+ ----------
881
+ orbit_name : str
882
+ Orbit file name.
883
+
884
+ Returns
885
+ -------
886
+ time_start : datetime.datetime
887
+ Validity start time.
888
+ time_stop : datetime.datetime
889
+ Validity stop time.
890
+ """
891
+ time_start = string_to_datetime(start_time_str_from_orbit_name(orbit_name))
892
+ time_stop = string_to_datetime(stop_time_str_from_orbit_name(orbit_name))
893
+ return time_start, time_stop
894
+
895
+
896
+ class S1Safe:
897
+ """
898
+ Sentinel-1 SAFE product reader.
899
+
900
+ Parses the manifest and builds one ``S1Channel`` per polarization and swath (or per wave mode image) from the
901
+ annotation, noise, calibration, RFI and measurement files of the product.
902
+
903
+ Parameters
904
+ ----------
905
+ safe_path : str or pathlib.Path
906
+ Path to the SAFE product directory.
907
+ img_num : str, optional
908
+ Unused. Default is ``None``.
909
+
910
+ Attributes
911
+ ----------
912
+ path : pathlib.Path
913
+ Path to the SAFE product directory.
914
+ name : str
915
+ SAFE product name.
916
+ manifest : xml.etree.ElementTree.ElementTree
917
+ Parsed ``manifest.safe``.
918
+ mode : str
919
+ Acquisition mode (e.g. ``IW``, ``EW``).
920
+ product_type : str
921
+ Product type (e.g. ``SLC``, ``GRDH``).
922
+ ads_header : list
923
+ Placeholder for the ADS header, currently always empty.
924
+
925
+ Raises
926
+ ------
927
+ ValueError
928
+ If ``safe_path`` does not exist.
929
+ StopIteration
930
+ If an annotation, noise, calibration or RFI file of a channel is missing.
931
+ """
932
+ def __init__(self, safe_path, img_num=None):
933
+ self.path = Path(safe_path)
934
+ if not self.path.exists():
935
+ raise ValueError('Path does not exist: {}'.format(safe_path))
936
+ self.name = self.path.name
937
+ self.manifest = ET.parse(self.path / 'manifest.safe')
938
+ self.mode = mode_from_safe_name(self.name)
939
+ self.product_type = product_type_from_safe_name(self.name)
940
+ self._channels = self._build_channels_dict()
941
+ self.ads_header = self.get_ads_header()
942
+
943
+ def _build_channels_dict(self):
944
+ """
945
+ Build the channels of the product.
946
+
947
+ GRD products have a single swath named after the mode. Wave mode channels are discovered from the measurement
948
+ files. A missing measurement file (annotation-only product) results in a channel without measurement.
949
+
950
+ Returns
951
+ -------
952
+ dict
953
+ Mapping from ``(pol, swath_id)``, or ``(pol, swath_id, img_num)`` for wave mode, to ``S1Channel``.
954
+ """
955
+ channels = dict()
956
+ pol_list = polarizations_from_safe_name(self.name)
957
+ if self.is_grd():
958
+ swath_id_list = [self.mode]
959
+ else:
960
+ swath_id_list = subswaths_from_mode(self.mode)
961
+
962
+
963
+ if self.mode == 'WV':
964
+ for filename in os.listdir(os.path.join(self.path, 'measurement')):
965
+ swath_id = filename.split('-')[1].upper()
966
+ pol = filename.split('-')[3].upper()
967
+ img_num = (filename.split('.')[0]).split('-')[8]
968
+
969
+ annot_file = annotation_file_from_safe_path(self.path, pol, swath_id, img_num)
970
+ noise_file = noise_file_from_safe_path(self.path, pol, swath_id, img_num)
971
+ cal_file = calibration_file_from_safe_path(self.path, pol, swath_id, img_num)
972
+ rfi_file = rfi_file_from_safe_path(self.path, pol, swath_id, img_num)
973
+
974
+ try:
975
+ meas_file = measurement_file_from_safe_path(self.path, pol, swath_id, img_num)
976
+ except StopIteration:
977
+ #In case of L1 Annotation product
978
+ meas_file = None
979
+ channels[(pol, swath_id, img_num)] = S1Channel(pol, swath_id, annot_file, noise_file, cal_file, rfi_file, meas_file)
980
+
981
+ else:
982
+ for pol in pol_list:
983
+ for swath_id in swath_id_list:
984
+ annot_file = annotation_file_from_safe_path(self.path, pol, swath_id)
985
+ noise_file = noise_file_from_safe_path(self.path, pol, swath_id)
986
+ cal_file = calibration_file_from_safe_path(self.path, pol, swath_id)
987
+ rfi_file = rfi_file_from_safe_path(self.path, pol, swath_id)
988
+ try:
989
+ meas_file = measurement_file_from_safe_path(self.path, pol, swath_id)
990
+ except StopIteration:
991
+ #In case of L1 Annotation product
992
+ meas_file = None
993
+ channels[ch_idx(pol, swath_id)] = S1Channel(pol, swath_id, annot_file, noise_file, cal_file, rfi_file, meas_file)
994
+
995
+ return channels
996
+
997
+ def get_ads_header(self):
998
+ """
999
+ Return the ADS header of the product.
1000
+
1001
+ Returns
1002
+ -------
1003
+ list
1004
+ Always an empty list (placeholder).
1005
+ """
1006
+ ads_header = []
1007
+ return ads_header
1008
+
1009
+ def is_grd(self):
1010
+ """
1011
+ Check whether the product is a GRD product.
1012
+
1013
+ Returns
1014
+ -------
1015
+ bool
1016
+ ``True`` if the product is GRD.
1017
+ """
1018
+ return is_grd(self.name)
1019
+
1020
+ def is_topsar(self):
1021
+ """
1022
+ Check whether the product was acquired in a TOPSAR mode (IW or EW).
1023
+
1024
+ Returns
1025
+ -------
1026
+ bool
1027
+ ``True`` if the product is IW or EW.
1028
+ """
1029
+ return is_topsar(self.name)
1030
+
1031
+ def is_stripmap(self):
1032
+ """
1033
+ Check whether the product was acquired in a stripmap mode.
1034
+
1035
+ Returns
1036
+ -------
1037
+ bool
1038
+ ``True`` if the product is a stripmap product.
1039
+ """
1040
+ return is_stripmap(self.name)
1041
+
1042
+ def is_wave(self):
1043
+ """
1044
+ Check whether the product was acquired in wave mode.
1045
+
1046
+ Returns
1047
+ -------
1048
+ bool or None
1049
+ ``True`` if the mode is ``WV``, ``None`` otherwise.
1050
+ """
1051
+ return is_wave(self.name)
1052
+
1053
+ def channel(self, pol, swath_id, img_num=None):
1054
+ """
1055
+ Return the channel of a given polarization and swath.
1056
+
1057
+ Parameters
1058
+ ----------
1059
+ pol : str
1060
+ Polarization (e.g. ``VV``).
1061
+ swath_id : str
1062
+ Swath identifier (e.g. ``IW1``).
1063
+ img_num : str, optional
1064
+ Image number, used only for wave mode products. Default is ``None``.
1065
+
1066
+ Returns
1067
+ -------
1068
+ S1Channel
1069
+ The requested channel.
1070
+
1071
+ Raises
1072
+ ------
1073
+ KeyError
1074
+ If the channel does not exist in the product.
1075
+ """
1076
+
1077
+ if self.mode == 'WV' and (img_num is not None):
1078
+ return self._channels[(pol, swath_id, img_num)]
1079
+ else:
1080
+ return self._channels[ch_idx(pol, swath_id)]
1081
+
1082
+ def polarization_list(self):
1083
+ """
1084
+ Return the polarizations available in the product.
1085
+
1086
+ Returns
1087
+ -------
1088
+ list of str
1089
+ Unique polarizations, in channel order.
1090
+ """
1091
+ pol_list = [k[0] for k in self._channels.keys()]
1092
+ return list(dict.fromkeys(pol_list))
1093
+
1094
+ def swath_id_list(self):
1095
+ """
1096
+ Return the swath identifiers available in the product.
1097
+
1098
+ Returns
1099
+ -------
1100
+ list of str
1101
+ Unique swath identifiers, in channel order.
1102
+ """
1103
+ pol_list = [k[1] for k in self._channels.keys()]
1104
+ return list(dict.fromkeys(pol_list))
1105
+
1106
+ def read_quicklook(self):
1107
+ """
1108
+ Read the quick-look image of the product.
1109
+
1110
+ Returns
1111
+ -------
1112
+ numpy.ndarray or None
1113
+ Content of ``preview/quick-look.png``, or ``None`` if the file does not exist.
1114
+ """
1115
+ ql_path = self.path / 'preview/quick-look.png'
1116
+ if not ql_path.exists():
1117
+ return None
1118
+ ql_image = Image.open(ql_path)
1119
+ return np.asarray(ql_image)
1120
+
1121
+ class S1Channel:
1122
+ """
1123
+ Single channel (polarization and swath) of a Sentinel-1 SAFE product.
1124
+
1125
+ Parameters
1126
+ ----------
1127
+ pol : str
1128
+ Polarization (e.g. ``VV``).
1129
+ swath_id : str
1130
+ Swath identifier (e.g. ``IW1``).
1131
+ annnot_file : str or pathlib.Path
1132
+ Product annotation XML file.
1133
+ noise_file : str or pathlib.Path
1134
+ Noise annotation XML file.
1135
+ calibration_file : str or pathlib.Path
1136
+ Calibration annotation XML file.
1137
+ rfi_file : str or pathlib.Path
1138
+ RFI annotation XML file.
1139
+ meas_file : str or pathlib.Path, optional
1140
+ Measurement TIFF file. Default is ``None`` (annotation-only product).
1141
+
1142
+ Attributes
1143
+ ----------
1144
+ pol : str
1145
+ Polarization.
1146
+ swath_id : str
1147
+ Swath identifier.
1148
+ annotation : xml.etree.ElementTree.ElementTree
1149
+ Parsed product annotation.
1150
+ noise : xml.etree.ElementTree.ElementTree
1151
+ Parsed noise annotation.
1152
+ calibration : xml.etree.ElementTree.ElementTree
1153
+ Parsed calibration annotation.
1154
+ rfi : xml.etree.ElementTree.ElementTree
1155
+ Parsed RFI annotation.
1156
+ measurement : pathlib.Path or None
1157
+ Measurement TIFF file, or ``None`` if not available.
1158
+ """
1159
+ def __init__(self, pol, swath_id, annnot_file, noise_file, calibration_file, rfi_file, meas_file=None):
1160
+ self.swath_id = swath_id
1161
+ self.pol = pol
1162
+ self.annotation = ET.parse(annnot_file)
1163
+ self.noise = ET.parse(noise_file)
1164
+ self.calibration = ET.parse(calibration_file)
1165
+ self.rfi = ET.parse(rfi_file)
1166
+ if meas_file is not None:
1167
+ meas_file = Path(meas_file)
1168
+ if not meas_file.exists():
1169
+ ValueError('File does not exist: {}'.format(meas_file))
1170
+ self.measurement = meas_file
1171
+ else:
1172
+ self.measurement = None
1173
+
1174
+ def size(self):
1175
+ """
1176
+ Return the image size from the product annotation.
1177
+
1178
+ Returns
1179
+ -------
1180
+ num_azi : int
1181
+ Number of azimuth lines.
1182
+ num_rng : int
1183
+ Number of range samples.
1184
+ """
1185
+ num_rng = int(self.annotation.find('imageAnnotation/imageInformation/numberOfSamples').text)
1186
+ num_azi = int(self.annotation.find('imageAnnotation/imageInformation/numberOfLines').text)
1187
+ return num_azi, num_rng
1188
+
1189
+ @property
1190
+ def num_bursts(self):
1191
+ """Number of bursts listed in ``swathTiming/burstList`` of the product annotation."""
1192
+ return len(self.annotation.find('swathTiming/burstList'))
1193
+
1194
+ def roi_from_burst(self, burst_idx):
1195
+ """
1196
+ Return the region of interest covering a burst.
1197
+
1198
+ Parameters
1199
+ ----------
1200
+ burst_idx : int
1201
+ Zero-based burst index.
1202
+
1203
+ Returns
1204
+ -------
1205
+ S1Roi
1206
+ ROI spanning ``linesPerBurst`` lines from ``burst_idx * linesPerBurst`` and ``samplesPerBurst`` samples
1207
+ from range sample 0.
1208
+
1209
+ Raises
1210
+ ------
1211
+ ValueError
1212
+ If ``burst_idx`` is not lower than the number of bursts.
1213
+ """
1214
+ num_bursts = self.num_bursts
1215
+ if burst_idx >= num_bursts:
1216
+ raise ValueError('Invalid burst index: {}. Number of bursts is {}'.format(burst_idx, num_bursts))
1217
+ lines_per_burst = int(self.annotation.find('swathTiming/linesPerBurst').text)
1218
+ samples_per_burst = int(self.annotation.find('swathTiming/samplesPerBurst').text)
1219
+ return S1Roi(range_start_idx=0,
1220
+ azi_start_idx=burst_idx * lines_per_burst,
1221
+ num_range_samples=samples_per_burst,
1222
+ num_azi_lines=lines_per_burst)
1223
+
1224
+ def read_swath_bounds(self):
1225
+ """
1226
+ Read the swath merging bounds from the product annotation (GRD products).
1227
+
1228
+ Returns
1229
+ -------
1230
+ dict of str to list of dict
1231
+ Mapping from swath identifier to its list of bounds, each with keys ``firstAzimuthLine``,
1232
+ ``lastAzimuthLine``, ``firstRangeSample`` and ``lastRangeSample`` (int).
1233
+ """
1234
+ swath_merge_dict = dict()
1235
+ for swathMerge in self.annotation.findall('swathMerging/swathMergeList/swathMerge'):
1236
+ swath_id = swathMerge.find('swath').text
1237
+ swath_merge_dict[swath_id] = list()
1238
+ for swathBound in swathMerge.findall('swathBoundsList/swathBounds'):
1239
+ swath_bounds_dict = dict()
1240
+ swath_bounds_dict['firstAzimuthLine'] = int(swathBound.findall('firstAzimuthLine')[0].text)
1241
+ swath_bounds_dict['lastAzimuthLine'] = int(swathBound.findall('lastAzimuthLine')[0].text)
1242
+ swath_bounds_dict['firstRangeSample'] = int(swathBound.findall('firstRangeSample')[0].text)
1243
+ swath_bounds_dict['lastRangeSample'] = int(swathBound.findall('lastRangeSample')[0].text)
1244
+ swath_merge_dict[swath_id].append(swath_bounds_dict)
1245
+ return swath_merge_dict
1246
+
1247
+ def read_range_noise_vectors(self):
1248
+ """
1249
+ Read the range noise vectors of the channel.
1250
+
1251
+ Returns
1252
+ -------
1253
+ list of dict
1254
+ Range noise vectors, see ``read_range_noise_vectors``.
1255
+ """
1256
+ return read_range_noise_vectors(self.noise)
1257
+
1258
+ def read_azimuth_noise_vectors(self):
1259
+ """
1260
+ Read the azimuth noise vectors of the channel, grouped by swath.
1261
+
1262
+ Returns
1263
+ -------
1264
+ dict of str to list of dict
1265
+ Azimuth noise vectors, see ``read_azimuth_noise_vectors``.
1266
+ """
1267
+ return read_azimuth_noise_vectors(self.noise)
1268
+
1269
+ def read_calibration_vectors(self, calibration):
1270
+ """
1271
+ Read the calibration vectors of a given type for the channel.
1272
+
1273
+ Parameters
1274
+ ----------
1275
+ calibration : str
1276
+ Calibration type, one of ``dn``, ``betaNought``, ``gamma``, ``sigmaNought``.
1277
+
1278
+ Returns
1279
+ -------
1280
+ list of dict
1281
+ Calibration vectors, see ``read_calibration_vectors``.
1282
+ """
1283
+ return read_calibration_vectors(self.calibration, calibration)
1284
+
1285
+ def calibrate(self, data, roi, calibration=None):
1286
+ """
1287
+ Apply radiometric calibration to channel data over a region of interest.
1288
+
1289
+ Parameters
1290
+ ----------
1291
+ data : numpy.ndarray
1292
+ Data over the ROI, with shape ``(roi.num_azi_lines, roi.num_range_samples)``.
1293
+ roi : S1Roi
1294
+ Region of interest covered by ``data``.
1295
+ calibration : str, optional
1296
+ Calibration type, one of ``dn``, ``betaNought``, ``gamma``, ``sigmaNought``. Default is ``None`` (no
1297
+ calibration).
1298
+
1299
+ Returns
1300
+ -------
1301
+ numpy.ndarray
1302
+ Calibrated data in linear power units, or ``data`` unchanged if ``calibration`` is ``None``.
1303
+ """
1304
+ return calibrate(self.annotation, self.calibration, data, roi, calibration)
1305
+
1306
+ def read_data(self, roi=None, calibration=None):
1307
+ """
1308
+ Read measurement data over a region of interest, optionally calibrated.
1309
+
1310
+ Parameters
1311
+ ----------
1312
+ roi : S1Roi, optional
1313
+ Region of interest. Default is ``None`` (whole image).
1314
+ calibration : str, optional
1315
+ Calibration type, one of ``dn``, ``betaNought``, ``gamma``, ``sigmaNought``. Default is ``None`` (no
1316
+ calibration).
1317
+
1318
+ Returns
1319
+ -------
1320
+ numpy.ndarray
1321
+ Data of the first raster band over the ROI (integer data are converted to float), or calibrated power if
1322
+ ``calibration`` is given.
1323
+ """
1324
+ with rasterio.open(self.measurement) as dataset:
1325
+ if roi is None:
1326
+ roi = S1Roi(0, 0, dataset.width, dataset.height)
1327
+ data = dataset.read(1, window=roi.as_window())
1328
+ if issubclass(data.dtype.type, np.integer):
1329
+ data = data.astype(float)
1330
+ return self.calibrate(data, roi, calibration)
1331
+
1332
+ def read_noise(self, roi, calibration=None):
1333
+ """
1334
+ Compute the annotated noise over a region of interest, optionally calibrated.
1335
+
1336
+ Parameters
1337
+ ----------
1338
+ roi : S1Roi or None
1339
+ Region of interest; ``None`` means the whole image.
1340
+ calibration : str, optional
1341
+ Calibration type, one of ``dn``, ``betaNought``, ``gamma``, ``sigmaNought``. Default is ``None`` (no
1342
+ calibration).
1343
+
1344
+ Returns
1345
+ -------
1346
+ numpy.ndarray
1347
+ Noise amplitude map (see ``compute_noise_map``), or calibrated noise power if ``calibration`` is given.
1348
+ """
1349
+ data_size = self.size()
1350
+ if roi is None:
1351
+ roi = S1Roi(0, 0, data_size[1], data_size[0])
1352
+ noise = compute_noise_map(self.noise, data_size, roi=roi.as_tuple())
1353
+ return self.calibrate(noise, roi, calibration)
1354
+
1355
+ class S1Roi:
1356
+ """
1357
+ Rectangular region of interest in image coordinates.
1358
+
1359
+ Parameters
1360
+ ----------
1361
+ range_start_idx : int
1362
+ First range sample.
1363
+ azi_start_idx : int
1364
+ First azimuth line.
1365
+ num_range_samples : int
1366
+ Number of range samples.
1367
+ num_azi_lines : int
1368
+ Number of azimuth lines.
1369
+
1370
+ Attributes
1371
+ ----------
1372
+ range_start_idx : int
1373
+ First range sample.
1374
+ azi_start_idx : int
1375
+ First azimuth line.
1376
+ num_range_samples : int
1377
+ Number of range samples.
1378
+ num_azi_lines : int
1379
+ Number of azimuth lines.
1380
+ """
1381
+ def __init__(self, range_start_idx, azi_start_idx, num_range_samples, num_azi_lines):
1382
+ self.range_start_idx = range_start_idx
1383
+ self.azi_start_idx = azi_start_idx
1384
+ self.num_range_samples = num_range_samples
1385
+ self.num_azi_lines = num_azi_lines
1386
+
1387
+ def __repr__(self):
1388
+ """Return a string listing the ROI fields."""
1389
+ return 'range_start_idx:{} azi_start_idx:{} num_range_samples:{} num_azi_lines:{}'.format(self.range_start_idx,
1390
+ self.azi_start_idx,
1391
+ self.num_range_samples,
1392
+ self.num_azi_lines)
1393
+
1394
+ def as_tuple(self):
1395
+ """
1396
+ Return the ROI as a tuple.
1397
+
1398
+ Returns
1399
+ -------
1400
+ tuple of int
1401
+ ``(range_start_idx, azi_start_idx, num_range_samples, num_azi_lines)``.
1402
+ """
1403
+ return self.range_start_idx, self.azi_start_idx, self.num_range_samples, self.num_azi_lines
1404
+
1405
+ def as_window(self):
1406
+ """
1407
+ Return the ROI as a rasterio window.
1408
+
1409
+ Returns
1410
+ -------
1411
+ rasterio.windows.Window
1412
+ Window with column offset, row offset, width and height taken from ``as_tuple``.
1413
+ """
1414
+ return Window(*self.as_tuple())
1415
+