diff --git a/docs/install.md b/docs/install.md index b445ad4..a4c94ba 100644 --- a/docs/install.md +++ b/docs/install.md @@ -15,12 +15,26 @@ pip install siapy ### Alternative Installation Methods -__Using Conda__ +__Python package and dependency managers__ -You can also install siapy using conda: +You can also install siapy using other popular Python package and dependency managers: + +- PDM: + +```bash +pdm add siapy +``` + +- Poetry: + +```bash +poetry add siapy +``` + +- uv: ```bash -conda install -c conda-forge siapy +uv add siapy ``` __Manually__ diff --git a/siapy/entities/imagesets.py b/siapy/entities/imagesets.py index 7ef6e8b..8242906 100644 --- a/siapy/entities/imagesets.py +++ b/siapy/entities/imagesets.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from pathlib import Path -from typing import Any, Iterator +from typing import Any, Iterator, Sequence import numpy as np from rich.progress import track @@ -31,8 +31,8 @@ def __getitem__(self, index) -> SpectralImage: def from_paths( cls, *, - header_paths: list[str | Path], - image_paths: list[str | Path] | None = None, + header_paths: Sequence[str | Path], + image_paths: Sequence[str | Path] | None = None, ): if image_paths is not None and len(header_paths) != len(image_paths): raise ValueError("The length of hdr_paths and img_path must be equal.") diff --git a/siapy/entities/pixels.py b/siapy/entities/pixels.py index abe8c0e..6dc877d 100644 --- a/siapy/entities/pixels.py +++ b/siapy/entities/pixels.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from pathlib import Path from typing import Annotated, ClassVar, Iterable, NamedTuple import numpy as np @@ -28,10 +29,15 @@ def from_iterable( Annotated[int, "v coordinate on the image"], ] ], - ): + ) -> "Pixels": df = pd.DataFrame(iterable, columns=[Pixels.coords.U, Pixels.coords.V]) return cls(df) + @classmethod + def load_from_parquet(cls, filepath: str | Path) -> "Pixels": + df = pd.read_parquet(filepath) + return cls(df) + @property def df(self) -> pd.DataFrame: return self._data @@ -49,3 +55,6 @@ def v(self) -> pd.Series: def to_numpy(self) -> np.ndarray: return self.df.to_numpy() + + def save_to_parquet(self, filepath: str | Path) -> None: + self.df.to_parquet(filepath, index=True) diff --git a/siapy/entities/signatures.py b/siapy/entities/signatures.py index 7cfb88f..ccbe606 100644 --- a/siapy/entities/signatures.py +++ b/siapy/entities/signatures.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +from pathlib import Path from typing import Any import numpy as np @@ -13,6 +14,11 @@ class Signals: _data: pd.DataFrame + @classmethod + def load_from_parquet(cls, filepath: str | Path) -> "Signals": + df = pd.read_parquet(filepath) + return cls(df) + @property def df(self) -> pd.DataFrame: return self._data @@ -23,6 +29,9 @@ def to_numpy(self) -> np.ndarray: def mean(self) -> np.ndarray: return np.nanmean(self.to_numpy(), axis=0) + def save_to_parquet(self, filepath: str | Path) -> None: + self.df.to_parquet(filepath, index=True) + @dataclass class SignaturesFilter: @@ -84,6 +93,11 @@ def from_dataframe(cls, dataframe: pd.DataFrame) -> "Signatures": signals = Signals(dataframe.drop(columns=[Pixels.coords.U, Pixels.coords.V])) return cls._create(pixels, signals) + @classmethod + def load_from_parquet(cls, filepath: str | Path) -> "Signatures": + df = pd.read_parquet(filepath) + return cls.from_dataframe(df) + @property def pixels(self) -> Pixels: return self._pixels @@ -100,3 +114,6 @@ def to_numpy(self) -> np.ndarray: def filter(self) -> SignaturesFilter: return SignaturesFilter(self.pixels, self.signals) + + def save_to_parquet(self, filepath: str | Path) -> None: + self.to_dataframe().to_parquet(filepath, index=True) diff --git a/siapy/utils/plots.py b/siapy/utils/plots.py index 6e4ad08..36ac5ee 100644 --- a/siapy/utils/plots.py +++ b/siapy/utils/plots.py @@ -1,4 +1,5 @@ import sys +from typing import Any import matplotlib.pyplot as plt import numpy as np @@ -60,7 +61,9 @@ def onexit(event): return Pixels.from_iterable(coordinates) -def pixels_select_lasso(image: ImageType) -> list[Pixels]: +def pixels_select_lasso( + image: ImageType, selector_props: dict[str, Any] | None = None +) -> list[Pixels]: image_display = validate_image_to_numpy_3channels(image) x, y = np.meshgrid( @@ -100,7 +103,12 @@ def accept(event): enter_clicked = 1 plt.close() - lasso = LassoSelector(ax, onselect) # noqa: F841 + props = ( + selector_props + if selector_props is not None + else {"color": "red", "linewidth": 2, "linestyle": "-"} + ) + lasso = LassoSelector(ax, onselect, props=props) # noqa: F841 fig.canvas.mpl_connect("button_release_event", onrelease) fig.canvas.mpl_connect("close_event", onexit) fig.canvas.mpl_connect("key_press_event", accept) diff --git a/tests/entities/test_entities_pixels.py b/tests/entities/test_entities_pixels.py index cc526f1..28d3bbe 100644 --- a/tests/entities/test_entities_pixels.py +++ b/tests/entities/test_entities_pixels.py @@ -1,3 +1,7 @@ +import os +from pathlib import Path +from tempfile import TemporaryDirectory + import numpy as np import pandas as pd @@ -51,3 +55,14 @@ def test_to_numpy(): pixels = Pixels(df) expected_array = df.to_numpy() assert np.array_equal(pixels.to_numpy(), expected_array) + + +def test_save_and_load_to_parquet(): + pixels = Pixels.from_iterable(iterable) + with TemporaryDirectory() as tmpdir: + parquet_file = Path(tmpdir, "test_pixels.parquet") + pixels.save_to_parquet(parquet_file) + assert os.path.exists(parquet_file) + loaded_pixels = Pixels.load_from_parquet(parquet_file) + assert isinstance(loaded_pixels, Pixels) + assert loaded_pixels.df.equals(pixels.df) diff --git a/tests/entities/test_entities_signatures.py b/tests/entities/test_entities_signatures.py index 71aa5ee..9a9c4e2 100644 --- a/tests/entities/test_entities_signatures.py +++ b/tests/entities/test_entities_signatures.py @@ -1,3 +1,7 @@ +import os +from pathlib import Path +from tempfile import TemporaryDirectory + import numpy as np import pandas as pd import pytest @@ -22,6 +26,18 @@ def test_signals_mean(): assert signals_mean.shape == (4,) +def test_signals_save_and_load_to_parquet(): + signals_df = pd.DataFrame([[1, 2, 4, 6], [3, 4, 3, 5]]) + signals = Signals(signals_df) + with TemporaryDirectory() as tmpdir: + parquet_file = Path(tmpdir, "test_signals.parquet") + signals.save_to_parquet(parquet_file) + assert os.path.exists(parquet_file) + loaded_signals = Signals.load_from_parquet(parquet_file) + assert isinstance(loaded_signals, Signals) + assert loaded_signals.df.equals(signals.df) + + def test_signatures_filter_create(): pixels_df = pd.DataFrame({"u": [0, 1], "v": [0, 1]}) signals_df = pd.DataFrame([[1, 2], [3, 4]]) @@ -140,3 +156,15 @@ def test_signatures_from_dataframe(): with pytest.raises(ValueError): Signatures.from_dataframe(df_missing_v) + + +def test_signatures_save_and_load_to_parquet(): + df = pd.DataFrame({"u": [0, 1], "v": [0, 1], "0": [1, 2], "1": [3, 4]}) + signatures = Signatures.from_dataframe(df) + with TemporaryDirectory() as tmpdir: + parquet_file = Path(tmpdir, "test_signatures.parquet") + signatures.save_to_parquet(parquet_file) + assert os.path.exists(parquet_file) + loaded_signatures = Signatures.load_from_parquet(parquet_file) + assert isinstance(loaded_signatures, Signatures) + assert loaded_signatures.to_dataframe().equals(signatures.to_dataframe()) diff --git a/tests/utils/test_utils_plots.py b/tests/utils/test_utils_plots.py index b12252e..512e4fa 100644 --- a/tests/utils/test_utils_plots.py +++ b/tests/utils/test_utils_plots.py @@ -23,7 +23,7 @@ def test_pixels_select_click_manual(spectral_images): @pytest.mark.manual def test_pixels_select_lasso_manual(spectral_images): image_vnir = spectral_images.vnir - selected_areas = pixels_select_lasso(image_vnir) + selected_areas = pixels_select_lasso(image_vnir, selector_props={"color": "blue"}) display_image_with_areas(image_vnir, selected_areas, color="blue")