• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

johntruckenbrodt / pyroSAR / 16620069336

29 Jul 2025 11:31AM UTC coverage: 51.339% (-0.03%) from 51.368%
16620069336

push

github

johntruckenbrodt
[drivers.ESA.scanMetadata] call function with <key>=<value> declaration

3718 of 7242 relevant lines covered (51.34%)

0.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

42.31
/pyroSAR/S1/auxil.py
1
###############################################################################
2
# general utilities for Sentinel-1
3

4
# Copyright (c) 2016-2025, the pyroSAR Developers.
5

6
# This file is part of the pyroSAR Project. It is subject to the
7
# license terms in the LICENSE.txt file found in the top-level
8
# directory of this distribution and at
9
# https://github.com/johntruckenbrodt/pyroSAR/blob/master/LICENSE.txt.
10
# No part of the pyroSAR project, including this file, may be
11
# copied, modified, propagated, or distributed except according
12
# to the terms contained in the LICENSE.txt file.
13
###############################################################################
14

15
import os
1✔
16
import re
1✔
17
import sys
1✔
18
import requests
1✔
19
import tempfile
1✔
20
import zipfile as zf
1✔
21
from io import BytesIO
1✔
22
from datetime import datetime, timedelta
1✔
23
from dateutil import parser as dateutil_parser
1✔
24
from dateutil.relativedelta import relativedelta
1✔
25
import xml.etree.ElementTree as ET
1✔
26
import numpy as np
1✔
27
from osgeo import gdal
1✔
28
from osgeo.gdalconst import GA_Update
1✔
29
from . import linesimplify as ls
1✔
30
from pyroSAR.examine import ExamineSnap
1✔
31
import progressbar as pb
1✔
32

33
from spatialist.ancillary import finder
1✔
34

35
import logging
1✔
36

37
log = logging.getLogger(__name__)
1✔
38

39
try:
1✔
40
    import argparse
1✔
41
except ImportError:
×
42
    try:
×
43
        os.remove(os.path.join(os.path.dirname(sys.argv[0]), 'locale.pyc'))
×
44
    finally:
45
        import argparse
×
46

47

48
def init_parser():
1✔
49
    """
50
    initialize argument parser for S1 processing utilities
51
    """
52
    parser = argparse.ArgumentParser()
×
53
    parser.add_argument('-t', '--transform', action='store_true', help='transform the final DEM to UTM coordinates')
×
54
    parser.add_argument('-l', '--logfiles', action='store_true', help='create logfiles of the executed GAMMA commands')
×
55
    parser.add_argument('-i', '--intermediates', action='store_true', help='keep intermediate files')
×
56
    parser.add_argument('-q', '--quiet', action='store_true', help='suppress standard console prints')
×
57
    parser.add_argument('-tr', '--targetresolution', default=20, help='the target resolution in meters for x and y',
×
58
                        type=int)
59
    parser.add_argument('-fg', '--func_geoback', default=2, help='backward geocoding interpolation function; '
×
60
                                                                 '0 - Nearest Neighbor, 1 - Bicubic Spline, 2 - Bicubic Spline-Log; '
61
                                                                 'method 1: negative values possible (e.g. in urban areas) - use method 2 to avoid this',
62
                        type=int)
63
    parser.add_argument('-fi', '--func_interp', default=0,
×
64
                        help='function for interpolation of layover/shadow/foreshortening/DEM gaps; '
65
                             '0 - set to 0, 1 - linear interpolation, 2 - actual value, 3 - nn-thinned', type=int)
66
    parser.add_argument('-poe', '--poedir', default=None,
×
67
                        help='directory containing aux_poeorb (precise orbit ephemerides) orbit state vector files')
68
    parser.add_argument('-res', '--resdir', default=None,
×
69
                        help='directory containing aux_resorb (restituted orbit) orbit state vector files')
70
    parser.add_argument('zipfile', help='S1 zipped scene archive to be used')
×
71
    parser.add_argument('tempdir', help='temporary directory for intermediate files')
×
72
    parser.add_argument('outdir', help='output directory')
×
73
    parser.add_argument('srtmdir', help='directory containing SRTM hgt tiles (subdirectories possible)')
×
74
    return parser
×
75

76

77
# todo check existence not by file name but by start and stop time; files are sometimes re-published
78
class OSV(object):
1✔
79
    """
80
    interface for management of S1 Orbit State Vector (OSV) files
81

82
    input is a directory which is supposed to contain, or already contains, OSV files.
83
    Two subdirectories are expected and created otherwise:
84
    one for Precise Orbit Ephemerides (POE) named POEORB and one for Restituted Orbit (RES) files named RESORB
85

86
    Using method :meth:`match` the corresponding POE (priority) or RES file is returned for a timestamp.
87
    Timestamps are always handled in the format YYYYmmddTHHMMSS.
88

89
    Parameters
90
    ----------
91
    osvdir: str
92
        the directory to write the orbit files to
93
    timeout: int or tuple or None
94
        the timeout in seconds for downloading OSV files as provided to :func:`requests.get`
95
    
96
    See Also
97
    --------
98
    `requests timeouts <https://requests.readthedocs.io/en/master/user/advanced/#timeouts>`_
99
    """
