diff --git a/README.md b/README.md index 2200c0f..89a72d4 100644 --- a/README.md +++ b/README.md @@ -1,112 +1,121 @@ -# **FEZrs** - -[![DOI](https://zenodo.org/badge/710286874.svg)](https://doi.org/10.5281/zenodo.14938038) [![Tests](https://github.com/FEZtool-team/FEZrs/actions/workflows/FEZrs_Tests.yml/badge.svg?branch=main)](https://github.com/FEZtool-team/FEZrs/actions/workflows/FEZrs_Tests.yml) [![Documentation](https://img.shields.io/website?url=https%3A%2F%2Ffeztool-team.github.io%2FFEZdocs%2Flibraries%2Ffezrs%2F&label=docs)](https://feztool-team.github.io/FEZdocs/libraries/fezrs/) [![Downloads/month](https://static.pepy.tech/badge/fezrs/month)](https://pepy.tech/project/fezrs) [![PyPI version](https://img.shields.io/pypi/v/fezrs.svg)](https://pypi.org/project/fezrs/) -[![Conda Version](https://img.shields.io/conda/vn/FEZtool/fezrs?label=Anaconda&color=orange&logo=anaconda)](https://anaconda.org/FEZtool/fezrs) ![License](https://img.shields.io/pypi/l/FEZrs) [![PyPI Downloads](https://static.pepy.tech/badge/fezrs)](https://pepy.tech/projects/fezrs) ![GitHub last commit](https://img.shields.io/github/last-commit/FEZtool-team/fezrs) [![Platform](https://img.shields.io/conda/pn/feztool/fezrs?color=blue&label=Platform&style=flat)](https://anaconda.org/feztool/fezrs) ![GitHub stars](https://img.shields.io/github/stars/FEZtool-team/FEZrs?style=social) - -**FEZrs** is an advanced Python library developed by [**FEZtool**](https://feztool.com/) for Remote Sensing applications. It provides a set of powerful tools for image processing, feature extraction, and analysis of geospatial data. - -## **Features** - -✅ Apply various image filtering techniques (Gaussian, Laplacian, Sobel, Median, Mean) -✅ Contrast enhancement and edge detection -✅ Support for geospatial raster data (TIFF) -✅ Designed for remote sensing and satellite imagery analysis -✅ Easy integration with FastAPI for web-based processing - -## Documentation - -Full documentation, API references, tutorials, and examples are available here: -https://feztool-team.github.io/FEZdocs/libraries/fezrs/ - -## **📦 Installation** - -You can install **FEZrs** using your preferred Python package manager: - -### Using `pip` (PyPI) - -```bash -pip install fezrs -``` - -### Using `conda` (Anaconda) - -```bash -conda install -c FEZtool fezrs -``` - -### Using `mamba` (optional, faster conda alternative) - -```bash -mamba install FEZtool::fezrs -``` - -> **Note:** The `mamba` command requires [Mamba](https://github.com/mamba-org/mamba) to be installed. If it's not installed, use the `conda` command instead. - -## **Usage** - -Example of applying a Gaussian filter to an image: - -```python -from fezrs import EqualizeRGBCalculator - -equalize = EqualizeRGBCalculator( - blue_path="path/to/your/image_band.tif", - green_path="path/to/your/image_band.tif", - red_path="path/to/your/image_band.tif", -) - -equalize.chart_export(output_path="./your/export/path") -equalize.execute(output_path="./your/export/path") -``` - -## **Modules** - -- `KMeansCalculator` -- `GaussianCalculator` -- `LaplacianCalculator` -- `MeanCalculator` -- `MedianCalculator` -- `SobelCalculator` -- `GLCMCalculator` -- `HSVCalculator` -- `IRHSVCalculator` -- `AdaptiveCalculator` -- `AdaptiveRGBCalculator` -- `EqualizeCalculator` -- `EqualizeRGBCalculator` -- `FloatCalculator` -- `GammaCalculator` -- `GammaRGBCalculator` -- `LogAdjustCalculator` -- `OriginalCalculator` -- `OriginalRGBCalculator` -- `SigmoidAdjustCalculator` -- `PCACalculator` -- `AFRICalculator` -- `BICalculator` -- `NDVICalculator` -- `NDWICalculator` -- `SAVICalculator` -- `UICalculator` -- `SpectralProfileCalculator` - -## **Contributing** - -We welcome contributions! To contribute: - -1. Fork the repository -2. Create a new branch (`git checkout -b feature-name`) -3. Commit your changes (`git commit -m "Add new feature"`) -4. Push to your branch (`git push origin feature-name`) -5. Open a Pull Request - -## **Acknowledgment** - -Special thanks to [**Chakad Cafe**](https://www.chakadcoffee.com/) for the coffee that kept us fueled during development! ☕ - -## **License** - -This project is licensed under the [**Apache-2.0 license**.](https://github.com/FEZtool-team/FEZrs/edit/main/LICENSE) - - - +# **FEZrs** + +[![DOI](https://zenodo.org/badge/710286874.svg)](https://doi.org/10.5281/zenodo.14938038) [![Tests](https://github.com/FEZtool-team/FEZrs/actions/workflows/FEZrs_Tests.yml/badge.svg?branch=main)](https://github.com/FEZtool-team/FEZrs/actions/workflows/FEZrs_Tests.yml) [![Documentation](https://img.shields.io/website?url=https%3A%2F%2Ffeztool-team.github.io%2FFEZdocs%2Flibraries%2Ffezrs%2F&label=docs)](https://feztool-team.github.io/FEZdocs/libraries/fezrs/) [![Downloads/month](https://static.pepy.tech/badge/fezrs/month)](https://pepy.tech/project/fezrs) [![PyPI version](https://img.shields.io/pypi/v/fezrs.svg)](https://pypi.org/project/fezrs/) +[![Conda Version](https://img.shields.io/conda/vn/FEZtool/fezrs?label=Anaconda&color=orange&logo=anaconda)](https://anaconda.org/FEZtool/fezrs) ![License](https://img.shields.io/pypi/l/FEZrs) [![PyPI Downloads](https://static.pepy.tech/badge/fezrs)](https://pepy.tech/projects/fezrs) ![GitHub last commit](https://img.shields.io/github/last-commit/FEZtool-team/fezrs) [![Platform](https://img.shields.io/conda/pn/feztool/fezrs?color=blue&label=Platform&style=flat)](https://anaconda.org/feztool/fezrs) ![GitHub stars](https://img.shields.io/github/stars/FEZtool-team/FEZrs?style=social) + +**FEZrs** is an advanced Python library developed by [**FEZtool**](https://github.com/FEZtool-team) for Remote Sensing applications. It provides a set of powerful tools for image processing, feature extraction, and analysis of geospatial data. + +## **Features** + +✅ Apply various image filtering techniques (Gaussian, Laplacian, Sobel, Median, Mean) +✅ Contrast enhancement and edge detection +✅ Support for geospatial raster data (TIFF) +✅ Designed for remote sensing and satellite imagery analysis +✅ Easy integration with FastAPI for web-based processing + +## Documentation + +Full documentation, API references, tutorials, and examples are available here: +https://feztool-team.github.io/FEZdocs/libraries/fezrs/ + +## **📦 Installation** + +You can install **FEZrs** using your preferred Python package manager: + +### Using `pip` (PyPI) + +```bash +pip install fezrs +``` + +### Using `conda` (Anaconda) + +```bash +conda install -c FEZtool fezrs +``` + +### Using `mamba` (optional, faster conda alternative) + +```bash +mamba install FEZtool::fezrs +``` + +> **Note:** The `mamba` command requires [Mamba](https://github.com/mamba-org/mamba) to be installed. If it's not installed, use the `conda` command instead. + +## **Usage** + +Example of applying a Gaussian filter to an image: + +```python +from fezrs import EqualizeRGBCalculator + +equalize = EqualizeRGBCalculator( + blue_path="path/to/your/image_band.tif", + green_path="path/to/your/image_band.tif", + red_path="path/to/your/image_band.tif", +) + +equalize.execute(output_path="./your/export/path") +equalize.histogram_export(output_path="./your/export/path") +``` + +## **Modules** + +- `KMeansCalculator` +- `GaussianCalculator` +- `LaplacianCalculator` +- `MeanCalculator` +- `MedianCalculator` +- `SobelCalculator` +- `GLCMCalculator` +- `HSVCalculator` +- `IRHSVCalculator` +- `AdaptiveCalculator` +- `AdaptiveRGBCalculator` +- `EqualizeCalculator` +- `EqualizeRGBCalculator` +- `FloatCalculator` +- `GammaCalculator` +- `GammaRGBCalculator` +- `LogAdjustCalculator` +- `OriginalCalculator` +- `OriginalRGBCalculator` +- `SigmoidAdjustCalculator` +- `PCACalculator` +- `AFRICalculator` +- `BICalculator` +- `NDVICalculator` +- `NDWICalculator` +- `SAVICalculator` +- `UICalculator` +- `SpectralProfileCalculator` +- `MosaicCalculator` +- `SVMCalculator` +- `Landsat8Calculator` +- `GeoeyeCalculator` +- `BurnCalculator` +- `IndicesCalculator` +- `MagDirCalculator` +- `SubDivCalculator` +- `TimeCalculator` + +## **Contributing** + +We welcome contributions! To contribute: + +1. Fork the repository +2. Create a new branch (`git checkout -b feature-name`) +3. Commit your changes (`git commit -m "Add new feature"`) +4. Push to your branch (`git push origin feature-name`) +5. Open a Pull Request + +## **Acknowledgment** + +Special thanks to [**Chakad Cafe**](https://www.chakadcoffee.com/) for the coffee that kept us fueled during development! ☕ + +## **License** + +This project is licensed under the [**Apache-2.0 license**.](https://github.com/FEZtool-team/FEZrs/edit/main/LICENSE) + + + diff --git a/docs/filters.md b/docs/filters.md index d7a1044..6fca6d9 100644 --- a/docs/filters.md +++ b/docs/filters.md @@ -15,7 +15,7 @@ The `filters` module provides a comprehensive suite of digital image processing ▼ ▼ [Linear Shift-Invariant Filters] [Non-Linear Statistical Filters] ├─ MeanCalculator └─ MedianCalculator -├─ GuassianCalculator +├─ GaussianCalculator ├─ SobelCalculator └─ LaplacianCalculator ``` @@ -54,11 +54,11 @@ This approach minimizes edge artifacts and prevents artificial gradient boundari ## Comprehensive Class Specifications -### GuassianCalculator` — Isotropic Low-Pass Smoothing +### GaussianCalculator` — Isotropic Low-Pass Smoothing #### Scientific & Physical Objective -The algorithmic goal of `GuassianCalculator` is to apply an isotropic low-pass filter to remove continuous high-frequency background noise (such as thermal electronic sensor noise or atmospheric scattering). This smoothing step helps maintain structural boundaries and edge locations more effectively than a standard unweighted box-average filter. +The algorithmic goal of `GaussianCalculator` is to apply an isotropic low-pass filter to remove continuous high-frequency background noise (such as thermal electronic sensor noise or atmospheric scattering). This smoothing step helps maintain structural boundaries and edge locations more effectively than a standard unweighted box-average filter. #### Theoretical Foundation & Mathematical Formulations @@ -102,10 +102,10 @@ In the frequency domain, the Fourier transform of a Gaussian kernel is itself a ```Python from pathlib import Path -from fezrs.tools.filters import GuassianCalculator +from fezrs.tools.filters import GaussianCalculator # Initialize low-pass isotropic Gaussian engine -gaussian_blur = GuassianCalculator( +gaussian_blur = GaussianCalculator( tif_path=Path("./data/Landsat8_Band5.tif") ) diff --git a/docs/image-enhancement.md b/docs/image-enhancement.md index bd0105d..1d719b0 100644 --- a/docs/image-enhancement.md +++ b/docs/image-enhancement.md @@ -10,7 +10,7 @@ The `enhancement` module delivers a comprehensive suite of linear and non-linear │ ▼ ┌───────────────────────────────────────┐ - │ fezrs.tools.enhancement Module │ + │ fezrs.tools.image_enhancement Module │ └───────────────────┬───────────────────┘ │ ┌────────────────────────────────┴────────────────────────────────┐ @@ -96,7 +96,7 @@ $$I_{\text{eq}}(x, y) = \frac{1}{N} \sum_{j=1}^{\text{rank}(I(x, y))} \text{coun ```Python from pathlib import Path -from fezrs.tools.enhancement import EqualizeCalculator +from fezrs.tools.image_enhancement import EqualizeCalculator # Initialize global histogram equalization pipeline equalizer = EqualizeCalculator(nir_path=Path("./data/Hazy_NIR.tif")) @@ -145,7 +145,7 @@ $$I_{\text{CLAHE}}(x, y) = \text{BilinearInterpolate}\left(T_{t_1}, T_{t_2}, T_{ ```Python from pathlib import Path -from fezrs.tools.enhancement import AdaptiveCalculator +from fezrs.tools.image_enhancement import AdaptiveCalculator # Execute local adaptive contrast optimization via CLAHE clahe_engine = AdaptiveCalculator( @@ -195,7 +195,7 @@ Where: ```Python from pathlib import Path -from fezrs.tools.enhancement import GammaCalculator +from fezrs.tools.image_enhancement import GammaCalculator # Initialize power-law gamma transformation engine gamma_corrector = GammaCalculator( diff --git a/docs/pca.md b/docs/pca.md index ccd3c4a..56fbbd1 100644 --- a/docs/pca.md +++ b/docs/pca.md @@ -127,7 +127,25 @@ These higher-order components capture progressively smaller variations in the da - `swir2_path` (`str` | `Path`): File path to the Short-Wave Infrared 2 band raster layer. - - `selectBand` (`Literal["red","green","blue","nir","swir1","swir2", None]`): Optional parameter. Selects a specific input band to map against the component outputs during specialized diagnostic profiling. + - `component` (`int | None`): Principal component to inspect, numbered $1 \dots 6$ in order of decreasing explained variance. Required by `histogram_export()`. + + - `standardize` (`bool`, default `False`): Decompose the **correlation** matrix instead of the covariance matrix, by scaling each band to unit variance first. `sklearn` decomposes the covariance matrix, which lets whichever band carries the widest digital-number range dominate the leading components regardless of how much information it holds. Standardized PCA is the usual choice for multispectral work where band ranges differ substantially. + + - `selectBand` (`Literal["red","green","blue","nir","swir1","swir2", None]`): **Deprecated.** A band name that resolves to a fixed component index, emitting a `DeprecationWarning` naming the component it maps to. + + > **Why `selectBand` was replaced.** A principal component is a linear combination of *all six* input bands, weighted by the corresponding eigenvector, so **no component corresponds to an input band**. The parameter indexed into a fixed name-to-index map, meaning `selectBand="red"` plotted the **first principal component** and titled the figure "Histogram of PCA Band Red" — attributing the output to a band that did not produce it. A user selecting `"swir2"` to inspect the SWIR2 response was shown the fifth component. The mapping order was also arbitrary: it followed the insertion order of the band dictionary in `FileHandler.__init__`, which is why it read `red, nir, blue, swir1, swir2, green` rather than any order a user would expect. + +#### Eigen-structure Accessors + +Both raise `RuntimeError` before `process()` has run. + +- `explained_variance_ratio_` (`np.ndarray`, shape `(6,)`): Share of total variance per component, ordered decreasing. On the bundled Landsat subset PC1 carries 90.6%, PC2 7.3% and PC3 1.8% — the strong concentration typical of multispectral imagery, where PC1 largely captures scene brightness. + +- `components_` (`np.ndarray`, shape `(6, 6)` as `(component, band)`): Eigenvector loadings, with band order given by `band_order`. + + **Loadings, not variance share, are what identify a useful component.** A target is isolated by the component in which the diagnostic bands carry high loadings of *opposing* sign, and that component is frequently not the one with the most variance — PC1 usually just encodes albedo. + +> **Component signs are fixed.** Eigenvector signs are mathematically arbitrary, so without a convention the same scene can yield an inverted component image between runs, or between a scene and a crop of it. FEZrs forces the largest-magnitude loading of each component positive, making repeated runs directly comparable. #### Processing Pipeline Lifecycle (`process()`) diff --git a/docs/spectral-indices.md b/docs/spectral-indices.md index dbae6e6..c6bf71a 100644 --- a/docs/spectral-indices.md +++ b/docs/spectral-indices.md @@ -232,7 +232,7 @@ The following matrix cross-references the required sensor channels, target range ```Python from pathlib import Path -from fezrs.tools.indices import NDVICalculator +from fezrs.tools.spectral_indices import NDVICalculator # Instantiate the NDVI processing engine using Landsat 8 paths ndvi_engine = NDVICalculator( @@ -253,7 +253,7 @@ ndvi_engine.execute( ```Python from pathlib import Path -from fezrs.tools.indices import NDWICalculator +from fezrs.tools.spectral_indices import NDWICalculator # Instantiate McFeeters NDWI calculator using Sentinel-2 paths ndwi_engine = NDWICalculator( diff --git a/example/clustering.py b/example/clustering.py index c77e3be..087ea4a 100644 --- a/example/clustering.py +++ b/example/clustering.py @@ -1,5 +1,18 @@ +""" +K-means clustering example. + +Paths resolve relative to this file, so the script runs from any directory. +""" + +from pathlib import Path + from fezrs import KMeansCalculator -KMeansCalculator(nir_path="./data/nir.tif", n_clusters=4, random_state=0).execute( - output_path="./outputs/clustering", +DATA = Path(__file__).parent / "data" +OUTPUTS = Path(__file__).parent / "outputs" + +KMeansCalculator( + nir_path=DATA / "nir.tif", n_clusters=4, random_state=0 +).execute( + output_path=OUTPUTS / "clustering", ) diff --git a/example/filters.py b/example/filters.py index 5d43132..701edc4 100644 --- a/example/filters.py +++ b/example/filters.py @@ -1,3 +1,11 @@ +""" +Spatial filtering examples. + +Paths resolve relative to this file, so the script runs from any directory. +""" + +from pathlib import Path + from fezrs import ( GaussianCalculator, LaplacianCalculator, @@ -6,24 +14,18 @@ SobelCalculator, ) -GaussianCalculator( - tif_path="./data/pan_img.tif", -).execute( - output_path="./outputs/filter", -) +DATA = Path(__file__).parent / "data" +OUTPUTS = Path(__file__).parent / "outputs" -LaplacianCalculator(tif_path="./data/pan_img.tif", kernel_size=7).execute( - output_path="./outputs/filter", -) +PAN = DATA / "pan_img.tif" +FILTER_OUTPUT = OUTPUTS / "filter" -MeanCalculator(tif_path="./data/pan_img.tif").execute( - output_path="./outputs/filter", -) +GaussianCalculator(tif_path=PAN).execute(output_path=FILTER_OUTPUT) -MedianCalculator(tif_path="./data/pan_img.tif", kernel_size=5).execute( - output_path="./outputs/filter", -) +LaplacianCalculator(tif_path=PAN, kernel_size=7).execute(output_path=FILTER_OUTPUT) -SobelCalculator(tif_path="./data/pan_img.tif", kernel_size=7).execute( - output_path="./outputs/filter", -) +MeanCalculator(tif_path=PAN).execute(output_path=FILTER_OUTPUT) + +MedianCalculator(tif_path=PAN, kernel_size=5).execute(output_path=FILTER_OUTPUT) + +SobelCalculator(tif_path=PAN, kernel_size=7).execute(output_path=FILTER_OUTPUT) diff --git a/example/hsv.py b/example/hsv.py index 40cbc90..03d953f 100644 --- a/example/hsv.py +++ b/example/hsv.py @@ -1,12 +1,23 @@ +""" +HSV colour-space example. + +Paths resolve relative to this file, so the script runs from any directory. +""" + +from pathlib import Path + from fezrs import HSVCalculator +DATA = Path(__file__).parent / "data" +OUTPUTS = Path(__file__).parent / "outputs" + HSVCalculator( - blue_path="./data/blue.tif", - green_path="./data/green.tif", - nir_path="./data/nir.tif", + blue_path=DATA / "blue.tif", + green_path=DATA / "green.tif", + nir_path=DATA / "nir.tif", channel="hsv", ).execute( - output_path="./outputs/hsv", + output_path=OUTPUTS / "hsv", show_axis=False, show_colorbar=False, ) diff --git a/example/outputs/clustering/KMeans_output_f7e25948ead540c6a314ae17315db781.png b/example/outputs/clustering/KMeans_output_f7e25948ead540c6a314ae17315db781.png new file mode 100644 index 0000000..e8e5d17 Binary files /dev/null and b/example/outputs/clustering/KMeans_output_f7e25948ead540c6a314ae17315db781.png differ diff --git a/example/outputs/filter/Gaussian_output_68ccd636feb748d38609471b3793fa0c.png b/example/outputs/filter/Gaussian_output_68ccd636feb748d38609471b3793fa0c.png new file mode 100644 index 0000000..f801c4d Binary files /dev/null and b/example/outputs/filter/Gaussian_output_68ccd636feb748d38609471b3793fa0c.png differ diff --git a/example/outputs/filter/Laplacian_output_d293e08b11564d3e8703e29d0ec13fe6.png b/example/outputs/filter/Laplacian_output_d293e08b11564d3e8703e29d0ec13fe6.png new file mode 100644 index 0000000..8624e6c Binary files /dev/null and b/example/outputs/filter/Laplacian_output_d293e08b11564d3e8703e29d0ec13fe6.png differ diff --git a/example/outputs/filter/Mean_output_a0f8a7967af14fa19eeed1b7cba2ef0b.png b/example/outputs/filter/Mean_output_a0f8a7967af14fa19eeed1b7cba2ef0b.png new file mode 100644 index 0000000..bc05e7e Binary files /dev/null and b/example/outputs/filter/Mean_output_a0f8a7967af14fa19eeed1b7cba2ef0b.png differ diff --git a/example/outputs/filter/Median_output_154b7b33f7014fd69d6751a9a4489b72.png b/example/outputs/filter/Median_output_154b7b33f7014fd69d6751a9a4489b72.png new file mode 100644 index 0000000..33fe2cc Binary files /dev/null and b/example/outputs/filter/Median_output_154b7b33f7014fd69d6751a9a4489b72.png differ diff --git a/example/outputs/filter/Sobel_output_1cc857f7a0fa403b97177d4dc2c30991.png b/example/outputs/filter/Sobel_output_1cc857f7a0fa403b97177d4dc2c30991.png new file mode 100644 index 0000000..acbe373 Binary files /dev/null and b/example/outputs/filter/Sobel_output_1cc857f7a0fa403b97177d4dc2c30991.png differ diff --git a/example/outputs/hsv/HSV_output_4c01ba01d6a44263bd75d8b6c859bb0a.png b/example/outputs/hsv/HSV_output_4c01ba01d6a44263bd75d8b6c859bb0a.png new file mode 100644 index 0000000..bcecc26 Binary files /dev/null and b/example/outputs/hsv/HSV_output_4c01ba01d6a44263bd75d8b6c859bb0a.png differ diff --git a/example/outputs/pca/Histogram_PCA_Tool_output_7c08f58900734cb8b9057156a3fd76a7.png b/example/outputs/pca/Histogram_PCA_Tool_output_7c08f58900734cb8b9057156a3fd76a7.png new file mode 100644 index 0000000..ffb1648 Binary files /dev/null and b/example/outputs/pca/Histogram_PCA_Tool_output_7c08f58900734cb8b9057156a3fd76a7.png differ diff --git a/example/outputs/pca/None_837cd8314d254e3a9b80cf7a945d37c0.png b/example/outputs/pca/None_837cd8314d254e3a9b80cf7a945d37c0.png new file mode 100644 index 0000000..87923b7 Binary files /dev/null and b/example/outputs/pca/None_837cd8314d254e3a9b80cf7a945d37c0.png differ diff --git a/example/pca.py b/example/pca.py index 9e7e19a..f2987c4 100644 --- a/example/pca.py +++ b/example/pca.py @@ -1,14 +1,30 @@ +""" +Principal component analysis example. + +Paths resolve relative to this file, so the script runs from any directory. +""" + from pathlib import Path + from fezrs import PCACalculator -PCACalculator( - blue_path= Path.cwd() / "./example/data/blue.tif", - green_path= Path.cwd() / "./example/data/green.tif", - red_path= Path.cwd() / "./example/data/red.tif", - nir_path= Path.cwd() / "./example/data/nir.tif", - swir1_path= Path.cwd() / "./example/data/swir_1.tif", - swir2_path= Path.cwd() / "./example/data/swir_2.tif", - selectBand="blue", -).execute( - output_path=Path.cwd() /"./example/outputs/pca", -).histogram_export(output_path=Path.cwd() /"./example/outputs/pca") +DATA = Path(__file__).parent / "data" +OUTPUTS = Path(__file__).parent / "outputs" + +calculator = PCACalculator( + blue_path=DATA / "blue.tif", + green_path=DATA / "green.tif", + red_path=DATA / "red.tif", + nir_path=DATA / "nir.tif", + swir1_path=DATA / "swir_1.tif", + swir2_path=DATA / "swir_2.tif", + component=1, +) + +calculator.execute(output_path=OUTPUTS / "pca") +calculator.histogram_export(output_path=OUTPUTS / "pca") + +# Variance share and band loadings are what decide which component is worth +# inspecting -- the largest component is not always the informative one. +for index, share in enumerate(calculator.explained_variance_ratio_, start=1): + print(f"PC{index}: {share:.1%} of total variance") diff --git a/fezrs/__init__.py b/fezrs/__init__.py index 00e5cc4..f830c5e 100644 --- a/fezrs/__init__.py +++ b/fezrs/__init__.py @@ -44,8 +44,14 @@ MosaicCalculator, ) -from fezrs.tools.import_tools.geoeye_calculator import Geoeye_Calculator -from fezrs.tools.import_tools.landsat8_calculator import Landsat8_Calculator +from fezrs.tools.import_tools.geoeye_calculator import ( + Geoeye_Calculator, + GeoeyeCalculator, +) +from fezrs.tools.import_tools.landsat8_calculator import ( + Landsat8_Calculator, + Landsat8Calculator, +) from fezrs.tools.change_detection.burn_calculator import BurnCalculator from fezrs.tools.change_detection.indices_calculator import IndicesCalculator @@ -87,7 +93,9 @@ "SpectralProfileCalculator", "MosaicCalculator", "Geoeye_Calculator", + "GeoeyeCalculator", "Landsat8_Calculator", + "Landsat8Calculator", "BurnCalculator", "IndicesCalculator", "MagDirCalculator", diff --git a/fezrs/base.py b/fezrs/base.py index 73eb7db..b8e55b6 100644 --- a/fezrs/base.py +++ b/fezrs/base.py @@ -74,7 +74,7 @@ def _export_file( show_axis: bool = False, colormap: str = None, show_colorbar: bool = False, - filename_prefix: str = "Tool_output", + filename_prefix: str | None = None, dpi: int = 500, bbox_inches: str = "tight", grid: bool = True, @@ -101,7 +101,11 @@ def _export_file( Returns: The path to the saved image file. """ - filename_prefix = self.__tool_name + # Fall back to the tool name only when the caller did not choose a + # prefix. Previously the argument was accepted, documented, and then + # overwritten on this line before it was ever used. + if filename_prefix is None: + filename_prefix = self.__tool_name # Check output property is not empty if self._output is None: @@ -144,7 +148,7 @@ def execute( show_axis: bool = False, colormap: str = None, show_colorbar: bool = False, - filename_prefix: str = "Tool_output", + filename_prefix: str | None = None, dpi: int = 500, bbox_inches: str = "tight", grid: bool = True, @@ -184,5 +188,9 @@ def execute( dpi, bbox_inches, grid, + # Forwarded rather than dropped: these were declared and documented + # on execute() but never reached plt.subplots(). + 1 if nrows is None else nrows, + 1 if ncols is None else ncols, ) return self diff --git a/fezrs/media/__init__.py b/fezrs/media/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fezrs/tools/change_detection/__init__.py b/fezrs/tools/change_detection/__init__.py new file mode 100644 index 0000000..2627e68 --- /dev/null +++ b/fezrs/tools/change_detection/__init__.py @@ -0,0 +1,5 @@ +from .burn_calculator import BurnCalculator +from .indices_calculator import IndicesCalculator +from .magdir_calculator import MagDirCalculator +from .subdiv_calculator import SubDivCalculator +from .time_calculator import TimeCalculator diff --git a/fezrs/tools/change_detection/burn_calculator.py b/fezrs/tools/change_detection/burn_calculator.py index 0216ebf..7c19fca 100644 --- a/fezrs/tools/change_detection/burn_calculator.py +++ b/fezrs/tools/change_detection/burn_calculator.py @@ -50,7 +50,7 @@ def execute( show_axis=True, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=500, bbox_inches="tight", grid=False, diff --git a/fezrs/tools/change_detection/indices_calculator.py b/fezrs/tools/change_detection/indices_calculator.py index 22c3fd0..e34c71e 100644 --- a/fezrs/tools/change_detection/indices_calculator.py +++ b/fezrs/tools/change_detection/indices_calculator.py @@ -71,7 +71,7 @@ def execute( show_axis=False, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=500, bbox_inches="tight", grid=False, diff --git a/fezrs/tools/change_detection/magdir_calculator.py b/fezrs/tools/change_detection/magdir_calculator.py index ebeda1b..ac45155 100644 --- a/fezrs/tools/change_detection/magdir_calculator.py +++ b/fezrs/tools/change_detection/magdir_calculator.py @@ -118,7 +118,7 @@ def execute( show_axis=True, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=500, bbox_inches="tight", grid=False, diff --git a/fezrs/tools/change_detection/subdiv_calculator.py b/fezrs/tools/change_detection/subdiv_calculator.py index ea4f1a0..990dbf5 100644 --- a/fezrs/tools/change_detection/subdiv_calculator.py +++ b/fezrs/tools/change_detection/subdiv_calculator.py @@ -57,7 +57,7 @@ def execute( show_axis=False, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=500, bbox_inches="tight", grid=False, diff --git a/fezrs/tools/change_detection/time_calculator.py b/fezrs/tools/change_detection/time_calculator.py index 341b5f3..8af55c2 100644 --- a/fezrs/tools/change_detection/time_calculator.py +++ b/fezrs/tools/change_detection/time_calculator.py @@ -47,7 +47,7 @@ def execute( show_axis=True, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=500, bbox_inches="tight", grid=False, diff --git a/fezrs/tools/clustering/kmeans_calculator.py b/fezrs/tools/clustering/kmeans_calculator.py index 7061675..3b92b0e 100644 --- a/fezrs/tools/clustering/kmeans_calculator.py +++ b/fezrs/tools/clustering/kmeans_calculator.py @@ -101,10 +101,12 @@ def execute( show_axis=False, colormap=None, show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=500, bbox_inches="tight", grid=False, + nrows=None, + ncols=None, ): return super().execute( output_path, @@ -117,4 +119,6 @@ def execute( dpi, bbox_inches, grid, + nrows, + ncols, ) diff --git a/fezrs/tools/filters/gaussian_calculator.py b/fezrs/tools/filters/gaussian_calculator.py index 060dd23..52e3b8f 100644 --- a/fezrs/tools/filters/gaussian_calculator.py +++ b/fezrs/tools/filters/gaussian_calculator.py @@ -35,10 +35,12 @@ def execute( show_axis=False, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=1000, bbox_inches="tight", grid=False, + nrows=None, + ncols=None, ): return super().execute( output_path, @@ -51,4 +53,6 @@ def execute( dpi, bbox_inches, grid, + nrows, + ncols, ) diff --git a/fezrs/tools/filters/laplacian_calculator.py b/fezrs/tools/filters/laplacian_calculator.py index dd030a3..d0dc0f8 100644 --- a/fezrs/tools/filters/laplacian_calculator.py +++ b/fezrs/tools/filters/laplacian_calculator.py @@ -64,10 +64,12 @@ def execute( show_axis=False, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=1000, bbox_inches="tight", grid=False, + nrows=None, + ncols=None, ): return super().execute( output_path, @@ -80,4 +82,6 @@ def execute( dpi, bbox_inches, grid, + nrows, + ncols, ) diff --git a/fezrs/tools/filters/mean_calculator.py b/fezrs/tools/filters/mean_calculator.py index 9500e9a..5585e1e 100644 --- a/fezrs/tools/filters/mean_calculator.py +++ b/fezrs/tools/filters/mean_calculator.py @@ -33,10 +33,12 @@ def execute( show_axis=False, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=1000, bbox_inches="tight", grid=False, + nrows=None, + ncols=None, ): return super().execute( output_path, @@ -49,4 +51,6 @@ def execute( dpi, bbox_inches, grid, + nrows, + ncols, ) diff --git a/fezrs/tools/filters/median_calculator.py b/fezrs/tools/filters/median_calculator.py index 861449d..7fdc8aa 100644 --- a/fezrs/tools/filters/median_calculator.py +++ b/fezrs/tools/filters/median_calculator.py @@ -64,10 +64,12 @@ def execute( show_axis=False, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=1000, bbox_inches="tight", grid=False, + nrows=None, + ncols=None, ): return super().execute( output_path, @@ -80,4 +82,6 @@ def execute( dpi, bbox_inches, grid, + nrows, + ncols, ) diff --git a/fezrs/tools/filters/sobel_calculator.py b/fezrs/tools/filters/sobel_calculator.py index 14ba79d..8edd838 100644 --- a/fezrs/tools/filters/sobel_calculator.py +++ b/fezrs/tools/filters/sobel_calculator.py @@ -68,10 +68,12 @@ def execute( show_axis=False, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=1000, bbox_inches="tight", grid=False, + nrows=None, + ncols=None, ): return super().execute( output_path, @@ -84,4 +86,6 @@ def execute( dpi, bbox_inches, grid, + nrows, + ncols, ) diff --git a/fezrs/tools/glcm/glcm_calculator.py b/fezrs/tools/glcm/glcm_calculator.py index cfd5551..df20ac6 100644 --- a/fezrs/tools/glcm/glcm_calculator.py +++ b/fezrs/tools/glcm/glcm_calculator.py @@ -220,7 +220,7 @@ def execute( show_axis=False, colormap=None, show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=500, bbox_inches="tight", grid=False, diff --git a/fezrs/tools/hsv/hsv_calculator.py b/fezrs/tools/hsv/hsv_calculator.py index b97af42..eb7f952 100644 --- a/fezrs/tools/hsv/hsv_calculator.py +++ b/fezrs/tools/hsv/hsv_calculator.py @@ -68,10 +68,12 @@ def execute( show_axis=True, colormap=None, show_colorbar=True, - filename_prefix="Tool_output", + filename_prefix=None, dpi=1000, bbox_inches="tight", grid=False, + nrows=None, + ncols=None, ): return super().execute( output_path, @@ -84,4 +86,6 @@ def execute( dpi, bbox_inches, grid, + nrows, + ncols, ) diff --git a/fezrs/tools/hsv/irhsv_calculator.py b/fezrs/tools/hsv/irhsv_calculator.py index aa7ef24..a922ee7 100644 --- a/fezrs/tools/hsv/irhsv_calculator.py +++ b/fezrs/tools/hsv/irhsv_calculator.py @@ -67,10 +67,12 @@ def execute( show_axis=True, colormap=None, show_colorbar=True, - filename_prefix="Tool_output", + filename_prefix=None, dpi=1000, bbox_inches="tight", grid=False, + nrows=None, + ncols=None, ): return super().execute( output_path, @@ -83,4 +85,6 @@ def execute( dpi, bbox_inches, grid, + nrows, + ncols, ) diff --git a/fezrs/tools/image_enhancement/adaptive_calculator.py b/fezrs/tools/image_enhancement/adaptive_calculator.py index 3d9a725..8089048 100644 --- a/fezrs/tools/image_enhancement/adaptive_calculator.py +++ b/fezrs/tools/image_enhancement/adaptive_calculator.py @@ -75,7 +75,7 @@ def execute( show_axis=False, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=500, bbox_inches="tight", grid=True, diff --git a/fezrs/tools/image_enhancement/adaptive_rgb_calculator.py b/fezrs/tools/image_enhancement/adaptive_rgb_calculator.py index 0ec9545..7e64827 100644 --- a/fezrs/tools/image_enhancement/adaptive_rgb_calculator.py +++ b/fezrs/tools/image_enhancement/adaptive_rgb_calculator.py @@ -91,7 +91,7 @@ def execute( show_axis=False, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=500, bbox_inches="tight", grid=True, diff --git a/fezrs/tools/image_enhancement/equalize_calculator.py b/fezrs/tools/image_enhancement/equalize_calculator.py index 5b60b0a..df948ff 100644 --- a/fezrs/tools/image_enhancement/equalize_calculator.py +++ b/fezrs/tools/image_enhancement/equalize_calculator.py @@ -67,7 +67,7 @@ def execute( show_axis=False, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=500, bbox_inches="tight", grid=False, diff --git a/fezrs/tools/image_enhancement/equalize_rgb_calculator.py b/fezrs/tools/image_enhancement/equalize_rgb_calculator.py index 80a2a95..c34fd31 100644 --- a/fezrs/tools/image_enhancement/equalize_rgb_calculator.py +++ b/fezrs/tools/image_enhancement/equalize_rgb_calculator.py @@ -91,7 +91,7 @@ def execute( show_axis=False, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=500, bbox_inches="tight", grid=True, diff --git a/fezrs/tools/image_enhancement/float_calculator.py b/fezrs/tools/image_enhancement/float_calculator.py index 768c4f6..20567f4 100644 --- a/fezrs/tools/image_enhancement/float_calculator.py +++ b/fezrs/tools/image_enhancement/float_calculator.py @@ -58,7 +58,7 @@ def execute( show_axis=False, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=500, bbox_inches="tight", grid=True, diff --git a/fezrs/tools/image_enhancement/gamma_calculator.py b/fezrs/tools/image_enhancement/gamma_calculator.py index c305e81..3c18e01 100644 --- a/fezrs/tools/image_enhancement/gamma_calculator.py +++ b/fezrs/tools/image_enhancement/gamma_calculator.py @@ -82,7 +82,7 @@ def execute( show_axis=False, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=500, bbox_inches="tight", grid=False, diff --git a/fezrs/tools/image_enhancement/gamma_rgb_calculator.py b/fezrs/tools/image_enhancement/gamma_rgb_calculator.py index c18a832..5e49e82 100644 --- a/fezrs/tools/image_enhancement/gamma_rgb_calculator.py +++ b/fezrs/tools/image_enhancement/gamma_rgb_calculator.py @@ -87,7 +87,7 @@ def execute( show_axis=False, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=500, bbox_inches="tight", grid=True, diff --git a/fezrs/tools/image_enhancement/log_adjust_calculator.py b/fezrs/tools/image_enhancement/log_adjust_calculator.py index fb112e6..69d1438 100644 --- a/fezrs/tools/image_enhancement/log_adjust_calculator.py +++ b/fezrs/tools/image_enhancement/log_adjust_calculator.py @@ -75,7 +75,7 @@ def execute( show_axis=False, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=500, bbox_inches="tight", grid=False, diff --git a/fezrs/tools/image_enhancement/original_calculator.py b/fezrs/tools/image_enhancement/original_calculator.py index d1c6eff..3dbb181 100644 --- a/fezrs/tools/image_enhancement/original_calculator.py +++ b/fezrs/tools/image_enhancement/original_calculator.py @@ -66,7 +66,7 @@ def execute( show_axis=False, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=500, bbox_inches="tight", grid=True, diff --git a/fezrs/tools/image_enhancement/original_rgb_calculator.py b/fezrs/tools/image_enhancement/original_rgb_calculator.py index b1dd47d..ab564de 100644 --- a/fezrs/tools/image_enhancement/original_rgb_calculator.py +++ b/fezrs/tools/image_enhancement/original_rgb_calculator.py @@ -83,7 +83,7 @@ def execute( show_axis=False, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=500, bbox_inches="tight", grid=True, diff --git a/fezrs/tools/image_enhancement/sigmoid_adjust_calculator.py b/fezrs/tools/image_enhancement/sigmoid_adjust_calculator.py index c858af2..90dd035 100644 --- a/fezrs/tools/image_enhancement/sigmoid_adjust_calculator.py +++ b/fezrs/tools/image_enhancement/sigmoid_adjust_calculator.py @@ -78,7 +78,7 @@ def execute( show_axis=False, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=500, bbox_inches="tight", grid=False, diff --git a/fezrs/tools/import_tools/__init__.py b/fezrs/tools/import_tools/__init__.py index 92d7eed..c7aabf5 100644 --- a/fezrs/tools/import_tools/__init__.py +++ b/fezrs/tools/import_tools/__init__.py @@ -1,2 +1,2 @@ -from .geoeye_calculator import Geoeye_Calculator -from .landsat8_calculator import Landsat8_Calculator +from .geoeye_calculator import Geoeye_Calculator, GeoeyeCalculator +from .landsat8_calculator import Landsat8_Calculator, Landsat8Calculator diff --git a/fezrs/tools/import_tools/geoeye_calculator.py b/fezrs/tools/import_tools/geoeye_calculator.py index 24a0efe..bedac34 100644 --- a/fezrs/tools/import_tools/geoeye_calculator.py +++ b/fezrs/tools/import_tools/geoeye_calculator.py @@ -34,7 +34,7 @@ def execute( show_axis=True, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=500, bbox_inches="tight", grid=False, @@ -55,3 +55,8 @@ def execute( nrows, ncols, ) + + +# PEP 8 conformant alias; the underscored name is retained for compatibility +# with existing code and remains exported. +GeoeyeCalculator = Geoeye_Calculator diff --git a/fezrs/tools/import_tools/landsat8_calculator.py b/fezrs/tools/import_tools/landsat8_calculator.py index 901fc0b..afb0211 100644 --- a/fezrs/tools/import_tools/landsat8_calculator.py +++ b/fezrs/tools/import_tools/landsat8_calculator.py @@ -74,7 +74,7 @@ def execute( show_axis=False, colormap=None, show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=500, bbox_inches="tight", grid=False, @@ -95,3 +95,8 @@ def execute( nrows, ncols, ) + + +# PEP 8 conformant alias; the underscored name is retained for compatibility +# with existing code and remains exported. +Landsat8Calculator = Landsat8_Calculator diff --git a/fezrs/tools/mosaic/__init__.py b/fezrs/tools/mosaic/__init__.py index 73fba6f..727ddee 100644 --- a/fezrs/tools/mosaic/__init__.py +++ b/fezrs/tools/mosaic/__init__.py @@ -1 +1 @@ -from .mosaic_calculator import BaseTool +from .mosaic_calculator import MosaicCalculator diff --git a/fezrs/tools/mosaic/mosaic_calculator.py b/fezrs/tools/mosaic/mosaic_calculator.py index bf82ae4..396259b 100644 --- a/fezrs/tools/mosaic/mosaic_calculator.py +++ b/fezrs/tools/mosaic/mosaic_calculator.py @@ -40,7 +40,7 @@ def _export_file( show_axis=False, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=100, bbox_inches="tight", grid=False, @@ -71,7 +71,7 @@ def execute( show_axis=False, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=100, bbox_inches="tight", grid=False, diff --git a/fezrs/tools/pca/pca_calculator.py b/fezrs/tools/pca/pca_calculator.py index 7274b63..8c9497c 100644 --- a/fezrs/tools/pca/pca_calculator.py +++ b/fezrs/tools/pca/pca_calculator.py @@ -1,3 +1,4 @@ +import warnings from uuid import uuid4 import matplotlib.pyplot as plt @@ -20,7 +21,22 @@ def __init__( swir1_path: BandPathType, swir2_path: BandPathType, selectBand: BandNamePCAType | None = None, + component: int | None = None, + standardize: bool = False, ): + """ + Args: + component: Principal component to inspect, numbered 1..6 in order of + decreasing explained variance. + selectBand: Deprecated. A band name that resolves to a fixed + component index. A principal component is a linear combination + of all six input bands, so no component corresponds to an input + band; use ``component`` instead. + standardize: Run PCA on the correlation matrix rather than the + covariance matrix, by scaling each band to unit variance first. + Without it, bands with the largest digital-number range dominate + the leading components regardless of their information content. + """ super().__init__( red_path=red_path, green_path=green_path, @@ -47,7 +63,7 @@ def __init__( self.metadata_bands["red"]["width"], ) - self.selectBand = selectBand + self.standardize = standardize # Input band order. # @@ -62,6 +78,40 @@ def __init__( "green": 5, } + self.band_order = [ + name + for name, _ in sorted( + self.bindTheBandsToNumber.items(), key=lambda item: item[1] + ) + ] + + if component is not None and selectBand is not None: + raise ValueError("Pass either 'component' or 'selectBand', not both.") + + if selectBand is not None: + if selectBand not in self.bindTheBandsToNumber: + raise ValueError(f"Invalid PCA band: {selectBand}") + resolved = self.bindTheBandsToNumber[selectBand] + 1 + warnings.warn( + f"PCACalculator's 'selectBand' is deprecated: {selectBand!r} " + f"resolves to principal component {resolved}. A principal " + "component is a linear combination of all six input bands, so " + "no component corresponds to an input band. Pass " + f"component={resolved} instead.", + DeprecationWarning, + stacklevel=2, + ) + component = resolved + + if component is not None and not 1 <= component <= 6: + raise ValueError( + f"component must be between 1 and 6, received {component}." + ) + + self.component = component + self.selectBand = selectBand + self._pca = None + def _validate(self): """ Validate the input raster bands before performing PCA. @@ -100,11 +150,10 @@ def _validate(self): f"Missing required PCA bands: {missing_bands}" ) - if self.selectBand is not None: - if self.selectBand not in self.bindTheBandsToNumber: - raise ValueError( - f"Invalid PCA band: {self.selectBand}" - ) + if self.component is not None and not 1 <= self.component <= 6: + raise ValueError( + f"component must be between 1 and 6, received {self.component}." + ) def process(self): """ @@ -173,11 +222,32 @@ def process(self): "No valid pixels were found for PCA." ) + valid_images = images[valid_mask] + + if self.standardize: + # Correlation-matrix PCA. sklearn decomposes the covariance matrix, + # which lets whichever band happens to carry the widest + # digital-number range dominate the leading components regardless of + # how much information it holds. + band_std = valid_images.std(axis=0) + band_std[band_std == 0] = 1.0 + valid_images = (valid_images - valid_images.mean(axis=0)) / band_std + pca = skpc(n_components=6) - transformed_valid = pca.fit_transform( - images[valid_mask] - ) + transformed_valid = pca.fit_transform(valid_images) + + # Eigenvector signs are arbitrary: the same scene can yield an inverted + # component image between runs, or between a scene and a crop of it, + # which makes results non-reproducible and the imagery hard to read. + # Fix the convention so the largest-magnitude loading is always + # positive. + dominant = np.argmax(np.abs(pca.components_), axis=1) + signs = np.sign(pca.components_[np.arange(6), dominant]) + signs[signs == 0] = 1.0 + + pca.components_ = pca.components_ * signs[:, np.newaxis] + transformed_valid = transformed_valid * signs transformed = np.full( (images.shape[0], 6), @@ -197,6 +267,43 @@ def process(self): return self._output + @property + def explained_variance_ratio_(self): + """ + Share of total variance carried by each principal component. + + Returns: + np.ndarray: Shape ``(6,)``, ordered by decreasing variance. + + Raises: + RuntimeError: If ``process()`` has not run yet. + """ + if self._pca is None: + raise RuntimeError( + "Run process() before reading explained_variance_ratio_." + ) + return self._pca.explained_variance_ratio_ + + @property + def components_(self): + """ + Eigenvector loadings, shape ``(6, 6)`` as ``(component, band)``. + + Band order is given by ``band_order``. These loadings, not the variance + share, are what identify a useful component: a target is isolated by the + component in which the relevant bands carry high loadings of opposing + sign. + + Signs follow a fixed convention (largest-magnitude loading positive), so + repeated runs and crops of the same scene are directly comparable. + + Raises: + RuntimeError: If ``process()`` has not run yet. + """ + if self._pca is None: + raise RuntimeError("Run process() before reading components_.") + return self._pca.components_ + def _customize_export_file(self, ax): pass @@ -212,10 +319,10 @@ def histogram_export( ): - if self.selectBand is None: + if self.component is None: raise ValueError( - "You cannot use histogram_export() without " - "passing selectBand." + "You cannot use histogram_export() without passing component " + "(or the deprecated selectBand)." ) self._validate() @@ -223,9 +330,7 @@ def histogram_export( if not hasattr(self, "_output"): self.process() - component_index = self.bindTheBandsToNumber[ - self.selectBand - ] + component_index = self.component - 1 pca_component = self._output[component_index] @@ -239,9 +344,10 @@ def histogram_export( color="black", ) + # Labelling this by an input band name attributed the output to a band + # that did not produce it: every component mixes all six inputs. ax.set_title( - f"Histogram of PCA Band " - f"{self.selectBand.capitalize()}" + f"Histogram of Principal Component {self.component}" ) if title: @@ -249,7 +355,7 @@ def histogram_export( f"{title}-FEZrs" ) - ax.set_xlabel("PCA Value") + ax.set_xlabel(f"PC{self.component} Value") ax.set_ylabel("Density") ax.grid(grid) @@ -275,7 +381,7 @@ def _export_file( show_axis=False, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=1000, bbox_inches="tight", grid=False, @@ -364,7 +470,7 @@ def execute( show_axis=False, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=500, bbox_inches="tight", grid=True, diff --git a/fezrs/tools/spectral_indices/afri_calculator.py b/fezrs/tools/spectral_indices/afri_calculator.py index 39d8f00..07a8143 100644 --- a/fezrs/tools/spectral_indices/afri_calculator.py +++ b/fezrs/tools/spectral_indices/afri_calculator.py @@ -105,10 +105,12 @@ def execute( show_axis=False, colormap="gray", show_colorbar=True, - filename_prefix="Tool_output", + filename_prefix=None, dpi=1000, bbox_inches="tight", grid=True, + nrows=None, + ncols=None, ): return super().execute( output_path, @@ -121,4 +123,6 @@ def execute( dpi, bbox_inches, grid, + nrows, + ncols, ) diff --git a/fezrs/tools/spectral_indices/bi_calculator.py b/fezrs/tools/spectral_indices/bi_calculator.py index 77b90eb..8f57490 100644 --- a/fezrs/tools/spectral_indices/bi_calculator.py +++ b/fezrs/tools/spectral_indices/bi_calculator.py @@ -138,10 +138,12 @@ def execute( show_axis=False, colormap="gray", show_colorbar=True, - filename_prefix="Tool_output", + filename_prefix=None, dpi=1000, bbox_inches="tight", grid=True, + nrows=None, + ncols=None, ): return super().execute( output_path, @@ -154,4 +156,6 @@ def execute( dpi, bbox_inches, grid, + nrows, + ncols, ) diff --git a/fezrs/tools/spectral_indices/ndvi_calculator.py b/fezrs/tools/spectral_indices/ndvi_calculator.py index 494ff5e..080fd00 100644 --- a/fezrs/tools/spectral_indices/ndvi_calculator.py +++ b/fezrs/tools/spectral_indices/ndvi_calculator.py @@ -42,10 +42,12 @@ def execute( show_axis=False, colormap=cm.Grays, show_colorbar=True, - filename_prefix="Tool_output", + filename_prefix=None, dpi=1000, bbox_inches="tight", grid=True, + nrows=None, + ncols=None, ): return super().execute( output_path, @@ -58,4 +60,6 @@ def execute( dpi, bbox_inches, grid, + nrows, + ncols, ) diff --git a/fezrs/tools/spectral_indices/ndwi_calculator.py b/fezrs/tools/spectral_indices/ndwi_calculator.py index bdda774..4ca5af6 100644 --- a/fezrs/tools/spectral_indices/ndwi_calculator.py +++ b/fezrs/tools/spectral_indices/ndwi_calculator.py @@ -42,10 +42,12 @@ def execute( show_axis=False, colormap=cm.Grays, show_colorbar=True, - filename_prefix="Tool_output", + filename_prefix=None, dpi=1000, bbox_inches="tight", grid=True, + nrows=None, + ncols=None, ): return super().execute( output_path, @@ -58,4 +60,6 @@ def execute( dpi, bbox_inches, grid, + nrows, + ncols, ) diff --git a/fezrs/tools/spectral_indices/savi_calculator.py b/fezrs/tools/spectral_indices/savi_calculator.py index ce9a48b..5b5b937 100644 --- a/fezrs/tools/spectral_indices/savi_calculator.py +++ b/fezrs/tools/spectral_indices/savi_calculator.py @@ -39,10 +39,12 @@ def execute( show_axis=False, colormap="gray", show_colorbar=True, - filename_prefix="Tool_output", + filename_prefix=None, dpi=1000, bbox_inches="tight", grid=True, + nrows=None, + ncols=None, ): return super().execute( output_path, @@ -55,4 +57,6 @@ def execute( dpi, bbox_inches, grid, + nrows, + ncols, ) diff --git a/fezrs/tools/spectral_indices/ui_calculator.py b/fezrs/tools/spectral_indices/ui_calculator.py index 06f4405..ce9f139 100644 --- a/fezrs/tools/spectral_indices/ui_calculator.py +++ b/fezrs/tools/spectral_indices/ui_calculator.py @@ -42,10 +42,12 @@ def execute( show_axis=False, colormap=cm.gray, show_colorbar=True, - filename_prefix="Tool_output", + filename_prefix=None, dpi=1000, bbox_inches="tight", grid=True, + nrows=None, + ncols=None, ): return super().execute( output_path, @@ -58,4 +60,6 @@ def execute( dpi, bbox_inches, grid, + nrows, + ncols, ) diff --git a/fezrs/tools/spectral_profile/spectral_profile_calculator.py b/fezrs/tools/spectral_profile/spectral_profile_calculator.py index 29535e2..29f952a 100644 --- a/fezrs/tools/spectral_profile/spectral_profile_calculator.py +++ b/fezrs/tools/spectral_profile/spectral_profile_calculator.py @@ -104,10 +104,12 @@ def execute( show_axis=True, colormap="gray", show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=1000, bbox_inches="tight", grid=True, + nrows=None, + ncols=None, ): return super().execute( output_path, @@ -120,4 +122,6 @@ def execute( dpi, bbox_inches, grid, + nrows, + ncols, ) diff --git a/fezrs/tools/svm/__init__.py b/fezrs/tools/svm/__init__.py new file mode 100644 index 0000000..9f65257 --- /dev/null +++ b/fezrs/tools/svm/__init__.py @@ -0,0 +1 @@ +from .svm_calculator import SVMCalculator diff --git a/fezrs/tools/svm/svm_calculator.py b/fezrs/tools/svm/svm_calculator.py index 69a598e..4fe3d18 100644 --- a/fezrs/tools/svm/svm_calculator.py +++ b/fezrs/tools/svm/svm_calculator.py @@ -152,7 +152,7 @@ def _export_file( show_axis=True, colormap=None, show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=500, bbox_inches="tight", grid=False, @@ -182,7 +182,7 @@ def execute( show_axis=True, colormap=None, show_colorbar=False, - filename_prefix="Tool_output", + filename_prefix=None, dpi=500, bbox_inches="tight", grid=False, diff --git a/paper/paper.md b/paper/paper.md index 87964ac..4536827 100644 --- a/paper/paper.md +++ b/paper/paper.md @@ -106,7 +106,7 @@ FEZrs was built to address this fragmentation by consolidating frequently used r # Software design -FEZrs is organized around a shared abstract base class (`BaseTool`) that standardizes the lifecycle of every analysis module: input validation, processing, visualization customization, and export. Each calculator (for example, `NDVICalculator`, `GaussianCalculator`, or `KMeansCalculator`) inherits this interface, accepts band file paths through a common file-handling layer, and exposes a small set of methods such as `execute` and `chart_export`. This pattern keeps the public API uniform across spectral indices, filters, enhancement tools, change-detection utilities, and machine-learning modules, reducing cognitive overhead when composing multi-step workflows. +FEZrs is organized around a shared abstract base class (`BaseTool`) that standardizes the lifecycle of every analysis module: input validation, processing, visualization customization, and export. Each calculator (for example, `NDVICalculator`, `GaussianCalculator`, or `KMeansCalculator`) inherits this interface, accepts band file paths through a common file-handling layer, and exposes a small set of methods such as `execute` and `histogram_export`. This pattern keeps the public API uniform across spectral indices, filters, enhancement tools, change-detection utilities, and machine-learning modules, reducing cognitive overhead when composing multi-step workflows. The package is modular by domain: (`spectral_indices`, `filters`, `image_enhancement`, `change_detection`, `clustering`, `glcm`, `pca`, `svm`, and others), while shared utilities handle band-path typing, file I/O, and histogram support. Raster inputs are typically multi-band geospatial imagery. Outputs are written as figures and processed products suitable for inspection, reporting, and further analysis. The design deliberately favors composition of independent calculators over a single monolithic pipeline object, so researchers can select only the methods required for a given study while still benefiting from consistent validation and export behavior. diff --git a/tests/base_test.py b/tests/base_test.py index b6a3094..605e67f 100644 --- a/tests/base_test.py +++ b/tests/base_test.py @@ -1,3 +1,5 @@ +from pathlib import Path + import numpy as np import pytest @@ -90,3 +92,87 @@ def test_execute_returns_self(): result = tool.execute(".") assert result is tool + + +# --- Export arguments that were accepted and then ignored (issue #42) --------- + + +def test_export_file_honours_filename_prefix(tmp_path): + """ + filename_prefix was accepted by all 37 calculators, forwarded correctly, and + then overwritten on the first line of _export_file() before it was used. + """ + tool = DummyExportTool() + tool._output = np.array([[1, 2], [3, 4]]) + + filename = tool._export_file(tmp_path, filename_prefix="my_run") + + assert Path(filename).name.startswith("my_run_output_") + + +def test_export_file_defaults_to_the_tool_name(tmp_path): + """Omitting the prefix must keep the previous default filenames.""" + tool = DummyExportTool() + tool._output = np.array([[1, 2], [3, 4]]) + + filename = tool._export_file(tmp_path) + + assert Path(filename).name.startswith("Dummy_output_") + + +def test_execute_forwards_nrows_and_ncols(): + """ + nrows/ncols were declared and documented on execute() but never reached + _export_file(), so a multi-panel layout was silently ignored. + """ + captured = {} + + class RecordingTool(BaseTool): + def __init__(self): + self._output = np.array([[1]]) + + def _validate(self): + pass + + def process(self): + pass + + def _export_file(self, *args, **kwargs): + captured["args"] = args + return "out.png" + + RecordingTool().execute(".", nrows=2, ncols=3) + + # positional tail of _export_file(...) is (..., grid, nrows, ncols) + assert captured["args"][-2:] == (2, 3) + + +def test_execute_defaults_nrows_and_ncols_to_single_panel(): + captured = {} + + class RecordingTool(BaseTool): + def __init__(self): + self._output = np.array([[1]]) + + def _validate(self): + pass + + def process(self): + pass + + def _export_file(self, *args, **kwargs): + captured["args"] = args + return "out.png" + + RecordingTool().execute(".") + + assert captured["args"][-2:] == (1, 1) + + +def test_export_file_produces_the_requested_grid(tmp_path): + tool = DummyExportTool() + tool._output = np.array([[1, 2], [3, 4]]) + + filename = tool._export_file(tmp_path, nrows=1, ncols=1) + + assert Path(filename).is_file() diff --git a/tests/docs_imports_test.py b/tests/docs_imports_test.py new file mode 100644 index 0000000..fd1b119 --- /dev/null +++ b/tests/docs_imports_test.py @@ -0,0 +1,107 @@ +""" +Execute every import statement that appears in the documentation. + +Issue #42 found 13 of 23 documented import lines failing, from five distinct +causes -- two of them packaging bugs rather than documentation errors. This test +extracts the import lines from `docs/`, the README and the paper, and runs them, +so documentation and package layout cannot drift apart again. +""" + +import re +from pathlib import Path + +import pytest + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + +DOC_SOURCES = ( + sorted((PROJECT_ROOT / "docs").glob("*.md")) + + [PROJECT_ROOT / "README.md"] + + sorted((PROJECT_ROOT / "paper").glob("*.md")) +) + +IMPORT_PATTERN = re.compile( + r"^\s*(from fezrs[\w.]* import [\w\s,()]+?|import fezrs[\w.]*)\s*$", + re.MULTILINE, +) + + +def _documented_imports(): + found = [] + for source in DOC_SOURCES: + if not source.is_file(): + continue + for match in IMPORT_PATTERN.finditer(source.read_text()): + statement = " ".join(match.group(1).split()) + found.append(pytest.param(statement, id=f"{source.name}::{statement}")) + return found + + +DOCUMENTED_IMPORTS = _documented_imports() + + +def test_documentation_contains_import_examples(): + """Guards against the extraction silently matching nothing.""" + assert len(DOCUMENTED_IMPORTS) >= 15 + + +@pytest.mark.parametrize("statement", DOCUMENTED_IMPORTS) +def test_documented_import_works(statement): + exec(statement, {}) + + +def test_every_exported_calculator_is_listed_in_the_readme(): + """ + The README listed 28 calculators while the package exported 37, omitting the + entire change-detection group and SVM. + """ + import fezrs + + readme = (PROJECT_ROOT / "README.md").read_text() + + exported = [name for name in fezrs.__all__ if name.endswith("Calculator")] + # The underscored legacy names stay exported but the README documents the + # PEP 8 aliases they point at. + documented = [name for name in exported if "_" not in name] + + missing = [name for name in documented if f"`{name}`" not in readme] + + assert not missing, f"calculators exported but absent from README: {missing}" + + +def test_subpackages_expose_their_calculators(): + """ + change_detection/ and svm/ had no __init__.py, so nothing could be imported + from either, and mosaic/__init__.py re-exported BaseTool instead of + MosaicCalculator. + """ + from fezrs.tools.change_detection import BurnCalculator # noqa: F401 + from fezrs.tools.mosaic import MosaicCalculator + from fezrs.tools.svm import SVMCalculator # noqa: F401 + + assert MosaicCalculator.__name__ == "MosaicCalculator" + + +def test_media_package_is_importable(): + """ + fezrs/media/ holds the watermark but had no __init__.py, so + find_packages(include=["fezrs", "fezrs.*"]) in setup.py skipped it. + """ + import fezrs.media + + assert fezrs.media is not None + + +def test_setup_find_packages_collects_every_tool_subpackage(): + setuptools = pytest.importorskip("setuptools") + find_packages = setuptools.find_packages + + packages = set(find_packages(include=["fezrs", "fezrs.*"])) + + for required in ( + "fezrs.media", + "fezrs.tools.change_detection", + "fezrs.tools.svm", + "fezrs.tools.mosaic", + ): + assert required in packages, f"{required} would not be packaged" diff --git a/tests/tools/image_enhancement/float_calculator_test.py b/tests/tools/image_enhancement/float_calculator_test.py index 1ac18c1..ec9290c 100644 --- a/tests/tools/image_enhancement/float_calculator_test.py +++ b/tests/tools/image_enhancement/float_calculator_test.py @@ -194,7 +194,7 @@ def test_execute_with_default_parameters(mock_float_calculator): False, "gray", False, - "Tool_output", + None, 500, "tight", True, diff --git a/tests/tools/import_tools/geoeye_calculator_test.py b/tests/tools/import_tools/geoeye_calculator_test.py index 76fa566..bdfaef6 100644 --- a/tests/tools/import_tools/geoeye_calculator_test.py +++ b/tests/tools/import_tools/geoeye_calculator_test.py @@ -133,7 +133,7 @@ def test_execute_with_default_parameters(mock_geoeye_calculator): True, "gray", False, - "Tool_output", + None, 500, "tight", False, diff --git a/tests/tools/import_tools/landsat8_calculator_test.py b/tests/tools/import_tools/landsat8_calculator_test.py index 21cccd8..02b0745 100644 --- a/tests/tools/import_tools/landsat8_calculator_test.py +++ b/tests/tools/import_tools/landsat8_calculator_test.py @@ -248,7 +248,7 @@ def test_execute_with_default_parameters(mock_landsat8_calculator): False, None, False, - "Tool_output", + None, 500, "tight", False, diff --git a/tests/tools/mosaic/mosaic_calculator_test.py b/tests/tools/mosaic/mosaic_calculator_test.py index 659474d..6c64568 100644 --- a/tests/tools/mosaic/mosaic_calculator_test.py +++ b/tests/tools/mosaic/mosaic_calculator_test.py @@ -276,7 +276,7 @@ def test_execute_with_default_parameters(mock_mosaic_calculator): False, "gray", False, - "Tool_output", + None, 100, "tight", False, diff --git a/tests/tools/pca/pca_calculator_test.py b/tests/tools/pca/pca_calculator_test.py index ba59942..9ba6490 100644 --- a/tests/tools/pca/pca_calculator_test.py +++ b/tests/tools/pca/pca_calculator_test.py @@ -173,13 +173,13 @@ def test_process_pixels_are_samples_not_bands(mock_pca_calculator): assert np.linalg.matrix_rank(centered_bands) <= N_BANDS - 1 -def test_histogram_export_requires_select_band(mock_pca_calculator): +def test_histogram_export_requires_a_component(mock_pca_calculator): mock_pca_calculator.selectBand = None + mock_pca_calculator.component = None with pytest.raises( ValueError, - match="You cannot use histogram_export\\(\\) without " - "passing selectBand\\.", + match="without passing component", ): mock_pca_calculator.histogram_export( output_path="./output" @@ -255,3 +255,146 @@ def test_execute(mock_pca_calculator): assert result is mock_pca_calculator mock_execute.assert_called_once() + + +# --- Component selection and eigen-structure (issue #42) ---------------------- + + +def _synthetic_pca_calculator(component=None, selectBand=None, standardize=False): + """ + Build a PCACalculator over six correlated synthetic bands, with + BaseTool.__init__ patched out so nothing touches disk. + """ + rng = np.random.default_rng(11) + base = rng.normal(size=(16, 16)) + bands = [ + base * weight + rng.normal(scale=0.2, size=(16, 16)) + for weight in (1.0, 0.8, 0.6, 1.4, 1.2, 0.4) + ] + # Deliberately unequal band variances, so covariance and correlation PCA + # give different answers. + bands[3] = bands[3] * 40.0 + + handler = MagicMock() + handler.get_metadata_bands.return_value = { + name: {"height": 16, "width": 16, "image_skimage": bands[0]} + for name in ("nir", "blue", "green", "red", "swir1", "swir2") + } + handler.get_images_collection.return_value = bands + + def fake_init(self, *args, **kwargs): + self.files_handler = handler + self._output = None + + with patch("fezrs.tools.pca.pca_calculator.BaseTool.__init__", fake_init): + return PCACalculator( + red_path="red.tif", + green_path="green.tif", + blue_path="blue.tif", + nir_path="nir.tif", + swir1_path="swir1.tif", + swir2_path="swir2.tif", + component=component, + selectBand=selectBand, + standardize=standardize, + ) + + +def test_component_selects_a_principal_component(): + calculator = _synthetic_pca_calculator(component=3) + + assert calculator.component == 3 + + +@pytest.mark.parametrize("component", [0, 7, -1]) +def test_component_must_be_within_range(component): + with pytest.raises(ValueError, match="component must be between 1 and 6"): + _synthetic_pca_calculator(component=component) + + +def test_select_band_is_deprecated_and_names_its_component(): + """ + selectBand='red' plotted the first principal component and titled the figure + "Histogram of PCA Band Red", attributing the output to an input band that + did not produce it. + """ + with pytest.warns(DeprecationWarning, match="resolves to principal component 1"): + calculator = _synthetic_pca_calculator(selectBand="red") + + assert calculator.component == 1 + + +def test_component_and_select_band_are_mutually_exclusive(): + with pytest.raises(ValueError, match="not both"): + _synthetic_pca_calculator(component=2, selectBand="red") + + +def test_explained_variance_ratio_is_exposed(): + calculator = _synthetic_pca_calculator(component=1) + + with pytest.raises(RuntimeError, match="Run process\\(\\)"): + calculator.explained_variance_ratio_ + + calculator.process() + + ratios = calculator.explained_variance_ratio_ + assert ratios.shape == (6,) + np.testing.assert_allclose(ratios.sum(), 1.0, atol=1e-6) + # Ordered by decreasing variance. + assert np.all(np.diff(ratios) <= 1e-12) + + +def test_components_loadings_are_exposed(): + calculator = _synthetic_pca_calculator(component=1) + + with pytest.raises(RuntimeError, match="Run process\\(\\)"): + calculator.components_ + + calculator.process() + + assert calculator.components_.shape == (6, 6) + assert len(calculator.band_order) == 6 + + +def test_component_signs_are_deterministic(): + """ + Eigenvector signs are arbitrary in PCA, so without a fixed convention the + same scene can produce an inverted component image between runs -- a + reproducibility defect of the same kind as issue #37. + """ + first = _synthetic_pca_calculator(component=1) + second = _synthetic_pca_calculator(component=1) + + first.process() + second.process() + + np.testing.assert_allclose(first.components_, second.components_) + np.testing.assert_allclose(first._output, second._output) + + # The convention itself: the largest-magnitude loading is positive. + dominant = np.argmax(np.abs(first.components_), axis=1) + leading = first.components_[np.arange(6), dominant] + assert np.all(leading > 0) + + +def test_standardize_changes_the_variance_structure(): + """ + sklearn decomposes the covariance matrix, so a band with a much wider + digital-number range dominates the leading components regardless of its + information content. Standardizing scales each band to unit variance first. + """ + covariance = _synthetic_pca_calculator(component=1, standardize=False) + correlation = _synthetic_pca_calculator(component=1, standardize=True) + + covariance.process() + correlation.process() + + assert not np.allclose( + covariance.explained_variance_ratio_, + correlation.explained_variance_ratio_, + ) + # The inflated band no longer swamps PC1. + assert ( + correlation.explained_variance_ratio_[0] + < covariance.explained_variance_ratio_[0] + ) diff --git a/tests/tools/spectral_indices/afri_calculator_test.py b/tests/tools/spectral_indices/afri_calculator_test.py index 6813175..2d3f284 100644 --- a/tests/tools/spectral_indices/afri_calculator_test.py +++ b/tests/tools/spectral_indices/afri_calculator_test.py @@ -124,6 +124,8 @@ def test_execute_calls_base_execute(mock_afri_calculator): 500, "tight", False, + None, + None, ) assert result == "executed" @@ -142,9 +144,11 @@ def test_execute_with_default_parameters(mock_afri_calculator): False, "gray", True, - "Tool_output", + None, 1000, "tight", True, + None, + None, ) assert result == "executed" diff --git a/tests/tools/spectral_indices/bi_calculator_test.py b/tests/tools/spectral_indices/bi_calculator_test.py index 430fce8..b4183f5 100644 --- a/tests/tools/spectral_indices/bi_calculator_test.py +++ b/tests/tools/spectral_indices/bi_calculator_test.py @@ -124,6 +124,8 @@ def test_execute_calls_base_execute(mock_bi_calculator): 500, "tight", False, + None, + None, ) assert result == "executed" @@ -142,9 +144,11 @@ def test_execute_with_default_parameters(mock_bi_calculator): False, "gray", True, - "Tool_output", + None, 1000, "tight", True, + None, + None, ) assert result == "executed" diff --git a/tests/tools/spectral_indices/ndvi_calculator_test.py b/tests/tools/spectral_indices/ndvi_calculator_test.py index 56c8239..5cd1dcb 100644 --- a/tests/tools/spectral_indices/ndvi_calculator_test.py +++ b/tests/tools/spectral_indices/ndvi_calculator_test.py @@ -129,6 +129,8 @@ def test_execute_calls_base_execute(mock_ndvi_calculator): 500, "tight", False, + None, + None, ) assert result == "executed" @@ -147,9 +149,11 @@ def test_execute_with_default_parameters(mock_ndvi_calculator): False, cm.Grays, True, - "Tool_output", + None, 1000, "tight", True, + None, + None, ) assert result == "executed" diff --git a/tests/tools/spectral_indices/ndwi_calculator_test.py b/tests/tools/spectral_indices/ndwi_calculator_test.py index cb55a4e..0b9965b 100644 --- a/tests/tools/spectral_indices/ndwi_calculator_test.py +++ b/tests/tools/spectral_indices/ndwi_calculator_test.py @@ -129,6 +129,8 @@ def test_execute_calls_base_execute(mock_ndwi_calculator): 500, "tight", False, + None, + None, ) assert result == "executed" @@ -147,9 +149,11 @@ def test_execute_with_default_parameters(mock_ndwi_calculator): False, cm.Grays, True, - "Tool_output", + None, 1000, "tight", True, + None, + None, ) assert result == "executed" diff --git a/tests/tools/spectral_indices/savi_calculator_test.py b/tests/tools/spectral_indices/savi_calculator_test.py index 62c3141..97eac4f 100644 --- a/tests/tools/spectral_indices/savi_calculator_test.py +++ b/tests/tools/spectral_indices/savi_calculator_test.py @@ -117,6 +117,8 @@ def test_execute_calls_base_execute(mock_savi_calculator): 500, "tight", False, + None, + None, ) assert result == "executed" @@ -135,9 +137,11 @@ def test_execute_with_default_parameters(mock_savi_calculator): False, "gray", True, - "Tool_output", + None, 1000, "tight", True, + None, + None, ) assert result == "executed" diff --git a/tests/tools/spectral_indices/ui_calculator_test.py b/tests/tools/spectral_indices/ui_calculator_test.py index 596d04a..07abd99 100644 --- a/tests/tools/spectral_indices/ui_calculator_test.py +++ b/tests/tools/spectral_indices/ui_calculator_test.py @@ -129,6 +129,8 @@ def test_execute_calls_base_execute(mock_ui_calculator): 500, "tight", False, + None, + None, ) assert result == "executed" @@ -147,9 +149,11 @@ def test_execute_with_default_parameters(mock_ui_calculator): False, cm.gray, True, - "Tool_output", + None, 1000, "tight", True, + None, + None, ) assert result == "executed" diff --git a/tests/tools/spectral_profile/spectral_profile_calculator_test.py b/tests/tools/spectral_profile/spectral_profile_calculator_test.py index 67721fa..5577918 100644 --- a/tests/tools/spectral_profile/spectral_profile_calculator_test.py +++ b/tests/tools/spectral_profile/spectral_profile_calculator_test.py @@ -208,6 +208,8 @@ def test_execute_calls_base_execute(mock_spectral_profile_calculator): 500, "tight", False, + None, + None, ) assert result == "executed" @@ -226,10 +228,12 @@ def test_execute_with_default_parameters(mock_spectral_profile_calculator): True, "gray", False, - "Tool_output", + None, 1000, "tight", True, + None, + None, ) assert result == "executed" diff --git a/tests/tools/svm/svm_calculator_test.py b/tests/tools/svm/svm_calculator_test.py index 777ee9a..485f38b 100644 --- a/tests/tools/svm/svm_calculator_test.py +++ b/tests/tools/svm/svm_calculator_test.py @@ -187,7 +187,7 @@ def test_execute_with_default_parameters(mock_svm_calculator): True, None, False, - "Tool_output", + None, 500, "tight", False,