###############################################################################
# ancillary routines for software pyroSAR
# Copyright (c) 2014-2023, the pyroSAR Developers.
# This file is part of the pyroSAR Project. It is subject to the
# license terms in the LICENSE.txt file found in the top-level
# directory of this distribution and at
# https://github.com/johntruckenbrodt/pyroSAR/blob/master/LICENSE.txt.
# No part of the pyroSAR project, including this file, may be
# copied, modified, propagated, or distributed except according
# to the terms contained in the LICENSE.txt file.
###############################################################################
"""
This module gathers central functions and classes for general pyroSAR applications.
"""
import os
import re
from math import sin, radians
import inspect
from datetime import datetime
from . import patterns
from spatialist.ancillary import finder
[docs]def groupby(images, attribute):
"""
group a list of images by a metadata attribute
Parameters
----------
images: list[str]
the names of the images to be sorted
attribute: str
the name of the attribute used for sorting;
see :func:`parse_datasetname` for options
Returns
-------
list[list[str]]
a list of sub-lists containing the grouped images
"""
images_sort = sorted(images, key=lambda x: re.search(patterns.pyrosar, x).group(attribute))
out_meta = [[parse_datasetname(images_sort.pop(0))]]
while len(images_sort) > 0:
filename = images_sort.pop(0)
meta = parse_datasetname(filename)
if out_meta[-1][0][attribute] == meta[attribute]:
out_meta[-1].append(meta)
else:
out_meta.append([meta])
out = [[x['filename'] for x in y] for y in out_meta]
return out
[docs]def groupbyTime(images, function, time):
"""
function to group images by their acquisition time difference
Parameters
----------
images: list[str]
a list of image names
function: function
a function to derive the time from the image names; see e.g. :func:`seconds`
time: int or float
a time difference in seconds by which to group the images
Returns
-------
list[list[str]]
a list of sub-lists containing the grouped images
"""
# sort images by time stamp
srcfiles = sorted(images, key=function)
groups = [[srcfiles[0]]]
group = groups[0]
for i in range(1, len(srcfiles)):
item = srcfiles[i]
timediff = abs(function(item) - function(group[-1]))
if timediff <= time:
group.append(item)
else:
groups.append([item])
group = groups[-1]
return [x[0] if len(x) == 1 else x for x in groups]
[docs]def multilook_factors(source_rg, source_az, target, geometry, incidence):
"""
compute multi-looking factors to approximate a square pixel with defined target ground range pixel spacing.
Parameters
----------
source_rg: int or float
the range pixel spacing
source_az: int or float
the azimuth pixel spacing
target: int or float
the target pixel spacing of an approximately square pixel
geometry: str
the imaging geometry; either 'SLANT_RANGE' or 'GROUND_RANGE'
incidence: int or float
the angle of incidence
Returns
-------
tuple[int]
the multi-looking factors as (range looks, azimuth looks)
Examples
--------
>>> from pyroSAR.ancillary import multilook_factors
>>> rlks, azlks = multilook_factors(source_rg=2, source_az=13, target=10,
>>> geometry='SLANT_RANGE', incidence=39)
>>> print(rlks, azlks)
4 1
"""
azlks = int(round(float(target) / source_az))
azlks = azlks if azlks > 0 else 1
if geometry == 'SLANT_RANGE':
rlks = float(azlks) * source_az * sin(radians(incidence)) / source_rg
elif geometry == 'GROUND_RANGE':
rlks = float(azlks) * source_az / source_rg
else:
raise ValueError("parameter 'geometry' must be either 'SLANT_RANGE' or 'GROUND_RANGE'")
rlks = int(round(rlks))
return rlks, azlks
[docs]def seconds(filename):
"""
function to extract time in seconds from a file name.
the format must follow a fixed pattern: YYYYmmddTHHMMSS
Images processed with pyroSAR functionalities via module snap or gamma will contain this information.
Parameters
----------
filename: str
the name of a file from which to extract the time from
Returns
-------
float
the difference between the time stamp in filename and Jan 01 1900 in seconds
"""
# return mktime(strptime(re.findall('[0-9T]{15}', filename)[0], '%Y%m%dT%H%M%S'))
td = datetime.strptime(re.findall('[0-9T]{15}', filename)[0], '%Y%m%dT%H%M%S') - datetime(1900, 1, 1)
return td.total_seconds()
[docs]def parse_datasetname(name, parse_date=False):
"""
Parse the name of a pyroSAR processing product and extract its metadata components as dictionary
Parameters
----------
name: str
the name of the file to be parsed
parse_date: bool
parse the start date to a :class:`~datetime.datetime` object or just return the string?
Returns
-------
dict
the metadata attributes
Examples
--------
>>> meta = parse_datasetname('S1A__IW___A_20150309T173017_VV_grd_mli_geo_norm_db.tif')
>>> print(sorted(meta.keys()))
['acquisition_mode', 'extensions', 'filename', 'orbit',
'outname_base', 'polarization', 'proc_steps', 'sensor', 'start']
"""
filename = os.path.abspath(name) if os.path.isfile(name) else name
match = re.match(re.compile(patterns.pyrosar), filename)
if not match:
return
out = match.groupdict()
if out['extensions'] == '':
out['extensions'] = None
if out['proc_steps'] is not None:
out['proc_steps'] = out['proc_steps'].split('_')
if parse_date:
out['start'] = datetime.strptime(out['start'], '%Y%m%dT%H%M%S')
out['filename'] = filename
out['outname_base'] = out['outname_base'].strip('_')
return out
[docs]def find_datasets(directory, recursive=False, **kwargs):
"""
find pyroSAR datasets in a directory based on their metadata
Parameters
----------
directory: str
the name of the directory to be searched
recursive: bool
search the directory recursively into subdirectories?
kwargs:
Metadata attributes for filtering the scene list supplied as `key=value`. e.g. `sensor='S1A'`.
Multiple allowed options can be provided in tuples, e.g. `sensor=('S1A', 'S1B')`.
Any types other than tuples require an exact match, e.g. `proc_steps=['grd', 'mli', 'geo', 'norm', 'db']`
will be matched only if these processing steps are contained in the product name in this exact order.
The special attributes `start` and `stop` can be used for time filtering where `start<=value<=stop`.
See function :func:`parse_datasetname` for further options.
Returns
-------
list of str
the file names found in the directory and filtered by metadata attributes
Examples
--------
>>> selection = find_datasets('path/to/files', sensor=('S1A', 'S1B'), polarization='VV')
"""
files = finder(directory, [patterns.pyrosar], regex=True, recursive=recursive)
selection = []
for file in files:
meta = parse_datasetname(file)
matches = []
for key, val in kwargs.items():
if key == 'start':
match = val <= meta['start']
elif key == 'stop':
match = val >= meta['start'] # only the start time stamp is contained in the filename
elif isinstance(val, tuple):
match = meta[key] in val
else:
match = meta[key] == val
matches.append(match)
if all(matches):
selection.append(file)
return selection
[docs]def getargs(func):
"""
get the arguments of a function
Parameters
----------
func: function
the function to be checked
Returns
-------
list or str
the argument names
"""
return sorted(inspect.getfullargspec(func).args)
[docs]def hasarg(func, arg):
"""
simple check whether a function takes a parameter as input
Parameters
----------
func: function
the function to be checked
arg: str
the argument name to be found
Returns
-------
bool
does the function take this as argument?
"""
return arg in getargs(func)
[docs]def windows_fileprefix(func, path, exc_info):
"""
Helper function for :func:`shutil.rmtree` to exceed Windows' file name length limit of 256 characters.
See `here <https://stackoverflow.com/questions/36219317/pathname-too-long-to-open>`_ for details.
Parameters
----------
func: function
the function to be executed, i.e. :func:`shutil.rmtree`
path: str
the path to be deleted
exc_info: tuple
execution info as returned by :func:`sys.exc_info`
Returns
-------
Examples
--------
>>> import shutil
>>> from pyroSAR.ancillary import windows_fileprefix
>>> shutil.rmtree('/path', onerror=windows_fileprefix)
"""
func(u'\\\\?\\' + path)