100
    
101
    def __init__(self, osvdir=None, timeout=300):
1✔
102
        self.timeout = timeout
1✔
103
        if osvdir is None:
1✔
104
            try:
1✔
105
                auxdatapath = ExamineSnap().auxdatapath
1✔
106
            except AttributeError:
×
107
                auxdatapath = os.path.join(os.path.expanduser('~'), '.snap', 'auxdata')
×
108
            osvdir = os.path.join(auxdatapath, 'Orbits', 'Sentinel-1')
1✔
109
        self.outdir_poe = os.path.join(osvdir, 'POEORB')
1✔
110
        self.outdir_res = os.path.join(osvdir, 'RESORB')
1✔
111
        self.pattern = r'S1[ABCD]_OPER_AUX_(?:POE|RES)ORB_OPOD_[0-9TV_]{48}\.EOF'
1✔
112
        self.pattern_fine = r'(?P<sensor>S1[ABCD])_OPER_AUX_' \
1✔
113
                            r'(?P<type>(?:POE|RES)ORB)_OPOD_' \
114
                            r'(?P<publish>[0-9]{8}T[0-9]{6})_V' \
115
                            r'(?P<start>[0-9]{8}T[0-9]{6})_' \
116
                            r'(?P<stop>[0-9]{8}T[0-9]{6})\.EOF'
117
        self._init_dir()
1✔
118
        self._reorganize()
1✔
119
    
120
    def __enter__(self):
1✔
121
        return self
1✔
122
    
123
    def __exit__(self, exc_type, exc_val, exc_tb):
1✔
124
        return
1✔
125
    
126
    def _init_dir(self):
1✔
127
        """
128
        create directories if they don't exist yet
129
        """
130
        for dir in [self.outdir_poe, self.outdir_res]:
1✔
131
            if not os.path.isdir(dir):
1✔
132
                os.makedirs(dir)
1✔
133
    
134
    def _parse(self, file):
1✔
135
        basename = os.path.basename(file)
1✔
136
        groups = re.match(self.pattern_fine, basename).groupdict()
1✔
137
        return groups
1✔
138
    
139
    def _reorganize(self):
1✔
140
        """
141
        compress and move EOF files into subdirectories
142

143
        Returns
144
        -------
145

146
        """
147
        message = True
1✔
148
        for subdir in [self.outdir_poe, self.outdir_res]:
1✔
149
            if not os.path.isdir(subdir):
1✔
150
                continue
×
151
            files = finder(subdir, [self.pattern], recursive=False, regex=True)
1✔
152
            for eof in files:
1✔
153
                base = os.path.basename(eof)
×
154
                target = os.path.join(self._subdir(eof), base + '.zip')
×
155
                os.makedirs(os.path.dirname(target), exist_ok=True)
×
156
                if not os.path.isfile(target):
×
157
                    if message:
×
158
                        log.info('compressing and reorganizing EOF files')
×
159
                        message = False
×
160
                    with zf.ZipFile(file=target,
×
161
                                    mode='w',
162
                                    compression=zf.ZIP_DEFLATED) as zip:
163
                        zip.write(filename=eof,
×
164
                                  arcname=base)
165
                os.remove(eof)
×
166
    
167
    def _typeEvaluate(self, osvtype):
1✔
168
        """
169
        evaluate the 'osvtype' method argument and return the corresponding remote repository and local directory
170

171
        Parameters
172
        ----------
173
        osvtype: str
174
            the type of orbit files required; either 'POE' or 'RES'
175

176
        Returns
177
        -------
178
        tuple of str
179
            the remote repository and local directory of the osv type
180
        """
181
        if osvtype not in ['POE', 'RES']:
1✔
182
            raise IOError('type must be either "POE" or "RES"')
×
183
        if osvtype == 'POE':
1✔
184
            return self.outdir_poe
1✔
185
        else:
186
            return self.outdir_res
1✔
187
    
188
    def __catch_aux_sentinel(self, sensor, start, stop, osvtype='POE'):
1✔
189
        url = 'http://aux.sentinel1.eo.esa.int'
×
190
        skeleton = '{url}/{osvtype}ORB/{year}/{month:02d}/{day:02d}/'
×
191
        
192
        files = []
×
193
        date_search = start
×
194
        busy = True
×
195
        while busy:
×
196
            url_sub = skeleton.format(url=url,
×
197
                                      osvtype=osvtype,
198
                                      year=date_search.year,
199
                                      month=date_search.month,
200
                                      day=date_search.day)
201
            response = requests.get(url_sub, timeout=self.timeout)
