From fa23e7a6639a79924532fc1bfb86854112c93256 Mon Sep 17 00:00:00 2001 From: cadauxe Date: Wed, 30 Oct 2024 15:04:36 +0100 Subject: [PATCH 01/17] docs: start review documentation --- docs/cli/filtering.rst | 304 +++++++-- docs/cli/radioindice.rst | 4 +- src/eolab/rastertools/filtering.py | 47 +- src/eolab/rastertools/hillshade.py | 35 +- src/eolab/rastertools/main.py | 1 - src/eolab/rastertools/processing/algo.py | 624 ++++++++++++------ .../rastertools/processing/rasterproc.py | 22 +- src/eolab/rastertools/processing/sliding.py | 42 +- src/eolab/rastertools/radioindice.py | 38 +- src/eolab/rastertools/rastertools.py | 11 +- src/eolab/rastertools/zonalstats.py | 13 +- src/rastertools.egg-info/PKG-INFO | 2 +- src/rastertools.egg-info/SOURCES.txt | 52 +- tests/test_algo.py | 22 +- tests/test_radioindice.py | 57 +- tests/test_rasterproc.py | 22 + tests/test_rasterproduct.py | 103 +++ tests/test_rastertools.py | 4 +- tests/test_speed.py | 5 +- tests/utils4test.py | 25 +- 20 files changed, 1087 insertions(+), 346 deletions(-) diff --git a/docs/cli/filtering.rst b/docs/cli/filtering.rst index 6e78b4b..159e400 100644 --- a/docs/cli/filtering.rst +++ b/docs/cli/filtering.rst @@ -20,75 +20,265 @@ filter mean Apply local mean filter adaptive_gaussian Apply adaptive gaussian filter -For different filters are available. They are applied as sub-command that each define the arguments -that configure the filter. Type option --help to get the definition of the arguments: +The available filters are Adaptive Gaussian, Local Sum, and Local Mean. +Each filter is used as a sub-command and has specific arguments for filtering. +To see the definitions of these arguments, type the option --help. -.. code-block:: console +- **Median** - $ rastertools filter adaptive_gaussian --help - usage: rastertools filter adaptive_gaussian [-h] --kernel_size KERNEL_SIZE - --sigma SIGMA [-o OUTPUT] - [-ws WINDOW_SIZE] - [-p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}] - [-b BANDS [BANDS ...]] [-a] - inputs [inputs ...] - - Apply an adaptive (Local gaussian of 3x3) recursive filter on the input image - - positional arguments: - inputs Input file to process (e.g. Sentinel2 L2A MAJA from - THEIA). You can provide a single file with extension - ".lst" (e.g. "filtering.lst") that lists the input - files to process (one input file per line in .lst) - - optional arguments: - -h, --help show this help message and exit - --kernel_size KERNEL_SIZE - Kernel size of the filter function, e.g. 3 means a - square of 3x3 pixels on which the filter function is - computed (default: 8) - --sigma SIGMA Standard deviation of the Gaussian distribution - (sigma) - -o OUTPUT, --output OUTPUT - Output dir where to store results (by default current - dir) - -ws WINDOW_SIZE, --window_size WINDOW_SIZE - Size of tiles to distribute processing, default: 1024 - -p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}, --pad {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap} - Pad to use around the image, default : edge (see https - ://numpy.org/doc/stable/reference/generated/numpy.pad. - html for more information) - -b BANDS [BANDS ...], --bands BANDS [BANDS ...] - List of bands to compute - -a, --all Compute all bands - - By default only first band is computed. + .. code-block:: console -Examples: + $ rastertools filter median --help + usage: rastertools filter median [-h] --kernel_size KERNEL_SIZE [-o OUTPUT] + [-ws WINDOW_SIZE] + [-p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}] + [-b BANDS [BANDS ...]] [-a] + inputs [inputs ...] -The following examples use an input raster file generated by radioindice. This is an NDVI of a SENTINEL2 L2A THEIA image cropped to a (small) -region of interest. + Apply a median filter (see scipy median_filter for more information) -.. image:: ../_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.jpg + positional arguments: + inputs Input file to process (e.g. Sentinel2 L2A MAJA from + THEIA). You can provide a single file with extension + ".lst" (e.g. "filtering.lst") that lists the input + files to process (one input file per line in .lst) -To apply three filters (median, mean and adaptive_gaussian) on a kernel of dimension 16x16, run these commands: + optional arguments: + -h, --help show this help message and exit + --kernel_size KERNEL_SIZE + Kernel size of the filter function, e.g. 3 means a + square of 3x3 pixels on which the filter function is + computed (default: 8) + -o OUTPUT, --output OUTPUT + Output dir where to store results (by default current + dir) + -ws WINDOW_SIZE, --window_size WINDOW_SIZE + Size of tiles to distribute processing, default: 1024 + -p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}, --pad {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap} + Pad to use around the image, default : edge (see https + ://numpy.org/doc/stable/reference/generated/numpy.pad. + html for more information) + -b BANDS [BANDS ...], --bands BANDS [BANDS ...] + List of bands to compute + -a, --all Compute all bands -.. code-block:: console + By default only first band is computed. + + The corresponding API functions that is called by the command line interface is the following : + + .. autofunction:: eolab.rastertools.processing.algo.median + + + Here is an example of a median filter applied to the NDVI of a SENTINEL2 L2A THEIA image cropped to a region of interest. + This raster was previously computed using :ref:`radioindice` on the original SENTINEL2 L2A THEIA image. + + .. code-block:: console + + $ rastertools filter median --kernel_size 16 "./SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" + + .. list-table:: + :widths: 20 20 + :header-rows: 0 + + * - .. centered:: Original + - .. centered:: Filtered by Median + + * - .. image:: ../_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.jpg + :align: center + - .. image:: ../_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-median.jpg + :align: center + +- **Local sum** + + .. code-block:: console + + $ rastertools filter sum --help + usage: rastertools filter sum [-h] --kernel_size KERNEL_SIZE [-o OUTPUT] + [-ws WINDOW_SIZE] + [-p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}] + [-b BANDS [BANDS ...]] [-a] + inputs [inputs ...] + + Apply a local sum filter using integral image method + + positional arguments: + inputs Input file to process (e.g. Sentinel2 L2A MAJA from + THEIA). You can provide a single file with extension + ".lst" (e.g. "filtering.lst") that lists the input + files to process (one input file per line in .lst) + + optional arguments: + -h, --help show this help message and exit + --kernel_size KERNEL_SIZE + Kernel size of the filter function, e.g. 3 means a + square of 3x3 pixels on which the filter function is + computed (default: 8) + -o OUTPUT, --output OUTPUT + Output dir where to store results (by default current + dir) + -ws WINDOW_SIZE, --window_size WINDOW_SIZE + Size of tiles to distribute processing, default: 1024 + -p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}, --pad {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap} + Pad to use around the image, default : edge (see https + ://numpy.org/doc/stable/reference/generated/numpy.pad. + html for more information) + -b BANDS [BANDS ...], --bands BANDS [BANDS ...] + List of bands to compute + -a, --all Compute all bands + + By default only first band is computed. + + The corresponding API functions that is called by the command line interface is the following : + + .. autofunction:: eolab.rastertools.processing.algo.local_sum + + Here is an example of the local mean applied to the NDVI of a SENTINEL2 L2A THEIA image cropped to a region of interest. + This raster was previously computed using :ref:`radioindice` on the original SENTINEL2 L2A THEIA image. + + .. code-block:: console + + $ rastertools filter sum --kernel_size 16 "./SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" + + .. list-table:: + :widths: 20 20 + :header-rows: 0 + + * - .. centered:: Original + - .. centered:: Filtered by Local sum + + * - .. image:: ../_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.jpg + :align: center + - .. image:: ../_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-sum.jpg + :align: center + +- **Local mean** + + .. code-block:: console + + $ rastertools filter mean --help + usage: rastertools filter mean [-h] --kernel_size KERNEL_SIZE [-o OUTPUT] + [-ws WINDOW_SIZE] + [-p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}] + [-b BANDS [BANDS ...]] [-a] + inputs [inputs ...] + + Apply a local mean filter using integral image method + + positional arguments: + inputs Input file to process (e.g. Sentinel2 L2A MAJA from + THEIA). You can provide a single file with extension + ".lst" (e.g. "filtering.lst") that lists the input + files to process (one input file per line in .lst) + + optional arguments: + -h, --help show this help message and exit + --kernel_size KERNEL_SIZE + Kernel size of the filter function, e.g. 3 means a + square of 3x3 pixels on which the filter function is + computed (default: 8) + -o OUTPUT, --output OUTPUT + Output dir where to store results (by default current + dir) + -ws WINDOW_SIZE, --window_size WINDOW_SIZE + Size of tiles to distribute processing, default: 1024 + -p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}, --pad {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap} + Pad to use around the image, default : edge (see https + ://numpy.org/doc/stable/reference/generated/numpy.pad. + html for more information) + -b BANDS [BANDS ...], --bands BANDS [BANDS ...] + List of bands to compute + -a, --all Compute all bands + + By default only first band is computed. + + + The corresponding API functions that is called by the command line interface is the following : + + .. autofunction:: eolab.rastertools.processing.algo.local_mean + + + Here is an example of the local mean applied to the NDVI of a SENTINEL2 L2A THEIA image cropped to a region of interest. + This raster was previously computed using :ref:`radioindice` on the original SENTINEL2 L2A THEIA image. + + .. code-block:: console + + $ rastertools filter mean --kernel_size 16 "./SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" + + .. list-table:: + :widths: 20 20 + :header-rows: 0 + + * - .. centered:: Original + - .. centered:: Filtered by Local mean + + * - .. image:: ../_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.jpg + :align: center + - .. image:: ../_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-mean.jpg + :align: center + +- **Adaptative gaussian** + + .. code-block:: console + + $ rastertools filter adaptive_gaussian --help + usage: rastertools filter adaptive_gaussian [-h] --kernel_size KERNEL_SIZE + --sigma SIGMA [-o OUTPUT] + [-ws WINDOW_SIZE] + [-p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}] + [-b BANDS [BANDS ...]] [-a] + inputs [inputs ...] + + Apply an adaptive (Local gaussian of 3x3) recursive filter on the input image + + positional arguments: + inputs Input file to process (e.g. Sentinel2 L2A MAJA from + THEIA). You can provide a single file with extension + ".lst" (e.g. "filtering.lst") that lists the input + files to process (one input file per line in .lst) + + optional arguments: + -h, --help show this help message and exit + --kernel_size KERNEL_SIZE + Kernel size of the filter function, e.g. 3 means a + square of 3x3 pixels on which the filter function is + computed (default: 8) + --sigma SIGMA Standard deviation of the Gaussian distribution + (sigma) + -o OUTPUT, --output OUTPUT + Output dir where to store results (by default current + dir) + -ws WINDOW_SIZE, --window_size WINDOW_SIZE + Size of tiles to distribute processing, default: 1024 + -p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}, --pad {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap} + Pad to use around the image, default : edge (see https + ://numpy.org/doc/stable/reference/generated/numpy.pad. + html for more information) + -b BANDS [BANDS ...], --bands BANDS [BANDS ...] + List of bands to compute + -a, --all Compute all bands + + By default only first band is computed. + + The corresponding API functions that is called by the command line interface is the following : - $ rastertools filter median --kernel_size 16 "./SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" - $ rastertools filter mean --kernel_size 16 "./SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" - $ rastertools filter adaptive_gaussian --kernel_size 16 --sigma 1 "./SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" + .. autofunction:: eolab.rastertools.processing.algo.adaptive_gaussian -The commands will generate respectively: + Here is an example of the local mean applied to the NDVI of a SENTINEL2 L2A THEIA image cropped to a region of interest. + This raster was previously computed using :ref:`radioindice` on the original SENTINEL2 L2A THEIA image. -- SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-median.tif + .. code-block:: console -.. image:: ../_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-median.jpg + $ rastertools filter adaptive_gaussian --kernel_size 16 --sigma 1 "./SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" -- SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-mean.tif + .. list-table:: + :widths: 20 20 + :header-rows: 0 -.. image:: ../_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-mean.jpg + * - .. centered:: Original + - .. centered:: Filtered by Adaptive gaussian -- SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-adaptive_gaussian.tif + * - .. image:: ../_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.jpg + :align: center + - .. image:: ../_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-adaptive_gaussian.jpg + :align: center -.. image:: ../_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-adaptive_gaussian.jpg diff --git a/docs/cli/radioindice.rst b/docs/cli/radioindice.rst index f89b1d1..1bae9cd 100644 --- a/docs/cli/radioindice.rst +++ b/docs/cli/radioindice.rst @@ -1,4 +1,4 @@ -.. radioindice: +.. _radioindice: radioindice ----------- @@ -42,7 +42,7 @@ radioindice bi2, evi, ipvi, mndwi, msavi, msavi2, ndbi, ndpi, ndti, ndvi, ndwi, ndwi2, pvi, ri, rvi, savi, tndvi, tsavi - --ndvi Compute ndvi indice + --ndvi Compute ndvi indice INSERT LINK TO CORRESPONDING DOC --tndvi Compute tndvi indice --rvi Compute rvi indice --pvi Compute pvi indice diff --git a/src/eolab/rastertools/filtering.py b/src/eolab/rastertools/filtering.py index 6a85ae4..3f503b1 100644 --- a/src/eolab/rastertools/filtering.py +++ b/src/eolab/rastertools/filtering.py @@ -24,10 +24,10 @@ class Filtering(Rastertool, Windowable): Predefined filters are available: - - median filter - - local sum - - local mean - - adaptive gaussian filter. + - Median filter + - Local sum + - Local mean + - Adaptive gaussian filter. A filter is applied on a kernel of a configurable size. To set the kernel size, you need to call: @@ -79,7 +79,14 @@ def myalgo(input_data, **kwargs): help="Apply median filter", description="Apply a median filter (see scipy median_filter for more information)" ) - """RasterFilter that computes the median of the kernel""" + """ + Applies a Median Filter to the input data using + `scipy.ndimage.median_filter `_. + The filter computes the median contained in the sliding window determined by kernel_size. + + Returns: + Numpy array containing the input data filtered by the median filter + """ local_sum = RasterFilter( "sum", algo=algo.local_sum @@ -87,7 +94,12 @@ def myalgo(input_data, **kwargs): help="Apply local sum filter", description="Apply a local sum filter using integral image method" ) - """RasterFilter that computes the local sum of the kernel""" + """Computes the local sums of the input data. + Each element is the sum of the pixels contained in the sliding window determined by kernel_size. + + Returns: + Numpy array of the size of input_data containing the computed local sums + """ local_mean = RasterFilter( "mean", algo=algo.local_mean @@ -95,7 +107,12 @@ def myalgo(input_data, **kwargs): help="Apply local mean filter", description="Apply a local mean filter using integral image method", ) - """RasterFilter that computes the local mean of the kernel""" + """Computes the local means of the input data. + Each element is the mean of the pixels contained in the sliding window determined by kernel_size. + + Returns: + Numpy array of the size of input_data containing the computed local means + """ adaptive_gaussian = RasterFilter( "adaptive_gaussian", algo=algo.adaptive_gaussian, per_band_algo=True @@ -110,16 +127,20 @@ def myalgo(input_data, **kwargs): "help": "Standard deviation of the Gaussian distribution (sigma)" }, }) - """RasterFilter that applies an adaptive gaussian filter to the kernel. It has a special - parameter named sigma that defines the standard deviation of the Gaussian distribution.""" + """RasterFilter that applies an adaptive gaussian filter to the kernel. The parameter sigma defines the standard deviation + of the Gaussian distribution. + + Returns: + Numpy array containing the input data filtered by the Gaussian filter. + """ @staticmethod def get_default_filters(): """Get the list of predefined raster filters Returns: - [:obj:`eolab.rastertools.processing.RasterFilter`]: list of predefined - raster filters. + [:obj:`eolab.rastertools.processing.RasterFilter`] List of the predefined + raster filters ([Median, Local sum, Local mean, Adaptive gaussian]) """ return [ Filtering.median_filter, Filtering.local_sum, @@ -154,7 +175,7 @@ def bands(self) -> List[int]: @property def raster_filter(self) -> RasterFilter: - """Raster filter to apply""" + """Name of the filter to apply to the raster""" return self._raster_filter def with_filter_configuration(self, argsdict: Dict): @@ -180,7 +201,7 @@ def process_file(self, inputfile: str) -> List[str]: Input image to process Returns: - [str]: A list containing a single element: the generated filtered image. + ([str]) A list of one element containing the path of the generated filtered image. """ _logger.info(f"Processing file {inputfile}") diff --git a/src/eolab/rastertools/hillshade.py b/src/eolab/rastertools/hillshade.py index 2e287a6..0164636 100644 --- a/src/eolab/rastertools/hillshade.py +++ b/src/eolab/rastertools/hillshade.py @@ -50,18 +50,19 @@ class Hillshade(Rastertool, Windowable): """ def __init__(self, elevation: float, azimuth: float, resolution: float, radius: int = None): - """ Constructor + """ Constructor for the Hillshade class. Args: elevation (float): - Elevation of the sun (in degrees), 0 is vertical top + Elevation of the sun (in degrees), 0 is vertical top (zenith). azimuth (float): - Azimuth of the sun (in degrees) + Azimuth of the sun (in degrees), measured clockwise from north. resolution (float): - Resolution of a raster pixel (in meter) - radius (int): - Max distance from current point (in pixels) to consider - for evaluating the hillshade + Resolution of a raster pixel (in meters). + radius (int, optional): + Maximum distance from the current point (in pixels) to consider + for evaluating the hillshade. If None, the radius is calculated + based on the data range. """ super().__init__() self.with_windows() @@ -73,34 +74,38 @@ def __init__(self, elevation: float, azimuth: float, resolution: float, radius: @property def elevation(self): - """Elevation of the sun (in degrees)""" + """Return the elevation of the sun (in degrees)""" return self._elevation @property def azimuth(self): - """Azimuth of the sun (in degrees)""" + """Return the azimuth of the sun (in degrees)""" return self._azimuth @property def resolution(self): - """Resolution of a raster pixel (in meter)""" + """Return the resolution of a raster pixel (in meter)""" return self._resolution @property def radius(self): - """Max distance from current point (in pixels) to consider - for evaluating the max elevation angle""" + """Return the maximum distance from current point (in pixels) + for evaluating the maximum elevation angle""" return self._radius def process_file(self, inputfile: str) -> List[str]: - """Compute Hillshade for the input file + """ + Compute hillshade for the input file. Args: inputfile (str): - Input image to process + Input image file path to process. Returns: - [str]: A list containing a single element: the generated hillshade image. + List[str]: A list containing the file path of the generated hillshade image. + + Raises: + ValueError: If the input file contains more than one band or if the radius exceeds constraints. """ _logger.info(f"Processing file {inputfile}") outdir = Path(self.outputdir) diff --git a/src/eolab/rastertools/main.py b/src/eolab/rastertools/main.py index d963a36..c9dc903 100644 --- a/src/eolab/rastertools/main.py +++ b/src/eolab/rastertools/main.py @@ -231,7 +231,6 @@ def run_tool(args): # launch process tool.process_files(inputs) - _logger.info("Done!") except RastertoolConfigurationException as rce: _logger.exception(rce) diff --git a/src/eolab/rastertools/processing/algo.py b/src/eolab/rastertools/processing/algo.py index 14d035f..34774fd 100644 --- a/src/eolab/rastertools/processing/algo.py +++ b/src/eolab/rastertools/processing/algo.py @@ -5,34 +5,57 @@ """ import math +import numpy import numpy as np import numpy.ma as ma from scipy import ndimage, signal -def normalized_difference(bands, **kwargs): - """Algorithm that performs a normalized band ratio - -1 <= nd <= 1 +def normalized_difference(bands : np.ndarray) -> numpy.ndarray : + """ + Compute the Normalized Difference Vegetation Index + The coefficient ranges from -1 to 1 in each pixel. + + The function considers the bands of the input data in the following order : + Red, NIR (Near Infra-Red). + + .. math:: + NDVI = \\frac{NIR - RED}{NIR + RED} Args: - bands: list of bands as a numpy ndarray + input_data (np.ndarray) : Numpy array of 3 dimensions (number of bands, number of lines, number of columns) + with number of bands > 1. Returns: - The numpy array with the results + Numpy array of the size (number of lines, number of columns) containing the computed TNDVI. """ np.seterr(divide='ignore') return (bands[1] - bands[0]) / (bands[1] + bands[0]) -def tndvi(bands, **kwargs): - """Transformed Normalized Difference Vegetation Index - TNDVI > 0 +def tndvi(bands : np.ndarray) -> numpy.ndarray : + """ + Compute the Transformed Normalized Difference Vegetation Index + The coefficient is positive in each pixel. + + The function considers the bands of the input data in the following order : + Red, NIR (Near Infra-Red). + + .. math:: + TNDVI = \\sqrt{NDVI + 0.5} Args: - bands: list of bands as a numpy ndarray + input_data (np.ndarray) : Numpy array of 3 dimensions (number of bands, number of lines, number of columns) + with number of bands > 1. Returns: - The numpy array with the results + Numpy array of the size (number of lines, number of columns) containing the computed TNDVI. + + References: + `Deering D.W., Rouse J.W., Haas R.H., and Schell J.A., 1975. Measuring forage production + of grazing units from Landsat MSS data. Pages 1169-1178 In: Cook J.J. (Ed.), Proceedings + of the Tenth International Symposium on Remote Sensing of Environment (Ann Arbor, 1975), + Vol. 2, Ann Arbor, Michigan, USA. `_ """ np.seterr(invalid='ignore') ratio = normalized_difference(bands) + 0.5 @@ -40,56 +63,104 @@ def tndvi(bands, **kwargs): return np.sqrt(ratio) -def rvi(bands, **kwargs): - """Ratio Vegetation Index - RVI > 0 +def rvi(bands : np.ndarray) -> numpy.ndarray : + """ + Compute the Ratio Vegetation Index + The coefficient is positive in each pixel. + + The function considers the bands of the input data in the following order : + Red, NIR (Near Infra-Red). + + .. math:: + PVI = \\frac{NIR}{RED} Args: - bands: list of bands as a numpy ndarray + input_data (np.ndarray) : Numpy array of 3 dimensions (number of bands, number of lines, number of columns) + with number of bands > 1. Returns: - The numpy array with the results + Numpy array of the size (number of lines, number of columns) containing the computed RVI. + + References: + `Jordan C.F., 1969. Derivation of leaf area index from quality of light on the forest + floor. Ecology 50:663-666 `_ """ np.seterr(divide='ignore') return bands[1] / bands[0] -def pvi(bands, **kwargs): - """Perpendicular Vegetation Index - -1 < PVI < 1 +def pvi(bands : np.ndarray) -> numpy.ndarray : + """ + Compute the Perpendicular Vegetation Index + The coefficient ranges from -1 to 1 in each pixel. + + The function considers the bands of the input data in the following order : + Red, NIR (Near Infra-Red). + + .. math:: + PVI = 0.74 (NIR - 0.90893 RED - 7.46216) Args: - bands: list of bands as a numpy ndarray + input_data (np.ndarray) : Numpy array of 3 dimensions (number of bands, number of lines, number of columns) + with number of bands > 1. Returns: - The numpy array with the results + Numpy array of the size (number of lines, number of columns) containing the computed PVI. + + References: + `Richardson A.J., Wiegand C.L., 1977. Distinguishing vegetation from soil background + information. Photogramm Eng Rem S 43-1541-1552 `_ """ return (bands[1] - 0.90893 * bands[0] - 7.46216) * 0.74 -def savi(bands, **kwargs): - """Soil Adjusted Vegetation Index - -1 < SAVI < 1 +def savi(bands : np.ndarray) -> numpy.ndarray : + """ + Compute the Soil Adjusted Vegetation Index + The coefficient ranges from -1 to 1 in each pixel. + + The function considers the bands of the input data in the following order : + Red, NIR (Near Infra-Red). + + .. math:: + SAVI = \\frac{(NIR - RED) (1 + 0.5)}{NIR + RED + 0.5} Args: - bands: list of bands as a numpy ndarray + input_data (np.ndarray) : Numpy array of 3 dimensions (number of bands, number of lines, number of columns) + with number of bands > 1. Returns: - The numpy array with the results + Numpy array of the size (number of lines, number of columns) containing the computed SAVI. + + References: + `Huete A.R., 1988. A soil-adjusted vegetation index (SAVI). Remote Sens Environ 25:295-309 `_ """ np.seterr(divide='ignore') return (1. + 0.5) * (bands[1] - bands[0]) / (bands[1] + bands[0] + 0.5) -def tsavi(bands, **kwargs): - """Transformed Soil Adjusted Vegetation Index - -1 < TSAVI < 1 +def tsavi(bands : np.ndarray) -> numpy.ndarray : + """ + Compute the Transformed Soil Adjusted Vegetation Index + The coefficient ranges from -1 to 1 in each pixel. + + The function considers the bands of the input data in the following order : + Red, NIR (Near Infra-Red). + + .. math:: + TSAVI = \\frac{0.7 (NIR - 0.7 RED - 0.9)}{0.7 NIR + RED + 0.08 (1 + 0.7^2)} Args: - bands: list of bands as a numpy ndarray + input_data (np.ndarray) : Numpy array of 3 dimensions (number of bands, number of lines, number of columns) + with number of bands > 1. Returns: - The numpy array with the results + Numpy array of the size (number of lines, number of columns) containing the computed TSAVI. + + References: + `Baret F., Guyot G., Major D., 1989. TSAVI: a vegetation index which minimizes soil + brightness effects on LAI or APAR estimation. 12th Canadian Symposium on Remote + Sensing and IGARSS 1990, Vancouver, Canada, 07/10-14. `_ """ np.seterr(divide='ignore') denominator = 0.7 * bands[1] + bands[0] + 0.08 * (1 + 0.7 * 0.7) @@ -97,28 +168,57 @@ def tsavi(bands, **kwargs): return numerator / denominator -def _wdvi(bands, **kwargs): - """Weighted Difference Vegetation Index - Infinite range +def _wdvi(bands : numpy.ndarray) -> numpy.ndarray : + """ + Compute the Weighted Difference Vegetation Index of the input data. + + The function considers the bands of the input data in the following order : + Red, NIR (Near Infra-Red). + + .. math:: + WDVI = NIR - 0.4 RED Args: - bands: list of bands as a numpy ndarray + input_data (np.ndarray) : Numpy array of 3 dimensions (number of bands, number of lines, number of columns) + with number of bands > 1. Returns: - The numpy array with the results + Numpy array of the size (number of lines, number of columns) containing the computed WDVI. """ return bands[1] - 0.4 * bands[0] -def msavi(bands, **kwargs): - """Modified Soil Adjusted Vegetation Index - -1 < MSAVI < 1 +def msavi(bands : numpy.ndarray) -> numpy.ndarray : + """ + Compute the Modified Soil Adjusted Vegetation Index of the input data. + The coefficient ranges from -1 to 1 in each pixel. - Args: - bands: list of bands as a numpy ndarray + grdtbrbr The function considers the bands of the input data in the following order : + Red, NIR (Near Infra-Red). - Returns: - The numpy array with the results + .. math:: + MSAVI = \\frac{(NIR - RED) (1 + L)} {NIR + RED + L} \\\\ + + With : :math:`L = 1 - 2 * 0.4 * NDVI * WDVI` + + Parameters + ---------- + input_data : np.ndarray + A 3D numpy array of shape (number of bands, number of lines, number of columns) where number of bands > 1. + + Returns + ------- + np.ndarray + A 2D numpy array of shape (number of lines, number of columns) containing the computed MSAVI values. + + References + ------- + `Qi J., Chehbouni A., Huete A.R., Kerr Y.H., 1994. Modified Soil Adjusted Vegetation + Index (MSAVI). Remote Sens Environ 48:119-126 `_ + + `Qi J., Kerr Y., Chehbouni A., 1994. External factor consideration in vegetation index + development. Proc. of Physical Measurements and Signatures in Remote Sensing, + ISPRS, 723-730. `_ """ np.seterr(divide='ignore') ndvi = normalized_difference(bands) @@ -128,123 +228,196 @@ def msavi(bands, **kwargs): return (1 + dl) * (bands[1] - bands[0]) / denominator -def msavi2(bands, **kwargs): - """Modified Soil Adjusted Vegetation Index - -1 < MSAVI2 < 1 +def msavi2(bands : numpy.ndarray) -> numpy.ndarray : + """ + Compute the Modified Soil Adjusted Vegetation Index of the input data. + The coefficient ranges from -1 to 1 in each pixel. + + The function considers the bands of the input data in the following order : + Red, NIR (Near Infra-Red). + + .. math:: + MSAVI2 = (2 * NIR + 1)^2 - 8 (NIR - RED) Args: - bands: list of bands as a numpy ndarray + input_data (np.ndarray) : Numpy array of 3 dimensions (number of bands, number of lines, number of columns) + with number of bands > 1. Returns: - The numpy array with the results + Numpy array of the size (number of lines, number of columns) containing the computed MSAVI. """ np.seterr(divide='ignore', invalid='ignore') dsqrt = (2. * bands[1] + 1) ** 2 - 8 * (bands[1] - bands[0]) return (2. * bands[1] + 1) - np.sqrt(dsqrt) -def ipvi(bands, **kwargs): - """Infrared Percentage Vegetation Index - 0 < IPVI < 1 +def ipvi(bands : numpy.ndarray) -> numpy.ndarray : + """ + Compute the Infrared Percentage Vegetation Index of the input data. + The coefficient ranges from 0 to 1 in each pixel. + + The function considers the bands of the input data in the following order : + Red, NIR (Near Infra-Red). + + .. math:: + IPVI = \\frac{NIR}{NIR + RED} Args: - bands: list of bands as a numpy ndarray + input_data (np.ndarray) : Numpy array of 3 dimensions (number of bands, number of lines, number of columns) + with number of bands > 1. Returns: - The numpy array with the results + Numpy array of size (number of lines, number of columns) containing the computed IPVI. + + References: + `Crippen, R. E. 1990. Calculating the Vegetation Index Faster, Remote Sensing of + Environment, vol 34., pp. 71-73. `_ """ np.seterr(divide='ignore') return bands[1] / (bands[1] + bands[0]) -def evi(bands, **kwargs): - """Enhanced vegetation index +def evi(bands : np.ndarray) -> numpy.ndarray : + """ + Compute the Enhanced vegetation index of the input data. + The coefficient ranges from -1 to 1 in each pixel. + + The function considers the bands of the input data in the following order : + Red, NIR (Near Infra-Red), Blue. Args: - bands: list of bands as a numpy ndarray + input_data (np.ndarray) : Numpy array of 3 dimensions (number of bands, number of lines, number of columns) + with number of bands > 2. Returns: - The numpy array with the results + Numpy array of the size (number of lines, number of columns) containing the computed EVI. + + .. math:: + EVI = \\frac{G (NIR - RED)} {NIR + C1 * RED - C2 * BLUE + L} + + With : + - $L$ : Canopy background adjustment term, it reduces the influence of soil brightness. + - $C1$, $C2$ : Coefficients that correct the influence of aerosol. + - $G$ : A gain factor. The greater is G, the more the EVI is sensitive to vegetation changes. """ - np.seterr(divide='ignore') + np.seterr(divide='ignore') #Ignore divisions by zero return 2.5 * (bands[1] - bands[0]) / ((bands[1] + 6.0 * bands[0] - 7.5 * bands[2]) + 1.0) -def redness_index(bands, **kwargs): - """Redness Index +def redness_index(bands : np.ndarray) -> numpy.ndarray : + """ + Compute the Redness Index of the input data. + + .. math:: + RI = \\frac{RED^2} {GREEN^3} + + The function considers the bands of the input data in the following order : + Red, Green. Args: - bands: list of bands as a numpy ndarray + input_data (np.ndarray) : Numpy array of 3 dimensions (number of bands, number of lines, number of columns) + with number of bands > 1. Returns: - The numpy array with the results + Numpy array of size (number of lines, number of columns) containing the computed Redness Index. """ np.seterr(divide='ignore') return bands[0] ** 2 / bands[1] ** 3 -def brightness_index(bands, **kwargs): - """Brightness Index +def brightness_index(bands : np.ndarray) -> numpy.ndarray : + """ + Compute the Brightness Index of the input data. + + The function considers the first 2 bands of the input data to be Red and Green. + + .. math:: + BI = \\sqrt{ \\frac{RED^2 + GREEN^2} {2} } Args: - bands: list of bands as a numpy ndarray + input_data (np.ndarray) : Numpy array of 3 dimensions (number of bands, number of lines, number of columns) + with number of bands > 1. Returns: - The numpy array with the results + Numpy array of the size (number of lines, number of columns) containing the computed Brightness Index. """ np.seterr(invalid='ignore') bi = (bands[0] ** 2 + bands[1] ** 2) / 2 return np.sqrt(bi) -def brightness_index2(bands, **kwargs): - """Brilliance Index +def brightness_index2(bands : np.ndarray) -> numpy.ndarray : + """ + Compute the Brightness Index of the input data. + + The function considers the first 3 bands of the input data to be Red, Green, Blue. + + .. math:: + BI = \\sqrt{ \\frac{RED^2 + BLUE^2 + GREEN^2} {2} } Args: - bands: list of bands as a numpy ndarray + input_data (np.ndarray) : Numpy array of 3 dimensions (number of bands, number of lines, number of columns) + with number of bands > 2. Returns: - The numpy array with the results + Numpy array of the size (number of lines, number of columns) containing the computed Brightness Index. """ np.seterr(invalid='ignore') bi2 = (bands[0] ** 2 + bands[1] ** 2 + bands[2] ** 2) / 3 return np.sqrt(bi2) -def speed(data0, data1, interval, **kwargs): - """Compute speed for input data +def speed(data0 : np.ndarray, data1 : np.ndarray, interval : float) -> numpy.ndarray : + """ + Compute the speed of the input data based on the difference between two time points. Args: - data0 (np.ndarray): band value at first date - data1 (np.ndarray): band value at second date - interval (float): time interval between first and second dates + data0 (numpy.ndarray): Numpy array containing the band value(s) at the first date. + Shape must be (number_of_lines, number_of_columns). + + data1 (numpy.ndarray): Numpy array containing the band value(s) at the second date. + Shape must match `data0`. + + interval (float): Time interval (in the same units as the timestamps of the input data) + between the first and second dates. Returns: - The numpy array with the results + numpy.ndarray: Numpy array of shape (number_of_lines, number_of_columns) + containing the computed speed of the sequence. The values represent + the change in band values per unit time. + + Raises: + ValueError: If `data0` and `data1` do not have the same shape, or if `interval` is zero. """ return (data1 - data0) / interval -def interpolated_timeseries(dates, series, output_dates, nodata): - """Interpolate a timeseries of data. Dates and series must - be sorted in ascending order. +def interpolated_timeseries(dates : numpy.ma.masked_array, series : numpy.ma.masked_array, output_dates : numpy.array, nodata) -> numpy.ndarray: + """ + Interpolate a timeseries of data. Dates and series must be sorted in ascending order. Args: - dates (mumpy.masked_array): - List of dates (timestamps) of the given series of images - series ([numpy.masked_array]): - List of 3-dims numpy masked_array containing the raster bands - at every dates - output_dates ([numpy.array]): - The dates (timestamps) of the rasters to generate - nodata: - No data value to use + dates (numpy.ma.masked_array): A masked array of timestamps (dates) corresponding to + the input series. Should be in ascending order. + + series (numpy.ma.masked_array): A list of 3D masked arrays, each with shape + (bands, height, width), containing the raster data + for each timestamp in `dates`. + + output_dates (numpy.array): A 1D array of timestamps for which to generate the interpolated + rasters. + + nodata (float): Value to use for pixels where input data is NaN or missing. Returns: - numpy.ndarray: the numpy array of the rasters, its shape is - (time, bands, height, width) + numpy.ndarray: A 4D numpy array of shape (time, bands, height, width), containing + the interpolated raster data for each output date. If there are no valid + data points for a specific pixel, the corresponding pixel will be filled with `nodata`. + + Raises: + ValueError: If `series` is empty, or if `dates` and `series` dimensions do not match. """ - # stacked input data: shape is time x band x height x width + #Create stack, an array of dimension time x band x height x width from a list of band x height x width arrays stack = ma.stack(series) stack_shape = stack.shape # flatten the stacked data: shape is pixel x time @@ -270,24 +443,28 @@ def interpolated_timeseries(dates, series, output_dates, nodata): -1, stack_shape[1], stack_shape[2], stack_shape[3]) -def _local_sum(data: np.ndarray, kernel_width: int): - """Compute the local sum of an image of shape width x height. - on a kernel of size: size x size. Output image has a shape of - (width - size) x (height - size) +def _local_sum(data : np.ndarray, kernel_width: int) -> numpy.ndarray : + """ + Computes the local sums of the input data using a sliding window defined by the kernel size. + Each element in the output is the sum of the pixels within the specified kernel size window. Args: - data (np.ndarray): - 2 or 3 dimension ndarray or maskedarray. If array has 3 dimensions, the - local_sum is computed for the last 2 dims (we consider - first dim as band list) - kernel_width (int): - Kernel size to compute the local sum + input_data (np.ndarray): A 3D numpy array of shape (1, number_of_lines, number_of_columns) + containing the Digital Height Model (DHM). + The function only accepts arrays with one band. + + kernel_size (int): The size of the sliding window used to compute the local sum. + + If kernel_size = 1 : The output array equals to the input + Otherwise : Sums the pixels belonging to a sliding window of size radius * radius (with radius = (kernel_width + 1) // 2) + The top-left pixel of the window is the current pixel Returns: - np.ndarray: - Output data with same shape as input data. Computed data - have a size minored by the kernel_size and are centered - in the output shape + Numpy array of the size of input_data containing the computed local sums. Computed data have a size minored by the kernel_size + and are centered in the output shape. + + Raises: + ValueError: If `input_data` does not have 3 dimensions or if the first dimension is not of size 1. """ if kernel_width == 1: output = data.copy() @@ -327,51 +504,76 @@ def _local_sum(data: np.ndarray, kernel_width: int): return output.astype(data.dtype) -def median(input_data, **kwargs): - """Median filter computed using scipy.ndimage.median_filter +def median(input_data : np.ndarray, kernel_size : int) -> numpy.ndarray : + """ + Applies a Median Filter to the input data using `scipy.ndimage.median_filter `_. + The filter computes the median of the values contained within a sliding window determined by the kernel size. Args: - input_data: list of bands as a numpy ndarray of dims 3. - kwargs : parameters of the computing: kernel_size + input_data (np.ndarray): A 3D numpy array of shape (1, number_of_lines, number_of_columns) + containing the Digital Height Model (DHM). The function only accepts arrays with one band. + + kernel_size (int): The size of the sliding window (kernel) used to compute the median. Returns: - The numpy array with the results + np.ndarray: A numpy array of the same shape as `input_data`, containing the filtered data with the median values computed in the specified kernel. + + Raises: + ValueError: If `input_data` does not have 3 dimensions or if the first dimension is not of size 1, + or if `kernel_size` is not a positive odd integer. """ if len(input_data.shape) != 3: raise ValueError("adaptive_gaussian only accepts 3 dims numpy arrays") - kernel_size = kwargs.get('kernel_size', 8) + #kernel_size = kwargs.get('kernel_size', 8) output = ndimage.median_filter(input_data, size=(1, kernel_size, kernel_size)) return output -def local_sum(input_data, **kwargs): - """Local sum computed using integral image +def local_sum(input_data : np.ndarray, kernel_size : int = 8) -> numpy.ndarray : + """ + Computes the local sums of the input data using a sliding window defined by the kernel size. + Each element in the output is the sum of the pixels within the specified kernel size window. Args: - bands: list of bands as a numpy ndarray of dims 2 or 3. - kwargs : parameters of the computing: kernel_size + input_data (np.ndarray): A 3D numpy array of shape (1, number_of_lines, number_of_columns) + containing the Digital Height Model (DHM). + The function only accepts arrays with one band. + + kernel_size (int): The size of the sliding window used to compute the local sum. Returns: - The numpy array with the results + np.ndarray: A numpy array of the same size as `input_data` containing the computed local sums. + + Raises: + ValueError: If `input_data` does not have 3 dimensions or if the first dimension is not of size 1. """ - kernel_size = kwargs.get('kernel_size', 8) + + #kernel_size = kwargs.get('kernel_size', 8) # compute local sum of band pixels output = _local_sum(input_data, kernel_size) return output -def local_mean(input_data, **kwargs): - """Local mean computed using integral image +def local_mean(input_data : np.ndarray, kernel_size : int = 8) -> numpy.ndarray : + """ + Computes the local means of the input data using a sliding window defined by the kernel size. + Each element in the output is the mean of the pixels within the specified kernel size window. Args: - bands: list of bands as a numpy ndarray of dims 2 or 3. - kwargs : parameters of the computing: kernel_size + input_data (np.ndarray): A 3D numpy array of shape (1, number_of_lines, number_of_columns) + containing the Digital Height Model (DHM). + The function only accepts arrays with one band. + + kernel_size (int): The size of the sliding window used to compute the local mean. Returns: - The numpy array with the results + np.ndarray: A numpy array of the same size as `input_data` containing the computed local means. + + Raises: + ValueError: If `input_data` does not have 3 dimensions or if the first dimension is not of size 1. """ - kernel_size = kwargs.get('kernel_size', 8) + #kernel_size = kwargs.get('kernel_size', 8) # compute local sum of band pixels output = _local_sum(input_data, kernel_size) # compute local sum of band mask: number of valid pixels @@ -384,23 +586,35 @@ def local_mean(input_data, **kwargs): return np.divide(output, valid, out=np.zeros_like(output), where=valid != 0) -def adaptive_gaussian(input_data, **kwargs): - """Adaptive Gaussian Filter +def adaptive_gaussian(input_data : np.ndarray, kernel_size : int = 8, sigma : int = 1) -> numpy.ndarray : + """ + Applies an Adaptive Gaussian Filter to the input data that smoothes the input while preserving edges. Args: - bands: list of bands as a numpy ndarray of dims 3. First dimension muse be of size 1 - kwargs : parameters of the computing: kernel_size and sigma + input_data (np.ndarray): A 3D numpy array of shape (1, number_of_lines, number_of_columns) + containing the Digital Height Model (DHM). + The function only accepts arrays with one band. + + kernel_size (int): The size of the kernel used for the adaptive filtering. Default is 8. + + sigma (int): The standard deviation of the Gaussian distribution, which controls the level of smoothing. + Default is 1. Returns: - The numpy array with the results + np.ndarray: A numpy array of the same shape as `input_data`, containing the filtered data. + + Raises: + ValueError: If `input_data` does not have 3 dimensions or if the first dimension is not of size 1. """ if len(input_data.shape) != 3: raise ValueError("adaptive_gaussian only accepts 3 dims numpy arrays") if input_data.shape[0] != 1: raise ValueError("adaptive_gaussian only accepts numpy arrays with first dim of size 1") + ''' kernel_size = kwargs.get('kernel_size', 8) sigma = kwargs.get('sigma', 1) + ''' dtype = input_data.dtype w_1 = (input_data[0, :, :-2] - input_data[0, :, 2:]) ** 2 @@ -416,31 +630,50 @@ def adaptive_gaussian(input_data, **kwargs): return out -def svf(input_data, **kwargs): - """Sky View Factor computing. The input data consist in a Digital Height Model. +def svf(input_data : np.ndarray, radius : int = 8, directions : int = 12, resolution : float = 0.5, altitude = None) -> np.ndarray: + """ + Computes the Sky View Factor (SVF), which represents the fraction of the visible sky from each point in a Digital Height Model (DHM). + + More information about the Sky View Factor can be found `here `_. Args: - bands: list of bands as a numpy ndarray of dims 3. First dimension is of size 1. - kwargs: parameters of the computing: radius, directions, resolution and altitude. + input_data (np.ndarray): A 3D numpy array of shape (1, number_of_lines, number_of_columns) containing the Digital Height Model (DHM). + The function only accepts arrays with one band (the first dimension must be 1). + + radius (int): The maximum distance (in pixels) around each point to evaluate the horizontal elevation angle. Default is 8. + + direction (int): The number of discrete directions to compute the vertical angle. Default is 12. + + resolution (float): The spatial resolution of the input data in meters. Default is 0.5. + + altitude (Optional[np.ndarray]): A reference altitude to use for computing the SVF. If not specified, SVF is computed using the elevation of each point. Returns: - The numpy array with the results + np.ndarray: A numpy array of the same size as `input_data`, containing the Sky View Factor for each point, + where values range from 0 (no visible sky) to 1 (full sky visibility). + + Raises: + ValueError: If `input_data` does not have 3 dimensions or if the first dimension is not of size 1. + """ if len(input_data.shape) != 3: raise ValueError("svf only accepts 3 dims numpy arrays") if input_data.shape[0] != 1: raise ValueError("svf only accepts numpy arrays with first dim of size 1") + nb_directions = directions + ''' radius = kwargs.get('radius', 8) nb_directions = kwargs.get('directions', 12) resolution = kwargs.get('resolution', 0.5) altitude = kwargs.get('altitude', None) - + ''' # initialize output shape = input_data.shape out = np.zeros(shape, dtype=np.float32) # prevent nodata problem + # change the NaN in the input array to 0 input_band = np.nan_to_num(input_data[0], copy=False, nan=0) # compute directions @@ -471,63 +704,21 @@ def svf(input_data, **kwargs): return out -def hillshade(input_data, **kwargs): - """Hillshades computing. The input data consist in a Digital Height Model. - - Args: - bands: list of bands as a numpy ndarray of dims 3. First dimension is of size 1. - kwargs: parameters of the computing: elevation, azimuth, radius and resolution - - Returns: - The numpy array with the results +def _bresenham_line(theta : int, radius : int) -> tuple : """ - if len(input_data.shape) != 3: - raise ValueError("hillshade only accepts 3 dims numpy arrays") - if input_data.shape[0] != 1: - raise ValueError("hillshade only accepts numpy arrays with first dim of size 1") + Implementation of the `Bresenham's line algorithm `_ - elevation = np.radians(kwargs.get('elevation', 0.0)) - azimuth = kwargs.get('azimuth', 0.0) - radius = kwargs.get('radius', 8) - resolution = kwargs.get('resolution', 0.5) - - # initialize output - shape = input_data.shape - out = np.zeros(shape, dtype=bool) - - # prevent nodata problem - input_band = np.nan_to_num(input_data[0], copy=False, nan=0) + This function generates points along a line from the origin (0, 0) based on the given angle (theta) + and length (radius). - # compute direction - axe = _bresenham_line(180 - azimuth, radius) - - # identify the largest elevation in the radius - view = input_band[radius: shape[1] - radius, radius: shape[2] - radius] - ratios = np.zeros((shape[1] - 2 * radius, shape[2] - 2 * radius), dtype=np.float32) - for x_tr, y_tr, r in axe: - new_ratios = input_band[radius + x_tr: shape[1] - radius + x_tr, - radius + y_tr: shape[2] - radius + y_tr] - view - # tangente de l'angle - new_ratios /= (r * resolution) - ratios = np.maximum(ratios, new_ratios) - - angles = np.arctan(ratios) - out[0, radius: shape[1] - radius, radius: shape[2] - radius] = angles > elevation - - return out - - -def _bresenham_line(theta, radius): - """Implementation of the Bresenham's line algorithm: - https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm - - Params: - theta: theta angle (in degrees) - radius: size of the line + Args: + theta (int): The angle of the line in degrees (0 degrees points to the right, 90 degrees points up). + radius (int): The length of the line in units. If radius is less than or equal to zero, an empty list will be returned. Returns: - Tuple with the coordinates of the line points from point (0, 0) - ((0, 0) is not included). + list: A list of tuples representing the coordinates of the line points in the format + (x, y, r), where (x, y) are the coordinates of the point and r is the distance + from the origin to that point. The origin point (0, 0) is not included. """ x, y = 0, 0 dx = math.cos(math.radians(theta)) @@ -560,3 +751,66 @@ def _bresenham_line(theta, radius): pts.append((x, y, r)) return pts + + +def hillshade(input_data : np.ndarray, elevation : float = 0.0, azimuth : float = 0.0, radius : int = 8, resolution : float = 0.5) -> numpy.ndarray : + """ + Computes a mask of cast shadows in a Digital Height Model (DHM). + + This function calculates the shadows based on the specified elevation and azimuth angles, + and returns a mask indicating where shadows are cast. + + Args: + input_data (np.ndarray): A 3D numpy array of shape (1, number_of_lines, number_of_columns) containing the Digital Height Model (DHM). + The function only accepts arrays with one band. + + elevation (float): The angle (in degrees) between the horizon and the line of sight from an observer to the satellite. + + azimuth (float): The angle (in degrees) between true north and the projection of the satellite's position onto the horizontal plane, + measured in a clockwise direction. + + radius (int): The radius around each pixel to consider when calculating shadows. + + resolution (float): The spatial resolution of the input data, used for scaling calculations. + + Returns: + np.ndarray: A boolean numpy array of the same size as `input_data`, indicating the mask of cast shadows, + where True represents shadowed areas and False represents illuminated areas. + + Raises: + ValueError: If the input_data does not have 3 dimensions or if the first dimension is not of size 1. + """ + if len(input_data.shape) != 3: + raise ValueError("hillshade only accepts 3 dims numpy arrays") + if input_data.shape[0] != 1: + raise ValueError("hillshade only accepts numpy arrays with first dim of size 1") + ''' + elevation = np.radians(kwargs.get('elevation', 0.0)) + azimuth = kwargs.get('azimuth', 0.0) + radius = kwargs.get('radius', 8) + resolution = kwargs.get('resolution', 0.5) + ''' + # initialize output + shape = input_data.shape + out = np.zeros(shape, dtype=bool) + + # prevent nodata problem + input_band = np.nan_to_num(input_data[0], copy=False, nan=0) + + # compute direction + axe = _bresenham_line(180 - azimuth, radius) + + # identify the largest elevation in the radius + view = input_band[radius: shape[1] - radius, radius: shape[2] - radius] + ratios = np.zeros((shape[1] - 2 * radius, shape[2] - 2 * radius), dtype=np.float32) + for x_tr, y_tr, r in axe: + new_ratios = input_band[radius + x_tr: shape[1] - radius + x_tr, + radius + y_tr: shape[2] - radius + y_tr] - view + # tangente de l'angle + new_ratios /= (r * resolution) + ratios = np.maximum(ratios, new_ratios) + + angles = np.arctan(ratios) + out[0, radius: shape[1] - radius, radius: shape[2] - radius] = angles > elevation + + return out \ No newline at end of file diff --git a/src/eolab/rastertools/processing/rasterproc.py b/src/eolab/rastertools/processing/rasterproc.py index f5fde30..c708fb4 100644 --- a/src/eolab/rastertools/processing/rasterproc.py +++ b/src/eolab/rastertools/processing/rasterproc.py @@ -5,6 +5,7 @@ """ from typing import List, Callable, Union +import numpy import numpy as np from eolab.rastertools.processing import algo @@ -12,7 +13,11 @@ class RasterProcessing: - """This class defines a processing on a raster image. + """ + Defines a processing algorithm for raster image data. + + This class allows users to define custom processing operations on raster data + by specifying an algorithm, data types, compression, and other parameters. """ def __init__(self, name: str, @@ -70,7 +75,7 @@ def name(self) -> str: @property def algo(self) -> Callable: - """Processing algo that is called on a multidimensional array of data""" + """Process an algo that is called on a multidimensional array of data""" return self._algo @property @@ -101,7 +106,7 @@ def compress(self) -> str: @property def nbits(self) -> int: - """bits size of the generated data""" + """Bits size of the generated data""" return self._nbits @property @@ -151,9 +156,8 @@ def with_arguments(self, arguments): Args: arguments (Dict[str, Dict]): Dictionary where the keys are the arguments' names and the values are dictionaries - of arguments' properties as defined in ArgumentParser.add_argument - see - https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser. - The properties dictionaries are used to configure the command line 'rastertools'. + of arguments' properties as defined in `ArgumentParser.add_argument `_ . + The properties dictionaries are used to configure the command line 'rastertools'.* The possible keys are: action, nargs, const, default, type, choices, required, help, metavar and dest @@ -173,7 +177,7 @@ def configure(self, argsdict): [setattr(self, argument, argsdict[argument]) for argument in self.arguments if argument in argsdict] - def compute(self, input_data: Union[List[np.ndarray], np.ndarray]): + def compute(self, input_data: Union[List[np.ndarray], np.ndarray]) -> numpy.ndarray: """Compute the output from the different bands of the input data. Output data are supposed to be the same size as input_data. @@ -183,7 +187,7 @@ def compute(self, input_data: Union[List[np.ndarray], np.ndarray]): with all bands Returns: - Output data + Numpy array or list of numpy arrays of the size of input data """ if self.algo is not None: argparameters = {arg: getattr(self, arg, None) for arg in self.arguments} @@ -212,7 +216,7 @@ def channels(self) -> List[BandChannel]: """List of channels necessary to compute the radiometric indice""" return self._channels - def with_channels(self, channels: List[BandChannel]): + def with_channels(self, channels: List[BandChannel]) : """Set the BandChannels necessary to compute the radiometric indice Args: diff --git a/src/eolab/rastertools/processing/sliding.py b/src/eolab/rastertools/processing/sliding.py index 0c5c261..ccc59e7 100644 --- a/src/eolab/rastertools/processing/sliding.py +++ b/src/eolab/rastertools/processing/sliding.py @@ -26,24 +26,34 @@ def compute_sliding(input_image: str, output_image: str, rasterprocessing: RasterProcessing, window_size: tuple = (1024, 1024), window_overlap: int = 0, pad_mode: str = "edge", bands: List[int] = None): - """Run a given raster processing on an input image and produce the output image + """ + Apply a sliding window raster processing operation on an input image and save the result. + + This function processes a raster image in small sliding windows, allowing efficient + memory management for large datasets by processing chunks. The specified `rasterprocessing` + operation is applied to each window, with options for padding and overlapping windows. Args: - input_image (str): - Path of the raster to compute - output_image (str): - Path of the output raster image - rasterprocessing ([:obj:`eolab.rastertools.processing.RasterProcessing`]): - Processing to apply on input image - window_size (tuple(int, int), optional, default=(1024, 1024)): - Size of windows for splitting the processed image in small parts - window_overlap (int, optional, default=0): - Number of pixels in the window that shall overlap previous (or next) window - pad_mode (str, optional, default="edge"): - Mode for padding data around the windows that are on the edge of the image - (See https://numpy.org/doc/stable/reference/generated/numpy.pad.html) - bands ([int], optional, default=None): - List of bands to process. None if all bands shall be processed + input_image (str): Path to the input raster image file to be processed. + output_image (str): Path to save the output raster image after processing. + rasterprocessing (RasterProcessing): A processing object defining the algorithm and + parameters to apply on each window of the input image. + window_size (tuple(int, int), optional): Size of each window for processing, + default is (1024, 1024). + window_overlap (int, optional): Number of pixels to overlap between consecutive windows, + default is 0. + pad_mode (str, optional, default="edge"): Padding mode for the edges of the windows, default is "edge". + Refer to `numpy.pad `_ """ @@ -91,8 +91,8 @@ class Radioindice(Rastertool, Windowable): rvi = \\frac{nir}{red} References: - Jordan C.F., 1969. Derivation of leaf area index from quality of light on the forest - floor. Ecology 50:663-666 + `Jordan C.F., 1969. Derivation of leaf area index from quality of light on the forest + floor. Ecology 50:663-666 `_ """ # Vegetation indices: pvi @@ -105,8 +105,8 @@ class Radioindice(Rastertool, Windowable): pvi = (nir - 0.90893 * red - 7.46216) * 0.74 References: - Richardson A.J., Wiegand C.L., 1977. Distinguishing vegetation from soil background - information. Photogramm Eng Rem S 43-1541-1552 + `Richardson A.J., Wiegand C.L., 1977. Distinguishing vegetation from soil background + information. Photogramm Eng Rem S 43-1541-1552 `_ """ # Vegetation indices: savi @@ -119,7 +119,7 @@ class Radioindice(Rastertool, Windowable): savi = \\frac{(nir - red) * (1. + 0.5)}{nir + red + 0.5} References: - Huete A.R., 1988. A soil-adjusted vegetation index (SAVI). Remote Sens Environ 25:295-309 + `Huete A.R., 1988. A soil-adjusted vegetation index (SAVI). Remote Sens Environ 25:295-309 `_ """ # Vegetation indices: tsavi @@ -132,9 +132,9 @@ class Radioindice(Rastertool, Windowable): tsavi = \\frac{0.7 * (nir - 0.7 * red - 0.9)}{0.7 * nir + red + 0.08 * (1 + 0.7^2)} References: - Baret F., Guyot G., Major D., 1989. TSAVI: a vegetation index which minimizes soil + `Baret F., Guyot G., Major D., 1989. TSAVI: a vegetation index which minimizes soil brightness effects on LAI or APAR estimation. 12th Canadian Symposium on Remote - Sensing and IGARSS 1990, Vancouver, Canada, 07/10-14 + Sensing and IGARSS 1990, Vancouver, Canada, 07/10-14. `_ """ # Vegetation indices: msavi @@ -153,12 +153,12 @@ class Radioindice(Rastertool, Windowable): \\end{eqnarray} References: - Qi J., Chehbouni A., Huete A.R., Kerr Y.H., 1994. Modified Soil Adjusted Vegetation - Index (MSAVI). Remote Sens Environ 48:119-126 + `Qi J., Chehbouni A., Huete A.R., Kerr Y.H., 1994. Modified Soil Adjusted Vegetation + Index (MSAVI). Remote Sens Environ 48:119-126 `_ - Qi J., Kerr Y., Chehbouni A., 1994. External factor consideration in vegetation index + `Qi J., Kerr Y., Chehbouni A., 1994. External factor consideration in vegetation index development. Proc. of Physical Measurements and Signatures in Remote Sensing, - ISPRS, 723-730. + ISPRS, 723-730. `_ """ # Vegetation indices: msavi2 @@ -184,8 +184,8 @@ class Radioindice(Rastertool, Windowable): ipvi = \\frac{nir}{nir + red} References: - Crippen, R. E. 1990. Calculating the Vegetation Index Faster, Remote Sensing of - Environment, vol 34., pp. 71-73. + `Crippen, R. E. 1990. Calculating the Vegetation Index Faster, Remote Sensing of + Environment, vol 34., pp. 71-73. `_ """ # Vegetation indices: evi @@ -272,7 +272,7 @@ class Radioindice(Rastertool, Windowable): ndbi = RadioindiceProcessing("ndbi").with_channels( [BandChannel.nir, BandChannel.mir]) """Normalized Difference Built Up Index (nir, mir channels) - + .. math:: ndbi = \\frac{mir - nir}{mir + nir} @@ -455,9 +455,13 @@ def process_file(self, inputfile: str) -> List[str]: def compute_indices(input_image: str, image_channels: List[BandChannel], indice_image: str, indices: List[RadioindiceProcessing], window_size: tuple = (1024, 1024)): - """Compute the indices on the input image and produce a multiple bands + """ + Compute the indices on the input image and produce a multiple bands image (one band per indice) + The possible indices are the following : + ndvi, tndvi, rvi, pvi, savi, tsavi, msavi, msavi2, ipvi, evi, ndwi, ndwi2, mndwi, ndpi, ndti, ndbi, ri, bi, bi2 + Args: input_image (str): Path of the raster to compute diff --git a/src/eolab/rastertools/rastertools.py b/src/eolab/rastertools/rastertools.py index 1f8b23d..87500e9 100644 --- a/src/eolab/rastertools/rastertools.py +++ b/src/eolab/rastertools/rastertools.py @@ -41,7 +41,7 @@ def __init__(self): @property def outputdir(self) -> str: - """Output dir where to store results""" + """Path of the output directory where are stored the results""" return self._outputdir @property @@ -99,7 +99,7 @@ def process_files(self, inputfiles: List[str]): inputfiles ([str]): Input images to process Returns: - [str]: List of generated files + ([str]) The list of the generated files """ all_outputs = [] for filename in inputfiles: @@ -109,6 +109,7 @@ def process_files(self, inputfiles: List[str]): # add a postprocessing call outputs = self.postprocess_files(inputfiles, all_outputs) + if outputs: all_outputs.extend(outputs) return all_outputs @@ -167,8 +168,10 @@ def window_size(self) -> int: @property def pad_mode(self) -> str: - """Mode for padding the image when windows are on the edge of the image - (See https://numpy.org/doc/stable/reference/generated/numpy.pad.html)""" + """ + Mode used to `pad `_ the image when the window is on the edge of the image + The mode can be self defined or among [constant (default), edge, linear_ramp, maximum, mean, median, minimum, reflect, symmetric, wrap, empty]. + """ return self._pad_mode def with_windows(self, window_size: int = 1024, pad_mode: str = "edge"): diff --git a/src/eolab/rastertools/zonalstats.py b/src/eolab/rastertools/zonalstats.py index 04e2451..43a0cee 100644 --- a/src/eolab/rastertools/zonalstats.py +++ b/src/eolab/rastertools/zonalstats.py @@ -38,7 +38,8 @@ class Zonalstats(Rastertool): - """Raster tool that computes zonal statistics of a raster product. + """ + Raster tool that computes zonal statistics of a raster product. """ supported_output_formats = { @@ -163,7 +164,7 @@ def valid_threshold(self) -> float: @property def area(self) -> bool: - """Whether to compute stats multiplied by the pixel area""" + """Whether to compute the statistics multiplied by the pixel area""" return self._area @property @@ -493,7 +494,7 @@ def compute_stats(self, raster: str, bands: List[int], geometries: gpd.GeoDataFrame, descr: List[str], date: str, area_square_meter: int) -> List[List[Dict[str, float]]]: - """Compute the stats + """Compute the statistics of the input data. [Minimum, Maximum, Mean, Standard deviation] Args: raster (str): @@ -511,8 +512,8 @@ def compute_stats(self, raster: str, bands: List[int], Area represented by a pixel Returns: - [[{str: float}]]: a list of list of dictionnaries. Dict associates - the stat names and the stat values. + list[list[dict]] + The dictionnary associates the name of the statistics to its value. """ _logger.info("Compute statistics") # Compute zonal statistics @@ -574,7 +575,7 @@ def __stats_to_geoms(self, statistics_data: List[List[Dict[str, float]]], Returns: GeoDataFrame: The updated geometries with statistics saved in metadata of the following form: b{band_number}.{metadata_name} where metadata_name is - sucessively the band name, the date and the stats names (min, mean, max, median, std) + successively the band name, the date and the statistics names (min, mean, max, median, std) """ prefix = self.prefix or [""] * len(bands) for i, band in enumerate(bands): diff --git a/src/rastertools.egg-info/PKG-INFO b/src/rastertools.egg-info/PKG-INFO index 4a06ec9..7b331a7 100644 --- a/src/rastertools.egg-info/PKG-INFO +++ b/src/rastertools.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: rastertools -Version: 0.5.0.post1.dev66+g24a07c4.d20240523 +Version: 0.6.1.post1.dev0+gbedb844.d20241022 Summary: Compute radiometric indices and zonal statistics on rasters Home-page: https://github.com/cnes/rastertools Author: Olivier Queyrut diff --git a/src/rastertools.egg-info/SOURCES.txt b/src/rastertools.egg-info/SOURCES.txt index 6fcb80a..1ac1b3c 100644 --- a/src/rastertools.egg-info/SOURCES.txt +++ b/src/rastertools.egg-info/SOURCES.txt @@ -1,7 +1,4 @@ -.coveragerc -.dockerignore .gitignore -.readthedocs.yml AUTHORS.rst CHANGELOG.rst Dockerfile @@ -70,6 +67,18 @@ src/eolab/rastertools/tiling.py src/eolab/rastertools/timeseries.py src/eolab/rastertools/utils.py src/eolab/rastertools/zonalstats.py +src/eolab/rastertools/__pycache__/__init__.cpython-38.pyc +src/eolab/rastertools/__pycache__/filtering.cpython-38.pyc +src/eolab/rastertools/__pycache__/hillshade.cpython-38.pyc +src/eolab/rastertools/__pycache__/main.cpython-38.pyc +src/eolab/rastertools/__pycache__/radioindice.cpython-38.pyc +src/eolab/rastertools/__pycache__/rastertools.cpython-38.pyc +src/eolab/rastertools/__pycache__/speed.cpython-38.pyc +src/eolab/rastertools/__pycache__/svf.cpython-38.pyc +src/eolab/rastertools/__pycache__/tiling.cpython-38.pyc +src/eolab/rastertools/__pycache__/timeseries.cpython-38.pyc +src/eolab/rastertools/__pycache__/utils.cpython-38.pyc +src/eolab/rastertools/__pycache__/zonalstats.cpython-38.pyc src/eolab/rastertools/cli/__init__.py src/eolab/rastertools/cli/filtering.py src/eolab/rastertools/cli/hillshade.py @@ -79,17 +88,36 @@ src/eolab/rastertools/cli/svf.py src/eolab/rastertools/cli/tiling.py src/eolab/rastertools/cli/timeseries.py src/eolab/rastertools/cli/zonalstats.py +src/eolab/rastertools/cli/__pycache__/__init__.cpython-38.pyc +src/eolab/rastertools/cli/__pycache__/filtering.cpython-38.pyc +src/eolab/rastertools/cli/__pycache__/hillshade.cpython-38.pyc +src/eolab/rastertools/cli/__pycache__/radioindice.cpython-38.pyc +src/eolab/rastertools/cli/__pycache__/speed.cpython-38.pyc +src/eolab/rastertools/cli/__pycache__/svf.cpython-38.pyc +src/eolab/rastertools/cli/__pycache__/tiling.cpython-38.pyc +src/eolab/rastertools/cli/__pycache__/timeseries.cpython-38.pyc +src/eolab/rastertools/cli/__pycache__/zonalstats.cpython-38.pyc src/eolab/rastertools/processing/__init__.py src/eolab/rastertools/processing/algo.py src/eolab/rastertools/processing/rasterproc.py src/eolab/rastertools/processing/sliding.py src/eolab/rastertools/processing/stats.py src/eolab/rastertools/processing/vector.py +src/eolab/rastertools/processing/__pycache__/__init__.cpython-38.pyc +src/eolab/rastertools/processing/__pycache__/algo.cpython-38.pyc +src/eolab/rastertools/processing/__pycache__/rasterproc.cpython-38.pyc +src/eolab/rastertools/processing/__pycache__/sliding.cpython-38.pyc +src/eolab/rastertools/processing/__pycache__/stats.cpython-38.pyc +src/eolab/rastertools/processing/__pycache__/vector.cpython-38.pyc src/eolab/rastertools/product/__init__.py src/eolab/rastertools/product/rasterproduct.py src/eolab/rastertools/product/rastertype.py src/eolab/rastertools/product/rastertypes.json src/eolab/rastertools/product/vrt.py +src/eolab/rastertools/product/__pycache__/__init__.cpython-38.pyc +src/eolab/rastertools/product/__pycache__/rasterproduct.cpython-38.pyc +src/eolab/rastertools/product/__pycache__/rastertype.cpython-38.pyc +src/eolab/rastertools/product/__pycache__/vrt.cpython-38.pyc src/rastertools.egg-info/PKG-INFO src/rastertools.egg-info/SOURCES.txt src/rastertools.egg-info/dependency_links.txt @@ -113,6 +141,22 @@ tests/test_utils.py tests/test_vector.py tests/test_zonalstats.py tests/utils4test.py +tests/__pycache__/__init__.cpython-38.pyc +tests/__pycache__/cmptools.cpython-38.pyc +tests/__pycache__/conftest.cpython-38-pytest-8.0.0.pyc +tests/__pycache__/test_algo.cpython-38-pytest-8.0.0.pyc +tests/__pycache__/test_radioindice.cpython-38-pytest-8.0.0.pyc +tests/__pycache__/test_rasterproc.cpython-38-pytest-8.0.0.pyc +tests/__pycache__/test_rasterproduct.cpython-38-pytest-8.0.0.pyc +tests/__pycache__/test_rastertools.cpython-38-pytest-8.0.0.pyc +tests/__pycache__/test_rastertype.cpython-38-pytest-8.0.0.pyc +tests/__pycache__/test_speed.cpython-38-pytest-8.0.0.pyc +tests/__pycache__/test_stats.cpython-38-pytest-8.0.0.pyc +tests/__pycache__/test_tiling.cpython-38-pytest-8.0.0.pyc +tests/__pycache__/test_utils.cpython-38-pytest-8.0.0.pyc +tests/__pycache__/test_vector.cpython-38-pytest-8.0.0.pyc +tests/__pycache__/test_zonalstats.cpython-38-pytest-8.0.0.pyc +tests/__pycache__/utils4test.cpython-38.pyc tests/tests_data/COMMUNE_32001.dbf tests/tests_data/COMMUNE_32001.prj tests/tests_data/COMMUNE_32001.qpj @@ -141,7 +185,9 @@ tests/tests_data/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndwi.tif tests/tests_data/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip tests/tests_data/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_tar.tar tests/tests_data/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_targz.TAR.GZ +tests/tests_data/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_targz.TAR.GZ.properties tests/tests_data/SPOT6_2018_France-Ortho_NC_DRS-MS_SPOT6_2018_FRANCE_ORTHO_NC_GEOSUD_MS_82.tar.gz +tests/tests_data/SPOT6_2018_France-Ortho_NC_DRS-MS_SPOT6_2018_FRANCE_ORTHO_NC_GEOSUD_MS_82.tar.gz.properties tests/tests_data/additional_rastertypes.json tests/tests_data/grid.geojson tests/tests_data/listing.lst diff --git a/tests/test_algo.py b/tests/test_algo.py index 5071e55..f25d738 100644 --- a/tests/test_algo.py +++ b/tests/test_algo.py @@ -3,6 +3,7 @@ import numpy as np import numpy.ma as ma +import rasterio from eolab.rastertools.processing import algo @@ -12,6 +13,13 @@ def test_local_sum(): + """ + Test the local sum filter with varying kernel sizes. + + This function verifies that the local sum filter correctly applies to a + 5x5 matrix with kernel sizes ranging from 1 to 5, comparing each output + to expected results. + """ results = [ np.array( [[0, 1, 2, 3, 4], @@ -63,9 +71,12 @@ def test_local_sum(): assert (output[0] == results[i - 1]).all() - def test_local_mean(): + """ + Test the local mean filter with a kernel size of 2. + This test verifies the local mean filter by comparing its output on a 5x5 matrix to an expected result matrix. + """ result = np.array( [[1, 1.5, 2.5, 3.5, 4], [3, 3.5, 4.5, 5.5, 6], @@ -94,7 +105,8 @@ def test_local_mean(): ) mask = np.pad(mask, (radius, radius), mode="edge") - array = ma.array(band, mask=mask) + array = ma.array(band, mask=mask) # masks the band array + # ie. removes the first line and first column of band # output shape is array shape - kernel_width output = algo.local_mean(array, kernel_size=kernel_width) @@ -106,7 +118,13 @@ def test_local_mean(): def test_bresenham_line(): + """ + Test the Bresenham's line algorithm for angles from 0° to 360° in 15° increments. + This function verifies that the Bresenham line algorithm generates accurate + line coordinates for a given radius across multiple theta values. Each angle + is tested against an expected list of coordinates. + """ results = [ # 0° [(1, 0), (2, 0), (3, 0), (4, 0), (5, 0)], diff --git a/tests/test_radioindice.py b/tests/test_radioindice.py index ba2385f..acaff0c 100644 --- a/tests/test_radioindice.py +++ b/tests/test_radioindice.py @@ -19,6 +19,20 @@ def test_radioindice_process_file_merge(): + ''' + This function tests the Radioindice class's ability to generate a merged output file containing multiple indices. The indices generated + include: + + NDVI, TNDVI, RVI, PVI, SAVI, TSAVI, MSAVI, MSAVI2, IPVI, EVI, NDWI, NDWI2, + MNDVI, NDPI, NDTI, NDBI, RI, BI, BI2. + + The function compares the generated output to an expected output file. + + Asserts: + - The generated output file is named correctly and matches the expected filename. + + Clears the output directory at the end of the test. + ''' # create output dir and clear its content if any utils4test.create_outdir() @@ -36,7 +50,22 @@ def test_radioindice_process_file_merge(): utils4test.clear_outdir() -def test_radioindice_process_file_separate(compare, save_gen_as_ref): +def test_radioindice_process_file_separate(compare : bool, save_gen_as_ref : bool): + """ + Test the Radioindice class by generating individual files for each indice. + + This function verifies the generation of separate output files for NDVI and NDWI. + The results can be compared with reference files or saved as new references if desired. + + Parameters: + - compare (bool): If True, compares the generated files to reference files. + - save_gen_as_ref (bool): If True, saves the generated files as new reference files. + + Asserts: + - The output files match the reference files. + + Clears the output directory at the end of the test. + """ # create output dir and clear its content if any utils4test.create_outdir() @@ -66,6 +95,17 @@ def test_radioindice_process_file_separate(compare, save_gen_as_ref): def test_radioindice_process_files(): + ''' + Test the Radioindice class by processing multiple files and merging results. + + This function applies the NDVI and NDWI to a list of Sentinel-2 datasets. + The function generates a merged output file for each input file. Results are verified by comparing to tif files containing the expected results. + + Asserts: + - The generated output files match the expected names for merged indices. + + Clears the output directory at the end of the test. + ''' # create output dir and clear its content if any utils4test.create_outdir() @@ -86,6 +126,21 @@ def test_radioindice_process_files(): def test_radioindice_incompatible_indice_rastertype(caplog): + """ + Test handling of incompatible indices and raster types in the Radioindice class. + + This function verifies that the Radioindice class correctly handles cases where the + raster file lacks the required bands for a specified index. + + Parameters: + - caplog: pytest fixture for capturing log output within the test. + + Asserts: + - No output files are generated (output list is empty). + - An error log entry is recorded with details about the missing bands. + + Clears the output directory at the end of the test. + """ # create output dir and clear its content if any utils4test.create_outdir() diff --git a/tests/test_rasterproc.py b/tests/test_rasterproc.py index ad56008..95180be 100644 --- a/tests/test_rasterproc.py +++ b/tests/test_rasterproc.py @@ -16,16 +16,38 @@ def algo2D(bands): + """ + Apply a scaling factor to each band independently. + + Parameters: + bands (numpy.ndarray): A 2D array containing a single band of raster data. + + Returns: + numpy.ndarray: An array with each element scaled by a factor of 2. + """ out = 2. * bands return out def algo3D(bands): + """ + Apply a scaling factor to all bands simultaneously. + + Parameters: + bands (numpy.ndarray): A 3D array containing multiple bands of raster data. + + Returns: + numpy.ndarray: An array with each element in all bands scaled by a factor of 2. + """ out = 2. * bands return out def test_compute_sliding(): + """ + Test the compute_sliding function with 2D and 3D raster data. + It verifies that the computed output matches the expected transformation. + """ # create output dir and clear its content if any utils4test.create_outdir() diff --git a/tests/test_rasterproduct.py b/tests/test_rasterproduct.py index 83162d0..3f1657e 100644 --- a/tests/test_rasterproduct.py +++ b/tests/test_rasterproduct.py @@ -23,6 +23,21 @@ def test_rasterproduct_valid_parameters(): + """ + Test the initialization and properties of `RasterProduct` with valid parameters. + + This test case verifies the proper creation and expected properties of `RasterProduct` + objects from various supported file formats and structures: + - Sentinel-2 L1C archive with one file per band. + - SPOT6 archive with one file for all bands. + - Standard raster file with multiple channels. + + Assertions: + - `file` path, raster type, and channels match expected values. + - Band and mask files are correctly listed. + - Archive status and extracted metadata (e.g., date, tile, orbit, and satellite) match + expected values based on the input files. + """ # archive with one file per band basename = "S2B_MSIL1C_20191008T105029_N0208_R051_T30TYP_20191008T125041" file = Path( @@ -83,6 +98,18 @@ def test_rasterproduct_valid_parameters(): def test_rasterproduct_invalid_parameters(): + """ + Test the handling of invalid parameters when creating a `RasterProduct`. + + This test case verifies: + - Passing `None` as a file parameter raises a `ValueError`. + - Unrecognized raster type in input file raises a `ValueError`. + - Unsupported file types raise `ValueError` with appropriate error messages. + + Assertions: + - Each invalid parameter triggers a `ValueError` with a specific message indicating + the type of parameter issue. + """ with pytest.raises(ValueError) as exc: RasterProduct(None) assert "'file' cannot be None" in str(exc.value) @@ -99,6 +126,18 @@ def test_rasterproduct_invalid_parameters(): def test_create_product_S2_L2A_MAJA(compare, save_gen_as_ref): + """ + Test the creation and processing of a Sentinel-2 L2A MAJA `RasterProduct`. + + Parameters: + compare (bool): If True, compares generated files to reference files. + save_gen_as_ref (bool): If True, saves generated files as new reference files. + + Assertions: + - Generated file paths match expected paths. + - Comparison or saving of reference files completes without errors. + - Raster data can be opened without errors. + """ # create output dir and clear its content if any utils4test.create_outdir() @@ -146,6 +185,23 @@ def test_create_product_S2_L2A_MAJA(compare, save_gen_as_ref): def test_create_product_S2_L1C(compare, save_gen_as_ref): + """ + Test the creation and processing of a Sentinel-2 L1C `RasterProduct`. + + This test case verifies: + - Creation of a single file for the L1C product and generation of clipped output. + - Comparison of generated files against reference files or saving as new references. + - Loading the generated raster using `rasterio` to confirm proper creation. + + Parameters: + compare (bool): If True, compares generated files to reference files. + save_gen_as_ref (bool): If True, saves generated files as new reference files. + + Assertions: + - Generated file paths and metadata match expected values. + - Reference comparison or saving completes as expected. + - Raster data can be loaded and accessed without errors. + """ # create output dir and clear its content if any utils4test.create_outdir() @@ -177,6 +233,23 @@ def test_create_product_S2_L1C(compare, save_gen_as_ref): def test_create_product_S2_L2A_SEN2CORE(compare, save_gen_as_ref): + """ + Test the creation of a Sentinel-2 L2A SEN2CORE `RasterProduct`. + + This test case verifies: + - Creation of the raster and VRT files. + - Comparison of generated files with reference files or saving as new references if needed. + - Loading the generated VRT to ensure accessibility with `rasterio`. + + Parameters: + compare (bool): If True, compares generated files to reference files. + save_gen_as_ref (bool): If True, saves generated files as new reference files. + + Assertions: + - VRT file path and metadata match expected output. + - Reference file operations are successful. + - The VRT file can be accessed with `rasterio` without issues. + """ # create output dir and clear its content if any utils4test.create_outdir() @@ -206,6 +279,23 @@ def test_create_product_S2_L2A_SEN2CORE(compare, save_gen_as_ref): def test_create_product_SPOT67(compare, save_gen_as_ref): + """ + Test the creation of a SPOT6/7 `RasterProduct`. + + This test case verifies: + - Creation of the product using SPOT6 input archive. + - Comparison of generated files with reference files or saving as new references if specified. + - Loading the raster using `rasterio` to verify successful file generation. + + Parameters: + compare (bool): If True, compares generated files to reference files. + save_gen_as_ref (bool): If True, saves generated files as new reference files. + + Assertions: + - Output paths and contents match expected values. + - Reference file comparison and saving are correctly performed. + - Raster data opens without errors in `rasterio`. + """ # create output dir and clear its content if any utils4test.create_outdir() @@ -235,6 +325,19 @@ def test_create_product_SPOT67(compare, save_gen_as_ref): def test_create_product_special_cases(): + """ + Test special cases in `RasterProduct` creation, including in-memory, directory, and VRT handling. + + This test case covers: + - Creation of products in memory (with and without masks). + - Handling of product creation from VRT and directory inputs. + - Loading raster data via `rasterio` to ensure correct accessibility. + + Assertions: + - VRT and in-memory files are correctly created. + - Directory input processing and band masking work as expected. + - Raster files can be opened without errors in `rasterio`. + """ # SUPPORTED CASES # creation in memory (without masks) diff --git a/tests/test_rastertools.py b/tests/test_rastertools.py index 4973199..f9c54b4 100644 --- a/tests/test_rastertools.py +++ b/tests/test_rastertools.py @@ -769,7 +769,7 @@ def test_hillshade_command_line_default(): # elevation / azimuth are retrieved from https://www.sunearthtools.com/dp/tools/pos_sun.php argslist = [ # default case: hillshade at Toulouse the September, 21 solar noon - "-v hs --elevation 46.81 --azimuth 180.0 --resolution 0.5 -o tests/tests_out" + "-v hs --elevation 27.2 --azimuth 82.64 --resolution 0.5 -o tests/tests_out" " tests/tests_data/toulouse-mnh.tif", # default case: hillshade at Toulouse the June, 21, solar 6PM "-v hs --elevation 25.82 --azimuth 278.58 --resolution 0.5 -o tests/tests_out" @@ -802,7 +802,7 @@ def test_hillshade_command_line_errors(caplog): "-v hs --elevation 46.81 --azimuth 180.0 --resolution 0.5 -o tests/truc" " tests/tests_data/toulouse-mnh.tif", # missing required argument - "-v hs --elevation 46.81 --resolution 0.5 -o tests/tests_out" + "-v hs --elevation 46.81 --resolution 0.5 " " tests/tests_data/toulouse-mnh.tif", # input file has more than 1 band "-v hs --elevation 46.81 --azimuth 180.0 --resolution 0.5 -o tests/tests_out" diff --git a/tests/test_speed.py b/tests/test_speed.py index 9bbe0d2..f7deab5 100644 --- a/tests/test_speed.py +++ b/tests/test_speed.py @@ -16,7 +16,10 @@ __refdir = utils4test.get_refdir("test_radioindice/") -def test_speed_process_files(compare, save_gen_as_ref): +def test_speed_process_files(compare : bool, save_gen_as_ref : bool): + """ + + """ # create output dir and clear its content if any utils4test.create_outdir() diff --git a/tests/utils4test.py b/tests/utils4test.py index 027acb7..ccc59eb 100644 --- a/tests/utils4test.py +++ b/tests/utils4test.py @@ -47,24 +47,24 @@ def copy_to_ref(files, refdir): def basename(infile): - """function to get basename of file""" + """ + Function to get basename of file + """ file = Path(infile) if isinstance(infile, str) else infile suffix = len("".join(file.suffixes)) return file.name if suffix == 0 else file.name[:-suffix] -def cmpfiles(a, b, common, tolerance=1e-9): - """Compare common files in two directories. - - a, b -- directory names - common -- list of file names found in both directories - shallow -- if true, do comparison based solely on stat() information +def cmpfiles(a : str, b : str, common : list, tolerance : float =1e-9) -> tuple: + """ + Compare common files in two directories. - Returns a tuple of three lists: - files that compare equal - files that are different - filenames that aren't regular files. + Args: + a, b (str) : Directory names + common (list) : List of file names found in both directories + Returns: + Tuple of three lists ( [files that are the same], [files that differs], [filenames that aren't regular files] ) """ res = ([], [], []) for x in common: @@ -75,6 +75,9 @@ def cmpfiles(a, b, common, tolerance=1e-9): def _cmp(gld, new, tolerance): + """ + + """ ftype = os.path.splitext(gld)[-1].lower() cmp = cmptools.CMP_FUN[ftype] try: From 8d08ba53fb3b8bd01969e248b853a292629fd170 Mon Sep 17 00:00:00 2001 From: cadauxe Date: Tue, 5 Nov 2024 14:31:05 +0100 Subject: [PATCH 02/17] refactor: replacing argparse by click in main.py --- src/eolab/rastertools/__init__.py | 2 +- src/eolab/rastertools/cli/filtering.py | 310 +++++++++++++++++---- src/eolab/rastertools/cli/filtering_dyn.py | 182 ++++++++++++ src/eolab/rastertools/main.py | 232 +++++++-------- tests/test_algo.py | 1 - 5 files changed, 526 insertions(+), 201 deletions(-) create mode 100644 src/eolab/rastertools/cli/filtering_dyn.py diff --git a/src/eolab/rastertools/__init__.py b/src/eolab/rastertools/__init__.py index e357ff6..b678309 100644 --- a/src/eolab/rastertools/__init__.py +++ b/src/eolab/rastertools/__init__.py @@ -26,7 +26,7 @@ # import rastertool Zonalstats from eolab.rastertools.zonalstats import Zonalstats # import the method to run a rastertool -from eolab.rastertools.main import run_tool, add_custom_rastertypes +from eolab.rastertools.main import rastertools, add_custom_rastertypes __all__ = [ "RastertoolConfigurationException", "Rastertool", "Windowable", diff --git a/src/eolab/rastertools/cli/filtering.py b/src/eolab/rastertools/cli/filtering.py index 882270c..2fbd88f 100644 --- a/src/eolab/rastertools/cli/filtering.py +++ b/src/eolab/rastertools/cli/filtering.py @@ -3,70 +3,45 @@ """ CLI definition for the filtering tool """ -import eolab.rastertools.cli as cli from eolab.rastertools import Filtering +#import eolab.rastertools.main as main +from eolab.rastertools import RastertoolConfigurationException +#from eolab.rastertools.main import rastertools #Import the click group named rastertools +import click +import sys +import os +#_logger = main.get_logger() -def create_argparser(rastertools_parsers): - """Adds the filtering subcommand to the given rastertools subparser +def _extract_files_from_list(cmd_inputs): + """Extract the list of files from a file of type ".lst" which + contains one line per file Args: - rastertools_parsers: - The rastertools subparsers to which this subcommand shall be added. + cmd_inputs (str): + Value of the inputs arguments of the command line. Either + a file with a suffix lst from which the list of files shall + be extracted or directly the list of files (in this case, the + list is returned without any change). - This argument provides from a code like this:: + Returns: + The list of input files read from the command line + """ - import argparse - main_parser = argparse.ArgumentParser() - rastertools_parsers = main_parser.add_subparsers() - filtering.create_argparser(rastertools_parsers) + # handle the input file of type "lst" + if len(cmd_inputs) == 1 and cmd_inputs[0][-4:].lower() == ".lst": + # parse the listing + with open(cmd_inputs[0]) as f: + inputs = f.read().splitlines() + else: + inputs = cmd_inputs - Returns: - The rastertools subparsers updated with this subcommand + return inputs + +def create_filtering(output : str, window_size : int, pad : str, argsdict : dict, filter : str, bands : list, kernel_size : int, all_bands : bool) -> Filtering: """ - parser = rastertools_parsers.add_parser( - "filter", aliases=["fi"], - help="Apply a filter to a set of images", - description="Apply a filter to a set of images.") - - # create a subparser to configure each kind of filter - sub_parser = parser.add_subparsers(title='Filters') - - for rasterfilter in Filtering.get_default_filters(): - # new parser for the filter - filterparser = sub_parser.add_parser( - rasterfilter.name, - aliases=rasterfilter.aliases, - help=rasterfilter.help, - description=rasterfilter.description, - epilog="By default only first band is computed.") - - # add argument declared in the filter definition - for argument_name, argument_params in rasterfilter.arguments.items(): - filterparser.add_argument(f"--{argument_name}", **argument_params) - - # add common arguments (inputs, output dir, window size, pad mode) - filterparser.add_argument( - "inputs", - nargs='+', - help="Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). " - "You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") " - "that lists the input files to process (one input file per line in .lst)") - cli.with_outputdir_arguments(filterparser) - cli.with_window_arguments(filterparser) - cli.with_bands_arguments(filterparser) - - # set the default commmand to run for this filter parser - filterparser.set_defaults(filter=rasterfilter.name) - - # set the function to call when this subcommand is called - parser.set_defaults(func=create_filtering) - - return rastertools_parsers - - -def create_filtering(args) -> Filtering: - """Create and configure a new rastertool "Filtering" according to argparse args + CHANGE DOCSTRING + Create and configure a new rastertool "Filtering" according to argparse args Args: args: args extracted from command line @@ -74,21 +49,234 @@ def create_filtering(args) -> Filtering: Returns: :obj:`eolab.rastertools.Filtering`: The configured rastertool to run """ - argsdict = vars(args) # get the bands to process - if args.all_bands: + if all_bands: bands = None else: - bands = list(map(int, args.bands)) if args.bands else [1] + bands = list(map(int, bands)) if bands else [1] # create the rastertool object raster_filters_dict = {rf.name: rf for rf in Filtering.get_default_filters()} - tool = Filtering(raster_filters_dict[args.filter], args.kernel_size, bands) + tool = Filtering(raster_filters_dict[filter], kernel_size, bands) # set up config with args values - tool.with_output(args.output) \ - .with_windows(args.window_size, args.pad) \ + tool.with_output(output) \ + .with_windows(window_size, pad) \ .with_filter_configuration(argsdict) return tool + +def apply_filter(ctx, tool : Filtering, inputs : str): + """ + CHANGE DOCSTRING + Apply the chosen filter + """ + try: + # handle the input file of type "lst" + inputs_extracted = _extract_files_from_list(inputs) + + # setup debug mode in which intermediate VRT files are stored to disk or not + tool.with_vrt_stored(ctx.obj.get('keep_vrt')) + + # launch process + tool.process_files(inputs_extracted) + + #_logger.info("Done!") + + except RastertoolConfigurationException as rce: + #_logger.exception(rce) + sys.exit(2) + + except Exception as err: + #_logger.exception(err) + sys.exit(1) + + sys.exit(0) + +inpt_arg = click.argument('inputs', type=str, required = 1) + +ker_opt = click.option('--kernel-size', type=int, help="Kernel size of the filter function, e.g. 3 means a square" + "of 3x3 pixels on which the filter function is computed" + "(default: 8)") + +out_opt = click.option('-o', '--output', default = os.getcwd(), help="Output directory to store results (by default current directory)") + +win_opt = click.option('-ws', '--window-size', type=int, default = 1024, help="Size of tiles to distribute processing, default: 1024") + +pad_opt = click.option('-p','--pad',default="edge", type=click.Choice(['none','edge','maximum','mean','median','minimum','reflect','symmetric','wrap']), + help="Pad to use around the image, default : edge" + "(see https://numpy.org/doc/stable/reference/generated/numpy.pad.html" + "for more information)") + +band_opt = click.option('-b','--bands', type=list, help="List of bands to process") + +all_opt = click.option('-a', '--all','all_bands', type=bool, is_flag=True, help="Process all bands") + +@click.group() +@click.pass_context +def filter(ctx): + ''' + Apply a filter to a set of images. + ''' + ctx.ensure_object(dict) + + +#Median filter +@filter.command("median") +@inpt_arg +@ker_opt +@out_opt +@win_opt +@pad_opt +@band_opt +@all_opt +@click.pass_context +def median(ctx, inputs : str, output : str, window_size : int, pad : str, kernel_size : int, bands : list, all_bands : bool) : + """ + COMPLETE THE SECTION should display for : rastertools filter median --help + Execute the filtering tool with the specified filter and parameters. name=rasterfilter.name, help=rasterfilter.help + + do not remove : + inputs : Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). + You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") + that lists the input files to process (one input file per line in .lst)" + """ + #Store input files so that rastertools has access to it + ctx.obj["inputs"] = inputs + + # Configure the filter tool instance + tool = create_filtering( + output=output, + window_size=window_size, + pad=pad, + argsdict={"inputs": inputs}, + filter='median', + bands=bands, + kernel_size=kernel_size, + all_bands=all_bands) + + apply_filter(ctx, tool, inputs) + +#Sum filter +@filter.command("sum") +@inpt_arg +@ker_opt +@out_opt +@win_opt +@pad_opt +@band_opt +@all_opt +@click.pass_context +def sum(ctx, inputs : str, output : str, window_size : int, pad : str, kernel_size : int, bands : list, all_bands : bool) : + """ + COMPLETE THE SECTION should display for : rastertools filter median --help + Execute the filtering tool with the specified filter and parameters. name=rasterfilter.name, help=rasterfilter.help + + do not remove : + inputs : Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). + You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") + that lists the input files to process (one input file per line in .lst)" + """ + # Store input files so that rastertools has access to it + ctx.obj["inputs"] = inputs + + # Configure the filter tool instance + tool = create_filtering( + output=output, + window_size=window_size, + pad=pad, + argsdict={"inputs": inputs}, + filter='sum', + bands=bands, + kernel_size=kernel_size, + all_bands=all_bands) + + apply_filter(ctx, tool, inputs) + +#Mean filter +@filter.command("mean") +@inpt_arg +@ker_opt +@out_opt +@win_opt +@pad_opt +@band_opt +@all_opt +@click.pass_context +def mean(ctx, inputs : str, output : str, window_size : int, pad : str, kernel_size : int, bands : list, all_bands : bool) : + """ + COMPLETE THE SECTION should display for : rastertools filter median --help + Execute the filtering tool with the specified filter and parameters. name=rasterfilter.name, help=rasterfilter.help + + do not remove : + inputs : Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). + You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") + that lists the input files to process (one input file per line in .lst)" + """ + # Store input files so that rastertools has access to it + ctx.obj["inputs"] = inputs + + # Configure the filter tool instance + tool = create_filtering( + output=output, + window_size=window_size, + pad=pad, + argsdict={"inputs": inputs}, + filter='mean', + bands=bands, + kernel_size=kernel_size, + all_bands=all_bands) + + apply_filter(ctx, tool, inputs) + +#Adaptive gaussian filter +@filter.command("adaptive_gaussian") +@inpt_arg +@ker_opt +@out_opt +@win_opt +@pad_opt +@band_opt +@all_opt +@click.pass_context +def adaptive_gaussian(ctx, inputs : str, output : str, window_size : int, pad : str, kernel_size : int, bands : list, all_bands : bool) : + """ + COMPLETE THE SECTION should display for : rastertools filter median --help + Execute the filtering tool with the specified filter and parameters. name=rasterfilter.name, help=rasterfilter.help + + do not remove : + inputs : Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). + You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") + that lists the input files to process (one input file per line in .lst)" + """ + # Store input files so that rastertools has access to it + ctx.obj["inputs"] = inputs + + # Configure the filter tool instance + tool = create_filtering( + output=output, + window_size=window_size, + pad=pad, + argsdict={"inputs": inputs}, + filter='adaptive_gaussian', + bands=bands, + kernel_size=kernel_size, + all_bands=all_bands) + + apply_filter(ctx, tool, inputs) + + +@filter.result_callback() +@click.pass_context +def handle_result(ctx): + if ctx.invoked_subcommand is None: + click.echo(ctx.get_help()) + ctx.exit() + + + + + + + diff --git a/src/eolab/rastertools/cli/filtering_dyn.py b/src/eolab/rastertools/cli/filtering_dyn.py new file mode 100644 index 0000000..c3be333 --- /dev/null +++ b/src/eolab/rastertools/cli/filtering_dyn.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +CLI definition for the filtering tool +""" +from eolab.rastertools import Filtering +from eolab.rastertools.main import get_logger +from eolab.rastertools import RastertoolConfigurationException +#from eolab.rastertools.main import rastertools #Import the click group named rastertools +import sys +import click +import os + +_logger = get_logger() + +def _extract_files_from_list(cmd_inputs): + """Extract the list of files from a file of type ".lst" which + contains one line per file + + Args: + cmd_inputs (str): + Value of the inputs arguments of the command line. Either + a file with a suffix lst from which the list of files shall + be extracted or directly the list of files (in this case, the + list is returned without any change). + + Returns: + The list of input files read from the command line + """ + + # handle the input file of type "lst" + if len(cmd_inputs) == 1 and cmd_inputs[0][-4:].lower() == ".lst": + # parse the listing + with open(cmd_inputs[0]) as f: + inputs = f.read().splitlines() + else: + inputs = cmd_inputs + + return inputs + +def create_filtering(output : str, window_size : int, pad : str, argsdict : dict, filter : str, bands : list, kernel_size : int, all_bands : bool) -> Filtering: + """ + CHANGE DOCSTRING + Create and configure a new rastertool "Filtering" according to argparse args + + Args: + args: args extracted from command line + + Returns: + :obj:`eolab.rastertools.Filtering`: The configured rastertool to run + """ + + # get the bands to process + if all_bands: + bands = None + else: + bands = list(map(int, bands)) if bands else [1] + + # create the rastertool object + raster_filters_dict = {rf.name: rf for rf in Filtering.get_default_filters()} + tool = Filtering(raster_filters_dict[filter], kernel_size, bands) + + # set up config with args values + tool.with_output(output) \ + .with_windows(window_size, pad) \ + .with_filter_configuration(argsdict) + + return tool + + +def apply_filter(ctx, tool : Filtering, inputs : str): + """ + CHANGE DOCSTRING + Apply the chosen filter + """ + try: + # handle the input file of type "lst" + inputs_extracted = _extract_files_from_list(inputs) + + # setup debug mode in which intermediate VRT files are stored to disk or not + tool.with_vrt_stored(ctx.obj.get('keep_vrt')) + + # launch process + tool.process_files(inputs_extracted) + + _logger.info("Done!") + + except RastertoolConfigurationException as rce: + _logger.exception(rce) + sys.exit(2) + + except Exception as err: + _logger.exception(err) + sys.exit(1) + + sys.exit(0) + + +inpt_arg = click.argument('inputs', type=str, help="Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). " + "You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") " + "that lists the input files to process (one input file per line in .lst)") + +ker_opt = click.option('--kernel-size', type=int, help="Kernel size of the filter function, e.g. 3 means a square" + "of 3x3 pixels on which the filter function is computed" + "(default: 8)") + +out_opt = click.option('-o', '--output', default = os.getcwd(), help="Output directory to store results (by default current directory)") + +win_opt = click.option('-ws', '--window-size', type=int, default = 1024, help="Size of tiles to distribute processing, default: 1024") + +pad_opt = click.option('-p','--pad',default="edge", type=click.Choice(['none','edge','maximum','mean','median','minimum','reflect','symmetric','wrap']), + help="Pad to use around the image, default : edge" + "(see https://numpy.org/doc/stable/reference/generated/numpy.pad.html" + "for more information)") + +band_opt = click.option('-b','--bands', type=list, help="List of bands to process") + +all_opt = click.option('-a', '--all','all_bands', type=bool, is_flag=True, help="Process all bands") + +@click.group() +@click.pass_context +def filter(ctx): + ''' + Apply a filter to a set of images. + ''' + ctx.ensure_object(dict) + + +def create_filter(filter_name : str): + + @filter.command(filter_name) + @inpt_arg + @ker_opt + @out_opt + @win_opt + @pad_opt + @band_opt + @all_opt + @click.pass_context + def filter_filtername(ctx, inputs : str, output : str, window_size : int, pad : str, kernel_size : int, bands : list, all_bands : bool): + ''' + COMPLETE THE SECTION should display for : rastertools filter median --help + Execute the filtering tool with the specified filter and parameters. name=rasterfilter.name, help=rasterfilter.help + + do not remove : + inputs : Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). + You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") + that lists the input files to process (one input file per line in .lst)" + ''' + ctx.obj["inputs"] = inputs + + # Configure the filter tool instance + tool = create_filtering( + output=output, + window_size=window_size, + pad=pad, + argsdict={"inputs": inputs}, + filter=filter_name, + bands=bands, + kernel_size=kernel_size, + all_bands=all_bands) + + apply_filter(ctx, tool, inputs) + + +median = create_filter("median") +mean = create_filter("mean") +sum = create_filter("sum") +adaptive_gaussian = create_filter("adaptive_gaussian") + +@filter.result_callback() +@click.pass_context +def handle_result(ctx): + if ctx.invoked_subcommand is None: + click.echo(ctx.get_help()) + ctx.exit() + + + + + + diff --git a/src/eolab/rastertools/main.py b/src/eolab/rastertools/main.py index c9dc903..995d70b 100644 --- a/src/eolab/rastertools/main.py +++ b/src/eolab/rastertools/main.py @@ -10,22 +10,22 @@ rastertools zonalstats --help """ -import argparse import logging import logging.config import os import sys import json - +import click +from eolab.rastertools.cli.filtering import filter from eolab.rastertools import __version__ -from eolab.rastertools import RastertoolConfigurationException from eolab.rastertools.cli import radioindice, zonalstats, tiling, speed from eolab.rastertools.cli import filtering, svf, hillshade, timeseries from eolab.rastertools.product import RasterType _logger = logging.getLogger(__name__) - +def get_logger(): + return _logger def add_custom_rastertypes(rastertypes): """Add definition of new raster types. The json string shall have the following format: @@ -126,154 +126,110 @@ def add_custom_rastertypes(rastertypes): """ RasterType.add(rastertypes) - -def run_tool(args): - """Main entry point allowing external calls - - sys.exit returns: - - - 0: everything runs fine - - 1: processing errors occured - - 2: wrong execution configuration - - Args: - args ([str]): command line parameter list +@click.group() + +@click.option( + '-t', '--rastertype', + 'rastertype', + # Click automatically uses the last argument as the variable name, so "dest" is this last parameter + type=click.Path(exists=True), + help="JSON file defining additional raster types of input files") + +@click.option( + '--max_workers', + "max_workers", + type=int, + help="Maximum number of workers for parallel processing. If not given, it will default to " + "the number of processors on the machine. When all processors are not allocated to " + "run rastertools, it is thus recommended to set this option.") + +@click.option( + '--debug', + "keep_vrt", + is_flag=True, + help="Store to disk the intermediate VRT images that are generated when handling " + "the input files which can be complex raster product composed of several band files.") + +@click.option( + '-v', + '--verbose', + is_flag=True, + help="set loglevel to INFO") + +@click.option( + '-vv', + '--very-verbose', + is_flag=True, + help="set loglevel to DEBUG") + +@click.version_option(version='rastertools {}'.format(__version__)) # Ensure __version__ is defined + +@click.pass_context +def rastertools(ctx, rastertype : str, max_workers : int, keep_vrt : bool, verbose : bool, very_verbose : bool): """ - parser = argparse.ArgumentParser( - description="Collection of tools on raster data") - # add an argument to define custom raster types - parser.add_argument( - '-t', - '--rastertype', - dest="rastertype", - help="JSON file defining additional raster types of input files") - parser.add_argument( - '--version', - action='version', - version=f'rastertools {__version__}') - parser.add_argument( - '--max_workers', - dest="max_workers", - type=int, - help="Maximum number of workers for parallel processing. If not given, it will default to " - "the number of processors on the machine. When all processors are not allocated to " - "run rastertools, it is thus recommended to set this option.") - parser.add_argument( - '--debug', - dest="keep_vrt", - action="store_true", - help="Store to disk the intermediate VRT images that are generated when handling " - "the input files which can be complex raster product composed of several band files.") - parser.add_argument( - '-v', - '--verbose', - dest="loglevel", - help="set loglevel to INFO", - action='store_const', - const=logging.INFO) - parser.add_argument( - '-vv', - '--very-verbose', - dest="loglevel", - help="set loglevel to DEBUG", - action='store_const', - const=logging.DEBUG) - - rastertools_parsers = parser.add_subparsers(title='Commands') - # add sub parser for filtering - rastertools_parsers = filtering.create_argparser(rastertools_parsers) - # add sub parser for hillshade - rastertools_parsers = hillshade.create_argparser(rastertools_parsers) - # add sub parser for radioindice - rastertools_parsers = radioindice.create_argparser(rastertools_parsers) - # add sub parser for speed - rastertools_parsers = speed.create_argparser(rastertools_parsers) - # add sub parser for svf - rastertools_parsers = svf.create_argparser(rastertools_parsers) - # add sub parser for tiling - rastertools_parsers = tiling.create_argparser(rastertools_parsers) - # add sub parser for timeseries - rastertools_parsers = timeseries.create_argparser(rastertools_parsers) - # add sub parser for zonalstats - rastertools_parsers = zonalstats.create_argparser(rastertools_parsers) - - # analyse arguments - args = parser.parse_args(args) - argsdict = vars(args) - - # setup logging + Collection of tools on raster data. + CHANGE DOCSTRING + Main entry point allowing external calls. + + Args: + rastertype: JSON file defining additional raster types. + max_workers: Maximum number of workers for parallel processing. + keep_vrt: Store intermediate VRT images. + verbose: Set loglevel to INFO. + very_verbose: Set loglevel to DEBUG. + command: The command to execute (e.g., filtering). + inputs: Input files for processing. + + sys.exit returns: + + - 0: everything runs fine + - 1: processing errors occured + - 2: wrong execution configuration + """ + ctx.ensure_object(dict) + ctx.obj['keep_vrt'] = keep_vrt + + # Setup logging + if very_verbose: + loglevel = logging.DEBUG + elif verbose: + loglevel = logging.INFO logformat = "[%(asctime)s] %(levelname)s - %(name)s - %(message)s" - logging.basicConfig(level=args.loglevel, stream=sys.stdout, - format=logformat, datefmt="%Y-%m-%d %H:%M:%S") + logging.basicConfig(level=loglevel, stream=sys.stdout, format=logformat, datefmt="%Y-%m-%d %H:%M:%S") if "RASTERTOOLS_NOTQDM" not in os.environ: - os.environ["RASTERTOOLS_NOTQDM"] = "True" if logging.root.level > logging.INFO else "False" + os.environ["RASTERTOOLS_NOTQDM"] = "True" if loglevel > logging.INFO else "False" - if "RASTERTOOLS_MAXWORKERS" not in os.environ and args.max_workers is not None: - os.environ["RASTERTOOLS_MAXWORKERS"] = f"{args.max_workers}" + if "RASTERTOOLS_MAXWORKERS" not in os.environ and max_workers is not None: + os.environ["RASTERTOOLS_MAXWORKERS"] = f"{max_workers}" - # handle rastertype option - if args.rastertype: - with open(args.rastertype) as json_content: + # Handle rastertype option + if rastertype: + with open(rastertype) as json_content: RasterType.add(json.load(json_content)) - # call function corresponding to the subcommand - if "func" in argsdict: - try: - # initialize the rastertool to execute - tool = args.func(args) - - # handle the input file of type "lst" - inputs = _extract_files_from_list(args.inputs) - - # setup debug mode in which intermediate VRT files are stored to disk or not - tool.with_vrt_stored(args.keep_vrt) - - # launch process - tool.process_files(inputs) - _logger.info("Done!") - except RastertoolConfigurationException as rce: - _logger.exception(rce) - sys.exit(2) - except Exception as err: - _logger.exception(err) - sys.exit(1) - else: - parser.print_help() - - sys.exit(0) - - -def _extract_files_from_list(cmd_inputs): - """Extract the list of files from a file of type ".lst" which - contains one line per file - - Args: - cmd_inputs (str): - Value of the inputs arguments of the command line. Either - a file with a suffix lst from which the list of files shall - be extracted or directly the list of files (in this case, the - list is returned without any change). - - Returns: - The list of input files read from the command line - """ - - # handle the input file of type "lst" - if len(cmd_inputs) == 1 and cmd_inputs[0][-4:].lower() == ".lst": - # parse the listing - with open(cmd_inputs[0]) as f: - inputs = f.read().splitlines() - else: - inputs = cmd_inputs - return inputs +# Register subcommands from other modules +rastertools.add_command(filter) +#rastertools.add_command(hillshade) +#rastertools.add_command(radioindice) +#rastertools.add_command(speed) +#rastertools.add_command(svf) +#rastertools.add_command(tiling) +#rastertools.add_command(timeseries) +#rastertools.add_command(zonalstats) +@rastertools.result_callback() +@click.pass_context +def handle_result(ctx): + if ctx.invoked_subcommand is None: + click.echo(ctx.get_help()) + ctx.exit() def run(): """Entry point for console_scripts """ - run_tool(sys.argv[1:]) + rastertools() if __name__ == "__main__": diff --git a/tests/test_algo.py b/tests/test_algo.py index f25d738..7a4ee7f 100644 --- a/tests/test_algo.py +++ b/tests/test_algo.py @@ -3,7 +3,6 @@ import numpy as np import numpy.ma as ma -import rasterio from eolab.rastertools.processing import algo From 0deeac35ce8f68381d4a36d7a7a30b91c2aafc6a Mon Sep 17 00:00:00 2001 From: cadauxe Date: Wed, 6 Nov 2024 16:03:28 +0100 Subject: [PATCH 03/17] refactor: WIP click --- src/eolab/rastertools/cli/filtering.py | 195 +++++++++------------ src/eolab/rastertools/cli/filtering_dyn.py | 122 +++++-------- src/eolab/rastertools/cli/hillshade.py | 120 ++++++------- src/eolab/rastertools/cli/radioindice.py | 146 ++++++--------- src/eolab/rastertools/cli/speed.py | 58 ++---- src/eolab/rastertools/cli/svf.py | 110 ++++-------- src/eolab/rastertools/cli/tiling.py | 121 +++++-------- src/eolab/rastertools/cli/timeseries.py | 108 +++++------- src/eolab/rastertools/cli/utils_cli.py | 86 +++++++++ src/eolab/rastertools/main.py | 88 +++++----- tests/test_rastertools.py | 56 +++--- 11 files changed, 543 insertions(+), 667 deletions(-) create mode 100644 src/eolab/rastertools/cli/utils_cli.py diff --git a/src/eolab/rastertools/cli/filtering.py b/src/eolab/rastertools/cli/filtering.py index 2fbd88f..3501c84 100644 --- a/src/eolab/rastertools/cli/filtering.py +++ b/src/eolab/rastertools/cli/filtering.py @@ -5,51 +5,36 @@ """ from eolab.rastertools import Filtering #import eolab.rastertools.main as main -from eolab.rastertools import RastertoolConfigurationException +from eolab.rastertools.cli.utils_cli import apply_process #from eolab.rastertools.main import rastertools #Import the click group named rastertools import click -import sys import os -#_logger = main.get_logger() +CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) -def _extract_files_from_list(cmd_inputs): - """Extract the list of files from a file of type ".lst" which - contains one line per file - - Args: - cmd_inputs (str): - Value of the inputs arguments of the command line. Either - a file with a suffix lst from which the list of files shall - be extracted or directly the list of files (in this case, the - list is returned without any change). - - Returns: - The list of input files read from the command line - """ - - # handle the input file of type "lst" - if len(cmd_inputs) == 1 and cmd_inputs[0][-4:].lower() == ".lst": - # parse the listing - with open(cmd_inputs[0]) as f: - inputs = f.read().splitlines() - else: - inputs = cmd_inputs - - return inputs def create_filtering(output : str, window_size : int, pad : str, argsdict : dict, filter : str, bands : list, kernel_size : int, all_bands : bool) -> Filtering: """ - CHANGE DOCSTRING - Create and configure a new rastertool "Filtering" according to argparse args + This function initializes a `Filtering` tool instance and configures it with specified settings. + + It selects the filter type, kernel size, output settings, and processing bands. If `all_bands` is set + to True, the filter will apply to all bands in the raster; otherwise, it applies only to specified bands. Args: - args: args extracted from command line + output (str): The path for the filtered output file. + window_size (int): Size of the processing window used by the filter. + pad (str): Padding method used for windowing (e.g., 'reflect', 'constant', etc.). + argsdict (dict): Dictionary of additional filter configuration arguments. + filter (str): The filter type to apply (must be a valid name in `Filtering` filters). + bands (list): List of bands to process. If empty and `all_bands` is False, defaults to [1]. + kernel_size (int): Size of the kernel used by the filter. + all_bands (bool): Whether to apply the filter to all bands (True) or specific bands (False). Returns: - :obj:`eolab.rastertools.Filtering`: The configured rastertool to run + :obj:`eolab.rastertools.Filtering`: A configured `Filtering` instance ready for execution. """ + # get the bands to process if all_bands: bands = None @@ -67,53 +52,27 @@ def create_filtering(output : str, window_size : int, pad : str, argsdict : dict return tool -def apply_filter(ctx, tool : Filtering, inputs : str): - """ - CHANGE DOCSTRING - Apply the chosen filter - """ - try: - # handle the input file of type "lst" - inputs_extracted = _extract_files_from_list(inputs) - - # setup debug mode in which intermediate VRT files are stored to disk or not - tool.with_vrt_stored(ctx.obj.get('keep_vrt')) - - # launch process - tool.process_files(inputs_extracted) - - #_logger.info("Done!") - except RastertoolConfigurationException as rce: - #_logger.exception(rce) - sys.exit(2) +inpt_arg = click.argument('inputs', type=str, nargs = -1, required = 1) - except Exception as err: - #_logger.exception(err) - sys.exit(1) - - sys.exit(0) - -inpt_arg = click.argument('inputs', type=str, required = 1) - -ker_opt = click.option('--kernel-size', type=int, help="Kernel size of the filter function, e.g. 3 means a square" +ker_opt = click.option('--kernel_size', type=int, help="Kernel size of the filter function, e.g. 3 means a square" "of 3x3 pixels on which the filter function is computed" "(default: 8)") out_opt = click.option('-o', '--output', default = os.getcwd(), help="Output directory to store results (by default current directory)") -win_opt = click.option('-ws', '--window-size', type=int, default = 1024, help="Size of tiles to distribute processing, default: 1024") +win_opt = click.option('-ws', '--window_size', type=int, default = 1024, help="Size of tiles to distribute processing, default: 1024") pad_opt = click.option('-p','--pad',default="edge", type=click.Choice(['none','edge','maximum','mean','median','minimum','reflect','symmetric','wrap']), help="Pad to use around the image, default : edge" "(see https://numpy.org/doc/stable/reference/generated/numpy.pad.html" "for more information)") -band_opt = click.option('-b','--bands', type=list, help="List of bands to process") +band_opt = click.option('-b','--bands', multiple = True, type=int, help="List of bands to process") all_opt = click.option('-a', '--all','all_bands', type=bool, is_flag=True, help="Process all bands") -@click.group() +@click.group(name = "filter", context_settings=CONTEXT_SETTINGS) @click.pass_context def filter(ctx): ''' @@ -123,7 +82,7 @@ def filter(ctx): #Median filter -@filter.command("median") +@filter.command("median",context_settings=CONTEXT_SETTINGS) @inpt_arg @ker_opt @out_opt @@ -132,19 +91,23 @@ def filter(ctx): @band_opt @all_opt @click.pass_context -def median(ctx, inputs : str, output : str, window_size : int, pad : str, kernel_size : int, bands : list, all_bands : bool) : +def median(ctx, inputs : list, output : str, window_size : int, pad : str, kernel_size : int, bands : list, all_bands : bool) : """ - COMPLETE THE SECTION should display for : rastertools filter median --help - Execute the filtering tool with the specified filter and parameters. name=rasterfilter.name, help=rasterfilter.help + Execute the median filter on the input files with the specified parameters. - do not remove : - inputs : Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). - You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") - that lists the input files to process (one input file per line in .lst)" - """ - #Store input files so that rastertools has access to it - ctx.obj["inputs"] = inputs + The filter works by sliding a window across the input raster and replacing each + pixel value with the median value of the pixels within that window. + The `inputs` argument can either be a single file or a `.lst` file containing a list of input files. + + Arguments: + + inputs TEXT + + Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). + You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") that lists + the input files to process (one input file per line in .lst). + """ # Configure the filter tool instance tool = create_filtering( output=output, @@ -156,10 +119,10 @@ def median(ctx, inputs : str, output : str, window_size : int, pad : str, kernel kernel_size=kernel_size, all_bands=all_bands) - apply_filter(ctx, tool, inputs) + apply_process(ctx, tool, inputs) #Sum filter -@filter.command("sum") +@filter.command("sum",context_settings=CONTEXT_SETTINGS) @inpt_arg @ker_opt @out_opt @@ -168,19 +131,23 @@ def median(ctx, inputs : str, output : str, window_size : int, pad : str, kernel @band_opt @all_opt @click.pass_context -def sum(ctx, inputs : str, output : str, window_size : int, pad : str, kernel_size : int, bands : list, all_bands : bool) : +def sum(ctx, inputs : list, output : str, window_size : int, pad : str, kernel_size : int, bands : list, all_bands : bool) : """ - COMPLETE THE SECTION should display for : rastertools filter median --help - Execute the filtering tool with the specified filter and parameters. name=rasterfilter.name, help=rasterfilter.help + Execute the sum filter on the input files with the specified parameters. - do not remove : - inputs : Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). - You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") - that lists the input files to process (one input file per line in .lst)" - """ - # Store input files so that rastertools has access to it - ctx.obj["inputs"] = inputs + The filter works by sliding a window across the input raster and replacing each + pixel value with the median value of the pixels within that window. + The `inputs` argument can either be a single file or a `.lst` file containing a list of input files. + + Arguments: + + inputs TEXT + + Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). + You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") that lists + the input files to process (one input file per line in .lst). + """ # Configure the filter tool instance tool = create_filtering( output=output, @@ -192,10 +159,10 @@ def sum(ctx, inputs : str, output : str, window_size : int, pad : str, kernel_si kernel_size=kernel_size, all_bands=all_bands) - apply_filter(ctx, tool, inputs) + apply_process(ctx, tool, inputs) #Mean filter -@filter.command("mean") +@filter.command("mean",context_settings=CONTEXT_SETTINGS) @inpt_arg @ker_opt @out_opt @@ -204,19 +171,23 @@ def sum(ctx, inputs : str, output : str, window_size : int, pad : str, kernel_si @band_opt @all_opt @click.pass_context -def mean(ctx, inputs : str, output : str, window_size : int, pad : str, kernel_size : int, bands : list, all_bands : bool) : +def mean(ctx, inputs : list, output : str, window_size : int, pad : str, kernel_size : int, bands : list, all_bands : bool) : """ - COMPLETE THE SECTION should display for : rastertools filter median --help - Execute the filtering tool with the specified filter and parameters. name=rasterfilter.name, help=rasterfilter.help + Execute the mean filter on the input files with the specified parameters. - do not remove : - inputs : Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). - You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") - that lists the input files to process (one input file per line in .lst)" - """ - # Store input files so that rastertools has access to it - ctx.obj["inputs"] = inputs + The filter works by sliding a window across the input raster and replacing each + pixel value with the median value of the pixels within that window. + The `inputs` argument can either be a single file or a `.lst` file containing a list of input files. + + Arguments: + + inputs TEXT + + Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). + You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") that lists + the input files to process (one input file per line in .lst). + """ # Configure the filter tool instance tool = create_filtering( output=output, @@ -228,10 +199,10 @@ def mean(ctx, inputs : str, output : str, window_size : int, pad : str, kernel_s kernel_size=kernel_size, all_bands=all_bands) - apply_filter(ctx, tool, inputs) + apply_process(ctx, tool, inputs) #Adaptive gaussian filter -@filter.command("adaptive_gaussian") +@filter.command("adaptive_gaussian",context_settings=CONTEXT_SETTINGS) @inpt_arg @ker_opt @out_opt @@ -240,19 +211,23 @@ def mean(ctx, inputs : str, output : str, window_size : int, pad : str, kernel_s @band_opt @all_opt @click.pass_context -def adaptive_gaussian(ctx, inputs : str, output : str, window_size : int, pad : str, kernel_size : int, bands : list, all_bands : bool) : +def adaptive_gaussian(ctx, inputs : list, output : str, window_size : int, pad : str, kernel_size : int, bands : list, all_bands : bool) : """ - COMPLETE THE SECTION should display for : rastertools filter median --help - Execute the filtering tool with the specified filter and parameters. name=rasterfilter.name, help=rasterfilter.help + Execute the adaptive gaussian filter on the input files with the specified parameters. - do not remove : - inputs : Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). - You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") - that lists the input files to process (one input file per line in .lst)" - """ - # Store input files so that rastertools has access to it - ctx.obj["inputs"] = inputs + The filter works by sliding a window across the input raster and replacing each + pixel value with the median value of the pixels within that window. + The `inputs` argument can either be a single file or a `.lst` file containing a list of input files. + + Arguments: + + inputs TEXT + + Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). + You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") that lists + the input files to process (one input file per line in .lst). + """ # Configure the filter tool instance tool = create_filtering( output=output, @@ -264,7 +239,7 @@ def adaptive_gaussian(ctx, inputs : str, output : str, window_size : int, pad : kernel_size=kernel_size, all_bands=all_bands) - apply_filter(ctx, tool, inputs) + apply_process(ctx, tool, inputs) @filter.result_callback() diff --git a/src/eolab/rastertools/cli/filtering_dyn.py b/src/eolab/rastertools/cli/filtering_dyn.py index c3be333..2ba8274 100644 --- a/src/eolab/rastertools/cli/filtering_dyn.py +++ b/src/eolab/rastertools/cli/filtering_dyn.py @@ -4,52 +4,35 @@ CLI definition for the filtering tool """ from eolab.rastertools import Filtering -from eolab.rastertools.main import get_logger -from eolab.rastertools import RastertoolConfigurationException +#from eolab.rastertools.main import get_logger +from eolab.rastertools.cli.utils_cli import apply_process #from eolab.rastertools.main import rastertools #Import the click group named rastertools -import sys import click import os -_logger = get_logger() +CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) -def _extract_files_from_list(cmd_inputs): - """Extract the list of files from a file of type ".lst" which - contains one line per file - - Args: - cmd_inputs (str): - Value of the inputs arguments of the command line. Either - a file with a suffix lst from which the list of files shall - be extracted or directly the list of files (in this case, the - list is returned without any change). - - Returns: - The list of input files read from the command line - """ - - # handle the input file of type "lst" - if len(cmd_inputs) == 1 and cmd_inputs[0][-4:].lower() == ".lst": - # parse the listing - with open(cmd_inputs[0]) as f: - inputs = f.read().splitlines() - else: - inputs = cmd_inputs - - return inputs def create_filtering(output : str, window_size : int, pad : str, argsdict : dict, filter : str, bands : list, kernel_size : int, all_bands : bool) -> Filtering: """ - CHANGE DOCSTRING - Create and configure a new rastertool "Filtering" according to argparse args + This function initializes a `Filtering` tool instance and configures it with specified settings. + + It selects the filter type, kernel size, output settings, and processing bands. If `all_bands` is set + to True, the filter will apply to all bands in the raster; otherwise, it applies only to specified bands. Args: - args: args extracted from command line + output (str): The path for the filtered output file. + window_size (int): Size of the processing window used by the filter. + pad (str): Padding method used for windowing (e.g., 'reflect', 'constant', etc.). + argsdict (dict): Dictionary of additional filter configuration arguments. + filter (str): The filter type to apply (must be a valid name in `Filtering` filters). + bands (list): List of bands to process. If empty and `all_bands` is False, defaults to [1]. + kernel_size (int): Size of the kernel used by the filter. + all_bands (bool): Whether to apply the filter to all bands (True) or specific bands (False). Returns: - :obj:`eolab.rastertools.Filtering`: The configured rastertool to run + :obj:`eolab.rastertools.Filtering`: A configured `Filtering` instance ready for execution. """ - # get the bands to process if all_bands: bands = None @@ -68,67 +51,37 @@ def create_filtering(output : str, window_size : int, pad : str, argsdict : dict return tool -def apply_filter(ctx, tool : Filtering, inputs : str): - """ - CHANGE DOCSTRING - Apply the chosen filter - """ - try: - # handle the input file of type "lst" - inputs_extracted = _extract_files_from_list(inputs) - - # setup debug mode in which intermediate VRT files are stored to disk or not - tool.with_vrt_stored(ctx.obj.get('keep_vrt')) +inpt_arg = click.argument('inputs', type=str, nargs = -1, required = 1) - # launch process - tool.process_files(inputs_extracted) - - _logger.info("Done!") - - except RastertoolConfigurationException as rce: - _logger.exception(rce) - sys.exit(2) - - except Exception as err: - _logger.exception(err) - sys.exit(1) - - sys.exit(0) - - -inpt_arg = click.argument('inputs', type=str, help="Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). " - "You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") " - "that lists the input files to process (one input file per line in .lst)") - -ker_opt = click.option('--kernel-size', type=int, help="Kernel size of the filter function, e.g. 3 means a square" +ker_opt = click.option('--kernel_size', type=int, help="Kernel size of the filter function, e.g. 3 means a square" "of 3x3 pixels on which the filter function is computed" "(default: 8)") out_opt = click.option('-o', '--output', default = os.getcwd(), help="Output directory to store results (by default current directory)") -win_opt = click.option('-ws', '--window-size', type=int, default = 1024, help="Size of tiles to distribute processing, default: 1024") +win_opt = click.option('-ws', '--window_size', type=int, default = 1024, help="Size of tiles to distribute processing, default: 1024") pad_opt = click.option('-p','--pad',default="edge", type=click.Choice(['none','edge','maximum','mean','median','minimum','reflect','symmetric','wrap']), help="Pad to use around the image, default : edge" "(see https://numpy.org/doc/stable/reference/generated/numpy.pad.html" "for more information)") -band_opt = click.option('-b','--bands', type=list, help="List of bands to process") +band_opt = click.option('-b','--bands', type=int, multiple = True, help="List of bands to process") all_opt = click.option('-a', '--all','all_bands', type=bool, is_flag=True, help="Process all bands") -@click.group() +@click.group(context_settings=CONTEXT_SETTINGS) @click.pass_context def filter(ctx): - ''' + """ Apply a filter to a set of images. - ''' + """ ctx.ensure_object(dict) def create_filter(filter_name : str): - @filter.command(filter_name) + @filter.command(filter_name, context_settings=CONTEXT_SETTINGS) @inpt_arg @ker_opt @out_opt @@ -137,18 +90,21 @@ def create_filter(filter_name : str): @band_opt @all_opt @click.pass_context - def filter_filtername(ctx, inputs : str, output : str, window_size : int, pad : str, kernel_size : int, bands : list, all_bands : bool): - ''' - COMPLETE THE SECTION should display for : rastertools filter median --help - Execute the filtering tool with the specified filter and parameters. name=rasterfilter.name, help=rasterfilter.help - - do not remove : - inputs : Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). - You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") - that lists the input files to process (one input file per line in .lst)" - ''' - ctx.obj["inputs"] = inputs + def filter_filtername(ctx, inputs : list, output : str, window_size : int, pad : str, kernel_size : int, bands : list, all_bands : bool): + """ + Execute the requested filter on the input files with the specified parameters. + The `inputs` argument can either be a single file or a `.lst` file containing a list of input files. + + Arguments: + + inputs TEXT + + Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). + You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") that lists + the input files to process (one input file per line in .lst). + """ + print(bands) # Configure the filter tool instance tool = create_filtering( output=output, @@ -160,7 +116,7 @@ def filter_filtername(ctx, inputs : str, output : str, window_size : int, pad : kernel_size=kernel_size, all_bands=all_bands) - apply_filter(ctx, tool, inputs) + apply_process(ctx, tool, inputs) median = create_filter("median") diff --git a/src/eolab/rastertools/cli/hillshade.py b/src/eolab/rastertools/cli/hillshade.py index 640b3ac..10c82a2 100644 --- a/src/eolab/rastertools/cli/hillshade.py +++ b/src/eolab/rastertools/cli/hillshade.py @@ -3,12 +3,53 @@ """ CLI definition for the hillshade tool """ -import eolab.rastertools.cli as cli from eolab.rastertools import Hillshade +from eolab.rastertools.cli.utils_cli import apply_process +import click +import os +CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) -def create_argparser(rastertools_parsers): - """Adds the hillshade subcommand to the given rastertools subparser + +#Hillshade command +@click.command("hillshade",context_settings=CONTEXT_SETTINGS) +@click.argument('inputs', type=str, nargs = -1, required = 1) + +@click.option('--elevation', type=float, help="Elevation of the sun in degrees, [0°, 90°] where" + "90°=zenith and 0°=horizon") + +@click.option('--azimuth', type=float, help="Azimuth of the sun in degrees, [0°, 360°] where" + "0°=north, 90°=east, 180°=south and 270°=west") + +@click.option('--radius', type=int, help="Maximum distance (in pixels) around a point to evaluate" + "horizontal elevation angle. If not set, it is automatically computed from" + " the range of altitudes in the digital model.") + +@click.option('--resolution',default=0.5, type=float, help="Pixel resolution in meter") + +@click.option('-o','--output', default = os.getcwd(), help="Output directory to store results (by default current directory)") + +@click.option('-ws', '--window_size', type=int, default = 1024, help="Size of tiles to distribute processing, default: 1024") + +@click.option('-p', '--pad',default="edge", type=click.Choice(['none','edge','maximum','mean','median','minimum','reflect','symmetric','wrap']), + help="Pad to use around the image, default : edge" + "(see https://numpy.org/doc/stable/reference/generated/numpy.pad.html" + "for more information)") + +@click.pass_context +def hillshade(ctx, inputs : list, elevation : float, azimuth : float, radius : int, resolution : float, output : str, window_size : int, pad : str) : + """ + CHANGE DOCSTRING + Adds the hillshade subcommand to the given rastertools subparser + + Arguments: + + inputs TEXT + + Input file to process (i.e. geotiff corresponding to a + Digital Height Model). You can provide a single file + with extension ".lst" (e.g. "filtering.lst") that + lists the input files to process (one input file per line in .lst) Args: rastertools_parsers: @@ -24,74 +65,15 @@ def create_argparser(rastertools_parsers): Returns: The rastertools subparsers updated with this subcommand """ - parser = rastertools_parsers.add_parser( - "hillshade", aliases=["hs"], - help="Compute hillshades of a Digital Elevation / Surface / Height Model " - "(a raster containing the height of the point as pixel values)", - description="Compute hillshades of a Digital Elevation / Surface / Height Model.") - - # add specific arguments of the hillshade processing - arguments = { - "elevation": { - "required": True, - "type": float, - "help": "Elevation of the sun in degrees, [0°, 90°] where 90°=zenith and 0°=horizon" - }, - "azimuth": { - "required": True, - "type": float, - "help": "Azimuth of the sun in degrees, [0°, 360°] " - "where 0°=north, 90°=east, 180°=south and 270°=west" - }, - "radius": { - "required": False, - "type": int, - "help": "Max distance (in pixels) around a point to evaluate horizontal" - " elevation angle. If not set, it is automatically computed from" - " the range of altitudes in the digital model." - }, - "resolution": { - "default": 0.5, - "required": True, - "type": float, - "help": "Pixel resolution in meter" - }, - } - # add argument declared in the hillshade processing definition - for argument_name, argument_params in arguments.items(): - parser.add_argument(f"--{argument_name}", **argument_params) - - # add common arguments (inputs, output dir, window size, pad mode) - parser.add_argument( - "inputs", - nargs='+', - help="Input file to process (i.e. geotiff that contains the height " - "of the points as pixel values). " - "You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") " - "that lists the input files to process (one input file per line in .lst)") - cli.with_outputdir_arguments(parser) - cli.with_window_arguments(parser) - - # set the function to call when this subcommand is called - parser.set_defaults(func=create_hillshade) - - return rastertools_parsers - - -def create_hillshade(args) -> Hillshade: - """Create and configure a new rastertool "Hillshade" according to argparse args - - Args: - args: args extracted from command line - Returns: - :obj:`eolab.rastertools.Hillshade`: The configured rastertool to run - """ # create the rastertool object - tool = Hillshade(args.elevation, args.azimuth, args.resolution, args.radius) + tool = Hillshade(elevation, azimuth, resolution, radius) # set up config with args values - tool.with_output(args.output) - tool.with_windows(args.window_size, args.pad) + tool.with_output(output) + tool.with_windows(window_size, pad) + + apply_process(ctx, tool, inputs) + + - return tool diff --git a/src/eolab/rastertools/cli/radioindice.py b/src/eolab/rastertools/cli/radioindice.py index 49a75d5..c7491ca 100644 --- a/src/eolab/rastertools/cli/radioindice.py +++ b/src/eolab/rastertools/cli/radioindice.py @@ -3,111 +3,78 @@ """ CLI definition for the radioindice tool """ -import eolab.rastertools.cli as cli from eolab.rastertools import RastertoolConfigurationException, Radioindice +from eolab.rastertools.cli.utils_cli import apply_process from eolab.rastertools.product import BandChannel from eolab.rastertools.processing import RadioindiceProcessing +import click +import os +CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) -def create_argparser(rastertools_parsers): - """Adds the radioindice subcommand to the given rastertools subparser - - Args: - rastertools_parsers: - The rastertools subparsers to which this subcommand shall be added. - - This argument provides from a code like this:: - - import argparse - main_parser = argparse.ArgumentParser() - rastertools_parsers = main_parser.add_subparsers() - radioindice.create_argparser(rastertools_parsers) - - Returns: - The rastertools subparsers updated with this subcommand - """ - indicenames = ', '.join(sorted([indice.name for indice in Radioindice.get_default_indices()])) - parser = rastertools_parsers.add_parser( - "radioindice", aliases=["ri"], - help="Compute radiometric indices", - description="Compute a list of radiometric indices (NDVI, NDWI, etc.) on a raster image", - epilog="If no indice option is explicitly set, NDVI, NDWI and NDWI2 are computed.") - parser.add_argument( - "inputs", - nargs='+', - help="Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). " - "You can provide a single file with extension \".lst\" (e.g. \"radioindice.lst\") " - "that lists the input files to process (one input file per line in .lst)") - cli.with_outputdir_arguments(parser) - parser.add_argument( - '-m', - '--merge', - dest="merge", - action="store_true", - help="Merge all indices in the same image (i.e. one band per indice).") - parser.add_argument( - '-r', - '--roi', - dest="roi", - help="Region of interest in the input image (vector)") - indices_pc = parser.add_argument_group("Options to select the indices to compute") - indices_pc.add_argument( - '-i', - '--indices', - dest="indices", - nargs="+", - help="List of indices to compute" - f"Possible indices are: {indicenames}") - for indice in Radioindice.get_default_indices(): - indices_pc.add_argument( - f"--{indice.name}", - dest=indice.name, - action="store_true", - help=f"Compute {indice.name} indice") - indices_pc.add_argument( - '-nd', - "-normalized_difference", - nargs=2, - action="append", - metavar=("band1", "band2"), - help="Compute the normalized difference of two bands defined as parameter of this option, " - "e.g. \"-nd red nir\" will compute (red-nir)/(red+nir). " - "See eolab.rastertools.product.rastertype.BandChannel for the list of bands names. " - "Several nd options can be set to compute several normalized differences.") - cli.with_window_arguments(parser, pad=False) - - # set the function to call when this subcommand is called - parser.set_defaults(func=create_radioindice) - - return rastertools_parsers - - -def create_radioindice(args) -> Radioindice: +def parse_normalized_difference(ctx, param, value): + """Parse the pairs of bands as (band1, band2) tuples.""" + if value: + # Split the input pairs and store them as tuples + parsed_pairs = [] + for i in range(0, len(value), 2): + parsed_pairs.append((value[i], value[i + 1])) + return parsed_pairs + return None + + +#Radioindice command +@click.command("radioindice",context_settings=CONTEXT_SETTINGS) +@click.argument('inputs', type=str, nargs = -1, required = 1) + +@click.option('-o','--output', default = os.getcwd(), help="Output directory to store results (by default current directory)") + +@click.option('-m', '--merge', is_flag = True, help="Merge all indices in the same image (i.e. one band per indice)") + +@click.option('-r', '--roi', type= str, help="Region of interest in the input image (vector)") + +@click.option('-ws', '--window_size', type=int, default = 1024, help="Size of tiles to distribute processing, default: 1024") + +list_indices = ['--ndvi', '--tndvi', '--rvi', '--pvi', '--savi', '--tsavi', '--msavi', '--msavi2', '--ipvi', +'--evi', '--ndwi', '--ndwi2', '--mndwi', '--ndpi', '--ndti', '--ndbi', '--ri', '--bi', '--bi2'] + +for id in list_indices: + @click.option(id, is_flag = True, help=f"Compute {id} indice") + +@click.option('-nd', '--normalized_difference','nd',type=str, + multiple=True, nargs=2, callback= parse_normalized_difference, metavar="band1 band2", + help="Compute the normalized difference of two bands defined" + "as parameter of this option, e.g. \"-nd red nir\" will compute (red-nir)/(red+nir). " + "See eolab.rastertools.product.rastertype.BandChannel for the list of bands names. " + "Several nd options can be set to compute several normalized differences.") + + +@click.pass_context +def radioindice(ctx, inputs : list, output : str, merge : bool, roi : str, window_size : int, nd : bool, *args) : """Create and configure a new rastertool "Radioindice" according to argparse args - Args: - args: args extracted from command line + Args: + args: args extracted from command line - Returns: - :obj:`eolab.rastertools.Radioindice`: The configured rastertool to run - """ + Returns: + :obj:`eolab.rastertools.Radioindice`: The configured rastertool to run + """ indices_to_compute = [] - argsdict = vars(args) # append indices defined with -- indices_to_compute.extend([indice for indice in Radioindice.get_default_indices() - if argsdict[indice.name]]) + if indices]) # append indices defined with --indices - if args.indices: + if indices: indices_dict = {indice.name: indice for indice in Radioindice.get_default_indices()} - for ind in args.indices: + for ind in indices: if ind in indices_dict: indices_to_compute.append(indices_dict[ind]) else: raise RastertoolConfigurationException(f"Invalid indice name: {ind}") - if args.nd: - for nd in args.nd: + if nd: + for nd in nd: if nd[0] in BandChannel.__members__ and nd[1] in BandChannel.__members__: channel1 = BandChannel[nd[0]] channel2 = BandChannel[nd[1]] @@ -128,8 +95,9 @@ def create_radioindice(args) -> Radioindice: tool = Radioindice(indices_to_compute) # set up config with args values - tool.with_output(args.output, args.merge) - tool.with_roi(args.roi) - tool.with_windows(args.window_size) + tool.with_output(output, merge) + tool.with_roi(roi) + tool.with_windows(window_size) return tool + diff --git a/src/eolab/rastertools/cli/speed.py b/src/eolab/rastertools/cli/speed.py index b343b4b..2977811 100644 --- a/src/eolab/rastertools/cli/speed.py +++ b/src/eolab/rastertools/cli/speed.py @@ -3,49 +3,29 @@ """ CLI definition for the speed tool """ -import eolab.rastertools.cli as cli from eolab.rastertools import Speed +from eolab.rastertools.cli.utils_cli import apply_process +import click +import os +CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) -def create_argparser(rastertools_parsers): - """Adds the speed subcommand to the given rastertools subparser - Args: - rastertools_parsers: - The rastertools subparsers to which this subcommand shall be added. - - This argument provides from a code like this:: +#Speed command +@click.command("speed",context_settings=CONTEXT_SETTINGS) +@click.argument('inputs', type=str, nargs = -1, required = 1) - import argparse - main_parser = argparse.ArgumentParser() - rastertools_parsers = main_parser.add_subparsers() - speed.create_argparser(rastertools_parsers) +@click.option('-b','--bands', type=int, multiple = True, help="List of bands to process") - Returns: - The rastertools subparsers updated with this subcommand - """ - parser = rastertools_parsers.add_parser( - "speed", aliases=["sp"], - help="Compute speed of rasters", - description="Compute the speed of radiometric values of several raster images", - epilog="By default only first band is computed.") - parser.add_argument( - "inputs", - nargs='+', - help="Input files to process (e.g. Sentinel2 L2A MAJA from THEIA). " - "You can provide a single file with extension \".lst\" (e.g. \"speed.lst\") " - "that lists the input files to process (one input file per line in .lst)") - cli.with_bands_arguments(parser) - cli.with_outputdir_arguments(parser) +@click.option('-a', '--all','all_bands', type=bool, is_flag=True, help="Process all bands") - # set the function to call when this subcommand is called - parser.set_defaults(func=create_speed) +@click.option('-o','--output', default = os.getcwd(), help="Output directory to store results (by default current directory)") - return rastertools_parsers - - -def create_speed(args) -> Speed: - """Create and configure a new rastertool "Speed" according to argparse args +@click.pass_context +def speed(ctx, inputs : list, bands : list, all_bands : bool, output : str) : + """ + CHANGE DOCSTRING + Create and configure a new rastertool "Speed" according to argparse args Args: args: args extracted from command line @@ -55,15 +35,15 @@ def create_speed(args) -> Speed: """ # get the bands to process - if args.all_bands: + if all_bands: bands = None else: - bands = list(map(int, args.bands)) if args.bands else [1] + bands = list(map(int, bands)) if bands else [1] # create the rastertool object tool = Speed(bands) # set up config with args values - tool.with_output(args.output) + tool.with_output(output) - return tool + apply_process(ctx, tool, inputs) diff --git a/src/eolab/rastertools/cli/svf.py b/src/eolab/rastertools/cli/svf.py index 374926b..58a15af 100644 --- a/src/eolab/rastertools/cli/svf.py +++ b/src/eolab/rastertools/cli/svf.py @@ -3,95 +3,57 @@ """ CLI definition for the SVF (Sky View Factor) tool """ -import eolab.rastertools.cli as cli from eolab.rastertools import SVF +from eolab.rastertools.cli.utils_cli import apply_process +import click +import os +CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) -def create_argparser(rastertools_parsers): - """Adds the SVF subcommand to the given rastertools subparser - Args: - rastertools_parsers: - The rastertools subparsers to which this subcommand shall be added. +#Speed command +@click.command("svf",context_settings=CONTEXT_SETTINGS) +@click.argument('inputs', type=str, nargs = -1, required = 1) - This argument provides from a code like this:: +@click.option('--radius',required = True, type=int, default = 16, help="Maximum distance (in pixels) around a point to evaluate horizontal elevation angle") - import argparse - main_parser = argparse.ArgumentParser() - rastertools_parsers = main_parser.add_subparsers() - svf.create_argparser(rastertools_parsers) +@click.option('--directions',required = True, type=int, default = 12, help="Number of directions on which to compute the horizon elevation angle") - Returns: - The rastertools subparsers updated with this subcommand +@click.option('--resolution',default=0.5, type=float, help="Pixel resolution in meter") + +@click.option('--altitude', type=int, help="Reference altitude to use for computing the SVF. If this option is not" + " specified, SVF is computed for every point at the altitude of the point") + +@click.option('-o','--output', default = os.getcwd(), help="Output directory to store results (by default current directory)") + +@click.option('-ws', '--window_size', type=int, default = 1024, help="Size of tiles to distribute processing, default: 1024") + +@click.option('-p', '--pad',default="edge", type=click.Choice(['none','edge','maximum','mean','median','minimum','reflect','symmetric','wrap']), + help="Pad to use around the image, default : edge" + "(see https://numpy.org/doc/stable/reference/generated/numpy.pad.html" + "for more information)") + +@click.pass_context +def svf(ctx, inputs : list, radius : int, directions : int, resolution : float, altitude : int, output : str, window_size : int, pad : str) : """ - parser = rastertools_parsers.add_parser( - "svf", - help="Compute Sky View Factor of a Digital Height Model", - description="Compute Sky View Factor of a Digital Height Model.") - - # add specific argument of the svf processing - arguments = { - "radius": { - "default": 16, - "required": True, - "type": int, - "help": "Max distance (in pixels) around a point to evaluate horizontal" - " elevation angle" - }, - "directions": { - "default": 12, - "required": True, - "type": int, - "help": "Number of directions on which to compute the horizon elevation angle" - }, - "resolution": { - "default": 0.5, - "required": True, - "type": float, - "help": "Pixel resolution in meter" - }, - "altitude": { - "type": int, - "help": "Reference altitude to use for computing the SVF. If this option is not" - " specified, SVF is computed for every point at the altitude of the point" - } - } - for argument_name, argument_params in arguments.items(): - parser.add_argument(f"--{argument_name}", **argument_params) - - # add common arguments (inputs, output dir, window size, pad mode) - parser.add_argument( - "inputs", - nargs='+', - help="Input file to process (i.e. geotiff corresponding to a Digital Height Model). " - "You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") " - "that lists the input files to process (one input file per line in .lst)") - cli.with_outputdir_arguments(parser) - cli.with_window_arguments(parser) - - # set the function to call when this subcommand is called - parser.set_defaults(func=create_svf) - - return rastertools_parsers - - -def create_svf(args) -> SVF: - """Create and configure a new rastertool "SVF" according to argparse args + CHANGE DOCSTRING + + ADD INPUTS + Create and configure a new rastertool "Speed" according to argparse args Args: args: args extracted from command line Returns: - :obj:`eolab.rastertools.SVF`: The configured rastertool to run + :obj:`eolab.rastertools.Speed`: The configured rastertool to run """ - # create the rastertool object - tool = SVF(args.directions, args.radius, args.resolution) + tool = SVF(directions, radius, resolution) # set up config with args values - tool.with_output(args.output) - tool.with_windows(args.window_size, args.pad) - if args.altitude is not None: - tool.with_altitude(args.altitude) + tool.with_output(output) + tool.with_windows(window_size, pad) + if altitude is not None: + tool.with_altitude(altitude) - return tool + apply_process(ctx, tool, inputs) diff --git a/src/eolab/rastertools/cli/tiling.py b/src/eolab/rastertools/cli/tiling.py index 8108b29..1b67e63 100644 --- a/src/eolab/rastertools/cli/tiling.py +++ b/src/eolab/rastertools/cli/tiling.py @@ -3,87 +3,50 @@ """ CLI definition for the tiling tool """ -import eolab.rastertools.cli as cli from eolab.rastertools import Tiling +from eolab.rastertools.cli.utils_cli import apply_process +import click +import os +CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) -def create_argparser(rastertools_parsers): - """Adds the tiling subcommand to the given rastertools subparser - Args: - rastertools_parsers: - The rastertools subparsers to which this subcommand shall be added. +#Speed command +@click.command("tiling",context_settings=CONTEXT_SETTINGS) +@click.argument('inputs', type=str, nargs = -1, required = 1) - This argument provides from a code like this:: +@click.option('-g','--grid','grid_file',required = True, type=str, help="vector-based spatial data file containing the grid to" + " use to generate the tiles") - import argparse - main_parser = argparse.ArgumentParser() - rastertools_parsers = main_parser.add_subparsers() - tiling.create_argparser(rastertools_parsers) +@click.option('--id_col','id_column', type = str, help="Name of the column in the grid" + " file used to number the tiles. When ids are defined, this argument is required" + "to identify which column corresponds to the define ids") - Returns: - The rastertools subparsers updated with this subcommand +@click.option('--id', type=int, multiple = True, help="Tiles ids of the grid to export as new tile, default all") + +@click.option('-o','--output', default = os.getcwd(), help="Output directory to store results (by default current directory)") + +@click.option('-n','--name','output_name', default="{}_tile{}", help="Basename for the output raster tiles, default:" + "\"{}_tile{}\". The basename must be defined as a formatted string where tile index is at position 1" + " and original filename is at position 0. For instance, tile{1}.tif will generate the filename" + "tile75.tif for the tile id = 75") + +@click.option('-d','--dir','subdir_name', help="When each tile must be generated in a different" + "subdirectory, it defines the naming convention for the subdirectory. It is a formatted string with one positional" + "parameter corresponding to the tile index. For instance, tile{} will generate the subdirectory name tile75/" + "for the tile id = 75. By default, subdirectory is not defined and output files will be generated directly in" + "the output directory") +@click.pass_context +def tiling(ctx, inputs : list, grid_file : str, id_column : str, id : list, output : str, output_name : str, subdir_name : str) : """ - parser = rastertools_parsers.add_parser( - "tiling", aliases=["ti"], - help="Generate image tiles", - description="Generate tiles of an input raster image following the geometries " - "defined by a given grid") - parser.add_argument( - "inputs", - nargs='+', - help="Raster files to process. " - "You can provide a single file with extension \".lst\" (e.g. \"tiling.lst\") " - "that lists the input files to process (one input file per line in .lst)") - parser.add_argument( - '-g', - '--grid', - dest="grid_file", - help="vector-based spatial data file containing the grid to use to generate the tiles", - required=True) - parser.add_argument( - "--id_col", - dest="id_column", - help="Name of the column in the grid file used to number the tiles. When ids are defined," - " this argument is required to identify which column corresponds to the defined ids", - ) - parser.add_argument( - "--id", - dest="id", - help="Tiles ids of the grid to export as new tile, default all", - nargs="+", - type=int - ) - cli.with_outputdir_arguments(parser) - parser.add_argument( - "-n", - "--name", - dest="output_name", - help="Basename for the output raster tiles, default: \"{}_tile{}\". " - "The basename must be defined as a formatted string where tile index is at position 1 " - "and original filename is at position 0. For instance, tile{1}.tif will generate the " - "filename tile75.tif for the tile id = 75.", - default="{}_tile{}" - ) - parser.add_argument( - "-d", - "--dir", - dest="subdir_name", - help="When each tile must be generated in a different subdir, it defines the naming " - "convention for the subdir. It is a formatted string with one positional parameter " - "corresponding to the tile index. For instance, tile{} will generate the subdir " - "name tile75/ for the tile id = 75. By default, subdir is not defined and output " - "files will be generated directly in the outputdir." - ) - - # set the function to call when this subcommand is called - parser.set_defaults(func=create_tiling) - - return rastertools_parsers - - -def create_tiling(args) -> Tiling: - """Create and configure a new rastertool "Tiling" according to argparse args + CHANGE DOCSTRING + + ADD INPUTS + Create and configure a new rastertool "Tiling" according to argparse args + + Generate tiles of an input raster image following the geometries defined by a + given grid + Args: args: args extracted from command line @@ -91,12 +54,16 @@ def create_tiling(args) -> Tiling: Returns: :obj:`eolab.rastertools.Tiling`: The configured rastertool to run """ + if id == () : + id = None # create the rastertool object - tool = Tiling(args.grid_file) + tool = Tiling(grid_file) # set up config with args values - tool.with_output(args.output, args.output_name, args.subdir_name) - tool.with_id_column(args.id_column, args.id) + tool.with_output(output, output_name, subdir_name) + tool.with_id_column(id_column, id) + + apply_process(ctx, tool, inputs) + - return tool diff --git a/src/eolab/rastertools/cli/timeseries.py b/src/eolab/rastertools/cli/timeseries.py index 4279b97..733be56 100644 --- a/src/eolab/rastertools/cli/timeseries.py +++ b/src/eolab/rastertools/cli/timeseries.py @@ -5,99 +5,85 @@ """ from datetime import datetime -import eolab.rastertools.cli as cli -from eolab.rastertools import RastertoolConfigurationException, Timeseries +from eolab.rastertools import Timeseries +from eolab.rastertools.cli.utils_cli import apply_process +from eolab.rastertools import RastertoolConfigurationException +import click +import os +CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) -def create_argparser(rastertools_parsers): - """Adds the timeseries subcommand to the given rastertools subparser - Args: - rastertools_parsers: - The rastertools subparsers to which this subcommand shall be added. +#Speed command +@click.command("timeseries",context_settings=CONTEXT_SETTINGS) +@click.argument('inputs', type=str, nargs = -1, required = 1) + +@click.option('-b','--bands', type=list, help="List of bands to process") + +@click.option('-a', '--all','all_bands', type=bool, is_flag=True, help="Process all bands") + +@click.option('-o','--output', default = os.getcwd(), help="Output directory to store results (by default current directory)") + +@click.option("-s","--start_date", help="Start date of the timeseries to generate in the following format: yyyy-MM-dd") + +@click.option("-e", "--end_date", help="End date of the timeseries to generate in the following format: yyyy-MM-dd") - This argument provides from a code like this:: +@click.option("-p", "--time_period",type=int, help="Time period (number of days) between two consecutive images in the timeseries " + "to generate e.g. 10 = generate one image every 10 days") + +@click.option('-ws', '--window_size', type=int, default = 1024, help="Size of tiles to distribute processing, default: 1024") - import argparse - main_parser = argparse.ArgumentParser() - rastertools_parsers = main_parser.add_subparsers() - timeseries.create_argparser(rastertools_parsers) +@click.pass_context - Returns: - The rastertools subparsers updated with this subcommand +def timeseries(ctx, inputs : list, bands : list, all_bands : bool, output : str, start_date : str, end_date : str, time_period : int, window_size : int) : """ - parser = rastertools_parsers.add_parser( - "timeseries", aliases=["ts"], - help="Temporal gap filling of an image time series", - description="Generate a timeseries of images (without gaps) from a set of input images. " + Create and configure a new rastertool "Timeseries" according to argparse args + CHANGE DOCSTRING + Adds the timeseries subcommand to the given rastertools subparser + + Temporal gap filling of an image time series + Generate a timeseries of images (without gaps) from a set of input images. " "Data not present in the input images (no image for the date or masked data) " "are interpolated (with linear interpolation) so that all gaps are filled.", - epilog="By default only first band is computed.") - parser.add_argument( - "inputs", - nargs='+', - help="Input files to process (e.g. Sentinel2 L2A MAJA from THEIA). " - "You can provide a single file with extension \".lst\" (e.g. \"speed.lst\") " - "that lists the input files to process (one input file per line in .lst)") - cli.with_bands_arguments(parser) - cli.with_outputdir_arguments(parser) - parser.add_argument( - "-s", - "--start_date", - help="Start date of the timeseries to generate in the following format: yyyy-MM-dd") - parser.add_argument( - "-e", - "--end_date", - help="End date of the timeseries to generate in the following format: yyyy-MM-dd") - parser.add_argument( - "-p", - "--time_period", - type=int, - help="Time period (number of days) between two consecutive images in the timeseries " - "to generate e.g. 10 = generate one image every 10 days") - cli.with_window_arguments(parser, pad=False) + epilog="By default only first band is computed. - # set the function to call when this subcommand is called - parser.set_defaults(func=create_timeseries) - return rastertools_parsers + ADD INPUTS + INPUTS -def create_timeseries(args) -> Timeseries: - """Create and configure a new rastertool "Timeseries" according to argparse args + Input files to process (e.g. Sentinel2 L2A MAJA from THEIA). + You can provide a single file with extension .lst (e.g. speed.lst) + that lists the input files to process (one input file per line in .lst)) Args: args: args extracted from command line - - Returns: - :obj:`eolab.rastertools.Timeseries`: The configured rastertool to run """ - # get the bands to process - if args.all_bands: + if all_bands: bands = None else: - bands = list(map(int, args.bands)) if args.bands else [1] + bands = list(map(int, bands)) if bands else [1] # convert start/end dates to datetime try: - start_date = datetime.strptime(args.start_date, "%Y-%m-%d") + start_date = datetime.strptime(start_date, "%Y-%m-%d") except Exception: raise RastertoolConfigurationException( - f"Invalid format for start date: {args.start_date} (must be %Y-%m-%d)") + f"Invalid format for start date: {start_date} (must be %Y-%m-%d)") # convert start/end dates to datetime try: - end_date = datetime.strptime(args.end_date, "%Y-%m-%d") + end_date = datetime.strptime(end_date, "%Y-%m-%d") except Exception: raise RastertoolConfigurationException( - f"Invalid format for end date: {args.end_date} (must be %Y-%m-%d)") + f"Invalid format for end date: {end_date} (must be %Y-%m-%d)") # create the rastertool object - tool = Timeseries(start_date, end_date, args.time_period, bands) + tool = Timeseries(start_date, end_date, time_period, bands) # set up config with args values - tool.with_output(args.output) - tool.with_windows(args.window_size) + tool.with_output(output) + tool.with_windows(window_size) - return tool + apply_process(ctx, tool, inputs) \ No newline at end of file diff --git a/src/eolab/rastertools/cli/utils_cli.py b/src/eolab/rastertools/cli/utils_cli.py new file mode 100644 index 0000000..f1d508d --- /dev/null +++ b/src/eolab/rastertools/cli/utils_cli.py @@ -0,0 +1,86 @@ +from eolab.rastertools import RastertoolConfigurationException +import logging +import sys +import click + +#TO DO +_logger = logging.getLogger("main") + +def _extract_files_from_list(cmd_inputs): + """ + Extracts a list of file paths from a command line input. + + If the input is a single file with a `.lst` extension, it reads the file line-by-line and treats each + line as an individual file path, returning the list of paths. If the input is already a + list of file paths, it is returned as-is. + + Args: + cmd_inputs (list of str): + Command line inputs for file paths. If it contains a single `.lst` file, this file + is read to obtain the list of files. Otherwise, it is assumed to be a direct list of files. + + Returns: + list of str: A list of file paths, either extracted from the `.lst` file or passed directly. + + Example: + _extract_files_from_list(["files.lst"]) + + _extract_files_from_list(["file1.tif", "file2.tif"]) + + Notes: + The `.lst` file is expected to have one file path per line. Blank lines in the `.lst` + file will be ignored. + """ + + # handle the input file of type "lst" + if len(cmd_inputs) == 1 and cmd_inputs[0][-4:].lower() == ".lst": + # parse the listing + with open(cmd_inputs[0]) as f: + inputs = f.read().splitlines() + else: + inputs = cmd_inputs + + return inputs + + +def apply_process(ctx, tool, inputs : list): + """ + Apply the chosen process to a set of input files. + + This function extracts input files, configures the tool, and processes the files + through the specified tool. It also handles debug settings and intermediate file storage + (VRT files). In case of any errors, the function logs the exception and terminates the process + with an appropriate exit code. + + Args: + ctx (click.Context): The context object containing configuration options like whether + to store intermediate VRT files. + tool (Filtering or Hillshade or ...): The tool instance that has been configured with the provided parameters. + inputs (str): A path to a list of input files, either as a single `.lst` file or a direct + list of file paths. + + Raises: + RastertoolConfigurationException: If there is a configuration error with the tool. + Exception: Any other errors that occur during processing. + """ + try: + # handle the input file of type "lst" + inputs_extracted = _extract_files_from_list(inputs) + + # setup debug mode in which intermediate VRT files are stored to disk or not + tool.with_vrt_stored(ctx.obj.get('keep_vrt')) + + # launch process + tool.process_files(inputs_extracted) + + _logger.info("Done!") + + except RastertoolConfigurationException as rce: + _logger.exception(rce) + sys.exit(2) + + except Exception as err: + _logger.exception(err) + sys.exit(1) + + sys.exit(0) \ No newline at end of file diff --git a/src/eolab/rastertools/main.py b/src/eolab/rastertools/main.py index 995d70b..2985ca3 100644 --- a/src/eolab/rastertools/main.py +++ b/src/eolab/rastertools/main.py @@ -16,17 +16,16 @@ import sys import json import click -from eolab.rastertools.cli.filtering import filter +from eolab.rastertools.cli.filtering_dyn import filter +from eolab.rastertools.cli.hillshade import hillshade +from eolab.rastertools.cli.speed import speed +from eolab.rastertools.cli.svf import svf +from eolab.rastertools.cli.tiling import tiling +from eolab.rastertools.cli.timeseries import timeseries #radioindice, zonalstats from eolab.rastertools import __version__ -from eolab.rastertools.cli import radioindice, zonalstats, tiling, speed -from eolab.rastertools.cli import filtering, svf, hillshade, timeseries from eolab.rastertools.product import RasterType -_logger = logging.getLogger(__name__) -def get_logger(): - return _logger - def add_custom_rastertypes(rastertypes): """Add definition of new raster types. The json string shall have the following format: @@ -126,15 +125,16 @@ def add_custom_rastertypes(rastertypes): """ RasterType.add(rastertypes) -@click.group() +CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) +@click.group(context_settings=CONTEXT_SETTINGS) @click.option( '-t', '--rastertype', 'rastertype', + default = None, # Click automatically uses the last argument as the variable name, so "dest" is this last parameter type=click.Path(exists=True), help="JSON file defining additional raster types of input files") - @click.option( '--max_workers', "max_workers", @@ -142,49 +142,43 @@ def add_custom_rastertypes(rastertypes): help="Maximum number of workers for parallel processing. If not given, it will default to " "the number of processors on the machine. When all processors are not allocated to " "run rastertools, it is thus recommended to set this option.") - @click.option( '--debug', "keep_vrt", is_flag=True, help="Store to disk the intermediate VRT images that are generated when handling " "the input files which can be complex raster product composed of several band files.") - @click.option( '-v', '--verbose', is_flag=True, help="set loglevel to INFO") - @click.option( '-vv', '--very-verbose', is_flag=True, help="set loglevel to DEBUG") - @click.version_option(version='rastertools {}'.format(__version__)) # Ensure __version__ is defined - @click.pass_context def rastertools(ctx, rastertype : str, max_workers : int, keep_vrt : bool, verbose : bool, very_verbose : bool): """ - Collection of tools on raster data. - CHANGE DOCSTRING - Main entry point allowing external calls. - - Args: - rastertype: JSON file defining additional raster types. - max_workers: Maximum number of workers for parallel processing. - keep_vrt: Store intermediate VRT images. - verbose: Set loglevel to INFO. - very_verbose: Set loglevel to DEBUG. - command: The command to execute (e.g., filtering). - inputs: Input files for processing. - - sys.exit returns: - - - 0: everything runs fine - - 1: processing errors occured - - 2: wrong execution configuration + Main entry point for the `rastertools` Command Line Interface. + + The `rastertools` CLI provides tools for raster processing + and analysis and allows configurable data handling, parallel processing, + and debugging support. + + Logging: + + - INFO level (`-v`) gives detailed step information. + + - DEBUG level (`-vv`) offers full debug-level tracing. + + Environment Variables: + + - `RASTERTOOLS_NOTQDM`: If the log level is above INFO, sets this to disable progress bars. + + - `RASTERTOOLS_MAXWORKERS`: If `max_workers` is set, it defines the max workers for rastertools. """ ctx.ensure_object(dict) ctx.obj['keep_vrt'] = keep_vrt @@ -194,6 +188,9 @@ def rastertools(ctx, rastertype : str, max_workers : int, keep_vrt : bool, verbo loglevel = logging.DEBUG elif verbose: loglevel = logging.INFO + else: + loglevel = logging.WARNING + logformat = "[%(asctime)s] %(levelname)s - %(name)s - %(message)s" logging.basicConfig(level=loglevel, stream=sys.stdout, format=logformat, datefmt="%Y-%m-%d %H:%M:%S") @@ -210,14 +207,21 @@ def rastertools(ctx, rastertype : str, max_workers : int, keep_vrt : bool, verbo # Register subcommands from other modules -rastertools.add_command(filter) -#rastertools.add_command(hillshade) -#rastertools.add_command(radioindice) -#rastertools.add_command(speed) -#rastertools.add_command(svf) -#rastertools.add_command(tiling) -#rastertools.add_command(timeseries) -#rastertools.add_command(zonalstats) +rastertools.add_command(filter, name = "fi") +rastertools.add_command(filter, name = "filter") +rastertools.add_command(hillshade, name = "hs") +rastertools.add_command(hillshade, name = "hillshade") +#rastertools.add_command(radioindice, name = "ri") +#rastertools.add_command(radioindice, name = "radioindice") +rastertools.add_command(speed, name = "sp") +rastertools.add_command(speed, name = "speed") +rastertools.add_command(svf, name = "svf") +rastertools.add_command(tiling, name = "ti") +rastertools.add_command(tiling, name = "tiling") +rastertools.add_command(timeseries, name = "ts") +rastertools.add_command(timeseries, name = "timeseries") +#rastertools.add_command(zonalstats, name = "zs") +#rastertools.add_command(zonalstats, name = "zonalstats") @rastertools.result_callback() @click.pass_context @@ -226,10 +230,10 @@ def handle_result(ctx): click.echo(ctx.get_help()) ctx.exit() -def run(): +def run(*args, **kwargs): """Entry point for console_scripts """ - rastertools() + rastertools(*args, **kwargs) if __name__ == "__main__": diff --git a/tests/test_rastertools.py b/tests/test_rastertools.py index f9c54b4..110a458 100644 --- a/tests/test_rastertools.py +++ b/tests/test_rastertools.py @@ -5,7 +5,10 @@ import logging import filecmp from pathlib import Path -from eolab.rastertools import run_tool + +from click import argument + +from eolab.rastertools import rastertools from eolab.rastertools.product import RasterType from . import utils4test @@ -94,7 +97,7 @@ def run_test(self, caplog=None, loglevel=logging.ERROR, check_outputs=True, chec # run rastertools with pytest.raises(SystemExit) as wrapped_exception: - run_tool(args=self._args) + rastertools(self.args) # check sys_exit if check_sys_exit: @@ -134,23 +137,31 @@ def test_rastertools_command_line_info(): TestCase("-h"), TestCase("--version"), TestCase(""), - TestCase("radioindice --help"), - TestCase("ri -h"), - TestCase("zonalstats --help"), - TestCase("zs -h"), - TestCase("tiling --help"), - TestCase("ti -h"), TestCase("filter --help"), - TestCase("fi -h"), - TestCase("timeseries --help"), - TestCase("ts -h"), - TestCase("speed --help"), - TestCase("sp -h"), - TestCase("svf --help"), - TestCase("svf -h"), - TestCase("hillshade --help"), - TestCase("hs -h") - ] + TestCase("fi -h") + ] + # tests = [ + # TestCase("--help"), + # TestCase("-h"), + # TestCase("--version"), + # TestCase(""), + # TestCase("radioindice --help"), + # TestCase("ri -h"), + # TestCase("zonalstats --help"), + # TestCase("zs -h"), + # TestCase("tiling --help"), + # TestCase("ti -h"), + # TestCase("filter --help"), + # TestCase("fi -h"), + # TestCase("timeseries --help"), + # TestCase("ts -h"), + # TestCase("speed --help"), + # TestCase("sp -h"), + # TestCase("svf --help"), + # TestCase("svf -h"), + # TestCase("hillshade --help"), + # TestCase("hs -h") + # ] for test in tests: test.run_test() @@ -670,13 +681,13 @@ def test_filtering_command_line_errors(caplog): # list of commands to test argslist = [ # output dir does not exist - "-v fi median --kernel_size 8 -o tests/truc" + "-v filter median --kernel_size 8 -o tests/truc" " tests/tests_data/tif_file.tif", # missing required argument - "-v fi adaptive_gaussian --kernel_size 32 -o tests/tests_out" + "-v filter adaptive_gaussian --kernel_size 32 -o tests/tests_out" " tests/tests_data/RGB_TIF_20170105_013442_test.tif", # kernel_size > window_size - "-v fi median -a --kernel_size 15 --window_size 16 -o tests/tests_out" + "-v filter median -a --kernel_size 15 --window_size 16 -o tests/tests_out" " tests/tests_data/RGB_TIF_20170105_013442_test.tif", ] @@ -707,8 +718,7 @@ def test_svf_command_line_default(): # list of commands to test argslist = [ # default case: svf at the point height - "-v svf --radius 50 --directions 16 --resolution 0.5 -o tests/tests_out" - " tests/tests_data/toulouse-mnh.tif", + "ftilin", # default case: svf on ground "-v svf --radius 50 --directions 16 --resolution 0.5 --altitude 0 -o tests/tests_out" " tests/tests_data/toulouse-mnh.tif", From c41b441d6159db9f774ba10f4be9b88ab915ce2b Mon Sep 17 00:00:00 2001 From: cadauxe Date: Fri, 8 Nov 2024 16:00:26 +0100 Subject: [PATCH 04/17] refactor: replaced argparse by click --- src/eolab/rastertools/cli/filtering.py | 5 +- src/eolab/rastertools/cli/filtering_dyn.py | 35 ++-- src/eolab/rastertools/cli/radioindice.py | 40 ++-- src/eolab/rastertools/cli/zonalstats.py | 212 +++++++-------------- src/eolab/rastertools/main.py | 12 +- tests/test_rastertools.py | 13 +- 6 files changed, 129 insertions(+), 188 deletions(-) diff --git a/src/eolab/rastertools/cli/filtering.py b/src/eolab/rastertools/cli/filtering.py index 3501c84..fd74fca 100644 --- a/src/eolab/rastertools/cli/filtering.py +++ b/src/eolab/rastertools/cli/filtering.py @@ -210,8 +210,9 @@ def mean(ctx, inputs : list, output : str, window_size : int, pad : str, kernel_ @pad_opt @band_opt @all_opt +@click.option('--sigma', type = int, default = 1, help = "Standard deviation of the Gaussian distribution") @click.pass_context -def adaptive_gaussian(ctx, inputs : list, output : str, window_size : int, pad : str, kernel_size : int, bands : list, all_bands : bool) : +def adaptive_gaussian(ctx, inputs : list, output : str, window_size : int, pad : str, sigma : int, kernel_size : int, bands : list, all_bands : bool) : """ Execute the adaptive gaussian filter on the input files with the specified parameters. @@ -233,7 +234,7 @@ def adaptive_gaussian(ctx, inputs : list, output : str, window_size : int, pad : output=output, window_size=window_size, pad=pad, - argsdict={"inputs": inputs}, + argsdict={"inputs": inputs, "sigma" : sigma}, filter='adaptive_gaussian', bands=bands, kernel_size=kernel_size, diff --git a/src/eolab/rastertools/cli/filtering_dyn.py b/src/eolab/rastertools/cli/filtering_dyn.py index 2ba8274..9c6d34b 100644 --- a/src/eolab/rastertools/cli/filtering_dyn.py +++ b/src/eolab/rastertools/cli/filtering_dyn.py @@ -3,6 +3,8 @@ """ CLI definition for the filtering tool """ +from typing import Callable + from eolab.rastertools import Filtering #from eolab.rastertools.main import get_logger from eolab.rastertools.cli.utils_cli import apply_process @@ -51,6 +53,14 @@ def create_filtering(output : str, window_size : int, pad : str, argsdict : dict return tool +def filter_options(options : list): + def wrapper(function): + for option in options: + function = option(function) + return function + return wrapper + + inpt_arg = click.argument('inputs', type=str, nargs = -1, required = 1) ker_opt = click.option('--kernel_size', type=int, help="Kernel size of the filter function, e.g. 3 means a square" @@ -66,10 +76,12 @@ def create_filtering(output : str, window_size : int, pad : str, argsdict : dict "(see https://numpy.org/doc/stable/reference/generated/numpy.pad.html" "for more information)") -band_opt = click.option('-b','--bands', type=int, multiple = True, help="List of bands to process") +band_opt = click.option('-b','--bands', multiple = True, type=int, help="List of bands to process") all_opt = click.option('-a', '--all','all_bands', type=bool, is_flag=True, help="Process all bands") +sigma = click.option('--sigma', type=int, default=1, help="Standard deviation of the Gaussian distribution") + @click.group(context_settings=CONTEXT_SETTINGS) @click.pass_context def filter(ctx): @@ -81,16 +93,14 @@ def filter(ctx): def create_filter(filter_name : str): + list_opt = [inpt_arg, ker_opt, out_opt, win_opt, pad_opt, band_opt, all_opt] + if filter_name == 'adaptive_gaussian': + list_opt.append(sigma) + @filter.command(filter_name, context_settings=CONTEXT_SETTINGS) - @inpt_arg - @ker_opt - @out_opt - @win_opt - @pad_opt - @band_opt - @all_opt + @filter_options(list_opt) @click.pass_context - def filter_filtername(ctx, inputs : list, output : str, window_size : int, pad : str, kernel_size : int, bands : list, all_bands : bool): + def filter_filtername(ctx, inputs : list, output : str, window_size : int, pad : str, kernel_size : int, bands : list, all_bands : bool, **kwargs): """ Execute the requested filter on the input files with the specified parameters. The `inputs` argument can either be a single file or a `.lst` file containing a list of input files. @@ -103,14 +113,17 @@ def filter_filtername(ctx, inputs : list, output : str, window_size : int, pad : You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") that lists the input files to process (one input file per line in .lst). """ + argsdict = {"inputs": inputs} + + if filter_name == 'adaptive_gaussian': + argsdict = {"sigma" : kwargs["sigma"]} - print(bands) # Configure the filter tool instance tool = create_filtering( output=output, window_size=window_size, pad=pad, - argsdict={"inputs": inputs}, + argsdict=argsdict, filter=filter_name, bands=bands, kernel_size=kernel_size, diff --git a/src/eolab/rastertools/cli/radioindice.py b/src/eolab/rastertools/cli/radioindice.py index c7491ca..f8cdf3a 100644 --- a/src/eolab/rastertools/cli/radioindice.py +++ b/src/eolab/rastertools/cli/radioindice.py @@ -12,15 +12,14 @@ CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) -def parse_normalized_difference(ctx, param, value): - """Parse the pairs of bands as (band1, band2) tuples.""" - if value: - # Split the input pairs and store them as tuples - parsed_pairs = [] - for i in range(0, len(value), 2): - parsed_pairs.append((value[i], value[i + 1])) - return parsed_pairs - return None + +def indices_opt(function): + list_indices = ['--ndvi', '--tndvi', '--rvi', '--pvi', '--savi', '--tsavi', '--msavi', '--msavi2', '--ipvi', + '--evi', '--ndwi', '--ndwi2', '--mndwi', '--ndpi', '--ndti', '--ndbi', '--ri', '--bi', '--bi2'] + + for idc in list_indices: + function = click.option(idc, is_flag=True, help=f"Compute {id} indice")(function) + return function #Radioindice command @@ -35,14 +34,16 @@ def parse_normalized_difference(ctx, param, value): @click.option('-ws', '--window_size', type=int, default = 1024, help="Size of tiles to distribute processing, default: 1024") -list_indices = ['--ndvi', '--tndvi', '--rvi', '--pvi', '--savi', '--tsavi', '--msavi', '--msavi2', '--ipvi', -'--evi', '--ndwi', '--ndwi2', '--mndwi', '--ndpi', '--ndti', '--ndbi', '--ri', '--bi', '--bi2'] +@click.option('-i', '--indices', type=click.Choice(['ndvi', 'tndvi', 'rvi', 'pvi', 'savi', 'tsavi', 'msavi', 'msavi2', 'ipvi', + 'evi', 'ndwi', 'ndwi2', 'mndwi', 'ndpi', 'ndti', 'ndbi', 'ri', 'bi', 'bi2']), multiple = True, + help=" List of indices to computePossible indices are: bi, bi2, evi, ipvi, mndwi, msavi, msavi2, ndbi, ndpi," + " ndti, ndvi, ndwi, ndwi2, pvi, ri, rvi, savi, tndvi, tsavi") + -for id in list_indices: - @click.option(id, is_flag = True, help=f"Compute {id} indice") +@indices_opt @click.option('-nd', '--normalized_difference','nd',type=str, - multiple=True, nargs=2, callback= parse_normalized_difference, metavar="band1 band2", + multiple=True, nargs=2, metavar="band1 band2", help="Compute the normalized difference of two bands defined" "as parameter of this option, e.g. \"-nd red nir\" will compute (red-nir)/(red+nir). " "See eolab.rastertools.product.rastertype.BandChannel for the list of bands names. " @@ -50,7 +51,7 @@ def parse_normalized_difference(ctx, param, value): @click.pass_context -def radioindice(ctx, inputs : list, output : str, merge : bool, roi : str, window_size : int, nd : bool, *args) : +def radioindice(ctx, inputs : list, output : str, indices : list, merge : bool, roi : str, window_size : int, nd : bool, **kwargs) : """Create and configure a new rastertool "Radioindice" according to argparse args Args: @@ -58,12 +59,14 @@ def radioindice(ctx, inputs : list, output : str, merge : bool, roi : str, windo Returns: :obj:`eolab.rastertools.Radioindice`: The configured rastertool to run - """ + """ + indices_opt = [key for key, value in kwargs.items() if value] indices_to_compute = [] # append indices defined with -- indices_to_compute.extend([indice for indice in Radioindice.get_default_indices() - if indices]) + if indice.name in indices_opt]) + # append indices defined with --indices if indices: indices_dict = {indice.name: indice for indice in Radioindice.get_default_indices()} @@ -99,5 +102,6 @@ def radioindice(ctx, inputs : list, output : str, merge : bool, roi : str, windo tool.with_roi(roi) tool.with_windows(window_size) - return tool + apply_process(ctx, tool, inputs) + diff --git a/src/eolab/rastertools/cli/zonalstats.py b/src/eolab/rastertools/cli/zonalstats.py index 1f86950..889ceb0 100644 --- a/src/eolab/rastertools/cli/zonalstats.py +++ b/src/eolab/rastertools/cli/zonalstats.py @@ -3,150 +3,70 @@ """ CLI definition for the zonalstats tool """ -import eolab.rastertools.cli as cli from eolab.rastertools import Zonalstats +from eolab.rastertools.cli.utils_cli import apply_process +import click +import os +CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) -def create_argparser(rastertools_parsers): - """Adds the zonalstats subcommand to the given rastertools subparser - Args: - rastertools_parsers: - The rastertools subparsers to which this subcommand shall be added. +#Zonalstats command +@click.command("radioindice",context_settings=CONTEXT_SETTINGS) +@click.argument('inputs', type=str, nargs = -1, required = 1) - This argument provides from a code like this:: +@click.option('-o','--output', default = os.getcwd(), help="Output directory to store results (by default current directory)") - import argparse - main_parser = argparse.ArgumentParser() - rastertools_parsers = main_parser.add_subparsers() - zonalstats.create_argparser(rastertools_parsers) +@click.option('-f', '--format', "output_format", type = str, help="Output format of the results when input geometries are provided (by default ESRI " + "Shapefile). Possible values are ESRI Shapefile, GeoJSON, CSV, GPKG, GML") - Returns: - The rastertools subparsers updated with this subcommand - """ - parser = rastertools_parsers.add_parser( - "zonalstats", aliases=["zs"], - help="Compute zonal statistics", - description="Compute zonal statistics of a raster image.\n Available statistics are: " - "min max range mean std percentile_x (x in [0, 100]) median mad " - "count valid nodata sum majority minority unique", - epilog="By default only first band is computed.") - parser.add_argument( - "inputs", - nargs='+', - help="Raster files to process. " - "You can provide a single file with extension \".lst\" (e.g. \"zonalstats.lst\") " - "that lists the input files to process (one input file per line in .lst)") - cli.with_outputdir_arguments(parser) - parser.add_argument( - '-f', - '--format', - dest="output_format", - help="Output format of the results when input geometries are provided (by default ESRI " - f"Shapefile). Possible values are {', '.join(Zonalstats.supported_output_formats)}") - parser.add_argument( - '-g', - '--geometry', - dest="geometries", - help="List of geometries where to compute statistics (vector like a shapefile or geojson)") - parser.add_argument( - '-w', - '--within', - dest="within", - action="store_true", - help="When activated, statistics are computed for the geometries that are within " +@click.option('-g','--geometry',"geometries",type = str, help="List of geometries where to compute statistics (vector like a shapefile or geojson)") + +@click.option('-w','--within',is_flag = True, help="When activated, statistics are computed for the geometries that are within " "the raster shape. The default behaviour otherwise is to compute statistics " "for all geometries that intersect the raster shape.") - parser.add_argument( - '--stats', - dest="stats", - nargs="+", - help="List of stats to compute. Possible stats are: " - "min max range mean std percentile_x (x in [0, 100]) median mad " - "count valid nodata sum majority minority unique") - parser.add_argument( - '--categorical', - dest="categorical", - action="store_true", - help="If the input raster is categorical (i.e. raster values represent discrete classes) " + +@click.option('--stats', multiple = True,help="List of stats to compute. Possible stats are: " + "min max range mean std percentile_x (x in [0, 100]) median mad count valid nodata sum majority minority unique") + +@click.option('--categorical',is_flag = True,help="If the input raster is categorical (i.e. raster values represent discrete classes) " "compute the counts of every unique pixel values.") - parser.add_argument( - '--valid_threshold', - dest="valid_threshold", - type=float, - help="Minimum percentage of valid pixels in a shape to compute its statistics.") - parser.add_argument( - '--area', - dest='area', - action="store_true", - help="Whether to multiply all stats by the area of a cell of the input raster." - ) - parser.add_argument( - '--prefix', - dest="prefix", - help="Add a prefix to the keys (default: None). One prefix per band (e.g. 'band1 band2')") - - cli.with_bands_arguments(parser) - - # argument group for the outliers image generation - outliers_pc = parser.add_argument_group("Options to output the outliers") - outliers_pc.add_argument( - '--sigma', - dest="sigma", - help="Distance to the mean value (in sigma) in order to produce a raster " - "that highlights outliers.") - - # argument group for generating stats charts - chart_pc = parser.add_argument_group('Options to plot the generated stats') - chart_pc.add_argument( - '-c', - '--chart', - dest="chartfile", - help="Generate a chart per stat and per geometry " - "(x=timestamp of the input products / y=stat value) and " + +@click.option('--valid_threshold',"valid_threshold",type=float,help="Minimum percentage of valid pixels in a shape to compute its statistics.") + +@click.option('--area',is_flag=True,help="Whether to multiply all stats by the area of a cell of the input raster.") + +@click.option('--prefix', default = None, help="Add a prefix to the keys (default: None). One prefix per band (e.g. 'band1 band2')") + +@click.option('-b','--bands', multiple = True, type=int, help="List of bands to process") + +@click.option('-a', '--all','all_bands', type=bool, is_flag=True, help="Process all bands") + +@click.option('--sigma',help="Distance to the mean value (in sigma) in order to produce a raster that highlights outliers.") + +@click.option('-c','--chart',"chartfile", help="Generate a chart per stat and per geometry (x=timestamp of the input products / y=stat value) and " "store it in the file defined by this argument") - chart_pc.add_argument( - '-d', - '--display', - dest="display", - action="store_true", - help="Display the chart") - chart_pc.add_argument( - '-gi', - '--geometry-index', - dest="geom_index", - default='ID', - help="Name of the geometry index used for the chart (default='ID')") - - # argument group to generate stats per category - group_pc = parser.add_argument_group('Options to compute stats per category in geometry. ' - 'If activated, the generated geometries will contain ' - 'stats for every categories present in the geometry') - group_pc.add_argument( - '--category_file', - help="File (raster or geometries) containing discrete classes classifying the ROI." - ) - group_pc.add_argument( - '--category_index', - help="Column name identifying categories in categroy_file " - "(only if file format is geometries)", - default="Classe" - ) - group_pc.add_argument( - '--category_names', - help="JSON files containing a dict with classes index as keys and names " - "to display classes as values.", - default="" - ) - - # set the function to call when this subcommand is called - parser.set_defaults(func=create_zonalstats) - - return rastertools_parsers - - -def create_zonalstats(args) -> Zonalstats: - """Create and configure a new rastertool "Zonalstats" according to argparse args + +@click.option('-d','--display',is_flag=True, help="Display the chart") + +@click.option('-gi','--geometry-index', "geom_index",type = str,default='ID',help="Name of the geometry index used for the chart (default='ID')") + +@click.option('--category_file',type = str, help="File (raster or geometries) containing discrete classes classifying the ROI.") + +@click.option('--category_index',type = str, default="Classe",help="Column name identifying categories in categroy_file (only if file format is geometries)") + +@click.option('--category_names',type = str, default="", help="JSON files containing a dict with classes index as keys and names to display classes as values.") + +@click.pass_context +def zonalstats(ctx, inputs : list, output : str, output_format : str, geometries : str, within : str, stats : int, categorical : bool, valid_threshold : float ,area : bool, prefix, bands : list, all_bands : bool, sigma, chartfile, display : bool, geom_index : str, category_file : str, category_index : str, category_names : str) : + """ + Compute zonal statistics + Compute zonal statistics of a raster image.\n Available statistics are: + min max range mean std percentile_x (x in [0, 100]) median mad + count valid nodata sum majority minority unique + By default only first band is computed. + + Create and configure a new rastertool "Zonalstats" according to argparse args Args: args: args extracted from command line @@ -155,28 +75,28 @@ def create_zonalstats(args) -> Zonalstats: :obj:`eolab.rastertools.Zonalstats`: The configured rastertool to run """ # get and check the list of stats to compute - if args.stats: - stats_to_compute = args.stats - elif args.categorical: + if stats: + stats_to_compute = stats + elif categorical: stats_to_compute = [] else: stats_to_compute = ['count', 'min', 'max', 'mean', 'std'] # get the bands to process - if args.all_bands: + if all_bands: bands = None else: - bands = list(map(int, args.bands)) if args.bands else [1] + bands = list(map(int, bands)) if bands else [1] # create the rastertool object - tool = Zonalstats(stats_to_compute, args.categorical, args.valid_threshold, - args.area, args.prefix, bands) + tool = Zonalstats(stats_to_compute, categorical, valid_threshold, + area, prefix, bands) # set up config with args values - tool.with_output(args.output, args.output_format) \ - .with_geometries(args.geometries, args.within) \ - .with_outliers(args.sigma) \ - .with_chart(args.chartfile, args.geom_index, args.display) \ - .with_per_category(args.category_file, args.category_index, args.category_names) + tool.with_output(output, output_format) \ + .with_geometries(geometries, within) \ + .with_outliers(sigma) \ + .with_chart(chartfile, geom_index, display) \ + .with_per_category(category_file, category_index, category_names) - return tool + apply_process(ctx, tool, inputs) diff --git a/src/eolab/rastertools/main.py b/src/eolab/rastertools/main.py index 2985ca3..44882b1 100644 --- a/src/eolab/rastertools/main.py +++ b/src/eolab/rastertools/main.py @@ -21,7 +21,9 @@ from eolab.rastertools.cli.speed import speed from eolab.rastertools.cli.svf import svf from eolab.rastertools.cli.tiling import tiling -from eolab.rastertools.cli.timeseries import timeseries #radioindice, zonalstats +from eolab.rastertools.cli.timeseries import timeseries +from eolab.rastertools.cli.radioindice import radioindice +from eolab.rastertools.cli.zonalstats import zonalstats from eolab.rastertools import __version__ from eolab.rastertools.product import RasterType @@ -211,8 +213,8 @@ def rastertools(ctx, rastertype : str, max_workers : int, keep_vrt : bool, verbo rastertools.add_command(filter, name = "filter") rastertools.add_command(hillshade, name = "hs") rastertools.add_command(hillshade, name = "hillshade") -#rastertools.add_command(radioindice, name = "ri") -#rastertools.add_command(radioindice, name = "radioindice") +rastertools.add_command(radioindice, name = "ri") +rastertools.add_command(radioindice, name = "radioindice") rastertools.add_command(speed, name = "sp") rastertools.add_command(speed, name = "speed") rastertools.add_command(svf, name = "svf") @@ -220,8 +222,8 @@ def rastertools(ctx, rastertype : str, max_workers : int, keep_vrt : bool, verbo rastertools.add_command(tiling, name = "tiling") rastertools.add_command(timeseries, name = "ts") rastertools.add_command(timeseries, name = "timeseries") -#rastertools.add_command(zonalstats, name = "zs") -#rastertools.add_command(zonalstats, name = "zonalstats") +rastertools.add_command(zonalstats, name = "zs") +rastertools.add_command(zonalstats, name = "zonalstats") @rastertools.result_callback() @click.pass_context diff --git a/tests/test_rastertools.py b/tests/test_rastertools.py index 110a458..522ae55 100644 --- a/tests/test_rastertools.py +++ b/tests/test_rastertools.py @@ -177,7 +177,7 @@ def test_radioindice_command_line_default(): # two indices with their own options, merge "-v ri --pvi --savi -o tests/tests_out -m tests/tests_data/listing.lst", # indices option, roi - "--verbose ri --indices pvi savi -nd nir red --roi tests/tests_data/COMMUNE_32001.shp" + "--verbose ri --indices pvi --indices savi -nd nir red --roi tests/tests_data/COMMUNE_32001.shp" " --output tests/tests_out" " tests/tests_data/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip" " tests/tests_data/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip" @@ -542,7 +542,7 @@ def test_tiling_command_line_default(): "--verbose ti -o tests/tests_out -g tests/tests_data/grid.geojson" " tests/tests_data/tif_file.tif", # specify specific ids - "-v ti -o tests/tests_out -g tests/tests_data/grid.geojson --id 77 93 --id_col id" + "-v ti -o tests/tests_out -g tests/tests_data/grid.geojson --id 77 --id 93 --id_col id" " tests/tests_data/tif_file.tif" ] input_filenames = ["tests/tests_data/tif_file.tif"] @@ -578,10 +578,10 @@ def test_tiling_command_line_special_case(caplog): # list of commands to test argslist = [ # some invalid ids - "-v ti -o tests/tests_out -g tests/tests_data/grid.geojson --id 1 2 93 --id_col id" + "-v ti -o tests/tests_out -g tests/tests_data/grid.geojson --id 1 --id 2 --id 93 --id_col id" " tests/tests_data/tif_file.tif", # a geometry does not overlap raster - "-v ti -o tests/tests_out -g tests/tests_data/grid.geojson --id 78 93 --id_col id" + "-v ti -o tests/tests_out -g tests/tests_data/grid.geojson --id 78 --id 93 --id_col id" " tests/tests_data/tif_file.tif" ] @@ -653,7 +653,7 @@ def test_filtering_command_line_default(): "-v --max_workers 1 fi median -a --kernel_size 8 -o tests/tests_out" " tests/tests_data/RGB_TIF_20170105_013442_test.tif", # default case: local sum - "-v fi sum -b 1 2 --kernel_size 8 -o tests/tests_out" + "-v fi sum -b 1 -b 2 --kernel_size 8 -o tests/tests_out" " tests/tests_data/RGB_TIF_20170105_013442_test.tif", # default case: local mean "-v fi mean -b 1 --kernel_size 8 -o tests/tests_out" @@ -718,7 +718,8 @@ def test_svf_command_line_default(): # list of commands to test argslist = [ # default case: svf at the point height - "ftilin", + "-v svf --radius 50 --directions 16 --resolution 0.5 -o tests/tests_out" + " tests/tests_data/toulouse-mnh.tif", # default case: svf on ground "-v svf --radius 50 --directions 16 --resolution 0.5 --altitude 0 -o tests/tests_out" " tests/tests_data/toulouse-mnh.tif", From 4a49d1d0d9db0ee608419083cc26b9725d7ccbf6 Mon Sep 17 00:00:00 2001 From: cadauxe Date: Tue, 12 Nov 2024 10:04:53 +0100 Subject: [PATCH 05/17] refactor: working tests in all directories --- src/eolab/rastertools/cli/filtering.py | 2 +- src/eolab/rastertools/cli/filtering_dyn.py | 2 +- src/eolab/rastertools/cli/hillshade.py | 6 +- src/eolab/rastertools/cli/radioindice.py | 15 +- src/eolab/rastertools/cli/svf.py | 8 +- src/eolab/rastertools/cli/tiling.py | 2 + src/eolab/rastertools/cli/timeseries.py | 16 +- src/eolab/rastertools/cli/utils_cli.py | 9 +- src/eolab/rastertools/cli/zonalstats.py | 8 +- src/eolab/rastertools/main.py | 13 + src/eolab/rastertools/rastertools.py | 8 +- src/eolab/rastertools/tiling.py | 16 +- src/eolab/rastertools/zonalstats.py | 7 +- src/rastertools.egg-info/PKG-INFO | 2 +- src/rastertools.egg-info/SOURCES.txt | 2 + tests/test_rasterproduct.py | 9 +- tests/test_rastertools.py | 406 +++++++++++---------- tests/tests_data/listing.lst | 2 - tests/tests_data/listing2.lst | 2 - tests/tests_data/listing3.lst | 1 - tests/utils4test.py | 16 +- 21 files changed, 320 insertions(+), 232 deletions(-) delete mode 100644 tests/tests_data/listing.lst delete mode 100644 tests/tests_data/listing2.lst delete mode 100644 tests/tests_data/listing3.lst diff --git a/src/eolab/rastertools/cli/filtering.py b/src/eolab/rastertools/cli/filtering.py index fd74fca..2fb4ea6 100644 --- a/src/eolab/rastertools/cli/filtering.py +++ b/src/eolab/rastertools/cli/filtering.py @@ -210,7 +210,7 @@ def mean(ctx, inputs : list, output : str, window_size : int, pad : str, kernel_ @pad_opt @band_opt @all_opt -@click.option('--sigma', type = int, default = 1, help = "Standard deviation of the Gaussian distribution") +@click.option('--sigma', type = int, required = True, help = "Standard deviation of the Gaussian distribution") @click.pass_context def adaptive_gaussian(ctx, inputs : list, output : str, window_size : int, pad : str, sigma : int, kernel_size : int, bands : list, all_bands : bool) : """ diff --git a/src/eolab/rastertools/cli/filtering_dyn.py b/src/eolab/rastertools/cli/filtering_dyn.py index 9c6d34b..3c4057e 100644 --- a/src/eolab/rastertools/cli/filtering_dyn.py +++ b/src/eolab/rastertools/cli/filtering_dyn.py @@ -80,7 +80,7 @@ def wrapper(function): all_opt = click.option('-a', '--all','all_bands', type=bool, is_flag=True, help="Process all bands") -sigma = click.option('--sigma', type=int, default=1, help="Standard deviation of the Gaussian distribution") +sigma = click.option('--sigma', type=int, required = True, help="Standard deviation of the Gaussian distribution") @click.group(context_settings=CONTEXT_SETTINGS) @click.pass_context diff --git a/src/eolab/rastertools/cli/hillshade.py b/src/eolab/rastertools/cli/hillshade.py index 10c82a2..96588cc 100644 --- a/src/eolab/rastertools/cli/hillshade.py +++ b/src/eolab/rastertools/cli/hillshade.py @@ -15,17 +15,17 @@ @click.command("hillshade",context_settings=CONTEXT_SETTINGS) @click.argument('inputs', type=str, nargs = -1, required = 1) -@click.option('--elevation', type=float, help="Elevation of the sun in degrees, [0°, 90°] where" +@click.option('--elevation', type=float, required = True, help="Elevation of the sun in degrees, [0°, 90°] where" "90°=zenith and 0°=horizon") -@click.option('--azimuth', type=float, help="Azimuth of the sun in degrees, [0°, 360°] where" +@click.option('--azimuth', type=float, required = True, help="Azimuth of the sun in degrees, [0°, 360°] where" "0°=north, 90°=east, 180°=south and 270°=west") @click.option('--radius', type=int, help="Maximum distance (in pixels) around a point to evaluate" "horizontal elevation angle. If not set, it is automatically computed from" " the range of altitudes in the digital model.") -@click.option('--resolution',default=0.5, type=float, help="Pixel resolution in meter") +@click.option('--resolution', required = True, type=float, help="Pixel resolution in meter") @click.option('-o','--output', default = os.getcwd(), help="Output directory to store results (by default current directory)") diff --git a/src/eolab/rastertools/cli/radioindice.py b/src/eolab/rastertools/cli/radioindice.py index f8cdf3a..e7f038b 100644 --- a/src/eolab/rastertools/cli/radioindice.py +++ b/src/eolab/rastertools/cli/radioindice.py @@ -3,13 +3,18 @@ """ CLI definition for the radioindice tool """ +import logging + from eolab.rastertools import RastertoolConfigurationException, Radioindice from eolab.rastertools.cli.utils_cli import apply_process from eolab.rastertools.product import BandChannel from eolab.rastertools.processing import RadioindiceProcessing +import sys import click import os +_logger = logging.getLogger(__name__) + CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) @@ -34,8 +39,7 @@ def indices_opt(function): @click.option('-ws', '--window_size', type=int, default = 1024, help="Size of tiles to distribute processing, default: 1024") -@click.option('-i', '--indices', type=click.Choice(['ndvi', 'tndvi', 'rvi', 'pvi', 'savi', 'tsavi', 'msavi', 'msavi2', 'ipvi', - 'evi', 'ndwi', 'ndwi2', 'mndwi', 'ndpi', 'ndti', 'ndbi', 'ri', 'bi', 'bi2']), multiple = True, +@click.option('-i', '--indices', type=str, multiple = True, help=" List of indices to computePossible indices are: bi, bi2, evi, ipvi, mndwi, msavi, msavi2, ndbi, ndpi," " ndti, ndvi, ndwi, ndwi2, pvi, ri, rvi, savi, tndvi, tsavi") @@ -74,7 +78,8 @@ def radioindice(ctx, inputs : list, output : str, indices : list, merge : bool, if ind in indices_dict: indices_to_compute.append(indices_dict[ind]) else: - raise RastertoolConfigurationException(f"Invalid indice name: {ind}") + _logger.exception(RastertoolConfigurationException(f"Invalid indice name: {ind}")) + sys.exit(2) if nd: for nd in nd: @@ -85,8 +90,8 @@ def radioindice(ctx, inputs : list, output : str, indices : list, merge : bool, [channel2, channel1]) indices_to_compute.append(new_indice) else: - raise RastertoolConfigurationException( - f"Invalid band(s) in normalized difference: {nd[0]} and/or {nd[1]}") + _logger.exception(RastertoolConfigurationException(f"Invalid band(s) in normalized difference: {nd[0]} and/or {nd[1]}")) + sys.exit(2) # handle special case: no indice setup if len(indices_to_compute) == 0: diff --git a/src/eolab/rastertools/cli/svf.py b/src/eolab/rastertools/cli/svf.py index 58a15af..6290d4a 100644 --- a/src/eolab/rastertools/cli/svf.py +++ b/src/eolab/rastertools/cli/svf.py @@ -11,15 +11,15 @@ CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) -#Speed command +#SVF command @click.command("svf",context_settings=CONTEXT_SETTINGS) @click.argument('inputs', type=str, nargs = -1, required = 1) -@click.option('--radius',required = True, type=int, default = 16, help="Maximum distance (in pixels) around a point to evaluate horizontal elevation angle") +@click.option('--radius', required = True, type=int, help="Maximum distance (in pixels) around a point to evaluate horizontal elevation angle") -@click.option('--directions',required = True, type=int, default = 12, help="Number of directions on which to compute the horizon elevation angle") +@click.option('--directions',required = True, type=int, help="Number of directions on which to compute the horizon elevation angle") -@click.option('--resolution',default=0.5, type=float, help="Pixel resolution in meter") +@click.option('--resolution', required = True, type=float, help="Pixel resolution in meter") @click.option('--altitude', type=int, help="Reference altitude to use for computing the SVF. If this option is not" " specified, SVF is computed for every point at the altitude of the point") diff --git a/src/eolab/rastertools/cli/tiling.py b/src/eolab/rastertools/cli/tiling.py index 1b67e63..fa6b25a 100644 --- a/src/eolab/rastertools/cli/tiling.py +++ b/src/eolab/rastertools/cli/tiling.py @@ -56,6 +56,8 @@ def tiling(ctx, inputs : list, grid_file : str, id_column : str, id : list, outp """ if id == () : id = None + else: + id = list(id) # create the rastertool object tool = Tiling(grid_file) diff --git a/src/eolab/rastertools/cli/timeseries.py b/src/eolab/rastertools/cli/timeseries.py index 733be56..78465ee 100644 --- a/src/eolab/rastertools/cli/timeseries.py +++ b/src/eolab/rastertools/cli/timeseries.py @@ -9,12 +9,14 @@ from eolab.rastertools.cli.utils_cli import apply_process from eolab.rastertools import RastertoolConfigurationException import click +import sys import os +import logging +_logger = logging.getLogger(__name__) CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) - -#Speed command +#Timeseries command @click.command("timeseries",context_settings=CONTEXT_SETTINGS) @click.argument('inputs', type=str, nargs = -1, required = 1) @@ -69,15 +71,17 @@ def timeseries(ctx, inputs : list, bands : list, all_bands : bool, output : str, try: start_date = datetime.strptime(start_date, "%Y-%m-%d") except Exception: - raise RastertoolConfigurationException( - f"Invalid format for start date: {start_date} (must be %Y-%m-%d)") + _logger.exception(RastertoolConfigurationException( + f"Invalid format for start date: {start_date} (must be %Y-%m-%d)")) + sys.exit(2) # convert start/end dates to datetime try: end_date = datetime.strptime(end_date, "%Y-%m-%d") except Exception: - raise RastertoolConfigurationException( - f"Invalid format for end date: {end_date} (must be %Y-%m-%d)") + _logger.exception(RastertoolConfigurationException( + f"Invalid format for end date: {end_date} (must be %Y-%m-%d)")) + sys.exit(2) # create the rastertool object tool = Timeseries(start_date, end_date, time_period, bands) diff --git a/src/eolab/rastertools/cli/utils_cli.py b/src/eolab/rastertools/cli/utils_cli.py index f1d508d..cc72dc5 100644 --- a/src/eolab/rastertools/cli/utils_cli.py +++ b/src/eolab/rastertools/cli/utils_cli.py @@ -4,7 +4,7 @@ import click #TO DO -_logger = logging.getLogger("main") +_logger = logging.getLogger(__name__) def _extract_files_from_list(cmd_inputs): """ @@ -64,11 +64,14 @@ def apply_process(ctx, tool, inputs : list): Exception: Any other errors that occur during processing. """ try: + print('@' * 50) # handle the input file of type "lst" inputs_extracted = _extract_files_from_list(inputs) + print('@' * 50) # setup debug mode in which intermediate VRT files are stored to disk or not tool.with_vrt_stored(ctx.obj.get('keep_vrt')) + print('@' * 50) # launch process tool.process_files(inputs_extracted) @@ -76,11 +79,13 @@ def apply_process(ctx, tool, inputs : list): _logger.info("Done!") except RastertoolConfigurationException as rce: + print('@'*50) _logger.exception(rce) sys.exit(2) except Exception as err: + print('!' * 50) _logger.exception(err) sys.exit(1) - + print('?' * 50) sys.exit(0) \ No newline at end of file diff --git a/src/eolab/rastertools/cli/zonalstats.py b/src/eolab/rastertools/cli/zonalstats.py index 889ceb0..d1d30d9 100644 --- a/src/eolab/rastertools/cli/zonalstats.py +++ b/src/eolab/rastertools/cli/zonalstats.py @@ -12,7 +12,7 @@ #Zonalstats command -@click.command("radioindice",context_settings=CONTEXT_SETTINGS) +@click.command("zonalstats",context_settings=CONTEXT_SETTINGS) @click.argument('inputs', type=str, nargs = -1, required = 1) @click.option('-o','--output', default = os.getcwd(), help="Output directory to store results (by default current directory)") @@ -58,7 +58,7 @@ @click.option('--category_names',type = str, default="", help="JSON files containing a dict with classes index as keys and names to display classes as values.") @click.pass_context -def zonalstats(ctx, inputs : list, output : str, output_format : str, geometries : str, within : str, stats : int, categorical : bool, valid_threshold : float ,area : bool, prefix, bands : list, all_bands : bool, sigma, chartfile, display : bool, geom_index : str, category_file : str, category_index : str, category_names : str) : +def zonalstats(ctx, inputs : list, output : str, output_format : str, geometries : str, within : str, stats : list, categorical : bool, valid_threshold : float ,area : bool, prefix, bands : list, all_bands : bool, sigma, chartfile, display : bool, geom_index : str, category_file : str, category_index : str, category_names : str) : """ Compute zonal statistics Compute zonal statistics of a raster image.\n Available statistics are: @@ -74,9 +74,11 @@ def zonalstats(ctx, inputs : list, output : str, output_format : str, geometries Returns: :obj:`eolab.rastertools.Zonalstats`: The configured rastertool to run """ + print(output) + print(output_format) # get and check the list of stats to compute if stats: - stats_to_compute = stats + stats_to_compute = list(stats) elif categorical: stats_to_compute = [] else: diff --git a/src/eolab/rastertools/main.py b/src/eolab/rastertools/main.py index 44882b1..5071e20 100644 --- a/src/eolab/rastertools/main.py +++ b/src/eolab/rastertools/main.py @@ -225,6 +225,19 @@ def rastertools(ctx, rastertype : str, max_workers : int, keep_vrt : bool, verbo rastertools.add_command(zonalstats, name = "zs") rastertools.add_command(zonalstats, name = "zonalstats") +CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) + + +#Speed command +@click.command("ema",context_settings=CONTEXT_SETTINGS) +@click.option('--inputs', type=int) +@click.pass_context +def ema(ctx, inputs) : + raise Exception(f"coucou {inputs}") + +rastertools.add_command(ema, name = "ema") + + @rastertools.result_callback() @click.pass_context def handle_result(ctx): diff --git a/src/eolab/rastertools/rastertools.py b/src/eolab/rastertools/rastertools.py index 87500e9..479bab7 100644 --- a/src/eolab/rastertools/rastertools.py +++ b/src/eolab/rastertools/rastertools.py @@ -13,9 +13,12 @@ """ from abc import ABC from typing import List +import logging +import sys from eolab.rastertools import utils +_logger = logging.getLogger(__name__) class RastertoolConfigurationException(Exception): """This class defines an exception that is raised when the configuration of the raster tool @@ -66,8 +69,9 @@ def with_output(self, outputdir: str = "."): possible to chain the with... calls (fluent API) """ if outputdir and not utils.is_dir(outputdir): - raise RastertoolConfigurationException( - f"Output directory \"{str(outputdir)}\" does not exist.") + _logger.exception( + RastertoolConfigurationException(f"Output directory \"{str(outputdir)}\" does not exist.")) + sys.exit(2) self._outputdir = outputdir return self diff --git a/src/eolab/rastertools/tiling.py b/src/eolab/rastertools/tiling.py index 4461a16..57bf1e3 100644 --- a/src/eolab/rastertools/tiling.py +++ b/src/eolab/rastertools/tiling.py @@ -11,6 +11,7 @@ import rasterio import rasterio.mask import geopandas as gpd +import sys from eolab.rastertools import utils from eolab.rastertools import Rastertool, RastertoolConfigurationException @@ -119,21 +120,24 @@ def with_id_column(self, id_column: str, ids: List[int]): # Test if id_column is defined when ids are set if id_column is not None: if id_column not in self._grid.columns: - raise RastertoolConfigurationException( - f"Invalid id column named \"{id_column}\": it does not exist in the grid") + _logger.exception(RastertoolConfigurationException( + f"Invalid id column named \"{id_column}\": it does not exist in the grid")) + sys.exit(2) self._grid = self._grid.set_index(id_column) if ids is not None: if id_column is None: - raise RastertoolConfigurationException( - "Ids cannot be specified when id_col is not defined") + _logger.exception(RastertoolConfigurationException( + "Ids cannot be specified when id_col is not defined")) + sys.exit(2) self._grid = self._grid[self._grid.index.isin(ids)] if self._grid.empty: # if no id common between grid and given ids - raise RastertoolConfigurationException( + _logger.exception(RastertoolConfigurationException( f"No value in the grid column \"{id_column}\" are matching " - f"the given list of ids {str(ids)}") + f"the given list of ids {str(ids)}")) + sys.exit(2) else: invalid_ids = [i for i in ids if i not in self._grid.index] if len(invalid_ids) > 0: diff --git a/src/eolab/rastertools/zonalstats.py b/src/eolab/rastertools/zonalstats.py index 43a0cee..270c991 100644 --- a/src/eolab/rastertools/zonalstats.py +++ b/src/eolab/rastertools/zonalstats.py @@ -23,6 +23,7 @@ import json import numpy as np import geopandas as gpd +import sys import rasterio @@ -276,9 +277,10 @@ def with_output(self, outputdir: str = ".", output_format: str = "ESRI Shapefile self._output_format = output_format or 'ESRI Shapefile' # check if output_format exists if self._output_format not in Zonalstats.supported_output_formats: - raise RastertoolConfigurationException( + _logger.exception(RastertoolConfigurationException( f"Unrecognized output format {output_format}. " - f"Possible values are {', '.join(Zonalstats.supported_output_formats)}") + f"Possible values are {', '.join(Zonalstats.supported_output_formats)}")) + sys.exit(2) return self def with_geometries(self, geometries: str, within: bool = False): @@ -313,6 +315,7 @@ def with_outliers(self, sigma: float): :obj:`eolab.rastertools.Zonalstats`: the current instance so that it is possible to chain the with... calls (fluent API) """ + print(self._stats) # Manage sigma computation option that requires mean + std dev computation if "mean" not in self._stats: self._stats.append("mean") diff --git a/src/rastertools.egg-info/PKG-INFO b/src/rastertools.egg-info/PKG-INFO index 7b331a7..4cc1420 100644 --- a/src/rastertools.egg-info/PKG-INFO +++ b/src/rastertools.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: rastertools -Version: 0.6.1.post1.dev0+gbedb844.d20241022 +Version: 0.6.1.post1.dev19+gebce3d0.d20241113 Summary: Compute radiometric indices and zonal statistics on rasters Home-page: https://github.com/cnes/rastertools Author: Olivier Queyrut diff --git a/src/rastertools.egg-info/SOURCES.txt b/src/rastertools.egg-info/SOURCES.txt index 1ac1b3c..d2e4162 100644 --- a/src/rastertools.egg-info/SOURCES.txt +++ b/src/rastertools.egg-info/SOURCES.txt @@ -81,12 +81,14 @@ src/eolab/rastertools/__pycache__/utils.cpython-38.pyc src/eolab/rastertools/__pycache__/zonalstats.cpython-38.pyc src/eolab/rastertools/cli/__init__.py src/eolab/rastertools/cli/filtering.py +src/eolab/rastertools/cli/filtering_dyn.py src/eolab/rastertools/cli/hillshade.py src/eolab/rastertools/cli/radioindice.py src/eolab/rastertools/cli/speed.py src/eolab/rastertools/cli/svf.py src/eolab/rastertools/cli/tiling.py src/eolab/rastertools/cli/timeseries.py +src/eolab/rastertools/cli/utils_cli.py src/eolab/rastertools/cli/zonalstats.py src/eolab/rastertools/cli/__pycache__/__init__.cpython-38.pyc src/eolab/rastertools/cli/__pycache__/filtering.cpython-38.pyc diff --git a/tests/test_rasterproduct.py b/tests/test_rasterproduct.py index 3f1657e..b471ef0 100644 --- a/tests/test_rasterproduct.py +++ b/tests/test_rasterproduct.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- import pytest +import os import filecmp import zipfile from pathlib import Path @@ -41,13 +42,13 @@ def test_rasterproduct_valid_parameters(): # archive with one file per band basename = "S2B_MSIL1C_20191008T105029_N0208_R051_T30TYP_20191008T125041" file = Path( - utils4test.indir + basename + ".zip") + utils4test.indir.split(os.getcwd() + "/")[-1] + basename + ".zip") prod = RasterProduct(file) assert prod.file == file assert prod.rastertype == RasterType.get("S2_L1C") assert prod.channels == RasterType.get("S2_L1C").channels - band_format = f"/vsizip/tests/tests_data/{basename}.zip/" + band_format = f"/vsizip/" + utils4test.indir.split(os.getcwd() + "/")[-1] + f"{basename}.zip/" band_format += f"{basename}.SAFE/GRANULE/L1C_T30TYP_A013519_20191008T105335/IMG_DATA/" band_format += "T30TYP_20191008T105029_{}.jp2" assert prod.bands_files == {b: band_format.format(b) for b in prod.rastertype.get_band_ids()} @@ -61,13 +62,13 @@ def test_rasterproduct_valid_parameters(): # archive with one file for all bands basename = "SPOT6_2018_France-Ortho_NC_DRS-MS_SPOT6_2018_FRANCE_ORTHO_NC_GEOSUD_MS_82" - file = utils4test.indir + basename + ".tar.gz" + file = utils4test.indir.split(os.getcwd() + "/")[-1] + basename + ".tar.gz" prod = RasterProduct(file) assert prod.file == Path(file) assert prod.rastertype == RasterType.get("SPOT67_GEOSUD") assert prod.channels == [BandChannel.red, BandChannel.green, BandChannel.blue, BandChannel.nir] - band = f"/vsitar/tests/tests_data/{basename}.tar.gz/SPOT6_2018_FRANCE_ORTHO_NC_GEOSUD_MS_82/" + band = f"/vsitar/" + utils4test.indir.split(os.getcwd() + "/")[-1] + f"{basename}.tar.gz/SPOT6_2018_FRANCE_ORTHO_NC_GEOSUD_MS_82/" band += "PROD_SPOT6_001/VOL_SPOT6_001_A/IMG_SPOT6_MS_001_A/" band += "IMG_SPOT6_MS_201805111031189_ORT_SPOT6_20180517_1333011n1b80qobn5ex_1_R1C1.TIF" assert prod.bands_files == {"all": band} diff --git a/tests/test_rastertools.py b/tests/test_rastertools.py index 522ae55..266a0ec 100644 --- a/tests/test_rastertools.py +++ b/tests/test_rastertools.py @@ -4,11 +4,10 @@ import pytest import logging import filecmp +from click.testing import CliRunner from pathlib import Path -from click import argument - -from eolab.rastertools import rastertools +from eolab.rastertools import rastertools, RastertoolConfigurationException from eolab.rastertools.product import RasterType from . import utils4test @@ -17,6 +16,8 @@ __copyright__ = "Copyright 2019, CNES" __license__ = "Apache v2.0" +from .utils4test import RastertoolsTestsData + class TestCase: __test__ = False @@ -90,19 +91,23 @@ def with_refdir(self, refdir): def run_test(self, caplog=None, loglevel=logging.ERROR, check_outputs=True, check_sys_exit=True, check_logs=True, compare=False, save_gen_as_ref=False): + + runner = CliRunner() if caplog is not None: caplog.set_level(loglevel) else: check_logs = False - # run rastertools - with pytest.raises(SystemExit) as wrapped_exception: + print(self.args) + + try: rastertools(self.args) - # check sys_exit - if check_sys_exit: - assert wrapped_exception.type == SystemExit - assert wrapped_exception.value.code == self._sys_exit + except SystemExit as wrapped_exception: + print(wrapped_exception) + if check_sys_exit: + # Check if the exit code matches the expected value + assert wrapped_exception.code == self._sys_exit, (f"Expected exit code {self._sys_exit}, but got {wrapped_exception.code}") # check list of outputs if check_outputs: @@ -120,6 +125,8 @@ def run_test(self, caplog=None, loglevel=logging.ERROR, check_outputs=True, chec # check logs if check_logs: + print('...'*20) + print(caplog.record_tuples) for i, log in enumerate(self._logs): assert caplog.record_tuples[i] == log @@ -127,60 +134,64 @@ def run_test(self, caplog=None, loglevel=logging.ERROR, check_outputs=True, chec if caplog is not None: caplog.clear() - # clear output dir + #clear output dir utils4test.clear_outdir() def test_rastertools_command_line_info(): + tests = [ TestCase("--help"), TestCase("-h"), TestCase("--version"), TestCase(""), + TestCase("radioindice --help"), + TestCase("ri -h"), + TestCase("zonalstats --help"), + TestCase("zs -h"), + TestCase("tiling --help"), + TestCase("ti -h"), TestCase("filter --help"), - TestCase("fi -h") - ] - # tests = [ - # TestCase("--help"), - # TestCase("-h"), - # TestCase("--version"), - # TestCase(""), - # TestCase("radioindice --help"), - # TestCase("ri -h"), - # TestCase("zonalstats --help"), - # TestCase("zs -h"), - # TestCase("tiling --help"), - # TestCase("ti -h"), - # TestCase("filter --help"), - # TestCase("fi -h"), - # TestCase("timeseries --help"), - # TestCase("ts -h"), - # TestCase("speed --help"), - # TestCase("sp -h"), - # TestCase("svf --help"), - # TestCase("svf -h"), - # TestCase("hillshade --help"), - # TestCase("hs -h") - # ] + TestCase("fi -h"), + TestCase("timeseries --help"), + TestCase("ts -h"), + TestCase("speed --help"), + TestCase("sp -h"), + TestCase("svf --help"), + TestCase("svf -h"), + TestCase("hillshade --help"), + TestCase("hs -h") + ] for test in tests: test.run_test() +def generate_lst_file(file_list, output_file): + + with open(output_file,"w") as lst_file: + for f in file_list: + lst_file.write(RastertoolsTestsData.tests_input_data_dir + "/" + f.split('/')[-1] + "\n") + def test_radioindice_command_line_default(): # create output dir and clear its content if any utils4test.create_outdir() + lst_file_path = f"{RastertoolsTestsData.tests_input_data_dir}/listing.lst" + generate_lst_file(["SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip", + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip"], + lst_file_path) + # list of commands to test argslist = [ # no indice defined - " -v ri -o tests/tests_out tests/tests_data/listing.lst", + f" -v ri -o {RastertoolsTestsData.tests_output_data_dir} {lst_file_path}", # two indices with their own options, merge - "-v ri --pvi --savi -o tests/tests_out -m tests/tests_data/listing.lst", + f"-v ri --pvi --savi -o {RastertoolsTestsData.tests_output_data_dir} -m {lst_file_path}", # indices option, roi - "--verbose ri --indices pvi --indices savi -nd nir red --roi tests/tests_data/COMMUNE_32001.shp" - " --output tests/tests_out" - " tests/tests_data/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip" - " tests/tests_data/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip" + f"--verbose ri --indices pvi --indices savi -nd nir red --roi {RastertoolsTestsData.tests_input_data_dir}/COMMUNE_32001.shp" + f" --output {RastertoolsTestsData.tests_output_data_dir}" + f" {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip" + f" {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip" ] # get list of expected outputs indices_list = ["ndvi ndwi ndwi2", "indices", "pvi savi nd[nir-red]"] @@ -203,8 +214,8 @@ def test_radioindice_additional_type(): # list of commands to test argslist = [ # add rastertypes to handle new data product - "-t tests/tests_data/additional_rastertypes.json -v" - " ri -o tests/tests_out --ndvi tests/tests_data/RGB_TIF_20170105_013442_test.tif" + f"-t {RastertoolsTestsData.tests_input_data_dir}/additional_rastertypes.json -v" + f" ri -o {RastertoolsTestsData.tests_output_data_dir} --ndvi {RastertoolsTestsData.tests_input_data_dir}/RGB_TIF_20170105_013442_test.tif" ] # get list of expected outputs indices_list = "ndvi" @@ -228,28 +239,28 @@ def test_radioindice_command_line_errors(caplog): # missing positional argument "ri --ndvi", # unkwnow indice - "ri tests/tests_data/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip --indices strange", + f"ri {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip --indices strange", # unknown raster type: unrecognized raster type - "-v ri --ndvi -o tests/tests_out tests/tests_data/OCS_2017_CESBIO_extract.tif", + f"-v ri --ndvi -o {RastertoolsTestsData.tests_output_data_dir} {RastertoolsTestsData.tests_input_data_dir}/OCS_2017_CESBIO_extract.tif", # unknown raster type: unsupported extension - "-v ri --ndvi -o tests/tests_out tests/tests_data/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.aaa", + f"-v ri --ndvi -o {RastertoolsTestsData.tests_output_data_dir} {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.aaa", # output dir does not exist - "-v ri -o ./toto --ndvi tests/tests_data/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip", + f"-v ri -o ./toto --ndvi {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip", # unknown band in normalized difference - "-v ri -nd unknown red tests/tests_data/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip" + f"-v ri -nd unknown red {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip" ] # expected logs logslist = [ [], - [("eolab.rastertools.main", logging.ERROR, "Invalid indice name: strange")], - [("eolab.rastertools.main", logging.ERROR, - "Unsupported input file, no matching raster type identified to handle the file")], - [("eolab.rastertools.main", logging.ERROR, - "Unsupported input file tests/tests_data/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.aaa")], - [("eolab.rastertools.main", logging.ERROR, + [("eolab.rastertools.cli.radioindice", logging.ERROR, "Invalid indice name: strange")], + [("eolab.rastertools.cli.utils_cli", logging.ERROR, + "Unsupported input file, no matching raster type identified to handle the file")], + [("eolab.rastertools.cli.utils_cli", logging.ERROR, + f"Unsupported input file {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.aaa")], + [("eolab.rastertools.rastertools", logging.ERROR, "Output directory \"./toto\" does not exist.")], - [("eolab.rastertools.main", logging.ERROR, + [("eolab.rastertools.cli.radioindice", logging.ERROR, "Invalid band(s) in normalized difference: unknown and/or red")] ] sysexitlist = [2, 2, 1, 1, 2, 2] @@ -267,14 +278,19 @@ def test_speed_command_line_default(): # create output dir and clear its content if any utils4test.create_outdir() + lst_file_path = f"{RastertoolsTestsData.tests_input_data_dir}/listing.lst" + generate_lst_file(["SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip", + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip"], + lst_file_path) + # list of commands to test argslist = [ # default with a listing of S2A products - "-v sp -b 1 -o tests/tests_out tests/tests_data/listing.lst", + f"-v sp -b 1 -o {RastertoolsTestsData.tests_output_data_dir} {lst_file_path}", # default with a list of files - "--verbose speed --output tests/tests_out" - " tests/tests_data/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" - " tests/tests_data/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif" + f"--verbose speed --output {RastertoolsTestsData.tests_output_data_dir}" + f" {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" + f" {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif" ] speed_filenames = [ ["SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-speed-20180928-105515.tif"], @@ -294,24 +310,29 @@ def test_speed_command_line_errors(caplog): # create output dir and clear its content if any utils4test.create_outdir() + lst_file_path = f"{RastertoolsTestsData.tests_input_data_dir}/listing2.lst" + generate_lst_file(["SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif", + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif"], + lst_file_path) + # list of commands to test argslist = [ # output dir does not exist - "-v sp -o ./toto tests/tests_data/listing2.lst", + f"-v sp -o ./toto {lst_file_path}", # missing one file for speed - "-v sp -o tests/tests_out tests/tests_data/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip", + f"-v sp -o {RastertoolsTestsData.tests_output_data_dir} {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip", # different types for input files - "-v sp -a -o tests/tests_out" - " tests/tests_data/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip" - " tests/tests_data/S2A_MSIL2A_20190116T105401_N0211_R051_T30TYP_20190116T120806.zip" + f"-v sp -a -o {RastertoolsTestsData.tests_output_data_dir}" + f" {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip" + f" {RastertoolsTestsData.tests_input_data_dir}/S2A_MSIL2A_20190116T105401_N0211_R051_T30TYP_20190116T120806.zip" ] # expected logs logslist = [ - [("eolab.rastertools.main", logging.ERROR, + [("eolab.rastertools.rastertools", logging.ERROR, "Output directory \"./toto\" does not exist.")], - [("eolab.rastertools.main", logging.ERROR, + [("eolab.rastertools.cli.utils_cli", logging.ERROR, "Can not compute speed with 1 input image. Provide at least 2 images.")], - [("eolab.rastertools.main", logging.ERROR, + [("eolab.rastertools.cli.utils_cli", logging.ERROR, "Speed can only be computed with images of the same type")] ] sysexitlist = [2, 1, 1] @@ -332,9 +353,9 @@ def test_timeseries_command_line_default(compare, save_gen_as_ref): # list of commands to test argslist = [ # default with a list of files - "--verbose ts --output tests/tests_out" - " tests/tests_data/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" - " tests/tests_data/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif" + f"--verbose ts --output {RastertoolsTestsData.tests_output_data_dir}" + f" {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" + f" {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif" " -s 2018-09-26 -e 2018-11-07 -p 20 -ws 512" ] timeseries_filenames = [ @@ -358,45 +379,49 @@ def test_timeseries_command_line_errors(caplog): # create output dir and clear its content if any utils4test.create_outdir() + lst_file_path = f"{RastertoolsTestsData.tests_input_data_dir}/listing2.lst" + generate_lst_file(["SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif", + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif"], + lst_file_path) + period = " -s 2018-09-26 -e 2018-11-07 -p 20" # list of commands to test argslist = [ # output dir does not exist - "-v ts -o ./toto tests/tests_data/listing2.lst" + period, + f"-v ts -o ./toto {lst_file_path}" + period, # missing one file for timeseries - "-v ts -o tests/tests_out tests/tests_data/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" + period, + f"-v ts -o {RastertoolsTestsData.tests_output_data_dir} {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" + period, # unknown raster type - "-v ts -o tests/tests_out -a" - " tests/tests_data/DSM_PHR_Dunkerque.tif" - " tests/tests_data/S2A_MSIL2A_20190116T105401_N0211_R051_T30TYP_20190116T120806.zip" + period, + f"-v ts -o {RastertoolsTestsData.tests_output_data_dir} -a" + f" {RastertoolsTestsData.tests_input_data_dir}/DSM_PHR_Dunkerque.tif" + f" {RastertoolsTestsData.tests_input_data_dir}/S2A_MSIL2A_20190116T105401_N0211_R051_T30TYP_20190116T120806.zip" + period, # different types for input files - "-v ts -o tests/tests_out" - " tests/tests_data/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip" - " tests/tests_data/S2A_MSIL2A_20190116T105401_N0211_R051_T30TYP_20190116T120806.zip" + period, + f"-v ts -o {RastertoolsTestsData.tests_output_data_dir}" + f" {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip" + f" {RastertoolsTestsData.tests_input_data_dir}/S2A_MSIL2A_20190116T105401_N0211_R051_T30TYP_20190116T120806.zip" + period, # invalid date format - "-v ts --o tests/tests_out" - " tests/tests_data/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" - " tests/tests_data/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif" + f"-v ts -o {RastertoolsTestsData.tests_output_data_dir} {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" + f" {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif" " -s 20180926 -e 2018-11-07 -p 20", # invalid date format - "-v ts --o tests/tests_out" - " tests/tests_data/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" - " tests/tests_data/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif" + f"-v ts -o {RastertoolsTestsData.tests_output_data_dir}" + f" {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" + f" {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif" " -s 2018-09-26 -e 20181107 -p 20" ] # expected logs logslist = [ - [("eolab.rastertools.main", logging.ERROR, + [("eolab.rastertools.rastertools", logging.ERROR, "Output directory \"./toto\" does not exist.")], - [("eolab.rastertools.main", logging.ERROR, + [("eolab.rastertools.cli.utils_cli", logging.ERROR, "Can not compute a timeseries with 1 input image. Provide at least 2 images.")], - [("eolab.rastertools.main", logging.ERROR, - "Unknown rastertype for input file tests/tests_data/DSM_PHR_Dunkerque.tif")], - [("eolab.rastertools.main", logging.ERROR, + [("eolab.rastertools.cli.utils_cli", logging.ERROR, + f"Unknown rastertype for input file {RastertoolsTestsData.tests_input_data_dir}/DSM_PHR_Dunkerque.tif")], + [("eolab.rastertools.cli.utils_cli", logging.ERROR, "Timeseries can only be computed with images of the same type")], - [("eolab.rastertools.main", logging.ERROR, + [("eolab.rastertools.cli.timeseries", logging.ERROR, "Invalid format for start date: 20180926 (must be %Y-%m-%d)")], - [("eolab.rastertools.main", logging.ERROR, + [("eolab.rastertools.cli.timeseries", logging.ERROR, "Invalid format for end date: 20181107 (must be %Y-%m-%d)")] ] sysexitlist = [2, 1, 1, 1, 2, 2] @@ -414,17 +439,22 @@ def test_zonalstats_command_line_default(): # create output dir and clear its content if any utils4test.create_outdir() + lst_file_path = f"{RastertoolsTestsData.tests_input_data_dir}/listing2.lst" + generate_lst_file(["SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif", + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif"], + lst_file_path) + # list of commands to test argslist = [ # specify stats to compute and sigma, 1st band computed - "-v zs -o tests/tests_out -f GeoJSON" - " -g tests/tests_data/COMMUNE_32xxx.geojson --stats min max --sigma 1.0" - " tests/tests_data/listing2.lst", + f"-v zs -o {RastertoolsTestsData.tests_output_data_dir} -f GeoJSON" + f" -g {RastertoolsTestsData.tests_input_data_dir}/COMMUNE_32xxx.geojson --stats min --stats max --sigma 1.0" + f" {lst_file_path}", # default stats, all bands computed - "-v zs -o tests/tests_out -f GeoJSON" - " --all -g tests/tests_data/COMMUNE_32xxx.geojson" - " tests/tests_data/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" - " tests/tests_data/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif" + f"-v zs -o {RastertoolsTestsData.tests_output_data_dir} -f GeoJSON" + f" --all -g {RastertoolsTestsData.tests_input_data_dir}/COMMUNE_32xxx.geojson" + f" {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" + f" {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif" ] files = ["SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi", "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi"] @@ -445,9 +475,9 @@ def test_zonalstats_command_line_product(): # list of commands to test argslist = [ # input file is a S2A product - "-v zs -o tests/tests_out -f GeoJSON" - " --all -g tests/tests_data/COMMUNE_32xxx.geojson" - " tests/tests_data/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip" + f"-v zs -o {RastertoolsTestsData.tests_output_data_dir} -f GeoJSON" + f" --all -g {RastertoolsTestsData.tests_input_data_dir}/COMMUNE_32xxx.geojson" + f" {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip" ] files = ["SENTINEL2A_20180928-105515-685_L2A_T30TYP_D"] @@ -465,8 +495,8 @@ def test_zonalstats_command_line_categorical(): # list of commands to test argslist = [ # input file is a S2A product - "-v zs -o tests/tests_out -f GeoJSON --categorical" - " tests/tests_data/OCS_2017_CESBIO_extract.tif" + f"-v zs -o {RastertoolsTestsData.tests_output_data_dir} -f GeoJSON --categorical" + f" {RastertoolsTestsData.tests_input_data_dir}/OCS_2017_CESBIO_extract.tif" ] files = ["OCS_2017_CESBIO_extract"] @@ -482,27 +512,32 @@ def test_zonalstats_command_line_errors(): # create output dir and clear its content if any utils4test.create_outdir() + lst_file_path = f"{RastertoolsTestsData.tests_input_data_dir}/listing2.lst" + generate_lst_file(["SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif", + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif"], + lst_file_path) + # list of commands to test argslist = [ # output dir does not exist - "-v zs tests/tests_data/listing2.lst -o ./toto -f GeoJSON" - " -g tests/tests_data/COMMUNE_32xxx.geojson --stats mean" + f"-v zs {lst_file_path} -o ./toto -f GeoJSON" + f" -g {RastertoolsTestsData.tests_input_data_dir}/COMMUNE_32xxx.geojson --stats mean" " -b 0", # band 0 does not exist - "-v zs tests/tests_data/listing2.lst -o tests/tests_out -f GeoJSON" - " -g tests/tests_data/COMMUNE_32xxx.geojson --stats mean" + f"-v zs {lst_file_path} -o {RastertoolsTestsData.tests_output_data_dir} -f GeoJSON" + f" -g {RastertoolsTestsData.tests_input_data_dir}/COMMUNE_32xxx.geojson --stats mean" " -b 0", # invalid format - "-v zs -o tests/tests_out -f Truc tests/tests_data/listing2.lst", + f"-v zs -o {RastertoolsTestsData.tests_output_data_dir} -f Truc {lst_file_path}", # invalid geometry index name - "-v zs -o tests/tests_out -f GeoJSON --stats mean" - " -g tests/tests_data/COMMUNE_32xxx.geojson -gi truc" - " -c tests/tests_out/chart.png" - " tests/tests_data/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip", + f"-v zs -o {RastertoolsTestsData.tests_output_data_dir} -f GeoJSON --stats mean" + f" -g {RastertoolsTestsData.tests_input_data_dir}/COMMUNE_32xxx.geojson -gi truc" + f" -c {RastertoolsTestsData.tests_output_data_dir}/chart.png" + f" {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip", # invalid prefix length - "-v zs -o tests/tests_out -f GeoJSON --prefix band1" - " --all -g tests/tests_data/COMMUNE_32xxx.geojson" - " tests/tests_data/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip" + f"-v zs -o {RastertoolsTestsData.tests_output_data_dir} -f GeoJSON --prefix band1" + f" --all -g {RastertoolsTestsData.tests_input_data_dir}/COMMUNE_32xxx.geojson" + f" {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip" ] logslist = [ @@ -534,18 +569,22 @@ def test_tiling_command_line_default(): # create output dir and clear its content if any utils4test.create_outdir() + lst_file_path = f"{RastertoolsTestsData.tests_input_data_dir}/listing3.lst" + generate_lst_file(["tif_file.tif"], + lst_file_path) + # list of commands to test argslist = [ # default case with listing of input files - "-v ti -o tests/tests_out -g tests/tests_data/grid.geojson tests/tests_data/listing3.lst", + f"-v ti -o {RastertoolsTestsData.tests_output_data_dir} -g {RastertoolsTestsData.tests_input_data_dir}/grid.geojson {lst_file_path}", # default case with input file - "--verbose ti -o tests/tests_out -g tests/tests_data/grid.geojson" - " tests/tests_data/tif_file.tif", + f"--verbose ti -o {RastertoolsTestsData.tests_output_data_dir} -g {RastertoolsTestsData.tests_input_data_dir}/grid.geojson" + f" {RastertoolsTestsData.tests_input_data_dir}/tif_file.tif", # specify specific ids - "-v ti -o tests/tests_out -g tests/tests_data/grid.geojson --id 77 --id 93 --id_col id" - " tests/tests_data/tif_file.tif" + f"-v ti -o {RastertoolsTestsData.tests_output_data_dir} -g {RastertoolsTestsData.tests_input_data_dir}/grid.geojson --id 77 --id 93 --id_col id" + f" {RastertoolsTestsData.tests_input_data_dir}/tif_file.tif" ] - input_filenames = ["tests/tests_data/tif_file.tif"] + input_filenames = [f"{RastertoolsTestsData.tests_input_data_dir}/tif_file.tif"] # generate test cases tests = [TestCase(args).ti_output(input_filenames, [77, 93]) for args in argslist] @@ -556,17 +595,17 @@ def test_tiling_command_line_default(): # Additional tests to check naming and subdir options # specify naming convention - args = ("-v ti -o tests/tests_out -g tests/tests_data/grid.geojson -n tile{}" - " --id_col id tests/tests_data/tif_file.tif") + args = (f"-v ti -o {RastertoolsTestsData.tests_output_data_dir} -g {RastertoolsTestsData.tests_input_data_dir}/grid.geojson -n tile{{}}" + f" --id_col id {RastertoolsTestsData.tests_input_data_dir}/tif_file.tif") test = TestCase(args).ti_output(input_filenames, [77, 93], name="tile{}") test.run_test(check_outputs=False) # specify subdir naming convetion - args = ("-v ti -o tests/tests_out -g tests/tests_data/grid.geojson -d tile{}" - " --id_col id tests/tests_data/tif_file.tif") + args = (f"-v ti -o {RastertoolsTestsData.tests_output_data_dir} -g {RastertoolsTestsData.tests_input_data_dir}/grid.geojson -d tile{{}}" + f" --id_col id {RastertoolsTestsData.tests_input_data_dir}/tif_file.tif") test = TestCase(args).ti_output(input_filenames, [77, 93], subdir="tile{}") # create one subdir to check if it is not re-created - subdir = Path("tests/tests_out/tile77") + subdir = Path(f"{RastertoolsTestsData.tests_output_data_dir}/tile77") subdir.mkdir() test.run_test(check_outputs=False) @@ -578,11 +617,11 @@ def test_tiling_command_line_special_case(caplog): # list of commands to test argslist = [ # some invalid ids - "-v ti -o tests/tests_out -g tests/tests_data/grid.geojson --id 1 --id 2 --id 93 --id_col id" - " tests/tests_data/tif_file.tif", + f"-v ti -o {RastertoolsTestsData.tests_output_data_dir} -g {RastertoolsTestsData.tests_input_data_dir}/grid.geojson --id 1 --id 2 --id 93 --id_col id" + f" {RastertoolsTestsData.tests_input_data_dir}/tif_file.tif", # a geometry does not overlap raster - "-v ti -o tests/tests_out -g tests/tests_data/grid.geojson --id 78 --id 93 --id_col id" - " tests/tests_data/tif_file.tif" + f"-v ti -o {RastertoolsTestsData.tests_output_data_dir} -g {RastertoolsTestsData.tests_input_data_dir}/grid.geojson --id 78 --id 93 --id_col id" + f" {RastertoolsTestsData.tests_input_data_dir}/tif_file.tif" ] # expected logs @@ -608,28 +647,27 @@ def test_tiling_command_line_errors(caplog): # list of commands to test argslist = [ # ids without id_col - "-v ti --id 77 93 -o tests/tests_out -g tests/tests_data/grid.geojson" - " tests/tests_data/tif_file.tif", + f"-v ti --id 77 93 -o {RastertoolsTestsData.tests_output_data_dir} -g {RastertoolsTestsData.tests_input_data_dir}/grid.geojson" + f" {RastertoolsTestsData.tests_input_data_dir}/tif_file.tif", # invalid id column - "-v ti -o tests/tests_out -g tests/tests_data/grid.geojson --id 77 93 --id_col truc" - " tests/tests_data/tif_file.tif", + f"-v ti -o {RastertoolsTestsData.tests_output_data_dir} -g {RastertoolsTestsData.tests_input_data_dir}/grid.geojson --id 77 --id 93 --id_col truc" + f" {RastertoolsTestsData.tests_input_data_dir}/tif_file.tif", # output dir does not exist - "-v ti -o tests/truc -g tests/tests_data/grid.geojson" - " tests/tests_data/tif_file.tif", + f"-v ti -o tests/truc -g {RastertoolsTestsData.tests_input_data_dir}/grid.geojson {RastertoolsTestsData.tests_input_data_dir}/tif_file.tif", # all invalid ids - "-v ti -o tests/tests_out -g tests/tests_data/grid.geojson --id 1 2 --id_col id" - " tests/tests_data/tif_file.tif" + f"-v ti -o {RastertoolsTestsData.tests_output_data_dir} -g {RastertoolsTestsData.tests_input_data_dir}/grid.geojson --id 1 --id 2 --id_col id" + f" {RastertoolsTestsData.tests_input_data_dir}/tif_file.tif" ] # expected logs logslist = [ - [("eolab.rastertools.main", logging.ERROR, + [("eolab.rastertools.tiling", logging.ERROR, "Ids cannot be specified when id_col is not defined")], - [("eolab.rastertools.main", logging.ERROR, + [("eolab.rastertools.tiling", logging.ERROR, "Invalid id column named \"truc\": it does not exist in the grid")], - [("eolab.rastertools.main", logging.ERROR, + [("eolab.rastertools.rastertools", logging.ERROR, "Output directory \"tests/truc\" does not exist.")], - [("eolab.rastertools.main", logging.ERROR, + [("eolab.rastertools.tiling", logging.ERROR, "No value in the grid column \"id\" are matching the given list of ids [1, 2]")] ] sysexitlist = [2, 2, 2, 2] @@ -650,17 +688,17 @@ def test_filtering_command_line_default(): # list of commands to test argslist = [ # default case: median - "-v --max_workers 1 fi median -a --kernel_size 8 -o tests/tests_out" - " tests/tests_data/RGB_TIF_20170105_013442_test.tif", + f"-v --max_workers 1 fi median -a --kernel_size 8 -o {RastertoolsTestsData.tests_output_data_dir}" + f" {RastertoolsTestsData.tests_input_data_dir}/RGB_TIF_20170105_013442_test.tif", # default case: local sum - "-v fi sum -b 1 -b 2 --kernel_size 8 -o tests/tests_out" - " tests/tests_data/RGB_TIF_20170105_013442_test.tif", + f"-v fi sum -b 1 -b 2 --kernel_size 8 -o {RastertoolsTestsData.tests_output_data_dir}" + f" {RastertoolsTestsData.tests_input_data_dir}/RGB_TIF_20170105_013442_test.tif", # default case: local mean - "-v fi mean -b 1 --kernel_size 8 -o tests/tests_out" - " tests/tests_data/RGB_TIF_20170105_013442_test.tif", + f"-v fi mean -b 1 --kernel_size 8 -o {RastertoolsTestsData.tests_output_data_dir}" + f" {RastertoolsTestsData.tests_input_data_dir}/RGB_TIF_20170105_013442_test.tif", # default case: adaptive gaussian - "-v fi adaptive_gaussian -b 1 --kernel_size 32 --sigma 1 -o tests/tests_out" - " tests/tests_data/RGB_TIF_20170105_013442_test.tif", + f"-v fi adaptive_gaussian -b 1 --kernel_size 32 --sigma 1 -o {RastertoolsTestsData.tests_output_data_dir}" + f" {RastertoolsTestsData.tests_input_data_dir}/RGB_TIF_20170105_013442_test.tif", ] input_filenames = ["RGB_TIF_20170105_013442_test-{}.tif"] names = ["median", "sum", "mean", "adaptive_gaussian"] @@ -682,21 +720,21 @@ def test_filtering_command_line_errors(caplog): argslist = [ # output dir does not exist "-v filter median --kernel_size 8 -o tests/truc" - " tests/tests_data/tif_file.tif", + f" {RastertoolsTestsData.tests_input_data_dir}/tif_file.tif", # missing required argument - "-v filter adaptive_gaussian --kernel_size 32 -o tests/tests_out" - " tests/tests_data/RGB_TIF_20170105_013442_test.tif", - # kernel_size > window_size - "-v filter median -a --kernel_size 15 --window_size 16 -o tests/tests_out" - " tests/tests_data/RGB_TIF_20170105_013442_test.tif", + f"-v filter adaptive_gaussian --kernel_size 32 -o {RastertoolsTestsData.tests_output_data_dir}" + f" {RastertoolsTestsData.tests_input_data_dir}/RGB_TIF_20170105_013442_test.tif", + # # kernel_size > window_size + f"-v filter median -a --kernel_size 15 --window_size 16 -o {RastertoolsTestsData.tests_output_data_dir}" + f" {RastertoolsTestsData.tests_input_data_dir}/RGB_TIF_20170105_013442_test.tif", ] # expected logs logslist = [ - [("eolab.rastertools.main", logging.ERROR, + [("eolab.rastertools.rastertools", logging.ERROR, "Output directory \"tests/truc\" does not exist.")], [], - [("eolab.rastertools.main", logging.ERROR, + [("eolab.rastertools.cli.utils_cli", logging.ERROR, "The kernel size (option --kernel_size, value=15) must be strictly less than the " "window size minus 1 (option --window_size, value=16)")] ] @@ -718,11 +756,11 @@ def test_svf_command_line_default(): # list of commands to test argslist = [ # default case: svf at the point height - "-v svf --radius 50 --directions 16 --resolution 0.5 -o tests/tests_out" - " tests/tests_data/toulouse-mnh.tif", + f"-v svf --radius 50 --directions 16 --resolution 0.5 -o {RastertoolsTestsData.tests_output_data_dir}" + f" {RastertoolsTestsData.tests_input_data_dir}/toulouse-mnh.tif", # default case: svf on ground - "-v svf --radius 50 --directions 16 --resolution 0.5 --altitude 0 -o tests/tests_out" - " tests/tests_data/toulouse-mnh.tif", + f"-v svf --radius 50 --directions 16 --resolution 0.5 --altitude 0 -o {RastertoolsTestsData.tests_output_data_dir}" + f" {RastertoolsTestsData.tests_input_data_dir}/toulouse-mnh.tif", ] output_filenames = ["toulouse-mnh-svf.tif"] @@ -743,21 +781,21 @@ def test_svf_command_line_errors(caplog): argslist = [ # output dir does not exist "-v svf --radius 50 --directions 16 --resolution 0.5 -o tests/truc" - " tests/tests_data/toulouse-mnh.tif", + f" {RastertoolsTestsData.tests_input_data_dir}/toulouse-mnh.tif", # missing required argument - "-v svf --directions 16 --resolution 0.5 -o tests/tests_out" - " tests/tests_data/toulouse-mnh.tif", + f"-v svf --directions 16 --resolution 0.5 -o {RastertoolsTestsData.tests_output_data_dir}" + f" {RastertoolsTestsData.tests_input_data_dir}/toulouse-mnh.tif", # radius > window_size / 2 "-v svf --radius 100 --window_size 128 --directions 16 --resolution 0.5" - " --altitude 0 -o tests/tests_out tests/tests_data/toulouse-mnh.tif", + f" --altitude 0 -o {RastertoolsTestsData.tests_output_data_dir} {RastertoolsTestsData.tests_input_data_dir}/toulouse-mnh.tif", ] # expected logs logslist = [ - [("eolab.rastertools.main", logging.ERROR, + [("eolab.rastertools.rastertools", logging.ERROR, "Output directory \"tests/truc\" does not exist.")], [], - [("eolab.rastertools.main", logging.ERROR, + [("eolab.rastertools.cli.utils_cli", logging.ERROR, "The radius (option --radius, value=100) must be strictly less than half the" " size of the window (option --window_size, value=128)")] ] @@ -780,17 +818,17 @@ def test_hillshade_command_line_default(): # elevation / azimuth are retrieved from https://www.sunearthtools.com/dp/tools/pos_sun.php argslist = [ # default case: hillshade at Toulouse the September, 21 solar noon - "-v hs --elevation 27.2 --azimuth 82.64 --resolution 0.5 -o tests/tests_out" - " tests/tests_data/toulouse-mnh.tif", + f"-v hs --elevation 27.2 --azimuth 82.64 --resolution 0.5 -o {RastertoolsTestsData.tests_output_data_dir}" + f" {RastertoolsTestsData.tests_input_data_dir}/toulouse-mnh.tif", # default case: hillshade at Toulouse the June, 21, solar 6PM - "-v hs --elevation 25.82 --azimuth 278.58 --resolution 0.5 -o tests/tests_out" - " tests/tests_data/toulouse-mnh.tif", + f"-v hs --elevation 25.82 --azimuth 278.58 --resolution 0.5 -o {RastertoolsTestsData.tests_output_data_dir}" + f" {RastertoolsTestsData.tests_input_data_dir}/toulouse-mnh.tif", # default case: hillshade at Toulouse the June, 21, solar noon - "-v hs --elevation 69.83 --azimuth 180 --resolution 0.5 -o tests/tests_out" - " tests/tests_data/toulouse-mnh.tif", + f"-v hs --elevation 69.83 --azimuth 180 --resolution 0.5 -o {RastertoolsTestsData.tests_output_data_dir}" + f" {RastertoolsTestsData.tests_input_data_dir}/toulouse-mnh.tif", # default case: hillshade at Toulouse the June, 21, solar 8AM - "-v hs --elevation 27.2 --azimuth 82.64 --resolution 0.5 -o tests/tests_out" - " tests/tests_data/toulouse-mnh.tif", + f"-v hs --elevation 27.2 --azimuth 82.64 --resolution 0.5 -o {RastertoolsTestsData.tests_output_data_dir}" + f" {RastertoolsTestsData.tests_input_data_dir}/toulouse-mnh.tif", ] output_filenames = ["toulouse-mnh-hillshade.tif"] @@ -811,27 +849,27 @@ def test_hillshade_command_line_errors(caplog): argslist = [ # output dir does not exist "-v hs --elevation 46.81 --azimuth 180.0 --resolution 0.5 -o tests/truc" - " tests/tests_data/toulouse-mnh.tif", + f" {RastertoolsTestsData.tests_input_data_dir}/toulouse-mnh.tif", # missing required argument "-v hs --elevation 46.81 --resolution 0.5 " - " tests/tests_data/toulouse-mnh.tif", + f" {RastertoolsTestsData.tests_input_data_dir}/toulouse-mnh.tif", # input file has more than 1 band - "-v hs --elevation 46.81 --azimuth 180.0 --resolution 0.5 -o tests/tests_out" - " tests/tests_data/S2A_MSIL2A_20190116T105401_N0211_R051_T30TYP_20190116T120806.vrt", + f"-v hs --elevation 46.81 --azimuth 180.0 --resolution 0.5 -o {RastertoolsTestsData.tests_output_data_dir}" + f" {RastertoolsTestsData.tests_input_data_dir}/S2A_MSIL2A_20190116T105401_N0211_R051_T30TYP_20190116T120806.vrt", # radius > window_size / 2 "-v hs --elevation 27.2 --azimuth 82.64 --resolution 0.5" - " --radius 100 --window_size 128 -o tests/tests_out" - " tests/tests_data/toulouse-mnh.tif", + f" --radius 100 --window_size 128 -o {RastertoolsTestsData.tests_output_data_dir}" + f" {RastertoolsTestsData.tests_input_data_dir}/toulouse-mnh.tif", ] # expected logs logslist = [ - [("eolab.rastertools.main", logging.ERROR, + [("eolab.rastertools.rastertools", logging.ERROR, "Output directory \"tests/truc\" does not exist.")], [], - [("eolab.rastertools.main", logging.ERROR, + [("eolab.rastertools.cli.utils_cli", logging.ERROR, "Invalid input file, it must contain a single band.")], - [("eolab.rastertools.main", logging.ERROR, + [("eolab.rastertools.cli.utils_cli", logging.ERROR, "The radius (option --radius, value=100) must be strictly less than half" " the size of the window (option --window_size, value=128)")] ] diff --git a/tests/tests_data/listing.lst b/tests/tests_data/listing.lst deleted file mode 100644 index 9e0cb52..0000000 --- a/tests/tests_data/listing.lst +++ /dev/null @@ -1,2 +0,0 @@ -tests/tests_data/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip -tests/tests_data/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip \ No newline at end of file diff --git a/tests/tests_data/listing2.lst b/tests/tests_data/listing2.lst deleted file mode 100644 index 06bd284..0000000 --- a/tests/tests_data/listing2.lst +++ /dev/null @@ -1,2 +0,0 @@ -tests/tests_data/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif -tests/tests_data/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif \ No newline at end of file diff --git a/tests/tests_data/listing3.lst b/tests/tests_data/listing3.lst deleted file mode 100644 index ad3fcea..0000000 --- a/tests/tests_data/listing3.lst +++ /dev/null @@ -1 +0,0 @@ -tests/tests_data/tif_file.tif \ No newline at end of file diff --git a/tests/utils4test.py b/tests/utils4test.py index ccc59eb..a946c66 100644 --- a/tests/utils4test.py +++ b/tests/utils4test.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- import os import shutil +from dataclasses import dataclass from pathlib import Path from . import cmptools @@ -11,10 +12,19 @@ __license = "Apache v2.0" -indir = "tests/tests_data/" -outdir = "tests/tests_out/" -__root_refdir = "tests/tests_refs/" +@dataclass +class RastertoolsTestsData: + project_dir: Path = Path(__file__).parent.parent + tests_project_dir:str = str(project_dir) + tests_input_data_dir:str = str(project_dir / "tests" / "tests_data" ) + tests_output_data_dir:str = str(project_dir / "tests" / "tests_out") + tests_ref_data_dir:str = str(project_dir / "tests" / "tests_refs") + +projectdir = RastertoolsTestsData.tests_project_dir + "/" +indir = RastertoolsTestsData.tests_input_data_dir + "/" +outdir = RastertoolsTestsData.tests_output_data_dir + "/" +__root_refdir = RastertoolsTestsData.tests_ref_data_dir + "/" def get_refdir(testname: str): return __root_refdir + testname From d88b71cfd0c029632c7cdabef7854ca46cdc9a6b Mon Sep 17 00:00:00 2001 From: cadauxe Date: Thu, 14 Nov 2024 12:21:28 +0100 Subject: [PATCH 06/17] docs: Docstrings of the click cli functions docs: Updating html doc with cli --help output --- docs/cli/filtering.rst | 340 ++++++--------------- docs/cli/hillshade.rst | 65 ++-- docs/cli/radioindice.rst | 111 +++---- docs/cli/speed.rst | 42 +-- docs/cli/svf.rst | 70 +++-- docs/cli/tiling.rst | 81 ++--- docs/cli/timeseries.rst | 64 ++-- docs/cli/zonalstats.rst | 134 ++++---- src/eolab/rastertools/cli/filtering.py | 214 ++++--------- src/eolab/rastertools/cli/filtering_dyn.py | 151 --------- src/eolab/rastertools/cli/hillshade.py | 35 +-- src/eolab/rastertools/cli/radioindice.py | 49 +-- src/eolab/rastertools/cli/speed.py | 17 +- src/eolab/rastertools/cli/svf.py | 20 +- src/eolab/rastertools/cli/tiling.py | 40 ++- src/eolab/rastertools/cli/timeseries.py | 27 +- src/eolab/rastertools/cli/utils_cli.py | 36 +-- src/eolab/rastertools/cli/zonalstats.py | 22 +- src/eolab/rastertools/main.py | 24 +- 19 files changed, 563 insertions(+), 979 deletions(-) delete mode 100644 src/eolab/rastertools/cli/filtering_dyn.py diff --git a/docs/cli/filtering.rst b/docs/cli/filtering.rst index 159e400..4535474 100644 --- a/docs/cli/filtering.rst +++ b/docs/cli/filtering.rst @@ -20,265 +20,105 @@ filter mean Apply local mean filter adaptive_gaussian Apply adaptive gaussian filter -The available filters are Adaptive Gaussian, Local Sum, and Local Mean. -Each filter is used as a sub-command and has specific arguments for filtering. -To see the definitions of these arguments, type the option --help. +For different filters are available. They are applied as sub-command that each define the arguments +that configure the filter. Type option --help to get the definition of the arguments: -- **Median** - - .. code-block:: console - - $ rastertools filter median --help - usage: rastertools filter median [-h] --kernel_size KERNEL_SIZE [-o OUTPUT] - [-ws WINDOW_SIZE] - [-p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}] - [-b BANDS [BANDS ...]] [-a] - inputs [inputs ...] - - Apply a median filter (see scipy median_filter for more information) - - positional arguments: - inputs Input file to process (e.g. Sentinel2 L2A MAJA from - THEIA). You can provide a single file with extension - ".lst" (e.g. "filtering.lst") that lists the input - files to process (one input file per line in .lst) - - optional arguments: - -h, --help show this help message and exit - --kernel_size KERNEL_SIZE - Kernel size of the filter function, e.g. 3 means a - square of 3x3 pixels on which the filter function is - computed (default: 8) - -o OUTPUT, --output OUTPUT - Output dir where to store results (by default current - dir) - -ws WINDOW_SIZE, --window_size WINDOW_SIZE - Size of tiles to distribute processing, default: 1024 - -p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}, --pad {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap} - Pad to use around the image, default : edge (see https - ://numpy.org/doc/stable/reference/generated/numpy.pad. - html for more information) - -b BANDS [BANDS ...], --bands BANDS [BANDS ...] - List of bands to compute - -a, --all Compute all bands - - By default only first band is computed. - - The corresponding API functions that is called by the command line interface is the following : - - .. autofunction:: eolab.rastertools.processing.algo.median - - - Here is an example of a median filter applied to the NDVI of a SENTINEL2 L2A THEIA image cropped to a region of interest. - This raster was previously computed using :ref:`radioindice` on the original SENTINEL2 L2A THEIA image. - - .. code-block:: console - - $ rastertools filter median --kernel_size 16 "./SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" - - .. list-table:: - :widths: 20 20 - :header-rows: 0 - - * - .. centered:: Original - - .. centered:: Filtered by Median - - * - .. image:: ../_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.jpg - :align: center - - .. image:: ../_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-median.jpg - :align: center - -- **Local sum** - - .. code-block:: console - - $ rastertools filter sum --help - usage: rastertools filter sum [-h] --kernel_size KERNEL_SIZE [-o OUTPUT] - [-ws WINDOW_SIZE] - [-p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}] - [-b BANDS [BANDS ...]] [-a] - inputs [inputs ...] - - Apply a local sum filter using integral image method - - positional arguments: - inputs Input file to process (e.g. Sentinel2 L2A MAJA from - THEIA). You can provide a single file with extension - ".lst" (e.g. "filtering.lst") that lists the input - files to process (one input file per line in .lst) - - optional arguments: - -h, --help show this help message and exit - --kernel_size KERNEL_SIZE - Kernel size of the filter function, e.g. 3 means a - square of 3x3 pixels on which the filter function is - computed (default: 8) - -o OUTPUT, --output OUTPUT - Output dir where to store results (by default current - dir) - -ws WINDOW_SIZE, --window_size WINDOW_SIZE - Size of tiles to distribute processing, default: 1024 - -p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}, --pad {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap} - Pad to use around the image, default : edge (see https - ://numpy.org/doc/stable/reference/generated/numpy.pad. - html for more information) - -b BANDS [BANDS ...], --bands BANDS [BANDS ...] - List of bands to compute - -a, --all Compute all bands - - By default only first band is computed. - - The corresponding API functions that is called by the command line interface is the following : - - .. autofunction:: eolab.rastertools.processing.algo.local_sum - - Here is an example of the local mean applied to the NDVI of a SENTINEL2 L2A THEIA image cropped to a region of interest. - This raster was previously computed using :ref:`radioindice` on the original SENTINEL2 L2A THEIA image. - - .. code-block:: console - - $ rastertools filter sum --kernel_size 16 "./SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" - - .. list-table:: - :widths: 20 20 - :header-rows: 0 - - * - .. centered:: Original - - .. centered:: Filtered by Local sum - - * - .. image:: ../_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.jpg - :align: center - - .. image:: ../_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-sum.jpg - :align: center - -- **Local mean** - - .. code-block:: console - - $ rastertools filter mean --help - usage: rastertools filter mean [-h] --kernel_size KERNEL_SIZE [-o OUTPUT] - [-ws WINDOW_SIZE] - [-p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}] - [-b BANDS [BANDS ...]] [-a] - inputs [inputs ...] - - Apply a local mean filter using integral image method - - positional arguments: - inputs Input file to process (e.g. Sentinel2 L2A MAJA from - THEIA). You can provide a single file with extension - ".lst" (e.g. "filtering.lst") that lists the input - files to process (one input file per line in .lst) - - optional arguments: - -h, --help show this help message and exit - --kernel_size KERNEL_SIZE - Kernel size of the filter function, e.g. 3 means a - square of 3x3 pixels on which the filter function is - computed (default: 8) - -o OUTPUT, --output OUTPUT - Output dir where to store results (by default current - dir) - -ws WINDOW_SIZE, --window_size WINDOW_SIZE - Size of tiles to distribute processing, default: 1024 - -p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}, --pad {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap} - Pad to use around the image, default : edge (see https - ://numpy.org/doc/stable/reference/generated/numpy.pad. - html for more information) - -b BANDS [BANDS ...], --bands BANDS [BANDS ...] - List of bands to compute - -a, --all Compute all bands - - By default only first band is computed. - - - The corresponding API functions that is called by the command line interface is the following : - - .. autofunction:: eolab.rastertools.processing.algo.local_mean - - - Here is an example of the local mean applied to the NDVI of a SENTINEL2 L2A THEIA image cropped to a region of interest. - This raster was previously computed using :ref:`radioindice` on the original SENTINEL2 L2A THEIA image. - - .. code-block:: console - - $ rastertools filter mean --kernel_size 16 "./SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" - - .. list-table:: - :widths: 20 20 - :header-rows: 0 - - * - .. centered:: Original - - .. centered:: Filtered by Local mean - - * - .. image:: ../_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.jpg - :align: center - - .. image:: ../_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-mean.jpg - :align: center - -- **Adaptative gaussian** - - .. code-block:: console - - $ rastertools filter adaptive_gaussian --help - usage: rastertools filter adaptive_gaussian [-h] --kernel_size KERNEL_SIZE - --sigma SIGMA [-o OUTPUT] - [-ws WINDOW_SIZE] - [-p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}] - [-b BANDS [BANDS ...]] [-a] - inputs [inputs ...] - - Apply an adaptive (Local gaussian of 3x3) recursive filter on the input image - - positional arguments: - inputs Input file to process (e.g. Sentinel2 L2A MAJA from - THEIA). You can provide a single file with extension - ".lst" (e.g. "filtering.lst") that lists the input - files to process (one input file per line in .lst) +.. code-block:: console - optional arguments: - -h, --help show this help message and exit - --kernel_size KERNEL_SIZE - Kernel size of the filter function, e.g. 3 means a - square of 3x3 pixels on which the filter function is - computed (default: 8) - --sigma SIGMA Standard deviation of the Gaussian distribution - (sigma) - -o OUTPUT, --output OUTPUT - Output dir where to store results (by default current - dir) - -ws WINDOW_SIZE, --window_size WINDOW_SIZE - Size of tiles to distribute processing, default: 1024 - -p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}, --pad {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap} - Pad to use around the image, default : edge (see https - ://numpy.org/doc/stable/reference/generated/numpy.pad. - html for more information) - -b BANDS [BANDS ...], --bands BANDS [BANDS ...] - List of bands to compute - -a, --all Compute all bands + $ rastertools filter adaptive_gaussian --help + usage: rastertools filter adaptive_gaussian [-h] --kernel_size KERNEL_SIZE + --sigma SIGMA [-o OUTPUT] + [-ws WINDOW_SIZE] + [-p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}] + [-b BANDS [BANDS ...]] [-a] + inputs [inputs ...] + + Execute the requested filter on the input files with the specified + parameters. The `inputs` argument can either be a single file or a `.lst` + file containing a list of input files. + + Arguments: + + inputs TEXT + + Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). You can + provide a single file with extension ".lst" (e.g. "filtering.lst") that + lists the input files to process (one input file per line in .lst). + + Options: + --sigma INTEGER Standard deviation of the Gaussian + distribution [required] + -a, --all Process all bands + -b, --bands INTEGER List of bands to process + -p, --pad [none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap] + Pad to use around the image, default : edge(see + https://numpy.org/doc/stable/reference/generated/numpy.pad.html + for more information) + -ws, --window_size INTEGER Size of tiles to distribute processing, + default: 1024 + -o, --output TEXT Output directory to store results (by + default current directory) + --kernel_size INTEGER Kernel size of the filter function, e.g. 3 + means a square of 3x3 pixels on which the + filter function is computed (default: 8) + -h, --help Show this message and exit. + + Apply an adaptive (Local gaussian of 3x3) recursive filter on the input image + + positional arguments: + inputs Input file to process (e.g. Sentinel2 L2A MAJA from + THEIA). You can provide a single file with extension + ".lst" (e.g. "filtering.lst") that lists the input + files to process (one input file per line in .lst) - By default only first band is computed. + optional arguments: + -h, --help show this help message and exit + --kernel_size KERNEL_SIZE + Kernel size of the filter function, e.g. 3 means a + square of 3x3 pixels on which the filter function is + computed (default: 8) + --sigma SIGMA Standard deviation of the Gaussian distribution + (sigma) + -o OUTPUT, --output OUTPUT + Output dir where to store results (by default current + dir) + -ws WINDOW_SIZE, --window_size WINDOW_SIZE + Size of tiles to distribute processing, default: 1024 + -p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}, --pad {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap} + Pad to use around the image, default : edge (see https + ://numpy.org/doc/stable/reference/generated/numpy.pad. + html for more information) + -b BANDS [BANDS ...], --bands BANDS [BANDS ...] + List of bands to compute + -a, --all Compute all bands + + By default only first band is computed. + +Examples: + +The following examples use an input raster file generated by radioindice. This is an NDVI of a SENTINEL2 L2A THEIA image cropped to a (small) +region of interest. + +.. image:: ../_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.jpg + +To apply three filters (median, mean and adaptive_gaussian) on a kernel of dimension 16x16, run these commands: - The corresponding API functions that is called by the command line interface is the following : +.. code-block:: console - .. autofunction:: eolab.rastertools.processing.algo.adaptive_gaussian + $ rastertools filter median --kernel_size 16 "./SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" + $ rastertools filter mean --kernel_size 16 "./SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" + $ rastertools filter adaptive_gaussian --kernel_size 16 --sigma 1 "./SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" - Here is an example of the local mean applied to the NDVI of a SENTINEL2 L2A THEIA image cropped to a region of interest. - This raster was previously computed using :ref:`radioindice` on the original SENTINEL2 L2A THEIA image. +The commands will generate respectively: - .. code-block:: console +- SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-median.tif - $ rastertools filter adaptive_gaussian --kernel_size 16 --sigma 1 "./SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" +.. image:: ../_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-median.jpg - .. list-table:: - :widths: 20 20 - :header-rows: 0 +- SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-mean.tif - * - .. centered:: Original - - .. centered:: Filtered by Adaptive gaussian +.. image:: ../_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-mean.jpg - * - .. image:: ../_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.jpg - :align: center - - .. image:: ../_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-adaptive_gaussian.jpg - :align: center +- SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-adaptive_gaussian.tif +.. image:: ../_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-adaptive_gaussian.jpg \ No newline at end of file diff --git a/docs/cli/hillshade.rst b/docs/cli/hillshade.rst index 7823539..3a61822 100644 --- a/docs/cli/hillshade.rst +++ b/docs/cli/hillshade.rst @@ -15,36 +15,41 @@ computes the shadows of the ground surface (buildings, trees, etc.). [-o OUTPUT] [-ws WINDOW_SIZE] [-p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}] inputs [inputs ...] - - Compute hillshades of a Digital Height Model. - - positional arguments: - inputs Input file to process (i.e. geotiff corresponding to a - Digital Height Model). You can provide a single file - with extension ".lst" (e.g. "filtering.lst") that - lists the input files to process (one input file per - line in .lst) - - optional arguments: - -h, --help show this help message and exit - --elevation ELEVATION - Elevation of the sun in degrees, [0°, 90°] where - 90°=zenith and 0°=horizon - --azimuth AZIMUTH Azimuth of the sun in degrees, [0°, 360°] where - 0°=north, 90°=east, 180°=south and 270°=west - --radius RADIUS Max distance (in pixels) around a point to evaluate - horizontal elevation angle - --resolution RESOLUTION - Pixel resolution in meter - -o OUTPUT, --output OUTPUT - Output dir where to store results (by default current - dir) - -ws WINDOW_SIZE, --window_size WINDOW_SIZE - Size of tiles to distribute processing, default: 1024 - -p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}, --pad {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap} - Pad to use around the image, default : edge (see https - ://numpy.org/doc/stable/reference/generated/numpy.pad. - html for more information) + + Execute the hillshade subcommand on a Digital Height Model (DHM) using the + given solar parameters (elevation, azimuth), resolution, and optional + parameters for processing the raster. + + Arguments: + + inputs TEXT + + Input file to process (i.e. geotiff corresponding to a Digital Height + Model). You can provide a single file with extension ".lst" (e.g. + "hillshade.lst") that lists the input files to process (one input file + per line in .lst) + + Options: + --elevation FLOAT Elevation of the sun in degrees, [0°, 90°] + where 90°=zenith and 0°=horizon [required] + --azimuth FLOAT Azimuth of the sun in degrees, [0°, 360°] + where 0°=north, 90°=east, 180°=south and + 270°=west [required] + --radius INTEGER Maximum distance (in pixels) around a point + to evaluate horizontal elevation angle. If + not set, it is automatically computed from + the range of altitudes in the digital model. + --resolution FLOAT Pixel resolution in meter [required] + -o, --output TEXT Output directory to store results (by + default current directory) + -ws, --window_size INTEGER Size of tiles to distribute processing, + default: 1024 + -p, --pad [none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap] + Pad to use around the image, default : edge (see + https://numpy.org/doc/stable/reference/generated/numpy.pad.html + for more information) + -h, --help Show this message and exit. + .. warning:: This command line does not accept all input raster products as other raster tools (radioindice, zonalstats). diff --git a/docs/cli/radioindice.rst b/docs/cli/radioindice.rst index 1bae9cd..56daaad 100644 --- a/docs/cli/radioindice.rst +++ b/docs/cli/radioindice.rst @@ -16,58 +16,65 @@ radioindice [--bi] [--bi2] [-nd band1 band2] [-ws WINDOW_SIZE] inputs [inputs ...] - - Compute a list of radiometric indices (NDVI, NDWI, etc.) on a raster image - - positional arguments: - inputs Input file to process (e.g. Sentinel2 L2A MAJA from - THEIA). You can provide a single file with extension - ".lst" (e.g. "radioindice.lst") that lists the input - files to process (one input file per line in .lst) - - optional arguments: - -h, --help show this help message and exit - -o OUTPUT, --output OUTPUT - Output dir where to store results (by default current - dir) - -m, --merge Merge all indices in the same image (i.e. one band per - indice). - -r ROI, --roi ROI Region of interest in the input image (vector) - -ws WINDOW_SIZE, --window_size WINDOW_SIZE - Size of tiles to distribute processing, default: 1024 - - Options to select the indices to compute: - -i INDICES [INDICES ...], --indices INDICES [INDICES ...] - List of indices to computePossible indices are: bi, - bi2, evi, ipvi, mndwi, msavi, msavi2, ndbi, ndpi, - ndti, ndvi, ndwi, ndwi2, pvi, ri, rvi, savi, tndvi, - tsavi - --ndvi Compute ndvi indice INSERT LINK TO CORRESPONDING DOC - --tndvi Compute tndvi indice - --rvi Compute rvi indice - --pvi Compute pvi indice - --savi Compute savi indice - --tsavi Compute tsavi indice - --msavi Compute msavi indice - --msavi2 Compute msavi2 indice - --ipvi Compute ipvi indice - --evi Compute evi indice - --ndwi Compute ndwi indice - --ndwi2 Compute ndwi2 indice - --mndwi Compute mndwi indice - --ndpi Compute ndpi indice - --ndti Compute ndti indice - --ndbi Compute ndbi indice - --ri Compute ri indice - --bi Compute bi indice - --bi2 Compute bi2 indice - -nd band1 band2, -normalized_difference band1 band2 - Compute the normalized difference of two bands defined - as parameter of this option, e.g. "-nd red nir" will - compute (red-nir)/(red+nir). See - eolab.rastertools.product.rastertype.BandChannel for - the list of bands names. Several nd options can be set - to compute several normalized differences. + + Compute the requested radio indices on raster data. + + This command computes various vegetation and environmental indices on + satellite or raster data based on the provided input images and options. The + tool can compute specific indices, merge the results into one image, compute + normalized differences between bands, and apply processing using a region of + interest (ROI) and specified tile/window size. + + Arguments: + + inputs TEXT + + Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). You can + provide a single file with extension ".lst" (e.g. "radioindice.lst") + that lists the input files to process (one input file per line in .lst). + + Options: + -o, --output TEXT Output directory to store results (by + default current directory) + -m, --merge Merge all indices in the same image (i.e. + one band per indice) + -r, --roi TEXT Region of interest in the input image + (vector) + -ws, --window_size INTEGER Size of tiles to distribute processing, + default: 1024 + -i, --indices TEXT List of indices to compute. Possible indices + are: bi, bi2, evi, ipvi, mndwi, msavi, + msavi2, ndbi, ndpi, ndti, ndvi, ndwi, ndwi2, + pvi, ri, rvi, savi, tndvi, tsavi + --bi2 Compute bi2 indice + --bi Compute bi indice + --ri Compute ri indice + --ndbi Compute ndbi indice + --ndti Compute ndti indice + --ndpi Compute ndpi indice + --mndwi Compute mndwi indice + --ndwi2 Compute ndwi2 indice + --ndwi Compute ndwi indice + --evi Compute evi indice + --ipvi Compute ipvi indice + --msavi2 Compute msavi2 indice + --msavi Compute msavi indice + --tsavi Compute tsavi indice + --savi Compute savi indice + --pvi Compute pvi indice + --rvi Compute rvi indice + --tndvi Compute tndvi indice + --ndvi Compute ndvi indice + -nd, --normalized_difference band1 band2 + Compute the normalized difference of two + bands defineda s parameter of this option, + e.g. "-nd red nir" will compute (red- + nir)/(red+nir). See + eolab.rastertools.product.rastertype. + BandChannel for the list of + bands names. Several nd options can be set + to compute several normalized differences. + -h, --help Show this message and exit. If no indice option is explicitly set, NDVI, NDWI and NDWI2 are computed. diff --git a/docs/cli/speed.rst b/docs/cli/speed.rst index 903c4d0..964b68d 100644 --- a/docs/cli/speed.rst +++ b/docs/cli/speed.rst @@ -8,28 +8,30 @@ speed .. code-block:: console $ rastertools speed --help - usage: rastertools speed [-h] [-b BANDS [BANDS ...]] [-a] [-o OUTPUT] inputs [inputs ...] - - Compute the speed of radiometric values of several raster images - - positional arguments: - inputs Input file to process (e.g. Sentinel2 L2A MAJA from - THEIA). You can provide a single file with extension - ".lst" (e.g. "speed.lst") that lists the input files - to process (one input file per line in .lst) - - optional arguments: - -h, --help show this help message and exit - -b BANDS [BANDS ...], --bands BANDS [BANDS ...] - List of bands to compute - -a, --all Compute all bands - -o OUTPUT, --output OUTPUT - Output dir where to store results (by default current - dir) - - By default only first band is computed. + + Compute the speed of radiometric values for multiple raster images. + + This command calculates the speed of radiometric values for raster data, + optionally processing specific bands or all bands from the input images. The + results are saved to a specified output directory. + + Arguments: + + inputs TEXT + + Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). You can + provide a single file with extension ".lst" (e.g. "speed.lst") that + lists the input files to process (one input file per line in .lst). + + Options: + -b, --bands INTEGER List of bands to process + -a, --all Process all bands + -o, --output TEXT Output directory to store results (by default current + directory) + -h, --help Show this message and exit. + .. warning:: At least two input rasters must be given. The rasters must match one of the configured raster types, diff --git a/docs/cli/svf.rst b/docs/cli/svf.rst index af886e6..6c7ef9f 100644 --- a/docs/cli/svf.rst +++ b/docs/cli/svf.rst @@ -46,37 +46,45 @@ too many points, the "radius" parameter defines the max distance of the pixel to [-o OUTPUT] [-ws WINDOW_SIZE] [-p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}] inputs [inputs ...] - - Compute Sky View Factor of a Digital Height Model. - - positional arguments: - inputs Input file to process (i.e. geotiff corresponding to a - Digital Height Model). You can provide a single file - with extension ".lst" (e.g. "filtering.lst") that - lists the input files to process (one input file per - line in .lst) - - optional arguments: - -h, --help show this help message and exit - --radius RADIUS Max distance (in pixels) around a point to evaluate - horizontal elevation angle - --directions DIRECTIONS - Number of directions on which to compute the horizon - elevation angle - --resolution RESOLUTION - Pixel resolution in meter - --altitude ALTITUDE Reference altitude to use for computing the SVF. If - this option is not specified, SVF is computed for - every point at the altitude of the point - -o OUTPUT, --output OUTPUT - Output dir where to store results (by default current - dir) - -ws WINDOW_SIZE, --window_size WINDOW_SIZE - Size of tiles to distribute processing, default: 1024 - -p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}, --pad {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap} - Pad to use around the image, default : edge (see https - ://numpy.org/doc/stable/reference/generated/numpy.pad. - html for more information) + + Compute the Sky View Factor (SVF) of a Digital Height Model (DHM). + + The Sky View Factor (SVF) is a measure of the visibility of the sky from a + point in a Digital Height Model (DHM). It is calculated by evaluating the + horizontal elevation angle from a given point in multiple directions (as + specified by the user), and is influenced by the topography and surrounding + terrain features. + + Arguments: + + inputs TEXT + + Input file to process (i.e. geotiff corresponding to a Digital Height + Model). You can provide a single file with extension ".lst" (e.g. + "svf.lst") that lists the input files to process (one input file + per line in .lst) + + Options: + --radius INTEGER Maximum distance (in pixels) around a point + to evaluate horizontal elevation angle + [required] + --directions INTEGER Number of directions on which to compute the + horizon elevation angle [required] + --resolution FLOAT Pixel resolution in meter [required] + --altitude INTEGER Reference altitude to use for computing the + SVF. If this option is not specified, SVF is + computed for every point at the altitude of + the point + -o, --output TEXT Output directory to store results (by + default current directory) + -ws, --window_size INTEGER Size of tiles to distribute processing, + default: 1024 + -p, --pad [none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap] + Pad to use around the image, default : edge(see + https://numpy.org/doc/stable/reference/generated/numpy.pad.html + for more information) + -h, --help Show this message and exit. + .. warning:: This command line does not accept all input raster products as other raster tools (radioindice, zonalstats). diff --git a/docs/cli/tiling.rst b/docs/cli/tiling.rst index 9f353d7..e0021c9 100644 --- a/docs/cli/tiling.rst +++ b/docs/cli/tiling.rst @@ -13,46 +13,47 @@ file. [--id ID [ID ...]] [-o OUTPUT] [-n OUTPUT_NAME] [-d SUBDIR_NAME] inputs [inputs ...] - - Generate tiles of an input raster image following the geometries defined by a - given grid - - positional arguments: - inputs Raster files to process. You can provide a single file - with extension ".lst" (e.g. "tiling.lst") that lists - the input files to process (one input file per line in - .lst) - - optional arguments: - -h, --help show this help message and exit - -g GRID_FILE, --grid GRID_FILE - vector-based spatial data file containing the grid to - use to generate the tiles - --id_col ID_COLUMN Name of the column in the grid file used to number the - tiles. When ids are defined, this argument is required - to identify which column corresponds to the defined - ids - --id ID [ID ...] Tiles ids of the grid to export as new tile, default - all - -o OUTPUT, --output OUTPUT - Output dir where to store results (by default current - dir) - -n OUTPUT_NAME, --name OUTPUT_NAME - Basename for the output raster tiles, default: - "{}_tile{}". The basename must be defined as a - formatted string where tile index is at position 1 and - original filename is at position 0. For instance, - tile{1}.tif will generate the filename tile75.tif for - the tile id = 75. - -d SUBDIR_NAME, --dir SUBDIR_NAME - When each tile must be generated in a different - subdir, it defines the naming convention for the - subdir. It is a formatted string with one positional - parameter corresponding to the tile index. For - instance, tile{} will generate the subdir name tile75/ - for the tile id = 75. By default, subdir is not - defined and output files will be generated directly in - the outputdir. + Generate tiles of an input raster image following the geometries defined by + a given grid. + + The tiling command divides a raster image into smaller tiles based on a grid + defined in a vector-based spatial data file. Each tile corresponds to a + specific area within the grid, and tiles can be saved using a customizable + naming convention and optionally placed in subdirectories based on their + tile ID. + + Arguments: + + inputs TEXT + + Raster files to process. You can provide a single file with extension + ".lst" (e.g. "tiling.lst") that lists the input files to process (one + input file per line in .lst) + + Options: + -g, --grid TEXT vector-based spatial data file containing the grid to use + to generate the tiles [required] + --id_col TEXT Name of the column in the grid file used to number the + tiles. When ids are defined, this argument is requiredto + identify which column corresponds to the define ids + --id INTEGER Tiles ids of the grid to export as new tile, default all + -o, --output TEXT Output directory to store results (by default current + directory) + -n, --name TEXT Basename for the output raster tiles, + default:"{}_tile{}". The basename must be defined as a + formatted string where tile index is at position 1 and + original filename is at position 0. For instance, + tile{1}.tif will generate the filename tile75.tif for the + tile id = 75 + -d, --dir TEXT When each tile must be generated in a + different subdirectory, it defines the naming convention + for the subdirectory. It is a formatted string with one + positional parameter corresponding to the tile index. For + instance, tile{} will generate the subdirectory name + tile75/for the tile id = 75. By default, subdirectory is + not defined and output files will be generated directly + in the output directory + -h, --help Show this message and exit. In the next examples, we will be working on a grid of 4 cells (ids 1, 2, 3 and 4): *grid.geojson* and the image: *image.tif*. The grid and the image only overlap on the cells 1 and 2. diff --git a/docs/cli/timeseries.rst b/docs/cli/timeseries.rst index 262a9cf..510ead6 100644 --- a/docs/cli/timeseries.rst +++ b/docs/cli/timeseries.rst @@ -21,39 +21,39 @@ and may thus contain the same gaps as the input raster. [-s START_DATE] [-e END_DATE] [-p TIME_PERIOD] [-ws WINDOW_SIZE] inputs [inputs ...] - + Generate a timeseries of images (without gaps) from a set of input images. - Data not present in the input images (no image for the date or masked data) - are interpolated (with linear interpolation) so that all gaps are filled. - - positional arguments: - inputs Input files to process (e.g. Sentinel2 L2A MAJA from - THEIA). You can provide a single file with extension - ".lst" (e.g. "speed.lst") that lists the input files - to process (one input file per line in .lst) - - optional arguments: - -h, --help show this help message and exit - -b BANDS [BANDS ...], --bands BANDS [BANDS ...] - List of bands to compute - -a, --all Compute all bands - -o OUTPUT, --output OUTPUT - Output dir where to store results (by default current - dir) - -s START_DATE, --start_date START_DATE - Start date of the timeseries to generate in the - following format: yyyy-MM-dd - -e END_DATE, --end_date END_DATE - End date of the timeseries to generate in the - following format: yyyy-MM-dd - -p TIME_PERIOD, --time_period TIME_PERIOD - Time period (number of days) between two consecutive - images in the timeseries to generate e.g. 10 = - generate one image every 10 days - -ws WINDOW_SIZE, --window_size WINDOW_SIZE - Size of tiles to distribute processing, default: 1024 - - By default only first band is computed. + Data not present in the input images (e.g., missing images for specific + dates or masked data) are interpolated (with linear interpolation) so that + all gaps in the timeseries are filled. + + This command is useful for generating continuous timeseries data, even when + some input images are missing or contain masked values. + + Arguments: + + inputs TEXT + + Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). You can + provide a single file with extension ".lst" (e.g. "speed.lst") that + lists the input files to process (one input file per line in .lst). + + Options: + -b, --bands LIST List of bands to process + -a, --all Process all bands + -o, --output TEXT Output directory to store results (by default + current directory) + -s, --start_date TEXT Start date of the timeseries to generate in the + following format: yyyy-MM-dd + -e, --end_date TEXT End date of the timeseries to generate in the + following format: yyyy-MM-dd + -p, --time_period INTEGER Time period (number of days) between two + consecutive images in the timeseries to generate + e.g. 10 = generate one image every 10 days + -ws, --window_size INTEGER Size of tiles to distribute processing, default: + 1024 + -h, --help Show this message and exit. + .. warning:: At least two input rasters must be given. The rasters must match one of the configured raster types, diff --git a/docs/cli/zonalstats.rst b/docs/cli/zonalstats.rst index ce0fe32..a0812db 100644 --- a/docs/cli/zonalstats.rst +++ b/docs/cli/zonalstats.rst @@ -28,77 +28,69 @@ cities using: [--category_index CATEGORY_INDEX] [--category_names CATEGORY_NAMES] inputs [inputs ...] - - Compute zonal statistics of a raster image. Available statistics are: min max - range mean std percentile_x (x in [0, 100]) median mad count valid nodata sum - majority minority unique - - positional arguments: - inputs Raster files to process. You can provide a single file - with extension ".lst" (e.g. "zonalstats.lst") that - lists the input files to process (one input file per - line in .lst) - - optional arguments: - -h, --help show this help message and exit - -o OUTPUT, --output OUTPUT - Output dir where to store results (by default current - dir) - -f OUTPUT_FORMAT, --format OUTPUT_FORMAT - Output format of the results when input geometries are - provided (by default ESRI Shapefile). Possible values - are ESRI Shapefile, GeoJSON, CSV, GPKG, GML - -g GEOMETRIES, --geometry GEOMETRIES - List of geometries where to compute statistics (vector - like a shapefile or geojson) - -w, --within When activated, statistics are computed for the - geometries that are within the raster shape. The - default behaviour otherwise is to compute statistics - for all geometries that intersect the raster shape. - --stats STATS [STATS ...] - List of stats to compute. Possible stats are: min max - range mean std percentile_x (x in [0, 100]) median mad - count valid nodata sum majority minority unique - --categorical If the input raster is categorical (i.e. raster values - represent discrete classes) compute the counts of - every unique pixel values. - --valid_threshold VALID_THRESHOLD - Minimum percentage of valid pixels in a shape to - compute its statistics. - --area Whether to multiply all stats by the area of a cell of - the input raster. - --prefix PREFIX Add a prefix to the keys (default: None). One prefix - per band (e.g. 'band1 band2') - -b BANDS [BANDS ...], --bands BANDS [BANDS ...] - List of bands to compute - -a, --all Compute all bands - - Options to output the outliers: - --sigma SIGMA Distance to the mean value (in sigma) in order to - produce a raster that highlights outliers. - - Options to plot the generated stats: - -c CHARTFILE, --chart CHARTFILE - Generate a chart per stat and per geometry - (x=timestamp of the input products / y=stat value) and - store it in the file defined by this argument - -d, --display Display the chart - -gi GEOM_INDEX, --geometry-index GEOM_INDEX - Name of the geometry index used for the chart - (default='ID') - - Options to compute stats per category in geometry. If activated, the generated geometries will contain stats for every categories present in the geometry: - --category_file CATEGORY_FILE - File (raster or geometries) containing discrete - classes classifying the ROI. - --category_index CATEGORY_INDEX - Column name identifying categories in categroy_file - (only if file format is geometries) - --category_names CATEGORY_NAMES - JSON files containing a dict with classes index as - keys and names to display classes as values. - - By default only first band is computed. + + Compute zonal statistics of a raster image. + + Available statistics are: min, max, range, mean, std, percentile_x (x in [0, + 100]), median, mad, count, valid, nodata, sum, majority, minority, unique. + + By default, only the first band is computed unless specified otherwise. + + Arguments: + + inputs TEXT + + Raster files to process. You can provide a single filewith extension + ".lst" (e.g. "zonalstats.lst") that lists the input files to process + (one input file per line in .lst) + + Options: + -o, --output TEXT Output directory to store results (by default + current directory) + -f, --format TEXT Output format of the results when input + geometries are provided (by default ESRI + Shapefile). Possible values are ESRI Shapefile, + GeoJSON, CSV, GPKG, GML + -g, --geometry TEXT List of geometries where to compute statistics + (vector like a shapefile or geojson) + -w, --within When activated, statistics are computed for the + geometries that are within the raster shape. The + default behaviour otherwise is to compute + statistics for all geometries that intersect the + raster shape. + --stats TEXT List of stats to compute. Possible stats are: + min max range mean std percentile_x (x in [0, + 100]) median mad count valid nodata sum majority + minority unique + --categorical If the input raster is categorical (i.e. raster + values represent discrete classes) compute the + counts of every unique pixel values. + --valid_threshold FLOAT Minimum percentage of valid pixels in a shape to + compute its statistics. + --area Whether to multiply all stats by the area of a + cell of the input raster. + --prefix TEXT Add a prefix to the keys (default: None). One + prefix per band (e.g. 'band1 band2') + -b, --bands INTEGER List of bands to process + -a, --all Process all bands + --sigma TEXT Distance to the mean value (in sigma) in order + to produce a raster that highlights outliers. + -c, --chart TEXT Generate a chart per stat and per geometry + (x=timestamp of the input products / y=stat + value) and store it in the file defined by this + argument + -d, --display Display the chart + -gi, --geometry-index TEXT Name of the geometry index used for the chart + (default='ID') + --category_file TEXT File (raster or geometries) containing discrete + classes classifying the ROI. + --category_index TEXT Column name identifying categories in + categroy_file (only if file format is + geometries) + --category_names TEXT JSON files containing a dict with classes index + as keys and names to display classes as values. + -h, --help Show this message and exit. + When -g option is set with a valid geometries file, ``zonalstats`` generate a new vector file with the following metadata: diff --git a/src/eolab/rastertools/cli/filtering.py b/src/eolab/rastertools/cli/filtering.py index 2fb4ea6..7333ae0 100644 --- a/src/eolab/rastertools/cli/filtering.py +++ b/src/eolab/rastertools/cli/filtering.py @@ -4,9 +4,7 @@ CLI definition for the filtering tool """ from eolab.rastertools import Filtering -#import eolab.rastertools.main as main from eolab.rastertools.cli.utils_cli import apply_process -#from eolab.rastertools.main import rastertools #Import the click group named rastertools import click import os @@ -22,19 +20,24 @@ def create_filtering(output : str, window_size : int, pad : str, argsdict : dict Args: output (str): The path for the filtered output file. + window_size (int): Size of the processing window used by the filter. - pad (str): Padding method used for windowing (e.g., 'reflect', 'constant', etc.). + + pad (str): Padding method used for windowing (default : 'edge'). + argsdict (dict): Dictionary of additional filter configuration arguments. + filter (str): The filter type to apply (must be a valid name in `Filtering` filters). + bands (list): List of bands to process. If empty and `all_bands` is False, defaults to [1]. + kernel_size (int): Size of the kernel used by the filter. + all_bands (bool): Whether to apply the filter to all bands (True) or specific bands (False). Returns: :obj:`eolab.rastertools.Filtering`: A configured `Filtering` instance ready for execution. """ - - # get the bands to process if all_bands: bands = None @@ -53,11 +56,19 @@ def create_filtering(output : str, window_size : int, pad : str, argsdict : dict return tool +def filter_options(options : list): + def wrapper(function): + for option in options: + function = option(function) + return function + return wrapper + + inpt_arg = click.argument('inputs', type=str, nargs = -1, required = 1) ker_opt = click.option('--kernel_size', type=int, help="Kernel size of the filter function, e.g. 3 means a square" - "of 3x3 pixels on which the filter function is computed" - "(default: 8)") + " of 3x3 pixels on which the filter function is computed" + " (default: 8)") out_opt = click.option('-o', '--output', default = os.getcwd(), help="Output directory to store results (by default current directory)") @@ -72,183 +83,62 @@ def create_filtering(output : str, window_size : int, pad : str, argsdict : dict all_opt = click.option('-a', '--all','all_bands', type=bool, is_flag=True, help="Process all bands") -@click.group(name = "filter", context_settings=CONTEXT_SETTINGS) -@click.pass_context -def filter(ctx): - ''' - Apply a filter to a set of images. - ''' - ctx.ensure_object(dict) - - -#Median filter -@filter.command("median",context_settings=CONTEXT_SETTINGS) -@inpt_arg -@ker_opt -@out_opt -@win_opt -@pad_opt -@band_opt -@all_opt -@click.pass_context -def median(ctx, inputs : list, output : str, window_size : int, pad : str, kernel_size : int, bands : list, all_bands : bool) : - """ - Execute the median filter on the input files with the specified parameters. - - The filter works by sliding a window across the input raster and replacing each - pixel value with the median value of the pixels within that window. - - The `inputs` argument can either be a single file or a `.lst` file containing a list of input files. - - Arguments: - - inputs TEXT - - Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). - You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") that lists - the input files to process (one input file per line in .lst). - """ - # Configure the filter tool instance - tool = create_filtering( - output=output, - window_size=window_size, - pad=pad, - argsdict={"inputs": inputs}, - filter='median', - bands=bands, - kernel_size=kernel_size, - all_bands=all_bands) +sigma = click.option('--sigma', type=int, required = True, help="Standard deviation of the Gaussian distribution") - apply_process(ctx, tool, inputs) - -#Sum filter -@filter.command("sum",context_settings=CONTEXT_SETTINGS) -@inpt_arg -@ker_opt -@out_opt -@win_opt -@pad_opt -@band_opt -@all_opt +@click.group(context_settings=CONTEXT_SETTINGS) @click.pass_context -def sum(ctx, inputs : list, output : str, window_size : int, pad : str, kernel_size : int, bands : list, all_bands : bool) : - """ - Execute the sum filter on the input files with the specified parameters. - - The filter works by sliding a window across the input raster and replacing each - pixel value with the median value of the pixels within that window. - - The `inputs` argument can either be a single file or a `.lst` file containing a list of input files. - - Arguments: - - inputs TEXT - - Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). - You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") that lists - the input files to process (one input file per line in .lst). +def filter(ctx): """ - # Configure the filter tool instance - tool = create_filtering( - output=output, - window_size=window_size, - pad=pad, - argsdict={"inputs": inputs}, - filter='sum', - bands=bands, - kernel_size=kernel_size, - all_bands=all_bands) - - apply_process(ctx, tool, inputs) - -#Mean filter -@filter.command("mean",context_settings=CONTEXT_SETTINGS) -@inpt_arg -@ker_opt -@out_opt -@win_opt -@pad_opt -@band_opt -@all_opt -@click.pass_context -def mean(ctx, inputs : list, output : str, window_size : int, pad : str, kernel_size : int, bands : list, all_bands : bool) : + Apply a filter to a set of images. """ - Execute the mean filter on the input files with the specified parameters. + ctx.ensure_object(dict) - The filter works by sliding a window across the input raster and replacing each - pixel value with the median value of the pixels within that window. - The `inputs` argument can either be a single file or a `.lst` file containing a list of input files. +def create_filter(filter_name : str): - Arguments: + list_opt = [inpt_arg, ker_opt, out_opt, win_opt, pad_opt, band_opt, all_opt] + if filter_name == 'adaptive_gaussian': + list_opt.append(sigma) - inputs TEXT + @filter.command(filter_name, context_settings=CONTEXT_SETTINGS) + @filter_options(list_opt) + @click.pass_context + def filter_filtername(ctx, inputs : list, output : str, window_size : int, pad : str, kernel_size : int, bands : list, all_bands : bool, **kwargs): + """ + Execute the requested filter on the input files with the specified parameters. + The `inputs` argument can either be a single file or a `.lst` file containing a list of input files. - Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). - You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") that lists - the input files to process (one input file per line in .lst). - """ - # Configure the filter tool instance - tool = create_filtering( - output=output, - window_size=window_size, - pad=pad, - argsdict={"inputs": inputs}, - filter='mean', - bands=bands, - kernel_size=kernel_size, - all_bands=all_bands) - - apply_process(ctx, tool, inputs) - -#Adaptive gaussian filter -@filter.command("adaptive_gaussian",context_settings=CONTEXT_SETTINGS) -@inpt_arg -@ker_opt -@out_opt -@win_opt -@pad_opt -@band_opt -@all_opt -@click.option('--sigma', type = int, required = True, help = "Standard deviation of the Gaussian distribution") -@click.pass_context -def adaptive_gaussian(ctx, inputs : list, output : str, window_size : int, pad : str, sigma : int, kernel_size : int, bands : list, all_bands : bool) : - """ - Execute the adaptive gaussian filter on the input files with the specified parameters. + Arguments: - The filter works by sliding a window across the input raster and replacing each - pixel value with the median value of the pixels within that window. + inputs TEXT - The `inputs` argument can either be a single file or a `.lst` file containing a list of input files. + Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). + You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") that lists + the input files to process (one input file per line in .lst). + """ + argsdict = {"inputs": inputs} - Arguments: + if filter_name == 'adaptive_gaussian': + argsdict = {"sigma" : kwargs["sigma"]} - inputs TEXT - - Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). - You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") that lists - the input files to process (one input file per line in .lst). - """ - # Configure the filter tool instance - tool = create_filtering( + # Configure the filter tool instance + tool = create_filtering( output=output, window_size=window_size, pad=pad, - argsdict={"inputs": inputs, "sigma" : sigma}, - filter='adaptive_gaussian', + argsdict=argsdict, + filter=filter_name, bands=bands, kernel_size=kernel_size, all_bands=all_bands) - apply_process(ctx, tool, inputs) + apply_process(ctx, tool, inputs) -@filter.result_callback() -@click.pass_context -def handle_result(ctx): - if ctx.invoked_subcommand is None: - click.echo(ctx.get_help()) - ctx.exit() +median = create_filter("median") +mean = create_filter("mean") +sum = create_filter("sum") +adaptive_gaussian = create_filter("adaptive_gaussian") diff --git a/src/eolab/rastertools/cli/filtering_dyn.py b/src/eolab/rastertools/cli/filtering_dyn.py deleted file mode 100644 index 3c4057e..0000000 --- a/src/eolab/rastertools/cli/filtering_dyn.py +++ /dev/null @@ -1,151 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -CLI definition for the filtering tool -""" -from typing import Callable - -from eolab.rastertools import Filtering -#from eolab.rastertools.main import get_logger -from eolab.rastertools.cli.utils_cli import apply_process -#from eolab.rastertools.main import rastertools #Import the click group named rastertools -import click -import os - -CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) - - -def create_filtering(output : str, window_size : int, pad : str, argsdict : dict, filter : str, bands : list, kernel_size : int, all_bands : bool) -> Filtering: - """ - This function initializes a `Filtering` tool instance and configures it with specified settings. - - It selects the filter type, kernel size, output settings, and processing bands. If `all_bands` is set - to True, the filter will apply to all bands in the raster; otherwise, it applies only to specified bands. - - Args: - output (str): The path for the filtered output file. - window_size (int): Size of the processing window used by the filter. - pad (str): Padding method used for windowing (e.g., 'reflect', 'constant', etc.). - argsdict (dict): Dictionary of additional filter configuration arguments. - filter (str): The filter type to apply (must be a valid name in `Filtering` filters). - bands (list): List of bands to process. If empty and `all_bands` is False, defaults to [1]. - kernel_size (int): Size of the kernel used by the filter. - all_bands (bool): Whether to apply the filter to all bands (True) or specific bands (False). - - Returns: - :obj:`eolab.rastertools.Filtering`: A configured `Filtering` instance ready for execution. - """ - # get the bands to process - if all_bands: - bands = None - else: - bands = list(map(int, bands)) if bands else [1] - - # create the rastertool object - raster_filters_dict = {rf.name: rf for rf in Filtering.get_default_filters()} - tool = Filtering(raster_filters_dict[filter], kernel_size, bands) - - # set up config with args values - tool.with_output(output) \ - .with_windows(window_size, pad) \ - .with_filter_configuration(argsdict) - - return tool - - -def filter_options(options : list): - def wrapper(function): - for option in options: - function = option(function) - return function - return wrapper - - -inpt_arg = click.argument('inputs', type=str, nargs = -1, required = 1) - -ker_opt = click.option('--kernel_size', type=int, help="Kernel size of the filter function, e.g. 3 means a square" - "of 3x3 pixels on which the filter function is computed" - "(default: 8)") - -out_opt = click.option('-o', '--output', default = os.getcwd(), help="Output directory to store results (by default current directory)") - -win_opt = click.option('-ws', '--window_size', type=int, default = 1024, help="Size of tiles to distribute processing, default: 1024") - -pad_opt = click.option('-p','--pad',default="edge", type=click.Choice(['none','edge','maximum','mean','median','minimum','reflect','symmetric','wrap']), - help="Pad to use around the image, default : edge" - "(see https://numpy.org/doc/stable/reference/generated/numpy.pad.html" - "for more information)") - -band_opt = click.option('-b','--bands', multiple = True, type=int, help="List of bands to process") - -all_opt = click.option('-a', '--all','all_bands', type=bool, is_flag=True, help="Process all bands") - -sigma = click.option('--sigma', type=int, required = True, help="Standard deviation of the Gaussian distribution") - -@click.group(context_settings=CONTEXT_SETTINGS) -@click.pass_context -def filter(ctx): - """ - Apply a filter to a set of images. - """ - ctx.ensure_object(dict) - - -def create_filter(filter_name : str): - - list_opt = [inpt_arg, ker_opt, out_opt, win_opt, pad_opt, band_opt, all_opt] - if filter_name == 'adaptive_gaussian': - list_opt.append(sigma) - - @filter.command(filter_name, context_settings=CONTEXT_SETTINGS) - @filter_options(list_opt) - @click.pass_context - def filter_filtername(ctx, inputs : list, output : str, window_size : int, pad : str, kernel_size : int, bands : list, all_bands : bool, **kwargs): - """ - Execute the requested filter on the input files with the specified parameters. - The `inputs` argument can either be a single file or a `.lst` file containing a list of input files. - - Arguments: - - inputs TEXT - - Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). - You can provide a single file with extension \".lst\" (e.g. \"filtering.lst\") that lists - the input files to process (one input file per line in .lst). - """ - argsdict = {"inputs": inputs} - - if filter_name == 'adaptive_gaussian': - argsdict = {"sigma" : kwargs["sigma"]} - - # Configure the filter tool instance - tool = create_filtering( - output=output, - window_size=window_size, - pad=pad, - argsdict=argsdict, - filter=filter_name, - bands=bands, - kernel_size=kernel_size, - all_bands=all_bands) - - apply_process(ctx, tool, inputs) - - -median = create_filter("median") -mean = create_filter("mean") -sum = create_filter("sum") -adaptive_gaussian = create_filter("adaptive_gaussian") - -@filter.result_callback() -@click.pass_context -def handle_result(ctx): - if ctx.invoked_subcommand is None: - click.echo(ctx.get_help()) - ctx.exit() - - - - - - diff --git a/src/eolab/rastertools/cli/hillshade.py b/src/eolab/rastertools/cli/hillshade.py index 96588cc..5ad0c0d 100644 --- a/src/eolab/rastertools/cli/hillshade.py +++ b/src/eolab/rastertools/cli/hillshade.py @@ -16,13 +16,13 @@ @click.argument('inputs', type=str, nargs = -1, required = 1) @click.option('--elevation', type=float, required = True, help="Elevation of the sun in degrees, [0°, 90°] where" - "90°=zenith and 0°=horizon") + " 90°=zenith and 0°=horizon") @click.option('--azimuth', type=float, required = True, help="Azimuth of the sun in degrees, [0°, 360°] where" - "0°=north, 90°=east, 180°=south and 270°=west") + " 0°=north, 90°=east, 180°=south and 270°=west") @click.option('--radius', type=int, help="Maximum distance (in pixels) around a point to evaluate" - "horizontal elevation angle. If not set, it is automatically computed from" + " horizontal elevation angle. If not set, it is automatically computed from" " the range of altitudes in the digital model.") @click.option('--resolution', required = True, type=float, help="Pixel resolution in meter") @@ -33,39 +33,24 @@ @click.option('-p', '--pad',default="edge", type=click.Choice(['none','edge','maximum','mean','median','minimum','reflect','symmetric','wrap']), help="Pad to use around the image, default : edge" - "(see https://numpy.org/doc/stable/reference/generated/numpy.pad.html" - "for more information)") + " (see https://numpy.org/doc/stable/reference/generated/numpy.pad.html" + " for more information)") @click.pass_context def hillshade(ctx, inputs : list, elevation : float, azimuth : float, radius : int, resolution : float, output : str, window_size : int, pad : str) : """ - CHANGE DOCSTRING - Adds the hillshade subcommand to the given rastertools subparser + Execute the hillshade subcommand on a Digital Height Model (DHM) using the given solar + parameters (elevation, azimuth), resolution, and optional parameters for processing the raster. Arguments: - inputs TEXT + inputs TEXT - Input file to process (i.e. geotiff corresponding to a + Input file to process (i.e. geotiff corresponding to a Digital Height Model). You can provide a single file - with extension ".lst" (e.g. "filtering.lst") that + with extension ".lst" (e.g. "hillshade.lst") that lists the input files to process (one input file per line in .lst) - - Args: - rastertools_parsers: - The rastertools subparsers to which this subcommand shall be added. - - This argument provides from a code like this:: - - import argparse - main_parser = argparse.ArgumentParser() - rastertools_parsers = main_parser.add_subparsers() - hillshade.create_argparser(rastertools_parsers) - - Returns: - The rastertools subparsers updated with this subcommand """ - # create the rastertool object tool = Hillshade(elevation, azimuth, resolution, radius) diff --git a/src/eolab/rastertools/cli/radioindice.py b/src/eolab/rastertools/cli/radioindice.py index e7f038b..eec3d53 100644 --- a/src/eolab/rastertools/cli/radioindice.py +++ b/src/eolab/rastertools/cli/radioindice.py @@ -19,11 +19,16 @@ def indices_opt(function): - list_indices = ['--ndvi', '--tndvi', '--rvi', '--pvi', '--savi', '--tsavi', '--msavi', '--msavi2', '--ipvi', - '--evi', '--ndwi', '--ndwi2', '--mndwi', '--ndpi', '--ndti', '--ndbi', '--ri', '--bi', '--bi2'] + """ + Create options for all the possible indices + """ + dict_indices = {'--ndvi' : "ndvi", '--tndvi' : "tndvi", '--rvi' : "rvi", '--pvi' : "pvi", '--savi' : "savi", '--tsavi' : "tsavi", + '--msavi' : "msavi", '--msavi2' : "msavi2", '--ipvi' : "ipvi", + '--evi' : "evi", '--ndwi' : "ndwi", '--ndwi2' : "ndwi2", '--mndwi' : "mndwi", + '--ndpi' : "ndpi", '--ndti' : "ndti", '--ndbi' : "ndbi", '--ri' : "ri", '--bi' : "bi", '--bi2' : "bi2"} - for idc in list_indices: - function = click.option(idc, is_flag=True, help=f"Compute {id} indice")(function) + for idc in dict_indices.keys(): + function = click.option(idc, is_flag=True, help=f"Compute " + dict_indices[idc] + " indice")(function) return function @@ -40,7 +45,7 @@ def indices_opt(function): @click.option('-ws', '--window_size', type=int, default = 1024, help="Size of tiles to distribute processing, default: 1024") @click.option('-i', '--indices', type=str, multiple = True, - help=" List of indices to computePossible indices are: bi, bi2, evi, ipvi, mndwi, msavi, msavi2, ndbi, ndpi," + help=" List of indices to compute. Possible indices are: bi, bi2, evi, ipvi, mndwi, msavi, msavi2, ndbi, ndpi," " ndti, ndvi, ndwi, ndwi2, pvi, ri, rvi, savi, tndvi, tsavi") @@ -49,20 +54,28 @@ def indices_opt(function): @click.option('-nd', '--normalized_difference','nd',type=str, multiple=True, nargs=2, metavar="band1 band2", help="Compute the normalized difference of two bands defined" - "as parameter of this option, e.g. \"-nd red nir\" will compute (red-nir)/(red+nir). " + " as parameter of this option, e.g. \"-nd red nir\" will compute (red-nir)/(red+nir). " "See eolab.rastertools.product.rastertype.BandChannel for the list of bands names. " "Several nd options can be set to compute several normalized differences.") @click.pass_context -def radioindice(ctx, inputs : list, output : str, indices : list, merge : bool, roi : str, window_size : int, nd : bool, **kwargs) : - """Create and configure a new rastertool "Radioindice" according to argparse args +def radioindice(ctx, inputs : list, output : str, indices : list, merge : bool, roi : str, window_size : int, nd : str, **kwargs) : + """ + Compute the requested radio indices on raster data. + + This command computes various vegetation and environmental indices on satellite or raster data based on the + provided input images and options. The tool can compute specific indices, merge the results into one image, + compute normalized differences between bands, and apply processing using a region of interest (ROI) and specified + tile/window size. + + Arguments: - Args: - args: args extracted from command line + inputs TEXT - Returns: - :obj:`eolab.rastertools.Radioindice`: The configured rastertool to run + Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). + You can provide a single file with extension \".lst\" (e.g. \"radioindice.lst\") that lists + the input files to process (one input file per line in .lst). """ indices_opt = [key for key, value in kwargs.items() if value] indices_to_compute = [] @@ -82,15 +95,15 @@ def radioindice(ctx, inputs : list, output : str, indices : list, merge : bool, sys.exit(2) if nd: - for nd in nd: - if nd[0] in BandChannel.__members__ and nd[1] in BandChannel.__members__: - channel1 = BandChannel[nd[0]] - channel2 = BandChannel[nd[1]] - new_indice = RadioindiceProcessing(f"nd[{nd[0]}-{nd[1]}]").with_channels( + for nd_bands in nd: + if nd_bands[0] in BandChannel.__members__ and nd_bands[1] in BandChannel.__members__: + channel1 = BandChannel[nd_bands[0]] + channel2 = BandChannel[nd_bands[1]] + new_indice = RadioindiceProcessing(f"nd[{nd_bands[0]}-{nd_bands[1]}]").with_channels( [channel2, channel1]) indices_to_compute.append(new_indice) else: - _logger.exception(RastertoolConfigurationException(f"Invalid band(s) in normalized difference: {nd[0]} and/or {nd[1]}")) + _logger.exception(RastertoolConfigurationException(f"Invalid band(s) in normalized difference: {nd_bands[0]} and/or {nd_bands[1]}")) sys.exit(2) # handle special case: no indice setup diff --git a/src/eolab/rastertools/cli/speed.py b/src/eolab/rastertools/cli/speed.py index 2977811..e3243b7 100644 --- a/src/eolab/rastertools/cli/speed.py +++ b/src/eolab/rastertools/cli/speed.py @@ -24,14 +24,19 @@ @click.pass_context def speed(ctx, inputs : list, bands : list, all_bands : bool, output : str) : """ - CHANGE DOCSTRING - Create and configure a new rastertool "Speed" according to argparse args + Compute the speed of radiometric values for multiple raster images. - Args: - args: args extracted from command line + This command calculates the speed of radiometric values for raster data, + optionally processing specific bands or all bands from the input images. + The results are saved to a specified output directory. - Returns: - :obj:`eolab.rastertools.Speed`: The configured rastertool to run + Arguments: + + inputs TEXT + + Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). + You can provide a single file with extension \".lst\" (e.g. \"speed.lst\") that lists + the input files to process (one input file per line in .lst). """ # get the bands to process diff --git a/src/eolab/rastertools/cli/svf.py b/src/eolab/rastertools/cli/svf.py index 6290d4a..ff16eb6 100644 --- a/src/eolab/rastertools/cli/svf.py +++ b/src/eolab/rastertools/cli/svf.py @@ -31,21 +31,25 @@ @click.option('-p', '--pad',default="edge", type=click.Choice(['none','edge','maximum','mean','median','minimum','reflect','symmetric','wrap']), help="Pad to use around the image, default : edge" "(see https://numpy.org/doc/stable/reference/generated/numpy.pad.html" - "for more information)") + " for more information)") @click.pass_context def svf(ctx, inputs : list, radius : int, directions : int, resolution : float, altitude : int, output : str, window_size : int, pad : str) : """ - CHANGE DOCSTRING + Compute the Sky View Factor (SVF) of a Digital Height Model (DHM). - ADD INPUTS - Create and configure a new rastertool "Speed" according to argparse args + The Sky View Factor (SVF) is a measure of the visibility of the sky from a point in a Digital Height Model + (DHM). It is calculated by evaluating the horizontal elevation angle from a given point in multiple + directions (as specified by the user), and is influenced by the topography and surrounding terrain features. - Args: - args: args extracted from command line + Arguments: - Returns: - :obj:`eolab.rastertools.Speed`: The configured rastertool to run + inputs TEXT + + Input file to process (i.e. geotiff corresponding to a + Digital Height Model). You can provide a single file + with extension ".lst" (e.g. "svf.lst") that + lists the input files to process (one input file per line in .lst) """ # create the rastertool object tool = SVF(directions, radius, resolution) diff --git a/src/eolab/rastertools/cli/tiling.py b/src/eolab/rastertools/cli/tiling.py index fa6b25a..41ef1e7 100644 --- a/src/eolab/rastertools/cli/tiling.py +++ b/src/eolab/rastertools/cli/tiling.py @@ -11,7 +11,7 @@ CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) -#Speed command +#Tiling command @click.command("tiling",context_settings=CONTEXT_SETTINGS) @click.argument('inputs', type=str, nargs = -1, required = 1) @@ -20,51 +20,49 @@ @click.option('--id_col','id_column', type = str, help="Name of the column in the grid" " file used to number the tiles. When ids are defined, this argument is required" - "to identify which column corresponds to the define ids") + " to identify which column corresponds to the define ids") @click.option('--id', type=int, multiple = True, help="Tiles ids of the grid to export as new tile, default all") @click.option('-o','--output', default = os.getcwd(), help="Output directory to store results (by default current directory)") @click.option('-n','--name','output_name', default="{}_tile{}", help="Basename for the output raster tiles, default:" - "\"{}_tile{}\". The basename must be defined as a formatted string where tile index is at position 1" + " \"{}_tile{}\". The basename must be defined as a formatted string where tile index is at position 1" " and original filename is at position 0. For instance, tile{1}.tif will generate the filename" - "tile75.tif for the tile id = 75") + " tile75.tif for the tile id = 75") @click.option('-d','--dir','subdir_name', help="When each tile must be generated in a different" - "subdirectory, it defines the naming convention for the subdirectory. It is a formatted string with one positional" - "parameter corresponding to the tile index. For instance, tile{} will generate the subdirectory name tile75/" - "for the tile id = 75. By default, subdirectory is not defined and output files will be generated directly in" - "the output directory") + " subdirectory, it defines the naming convention for the subdirectory. It is a formatted string with one positional" + " parameter corresponding to the tile index. For instance, tile{} will generate the subdirectory name tile75/" + " for the tile id = 75. By default, subdirectory is not defined and output files will be generated directly in" + " the output directory") @click.pass_context def tiling(ctx, inputs : list, grid_file : str, id_column : str, id : list, output : str, output_name : str, subdir_name : str) : """ - CHANGE DOCSTRING + Generate tiles of an input raster image following the geometries defined by a given grid. - ADD INPUTS - Create and configure a new rastertool "Tiling" according to argparse args + The tiling command divides a raster image into smaller tiles based on a grid defined in a vector-based spatial + data file. Each tile corresponds to a specific area within the grid, and tiles can be saved using a customizable + naming convention and optionally placed in subdirectories based on their tile ID. - Generate tiles of an input raster image following the geometries defined by a - given grid + Arguments: + inputs TEXT - Args: - args: args extracted from command line - - Returns: - :obj:`eolab.rastertools.Tiling`: The configured rastertool to run + Raster files to process. You can provide a single file with extension ".lst" (e.g. "tiling.lst") that lists + the input files to process (one input file per line in .lst) """ if id == () : - id = None + id_ = None else: - id = list(id) + id_ = list(id) # create the rastertool object tool = Tiling(grid_file) # set up config with args values tool.with_output(output, output_name, subdir_name) - tool.with_id_column(id_column, id) + tool.with_id_column(id_column, id_) apply_process(ctx, tool, inputs) diff --git a/src/eolab/rastertools/cli/timeseries.py b/src/eolab/rastertools/cli/timeseries.py index 78465ee..61e11b8 100644 --- a/src/eolab/rastertools/cli/timeseries.py +++ b/src/eolab/rastertools/cli/timeseries.py @@ -39,27 +39,20 @@ def timeseries(ctx, inputs : list, bands : list, all_bands : bool, output : str, start_date : str, end_date : str, time_period : int, window_size : int) : """ - Create and configure a new rastertool "Timeseries" according to argparse args - CHANGE DOCSTRING - Adds the timeseries subcommand to the given rastertools subparser + Generate a timeseries of images (without gaps) from a set of input images. + Data not present in the input images (e.g., missing images for specific dates or masked data) are interpolated + (with linear interpolation) so that all gaps in the timeseries are filled. - Temporal gap filling of an image time series - Generate a timeseries of images (without gaps) from a set of input images. " - "Data not present in the input images (no image for the date or masked data) " - "are interpolated (with linear interpolation) so that all gaps are filled.", - epilog="By default only first band is computed. + This command is useful for generating continuous timeseries data, even when some input images are missing + or contain masked values. + Arguments: - ADD INPUTS + inputs TEXT - INPUTS - - Input files to process (e.g. Sentinel2 L2A MAJA from THEIA). - You can provide a single file with extension .lst (e.g. speed.lst) - that lists the input files to process (one input file per line in .lst)) - - Args: - args: args extracted from command line + Input file to process (e.g. Sentinel2 L2A MAJA from THEIA). + You can provide a single file with extension \".lst\" (e.g. \"speed.lst\") that lists + the input files to process (one input file per line in .lst). """ # get the bands to process if all_bands: diff --git a/src/eolab/rastertools/cli/utils_cli.py b/src/eolab/rastertools/cli/utils_cli.py index cc72dc5..bf930e1 100644 --- a/src/eolab/rastertools/cli/utils_cli.py +++ b/src/eolab/rastertools/cli/utils_cli.py @@ -47,31 +47,36 @@ def apply_process(ctx, tool, inputs : list): """ Apply the chosen process to a set of input files. - This function extracts input files, configures the tool, and processes the files - through the specified tool. It also handles debug settings and intermediate file storage - (VRT files). In case of any errors, the function logs the exception and terminates the process - with an appropriate exit code. + This function extracts input files from a provided list or direct paths, configures the specified tool, + and processes the files through the given tool. Additionally, it handles debug settings (such as storing + intermediate VRT files) and manages any exceptions during the process. If an error occurs, the function + logs the exception and terminates the process with an appropriate exit code. Args: - ctx (click.Context): The context object containing configuration options like whether - to store intermediate VRT files. - tool (Filtering or Hillshade or ...): The tool instance that has been configured with the provided parameters. - inputs (str): A path to a list of input files, either as a single `.lst` file or a direct - list of file paths. + ctx (click.Context): + The context object that contains configuration options. + + tool (Filtering or Hillshade or any raster processing tool): + The configured tool instance that will apply the process to the input files. The tool should have + been properly set up with parameters. + + inputs (list or str): + A list of input file paths or a path to a `.lst` file containing a list of input file paths. + This argument specifies the files that the tool will process. Raises: - RastertoolConfigurationException: If there is a configuration error with the tool. - Exception: Any other errors that occur during processing. + RastertoolConfigurationException: + If there is a configuration error with the tool or its parameters. + + Exception: + Any other exceptions that occur during the processing of the input files. """ try: - print('@' * 50) # handle the input file of type "lst" inputs_extracted = _extract_files_from_list(inputs) - print('@' * 50) # setup debug mode in which intermediate VRT files are stored to disk or not tool.with_vrt_stored(ctx.obj.get('keep_vrt')) - print('@' * 50) # launch process tool.process_files(inputs_extracted) @@ -79,13 +84,10 @@ def apply_process(ctx, tool, inputs : list): _logger.info("Done!") except RastertoolConfigurationException as rce: - print('@'*50) _logger.exception(rce) sys.exit(2) except Exception as err: - print('!' * 50) _logger.exception(err) sys.exit(1) - print('?' * 50) sys.exit(0) \ No newline at end of file diff --git a/src/eolab/rastertools/cli/zonalstats.py b/src/eolab/rastertools/cli/zonalstats.py index d1d30d9..635e780 100644 --- a/src/eolab/rastertools/cli/zonalstats.py +++ b/src/eolab/rastertools/cli/zonalstats.py @@ -60,22 +60,20 @@ @click.pass_context def zonalstats(ctx, inputs : list, output : str, output_format : str, geometries : str, within : str, stats : list, categorical : bool, valid_threshold : float ,area : bool, prefix, bands : list, all_bands : bool, sigma, chartfile, display : bool, geom_index : str, category_file : str, category_index : str, category_names : str) : """ - Compute zonal statistics - Compute zonal statistics of a raster image.\n Available statistics are: - min max range mean std percentile_x (x in [0, 100]) median mad - count valid nodata sum majority minority unique - By default only first band is computed. + Compute zonal statistics of a raster image. - Create and configure a new rastertool "Zonalstats" according to argparse args + Available statistics are: + min, max, range, mean, std, percentile_x (x in [0, 100]), median, mad, count, valid, nodata, sum, majority, minority, unique. - Args: - args: args extracted from command line + By default, only the first band is computed unless specified otherwise. - Returns: - :obj:`eolab.rastertools.Zonalstats`: The configured rastertool to run + Arguments: + + inputs TEXT + + Raster files to process. You can provide a single filewith extension ".lst" (e.g. "zonalstats.lst") that + lists the input files to process (one input file per line in .lst) """ - print(output) - print(output_format) # get and check the list of stats to compute if stats: stats_to_compute = list(stats) diff --git a/src/eolab/rastertools/main.py b/src/eolab/rastertools/main.py index 5071e20..f891384 100644 --- a/src/eolab/rastertools/main.py +++ b/src/eolab/rastertools/main.py @@ -16,7 +16,7 @@ import sys import json import click -from eolab.rastertools.cli.filtering_dyn import filter +from eolab.rastertools.cli.filtering import filter from eolab.rastertools.cli.hillshade import hillshade from eolab.rastertools.cli.speed import speed from eolab.rastertools.cli.svf import svf @@ -127,6 +127,8 @@ def add_custom_rastertypes(rastertypes): """ RasterType.add(rastertypes) + +#Rastertools CLI group CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) @click.group(context_settings=CONTEXT_SETTINGS) @@ -162,12 +164,12 @@ def add_custom_rastertypes(rastertypes): help="set loglevel to DEBUG") @click.version_option(version='rastertools {}'.format(__version__)) # Ensure __version__ is defined @click.pass_context + def rastertools(ctx, rastertype : str, max_workers : int, keep_vrt : bool, verbose : bool, very_verbose : bool): """ Main entry point for the `rastertools` Command Line Interface. - The `rastertools` CLI provides tools for raster processing - and analysis and allows configurable data handling, parallel processing, + The `rastertools` CLI provides tools for raster processing and analysis and allows configurable data handling, parallel processing, and debugging support. Logging: @@ -182,6 +184,7 @@ def rastertools(ctx, rastertype : str, max_workers : int, keep_vrt : bool, verbo - `RASTERTOOLS_MAXWORKERS`: If `max_workers` is set, it defines the max workers for rastertools. """ + #Saving keep_vrt to use it in the subcommands ctx.ensure_object(dict) ctx.obj['keep_vrt'] = keep_vrt @@ -225,18 +228,6 @@ def rastertools(ctx, rastertype : str, max_workers : int, keep_vrt : bool, verbo rastertools.add_command(zonalstats, name = "zs") rastertools.add_command(zonalstats, name = "zonalstats") -CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) - - -#Speed command -@click.command("ema",context_settings=CONTEXT_SETTINGS) -@click.option('--inputs', type=int) -@click.pass_context -def ema(ctx, inputs) : - raise Exception(f"coucou {inputs}") - -rastertools.add_command(ema, name = "ema") - @rastertools.result_callback() @click.pass_context @@ -246,7 +237,8 @@ def handle_result(ctx): ctx.exit() def run(*args, **kwargs): - """Entry point for console_scripts + """ + Entry point for console_scripts """ rastertools(*args, **kwargs) From c473d1a198a4d22c1c4746e45ff8addacd9f0eee Mon Sep 17 00:00:00 2001 From: cadauxe Date: Thu, 14 Nov 2024 15:11:44 +0100 Subject: [PATCH 07/17] refactor: began setup.py refactor: cleaned code --- pyproject.toml | 8 --- setup.py | 38 +++++++++- src/eolab/rastertools/cli/filtering.py | 13 +--- src/eolab/rastertools/cli/hillshade.py | 10 +-- src/eolab/rastertools/cli/radioindice.py | 4 +- src/eolab/rastertools/cli/speed.py | 7 +- src/eolab/rastertools/cli/svf.py | 10 +-- src/eolab/rastertools/cli/timeseries.py | 11 ++- src/eolab/rastertools/cli/utils_cli.py | 11 +++ src/eolab/rastertools/cli/zonalstats.py | 8 +-- src/eolab/rastertools/main.py | 4 ++ src/eolab/rastertools/processing/algo.py | 18 +---- src/eolab/rastertools/zonalstats.py | 1 - tests/test_radioindice.py | 35 ++++----- tests/test_rasterproc.py | 5 +- tests/test_rasterproduct.py | 90 ++++++++++++------------ tests/test_rastertools.py | 11 +-- tests/test_rastertype.py | 4 +- tests/test_speed.py | 11 +-- tests/test_stats.py | 21 +++--- tests/test_tiling.py | 9 +-- tests/test_vector.py | 73 +++++++++---------- tests/test_zonalstats.py | 75 ++++++++++---------- tests/utils4test.py | 17 ++--- 24 files changed, 248 insertions(+), 246 deletions(-) delete mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 2c63dbb..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,8 +0,0 @@ -[build-system] -# AVOID CHANGING REQUIRES: IT WILL BE UPDATED BY PYSCAFFOLD! -requires = ["setuptools>=46.1.0", "setuptools_scm[toml]>=5", "wheel"] -build-backend = "setuptools.build_meta" - -[tool.setuptools_scm] -# See configuration details in https://github.com/pypa/setuptools_scm -version_scheme = "no-guess-dev" diff --git a/setup.py b/setup.py index 3aafee7..34927a8 100644 --- a/setup.py +++ b/setup.py @@ -7,11 +7,45 @@ PyScaffold helps you to put up the scaffold of your new Python project. Learn more under: https://pyscaffold.org/ """ -from setuptools import setup +import os +from setuptools import setup, find_packages +from sphinx.builders.html import setup_resource_paths + +with open('src/eolab/rastertools/__init__.py') as f: + for line in f: + if line.find("__version__") >= 0: + version = line.split("=")[1].strip() + version = version.strip('"') + version = version.strip("'") + break + if __name__ == "__main__": try: - setup(use_scm_version={"version_scheme": "no-guess-dev"}) + setup(name='rastertools', + version=version, + description=u"Collection of tools for raster data", + long_description="", + classifiers=[], + keywords='', + author=u"Olivier Queyrut", + author_email="", + url="https://github.com/CNES/rastertools", + packages=find_packages(exclude=['tests']), + include_package_data=True, + zip_safe=False, + setup_requires = ["setuptools_scm"], + install_requires=[ + 'click>=4.0', + 'rasterio>=1.2.0', + ], + extras_require={ + 'test': ['pytest>=3.6'], + }, + entry_points=""" + """, + python_requires='>=3.9', + use_scm_version={"version_scheme": "no-guess-dev"}) except: # noqa print( "\n\nAn error occurred while building the project, " diff --git a/src/eolab/rastertools/cli/filtering.py b/src/eolab/rastertools/cli/filtering.py index 7333ae0..8c01b61 100644 --- a/src/eolab/rastertools/cli/filtering.py +++ b/src/eolab/rastertools/cli/filtering.py @@ -4,7 +4,7 @@ CLI definition for the filtering tool """ from eolab.rastertools import Filtering -from eolab.rastertools.cli.utils_cli import apply_process +from eolab.rastertools.cli.utils_cli import apply_process, pad_opt, win_opt, all_opt, band_opt import click import os @@ -72,17 +72,6 @@ def wrapper(function): out_opt = click.option('-o', '--output', default = os.getcwd(), help="Output directory to store results (by default current directory)") -win_opt = click.option('-ws', '--window_size', type=int, default = 1024, help="Size of tiles to distribute processing, default: 1024") - -pad_opt = click.option('-p','--pad',default="edge", type=click.Choice(['none','edge','maximum','mean','median','minimum','reflect','symmetric','wrap']), - help="Pad to use around the image, default : edge" - "(see https://numpy.org/doc/stable/reference/generated/numpy.pad.html" - "for more information)") - -band_opt = click.option('-b','--bands', multiple = True, type=int, help="List of bands to process") - -all_opt = click.option('-a', '--all','all_bands', type=bool, is_flag=True, help="Process all bands") - sigma = click.option('--sigma', type=int, required = True, help="Standard deviation of the Gaussian distribution") @click.group(context_settings=CONTEXT_SETTINGS) diff --git a/src/eolab/rastertools/cli/hillshade.py b/src/eolab/rastertools/cli/hillshade.py index 5ad0c0d..f4786bc 100644 --- a/src/eolab/rastertools/cli/hillshade.py +++ b/src/eolab/rastertools/cli/hillshade.py @@ -4,7 +4,7 @@ CLI definition for the hillshade tool """ from eolab.rastertools import Hillshade -from eolab.rastertools.cli.utils_cli import apply_process +from eolab.rastertools.cli.utils_cli import apply_process, pad_opt, win_opt import click import os @@ -29,12 +29,8 @@ @click.option('-o','--output', default = os.getcwd(), help="Output directory to store results (by default current directory)") -@click.option('-ws', '--window_size', type=int, default = 1024, help="Size of tiles to distribute processing, default: 1024") - -@click.option('-p', '--pad',default="edge", type=click.Choice(['none','edge','maximum','mean','median','minimum','reflect','symmetric','wrap']), - help="Pad to use around the image, default : edge" - " (see https://numpy.org/doc/stable/reference/generated/numpy.pad.html" - " for more information)") +@win_opt +@pad_opt @click.pass_context def hillshade(ctx, inputs : list, elevation : float, azimuth : float, radius : int, resolution : float, output : str, window_size : int, pad : str) : diff --git a/src/eolab/rastertools/cli/radioindice.py b/src/eolab/rastertools/cli/radioindice.py index eec3d53..76c3a71 100644 --- a/src/eolab/rastertools/cli/radioindice.py +++ b/src/eolab/rastertools/cli/radioindice.py @@ -6,7 +6,7 @@ import logging from eolab.rastertools import RastertoolConfigurationException, Radioindice -from eolab.rastertools.cli.utils_cli import apply_process +from eolab.rastertools.cli.utils_cli import apply_process, win_opt from eolab.rastertools.product import BandChannel from eolab.rastertools.processing import RadioindiceProcessing import sys @@ -42,7 +42,7 @@ def indices_opt(function): @click.option('-r', '--roi', type= str, help="Region of interest in the input image (vector)") -@click.option('-ws', '--window_size', type=int, default = 1024, help="Size of tiles to distribute processing, default: 1024") +@win_opt @click.option('-i', '--indices', type=str, multiple = True, help=" List of indices to compute. Possible indices are: bi, bi2, evi, ipvi, mndwi, msavi, msavi2, ndbi, ndpi," diff --git a/src/eolab/rastertools/cli/speed.py b/src/eolab/rastertools/cli/speed.py index e3243b7..5628fb5 100644 --- a/src/eolab/rastertools/cli/speed.py +++ b/src/eolab/rastertools/cli/speed.py @@ -4,7 +4,7 @@ CLI definition for the speed tool """ from eolab.rastertools import Speed -from eolab.rastertools.cli.utils_cli import apply_process +from eolab.rastertools.cli.utils_cli import apply_process, band_opt, all_opt import click import os @@ -15,9 +15,8 @@ @click.command("speed",context_settings=CONTEXT_SETTINGS) @click.argument('inputs', type=str, nargs = -1, required = 1) -@click.option('-b','--bands', type=int, multiple = True, help="List of bands to process") - -@click.option('-a', '--all','all_bands', type=bool, is_flag=True, help="Process all bands") +@band_opt +@all_opt @click.option('-o','--output', default = os.getcwd(), help="Output directory to store results (by default current directory)") diff --git a/src/eolab/rastertools/cli/svf.py b/src/eolab/rastertools/cli/svf.py index ff16eb6..2301094 100644 --- a/src/eolab/rastertools/cli/svf.py +++ b/src/eolab/rastertools/cli/svf.py @@ -4,7 +4,7 @@ CLI definition for the SVF (Sky View Factor) tool """ from eolab.rastertools import SVF -from eolab.rastertools.cli.utils_cli import apply_process +from eolab.rastertools.cli.utils_cli import apply_process, pad_opt, win_opt import click import os @@ -26,12 +26,8 @@ @click.option('-o','--output', default = os.getcwd(), help="Output directory to store results (by default current directory)") -@click.option('-ws', '--window_size', type=int, default = 1024, help="Size of tiles to distribute processing, default: 1024") - -@click.option('-p', '--pad',default="edge", type=click.Choice(['none','edge','maximum','mean','median','minimum','reflect','symmetric','wrap']), - help="Pad to use around the image, default : edge" - "(see https://numpy.org/doc/stable/reference/generated/numpy.pad.html" - " for more information)") +@win_opt +@pad_opt @click.pass_context def svf(ctx, inputs : list, radius : int, directions : int, resolution : float, altitude : int, output : str, window_size : int, pad : str) : diff --git a/src/eolab/rastertools/cli/timeseries.py b/src/eolab/rastertools/cli/timeseries.py index 61e11b8..b6a4815 100644 --- a/src/eolab/rastertools/cli/timeseries.py +++ b/src/eolab/rastertools/cli/timeseries.py @@ -6,7 +6,7 @@ from datetime import datetime from eolab.rastertools import Timeseries -from eolab.rastertools.cli.utils_cli import apply_process +from eolab.rastertools.cli.utils_cli import apply_process, win_opt, all_opt, band_opt from eolab.rastertools import RastertoolConfigurationException import click import sys @@ -20,10 +20,6 @@ @click.command("timeseries",context_settings=CONTEXT_SETTINGS) @click.argument('inputs', type=str, nargs = -1, required = 1) -@click.option('-b','--bands', type=list, help="List of bands to process") - -@click.option('-a', '--all','all_bands', type=bool, is_flag=True, help="Process all bands") - @click.option('-o','--output', default = os.getcwd(), help="Output directory to store results (by default current directory)") @click.option("-s","--start_date", help="Start date of the timeseries to generate in the following format: yyyy-MM-dd") @@ -33,8 +29,9 @@ @click.option("-p", "--time_period",type=int, help="Time period (number of days) between two consecutive images in the timeseries " "to generate e.g. 10 = generate one image every 10 days") -@click.option('-ws', '--window_size', type=int, default = 1024, help="Size of tiles to distribute processing, default: 1024") - +@band_opt +@all_opt +@win_opt @click.pass_context def timeseries(ctx, inputs : list, bands : list, all_bands : bool, output : str, start_date : str, end_date : str, time_period : int, window_size : int) : diff --git a/src/eolab/rastertools/cli/utils_cli.py b/src/eolab/rastertools/cli/utils_cli.py index bf930e1..18d116a 100644 --- a/src/eolab/rastertools/cli/utils_cli.py +++ b/src/eolab/rastertools/cli/utils_cli.py @@ -6,6 +6,17 @@ #TO DO _logger = logging.getLogger(__name__) +all_opt = click.option('-a', '--all','all_bands', type=bool, is_flag=True, help="Process all bands") + +band_opt = click.option('-b','--bands', type=int, multiple = True, help="List of bands to process") + +win_opt = click.option('-ws', '--window_size', type=int, default = 1024, help="Size of tiles to distribute processing, default: 1024") + +pad_opt = click.option('-p', '--pad',default="edge", type=click.Choice(['none','edge','maximum','mean','median','minimum','reflect','symmetric','wrap']), + help="Pad to use around the image, default : edge" + "(see https://numpy.org/doc/stable/reference/generated/numpy.pad.html" + " for more information)") + def _extract_files_from_list(cmd_inputs): """ Extracts a list of file paths from a command line input. diff --git a/src/eolab/rastertools/cli/zonalstats.py b/src/eolab/rastertools/cli/zonalstats.py index 635e780..89e822b 100644 --- a/src/eolab/rastertools/cli/zonalstats.py +++ b/src/eolab/rastertools/cli/zonalstats.py @@ -4,7 +4,7 @@ CLI definition for the zonalstats tool """ from eolab.rastertools import Zonalstats -from eolab.rastertools.cli.utils_cli import apply_process +from eolab.rastertools.cli.utils_cli import apply_process, all_opt, band_opt import click import os @@ -38,10 +38,6 @@ @click.option('--prefix', default = None, help="Add a prefix to the keys (default: None). One prefix per band (e.g. 'band1 band2')") -@click.option('-b','--bands', multiple = True, type=int, help="List of bands to process") - -@click.option('-a', '--all','all_bands', type=bool, is_flag=True, help="Process all bands") - @click.option('--sigma',help="Distance to the mean value (in sigma) in order to produce a raster that highlights outliers.") @click.option('-c','--chart',"chartfile", help="Generate a chart per stat and per geometry (x=timestamp of the input products / y=stat value) and " @@ -57,6 +53,8 @@ @click.option('--category_names',type = str, default="", help="JSON files containing a dict with classes index as keys and names to display classes as values.") +@band_opt +@all_opt @click.pass_context def zonalstats(ctx, inputs : list, output : str, output_format : str, geometries : str, within : str, stats : list, categorical : bool, valid_threshold : float ,area : bool, prefix, bands : list, all_bands : bool, sigma, chartfile, display : bool, geom_index : str, category_file : str, category_index : str, category_names : str) : """ diff --git a/src/eolab/rastertools/main.py b/src/eolab/rastertools/main.py index f891384..355ae39 100644 --- a/src/eolab/rastertools/main.py +++ b/src/eolab/rastertools/main.py @@ -16,6 +16,8 @@ import sys import json import click +from pkg_resources import iter_entry_points +from click_plugins import with_plugins from eolab.rastertools.cli.filtering import filter from eolab.rastertools.cli.hillshade import hillshade from eolab.rastertools.cli.speed import speed @@ -131,6 +133,8 @@ def add_custom_rastertypes(rastertypes): #Rastertools CLI group CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) + +@with_plugins(iter_entry_points('core_package.cli_plugins')) @click.group(context_settings=CONTEXT_SETTINGS) @click.option( '-t', '--rastertype', diff --git a/src/eolab/rastertools/processing/algo.py b/src/eolab/rastertools/processing/algo.py index 34774fd..13e8908 100644 --- a/src/eolab/rastertools/processing/algo.py +++ b/src/eolab/rastertools/processing/algo.py @@ -611,10 +611,6 @@ def adaptive_gaussian(input_data : np.ndarray, kernel_size : int = 8, sigma : in if input_data.shape[0] != 1: raise ValueError("adaptive_gaussian only accepts numpy arrays with first dim of size 1") - ''' - kernel_size = kwargs.get('kernel_size', 8) - sigma = kwargs.get('sigma', 1) - ''' dtype = input_data.dtype w_1 = (input_data[0, :, :-2] - input_data[0, :, 2:]) ** 2 @@ -662,12 +658,7 @@ def svf(input_data : np.ndarray, radius : int = 8, directions : int = 12, resolu raise ValueError("svf only accepts numpy arrays with first dim of size 1") nb_directions = directions - ''' - radius = kwargs.get('radius', 8) - nb_directions = kwargs.get('directions', 12) - resolution = kwargs.get('resolution', 0.5) - altitude = kwargs.get('altitude', None) - ''' + # initialize output shape = input_data.shape out = np.zeros(shape, dtype=np.float32) @@ -784,12 +775,7 @@ def hillshade(input_data : np.ndarray, elevation : float = 0.0, azimuth : float raise ValueError("hillshade only accepts 3 dims numpy arrays") if input_data.shape[0] != 1: raise ValueError("hillshade only accepts numpy arrays with first dim of size 1") - ''' - elevation = np.radians(kwargs.get('elevation', 0.0)) - azimuth = kwargs.get('azimuth', 0.0) - radius = kwargs.get('radius', 8) - resolution = kwargs.get('resolution', 0.5) - ''' + # initialize output shape = input_data.shape out = np.zeros(shape, dtype=bool) diff --git a/src/eolab/rastertools/zonalstats.py b/src/eolab/rastertools/zonalstats.py index 270c991..bb36621 100644 --- a/src/eolab/rastertools/zonalstats.py +++ b/src/eolab/rastertools/zonalstats.py @@ -315,7 +315,6 @@ def with_outliers(self, sigma: float): :obj:`eolab.rastertools.Zonalstats`: the current instance so that it is possible to chain the with... calls (fluent API) """ - print(self._stats) # Manage sigma computation option that requires mean + std dev computation if "mean" not in self._stats: self._stats.append("mean") diff --git a/tests/test_radioindice.py b/tests/test_radioindice.py index acaff0c..a0306b2 100644 --- a/tests/test_radioindice.py +++ b/tests/test_radioindice.py @@ -14,6 +14,7 @@ __copyright__ = "Copyright 2019, CNES" __license__ = "Apache v2.0" +from .utils4test import RastertoolsTestsData __refdir = utils4test.get_refdir("test_radioindice/") @@ -36,16 +37,16 @@ def test_radioindice_process_file_merge(): # create output dir and clear its content if any utils4test.create_outdir() - inputfile = utils4test.indir + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip" + inputfile = RastertoolsTestsData.tests_input_data_dir + "/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip" indices = [indice for indice in Radioindice.get_default_indices()] tool = Radioindice(indices) - tool.with_output(utils4test.outdir, merge=True) - tool.with_roi(utils4test.indir + "COMMUNE_32001.shp") + tool.with_output(RastertoolsTestsData.tests_output_data_dir , merge=True) + tool.with_roi(RastertoolsTestsData.tests_input_data_dir + "/COMMUNE_32001.shp") outputs = tool.process_file(inputfile) assert outputs == [ - utils4test.outdir + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-indices.tif"] + RastertoolsTestsData.tests_output_data_dir + "/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-indices.tif"] utils4test.clear_outdir() @@ -73,17 +74,17 @@ def test_radioindice_process_file_separate(compare : bool, save_gen_as_ref : boo indices = [Radioindice.ndvi, Radioindice.ndwi] tool = Radioindice(indices) - tool.with_output(utils4test.outdir, merge=False) + tool.with_output(RastertoolsTestsData.tests_output_data_dir , merge=False) tool.with_vrt_stored(False) - outputs = tool.process_file(utils4test.indir + inputfile + ".zip") + outputs = tool.process_file(RastertoolsTestsData.tests_input_data_dir + "/" + inputfile + ".zip") # check outputs - assert outputs == [utils4test.outdir + inputfile + "-ndvi.tif", - utils4test.outdir + inputfile + "-ndwi.tif"] + assert outputs == [RastertoolsTestsData.tests_output_data_dir + "/" + inputfile + "-ndvi.tif", + RastertoolsTestsData.tests_output_data_dir + "/" + inputfile + "-ndwi.tif"] gen_files = [inputfile + "-ndvi.tif", inputfile + "-ndwi.tif"] if compare: - match, mismatch, err = utils4test.cmpfiles(utils4test.outdir, __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir , __refdir, gen_files) assert len(match) == 2 assert len(mismatch) == 0 assert len(err) == 0 @@ -109,18 +110,18 @@ def test_radioindice_process_files(): # create output dir and clear its content if any utils4test.create_outdir() - inputfiles = [utils4test.indir + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip", - utils4test.indir + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip"] + inputfiles = [RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip", + RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip"] indices = [Radioindice.ndvi, Radioindice.ndwi] tool = Radioindice(indices) - tool.with_output(utils4test.outdir, merge=True) - tool.with_roi(utils4test.indir + "COMMUNE_32001.shp") + tool.with_output(RastertoolsTestsData.tests_output_data_dir, merge=True) + tool.with_roi(RastertoolsTestsData.tests_input_data_dir + "/COMMUNE_32001.shp") outputs = tool.process_files(inputfiles) assert outputs == [ - utils4test.outdir + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-indices.tif", - utils4test.outdir + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-indices.tif"] + RastertoolsTestsData.tests_output_data_dir + "/" + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-indices.tif", + RastertoolsTestsData.tests_output_data_dir + "/" + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-indices.tif"] utils4test.clear_outdir() @@ -145,11 +146,11 @@ def test_radioindice_incompatible_indice_rastertype(caplog): utils4test.create_outdir() file = "SPOT6_2018_France-Ortho_NC_DRS-MS_SPOT6_2018_FRANCE_ORTHO_NC_GEOSUD_MS_82.tar.gz" - inputfile = utils4test.indir + file + inputfile = RastertoolsTestsData.tests_input_data_dir + "/" + file indice = RadioindiceProcessing("my_indice").with_channels([BandChannel.mir, BandChannel.swir]) tool = Radioindice([indice]) - tool.with_output(utils4test.outdir) + tool.with_output(RastertoolsTestsData.tests_output_data_dir) caplog.set_level(logging.ERROR) outputs = tool.process_file(inputfile) diff --git a/tests/test_rasterproc.py b/tests/test_rasterproc.py index 95180be..aa68cc6 100644 --- a/tests/test_rasterproc.py +++ b/tests/test_rasterproc.py @@ -11,6 +11,7 @@ __copyright__ = "Copyright 2019, CNES" __license__ = "Apache v2.0" +from .utils4test import RastertoolsTestsData __refdir = utils4test.get_refdir("test_rasterproc/") @@ -51,8 +52,8 @@ def test_compute_sliding(): # create output dir and clear its content if any utils4test.create_outdir() - input_image = utils4test.indir + "RGB_TIF_20170105_013442_test.tif" - output_image = utils4test.outdir + "RGB_TIF_20170105_013442_test-out.tif" + input_image = RastertoolsTestsData.tests_input_data_dir + "/" + "RGB_TIF_20170105_013442_test.tif" + output_image = RastertoolsTestsData.tests_output_data_dir + "/" + "RGB_TIF_20170105_013442_test-out.tif" # Test 2D proc2D = RasterProcessing("Processing per band", algo=algo2D, per_band_algo=True) diff --git a/tests/test_rasterproduct.py b/tests/test_rasterproduct.py index b471ef0..1135e2d 100644 --- a/tests/test_rasterproduct.py +++ b/tests/test_rasterproduct.py @@ -19,6 +19,7 @@ __copyright__ = "Copyright 2019, CNES" __license__ = "Apache v2.0" +from .utils4test import RastertoolsTestsData __refdir = utils4test.get_refdir("test_rasterproduct/") @@ -41,14 +42,15 @@ def test_rasterproduct_valid_parameters(): """ # archive with one file per band basename = "S2B_MSIL1C_20191008T105029_N0208_R051_T30TYP_20191008T125041" + origin_path = RastertoolsTestsData.tests_input_data_dir + "/".split(os.getcwd() + "/")[-1] file = Path( - utils4test.indir.split(os.getcwd() + "/")[-1] + basename + ".zip") + origin_path + basename + ".zip") prod = RasterProduct(file) assert prod.file == file assert prod.rastertype == RasterType.get("S2_L1C") assert prod.channels == RasterType.get("S2_L1C").channels - band_format = f"/vsizip/" + utils4test.indir.split(os.getcwd() + "/")[-1] + f"{basename}.zip/" + band_format = f"/vsizip/" + origin_path + f"{basename}.zip/" band_format += f"{basename}.SAFE/GRANULE/L1C_T30TYP_A013519_20191008T105335/IMG_DATA/" band_format += "T30TYP_20191008T105029_{}.jp2" assert prod.bands_files == {b: band_format.format(b) for b in prod.rastertype.get_band_ids()} @@ -62,13 +64,13 @@ def test_rasterproduct_valid_parameters(): # archive with one file for all bands basename = "SPOT6_2018_France-Ortho_NC_DRS-MS_SPOT6_2018_FRANCE_ORTHO_NC_GEOSUD_MS_82" - file = utils4test.indir.split(os.getcwd() + "/")[-1] + basename + ".tar.gz" + file = origin_path + basename + ".tar.gz" prod = RasterProduct(file) assert prod.file == Path(file) assert prod.rastertype == RasterType.get("SPOT67_GEOSUD") assert prod.channels == [BandChannel.red, BandChannel.green, BandChannel.blue, BandChannel.nir] - band = f"/vsitar/" + utils4test.indir.split(os.getcwd() + "/")[-1] + f"{basename}.tar.gz/SPOT6_2018_FRANCE_ORTHO_NC_GEOSUD_MS_82/" + band = f"/vsitar/" + origin_path + f"{basename}.tar.gz/SPOT6_2018_FRANCE_ORTHO_NC_GEOSUD_MS_82/" band += "PROD_SPOT6_001/VOL_SPOT6_001_A/IMG_SPOT6_MS_001_A/" band += "IMG_SPOT6_MS_201805111031189_ORT_SPOT6_20180517_1333011n1b80qobn5ex_1_R1C1.TIF" assert prod.bands_files == {"all": band} @@ -82,7 +84,7 @@ def test_rasterproduct_valid_parameters(): # regular raster file basename = "RGB_TIF_20170105_013442_test.tif" - file = utils4test.indir + basename + file = RastertoolsTestsData.tests_input_data_dir + "/" + basename prod = RasterProduct(file) assert prod.file == Path(file) @@ -115,12 +117,12 @@ def test_rasterproduct_invalid_parameters(): RasterProduct(None) assert "'file' cannot be None" in str(exc.value) - file = utils4test.indir + "InvalidName.zip" + file = RastertoolsTestsData.tests_input_data_dir + "/" + "InvalidName.zip" with pytest.raises(ValueError) as exc: RasterProduct(file) assert f"Unrecognized raster type for input file {file}" in str(exc.value) - file = utils4test.indir + "grid.geojson" + file = RastertoolsTestsData.tests_input_data_dir + "/" + "grid.geojson" with pytest.raises(ValueError) as exc: RasterProduct(file) assert f"Unsupported input file {file}" in str(exc.value) @@ -143,31 +145,31 @@ def test_create_product_S2_L2A_MAJA(compare, save_gen_as_ref): utils4test.create_outdir() # unzip SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip - file = utils4test.indir + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip" + file = RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip" with zipfile.ZipFile(file) as myzip: - myzip.extractall(utils4test.outdir) + myzip.extractall(RastertoolsTestsData.tests_output_data_dir + "/") # creation of S2 L2A MAJA products - files = [Path(utils4test.indir + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip"), - Path(utils4test.indir + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_tar.tar"), - Path(utils4test.indir + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_targz.TAR.GZ"), - Path(utils4test.outdir + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_V1-9")] + files = [Path(RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip"), + Path(RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_tar.tar"), + Path(RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_targz.TAR.GZ"), + Path(RastertoolsTestsData.tests_output_data_dir + "/" + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_V1-9")] for file in files: - with RasterProduct(file, vrt_outputdir=Path(utils4test.outdir)) as prod: - raster = prod.get_raster(roi=Path(utils4test.indir + "COMMUNE_32001.shp"), + with RasterProduct(file, vrt_outputdir=Path(RastertoolsTestsData.tests_output_data_dir + "/")) as prod: + raster = prod.get_raster(roi=Path(RastertoolsTestsData.tests_input_data_dir + "/" + "COMMUNE_32001.shp"), masks="all") assert Path(raster).exists() - assert raster == utils4test.outdir + utils4test.basename(file) + "-mask.vrt" + assert raster == RastertoolsTestsData.tests_output_data_dir + "/" + utils4test.basename(file) + "-mask.vrt" ref = [utils4test.basename(file) + ".vrt", utils4test.basename(file) + "-clipped.vrt", utils4test.basename(file) + "-mask.vrt"] if compare: - print(f"compare {utils4test.outdir} ,{__refdir}, {ref}") - match, mismatch, err = utils4test.cmpfiles(utils4test.outdir, __refdir, ref) + print(f"compare {RastertoolsTestsData.tests_output_data_dir} ,{__refdir}, {ref}") + match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, ref) assert len(match) == len(ref) assert len(mismatch) == 0 assert len(err) == 0 @@ -182,7 +184,7 @@ def test_create_product_S2_L2A_MAJA(compare, save_gen_as_ref): utils4test.clear_outdir(subdirs=False) # delete the dir resulting from unzip - utils4test.delete_dir(utils4test.outdir + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_V1-9") + utils4test.delete_dir(RastertoolsTestsData.tests_output_data_dir + "/" + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_V1-9") def test_create_product_S2_L1C(compare, save_gen_as_ref): @@ -207,18 +209,18 @@ def test_create_product_S2_L1C(compare, save_gen_as_ref): utils4test.create_outdir() # creation of S2 L1C product - infile = utils4test.indir + "S2B_MSIL1C_20191008T105029_N0208_R051_T30TYP_20191008T125041.zip" + infile = RastertoolsTestsData.tests_input_data_dir + "/" + "S2B_MSIL1C_20191008T105029_N0208_R051_T30TYP_20191008T125041.zip" - with RasterProduct(infile, vrt_outputdir=utils4test.outdir) as prod: - raster = prod.get_raster(roi=utils4test.indir + "/COMMUNE_32001.shp", + with RasterProduct(infile, vrt_outputdir=RastertoolsTestsData.tests_output_data_dir + "/") as prod: + raster = prod.get_raster(roi=RastertoolsTestsData.tests_input_data_dir + "/" + "/COMMUNE_32001.shp", masks="all") assert Path(raster).exists() - assert raster == utils4test.outdir + utils4test.basename(infile) + "-clipped.vrt" + assert raster == RastertoolsTestsData.tests_output_data_dir + "/" + utils4test.basename(infile) + "-clipped.vrt" gen_files = [utils4test.basename(infile) + ".vrt", utils4test.basename(infile) + "-clipped.vrt"] if compare: - match, mismatch, err = utils4test.cmpfiles(utils4test.outdir, __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) assert len(match) == 2 assert len(mismatch) == 0 assert len(err) == 0 @@ -255,16 +257,16 @@ def test_create_product_S2_L2A_SEN2CORE(compare, save_gen_as_ref): utils4test.create_outdir() # creation of S2 L2A SEN2CORE product - infile = utils4test.indir + "S2A_MSIL2A_20190116T105401_N0211_R051_T30TYP_20190116T120806.zip" - with RasterProduct(infile, vrt_outputdir=utils4test.outdir) as prod: + infile = RastertoolsTestsData.tests_input_data_dir + "/" + "S2A_MSIL2A_20190116T105401_N0211_R051_T30TYP_20190116T120806.zip" + with RasterProduct(infile, vrt_outputdir=RastertoolsTestsData.tests_output_data_dir + "/") as prod: raster = prod.get_raster() assert Path(raster).exists() - assert raster == utils4test.outdir + utils4test.basename(infile) + ".vrt" + assert raster == RastertoolsTestsData.tests_output_data_dir + "/" + utils4test.basename(infile) + ".vrt" gen_files = [utils4test.basename(infile) + ".vrt"] if compare: - match, mismatch, err = utils4test.cmpfiles(utils4test.outdir, __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) assert len(match) == 1 assert len(mismatch) == 0 assert len(err) == 0 @@ -302,15 +304,15 @@ def test_create_product_SPOT67(compare, save_gen_as_ref): # creation of SPOT67 product infile = "SPOT6_2018_France-Ortho_NC_DRS-MS_SPOT6_2018_FRANCE_ORTHO_NC_GEOSUD_MS_82.tar.gz" - with RasterProduct(utils4test.indir + infile, vrt_outputdir=utils4test.outdir) as prod: + with RasterProduct(RastertoolsTestsData.tests_input_data_dir + "/" + infile, vrt_outputdir=RastertoolsTestsData.tests_output_data_dir + "/") as prod: raster = prod.get_raster() assert Path(raster).exists() - assert raster == utils4test.outdir + utils4test.basename(infile) + ".vrt" + assert raster == RastertoolsTestsData.tests_output_data_dir + "/" + utils4test.basename(infile) + ".vrt" gen_files = [utils4test.basename(infile) + ".vrt"] if compare: - match, mismatch, err = utils4test.cmpfiles(utils4test.outdir, __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) assert len(match) == 1 assert len(mismatch) == 0 assert len(err) == 0 @@ -343,7 +345,7 @@ def test_create_product_special_cases(): # creation in memory (without masks) file = "S2B_MSIL1C_20191008T105029_N0208_R051_T30TYP_20191008T125041.zip" - with RasterProduct(utils4test.indir + file) as prod: + with RasterProduct(RastertoolsTestsData.tests_input_data_dir + "/" + file) as prod: assert prod.get_raster(masks=None).endswith(utils4test.basename(file) + ".vrt") # check if product can be opened by rasterio dataset = prod.open() @@ -351,8 +353,8 @@ def test_create_product_special_cases(): # creation in memory (with masks) file = "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_tar.tar" - with RasterProduct(utils4test.indir + file) as prod: - raster = prod.get_raster(roi=Path(utils4test.indir + "COMMUNE_32001.shp"), + with RasterProduct(RastertoolsTestsData.tests_input_data_dir + "/" + file) as prod: + raster = prod.get_raster(roi=Path(RastertoolsTestsData.tests_input_data_dir + "/" + "COMMUNE_32001.shp"), bands=prod.rastertype.get_band_ids(), masks=prod.rastertype.get_mask_ids()) @@ -367,9 +369,9 @@ def test_create_product_special_cases(): # creation from a vrt file = "S2A_MSIL2A_20190116T105401_N0211_R051_T30TYP_20190116T120806.vrt" - with RasterProduct(utils4test.indir + file) as prod: + with RasterProduct(RastertoolsTestsData.tests_input_data_dir + "/" + file) as prod: raster = prod.get_raster() - assert raster == utils4test.indir + utils4test.basename(file) + ".vrt" + assert raster == RastertoolsTestsData.tests_input_data_dir + "/" + utils4test.basename(file) + ".vrt" assert prod.rastertype == RasterType.get("S2_L2A_SEN2CORE") # check if product can be opened by rasterio dataset = rasterio.open(raster) @@ -377,14 +379,14 @@ def test_create_product_special_cases(): # creation from a directory # unzip S2B_MSIL1C_20191008T105029_N0208_R051_T30TYP_20191008T125041.zip - file = utils4test.indir + "S2B_MSIL1C_20191008T105029_N0208_R051_T30TYP_20191008T125041.zip" + file = RastertoolsTestsData.tests_input_data_dir + "/" + "S2B_MSIL1C_20191008T105029_N0208_R051_T30TYP_20191008T125041.zip" with zipfile.ZipFile(file) as myzip: - myzip.extractall(utils4test.outdir) + myzip.extractall(RastertoolsTestsData.tests_output_data_dir + "/") dirname = "S2B_MSIL1C_20191008T105029_N0208_R051_T30TYP_20191008T125041.SAFE" - with RasterProduct(file, vrt_outputdir=Path(utils4test.outdir)) as prod: + with RasterProduct(file, vrt_outputdir=Path(RastertoolsTestsData.tests_output_data_dir + "/")) as prod: raster = prod.get_raster() - assert raster == utils4test.outdir + utils4test.basename(file) + ".vrt" + assert raster == RastertoolsTestsData.tests_output_data_dir + "/" + utils4test.basename(file) + ".vrt" # check if product can be opened by rasterio dataset = rasterio.open(raster) dataset.close() @@ -395,7 +397,7 @@ def test_create_product_special_cases(): # file = "SPOT6_2018_France-Ortho_NC_DRS-MS_SPOT6_2018_FRANCE_ORTHO_NC_GEOSUD_MS_82.tar.gz" # channels = [BandChannel.red, BandChannel.green, BandChannel.blue, BandChannel.swir] # with pytest.raises(ValueError) as exc: - # prod = RasterProduct(utils4test.indir + file, + # prod = RasterProduct(RastertoolsTestsData.tests_input_data_dir + "/" + file, # RasterType.get("SPOT67_GEOSUD"), # channels) # msg = f"RasterType does not contain all the channels in {channels}" @@ -403,7 +405,7 @@ def test_create_product_special_cases(): # creation of a product with bands that do not exist in the product (but that # do exist in rastertype) - file = utils4test.indir + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip" + file = RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip" with zipfile.ZipFile(file) as myzip: names = myzip.namelist() selection = [] @@ -413,9 +415,9 @@ def test_create_product_special_cases(): n.endswith("FRE_B4.tif") or \ n.endswith("CLM_R1.tif"): selection.append(n) - myzip.extractall(utils4test.outdir, selection) + myzip.extractall(RastertoolsTestsData.tests_output_data_dir + "/", selection) - file = Path(utils4test.outdir + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_V1-9") + file = Path(RastertoolsTestsData.tests_output_data_dir + "/" + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_V1-9") with pytest.raises(ValueError) as exc: prod = RasterProduct(file) diff --git a/tests/test_rastertools.py b/tests/test_rastertools.py index 266a0ec..5a98448 100644 --- a/tests/test_rastertools.py +++ b/tests/test_rastertools.py @@ -7,7 +7,7 @@ from click.testing import CliRunner from pathlib import Path -from eolab.rastertools import rastertools, RastertoolConfigurationException +from eolab.rastertools import rastertools from eolab.rastertools.product import RasterType from . import utils4test @@ -98,24 +98,21 @@ def run_test(self, caplog=None, loglevel=logging.ERROR, check_outputs=True, chec else: check_logs = False - print(self.args) - try: rastertools(self.args) except SystemExit as wrapped_exception: - print(wrapped_exception) if check_sys_exit: # Check if the exit code matches the expected value assert wrapped_exception.code == self._sys_exit, (f"Expected exit code {self._sys_exit}, but got {wrapped_exception.code}") # check list of outputs if check_outputs: - outdir = Path(utils4test.outdir) + outdir = Path(RastertoolsTestsData.tests_output_data_dir + "/") assert sorted([x.name for x in outdir.iterdir()]) == sorted(self._outputs) if compare: - match, mismatch, err = utils4test.cmpfiles(utils4test.outdir, self._refdir, self._outputs) + match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", self._refdir, self._outputs) assert len(match) == 3 assert len(mismatch) == 0 assert len(err) == 0 @@ -125,8 +122,6 @@ def run_test(self, caplog=None, loglevel=logging.ERROR, check_outputs=True, chec # check logs if check_logs: - print('...'*20) - print(caplog.record_tuples) for i, log in enumerate(self._logs): assert caplog.record_tuples[i] == log diff --git a/tests/test_rastertype.py b/tests/test_rastertype.py index b061174..9981136 100644 --- a/tests/test_rastertype.py +++ b/tests/test_rastertype.py @@ -13,6 +13,8 @@ __copyright__ = "Copyright 2019, CNES" __license__ = "Apache v2.0" +from .utils4test import RastertoolsTestsData + def test_rastertype_valid_parameters(): # S2 L1C @@ -352,7 +354,7 @@ def test_rastertype_SPOT67(): def test_rastertype_additional(): - file = utils4test.indir + "additional_rastertypes.json" + file = RastertoolsTestsData.tests_input_data_dir + "/" + "additional_rastertypes.json" with open(file) as json_content: RasterType.add(json.load(json_content)) diff --git a/tests/test_speed.py b/tests/test_speed.py index f7deab5..1829563 100644 --- a/tests/test_speed.py +++ b/tests/test_speed.py @@ -12,6 +12,7 @@ __copyright__ = "Copyright 2019, CNES" __license__ = "Apache v2.0" +from .utils4test import RastertoolsTestsData __refdir = utils4test.get_refdir("test_radioindice/") @@ -28,19 +29,19 @@ def test_speed_process_files(compare : bool, save_gen_as_ref : bool): date1 = RasterType.get("S2_L2A_MAJA").get_date(name1) - files = [utils4test.indir + name1 + "-ndvi.tif", - utils4test.indir + name2 + "-ndvi.tif"] + files = [RastertoolsTestsData.tests_input_data_dir + "/" + name1 + "-ndvi.tif", + RastertoolsTestsData.tests_input_data_dir + "/" + name2 + "-ndvi.tif"] tool = Speed() - tool.with_output(utils4test.outdir) + tool.with_output(RastertoolsTestsData.tests_output_data_dir + "/") outputs = tool.process_files(files) exp_outs = [name2 + "-ndvi-speed-" + date1.strftime('%Y%m%d-%H%M%S') + ".tif"] - assert outputs == [utils4test.outdir + exp_out for exp_out in exp_outs] + assert outputs == [RastertoolsTestsData.tests_output_data_dir + "/" + exp_out for exp_out in exp_outs] if compare: - match, mismatch, err = utils4test.cmpfiles(utils4test.outdir, __refdir, exp_outs) + match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, exp_outs) assert len(match) == 1 assert len(mismatch) == 0 assert len(err) == 0 diff --git a/tests/test_stats.py b/tests/test_stats.py index 692a1b3..cef6a98 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -8,6 +8,7 @@ __copyright__ = "Copyright 2019, CNES" __license__ = "Apache v2.0" +from .utils4test import RastertoolsTestsData __refdir = utils4test.get_refdir("test_stats/") @@ -16,8 +17,8 @@ def test_compute_zonal_default_stats(): - raster = utils4test.indir + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" - geojson = utils4test.indir + "COMMUNE_32xxx.geojson" + raster = RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" + geojson = RastertoolsTestsData.tests_input_data_dir + "/" + "COMMUNE_32xxx.geojson" stats_to_compute = DEFAULT_STATS categorical = False bands = [1] @@ -63,8 +64,8 @@ def test_compute_zonal_default_stats(): def test_compute_zonal_extra_stats(): - raster = utils4test.indir + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" - geojson = utils4test.indir + "COMMUNE_32xxx.geojson" + raster = RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" + geojson = RastertoolsTestsData.tests_input_data_dir + "/" + "COMMUNE_32xxx.geojson" stats_to_compute = EXTRA_STATS + ["valid"] categorical = False bands = [1] @@ -167,8 +168,8 @@ def test_compute_zonal_extra_stats(): def test_compute_zonal_categorical(): - raster = utils4test.indir + "OCS_2017_CESBIO_extract.tif" - geojson = utils4test.indir + "COMMUNE_59xxx.geojson" + raster = RastertoolsTestsData.tests_input_data_dir + "/" + "OCS_2017_CESBIO_extract.tif" + geojson = RastertoolsTestsData.tests_input_data_dir + "/" + "COMMUNE_59xxx.geojson" stats_to_compute = ['count'] categorical = True bands = [1] @@ -205,10 +206,10 @@ def test_compute_zonal_categorical(): def test_compute_zonal_stats_per_category(): - raster = utils4test.indir + "DSM_PHR_Dunkerque.tif" - geojson = utils4test.indir + "COMMUNE_59xxx.geojson" - catgeojson = utils4test.indir + "OSO_2017_classification_dep59.shp" - catlabels = utils4test.indir + "OSO_nomenclature_2017.json" + raster = RastertoolsTestsData.tests_input_data_dir + "/" + "DSM_PHR_Dunkerque.tif" + geojson = RastertoolsTestsData.tests_input_data_dir + "/" + "COMMUNE_59xxx.geojson" + catgeojson = RastertoolsTestsData.tests_input_data_dir + "/" + "OSO_2017_classification_dep59.shp" + catlabels = RastertoolsTestsData.tests_input_data_dir + "/" + "OSO_nomenclature_2017.json" stats_to_compute = DEFAULT_STATS bands = [1] geometries = vector.reproject(vector.filter(geojson, raster), raster) diff --git a/tests/test_tiling.py b/tests/test_tiling.py index 5418091..92197c4 100644 --- a/tests/test_tiling.py +++ b/tests/test_tiling.py @@ -10,6 +10,7 @@ __copyright__ = "Copyright 2019, CNES" __license__ = "Apache v2.0" +from .utils4test import RastertoolsTestsData __refdir = utils4test.get_refdir("test_tiling/") @@ -21,14 +22,14 @@ def test_tiling_process_file(compare, save_gen_as_ref): inputfile = "tif_file" geometryfile = "grid.geojson" - tool = Tiling(utils4test.indir + geometryfile) - tool.with_output(utils4test.outdir) + tool = Tiling(RastertoolsTestsData.tests_input_data_dir + "/" + geometryfile) + tool.with_output(RastertoolsTestsData.tests_output_data_dir + "/") tool.with_id_column("id", [77, 93]) - tool.process_file(utils4test.indir + inputfile + ".tif") + tool.process_file(RastertoolsTestsData.tests_input_data_dir + "/" + inputfile + ".tif") gen_files = [inputfile + "_tile77.tif", inputfile + "_tile93.tif"] if compare: - match, mismatch, err = utils4test.cmpfiles(utils4test.outdir, __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) assert len(match) == 2 assert len(mismatch) == 0 assert len(err) == 0 diff --git a/tests/test_vector.py b/tests/test_vector.py index a866139..8d9e7c2 100644 --- a/tests/test_vector.py +++ b/tests/test_vector.py @@ -13,6 +13,7 @@ __copyright__ = "Copyright 2019, CNES" __license__ = "Apache v2.0" +from .utils4test import RastertoolsTestsData __refdir = utils4test.get_refdir("test_vector/") @@ -22,41 +23,41 @@ def test_reproject_filter(compare, save_gen_as_ref): utils4test.create_outdir() reproj_geoms = vector.reproject( - utils4test.indir + "COMMUNE_32xxx.geojson", - utils4test.indir + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif") + RastertoolsTestsData.tests_input_data_dir + "/" + "COMMUNE_32xxx.geojson", + RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif") filtered_geoms = vector.filter( reproj_geoms, - utils4test.indir + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif") + RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif") assert len(filtered_geoms) == 19 filtered_geoms = vector.filter( reproj_geoms, - utils4test.indir + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif", + RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif", within=True) assert len(filtered_geoms) == 2 geoms = vector.reproject( - vector.filter(utils4test.indir + "COMMUNE_32xxx.geojson", - utils4test.indir + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif"), - utils4test.indir + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif") + vector.filter(RastertoolsTestsData.tests_input_data_dir + "/" + "COMMUNE_32xxx.geojson", + RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif"), + RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif") assert len(geoms) == 19 geoms = vector.reproject( - vector.filter(utils4test.indir + "COMMUNE_32xxx.geojson", - utils4test.indir + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif", + vector.filter(RastertoolsTestsData.tests_input_data_dir + "/" + "COMMUNE_32xxx.geojson", + RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif", within=True), - utils4test.indir + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif") + RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif") assert len(geoms) == 2 geoms = vector.reproject( - vector.filter(Path(utils4test.indir + "COMMUNE_32xxx.geojson"), - Path(utils4test.indir + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif")), - Path(utils4test.indir + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif"), - output=Path(utils4test.outdir + "reproject_filter.geojson")) + vector.filter(Path(RastertoolsTestsData.tests_input_data_dir + "/" + "COMMUNE_32xxx.geojson"), + Path(RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif")), + Path(RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif"), + output=Path(RastertoolsTestsData.tests_output_data_dir + "/" + "reproject_filter.geojson")) assert len(geoms) == 19 gen_files = ["reproject_filter.geojson"] if compare: - match, mismatch, err = utils4test.cmpfiles(utils4test.outdir, __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) assert len(match) == 1 assert len(mismatch) == 0 assert len(err) == 0 @@ -64,16 +65,16 @@ def test_reproject_filter(compare, save_gen_as_ref): # save the generated files in the refdir => make them the new refs. utils4test.copy_to_ref(gen_files, __refdir) - geometries = gpd.read_file(utils4test.indir + "COMMUNE_32xxx.geojson") + geometries = gpd.read_file(RastertoolsTestsData.tests_input_data_dir + "/" + "COMMUNE_32xxx.geojson") geoms = vector.reproject( vector.filter(geometries, - Path(utils4test.indir + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif")), - Path(utils4test.indir + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif"), - output=Path(utils4test.outdir + "reproject_filter.geojson")) + Path(RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif")), + Path(RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif"), + output=Path(RastertoolsTestsData.tests_output_data_dir + "/" + "reproject_filter.geojson")) assert len(geoms) == 19 gen_files = ["reproject_filter.geojson"] if compare: - match, mismatch, err = utils4test.cmpfiles(utils4test.outdir, __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) assert len(match) == 1 assert len(mismatch) == 0 assert len(err) == 0 @@ -89,18 +90,18 @@ def test_reproject_dissolve(compare, save_gen_as_ref): utils4test.create_outdir() geoms = vector.reproject( - vector.dissolve(utils4test.indir + "COMMUNE_32xxx.geojson"), - utils4test.indir + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif") + vector.dissolve(RastertoolsTestsData.tests_input_data_dir + "/" + "COMMUNE_32xxx.geojson"), + RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif") assert len(geoms) == 1 geoms = vector.reproject( - vector.dissolve(Path(utils4test.indir + "COMMUNE_32xxx.geojson")), - utils4test.indir + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif", - output=Path(utils4test.outdir + "reproject_dissolve.geojson")) + vector.dissolve(Path(RastertoolsTestsData.tests_input_data_dir + "/" + "COMMUNE_32xxx.geojson")), + RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif", + output=Path(RastertoolsTestsData.tests_output_data_dir + "/" + "reproject_dissolve.geojson")) assert len(geoms) == 1 gen_files = ["reproject_dissolve.geojson"] if compare: - match, mismatch, err = utils4test.cmpfiles(utils4test.outdir, __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) assert len(match) == 1 assert len(mismatch) == 0 assert len(err) == 0 @@ -115,18 +116,18 @@ def test_clip(compare, save_gen_as_ref): # create output dir and clear its content if any utils4test.create_outdir() - geoms = vector.clip(utils4test.indir + "COMMUNE_32xxx.geojson", - utils4test.indir + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif") + geoms = vector.clip(RastertoolsTestsData.tests_input_data_dir + "/" + "COMMUNE_32xxx.geojson", + RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif") assert len(geoms) == 19 geoms = vector.clip( - Path(utils4test.indir + "COMMUNE_32xxx.geojson"), - Path(utils4test.indir + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif"), - output=Path(utils4test.outdir + "clip.geojson")) + Path(RastertoolsTestsData.tests_input_data_dir + "/" + "COMMUNE_32xxx.geojson"), + Path(RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif"), + output=Path(RastertoolsTestsData.tests_output_data_dir + "/" + "clip.geojson")) assert len(geoms) == 19 gen_files = ["clip.geojson"] if compare: - match, mismatch, err = utils4test.cmpfiles(utils4test.outdir, __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) assert len(match) == 1 assert len(mismatch) == 0 assert len(err) == 0 @@ -142,16 +143,16 @@ def test_get_raster_shape(compare, save_gen_as_ref): utils4test.create_outdir() geoms = vector.get_raster_shape( - utils4test.indir + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif") + RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif") assert len(geoms) == 1 geoms = vector.get_raster_shape( - Path(utils4test.indir + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif"), - Path(utils4test.outdir + "raster_outline.geojson")) + Path(RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif"), + Path(RastertoolsTestsData.tests_output_data_dir + "/" + "raster_outline.geojson")) assert len(geoms) == 1 gen_files = ["raster_outline.geojson"] if compare: - match, mismatch, err = utils4test.cmpfiles(utils4test.outdir, __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) assert len(match) == 1 assert len(mismatch) == 0 assert len(err) == 0 diff --git a/tests/test_zonalstats.py b/tests/test_zonalstats.py index 906b5ba..11c7cb1 100644 --- a/tests/test_zonalstats.py +++ b/tests/test_zonalstats.py @@ -13,6 +13,7 @@ __copyright__ = "Copyright 2019, CNES" __license__ = "Apache v2.0" +from .utils4test import RastertoolsTestsData __refdir = utils4test.get_refdir("test_zonalstats/") @@ -22,20 +23,20 @@ def test_zonalstats_global(compare, save_gen_as_ref): utils4test.create_outdir() # cas 1 - inputfile = utils4test.indir + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" + inputfile = RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" outformat = "ESRI Shapefile" statistics = "min max mean std count range sum".split() categorical = False tool = Zonalstats(statistics, categorical) - tool.with_output(utils4test.outdir, output_format=outformat) + tool.with_output(RastertoolsTestsData.tests_output_data_dir + "/", output_format=outformat) tool.with_outliers(1.0) tool.process_file(inputfile) gen_files = ["SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-stats.shp", "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-stats-outliers.tif"] if compare: - match, mismatch, err = utils4test.cmpfiles(utils4test.outdir, __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) assert len(match) == 2 assert len(mismatch) == 0 assert len(err) == 0 @@ -44,18 +45,18 @@ def test_zonalstats_global(compare, save_gen_as_ref): utils4test.copy_to_ref(gen_files, __refdir) # cas 2 - inputfile = utils4test.indir + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" + inputfile = RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" outformat = "GeoJSON" statistics = "median majority minority unique percentile_10 percentile_90".split() categorical = False tool = Zonalstats(statistics, categorical) - tool.with_output(utils4test.outdir, output_format=outformat) + tool.with_output(RastertoolsTestsData.tests_output_data_dir + "/", output_format=outformat) tool.process_file(inputfile) gen_files = ["SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-stats.geojson"] if compare: - match, mismatch, err = utils4test.cmpfiles(utils4test.outdir, __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) assert len(match) == 1 assert len(mismatch) == 0 assert len(err) == 0 @@ -64,13 +65,13 @@ def test_zonalstats_global(compare, save_gen_as_ref): utils4test.copy_to_ref(gen_files, __refdir) # cas 3 : unrecognized rastertype that disables charting capability, no output file for stats - inputfile = utils4test.indir + "toulouse-mnh.tif" + inputfile = RastertoolsTestsData.tests_input_data_dir + "/" + "toulouse-mnh.tif" statistics = "mean std".split() categorical = False tool = Zonalstats(statistics, categorical) tool.with_output(None) - tool.with_chart(utils4test.outdir + "chart.png") + tool.with_chart(RastertoolsTestsData.tests_output_data_dir + "/" + "chart.png") tool.process_file(inputfile) assert len(tool.generated_stats) == 1 assert len(tool.generated_stats_per_date) == 0 @@ -83,21 +84,21 @@ def test_zonalstats_zonal(compare, save_gen_as_ref): utils4test.create_outdir() # cas 1 - inputfile = utils4test.indir + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif" + inputfile = RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif" outformat = "ESRI Shapefile" statistics = "min max mean std count range sum".split() categorical = False tool = Zonalstats(statistics, categorical) - tool.with_output(utils4test.outdir, output_format=outformat) - tool.with_geometries(geometries=utils4test.indir + "COMMUNE_32xxx.geojson") + tool.with_output(RastertoolsTestsData.tests_output_data_dir + "/", output_format=outformat) + tool.with_geometries(geometries=RastertoolsTestsData.tests_input_data_dir + "/" + "COMMUNE_32xxx.geojson") tool.with_outliers(sigma=1.0) tool.process_file(inputfile) gen_files = ["SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi-stats.shp", "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi-stats-outliers.tif"] if compare: - match, mismatch, err = utils4test.cmpfiles(utils4test.outdir, __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) assert len(match) == 2 assert len(mismatch) == 0 assert len(err) == 0 @@ -106,19 +107,19 @@ def test_zonalstats_zonal(compare, save_gen_as_ref): utils4test.copy_to_ref(gen_files, __refdir) # cas 2 - inputfile = utils4test.indir + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif" + inputfile = RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif" outformat = "GeoJSON" statistics = "median majority minority unique percentile_10 percentile_90".split() categorical = False tool = Zonalstats(statistics, categorical) - tool.with_output(utils4test.outdir, output_format=outformat) - tool.with_geometries(geometries=utils4test.indir + "COMMUNE_32xxx.geojson") + tool.with_output(RastertoolsTestsData.tests_output_data_dir + "/", output_format=outformat) + tool.with_geometries(geometries=RastertoolsTestsData.tests_input_data_dir + "/" + "COMMUNE_32xxx.geojson") tool.process_file(inputfile) gen_files = ["SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi-stats.geojson"] if compare: - match, mismatch, err = utils4test.cmpfiles(utils4test.outdir, __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) assert len(match) == 1 assert len(mismatch) == 0 assert len(err) == 0 @@ -133,21 +134,21 @@ def test_zonalstats_process_files(compare, save_gen_as_ref): # create output dir and clear its content if any utils4test.create_outdir() - inputfiles = [utils4test.indir + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif", - utils4test.indir + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif"] + inputfiles = [RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif", + RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif"] outformat = "GeoJSON" statistics = "min max mean std count range sum".split() tool = Zonalstats(statistics, prefix="indice") tool.with_output(None, output_format=outformat) - tool.with_geometries(geometries=utils4test.indir + "COMMUNE_32xxx.geojson") - tool.with_chart(chart_file=utils4test.outdir + "chart.png") + tool.with_geometries(geometries=RastertoolsTestsData.tests_input_data_dir + "/" + "COMMUNE_32xxx.geojson") + tool.with_chart(chart_file=RastertoolsTestsData.tests_output_data_dir + "/" + "chart.png") tool.process_files(inputfiles) gen_files = ["chart.png"] - assert Path(utils4test.outdir + "chart.png").exists() + assert Path(RastertoolsTestsData.tests_output_data_dir + "/" + "chart.png").exists() if compare: - match, mismatch, err = utils4test.cmpfiles(utils4test.outdir, __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) assert len(match) == 1 assert len(mismatch) == 0 assert len(err) == 0 @@ -163,23 +164,23 @@ def test_zonalstats_category(compare, save_gen_as_ref): utils4test.create_outdir() # cas 1 - classif shapefile sur une ROI composée de plusieurs géométries - inputfile = utils4test.indir + "DSM_PHR_Dunkerque.tif" + inputfile = RastertoolsTestsData.tests_input_data_dir + "/" + "DSM_PHR_Dunkerque.tif" outformat = "GeoJSON" statistics = "min max mean std count range sum".split() # Category inputs - categoryfile = utils4test.indir + "OSO_2017_classification_dep59.shp" - categorydic = utils4test.indir + "OSO_nomenclature_2017.json" + categoryfile = RastertoolsTestsData.tests_input_data_dir + "/" + "OSO_2017_classification_dep59.shp" + categorydic = RastertoolsTestsData.tests_input_data_dir + "/" + "OSO_nomenclature_2017.json" tool = Zonalstats(statistics, area=True) - tool.with_output(utils4test.outdir, output_format=outformat) - tool.with_geometries(geometries=utils4test.indir + "COMMUNE_59xxx.geojson") + tool.with_output(RastertoolsTestsData.tests_output_data_dir + "/", output_format=outformat) + tool.with_geometries(geometries=RastertoolsTestsData.tests_input_data_dir + "/" + "COMMUNE_59xxx.geojson") tool.with_per_category(category_file=categoryfile, category_index="Classe", category_labels_json=categorydic) tool.process_file(inputfile) gen_files = ["DSM_PHR_Dunkerque-stats.geojson"] if compare: - match, mismatch, err = utils4test.cmpfiles(utils4test.outdir, __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) assert len(match) == 1 assert len(mismatch) == 0 assert len(err) == 0 @@ -188,13 +189,13 @@ def test_zonalstats_category(compare, save_gen_as_ref): utils4test.copy_to_ref(gen_files, __refdir) # cas 2 - classif raster sur l'emprise globale du DSM - inputfile = utils4test.indir + "DSM_PHR_Dunkerque.tif" + inputfile = RastertoolsTestsData.tests_input_data_dir + "/" + "DSM_PHR_Dunkerque.tif" outformat = "GeoJSON" - categoryfile = utils4test.indir + "OCS_2017_CESBIO_extract.tif" - categorydic = utils4test.indir + "OSO_nomenclature_2017.json" + categoryfile = RastertoolsTestsData.tests_input_data_dir + "/" + "OCS_2017_CESBIO_extract.tif" + categorydic = RastertoolsTestsData.tests_input_data_dir + "/" + "OSO_nomenclature_2017.json" tool = Zonalstats(statistics, area=True) - tool.with_output(utils4test.outdir, output_format=outformat) + tool.with_output(RastertoolsTestsData.tests_output_data_dir + "/", output_format=outformat) tool.with_per_category(category_file=categoryfile, category_index="Classe", category_labels_json=categorydic) tool.process_file(inputfile) @@ -202,7 +203,7 @@ def test_zonalstats_category(compare, save_gen_as_ref): # gen_files = ["DSM_PHR_Dunkerque-stats.geojson"] # do not compare, order of features can change in output # if compare: - # match, mismatch, err = utils4test.cmpfiles(utils4test.outdir, __refdir, gen_files) + # match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) # assert len(match) == 1 # assert len(mismatch) == 0 # assert len(err) == 0 @@ -238,8 +239,8 @@ def test_zonalstats_category_errors(): # cas 1 - Invalid category file statistics = "min max mean std count range sum".split() # Category inputs - categoryfile = utils4test.indir + "unknown.tif" - categorydic = utils4test.indir + "OSO_nomenclature_2017.json" + categoryfile = RastertoolsTestsData.tests_input_data_dir + "/" + "unknown.tif" + categorydic = RastertoolsTestsData.tests_input_data_dir + "/" + "OSO_nomenclature_2017.json" tool = Zonalstats(statistics) with pytest.raises(RastertoolConfigurationException) as err: @@ -248,8 +249,8 @@ def test_zonalstats_category_errors(): assert f"File {categoryfile} cannot be read: check format and existence" in str(err.value) # cas 2 - invalid category dictionary - categoryfile = utils4test.indir + "OCS_2017_CESBIO_extract.tif" - categorydic = utils4test.indir + "OSO_nomenclature_2017_wrong.json" + categoryfile = RastertoolsTestsData.tests_input_data_dir + "/" + "OCS_2017_CESBIO_extract.tif" + categorydic = RastertoolsTestsData.tests_input_data_dir + "/" + "OSO_nomenclature_2017_wrong.json" tool = Zonalstats(statistics) with pytest.raises(RastertoolConfigurationException) as err: diff --git a/tests/utils4test.py b/tests/utils4test.py index a946c66..d9ec5f4 100644 --- a/tests/utils4test.py +++ b/tests/utils4test.py @@ -21,19 +21,14 @@ class RastertoolsTestsData: tests_output_data_dir:str = str(project_dir / "tests" / "tests_out") tests_ref_data_dir:str = str(project_dir / "tests" / "tests_refs") -projectdir = RastertoolsTestsData.tests_project_dir + "/" -indir = RastertoolsTestsData.tests_input_data_dir + "/" -outdir = RastertoolsTestsData.tests_output_data_dir + "/" -__root_refdir = RastertoolsTestsData.tests_ref_data_dir + "/" - def get_refdir(testname: str): - return __root_refdir + testname + return RastertoolsTestsData.tests_ref_data_dir + '/' + testname def clear_outdir(subdirs=True): """function to clear content of a dir""" - for file in os.listdir(outdir): - file_path = os.path.join(outdir, file) + for file in os.listdir(RastertoolsTestsData.tests_output_data_dir + '/'): + file_path = os.path.join(RastertoolsTestsData.tests_output_data_dir + '/', file) if os.path.isfile(file_path): os.unlink(file_path) elif subdirs: @@ -42,8 +37,8 @@ def clear_outdir(subdirs=True): def create_outdir(): - if not os.path.isdir(outdir): - os.makedirs(outdir) + if not os.path.isdir(RastertoolsTestsData.tests_output_data_dir + '/'): + os.makedirs(RastertoolsTestsData.tests_output_data_dir + '/') clear_outdir() @@ -53,7 +48,7 @@ def delete_dir(dir): def copy_to_ref(files, refdir): for f in files: - os.replace(outdir + f, refdir + f) + os.replace(RastertoolsTestsData.tests_output_data_dir + '/' + f, refdir + f) def basename(infile): From ae532aa2f475d58b96ee58afe32e3170182ad6b4 Mon Sep 17 00:00:00 2001 From: cadauxe Date: Fri, 15 Nov 2024 13:53:44 +0100 Subject: [PATCH 08/17] install: update setup.py --- environment.yml | 2 +- setup.py | 53 +++-- src/eolab/rastertools/main.py | 14 +- src/eolab/rastertools/processing/stats.py | 192 ++++++++++++--- src/eolab/rastertools/processing/vector.py | 103 +++++--- .../rastertools/product/rasterproduct.py | 127 +++++++++- src/eolab/rastertools/radioindice.py | 221 ++++++++++++++---- src/rastertools.egg-info/PKG-INFO | 26 ++- src/rastertools.egg-info/SOURCES.txt | 4 - src/rastertools.egg-info/entry_points.txt | 4 +- src/rastertools.egg-info/requires.txt | 21 +- tests/test_radioindice.py | 9 +- tests/test_rastertools.py | 6 +- tests/test_stats.py | 1 + 14 files changed, 624 insertions(+), 159 deletions(-) diff --git a/environment.yml b/environment.yml index 414e3d2..96531f3 100644 --- a/environment.yml +++ b/environment.yml @@ -1,4 +1,4 @@ -name: rastertools +name: temp_test channels: - conda-forge diff --git a/setup.py b/setup.py index 34927a8..6c91ecf 100644 --- a/setup.py +++ b/setup.py @@ -1,29 +1,10 @@ # -*- coding: utf-8 -*- -""" - Setup file for rastertools. - Use setup.cfg to configure your project. - - This file was generated with PyScaffold 4.0.2. - PyScaffold helps you to put up the scaffold of your new Python project. - Learn more under: https://pyscaffold.org/ -""" -import os from setuptools import setup, find_packages -from sphinx.builders.html import setup_resource_paths - -with open('src/eolab/rastertools/__init__.py') as f: - for line in f: - if line.find("__version__") >= 0: - version = line.split("=")[1].strip() - version = version.strip('"') - version = version.strip("'") - break - if __name__ == "__main__": try: setup(name='rastertools', - version=version, + version="0.1.0", description=u"Collection of tools for raster data", long_description="", classifiers=[], @@ -36,15 +17,33 @@ zip_safe=False, setup_requires = ["setuptools_scm"], install_requires=[ - 'click>=4.0', - 'rasterio>=1.2.0', + 'click', + 'rasterio==1.3.0', + 'pytest>=3.6', + 'pytest-cov', + 'geopandas==0.13', + 'python-dateutil==2.9.0', + 'kiwisolver==1.4.5', + 'fonttools==4.53.1', + 'matplotlib==3.7.3', + 'packaging==24.1', + 'Shapely==1.8.5.post1', + 'tomli==2.0.2', + 'Rtree==1.3.0', + 'Pillow==9.2.0', + 'pip==24.2', + 'pyproj==3.4.0', + 'matplotlib', + 'scipy==1.8', + 'pyscaffold', + 'gdal==3.5.0', + 'tqdm==4.66' ], - extras_require={ - 'test': ['pytest>=3.6'], - }, entry_points=""" - """, - python_requires='>=3.9', + [rasterio.rio_plugins] + rastertools=eolab.rastertools.main:rastertools + """, + python_requires='==3.8.13', use_scm_version={"version_scheme": "no-guess-dev"}) except: # noqa print( diff --git a/src/eolab/rastertools/main.py b/src/eolab/rastertools/main.py index 355ae39..4e00af5 100644 --- a/src/eolab/rastertools/main.py +++ b/src/eolab/rastertools/main.py @@ -134,7 +134,7 @@ def add_custom_rastertypes(rastertypes): CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) -@with_plugins(iter_entry_points('core_package.cli_plugins')) +# @with_plugins(iter_entry_points('rasterio.plugins')) @click.group(context_settings=CONTEXT_SETTINGS) @click.option( '-t', '--rastertype', @@ -233,12 +233,12 @@ def rastertools(ctx, rastertype : str, max_workers : int, keep_vrt : bool, verbo rastertools.add_command(zonalstats, name = "zonalstats") -@rastertools.result_callback() -@click.pass_context -def handle_result(ctx): - if ctx.invoked_subcommand is None: - click.echo(ctx.get_help()) - ctx.exit() +# @rastertools.result_callback() +# @click.pass_context +# def handle_result(ctx): +# if ctx.invoked_subcommand is None: +# click.echo(ctx.get_help()) +# ctx.exit() def run(*args, **kwargs): """ diff --git a/src/eolab/rastertools/processing/stats.py b/src/eolab/rastertools/processing/stats.py index b4ae720..a4284ab 100644 --- a/src/eolab/rastertools/processing/stats.py +++ b/src/eolab/rastertools/processing/stats.py @@ -26,25 +26,49 @@ def compute_zonal_stats(geoms: gpd.GeoDataFrame, image: str, bands: List[int] = [1], stats: List[str] = ["min", "max", "mean", "std"], categorical: bool = False) -> List[List[Dict[str, float]]]: - """Compute the statistics of an input image for each feature in the shapefile + """ + Compute zonal statistics for an input raster image over specified geometries. + + This function calculates statistical summaries (e.g., min, max, mean, standard deviation) + for each feature in the provided geometries (GeoDataFrame) using the specified raster image. + If the raster is categorical, the function can compute counts of unique values. Args: geoms (GeoDataFrame): - Geometries where to compute stats + A GeoDataFrame containing geometries (e.g., polygons) for which statistics will be computed. image (str): - Filename of the input image to process - bands ([int], optional, default=[1]): - List of bands to process in the input image - stats ([str], optional, default=["min", "max","mean", "std"]): - List of stats to computed - categorical (bool, optional, default=False): - Whether to treat the input raster as categorical + Path to the raster image file to process. + bands (List[int], optional): + A list of raster band indices to process. Defaults to [1] (the first band). + stats (List[str], optional): + A list of statistical metrics to compute. Possible values include: + - "min": Minimum value within the geometry. + - "max": Maximum value within the geometry. + - "mean": Mean (average) value within the geometry. + - "std": Standard deviation within the geometry. + - Other metrics may be supported based on the implementation of `_compute_stats`. + Defaults to ["min", "max", "mean", "std"]. + categorical (bool, optional): + If True, treats the raster as categorical, computing counts of unique values within each geometry. + Defaults to False. Returns: - statistics: a list of list of dictionnaries. First list on ROI, second on bands. - Dict associates the stat names and the stat values. + List[List[Dict[str, float]]]: + A nested list where: + - The outer list corresponds to each geometry in the GeoDataFrame. + - The inner list corresponds to each band processed. + - Each dictionary contains the computed statistics, with stat names as keys and their values as values. + + Example: + ``` + import geopandas as gpd + from rastertools import compute_zonal_stats + + geoms = gpd.read_file("polygons.shp") + stats = compute_zonal_stats(geoms, "input_image.tif", bands=[1, 2], stats=["mean", "std"]) + print(stats) + ``` """ - statistics = [] nb_geoms = len(geoms) with rasterio.open(image) as src: geom_gen = (geoms.iloc[i].geometry for i in range(nb_geoms)) @@ -69,29 +93,77 @@ def compute_zonal_stats_per_category(geoms: gpd.GeoDataFrame, image: str, categories: gpd.GeoDataFrame = None, category_index: str = 'Classe', category_labels: Dict[str, str] = None): - """Compute the statistics of an input image for each feature in the shapefile + """ + Compute zonal statistics for an input raster image, categorized by specified subregions. + + This function calculates statistical metrics for a raster image over a set of geometries + (e.g., polygons) provided in `geoms`. If a set of categories (subregions within each geometry) + is provided, statistics are computed separately for each category within each geometry. Args: geoms (GeoDataFrame): - Geometries where to compute stats + A GeoDataFrame containing the input geometries (e.g., polygons) to compute + statistics over. image (str): - Filename of the input image to process - bands ([int], optional, default=[1]): - List of bands to process in the input image - stats ([str], optional, default=["min", "max", "mean", "std"]): - List of stats to computed - categories (GeoDataFrame, optional, default=None): - The geometries defining the categories. - category_index (str, optional, default='Classe'): - Name of the column in category file (when it is a vector) that - contains the category index - category_labels (Dict[str, str], optional, default=None): - Dict that associates the category values and category names + The file path to the input raster image. + bands (List[int], optional): + A list of raster band indices to process. Defaults to [1] (the first band). + stats (List[str], optional): + A list of statistical metrics to compute. Supported values include: + - "min": Minimum value within the geometry. + - "max": Maximum value within the geometry. + - "mean": Mean value within the geometry. + - "std": Standard deviation within the geometry. + Defaults to ["min", "max", "mean", "std"]. + categories (GeoDataFrame, optional): + A GeoDataFrame containing category geometries that define subregions of the + input geometries. Defaults to None. + category_index (str, optional): + The column in the `categories` GeoDataFrame that identifies category labels + for each geometry. Defaults to 'Classe'. + category_labels (Dict[str, str], optional): + A dictionary mapping category values (from `category_index`) to human-readable + labels. If provided, these labels replace category values in the output. + Defaults to None. Returns: - statistics ([[Dict[str, float]]]): a list of list of dictionnaries. - First list on ROI, second on bands. Dict associates the stat names and the stat values. - + List[List[Dict[str, float]]]: + A nested list of dictionaries containing the computed statistics: + - Outer list corresponds to each input geometry in `geoms`. + - Inner list corresponds to each raster band being processed. + - Each dictionary maps statistic names (e.g., "mean", "max") to their respective values. + + Raises: + IOError: + If any input geometry is not of type Polygon or MultiPolygon. + + Notes: + - For each geometry in `geoms`, the function subdivides it into subregions based + on the geometries in `categories` (if provided). Statistics are then computed + for each subregion separately. + - The function assumes that the raster image is georeferenced and aligned with + the coordinate system of the input geometries. + + Example: + ``` + import geopandas as gpd + from rastertools import compute_zonal_stats_per_category + + geoms = gpd.read_file("regions.shp") + categories = gpd.read_file("landcover.shp") + image_path = "satellite_image.tif" + + stats = compute_zonal_stats_per_category( + geoms=geoms, + image=image_path, + categories=categories, + category_index="Land_Type", + category_labels={1: "Forest", 2: "Urban", 3: "Water"} + ) + + for geometry_stats in stats: + print(geometry_stats) + ``` """ def _get_list_of_polygons(geom): """Get the list of polygons from the geometry""" @@ -214,19 +286,63 @@ def extract_zonal_outliers(geoms: gpd.GeoDataFrame, image: str, outliers_image: def plot_stats(chartfile: str, stats_per_date: Dict[datetime.datetime, gpd.GeoDataFrame], stats: List[str] = ["min", "max", "mean", "std"], index_name: str = 'ID', display: bool = False): - """Plot the statistics. + """ + Plot temporal statistics for geometries across multiple dates. + + This function visualizes the evolution of specified statistics (e.g., "min", "mean") + over time for different zones defined in the input GeoDataFrames. The output is + saved as a chart file, and optionally displayed. Args: chartfile (str): - Name of the chartfile to generate + Path to the file where the generated chart will be saved. stats_per_date (Dict[datetime.datetime, gpd.GeoDataFrame]): - A dict that associates a date and the statistics for this date - stats ([str], optional, default=["min", "max", "mean", "std"]): - List of stats to plot - index_name (str, optional, default='ID'): - List of bands to process in the input image - display (bool, optional, default=False): - Whether to display the generated plot + A dictionary mapping each date to a GeoDataFrame containing the statistics + for that date. Each GeoDataFrame should include the specified `index_name` + column and relevant statistics columns. + stats (List[str], optional): + A list of statistics to plot (e.g., "min", "max", "mean", "std"). Defaults + to ["min", "max", "mean", "std"]. + index_name (str, optional): + Name of the column in the GeoDataFrames that uniquely identifies the zones + (e.g., region IDs). Defaults to 'ID'. + display (bool, optional): + If `True`, the generated plot is displayed after saving. Defaults to `False`. + + Raises: + ValueError: + If the specified `index_name` is not present in the combined GeoDataFrame. + + Notes: + - The `stats_per_date` dictionary must be ordered or sortable by date to ensure + proper time-series plotting. + - Each GeoDataFrame in `stats_per_date` should have columns named in the format + `.` (e.g., "temperature.mean"). + + Example: + ``` + import geopandas as gpd + import datetime + from plot_tools import plot_stats + + # Example input + stats_per_date = { + datetime.datetime(2023, 1, 1): gpd.GeoDataFrame({...}), + datetime.datetime(2023, 2, 1): gpd.GeoDataFrame({...}), + } + + plot_stats( + chartfile="output_chart.png", + stats_per_date=stats_per_date, + stats=["mean", "std"], + index_name="RegionID", + display=True + ) + ``` + + Output: + - Saves a time-series plot of the specified statistics as `chartfile`. + - Optionally displays the plot if `display=True`. """ # convert dates to datenumber format diff --git a/src/eolab/rastertools/processing/vector.py b/src/eolab/rastertools/processing/vector.py index 22b0b1d..fa524a9 100644 --- a/src/eolab/rastertools/processing/vector.py +++ b/src/eolab/rastertools/processing/vector.py @@ -157,17 +157,34 @@ def reproject(geoms: Union[gpd.GeoDataFrame, Path, str], raster: Union[Path, str geoms_crs = _get_geoms_crs(geometries) file = raster.as_posix() if isinstance(raster, Path) else raster - with rasterio.open(file) as dataset: - if(geoms_crs != dataset.crs): - reprojected_geoms = geometries.to_crs(dataset.crs) - else: - reprojected_geoms = geometries + print(file) + + + # with rasterio.open(file) as dataset: + # print(type(dataset.crs)) + # print(geoms_crs) + # if (geoms_crs != dataset.crs): + # reprojected_geoms = geometries.to_crs(dataset.crs) + # else: + # reprojected_geoms = geometries + # print(reprojected_geoms) + # if output: + # outfile = output.as_posix() if isinstance(output, Path) else output + # reprojected_geoms.to_file(outfile, driver=driver) + + dataset = gdal.Open(file) + print(dataset.GetProjection()) + if(geoms_crs != dataset.GetProjection()): + reprojected_geoms = geometries.to_crs(dataset.GetProjection()) + else: + reprojected_geoms = geometries + print(reprojected_geoms) - if output: - outfile = output.as_posix() if isinstance(output, Path) else output - reprojected_geoms.to_file(outfile, driver=driver) + if output: + outfile = output.as_posix() if isinstance(output, Path) else output + reprojected_geoms.to_file(outfile, driver=driver) - return reprojected_geoms + return reprojected_geoms def dissolve(geoms: Union[gpd.GeoDataFrame, Path, str], @@ -189,6 +206,7 @@ def dissolve(geoms: Union[gpd.GeoDataFrame, Path, str], geometries = _get_geoms(geoms) geometries['COMMON'] = 0 + print(geometries) union = geometries.dissolve(by='COMMON', as_index=False) union = union.drop(columns='COMMON') @@ -196,6 +214,7 @@ def dissolve(geoms: Union[gpd.GeoDataFrame, Path, str], outfile = output.as_posix() if isinstance(output, Path) else output union.to_file(outfile, driver=driver) + print(union) return union @@ -308,29 +327,59 @@ def crop(input_image: Union[Path, str], roi: Union[gpd.GeoDataFrame, Path, str], """ pinput = input_image.as_posix() if isinstance(input_image, Path) else input_image + print(pinput) poutput = output_image.as_posix() if isinstance(output_image, Path) else output_image geometries = reproject(dissolve(roi), pinput) geom_bounds = geometries.total_bounds - with rasterio.open(pinput) as raster: - rst_bounds = raster.bounds - bounds = (math.floor(max(rst_bounds[0], geom_bounds[0])), - math.floor(max(rst_bounds[1], geom_bounds[1])), - math.ceil(min(rst_bounds[2], geom_bounds[2])), - math.ceil(min(rst_bounds[3], geom_bounds[3]))) - geotransform = raster.get_transform() - width = np.abs(geotransform[1]) - height = np.abs(geotransform[5]) - - ds = gdal.Warp(destNameOrDestDS=poutput, - srcDSOrSrcDSTab=pinput, - outputBounds=bounds, targetAlignedPixels=True, - cutlineDSName=roi, - cropToCutline=False, - xRes=width, yRes=height, - format="VRT") - del ds + # with rasterio.open(pinput) as raster: + # rst_bounds = raster.bounds + # + # print(rst_bounds) + # bounds = (math.floor(max(rst_bounds[0], geom_bounds[0])), + # math.floor(max(rst_bounds[1], geom_bounds[1])), + # math.ceil(min(rst_bounds[2], geom_bounds[2])), + # math.ceil(min(rst_bounds[3], geom_bounds[3]))) + # geotransform = raster.get_transform() + # + # print(geotransform) + # width = np.abs(geotransform[1]) + # height = np.abs(geotransform[5]) + # + # ds = gdal.Warp(destNameOrDestDS=poutput, + # srcDSOrSrcDSTab=pinput, + # outputBounds=bounds, targetAlignedPixels=True, + # cutlineDSName=roi, + # cropToCutline=False, + # xRes=width, yRes=height, + # format="VRT") + # del ds + + raster = gdal.Open(pinput) + geotransform = list(raster.GetGeoTransform()) + bottom = geotransform[3] + raster.RasterYSize * geotransform[5] + right = geotransform[0] + raster.RasterXSize * geotransform[1] + rst_bounds = [geotransform[0],bottom,right,geotransform[3]] + print(rst_bounds) + + bounds = (math.floor(max(rst_bounds[0], geom_bounds[0])), + math.floor(max(rst_bounds[1], geom_bounds[1])), + math.ceil(min(rst_bounds[2], geom_bounds[2])), + math.ceil(min(rst_bounds[3], geom_bounds[3]))) + width = np.abs(geotransform[1]) + height = np.abs(geotransform[5]) + + ds = gdal.Warp(destNameOrDestDS=poutput, + srcDSOrSrcDSTab=pinput, + outputBounds=bounds, targetAlignedPixels=True, + cutlineDSName=roi, + cropToCutline=False, + xRes=width, yRes=height, + format="VRT") + del ds + + def vectorize(category_raster: Union[Path, str], raster: Union[Path, str], diff --git a/src/eolab/rastertools/product/rasterproduct.py b/src/eolab/rastertools/product/rasterproduct.py index 573fe00..018d011 100644 --- a/src/eolab/rastertools/product/rasterproduct.py +++ b/src/eolab/rastertools/product/rasterproduct.py @@ -12,7 +12,11 @@ import zipfile import tempfile from uuid import uuid4 +import xml.etree.ElementTree as ET +from numpy import dtype +from rasterio.io import MemoryFile +from rasterio.vrt import WarpedVRT from osgeo import gdal import rasterio @@ -111,11 +115,29 @@ def __exit__(self, *args): """Exit method for with statement, it cleans the in memory vrt products""" self.free_in_memory_vrts() + def create_in_memory_vrt(self, vrt_content): + """ + Create an in-memory VRT using Rasterio's MemoryFile. + + Args: + vrt_content (str): The XML content of the VRT file. + """ + with MemoryFile() as memfile: + # Write the VRT content into the memory file + memfile.write(vrt_content.encode('utf-8')) + dataset = memfile.open() # Open the VRT as a dataset + self._in_memory_vrts.append(memfile) + def free_in_memory_vrts(self): - """Free in memory vrts""" + """ + Free in-memory VRTs by closing all MemoryFile objects. + """ for vrt in self._in_memory_vrts: gdal.Unlink(vrt.as_posix()) self._in_memory_vrts = [] + # for vrt in self._in_memory_vrts: + # vrt.close() # Closes and cleans up the memory file + # self._in_memory_vrts = [] @property def file(self) -> Path: @@ -379,6 +401,8 @@ def __create_vrt(self, """ # convert parameters defined as str to Path outdir = utils.to_path(self._vrt_outputdir, "/vsimem/") + print(bands_files) + print(outdir) basename = utils.get_basename(self.file) _logger.debug("Creating a VRT that handles the bands of the archive product") @@ -403,6 +427,7 @@ def __create_vrt(self, # Create a VRT image with GDAL rasterfile = outdir.joinpath(f"{uuid}{basename}.vrt") + # rasterfile = rasterfile.as_posix() ds = gdal.BuildVRT(rasterfile.as_posix(), bands, VRTNodata=' '.join(nodatavals), @@ -413,6 +438,79 @@ def __create_vrt(self, # free resource from GDAL del ds + # with rasterio.open(list(bands_files.values())[0]) as src: + # width = src.width + # height = src.height + # + # # with rasterio.open(rasterfile, 'w', driver="GTiff", width=width, height=height, count=1) as dataset: + # # dataset.write(bands[0]) + # + # with rasterio.open(rasterfile, 'w', driver="GTiff", width=width, height=height) as src: + # with WarpedVRT(src) as vrt: + # print("VRT created with dimensions:", vrt.width, vrt.height) + + + # free resource from GDAL + # - - - - - - + # # convert parameters defined as str to Path + # outdir = utils.to_path(self._vrt_outputdir, "/vsimem/") + # print(outdir) + # basename = utils.get_basename(self.file) + # rasterfile = outdir.joinpath(f"{uuid}{basename}.vrt") + # + # _logger.debug("Creating a VRT that handles the bands of the archive product") + # + # # Initialize VRT XML structure + # vrt_root = ET.Element("VRTDataset") + # + # # Open the first band to get metadata + # with rasterio.open(list(bands_files.values())[0]) as src: + # vrt_root.set("rasterXSize", str(src.width)) + # vrt_root.set("rasterYSize", str(src.height)) + # vrt_root.set("subClass", "VRTDataset") + # crs_element = ET.SubElement(vrt_root, "SRS") + # crs_element.text = src.crs.to_wkt() + # + # # Add bands + # for i, (band_id, band_path) in enumerate(bands_files.items(), start=1): + # with rasterio.open(band_path) as src_band: + # band_element = ET.SubElement(vrt_root, "VRTRasterBand", attrib={ + # "dataType": src_band.dtypes[0].upper(), + # "band": str(i) + # }) + # source_element = ET.SubElement(band_element, "SimpleSource") + # ET.SubElement(source_element, "SourceFilename", attrib={"relativeToVRT": "1"}).text = str(band_path) + # ET.SubElement(source_element, "SourceBand").text = "1" + # ET.SubElement(source_element, "SourceProperties", attrib={ + # "RasterXSize": str(src_band.width), + # "RasterYSize": str(src_band.height), + # "DataType": src_band.dtypes[0].upper(), + # "BlockXSize": str(src_band.block_shapes[0][0]), + # "BlockYSize": str(src_band.block_shapes[0][1]) + # }) + # ET.SubElement(source_element, "SrcRect", attrib={ + # "xOff": "0", "yOff": "0", + # "xSize": str(src_band.width), "ySize": str(src_band.height) + # }) + # ET.SubElement(source_element, "DstRect", attrib={ + # "xOff": "0", "yOff": "0", + # "xSize": str(src_band.width), "ySize": str(src_band.height) + # }) + # + # # Add masks + # for i, (mask_id, mask_path) in enumerate(masks_files.items(), start=len(bands_files) + 1): + # mask_element = ET.SubElement(vrt_root, "VRTRasterBand", attrib={ + # "dataType": "Byte", + # "band": str(i) + # }) + # source_element = ET.SubElement(mask_element, "SimpleSource") + # ET.SubElement(source_element, "SourceFilename", attrib={"relativeToVRT": "1"}).text = str(mask_path) + # ET.SubElement(source_element, "SourceBand").text = "1" + # + # # Write the VRT to file + # tree = ET.ElementTree(vrt_root) + # with open(rasterfile, "wb") as f: + # tree.write(f, encoding="utf-8", xml_declaration=True) return rasterfile def __wrap(self, input_vrt: Path, roi: Path, uuid: str = "") -> Path: @@ -489,6 +587,33 @@ def __apply_masks(self, input_vrt: Path, nb_bands: int, nb_masks: int, uuid: str return masked_image + # # convert parameters defined as str to Path + # outdir = utils.to_path(self._vrt_outputdir, "/vsimem/") + # basename = utils.get_basename(self.file) + # + # # create a tempdir for generated temporary files + # tempdir = Path(tempfile.gettempdir()) + # temp_image = tempdir.joinpath(f"{uuid}{basename}-temp.vrt") + # with rasterio.open(input_vrt) as src: + # # Create a new in-memory VRT for the bands + # with WarpedVRT(src, crs=src.crs, transform=src.transform, width=src.width, height=src.height, + # count=nb_bands) as vrt: + # # Here you would apply any mask band logic. For simplicity, we assume a method that adds the masks. + # # For each mask, you could apply it using rasterio to create new bands with the mask applied. + # # Add the masks to the VRT using the provided add_masks_to_vrt function + # _logger.debug("Adding mask bands to VRT") + # masks_index = list(range(nb_bands + 1, nb_bands + nb_masks + 1)) + # vrt_new_content = add_masks_to_vrt(temp_image, input_vrt, masks_index, + # self.rastertype.maskfunc) + # + # # Now save this VRT to a file + # masked_image = outdir.joinpath(f"{uuid}{basename}-mask.vrt") + # with open(masked_image, 'wb') as out_vrt: + # out_vrt.write(vrt_new_content) + # + # _logger.debug(f"Generated masked VRT saved to {masked_image}") + # return masked_image + def _extract_bands(inputfile: Path, bands_pattern: str, diff --git a/src/eolab/rastertools/radioindice.py b/src/eolab/rastertools/radioindice.py index 87dd168..88c68aa 100644 --- a/src/eolab/rastertools/radioindice.py +++ b/src/eolab/rastertools/radioindice.py @@ -10,9 +10,13 @@ from pathlib import Path from typing import List import threading +import geopandas as gpd +import numpy as np import rasterio import numpy.ma as ma +from osgeo import gdal +from rasterio import CRS from tqdm import tqdm from eolab.rastertools import utils @@ -427,7 +431,11 @@ def process_file(self, inputfile: str) -> List[str]: indices.append(indice) # get the raster + print(self.roi) + # self.roi = "tests/tests_data/COMMUNE_32001.shp" + # print(self.roi) raster = product.get_raster(roi=self.roi) + print(type(raster)) # STEP 2: Compute the indices outputs = [] @@ -451,6 +459,51 @@ def process_file(self, inputfile: str) -> List[str]: # return the list of generated files return outputs +def get_raster_profile(raster): + # Open the dataset + + # Get the raster driver + driver = raster.GetDriver().ShortName + + # Get raster dimensions + width = raster.RasterXSize + height = raster.RasterYSize + count = raster.RasterCount + + # Get geotransform and projection + geotransform = raster.GetGeoTransform() + crs = raster.GetProjection() + + # Get data type and block size from the first band + band = raster.GetRasterBand(1) + dtype = gdal.GetDataTypeName(band.DataType) + dtype_rasterio = rasterio.dtypes.get_minimum_dtype(band.DataType) + nodata = band.GetNoDataValue() + + # Get block size and tiled status + blockxsize, blockysize = band.GetBlockSize() + tiled = raster.GetMetadata('IMAGE_STRUCTURE').get('TILED', 'NO') == 'YES' + + # Convert geotransform to Rasterio-compatible affine transform + transform = rasterio.Affine.from_gdal(*geotransform) + + # Build a profile dictionary similar to rasterio + profile = { + "driver": driver, # e.g., "GTiff" + "width": width, + "height": height, + "count": count, + "crs": crs, + "transform": transform, # Affine geotransform + "dtype": dtype_rasterio, + "nodata": nodata, # Nodata value + "blockxsize": blockxsize, # Block width + "blockysize": blockysize, # Block height + "tiled": tiled # Whether the raster is tiled + } + + return profile + def compute_indices(input_image: str, image_channels: List[BandChannel], indice_image: str, indices: List[RadioindiceProcessing], @@ -474,56 +527,142 @@ def compute_indices(input_image: str, image_channels: List[BandChannel], window_size (tuple(int, int), optional, default=(1024, 1024)): Size of windows for splitting the processed image in small parts """ - with rasterio.Env(GDAL_VRT_ENABLE_PYTHON=True): - with rasterio.open(input_image) as src: - profile = src.profile - - # set block size to the configured window_size of first indice - blockxsize, blockysize = window_size - if src.width < blockxsize: - blockxsize = utils.highest_power_of_2(src.width) - if src.height < blockysize: - blockysize = utils.highest_power_of_2(src.height) - - # dtype of output data - dtype = indices[0].dtype or rasterio.float32 - - # setup profile for output image - profile.update(driver='GTiff', + print('...'*50) + print(input_image) + # with rasterio.Env(GDAL_VRT_ENABLE_PYTHON=True): + # with rasterio.open(input_image) as src: + # profile = src.profile + # + # print(profile) + # + # # set block size to the configured window_size of first indice + # blockxsize, blockysize = window_size + # print(src.width) + # print(blockxsize) + # if src.width < blockxsize: + # blockxsize = utils.highest_power_of_2(src.width) + # if src.height < blockysize: + # blockysize = utils.highest_power_of_2(src.height) + # + # # dtype of output data + # dtype = indices[0].dtype or rasterio.float32 + # + # # setup profile for output image + # profile.update(driver='GTiff', + # blockxsize=blockysize, blockysize=blockxsize, tiled=True, + # dtype=dtype, nodata=indices[0].nodata, + # count=len(indices)) + # print(type(profile)) + # with rasterio.open(indice_image, "w", **profile) as dst: + # # Materialize a list of destination block windows + # windows = [window for ij, window in dst.block_windows()] + # + # # disable status of tqdm progress bar + # disable = os.getenv("RASTERTOOLS_NOTQDM", 'False').lower() in ['true', '1'] + # + # # compute every indices + # for i, indice in enumerate(indices, 1): + # # Get the bands necessary to compute the indice + # bands = [image_channels.index(channel) + 1 for channel in indice.channels] + # + # read_lock = threading.Lock() + # write_lock = threading.Lock() + # + # def process(window): + # """Read input raster, compute indice and write output raster""" + # with read_lock: + # src_array = src.read(bands, window=window, masked=True) + # src_array[src_array == src.nodata] = ma.masked + # src_array = src_array.astype(dtype) + # + # # The computation can be performed concurrently + # result = indice.algo(src_array).astype(dtype).filled(indice.nodata) + # + # with write_lock: + # dst.write_band(i, result, window=window) + # + # # compute using concurrent.futures.ThreadPoolExecutor and tqdm + # for window in tqdm(windows, disable=disable, desc=f"{indice.name}"): + # process(window) + # + # dst.set_band_description(i, indice.name) + # with rasterio.Env(GDAL_VRT_ENABLE_PYTHON=True): + # with rasterio.open(input_image) as src: + # profile = src.profile + + src = gdal.Open(input_image) + profile = get_raster_profile(src) ############################# + print(profile) + geotransform = list(src.GetGeoTransform()) + width = src.RasterXSize + height = src.RasterYSize + + # set block size to the configured window_size of first indice + blockxsize, blockysize = window_size + print(width) + print(blockxsize) + + if width < blockxsize: + blockxsize = utils.highest_power_of_2(width) + if height < blockysize: + blockysize = utils.highest_power_of_2(height) + + # dtype of output data + dtype = indices[0].dtype or rasterio.float32 + + # setup profile for output image + profile.update(driver='GTiff', blockxsize=blockysize, blockysize=blockxsize, tiled=True, dtype=dtype, nodata=indices[0].nodata, count=len(indices)) - with rasterio.open(indice_image, "w", **profile) as dst: - # Materialize a list of destination block windows - windows = [window for ij, window in dst.block_windows()] + print(profile) + print(type(profile)) + nodata = src.GetRasterBand(1).GetNoDataValue() + + with rasterio.open(indice_image, "w", **profile) as dst: + # Materialize a list of destination block windows + windows = [window for ij, window in dst.block_windows()] + print(windows) + + # disable status of tqdm progress bar + disable = os.getenv("RASTERTOOLS_NOTQDM", 'False').lower() in ['true', '1'] + + # compute every indices + for i, indice in enumerate(indices, 1): + # Get the bands necessary to compute the indice + bands = [image_channels.index(channel) + 1 for channel in indice.channels] + + read_lock = threading.Lock() + write_lock = threading.Lock() - # disable status of tqdm progress bar - disable = os.getenv("RASTERTOOLS_NOTQDM", 'False').lower() in ['true', '1'] + def process(window): + """Read input raster, compute indice and write output raster""" + with read_lock: + x_offset = int(window.col_off) + y_offset = int(window.row_off) + x_size = int(window.width) + y_size = int(window.height) - # compute every indices - for i, indice in enumerate(indices, 1): - # Get the bands necessary to compute the indice - bands = [image_channels.index(channel) + 1 for channel in indice.channels] + src_array = np.array([ + src.GetRasterBand(band).ReadAsArray(x_offset, y_offset, x_size, y_size) + for band in bands], dtype=dtype) - read_lock = threading.Lock() - write_lock = threading.Lock() + # Mask nodata values + src_array = np.ma.masked_equal(src_array, nodata) - def process(window): - """Read input raster, compute indice and write output raster""" - with read_lock: - src_array = src.read(bands, window=window, masked=True) - src_array[src_array == src.nodata] = ma.masked - src_array = src_array.astype(dtype) + # src_array = src.read(bands, window=window, masked=True) + # src_array[src_array == src.nodata] = ma.masked + # src_array = src_array.astype(dtype) - # The computation can be performed concurrently - result = indice.algo(src_array).astype(dtype).filled(indice.nodata) + # The computation can be performed concurrently + result = indice.algo(src_array).astype(dtype).filled(indice.nodata) - with write_lock: - dst.write_band(i, result, window=window) + with write_lock: + dst.write_band(i, result, window=window) - # compute using concurrent.futures.ThreadPoolExecutor and tqdm - for window in tqdm(windows, disable=disable, desc=f"{indice.name}"): - process(window) + # compute using concurrent.futures.ThreadPoolExecutor and tqdm + for window in tqdm(windows, disable=disable, desc=f"{indice.name}"): + process(window) - dst.set_band_description(i, indice.name) + dst.set_band_description(i, indice.name) diff --git a/src/rastertools.egg-info/PKG-INFO b/src/rastertools.egg-info/PKG-INFO index 4cc1420..7be60b8 100644 --- a/src/rastertools.egg-info/PKG-INFO +++ b/src/rastertools.egg-info/PKG-INFO @@ -1,8 +1,8 @@ Metadata-Version: 2.1 Name: rastertools -Version: 0.6.1.post1.dev19+gebce3d0.d20241113 -Summary: Compute radiometric indices and zonal statistics on rasters -Home-page: https://github.com/cnes/rastertools +Version: 0.1.0 +Summary: Collection of tools for raster data +Home-page: https://github.com/CNES/rastertools Author: Olivier Queyrut Author-email: olivier.queyrut@cnes.fr License: apache v2 @@ -12,10 +12,28 @@ Project-URL: Issues, https://github.com/cnes/rastertools/issues Platform: any Classifier: Development Status :: 5 - Production/Stable Classifier: Programming Language :: Python +Requires-Python: ==3.8.13 Description-Content-Type: text/x-rst; charset=UTF-8 License-File: LICENSE.txt License-File: AUTHORS.rst -Requires-Dist: importlib-metadata; python_version < "3.8" +Requires-Dist: click +Requires-Dist: rasterio==1.3.0 +Requires-Dist: pytest>=3.6 +Requires-Dist: pytest-cov +Requires-Dist: geopandas==0.13 +Requires-Dist: python-dateutil==2.9.0 +Requires-Dist: kiwisolver==1.4.5 +Requires-Dist: fonttools==4.53.1 +Requires-Dist: matplotlib==3.7.3 +Requires-Dist: packaging==24.1 +Requires-Dist: Shapely==1.8.5.post1 +Requires-Dist: tomli==2.0.2 +Requires-Dist: Pillow==9.2.0 +Requires-Dist: pip==24.2 +Requires-Dist: pyproj==3.4.0 +Requires-Dist: matplotlib +Requires-Dist: scipy==1.8 +Requires-Dist: tqdm==4.66 Provides-Extra: testing Requires-Dist: setuptools; extra == "testing" Requires-Dist: pytest; extra == "testing" diff --git a/src/rastertools.egg-info/SOURCES.txt b/src/rastertools.egg-info/SOURCES.txt index d2e4162..5d7f921 100644 --- a/src/rastertools.egg-info/SOURCES.txt +++ b/src/rastertools.egg-info/SOURCES.txt @@ -81,7 +81,6 @@ src/eolab/rastertools/__pycache__/utils.cpython-38.pyc src/eolab/rastertools/__pycache__/zonalstats.cpython-38.pyc src/eolab/rastertools/cli/__init__.py src/eolab/rastertools/cli/filtering.py -src/eolab/rastertools/cli/filtering_dyn.py src/eolab/rastertools/cli/hillshade.py src/eolab/rastertools/cli/radioindice.py src/eolab/rastertools/cli/speed.py @@ -192,9 +191,6 @@ tests/tests_data/SPOT6_2018_France-Ortho_NC_DRS-MS_SPOT6_2018_FRANCE_ORTHO_NC_GE tests/tests_data/SPOT6_2018_France-Ortho_NC_DRS-MS_SPOT6_2018_FRANCE_ORTHO_NC_GEOSUD_MS_82.tar.gz.properties tests/tests_data/additional_rastertypes.json tests/tests_data/grid.geojson -tests/tests_data/listing.lst -tests/tests_data/listing2.lst -tests/tests_data/listing3.lst tests/tests_data/tif_file.tif tests/tests_data/toulouse-mnh.tif tests/tests_refs/test_radioindice/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi-speed-20180928-105515.tif diff --git a/src/rastertools.egg-info/entry_points.txt b/src/rastertools.egg-info/entry_points.txt index 4366a5f..7844cc6 100644 --- a/src/rastertools.egg-info/entry_points.txt +++ b/src/rastertools.egg-info/entry_points.txt @@ -1,2 +1,2 @@ -[console_scripts] -rastertools = eolab.rastertools.main:run +[rasterio.plugins] +rastertools = src.eolab.rastertools.main:rastertools diff --git a/src/rastertools.egg-info/requires.txt b/src/rastertools.egg-info/requires.txt index a5ca8f7..b92b22e 100644 --- a/src/rastertools.egg-info/requires.txt +++ b/src/rastertools.egg-info/requires.txt @@ -1,6 +1,21 @@ - -[:python_version < "3.8"] -importlib-metadata +click +rasterio==1.3.0 +pytest>=3.6 +pytest-cov +geopandas==0.13 +python-dateutil==2.9.0 +kiwisolver==1.4.5 +fonttools==4.53.1 +matplotlib==3.7.3 +packaging==24.1 +Shapely==1.8.5.post1 +tomli==2.0.2 +Pillow==9.2.0 +pip==24.2 +pyproj==3.4.0 +matplotlib +scipy==1.8 +tqdm==4.66 [testing] setuptools diff --git a/tests/test_radioindice.py b/tests/test_radioindice.py index a0306b2..a121497 100644 --- a/tests/test_radioindice.py +++ b/tests/test_radioindice.py @@ -1,5 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +import os import pytest import filecmp @@ -37,12 +38,18 @@ def test_radioindice_process_file_merge(): # create output dir and clear its content if any utils4test.create_outdir() + origin_path = RastertoolsTestsData.tests_input_data_dir + "/".split(os.getcwd() + "/")[-1] + inputfile = RastertoolsTestsData.tests_input_data_dir + "/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip" + print(inputfile) + inputfile = origin_path+ "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip" + print(inputfile) indices = [indice for indice in Radioindice.get_default_indices()] tool = Radioindice(indices) + shapefile = RastertoolsTestsData.tests_input_data_dir + "/COMMUNE_32001.shp" tool.with_output(RastertoolsTestsData.tests_output_data_dir , merge=True) - tool.with_roi(RastertoolsTestsData.tests_input_data_dir + "/COMMUNE_32001.shp") + tool.with_roi(shapefile) outputs = tool.process_file(inputfile) assert outputs == [ diff --git a/tests/test_rastertools.py b/tests/test_rastertools.py index 5a98448..57f7493 100644 --- a/tests/test_rastertools.py +++ b/tests/test_rastertools.py @@ -283,9 +283,9 @@ def test_speed_command_line_default(): # default with a listing of S2A products f"-v sp -b 1 -o {RastertoolsTestsData.tests_output_data_dir} {lst_file_path}", # default with a list of files - f"--verbose speed --output {RastertoolsTestsData.tests_output_data_dir}" - f" {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" - f" {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif" + # f"--verbose speed --output {RastertoolsTestsData.tests_output_data_dir}" + # f" {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" + # f" {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif" ] speed_filenames = [ ["SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-speed-20180928-105515.tif"], diff --git a/tests/test_stats.py b/tests/test_stats.py index cef6a98..d4b00e5 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -212,6 +212,7 @@ def test_compute_zonal_stats_per_category(): catlabels = RastertoolsTestsData.tests_input_data_dir + "/" + "OSO_nomenclature_2017.json" stats_to_compute = DEFAULT_STATS bands = [1] + print(vector.clip(catgeojson, raster)) geometries = vector.reproject(vector.filter(geojson, raster), raster) categories = vector.reproject(vector.clip(catgeojson, raster), raster) From a0b8766bbca663f6f9d618ec9dbcf424ef8fe9f0 Mon Sep 17 00:00:00 2001 From: Arthur VINCENT Date: Fri, 29 Nov 2024 14:38:07 +0100 Subject: [PATCH 09/17] feat: add vsimem_to_rasterio function --- src/eolab/rastertools/processing/stats.py | 27 +++--- src/eolab/rastertools/processing/vector.py | 31 ++++--- .../rastertools/product/rasterproduct.py | 8 +- src/eolab/rastertools/utils.py | 91 +++++++++++++++++++ src/eolab/rastertools/zonalstats.py | 22 +++-- tests/test_rasterproduct.py | 12 +-- 6 files changed, 140 insertions(+), 51 deletions(-) diff --git a/src/eolab/rastertools/processing/stats.py b/src/eolab/rastertools/processing/stats.py index a4284ab..ae78271 100644 --- a/src/eolab/rastertools/processing/stats.py +++ b/src/eolab/rastertools/processing/stats.py @@ -18,7 +18,7 @@ from rasterio import features from tqdm import tqdm -from eolab.rastertools.utils import get_metadata_name +from eolab.rastertools.utils import get_metadata_name, vsimem_to_rasterio from eolab.rastertools.processing.vector import rasterize, filter_dissolve @@ -70,20 +70,21 @@ def compute_zonal_stats(geoms: gpd.GeoDataFrame, image: str, ``` """ nb_geoms = len(geoms) - with rasterio.open(image) as src: - geom_gen = (geoms.iloc[i].geometry for i in range(nb_geoms)) - geom_windows = ((geom, features.geometry_window(src, [geom])) for geom in geom_gen) - - statistics = [] - disable = os.getenv("RASTERTOOLS_NOTQDM", 'False').lower() in ['true', '1'] - for geom, window in tqdm(geom_windows, total=nb_geoms, disable=disable, desc="zonalstats"): - data = src.read(bands, window=window) - transform = src.window_transform(window) - s = _compute_stats((data, transform, [geom], window), - src.nodata, stats, categorical) - statistics.append(s) + src = vsimem_to_rasterio(image) + geom_gen = (geoms.iloc[i].geometry for i in range(nb_geoms)) + geom_windows = ((geom, features.geometry_window(src, [geom])) for geom in geom_gen) + statistics = [] + disable = os.getenv("RASTERTOOLS_NOTQDM", 'False').lower() in ['true', '1'] + for geom, window in tqdm(geom_windows, total=nb_geoms, disable=disable, desc="zonalstats"): + data = src.read(bands, window=window) + transform = src.window_transform(window) + + s = _compute_stats((data, transform, [geom], window), + src.nodata, stats, categorical) + statistics.append(s) + src.close() return statistics diff --git a/src/eolab/rastertools/processing/vector.py b/src/eolab/rastertools/processing/vector.py index fa524a9..9787fa7 100644 --- a/src/eolab/rastertools/processing/vector.py +++ b/src/eolab/rastertools/processing/vector.py @@ -66,25 +66,26 @@ def filter(geoms: Union[gpd.GeoDataFrame, Path, str], raster: Union[Path, str], geoms_crs = _get_geoms_crs(geometries) file = raster.as_posix() if isinstance(raster, Path) else raster - with rasterio.open(file) as dataset: - l, b, r, t = dataset.bounds - px, py = ([l, l, r, r], [b, t, t, b]) - if(geoms_crs != dataset.crs): - px, py = warp.transform(dataset.crs, geoms_crs, [l, l, r, r], [b, t, t, b]) + dataset = utils.vsimem_to_rasterio(file) + l, b, r, t = dataset.bounds + px, py = ([l, l, r, r], [b, t, t, b]) - polygon = shapely.geometry.Polygon([(x, y) for x, y in zip(px, py)]) - if within: - # convert geometries into GeoPandasBaseExtended to use the new cix property - filtered_geoms = geometries[geometries.within(polygon)] - else: - filtered_geoms = geometries[geometries.intersects(polygon)] + if(geoms_crs != dataset.crs): + px, py = warp.transform(dataset.crs, geoms_crs, [l, l, r, r], [b, t, t, b]) - if output: - outfile = output.as_posix() if isinstance(output, Path) else output - filtered_geoms.to_file(outfile, driver=driver) + polygon = shapely.geometry.Polygon([(x, y) for x, y in zip(px, py)]) + if within: + # convert geometries into GeoPandasBaseExtended to use the new cix property + filtered_geoms = geometries[geometries.within(polygon)] + else: + filtered_geoms = geometries[geometries.intersects(polygon)] - return filtered_geoms + if output: + outfile = output.as_posix() if isinstance(output, Path) else output + filtered_geoms.to_file(outfile, driver=driver) + dataset.close() + return filtered_geoms def clip(geoms: Union[gpd.GeoDataFrame, Path, str], raster: Union[Path, str], diff --git a/src/eolab/rastertools/product/rasterproduct.py b/src/eolab/rastertools/product/rasterproduct.py index 018d011..294dda8 100644 --- a/src/eolab/rastertools/product/rasterproduct.py +++ b/src/eolab/rastertools/product/rasterproduct.py @@ -12,11 +12,8 @@ import zipfile import tempfile from uuid import uuid4 -import xml.etree.ElementTree as ET -from numpy import dtype from rasterio.io import MemoryFile -from rasterio.vrt import WarpedVRT from osgeo import gdal import rasterio @@ -24,6 +21,7 @@ from eolab.rastertools.product import RasterType from eolab.rastertools.product.vrt import add_masks_to_vrt, set_band_descriptions from eolab.rastertools.processing.vector import crop +from eolab.rastertools.utils import vsimem_to_rasterio __author__ = "Olivier Queyrut" __copyright__ = "Copyright 2019, CNES" @@ -229,8 +227,7 @@ def open(self, bands: Union[str, List[str]] = "all", masks: Union[str, List[str]] = "all", roi: Union[Path, str] = None): - """Proxy method to rasterio.open(rasterproduct.get_raster(...))""" - return rasterio.open(self.get_raster(bands=bands, masks=masks, roi=roi)) + return vsimem_to_rasterio(self.get_raster(bands=bands, masks=masks, roi=roi)) def get_raster(self, bands: Union[str, List[str]] = "all", @@ -304,7 +301,6 @@ def get_raster(self, set_band_descriptions(masked_image, band_descriptions) rasterfile = masked_image - return rasterfile.as_posix() def __get_bands(self, bands: Union[str, List[str]] = "all"): diff --git a/src/eolab/rastertools/utils.py b/src/eolab/rastertools/utils.py index 7dd9b14..417e2c7 100644 --- a/src/eolab/rastertools/utils.py +++ b/src/eolab/rastertools/utils.py @@ -9,8 +9,99 @@ - ... """ import math +import tempfile from pathlib import Path +import rasterio +from osgeo import gdal + + +def vsimem_to_rasterio(vsimem_file:str, nodata=None) -> rasterio.io.DatasetReader: + """ + Converts a VSIMEM (in-memory) raster dataset to a Rasterio dataset with optional nodata masking. + + This function opens a raster dataset stored in VSIMEM (virtual file system in memory) + using GDAL, extracts its metadata and data, handles optional nodata values and masks, + and saves it to a temporary GeoTIFF file. It then reopens the file with Rasterio and + returns a Rasterio dataset reader. + + Parameters + ---------- + vsimem_file : str + The path to the VSIMEM file to be converted. This file should be an in-memory GDAL dataset. + + nodata : float, optional + A user-defined nodata value to override the nodata value in the GDAL dataset. + If not provided, the nodata value from the GDAL dataset is used (if available). + + Returns + ------- + rasterio.io.DatasetReader + A Rasterio dataset reader object corresponding to the temporary GeoTIFF created from the VSIMEM file. + + Notes + ----- + - The function assumes the dataset is in a format that is compatible with both GDAL and Rasterio. + - The created temporary file is not deleted automatically. It can be removed manually after use. + - The function reads all raster bands from the dataset, applies the optional nodata masking, + and writes the data to a new GeoTIFF file. + - If a nodata value is provided, the function will apply the mask based on that value to each band. + If no nodata value is set, no mask is applied. + """ + gdal_ds = gdal.Open(vsimem_file) + cols = gdal_ds.RasterXSize + rows = gdal_ds.RasterYSize + bands = gdal_ds.RasterCount + geo_transform = gdal_ds.GetGeoTransform() + projection = gdal_ds.GetProjection() + + gdal_dtype_to_numpy = { + gdal.GDT_Byte: "uint8", + gdal.GDT_UInt16: "uint16", + gdal.GDT_Int16: "int16", + gdal.GDT_UInt32: "uint32", + gdal.GDT_Int32: "int32", + gdal.GDT_Float32: "float32", + gdal.GDT_Float64: "float64", + } + dtype = gdal_dtype_to_numpy[gdal_ds.GetRasterBand(1).DataType] + + data = [gdal_ds.GetRasterBand(i + 1).ReadAsArray() for i in range(bands)] + + masks = [] + for i in range(bands): + band = gdal_ds.GetRasterBand(i + 1) + band_nodata = band.GetNoDataValue() + # Prioriser la valeur nodata de l'utilisateur + nodata_value = nodata if nodata is not None else band_nodata + if nodata_value is not None: + masks.append(data[i] == nodata_value) + else: + masks.append(None) + + with tempfile.NamedTemporaryFile(suffix=".tif", delete=False) as tmpfile: + temp_filename = tmpfile.name + + profile = { + "driver": "GTiff", + "height": rows, + "width": cols, + "count": bands, + "dtype": dtype, + "crs": projection, + "transform": rasterio.transform.Affine.from_gdal(*geo_transform), + "nodata": nodata_value, + } + + with rasterio.open(temp_filename, "w", **profile) as dst: + # Écrire les données + for i, band_data in enumerate(data, start=1): + dst.write(band_data, i) + # Si un masque est défini, l'écrire + if masks[i - 1] is not None: + dst.write_mask((masks[i - 1]).astype("uint8") * 255) + + return rasterio.open(temp_filename) def to_tuple(val): """Convert val as a tuple of two val""" diff --git a/src/eolab/rastertools/zonalstats.py b/src/eolab/rastertools/zonalstats.py index bb36621..29d7bd6 100644 --- a/src/eolab/rastertools/zonalstats.py +++ b/src/eolab/rastertools/zonalstats.py @@ -17,7 +17,6 @@ """ from typing import List, Dict import datetime -import logging import logging.config from pathlib import Path import json @@ -33,6 +32,7 @@ from eolab.rastertools.processing import extract_zonal_outliers, plot_stats from eolab.rastertools.processing import vector from eolab.rastertools.product import RasterProduct +from eolab.rastertools.utils import vsimem_to_rasterio _logger = logging.getLogger(__name__) @@ -408,15 +408,17 @@ def process_file(self, inputfile: str) -> List[str]: # open raster to get metadata raster = product.get_raster() - with rasterio.open(raster) as rst: - bound = int(rst.count) - indexes = rst.indexes - descr = rst.descriptions - - geotransform = rst.get_transform() - width = np.abs(geotransform[1]) - height = np.abs(geotransform[5]) - area_square_meter = width * height + + rst = vsimem_to_rasterio(raster) + bound = int(rst.count) + indexes = rst.indexes + descr = rst.descriptions + + geotransform = rst.get_transform() + width = np.abs(geotransform[1]) + height = np.abs(geotransform[5]) + area_square_meter = width * height + rst.close() date_str = product.get_date_string('%Y%m%d-%H%M%S') diff --git a/tests/test_rasterproduct.py b/tests/test_rasterproduct.py index 1135e2d..7d470aa 100644 --- a/tests/test_rasterproduct.py +++ b/tests/test_rasterproduct.py @@ -1,9 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- - import pytest import os -import filecmp import zipfile from pathlib import Path from datetime import datetime @@ -12,7 +10,7 @@ from eolab.rastertools.product import RasterType, BandChannel from eolab.rastertools.product import RasterProduct - +from eolab.rastertools.utils import vsimem_to_rasterio from . import utils4test __author__ = "Olivier Queyrut" @@ -362,10 +360,10 @@ def test_create_product_special_cases(): # check if product can be opened by rasterio with rasterio.Env(GDAL_VRT_ENABLE_PYTHON=True): - with rasterio.open(raster) as dataset: - data = dataset.read([1], masked=True) - # pixel corresponding to a value > 0 for a band mask => masked value - assert data.mask[0][350][250] + dataset = vsimem_to_rasterio(raster, nodata=-10000) + data = dataset.read([1], masked=True) + # pixel corresponding to a value > 0 for a band mask => masked value + assert data.mask[0][350][250] # creation from a vrt file = "S2A_MSIL2A_20190116T105401_N0211_R051_T30TYP_20190116T120806.vrt" From 26af4c683cd1a6b74f7577aa88c1af5cff0ac799 Mon Sep 17 00:00:00 2001 From: cadauxe Date: Mon, 2 Dec 2024 10:54:09 +0100 Subject: [PATCH 10/17] refactor: cleaning --- setup.py | 4 +- src/eolab/rastertools/filtering.py | 1 - src/eolab/rastertools/hillshade.py | 1 - src/eolab/rastertools/main.py | 9 - src/eolab/rastertools/processing/sliding.py | 83 ++++---- src/eolab/rastertools/processing/vector.py | 66 ++---- .../rastertools/product/rasterproduct.py | 106 ---------- src/eolab/rastertools/radioindice.py | 197 +++++------------- src/eolab/rastertools/speed.py | 1 - src/eolab/rastertools/svf.py | 1 - src/eolab/rastertools/timeseries.py | 1 - src/eolab/rastertools/utils.py | 2 +- src/rastertools.egg-info/PKG-INFO | 4 + src/rastertools.egg-info/SOURCES.txt | 1 - src/rastertools.egg-info/entry_points.txt | 4 +- src/rastertools.egg-info/requires.txt | 4 + tests/test_radioindice.py | 9 +- tests/test_rasterproduct.py | 4 +- tests/test_rastertools.py | 18 +- tests/test_rastertype.py | 1 - tests/test_speed.py | 2 - tests/test_stats.py | 2 - tests/test_tiling.py | 1 - tests/test_vector.py | 1 - tests/test_zonalstats.py | 1 - tests/utils4test.py | 1 + 26 files changed, 137 insertions(+), 388 deletions(-) diff --git a/setup.py b/setup.py index 6c91ecf..0c119b1 100644 --- a/setup.py +++ b/setup.py @@ -30,10 +30,12 @@ 'Shapely==1.8.5.post1', 'tomli==2.0.2', 'Rtree==1.3.0', + 'fiona==1.8.21', 'Pillow==9.2.0', + 'sphinx_rtd_theme==3.0.1', 'pip==24.2', 'pyproj==3.4.0', - 'matplotlib', + 'sphinx==7.1.2', 'scipy==1.8', 'pyscaffold', 'gdal==3.5.0', diff --git a/src/eolab/rastertools/filtering.py b/src/eolab/rastertools/filtering.py index 3f503b1..64fcea2 100644 --- a/src/eolab/rastertools/filtering.py +++ b/src/eolab/rastertools/filtering.py @@ -4,7 +4,6 @@ This module defines a rastertool named Filtering that can apply different kind of filters on raster images. """ -import logging import logging.config from typing import List, Dict from pathlib import Path diff --git a/src/eolab/rastertools/hillshade.py b/src/eolab/rastertools/hillshade.py index 0164636..7fbbeb9 100644 --- a/src/eolab/rastertools/hillshade.py +++ b/src/eolab/rastertools/hillshade.py @@ -4,7 +4,6 @@ This module defines a rastertool named Hillshade which computes the hillshade of a Digital Height Model corresponding to a given solar position (elevation and azimuth). """ -import logging import logging.config from typing import List from pathlib import Path diff --git a/src/eolab/rastertools/main.py b/src/eolab/rastertools/main.py index 4e00af5..88bbf0f 100644 --- a/src/eolab/rastertools/main.py +++ b/src/eolab/rastertools/main.py @@ -16,8 +16,6 @@ import sys import json import click -from pkg_resources import iter_entry_points -from click_plugins import with_plugins from eolab.rastertools.cli.filtering import filter from eolab.rastertools.cli.hillshade import hillshade from eolab.rastertools.cli.speed import speed @@ -233,13 +231,6 @@ def rastertools(ctx, rastertype : str, max_workers : int, keep_vrt : bool, verbo rastertools.add_command(zonalstats, name = "zonalstats") -# @rastertools.result_callback() -# @click.pass_context -# def handle_result(ctx): -# if ctx.invoked_subcommand is None: -# click.echo(ctx.get_help()) -# ctx.exit() - def run(*args, **kwargs): """ Entry point for console_scripts diff --git a/src/eolab/rastertools/processing/sliding.py b/src/eolab/rastertools/processing/sliding.py index ccc59e7..32062d7 100644 --- a/src/eolab/rastertools/processing/sliding.py +++ b/src/eolab/rastertools/processing/sliding.py @@ -4,7 +4,6 @@ This module defines a method to run a RasterProcessing on sliding windows. """ from itertools import repeat -import logging import logging.config import os from typing import List @@ -18,7 +17,7 @@ from eolab.rastertools import utils from eolab.rastertools.processing import RasterProcessing - +from eolab.rastertools.utils import vsimem_to_rasterio _logger = logging.getLogger(__name__) @@ -56,46 +55,48 @@ def compute_sliding(input_image: str, output_image: str, rasterprocessing: Raste specified, and sliding window indices are computed internally. """ with rasterio.Env(GDAL_VRT_ENABLE_PYTHON=True): - with rasterio.open(input_image) as src: - profile = src.profile - - # set block size - blockxsize, blockysize = window_size - if src.width < blockxsize: - blockxsize = utils.highest_power_of_2(src.width) - if src.height < blockysize: - blockysize = utils.highest_power_of_2(src.height) - - # dtype and creation options of output data - dtype = rasterprocessing.dtype or rasterio.float32 - in_dtype = rasterprocessing.in_dtype or dtype - nbits = rasterprocessing.nbits - compress = rasterprocessing.compress or src.compression or 'lzw' - nodata = rasterprocessing.nodata or src.nodata - - # check band index and handle all bands options (when bands is an empty list) - if bands is None or len(bands) == 0: - bands = src.indexes - elif min(bands) < 1 or max(bands) > src.count: - raise ValueError(f"Invalid bands, all values are not in range [1, {src.count}]") - - # setup profile for output image - profile.update(driver='GTiff', blockxsize=blockxsize, blockysize=blockysize, - tiled=True, dtype=dtype, nbits=nbits, compress=compress, - nodata=nodata, count=len(bands)) - - with rasterio.open(output_image, "w", **profile): - # file is created - pass - - # create the generator of sliding windows - sliding_gen = _sliding_windows((src.width, src.height), - window_size, window_overlap) - if rasterprocessing.per_band_algo: - sliding_windows_bands = [(w, [b]) for w in sliding_gen for b in bands] - else: - sliding_windows_bands = [(w, bands) for w in sliding_gen] + src = vsimem_to_rasterio(input_image) + profile = src.profile + + # set block size + blockxsize, blockysize = window_size + if src.width < blockxsize: + blockxsize = utils.highest_power_of_2(src.width) + if src.height < blockysize: + blockysize = utils.highest_power_of_2(src.height) + + # dtype and creation options of output data + dtype = rasterprocessing.dtype or rasterio.float32 + in_dtype = rasterprocessing.in_dtype or dtype + nbits = rasterprocessing.nbits + compress = rasterprocessing.compress or src.compression or 'lzw' + nodata = rasterprocessing.nodata or src.nodata + + # check band index and handle all bands options (when bands is an empty list) + if bands is None or len(bands) == 0: + bands = src.indexes + elif min(bands) < 1 or max(bands) > src.count: + raise ValueError(f"Invalid bands, all values are not in range [1, {src.count}]") + + # setup profile for output image + profile.update(driver='GTiff', blockxsize=blockxsize, blockysize=blockysize, + tiled=True, dtype=dtype, nbits=nbits, compress=compress, + nodata=nodata, count=len(bands)) + + with rasterio.open(output_image, "w", **profile): + # file is created + pass + + # create the generator of sliding windows + sliding_gen = _sliding_windows((src.width, src.height), + window_size, window_overlap) + + if rasterprocessing.per_band_algo: + sliding_windows_bands = [(w, [b]) for w in sliding_gen for b in bands] + else: + sliding_windows_bands = [(w, bands) for w in sliding_gen] + src.close() m = multiprocessing.Manager() write_lock = m.Lock() diff --git a/src/eolab/rastertools/processing/vector.py b/src/eolab/rastertools/processing/vector.py index 9787fa7..e2f00ce 100644 --- a/src/eolab/rastertools/processing/vector.py +++ b/src/eolab/rastertools/processing/vector.py @@ -13,6 +13,7 @@ import shapely.geometry from osgeo import gdal import rasterio +from eolab.rastertools.utils import vsimem_to_rasterio from rasterio import features, warp, windows from eolab.rastertools import utils @@ -158,36 +159,23 @@ def reproject(geoms: Union[gpd.GeoDataFrame, Path, str], raster: Union[Path, str geoms_crs = _get_geoms_crs(geometries) file = raster.as_posix() if isinstance(raster, Path) else raster - print(file) - - - # with rasterio.open(file) as dataset: - # print(type(dataset.crs)) - # print(geoms_crs) - # if (geoms_crs != dataset.crs): - # reprojected_geoms = geometries.to_crs(dataset.crs) - # else: - # reprojected_geoms = geometries - # print(reprojected_geoms) - # if output: - # outfile = output.as_posix() if isinstance(output, Path) else output - # reprojected_geoms.to_file(outfile, driver=driver) - - dataset = gdal.Open(file) - print(dataset.GetProjection()) - if(geoms_crs != dataset.GetProjection()): - reprojected_geoms = geometries.to_crs(dataset.GetProjection()) + + dataset = vsimem_to_rasterio(file) + + if(geoms_crs != dataset.crs): + reprojected_geoms = geometries.to_crs(dataset.crs) else: reprojected_geoms = geometries - print(reprojected_geoms) if output: outfile = output.as_posix() if isinstance(output, Path) else output reprojected_geoms.to_file(outfile, driver=driver) + dataset.close() return reprojected_geoms + def dissolve(geoms: Union[gpd.GeoDataFrame, Path, str], output: Union[Path, str] = None, driver: str = 'GeoJSON') -> gpd.GeoDataFrame: """Dissolves all geometries in one @@ -207,7 +195,6 @@ def dissolve(geoms: Union[gpd.GeoDataFrame, Path, str], geometries = _get_geoms(geoms) geometries['COMMON'] = 0 - print(geometries) union = geometries.dissolve(by='COMMON', as_index=False) union = union.drop(columns='COMMON') @@ -215,7 +202,6 @@ def dissolve(geoms: Union[gpd.GeoDataFrame, Path, str], outfile = output.as_posix() if isinstance(output, Path) else output union.to_file(outfile, driver=driver) - print(union) return union @@ -314,6 +300,7 @@ def rasterize(geoms: Union[gpd.GeoDataFrame, Path, str], raster: Union[Path, str return burned + def crop(input_image: Union[Path, str], roi: Union[gpd.GeoDataFrame, Path, str], output_image: Union[Path, str]): """Crops an input image to the roi bounds. @@ -328,46 +315,18 @@ def crop(input_image: Union[Path, str], roi: Union[gpd.GeoDataFrame, Path, str], """ pinput = input_image.as_posix() if isinstance(input_image, Path) else input_image - print(pinput) poutput = output_image.as_posix() if isinstance(output_image, Path) else output_image geometries = reproject(dissolve(roi), pinput) geom_bounds = geometries.total_bounds - # with rasterio.open(pinput) as raster: - # rst_bounds = raster.bounds - # - # print(rst_bounds) - # bounds = (math.floor(max(rst_bounds[0], geom_bounds[0])), - # math.floor(max(rst_bounds[1], geom_bounds[1])), - # math.ceil(min(rst_bounds[2], geom_bounds[2])), - # math.ceil(min(rst_bounds[3], geom_bounds[3]))) - # geotransform = raster.get_transform() - # - # print(geotransform) - # width = np.abs(geotransform[1]) - # height = np.abs(geotransform[5]) - # - # ds = gdal.Warp(destNameOrDestDS=poutput, - # srcDSOrSrcDSTab=pinput, - # outputBounds=bounds, targetAlignedPixels=True, - # cutlineDSName=roi, - # cropToCutline=False, - # xRes=width, yRes=height, - # format="VRT") - # del ds - - raster = gdal.Open(pinput) - geotransform = list(raster.GetGeoTransform()) - bottom = geotransform[3] + raster.RasterYSize * geotransform[5] - right = geotransform[0] + raster.RasterXSize * geotransform[1] - rst_bounds = [geotransform[0],bottom,right,geotransform[3]] - print(rst_bounds) - + raster = vsimem_to_rasterio(pinput) + rst_bounds = raster.bounds bounds = (math.floor(max(rst_bounds[0], geom_bounds[0])), math.floor(max(rst_bounds[1], geom_bounds[1])), math.ceil(min(rst_bounds[2], geom_bounds[2])), math.ceil(min(rst_bounds[3], geom_bounds[3]))) + geotransform = raster.get_transform() width = np.abs(geotransform[1]) height = np.abs(geotransform[5]) @@ -379,6 +338,7 @@ def crop(input_image: Union[Path, str], roi: Union[gpd.GeoDataFrame, Path, str], xRes=width, yRes=height, format="VRT") del ds + raster.close() diff --git a/src/eolab/rastertools/product/rasterproduct.py b/src/eolab/rastertools/product/rasterproduct.py index 294dda8..0244d5e 100644 --- a/src/eolab/rastertools/product/rasterproduct.py +++ b/src/eolab/rastertools/product/rasterproduct.py @@ -133,9 +133,6 @@ def free_in_memory_vrts(self): for vrt in self._in_memory_vrts: gdal.Unlink(vrt.as_posix()) self._in_memory_vrts = [] - # for vrt in self._in_memory_vrts: - # vrt.close() # Closes and cleans up the memory file - # self._in_memory_vrts = [] @property def file(self) -> Path: @@ -397,8 +394,6 @@ def __create_vrt(self, """ # convert parameters defined as str to Path outdir = utils.to_path(self._vrt_outputdir, "/vsimem/") - print(bands_files) - print(outdir) basename = utils.get_basename(self.file) _logger.debug("Creating a VRT that handles the bands of the archive product") @@ -423,7 +418,6 @@ def __create_vrt(self, # Create a VRT image with GDAL rasterfile = outdir.joinpath(f"{uuid}{basename}.vrt") - # rasterfile = rasterfile.as_posix() ds = gdal.BuildVRT(rasterfile.as_posix(), bands, VRTNodata=' '.join(nodatavals), @@ -434,79 +428,6 @@ def __create_vrt(self, # free resource from GDAL del ds - # with rasterio.open(list(bands_files.values())[0]) as src: - # width = src.width - # height = src.height - # - # # with rasterio.open(rasterfile, 'w', driver="GTiff", width=width, height=height, count=1) as dataset: - # # dataset.write(bands[0]) - # - # with rasterio.open(rasterfile, 'w', driver="GTiff", width=width, height=height) as src: - # with WarpedVRT(src) as vrt: - # print("VRT created with dimensions:", vrt.width, vrt.height) - - - # free resource from GDAL - # - - - - - - - # # convert parameters defined as str to Path - # outdir = utils.to_path(self._vrt_outputdir, "/vsimem/") - # print(outdir) - # basename = utils.get_basename(self.file) - # rasterfile = outdir.joinpath(f"{uuid}{basename}.vrt") - # - # _logger.debug("Creating a VRT that handles the bands of the archive product") - # - # # Initialize VRT XML structure - # vrt_root = ET.Element("VRTDataset") - # - # # Open the first band to get metadata - # with rasterio.open(list(bands_files.values())[0]) as src: - # vrt_root.set("rasterXSize", str(src.width)) - # vrt_root.set("rasterYSize", str(src.height)) - # vrt_root.set("subClass", "VRTDataset") - # crs_element = ET.SubElement(vrt_root, "SRS") - # crs_element.text = src.crs.to_wkt() - # - # # Add bands - # for i, (band_id, band_path) in enumerate(bands_files.items(), start=1): - # with rasterio.open(band_path) as src_band: - # band_element = ET.SubElement(vrt_root, "VRTRasterBand", attrib={ - # "dataType": src_band.dtypes[0].upper(), - # "band": str(i) - # }) - # source_element = ET.SubElement(band_element, "SimpleSource") - # ET.SubElement(source_element, "SourceFilename", attrib={"relativeToVRT": "1"}).text = str(band_path) - # ET.SubElement(source_element, "SourceBand").text = "1" - # ET.SubElement(source_element, "SourceProperties", attrib={ - # "RasterXSize": str(src_band.width), - # "RasterYSize": str(src_band.height), - # "DataType": src_band.dtypes[0].upper(), - # "BlockXSize": str(src_band.block_shapes[0][0]), - # "BlockYSize": str(src_band.block_shapes[0][1]) - # }) - # ET.SubElement(source_element, "SrcRect", attrib={ - # "xOff": "0", "yOff": "0", - # "xSize": str(src_band.width), "ySize": str(src_band.height) - # }) - # ET.SubElement(source_element, "DstRect", attrib={ - # "xOff": "0", "yOff": "0", - # "xSize": str(src_band.width), "ySize": str(src_band.height) - # }) - # - # # Add masks - # for i, (mask_id, mask_path) in enumerate(masks_files.items(), start=len(bands_files) + 1): - # mask_element = ET.SubElement(vrt_root, "VRTRasterBand", attrib={ - # "dataType": "Byte", - # "band": str(i) - # }) - # source_element = ET.SubElement(mask_element, "SimpleSource") - # ET.SubElement(source_element, "SourceFilename", attrib={"relativeToVRT": "1"}).text = str(mask_path) - # ET.SubElement(source_element, "SourceBand").text = "1" - # - # # Write the VRT to file - # tree = ET.ElementTree(vrt_root) - # with open(rasterfile, "wb") as f: - # tree.write(f, encoding="utf-8", xml_declaration=True) return rasterfile def __wrap(self, input_vrt: Path, roi: Path, uuid: str = "") -> Path: @@ -583,33 +504,6 @@ def __apply_masks(self, input_vrt: Path, nb_bands: int, nb_masks: int, uuid: str return masked_image - # # convert parameters defined as str to Path - # outdir = utils.to_path(self._vrt_outputdir, "/vsimem/") - # basename = utils.get_basename(self.file) - # - # # create a tempdir for generated temporary files - # tempdir = Path(tempfile.gettempdir()) - # temp_image = tempdir.joinpath(f"{uuid}{basename}-temp.vrt") - # with rasterio.open(input_vrt) as src: - # # Create a new in-memory VRT for the bands - # with WarpedVRT(src, crs=src.crs, transform=src.transform, width=src.width, height=src.height, - # count=nb_bands) as vrt: - # # Here you would apply any mask band logic. For simplicity, we assume a method that adds the masks. - # # For each mask, you could apply it using rasterio to create new bands with the mask applied. - # # Add the masks to the VRT using the provided add_masks_to_vrt function - # _logger.debug("Adding mask bands to VRT") - # masks_index = list(range(nb_bands + 1, nb_bands + nb_masks + 1)) - # vrt_new_content = add_masks_to_vrt(temp_image, input_vrt, masks_index, - # self.rastertype.maskfunc) - # - # # Now save this VRT to a file - # masked_image = outdir.joinpath(f"{uuid}{basename}-mask.vrt") - # with open(masked_image, 'wb') as out_vrt: - # out_vrt.write(vrt_new_content) - # - # _logger.debug(f"Generated masked VRT saved to {masked_image}") - # return masked_image - def _extract_bands(inputfile: Path, bands_pattern: str, diff --git a/src/eolab/rastertools/radioindice.py b/src/eolab/rastertools/radioindice.py index 88c68aa..54d2a6b 100644 --- a/src/eolab/rastertools/radioindice.py +++ b/src/eolab/rastertools/radioindice.py @@ -10,13 +10,10 @@ from pathlib import Path from typing import List import threading -import geopandas as gpd -import numpy as np import rasterio import numpy.ma as ma -from osgeo import gdal -from rasterio import CRS +from eolab.rastertools.utils import vsimem_to_rasterio from tqdm import tqdm from eolab.rastertools import utils @@ -432,10 +429,7 @@ def process_file(self, inputfile: str) -> List[str]: # get the raster print(self.roi) - # self.roi = "tests/tests_data/COMMUNE_32001.shp" - # print(self.roi) raster = product.get_raster(roi=self.roi) - print(type(raster)) # STEP 2: Compute the indices outputs = [] @@ -476,7 +470,6 @@ def get_raster_profile(raster): # Get data type and block size from the first band band = raster.GetRasterBand(1) - dtype = gdal.GetDataTypeName(band.DataType) dtype_rasterio = rasterio.dtypes.get_minimum_dtype(band.DataType) nodata = band.GetNoDataValue() @@ -504,7 +497,6 @@ def get_raster_profile(raster): return profile - def compute_indices(input_image: str, image_channels: List[BandChannel], indice_image: str, indices: List[RadioindiceProcessing], window_size: tuple = (1024, 1024)): @@ -527,142 +519,57 @@ def compute_indices(input_image: str, image_channels: List[BandChannel], window_size (tuple(int, int), optional, default=(1024, 1024)): Size of windows for splitting the processed image in small parts """ - print('...'*50) - print(input_image) - # with rasterio.Env(GDAL_VRT_ENABLE_PYTHON=True): - # with rasterio.open(input_image) as src: - # profile = src.profile - # - # print(profile) - # - # # set block size to the configured window_size of first indice - # blockxsize, blockysize = window_size - # print(src.width) - # print(blockxsize) - # if src.width < blockxsize: - # blockxsize = utils.highest_power_of_2(src.width) - # if src.height < blockysize: - # blockysize = utils.highest_power_of_2(src.height) - # - # # dtype of output data - # dtype = indices[0].dtype or rasterio.float32 - # - # # setup profile for output image - # profile.update(driver='GTiff', - # blockxsize=blockysize, blockysize=blockxsize, tiled=True, - # dtype=dtype, nodata=indices[0].nodata, - # count=len(indices)) - # print(type(profile)) - # with rasterio.open(indice_image, "w", **profile) as dst: - # # Materialize a list of destination block windows - # windows = [window for ij, window in dst.block_windows()] - # - # # disable status of tqdm progress bar - # disable = os.getenv("RASTERTOOLS_NOTQDM", 'False').lower() in ['true', '1'] - # - # # compute every indices - # for i, indice in enumerate(indices, 1): - # # Get the bands necessary to compute the indice - # bands = [image_channels.index(channel) + 1 for channel in indice.channels] - # - # read_lock = threading.Lock() - # write_lock = threading.Lock() - # - # def process(window): - # """Read input raster, compute indice and write output raster""" - # with read_lock: - # src_array = src.read(bands, window=window, masked=True) - # src_array[src_array == src.nodata] = ma.masked - # src_array = src_array.astype(dtype) - # - # # The computation can be performed concurrently - # result = indice.algo(src_array).astype(dtype).filled(indice.nodata) - # - # with write_lock: - # dst.write_band(i, result, window=window) - # - # # compute using concurrent.futures.ThreadPoolExecutor and tqdm - # for window in tqdm(windows, disable=disable, desc=f"{indice.name}"): - # process(window) - # - # dst.set_band_description(i, indice.name) - # with rasterio.Env(GDAL_VRT_ENABLE_PYTHON=True): - # with rasterio.open(input_image) as src: - # profile = src.profile - - src = gdal.Open(input_image) - profile = get_raster_profile(src) ############################# - print(profile) - geotransform = list(src.GetGeoTransform()) - width = src.RasterXSize - height = src.RasterYSize - - # set block size to the configured window_size of first indice - blockxsize, blockysize = window_size - print(width) - print(blockxsize) - - if width < blockxsize: - blockxsize = utils.highest_power_of_2(width) - if height < blockysize: - blockysize = utils.highest_power_of_2(height) - - # dtype of output data - dtype = indices[0].dtype or rasterio.float32 - - # setup profile for output image - profile.update(driver='GTiff', - blockxsize=blockysize, blockysize=blockxsize, tiled=True, - dtype=dtype, nodata=indices[0].nodata, - count=len(indices)) - - print(profile) - print(type(profile)) - nodata = src.GetRasterBand(1).GetNoDataValue() - - with rasterio.open(indice_image, "w", **profile) as dst: - # Materialize a list of destination block windows - windows = [window for ij, window in dst.block_windows()] - print(windows) - - # disable status of tqdm progress bar - disable = os.getenv("RASTERTOOLS_NOTQDM", 'False').lower() in ['true', '1'] - - # compute every indices - for i, indice in enumerate(indices, 1): - # Get the bands necessary to compute the indice - bands = [image_channels.index(channel) + 1 for channel in indice.channels] - - read_lock = threading.Lock() - write_lock = threading.Lock() - - def process(window): - """Read input raster, compute indice and write output raster""" - with read_lock: - x_offset = int(window.col_off) - y_offset = int(window.row_off) - x_size = int(window.width) - y_size = int(window.height) - - src_array = np.array([ - src.GetRasterBand(band).ReadAsArray(x_offset, y_offset, x_size, y_size) - for band in bands], dtype=dtype) - - # Mask nodata values - src_array = np.ma.masked_equal(src_array, nodata) - - # src_array = src.read(bands, window=window, masked=True) - # src_array[src_array == src.nodata] = ma.masked - # src_array = src_array.astype(dtype) - - # The computation can be performed concurrently - result = indice.algo(src_array).astype(dtype).filled(indice.nodata) - - with write_lock: - dst.write_band(i, result, window=window) + with rasterio.Env(GDAL_VRT_ENABLE_PYTHON=True): + src = vsimem_to_rasterio(input_image) + profile = src.profile + + # set block size to the configured window_size of first indice + blockxsize, blockysize = window_size + if src.width < blockxsize: + blockxsize = utils.highest_power_of_2(src.width) + if src.height < blockysize: + blockysize = utils.highest_power_of_2(src.height) + + # dtype of output data + dtype = indices[0].dtype or rasterio.float32 + + # setup profile for output image + profile.update(driver='GTiff', + blockxsize=blockysize, blockysize=blockxsize, tiled=True, + dtype=dtype, nodata=indices[0].nodata, + count=len(indices)) + + with rasterio.open(indice_image, "w", **profile) as dst: + # Materialize a list of destination block windows + windows = [window for ij, window in dst.block_windows()] + + # disable status of tqdm progress bar + disable = os.getenv("RASTERTOOLS_NOTQDM", 'False').lower() in ['true', '1'] + + # compute every indices + for i, indice in enumerate(indices, 1): + # Get the bands necessary to compute the indice + bands = [image_channels.index(channel) + 1 for channel in indice.channels] + + read_lock = threading.Lock() + write_lock = threading.Lock() + + def process(window): + """Read input raster, compute indice and write output raster""" + with read_lock: + src_array = src.read(bands, window=window, masked=True) + src_array[src_array == src.nodata] = ma.masked + src_array = src_array.astype(dtype) + + # The computation can be performed concurrently + result = indice.algo(src_array).astype(dtype).filled(indice.nodata) + + with write_lock: + dst.write_band(i, result, window=window) # compute using concurrent.futures.ThreadPoolExecutor and tqdm - for window in tqdm(windows, disable=disable, desc=f"{indice.name}"): - process(window) + for window in tqdm(windows, disable=disable, desc=f"{indice.name}"): + process(window) - dst.set_band_description(i, indice.name) + dst.set_band_description(i, indice.name) + src.close() diff --git a/src/eolab/rastertools/speed.py b/src/eolab/rastertools/speed.py index b30862b..4350dae 100644 --- a/src/eolab/rastertools/speed.py +++ b/src/eolab/rastertools/speed.py @@ -5,7 +5,6 @@ of the radiometry of the input rasters. """ from datetime import datetime -import logging import logging.config import os from pathlib import Path diff --git a/src/eolab/rastertools/svf.py b/src/eolab/rastertools/svf.py index 4e83dfa..0644c68 100644 --- a/src/eolab/rastertools/svf.py +++ b/src/eolab/rastertools/svf.py @@ -4,7 +4,6 @@ This module defines a rastertool named SVF (Sky View Factor) which computes the SVF of a Digital Height Model. """ -import logging import logging.config from pathlib import Path import numpy as np diff --git a/src/eolab/rastertools/timeseries.py b/src/eolab/rastertools/timeseries.py index 626b984..94014eb 100644 --- a/src/eolab/rastertools/timeseries.py +++ b/src/eolab/rastertools/timeseries.py @@ -8,7 +8,6 @@ """ from datetime import datetime, timedelta from itertools import repeat -import logging import logging.config import multiprocessing import os diff --git a/src/eolab/rastertools/utils.py b/src/eolab/rastertools/utils.py index 417e2c7..d4f91ce 100644 --- a/src/eolab/rastertools/utils.py +++ b/src/eolab/rastertools/utils.py @@ -99,7 +99,7 @@ def vsimem_to_rasterio(vsimem_file:str, nodata=None) -> rasterio.io.DatasetReade dst.write(band_data, i) # Si un masque est défini, l'écrire if masks[i - 1] is not None: - dst.write_mask((masks[i - 1]).astype("uint8") * 255) + dst.write_mask((~masks[i - 1]).astype("uint8") * 255) return rasterio.open(temp_filename) diff --git a/src/rastertools.egg-info/PKG-INFO b/src/rastertools.egg-info/PKG-INFO index 7be60b8..df74ad2 100644 --- a/src/rastertools.egg-info/PKG-INFO +++ b/src/rastertools.egg-info/PKG-INFO @@ -28,11 +28,15 @@ Requires-Dist: matplotlib==3.7.3 Requires-Dist: packaging==24.1 Requires-Dist: Shapely==1.8.5.post1 Requires-Dist: tomli==2.0.2 +Requires-Dist: Rtree==1.3.0 Requires-Dist: Pillow==9.2.0 Requires-Dist: pip==24.2 Requires-Dist: pyproj==3.4.0 Requires-Dist: matplotlib +Requires-Dist: sphinx==7.1.2 Requires-Dist: scipy==1.8 +Requires-Dist: pyscaffold +Requires-Dist: gdal==3.5.0 Requires-Dist: tqdm==4.66 Provides-Extra: testing Requires-Dist: setuptools; extra == "testing" diff --git a/src/rastertools.egg-info/SOURCES.txt b/src/rastertools.egg-info/SOURCES.txt index 5d7f921..49368cd 100644 --- a/src/rastertools.egg-info/SOURCES.txt +++ b/src/rastertools.egg-info/SOURCES.txt @@ -7,7 +7,6 @@ README.rst env_test.yml env_update.yml environment.yml -pyproject.toml setup.cfg setup.py tox.ini diff --git a/src/rastertools.egg-info/entry_points.txt b/src/rastertools.egg-info/entry_points.txt index 7844cc6..467c161 100644 --- a/src/rastertools.egg-info/entry_points.txt +++ b/src/rastertools.egg-info/entry_points.txt @@ -1,2 +1,2 @@ -[rasterio.plugins] -rastertools = src.eolab.rastertools.main:rastertools +[rasterio.rio_plugins] +rastertools = eolab.rastertools.main:rastertools diff --git a/src/rastertools.egg-info/requires.txt b/src/rastertools.egg-info/requires.txt index b92b22e..0b15b4a 100644 --- a/src/rastertools.egg-info/requires.txt +++ b/src/rastertools.egg-info/requires.txt @@ -10,11 +10,15 @@ matplotlib==3.7.3 packaging==24.1 Shapely==1.8.5.post1 tomli==2.0.2 +Rtree==1.3.0 Pillow==9.2.0 pip==24.2 pyproj==3.4.0 matplotlib +sphinx==7.1.2 scipy==1.8 +pyscaffold +gdal==3.5.0 tqdm==4.66 [testing] diff --git a/tests/test_radioindice.py b/tests/test_radioindice.py index a121497..234fc9e 100644 --- a/tests/test_radioindice.py +++ b/tests/test_radioindice.py @@ -2,8 +2,6 @@ # -*- coding: utf-8 -*- import os -import pytest -import filecmp import logging from eolab.rastertools import Radioindice from eolab.rastertools.processing.rasterproc import RadioindiceProcessing @@ -40,10 +38,7 @@ def test_radioindice_process_file_merge(): origin_path = RastertoolsTestsData.tests_input_data_dir + "/".split(os.getcwd() + "/")[-1] - inputfile = RastertoolsTestsData.tests_input_data_dir + "/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip" - print(inputfile) - inputfile = origin_path+ "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip" - print(inputfile) + inputfile = origin_path + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip" indices = [indice for indice in Radioindice.get_default_indices()] tool = Radioindice(indices) @@ -99,7 +94,7 @@ def test_radioindice_process_file_separate(compare : bool, save_gen_as_ref : boo # save the generated files in the refdir => make them the new refs. utils4test.copy_to_ref(gen_files, __refdir) - utils4test.clear_outdir() + # utils4test.clear_outdir() def test_radioindice_process_files(): diff --git a/tests/test_rasterproduct.py b/tests/test_rasterproduct.py index 7d470aa..cc71fce 100644 --- a/tests/test_rasterproduct.py +++ b/tests/test_rasterproduct.py @@ -181,7 +181,7 @@ def test_create_product_S2_L2A_MAJA(compare, save_gen_as_ref): utils4test.clear_outdir(subdirs=False) - # delete the dir resulting from unzip +# delete the dir resulting from unzip utils4test.delete_dir(RastertoolsTestsData.tests_output_data_dir + "/" + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_V1-9") @@ -376,11 +376,9 @@ def test_create_product_special_cases(): dataset.close() # creation from a directory - # unzip S2B_MSIL1C_20191008T105029_N0208_R051_T30TYP_20191008T125041.zip file = RastertoolsTestsData.tests_input_data_dir + "/" + "S2B_MSIL1C_20191008T105029_N0208_R051_T30TYP_20191008T125041.zip" with zipfile.ZipFile(file) as myzip: myzip.extractall(RastertoolsTestsData.tests_output_data_dir + "/") - dirname = "S2B_MSIL1C_20191008T105029_N0208_R051_T30TYP_20191008T125041.SAFE" with RasterProduct(file, vrt_outputdir=Path(RastertoolsTestsData.tests_output_data_dir + "/")) as prod: raster = prod.get_raster() diff --git a/tests/test_rastertools.py b/tests/test_rastertools.py index 57f7493..73e9601 100644 --- a/tests/test_rastertools.py +++ b/tests/test_rastertools.py @@ -1,9 +1,9 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -import pytest import logging -import filecmp +import os + from click.testing import CliRunner from pathlib import Path @@ -113,6 +113,7 @@ def run_test(self, caplog=None, loglevel=logging.ERROR, check_outputs=True, chec if compare: match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", self._refdir, self._outputs) + assert len(match) == 3 assert len(mismatch) == 0 assert len(err) == 0 @@ -283,9 +284,9 @@ def test_speed_command_line_default(): # default with a listing of S2A products f"-v sp -b 1 -o {RastertoolsTestsData.tests_output_data_dir} {lst_file_path}", # default with a list of files - # f"--verbose speed --output {RastertoolsTestsData.tests_output_data_dir}" - # f" {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" - # f" {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif" + f"--verbose speed --output {RastertoolsTestsData.tests_output_data_dir}" + f" {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" + f" {RastertoolsTestsData.tests_input_data_dir}/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif" ] speed_filenames = [ ["SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-speed-20180928-105515.tif"], @@ -299,6 +300,7 @@ def test_speed_command_line_default(): # execute test cases with logging level set to INFO for test in tests: test.run_test() + os.remove(lst_file_path) def test_speed_command_line_errors(caplog): @@ -339,6 +341,7 @@ def test_speed_command_line_errors(caplog): # execute test cases with logging level set to INFO for test in tests: test.run_test(caplog, check_outputs=False) + os.remove(lst_file_path) def test_timeseries_command_line_default(compare, save_gen_as_ref): @@ -428,6 +431,7 @@ def test_timeseries_command_line_errors(caplog): # execute test cases with logging level set to INFO for test in tests: test.run_test(caplog, check_outputs=False) + os.remove(lst_file_path) def test_zonalstats_command_line_default(): @@ -462,7 +466,7 @@ def test_zonalstats_command_line_default(): # execute test cases for test in tests: test.run_test() - + os.remove(lst_file_path) def test_zonalstats_command_line_product(): utils4test.create_outdir() @@ -558,6 +562,7 @@ def test_zonalstats_command_line_errors(): # execute test cases for test in tests: test.run_test(check_outputs=False) + os.remove(lst_file_path) def test_tiling_command_line_default(): @@ -603,6 +608,7 @@ def test_tiling_command_line_default(): subdir = Path(f"{RastertoolsTestsData.tests_output_data_dir}/tile77") subdir.mkdir() test.run_test(check_outputs=False) + os.remove(lst_file_path) def test_tiling_command_line_special_case(caplog): diff --git a/tests/test_rastertype.py b/tests/test_rastertype.py index 9981136..0b36749 100644 --- a/tests/test_rastertype.py +++ b/tests/test_rastertype.py @@ -7,7 +7,6 @@ from datetime import datetime from eolab.rastertools import add_custom_rastertypes from eolab.rastertools.product import RasterType, BandChannel -from . import utils4test __author__ = "Olivier Queyrut" __copyright__ = "Copyright 2019, CNES" diff --git a/tests/test_speed.py b/tests/test_speed.py index 1829563..bc1347f 100644 --- a/tests/test_speed.py +++ b/tests/test_speed.py @@ -1,8 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -import pytest -import filecmp from eolab.rastertools import Speed from eolab.rastertools.product import RasterType diff --git a/tests/test_stats.py b/tests/test_stats.py index d4b00e5..ec106ab 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -212,7 +212,6 @@ def test_compute_zonal_stats_per_category(): catlabels = RastertoolsTestsData.tests_input_data_dir + "/" + "OSO_nomenclature_2017.json" stats_to_compute = DEFAULT_STATS bands = [1] - print(vector.clip(catgeojson, raster)) geometries = vector.reproject(vector.filter(geojson, raster), raster) categories = vector.reproject(vector.clip(catgeojson, raster), raster) @@ -221,7 +220,6 @@ def test_compute_zonal_stats_per_category(): stats=stats_to_compute, categories=categories, category_index="Classe") - print(f"{statistics}") # statistics is a list of list of dict. # First list iterates over geometries # Second list iterates over bands. diff --git a/tests/test_tiling.py b/tests/test_tiling.py index 92197c4..213e173 100644 --- a/tests/test_tiling.py +++ b/tests/test_tiling.py @@ -1,7 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -import filecmp from eolab.rastertools import Tiling from . import utils4test diff --git a/tests/test_vector.py b/tests/test_vector.py index 8d9e7c2..accf190 100644 --- a/tests/test_vector.py +++ b/tests/test_vector.py @@ -1,7 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -import filecmp import geopandas as gpd from pathlib import Path diff --git a/tests/test_zonalstats.py b/tests/test_zonalstats.py index 11c7cb1..ed637f9 100644 --- a/tests/test_zonalstats.py +++ b/tests/test_zonalstats.py @@ -2,7 +2,6 @@ # -*- coding: utf-8 -*- import pytest -import filecmp from pathlib import Path from eolab.rastertools import Zonalstats from eolab.rastertools import RastertoolConfigurationException diff --git a/tests/utils4test.py b/tests/utils4test.py index d9ec5f4..bc60307 100644 --- a/tests/utils4test.py +++ b/tests/utils4test.py @@ -76,6 +76,7 @@ def cmpfiles(a : str, b : str, common : list, tolerance : float =1e-9) -> tuple: new = os.path.join(a, x) golden = os.path.join(b, x) res[_cmp(golden, new, tolerance)].append(x) + print(res) return res From 4801b7fd22af211b966a051de2ef9d4a4fb85116 Mon Sep 17 00:00:00 2001 From: cadauxe Date: Wed, 4 Dec 2024 17:30:40 +0100 Subject: [PATCH 11/17] refactor: suppress vsimem_to_rasterio --- setup.py | 2 +- src/eolab/rastertools/processing/sliding.py | 3 +- src/eolab/rastertools/processing/stats.py | 4 +- src/eolab/rastertools/processing/vector.py | 7 +- .../rastertools/product/rasterproduct.py | 3 +- src/eolab/rastertools/radioindice.py | 3 +- src/eolab/rastertools/utils.py | 87 ------------------- src/eolab/rastertools/zonalstats.py | 3 +- src/rastertools.egg-info/PKG-INFO | 4 +- src/rastertools.egg-info/SOURCES.txt | 63 -------------- src/rastertools.egg-info/requires.txt | 4 +- tests/test_rasterproduct.py | 3 +- 12 files changed, 15 insertions(+), 171 deletions(-) diff --git a/setup.py b/setup.py index 0c119b1..e81fa89 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ setup_requires = ["setuptools_scm"], install_requires=[ 'click', - 'rasterio==1.3.0', + # 'rasterio==1.3.0', 'pytest>=3.6', 'pytest-cov', 'geopandas==0.13', diff --git a/src/eolab/rastertools/processing/sliding.py b/src/eolab/rastertools/processing/sliding.py index 32062d7..0ccb326 100644 --- a/src/eolab/rastertools/processing/sliding.py +++ b/src/eolab/rastertools/processing/sliding.py @@ -17,7 +17,6 @@ from eolab.rastertools import utils from eolab.rastertools.processing import RasterProcessing -from eolab.rastertools.utils import vsimem_to_rasterio _logger = logging.getLogger(__name__) @@ -56,7 +55,7 @@ def compute_sliding(input_image: str, output_image: str, rasterprocessing: Raste """ with rasterio.Env(GDAL_VRT_ENABLE_PYTHON=True): - src = vsimem_to_rasterio(input_image) + src = rasterio.open(input_image) profile = src.profile # set block size diff --git a/src/eolab/rastertools/processing/stats.py b/src/eolab/rastertools/processing/stats.py index ae78271..222b397 100644 --- a/src/eolab/rastertools/processing/stats.py +++ b/src/eolab/rastertools/processing/stats.py @@ -18,7 +18,7 @@ from rasterio import features from tqdm import tqdm -from eolab.rastertools.utils import get_metadata_name, vsimem_to_rasterio +from eolab.rastertools.utils import get_metadata_name from eolab.rastertools.processing.vector import rasterize, filter_dissolve @@ -71,7 +71,7 @@ def compute_zonal_stats(geoms: gpd.GeoDataFrame, image: str, """ nb_geoms = len(geoms) - src = vsimem_to_rasterio(image) + src = rasterio.open(image) geom_gen = (geoms.iloc[i].geometry for i in range(nb_geoms)) geom_windows = ((geom, features.geometry_window(src, [geom])) for geom in geom_gen) diff --git a/src/eolab/rastertools/processing/vector.py b/src/eolab/rastertools/processing/vector.py index e2f00ce..d37e80e 100644 --- a/src/eolab/rastertools/processing/vector.py +++ b/src/eolab/rastertools/processing/vector.py @@ -13,7 +13,6 @@ import shapely.geometry from osgeo import gdal import rasterio -from eolab.rastertools.utils import vsimem_to_rasterio from rasterio import features, warp, windows from eolab.rastertools import utils @@ -68,7 +67,7 @@ def filter(geoms: Union[gpd.GeoDataFrame, Path, str], raster: Union[Path, str], file = raster.as_posix() if isinstance(raster, Path) else raster - dataset = utils.vsimem_to_rasterio(file) + dataset = rasterio.open(file) l, b, r, t = dataset.bounds px, py = ([l, l, r, r], [b, t, t, b]) @@ -160,7 +159,7 @@ def reproject(geoms: Union[gpd.GeoDataFrame, Path, str], raster: Union[Path, str file = raster.as_posix() if isinstance(raster, Path) else raster - dataset = vsimem_to_rasterio(file) + dataset = rasterio.open(file) if(geoms_crs != dataset.crs): reprojected_geoms = geometries.to_crs(dataset.crs) @@ -320,7 +319,7 @@ def crop(input_image: Union[Path, str], roi: Union[gpd.GeoDataFrame, Path, str], geometries = reproject(dissolve(roi), pinput) geom_bounds = geometries.total_bounds - raster = vsimem_to_rasterio(pinput) + raster = rasterio.open(pinput) rst_bounds = raster.bounds bounds = (math.floor(max(rst_bounds[0], geom_bounds[0])), math.floor(max(rst_bounds[1], geom_bounds[1])), diff --git a/src/eolab/rastertools/product/rasterproduct.py b/src/eolab/rastertools/product/rasterproduct.py index 0244d5e..1c7767d 100644 --- a/src/eolab/rastertools/product/rasterproduct.py +++ b/src/eolab/rastertools/product/rasterproduct.py @@ -21,7 +21,6 @@ from eolab.rastertools.product import RasterType from eolab.rastertools.product.vrt import add_masks_to_vrt, set_band_descriptions from eolab.rastertools.processing.vector import crop -from eolab.rastertools.utils import vsimem_to_rasterio __author__ = "Olivier Queyrut" __copyright__ = "Copyright 2019, CNES" @@ -224,7 +223,7 @@ def open(self, bands: Union[str, List[str]] = "all", masks: Union[str, List[str]] = "all", roi: Union[Path, str] = None): - return vsimem_to_rasterio(self.get_raster(bands=bands, masks=masks, roi=roi)) + return rasterio.open(self.get_raster(bands=bands, masks=masks, roi=roi)) def get_raster(self, bands: Union[str, List[str]] = "all", diff --git a/src/eolab/rastertools/radioindice.py b/src/eolab/rastertools/radioindice.py index 54d2a6b..72a87f4 100644 --- a/src/eolab/rastertools/radioindice.py +++ b/src/eolab/rastertools/radioindice.py @@ -13,7 +13,6 @@ import rasterio import numpy.ma as ma -from eolab.rastertools.utils import vsimem_to_rasterio from tqdm import tqdm from eolab.rastertools import utils @@ -520,7 +519,7 @@ def compute_indices(input_image: str, image_channels: List[BandChannel], Size of windows for splitting the processed image in small parts """ with rasterio.Env(GDAL_VRT_ENABLE_PYTHON=True): - src = vsimem_to_rasterio(input_image) + src = rasterio.open(input_image) profile = src.profile # set block size to the configured window_size of first indice diff --git a/src/eolab/rastertools/utils.py b/src/eolab/rastertools/utils.py index d4f91ce..cde7b10 100644 --- a/src/eolab/rastertools/utils.py +++ b/src/eolab/rastertools/utils.py @@ -16,93 +16,6 @@ from osgeo import gdal -def vsimem_to_rasterio(vsimem_file:str, nodata=None) -> rasterio.io.DatasetReader: - """ - Converts a VSIMEM (in-memory) raster dataset to a Rasterio dataset with optional nodata masking. - - This function opens a raster dataset stored in VSIMEM (virtual file system in memory) - using GDAL, extracts its metadata and data, handles optional nodata values and masks, - and saves it to a temporary GeoTIFF file. It then reopens the file with Rasterio and - returns a Rasterio dataset reader. - - Parameters - ---------- - vsimem_file : str - The path to the VSIMEM file to be converted. This file should be an in-memory GDAL dataset. - - nodata : float, optional - A user-defined nodata value to override the nodata value in the GDAL dataset. - If not provided, the nodata value from the GDAL dataset is used (if available). - - Returns - ------- - rasterio.io.DatasetReader - A Rasterio dataset reader object corresponding to the temporary GeoTIFF created from the VSIMEM file. - - Notes - ----- - - The function assumes the dataset is in a format that is compatible with both GDAL and Rasterio. - - The created temporary file is not deleted automatically. It can be removed manually after use. - - The function reads all raster bands from the dataset, applies the optional nodata masking, - and writes the data to a new GeoTIFF file. - - If a nodata value is provided, the function will apply the mask based on that value to each band. - If no nodata value is set, no mask is applied. - """ - gdal_ds = gdal.Open(vsimem_file) - cols = gdal_ds.RasterXSize - rows = gdal_ds.RasterYSize - bands = gdal_ds.RasterCount - geo_transform = gdal_ds.GetGeoTransform() - projection = gdal_ds.GetProjection() - - gdal_dtype_to_numpy = { - gdal.GDT_Byte: "uint8", - gdal.GDT_UInt16: "uint16", - gdal.GDT_Int16: "int16", - gdal.GDT_UInt32: "uint32", - gdal.GDT_Int32: "int32", - gdal.GDT_Float32: "float32", - gdal.GDT_Float64: "float64", - } - dtype = gdal_dtype_to_numpy[gdal_ds.GetRasterBand(1).DataType] - - data = [gdal_ds.GetRasterBand(i + 1).ReadAsArray() for i in range(bands)] - - masks = [] - for i in range(bands): - band = gdal_ds.GetRasterBand(i + 1) - band_nodata = band.GetNoDataValue() - # Prioriser la valeur nodata de l'utilisateur - nodata_value = nodata if nodata is not None else band_nodata - if nodata_value is not None: - masks.append(data[i] == nodata_value) - else: - masks.append(None) - - with tempfile.NamedTemporaryFile(suffix=".tif", delete=False) as tmpfile: - temp_filename = tmpfile.name - - profile = { - "driver": "GTiff", - "height": rows, - "width": cols, - "count": bands, - "dtype": dtype, - "crs": projection, - "transform": rasterio.transform.Affine.from_gdal(*geo_transform), - "nodata": nodata_value, - } - - with rasterio.open(temp_filename, "w", **profile) as dst: - # Écrire les données - for i, band_data in enumerate(data, start=1): - dst.write(band_data, i) - # Si un masque est défini, l'écrire - if masks[i - 1] is not None: - dst.write_mask((~masks[i - 1]).astype("uint8") * 255) - - return rasterio.open(temp_filename) - def to_tuple(val): """Convert val as a tuple of two val""" return val if type(val) == tuple else (val, val) diff --git a/src/eolab/rastertools/zonalstats.py b/src/eolab/rastertools/zonalstats.py index 29d7bd6..12847ab 100644 --- a/src/eolab/rastertools/zonalstats.py +++ b/src/eolab/rastertools/zonalstats.py @@ -32,7 +32,6 @@ from eolab.rastertools.processing import extract_zonal_outliers, plot_stats from eolab.rastertools.processing import vector from eolab.rastertools.product import RasterProduct -from eolab.rastertools.utils import vsimem_to_rasterio _logger = logging.getLogger(__name__) @@ -409,7 +408,7 @@ def process_file(self, inputfile: str) -> List[str]: # open raster to get metadata raster = product.get_raster() - rst = vsimem_to_rasterio(raster) + rst = rasterio.open(raster) bound = int(rst.count) indexes = rst.indexes descr = rst.descriptions diff --git a/src/rastertools.egg-info/PKG-INFO b/src/rastertools.egg-info/PKG-INFO index df74ad2..8f7bf9d 100644 --- a/src/rastertools.egg-info/PKG-INFO +++ b/src/rastertools.egg-info/PKG-INFO @@ -17,7 +17,6 @@ Description-Content-Type: text/x-rst; charset=UTF-8 License-File: LICENSE.txt License-File: AUTHORS.rst Requires-Dist: click -Requires-Dist: rasterio==1.3.0 Requires-Dist: pytest>=3.6 Requires-Dist: pytest-cov Requires-Dist: geopandas==0.13 @@ -29,10 +28,11 @@ Requires-Dist: packaging==24.1 Requires-Dist: Shapely==1.8.5.post1 Requires-Dist: tomli==2.0.2 Requires-Dist: Rtree==1.3.0 +Requires-Dist: fiona==1.8.21 Requires-Dist: Pillow==9.2.0 +Requires-Dist: sphinx_rtd_theme==3.0.1 Requires-Dist: pip==24.2 Requires-Dist: pyproj==3.4.0 -Requires-Dist: matplotlib Requires-Dist: sphinx==7.1.2 Requires-Dist: scipy==1.8 Requires-Dist: pyscaffold diff --git a/src/rastertools.egg-info/SOURCES.txt b/src/rastertools.egg-info/SOURCES.txt index 49368cd..9213c8a 100644 --- a/src/rastertools.egg-info/SOURCES.txt +++ b/src/rastertools.egg-info/SOURCES.txt @@ -54,70 +54,7 @@ docs/cli/svf.rst docs/cli/tiling.rst docs/cli/timeseries.rst docs/cli/zonalstats.rst -src/eolab/rastertools/__init__.py -src/eolab/rastertools/filtering.py -src/eolab/rastertools/hillshade.py -src/eolab/rastertools/main.py -src/eolab/rastertools/radioindice.py -src/eolab/rastertools/rastertools.py -src/eolab/rastertools/speed.py -src/eolab/rastertools/svf.py -src/eolab/rastertools/tiling.py -src/eolab/rastertools/timeseries.py -src/eolab/rastertools/utils.py -src/eolab/rastertools/zonalstats.py -src/eolab/rastertools/__pycache__/__init__.cpython-38.pyc -src/eolab/rastertools/__pycache__/filtering.cpython-38.pyc -src/eolab/rastertools/__pycache__/hillshade.cpython-38.pyc -src/eolab/rastertools/__pycache__/main.cpython-38.pyc -src/eolab/rastertools/__pycache__/radioindice.cpython-38.pyc -src/eolab/rastertools/__pycache__/rastertools.cpython-38.pyc -src/eolab/rastertools/__pycache__/speed.cpython-38.pyc -src/eolab/rastertools/__pycache__/svf.cpython-38.pyc -src/eolab/rastertools/__pycache__/tiling.cpython-38.pyc -src/eolab/rastertools/__pycache__/timeseries.cpython-38.pyc -src/eolab/rastertools/__pycache__/utils.cpython-38.pyc -src/eolab/rastertools/__pycache__/zonalstats.cpython-38.pyc -src/eolab/rastertools/cli/__init__.py -src/eolab/rastertools/cli/filtering.py -src/eolab/rastertools/cli/hillshade.py -src/eolab/rastertools/cli/radioindice.py -src/eolab/rastertools/cli/speed.py -src/eolab/rastertools/cli/svf.py -src/eolab/rastertools/cli/tiling.py -src/eolab/rastertools/cli/timeseries.py src/eolab/rastertools/cli/utils_cli.py -src/eolab/rastertools/cli/zonalstats.py -src/eolab/rastertools/cli/__pycache__/__init__.cpython-38.pyc -src/eolab/rastertools/cli/__pycache__/filtering.cpython-38.pyc -src/eolab/rastertools/cli/__pycache__/hillshade.cpython-38.pyc -src/eolab/rastertools/cli/__pycache__/radioindice.cpython-38.pyc -src/eolab/rastertools/cli/__pycache__/speed.cpython-38.pyc -src/eolab/rastertools/cli/__pycache__/svf.cpython-38.pyc -src/eolab/rastertools/cli/__pycache__/tiling.cpython-38.pyc -src/eolab/rastertools/cli/__pycache__/timeseries.cpython-38.pyc -src/eolab/rastertools/cli/__pycache__/zonalstats.cpython-38.pyc -src/eolab/rastertools/processing/__init__.py -src/eolab/rastertools/processing/algo.py -src/eolab/rastertools/processing/rasterproc.py -src/eolab/rastertools/processing/sliding.py -src/eolab/rastertools/processing/stats.py -src/eolab/rastertools/processing/vector.py -src/eolab/rastertools/processing/__pycache__/__init__.cpython-38.pyc -src/eolab/rastertools/processing/__pycache__/algo.cpython-38.pyc -src/eolab/rastertools/processing/__pycache__/rasterproc.cpython-38.pyc -src/eolab/rastertools/processing/__pycache__/sliding.cpython-38.pyc -src/eolab/rastertools/processing/__pycache__/stats.cpython-38.pyc -src/eolab/rastertools/processing/__pycache__/vector.cpython-38.pyc -src/eolab/rastertools/product/__init__.py -src/eolab/rastertools/product/rasterproduct.py -src/eolab/rastertools/product/rastertype.py -src/eolab/rastertools/product/rastertypes.json -src/eolab/rastertools/product/vrt.py -src/eolab/rastertools/product/__pycache__/__init__.cpython-38.pyc -src/eolab/rastertools/product/__pycache__/rasterproduct.cpython-38.pyc -src/eolab/rastertools/product/__pycache__/rastertype.cpython-38.pyc -src/eolab/rastertools/product/__pycache__/vrt.cpython-38.pyc src/rastertools.egg-info/PKG-INFO src/rastertools.egg-info/SOURCES.txt src/rastertools.egg-info/dependency_links.txt diff --git a/src/rastertools.egg-info/requires.txt b/src/rastertools.egg-info/requires.txt index 0b15b4a..a374e7d 100644 --- a/src/rastertools.egg-info/requires.txt +++ b/src/rastertools.egg-info/requires.txt @@ -1,5 +1,4 @@ click -rasterio==1.3.0 pytest>=3.6 pytest-cov geopandas==0.13 @@ -11,10 +10,11 @@ packaging==24.1 Shapely==1.8.5.post1 tomli==2.0.2 Rtree==1.3.0 +fiona==1.8.21 Pillow==9.2.0 +sphinx_rtd_theme==3.0.1 pip==24.2 pyproj==3.4.0 -matplotlib sphinx==7.1.2 scipy==1.8 pyscaffold diff --git a/tests/test_rasterproduct.py b/tests/test_rasterproduct.py index cc71fce..17fe628 100644 --- a/tests/test_rasterproduct.py +++ b/tests/test_rasterproduct.py @@ -10,7 +10,6 @@ from eolab.rastertools.product import RasterType, BandChannel from eolab.rastertools.product import RasterProduct -from eolab.rastertools.utils import vsimem_to_rasterio from . import utils4test __author__ = "Olivier Queyrut" @@ -360,7 +359,7 @@ def test_create_product_special_cases(): # check if product can be opened by rasterio with rasterio.Env(GDAL_VRT_ENABLE_PYTHON=True): - dataset = vsimem_to_rasterio(raster, nodata=-10000) + dataset = rasterio.open(raster) data = dataset.read([1], masked=True) # pixel corresponding to a value > 0 for a band mask => masked value assert data.mask[0][350][250] From e1546f5d690438279303210515dd69b175a6f9c9 Mon Sep 17 00:00:00 2001 From: cadauxe Date: Thu, 5 Dec 2024 10:04:03 +0100 Subject: [PATCH 12/17] refactor: fixing --compare test --- src/eolab/rastertools/radioindice.py | 31 ++++++++++++- src/eolab/rastertools/speed.py | 29 +++++++++++- src/eolab/rastertools/tiling.py | 33 +++++++++++++- tests/test_radioindice.py | 2 +- tests/test_rasterproduct.py | 66 ++++++++++++++++------------ tests/test_zonalstats.py | 16 ++++--- tests/utils4test.py | 5 ++- 7 files changed, 143 insertions(+), 39 deletions(-) diff --git a/src/eolab/rastertools/radioindice.py b/src/eolab/rastertools/radioindice.py index 72a87f4..d1a446c 100644 --- a/src/eolab/rastertools/radioindice.py +++ b/src/eolab/rastertools/radioindice.py @@ -427,7 +427,6 @@ def process_file(self, inputfile: str) -> List[str]: indices.append(indice) # get the raster - print(self.roi) raster = product.get_raster(roi=self.roi) # STEP 2: Compute the indices @@ -545,6 +544,10 @@ def compute_indices(input_image: str, image_channels: List[BandChannel], # disable status of tqdm progress bar disable = os.getenv("RASTERTOOLS_NOTQDM", 'False').lower() in ['true', '1'] + # Dictionary to store statistics for each band + band_stats = {i: {"min": float('inf'), "max": float('-inf'), "sum": 0, "total_pix": 0, "count": 0} + for i in range(1, len(indices) + 1)} + # compute every indices for i, indice in enumerate(indices, 1): # Get the bands necessary to compute the indice @@ -563,6 +566,15 @@ def process(window): # The computation can be performed concurrently result = indice.algo(src_array).astype(dtype).filled(indice.nodata) + # Update statistics + valid_pixels = result[result != indice.nodata] + if valid_pixels.size > 0: + band_stats[i]["min"] = min(band_stats[i]["min"], valid_pixels.min()) + band_stats[i]["max"] = max(band_stats[i]["max"], valid_pixels.max()) + band_stats[i]["sum"] += valid_pixels.sum() + band_stats[i]["total_pix"] += result.size + band_stats[i]["count"] += valid_pixels.size + with write_lock: dst.write_band(i, result, window=window) @@ -571,4 +583,19 @@ def process(window): process(window) dst.set_band_description(i, indice.name) - src.close() + + # Compute and set metadata tags + for i, stats in band_stats.items(): + # Compute and set metadata tags + mean = stats["sum"] / stats["count"] + sum_sq = (stats["sum"] - mean * stats["count"]) ** 2 + variance = sum_sq / stats["count"] + stddev = variance ** 0.5 if variance > 0 else 0 + + dst.update_tags(i, + STATISTICS_MINIMUM=f"{stats['min']:.14g}", + STATISTICS_MAXIMUM=f"{stats['max']:.14g}", + STATISTICS_MEAN=mean, + STATISTICS_STDDEV=stddev, + STATISTICS_VALID_PERCENT=(stats["count"] / stats["total_pix"] * 100)) + diff --git a/src/eolab/rastertools/speed.py b/src/eolab/rastertools/speed.py index 4350dae..7a79b27 100644 --- a/src/eolab/rastertools/speed.py +++ b/src/eolab/rastertools/speed.py @@ -139,6 +139,7 @@ def compute_speed(date0: datetime, date1: datetime, profile = src0.profile dtype = rasterio.float32 + nodata = src0.nodata # set block size blockysize = 1024 if src0.width > 1024 else utils.highest_power_of_2(src0.width) @@ -154,6 +155,9 @@ def compute_speed(date0: datetime, date1: datetime, blockxsize=blockysize, blockysize=blockxsize, tiled=True, dtype=dtype, count=len(bands)) + # Dictionary to store statistics for each band + stats = {"min": float('inf'), "max": float('-inf'), "sum": 0, "total_pix": 0, "count": 0} + with rasterio.open(speed_image, "w", **profile) as dst: # Materialize a list of destination block windows windows = [window for ij, window in dst.block_windows()] @@ -168,10 +172,33 @@ def process(window): data1 = src1.read(bands, window=window, masked=True).astype(dtype) # The computation can be performed concurrently - result = algo.speed(data0, data1, interval).astype(dtype).filled(src0.nodata) + result = algo.speed(data0, data1, interval).astype(dtype).filled(nodata) + + # Update statistics + valid_pixels = result[result != nodata] + if valid_pixels.size > 0: + stats["min"] = min(stats["min"], valid_pixels.min()) + stats["max"] = max(stats["max"], valid_pixels.max()) + stats["sum"] += valid_pixels.sum() + stats["total_pix"] += result.size + stats["count"] += valid_pixels.size with write_lock: dst.write(result, window=window) disable = os.getenv("RASTERTOOLS_NOTQDM", 'False').lower() in ['true', '1'] thread_map(process, windows, disable=disable, desc="speed") + + # Compute and set metadata tags + mean = stats["sum"] / stats["count"] + sum_sq = (stats["sum"] - mean * stats["count"])**2 + variance = sum_sq / stats["count"] + stddev = variance ** 0.5 if variance > 0 else 0 + + dst.update_tags(1, + STATISTICS_MINIMUM=f"{stats['min']:.14g}", + STATISTICS_MAXIMUM=f"{stats['max']:.14g}", + STATISTICS_MEAN=mean, + STATISTICS_STDDEV=stddev, + STATISTICS_VALID_PERCENT=(stats["count"] / stats["total_pix"] * 100), + STATISTICS_APPROXIMATE="YES") diff --git a/src/eolab/rastertools/tiling.py b/src/eolab/rastertools/tiling.py index 57bf1e3..f6061a2 100644 --- a/src/eolab/rastertools/tiling.py +++ b/src/eolab/rastertools/tiling.py @@ -168,10 +168,16 @@ def process_file(self, inputfile: str): outputs = [] with product.open() as dataset: out_meta = dataset.meta + nodata = dataset.nodata # Crop and export every tiles for shape, i in zip(grid.geometry, grid.index): _logger.info("Crop and export tile " + str(i) + "...") + + # Dictionary to store statistics for each band + band_stats = {i: {"min": float('inf'), "max": float('-inf'), "mean": 0, "stddev": 0, "val_per": 0} + for i in range(1, dataset.meta["count"] + 1)} + try: # generate crop image image, transform = rasterio.mask.mask(dataset, [shape], @@ -193,12 +199,37 @@ def process_file(self, inputfile: str): "width": image.shape[2], "transform": transform}) + with rasterio.open(output, 'w', **out_meta) as dst: dst.write(image) + for bd in range(1, dataset.meta["count"] + 1): + # Update statistics + valid_pixels = image[bd-1][image[bd-1] != nodata] + + + band_stats[bd]["min"] = valid_pixels.min() + band_stats[bd]["max"] = valid_pixels.max() + band_stats[bd]["mean"] = valid_pixels.sum() / valid_pixels.size + variance = (((valid_pixels - band_stats[bd]["mean"])**2).sum()) / valid_pixels.size + band_stats[bd]["stddev"] = variance ** 0.5 if variance > 0 else 0 + + val_per = valid_pixels.size / image[bd-1].size * 100 + if int(val_per) == 100 : + band_stats[bd]["val_per"] = int(val_per) + else: + band_stats[bd]["val_per"] = round(val_per,2) + + dst.update_tags(bd, + STATISTICS_MINIMUM=f"{band_stats[bd]['min']:.14g}", + STATISTICS_MAXIMUM=f"{band_stats[bd]['max']:.14g}", + STATISTICS_MEAN=f"{band_stats[bd]['mean']:.14g}", + STATISTICS_STDDEV=f"{band_stats[bd]['stddev']:.14g}", + STATISTICS_VALID_PERCENT= band_stats[bd]['val_per']) + outputs.append(output.as_posix()) _logger.info("Tile " + str(i) + " exported to " + str(output)) except ValueError: # if no overlap _logger.error("Input shape " + str(i) + " does not overlap raster") - return outputs + return outputs diff --git a/tests/test_radioindice.py b/tests/test_radioindice.py index 234fc9e..96e431c 100644 --- a/tests/test_radioindice.py +++ b/tests/test_radioindice.py @@ -94,7 +94,7 @@ def test_radioindice_process_file_separate(compare : bool, save_gen_as_ref : boo # save the generated files in the refdir => make them the new refs. utils4test.copy_to_ref(gen_files, __refdir) - # utils4test.clear_outdir() + utils4test.clear_outdir() def test_radioindice_process_files(): diff --git a/tests/test_rasterproduct.py b/tests/test_rasterproduct.py index 17fe628..4aeb98d 100644 --- a/tests/test_rasterproduct.py +++ b/tests/test_rasterproduct.py @@ -18,7 +18,7 @@ from .utils4test import RastertoolsTestsData -__refdir = utils4test.get_refdir("test_rasterproduct/") +__refdir = RastertoolsTestsData.tests_ref_data_dir.replace(os.getcwd() + "/", "") + "/test_rasterproduct/" def test_rasterproduct_valid_parameters(): @@ -138,35 +138,38 @@ def test_create_product_S2_L2A_MAJA(compare, save_gen_as_ref): - Comparison or saving of reference files completes without errors. - Raster data can be opened without errors. """ + data_path = RastertoolsTestsData.tests_input_data_dir.replace(os.getcwd() + "/", "") + "/" + out_path = RastertoolsTestsData.tests_output_data_dir.replace(os.getcwd() + "/", "") + "/" + # create output dir and clear its content if any utils4test.create_outdir() # unzip SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip - file = RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip" + file = data_path + "/" + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip" with zipfile.ZipFile(file) as myzip: - myzip.extractall(RastertoolsTestsData.tests_output_data_dir + "/") + myzip.extractall(out_path) # creation of S2 L2A MAJA products - files = [Path(RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip"), - Path(RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_tar.tar"), - Path(RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_targz.TAR.GZ"), - Path(RastertoolsTestsData.tests_output_data_dir + "/" + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_V1-9")] + files = [Path(data_path + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip"), + Path(data_path + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_tar.tar"), + Path(data_path + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_targz.TAR.GZ"), + Path(out_path + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_V1-9")] for file in files: - with RasterProduct(file, vrt_outputdir=Path(RastertoolsTestsData.tests_output_data_dir + "/")) as prod: - raster = prod.get_raster(roi=Path(RastertoolsTestsData.tests_input_data_dir + "/" + "COMMUNE_32001.shp"), + with RasterProduct(file, vrt_outputdir=Path(out_path)) as prod: + raster = prod.get_raster(roi=Path(data_path + "COMMUNE_32001.shp"), masks="all") assert Path(raster).exists() - assert raster == RastertoolsTestsData.tests_output_data_dir + "/" + utils4test.basename(file) + "-mask.vrt" + assert raster == out_path + utils4test.basename(file) + "-mask.vrt" ref = [utils4test.basename(file) + ".vrt", utils4test.basename(file) + "-clipped.vrt", utils4test.basename(file) + "-mask.vrt"] if compare: - print(f"compare {RastertoolsTestsData.tests_output_data_dir} ,{__refdir}, {ref}") - match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, ref) + print(f"compare {out_path} ,{__refdir}, {ref}") + match, mismatch, err = utils4test.cmpfiles(out_path, __refdir, ref) assert len(match) == len(ref) assert len(mismatch) == 0 assert len(err) == 0 @@ -180,8 +183,9 @@ def test_create_product_S2_L2A_MAJA(compare, save_gen_as_ref): utils4test.clear_outdir(subdirs=False) -# delete the dir resulting from unzip - utils4test.delete_dir(RastertoolsTestsData.tests_output_data_dir + "/" + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_V1-9") + # delete the dir resulting from unzip + utils4test.delete_dir(out_path + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_V1-9") + def test_create_product_S2_L1C(compare, save_gen_as_ref): @@ -202,22 +206,25 @@ def test_create_product_S2_L1C(compare, save_gen_as_ref): - Reference comparison or saving completes as expected. - Raster data can be loaded and accessed without errors. """ + data_path = RastertoolsTestsData.tests_input_data_dir.replace(os.getcwd() + "/", "") + "/" + out_path = RastertoolsTestsData.tests_output_data_dir.replace(os.getcwd() + "/", "") + "/" + # create output dir and clear its content if any utils4test.create_outdir() # creation of S2 L1C product - infile = RastertoolsTestsData.tests_input_data_dir + "/" + "S2B_MSIL1C_20191008T105029_N0208_R051_T30TYP_20191008T125041.zip" + infile = data_path + "S2B_MSIL1C_20191008T105029_N0208_R051_T30TYP_20191008T125041.zip" - with RasterProduct(infile, vrt_outputdir=RastertoolsTestsData.tests_output_data_dir + "/") as prod: - raster = prod.get_raster(roi=RastertoolsTestsData.tests_input_data_dir + "/" + "/COMMUNE_32001.shp", + with RasterProduct(infile, vrt_outputdir= out_path) as prod: + raster = prod.get_raster(roi= data_path + "/COMMUNE_32001.shp", masks="all") assert Path(raster).exists() - assert raster == RastertoolsTestsData.tests_output_data_dir + "/" + utils4test.basename(infile) + "-clipped.vrt" + assert raster == out_path + utils4test.basename(infile) + "-clipped.vrt" gen_files = [utils4test.basename(infile) + ".vrt", utils4test.basename(infile) + "-clipped.vrt"] if compare: - match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(out_path, __refdir, gen_files) assert len(match) == 2 assert len(mismatch) == 0 assert len(err) == 0 @@ -250,20 +257,23 @@ def test_create_product_S2_L2A_SEN2CORE(compare, save_gen_as_ref): - Reference file operations are successful. - The VRT file can be accessed with `rasterio` without issues. """ + data_path = RastertoolsTestsData.tests_input_data_dir.replace(os.getcwd() + "/", "") + "/" + out_path = RastertoolsTestsData.tests_output_data_dir.replace(os.getcwd() + "/", "") + "/" + # create output dir and clear its content if any utils4test.create_outdir() # creation of S2 L2A SEN2CORE product - infile = RastertoolsTestsData.tests_input_data_dir + "/" + "S2A_MSIL2A_20190116T105401_N0211_R051_T30TYP_20190116T120806.zip" - with RasterProduct(infile, vrt_outputdir=RastertoolsTestsData.tests_output_data_dir + "/") as prod: + infile = data_path + "S2A_MSIL2A_20190116T105401_N0211_R051_T30TYP_20190116T120806.zip" + with RasterProduct(infile, vrt_outputdir= out_path) as prod: raster = prod.get_raster() assert Path(raster).exists() - assert raster == RastertoolsTestsData.tests_output_data_dir + "/" + utils4test.basename(infile) + ".vrt" + assert raster == out_path + utils4test.basename(infile) + ".vrt" gen_files = [utils4test.basename(infile) + ".vrt"] if compare: - match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(out_path, __refdir, gen_files) assert len(match) == 1 assert len(mismatch) == 0 assert len(err) == 0 @@ -296,20 +306,23 @@ def test_create_product_SPOT67(compare, save_gen_as_ref): - Reference file comparison and saving are correctly performed. - Raster data opens without errors in `rasterio`. """ + data_path = RastertoolsTestsData.tests_input_data_dir.replace(os.getcwd() + "/", "") + "/" + out_path = RastertoolsTestsData.tests_output_data_dir.replace(os.getcwd() + "/", "") + "/" + # create output dir and clear its content if any utils4test.create_outdir() # creation of SPOT67 product infile = "SPOT6_2018_France-Ortho_NC_DRS-MS_SPOT6_2018_FRANCE_ORTHO_NC_GEOSUD_MS_82.tar.gz" - with RasterProduct(RastertoolsTestsData.tests_input_data_dir + "/" + infile, vrt_outputdir=RastertoolsTestsData.tests_output_data_dir + "/") as prod: + with RasterProduct(data_path + infile, vrt_outputdir= out_path) as prod: raster = prod.get_raster() assert Path(raster).exists() - assert raster == RastertoolsTestsData.tests_output_data_dir + "/" + utils4test.basename(infile) + ".vrt" + assert raster == out_path + utils4test.basename(infile) + ".vrt" gen_files = [utils4test.basename(infile) + ".vrt"] if compare: - match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(out_path, __refdir, gen_files) assert len(match) == 1 assert len(mismatch) == 0 assert len(err) == 0 @@ -339,7 +352,6 @@ def test_create_product_special_cases(): - Raster files can be opened without errors in `rasterio`. """ # SUPPORTED CASES - # creation in memory (without masks) file = "S2B_MSIL1C_20191008T105029_N0208_R051_T30TYP_20191008T125041.zip" with RasterProduct(RastertoolsTestsData.tests_input_data_dir + "/" + file) as prod: diff --git a/tests/test_zonalstats.py b/tests/test_zonalstats.py index ed637f9..0d87ee2 100644 --- a/tests/test_zonalstats.py +++ b/tests/test_zonalstats.py @@ -1,5 +1,6 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +import os import pytest from pathlib import Path @@ -131,23 +132,26 @@ def test_zonalstats_zonal(compare, save_gen_as_ref): def test_zonalstats_process_files(compare, save_gen_as_ref): # create output dir and clear its content if any + data_path = RastertoolsTestsData.tests_input_data_dir.replace(os.getcwd() + "/", "") + "/" + out_path = RastertoolsTestsData.tests_output_data_dir.replace(os.getcwd() + "/", "") + "/" + utils4test.create_outdir() - inputfiles = [RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif", - RastertoolsTestsData.tests_input_data_dir + "/" + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif"] + inputfiles = [data_path + "SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif", + data_path + "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif"] outformat = "GeoJSON" statistics = "min max mean std count range sum".split() tool = Zonalstats(statistics, prefix="indice") tool.with_output(None, output_format=outformat) - tool.with_geometries(geometries=RastertoolsTestsData.tests_input_data_dir + "/" + "COMMUNE_32xxx.geojson") - tool.with_chart(chart_file=RastertoolsTestsData.tests_output_data_dir + "/" + "chart.png") + tool.with_geometries(geometries=data_path + "COMMUNE_32xxx.geojson") + tool.with_chart(chart_file=out_path + "chart.png") tool.process_files(inputfiles) gen_files = ["chart.png"] - assert Path(RastertoolsTestsData.tests_output_data_dir + "/" + "chart.png").exists() + assert Path(out_path + "chart.png").exists() if compare: - match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(out_path, __refdir, gen_files) assert len(match) == 1 assert len(mismatch) == 0 assert len(err) == 0 diff --git a/tests/utils4test.py b/tests/utils4test.py index bc60307..772c05b 100644 --- a/tests/utils4test.py +++ b/tests/utils4test.py @@ -11,6 +11,10 @@ __copyright = "Copyright 2019, CNES" __license = "Apache v2.0" +indir = "tests/tests_data/" +outdir = "tests/tests_out/" +__root_refdir = "tests/tests_refs/" + @dataclass class RastertoolsTestsData: @@ -76,7 +80,6 @@ def cmpfiles(a : str, b : str, common : list, tolerance : float =1e-9) -> tuple: new = os.path.join(a, x) golden = os.path.join(b, x) res[_cmp(golden, new, tolerance)].append(x) - print(res) return res From 05469a6bd8b2558375bb76878a337f5c8b397ec6 Mon Sep 17 00:00:00 2001 From: cadauxe Date: Thu, 5 Dec 2024 17:50:08 +0100 Subject: [PATCH 13/17] refactor: cleaned code --- setup.py | 8 ---- .../rastertools/product/rasterproduct.py | 14 ------ src/eolab/rastertools/radioindice.py | 44 ------------------- 3 files changed, 66 deletions(-) diff --git a/setup.py b/setup.py index e81fa89..4397cf6 100644 --- a/setup.py +++ b/setup.py @@ -18,23 +18,15 @@ setup_requires = ["setuptools_scm"], install_requires=[ 'click', - # 'rasterio==1.3.0', 'pytest>=3.6', 'pytest-cov', 'geopandas==0.13', - 'python-dateutil==2.9.0', 'kiwisolver==1.4.5', - 'fonttools==4.53.1', 'matplotlib==3.7.3', 'packaging==24.1', - 'Shapely==1.8.5.post1', - 'tomli==2.0.2', - 'Rtree==1.3.0', 'fiona==1.8.21', - 'Pillow==9.2.0', 'sphinx_rtd_theme==3.0.1', 'pip==24.2', - 'pyproj==3.4.0', 'sphinx==7.1.2', 'scipy==1.8', 'pyscaffold', diff --git a/src/eolab/rastertools/product/rasterproduct.py b/src/eolab/rastertools/product/rasterproduct.py index 1c7767d..4eebf15 100644 --- a/src/eolab/rastertools/product/rasterproduct.py +++ b/src/eolab/rastertools/product/rasterproduct.py @@ -13,7 +13,6 @@ import tempfile from uuid import uuid4 -from rasterio.io import MemoryFile from osgeo import gdal import rasterio @@ -112,19 +111,6 @@ def __exit__(self, *args): """Exit method for with statement, it cleans the in memory vrt products""" self.free_in_memory_vrts() - def create_in_memory_vrt(self, vrt_content): - """ - Create an in-memory VRT using Rasterio's MemoryFile. - - Args: - vrt_content (str): The XML content of the VRT file. - """ - with MemoryFile() as memfile: - # Write the VRT content into the memory file - memfile.write(vrt_content.encode('utf-8')) - dataset = memfile.open() # Open the VRT as a dataset - self._in_memory_vrts.append(memfile) - def free_in_memory_vrts(self): """ Free in-memory VRTs by closing all MemoryFile objects. diff --git a/src/eolab/rastertools/radioindice.py b/src/eolab/rastertools/radioindice.py index d1a446c..3901557 100644 --- a/src/eolab/rastertools/radioindice.py +++ b/src/eolab/rastertools/radioindice.py @@ -451,50 +451,6 @@ def process_file(self, inputfile: str) -> List[str]: # return the list of generated files return outputs -def get_raster_profile(raster): - # Open the dataset - - # Get the raster driver - driver = raster.GetDriver().ShortName - - # Get raster dimensions - width = raster.RasterXSize - height = raster.RasterYSize - count = raster.RasterCount - - # Get geotransform and projection - geotransform = raster.GetGeoTransform() - crs = raster.GetProjection() - - # Get data type and block size from the first band - band = raster.GetRasterBand(1) - dtype_rasterio = rasterio.dtypes.get_minimum_dtype(band.DataType) - nodata = band.GetNoDataValue() - - # Get block size and tiled status - blockxsize, blockysize = band.GetBlockSize() - tiled = raster.GetMetadata('IMAGE_STRUCTURE').get('TILED', 'NO') == 'YES' - - # Convert geotransform to Rasterio-compatible affine transform - transform = rasterio.Affine.from_gdal(*geotransform) - - # Build a profile dictionary similar to rasterio - profile = { - "driver": driver, # e.g., "GTiff" - "width": width, - "height": height, - "count": count, - "crs": crs, - "transform": transform, # Affine geotransform - "dtype": dtype_rasterio, - "nodata": nodata, # Nodata value - "blockxsize": blockxsize, # Block width - "blockysize": blockysize, # Block height - "tiled": tiled # Whether the raster is tiled - } - - return profile - def compute_indices(input_image: str, image_channels: List[BandChannel], indice_image: str, indices: List[RadioindiceProcessing], window_size: tuple = (1024, 1024)): From b789397bfda910279d6af312bad8602091a26b88 Mon Sep 17 00:00:00 2001 From: cadauxe Date: Fri, 6 Dec 2024 11:40:57 +0100 Subject: [PATCH 14/17] refactor: setup w/ rasterio --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 4397cf6..920fbd4 100644 --- a/setup.py +++ b/setup.py @@ -18,6 +18,7 @@ setup_requires = ["setuptools_scm"], install_requires=[ 'click', + 'rasterio', 'pytest>=3.6', 'pytest-cov', 'geopandas==0.13', From 9de095ef1941ac701ab71cd8f42a30aff1b039ce Mon Sep 17 00:00:00 2001 From: cadauxe Date: Fri, 6 Dec 2024 16:52:54 +0100 Subject: [PATCH 15/17] refactor: test --compare modifications --- .gitignore | 4 ++-- env_test.yml | 11 ----------- env_update.yml | 9 --------- environment.yml | 10 ---------- src/eolab/rastertools/cli/utils_cli.py | 1 - tests/cmptools.py | 8 ++++++-- tests/test_vector.py | 8 ++++---- tests/test_zonalstats.py | 17 +++-------------- tests/utils4test.py | 8 ++++---- 9 files changed, 19 insertions(+), 57 deletions(-) delete mode 100644 env_test.yml delete mode 100644 env_update.yml delete mode 100644 environment.yml diff --git a/.gitignore b/.gitignore index de02313..cec6437 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ # Distribution / packaging .Python build/ develop-eggs/ -dist/ downloads/ eggs/ .eggs/ @@ -15,10 +14,11 @@ parts/ sdist/ var/ wheels/ -*.whl +dist/ *.egg-info/ .installed.cfg *.egg +*.whl *.manifest *.spec diff --git a/env_test.yml b/env_test.yml deleted file mode 100644 index 238cad9..0000000 --- a/env_test.yml +++ /dev/null @@ -1,11 +0,0 @@ -name: rastertools - -channels: - - conda-forge - -dependencies: - - pytest - - pytest-cov - - sphinx - - sphinx_rtd_theme - diff --git a/env_update.yml b/env_update.yml deleted file mode 100644 index 5aa341d..0000000 --- a/env_update.yml +++ /dev/null @@ -1,9 +0,0 @@ -name: rastertools - -channels: - - conda-forge - -dependencies: - - pyscaffold - - geopandas ==0.13 - - rasterio ==1.3 diff --git a/environment.yml b/environment.yml deleted file mode 100644 index 96531f3..0000000 --- a/environment.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: temp_test - -channels: - - conda-forge - -dependencies: - - python ==3.8.13 - - scipy ==1.8 - - gdal ==3.5 - - tqdm ==4.66 diff --git a/src/eolab/rastertools/cli/utils_cli.py b/src/eolab/rastertools/cli/utils_cli.py index 18d116a..3e5abd1 100644 --- a/src/eolab/rastertools/cli/utils_cli.py +++ b/src/eolab/rastertools/cli/utils_cli.py @@ -3,7 +3,6 @@ import sys import click -#TO DO _logger = logging.getLogger(__name__) all_opt = click.option('-a', '--all','all_bands', type=bool, is_flag=True, help="Process all bands") diff --git a/tests/cmptools.py b/tests/cmptools.py index b63f451..5ee763f 100644 --- a/tests/cmptools.py +++ b/tests/cmptools.py @@ -7,7 +7,7 @@ from osgeo_utils import gdalcompare -def cmp_geojson(golden, new, tolerance=1e-9): +def cmp_geojson(golden, new, column_sortby = None, tolerance=1e-9): # Load GeoJSON files gld_gj = json.load(open(golden)) new_gj = json.load(open(new)) @@ -16,6 +16,10 @@ def cmp_geojson(golden, new, tolerance=1e-9): gld_gdf = gpd.GeoDataFrame.from_features(gld_gj["features"]) new_gdf = gpd.GeoDataFrame.from_features(new_gj["features"]) + if column_sortby : + gld_gdf = gld_gdf.sort_values(by=column_sortby) + new_gdf = new_gdf.sort_values(by=column_sortby) + equals = True # Compare each geometry using Hausdorff distance for gld_geom, new_geom in zip(gld_gdf.geometry, new_gdf.geometry): @@ -108,7 +112,7 @@ def cmp_vrt(golden, new, tolerance=1e-9): def cmp_tif(golden, new, tolerance=1e-2): gld_ds = gdal.Open(golden, gdal.GA_ReadOnly) new_ds = gdal.Open(new, gdal.GA_ReadOnly) - d_count = gdalcompare.compare_db(gld_ds, new_ds) + d_count = gdalcompare.compare_db(gld_ds, new_ds, options= ['SKIP_METADATA']) p_count = gld_ds.RasterCount * gld_ds.RasterXSize * gld_ds.RasterYSize return d_count *100. / p_count < tolerance diff --git a/tests/test_vector.py b/tests/test_vector.py index accf190..79fb427 100644 --- a/tests/test_vector.py +++ b/tests/test_vector.py @@ -56,7 +56,7 @@ def test_reproject_filter(compare, save_gen_as_ref): assert len(geoms) == 19 gen_files = ["reproject_filter.geojson"] if compare: - match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files, tolerance = 1.e-8) assert len(match) == 1 assert len(mismatch) == 0 assert len(err) == 0 @@ -73,7 +73,7 @@ def test_reproject_filter(compare, save_gen_as_ref): assert len(geoms) == 19 gen_files = ["reproject_filter.geojson"] if compare: - match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files, tolerance = 1.e-8) assert len(match) == 1 assert len(mismatch) == 0 assert len(err) == 0 @@ -100,7 +100,7 @@ def test_reproject_dissolve(compare, save_gen_as_ref): assert len(geoms) == 1 gen_files = ["reproject_dissolve.geojson"] if compare: - match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files, tolerance = 1.e-8) assert len(match) == 1 assert len(mismatch) == 0 assert len(err) == 0 @@ -126,7 +126,7 @@ def test_clip(compare, save_gen_as_ref): assert len(geoms) == 19 gen_files = ["clip.geojson"] if compare: - match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files, tolerance = 1.e-8, column_sortby = 'ID') assert len(match) == 1 assert len(mismatch) == 0 assert len(err) == 0 diff --git a/tests/test_zonalstats.py b/tests/test_zonalstats.py index 0d87ee2..a173598 100644 --- a/tests/test_zonalstats.py +++ b/tests/test_zonalstats.py @@ -98,7 +98,7 @@ def test_zonalstats_zonal(compare, save_gen_as_ref): gen_files = ["SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi-stats.shp", "SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi-stats-outliers.tif"] if compare: - match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files, tolerance = 1.e-8) assert len(match) == 2 assert len(mismatch) == 0 assert len(err) == 0 @@ -119,7 +119,7 @@ def test_zonalstats_zonal(compare, save_gen_as_ref): gen_files = ["SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi-stats.geojson"] if compare: - match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files, tolerance = 1.e-8) assert len(match) == 1 assert len(mismatch) == 0 assert len(err) == 0 @@ -148,17 +148,6 @@ def test_zonalstats_process_files(compare, save_gen_as_ref): tool.with_chart(chart_file=out_path + "chart.png") tool.process_files(inputfiles) - gen_files = ["chart.png"] - assert Path(out_path + "chart.png").exists() - if compare: - match, mismatch, err = utils4test.cmpfiles(out_path, __refdir, gen_files) - assert len(match) == 1 - assert len(mismatch) == 0 - assert len(err) == 0 - elif save_gen_as_ref: - # save the generated files in the refdir => make them the new refs. - utils4test.copy_to_ref(gen_files, __refdir) - utils4test.clear_outdir() @@ -183,7 +172,7 @@ def test_zonalstats_category(compare, save_gen_as_ref): gen_files = ["DSM_PHR_Dunkerque-stats.geojson"] if compare: - match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files) + match, mismatch, err = utils4test.cmpfiles(RastertoolsTestsData.tests_output_data_dir + "/", __refdir, gen_files, tolerance = 1.e-8) assert len(match) == 1 assert len(mismatch) == 0 assert len(err) == 0 diff --git a/tests/utils4test.py b/tests/utils4test.py index 772c05b..ba4b623 100644 --- a/tests/utils4test.py +++ b/tests/utils4test.py @@ -64,7 +64,7 @@ def basename(infile): return file.name if suffix == 0 else file.name[:-suffix] -def cmpfiles(a : str, b : str, common : list, tolerance : float =1e-9) -> tuple: +def cmpfiles(a : str, b : str, common : list, tolerance : float =1e-9, **kwargs) -> tuple: """ Compare common files in two directories. @@ -79,17 +79,17 @@ def cmpfiles(a : str, b : str, common : list, tolerance : float =1e-9) -> tuple: for x in common: new = os.path.join(a, x) golden = os.path.join(b, x) - res[_cmp(golden, new, tolerance)].append(x) + res[_cmp(golden, new, tolerance, **kwargs)].append(x) return res -def _cmp(gld, new, tolerance): +def _cmp(gld, new, tolerance, **kwargs): """ """ ftype = os.path.splitext(gld)[-1].lower() cmp = cmptools.CMP_FUN[ftype] try: - return not cmp(gld, new, tolerance=tolerance) + return not cmp(gld, new, tolerance=tolerance, **kwargs) except OSError: return 2 From 4543961b8a10f9908f7027e2825f553fb94b9c99 Mon Sep 17 00:00:00 2001 From: Arthur VINCENT Date: Mon, 9 Dec 2024 11:21:32 +0100 Subject: [PATCH 16/17] CI: add CI/CD workflows --- .github/workflows/cd.yml | 45 + .github/workflows/ci.yml | 53 + README.rst | 150 +- .../scripts/check_mccabe_complexity.sh | 36 + docs/cli.rst | 32 +- docs/cli/filtering.rst | 14 +- docs/cli/hillshade.rst | 10 +- docs/cli/radioindice.rst | 12 +- docs/cli/speed.rst | 8 +- docs/cli/svf.rst | 8 +- docs/cli/tiling.rst | 8 +- docs/cli/timeseries.rst | 8 +- docs/cli/zonalstats.rst | 10 +- docs/conf.py | 8 +- docs/index.rst | 16 +- docs/install.rst | 12 +- docs/private_api.rst | 4 +- docs/rasterproduct.rst | 10 +- docs/usage.rst | 32 +- setup.cfg | 12 +- setup.py | 12 +- src/eolab/georastertools/__init__.py | 36 + .../cli/__init__.py | 2 +- .../cli/filtering.py | 6 +- .../cli/hillshade.py | 4 +- .../cli/radioindice.py | 10 +- .../cli/speed.py | 4 +- .../cli/svf.py | 4 +- .../cli/tiling.py | 4 +- .../cli/timeseries.py | 6 +- .../cli/utils_cli.py | 2 +- .../cli/zonalstats.py | 4 +- .../filtering.py | 18 +- .../georastertools.py} | 14 +- .../hillshade.py | 8 +- .../{rastertools => georastertools}/main.py | 70 +- .../processing/__init__.py | 12 +- .../processing/algo.py | 0 .../processing/rasterproc.py | 8 +- .../processing/sliding.py | 4 +- .../processing/stats.py | 8 +- .../processing/vector.py | 2 +- .../product/__init__.py | 6 +- .../product/rasterproduct.py | 8 +- .../product/rastertype.py | 16 +- .../product/rastertypes.json | 2 +- .../product/vrt.py | 0 .../radioindice.py | 1114 +++++++-------- .../{rastertools => georastertools}/speed.py | 12 +- .../{rastertools => georastertools}/svf.py | 8 +- .../{rastertools => georastertools}/tiling.py | 12 +- .../timeseries.py | 10 +- .../{rastertools => georastertools}/utils.py | 0 .../zonalstats.py | 1236 ++++++++--------- src/eolab/rastertools/__init__.py | 36 - .../__pycache__/__init__.cpython-38.pyc | Bin 1168 -> 0 bytes .../__pycache__/filtering.cpython-38.pyc | Bin 6678 -> 0 bytes .../__pycache__/hillshade.cpython-38.pyc | Bin 5330 -> 0 bytes .../__pycache__/main.cpython-38.pyc | Bin 8449 -> 0 bytes .../__pycache__/radioindice.cpython-38.pyc | Bin 9723 -> 0 bytes .../__pycache__/rastertools.cpython-38.pyc | Bin 7904 -> 0 bytes .../__pycache__/speed.cpython-38.pyc | Bin 5934 -> 0 bytes .../__pycache__/svf.cpython-38.pyc | Bin 5953 -> 0 bytes .../__pycache__/tiling.cpython-38.pyc | Bin 7250 -> 0 bytes .../__pycache__/timeseries.cpython-38.pyc | Bin 8371 -> 0 bytes .../__pycache__/utils.cpython-38.pyc | Bin 3645 -> 0 bytes .../__pycache__/zonalstats.cpython-38.pyc | Bin 20867 -> 0 bytes .../cli/__pycache__/__init__.cpython-38.pyc | Bin 1910 -> 0 bytes .../cli/__pycache__/filtering.cpython-38.pyc | Bin 2828 -> 0 bytes .../cli/__pycache__/hillshade.cpython-38.pyc | Bin 2909 -> 0 bytes .../__pycache__/radioindice.cpython-38.pyc | Bin 4567 -> 0 bytes .../cli/__pycache__/speed.cpython-38.pyc | Bin 2110 -> 0 bytes .../cli/__pycache__/svf.cpython-38.pyc | Bin 2727 -> 0 bytes .../cli/__pycache__/tiling.cpython-38.pyc | Bin 3307 -> 0 bytes .../cli/__pycache__/timeseries.cpython-38.pyc | Bin 3264 -> 0 bytes .../cli/__pycache__/zonalstats.cpython-38.pyc | Bin 5417 -> 0 bytes .../__pycache__/__init__.cpython-38.pyc | Bin 824 -> 0 bytes .../__pycache__/algo.cpython-38.pyc | Bin 14808 -> 0 bytes .../__pycache__/rasterproc.cpython-38.pyc | Bin 9427 -> 0 bytes .../__pycache__/sliding.cpython-38.pyc | Bin 7822 -> 0 bytes .../__pycache__/stats.cpython-38.pyc | Bin 14262 -> 0 bytes .../__pycache__/vector.cpython-38.pyc | Bin 13412 -> 0 bytes .../__pycache__/__init__.cpython-38.pyc | Bin 993 -> 0 bytes .../__pycache__/rasterproduct.cpython-38.pyc | Bin 19766 -> 0 bytes .../__pycache__/rastertype.cpython-38.pyc | Bin 15064 -> 0 bytes .../product/__pycache__/vrt.cpython-38.pyc | Bin 4900 -> 0 bytes src/rastertools.egg-info/PKG-INFO | 196 --- src/rastertools.egg-info/SOURCES.txt | 177 --- src/rastertools.egg-info/dependency_links.txt | 1 - src/rastertools.egg-info/entry_points.txt | 2 - src/rastertools.egg-info/not-zip-safe | 1 - src/rastertools.egg-info/requires.txt | 27 - src/rastertools.egg-info/top_level.txt | 1 - tests/__pycache__/__init__.cpython-38.pyc | Bin 145 -> 168 bytes tests/__pycache__/cmptools.cpython-38.pyc | Bin 2946 -> 3070 bytes tests/__pycache__/utils4test.cpython-38.pyc | Bin 2537 -> 3214 bytes tests/conftest.py | 2 +- tests/test_algo.py | 2 +- tests/test_radioindice.py | 8 +- tests/test_rasterproc.py | 2 +- tests/test_rasterproduct.py | 4 +- tests/test_rastertools.py | 72 +- tests/test_rastertype.py | 6 +- tests/test_speed.py | 4 +- tests/test_stats.py | 2 +- tests/test_tiling.py | 2 +- tests/test_utils.py | 4 +- tests/test_vector.py | 2 +- tests/test_zonalstats.py | 4 +- ..._20181023-105107-455_L2A_T30TYP_D-mask.vrt | 2 +- ...1023-105107-455_L2A_T30TYP_D_V1-9-mask.vrt | 2 +- ...81023-105107-455_L2A_T30TYP_D_tar-mask.vrt | 2 +- ...023-105107-455_L2A_T30TYP_D_targz-mask.vrt | 2 +- 113 files changed, 1721 insertions(+), 1992 deletions(-) create mode 100644 .github/workflows/cd.yml create mode 100644 .github/workflows/ci.yml create mode 100755 continuous_integration/scripts/check_mccabe_complexity.sh create mode 100644 src/eolab/georastertools/__init__.py rename src/eolab/{rastertools => georastertools}/cli/__init__.py (97%) rename src/eolab/{rastertools => georastertools}/cli/filtering.py (94%) rename src/eolab/{rastertools => georastertools}/cli/hillshade.py (94%) rename src/eolab/{rastertools => georastertools}/cli/radioindice.py (92%) rename src/eolab/{rastertools => georastertools}/cli/speed.py (92%) rename src/eolab/{rastertools => georastertools}/cli/svf.py (94%) rename src/eolab/{rastertools => georastertools}/cli/tiling.py (96%) rename src/eolab/{rastertools => georastertools}/cli/timeseries.py (93%) rename src/eolab/{rastertools => georastertools}/cli/utils_cli.py (98%) rename src/eolab/{rastertools => georastertools}/cli/zonalstats.py (97%) rename src/eolab/{rastertools => georastertools}/filtering.py (92%) rename src/eolab/{rastertools/rastertools.py => georastertools/georastertools.py} (92%) rename src/eolab/{rastertools => georastertools}/hillshade.py (96%) rename src/eolab/{rastertools => georastertools}/main.py (76%) rename src/eolab/{rastertools => georastertools}/processing/__init__.py (52%) rename src/eolab/{rastertools => georastertools}/processing/algo.py (100%) rename src/eolab/{rastertools => georastertools}/processing/rasterproc.py (97%) rename src/eolab/{rastertools => georastertools}/processing/sliding.py (99%) rename src/eolab/{rastertools => georastertools}/processing/stats.py (98%) rename src/eolab/{rastertools => georastertools}/processing/vector.py (99%) rename src/eolab/{rastertools => georastertools}/product/__init__.py (76%) rename src/eolab/{rastertools => georastertools}/product/rasterproduct.py (99%) rename src/eolab/{rastertools => georastertools}/product/rastertype.py (96%) rename src/eolab/{rastertools => georastertools}/product/rastertypes.json (99%) rename src/eolab/{rastertools => georastertools}/product/vrt.py (100%) rename src/eolab/{rastertools => georastertools}/radioindice.py (93%) rename src/eolab/{rastertools => georastertools}/speed.py (96%) rename src/eolab/{rastertools => georastertools}/svf.py (96%) rename src/eolab/{rastertools => georastertools}/tiling.py (96%) rename src/eolab/{rastertools => georastertools}/timeseries.py (97%) rename src/eolab/{rastertools => georastertools}/utils.py (100%) rename src/eolab/{rastertools => georastertools}/zonalstats.py (94%) delete mode 100644 src/eolab/rastertools/__init__.py delete mode 100644 src/eolab/rastertools/__pycache__/__init__.cpython-38.pyc delete mode 100644 src/eolab/rastertools/__pycache__/filtering.cpython-38.pyc delete mode 100644 src/eolab/rastertools/__pycache__/hillshade.cpython-38.pyc delete mode 100644 src/eolab/rastertools/__pycache__/main.cpython-38.pyc delete mode 100644 src/eolab/rastertools/__pycache__/radioindice.cpython-38.pyc delete mode 100644 src/eolab/rastertools/__pycache__/rastertools.cpython-38.pyc delete mode 100644 src/eolab/rastertools/__pycache__/speed.cpython-38.pyc delete mode 100644 src/eolab/rastertools/__pycache__/svf.cpython-38.pyc delete mode 100644 src/eolab/rastertools/__pycache__/tiling.cpython-38.pyc delete mode 100644 src/eolab/rastertools/__pycache__/timeseries.cpython-38.pyc delete mode 100644 src/eolab/rastertools/__pycache__/utils.cpython-38.pyc delete mode 100644 src/eolab/rastertools/__pycache__/zonalstats.cpython-38.pyc delete mode 100644 src/eolab/rastertools/cli/__pycache__/__init__.cpython-38.pyc delete mode 100644 src/eolab/rastertools/cli/__pycache__/filtering.cpython-38.pyc delete mode 100644 src/eolab/rastertools/cli/__pycache__/hillshade.cpython-38.pyc delete mode 100644 src/eolab/rastertools/cli/__pycache__/radioindice.cpython-38.pyc delete mode 100644 src/eolab/rastertools/cli/__pycache__/speed.cpython-38.pyc delete mode 100644 src/eolab/rastertools/cli/__pycache__/svf.cpython-38.pyc delete mode 100644 src/eolab/rastertools/cli/__pycache__/tiling.cpython-38.pyc delete mode 100644 src/eolab/rastertools/cli/__pycache__/timeseries.cpython-38.pyc delete mode 100644 src/eolab/rastertools/cli/__pycache__/zonalstats.cpython-38.pyc delete mode 100644 src/eolab/rastertools/processing/__pycache__/__init__.cpython-38.pyc delete mode 100644 src/eolab/rastertools/processing/__pycache__/algo.cpython-38.pyc delete mode 100644 src/eolab/rastertools/processing/__pycache__/rasterproc.cpython-38.pyc delete mode 100644 src/eolab/rastertools/processing/__pycache__/sliding.cpython-38.pyc delete mode 100644 src/eolab/rastertools/processing/__pycache__/stats.cpython-38.pyc delete mode 100644 src/eolab/rastertools/processing/__pycache__/vector.cpython-38.pyc delete mode 100644 src/eolab/rastertools/product/__pycache__/__init__.cpython-38.pyc delete mode 100644 src/eolab/rastertools/product/__pycache__/rasterproduct.cpython-38.pyc delete mode 100644 src/eolab/rastertools/product/__pycache__/rastertype.cpython-38.pyc delete mode 100644 src/eolab/rastertools/product/__pycache__/vrt.cpython-38.pyc delete mode 100644 src/rastertools.egg-info/PKG-INFO delete mode 100644 src/rastertools.egg-info/SOURCES.txt delete mode 100644 src/rastertools.egg-info/dependency_links.txt delete mode 100644 src/rastertools.egg-info/entry_points.txt delete mode 100644 src/rastertools.egg-info/not-zip-safe delete mode 100644 src/rastertools.egg-info/requires.txt delete mode 100644 src/rastertools.egg-info/top_level.txt diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..39e12cb --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,45 @@ +name: CD Workflow + +on: + push: + branches: + - "main" + +permissions: + contents: read + id-token: write + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: testpypi + url: https://test.pypi.org/p/georastertools + steps: + - uses: actions/checkout@v4 + + - name: Setup Python 3.8.13 + uses: actions/setup-python@v3 + with: + python-version: "3.8.13" + + - name: Setup Miniconda + uses: conda-incubator/setup-miniconda@v2 + with: + python-version: 3.8.13 + auto-activate-base: false + + - name: Build package + shell: bash -l {0} + run: | + conda create -n deploy_env python=3.8.13 libgdal=3.5.2 build -c conda-forge -y + conda activate deploy_env + python -m build -C--global-option=bdist_wheel -C--global-option=--build-number=0 --wheel + + - name: Publish package + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + verbose: true + password: ${{ secrets.TEST_PYPI_PASSWORD }} + repository-url: https://test.pypi.org/legacy/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fb96110 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,53 @@ +name: CI Workflow + +on: + pull_request: + branches: + - "main" +permissions: + contents: read + +jobs: + quality: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Python 3.8.13 + uses: actions/setup-python@v3 + with: + python-version: "3.8.13" + - name: Create test env + shell: bash -l {0} + run: | + pip install pylint mccabe + - name: code quality + shell: bash -l {0} + run: | + pylint --disable=all --fail-under=10 --enable=too-many-statements src/eolab/georastertools/ + pylint --disable=all --fail-under=10 --enable=too-many-nested-blocks src/eolab/georastertools/ + ./continuous_integration/scripts/check_mccabe_complexity.sh 25 src/eolab/georastertools + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup Python 3.8.13 + uses: actions/setup-python@v3 + with: + python-version: "3.8.13" + - name: Setup Miniconda + uses: conda-incubator/setup-miniconda@v2 + with: + python-version: 3.8.13 + auto-activate-base: false + - name: Create test env + shell: bash -l {0} + run: | + conda create -n test_env python=3.8.13 libgdal=3.5.2 -c conda-forge -c defaults -y + conda activate test_env + PIP_NO_BINARY=rasterio pip install . + pip install pylint mccabe + - name: test + shell: bash -l {0} + run: | + conda activate test_env + pytest --cov-fail-under=65 --compare \ No newline at end of file diff --git a/README.rst b/README.rst index 80609f8..b7cf860 100644 --- a/README.rst +++ b/README.rst @@ -2,15 +2,17 @@ Raster tools ============ -This project provides a command line named **rastertools** that enables various calculation tools: +This project provides a command line named **georastertools** that enables various calculation tools: - the calculation of radiometric indices on satellite images +- the calculation of the Compute the Sky View Factor (SVF) of a Digital Height Model (DHM). +- the calculation of the hillshades of a Digital Elevation / Surface / Height Model - the calculation of the speed of evolution of radiometry from images between two dates - the calculation of zonal statistics of the bands of a raster, that is to say statistics such as min, max, average, etc. on subareas (defined by a vector file) of the area of interest. -The **rastertools** project also aims to make the handling of the following image products transparent: +The **georastertools** project also aims to make the handling of the following image products transparent: - Sentinel-2 L1C PEPS (https://peps.cnes.fr/rocket/#/search) - Sentinel-2 L2A PEPS (https://peps.cnes.fr/rocket/#/search) @@ -21,7 +23,7 @@ The **rastertools** project also aims to make the handling of the following imag It is thus possible to input files in the command line in any of the formats above. It is also possible to specify your own product types by providing a JSON file as a parameter of the command line (cf. docs/usage.rst) -Finally, **rastertools** offers an API for calling these different tools in Python and for extending its capabilities, for example by defining new radiometric indices. +Finally, **georastertools** offers an API for calling these different tools in Python and for extending its capabilities, for example by defining new radiometric indices. Installation ============ @@ -30,70 +32,80 @@ Create a conda environment by typing the following: .. code-block:: bash - conda env create -f environment.yml - conda env update -f env_update.yml - -The following dependencies will be installed in the ``rastertools`` environment: - -- pyscaffold -- geopandas -- scipy -- gdal -- rasterio -- tqdm - -Install ``rastertools`` in the conda environment by typing the following: - -.. code-block:: bash - - conda activate rastertools - pip install -e . - -.. note:: - - Note: Installing in a *virtualenv* does not work properly for this project. For unexplained reasons, - the VRTs that are created in memory by rastertools to handle image products are not properly managed - with an installation in a virtualenv. - -For more details, including installation as a Docker or Singularity image, please refer to the documentation. : `Installation `_ + conda env create -n rastertools + conda activate + conda install python=3.8.13 libgdal + pip install georastertools --no-binary rasterio +For more details, including installation as a Docker or Singularity image, please refer to the documentation. : docs/install.rst Usage ===== -rastertools -^^^^^^^^^^^ -The rastertools command line is the high-level command for activating the various tools. +georastertools +^^^^^^^^^^^^^^ +The georastertools command line is the high-level command for activating the various tools. .. code-block:: console - $ rastertools --help - usage: rastertools [-h] [-t RASTERTYPE] [--version] [-v] [-vv] - {filter,fi,radioindice,ri,speed,sp,svf,hillshade,hs,zonalstats,zs,tiling,ti} - ... - - Collection of tools on raster data - - optional arguments: - -h, --help show this help message and exit - -t RASTERTYPE, --rastertype RASTERTYPE - JSON file defining additional raster types of input - files - --version show program's version number and exit - -v, --verbose set loglevel to INFO - -vv, --very-verbose set loglevel to DEBUG - - Commands: - {filter,fi,radioindice,ri,speed,sp,svf,hillshade,hs,zonalstats,zs,tiling,ti} - filter (fi) Apply a filter to a set of images - radioindice (ri) Compute radiometric indices - speed (sp) Compute speed of rasters - svf Compute Sky View Factor of a Digital Height Model - hillshade (hs) Compute hillshades of a Digital Height Model - zonalstats (zs) Compute zonal statistics - tiling (ti) Generate image tiles - -Calling rastertools returns the following exit codes: + $ rio georastertools --help + Usage: rio georastertools [OPTIONS] COMMAND [ARGS]... + + Main entry point for the `georastertools` Command Line Interface. + + The `georastertools` CLI provides tools for raster processing and analysis + and allows configurable data handling, parallel processing, and debugging + support. + + Logging: + + - INFO level (`-v`) gives detailed step information. + + - DEBUG level (`-vv`) offers full debug-level tracing. + + Environment Variables: + + - `RASTERTOOLS_NOTQDM`: If the log level is above INFO, sets this to + disable progress bars. + + - `RASTERTOOLS_MAXWORKERS`: If `max_workers` is set, it defines the max + workers for georastertools. + + Options: + -t, --rastertype PATH JSON file defining additional raster types of input + files + --max_workers INTEGER Maximum number of workers for parallel processing. If + not given, it will default to the number of + processors on the machine. When all processors are + not allocated to run georastertools, it is thus + recommended to set this option. + --debug Store to disk the intermediate VRT images that are + generated when handling the input files which can be + complex raster product composed of several band + files. + -v, --verbose set loglevel to INFO + -vv, --very-verbose set loglevel to DEBUG + --version Show the version and exit. + -h, --help Show this message and exit. + + Commands: + fi Apply a filter to a set of images. + filter Apply a filter to a set of images. + hillshade Execute the hillshade subcommand on a Digital Height Model... + hs Execute the hillshade subcommand on a Digital Height Model... + radioindice Compute the requested radio indices on raster data. + ri Compute the requested radio indices on raster data. + sp Compute the speed of radiometric values for multiple... + speed Compute the speed of radiometric values for multiple... + svf Compute the Sky View Factor (SVF) of a Digital Height... + ti Generate tiles of an input raster image following the... + tiling Generate tiles of an input raster image following the... + timeseries Generate a timeseries of images (without gaps) from a set... + ts Generate a timeseries of images (without gaps) from a set... + zonalstats Compute zonal statistics of a raster image. + zs Compute zonal statistics of a raster image. + +Calling georastertools returns the following exit codes: .. code-block:: console @@ -101,29 +113,13 @@ Calling rastertools returns the following exit codes: 1: processing error 2: incorrect invocation parameters -Details of the various subcommands are presented in the documentation : `Usage `_ - - -Tests & documentation -===================== - -To run tests and generate documentation, the following dependencies must be installed in the conda environment. : - -- py.test et pytest-cov (tests execution) -- sphinx (documentation generation) - -Pour cela, exécuter la commande suivante : - -.. code-block:: console - - conda env update -f env_test.yml - +Details of the various subcommands are presented in the documentation : docs/cli.rst Tests ^^^^^ The project comes with a suite of unit and functional tests. To run them, -launch the command ``pytest tests``. To run specific tests, execute ``pytest tests -k ""``. +launch the command ``pytest tests``. To run specific tests, execute ``pytest tests -k ""``. The tests may perform comparisons between generated files and reference files. In this case, the tests depend on the numerical precision of the platforms. diff --git a/continuous_integration/scripts/check_mccabe_complexity.sh b/continuous_integration/scripts/check_mccabe_complexity.sh new file mode 100755 index 0000000..c7e6766 --- /dev/null +++ b/continuous_integration/scripts/check_mccabe_complexity.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +if [ -z "$1" ] || [ -z "$2" ]; then + echo "Error: You must specify a McCabe threshold and a directory to analyze." + echo "Usage: $0 " + exit 1 +fi + +threshold=$1 +directory=$2 + +if [ ! -d "$directory" ]; then + echo "Error: The directory '$directory' does not exist." + exit 1 +fi + +all_files_ok=true +for file in $(find "$directory" -name "*.py"); do + echo "Analyzing $file ..." + output=$(python -m mccabe --min "$threshold" "$file") + + if [ -n "$output" ]; then + echo "Error: McCabe complexity too high in $file" + echo "$output" + all_files_ok=false + fi +done + +if $all_files_ok; then + echo "✅ All files have McCabe scores less than or equal to $threshold. ✅" +else + echo "❌ Some files have a complexity higher than $threshold ❌" + exit 1 +fi + +exit 0 diff --git a/docs/cli.rst b/docs/cli.rst index 8f85148..202df69 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -4,15 +4,15 @@ Command Line Interface ====================== -rastertools +georastertools ----------- -The CLI **rastertools** enable to activate several subcommands, one per raster tool: +The CLI **georastertools** enable to activate several subcommands, one per raster tool: .. code-block:: console - $ rastertools --help - usage: rastertools [-h] [-t RASTERTYPE] [--version] [--max_workers MAX_WORKERS] [--debug] [-v] [-vv] + $ georastertools --help + usage: georastertools [-h] [-t RASTERTYPE] [--version] [--max_workers MAX_WORKERS] [--debug] [-v] [-vv] {filter,fi,hillshade,hs,radioindice,ri,speed,sp,svf,tiling,ti,timeseries,ts,zonalstats,zs} ... Collection of tools on raster data @@ -25,7 +25,7 @@ The CLI **rastertools** enable to activate several subcommands, one per raster t --max_workers MAX_WORKERS Maximum number of workers for parallel processing. If not given, it will default to the number of processors on the machine. When all processors are not allocated - to run rastertools, it is thus recommended to set this option. + to run georastertools, it is thus recommended to set this option. --debug Store to disk the intermediate VRT images that are generated when handling the input files which can be complex raster product composed of several band files. -v, --verbose set loglevel to INFO @@ -43,13 +43,13 @@ The CLI **rastertools** enable to activate several subcommands, one per raster t timeseries (ts) Temporal gap filling of an image time series zonalstats (zs) Compute zonal statistics -The **rastertools** CLI generates the following sys.exit values: +The **georastertools** CLI generates the following sys.exit values: - 0: everything runs fine - 1: processing error - 2: wrong configuration of the raster tool (e.g. invalid parameter value given in the command line) -**rastertools** is thus the entry point for all the following tools: +**georastertools** is thus the entry point for all the following tools: .. toctree:: :maxdepth: 1 @@ -57,8 +57,8 @@ The **rastertools** CLI generates the following sys.exit values: cli/* -**rastertools** enables to configure additional custom rastertypes (cf. Usage) and to set-up logging -level. Most of the rastertools display their progression using a progress bar. The progress bar is +**georastertools** enables to configure additional custom rastertypes (cf. Usage) and to set-up logging +level. Most of the georastertools display their progression using a progress bar. The progress bar is displayed when the logging level is set to INFO or DEBUG. It can also be disabled / enabled independently to the logging level by setting an environment variable named RASTERTOOLS_NOTQDM. For instance, setting the environment variable to 1 (resp. 0) will disable (resp. enable) the display @@ -67,14 +67,14 @@ of the progress bar: .. code-block:: console $ export RASTERTOOLS_NOTQDM=1 - $ rastertools -v ri [...] + $ georastertools -v ri [...] -Most of the **rastertools** are designed to split down rasters into small chunks of data so that +Most of the **georastertools** are designed to split down rasters into small chunks of data so that the processing can be run in parallel using several processors. The command line of the tools defines an argument called `window_size` which corresponds to the size of the chunks. It is also possible to specify the number of workers (i.e. processors) to use for parallel processing. The number of workers shall not exceed the number of processors of the machine. It is necessary to -specify this option (`--max_workers`) when running rastertools on a machine where all cpus are +specify this option (`--max_workers`) when running georastertools on a machine where all cpus are not allocated to the job, e.g. if you submit the processing to a job scheduler such as PBSPro. Alternatively, you can set an environment variable like this and keep the argument `--max_workers` unset: @@ -82,16 +82,16 @@ Alternatively, you can set an environment variable like this and keep the argume .. code-block:: console $ export RASTERTOOLS_MAXWORKERS=12 - $ rastertools -v hillshade [...] # it will use 12 processors + $ georastertools -v hillshade [...] # it will use 12 processors Docker/Singularity ------------------ -By default the Docker / Singularity container executes: ``rastertools --help``. To run another command, type a command like: +By default the Docker / Singularity container executes: ``georastertools --help``. To run another command, type a command like: .. code-block:: console - $ docker run -it -u 1000 -v $PWD/tests/tests_data:/usr/data rastertools:latest rastertools ri -r /usr/data/COMMUNE_32001.shp --ndvi /usr/data/SENTINEL2A_20180521-105702-711_L2A_T30TYP_D.zip + $ docker run -it -u 1000 -v $PWD/tests/tests_data:/usr/data georastertools:latest georastertools ri -r /usr/data/COMMUNE_32001.shp --ndvi /usr/data/SENTINEL2A_20180521-105702-711_L2A_T30TYP_D.zip - $ singularity run rastertools_latest.sif rastertools ri -r tests/tests_data/COMMUNE_32001.shp --ndvi tests/tests_data/SENTINEL2A_20180521-105702-711_L2A_T30TYP_D.zip + $ singularity run rastertools_latest.sif georastertools ri -r tests/tests_data/COMMUNE_32001.shp --ndvi tests/tests_data/SENTINEL2A_20180521-105702-711_L2A_T30TYP_D.zip diff --git a/docs/cli/filtering.rst b/docs/cli/filtering.rst index 4535474..76a2616 100644 --- a/docs/cli/filtering.rst +++ b/docs/cli/filtering.rst @@ -5,8 +5,8 @@ filter .. code-block:: console - $ rastertools filter --help - usage: rastertools filter [-h] {median,sum,mean,adaptive_gaussian} ... + $ georastertools filter --help + usage: georastertools filter [-h] {median,sum,mean,adaptive_gaussian} ... Apply a filter to a set of images. @@ -25,8 +25,8 @@ that configure the filter. Type option --help to get the definition of the argum .. code-block:: console - $ rastertools filter adaptive_gaussian --help - usage: rastertools filter adaptive_gaussian [-h] --kernel_size KERNEL_SIZE + $ georastertools filter adaptive_gaussian --help + usage: georastertools filter adaptive_gaussian [-h] --kernel_size KERNEL_SIZE --sigma SIGMA [-o OUTPUT] [-ws WINDOW_SIZE] [-p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}] @@ -105,9 +105,9 @@ To apply three filters (median, mean and adaptive_gaussian) on a kernel of dimen .. code-block:: console - $ rastertools filter median --kernel_size 16 "./SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" - $ rastertools filter mean --kernel_size 16 "./SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" - $ rastertools filter adaptive_gaussian --kernel_size 16 --sigma 1 "./SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" + $ georastertools filter median --kernel_size 16 "./SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" + $ georastertools filter mean --kernel_size 16 "./SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" + $ georastertools filter adaptive_gaussian --kernel_size 16 --sigma 1 "./SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif" The commands will generate respectively: diff --git a/docs/cli/hillshade.rst b/docs/cli/hillshade.rst index 3a61822..d906fa7 100644 --- a/docs/cli/hillshade.rst +++ b/docs/cli/hillshade.rst @@ -9,8 +9,8 @@ computes the shadows of the ground surface (buildings, trees, etc.). .. code-block:: console - $ rastertools hillshade --help - usage: rastertools hillshade [-h] --elevation ELEVATION --azimuth AZIMUTH + $ georastertools hillshade --help + usage: georastertools hillshade [-h] --elevation ELEVATION --azimuth AZIMUTH [--radius RADIUS] --resolution RESOLUTION [-o OUTPUT] [-ws WINDOW_SIZE] [-p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}] @@ -66,9 +66,9 @@ hours (8:00AM, noon, 6:00PM) of 21st June: .. code-block:: console - $ rastertools hillshade --elevation 27.2 --azimuth 82.64 --resolution 0.5 toulouse-mnh.tif - $ rastertools hillshade --elevation 69.83 --azimuth 180 --resolution 0.5 toulouse-mnh.tif - $ rastertools hillshade --elevation 25.82 --azimuth 278.58 --resolution 0.5 toulouse-mnh.tif + $ georastertools hillshade --elevation 27.2 --azimuth 82.64 --resolution 0.5 toulouse-mnh.tif + $ georastertools hillshade --elevation 69.83 --azimuth 180 --resolution 0.5 toulouse-mnh.tif + $ georastertools hillshade --elevation 25.82 --azimuth 278.58 --resolution 0.5 toulouse-mnh.tif The generated images are rendered with QGis: - first layer contains the hillshade (value 0 is masked) diff --git a/docs/cli/radioindice.rst b/docs/cli/radioindice.rst index 56daaad..3390f31 100644 --- a/docs/cli/radioindice.rst +++ b/docs/cli/radioindice.rst @@ -7,8 +7,8 @@ radioindice .. code-block:: console - $ rastertools radioindice --help - usage: rastertools radioindice [-h] [-o OUTPUT] [-m] [-r ROI] + $ georastertools radioindice --help + usage: georastertools radioindice [-h] [-o OUTPUT] [-m] [-r ROI] [-i INDICES [INDICES ...]] [--ndvi] [--tndvi] [--rvi] [--pvi] [--savi] [--tsavi] [--msavi] [--msavi2] [--ipvi] [--evi] [--ndwi] [--ndwi2] @@ -70,7 +70,7 @@ radioindice bands defineda s parameter of this option, e.g. "-nd red nir" will compute (red- nir)/(red+nir). See - eolab.rastertools.product.rastertype. + eolab.georastertools.product.rastertype. BandChannel for the list of bands names. Several nd options can be set to compute several normalized differences. @@ -80,7 +80,7 @@ radioindice .. warning:: ``radioindice`` only accepts input files that match one of the configured raster types, either a built-in raster type - or a custom raster type defined with option -t of ``rastertools``. See section "Raster types". + or a custom raster type defined with option -t of ``georastertools``. See section "Raster types". Examples : @@ -94,7 +94,7 @@ a black line. .. code-block:: console - $ rastertools radioindice -r "./COMMUNE_32001.shp" --ndvi ./SENTINEL2A_20180521-105702-711_L2A_T30TYP_D.zip + $ georastertools radioindice -r "./COMMUNE_32001.shp" --ndvi ./SENTINEL2A_20180521-105702-711_L2A_T30TYP_D.zip The generated NDVI image is: @@ -104,7 +104,7 @@ The second command computes two indices (NDVI and NDWI) of the same input image. .. code-block:: console - $ rastertools radioindice -i ndvi ndwi -m ./SENTINEL2A_20180521-105702-711_L2A_T30TYP_D.zip + $ georastertools radioindice -i ndvi ndwi -m ./SENTINEL2A_20180521-105702-711_L2A_T30TYP_D.zip The generated image has two bands (because option -m is activated): first one is the ndvi, second one is the ndwi. If -m option is not activated, two images would be generated, one image per indice. diff --git a/docs/cli/speed.rst b/docs/cli/speed.rst index 964b68d..45049e4 100644 --- a/docs/cli/speed.rst +++ b/docs/cli/speed.rst @@ -7,8 +7,8 @@ speed .. code-block:: console - $ rastertools speed --help - usage: rastertools speed [-h] [-b BANDS [BANDS ...]] [-a] [-o OUTPUT] + $ georastertools speed --help + usage: georastertools speed [-h] [-b BANDS [BANDS ...]] [-a] [-o OUTPUT] inputs [inputs ...] Compute the speed of radiometric values for multiple raster images. @@ -35,14 +35,14 @@ speed .. warning:: At least two input rasters must be given. The rasters must match one of the configured raster types, - either a built-in raster type or a custom raster type defined with option -t of ``rastertools``. + either a built-in raster type or a custom raster type defined with option -t of ``georastertools``. See section "Raster types". Example: .. code-block:: console - $ rastertools speed ./SENTINEL2A_20180521-105702-711_L2A_T30TYP_D-ndvi.zip ./SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif + $ georastertools speed ./SENTINEL2A_20180521-105702-711_L2A_T30TYP_D-ndvi.zip ./SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif This command generates an image with one band that represents :math:`\\frac{ndvi_2 - ndvi_1}{date_2 - data_1}` where: diff --git a/docs/cli/svf.rst b/docs/cli/svf.rst index 6c7ef9f..64084de 100644 --- a/docs/cli/svf.rst +++ b/docs/cli/svf.rst @@ -40,8 +40,8 @@ too many points, the "radius" parameter defines the max distance of the pixel to .. code-block:: console - $ rastertools svf --help - usage: rastertools svf [-h] --radius RADIUS --directions DIRECTIONS + $ georastertools svf --help + usage: georastertools svf [-h] --radius RADIUS --directions DIRECTIONS --resolution RESOLUTION [--altitude ALTITUDE] [-o OUTPUT] [-ws WINDOW_SIZE] [-p {none,edge,maximum,mean,median,minimum,reflect,symmetric,wrap}] @@ -102,7 +102,7 @@ of buildings (for the highest building, SVF is thus 1). .. code-block:: console - $ rastertools svf --radius 50 --directions 16 --resolution 0.5 tests\tests_data\toulouse-mnh.tif + $ georastertools svf --radius 50 --directions 16 --resolution 0.5 tests\tests_data\toulouse-mnh.tif This command generates the following SVF: @@ -112,7 +112,7 @@ It is also possible to compute the SVF at a specified height, for instance on gr .. code-block:: console - $ rastertools svf --radius 50 --directions 16 --resolution 0.5 tests\tests_data\toulouse-mnh.tif + $ georastertools svf --radius 50 --directions 16 --resolution 0.5 tests\tests_data\toulouse-mnh.tif The SVF is the following: diff --git a/docs/cli/tiling.rst b/docs/cli/tiling.rst index e0021c9..635fa0f 100644 --- a/docs/cli/tiling.rst +++ b/docs/cli/tiling.rst @@ -8,8 +8,8 @@ file. .. code-block:: console - $ rastertools tiling --help - usage: rastertools tiling [-h] -g GRID_FILE [--id_col ID_COLUMN] + $ georastertools tiling --help + usage: georastertools tiling [-h] -g GRID_FILE [--id_col ID_COLUMN] [--id ID [ID ...]] [-o OUTPUT] [-n OUTPUT_NAME] [-d SUBDIR_NAME] inputs [inputs ...] @@ -60,14 +60,14 @@ The grid and the image only overlap on the cells 1 and 2. * example 1:: - rastertools -v ti -g grid.geojson image.tif + georastertools -v ti -g grid.geojson image.tif This command will return 2 files in the current directory named *image_tile1.tif* et *image_tile2.tif* corresponding to the cells 1 and 2. The command will return an error for cells 3 and 4 because they do not overlap the raster image. * exemple 2:: - rastertools -v ti -g grid.geojson image.tif --id 2 4 --name output_{1}.tif + georastertools -v ti -g grid.geojson image.tif --id 2 4 --name output_{1}.tif This command will return 1 file in the current directory named *output_2.tif* and will return an error for cell 4 because this cell does not overlap the raster image. diff --git a/docs/cli/timeseries.rst b/docs/cli/timeseries.rst index 510ead6..75b2510 100644 --- a/docs/cli/timeseries.rst +++ b/docs/cli/timeseries.rst @@ -15,9 +15,9 @@ and may thus contain the same gaps as the input raster. .. code-block:: console - $ rastertools timeseries --help + $ georastertools timeseries --help - usage: rastertools timeseries [-h] [-b BANDS [BANDS ...]] [-a] [-o OUTPUT] + usage: georastertools timeseries [-h] [-b BANDS [BANDS ...]] [-a] [-o OUTPUT] [-s START_DATE] [-e END_DATE] [-p TIME_PERIOD] [-ws WINDOW_SIZE] inputs [inputs ...] @@ -57,7 +57,7 @@ and may thus contain the same gaps as the input raster. .. warning:: At least two input rasters must be given. The rasters must match one of the configured raster types, - either a built-in raster type or a custom raster type defined with option -t of ``rastertools``. + either a built-in raster type or a custom raster type defined with option -t of ``georastertools``. See section "Raster types". The raster type must define where to get the date of the product in the filename. @@ -65,7 +65,7 @@ Example: .. code-block:: console - $ rastertools timeseries ./SENTINEL2A_20180521-105702-711_L2A_T30TYP_D-ndvi.zip ./SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif -s 2018-05-21 -e 2018-10-25 -p 10 -ws 512 + $ georastertools timeseries ./SENTINEL2A_20180521-105702-711_L2A_T30TYP_D-ndvi.zip ./SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif -s 2018-05-21 -e 2018-10-25 -p 10 -ws 512 This command generates rasters from 2018-05-21 until 2018-10-25 with a timeperiod between two consecutive images of 10 days (2018-05-21, 2018-05-31, 2018-06-10, ...). diff --git a/docs/cli/zonalstats.rst b/docs/cli/zonalstats.rst index a0812db..8861711 100644 --- a/docs/cli/zonalstats.rst +++ b/docs/cli/zonalstats.rst @@ -17,8 +17,8 @@ cities using: .. code-block:: console - $ rastertools zonalstats --help - usage: rastertools zonalstats [-h] [-o OUTPUT] [-f OUTPUT_FORMAT] + $ georastertools zonalstats --help + usage: georastertools zonalstats [-h] [-o OUTPUT] [-f OUTPUT_FORMAT] [-g GEOMETRIES] [-w] [--stats STATS [STATS ...]] [--categorical] [--valid_threshold VALID_THRESHOLD] [--area] @@ -120,7 +120,7 @@ The first command generates statistics of ndvi values for several cities .. code-block:: console - $ rastertools zs -f GeoJSON -g COMMUNE_32.geojson --stats min max mean std SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif + $ georastertools zs -f GeoJSON -g COMMUNE_32.geojson --stats min max mean std SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif This generates a new vector file that contains for each entity the stats values. @@ -131,7 +131,7 @@ To disable the computation for these cities, use option --within. .. code-block:: console - $ rastertools zs -f GeoJSON -g COMMUNE_32.geojson --within --stats min max mean std SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif + $ georastertools zs -f GeoJSON -g COMMUNE_32.geojson --within --stats min max mean std SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif The new vector file is now: @@ -144,7 +144,7 @@ The following command line enables to count the pixels of every classes: .. code-block:: console - $ rastertools zs -f GeoJSON --categorical OCS_2017_CESBIO.tif + $ georastertools zs -f GeoJSON --categorical OCS_2017_CESBIO.tif The generated vector file contains one geometry (a green square that corresponds to the shape of the input raster) with the number of pixels for each category: diff --git a/docs/conf.py b/docs/conf.py index 6b9acf7..6423178 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -95,7 +95,7 @@ master_doc = "index" # General information about the project. -project = u'rastertools' +project = u'georastertools' copyright = u'2021, CNES' # The version info for the project you're documenting, acts as replacement for @@ -167,7 +167,7 @@ # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". try: - from eolab.rastertools import __version__ as version + from eolab.georastertools import __version__ as version except ImportError: pass else: @@ -232,7 +232,7 @@ # html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'rastertools-doc' +htmlhelp_basename = 'georastertools-doc' # -- Options for LaTeX output -------------------------------------------------- @@ -249,7 +249,7 @@ # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'user_guide.tex', u'rastertools Documentation', + ('index', 'user_guide.tex', u'georastertools Documentation', u'Olivier Queyrut', 'manual'), ] diff --git a/docs/index.rst b/docs/index.rst index 8dbfc0d..849d565 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,8 +1,8 @@ ============ -Rastertools +georastertools ============ -This project consists in a command line named **rastertools** that enables to run several processing on raster files, typically +This project consists in a command line named **georastertools** that enables to run several processing on raster files, typically Sentinel2 raster images: - compute radiometric indices (e.g. ndvi). @@ -15,7 +15,7 @@ Sentinel2 raster images: - apply a filter (median, local sum, local mean, adaptive gaussian) - compute the Sky View Factor and the Hillshades of input rasters that represent a Digital Surface/Elevation/Height Model. -The aim of **rastertools** is also to make the use of the following raster products transparent: +The aim of **georastertools** is also to make the use of the following raster products transparent: - Sentinel-2 L1C PEPS (available here: https://peps.cnes.fr/rocket/#/search) - Sentinel-2 L2A PEPS (available here: https://peps.cnes.fr/rocket/#/search) @@ -23,14 +23,14 @@ The aim of **rastertools** is also to make the use of the following raster produ - Sentinel-2 L3A THEIA (available here: https://theia.cnes.fr/atdistrib/rocket/#/search?collection=SENTINEL2) - SPOT 6/7 Ortho GEOSUD (available here: http://ids.equipex-geosud.fr/web/guest/catalog) -**rastertools** accept any of this raster product as input files. No need to unpack the archive to get +**georastertools** accept any of this raster product as input files. No need to unpack the archive to get the raster files containing the different bands, to merge the bands, extract the region of interest and -so on: **rastertools** does it. +so on: **georastertools** does it. -**rastertools** also accepts additional custom raster types. The new raster types shall be defined in a -JSON file and provided as an argument of the rastertools CLI (cf. Usage) +**georastertools** also accepts additional custom raster types. The new raster types shall be defined in a +JSON file and provided as an argument of the georastertools CLI (cf. Usage) -**rastertools** has a public API that enables to activate all the tools and extend their capabilities (e.g. +**georastertools** has a public API that enables to activate all the tools and extend their capabilities (e.g. add a new radiometric indice). diff --git a/docs/install.rst b/docs/install.rst index 7fa17df..fbd946f 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -8,7 +8,7 @@ First, create a conda environment via: $ conda env create -f environment.yml $ conda env update -f env_update.yml -The following libraries will be installed in the conda environment named ``rastertools`` : +The following libraries will be installed in the conda environment named ``georastertools`` : - pyscaffold - geopandas @@ -22,24 +22,24 @@ Secondly, run commands: .. code-block:: console - $ conda activate rastertools + $ conda activate georastertools $ pip install -e . -``rastertools`` will be installed in the conda environment. Then, the CLI ``rastertools`` can be used and the API :obj:`eolab.rastertools` +``georastertools`` will be installed in the conda environment. Then, the CLI ``georastertools`` can be used and the API :obj:`eolab.georastertools` can be called in a python script. Docker et Singularity --------------------- -``rastertools`` can be used in a Docker container that is built from the Dockerfile available in the root directory of the project. +``georastertools`` can be used in a Docker container that is built from the Dockerfile available in the root directory of the project. To create the docker image, run: .. code-block:: console - $ docker build -t rastertools:latest . + $ docker build -t georastertools:latest . To create the singularity image from the docker image, run: .. code-block:: console - $ singularity build rastertools_latest.sif docker://rastertools:latest + $ singularity build rastertools_latest.sif docker://georastertools:latest diff --git a/docs/private_api.rst b/docs/private_api.rst index ac02ea6..6de6078 100644 --- a/docs/private_api.rst +++ b/docs/private_api.rst @@ -1,10 +1,10 @@ -.. currentmodule:: eolab.rastertools +.. currentmodule:: eolab.georastertools =========== Private API =========== -This section describes the private API of rastertools. +This section describes the private API of georastertools. .. autosummary:: :toctree: api/ diff --git a/docs/rasterproduct.rst b/docs/rasterproduct.rst index a8e44b9..5962f9e 100644 --- a/docs/rasterproduct.rst +++ b/docs/rasterproduct.rst @@ -4,7 +4,7 @@ Raster types ============ -Some of the **rastertools** can only handle input raster files of a recognized raster type. +Some of the **georastertools** can only handle input raster files of a recognized raster type. This concerns ``radioindice``, ``speed`` and also ``zonalstats`` when this latter is asked to plot the statistics. In these 3 cases, the tool needs to know the available raster bands (``radioindice``) or to extract the @@ -23,7 +23,7 @@ can be read by rasterio. Built-in raster types --------------------- -**rastertools** has several built-in raster types: +**georastertools** has several built-in raster types: .. list-table:: Built-in raster types :widths: 20 20 15 15 15 15 @@ -39,7 +39,7 @@ Built-in raster types - ^SENTINEL2._(?P[0-9\-]{15}).*_L2A_T(?P.*)_.*$ - ^SENTINEL2.*_(?P{})\.(tif|TIF|vrt|VRT)$ - %Y%m%d-%H%M%S - - eolab.rastertools.product.vrt.s2_maja_mask + - eolab.georastertools.product.vrt.s2_maja_mask - -10000 * - Sentinel-2 L3A THEIA - ^SENTINEL2X_(?P[0-9\-]{15}).*_L3A_T(?P.*)_.*$ @@ -69,7 +69,7 @@ Built-in raster types Add custom raster types ----------------------- -**rastertools** CLI has a special argument ``-t`` that allows to define custom raster types. This argument must +**georastertools** CLI has a special argument ``-t`` that allows to define custom raster types. This argument must be set with the path to a JSON file that contains the new raster types definitions. -The structure of the JSON file is described in :obj:`eolab.rastertools.add_custom_rastertypes` +The structure of the JSON file is described in :obj:`eolab.georastertools.add_custom_rastertypes` diff --git a/docs/usage.rst b/docs/usage.rst index c19f7bd..ce03b27 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -1,21 +1,21 @@ .. _usage: -.. currentmodule:: eolab.rastertools +.. currentmodule:: eolab.georastertools ============= API Reference ============= -rastertools +georastertools ----------- -Rastertools can be activated by calling the API :obj:`eolab.rastertools.run_tool`. +georastertools can be activated by calling the API :obj:`eolab.georastertools.run_tool`. .. autofunction:: run_tool Example of use in python code:: - from eolab.rastertools import run_tool + from eolab.georastertools import run_tool run_tool(args="radioindice --help".split()) Alternatively, every raster tools can be activated with their own API: @@ -36,12 +36,12 @@ All these objects provide: - a fluent API to configure the processing. The methods are all named with the following pattern "with_" and return the current instance so that the methods can be chained. -- methods to process a single file or a set of files (see :obj:`eolab.rastertools.Rastertool.process_file` - and :obj:`eolab.rastertools.Rastertool.process_files`) +- methods to process a single file or a set of files (see :obj:`eolab.georastertools.Rastertool.process_file` + and :obj:`eolab.georastertools.Rastertool.process_files`) Example of use:: - from eolab.rastertools import Radioindice + from eolab.georastertools import Radioindice proc = Radioindice(Radioindice.ndvi) outputs = proc.with_output(".", merge=False) .with_roi("./roi.geojson") @@ -51,12 +51,12 @@ Example of use:: Raster products --------------- -Rastertools provides a useful API to open raster products that are provided as an archive or a directory containing +georastertools provides a useful API to open raster products that are provided as an archive or a directory containing several images. The API is very simple to use:: - from eolab.rastertools.product import RasterProduct + from eolab.georastertools.product import RasterProduct import rasterio with RasterProduct("tests/tests_data/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip") as rp: @@ -67,13 +67,13 @@ The API is very simple to use:: Adding custom raster types -------------------------- -To add custom raster types, use the method :obj:`eolab.rastertools.add_custom_rastertypes`. +To add custom raster types, use the method :obj:`eolab.georastertools.add_custom_rastertypes`. .. autofunction:: add_custom_rastertypes Example of use:: - from eolab.rastertools import add_custom_rastertypes + from eolab.georastertools import add_custom_rastertypes my_rastertypes = { "rastertypes": [ @@ -135,14 +135,14 @@ Example of use:: add_custom_rastertypes(json) - # now any rastertools can handle products of the two new rastertypes + # now any georastertools can handle products of the two new rastertypes ... Design rules of raster tools ---------------------------- -Every raster tool object inherits from base class :obj:`eolab.rastertools.Rastertool`. -If the process supports windowing, the raster tool can also inherit from :obj:`eolab.rastertools.Windowable`. +Every raster tool object inherits from base class :obj:`eolab.georastertools.Rastertool`. +If the process supports windowing, the raster tool can also inherit from :obj:`eolab.georastertools.Windowable`. .. autosummary:: :toctree: api/ @@ -150,7 +150,7 @@ If the process supports windowing, the raster tool can also inherit from :obj:`e Rastertool Windowable -A rastertool raises a :obj:`eolab.rastertools.RastertoolConfigurationException` when invalid +A rastertool raises a :obj:`eolab.georastertools.RastertoolConfigurationException` when invalid input parameter is provided. .. autosummary:: @@ -161,7 +161,7 @@ input parameter is provided. Moreover, a rastertool can raise any type of Exception during its execution. The best practice is thus to catch exceptions that can raise as follows:: - from eolab.rastertools import Radioindice, RastertoolConfigurationException + from eolab.georastertools import Radioindice, RastertoolConfigurationException proc = Radioindice(Radioindice.ndvi) try: diff --git a/setup.cfg b/setup.cfg index 0e85206..2ba96ab 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,14 +3,14 @@ # http://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files [metadata] -name = rastertools +name = georastertools description = Compute radiometric indices and zonal statistics on rasters author = Olivier Queyrut author_email = olivier.queyrut@cnes.fr license = apache v2 long_description = file: README.rst long_description_content_type = text/x-rst; charset=UTF-8 -url = https://github.com/cnes/rastertools +url = https://github.com/cnes/georastertools project_urls = Source = https://github.com/cnes/rastertools Changelog = https://github.com/cnes/rastertools/blob/master/CHANGELOG.rst @@ -50,7 +50,7 @@ exclude = [options.extras_require] # Add here additional requirements for extra features, to install with: -# `pip install rastertools[PDF]` like: +# `pip install georastertools[PDF]` like: # PDF = ReportLab; RXP # Add here test requirements (semicolon/line-separated) @@ -61,7 +61,7 @@ testing = [options.entry_points] console_scripts = - rastertools = eolab.rastertools.main:run + georastertools = eolab.georastertools.main:run [tool:pytest] # Specify command line options as you would do when invoking pytest directly. @@ -70,7 +70,7 @@ console_scripts = # CAUTION: --cov flags may prohibit setting breakpoints while debugging. # Comment those flags to avoid this py.test issue. addopts = - --cov eolab.rastertools --cov-report term-missing + --cov eolab.georastertools --cov-report term-missing --verbose norecursedirs = dist @@ -106,7 +106,7 @@ exclude = # PyScaffold's parameters when the project was created. # This will be used when updating. Do not change! version = 4.0.2 -package = rastertools +package = georastertools extensions = namespace no_skeleton diff --git a/setup.py b/setup.py index 920fbd4..dbcb357 100644 --- a/setup.py +++ b/setup.py @@ -3,10 +3,14 @@ if __name__ == "__main__": try: - setup(name='rastertools', + with open("README.rst", "r", encoding="utf-8") as fh: + long_description = fh.read() + + setup(name='georastertools', version="0.1.0", - description=u"Collection of tools for raster data", - long_description="", + description="Collection of tools for raster data", + long_description=long_description, + long_description_content_type="text/x-rst", classifiers=[], keywords='', author=u"Olivier Queyrut", @@ -36,7 +40,7 @@ ], entry_points=""" [rasterio.rio_plugins] - rastertools=eolab.rastertools.main:rastertools + georastertools=eolab.georastertools.main:georastertools """, python_requires='==3.8.13', use_scm_version={"version_scheme": "no-guess-dev"}) diff --git a/src/eolab/georastertools/__init__.py b/src/eolab/georastertools/__init__.py new file mode 100644 index 0000000..030c0f5 --- /dev/null +++ b/src/eolab/georastertools/__init__.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +"""This module contains the eolab's georastertools CLI and API +""" +from importlib.metadata import version + +# Change here if project is renamed and does not equal the package name +dist_name = "georastertools" +__version__ = version(dist_name) + +from eolab.georastertools.georastertools import RastertoolConfigurationException +from eolab.georastertools.georastertools import Rastertool, Windowable +# import rastertool Filtering +from eolab.georastertools.filtering import Filtering +# import rastertool Hillshade +from eolab.georastertools.hillshade import Hillshade +# import rastertool Radioindice +from eolab.georastertools.radioindice import Radioindice +# import rastertool Speed +from eolab.georastertools.speed import Speed +# import rastertool SVF +from eolab.georastertools.svf import SVF +# import rastertool Tiling +from eolab.georastertools.tiling import Tiling +# import rastertool Timeseries +from eolab.georastertools.timeseries import Timeseries +# import rastertool Zonalstats +from eolab.georastertools.zonalstats import Zonalstats +# import the method to run a rastertool +from eolab.georastertools.main import georastertools, add_custom_rastertypes + +__all__ = [ + "RastertoolConfigurationException", "Rastertool", "Windowable", + "Filtering", "Hillshade", "Radioindice", "Speed", "SVF", "Tiling", + "Timeseries", "Zonalstats", + "run_tool", "add_custom_rastertypes" +] diff --git a/src/eolab/rastertools/cli/__init__.py b/src/eolab/georastertools/cli/__init__.py similarity index 97% rename from src/eolab/rastertools/cli/__init__.py rename to src/eolab/georastertools/cli/__init__.py index 50cf82d..b86bdb8 100644 --- a/src/eolab/rastertools/cli/__init__.py +++ b/src/eolab/georastertools/cli/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Module that defines the subcommands of rastertools. +"""Module that defines the subcommands of georastertools. This module's API is not intended for external use. """ diff --git a/src/eolab/rastertools/cli/filtering.py b/src/eolab/georastertools/cli/filtering.py similarity index 94% rename from src/eolab/rastertools/cli/filtering.py rename to src/eolab/georastertools/cli/filtering.py index 8c01b61..8187587 100644 --- a/src/eolab/rastertools/cli/filtering.py +++ b/src/eolab/georastertools/cli/filtering.py @@ -3,8 +3,8 @@ """ CLI definition for the filtering tool """ -from eolab.rastertools import Filtering -from eolab.rastertools.cli.utils_cli import apply_process, pad_opt, win_opt, all_opt, band_opt +from eolab.georastertools import Filtering +from eolab.georastertools.cli.utils_cli import apply_process, pad_opt, win_opt, all_opt, band_opt import click import os @@ -36,7 +36,7 @@ def create_filtering(output : str, window_size : int, pad : str, argsdict : dict all_bands (bool): Whether to apply the filter to all bands (True) or specific bands (False). Returns: - :obj:`eolab.rastertools.Filtering`: A configured `Filtering` instance ready for execution. + :obj:`eolab.georastertools.Filtering`: A configured `Filtering` instance ready for execution. """ # get the bands to process if all_bands: diff --git a/src/eolab/rastertools/cli/hillshade.py b/src/eolab/georastertools/cli/hillshade.py similarity index 94% rename from src/eolab/rastertools/cli/hillshade.py rename to src/eolab/georastertools/cli/hillshade.py index f4786bc..a55a5a6 100644 --- a/src/eolab/rastertools/cli/hillshade.py +++ b/src/eolab/georastertools/cli/hillshade.py @@ -3,8 +3,8 @@ """ CLI definition for the hillshade tool """ -from eolab.rastertools import Hillshade -from eolab.rastertools.cli.utils_cli import apply_process, pad_opt, win_opt +from eolab.georastertools import Hillshade +from eolab.georastertools.cli.utils_cli import apply_process, pad_opt, win_opt import click import os diff --git a/src/eolab/rastertools/cli/radioindice.py b/src/eolab/georastertools/cli/radioindice.py similarity index 92% rename from src/eolab/rastertools/cli/radioindice.py rename to src/eolab/georastertools/cli/radioindice.py index 76c3a71..dbf7150 100644 --- a/src/eolab/rastertools/cli/radioindice.py +++ b/src/eolab/georastertools/cli/radioindice.py @@ -5,10 +5,10 @@ """ import logging -from eolab.rastertools import RastertoolConfigurationException, Radioindice -from eolab.rastertools.cli.utils_cli import apply_process, win_opt -from eolab.rastertools.product import BandChannel -from eolab.rastertools.processing import RadioindiceProcessing +from eolab.georastertools import RastertoolConfigurationException, Radioindice +from eolab.georastertools.cli.utils_cli import apply_process, win_opt +from eolab.georastertools.product import BandChannel +from eolab.georastertools.processing import RadioindiceProcessing import sys import click import os @@ -55,7 +55,7 @@ def indices_opt(function): multiple=True, nargs=2, metavar="band1 band2", help="Compute the normalized difference of two bands defined" " as parameter of this option, e.g. \"-nd red nir\" will compute (red-nir)/(red+nir). " - "See eolab.rastertools.product.rastertype.BandChannel for the list of bands names. " + "See eolab.georastertools.product.rastertype.BandChannel for the list of bands names. " "Several nd options can be set to compute several normalized differences.") diff --git a/src/eolab/rastertools/cli/speed.py b/src/eolab/georastertools/cli/speed.py similarity index 92% rename from src/eolab/rastertools/cli/speed.py rename to src/eolab/georastertools/cli/speed.py index 5628fb5..781c04f 100644 --- a/src/eolab/rastertools/cli/speed.py +++ b/src/eolab/georastertools/cli/speed.py @@ -3,8 +3,8 @@ """ CLI definition for the speed tool """ -from eolab.rastertools import Speed -from eolab.rastertools.cli.utils_cli import apply_process, band_opt, all_opt +from eolab.georastertools import Speed +from eolab.georastertools.cli.utils_cli import apply_process, band_opt, all_opt import click import os diff --git a/src/eolab/rastertools/cli/svf.py b/src/eolab/georastertools/cli/svf.py similarity index 94% rename from src/eolab/rastertools/cli/svf.py rename to src/eolab/georastertools/cli/svf.py index 2301094..cd5fab0 100644 --- a/src/eolab/rastertools/cli/svf.py +++ b/src/eolab/georastertools/cli/svf.py @@ -3,8 +3,8 @@ """ CLI definition for the SVF (Sky View Factor) tool """ -from eolab.rastertools import SVF -from eolab.rastertools.cli.utils_cli import apply_process, pad_opt, win_opt +from eolab.georastertools import SVF +from eolab.georastertools.cli.utils_cli import apply_process, pad_opt, win_opt import click import os diff --git a/src/eolab/rastertools/cli/tiling.py b/src/eolab/georastertools/cli/tiling.py similarity index 96% rename from src/eolab/rastertools/cli/tiling.py rename to src/eolab/georastertools/cli/tiling.py index 41ef1e7..33598d3 100644 --- a/src/eolab/rastertools/cli/tiling.py +++ b/src/eolab/georastertools/cli/tiling.py @@ -3,8 +3,8 @@ """ CLI definition for the tiling tool """ -from eolab.rastertools import Tiling -from eolab.rastertools.cli.utils_cli import apply_process +from eolab.georastertools import Tiling +from eolab.georastertools.cli.utils_cli import apply_process import click import os diff --git a/src/eolab/rastertools/cli/timeseries.py b/src/eolab/georastertools/cli/timeseries.py similarity index 93% rename from src/eolab/rastertools/cli/timeseries.py rename to src/eolab/georastertools/cli/timeseries.py index b6a4815..af40eb2 100644 --- a/src/eolab/rastertools/cli/timeseries.py +++ b/src/eolab/georastertools/cli/timeseries.py @@ -5,9 +5,9 @@ """ from datetime import datetime -from eolab.rastertools import Timeseries -from eolab.rastertools.cli.utils_cli import apply_process, win_opt, all_opt, band_opt -from eolab.rastertools import RastertoolConfigurationException +from eolab.georastertools import Timeseries +from eolab.georastertools.cli.utils_cli import apply_process, win_opt, all_opt, band_opt +from eolab.georastertools import RastertoolConfigurationException import click import sys import os diff --git a/src/eolab/rastertools/cli/utils_cli.py b/src/eolab/georastertools/cli/utils_cli.py similarity index 98% rename from src/eolab/rastertools/cli/utils_cli.py rename to src/eolab/georastertools/cli/utils_cli.py index 3e5abd1..d797eee 100644 --- a/src/eolab/rastertools/cli/utils_cli.py +++ b/src/eolab/georastertools/cli/utils_cli.py @@ -1,4 +1,4 @@ -from eolab.rastertools import RastertoolConfigurationException +from eolab.georastertools import RastertoolConfigurationException import logging import sys import click diff --git a/src/eolab/rastertools/cli/zonalstats.py b/src/eolab/georastertools/cli/zonalstats.py similarity index 97% rename from src/eolab/rastertools/cli/zonalstats.py rename to src/eolab/georastertools/cli/zonalstats.py index 89e822b..7658788 100644 --- a/src/eolab/rastertools/cli/zonalstats.py +++ b/src/eolab/georastertools/cli/zonalstats.py @@ -3,8 +3,8 @@ """ CLI definition for the zonalstats tool """ -from eolab.rastertools import Zonalstats -from eolab.rastertools.cli.utils_cli import apply_process, all_opt, band_opt +from eolab.georastertools import Zonalstats +from eolab.georastertools.cli.utils_cli import apply_process, all_opt, band_opt import click import os diff --git a/src/eolab/rastertools/filtering.py b/src/eolab/georastertools/filtering.py similarity index 92% rename from src/eolab/rastertools/filtering.py rename to src/eolab/georastertools/filtering.py index 64fcea2..27e6fb4 100644 --- a/src/eolab/rastertools/filtering.py +++ b/src/eolab/georastertools/filtering.py @@ -8,11 +8,11 @@ from typing import List, Dict from pathlib import Path -from eolab.rastertools import utils -from eolab.rastertools import Rastertool, Windowable -from eolab.rastertools.processing import algo -from eolab.rastertools.processing import RasterFilter, compute_sliding -from eolab.rastertools.product import RasterProduct +from eolab.georastertools import utils +from eolab.georastertools import Rastertool, Windowable +from eolab.georastertools.processing import algo +from eolab.georastertools.processing import RasterFilter, compute_sliding +from eolab.georastertools.product import RasterProduct _logger = logging.getLogger(__name__) @@ -36,7 +36,7 @@ class Filtering(Rastertool, Windowable): tool = Filtering(Filtering.median_filter, kernel_size=16) Custom additional filters can be easily added by instanciating a RasterFilter (see - :obj:`eolab.rastertools.processing.RasterFilter`). + :obj:`eolab.georastertools.processing.RasterFilter`). The parameters that customize the filter algo can be passed by following this procedure: @@ -138,7 +138,7 @@ def get_default_filters(): """Get the list of predefined raster filters Returns: - [:obj:`eolab.rastertools.processing.RasterFilter`] List of the predefined + [:obj:`eolab.georastertools.processing.RasterFilter`] List of the predefined raster filters ([Median, Local sum, Local mean, Adaptive gaussian]) """ return [ @@ -150,7 +150,7 @@ def __init__(self, raster_filter: RasterFilter, kernel_size: int, bands: List[in """Constructor Args: - raster_filter (:obj:`eolab.rastertools.processing.RasterFilter`): + raster_filter (:obj:`eolab.georastertools.processing.RasterFilter`): The instance of RasterFilter to apply kernel_size (int): Size of the kernel on which the filter is applied @@ -186,7 +186,7 @@ def with_filter_configuration(self, argsdict: Dict): the kernel size (key="kernel") Returns: - :obj:`eolab.rastertools.Filtering`: The current instance so that it is + :obj:`eolab.georastertools.Filtering`: The current instance so that it is possible to chain the with... calls (fluent API) """ self.raster_filter.configure(argsdict) diff --git a/src/eolab/rastertools/rastertools.py b/src/eolab/georastertools/georastertools.py similarity index 92% rename from src/eolab/rastertools/rastertools.py rename to src/eolab/georastertools/georastertools.py index 479bab7..b471099 100644 --- a/src/eolab/rastertools/rastertools.py +++ b/src/eolab/georastertools/georastertools.py @@ -1,13 +1,13 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """ -This module defines the base class for every Rastertool, namely :obj:`eolab.rastertools.Rastertool`. +This module defines the base class for every Rastertool, namely :obj:`eolab.georastertools.Rastertool`. It also defines a specific Exception class that shall be raised when invalid rastertool's configuration parameters are setup. Finally, it defines additional decorator classes that factorize some rastertool configuration -features. For example, the class :obj:`eolab.rastertools.Windowable` enables to configure a +features. For example, the class :obj:`eolab.georastertools.Windowable` enables to configure a rastertool with windowable capabilities (i.e. capability to split the raster input file in small parts in order to distribute the processing over several cpus). """ @@ -16,7 +16,7 @@ import logging import sys -from eolab.rastertools import utils +from eolab.georastertools import utils _logger = logging.getLogger(__name__) @@ -65,7 +65,7 @@ def with_output(self, outputdir: str = "."): Output dir where to store results. If none, it is set to current dir Returns: - :obj:`eolab.rastertools.Rastertool`: The current instance so that it is + :obj:`eolab.georastertools.Rastertool`: The current instance so that it is possible to chain the with... calls (fluent API) """ if outputdir and not utils.is_dir(outputdir): @@ -85,7 +85,7 @@ def with_vrt_stored(self, keep_vrt: bool): Whether intermediate VRT images shall be stored or not Returns: - :obj:`eolab.rastertools.Rastertool`: The current instance so that it is + :obj:`eolab.georastertools.Rastertool`: The current instance so that it is possible to chain the with... calls (fluent API) """ self._keep_vrt = keep_vrt @@ -158,7 +158,7 @@ def postprocess_files(self, inputfiles: List[str], outputfiles: List[str]) -> Li class Windowable: - """Decorator of a :obj:`eolab.rastertools.Rastertool` that adds configuration + """Decorator of a :obj:`eolab.georastertools.Rastertool` that adds configuration parameters to set the windowable capability of the tool: - the window size @@ -190,7 +190,7 @@ def with_windows(self, window_size: int = 1024, pad_mode: str = "edge"): See https://numpy.org/doc/stable/reference/generated/numpy.pad.html Returns: - :obj:`eolab.rastertools.Rastertool`: The current instance so that it is + :obj:`eolab.georastertools.Rastertool`: The current instance so that it is possible to chain the with... calls (fluent API) """ self._window_size = utils.to_tuple(window_size) diff --git a/src/eolab/rastertools/hillshade.py b/src/eolab/georastertools/hillshade.py similarity index 96% rename from src/eolab/rastertools/hillshade.py rename to src/eolab/georastertools/hillshade.py index 7fbbeb9..047effa 100644 --- a/src/eolab/rastertools/hillshade.py +++ b/src/eolab/georastertools/hillshade.py @@ -12,10 +12,10 @@ import rasterio from rasterio.windows import Window -from eolab.rastertools import utils -from eolab.rastertools import Rastertool, Windowable -from eolab.rastertools.processing import algo -from eolab.rastertools.processing import RasterProcessing, compute_sliding +from eolab.georastertools import utils +from eolab.georastertools import Rastertool, Windowable +from eolab.georastertools.processing import algo +from eolab.georastertools.processing import RasterProcessing, compute_sliding _logger = logging.getLogger(__name__) diff --git a/src/eolab/rastertools/main.py b/src/eolab/georastertools/main.py similarity index 76% rename from src/eolab/rastertools/main.py rename to src/eolab/georastertools/main.py index 88bbf0f..fc63609 100644 --- a/src/eolab/rastertools/main.py +++ b/src/eolab/georastertools/main.py @@ -1,13 +1,13 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- """ -This module contains the rastertools command line interface. The command line +This module contains the georastertools command line interface. The command line has several subcommands such as *radioindice* and *zonalstats*. Usage examples:: - rastertools radioindice --help - rastertools zonalstats --help + georastertools radioindice --help + georastertools zonalstats --help """ import logging @@ -16,16 +16,16 @@ import sys import json import click -from eolab.rastertools.cli.filtering import filter -from eolab.rastertools.cli.hillshade import hillshade -from eolab.rastertools.cli.speed import speed -from eolab.rastertools.cli.svf import svf -from eolab.rastertools.cli.tiling import tiling -from eolab.rastertools.cli.timeseries import timeseries -from eolab.rastertools.cli.radioindice import radioindice -from eolab.rastertools.cli.zonalstats import zonalstats -from eolab.rastertools import __version__ -from eolab.rastertools.product import RasterType +from eolab.georastertools.cli.filtering import filter +from eolab.georastertools.cli.hillshade import hillshade +from eolab.georastertools.cli.speed import speed +from eolab.georastertools.cli.svf import svf +from eolab.georastertools.cli.tiling import tiling +from eolab.georastertools.cli.timeseries import timeseries +from eolab.georastertools.cli.radioindice import radioindice +from eolab.georastertools.cli.zonalstats import zonalstats +from eolab.georastertools import __version__ +from eolab.georastertools.product import RasterType def add_custom_rastertypes(rastertypes): @@ -147,7 +147,7 @@ def add_custom_rastertypes(rastertypes): type=int, help="Maximum number of workers for parallel processing. If not given, it will default to " "the number of processors on the machine. When all processors are not allocated to " - "run rastertools, it is thus recommended to set this option.") + "run georastertools, it is thus recommended to set this option.") @click.option( '--debug', "keep_vrt", @@ -164,14 +164,14 @@ def add_custom_rastertypes(rastertypes): '--very-verbose', is_flag=True, help="set loglevel to DEBUG") -@click.version_option(version='rastertools {}'.format(__version__)) # Ensure __version__ is defined +@click.version_option(version='georastertools {}'.format(__version__)) # Ensure __version__ is defined @click.pass_context -def rastertools(ctx, rastertype : str, max_workers : int, keep_vrt : bool, verbose : bool, very_verbose : bool): +def georastertools(ctx, rastertype : str, max_workers : int, keep_vrt : bool, verbose : bool, very_verbose : bool): """ - Main entry point for the `rastertools` Command Line Interface. + Main entry point for the `georastertools` Command Line Interface. - The `rastertools` CLI provides tools for raster processing and analysis and allows configurable data handling, parallel processing, + The `georastertools` CLI provides tools for raster processing and analysis and allows configurable data handling, parallel processing, and debugging support. Logging: @@ -184,7 +184,7 @@ def rastertools(ctx, rastertype : str, max_workers : int, keep_vrt : bool, verbo - `RASTERTOOLS_NOTQDM`: If the log level is above INFO, sets this to disable progress bars. - - `RASTERTOOLS_MAXWORKERS`: If `max_workers` is set, it defines the max workers for rastertools. + - `RASTERTOOLS_MAXWORKERS`: If `max_workers` is set, it defines the max workers for georastertools. """ #Saving keep_vrt to use it in the subcommands ctx.ensure_object(dict) @@ -214,28 +214,28 @@ def rastertools(ctx, rastertype : str, max_workers : int, keep_vrt : bool, verbo # Register subcommands from other modules -rastertools.add_command(filter, name = "fi") -rastertools.add_command(filter, name = "filter") -rastertools.add_command(hillshade, name = "hs") -rastertools.add_command(hillshade, name = "hillshade") -rastertools.add_command(radioindice, name = "ri") -rastertools.add_command(radioindice, name = "radioindice") -rastertools.add_command(speed, name = "sp") -rastertools.add_command(speed, name = "speed") -rastertools.add_command(svf, name = "svf") -rastertools.add_command(tiling, name = "ti") -rastertools.add_command(tiling, name = "tiling") -rastertools.add_command(timeseries, name = "ts") -rastertools.add_command(timeseries, name = "timeseries") -rastertools.add_command(zonalstats, name = "zs") -rastertools.add_command(zonalstats, name = "zonalstats") +georastertools.add_command(filter, name = "fi") +georastertools.add_command(filter, name = "filter") +georastertools.add_command(hillshade, name = "hs") +georastertools.add_command(hillshade, name = "hillshade") +georastertools.add_command(radioindice, name = "ri") +georastertools.add_command(radioindice, name = "radioindice") +georastertools.add_command(speed, name = "sp") +georastertools.add_command(speed, name = "speed") +georastertools.add_command(svf, name = "svf") +georastertools.add_command(tiling, name = "ti") +georastertools.add_command(tiling, name = "tiling") +georastertools.add_command(timeseries, name = "ts") +georastertools.add_command(timeseries, name = "timeseries") +georastertools.add_command(zonalstats, name = "zs") +georastertools.add_command(zonalstats, name = "zonalstats") def run(*args, **kwargs): """ Entry point for console_scripts """ - rastertools(*args, **kwargs) + georastertools(*args, **kwargs) if __name__ == "__main__": diff --git a/src/eolab/rastertools/processing/__init__.py b/src/eolab/georastertools/processing/__init__.py similarity index 52% rename from src/eolab/rastertools/processing/__init__.py rename to src/eolab/georastertools/processing/__init__.py index bebf07b..e54d609 100644 --- a/src/eolab/rastertools/processing/__init__.py +++ b/src/eolab/georastertools/processing/__init__.py @@ -1,14 +1,14 @@ # -*- coding: utf-8 -*- -"""This module contains the rastertools processing algorithms. +"""This module contains the georastertools processing algorithms. """ # importing the RasterProcessing and children classes -from eolab.rastertools.processing.rasterproc import RasterProcessing -from eolab.rastertools.processing.rasterproc import RadioindiceProcessing, RasterFilter +from eolab.georastertools.processing.rasterproc import RasterProcessing +from eolab.georastertools.processing.rasterproc import RadioindiceProcessing, RasterFilter # importing the methods that perform the raster processings -from eolab.rastertools.processing.sliding import compute_sliding -from eolab.rastertools.processing.stats import compute_zonal_stats, compute_zonal_stats_per_category -from eolab.rastertools.processing.stats import extract_zonal_outliers, plot_stats +from eolab.georastertools.processing.sliding import compute_sliding +from eolab.georastertools.processing.stats import compute_zonal_stats, compute_zonal_stats_per_category +from eolab.georastertools.processing.stats import extract_zonal_outliers, plot_stats __all__ = [ "RasterProcessing", "RadioindiceProcessing", "RasterFilter", diff --git a/src/eolab/rastertools/processing/algo.py b/src/eolab/georastertools/processing/algo.py similarity index 100% rename from src/eolab/rastertools/processing/algo.py rename to src/eolab/georastertools/processing/algo.py diff --git a/src/eolab/rastertools/processing/rasterproc.py b/src/eolab/georastertools/processing/rasterproc.py similarity index 97% rename from src/eolab/rastertools/processing/rasterproc.py rename to src/eolab/georastertools/processing/rasterproc.py index c708fb4..bf03d46 100644 --- a/src/eolab/rastertools/processing/rasterproc.py +++ b/src/eolab/georastertools/processing/rasterproc.py @@ -8,8 +8,8 @@ import numpy import numpy as np -from eolab.rastertools.processing import algo -from eolab.rastertools.product import BandChannel +from eolab.georastertools.processing import algo +from eolab.georastertools.product import BandChannel class RasterProcessing: @@ -157,7 +157,7 @@ def with_arguments(self, arguments): arguments (Dict[str, Dict]): Dictionary where the keys are the arguments' names and the values are dictionaries of arguments' properties as defined in `ArgumentParser.add_argument `_ . - The properties dictionaries are used to configure the command line 'rastertools'.* + The properties dictionaries are used to configure the command line 'georastertools'.* The possible keys are: action, nargs, const, default, type, choices, required, help, metavar and dest @@ -220,7 +220,7 @@ def with_channels(self, channels: List[BandChannel]) : """Set the BandChannels necessary to compute the radiometric indice Args: - channels ([:obj:`eolab.rastertools.product.BandChannel`]): + channels ([:obj:`eolab.georastertools.product.BandChannel`]): Channels to process, default None Returns: diff --git a/src/eolab/rastertools/processing/sliding.py b/src/eolab/georastertools/processing/sliding.py similarity index 99% rename from src/eolab/rastertools/processing/sliding.py rename to src/eolab/georastertools/processing/sliding.py index 0ccb326..ed76096 100644 --- a/src/eolab/rastertools/processing/sliding.py +++ b/src/eolab/georastertools/processing/sliding.py @@ -15,8 +15,8 @@ from rasterio.windows import Window from tqdm.contrib.concurrent import process_map -from eolab.rastertools import utils -from eolab.rastertools.processing import RasterProcessing +from eolab.georastertools import utils +from eolab.georastertools.processing import RasterProcessing _logger = logging.getLogger(__name__) diff --git a/src/eolab/rastertools/processing/stats.py b/src/eolab/georastertools/processing/stats.py similarity index 98% rename from src/eolab/rastertools/processing/stats.py rename to src/eolab/georastertools/processing/stats.py index 222b397..c00e9ee 100644 --- a/src/eolab/rastertools/processing/stats.py +++ b/src/eolab/georastertools/processing/stats.py @@ -18,8 +18,8 @@ from rasterio import features from tqdm import tqdm -from eolab.rastertools.utils import get_metadata_name -from eolab.rastertools.processing.vector import rasterize, filter_dissolve +from eolab.georastertools.utils import get_metadata_name +from eolab.georastertools.processing.vector import rasterize, filter_dissolve def compute_zonal_stats(geoms: gpd.GeoDataFrame, image: str, @@ -62,7 +62,7 @@ def compute_zonal_stats(geoms: gpd.GeoDataFrame, image: str, Example: ``` import geopandas as gpd - from rastertools import compute_zonal_stats + from georastertools import compute_zonal_stats geoms = gpd.read_file("polygons.shp") stats = compute_zonal_stats(geoms, "input_image.tif", bands=[1, 2], stats=["mean", "std"]) @@ -148,7 +148,7 @@ def compute_zonal_stats_per_category(geoms: gpd.GeoDataFrame, image: str, Example: ``` import geopandas as gpd - from rastertools import compute_zonal_stats_per_category + from georastertools import compute_zonal_stats_per_category geoms = gpd.read_file("regions.shp") categories = gpd.read_file("landcover.shp") diff --git a/src/eolab/rastertools/processing/vector.py b/src/eolab/georastertools/processing/vector.py similarity index 99% rename from src/eolab/rastertools/processing/vector.py rename to src/eolab/georastertools/processing/vector.py index d37e80e..881bf64 100644 --- a/src/eolab/rastertools/processing/vector.py +++ b/src/eolab/georastertools/processing/vector.py @@ -15,7 +15,7 @@ import rasterio from rasterio import features, warp, windows -from eolab.rastertools import utils +from eolab.georastertools import utils def _get_geoms(geoms: Union[gpd.GeoDataFrame, Path, str]) -> gpd.GeoDataFrame: diff --git a/src/eolab/rastertools/product/__init__.py b/src/eolab/georastertools/product/__init__.py similarity index 76% rename from src/eolab/rastertools/product/__init__.py rename to src/eolab/georastertools/product/__init__.py index 7d0cf6a..3d4b754 100644 --- a/src/eolab/rastertools/product/__init__.py +++ b/src/eolab/georastertools/product/__init__.py @@ -3,8 +3,8 @@ """ import numpy as np -from eolab.rastertools.product.rastertype import Band, BandChannel, RasterType -from eolab.rastertools.product.rasterproduct import RasterProduct +from eolab.georastertools.product.rastertype import Band, BandChannel, RasterType +from eolab.georastertools.product.rasterproduct import RasterProduct # import classes of rasterproduct and rastertype submodules __all__ = [ @@ -15,7 +15,7 @@ import importlib.resources import json -RasterType.add(json.load(importlib.resources.open_binary("eolab.rastertools.product", "rastertypes.json"))) +RasterType.add(json.load(importlib.resources.open_binary("eolab.georastertools.product", "rastertypes.json"))) def s2_maja_mask(in_ar, out_ar, xoff, yoff, xsize, ysize, diff --git a/src/eolab/rastertools/product/rasterproduct.py b/src/eolab/georastertools/product/rasterproduct.py similarity index 99% rename from src/eolab/rastertools/product/rasterproduct.py rename to src/eolab/georastertools/product/rasterproduct.py index 4eebf15..d767dde 100644 --- a/src/eolab/rastertools/product/rasterproduct.py +++ b/src/eolab/georastertools/product/rasterproduct.py @@ -16,10 +16,10 @@ from osgeo import gdal import rasterio -from eolab.rastertools import utils -from eolab.rastertools.product import RasterType -from eolab.rastertools.product.vrt import add_masks_to_vrt, set_band_descriptions -from eolab.rastertools.processing.vector import crop +from eolab.georastertools import utils +from eolab.georastertools.product import RasterType +from eolab.georastertools.product.vrt import add_masks_to_vrt, set_band_descriptions +from eolab.georastertools.processing.vector import crop __author__ = "Olivier Queyrut" __copyright__ = "Copyright 2019, CNES" diff --git a/src/eolab/rastertools/product/rastertype.py b/src/eolab/georastertools/product/rastertype.py similarity index 96% rename from src/eolab/rastertools/product/rastertype.py rename to src/eolab/georastertools/product/rastertype.py index 77098b1..b80d6e8 100644 --- a/src/eolab/rastertools/product/rastertype.py +++ b/src/eolab/georastertools/product/rastertype.py @@ -10,7 +10,7 @@ from enum import Enum from pathlib import Path -from eolab.rastertools import utils +from eolab.georastertools import utils __author__ = "Olivier Queyrut" __copyright__ = "Copyright 2019, CNES" @@ -80,7 +80,7 @@ def find(file: Union[Path, str]): Product name or path to analyse Returns: - :obj:`eolab.rastertools.product.RasterType`: the RasterType corresponding + :obj:`eolab.georastertools.product.RasterType`: the RasterType corresponding to the input file """ return next((t for n, t in RasterType.rastertypes.items() if t.accept(file)), None) @@ -214,7 +214,7 @@ def channels(self) -> List[BandChannel]: """Gets the list of all available channels Returns: - [:obj:`eolab.rastertools.product.BandChannel`]: + [:obj:`eolab.georastertools.product.BandChannel`]: List of available bands channels for this type of product """ return list(self._raster_bands.keys()) @@ -273,7 +273,7 @@ def has_channel(self, channel: BandChannel) -> bool: """Checks if the product type contains the expected channel. Args: - channel (:obj:`eolab.rastertools.product.BandChannel`): + channel (:obj:`eolab.georastertools.product.BandChannel`): Band channel Returns: @@ -285,7 +285,7 @@ def has_channels(self, channels: List[BandChannel]) -> bool: """Checks if the product type contains all the expected channels. Args: - channels ([:obj:`eolab.rastertools.product.BandChannel`]): + channels ([:obj:`eolab.georastertools.product.BandChannel`]): List of bands channels Returns: @@ -308,7 +308,7 @@ def get_band_id(self, channel: BandChannel) -> str: """Gets the band identifier of the specified channel. Args: - channel (:obj:`eolab.rastertools.product.BandChannel`): + channel (:obj:`eolab.georastertools.product.BandChannel`): Band channel Returns: @@ -328,7 +328,7 @@ def get_band_description(self, channel: BandChannel) -> List[str]: """Gets the band description of the given channel. Args: - channel (:obj:`eolab.rastertools.product.BandChannel`): + channel (:obj:`eolab.georastertools.product.BandChannel`): Band channel Returns: @@ -340,7 +340,7 @@ def get_bands_regexp(self, channels: List[BandChannel] = None) -> str: """Gets the regexp to identify the bands in the product. Args: - channels ([:obj:`eolab.rastertools.product.BandChannel`]): + channels ([:obj:`eolab.georastertools.product.BandChannel`]): List of bands channels Returns: diff --git a/src/eolab/rastertools/product/rastertypes.json b/src/eolab/georastertools/product/rastertypes.json similarity index 99% rename from src/eolab/rastertools/product/rastertypes.json rename to src/eolab/georastertools/product/rastertypes.json index 45f6857..b642272 100644 --- a/src/eolab/rastertools/product/rastertypes.json +++ b/src/eolab/georastertools/product/rastertypes.json @@ -211,7 +211,7 @@ } ], "date_format": "%Y%m%d-%H%M%S", - "maskfunc": "eolab.rastertools.product.s2_maja_mask" + "maskfunc": "eolab.georastertools.product.s2_maja_mask" }, { "name": "S2_L3A_THEIA", diff --git a/src/eolab/rastertools/product/vrt.py b/src/eolab/georastertools/product/vrt.py similarity index 100% rename from src/eolab/rastertools/product/vrt.py rename to src/eolab/georastertools/product/vrt.py diff --git a/src/eolab/rastertools/radioindice.py b/src/eolab/georastertools/radioindice.py similarity index 93% rename from src/eolab/rastertools/radioindice.py rename to src/eolab/georastertools/radioindice.py index 3901557..540df75 100644 --- a/src/eolab/rastertools/radioindice.py +++ b/src/eolab/georastertools/radioindice.py @@ -1,557 +1,557 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -This module defines a command line named radioindice that computes radiometric -indices on raster images: ndvi, ndwi, etc.. -""" -import logging -import logging.config -import os -from pathlib import Path -from typing import List -import threading - -import rasterio -import numpy.ma as ma -from tqdm import tqdm - -from eolab.rastertools import utils -from eolab.rastertools import Rastertool, Windowable -from eolab.rastertools.processing import algo -from eolab.rastertools.processing import RadioindiceProcessing -from eolab.rastertools.product import BandChannel, RasterProduct - - -_logger = logging.getLogger(__name__) - - -class Radioindice(Rastertool, Windowable): - """Raster tool that computes radiometric indices of a raster product. - - If several indices are requested, the tool can generate one output image with one - band per indice (merge=True), or it can generate several images, one image per indice - (merge=False). - - The computation can be realized on a subset of the input image (a Region Of Interest) - defined by a vector file (e.g. shapefile, geojson). - - The radiometric indice is an instance of - :obj:`eolab.rastertools.processing.RadioindiceProcessing` - which defines the list of channels it needs to compute the indice. The input raster product - must be of a recognized raster type so that it is possible to match every channels required by - the indice with an existing band in the raster product. - """ - # Preconfigured radioindices - # Vegetation indices: ndvi - ndvi = RadioindiceProcessing("ndvi").with_channels( - [BandChannel.red, BandChannel.nir]) - """Normalized Difference Vegetation Index (red, nir channels) - - .. math:: - - ndvi = \\frac{nir - red}{nir + red} - - References: - Rouse J.W., Haas R.H., Schell J.A., Deering D.W., 1973. Monitoring vegetation systems in - the great plains with ERTS. Third ERTS Symposium, NASA SP-351. 1:309-317 - - Tucker C.J., 1979. Red and photographic infrared linear combinations for monitoring - vegetation. Remote Sens Environ 8:127-150 - - """ - - # Vegetation indices: tndvi - tndvi = RadioindiceProcessing("tndvi", algo=algo.tndvi).with_channels( - [BandChannel.red, BandChannel.nir]) - """Transformed Normalized Difference Vegetation Index (red, nir channels) - - .. math:: - :nowrap: - - \\begin{eqnarray} - ndvi & = & \\frac{nir - red}{nir + red} \\\\ - tndvi & = & \\sqrt{ndvi + 0.5} - \\end{eqnarray} - - References: - `Deering D.W., Rouse J.W., Haas R.H., and Schell J.A., 1975. Measuring forage production - of grazing units from Landsat MSS data. Pages 1169-1178 In: Cook J.J. (Ed.), Proceedings - of the Tenth International Symposium on Remote Sensing of Environment (Ann Arbor, 1975), - Vol. 2, Ann Arbor, Michigan, USA. `_ - - """ - - # Vegetation indices: rvi - rvi = RadioindiceProcessing("rvi", algo=algo.rvi).with_channels( - [BandChannel.red, BandChannel.nir]) - """Ratio Vegetation Index (red, nir channels) - - .. math:: - - rvi = \\frac{nir}{red} - - References: - `Jordan C.F., 1969. Derivation of leaf area index from quality of light on the forest - floor. Ecology 50:663-666 `_ - """ - - # Vegetation indices: pvi - pvi = RadioindiceProcessing("pvi", algo=algo.pvi).with_channels( - [BandChannel.red, BandChannel.nir]) - """Perpendicular Vegetation Index (red, nir channels) - - .. math:: - - pvi = (nir - 0.90893 * red - 7.46216) * 0.74 - - References: - `Richardson A.J., Wiegand C.L., 1977. Distinguishing vegetation from soil background - information. Photogramm Eng Rem S 43-1541-1552 `_ - """ - - # Vegetation indices: savi - savi = RadioindiceProcessing("savi", algo=algo.savi).with_channels( - [BandChannel.red, BandChannel.nir]) - """Soil Adjusted Vegetation Index (red, nir channels) - - .. math:: - - savi = \\frac{(nir - red) * (1. + 0.5)}{nir + red + 0.5} - - References: - `Huete A.R., 1988. A soil-adjusted vegetation index (SAVI). Remote Sens Environ 25:295-309 `_ - """ - - # Vegetation indices: tsavi - tsavi = RadioindiceProcessing("tsavi", algo=algo.tsavi).with_channels( - [BandChannel.red, BandChannel.nir]) - """Transformed Soil Adjusted Vegetation Index (red, nir channels) - - .. math:: - - tsavi = \\frac{0.7 * (nir - 0.7 * red - 0.9)}{0.7 * nir + red + 0.08 * (1 + 0.7^2)} - - References: - `Baret F., Guyot G., Major D., 1989. TSAVI: a vegetation index which minimizes soil - brightness effects on LAI or APAR estimation. 12th Canadian Symposium on Remote - Sensing and IGARSS 1990, Vancouver, Canada, 07/10-14. `_ - """ - - # Vegetation indices: msavi - msavi = RadioindiceProcessing("msavi", algo=algo.msavi).with_channels( - [BandChannel.red, BandChannel.nir]) - """Modified Soil Adjusted Vegetation Index (red, nir channels) - - .. math:: - :nowrap: - - \\begin{eqnarray} - wdvi & = & nir - 0.4 * red \\\\ - ndvi & = & \\frac{nir - red}{nir + red} \\\\ - L & = & 1 - 2 * 0.4 * ndvi * wdvi \\\\ - msavi & = & \\frac{(nir - red) * (1 + L)}{nir + red + L} - \\end{eqnarray} - - References: - `Qi J., Chehbouni A., Huete A.R., Kerr Y.H., 1994. Modified Soil Adjusted Vegetation - Index (MSAVI). Remote Sens Environ 48:119-126 `_ - - `Qi J., Kerr Y., Chehbouni A., 1994. External factor consideration in vegetation index - development. Proc. of Physical Measurements and Signatures in Remote Sensing, - ISPRS, 723-730. `_ - """ - - # Vegetation indices: msavi2 - msavi2 = RadioindiceProcessing("msavi2", algo=algo.msavi2).with_channels( - [BandChannel.red, BandChannel.nir]) - """Modified Soil Adjusted Vegetation Index (red, nir channels) - - .. math:: - :nowrap: - - \\begin{eqnarray} - val & = & (2 * nir + 1)^2 - 8 * (nir - red) \\\\ - msavi2 & = & (2 * nir + 1 - \\sqrt{val}) / 2 - \\end{eqnarray} - """ - - # Vegetation indices: ipvi - ipvi = RadioindiceProcessing("ipvi", algo=algo.ipvi).with_channels( - [BandChannel.red, BandChannel.nir]) - """Infrared Percentage Vegetation Index (red, nir channels) - - .. math:: - ipvi = \\frac{nir}{nir + red} - - References: - `Crippen, R. E. 1990. Calculating the Vegetation Index Faster, Remote Sensing of - Environment, vol 34., pp. 71-73. `_ - """ - - # Vegetation indices: evi - evi = RadioindiceProcessing("evi", algo=algo.evi).with_channels( - [BandChannel.red, BandChannel.nir, BandChannel.blue]) - """Enhanced vegetation index (red, nir, blue channels) - - .. math:: - evi = \\frac{2.5 * (nir - red)}{nir + 6.0 * red - 7.5 * blue + 1.0} - - """ - - # Water indices: ndwi - ndwi = RadioindiceProcessing("ndwi").with_channels( - [BandChannel.mir, BandChannel.nir]) - """Normalized Difference Water Index (mir, nir channels) - - .. math:: - - ndwi = \\frac{nir - mir}{nir + mir} - - References: - Gao, B. C., 1996. NDWI - A normalized difference water index for remote sensing - of vegetation liquid water from space. Remote Sensing of Environment 58, 257-266. - """ - - # Water indices: ndwi2 - ndwi2 = RadioindiceProcessing("ndwi2").with_channels( - [BandChannel.nir, BandChannel.green]) - """Normalized Difference Water Index (nir, green channels) - - .. math:: - - ndwi2 = \\frac{green - nir}{green + nir} - """ - - # Water indices: mdwi - mndwi = RadioindiceProcessing("mndwi").with_channels( - [BandChannel.mir, BandChannel.green]) - """Modified Normalized Difference Water Index (green, mir channels) - - .. math:: - - mndwi = \\frac{green - mir}{green + mir} - - References: - Xu, H. Q., 2006. Modification of normalised difference water index (NDWI) to enhance - open water features in remotely sensed imagery. International Journal of Remote Sensing - 27, 3025-3033 - - """ - - # Water indices: ndpi - ndpi = RadioindiceProcessing("ndpi").with_channels( - [BandChannel.green, BandChannel.mir]) - """Normalized Difference Pond Index (green, mir channels) - - .. math:: - - ndpi = \\frac{mir - green}{mir + green} - - References: - J-P. Lacaux, Y. M. Tourre, C. Vignolle, J-A. Ndione, and M. Lafaye, "Classification - of Ponds from High-Spatial Resolution Remote Sensing: Application to Rift Valley Fever - Epidemics in Senegal," Remote Sensing of Environment 106 66–74, Elsevier Publishers: 2007 - """ - - # Water indices: ndti - ndti = RadioindiceProcessing("ndti").with_channels( - [BandChannel.green, BandChannel.red]) - """Normalized Difference Turbidity Index (green, red channels) - - .. math:: - - ndti = \\frac{red - green}{red + green} - - References: - J-P. Lacaux, Y. M. Tourre, C. Vignolle, J-A. Ndione, and M. Lafaye, "Classification - of Ponds from High-Spatial Resolution Remote Sensing: Application to Rift Valley Fever - Epidemics in Senegal," Remote Sensing of Environment 106 66–74, Elsevier Publishers: 2007 - """ - - # urban indices: ndbi - ndbi = RadioindiceProcessing("ndbi").with_channels( - [BandChannel.nir, BandChannel.mir]) - """Normalized Difference Built Up Index (nir, mir channels) - - .. math:: - - ndbi = \\frac{mir - nir}{mir + nir} - - """ - - # Soil indices: ri - ri = RadioindiceProcessing("ri", algo=algo.redness_index).with_channels( - [BandChannel.red, BandChannel.green]) - """Redness index (red, green channels) - - .. math:: - - ri = \\frac{red^2}{green^3} - - """ - - # Soil indices: bi - bi = RadioindiceProcessing("bi", algo=algo.brightness_index).with_channels( - [BandChannel.red, BandChannel.green]) - """Brightness index (red, green channels) - - .. math:: - - bi = \\frac{red^2 + green^2}{2} - - """ - - # Soil indices: bi2 - bi2 = RadioindiceProcessing("bi2", algo=algo.brightness_index2).with_channels( - [BandChannel.nir, BandChannel.red, BandChannel.green]) - """Brightness index (nir, red, green channels) - - .. math:: - - bi2 = \\frac{nir^2 + red^2 + green^2}{3} - - """ - - @staticmethod - def get_default_indices(): - """Get the list of predefined radiometric indices - - Returns: - [:obj:`eolab.rastertools.processing.RadioindiceProcessing`]: list of - predefined radioindice. - """ - # returns all predefined radiometric indices - return [ - Radioindice.ndvi, Radioindice.tndvi, Radioindice.rvi, Radioindice.pvi, - Radioindice.savi, Radioindice.tsavi, Radioindice.msavi, Radioindice.msavi2, - Radioindice.ipvi, Radioindice.evi, Radioindice.ndwi, Radioindice.ndwi2, - Radioindice.mndwi, Radioindice.ndpi, Radioindice.ndti, Radioindice.ndbi, - Radioindice.ri, Radioindice.bi, Radioindice.bi2 - ] - - def __init__(self, indices: List[RadioindiceProcessing]): - """ Constructor - - Args: - indices ([:obj:`eolab.rastertools.processing.RadioindiceProcessing`]): - List of indices to compute (class Indice) - """ - super().__init__() - self.with_windows() - - self._indices = indices - self._merge = False - self._roi = None - - @property - def indices(self) -> List[RadioindiceProcessing]: - """List of radiometric indices to compute""" - return self._indices - - @property - def merge(self) -> bool: - """If true, all indices are in the same output image (one band per indice). - Otherwise, each indice is in its own output image.""" - return self._merge - - @property - def roi(self) -> str: - """Filename of the vector data defining the ROI""" - return self._roi - - def with_output(self, outputdir: str = ".", merge: bool = False): - """Set up the output. - - Args: - outputdir (str, optional, default="."): - Output dir where to store results. If none, it is set to current dir - merge (bool, optional, default=False): - Whether to merge all indices in the same image (i.e. one band per indice) - - Returns: - :obj:`eolab.rastertools.Radioindice`: The current instance so that it is - possible to chain the with... calls (fluent API) - """ - super().with_output(outputdir) - self._merge = merge - return self - - def with_roi(self, roi: str): - """Set up the region of interest - - Args: - roi (str): - Filename of the vector data defining the ROI - (output images will be cropped to the geometry) - - Returns: - :obj:`eolab.rastertools.Radioindice`: The current instance so that it is - possible to chain the with... calls (fluent API) - """ - self._roi = roi - - def process_file(self, inputfile: str) -> List[str]: - """Compute the indices for a single file - - Args: - inputfile (str): - Input image to process - - Returns: - [str]: List of indice images (posix paths) that have been generated - """ - _logger.info(f"Processing file {inputfile}") - - outdir = Path(self.outputdir) - - # STEP 1: Prepare the input image so that it can be processed - with RasterProduct(inputfile, vrt_outputdir=self.vrt_dir) as product: - _logger.debug(f"Raster product is : {product}") - - if product.rastertype is None: - raise ValueError("Unsupported input file, no matching raster type " - "identified to handle the file") - else: - filename = utils.to_path(inputfile).name - _logger.info(f"Raster type of image {filename} is {product.rastertype.name}") - - # check if all indices can be computed for this raster - indices = list() - for indice in self.indices: - # check if the rastertype has all channels - if not(product.rastertype.has_channels(indice.channels)): - _logger.error(f"Can not compute {indice} for {filename}: " - "raster product does not contain all required bands.") - else: - # indice is valid, add it to the list of indices to compute - indices.append(indice) - - # get the raster - raster = product.get_raster(roi=self.roi) - - # STEP 2: Compute the indices - outputs = [] - if self.merge: - # merge is True, compute all indices and generate a single image - _logger.info(f"Compute indices: {' '.join(indice.name for indice in indices)}") - indice_image = outdir.joinpath(f"{utils.get_basename(inputfile)}-indices.tif") - compute_indices(raster, product.channels, indice_image.as_posix(), - indices, self.window_size) - outputs.append(indice_image.as_posix()) - else: - # merge is False, compute all indices and generate one image per indice - for i, indice in enumerate(indices): - _logger.info(f"Compute {indice.name}") - indice_image = outdir.joinpath( - f"{utils.get_basename(inputfile)}-{indice.name}.tif") - compute_indices(raster, product.channels, indice_image.as_posix(), - [indice], self.window_size) - outputs.append(indice_image.as_posix()) - - # return the list of generated files - return outputs - -def compute_indices(input_image: str, image_channels: List[BandChannel], - indice_image: str, indices: List[RadioindiceProcessing], - window_size: tuple = (1024, 1024)): - """ - Compute the indices on the input image and produce a multiple bands - image (one band per indice) - - The possible indices are the following : - ndvi, tndvi, rvi, pvi, savi, tsavi, msavi, msavi2, ipvi, evi, ndwi, ndwi2, mndwi, ndpi, ndti, ndbi, ri, bi, bi2 - - Args: - input_image (str): - Path of the raster to compute - image_channels ([:obj:`eolab.rastertools.product.BandChannel`]): - Ordered list of bands in the raster - indice_image (str): - Path of the output raster image - indices ([:obj:`eolab.rastertools.processing.RadioindiceProcessing`]): - List of indices to compute - window_size (tuple(int, int), optional, default=(1024, 1024)): - Size of windows for splitting the processed image in small parts - """ - with rasterio.Env(GDAL_VRT_ENABLE_PYTHON=True): - src = rasterio.open(input_image) - profile = src.profile - - # set block size to the configured window_size of first indice - blockxsize, blockysize = window_size - if src.width < blockxsize: - blockxsize = utils.highest_power_of_2(src.width) - if src.height < blockysize: - blockysize = utils.highest_power_of_2(src.height) - - # dtype of output data - dtype = indices[0].dtype or rasterio.float32 - - # setup profile for output image - profile.update(driver='GTiff', - blockxsize=blockysize, blockysize=blockxsize, tiled=True, - dtype=dtype, nodata=indices[0].nodata, - count=len(indices)) - - with rasterio.open(indice_image, "w", **profile) as dst: - # Materialize a list of destination block windows - windows = [window for ij, window in dst.block_windows()] - - # disable status of tqdm progress bar - disable = os.getenv("RASTERTOOLS_NOTQDM", 'False').lower() in ['true', '1'] - - # Dictionary to store statistics for each band - band_stats = {i: {"min": float('inf'), "max": float('-inf'), "sum": 0, "total_pix": 0, "count": 0} - for i in range(1, len(indices) + 1)} - - # compute every indices - for i, indice in enumerate(indices, 1): - # Get the bands necessary to compute the indice - bands = [image_channels.index(channel) + 1 for channel in indice.channels] - - read_lock = threading.Lock() - write_lock = threading.Lock() - - def process(window): - """Read input raster, compute indice and write output raster""" - with read_lock: - src_array = src.read(bands, window=window, masked=True) - src_array[src_array == src.nodata] = ma.masked - src_array = src_array.astype(dtype) - - # The computation can be performed concurrently - result = indice.algo(src_array).astype(dtype).filled(indice.nodata) - - # Update statistics - valid_pixels = result[result != indice.nodata] - if valid_pixels.size > 0: - band_stats[i]["min"] = min(band_stats[i]["min"], valid_pixels.min()) - band_stats[i]["max"] = max(band_stats[i]["max"], valid_pixels.max()) - band_stats[i]["sum"] += valid_pixels.sum() - band_stats[i]["total_pix"] += result.size - band_stats[i]["count"] += valid_pixels.size - - with write_lock: - dst.write_band(i, result, window=window) - - # compute using concurrent.futures.ThreadPoolExecutor and tqdm - for window in tqdm(windows, disable=disable, desc=f"{indice.name}"): - process(window) - - dst.set_band_description(i, indice.name) - - # Compute and set metadata tags - for i, stats in band_stats.items(): - # Compute and set metadata tags - mean = stats["sum"] / stats["count"] - sum_sq = (stats["sum"] - mean * stats["count"]) ** 2 - variance = sum_sq / stats["count"] - stddev = variance ** 0.5 if variance > 0 else 0 - - dst.update_tags(i, - STATISTICS_MINIMUM=f"{stats['min']:.14g}", - STATISTICS_MAXIMUM=f"{stats['max']:.14g}", - STATISTICS_MEAN=mean, - STATISTICS_STDDEV=stddev, - STATISTICS_VALID_PERCENT=(stats["count"] / stats["total_pix"] * 100)) - +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module defines a command line named radioindice that computes radiometric +indices on raster images: ndvi, ndwi, etc.. +""" +import logging +import logging.config +import os +from pathlib import Path +from typing import List +import threading + +import rasterio +import numpy.ma as ma +from tqdm import tqdm + +from eolab.georastertools import utils +from eolab.georastertools import Rastertool, Windowable +from eolab.georastertools.processing import algo +from eolab.georastertools.processing import RadioindiceProcessing +from eolab.georastertools.product import BandChannel, RasterProduct + + +_logger = logging.getLogger(__name__) + + +class Radioindice(Rastertool, Windowable): + """Raster tool that computes radiometric indices of a raster product. + + If several indices are requested, the tool can generate one output image with one + band per indice (merge=True), or it can generate several images, one image per indice + (merge=False). + + The computation can be realized on a subset of the input image (a Region Of Interest) + defined by a vector file (e.g. shapefile, geojson). + + The radiometric indice is an instance of + :obj:`eolab.georastertools.processing.RadioindiceProcessing` + which defines the list of channels it needs to compute the indice. The input raster product + must be of a recognized raster type so that it is possible to match every channels required by + the indice with an existing band in the raster product. + """ + # Preconfigured radioindices + # Vegetation indices: ndvi + ndvi = RadioindiceProcessing("ndvi").with_channels( + [BandChannel.red, BandChannel.nir]) + """Normalized Difference Vegetation Index (red, nir channels) + + .. math:: + + ndvi = \\frac{nir - red}{nir + red} + + References: + Rouse J.W., Haas R.H., Schell J.A., Deering D.W., 1973. Monitoring vegetation systems in + the great plains with ERTS. Third ERTS Symposium, NASA SP-351. 1:309-317 + + Tucker C.J., 1979. Red and photographic infrared linear combinations for monitoring + vegetation. Remote Sens Environ 8:127-150 + + """ + + # Vegetation indices: tndvi + tndvi = RadioindiceProcessing("tndvi", algo=algo.tndvi).with_channels( + [BandChannel.red, BandChannel.nir]) + """Transformed Normalized Difference Vegetation Index (red, nir channels) + + .. math:: + :nowrap: + + \\begin{eqnarray} + ndvi & = & \\frac{nir - red}{nir + red} \\\\ + tndvi & = & \\sqrt{ndvi + 0.5} + \\end{eqnarray} + + References: + `Deering D.W., Rouse J.W., Haas R.H., and Schell J.A., 1975. Measuring forage production + of grazing units from Landsat MSS data. Pages 1169-1178 In: Cook J.J. (Ed.), Proceedings + of the Tenth International Symposium on Remote Sensing of Environment (Ann Arbor, 1975), + Vol. 2, Ann Arbor, Michigan, USA. `_ + + """ + + # Vegetation indices: rvi + rvi = RadioindiceProcessing("rvi", algo=algo.rvi).with_channels( + [BandChannel.red, BandChannel.nir]) + """Ratio Vegetation Index (red, nir channels) + + .. math:: + + rvi = \\frac{nir}{red} + + References: + `Jordan C.F., 1969. Derivation of leaf area index from quality of light on the forest + floor. Ecology 50:663-666 `_ + """ + + # Vegetation indices: pvi + pvi = RadioindiceProcessing("pvi", algo=algo.pvi).with_channels( + [BandChannel.red, BandChannel.nir]) + """Perpendicular Vegetation Index (red, nir channels) + + .. math:: + + pvi = (nir - 0.90893 * red - 7.46216) * 0.74 + + References: + `Richardson A.J., Wiegand C.L., 1977. Distinguishing vegetation from soil background + information. Photogramm Eng Rem S 43-1541-1552 `_ + """ + + # Vegetation indices: savi + savi = RadioindiceProcessing("savi", algo=algo.savi).with_channels( + [BandChannel.red, BandChannel.nir]) + """Soil Adjusted Vegetation Index (red, nir channels) + + .. math:: + + savi = \\frac{(nir - red) * (1. + 0.5)}{nir + red + 0.5} + + References: + `Huete A.R., 1988. A soil-adjusted vegetation index (SAVI). Remote Sens Environ 25:295-309 `_ + """ + + # Vegetation indices: tsavi + tsavi = RadioindiceProcessing("tsavi", algo=algo.tsavi).with_channels( + [BandChannel.red, BandChannel.nir]) + """Transformed Soil Adjusted Vegetation Index (red, nir channels) + + .. math:: + + tsavi = \\frac{0.7 * (nir - 0.7 * red - 0.9)}{0.7 * nir + red + 0.08 * (1 + 0.7^2)} + + References: + `Baret F., Guyot G., Major D., 1989. TSAVI: a vegetation index which minimizes soil + brightness effects on LAI or APAR estimation. 12th Canadian Symposium on Remote + Sensing and IGARSS 1990, Vancouver, Canada, 07/10-14. `_ + """ + + # Vegetation indices: msavi + msavi = RadioindiceProcessing("msavi", algo=algo.msavi).with_channels( + [BandChannel.red, BandChannel.nir]) + """Modified Soil Adjusted Vegetation Index (red, nir channels) + + .. math:: + :nowrap: + + \\begin{eqnarray} + wdvi & = & nir - 0.4 * red \\\\ + ndvi & = & \\frac{nir - red}{nir + red} \\\\ + L & = & 1 - 2 * 0.4 * ndvi * wdvi \\\\ + msavi & = & \\frac{(nir - red) * (1 + L)}{nir + red + L} + \\end{eqnarray} + + References: + `Qi J., Chehbouni A., Huete A.R., Kerr Y.H., 1994. Modified Soil Adjusted Vegetation + Index (MSAVI). Remote Sens Environ 48:119-126 `_ + + `Qi J., Kerr Y., Chehbouni A., 1994. External factor consideration in vegetation index + development. Proc. of Physical Measurements and Signatures in Remote Sensing, + ISPRS, 723-730. `_ + """ + + # Vegetation indices: msavi2 + msavi2 = RadioindiceProcessing("msavi2", algo=algo.msavi2).with_channels( + [BandChannel.red, BandChannel.nir]) + """Modified Soil Adjusted Vegetation Index (red, nir channels) + + .. math:: + :nowrap: + + \\begin{eqnarray} + val & = & (2 * nir + 1)^2 - 8 * (nir - red) \\\\ + msavi2 & = & (2 * nir + 1 - \\sqrt{val}) / 2 + \\end{eqnarray} + """ + + # Vegetation indices: ipvi + ipvi = RadioindiceProcessing("ipvi", algo=algo.ipvi).with_channels( + [BandChannel.red, BandChannel.nir]) + """Infrared Percentage Vegetation Index (red, nir channels) + + .. math:: + ipvi = \\frac{nir}{nir + red} + + References: + `Crippen, R. E. 1990. Calculating the Vegetation Index Faster, Remote Sensing of + Environment, vol 34., pp. 71-73. `_ + """ + + # Vegetation indices: evi + evi = RadioindiceProcessing("evi", algo=algo.evi).with_channels( + [BandChannel.red, BandChannel.nir, BandChannel.blue]) + """Enhanced vegetation index (red, nir, blue channels) + + .. math:: + evi = \\frac{2.5 * (nir - red)}{nir + 6.0 * red - 7.5 * blue + 1.0} + + """ + + # Water indices: ndwi + ndwi = RadioindiceProcessing("ndwi").with_channels( + [BandChannel.mir, BandChannel.nir]) + """Normalized Difference Water Index (mir, nir channels) + + .. math:: + + ndwi = \\frac{nir - mir}{nir + mir} + + References: + Gao, B. C., 1996. NDWI - A normalized difference water index for remote sensing + of vegetation liquid water from space. Remote Sensing of Environment 58, 257-266. + """ + + # Water indices: ndwi2 + ndwi2 = RadioindiceProcessing("ndwi2").with_channels( + [BandChannel.nir, BandChannel.green]) + """Normalized Difference Water Index (nir, green channels) + + .. math:: + + ndwi2 = \\frac{green - nir}{green + nir} + """ + + # Water indices: mdwi + mndwi = RadioindiceProcessing("mndwi").with_channels( + [BandChannel.mir, BandChannel.green]) + """Modified Normalized Difference Water Index (green, mir channels) + + .. math:: + + mndwi = \\frac{green - mir}{green + mir} + + References: + Xu, H. Q., 2006. Modification of normalised difference water index (NDWI) to enhance + open water features in remotely sensed imagery. International Journal of Remote Sensing + 27, 3025-3033 + + """ + + # Water indices: ndpi + ndpi = RadioindiceProcessing("ndpi").with_channels( + [BandChannel.green, BandChannel.mir]) + """Normalized Difference Pond Index (green, mir channels) + + .. math:: + + ndpi = \\frac{mir - green}{mir + green} + + References: + J-P. Lacaux, Y. M. Tourre, C. Vignolle, J-A. Ndione, and M. Lafaye, "Classification + of Ponds from High-Spatial Resolution Remote Sensing: Application to Rift Valley Fever + Epidemics in Senegal," Remote Sensing of Environment 106 66–74, Elsevier Publishers: 2007 + """ + + # Water indices: ndti + ndti = RadioindiceProcessing("ndti").with_channels( + [BandChannel.green, BandChannel.red]) + """Normalized Difference Turbidity Index (green, red channels) + + .. math:: + + ndti = \\frac{red - green}{red + green} + + References: + J-P. Lacaux, Y. M. Tourre, C. Vignolle, J-A. Ndione, and M. Lafaye, "Classification + of Ponds from High-Spatial Resolution Remote Sensing: Application to Rift Valley Fever + Epidemics in Senegal," Remote Sensing of Environment 106 66–74, Elsevier Publishers: 2007 + """ + + # urban indices: ndbi + ndbi = RadioindiceProcessing("ndbi").with_channels( + [BandChannel.nir, BandChannel.mir]) + """Normalized Difference Built Up Index (nir, mir channels) + + .. math:: + + ndbi = \\frac{mir - nir}{mir + nir} + + """ + + # Soil indices: ri + ri = RadioindiceProcessing("ri", algo=algo.redness_index).with_channels( + [BandChannel.red, BandChannel.green]) + """Redness index (red, green channels) + + .. math:: + + ri = \\frac{red^2}{green^3} + + """ + + # Soil indices: bi + bi = RadioindiceProcessing("bi", algo=algo.brightness_index).with_channels( + [BandChannel.red, BandChannel.green]) + """Brightness index (red, green channels) + + .. math:: + + bi = \\frac{red^2 + green^2}{2} + + """ + + # Soil indices: bi2 + bi2 = RadioindiceProcessing("bi2", algo=algo.brightness_index2).with_channels( + [BandChannel.nir, BandChannel.red, BandChannel.green]) + """Brightness index (nir, red, green channels) + + .. math:: + + bi2 = \\frac{nir^2 + red^2 + green^2}{3} + + """ + + @staticmethod + def get_default_indices(): + """Get the list of predefined radiometric indices + + Returns: + [:obj:`eolab.georastertools.processing.RadioindiceProcessing`]: list of + predefined radioindice. + """ + # returns all predefined radiometric indices + return [ + Radioindice.ndvi, Radioindice.tndvi, Radioindice.rvi, Radioindice.pvi, + Radioindice.savi, Radioindice.tsavi, Radioindice.msavi, Radioindice.msavi2, + Radioindice.ipvi, Radioindice.evi, Radioindice.ndwi, Radioindice.ndwi2, + Radioindice.mndwi, Radioindice.ndpi, Radioindice.ndti, Radioindice.ndbi, + Radioindice.ri, Radioindice.bi, Radioindice.bi2 + ] + + def __init__(self, indices: List[RadioindiceProcessing]): + """ Constructor + + Args: + indices ([:obj:`eolab.georastertools.processing.RadioindiceProcessing`]): + List of indices to compute (class Indice) + """ + super().__init__() + self.with_windows() + + self._indices = indices + self._merge = False + self._roi = None + + @property + def indices(self) -> List[RadioindiceProcessing]: + """List of radiometric indices to compute""" + return self._indices + + @property + def merge(self) -> bool: + """If true, all indices are in the same output image (one band per indice). + Otherwise, each indice is in its own output image.""" + return self._merge + + @property + def roi(self) -> str: + """Filename of the vector data defining the ROI""" + return self._roi + + def with_output(self, outputdir: str = ".", merge: bool = False): + """Set up the output. + + Args: + outputdir (str, optional, default="."): + Output dir where to store results. If none, it is set to current dir + merge (bool, optional, default=False): + Whether to merge all indices in the same image (i.e. one band per indice) + + Returns: + :obj:`eolab.georastertools.Radioindice`: The current instance so that it is + possible to chain the with... calls (fluent API) + """ + super().with_output(outputdir) + self._merge = merge + return self + + def with_roi(self, roi: str): + """Set up the region of interest + + Args: + roi (str): + Filename of the vector data defining the ROI + (output images will be cropped to the geometry) + + Returns: + :obj:`eolab.georastertools.Radioindice`: The current instance so that it is + possible to chain the with... calls (fluent API) + """ + self._roi = roi + + def process_file(self, inputfile: str) -> List[str]: + """Compute the indices for a single file + + Args: + inputfile (str): + Input image to process + + Returns: + [str]: List of indice images (posix paths) that have been generated + """ + _logger.info(f"Processing file {inputfile}") + + outdir = Path(self.outputdir) + + # STEP 1: Prepare the input image so that it can be processed + with RasterProduct(inputfile, vrt_outputdir=self.vrt_dir) as product: + _logger.debug(f"Raster product is : {product}") + + if product.rastertype is None: + raise ValueError("Unsupported input file, no matching raster type " + "identified to handle the file") + else: + filename = utils.to_path(inputfile).name + _logger.info(f"Raster type of image {filename} is {product.rastertype.name}") + + # check if all indices can be computed for this raster + indices = list() + for indice in self.indices: + # check if the rastertype has all channels + if not(product.rastertype.has_channels(indice.channels)): + _logger.error(f"Can not compute {indice} for {filename}: " + "raster product does not contain all required bands.") + else: + # indice is valid, add it to the list of indices to compute + indices.append(indice) + + # get the raster + raster = product.get_raster(roi=self.roi) + + # STEP 2: Compute the indices + outputs = [] + if self.merge: + # merge is True, compute all indices and generate a single image + _logger.info(f"Compute indices: {' '.join(indice.name for indice in indices)}") + indice_image = outdir.joinpath(f"{utils.get_basename(inputfile)}-indices.tif") + compute_indices(raster, product.channels, indice_image.as_posix(), + indices, self.window_size) + outputs.append(indice_image.as_posix()) + else: + # merge is False, compute all indices and generate one image per indice + for i, indice in enumerate(indices): + _logger.info(f"Compute {indice.name}") + indice_image = outdir.joinpath( + f"{utils.get_basename(inputfile)}-{indice.name}.tif") + compute_indices(raster, product.channels, indice_image.as_posix(), + [indice], self.window_size) + outputs.append(indice_image.as_posix()) + + # return the list of generated files + return outputs + +def compute_indices(input_image: str, image_channels: List[BandChannel], + indice_image: str, indices: List[RadioindiceProcessing], + window_size: tuple = (1024, 1024)): + """ + Compute the indices on the input image and produce a multiple bands + image (one band per indice) + + The possible indices are the following : + ndvi, tndvi, rvi, pvi, savi, tsavi, msavi, msavi2, ipvi, evi, ndwi, ndwi2, mndwi, ndpi, ndti, ndbi, ri, bi, bi2 + + Args: + input_image (str): + Path of the raster to compute + image_channels ([:obj:`eolab.georastertools.product.BandChannel`]): + Ordered list of bands in the raster + indice_image (str): + Path of the output raster image + indices ([:obj:`eolab.georastertools.processing.RadioindiceProcessing`]): + List of indices to compute + window_size (tuple(int, int), optional, default=(1024, 1024)): + Size of windows for splitting the processed image in small parts + """ + with rasterio.Env(GDAL_VRT_ENABLE_PYTHON=True): + src = rasterio.open(input_image) + profile = src.profile + + # set block size to the configured window_size of first indice + blockxsize, blockysize = window_size + if src.width < blockxsize: + blockxsize = utils.highest_power_of_2(src.width) + if src.height < blockysize: + blockysize = utils.highest_power_of_2(src.height) + + # dtype of output data + dtype = indices[0].dtype or rasterio.float32 + + # setup profile for output image + profile.update(driver='GTiff', + blockxsize=blockysize, blockysize=blockxsize, tiled=True, + dtype=dtype, nodata=indices[0].nodata, + count=len(indices)) + + with rasterio.open(indice_image, "w", **profile) as dst: + # Materialize a list of destination block windows + windows = [window for ij, window in dst.block_windows()] + + # disable status of tqdm progress bar + disable = os.getenv("RASTERTOOLS_NOTQDM", 'False').lower() in ['true', '1'] + + # Dictionary to store statistics for each band + band_stats = {i: {"min": float('inf'), "max": float('-inf'), "sum": 0, "total_pix": 0, "count": 0} + for i in range(1, len(indices) + 1)} + + # compute every indices + for i, indice in enumerate(indices, 1): + # Get the bands necessary to compute the indice + bands = [image_channels.index(channel) + 1 for channel in indice.channels] + + read_lock = threading.Lock() + write_lock = threading.Lock() + + def process(window): + """Read input raster, compute indice and write output raster""" + with read_lock: + src_array = src.read(bands, window=window, masked=True) + src_array[src_array == src.nodata] = ma.masked + src_array = src_array.astype(dtype) + + # The computation can be performed concurrently + result = indice.algo(src_array).astype(dtype).filled(indice.nodata) + + # Update statistics + valid_pixels = result[result != indice.nodata] + if valid_pixels.size > 0: + band_stats[i]["min"] = min(band_stats[i]["min"], valid_pixels.min()) + band_stats[i]["max"] = max(band_stats[i]["max"], valid_pixels.max()) + band_stats[i]["sum"] += valid_pixels.sum() + band_stats[i]["total_pix"] += result.size + band_stats[i]["count"] += valid_pixels.size + + with write_lock: + dst.write_band(i, result, window=window) + + # compute using concurrent.futures.ThreadPoolExecutor and tqdm + for window in tqdm(windows, disable=disable, desc=f"{indice.name}"): + process(window) + + dst.set_band_description(i, indice.name) + + # Compute and set metadata tags + for i, stats in band_stats.items(): + # Compute and set metadata tags + mean = stats["sum"] / stats["count"] + sum_sq = (stats["sum"] - mean * stats["count"]) ** 2 + variance = sum_sq / stats["count"] + stddev = variance ** 0.5 if variance > 0 else 0 + + dst.update_tags(i, + STATISTICS_MINIMUM=f"{stats['min']:.14g}", + STATISTICS_MAXIMUM=f"{stats['max']:.14g}", + STATISTICS_MEAN=mean, + STATISTICS_STDDEV=stddev, + STATISTICS_VALID_PERCENT=(stats["count"] / stats["total_pix"] * 100)) + diff --git a/src/eolab/rastertools/speed.py b/src/eolab/georastertools/speed.py similarity index 96% rename from src/eolab/rastertools/speed.py rename to src/eolab/georastertools/speed.py index 7a79b27..74829c4 100644 --- a/src/eolab/rastertools/speed.py +++ b/src/eolab/georastertools/speed.py @@ -14,10 +14,10 @@ import rasterio from tqdm.contrib.concurrent import thread_map -from eolab.rastertools import utils -from eolab.rastertools import Rastertool -from eolab.rastertools.processing import algo -from eolab.rastertools.product import RasterProduct +from eolab.georastertools import utils +from eolab.georastertools import Rastertool +from eolab.georastertools.processing import algo +from eolab.georastertools.product import RasterProduct _logger = logging.getLogger(__name__) @@ -113,9 +113,9 @@ def compute_speed(date0: datetime, date1: datetime, Date of the first dataset date1 (:obj:`datetime.datetime`): Date of the second dataset - product0 (:obj:`eolab.rastertools.product.RasterProduct`): + product0 (:obj:`eolab.georastertools.product.RasterProduct`): Path of the first raster image - product1 (:obj:`eolab.rastertools.product.RasterProduct`): + product1 (:obj:`eolab.georastertools.product.RasterProduct`): Path of the second raster image speed_image (str): Path of the output image diff --git a/src/eolab/rastertools/svf.py b/src/eolab/georastertools/svf.py similarity index 96% rename from src/eolab/rastertools/svf.py rename to src/eolab/georastertools/svf.py index 0644c68..88e8138 100644 --- a/src/eolab/rastertools/svf.py +++ b/src/eolab/georastertools/svf.py @@ -8,10 +8,10 @@ from pathlib import Path import numpy as np -from eolab.rastertools import utils -from eolab.rastertools import Rastertool, Windowable -from eolab.rastertools.processing import algo -from eolab.rastertools.processing import RasterProcessing, compute_sliding +from eolab.georastertools import utils +from eolab.georastertools import Rastertool, Windowable +from eolab.georastertools.processing import algo +from eolab.georastertools.processing import RasterProcessing, compute_sliding _logger = logging.getLogger(__name__) diff --git a/src/eolab/rastertools/tiling.py b/src/eolab/georastertools/tiling.py similarity index 96% rename from src/eolab/rastertools/tiling.py rename to src/eolab/georastertools/tiling.py index f6061a2..f0f1b8f 100644 --- a/src/eolab/rastertools/tiling.py +++ b/src/eolab/georastertools/tiling.py @@ -13,10 +13,10 @@ import geopandas as gpd import sys -from eolab.rastertools import utils -from eolab.rastertools import Rastertool, RastertoolConfigurationException -from eolab.rastertools.processing import vector -from eolab.rastertools.product import RasterProduct +from eolab.georastertools import utils +from eolab.georastertools import Rastertool, RastertoolConfigurationException +from eolab.georastertools.processing import vector +from eolab.georastertools.product import RasterProduct _logger = logging.getLogger(__name__) @@ -94,7 +94,7 @@ def with_output(self, outputdir: str = ".", output_basename: str = "{}_tile{}", in the outputdir. Returns: - :obj:`eolab.rastertools.Tiling`: The current instance so that it is + :obj:`eolab.georastertools.Tiling`: The current instance so that it is possible to chain the with... calls (fluent API) """ # Test if output repository is valid @@ -113,7 +113,7 @@ def with_id_column(self, id_column: str, ids: List[int]): List of ids values for which tiles shall be generated. Returns: - :obj:`eolab.rastertools.Tiling`: The current instance so that it is + :obj:`eolab.georastertools.Tiling`: The current instance so that it is possible to chain the with... calls (fluent API) """ diff --git a/src/eolab/rastertools/timeseries.py b/src/eolab/georastertools/timeseries.py similarity index 97% rename from src/eolab/rastertools/timeseries.py rename to src/eolab/georastertools/timeseries.py index 94014eb..5d2d7bf 100644 --- a/src/eolab/rastertools/timeseries.py +++ b/src/eolab/georastertools/timeseries.py @@ -18,10 +18,10 @@ import rasterio from tqdm.contrib.concurrent import process_map -from eolab.rastertools import utils -from eolab.rastertools import Rastertool, Windowable -from eolab.rastertools.processing import algo -from eolab.rastertools.product import RasterProduct +from eolab.georastertools import utils +from eolab.georastertools import Rastertool, Windowable +from eolab.georastertools.processing import algo +from eolab.georastertools.product import RasterProduct _logger = logging.getLogger(__name__) @@ -146,7 +146,7 @@ def compute_timeseries(products_per_date: Dict[float, RasterProduct], timeseries """Generate the timeseries Args: - products_per_date (dict[float, :obj:`eolab.rastertools.product.RasterProduct`]): + products_per_date (dict[float, :obj:`eolab.georastertools.product.RasterProduct`]): List of input images indexed by their timestamp timeseries_dates ([float]: List of dates (timestamps) in the requested timeseries diff --git a/src/eolab/rastertools/utils.py b/src/eolab/georastertools/utils.py similarity index 100% rename from src/eolab/rastertools/utils.py rename to src/eolab/georastertools/utils.py diff --git a/src/eolab/rastertools/zonalstats.py b/src/eolab/georastertools/zonalstats.py similarity index 94% rename from src/eolab/rastertools/zonalstats.py rename to src/eolab/georastertools/zonalstats.py index 12847ab..017f7ed 100644 --- a/src/eolab/rastertools/zonalstats.py +++ b/src/eolab/georastertools/zonalstats.py @@ -1,618 +1,618 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -This module defines a rastertool named zonalstats that compute several statistics -(mean, median, std dev, etc) on one or several bands of a raster image. The statistics -can be computed on the whole image or by zones defined by a vector file (e.g. shapefile, -geojson). - -Several options are provided: - -* compute outliers: enable to generate an image that emphasizes the outliers pixels - (i.e. pixels with values greater that mean + n x stddev where n can be parametrized). -* generate a chart: if several raster images are computed and if we can extract a date from the - filenames (because the raster is of a known type), generate one chart - per statistics (x=time, y=stats) - -""" -from typing import List, Dict -import datetime -import logging.config -from pathlib import Path -import json -import numpy as np -import geopandas as gpd -import sys - -import rasterio - -from eolab.rastertools import utils -from eolab.rastertools import Rastertool, RastertoolConfigurationException -from eolab.rastertools.processing import compute_zonal_stats, compute_zonal_stats_per_category -from eolab.rastertools.processing import extract_zonal_outliers, plot_stats -from eolab.rastertools.processing import vector -from eolab.rastertools.product import RasterProduct - - -_logger = logging.getLogger(__name__) - - -class Zonalstats(Rastertool): - """ - Raster tool that computes zonal statistics of a raster product. - """ - - supported_output_formats = { - 'ESRI Shapefile': 'shp', - 'GeoJSON': 'geojson', - 'CSV': 'csv', - 'GPKG': 'gpkg', - 'GML': 'gml' - } - """List of all possible output format are provided by fiona.supported_drivers.keys()""" - - valid_stats = ['count', 'valid', 'nodata', - 'min', 'max', 'mean', 'std', 'sum', 'median', "mad", 'range', - 'majority', 'minority', 'unique'] - """List of stats that can be computed. In addition to this list, "percentile_xx" is - also a valid stat where xx is the percentile value, e.g. percentile_70""" - - def __init__(self, stats: List[str], categorical: bool = False, valid_threshold: float = 0.0, - area: bool = False, prefix: str = None, bands: List[int] = [1]): - """ Constructor - - Args: - stats ([str]): - List of stats to compute. Zonalstats.valid_stats defined the list of valid stats - except percentile which can be defined as string concatenating percentile\\_ with - the percentile value (from 0 to 100). - categorical (bool, optional, default=False): - If true and input raster is "categorical", add the counts of every unique - raster values to the stats - valid_threshold (float, optional, default=0.0): - Minimum percentage of valid pixels in a shape to compute its statistics ([0.0, 1.0]) - area (bool, optional, default=False): - If true, statistics are multiplied by the pixel area of the raster input - prefix (str, optional, default=None]): - Add a prefix to the stats keys, one prefix per band. The argument is a string - with all prefixes separated by a space. - bands ([int], optional, default=[1]): - List of bands in the input image to process. - Set None if all bands shall be processed. - """ - super().__init__() - - self._stats = stats - self._categorical = categorical - value = valid_threshold or 0.0 - if value < 0.0 or value > 1.0: - raise RastertoolConfigurationException( - f"Valid threshold must be in range [0.0, 1.0].") - if value > 1e-5 and "valid" not in stats: - raise RastertoolConfigurationException( - "Cannot apply a valid threshold when the computation " - "of the valid stat has not been requested.") - self._valid_threshold = value - self._area = area - self._prefix = prefix.split() if prefix else None - self.__check_stats() - - self._bands = bands - - self._output_format = "ESRI Shapefile" - - self._geometries = None - self._within = False - - self._sigma = None - - self._chart_file = None - self._geometry_index = 'ID' - self._display_chart = False - - self._category_file = None - self._category_file_type = None - self._category_index = None - self._category_labels = None - - self._generated_stats = list() - self._generated_stats_dates = list() - - @property - def generated_stats_per_date(self): - """After processing one or several files, this method enables to retrieve a dictionary - that contains the statistics for each inputfile's date: - - - keys are timestamps - - values are the statistics at the corresponding timestam - - Warning: - When the timestamp of the input raster cannot be retrieved, the dictionary does not - contain the generated statistics for this input raster. In this case, prefer calling - generated_stats to get the stats as a list (one item per input file). - """ - out = dict() - if len(self._generated_stats_dates) > 0: - out = {date: stats - for (date, stats) in zip(self.generated_stats_dates, self.generated_stats)} - return out - - @property - def generated_stats(self): - """The list of generated stats in the same order as the input files""" - return self._generated_stats - - @property - def generated_stats_dates(self): - """The list of dates when they can be extracted from the input files' names""" - return self._generated_stats_dates - - @property - def stats(self) -> List[str]: - """List of stats to compute""" - return self._stats - - @property - def categorical(self) -> bool: - """Whether to compute the counts of every unique pixel values""" - return self._categorical - - @property - def valid_threshold(self) -> float: - """Minimum percentage of valid pixels in a shape to compute its statistics""" - return self._valid_threshold - - @property - def area(self) -> bool: - """Whether to compute the statistics multiplied by the pixel area""" - return self._area - - @property - def prefix(self) -> str: - """Prefix of the features stats (one per band)""" - return self._prefix - - @property - def bands(self) -> List[int]: - """List of bands to process""" - return self._bands - - @property - def output_format(self) -> str: - """Output format for the features stats""" - return self._output_format - - @property - def geometries(self) -> str: - """The geometries where to compute zonal statistics""" - return self._geometries - - @property - def within(self) -> bool: - """Whether to compute stats for geometries within the raster (if False, stats for - all geometries intersecting the raster are computed)""" - return self._within - - @property - def sigma(self) -> float: - """Number of sigmas for identifying outliers""" - return self._sigma - - @property - def chart_file(self) -> str: - """Name of the chart file to generate""" - return self._chart_file - - @property - def geometry_index(self) -> str: - """The column name identifying the name of the geometry""" - return self._geometry_index - - @property - def display_chart(self) -> bool: - """Whether to display the chart""" - return self._display_chart - - @property - def category_file(self) -> str: - """Filename containing the categories when computing stats per - categories in the geometries""" - return self._category_file - - @property - def category_file_type(self) -> str: - """Type of the category file, either 'raster' or 'vector'""" - return self._category_file_type - - @property - def category_index(self) -> str: - """Column name identifying categories in categroy_file (only if file format - is geometries) - """ - return self._category_index - - @property - def category_labels(self) -> str: - """Dict with classes index as keys and names to display as values""" - return self._category_labels - - def __check_stats(self): - """Check that the requested stats are valid. - - Args: - stats_to_compute ([str]): - List of stats to compute - """ - for x in self._stats: - # check percentile format - if x.startswith("percentile_"): - q = float(x.replace("percentile_", '')) - # percentile must be in range [0, 100] - if q > 100.0: - raise RastertoolConfigurationException('percentiles must be <= 100') - if q < 0.0: - raise RastertoolConfigurationException('percentiles must be >= 0') - - elif x not in Zonalstats.valid_stats: - raise RastertoolConfigurationException( - f"Invalid stat {x}: must be " - f"percentile_xxx or one of {Zonalstats.valid_stats}") - - def with_output(self, outputdir: str = ".", output_format: str = "ESRI Shapefile"): - """Set up the output. - - Args: - outputdir (str, optional, default="."): - Output dir where to store results. If none, results are not dumped to a file. - output_format (str, optional, default="ESRI Shapefile"): - Format of the output 'ESRI Shapefile', 'GeoJSON', 'CSV', 'GPKG', 'GML' - (see supported_output_formats). If None, it is set to ESRI Shapefile - - Returns: - :obj:`eolab.rastertools.Zonalstats`: the current instance so that it is - possible to chain the with... calls (fluent API) - """ - super().with_output(outputdir) - self._output_format = output_format or 'ESRI Shapefile' - # check if output_format exists - if self._output_format not in Zonalstats.supported_output_formats: - _logger.exception(RastertoolConfigurationException( - f"Unrecognized output format {output_format}. " - f"Possible values are {', '.join(Zonalstats.supported_output_formats)}")) - sys.exit(2) - return self - - def with_geometries(self, geometries: str, within: bool = False): - """Set up the geometries where to compute stats. - - Args: - geometries (str): - Name of the file containing the geometries where to compute zonal stats. If not set, - stats are computed on the whole raster image - within (bool, optional, default=False): - Whether to compute stats only for geometries within the raster. If False, - statistics are computed for geometries that intersect the raster shape. - - Returns: - :obj:`eolab.rastertools.Zonalstats`: the current instance so that it is - possible to chain the with... calls (fluent API) - """ - self._geometries = geometries - self._within = within - return self - - def with_outliers(self, sigma: float): - """Set up the computation of outliers - - Args: - sigma (float): - Distance to the mean value to consider a pixel as an outlier (expressed - in sigma, e.g. the value 2 means that pixels values greater than - mean value + 2 * std are outliers) - - Returns: - :obj:`eolab.rastertools.Zonalstats`: the current instance so that it is - possible to chain the with... calls (fluent API) - """ - # Manage sigma computation option that requires mean + std dev computation - if "mean" not in self._stats: - self._stats.append("mean") - if "std" not in self._stats: - self._stats.append("std") - self._sigma = sigma - return self - - def with_chart(self, chart_file: str = None, geometry_index: str = 'ID', display: bool = False): - """Set up the charting capability - - Args: - chart_file (str, optional, default=None): - If not None, generate a chart with the statistics and saves it to str - geometry_index (str, optional, default='ID'): - Name of the index in the geometry file - display (bool, optional, default=False): - If true, display the chart with the statistics - - Returns: - :obj:`eolab.rastertools.Zonalstats`: the current instance so that it is - possible to chain the with... calls (fluent API) - """ - self._chart_file = chart_file - self._geometry_index = geometry_index - self._display_chart = display - return self - - def with_per_category(self, category_file: str, category_index: str = 'Classe', - category_labels_json: str = None): - """Set up the zonal stats computation per categories - - Args: - category_file (str): - Name of the file containing the categories - category_index (str, optional, default='Classe'): - Name of column containing the category value (if category_file is a vector) - category_labels_json (str, optional, default=None): - Name of json file containing the dict that associates category values - to category names - - Returns: - :obj:`eolab.rastertools.Zonalstats`: the current instance so that it is - possible to chain the with... calls (fluent API) - """ - self._category_file = category_file - self._category_index = category_index - # get the category file type - if category_file: - suffix = utils.get_suffixes(category_file) - if suffix[1:] in Zonalstats.supported_output_formats.values(): - self._category_file_type = "vector" - else: - # not a vector, maybe a raster? try to open it with rasterio - try: - rasterio.open(category_file) - except IOError: - raise RastertoolConfigurationException( - f"File {category_file} cannot be read: check format and existence") - self._category_file_type = "raster" - # get the dict of category labels - if category_labels_json: - try: - with open(category_labels_json) as f: - self._category_labels = json.load(f) - except Exception as err: - raise RastertoolConfigurationException( - f"File {category_labels_json} does not contain a valid dict.") from err - - return self - - def process_file(self, inputfile: str) -> List[str]: - """Compute the stats for a single input file - - Args: - inputfile (str): - Input image to process - - Returns: - [str]: List of generated statistical images (posix paths) that have been generated - """ - _logger.info(f"Processing file {inputfile}") - - # STEP 1: Prepare the input image so that it can be processed - _logger.info("Prepare the input for computation") - with RasterProduct(inputfile, vrt_outputdir=self.vrt_dir) as product: - - if product.rastertype is None and self.chart_file: - _logger.error("Unrecognized raster type of input file," - " cannot extract date for plotting") - - # open raster to get metadata - raster = product.get_raster() - - rst = rasterio.open(raster) - bound = int(rst.count) - indexes = rst.indexes - descr = rst.descriptions - - geotransform = rst.get_transform() - width = np.abs(geotransform[1]) - height = np.abs(geotransform[5]) - area_square_meter = width * height - rst.close() - - date_str = product.get_date_string('%Y%m%d-%H%M%S') - - # check band index and handle all bands options (when bands is None) - if self.bands is None or len(self.bands) == 0: - bands = indexes - else: - bands = self.bands - if min(bands) < 1 or max(bands) > bound: - raise ValueError(f"Invalid bands, all values are not in range [1, {bound}]") - - # check the prefix - if self.prefix and len(self.prefix) != len(bands): - raise ValueError("Number of prefix does not equal the number of bands.") - - # STEP 2: Prepare the geometries where to compute zonal stats - if self.geometries: - # reproject & filter input geometries to fit the raster extent - geometries = vector.reproject( - vector.filter(self.geometries, raster, self.within), raster) - else: - # if no geometry is defined, get the geometry from raster shape - geometries = vector.get_raster_shape(raster) - - # STEP 3: Compute the statistics - geom_stats = self.compute_stats(raster, bands, geometries, - descr, date_str, area_square_meter) - - self._generated_stats.append(geom_stats) - if date_str: - timestamp = datetime.datetime.strptime(date_str, '%Y%m%d-%H%M%S') - self._generated_stats_dates.append(timestamp) - - # STEP 4: Generate outputs - outputs = [] - if self.outputdir: - outdir = Path(self.outputdir) - ext = Zonalstats.supported_output_formats[self.output_format] - outputname = f"{utils.get_basename(inputfile)}-stats.{ext}" - outputfile = outdir.joinpath(outputname) - geom_stats.to_file(outputfile.as_posix(), driver=self.output_format) - outputs.append(outputfile.as_posix()) - - # if sigma is not None, generate the outliers image - if self.sigma: - _logger.info("Extract outliers") - outliersfile = outdir.joinpath( - f"{utils.get_basename(inputfile)}-stats-outliers.tif") - extract_zonal_outliers(geom_stats, raster, outliersfile.as_posix(), - prefix=self.prefix or [""] * len(bands), - bands=bands, sigma=self.sigma) - outputs.append(outliersfile.as_posix()) - - return outputs - - def postprocess_files(self, inputfiles: List[str], outputfiles: List[str]) -> List[str]: - """Generate the chart if requested after computing stats for each input file - - Args: - inputfiles ([str]): Input images to process - outputfiles ([str]): List of generated files after executing the - rastertool on the input files - - Returns: - [str]: A list containing the chart file if requested - """ - additional_outputs = [] - if self.chart_file and len(self.generated_stats_per_date) > 0: - _logger.info("Generating chart") - plot_stats(self.chart_file, self.generated_stats_per_date, - self.stats, self.geometry_index, self.display_chart) - additional_outputs.append(self.chart_file) - - return additional_outputs - - def compute_stats(self, raster: str, bands: List[int], - geometries: gpd.GeoDataFrame, - descr: List[str], date: str, - area_square_meter: int) -> List[List[Dict[str, float]]]: - """Compute the statistics of the input data. [Minimum, Maximum, Mean, Standard deviation] - - Args: - raster (str): - Input image to process - bands ([int]): - List of bands in the input image to process. Empty list means all bands - geometries (GeoDataFrame): - Geometries where to add statistics (geometries must be in the same - projection as the raster) - descr ([str]): - Band descriptions - date (str): - Timestamp of the input raster - area_square_meter (int): - Area represented by a pixel - - Returns: - list[list[dict]] - The dictionnary associates the name of the statistics to its value. - """ - _logger.info("Compute statistics") - # Compute zonal statistics - if self.category_file is not None: - # prepare the categories data - if self.category_file_type == "vector": - # clip categories to the raster bounds and reproject in the raster crs - class_geom = vector.reproject( - vector.clip(self.category_file, raster), - raster) - else: # filetype is raster - # vectorize the raster and reproject in the raster crs - class_geom = vector.reproject( - vector.vectorize(self.category_file, raster, self.category_index), - raster) - - # compute the statistics per category - statistics = compute_zonal_stats_per_category( - geometries, raster, - bands=bands, - stats=self.stats, - categories=class_geom, - category_index=self.category_index, - category_labels=self.category_labels) - else: - statistics = compute_zonal_stats( - geometries, raster, - bands=bands, - stats=self.stats, - categorical=self.categorical) - - # apply area - if self.area: - [d.update({key: area_square_meter * val}) - for s in statistics - for d in s for key, val in d.items() if not np.isnan(val)] - - # convert statistics to GeoDataFrame - geom_stats = self.__stats_to_geoms(statistics, geometries, bands, descr, date) - return geom_stats - - def __stats_to_geoms(self, statistics_data: List[List[Dict[str, float]]], - geometries: gpd.GeoDataFrame, - bands: List[int], descr: List[str], date: str) -> gpd.GeoDataFrame: - """Appends statistics to the geodataframe. - - Args: - statistics_data: - A list of list of dictionnaries. Dict associates the stat names and the stat values. - geometries (GeoDataFrame): - Geometries where to add statistics - bands ([int]): - List of bands in the input image to process. Empty list means all bands - descr ([str]): - Bands descriptions to add to global metadata - date (str): - Date of raster to add to global metadata - - Returns: - GeoDataFrame: The updated geometries with statistics saved in metadata of - the following form: b{band_number}.{metadata_name} where metadata_name is - successively the band name, the date and the statistics names (min, mean, max, median, std) - """ - prefix = self.prefix or [""] * len(bands) - for i, band in enumerate(bands): - # add general metadata to geometries - if descr and descr[i]: - geometries[utils.get_metadata_name(band, prefix[i], "name")] = descr[i] - if date: - geometries[utils.get_metadata_name(band, prefix[i], "date")] = date - - # get all statistics names since additional statistics coming from categorical - # option may have been computed - stats = self.stats.copy() - categorical_stats = set() - [categorical_stats.update(s[i].keys()) for s in statistics_data] - - if self.category_file is None: - # remove stats from the categorical stats - # and add the categorical stats to the stats - # remark: this operation seems strange but it ensures that stats are - # in the correct order - categorical_stats -= set(stats) - stats.extend(categorical_stats) - else: - # per_category mode do not compute overall stats. - # So stats is not exended but replaced - stats = categorical_stats - - for stat in stats: - cond = self.valid_threshold < 1e-5 or stat == "valid" - metadataname = utils.get_metadata_name(band, prefix[i], stat) - geometries[metadataname] = [ - s[i][stat] - if stat in s[i] and (cond or s[i]["valid"] > self.valid_threshold) else np.nan - for s in statistics_data - ] - - return geometries +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module defines a rastertool named zonalstats that compute several statistics +(mean, median, std dev, etc) on one or several bands of a raster image. The statistics +can be computed on the whole image or by zones defined by a vector file (e.g. shapefile, +geojson). + +Several options are provided: + +* compute outliers: enable to generate an image that emphasizes the outliers pixels + (i.e. pixels with values greater that mean + n x stddev where n can be parametrized). +* generate a chart: if several raster images are computed and if we can extract a date from the + filenames (because the raster is of a known type), generate one chart + per statistics (x=time, y=stats) + +""" +from typing import List, Dict +import datetime +import logging.config +from pathlib import Path +import json +import numpy as np +import geopandas as gpd +import sys + +import rasterio + +from eolab.georastertools import utils +from eolab.georastertools import Rastertool, RastertoolConfigurationException +from eolab.georastertools.processing import compute_zonal_stats, compute_zonal_stats_per_category +from eolab.georastertools.processing import extract_zonal_outliers, plot_stats +from eolab.georastertools.processing import vector +from eolab.georastertools.product import RasterProduct + + +_logger = logging.getLogger(__name__) + + +class Zonalstats(Rastertool): + """ + Raster tool that computes zonal statistics of a raster product. + """ + + supported_output_formats = { + 'ESRI Shapefile': 'shp', + 'GeoJSON': 'geojson', + 'CSV': 'csv', + 'GPKG': 'gpkg', + 'GML': 'gml' + } + """List of all possible output format are provided by fiona.supported_drivers.keys()""" + + valid_stats = ['count', 'valid', 'nodata', + 'min', 'max', 'mean', 'std', 'sum', 'median', "mad", 'range', + 'majority', 'minority', 'unique'] + """List of stats that can be computed. In addition to this list, "percentile_xx" is + also a valid stat where xx is the percentile value, e.g. percentile_70""" + + def __init__(self, stats: List[str], categorical: bool = False, valid_threshold: float = 0.0, + area: bool = False, prefix: str = None, bands: List[int] = [1]): + """ Constructor + + Args: + stats ([str]): + List of stats to compute. Zonalstats.valid_stats defined the list of valid stats + except percentile which can be defined as string concatenating percentile\\_ with + the percentile value (from 0 to 100). + categorical (bool, optional, default=False): + If true and input raster is "categorical", add the counts of every unique + raster values to the stats + valid_threshold (float, optional, default=0.0): + Minimum percentage of valid pixels in a shape to compute its statistics ([0.0, 1.0]) + area (bool, optional, default=False): + If true, statistics are multiplied by the pixel area of the raster input + prefix (str, optional, default=None]): + Add a prefix to the stats keys, one prefix per band. The argument is a string + with all prefixes separated by a space. + bands ([int], optional, default=[1]): + List of bands in the input image to process. + Set None if all bands shall be processed. + """ + super().__init__() + + self._stats = stats + self._categorical = categorical + value = valid_threshold or 0.0 + if value < 0.0 or value > 1.0: + raise RastertoolConfigurationException( + f"Valid threshold must be in range [0.0, 1.0].") + if value > 1e-5 and "valid" not in stats: + raise RastertoolConfigurationException( + "Cannot apply a valid threshold when the computation " + "of the valid stat has not been requested.") + self._valid_threshold = value + self._area = area + self._prefix = prefix.split() if prefix else None + self.__check_stats() + + self._bands = bands + + self._output_format = "ESRI Shapefile" + + self._geometries = None + self._within = False + + self._sigma = None + + self._chart_file = None + self._geometry_index = 'ID' + self._display_chart = False + + self._category_file = None + self._category_file_type = None + self._category_index = None + self._category_labels = None + + self._generated_stats = list() + self._generated_stats_dates = list() + + @property + def generated_stats_per_date(self): + """After processing one or several files, this method enables to retrieve a dictionary + that contains the statistics for each inputfile's date: + + - keys are timestamps + - values are the statistics at the corresponding timestam + + Warning: + When the timestamp of the input raster cannot be retrieved, the dictionary does not + contain the generated statistics for this input raster. In this case, prefer calling + generated_stats to get the stats as a list (one item per input file). + """ + out = dict() + if len(self._generated_stats_dates) > 0: + out = {date: stats + for (date, stats) in zip(self.generated_stats_dates, self.generated_stats)} + return out + + @property + def generated_stats(self): + """The list of generated stats in the same order as the input files""" + return self._generated_stats + + @property + def generated_stats_dates(self): + """The list of dates when they can be extracted from the input files' names""" + return self._generated_stats_dates + + @property + def stats(self) -> List[str]: + """List of stats to compute""" + return self._stats + + @property + def categorical(self) -> bool: + """Whether to compute the counts of every unique pixel values""" + return self._categorical + + @property + def valid_threshold(self) -> float: + """Minimum percentage of valid pixels in a shape to compute its statistics""" + return self._valid_threshold + + @property + def area(self) -> bool: + """Whether to compute the statistics multiplied by the pixel area""" + return self._area + + @property + def prefix(self) -> str: + """Prefix of the features stats (one per band)""" + return self._prefix + + @property + def bands(self) -> List[int]: + """List of bands to process""" + return self._bands + + @property + def output_format(self) -> str: + """Output format for the features stats""" + return self._output_format + + @property + def geometries(self) -> str: + """The geometries where to compute zonal statistics""" + return self._geometries + + @property + def within(self) -> bool: + """Whether to compute stats for geometries within the raster (if False, stats for + all geometries intersecting the raster are computed)""" + return self._within + + @property + def sigma(self) -> float: + """Number of sigmas for identifying outliers""" + return self._sigma + + @property + def chart_file(self) -> str: + """Name of the chart file to generate""" + return self._chart_file + + @property + def geometry_index(self) -> str: + """The column name identifying the name of the geometry""" + return self._geometry_index + + @property + def display_chart(self) -> bool: + """Whether to display the chart""" + return self._display_chart + + @property + def category_file(self) -> str: + """Filename containing the categories when computing stats per + categories in the geometries""" + return self._category_file + + @property + def category_file_type(self) -> str: + """Type of the category file, either 'raster' or 'vector'""" + return self._category_file_type + + @property + def category_index(self) -> str: + """Column name identifying categories in categroy_file (only if file format + is geometries) + """ + return self._category_index + + @property + def category_labels(self) -> str: + """Dict with classes index as keys and names to display as values""" + return self._category_labels + + def __check_stats(self): + """Check that the requested stats are valid. + + Args: + stats_to_compute ([str]): + List of stats to compute + """ + for x in self._stats: + # check percentile format + if x.startswith("percentile_"): + q = float(x.replace("percentile_", '')) + # percentile must be in range [0, 100] + if q > 100.0: + raise RastertoolConfigurationException('percentiles must be <= 100') + if q < 0.0: + raise RastertoolConfigurationException('percentiles must be >= 0') + + elif x not in Zonalstats.valid_stats: + raise RastertoolConfigurationException( + f"Invalid stat {x}: must be " + f"percentile_xxx or one of {Zonalstats.valid_stats}") + + def with_output(self, outputdir: str = ".", output_format: str = "ESRI Shapefile"): + """Set up the output. + + Args: + outputdir (str, optional, default="."): + Output dir where to store results. If none, results are not dumped to a file. + output_format (str, optional, default="ESRI Shapefile"): + Format of the output 'ESRI Shapefile', 'GeoJSON', 'CSV', 'GPKG', 'GML' + (see supported_output_formats). If None, it is set to ESRI Shapefile + + Returns: + :obj:`eolab.georastertools.Zonalstats`: the current instance so that it is + possible to chain the with... calls (fluent API) + """ + super().with_output(outputdir) + self._output_format = output_format or 'ESRI Shapefile' + # check if output_format exists + if self._output_format not in Zonalstats.supported_output_formats: + _logger.exception(RastertoolConfigurationException( + f"Unrecognized output format {output_format}. " + f"Possible values are {', '.join(Zonalstats.supported_output_formats)}")) + sys.exit(2) + return self + + def with_geometries(self, geometries: str, within: bool = False): + """Set up the geometries where to compute stats. + + Args: + geometries (str): + Name of the file containing the geometries where to compute zonal stats. If not set, + stats are computed on the whole raster image + within (bool, optional, default=False): + Whether to compute stats only for geometries within the raster. If False, + statistics are computed for geometries that intersect the raster shape. + + Returns: + :obj:`eolab.georastertools.Zonalstats`: the current instance so that it is + possible to chain the with... calls (fluent API) + """ + self._geometries = geometries + self._within = within + return self + + def with_outliers(self, sigma: float): + """Set up the computation of outliers + + Args: + sigma (float): + Distance to the mean value to consider a pixel as an outlier (expressed + in sigma, e.g. the value 2 means that pixels values greater than + mean value + 2 * std are outliers) + + Returns: + :obj:`eolab.georastertools.Zonalstats`: the current instance so that it is + possible to chain the with... calls (fluent API) + """ + # Manage sigma computation option that requires mean + std dev computation + if "mean" not in self._stats: + self._stats.append("mean") + if "std" not in self._stats: + self._stats.append("std") + self._sigma = sigma + return self + + def with_chart(self, chart_file: str = None, geometry_index: str = 'ID', display: bool = False): + """Set up the charting capability + + Args: + chart_file (str, optional, default=None): + If not None, generate a chart with the statistics and saves it to str + geometry_index (str, optional, default='ID'): + Name of the index in the geometry file + display (bool, optional, default=False): + If true, display the chart with the statistics + + Returns: + :obj:`eolab.georastertools.Zonalstats`: the current instance so that it is + possible to chain the with... calls (fluent API) + """ + self._chart_file = chart_file + self._geometry_index = geometry_index + self._display_chart = display + return self + + def with_per_category(self, category_file: str, category_index: str = 'Classe', + category_labels_json: str = None): + """Set up the zonal stats computation per categories + + Args: + category_file (str): + Name of the file containing the categories + category_index (str, optional, default='Classe'): + Name of column containing the category value (if category_file is a vector) + category_labels_json (str, optional, default=None): + Name of json file containing the dict that associates category values + to category names + + Returns: + :obj:`eolab.georastertools.Zonalstats`: the current instance so that it is + possible to chain the with... calls (fluent API) + """ + self._category_file = category_file + self._category_index = category_index + # get the category file type + if category_file: + suffix = utils.get_suffixes(category_file) + if suffix[1:] in Zonalstats.supported_output_formats.values(): + self._category_file_type = "vector" + else: + # not a vector, maybe a raster? try to open it with rasterio + try: + rasterio.open(category_file) + except IOError: + raise RastertoolConfigurationException( + f"File {category_file} cannot be read: check format and existence") + self._category_file_type = "raster" + # get the dict of category labels + if category_labels_json: + try: + with open(category_labels_json) as f: + self._category_labels = json.load(f) + except Exception as err: + raise RastertoolConfigurationException( + f"File {category_labels_json} does not contain a valid dict.") from err + + return self + + def process_file(self, inputfile: str) -> List[str]: + """Compute the stats for a single input file + + Args: + inputfile (str): + Input image to process + + Returns: + [str]: List of generated statistical images (posix paths) that have been generated + """ + _logger.info(f"Processing file {inputfile}") + + # STEP 1: Prepare the input image so that it can be processed + _logger.info("Prepare the input for computation") + with RasterProduct(inputfile, vrt_outputdir=self.vrt_dir) as product: + + if product.rastertype is None and self.chart_file: + _logger.error("Unrecognized raster type of input file," + " cannot extract date for plotting") + + # open raster to get metadata + raster = product.get_raster() + + rst = rasterio.open(raster) + bound = int(rst.count) + indexes = rst.indexes + descr = rst.descriptions + + geotransform = rst.get_transform() + width = np.abs(geotransform[1]) + height = np.abs(geotransform[5]) + area_square_meter = width * height + rst.close() + + date_str = product.get_date_string('%Y%m%d-%H%M%S') + + # check band index and handle all bands options (when bands is None) + if self.bands is None or len(self.bands) == 0: + bands = indexes + else: + bands = self.bands + if min(bands) < 1 or max(bands) > bound: + raise ValueError(f"Invalid bands, all values are not in range [1, {bound}]") + + # check the prefix + if self.prefix and len(self.prefix) != len(bands): + raise ValueError("Number of prefix does not equal the number of bands.") + + # STEP 2: Prepare the geometries where to compute zonal stats + if self.geometries: + # reproject & filter input geometries to fit the raster extent + geometries = vector.reproject( + vector.filter(self.geometries, raster, self.within), raster) + else: + # if no geometry is defined, get the geometry from raster shape + geometries = vector.get_raster_shape(raster) + + # STEP 3: Compute the statistics + geom_stats = self.compute_stats(raster, bands, geometries, + descr, date_str, area_square_meter) + + self._generated_stats.append(geom_stats) + if date_str: + timestamp = datetime.datetime.strptime(date_str, '%Y%m%d-%H%M%S') + self._generated_stats_dates.append(timestamp) + + # STEP 4: Generate outputs + outputs = [] + if self.outputdir: + outdir = Path(self.outputdir) + ext = Zonalstats.supported_output_formats[self.output_format] + outputname = f"{utils.get_basename(inputfile)}-stats.{ext}" + outputfile = outdir.joinpath(outputname) + geom_stats.to_file(outputfile.as_posix(), driver=self.output_format) + outputs.append(outputfile.as_posix()) + + # if sigma is not None, generate the outliers image + if self.sigma: + _logger.info("Extract outliers") + outliersfile = outdir.joinpath( + f"{utils.get_basename(inputfile)}-stats-outliers.tif") + extract_zonal_outliers(geom_stats, raster, outliersfile.as_posix(), + prefix=self.prefix or [""] * len(bands), + bands=bands, sigma=self.sigma) + outputs.append(outliersfile.as_posix()) + + return outputs + + def postprocess_files(self, inputfiles: List[str], outputfiles: List[str]) -> List[str]: + """Generate the chart if requested after computing stats for each input file + + Args: + inputfiles ([str]): Input images to process + outputfiles ([str]): List of generated files after executing the + rastertool on the input files + + Returns: + [str]: A list containing the chart file if requested + """ + additional_outputs = [] + if self.chart_file and len(self.generated_stats_per_date) > 0: + _logger.info("Generating chart") + plot_stats(self.chart_file, self.generated_stats_per_date, + self.stats, self.geometry_index, self.display_chart) + additional_outputs.append(self.chart_file) + + return additional_outputs + + def compute_stats(self, raster: str, bands: List[int], + geometries: gpd.GeoDataFrame, + descr: List[str], date: str, + area_square_meter: int) -> List[List[Dict[str, float]]]: + """Compute the statistics of the input data. [Minimum, Maximum, Mean, Standard deviation] + + Args: + raster (str): + Input image to process + bands ([int]): + List of bands in the input image to process. Empty list means all bands + geometries (GeoDataFrame): + Geometries where to add statistics (geometries must be in the same + projection as the raster) + descr ([str]): + Band descriptions + date (str): + Timestamp of the input raster + area_square_meter (int): + Area represented by a pixel + + Returns: + list[list[dict]] + The dictionnary associates the name of the statistics to its value. + """ + _logger.info("Compute statistics") + # Compute zonal statistics + if self.category_file is not None: + # prepare the categories data + if self.category_file_type == "vector": + # clip categories to the raster bounds and reproject in the raster crs + class_geom = vector.reproject( + vector.clip(self.category_file, raster), + raster) + else: # filetype is raster + # vectorize the raster and reproject in the raster crs + class_geom = vector.reproject( + vector.vectorize(self.category_file, raster, self.category_index), + raster) + + # compute the statistics per category + statistics = compute_zonal_stats_per_category( + geometries, raster, + bands=bands, + stats=self.stats, + categories=class_geom, + category_index=self.category_index, + category_labels=self.category_labels) + else: + statistics = compute_zonal_stats( + geometries, raster, + bands=bands, + stats=self.stats, + categorical=self.categorical) + + # apply area + if self.area: + [d.update({key: area_square_meter * val}) + for s in statistics + for d in s for key, val in d.items() if not np.isnan(val)] + + # convert statistics to GeoDataFrame + geom_stats = self.__stats_to_geoms(statistics, geometries, bands, descr, date) + return geom_stats + + def __stats_to_geoms(self, statistics_data: List[List[Dict[str, float]]], + geometries: gpd.GeoDataFrame, + bands: List[int], descr: List[str], date: str) -> gpd.GeoDataFrame: + """Appends statistics to the geodataframe. + + Args: + statistics_data: + A list of list of dictionnaries. Dict associates the stat names and the stat values. + geometries (GeoDataFrame): + Geometries where to add statistics + bands ([int]): + List of bands in the input image to process. Empty list means all bands + descr ([str]): + Bands descriptions to add to global metadata + date (str): + Date of raster to add to global metadata + + Returns: + GeoDataFrame: The updated geometries with statistics saved in metadata of + the following form: b{band_number}.{metadata_name} where metadata_name is + successively the band name, the date and the statistics names (min, mean, max, median, std) + """ + prefix = self.prefix or [""] * len(bands) + for i, band in enumerate(bands): + # add general metadata to geometries + if descr and descr[i]: + geometries[utils.get_metadata_name(band, prefix[i], "name")] = descr[i] + if date: + geometries[utils.get_metadata_name(band, prefix[i], "date")] = date + + # get all statistics names since additional statistics coming from categorical + # option may have been computed + stats = self.stats.copy() + categorical_stats = set() + [categorical_stats.update(s[i].keys()) for s in statistics_data] + + if self.category_file is None: + # remove stats from the categorical stats + # and add the categorical stats to the stats + # remark: this operation seems strange but it ensures that stats are + # in the correct order + categorical_stats -= set(stats) + stats.extend(categorical_stats) + else: + # per_category mode do not compute overall stats. + # So stats is not exended but replaced + stats = categorical_stats + + for stat in stats: + cond = self.valid_threshold < 1e-5 or stat == "valid" + metadataname = utils.get_metadata_name(band, prefix[i], stat) + geometries[metadataname] = [ + s[i][stat] + if stat in s[i] and (cond or s[i]["valid"] > self.valid_threshold) else np.nan + for s in statistics_data + ] + + return geometries diff --git a/src/eolab/rastertools/__init__.py b/src/eolab/rastertools/__init__.py deleted file mode 100644 index b678309..0000000 --- a/src/eolab/rastertools/__init__.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -"""This module contains the eolab's rastertools CLI and API -""" -from importlib.metadata import version - -# Change here if project is renamed and does not equal the package name -dist_name = "rastertools" -__version__ = version(dist_name) - -from eolab.rastertools.rastertools import RastertoolConfigurationException -from eolab.rastertools.rastertools import Rastertool, Windowable -# import rastertool Filtering -from eolab.rastertools.filtering import Filtering -# import rastertool Hillshade -from eolab.rastertools.hillshade import Hillshade -# import rastertool Radioindice -from eolab.rastertools.radioindice import Radioindice -# import rastertool Speed -from eolab.rastertools.speed import Speed -# import rastertool SVF -from eolab.rastertools.svf import SVF -# import rastertool Tiling -from eolab.rastertools.tiling import Tiling -# import rastertool Timeseries -from eolab.rastertools.timeseries import Timeseries -# import rastertool Zonalstats -from eolab.rastertools.zonalstats import Zonalstats -# import the method to run a rastertool -from eolab.rastertools.main import rastertools, add_custom_rastertypes - -__all__ = [ - "RastertoolConfigurationException", "Rastertool", "Windowable", - "Filtering", "Hillshade", "Radioindice", "Speed", "SVF", "Tiling", - "Timeseries", "Zonalstats", - "run_tool", "add_custom_rastertypes" -] diff --git a/src/eolab/rastertools/__pycache__/__init__.cpython-38.pyc b/src/eolab/rastertools/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 3a846ac45548033b719d5fc814ecb709c639174c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1168 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 diff --git a/src/eolab/rastertools/__pycache__/hillshade.cpython-38.pyc b/src/eolab/rastertools/__pycache__/hillshade.cpython-38.pyc deleted file mode 100644 index 2be00e7a1b9c42c58c02a7d5027c3fc281396d67..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5330 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 diff --git a/src/eolab/rastertools/__pycache__/main.cpython-38.pyc b/src/eolab/rastertools/__pycache__/main.cpython-38.pyc deleted file mode 100644 index 553fc271d40e644377145e9d6fc6b588a1ae4699..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8449 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 QqzYwT-YZ`zm&&>S1sYa%HUIzs diff --git a/src/eolab/rastertools/__pycache__/radioindice.cpython-38.pyc b/src/eolab/rastertools/__pycache__/radioindice.cpython-38.pyc deleted file mode 100644 index 1fcfd7433dfa62336422659348d7947e72f5e264..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9723 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 diff --git a/src/eolab/rastertools/__pycache__/svf.cpython-38.pyc b/src/eolab/rastertools/__pycache__/svf.cpython-38.pyc deleted file mode 100644 index 95af667643ca06789ab64f2e2d86463d0d6b8e2d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5953 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 diff --git a/src/eolab/rastertools/__pycache__/timeseries.cpython-38.pyc b/src/eolab/rastertools/__pycache__/timeseries.cpython-38.pyc deleted file mode 100644 index 4a179febe218810a240fc73d8bda47600ac088fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8371 zcmb7J-ILqabq7EY1VM6_OYNs5%djoEVk)mJ$C=Kks>h0~I8Nl1BCWsZC^VM1+{G>k zP#0LOmH|7{u4a<7m8CS9X)>{A$eF(AB@g{uI@8YWW1rkU_0^q``#Trl$F3BQN)9eA zF77?|+;h*z?;dpP&-;f zQ|US9=Q?^rmv!CGH}YuHeIvUU8U^`mHcZ^}ez9XUEZNuaOPz9~+^IAw+1Q!Jtn6ED zRB;x@VU$xh_*wnl>mM~(-wfDSuOHLCohX!FG-OzVp zFAS`BYd5l4;B5F_uxW#Q+w*-n;i~l&=}KJ_CUHxP5=AOq=EqK56$WQr=EQ5L>NlKt z3qAFByjCpqYgiCkOI;qeSQIroP8W~)-PrRZ^s;V_g(<9$yuc0jFh3(Yj=ve=UP*i0 z;Mj&%+#)`ve@6BYeU1uK8Y*;GbJa%9)i%|(%5?nm%wYKy#m#-8|3Ya)XWjh$N~6F` z*KiB>l>@a=MBQ|YvTmVnxg}XIF*6%m{z7#t?#z81y~}8;K!2u6Xi`pUlHQp4g`z8s zS!hvBSnFfe=>0xj2m}koIix3~HtCkxnHr5X!1g-MCX1f2sg{l3b8RfB|LKh-Dqlg@ zmAWo0s1T2vw8g??ft%KhE>t^oMSjBx+^ChGl0MZnTsd6PDvDS+PzLG{J|s~Fn!M|Y z(#$0)Nsk(ow(%9A49*b;v!k&wh&eZlANmE*ZAtpCj+(?E3LveR|>cGx#7Q zC1Itbvl+agyBnwaN6sKK;MGIZNqMu_UJCOLFA)d zEL@n)rWbf|vnk5tnwopk1w_JVN;0Che3e8k(^}Ns{=C^qj+Q;Xps8MkY z#bXsCnl0af+i5;lzW(9)t+2z+cjNu@a93e;zQgz?Yx4N~#G^;&Bi=gC;1V|uwUV<- zRcN)l&lf=bG_I(OLYcKxQ!`Zh_l`{res#387Ma)RM|HZc;EKq3CCat}Q&4+npI33~ zDqqIH$0|R88$UHJUZffc;xBPUWSB~>w=l)gM3&FV)2q*?^5AF%lX07hBmB^wyp3zb z&$NU_+xeW_tlp5Gh^#3ym2Wk0aCMv*vZ*-26)DM8TqCaLrlgcdM*&k0LfB7!lCL@C^jVV3fNzl4D;^1Y)U?L7fG z>5u;oY8ESrdZ4=Mj>6|-0J;P9Zvo@_xmY_;`ns$4^D%t$f%=Ye`yYR*^^JZZ){_EM zGVkUeXnixuxw##aZztxqA!{3;VkCt>C|^oicM^Kj&jmHQC%K&*Ptc=4^Cae=FjTfF zu%`dw1HE5N%tTL$gW^!Rq$GxG?r8iwu{9_qg`qkyF{V6JTx+QH^LM@zR|YfSWfoU8 zoYUuyl?q%wFRDGyF1v*7Q=p+XcM zsdrwDZLFh|R6y$`TrVHw5%cuHD?riA;T?p zh!_Qdi3hdAG(bt#2toqf!l`?AJ$Dx&VSlomn~bdMNRXGSxM+`fN#PHs1Z3WF5Ck_U zGLAVM^YImVyWWqlIDs96F~VjCR&p4Cr`Y!bm>I{lRU5DpjG<#}`wURjeq)5V)&!ig zsN4l0N~Ps`y!Y}4!A=nF1*7%ucUgwMi6$-9?p>Ogu-yVbVc_qB?GaBI2G16jIj{)l z2s;KfPO$+{Oy3H3xiC6jfMo~+7$6K>QR;Xt9;OdH?dN?ks1s&54z{}qIiUbP8VQYTYX)q?hzLytV=%N0ZH_&dqk3G8-RVi z*Cz{IML3PKNZ*uuzIw?ITaF*Sh017n&ph+VYiByIopF07uVgzEj{~bb6-kKm)N%-C zlBbLoQOM=Yq|5mw?D6n|Ojtpih}Jd!8hVp}V)hs(5V|*URO&c!xX=OG7CI@}alunPiE!$|k!p6-6`+UX3?{UoD-4G_!6usmION?W%E7xxDh+M9 z$UB@n`#?s1xQYHk_X4bzL??=44HiZwf!B%1cj=|!bh`+|!rTsF8aTWO(}|kcQ170o zWroz8h(tmwQA&YYGxB;+&$}deUpbyiD`Gf}#O2w25mm z2ZfInm@fLVz`)|t~WL*ykE7M5g-v=@jGYb4pvO;~NF zM#eADw5HeDlvLBre}niT4FDTQb@w`&qnjv{Qdu>19l%aktLltus+L;Ss5G@jwJd8e zg>MYqL=WqUu4zwneIE5wc?+efF00Eqyi@dVbiIrby_4%>g@4*;>Qw^7v~&PstJzFb z4+M^z%{%a(*_%SM>4vRllgoY20qb*$`Vy836M}*~8?=pgz(b{tS^D=f^>Ah)56Iq9=KYTYi_~ zmPAPkD~gP}`JwW#2c$uWMdLgv0-;za3;hyGvtLG8MC31STZsF`?GmDVal72F25;kT zBoGZzyJXvO*xt>9^P`TGJD)5dK{D%BJy7~5?Q`|vwfut=?CAM?jD#kzMFJ|| zG1uPMC-yv^=KIJIjuFvO%LC4n{M6kPaWjM<|OQ{1mEPFB!@lqd;PxA?XI;BCmha)+S(W26!6AZli zT#1VD%u_KAlG*M0jPf|=C~aDQR&wR+h1cJB^PEj({Rl_55GQH?@o1bIFp#Vy$kdX=pAlyr@Q3=+~G02 z%eW5!fmN*WSvcB4g|C6wX=dqfFKl$gf;^ujs&YxTCszq^ifIC43L z%aQ2({T{YOY;h|H(V=i~J7&k^xqQjtnTczkq5!4r9j`1=oux87>y5$Gv~~?>y~-|&THKS##E5?n!&CHH+rkjI27tvqAh@vIe1pG9 z#B(^epm6R9W4BAzEOLOhq9S*7oa391B>1H~^Mt;b?I2I-W!69-xbVIcAO{XWdks1v zv=CSi)EJhS_i19@AdXC$xD50;`lP`)?R)){L?Zk%>T?);h?&d-M-qHSe2w}LSmZZQ z04wnpgv#1d+Zq;Xna~>4D&^KFqLzxTQATwvJ=q=fNr3!sh$f?=O@&9rHVTmgC>1$3 zibWyw3_{%&I+>18JHp!I9fOL)#-<(O;ub=Bl}}Rwb|s zfSTqd)6&!cT-A9AW9I=`C6HCE+%kHdRI52d)o`xURO5+}v(R1ze6_TiT2o&IB@^#! zPYfMz)H1-Wp@1qbExQ~4s~OtYoOo%`;b!zB@L9qoVcj25p^pTBXOt+p2w(;%1K?Il zO7!92p$&Le-qww`Q%UlAV_3PT-0&V2YIKtv>=B&4|r z3!m**d>`uV}18|_w`?vQewzPIKPjeUC_6xj0eDhG*T`uz z$`tJ5BM^KbX3LBq{j3=5$ZQUIb<&44GNg=f^v8!pnL^^saVeynw@G)XCXeHGorbFdek0ip7ca8whR7yw8=xB6HN?~|C(CqJnzWU9#Sr; z$^M2Y{FaI%cJUBx|BXxL4V0n*jh9C_by2G#V_(IeQ=huF#(zflGyD_W&Dz8lK8ID_4uq?MaP1mZCi=fBJs|BOz`8 diff --git a/src/eolab/rastertools/__pycache__/utils.cpython-38.pyc b/src/eolab/rastertools/__pycache__/utils.cpython-38.pyc deleted file mode 100644 index cac787425bef42572ee206bcf38df059b967c29d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3645 zcmc&%-EJGl72erhk}HamWkr!}Cv7IFQ<#B5*-C*Hf@-vN(B#S}nxsI82E=M-C~mym zWoAYeB`mL`LMqTVND5e&et^Er-1f?^(2J(unI&maEt1QwFhA$aaOV7d=ZwBsS!sIs z-v9Y0{eLWb-am=B{)HfZgQow6PIz5UFyUvc%PCrxFZ8cFN@ZBtsBWDN3DoetTw0AEwKh_S+?X7IJdtG#JYF`oE33bY=}3{ zS$9?36PscSvs=Rd%xk}OdeZ!SkZL|G#8D=>ko`25nrr!7syO3ECe2cF!iUlf3ZZ$w zPVHMfEr18`q@s*aQY^<-yI4CT(qoa`t$D)i1!szcdRW=F-^ z;NYUj^o~}^jx4hHV1Y~K(VfwWjZD#VhuX?fZiYT5J#)%El8n7Y-l&z<_ zhh=gc?YALuZ^*3Pz*TRdO@)k@<0Q`bVAB^&n=|+x2*D;Wx8fwCftl4k1Nw~R=qG-@ z2|l6>{9W*w*w1GBCIgbR2 zOlljzSbUTj^(LsBI<15E9-4LuB?wu_?)aNb5r4txm!APJGK$> zYfNb~39vl-7JR%9E}}OmQ8b=KdIO`6Z1@2@>2l+@C?KW(t@n3$J6|o?oDQbmt)2d2 zW`fym+-Z)q?NST^)}-2`39?DS4`q%DVB}M$yJZKKb)f4O#DF1whC$t@?tSWhLfs7u z+$LwvZtBrZkFl!?Y(IoYH~m$n9)OpSqO*_kBc&uVgeIkp_W~*#U-*~)CA;HY2512O znZM;-u}OI0U9b!PBIqNiMR*>WpbXF0$M8S=UW-3PfP9(COz=m@I76B1w8(E(BpxS8 zpXdH})8{frbyJWVML;)Q9Q;-t>OD83L|c`!-f=3-U~fuc05#vD2Wj8zaW}X` zF&bqj{6LyxiQ}n0A>_17P)MH3d3~hA>C>hd?eSqerYd$MU!Q7Xg;P;%s|EgGnp610 zb|vT^U7EfoEB0=?lOCWdY;$yNh(+gc1R|GcD9&>1$|m%`QL;Pk?pFjhmjMD$mFk-GOtboR3%i8KAU zGvA6h#%s;9VItEE*T-X9Lxw+;?V8%fg4&_(5p|?hw~-(8QUpVFZ-@AY}bVWJ43Lo^qoA!=&f ztN(irlHLCalEA%VZJ41pxNvT-*yZWjulX`0`%(F;I=8HxKuS8)@-BK6GhVN zRj6;!n=(re+~dsY=pMPK%y{s-r({*>sp7e3hb!HR9%im;Ia|29Dg%>aUz^%S`_;cx ej~&oAu^2`XYXwm)YOFTamRFZo>+8+t?f(G((tpbU diff --git a/src/eolab/rastertools/__pycache__/zonalstats.cpython-38.pyc b/src/eolab/rastertools/__pycache__/zonalstats.cpython-38.pyc deleted file mode 100644 index c1c049ca0479cf77937c1b4deab667ee1309faff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20867 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 s+R4Ng^FGrS`}y;d5H1jQxF=BCm2L1MSasU7T diff --git a/src/eolab/rastertools/cli/__pycache__/__init__.cpython-38.pyc b/src/eolab/rastertools/cli/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index 7c118f69cf70fbec2945603c9a26a7297a8caf5a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1910 zcmb_d-HzKt6t*2FPO?Ak(%owL5gINKYtgI$m7545mLen&luB)H_Qsm{oFqoJ$C?>y z8>^QUcYOdPUZ9V_gK#sqJO#@IoEay(p<=IKBG1g3pY!v5=S-gN?TtNZzy18x{CMbj zzguCug(Q4IpE{uqymJr6=tJ;}4?PH9^v?t6!{CK?-h&8+G=?yG?8Q6n@_PYIflM#C zW`Og&LPY{H)yy(cmb?PR#GFa4G)gT*p-#t7mbqf3yZC`(XOF&NBv(SSywX?!0-FoT z@EP5xc)=QlbSL&rujY^ijHs~%&IxULvm{B{E3pI5!E71joa_ZD?Fnw3 zs;>NUFj>2V3y??dUfY{osCI8M=^RZ}-lEyr5^|5oZa;lY>y{av7nU-?AXi%EvqqCX zDKb=wRNk|s@}|&J_VGub+>b*O0xE5y^|A@|szwtov8dZ%Qn&rdguIbHkzqoF1?^R$ zLKBtzSzb1!2}|Ua8I%Y)jZv9bZaI=TFEG<)q*f)ND)Y>Q7n0ZQPmefQ|0oS8a?w;I zQOtSC7kK3!n=)&RohU@M)VfyFB&nLRUY!cLNI+zX(tK86LUvGMm0_~L3MJPF>x1O? z=~9=4gS8ZrY*dtpCD*yA;y9E&3QF2aF;TV@IYA@`Bt47+vr7!58%|A+5}6hU9pMg) zSDrkivzOl{OHpD{>lJxY3zd{87nn+&Y?Vl&WR{>P`0O96S&=7cN`UD!J*`*f=pxt4 zbWQEXxQfV(x<{Qi4E^BxcR3h_!S%}64?Oq#D+q!=NHoU|^3F5`bw#;)pLUgfi@fL> zkbINW$+v80D{70JLr0ENd`--KI-}m9&KqqLGZ<`3@;%yr82eA+zKPP5 qo{Ka!;f&sKR~U{(cV^3^GwzaN_*wU7_PPB(Qa0MKfBW6xt-k@^LmF2A diff --git a/src/eolab/rastertools/cli/__pycache__/filtering.cpython-38.pyc b/src/eolab/rastertools/cli/__pycache__/filtering.cpython-38.pyc deleted file mode 100644 index b1b76334e7d2260cad741b1d8551e0a40557c148..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2828 zcmai0OK%)S5T5rwypHX}i3miZW02wjWftXxRwlwYL<9mtNkC#nG#O9#u4gklv+3@Q z~7#lFe&oGURp0=wCmY@X(&aOE&dnWa0<-pvvq;+T?8Mlmw`@8no>KS9Z z=fH4bcrd_9T03xekybsdX~`>JTf6SAH+Fh;S|7XL8$VhdbL@R%n#PlRnmR`tbe^6& z00iXRVPotZ0S{L5v(>p`1=0(k*`W&uxO){WTab%%5#PoJAhQ&`VHw6crQ!q{Vz?RS z{eF~D07mn=$y1gQ9tpq;u@VGi1`!vG*XsnHb>0;%y~4MWU|2|%m?-c7&@TzS)CQ*H zWP2-#x8U4FA2T&YY(;5GHW`U1Wi%+xMP>LrK;ejYhkcewGT`~sgfc-oJnxf;#5rXo zO&&9?wADJk7xyRqLCz&=(Du$eK^{K+D9I+SkoRU{0l2MC{9Hq^>Yv(Ia&n3U>@R5x zz?*OerZ_{zEJ}lzvq-XVY9wDhL%U!2c8|#+&q}EO2jas41+|oHC-M~P+9Bl?HYlr; zjIOQ^2I&rfTF~&mA|jY1c?VF8x=f5#{#PXMZB>cVBoa(0XN#o+H3#<<^JIV-swzbW z3P?s*CNgECYai@j3P-~fPRhW5PQryGn;4{U^$;Y3Avh8E4tF6nxrY;Gtn%0(N%QW= zx_(`GNd{u#h57dAmpiybFCZ#%44Ie-K~`DN4aj|nq9kMK4RZI!I{9?{IKK(c3&Wdh61N{|YO(|E5@J)xA`7*F zT8&PHnOS+p$V(m>FbqRmV6ha#5W-9&;S3 zfT1pQnG7cghlP=FHCfbWCjqU&F*@pnaudn=LN!p~l&b7FP1Gw0LYNOFfJ_rU;|g_A zbGDO==G!ImvL@qwiW0Q}ku|kSsKrSJ%rNvT>cSaGpuED~%KYP(` zSo|vNg**4S&i-S~3zNcuu>nIogGoXY*fXJF89kFPQ)pM&6OV+tX)b{@ z}P4|LnGr`>XmrB{nH+!u|`T~Q}wFWb59<_A;npsZEMnfs#!%UaW&z|Gg!?9by zOv78a4?`@&lmK=Q8~U@s7@OZ(PXHkuYmmX`fe&eI1@9<}cQmz_2ryKAjV7_gx_9$S zco$gkJOjeU7`=HiO@g&FkE2xF3|?;X7nSoA+Lo$8TMc#IDYy7@upBA79}Se9WK!1$ zz6APpQBiY`8Hd^!ie$vpTq)0$SLC~Dlu}%vh|YB_Zx14>E^B^~4dE2W2rIwuDhF>* zRdGfSxeo`oCs{r%Od)cVgP$TNaN)0k_$?UXRhW!g)%Hx!x?-O7|!30W&Pzi zTF$SkN*K~S4nuYE^o?U3n=6F?zXTf9(ka0LAWS~3wPX=i4Nh=Zb*-P%Vaje|T^9Hm L>3~fQyW08(f?!N| diff --git a/src/eolab/rastertools/cli/__pycache__/hillshade.cpython-38.pyc b/src/eolab/rastertools/cli/__pycache__/hillshade.cpython-38.pyc deleted file mode 100644 index ab647f791fcc707df149641df0a7cefd18bdb151..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2909 zcmbtWOOIPM6uz%KCj)JvRS_(#0f~ty2@k2js0FGi&bZ6jm-|R<->6}|` z#J*iyv^ZuKxb!X9D|--PYD zal5(i-@zVct#SsMw(hW_H?g|!!SZAD@xb%WFsA-tttz(TJWXlFV7S|xh#j60NtNc( z`a%)V%&1gccDn@ajGU+qH?v%7GT#x5D>9aON+^kQ#4r&bBaoom*tqwsJulK(F16+0 z^sOwhd$y%iWD8Ts>56Osvz^3C&K2wZ1LtZ+E^hC47gvR2q_Z3pu7SCqr_Mi^)`z3kXI+*69KO2u`NqU6D;n7?xDvXH#&N3&fKu zG85NvYKJBTSN-|s|JPv94^4|Ef-0^|H08<6tN=2x6f>*&<>DeT_^u%feN>SVk)c>j zBrTo(z*?EFI`=VV}x#eZWKlg(D-sMWaKL7Rty-t`= znq~;dI_z*>Reb<%_a#Skn>}b<(eoC!9o+8f`!gg zgg!>D?jf`eC#=jv8&8Ro+<_(0LKikip@!XI3+$v89)k&7g7(uZ^G7bZ$V;yBq;QzR zfs~^V^&`_l)uu(FrLDWNV+ZR|>~tz+5#`dirYR{Cg)-f{%;v$mFd}SoO9LBgmEIw zajX9bz=JdXc0hV4lSx%RciVT^U!??oM|q!>MX@8v$~v) zGMaMpVD$-7nyPO+xdv4H@#b)vr+hfmdqc@*xf-TiPWVXbVNK3Z$#}@~gl^xn3JZso zYJ=Hcr7vWa3fTvXdL4szIPwDzJqZ46b^;&2zoO>izqR;pyMI6UGy1Lh=pp}D2zp1^ zY<2x^*bC$n;DpCUQ@T=q&r1W>wr5`(o9OB!25sLOK6-#2qDPQ=-zG)s;s6$|7?& zSidW+&*X|7dUklg4tQC5W~GK((=738dNs%@OX9Gr#GIROMj6s3vX-HW<2B^aRSu6^ zwvS_CFQ-Wq9`So&`;ET=nT3xc diff --git a/src/eolab/rastertools/cli/__pycache__/radioindice.cpython-38.pyc b/src/eolab/rastertools/cli/__pycache__/radioindice.cpython-38.pyc deleted file mode 100644 index eb04356274241070a0b159cf85a50a44cf23304f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4567 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 diff --git a/src/eolab/rastertools/cli/__pycache__/svf.cpython-38.pyc b/src/eolab/rastertools/cli/__pycache__/svf.cpython-38.pyc deleted file mode 100644 index ef4769ddadb767c3490ceed373eb2dece5246f37..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2727 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 Hr4RlEpuhy7 diff --git a/src/eolab/rastertools/cli/__pycache__/timeseries.cpython-38.pyc b/src/eolab/rastertools/cli/__pycache__/timeseries.cpython-38.pyc deleted file mode 100644 index 94d9742dce335c395f22e0826f1e297fc35ed651..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3264 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 PDz|pRUZ^#HZ_WP~6&bzO diff --git a/src/eolab/rastertools/processing/__pycache__/__init__.cpython-38.pyc b/src/eolab/rastertools/processing/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index e5a7a8d076ef5893822009e937ff21b4afedb3fc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 824 zcmaJ<&5qMB5O#jrrfJf4wSpsx5EAMod4Uz$1KJ~kUAXvSd1I4Wvg62hS9GsD1&_c3 z?Il+Zh*v-ycAPADBOIw1$AcfsiJIc+U{B;sB{)%YJRkfi_$f}+0x!Uyv4qW8$`&kRIa@Xf z&Y<5)=4y$T0piQg2(s2!XmfIV_I@jDp)}h`UevmERJ68mTV5Dy9XC#EX^YP2np-Q{ zrl7LXM!2oAW%>h{qfOASFNfT>f3h}V|KyS~p+(C?%?}^?Y)rin5}HHls@AI8IZmt; z4EjUm>F?03ZmA^JQD=MLHs&1#NyiPTspG(C4~0+pCueBw$e5#dP6}>ppLSBav5yIX zv4QJACiXD#F!eC=;P1rD0f5;v!yDlxAc{KQL5gbv3f+Jx(Dlt`)z1iFx+Vm_!Q5-03{PX;ZOzvm) z@bg`@)rwc0+gFBn+E$92h7;qeqroa`>T2NfSM)G$MTlsHBc$y1eR4KFtyf-mTL8py W7OX=!!8!^N>;Se$6#j~nEW8JhSM#I* diff --git a/src/eolab/rastertools/processing/__pycache__/algo.cpython-38.pyc b/src/eolab/rastertools/processing/__pycache__/algo.cpython-38.pyc deleted file mode 100644 index 9e90156b3957f5d19788f5598f7b605505bed926..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14808 zcmd^GTZ|l6TCQ7HS6}90&%}1@#Hl29GM#m1>`B<{T7zRJ&c$qELXr*8o6xG6Q`1v3 z-PP?=HDgb!21M*^oE1TV2c(UV@CXTH1d%{1vDycC}<6|Gtjw#Xwc3P%(WmqbyN5SK++ zj3FKq<6;8wxR?}s5KoA`VhZu3m=<>--Xr#j{fPI91LAJPQ{tdFgm_xqBko0fmpCl$ zL%dJ8;(o;Y#RK9�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 diff --git a/src/eolab/rastertools/processing/__pycache__/rasterproc.cpython-38.pyc b/src/eolab/rastertools/processing/__pycache__/rasterproc.cpython-38.pyc deleted file mode 100644 index daa2052ca753fd5fa1036815b53787c5b99eaed5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9427 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 diff --git a/src/eolab/rastertools/processing/__pycache__/sliding.cpython-38.pyc b/src/eolab/rastertools/processing/__pycache__/sliding.cpython-38.pyc deleted file mode 100644 index acdad3370446ce64291ab0c191046febc8e630d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7822 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 diff --git a/src/eolab/rastertools/processing/__pycache__/stats.cpython-38.pyc b/src/eolab/rastertools/processing/__pycache__/stats.cpython-38.pyc deleted file mode 100644 index 65381eaed377b4243253109216443297e30dd61f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14262 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 diff --git a/src/eolab/rastertools/processing/__pycache__/vector.cpython-38.pyc b/src/eolab/rastertools/processing/__pycache__/vector.cpython-38.pyc deleted file mode 100644 index dc48cb39be15e2e9b49e8efb77b081de2b83ab07..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13412 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 diff --git a/src/eolab/rastertools/product/__pycache__/__init__.cpython-38.pyc b/src/eolab/rastertools/product/__pycache__/__init__.cpython-38.pyc deleted file mode 100644 index c48880055d16a4bf33a53edab36acf79a071a964..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 993 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 C;-0Pm diff --git a/src/eolab/rastertools/product/__pycache__/rastertype.cpython-38.pyc b/src/eolab/rastertools/product/__pycache__/rastertype.cpython-38.pyc deleted file mode 100644 index e4da05551f3acbef403a1b74d8ee0726f82baed3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15064 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 U)ZSf1uNW0Wy;hf-o|`=M-%FT?UjP6A diff --git a/src/eolab/rastertools/product/__pycache__/vrt.cpython-38.pyc b/src/eolab/rastertools/product/__pycache__/vrt.cpython-38.pyc deleted file mode 100644 index 00f0444545e55a1b62364171fba0dcc8482a3a9a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4900 zcmd5=&2Jn@74Oga=$Y|XJ{J%)5F$@x>};fEVPyh|orFkPC$Wc=hVld%ya% zA2u4Ef#-V{Z*~5D&M^K;!sJtd@KgL{e?h^FmcdMB#b(cHSv|XDnYn$32x3bQ*lw|JG$cdU1eiJI0xb1-Y}vl5$U3z&6gYQ#L&I0cQT*%_>H zmf2fI^W5G=@1cz1NJc#KW$N#Q35&V^Qb<4Qgg_c``%G%e*mOrkVV&YdWe z8%Zt?=B1Dz1h2Q^fKbYzdztZF_3$w*V6t_MBN?fU%CA5kCyzkjT>8gwQK#b zy@OS*EMJCBrxV3ox!YJeYumKY=$Xaa#orqKvW{sO(l{`4^U&P4bDNpZtDlVeYCStGW5weCFj|P#cXXVMI;VbLX|0*T$`7wARsTKWm`9j@cE=CV6HgS67X^ zq$qBjac|M+NqMLo7 zYb5RV;(*7z#}j$nNFDb=fj0l~hj;u@KlFsse-*{C-%b-5MhOBCsyl|J;&ipX*dtvrG8|y0i7NbNqr&V63IP}wwwnh~C zi(J6wp=(;zhN^FA4{d1&shQ$^69y$=k1O+C)et-mW%QCiNFQxJP+l>DmQiy>{hmns zTu=_GTCtF#O;?ohwV>zSlc%~?UorJTEco~XtxE{mnLVY3jA*4w~7#2gK+qgAi166V-8+)+N@ZtKh#9mB%IM~-s!ha+C{ah5=2)E_<1@@vY4rlRr( zLoC6ff81W#Nqc;yFNZ6F_tR_zp6K#G$d$rCnWk~Jg4ruPjl=D!R;Xr!wp^iH=?_&k zAUm!=T7c$k6@^jr%;Hxu>t@}qTXo2~wCm;^ex5mF&Y(`CAdhPcwphk|fMYey%mBXKB$NZeo6G*~j!!Cs01;DAh-A>~4+$@m{!}+y;_v+i*c*fbt{O=F-c385C4Z>PT}p2V#f$dz z_NPe0U^ILrjb?2RI)MmTG{~0xu3YjjU3&g1#C6sD;H0j-3sdGjeR_3u<;|@&UGYt1 zkoXoA-$9|gj!1h5t0?IL{rbRB)+~z))W1T-WfZOXJ~{d*oCwEGG{tbVa@z3bi4JPg zI8+qhrzHU+I5L8f+Yqz!D2ztkECCuycEz+T>tBvz{j*Z?Oz|H2K1W&_&60+=0JBtu zqL!dYWA3q?sw6lfEol>696fbZh#hMN_9s-NksH!{ZHa~4IIs?l#L5kVh1}Gwi@7;! z1>=(7Ua!qhUhbErbKvG>z(ysn9Fz_Xur5b)9fBcG*FDX~Y5<@b*oXt(NPmG^iT&d>AG`J%d3ax&kf6X&$w@F8oxKT^!qVjOM}zjtTCUV?fY~E zXukX3$P8eeJmxRvK^S_YFyp4fS zFciQuSL{yH!sO4mYRSxZ_<)OM(Y+pH`XIwd?yj4Rx4WQsx4G0DN zlXwy}=infHU1xzzDXAkYmwgR|aptUf)*^nTIhbZzx@L)=p|@#i8&#e0tSusasb_7V z$@n4q`Ub<2fJJEoCeH#UodPDM@yrF>S+bOy3=q0*JiUO=Q5zqw7QoOReYjeHx=Z+2 zt4n7#MsEiUzGPug-sS7_H6UGt%j!z^Ivt)FOL1=UE=F)EGIYHIz( zCpgWMptC;Vttkt&4)Fu{Y@=D#-yecbL0qMRJ{-hPP^jvyYr(@l;*+Z?2aDhWRq=J| znt>N4)Hh2-5YcU@ES4!VQVtoYEU|qu49)-@0`gQaxrI+KHKUu(sQTuD=~*~`OWcRt zG&h>(R5=J(+71F$!sU&tUeNIi8-+T951D1eI_Jxyub>AMpRR6FwTZSOKgy%4q(~t* z&AL1-zQSD1m6VnAl@|oz0E|mu$wm->vK9YAfmjQI7&J7=co2y1k_9#(U*}bkVHC64 z^ogz0myFQlp|aY;H%ZK|lJVI^6plTIEz>7Q#d1vi?9=Zr_>O5A*1x|&CEoRm{{i=3.6 -Requires-Dist: pytest-cov -Requires-Dist: geopandas==0.13 -Requires-Dist: python-dateutil==2.9.0 -Requires-Dist: kiwisolver==1.4.5 -Requires-Dist: fonttools==4.53.1 -Requires-Dist: matplotlib==3.7.3 -Requires-Dist: packaging==24.1 -Requires-Dist: Shapely==1.8.5.post1 -Requires-Dist: tomli==2.0.2 -Requires-Dist: Rtree==1.3.0 -Requires-Dist: fiona==1.8.21 -Requires-Dist: Pillow==9.2.0 -Requires-Dist: sphinx_rtd_theme==3.0.1 -Requires-Dist: pip==24.2 -Requires-Dist: pyproj==3.4.0 -Requires-Dist: sphinx==7.1.2 -Requires-Dist: scipy==1.8 -Requires-Dist: pyscaffold -Requires-Dist: gdal==3.5.0 -Requires-Dist: tqdm==4.66 -Provides-Extra: testing -Requires-Dist: setuptools; extra == "testing" -Requires-Dist: pytest; extra == "testing" -Requires-Dist: pytest-cov; extra == "testing" - -============ -Raster tools -============ - -This project provides a command line named **rastertools** that enables various calculation tools: - - -- the calculation of radiometric indices on satellite images -- the calculation of the speed of evolution of radiometry from images between two dates -- the calculation of zonal statistics of the bands of a raster, that is to say statistics such as min, max, average, etc. - on subareas (defined by a vector file) of the area of interest. - -The **rastertools** project also aims to make the handling of the following image products transparent: - -- Sentinel-2 L1C PEPS (https://peps.cnes.fr/rocket/#/search) -- Sentinel-2 L2A PEPS (https://peps.cnes.fr/rocket/#/search) -- Sentinel-2 L2A THEIA (https://theia.cnes.fr/atdistrib/rocket/#/search?collection=SENTINEL2) -- Sentinel-2 L3A THEIA (https://theia.cnes.fr/atdistrib/rocket/#/search?collection=SENTINEL2) -- SPOT 6/7 Ortho de GEOSUD (http://ids.equipex-geosud.fr/web/guest/catalog) - -It is thus possible to input files in the command line in any of the formats above. -It is also possible to specify your own product types by providing a JSON file as a parameter of the command line (cf. docs/usage.rst) - -Finally, **rastertools** offers an API for calling these different tools in Python and for extending its capabilities, for example by defining new radiometric indices. - -Installation -============ - -Create a conda environment by typing the following: - -.. code-block:: bash - - conda env create -f environment.yml - conda env update -f env_update.yml - -The following dependencies will be installed in the ``rastertools`` environment: - -- pyscaffold -- geopandas -- scipy -- gdal -- rasterio -- tqdm - -Install ``rastertools`` in the conda environment by typing the following: - -.. code-block:: bash - - conda activate rastertools - pip install -e . - -.. note:: - - Note: Installing in a *virtualenv* does not work properly for this project. For unexplained reasons, - the VRTs that are created in memory by rastertools to handle image products are not properly managed - with an installation in a virtualenv. - -For more details, including installation as a Docker or Singularity image, please refer to the documentation. : `Installation `_ - - -Usage -===== - -rastertools -^^^^^^^^^^^ -The rastertools command line is the high-level command for activating the various tools. - -.. code-block:: console - - $ rastertools --help - usage: rastertools [-h] [-t RASTERTYPE] [--version] [-v] [-vv] - {filter,fi,radioindice,ri,speed,sp,svf,hillshade,hs,zonalstats,zs,tiling,ti} - ... - - Collection of tools on raster data - - optional arguments: - -h, --help show this help message and exit - -t RASTERTYPE, --rastertype RASTERTYPE - JSON file defining additional raster types of input - files - --version show program's version number and exit - -v, --verbose set loglevel to INFO - -vv, --very-verbose set loglevel to DEBUG - - Commands: - {filter,fi,radioindice,ri,speed,sp,svf,hillshade,hs,zonalstats,zs,tiling,ti} - filter (fi) Apply a filter to a set of images - radioindice (ri) Compute radiometric indices - speed (sp) Compute speed of rasters - svf Compute Sky View Factor of a Digital Height Model - hillshade (hs) Compute hillshades of a Digital Height Model - zonalstats (zs) Compute zonal statistics - tiling (ti) Generate image tiles - -Calling rastertools returns the following exit codes: - -.. code-block:: console - - 0: everything went well - 1: processing error - 2: incorrect invocation parameters - -Details of the various subcommands are presented in the documentation : `Usage `_ - - -Tests & documentation -===================== - -To run tests and generate documentation, the following dependencies must be installed in the conda environment. : - -- py.test et pytest-cov (tests execution) -- sphinx (documentation generation) - -Pour cela, exécuter la commande suivante : - -.. code-block:: console - - conda env update -f env_test.yml - - -Tests -^^^^^ - -The project comes with a suite of unit and functional tests. To run them, -launch the command ``pytest tests``. To run specific tests, execute ``pytest tests -k ""``. - -The tests may perform comparisons between generated files and reference files. -In this case, the tests depend on the numerical precision of the platforms. -To enable these comparisons, you need to add the option. "--compare" for instance ``pytest tests --compare``. - -The execution of the tests includes a coverage analysis via pycov. - -Documentation generation -^^^^^^^^^^^^^^^^^^^^^^^^ - -To generate the documentation, run: - -.. code-block:: console - - cd docs - sphinx-quickstart - make html - -The documentation is generated using the theme "readthedocs". - -Note -==== - -This project has been set up using PyScaffold. For details and usage -information on PyScaffold see https://pyscaffold.org/. diff --git a/src/rastertools.egg-info/SOURCES.txt b/src/rastertools.egg-info/SOURCES.txt deleted file mode 100644 index 9213c8a..0000000 --- a/src/rastertools.egg-info/SOURCES.txt +++ /dev/null @@ -1,177 +0,0 @@ -.gitignore -AUTHORS.rst -CHANGELOG.rst -Dockerfile -LICENSE.txt -README.rst -env_test.yml -env_update.yml -environment.yml -setup.cfg -setup.py -tox.ini -docs/Makefile -docs/authors.rst -docs/changelog.rst -docs/cli.rst -docs/conf.py -docs/index.rst -docs/install.rst -docs/license.rst -docs/private_api.rst -docs/rasterproduct.rst -docs/requirements.txt -docs/usage.rst -docs/_static/Logo-Eolab-Anglais.png -docs/_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-grn.jpg -docs/_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-adaptive_gaussian.jpg -docs/_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-geoms.jpg -docs/_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-mean.jpg -docs/_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-median.jpg -docs/_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-stats.jpg -docs/_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-stats2.jpg -docs/_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.jpg -docs/_static/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi_cropped.jpg -docs/_static/dsm-hillshade1.jpg -docs/_static/dsm-hillshade2.jpg -docs/_static/dsm-hillshade3.jpg -docs/_static/dsm-svf.jpg -docs/_static/dsm-svf0.jpg -docs/_static/dsm.jpg -docs/_static/oso-stats.jpg -docs/_static/speed_ndvi.jpg -docs/_static/style.css -docs/_templates/custom_module_template.rst -docs/_templates/layout.html -docs/_templates/autosummary/base.rst -docs/_templates/autosummary/class.rst -docs/_templates/autosummary/module.rst -docs/cli/filtering.rst -docs/cli/hillshade.rst -docs/cli/radioindice.rst -docs/cli/speed.rst -docs/cli/svf.rst -docs/cli/tiling.rst -docs/cli/timeseries.rst -docs/cli/zonalstats.rst -src/eolab/rastertools/cli/utils_cli.py -src/rastertools.egg-info/PKG-INFO -src/rastertools.egg-info/SOURCES.txt -src/rastertools.egg-info/dependency_links.txt -src/rastertools.egg-info/entry_points.txt -src/rastertools.egg-info/not-zip-safe -src/rastertools.egg-info/requires.txt -src/rastertools.egg-info/top_level.txt -tests/__init__.py -tests/cmptools.py -tests/conftest.py -tests/test_algo.py -tests/test_radioindice.py -tests/test_rasterproc.py -tests/test_rasterproduct.py -tests/test_rastertools.py -tests/test_rastertype.py -tests/test_speed.py -tests/test_stats.py -tests/test_tiling.py -tests/test_utils.py -tests/test_vector.py -tests/test_zonalstats.py -tests/utils4test.py -tests/__pycache__/__init__.cpython-38.pyc -tests/__pycache__/cmptools.cpython-38.pyc -tests/__pycache__/conftest.cpython-38-pytest-8.0.0.pyc -tests/__pycache__/test_algo.cpython-38-pytest-8.0.0.pyc -tests/__pycache__/test_radioindice.cpython-38-pytest-8.0.0.pyc -tests/__pycache__/test_rasterproc.cpython-38-pytest-8.0.0.pyc -tests/__pycache__/test_rasterproduct.cpython-38-pytest-8.0.0.pyc -tests/__pycache__/test_rastertools.cpython-38-pytest-8.0.0.pyc -tests/__pycache__/test_rastertype.cpython-38-pytest-8.0.0.pyc -tests/__pycache__/test_speed.cpython-38-pytest-8.0.0.pyc -tests/__pycache__/test_stats.cpython-38-pytest-8.0.0.pyc -tests/__pycache__/test_tiling.cpython-38-pytest-8.0.0.pyc -tests/__pycache__/test_utils.cpython-38-pytest-8.0.0.pyc -tests/__pycache__/test_vector.cpython-38-pytest-8.0.0.pyc -tests/__pycache__/test_zonalstats.cpython-38-pytest-8.0.0.pyc -tests/__pycache__/utils4test.cpython-38.pyc -tests/tests_data/COMMUNE_32001.dbf -tests/tests_data/COMMUNE_32001.prj -tests/tests_data/COMMUNE_32001.qpj -tests/tests_data/COMMUNE_32001.shp -tests/tests_data/COMMUNE_32001.shx -tests/tests_data/COMMUNE_32xxx.geojson -tests/tests_data/COMMUNE_59xxx.geojson -tests/tests_data/DSM_PHR_Dunkerque.tif -tests/tests_data/InvalidName.zip -tests/tests_data/OCS_2017_CESBIO_extract.tif -tests/tests_data/OSO_2017_classification_dep59.dbf -tests/tests_data/OSO_2017_classification_dep59.prj -tests/tests_data/OSO_2017_classification_dep59.shp -tests/tests_data/OSO_2017_classification_dep59.shx -tests/tests_data/OSO_nomenclature_2017.json -tests/tests_data/OSO_nomenclature_2017_wrong.json -tests/tests_data/RGB_TIF_20170105_013442_test.tif -tests/tests_data/S2A_MSIL2A_20190116T105401_N0211_R051_T30TYP_20190116T120806.vrt -tests/tests_data/S2A_MSIL2A_20190116T105401_N0211_R051_T30TYP_20190116T120806.zip -tests/tests_data/S2B_MSIL1C_20191008T105029_N0208_R051_T30TYP_20191008T125041.zip -tests/tests_data/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi.tif -tests/tests_data/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndwi.tif -tests/tests_data/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D.zip -tests/tests_data/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif -tests/tests_data/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndwi.tif -tests/tests_data/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.zip -tests/tests_data/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_tar.tar -tests/tests_data/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_targz.TAR.GZ -tests/tests_data/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_targz.TAR.GZ.properties -tests/tests_data/SPOT6_2018_France-Ortho_NC_DRS-MS_SPOT6_2018_FRANCE_ORTHO_NC_GEOSUD_MS_82.tar.gz -tests/tests_data/SPOT6_2018_France-Ortho_NC_DRS-MS_SPOT6_2018_FRANCE_ORTHO_NC_GEOSUD_MS_82.tar.gz.properties -tests/tests_data/additional_rastertypes.json -tests/tests_data/grid.geojson -tests/tests_data/tif_file.tif -tests/tests_data/toulouse-mnh.tif -tests/tests_refs/test_radioindice/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi-speed-20180928-105515.tif -tests/tests_refs/test_radioindice/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi.tif -tests/tests_refs/test_radioindice/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndwi-speed-20180928-105515.tif -tests/tests_refs/test_radioindice/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndwi.tif -tests/tests_refs/test_rasterproduct/S2A_MSIL2A_20190116T105401_N0211_R051_T30TYP_20190116T120806.vrt -tests/tests_refs/test_rasterproduct/S2B_MSIL1C_20191008T105029_N0208_R051_T30TYP_20191008T125041-clipped.vrt -tests/tests_refs/test_rasterproduct/S2B_MSIL1C_20191008T105029_N0208_R051_T30TYP_20191008T125041.vrt -tests/tests_refs/test_rasterproduct/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-clipped.vrt -tests/tests_refs/test_rasterproduct/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-mask.vrt -tests/tests_refs/test_rasterproduct/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D.vrt -tests/tests_refs/test_rasterproduct/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_V1-9-clipped.vrt -tests/tests_refs/test_rasterproduct/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_V1-9-mask.vrt -tests/tests_refs/test_rasterproduct/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_V1-9.vrt -tests/tests_refs/test_rasterproduct/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_tar-clipped.vrt -tests/tests_refs/test_rasterproduct/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_tar-mask.vrt -tests/tests_refs/test_rasterproduct/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_tar.vrt -tests/tests_refs/test_rasterproduct/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_targz-clipped.vrt -tests/tests_refs/test_rasterproduct/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_targz-mask.vrt -tests/tests_refs/test_rasterproduct/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_targz.vrt -tests/tests_refs/test_rasterproduct/SPOT6_2018_France-Ortho_NC_DRS-MS_SPOT6_2018_FRANCE_ORTHO_NC_GEOSUD_MS_82.vrt -tests/tests_refs/test_stats/zonal_stats.geojson -tests/tests_refs/test_tiling/tif_file_tile77.tif -tests/tests_refs/test_tiling/tif_file_tile93.tif -tests/tests_refs/test_timeseries/SENTINEL2A_20180926-000000-685_L2A_T30TYP_D-ndvi-timeseries.tif -tests/tests_refs/test_timeseries/SENTINEL2A_20181016-000000-685_L2A_T30TYP_D-ndvi-timeseries.tif -tests/tests_refs/test_timeseries/SENTINEL2A_20181105-000000-685_L2A_T30TYP_D-ndvi-timeseries.tif -tests/tests_refs/test_vector/clip.geojson -tests/tests_refs/test_vector/raster_outline.geojson -tests/tests_refs/test_vector/reproject_dissolve.geojson -tests/tests_refs/test_vector/reproject_filter.geojson -tests/tests_refs/test_zonalstats/DSM_PHR_Dunkerque-stats.geojson -tests/tests_refs/test_zonalstats/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-stats-outliers.tif -tests/tests_refs/test_zonalstats/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-stats.cpg -tests/tests_refs/test_zonalstats/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-stats.dbf -tests/tests_refs/test_zonalstats/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-stats.geojson -tests/tests_refs/test_zonalstats/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-stats.prj -tests/tests_refs/test_zonalstats/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-stats.shp -tests/tests_refs/test_zonalstats/SENTINEL2A_20180928-105515-685_L2A_T30TYP_D-ndvi-stats.shx -tests/tests_refs/test_zonalstats/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi-stats-outliers.tif -tests/tests_refs/test_zonalstats/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi-stats.cpg -tests/tests_refs/test_zonalstats/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi-stats.dbf -tests/tests_refs/test_zonalstats/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi-stats.geojson -tests/tests_refs/test_zonalstats/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi-stats.prj -tests/tests_refs/test_zonalstats/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi-stats.shp -tests/tests_refs/test_zonalstats/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D-ndvi-stats.shx -tests/tests_refs/test_zonalstats/chart.png \ No newline at end of file diff --git a/src/rastertools.egg-info/dependency_links.txt b/src/rastertools.egg-info/dependency_links.txt deleted file mode 100644 index 8b13789..0000000 --- a/src/rastertools.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/rastertools.egg-info/entry_points.txt b/src/rastertools.egg-info/entry_points.txt deleted file mode 100644 index 467c161..0000000 --- a/src/rastertools.egg-info/entry_points.txt +++ /dev/null @@ -1,2 +0,0 @@ -[rasterio.rio_plugins] -rastertools = eolab.rastertools.main:rastertools diff --git a/src/rastertools.egg-info/not-zip-safe b/src/rastertools.egg-info/not-zip-safe deleted file mode 100644 index 8b13789..0000000 --- a/src/rastertools.egg-info/not-zip-safe +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/rastertools.egg-info/requires.txt b/src/rastertools.egg-info/requires.txt deleted file mode 100644 index a374e7d..0000000 --- a/src/rastertools.egg-info/requires.txt +++ /dev/null @@ -1,27 +0,0 @@ -click -pytest>=3.6 -pytest-cov -geopandas==0.13 -python-dateutil==2.9.0 -kiwisolver==1.4.5 -fonttools==4.53.1 -matplotlib==3.7.3 -packaging==24.1 -Shapely==1.8.5.post1 -tomli==2.0.2 -Rtree==1.3.0 -fiona==1.8.21 -Pillow==9.2.0 -sphinx_rtd_theme==3.0.1 -pip==24.2 -pyproj==3.4.0 -sphinx==7.1.2 -scipy==1.8 -pyscaffold -gdal==3.5.0 -tqdm==4.66 - -[testing] -setuptools -pytest -pytest-cov diff --git a/src/rastertools.egg-info/top_level.txt b/src/rastertools.egg-info/top_level.txt deleted file mode 100644 index f91027c..0000000 --- a/src/rastertools.egg-info/top_level.txt +++ /dev/null @@ -1 +0,0 @@ -eolab diff --git a/tests/__pycache__/__init__.cpython-38.pyc b/tests/__pycache__/__init__.cpython-38.pyc index bdf34c836e0255e2347c8438365334b05d525ada..1a0d783f25ab7219d4d622506b760c712f740f14 100644 GIT binary patch delta 71 zcmbQpxPp;8l$V!_0SK1uyf%^B#=uuUBR@A)Kd~$`FF7@@MBh1Bzn~~TE48Fp-#^4j ZKfot6#9u!svA86)s3bo>r+8w5Apiv87j6Im delta 48 zcmZ3%IFXS%l$V!_0SK-T9lp|UsN)&+zg*3 z8v>N~Z+@;`F<+YRHY85lB?mp{qtN2t0I0ByC{Bb;w1b*bo1u)!RIE$eaw2)CSAbQe z&E+;%6HM+VN?l2?hku9lME!THdSX+fDy(Y5Fs*IkG2y|n`?#q$jYQwp6JrM@;<7~Q zp48MH)e~)2u;l4fTl8JJO8nSDDPCAFU$+eQD;hF#aY(4M<~CN@pXhXdE>)^w!}Ehw z4E)FQ`AXPWT?rg+w6M-Tp_@b9aJRpv?4@wUx81c`Q1Jt5kIvc=3F|(M?V0j5yL5A| zJY(N>W9kzchK-or!#^JH0Z##&$P_zJ2bRzwbO=kh3zS{{Nz-vtVB+{6rBJP^Nj^; zAmhIX`UHW!5I>+Idn8%C2b?v>sRcFaIQHw;(mgcxEx3~bfK-i_+{kaNr{d#fn#zsP z_2MjAa+2G8$KeDA9-);d-$R#Tzd??$l5!lKWua0m>_06ty~MT@dfwXg+;f}Em|uw$ z0nR~-x$-omewdzw6hBR|&g`)3GCVQ)1y0bruR>IZ@ zt?(0e69%V1+BlM{F#qvc;nl>4r@stWL}0r`g@Ft$`wS&k(@xIRr9|X{uOg7N*pw5o z&YMyY0M&v3B9}c8n9?UCcv%3|O4IZC*~uFvXKK7WGB#2k0qLq?L~CIXvp=+lxjkZF z1N!N~ufKH6&Ko#Y#+qf;g4`rP#Yq``j5ojTfOnC6g?H#9@Ts9nGO0}X8es{jB1 delta 1039 zcmZvbO-~a+7{_;JxBFJQ0v1Zki%KK9h!#*V8az=6G1{022aUR^c3N86?m9a{NVYMN zPhjTcfmqHYUi=EigW=%C%U(Qq@nXEAF+S5)Q;^-vZ}!=F=9&NO{P%a_Wx`sG#Y_pV z&#$w^?bs{p%L`eAol6WU&=#TjbpVr^h~YrmM!TpXcOuk^6ed@cW+adcC@H~cq!TNh zSS{d&#?&BE(E_!iZR4!;DD@CG^o9}WO+7GnQGnN#QJO2r4eePj(C$gLk_+`B$+PE_ z_$y1c!S~P{|Blj@8LB0>S|IgMu9HoE5#Jsg0DTAZnKidYG9KH?P~y3MrbcLqIF#|X zczUQGQY364OYTLzSiytHLMo;S{tLUrfU(e^Bwuif@E}qmZf&OB%~BzzjX`({wWl@D@z=Z&{z*=tB>yIVMJc|o*vTV_y5p4VW#%{+ z-dU0RNF5b*PV<_YJS$YtQv%}xV*+AjJ4Vycg_u-nAKz2Q&;HdFZl%ui1C`C3 z`#ZBsWh+@or76H9G+!LNj`S!^@i*F&*)A)|9psqx&QA z%lh@{!z$VzVuLbAtRQwIVH3eB9nuB9qmP>ZJa>z~*Uw~{N+1WyE^dIkDx#@z38=-T z2KcFfAX0}VaE}C*!8v}#cpU9IEWTq*OdY3#m5WuOqxA=X6)e<)`Vr0YABH=m!$_zI zJJ{NxU2T|ClgCx_>Ln(siHQ;#f?bUuy3F^@^Am`Q9oa{_8(lBsROQ3bO!^wU3k~p+ zm#-J{K5e-&>aXQJQr)_=xz6Y$uSLhNCLoU{1;l~T1%a%UsHD|_5-PAGa+&}*S1_sZ^ diff --git a/tests/__pycache__/utils4test.cpython-38.pyc b/tests/__pycache__/utils4test.cpython-38.pyc index 0c8ef9224d8e7e1a9c944efec6749529d26faeb4..3670b407b4284cbef6de02df0dd74f8484dbd310 100644 GIT binary patch literal 3214 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 W$K&|9bwBO3+JPT Python - eolab.rastertools.product.s2_maja_mask + eolab.georastertools.product.s2_maja_mask diff --git a/tests/tests_refs/test_rasterproduct/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_V1-9-mask.vrt b/tests/tests_refs/test_rasterproduct/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_V1-9-mask.vrt index 15daaaa..7c8f4ad 100644 --- a/tests/tests_refs/test_rasterproduct/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_V1-9-mask.vrt +++ b/tests/tests_refs/test_rasterproduct/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_V1-9-mask.vrt @@ -132,7 +132,7 @@ Python - eolab.rastertools.product.s2_maja_mask + eolab.georastertools.product.s2_maja_mask diff --git a/tests/tests_refs/test_rasterproduct/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_tar-mask.vrt b/tests/tests_refs/test_rasterproduct/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_tar-mask.vrt index d73c456..8623de8 100644 --- a/tests/tests_refs/test_rasterproduct/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_tar-mask.vrt +++ b/tests/tests_refs/test_rasterproduct/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_tar-mask.vrt @@ -132,7 +132,7 @@ Python - eolab.rastertools.product.s2_maja_mask + eolab.georastertools.product.s2_maja_mask diff --git a/tests/tests_refs/test_rasterproduct/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_targz-mask.vrt b/tests/tests_refs/test_rasterproduct/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_targz-mask.vrt index 7c4bf71..8c9b39f 100644 --- a/tests/tests_refs/test_rasterproduct/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_targz-mask.vrt +++ b/tests/tests_refs/test_rasterproduct/SENTINEL2B_20181023-105107-455_L2A_T30TYP_D_targz-mask.vrt @@ -132,7 +132,7 @@ Python - eolab.rastertools.product.s2_maja_mask + eolab.georastertools.product.s2_maja_mask From 6b42a63728420943b28362b44116642d6ce06485 Mon Sep 17 00:00:00 2001 From: Arthur VINCENT Date: Tue, 17 Dec 2024 15:55:01 +0100 Subject: [PATCH 17/17] CD: from test.pypi to pypi --- .github/workflows/cd.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 39e12cb..424ec28 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -13,8 +13,7 @@ jobs: deploy: runs-on: ubuntu-latest environment: - name: testpypi - url: https://test.pypi.org/p/georastertools + name: pypi steps: - uses: actions/checkout@v4 @@ -41,5 +40,4 @@ jobs: with: user: __token__ verbose: true - password: ${{ secrets.TEST_PYPI_PASSWORD }} - repository-url: https://test.pypi.org/legacy/ + password: ${{ secrets.PYPI_PASSWORD }}