×
202
            response.raise_for_status()
×
203
            result = response.text
×
204
            files_sub = list(set(re.findall(self.pattern, result)))
×
205
            if len(files_sub) == 0:
×
206
                break
×
207
            for file in files_sub:
×
208
                match = re.match(self.pattern_fine, file)
×
209
                start2 = datetime.strptime(match.group('start'), '%Y%m%dT%H%M%S')
×
210
                stop2 = datetime.strptime(match.group('stop'), '%Y%m%dT%H%M%S')
×
211
                if sensor == match.group('sensor'):
×
212
                    if start2 < stop and stop2 > start:
×
213
                        log.info(url_sub)
×
214
                        files.append({'filename': file,
×
215
                                      'href': url_sub + '/' + file,
216
                                      'auth': None})
217
                if start2 >= stop:
×
218
                    busy = False
×
219
            date_search += timedelta(days=1)
×
220
        
221
        return files
×
222
    
223
    def __catch_step_auxdata(self, sensor, start, stop, osvtype='POE'):
1✔
224
        url = 'https://step.esa.int/auxdata/orbits/Sentinel-1'
1✔
225
        skeleton = '{url}/{osvtype}ORB/{sensor}/{year}/{month:02d}/'
1✔
226
        
227
        if osvtype not in ['POE', 'RES']:
1✔
228
            raise RuntimeError("osvtype must be either 'POE' or 'RES'")
1✔
229
        
230
        if isinstance(sensor, str):
1✔
231
            sensor = [sensor]
1✔
232
        
233
        files = []
1✔
234
        date_search_final = datetime(year=stop.year, month=stop.month, day=1)
1✔
235
        for sens in sensor:
1✔
236
            date_search = datetime(year=start.year,
1✔
237
                                   month=start.month,
238
                                   day=1)
239
            date_search -= relativedelta(months=1)
1✔
240
            busy = True
1✔
241
            while busy:
1✔
242
                url_sub = skeleton.format(url=url,
1✔
243
                                          osvtype=osvtype,
244
                                          sensor=sens,
245
                                          year=date_search.year,
246
                                          month=date_search.month)
247
                log.info(url_sub)
1✔
248
                response = requests.get(url_sub, timeout=self.timeout)
1✔
249
                if response.status_code != 404:
1✔
250
                    response.raise_for_status()
1✔
251
                    result = response.text
1✔
252
                    files_sub = list(set(re.findall(self.pattern, result)))
1✔
253
                    if len(files_sub) == 0:
1✔
254
                        break
×
255
                    for file in files_sub:
1✔
256
                        match = re.match(self.pattern_fine, file)
1✔
257
                        start2 = datetime.strptime(match.group('start'), '%Y%m%dT%H%M%S')
1✔
258
                        stop2 = datetime.strptime(match.group('stop'), '%Y%m%dT%H%M%S')
1✔
259
                        if start2 < stop and stop2 > start:
1✔
260
                            files.append({'filename': file,
1✔
261
                                          'href': url_sub + '/' + file + '.zip',
262
                                          'auth': None})
263
                if date_search == date_search_final:
1✔
264
                    busy = False
1✔
265
                date_search += relativedelta(months=1)
1✔
266
                if date_search > datetime.now():
1✔
267
                    busy = False
1✔
268
        return files
1✔
269
    
270
    def __catch_gnss(self, sensor, start, stop, osvtype='POE'):
1✔
271
        url = 'https://scihub.copernicus.eu/gnss'
×
272
        redirect = 'https://dhusfeed.dhus.onda-dias.net/gnss'
×
273
        auth = ('gnssguest', 'gnssguest')
×
274
        # a dictionary for storing the url arguments
275
        query = {}
×
276
        
277
        if osvtype == 'POE':
×
278
            query['producttype'] = 'AUX_POEORB'
×
279
        elif osvtype == 'RES':
×
280
            query['producttype'] = 'AUX_RESORB'
×
281
        else:
282
            raise RuntimeError("osvtype must be either 'POE' or 'RES'")
×
283
        
284
        if sensor in ['S1A', 'S1B', 'S1C', 'S1D']:
×
285
            query['platformname'] = 'Sentinel-1'
×
286
            # filename starts w/ sensor
287
            query['filename'] = '{}*'.format(sensor)
×
288
        elif sorted(sensor) == ['S1A', 'S1B', 'S1C', 'S1D']:
×
289
            query['platformname'] = 'Sentinel-1'
×
290
        else:
291
            raise RuntimeError('unsupported input for parameter sensor')
×
292
        
293
        # the collection of files to be returned
294
        collection = []
×
295
        
296
        date_start = start.strftime('%Y-%m-%dT%H:%M:%SZ')
×
297
        date_stop = stop.strftime('%Y-%m-%dT%H:%M:%SZ')
×
298
        
299
        # append the time frame to the query dictionary
300
        query['beginPosition'] = '[{} TO {}]'.format(date_start, date_stop)
×
301
        query['endPosition'] = '[{} TO {}]'.format(date_start, date_stop)
×
302
        query_list = []
×
303
        for keyword, value in query.items():
×
304
            query_elem = '{}:{}'.format(keyword, value)
×
305
            query_list.append(query_elem)
×
306
        query_str = ' '.join(query_list)
×
307
        target = '{}/search?q={}&format=json'.format(url, query_str)
×
308
        log.info(target)
×
309
        
310
        def _parse_gnsssearch_json(search_dict):
×
311
            parsed_dict = {}
×
312
            # Will return ['entry'] as dict if only one item
313
            # If so just make a list
314
            if isinstance(search_dict, dict):
×
315
                search_dict = [search_dict]
×
316
            for entry in search_dict:
×
317
                id = entry['id']
×
318
                entry_dict = {}
×
319
                
320
                for key, value in entry.items():
×
321
                    if key == 'title':
×
322
                        entry_dict[key] = value
×
323
                    elif key == 'id':
×
324
                        entry_dict[key] = value
×
325
                    elif key == 'ondemand':
×
326
                        if value.lower() == 'true':
×
327
                            entry_dict[key] = True
×
328
                        else:
329
                            entry_dict[key] = False
×
330
                    elif key == 'str':
×
331
                        for elem in value:
×
332
                            entry_dict[elem['name']] = elem['content']
×
333
                    elif key == 'link':
×
334
                        for elem in value:
×
335
                            if 'rel' in elem.keys():
×
336
                                href_key = 'href_' + elem['rel']
×
337
                                entry_dict[href_key] = elem['href']
×
338
                            else:
339
                                entry_dict['href'] = elem['href']
×
340
                    elif key == 'date':
×
341
                        for elem in value:
×
342
                            entry_dict[elem['name']] = dateutil_parser.parse(elem['content'])
×
343
                
344
                parsed_dict[id] = entry_dict
×
345
            return parsed_dict
×
346
        
347
        def _parse_gnsssearch_response(response_json):
×
348
            if 'entry' in response_json.keys():
×
349
                search_dict = response_json['entry']
×
350
                parsed_dict = _parse_gnsssearch_json(search_dict)
×
351
            else:
352
                parsed_dict = {}
×
353
            return parsed_dict
×
354
        
355
        response = requests.get(target, auth=auth, timeout=self.timeout)
×
356
        response.raise_for_status()
×
357
        response_json = response.json()['feed']
×
358
        total_results = response_json['opensearch:totalResults']
×
359
        subquery = [link['href'] for link in response_json['link'] if link['rel'] == 'self'][0]
×
360
        subquery = subquery.replace(redirect, url.strip())
×
361
        if int(total_results) > 10:
×
362
            subquery = subquery.replace('rows=10', 'rows=100')
×
363
        while subquery:
×
364
            subquery_response = requests.get(subquery, auth=auth, timeout=self.timeout)
×
365
            subquery_response.raise_for_status()
×
366
            subquery_json = subquery_response.json()['feed']
×
367
            subquery_products = _parse_gnsssearch_response(subquery_json)
×
368
            items = list(subquery_products.values())
×
369
            for item in items:
×
370
                item['auth'] = auth
×
371
            collection += list(subquery_products.values())
×
372
            if 'next' in [link['rel'] for link in subquery_json['link']]:
×
373
                subquery = [link['href'] for link in subquery_json['link'] if link['rel'] == 'next'][0]
×
374
                subquery = subquery.replace(redirect, url.strip())
×
375
            else:
376
                subquery = None
×
377
        if osvtype == 'RES' and self.maxdate('POE', 'stop') is not None:
×
378
            collection = [x for x in collection
×
379
                          if self.date(x['filename'], 'start') > self.maxdate('POE', 'stop')]
380
        for item in collection:
×
381
            item['href'] = item['href'].replace(redirect, url)
×
382
        return collection
×
383
    
384
    def catch(self, sensor, osvtype='POE', start=None, stop=None, url_option=1):
1✔
385
        """
386
        check a server for files
387

388
        Parameters
389
        ----------
390
        sensor: str or list[str]
391
            The S1 mission(s):
392
            
393
             - 'S1A'
394
             - 'S1B'
395
             - 'S1C'
396
             - 'S1D'
397
             - ['S1A', 'S1B', 'S1C', 'S1D']
398
        osvtype: str
399
            the type of orbit files required
400
        start: str or None
401
            the date to start searching for files in format YYYYmmddTHHMMSS
402
        stop: str or None
403
            the date to stop searching for files in format YYYYmmddTHHMMSS
404
        url_option: int
405
            the OSV download URL option
406
            
407
             - 1: https://step.esa.int/auxdata/orbits/Sentinel-1
408

409
        Returns
410
        -------
411
        list[dict]
412
            the product dictionary of the remote OSV files, with href
413
        """
414
        
415
        log.info('searching for new {} files'.format(osvtype))
1✔
416
        
417
        if start is not None:
1✔
418
            start = datetime.strptime(start, '%Y%m%dT%H%M%S')
1✔
419
        else:
420
            start = datetime.strptime('2014-07-31', '%Y-%m-%d')
1✔
421
        # set the defined date or the current date otherwise
422
        if stop is not None:
1✔
423
            stop = datetime.strptime(stop, '%Y%m%dT%H%M%S')
1✔
424
        else:
425
            stop = datetime.now()
1✔
426
        
427
        if url_option == 1:
1✔
428
            items = self.__catch_step_auxdata(sensor, start, stop, osvtype)
1✔
429
        else:
430
            raise ValueError("unknown URL option")
×
431
        
432
        if osvtype == 'RES' and self.maxdate('POE', 'stop') is not None:
1✔
433
            items = [x for x in items
1✔
434
                     if self.date(x['filename'], 'start') > self.maxdate('POE', 'stop')]
435
        log.info('found {} results'.format(len(items)))
1✔
436
        
437
        return items
1✔
438
    
439
    def date(self, file, datetype):
1✔
440
        """
441
        extract a date from an OSV file name
442

443
        Parameters
444
        ----------
445
        file: str
446
            the OSV file
447
        datetype: {'publish', 'start', 'stop'}
448
            one of three possible date types contained in the OSV filename
449

450
        Returns
451
        -------
452
        str
453
            a time stamp in the format YYYYmmddTHHMMSS
454
        """
455
        return self._parse(file)[datetype]
1✔
456
    
457
    def clean_res(self):
1✔
458
        """
459
        delete all RES files for whose date a POE file exists
460
        """
461
        maxdate_poe = self.maxdate('POE', 'stop')
1✔
462
        if maxdate_poe is not None:
1✔
463
            deprecated = [x for x in self.getLocals('RES') if self.date(x, 'stop') < maxdate_poe]
1✔
464
            log.info('deleting {} RES file{}'.format(len(deprecated), '' if len(deprecated) == 1 else 's'))
1✔
465
            for item in deprecated:
1✔
466
                os.remove(item)
×
467
    
468
    def getLocals(self, osvtype='POE'):
1✔
469
        """
470
        get a list of local files of specific type
471

472
        Parameters
473
        ----------
474
        osvtype: {'POE', 'RES'}
475
            the type of orbit files required
476

477
        Returns
478
        -------
479
        list[str]
480
            a selection of local OSV files
481
        """
482
        directory = self._typeEvaluate(osvtype)
1✔
483
        return finder(directory, [self.pattern], regex=True)
1✔
484
    
485
    def maxdate(self, osvtype='POE', datetype='stop'):
1✔
486
        """
487
        return the latest date of locally existing POE/RES files
488

489
        Parameters
490
        ----------
491
        osvtype: {'POE', 'RES'}
492
            the type of orbit files required
493
        datetype: {'publish', 'start', 'stop'}
494
            one of three possible date types contained in the OSV filename
495

496
        Returns
497
        -------
498
        str
499
            a timestamp in format YYYYmmddTHHMMSS
500
        """
501
        directory = self._typeEvaluate(osvtype)
1✔
502
        files = finder(directory, [self.pattern], regex=True)
1✔
503
        return max([self.date(x, datetype) for x in files]) if len(files) > 0 else None
1✔
504
    
505
    def mindate(self, osvtype='POE', datetype='start'):
1✔
506
        """
507
        return the earliest date of locally existing POE/RES files
508

509
        Parameters
510
        ----------
511
        osvtype: {'POE', 'RES'}
512
            the type of orbit files required
513
        datetype: {'publish', 'start', 'stop'}
514
            one of three possible date types contained in the OSV filename
515

516
        Returns
517
        -------
518
        str
519
            a timestamp in format YYYYmmddTHHMMSS
520
        """
521
        directory = self._typeEvaluate(osvtype)
1✔
522
        files = finder(directory, [self.pattern], regex=True)
1✔
523
        return min([self.date(x, datetype) for x in files]) if len(files) > 0 else None
1✔
524
    
525
    def match(self, sensor, timestamp, osvtype='POE'):
1✔
526
        """
527
        return the corresponding OSV file for the provided sensor and time stamp.
528
        The file returned is one which covers the acquisition time and, if multiple exist,
529
        the one which was published last.
530
        In case a list of options is provided as osvtype, the file of higher accuracy (i.e. POE over RES) is returned.
531

532
        Parameters
533
        ----------
534
        sensor: str
535
            The S1 mission:
536
            
537
             - 'S1A'
538
             - 'S1B'
539
        timestamp: str
540
            the time stamp in the format 'YYYmmddTHHMMSS'
541
        osvtype: str or list[str]
542
            the type of orbit files required; either 'POE', 'RES' or a list of both
543

544
        Returns
545
        -------
546
        str
547
            the best matching orbit file (overlapping time plus latest publication date)
548
        """
549
        # list all locally existing files of the defined type
550
        if osvtype in ['POE', 'RES']:
1✔
551
            locals = self.getLocals(osvtype)
1✔
552
            # filter the files to those which contain data for the defined time stamp
553
            files = [x for x in locals if self.date(x, 'start') <= timestamp <= self.date(x, 'stop')]
1✔
554
            files = [x for x in files if os.path.basename(x).startswith(sensor)]
1✔
555
            if len(files) > 0:
1✔
556
                # select the file which was published last
557
                best = self.sortByDate(files, 'publish')[-1]
1✔
558
                return best
1✔
559
            elif len(files) == 1:
1✔
560
                return files[0]
×
561
            return None
1✔
562
        elif sorted(osvtype) == ['POE', 'RES']:
1✔
563
            best = self.match(sensor=sensor, timestamp=timestamp, osvtype='POE')
1✔
564
            if not best:
1✔
565
                best = self.match(sensor=sensor, timestamp=timestamp, osvtype='RES')
×
566
            return best
1✔
567
    
568
    def retrieve(self, products, pbar=False):
1✔
569
        """
570
        download a list of product dictionaries into the respective subdirectories, i.e. POEORB or RESORB
571

572
        Parameters
573
        ----------
574
        products: list[dict]
575
            a list of remotely existing OSV product dictionaries as returned by method :meth:`catch`
576
        pbar: bool
577
            add a progressbar?
578

579
        Returns
580
        -------
581
        """
582
        downloads = []
1✔
583
        for product in products:
1✔
584
            if all(key not in ['filename', 'href'] for key in product.keys()):
1✔
585
                raise RuntimeError("product dictionaries must contain 'filename' and 'href' keys")
×
586
            basename = product['filename']
1✔
587
            remote = product['href']
1✔
588
            auth = product['auth']
1✔
589
            
590
            outdir = self._subdir(basename)
1✔
591
            os.makedirs(outdir, exist_ok=True)
1✔
592
            local = os.path.join(outdir, basename) + '.zip'
1✔
593
            if not os.path.isfile(local):
1✔
594
                downloads.append((remote, local, basename, auth))
1✔
595
        if len(downloads) == 0:
1✔
596
            return
×
597
        log.info('downloading {} file{}'.format(len(downloads), '' if len(downloads) == 1 else 's'))
1✔
598
        if pbar:
1✔
599
            progress = pb.ProgressBar(max_value=len(downloads))
×
600
        else:
601
            progress = None
1✔
602
        i = 0
1✔
603
        for remote, local, basename, auth in downloads:
1✔
604
            response = requests.get(remote, auth=auth, timeout=self.timeout)
1✔
605
            response.raise_for_status()
1✔
606
            infile = response.content
1✔
607
            
608
            # use a tempfile to allow atomic writes in the case of
609
            # parallel executions dependent on the same orbit files
610
            fd, tmp_path = tempfile.mkstemp(prefix=os.path.basename(local), dir=os.path.dirname(local))
1✔
611
            os.close(fd)
1✔
612
            try:
1✔
613
                if remote.endswith('.zip'):
1✔
614
                    with zf.ZipFile(file=BytesIO(infile)) as tmp:
1✔
615
                        members = tmp.namelist()
1✔
616
                        target = [x for x in members if re.search(basename, x)][0]
1✔
617
                        with zf.ZipFile(tmp_path, 'w') as outfile:
1✔
618
                            outfile.writestr(data=tmp.read(target),
1✔
619
                                             zinfo_or_arcname=basename)
620
                else:
621
                    with zf.ZipFile(file=tmp_path,
×
622
                                    mode='w',
623
                                    compression=zf.ZIP_DEFLATED) \
624
                            as outfile:
625
                        outfile.writestr(zinfo_or_arcname=basename,
×
626
                                         data=infile)
627
                os.rename(tmp_path, local)
1✔
628
            except Exception as e:
×
629
                os.unlink(tmp_path)
×
630
                raise
×
631
            
632
            if pbar:
1✔
633
                i += 1
×
634
                progress.update(i)
×
635
        if pbar:
1✔
636
            progress.finish()
×
637
        self.clean_res()
1✔
638
    
639
    def sortByDate(self, files, datetype='start'):
1✔
640
        """
641
        sort a list of OSV files by a specific date type
642

643
        Parameters
644
        ----------
645
        files: list[str]
646
            some OSV files
647
        datetype: {'publish', 'start', 'stop'}
648
            one of three possible date types contained in the OSV filename
649

650
        Returns
651
        -------
652
        list[str]
653
            the input OSV files sorted by the defined date
654
        """
655
        return sorted(files, key=lambda x: self.date(x, datetype))
1✔
656
    
657
    def _subdir(self, file):
1✔
658
        """
659
        | return the subdirectory in which to store the EOF file,
660
        | i.e. basedir/{type}ORB/{sensor}/{year}/{month}
661
        | e.g. basedir/POEORB/S1A/2018/12
662

663
        Parameters
664
        ----------
665
        file: str
666
            the EOF filename
667

668
        Returns
669
        -------
670
        str
671
            the target directory
672
        """
673
        attr = self._parse(file)
1✔
674
        outdir = self._typeEvaluate(attr['type'][:3])
1✔
675
        start = self.date(file, datetype='start')
1✔
676
        start = datetime.strptime(start, '%Y%m%dT%H%M%S')
1✔
677
        month = '{:02d}'.format(start.month)
1✔
678
        outdir = os.path.join(outdir, attr['sensor'],
1✔
679
                              str(start.year), month)
680
        return outdir
1✔
681

682

683
def removeGRDBorderNoise(scene, method='pyroSAR'):
1✔
684
    """
685
    Mask out Sentinel-1 image border noise. This function implements the method for removing GRD border noise as
686
    published by ESA :cite:`Miranda2018` and implemented in SNAP and additionally adds further refinement of the result using an image
687
    border line simplification approach. In this approach the border between valid and invalid pixels is first
688
    simplified using the poly-line vertex reduction method by Visvalingam and Whyatt :cite:`Visvalingam1993`.
689
    The line segments of the new border are then shifted until all pixels considered invalid before the simplification
690
    are again on one side of the line. See image below for further clarification.
691

692
    Parameters
693
    ----------
694
    scene: pyroSAR.drivers.SAFE
695
        the Sentinel-1 scene object
696
    method: str
697
        the border noise removal method to be applied; one of the following:
698
        
699
         - 'ESA': the pure implementation as described by ESA
700
         - 'pyroSAR': the ESA method plus the custom pyroSAR refinement
701

702

703
    .. figure:: figures/S1_bnr.png
704
        :scale: 30%
705

706
        Demonstration of the border noise removal for a vertical left image border. The area under the respective lines
707
        covers pixels considered valid, everything above will be masked out. The blue line is the result of the noise
708
        removal as recommended by ESA, in which a lot of noise is still present. The red line is the over-simplified
709
        result using the Visvalingam-Whyatt method. The green line is the final result after further correcting the
710
        VW-simplified result.
711

712
    """
713
    if scene.product != 'GRD':
×
714
        raise RuntimeError('this method is intended for GRD only')
×
715
    
716
    if scene.compression is not None:
×
717
        raise RuntimeError('scene is not yet unpacked')
×
718
    
719
    if method not in ['pyroSAR', 'ESA']:
×
720
        raise AttributeError("parameter 'method' must be either 'pyroSAR' or 'ESA'")
×
721
    
722
    blocksize = 2000
×
723
    
724
    # compute noise scaling factor
725
    if scene.meta['IPF_version'] >= 2.9:
×
726
        log.info('border noise removal not necessary for IPF version {}'.format(scene.meta['IPF_version']))
×
727
        return
×
728
    elif scene.meta['IPF_version'] <= 2.5:
×
729
        knoise = {'IW': 75088.7, 'EW': 56065.87}[scene.acquisition_mode]
×
730
        cads = scene.getFileObj(scene.findfiles('calibration-s1[ab]-[ie]w-grd-(?:hh|vv)')[0])
×
731
        caltree = ET.fromstring(cads.read())
×
732
        cads.close()
×
733
        adn = float(caltree.find('.//calibrationVector/dn').text.split()[0])
×
734
        if scene.meta['IPF_version'] < 2.34:
×
735
            scalingFactor = knoise * adn
×
736
        else:
737
            scalingFactor = knoise * adn * adn
×
738
    else:
739
        scalingFactor = 1
×
740
    
741
    # read noise vectors from corresponding annotation xml
742
    noisefile = scene.getFileObj(scene.findfiles('noise-s1[ab]-[ie]w-grd-(?:hh|vv)')[0])
×
743
    noisetree = ET.fromstring(noisefile.read())
×
744
    noisefile.close()
×
745
    noiseVectors = noisetree.findall('.//noiseVector')
×
746
    
747
    # define boundaries of image subsets to be masked (4x the first lines/samples of the image boundaries)
748
    subsets = [(0, 0, blocksize, scene.lines),
×
749
               (0, 0, scene.samples, blocksize),
750
               (scene.samples - blocksize, 0, scene.samples, scene.lines),
751
               (0, scene.lines - blocksize, scene.samples, scene.lines)]
752
    
753
    # extract column indices of noise vectors
754
    yi = np.array([int(x.find('line').text) for x in noiseVectors])
×
755
    
756
    # create links to the tif files for a master co-polarization and all other polarizations as slaves
757
    master = scene.findfiles('s1.*(?:vv|hh).*tiff')[0]
×
758
    ras_master = gdal.Open(master, GA_Update)
×
759
    ras_slaves = [gdal.Open(x, GA_Update) for x in scene.findfiles('s1.*tiff') if x != master]
×
760
    
761
    outband_master = ras_master.GetRasterBand(1)
×
762
    outband_slaves = [x.GetRasterBand(1) for x in ras_slaves]
×
763
    
764
    # iterate over the four image subsets
765
    for subset in subsets:
×
766
        log.info(subset)
×
767
        xmin, ymin, xmax, ymax = subset
×
768
        xdiff = xmax - xmin
×
769
        ydiff = ymax - ymin
×
770
        # linear interpolation of noise vectors to array
771
        noise_interp = np.empty((ydiff, xdiff), dtype=float)
×
772
        for i in range(0, len(noiseVectors)):
×
773
            if ymin <= yi[i] <= ymax:
×
774
                # extract row indices of noise vector
775
                xi = [int(x) for x in noiseVectors[i].find('pixel').text.split()]
×
776
                # extract noise values
777
                noise = [float(x) for x in noiseVectors[i].find('noiseLut').text.split()]
×
778
                # interpolate values along rows
779
                noise_interp[yi[i] - ymin, :] = np.interp(range(0, xdiff), xi, noise)
×
780
        for i in range(0, xdiff):
×
781
            yi_t = yi[(ymin <= yi) & (yi <= ymax)] - ymin
×
782
            # interpolate values along columns
783
            noise_interp[:, i] = np.interp(range(0, ydiff), yi_t, noise_interp[:, i][yi_t])
×
784
        
785
        # read subset of image to array and subtract interpolated noise (denoising)
786
        mat_master = outband_master.ReadAsArray(*[xmin, ymin, xdiff, ydiff])
×
787
        denoisedBlock = mat_master.astype(float) ** 2 - noise_interp * scalingFactor
×
788
        # mask out all pixels with a value below 0.5 in the denoised block or 30 in the original block
789
        denoisedBlock[(denoisedBlock < 0.5) | (mat_master < 30)] = 0
×
790
        denoisedBlock = np.sqrt(denoisedBlock)
×
791
        
792
        if method == 'pyroSAR':
×
793
            # helper functions for masking out negative values
794
            def helper1(x):
×
795
                return len(x) - np.argmax(x > 0)
×
796
            
797
            def helper2(x):
×
798
                return len(x) - np.argmax(x[::-1] > 0)
×
799
            
800
            # mask out negative values and simplify borders (custom implementation)
801
            if subset == (0, 0, blocksize, scene.lines):
×
802
                border = np.apply_along_axis(helper1, 1, denoisedBlock)
×
803
                border = blocksize - ls.reduce(border)
×
804
                for j in range(0, ydiff):
×
805
                    denoisedBlock[j, :border[j]] = 0
×
806
                    denoisedBlock[j, border[j]:] = 1
×
807
            elif subset == (0, scene.lines - blocksize, scene.samples, scene.lines):
×
808
                border = np.apply_along_axis(helper2, 0, denoisedBlock)
×
809
                border = ls.reduce(border)
×
810
                for j in range(0, xdiff):
×
811
                    denoisedBlock[border[j]:, j] = 0
×
812
                    denoisedBlock[:border[j], j] = 1
×
813
            elif subset == (scene.samples - blocksize, 0, scene.samples, scene.lines):
×
814
                border = np.apply_along_axis(helper2, 1, denoisedBlock)
×
815
                border = ls.reduce(border)
×
816
                for j in range(0, ydiff):
×
817
                    denoisedBlock[j, border[j]:] = 0
×
818
                    denoisedBlock[j, :border[j]] = 1
×
819
            elif subset == (0, 0, scene.samples, blocksize):
×
820
                border = np.apply_along_axis(helper1, 0, denoisedBlock)
×
821
                border = blocksize - ls.reduce(border)
×
822
                for j in range(0, xdiff):
×
823
                    denoisedBlock[:border[j], j] = 0
×
824
                    denoisedBlock[border[j]:, j] = 1
×
825
        
826
        mat_master[denoisedBlock == 0] = 0
×
827
        # write modified array back to original file
828
        outband_master.WriteArray(mat_master, xmin, ymin)
×
829
        outband_master.FlushCache()
×
830
        # perform reading, masking and writing for all other polarizations
831
        for outband in outband_slaves:
×
832
            mat = outband.ReadAsArray(*[xmin, ymin, xdiff, ydiff])
×
833
            mat[denoisedBlock == 0] = 0
×
834
            outband.WriteArray(mat, xmin, ymin)
×
835
            outband.FlushCache()
×
836
    # detach file links
837
    outband_master = None
×
838
    ras_master = None
×
839
    for outband in outband_slaves:
×
840
        outband = None
×
841
    for ras in ras_slaves:
×
842
        ras = None
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc