diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 31de2a1..b9039eb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,7 +10,7 @@ on: branches: ["main"] jobs: - build: + test: runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -32,7 +32,7 @@ jobs: python -m pip install poetry - name: Install dependencies run: | - python -m poetry install + python -m poetry install --extras test - name: Test with pytest and create coverage report run: | python -m poetry run coverage run --source=panoptica -m pytest @@ -43,3 +43,35 @@ jobs: uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} + + test-cuda: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: "pip" + + - name: Configure poetry + run: | + python -m pip install --upgrade pip + python -m pip install poetry + - name: Install dependencies with GPU extras + run: | + python -m poetry install --extras "gpu test" || python -m poetry install --extras test + - name: Test CUDA functionality (CPU fallback) + run: | + python -m poetry run pytest unit_tests/test_cupy_connected_components.py -v + - name: Upload coverage results to Codecov (Only on merge to main) + # Only upload to Codecov after a merge to the main branch + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py index c9a370c..a2740af 100644 --- a/benchmark/benchmark.py +++ b/benchmark/benchmark.py @@ -7,6 +7,16 @@ # scipy needs to be installed to run this benchmark, we use cc3d as it is quicker for 3D data from scipy import ndimage +# Try to import cupy for GPU acceleration +try: + import cupy as cp + from cupyx.scipy import ndimage as cp_ndimage + + CUPY_AVAILABLE = True +except ImportError: + CUPY_AVAILABLE = False + print("CuPy not available. GPU benchmarks will be skipped.") + def generate_random_binary_mask(size: Tuple[int, int, Union[int, None]]) -> np.ndarray: """ @@ -64,6 +74,64 @@ def label_cc3d(): return cc3d_time +def benchmark_cupy(mask: np.ndarray): + """ + Benchmark the performance of cupy.ndimage.label for connected component labeling on GPU. + + Args: + mask (np.ndarray): Binary mask to label. + + Returns: + float: Time taken to label the mask in seconds, or None if CuPy is not available. + """ + if not CUPY_AVAILABLE: + return None + + # Transfer data to GPU + mask_gpu = cp.asarray(mask) + + # Warmup phase + for _ in range(3): + cp_ndimage.label(mask_gpu) + cp.cuda.Stream.null.synchronize() + + def label_cupy(): + cp_ndimage.label(mask_gpu) + cp.cuda.Stream.null.synchronize() # Ensure GPU computation is complete + + cupy_time = timeit.timeit(label_cupy, number=10) + + # Clean up GPU memory + del mask_gpu + cp.get_default_memory_pool().free_all_blocks() + + return cupy_time + + +def benchmark_panoptica_cupy(mask: np.ndarray): + """ + Benchmark the performance of panoptica's CuPy backend for connected component labeling. + + Args: + mask (np.ndarray): Binary mask to label. + + Returns: + float: Time taken to label the mask in seconds, or None if CuPy is not available. + """ + if not CUPY_AVAILABLE: + return None + + from panoptica._functionals import _connected_components + from panoptica.utils.constants import CCABackend + + def label_panoptica_cupy(): + _connected_components(mask, CCABackend.cupy) + + panoptica_cupy_time = timeit.timeit(label_panoptica_cupy, number=10) + + return panoptica_cupy_time + + def run_benchmarks(volume_sizes: Tuple[Tuple[int, int, Union[int, None]]]) -> None: """ Run benchmark tests for connected component labeling with different volume sizes. @@ -80,10 +148,15 @@ def run_benchmarks(volume_sizes: Tuple[Tuple[int, int, Union[int, None]]]) -> No scipy_time = benchmark_scipy(mask) cc3d_time = benchmark_cc3d(mask) + cupy_time = benchmark_cupy(mask) print(f"Volume Size: {size}") print(f"Scipy Time: {scipy_time:.4f} seconds") print(f"CC3D Time: {cc3d_time:.4f} seconds") + if cupy_time is not None: + print(f"CuPy Time: {cupy_time:.4f} seconds") + else: + print("CuPy Time: Not available") print() diff --git a/panoptica/__init__.py b/panoptica/__init__.py index feb7387..4a43a37 100644 --- a/panoptica/__init__.py +++ b/panoptica/__init__.py @@ -2,7 +2,11 @@ ConnectedComponentsInstanceApproximator, CCABackend, ) -from panoptica.instance_matcher import NaiveThresholdMatching, MaxBipartiteMatching +from panoptica.instance_matcher import ( + NaiveThresholdMatching, + MaxBipartiteMatching, + RegionBasedMatching, +) from panoptica.panoptica_statistics import Panoptica_Statistic, ValueSummary from panoptica.panoptica_aggregator import Panoptica_Aggregator from panoptica.panoptica_evaluator import Panoptica_Evaluator diff --git a/panoptica/_functionals.py b/panoptica/_functionals.py index 98e4345..7168be7 100644 --- a/panoptica/_functionals.py +++ b/panoptica/_functionals.py @@ -63,6 +63,19 @@ def _connected_components( from scipy.ndimage import label cc_arr, n_instances = label(array) + elif cca_backend == CCABackend.cupy: + try: + import cupy as cp + from cupyx.scipy.ndimage import label as cp_label + + array_gpu = cp.asarray(array) + cc_arr, n_instances = cp_label(array_gpu) + cc_arr = cp.asnumpy(cc_arr) + except ImportError: + raise ImportError( + "CuPy is not installed. Please install CuPy to use the GPU backend. " + "You can install it using: pip install cupy-cuda11x or cupy-cuda12x depending on your CUDA version." + ) else: raise NotImplementedError(cca_backend) diff --git a/panoptica/instance_matcher.py b/panoptica/instance_matcher.py index f52e9ba..0eb4afc 100644 --- a/panoptica/instance_matcher.py +++ b/panoptica/instance_matcher.py @@ -3,11 +3,13 @@ from typing import Optional, Tuple, List import numpy as np +from scipy.ndimage import distance_transform_edt from panoptica._functionals import ( _calc_matching_metric_of_overlapping_labels, _calc_matching_metric_of_overlapping_partlabels, _map_labels, + _connected_components, ) from panoptica.metrics import Metric from panoptica.utils.processing_pair import ( @@ -17,6 +19,7 @@ from panoptica.utils.instancelabelmap import InstanceLabelMap from panoptica.utils.config import SupportsConfig from panoptica.utils.label_group import LabelGroup, LabelPartGroup +from panoptica.utils.constants import CCABackend @dataclass @@ -493,3 +496,118 @@ def _yaml_repr(cls, node) -> dict: "matching_metric": node._matching_metric, "matching_threshold": node._matching_threshold, } + + +class RegionBasedMatching(InstanceMatchingAlgorithm): + """ + Instance matching algorithm that performs region-based matching using spatial distance. + + This method assigns prediction instances to ground truth regions based on spatial proximity + rather than traditional overlap-based metrics. It uses connected components and distance + transforms to create region assignments. + + Note: This matching method does not produce traditional count metrics (TP/FP/FN) as it + assigns all predictions to regions. Count metrics will be set to NaN. + + Attributes: + cca_backend (CCABackend): Backend for connected component analysis. + """ + + def __init__( + self, + cca_backend: CCABackend = CCABackend.scipy, + ) -> None: + """ + Initialize the RegionBasedMatching instance. + + Args: + cca_backend (CCABackend): Backend for connected component analysis. + """ + self._cca_backend = cca_backend + + def _get_gt_regions(self, gt: np.ndarray) -> Tuple[np.ndarray, int]: + """ + Get ground truth regions using connected components and distance transforms. + + Args: + gt: Ground truth array + + Returns: + Tuple of (region_map, num_features) where region_map assigns each pixel + to the closest ground truth region. + """ + # Step 1: Connected Components + labeled_array, num_features = _connected_components(gt, self._cca_backend) + + # Step 2: Compute distance transform for each region + distance_map = np.full(gt.shape, np.inf, dtype=np.float32) + region_map = np.zeros(gt.shape, dtype=np.int32) + + for region_label in range(1, num_features + 1): + # Create region mask + region_mask = labeled_array == region_label + + # Compute distance transform + distance = distance_transform_edt(~region_mask) + + # Update pixels where this region is closer + update_mask = distance < distance_map + distance_map[update_mask] = distance[update_mask] + region_map[update_mask] = region_label + + return region_map, num_features + + def _match_instances( + self, + unmatched_instance_pair: UnmatchedInstancePair, + context: Optional[MatchingContext] = None, + **kwargs, + ) -> InstanceLabelMap: + """ + Perform region-based instance matching. + + Args: + unmatched_instance_pair (UnmatchedInstancePair): The unmatched instance pair to be matched. + context (Optional[MatchingContext]): The matching context. + **kwargs: Additional keyword arguments. + + Returns: + InstanceLabelMap: The result of the region-based matching. + """ + pred_arr = unmatched_instance_pair.prediction_arr + ref_arr = unmatched_instance_pair.reference_arr + pred_labels = unmatched_instance_pair.pred_labels + + labelmap = InstanceLabelMap() + + if len(pred_labels) == 0: + return labelmap + + # Get ground truth regions + region_map, num_features = self._get_gt_regions(ref_arr) + + # For each prediction instance, find which ground truth region it belongs to + for pred_label in pred_labels: + pred_mask = pred_arr == pred_label + + # Find the most common region assignment for this prediction instance + pred_regions = region_map[pred_mask] + + # Remove background (region 0) + pred_regions = pred_regions[pred_regions > 0] + + if len(pred_regions) > 0: + # Assign to the most common region + unique_regions, counts = np.unique(pred_regions, return_counts=True) + most_common_region = unique_regions[np.argmax(counts)] + + # Add to labelmap + labelmap.add_labelmap_entry(int(pred_label), int(most_common_region)) + + return labelmap + + @classmethod + def _yaml_repr(cls, node) -> dict: + return { + "cca_backend": node._cca_backend, + } diff --git a/panoptica/panoptica_evaluator.py b/panoptica/panoptica_evaluator.py index db6ec17..6b2e2cf 100644 --- a/panoptica/panoptica_evaluator.py +++ b/panoptica/panoptica_evaluator.py @@ -442,6 +442,11 @@ def panoptic_evaluate( else instance_metadata["original_num_refs"] ) + # For region-based matching, set TP to NaN since it doesn't use traditional counting + tp_value = processing_pair.tp + if instance_matcher.__class__.__name__ == "RegionBasedMatching": + tp_value = np.nan + processing_pair = PanopticaResult( reference_arr=processing_pair.reference_arr, prediction_arr=processing_pair.prediction_arr, @@ -450,7 +455,7 @@ def panoptic_evaluate( num_ref_instances=final_num_ref_instances, num_ref_labels=instance_metadata["num_ref_labels"], label_group=label_group, - tp=processing_pair.tp, + tp=tp_value, list_metrics=processing_pair.list_metrics, global_metrics=global_metrics, edge_case_handler=edge_case_handler, diff --git a/panoptica/panoptica_result.py b/panoptica/panoptica_result.py index f0cd6c0..aaaf560 100644 --- a/panoptica/panoptica_result.py +++ b/panoptica/panoptica_result.py @@ -25,7 +25,7 @@ def __init__( prediction_arr: np.ndarray, num_pred_instances: int, num_ref_instances: int, - tp: int, + tp: int | float, # Allow NaN (float) for region-based matching list_metrics: dict[Metric, list[float]], edge_case_handler: EdgeCaseHandler, global_metrics: list[Metric] = [], @@ -96,7 +96,7 @@ def __init__( default_value=num_pred_instances, was_calculated=True, ) - self.tp: int + self.tp: int | float # Allow NaN for region-based matching self._add_metric( "tp", MetricType.MATCHING, @@ -108,28 +108,28 @@ def __init__( # endregion # # region Basic - self.fp: int + self.fp: int | float # Allow NaN for region-based matching self._add_metric( "fp", MetricType.MATCHING, fp, long_name="False Positives", ) - self.fn: int + self.fn: int | float # Allow NaN for region-based matching self._add_metric( "fn", MetricType.MATCHING, fn, long_name="False Negatives", ) - self.prec: int + self.prec: int | float # Allow NaN for region-based matching self._add_metric( "prec", MetricType.NO_PRINT, prec, long_name="Precision (positive predictive value)", ) - self.rec: int + self.rec: int | float # Allow NaN for region-based matching self._add_metric( "rec", MetricType.NO_PRINT, @@ -773,18 +773,26 @@ def get_channel_metrics(self, metric_name: str): # region Basic def fp(res: PanopticaResult): + if np.isnan(res.tp): + return np.nan return res.num_pred_instances - res.tp def fn(res: PanopticaResult): + if np.isnan(res.tp): + return np.nan return res.num_ref_instances - res.tp def prec(res: PanopticaResult): + if np.isnan(res.tp): + return np.nan return res.tp / (res.tp + res.fp) def rec(res: PanopticaResult): + if np.isnan(res.tp): + return np.nan return res.tp / (res.tp + res.fn) @@ -795,6 +803,8 @@ def rq(res: PanopticaResult): Returns: float: Recognition Quality (RQ). """ + if np.isnan(res.tp): + return np.nan if res.tp == 0: return 0.0 if res.num_pred_instances + res.num_ref_instances > 0 else np.nan return res.tp / (res.tp + 0.5 * res.fp + 0.5 * res.fn) diff --git a/panoptica/utils/constants.py b/panoptica/utils/constants.py index 7dc88a0..463fad0 100644 --- a/panoptica/utils/constants.py +++ b/panoptica/utils/constants.py @@ -89,10 +89,13 @@ class CCABackend(_Enum_Compare): [CC3D Website](https://github.com/seung-lab/connected-components-3d) - scipy: Represents the SciPy backend for CCA. [SciPy Website](https://www.scipy.org/) + - cupy: Represents the CuPy backend for GPU-accelerated CCA. + [CuPy Website](https://cupy.dev/) """ cc3d = auto() scipy = auto() + cupy = auto() if __name__ == "__main__": diff --git a/panoptica/utils/input_check_and_conversion/check_torch_image.py b/panoptica/utils/input_check_and_conversion/check_torch_image.py index e920097..3806790 100644 --- a/panoptica/utils/input_check_and_conversion/check_torch_image.py +++ b/panoptica/utils/input_check_and_conversion/check_torch_image.py @@ -1,17 +1,27 @@ import numpy as np from importlib.util import find_spec from pathlib import Path +from typing import Union, TYPE_CHECKING from panoptica.utils.input_check_and_conversion.check_numpy_array import ( sanity_checker_numpy_array, ) -# Optional sitk import +# Optional torch import _spec = find_spec("torch") if _spec is not None: import torch +else: + torch = None + +if TYPE_CHECKING: + import torch -def load_torch_image(image_path: str | Path) -> torch.Tensor: +def load_torch_image(image_path: Union[str, Path]): + if torch is None: + raise ImportError( + "torch is not available. Please install torch to use this functionality." + ) try: image = torch.load( image_path, @@ -25,9 +35,9 @@ def load_torch_image(image_path: str | Path) -> torch.Tensor: def sanity_checker_torch_image( - prediction_image: torch.Tensor | str | Path, - reference_image: torch.Tensor | str | Path, -) -> tuple[bool, tuple[np.ndarray, np.ndarray] | str]: + prediction_image: Union["torch.Tensor", str, Path], + reference_image: Union["torch.Tensor", str, Path], +) -> tuple[bool, Union[tuple[np.ndarray, np.ndarray], str]]: """ This function performs sanity check on 2 Torch tensors by converting them to numpy arrays and using that check. @@ -38,6 +48,11 @@ def sanity_checker_torch_image( Returns: tuple[bool, tuple[np.ndarray, np.ndarray] | str]: A tuple where the first element is a boolean indicating if the images pass the sanity check, and the second element is either the numpy arrays of the images or an error message. """ + if torch is None: + raise ImportError( + "torch is not available. Please install torch to use this functionality." + ) + # load if necessary if isinstance(prediction_image, (str, Path)): prediction_image = load_torch_image(prediction_image) diff --git a/poetry.lock b/poetry.lock index 758b6b3..9221ce1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,12 +1,13 @@ -# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "accessible-pygments" version = "0.0.5" description = "A collection of accessible pygments styles" -optional = false +optional = true python-versions = ">=3.9" -groups = ["docs"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7"}, {file = "accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872"}, @@ -23,9 +24,10 @@ tests = ["hypothesis", "pytest"] name = "alabaster" version = "1.0.0" description = "A light, configurable Sphinx theme" -optional = false +optional = true python-versions = ">=3.10" -groups = ["docs"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"}, {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, @@ -35,9 +37,10 @@ files = [ name = "auxiliary" version = "0.4.1" description = "TODO." -optional = false +optional = true python-versions = "<4.0,>=3.10" -groups = ["dev"] +groups = ["main"] +markers = "extra == \"test\"" files = [ {file = "auxiliary-0.4.1-py3-none-any.whl", hash = "sha256:b35e28f90167a38c1e59d929e08083b2d93f57220cd82497e046a5fe5daef730"}, {file = "auxiliary-0.4.1.tar.gz", hash = "sha256:425e07dedd48ad00a0532fd10911e4dde02b56b603cb6f6dc4a7488afa482c57"}, @@ -58,9 +61,10 @@ dcm2niix = ["dcm2niix (>=1.0.20250506)"] name = "babel" version = "2.17.0" description = "Internationalization utilities" -optional = false +optional = true python-versions = ">=3.8" -groups = ["docs"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, @@ -73,9 +77,10 @@ dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)" name = "beautifulsoup4" version = "4.13.5" description = "Screen-scraping library" -optional = false +optional = true python-versions = ">=3.7.0" -groups = ["docs"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "beautifulsoup4-4.13.5-py3-none-any.whl", hash = "sha256:642085eaa22233aceadff9c69651bc51e8bf3f874fb6d7104ece2beb24b47c4a"}, {file = "beautifulsoup4-4.13.5.tar.gz", hash = "sha256:5e70131382930e7c3de33450a2f54a63d5e4b19386eab43a5b34d594268f3695"}, @@ -96,9 +101,10 @@ lxml = ["lxml"] name = "certifi" version = "2025.8.3" description = "Python package for providing Mozilla's CA Bundle." -optional = false +optional = true python-versions = ">=3.7" -groups = ["docs"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, @@ -108,9 +114,10 @@ files = [ name = "charset-normalizer" version = "3.4.3" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false +optional = true python-versions = ">=3.7" -groups = ["docs"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}, {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}, @@ -214,12 +221,12 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main", "dev", "docs"] +groups = ["main", "dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\" or platform_system == \"Windows\"", docs = "sys_platform == \"win32\""} +markers = {main = "(extra == \"test\" or extra == \"docs\") and sys_platform == \"win32\" or platform_system == \"Windows\"", dev = "sys_platform == \"win32\""} [[package]] name = "connected-components-3d" @@ -369,13 +376,34 @@ files = [ [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] +[[package]] +name = "cupy" +version = "13.6.0" +description = "CuPy: NumPy & SciPy for GPU" +optional = true +python-versions = ">=3.9" +groups = ["main"] +markers = "extra == \"gpu\"" +files = [ + {file = "cupy-13.6.0.tar.gz", hash = "sha256:3cba30ae3dd32b5d5c6536e710cb98015227cd4ba83c46b3f1825a7ae55b6667"}, +] + +[package.dependencies] +fastrlock = ">=0.5" +numpy = ">=1.22,<2.6" + +[package.extras] +all = ["Cython (>=3)", "optuna (>=2.0)", "scipy (>=1.7,<1.17)"] +test = ["hypothesis (>=6.37.2,<6.55.0)", "mpmath", "packaging", "pytest (>=7.2)"] + [[package]] name = "docutils" version = "0.21.2" description = "Docutils -- Python Documentation Utilities" -optional = false +optional = true python-versions = ">=3.9" -groups = ["docs"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, @@ -387,12 +415,12 @@ version = "1.3.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" -groups = ["dev"] -markers = "python_version == \"3.10\"" +groups = ["main", "dev"] files = [ {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, ] +markers = {main = "extra == \"test\" and python_version == \"3.10\"", dev = "python_version == \"3.10\""} [package.dependencies] typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} @@ -400,6 +428,86 @@ typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "fastrlock" +version = "0.8.3" +description = "Fast, re-entrant optimistic lock implemented in Cython" +optional = true +python-versions = "*" +groups = ["main"] +markers = "extra == \"gpu\"" +files = [ + {file = "fastrlock-0.8.3-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bbbe31cb60ec32672969651bf68333680dacaebe1a1ec7952b8f5e6e23a70aa5"}, + {file = "fastrlock-0.8.3-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:45055702fe9bff719cdc62caa849aa7dbe9e3968306025f639ec62ef03c65e88"}, + {file = "fastrlock-0.8.3-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac4fcc9b43160f7f64b49bd7ecfd129faf0793c1c8c6f0f56788c3bacae7f54a"}, + {file = "fastrlock-0.8.3-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d3ebb29de71bf9e330c2769c34a6b5e69d560126f02994e6c09635a2784f6de3"}, + {file = "fastrlock-0.8.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:cc5fa9166e05409f64a804d5b6d01af670979cdb12cd2594f555cb33cdc155bd"}, + {file = "fastrlock-0.8.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:7a77ebb0a24535ef4f167da2c5ee35d9be1e96ae192137e9dc3ff75b8dfc08a5"}, + {file = "fastrlock-0.8.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:d51f7fb0db8dab341b7f03a39a3031678cf4a98b18533b176c533c122bfce47d"}, + {file = "fastrlock-0.8.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:767ec79b7f6ed9b9a00eb9ff62f2a51f56fdb221c5092ab2dadec34a9ccbfc6e"}, + {file = "fastrlock-0.8.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d6a77b3f396f7d41094ef09606f65ae57feeb713f4285e8e417f4021617ca62"}, + {file = "fastrlock-0.8.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:92577ff82ef4a94c5667d6d2841f017820932bc59f31ffd83e4a2c56c1738f90"}, + {file = "fastrlock-0.8.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3df8514086e16bb7c66169156a8066dc152f3be892c7817e85bf09a27fa2ada2"}, + {file = "fastrlock-0.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:001fd86bcac78c79658bac496e8a17472d64d558cd2227fdc768aa77f877fe40"}, + {file = "fastrlock-0.8.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:f68c551cf8a34b6460a3a0eba44bd7897ebfc820854e19970c52a76bf064a59f"}, + {file = "fastrlock-0.8.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:55d42f6286b9d867370af4c27bc70d04ce2d342fe450c4a4fcce14440514e695"}, + {file = "fastrlock-0.8.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:bbc3bf96dcbd68392366c477f78c9d5c47e5d9290cb115feea19f20a43ef6d05"}, + {file = "fastrlock-0.8.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:77ab8a98417a1f467dafcd2226718f7ca0cf18d4b64732f838b8c2b3e4b55cb5"}, + {file = "fastrlock-0.8.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:04bb5eef8f460d13b8c0084ea5a9d3aab2c0573991c880c0a34a56bb14951d30"}, + {file = "fastrlock-0.8.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8c9d459ce344c21ff03268212a1845aa37feab634d242131bc16c2a2355d5f65"}, + {file = "fastrlock-0.8.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:33e6fa4af4f3af3e9c747ec72d1eadc0b7ba2035456c2afb51c24d9e8a56f8fd"}, + {file = "fastrlock-0.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:5e5f1665d8e70f4c5b4a67f2db202f354abc80a321ce5a26ac1493f055e3ae2c"}, + {file = "fastrlock-0.8.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:8cb2cf04352ea8575d496f31b3b88c42c7976e8e58cdd7d1550dfba80ca039da"}, + {file = "fastrlock-0.8.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:85a49a1f1e020097d087e1963e42cea6f307897d5ebe2cb6daf4af47ffdd3eed"}, + {file = "fastrlock-0.8.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5f13ec08f1adb1aa916c384b05ecb7dbebb8df9ea81abd045f60941c6283a670"}, + {file = "fastrlock-0.8.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0ea4e53a04980d646def0f5e4b5e8bd8c7884288464acab0b37ca0c65c482bfe"}, + {file = "fastrlock-0.8.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:38340f6635bd4ee2a4fb02a3a725759fe921f2ca846cb9ca44531ba739cc17b4"}, + {file = "fastrlock-0.8.3-cp312-cp312-win_amd64.whl", hash = "sha256:da06d43e1625e2ffddd303edcd6d2cd068e1c486f5fd0102b3f079c44eb13e2c"}, + {file = "fastrlock-0.8.3-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5264088185ca8e6bc83181dff521eee94d078c269c7d557cc8d9ed5952b7be45"}, + {file = "fastrlock-0.8.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a98ba46b3e14927550c4baa36b752d0d2f7387b8534864a8767f83cce75c160"}, + {file = "fastrlock-0.8.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbdea6deeccea1917c6017d353987231c4e46c93d5338ca3e66d6cd88fbce259"}, + {file = "fastrlock-0.8.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c6e5bfecbc0d72ff07e43fed81671747914d6794e0926700677ed26d894d4f4f"}, + {file = "fastrlock-0.8.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:2a83d558470c520ed21462d304e77a12639859b205759221c8144dd2896b958a"}, + {file = "fastrlock-0.8.3-cp313-cp313-win_amd64.whl", hash = "sha256:8d1d6a28291b4ace2a66bd7b49a9ed9c762467617febdd9ab356b867ed901af8"}, + {file = "fastrlock-0.8.3-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a0eadc772353cfa464b34c814b2a97c4f3c0ba0ed7b8e1c2e0ad3ebba84bf8e0"}, + {file = "fastrlock-0.8.3-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:350f517a7d22d383f8ef76652b0609dc79de6693880a99bafc8a05c100e8c5e7"}, + {file = "fastrlock-0.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:924abbf21eba69c1b35c04278f3ca081e8de1ef5933355756e86e05499123238"}, + {file = "fastrlock-0.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fd6727c1e0952ba93fdc5975753781039772be6c1a3911a3afc87b53460dc0"}, + {file = "fastrlock-0.8.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:9c2c24856d2adc60ab398780f7b7cd8a091e4bd0c0e3bb3e67f12bef2800f377"}, + {file = "fastrlock-0.8.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2b84b2fe858e64946e54e0e918b8a0e77fc7b09ca960ae1e50a130e8fbc9af8"}, + {file = "fastrlock-0.8.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:963123bafc41c9fba72e57145917a3f23086b5d631b6cda9cf858c428a606ff9"}, + {file = "fastrlock-0.8.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:314e787532ce555a7362d3c438f0a680cd88a82c69b655e7181a4dd5e67712f5"}, + {file = "fastrlock-0.8.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:494fc374afd0b6c7281c87f2ded9607c2731fc0057ec63bd3ba4451e7b7cb642"}, + {file = "fastrlock-0.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:da53350b90a67d5431df726816b041f1f96fd558ad6e2fc64948e13be3c7c29a"}, + {file = "fastrlock-0.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cdee8c02c20a0b17dbc52f54c48ede3bd421985e5d9cef5cd2136b14da967996"}, + {file = "fastrlock-0.8.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:558b538221e9c5502bb8725a1f51157ec38467a20498212838e385807e4d1b89"}, + {file = "fastrlock-0.8.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6ac082d670e195ad53ec8d0c5d2e87648f8838b0d48f7d44a6e696b8a9528e2"}, + {file = "fastrlock-0.8.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d7edaf0071a6a98340fc2ec45b0ba37b7a16ed7761479aab577e41e09b3565e1"}, + {file = "fastrlock-0.8.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9c4068f21fddc47393a3526ce95b180a2f4e1ac286db8d9e59e56771da50c815"}, + {file = "fastrlock-0.8.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d7f359bb989c01a5875e8dbde9acab37b9da0943b60ef97ba9887c4598eb3009"}, + {file = "fastrlock-0.8.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:239e85cbebda16f14be92468ce648d0bc25e2442a3d11818deca59a7c43a4416"}, + {file = "fastrlock-0.8.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:5eef1d32d7614e0ceb6db198cf53df2a5830685cccbcf141a3e116faca967384"}, + {file = "fastrlock-0.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:80876d9e04e8e35abbdb3e1a81a56558f4d5cf90c8592e428d4d12efce048347"}, + {file = "fastrlock-0.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:24522689f4b5311afad0c8f998daec84a3dbe3a70cf821a615a763f843903030"}, + {file = "fastrlock-0.8.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:de8c90c1a23fbe929d8a9628a6c1f0f1d8af6019e786354a682a26fa22ea21be"}, + {file = "fastrlock-0.8.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0ceefadde046a5f6a261bfeaf25de9e0eba3ee790a9795b1fa9634111d3220e"}, + {file = "fastrlock-0.8.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1dd7f1520f7424793c812e1a4090570f8ff312725dbaf10a925b688aef7425f1"}, + {file = "fastrlock-0.8.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:15e13a8b01a3bbf25f1615a6ac1d6ed40ad3bcb8db134ee5ffa7360214a8bc5c"}, + {file = "fastrlock-0.8.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fcb50e195ec981c92d0211a201704aecbd9e4f9451aea3a6f71ac5b1ec2c98cf"}, + {file = "fastrlock-0.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:3e77a3d0ca5b29695d86b7d03ea88029c0ed8905cfee658eb36052df3861855a"}, + {file = "fastrlock-0.8.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:668fad1c8322badbc8543673892f80ee563f3da9113e60e256ae9ddd5b23daa4"}, + {file = "fastrlock-0.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:40b328369005a0b32de14b699192aed32f549c2d2b27a5e1f614fb7ac4cec4e9"}, + {file = "fastrlock-0.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:6cbfb6f7731b5a280851c93883624424068fa5b22c2f546d8ae6f1fd9311e36d"}, + {file = "fastrlock-0.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1fced4cb0b3f1616be68092b70a56e9173713a4a943d02e90eb9c7897a7b5e07"}, + {file = "fastrlock-0.8.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:387b2ac642938a20170a50f528817026c561882ea33306c5cbe750ae10d0a7c2"}, + {file = "fastrlock-0.8.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a0d31840a28d66573047d2df410eb971135a2461fb952894bf51c9533cbfea5"}, + {file = "fastrlock-0.8.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0a9dc6fa73174f974dfb22778d05a44445b611a41d5d3776b0d5daa9e50225c6"}, + {file = "fastrlock-0.8.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9842b7722e4923fe76b08d8c58a9415a9a50d4c29b80673cffeae4874ea6626a"}, + {file = "fastrlock-0.8.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:05029d7080c0c61a81d5fee78e842c9a1bf22552cd56129451a252655290dcef"}, + {file = "fastrlock-0.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:accd897ab2799024bb87b489c0f087d6000b89af1f184a66e996d3d96a025a3b"}, + {file = "fastrlock-0.8.3.tar.gz", hash = "sha256:4af6734d92eaa3ab4373e6c9a1dd0d5ad1304e172b1521733c6c3b3d73c8fa5d"}, +] + [[package]] name = "filelock" version = "3.19.1" @@ -473,9 +581,10 @@ tqdm = ["tqdm"] name = "furo" version = "2025.7.19" description = "A clean customisable Sphinx documentation theme." -optional = false +optional = true python-versions = ">=3.8" -groups = ["docs"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "furo-2025.7.19-py3-none-any.whl", hash = "sha256:bdea869822dfd2b494ea84c0973937e35d1575af088b6721a29c7f7878adc9e3"}, {file = "furo-2025.7.19.tar.gz", hash = "sha256:4164b2cafcf4023a59bb3c594e935e2516f6b9d35e9a5ea83d8f6b43808fe91f"}, @@ -503,9 +612,10 @@ files = [ name = "idna" version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" -optional = false +optional = true python-versions = ">=3.6" -groups = ["docs"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -552,9 +662,10 @@ tifffile = ["tifffile"] name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" -optional = false +optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["docs"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, @@ -564,10 +675,10 @@ files = [ name = "importlib-resources" version = "6.5.2" description = "Read resources from Python packages" -optional = false +optional = true python-versions = ">=3.9" -groups = ["dev"] -markers = "python_version < \"3.12\"" +groups = ["main"] +markers = "python_version < \"3.12\" and extra == \"test\"" files = [ {file = "importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec"}, {file = "importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c"}, @@ -587,11 +698,12 @@ version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] +markers = {main = "extra == \"test\""} [[package]] name = "jinja2" @@ -599,11 +711,12 @@ version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" -groups = ["dev", "docs"] +groups = ["main", "dev"] files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, ] +markers = {main = "extra == \"docs\""} [package.dependencies] MarkupSafe = ">=2.0" @@ -615,9 +728,10 @@ i18n = ["Babel (>=2.7)"] name = "joblib" version = "1.5.2" description = "Lightweight pipelining with Python functions" -optional = false +optional = true python-versions = ">=3.9" -groups = ["dev"] +groups = ["main"] +markers = "extra == \"test\"" files = [ {file = "joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241"}, {file = "joblib-1.5.2.tar.gz", hash = "sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55"}, @@ -647,9 +761,10 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"] name = "loguru" version = "0.7.3" description = "Python logging made (stupidly) simple" -optional = false +optional = true python-versions = "<4.0,>=3.5" -groups = ["dev"] +groups = ["main"] +markers = "extra == \"test\"" files = [ {file = "loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"}, {file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"}, @@ -668,7 +783,7 @@ version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.8" -groups = ["main", "docs"] +groups = ["main"] files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, @@ -693,7 +808,7 @@ version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" -groups = ["dev", "docs"] +groups = ["main", "dev"] files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, @@ -757,6 +872,7 @@ files = [ {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] +markers = {main = "extra == \"docs\""} [[package]] name = "mccabe" @@ -774,9 +890,10 @@ files = [ name = "mdit-py-plugins" version = "0.5.0" description = "Collection of plugins for markdown-it-py" -optional = false +optional = true python-versions = ">=3.10" -groups = ["docs"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f"}, {file = "mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6"}, @@ -796,7 +913,7 @@ version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" -groups = ["main", "docs"] +groups = ["main"] files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, @@ -824,9 +941,10 @@ tests = ["pytest (>=4.6)"] name = "myst-parser" version = "4.0.1" description = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser," -optional = false +optional = true python-versions = ">=3.10" -groups = ["docs"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d"}, {file = "myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4"}, @@ -894,9 +1012,10 @@ test-extras = ["pytest-mpl", "pytest-randomly"] name = "nibabel" version = "5.3.2" description = "Access a multitude of neuroimaging data formats" -optional = false +optional = true python-versions = ">=3.9" -groups = ["dev"] +groups = ["main"] +markers = "extra == \"test\"" files = [ {file = "nibabel-5.3.2-py3-none-any.whl", hash = "sha256:52970a5a8a53b1b55249cba4d9bcfaa8cc57e3e5af35a29d7352237e8680a6f8"}, {file = "nibabel-5.3.2.tar.gz", hash = "sha256:0bdca6503b1c784b446c745a4542367de7756cfba0d72143b91f9ffb78be569b"}, @@ -928,7 +1047,7 @@ version = "2.2.6" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.10" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb"}, {file = "numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90"}, @@ -1423,7 +1542,7 @@ version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["main", "dev", "docs"] +groups = ["main", "dev"] files = [ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, @@ -1520,9 +1639,10 @@ xml = ["lxml (>=4.9.2)"] name = "path" version = "17.1.1" description = "A module wrapper for os.path" -optional = false +optional = true python-versions = ">=3.9" -groups = ["dev"] +groups = ["main"] +markers = "extra == \"test\"" files = [ {file = "path-17.1.1-py3-none-any.whl", hash = "sha256:ec7e136df29172e5030dd07e037d55f676bdb29d15bfa09b80da29d07d3b9303"}, {file = "path-17.1.1.tar.gz", hash = "sha256:2dfcbfec8b4d960f3469c52acf133113c2a8bf12ac7b98d629fa91af87248d42"}, @@ -1542,7 +1662,7 @@ version = "11.3.0" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.9" -groups = ["main", "dev"] +groups = ["main"] files = [ {file = "pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860"}, {file = "pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad"}, @@ -1683,11 +1803,12 @@ version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, ] +markers = {main = "extra == \"test\""} [package.extras] dev = ["pre-commit", "tox"] @@ -1723,7 +1844,7 @@ version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" -groups = ["main", "dev", "docs"] +groups = ["main", "dev"] files = [ {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, @@ -1736,9 +1857,10 @@ windows-terminal = ["colorama (>=0.4.6)"] name = "pynrrd" version = "1.1.3" description = "Pure python module for reading and writing NRRD files." -optional = false +optional = true python-versions = ">=3.7" -groups = ["dev"] +groups = ["main"] +markers = "extra == \"test\"" files = [ {file = "pynrrd-1.1.3-py3-none-any.whl", hash = "sha256:21f7e370045e7ef8a86841965b2ff3a18937efbb4e2078e346168463f8f34b7e"}, {file = "pynrrd-1.1.3.tar.gz", hash = "sha256:a331263bc9f05c3168182e61d6098e256a34e0fadbb7427a1d086d8942fbcbe0"}, @@ -1757,11 +1879,12 @@ version = "8.4.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, ] +markers = {main = "extra == \"test\""} [package.dependencies] colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} @@ -1824,9 +1947,10 @@ files = [ name = "pyyaml" version = "6.0.2" description = "YAML parser and emitter for Python" -optional = false +optional = true python-versions = ">=3.8" -groups = ["docs"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -1887,9 +2011,10 @@ files = [ name = "requests" version = "2.32.5" description = "Python HTTP for Humans." -optional = false +optional = true python-versions = ">=3.9" -groups = ["docs"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, @@ -1929,10 +2054,10 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] name = "roman-numerals-py" version = "3.1.0" description = "Manipulate well-formed Roman numerals" -optional = false +optional = true python-versions = ">=3.9" -groups = ["docs"] -markers = "python_version >= \"3.11\"" +groups = ["main"] +markers = "python_version >= \"3.11\" and extra == \"docs\"" files = [ {file = "roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c"}, {file = "roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d"}, @@ -2251,9 +2376,10 @@ files = [ name = "simpleitk" version = "2.5.2" description = "SimpleITK is a simplified interface to the Insight Toolkit (ITK) for image registration and segmentation" -optional = false +optional = true python-versions = "*" -groups = ["dev"] +groups = ["main"] +markers = "extra == \"test\"" files = [ {file = "simpleitk-2.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ddfae02f6fc67829fcf13cffb22e8ff9d5cc79d6453d546c9937c1abafa7d257"}, {file = "simpleitk-2.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76ec652d30762adfedb167fc8fe3918099f0b87397c7656a0bab1ba21cb2dfcd"}, @@ -2288,9 +2414,10 @@ files = [ name = "snowballstemmer" version = "3.0.1" description = "This package provides 32 stemmers for 30 languages generated from Snowball algorithms." -optional = false +optional = true python-versions = "!=3.0.*,!=3.1.*,!=3.2.*" -groups = ["docs"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064"}, {file = "snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895"}, @@ -2300,9 +2427,10 @@ files = [ name = "soupsieve" version = "2.8" description = "A modern CSS selector implementation for Beautiful Soup." -optional = false +optional = true python-versions = ">=3.9" -groups = ["docs"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c"}, {file = "soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f"}, @@ -2312,10 +2440,10 @@ files = [ name = "sphinx" version = "8.1.3" description = "Python documentation generator" -optional = false +optional = true python-versions = ">=3.10" -groups = ["docs"] -markers = "python_version == \"3.10\"" +groups = ["main"] +markers = "python_version == \"3.10\" and extra == \"docs\"" files = [ {file = "sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2"}, {file = "sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927"}, @@ -2349,10 +2477,10 @@ test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools name = "sphinx" version = "8.2.3" description = "Python documentation generator" -optional = false +optional = true python-versions = ">=3.11" -groups = ["docs"] -markers = "python_version >= \"3.11\"" +groups = ["main"] +markers = "python_version >= \"3.11\" and extra == \"docs\"" files = [ {file = "sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3"}, {file = "sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348"}, @@ -2386,9 +2514,10 @@ test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "pytest-xdis name = "sphinx-basic-ng" version = "1.0.0b2" description = "A modern skeleton for Sphinx themes." -optional = false +optional = true python-versions = ">=3.7" -groups = ["docs"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b"}, {file = "sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9"}, @@ -2404,9 +2533,10 @@ docs = ["furo", "ipython", "myst-parser", "sphinx-copybutton", "sphinx-inline-ta name = "sphinx-copybutton" version = "0.5.2" description = "Add a copy button to each of your code cells." -optional = false +optional = true python-versions = ">=3.7" -groups = ["docs"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd"}, {file = "sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e"}, @@ -2423,9 +2553,10 @@ rtd = ["ipython", "myst-nb", "sphinx", "sphinx-book-theme", "sphinx-examples"] name = "sphinxcontrib-applehelp" version = "2.0.0" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" -optional = false +optional = true python-versions = ">=3.9" -groups = ["docs"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, @@ -2440,9 +2571,10 @@ test = ["pytest"] name = "sphinxcontrib-devhelp" version = "2.0.0" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" -optional = false +optional = true python-versions = ">=3.9" -groups = ["docs"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, @@ -2457,9 +2589,10 @@ test = ["pytest"] name = "sphinxcontrib-htmlhelp" version = "2.1.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -optional = false +optional = true python-versions = ">=3.9" -groups = ["docs"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, @@ -2474,9 +2607,10 @@ test = ["html5lib", "pytest"] name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" -optional = false +optional = true python-versions = ">=3.5" -groups = ["docs"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, @@ -2489,9 +2623,10 @@ test = ["flake8", "mypy", "pytest"] name = "sphinxcontrib-qthelp" version = "2.0.0" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" -optional = false +optional = true python-versions = ">=3.9" -groups = ["docs"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, @@ -2506,9 +2641,10 @@ test = ["defusedxml (>=0.7.1)", "pytest"] name = "sphinxcontrib-serializinghtml" version = "2.0.0" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" -optional = false +optional = true python-versions = ">=3.9" -groups = ["docs"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, @@ -2559,7 +2695,7 @@ version = "2025.5.10" description = "Read and write TIFF files" optional = false python-versions = ">=3.10" -groups = ["main", "dev"] +groups = ["main"] markers = "python_version == \"3.10\"" files = [ {file = "tifffile-2025.5.10-py3-none-any.whl", hash = "sha256:e37147123c0542d67bc37ba5cdd67e12ea6fbe6e86c52bee037a9eb6a064e5ad"}, @@ -2583,7 +2719,7 @@ version = "2025.9.9" description = "Read and write TIFF files" optional = false python-versions = ">=3.11" -groups = ["main", "dev"] +groups = ["main"] markers = "python_version >= \"3.11\"" files = [ {file = "tifffile-2025.9.9-py3-none-any.whl", hash = "sha256:239247551fa10b5679036ee030cdbeb7762bc1b3f11b1ddaaf50759ef8b4eb26"}, @@ -2607,8 +2743,7 @@ version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" -groups = ["dev", "docs"] -markers = "python_version == \"3.10\"" +groups = ["main", "dev"] files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -2643,6 +2778,7 @@ files = [ {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] +markers = {main = "(extra == \"test\" or extra == \"docs\") and python_version == \"3.10\"", dev = "python_version == \"3.10\""} [[package]] name = "torch" @@ -2774,9 +2910,10 @@ pyyaml = ["pyyaml"] name = "tqdm" version = "4.67.1" description = "Fast, Extensible Progress Meter" -optional = false +optional = true python-versions = ">=3.7" -groups = ["dev"] +groups = ["main"] +markers = "extra == \"test\"" files = [ {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, @@ -2866,7 +3003,7 @@ version = "4.15.0" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" -groups = ["main", "dev", "docs"] +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, @@ -2888,9 +3025,10 @@ files = [ name = "urllib3" version = "2.5.0" description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false +optional = true python-versions = ">=3.9" -groups = ["docs"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, @@ -2906,10 +3044,10 @@ zstd = ["zstandard (>=0.18.0)"] name = "win32-setctime" version = "1.2.0" description = "A small Python utility to set file creation time on Windows" -optional = false +optional = true python-versions = ">=3.5" -groups = ["dev"] -markers = "sys_platform == \"win32\"" +groups = ["main"] +markers = "extra == \"test\" and sys_platform == \"win32\"" files = [ {file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"}, {file = "win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0"}, @@ -2918,7 +3056,12 @@ files = [ [package.extras] dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"] +[extras] +docs = ["Sphinx", "furo", "myst-parser", "sphinx-copybutton"] +gpu = ["cupy"] +test = ["SimpleITK", "auxiliary", "joblib", "nibabel", "pynrrd", "pytest", "tqdm"] + [metadata] lock-version = "2.1" -python-versions = "^3.10" -content-hash = "f57e595a27ca3a87a1c8f5338b21e0a06031c4426fc730af9ec96d6e653e8333" +python-versions = ">=3.10,<4.0" +content-hash = "8726eb553a9a36550a67ac718bddee671f650bb25d685fa69295edbf7c45ea07" diff --git a/pyproject.toml b/pyproject.toml index e79212e..3a28b04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,56 +6,69 @@ build-backend = "poetry_dynamic_versioning.backend" enable = true [tool.poetry] +exclude = ["examples", "benchmark"] + +[project] name = "panoptica" version = "0.0.0" description = "Panoptic Quality (PQ) computation for binary masks." authors = [ - "Hendrik Möller ", - "Florian Kofler ", + { name = "Hendrik Möller", email = "hendrik.moeller@tum.de" }, + { name = "Florian Kofler", email = "florian.kofler@tum.de" }, +] +readme = "README.md" +requires-python = ">=3.10,<4.0" +license = { file = "LICENSE" } +keywords = ["panoptic", "quality", "segmentation", "medical imaging"] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", ] +dependencies = [ + "numpy>=1.22,<2.3", + "connected-components-3d>=3.12.3,<4.0.0", + "scipy>=1.7.0,<2.0.0", + "rich>=13.6.0,<14.0.0", + "scikit-image>=0.22.0,<1.0.0", + "ruamel.yaml>=0.18.6,<1.0.0", + "plotly>=5.16.1,<6.0.0", + "pandas>=2.1.0,<3.0.0", + "typer>=0.15.0,<1.0.0", +] + +[project.urls] repository = "https://github.com/BrainLesion/panoptica" homepage = "https://github.com/BrainLesion/panoptica" documentation = "https://panoptica.readthedocs.io/" -readme = "README.md" - - -# Add the exclude field directly under [tool.poetry] -exclude = ["examples", "benchmark"] - -[tool.poetry.dependencies] -python = "^3.10" -numpy = ">=1.22,<2.3" -connected-components-3d = "^3.12.3" -scipy = "^1.7.0" -rich = "^13.6.0" -scikit-image = ">=0.22.0, <1.0.0" -"ruamel.yaml" = ">=0.18.6, <1.0.0" -plotly = "^5.16.1" -pandas = "^2.1.0" -typer = ">=0.15.0, <1.0.0" [tool.poetry.group.dev.dependencies] -pytest = ">=8.1.1" coverage = ">=7.0.1" pytest-mock = "^3.6.0" -joblib = "^1.3.2" future = ">=0.18.3, <1.0.0" flake8 = ">=4.0.1" -auxiliary = ">=0.1.0" -tqdm = ">=4.62.3" -SimpleITK = "^2.2.2" torch = "^2.1.0" -nibabel = "^5.1.0" -pynrrd = "^1.1.3" -[tool.poetry.group.docs] -optional = true -[tool.poetry.group.docs.dependencies] -Sphinx = ">=7.0.0" -sphinx-copybutton = ">=0.5.2, <1.0.0" -furo = ">=2024.8.6" -myst-parser = ">=2.0.0" +[project.optional-dependencies] +test = [ + "pytest>=8.1.1", + "SimpleITK>=2.2.2,<3.0.0", + "nibabel>=5.1.0,<6.0.0", + "pynrrd>=1.1.3,<2.0.0", + "joblib>=1.3.2,<2.0.0", + "tqdm>=4.62.3", + "auxiliary>=0.1.0", +] +docs = [ + "Sphinx>=7.0.0", + "sphinx-copybutton>=0.5.2,<1.0.0", + "furo>=2024.8.6", + "myst-parser>=2.0.0", +] +gpu = [ + "cupy>=12.0.0", +] [tool.poetry.scripts] panopticacli = "panoptica.cli:app" diff --git a/unit_tests/test_config.py b/unit_tests/test_config.py index b6e4ccd..a93a3c9 100644 --- a/unit_tests/test_config.py +++ b/unit_tests/test_config.py @@ -123,7 +123,7 @@ def test_SegmentationClassGroups_config_by_name(self): self.assertEqual(len(t[k].value_labels), len(v.value_labels)) def test_InstanceApproximator_config(self): - for backend in [None, CCABackend.cc3d, CCABackend.scipy]: + for backend in [None, CCABackend.cc3d, CCABackend.scipy, CCABackend.cupy]: t = ConnectedComponentsInstanceApproximator(cca_backend=backend) print(t) print() diff --git a/unit_tests/test_cupy_connected_components.py b/unit_tests/test_cupy_connected_components.py new file mode 100644 index 0000000..5615c50 --- /dev/null +++ b/unit_tests/test_cupy_connected_components.py @@ -0,0 +1,264 @@ +# Unit tests for CuPy connected components functionality +import os +import unittest +import numpy as np +from unittest.mock import patch, MagicMock + +from panoptica.utils.constants import CCABackend +from panoptica._functionals import _connected_components +from panoptica import ConnectedComponentsInstanceApproximator +from panoptica.utils.processing_pair import SemanticPair + + +class Test_CuPy_Connected_Components(unittest.TestCase): + def setUp(self) -> None: + os.environ["PANOPTICA_CITATION_REMINDER"] = "False" + return super().setUp() + + def create_test_binary_array(self): + """Create a simple test binary array with known connected components.""" + # Create a 3D array with 3 separate connected components + arr = np.zeros((10, 10, 10), dtype=np.bool_) + + # Component 1: small cube in corner + arr[1:3, 1:3, 1:3] = True + + # Component 2: larger block in middle + arr[4:7, 4:7, 4:7] = True + + # Component 3: single isolated voxel + arr[8, 8, 8] = True + + return arr + + def test_cupy_backend_enum_exists(self): + """Test that CuPy backend is properly defined in the enum.""" + self.assertTrue(hasattr(CCABackend, "cupy")) + self.assertEqual(CCABackend.cupy.name, "cupy") + + def test_cupy_not_available_error(self): + """Test that proper error is raised when CuPy is not available.""" + test_array = self.create_test_binary_array() + + # Mock the import to fail + with patch( + "builtins.__import__", side_effect=ImportError("No module named 'cupy'") + ): + with self.assertRaises(ImportError) as context: + _connected_components(test_array, CCABackend.cupy) + + self.assertIn("CuPy is not installed", str(context.exception)) + self.assertIn("pip install cupy-cuda", str(context.exception)) + + def test_cupy_connected_components_function(self): + """Test the _connected_components function with CuPy backend.""" + test_array = self.create_test_binary_array() + + try: + # Try to call the function - it should either work or raise ImportError + result_array, result_n_components = _connected_components( + test_array, CCABackend.cupy + ) + + # If CuPy is available, verify basic properties + self.assertEqual(result_array.shape, test_array.shape) + self.assertGreaterEqual(result_n_components, 0) + + except (ImportError, Exception) as e: + # CuPy not available or CUDA issues, skip this test + if "CUDA" in str(e) or "cupy" in str(e).lower(): + self.skipTest( + f"CuPy/CUDA not available for connected components test: {e}" + ) + else: + raise + + def test_cupy_backend_comparison_with_scipy(self): + """Test that CuPy and SciPy backends produce similar results (when CuPy is available).""" + test_array = self.create_test_binary_array() + + try: + # Try to get results from both backends + scipy_result, scipy_n = _connected_components(test_array, CCABackend.scipy) + cupy_result, cupy_n = _connected_components(test_array, CCABackend.cupy) + + # Both should find the same number of components + self.assertEqual(scipy_n, cupy_n) + + # The label values might be different, but the structure should be the same + # Check that both arrays have the same shape and dtype + self.assertEqual(scipy_result.shape, cupy_result.shape) + + # Check that both find the same connected regions (regardless of label values) + scipy_unique = len(np.unique(scipy_result)) - 1 # subtract 1 for background + cupy_unique = len(np.unique(cupy_result)) - 1 # subtract 1 for background + self.assertEqual(scipy_unique, cupy_unique) + + except (ImportError, Exception) as e: + # CuPy not available or CUDA issues, skip this test + if "CUDA" in str(e) or "cupy" in str(e).lower(): + self.skipTest(f"CuPy/CUDA not available for comparison test: {e}") + else: + raise + + def test_instance_approximator_with_cupy_backend(self): + """Test ConnectedComponentsInstanceApproximator with CuPy backend.""" + try: + # Create test semantic arrays + pred_arr = np.zeros((10, 10, 10), dtype=np.uint8) + ref_arr = np.zeros((10, 10, 10), dtype=np.uint8) + + # Add some semantic labels + pred_arr[2:5, 2:5, 2:5] = 1 # One region + pred_arr[6:8, 6:8, 6:8] = 1 # Another region (same semantic class) + + ref_arr[1:4, 1:4, 1:4] = 1 # Overlapping region + ref_arr[7:9, 7:9, 7:9] = 1 # Another overlapping region + + # Create semantic pair + semantic_pair = SemanticPair(pred_arr, ref_arr) + + # Create approximator with CuPy backend + approximator = ConnectedComponentsInstanceApproximator( + cca_backend=CCABackend.cupy + ) + + # Test approximation + result = approximator.approximate_instances(semantic_pair) + + # Verify that we get an UnmatchedInstancePair + from panoptica.utils.processing_pair import UnmatchedInstancePair + + self.assertIsInstance(result, UnmatchedInstancePair) + + # Verify that instances were found + self.assertGreater(result.n_prediction_instance, 0) + self.assertGreater(result.n_reference_instance, 0) + + except (ImportError, Exception) as e: + # CuPy not available or CUDA issues, skip this test + if "CUDA" in str(e) or "cupy" in str(e).lower(): + self.skipTest( + f"CuPy/CUDA not available for instance approximator test: {e}" + ) + else: + raise + + def test_cupy_backend_config_serialization(self): + """Test that CuPy backend can be serialized/deserialized in config.""" + from pathlib import Path + + test_file = Path(__file__).parent.joinpath("test_cupy.yaml") + + try: + # Test CuPy backend serialization + backend = CCABackend.cupy + backend.save_to_config(test_file) + + loaded_backend = CCABackend.load_from_config(test_file) + self.assertEqual(backend, loaded_backend) + + # Test with ConnectedComponentsInstanceApproximator + approximator = ConnectedComponentsInstanceApproximator( + cca_backend=CCABackend.cupy + ) + approximator.save_to_config(test_file) + + loaded_approximator = ( + ConnectedComponentsInstanceApproximator.load_from_config(test_file) + ) + self.assertEqual(loaded_approximator.cca_backend, CCABackend.cupy) + + finally: + # Clean up + if test_file.exists(): + os.remove(test_file) + + def test_various_array_shapes_with_cupy(self): + """Test CuPy backend with different array shapes and dimensions.""" + test_shapes = [ + (50, 50), # 2D + (20, 20, 20), # 3D + (10, 10, 10, 10), # 4D + ] + + for shape in test_shapes: + with self.subTest(shape=shape): + try: + # Create test array + arr = np.zeros(shape, dtype=np.bool_) + # Add a small component + slices = tuple(slice(1, 3) for _ in range(len(shape))) + arr[slices] = True + + # Test with CuPy + result_arr, n_components = _connected_components( + arr, CCABackend.cupy + ) + + # Should find at least one component + self.assertGreaterEqual(n_components, 1) + self.assertEqual(result_arr.shape, arr.shape) + + except (ImportError, Exception) as e: + # CuPy not available or CUDA issues + if "CUDA" in str(e) or "cupy" in str(e).lower(): + self.skipTest( + f"CuPy/CUDA not available for shape {shape} test: {e}" + ) + else: + raise + + def test_empty_array_with_cupy(self): + """Test CuPy backend with empty arrays.""" + try: + empty_arr = np.zeros((10, 10, 10), dtype=np.bool_) + + result_arr, n_components = _connected_components(empty_arr, CCABackend.cupy) + + # Should find no components + self.assertEqual(n_components, 0) + self.assertEqual(result_arr.shape, empty_arr.shape) + # All values should be 0 (background) + self.assertEqual(np.max(result_arr), 0) + + except (ImportError, Exception) as e: + if "CUDA" in str(e) or "cupy" in str(e).lower(): + self.skipTest(f"CuPy/CUDA not available for empty array test: {e}") + else: + raise + + def test_cupy_backend_with_large_array(self): + """Test CuPy backend with larger arrays to verify GPU memory handling.""" + try: + # Create a larger test array + large_arr = np.zeros((100, 100, 50), dtype=np.bool_) + + # Add several components + large_arr[10:20, 10:20, 10:20] = True # Component 1 + large_arr[30:40, 30:40, 30:40] = True # Component 2 + large_arr[60:70, 60:70, 10:20] = True # Component 3 + large_arr[80:90, 10:20, 30:40] = True # Component 4 + + result_arr, n_components = _connected_components(large_arr, CCABackend.cupy) + + # Should find 4 components + self.assertEqual(n_components, 4) + self.assertEqual(result_arr.shape, large_arr.shape) + + # Verify that we have the right number of unique labels (including background) + unique_labels = np.unique(result_arr) + self.assertEqual(len(unique_labels), 5) # 4 components + background (0) + + except ImportError: + self.skipTest("CuPy not available for large array test") + except Exception as e: + # If GPU memory issues or other CUDA errors, skip + if "CUDA" in str(e) or "memory" in str(e).lower(): + self.skipTest(f"GPU/CUDA issues: {e}") + else: + raise + + +if __name__ == "__main__": + unittest.main() diff --git a/unit_tests/test_input_sanity_checker.py b/unit_tests/test_input_sanity_checker.py index 8e68ae8..bda4039 100644 --- a/unit_tests/test_input_sanity_checker.py +++ b/unit_tests/test_input_sanity_checker.py @@ -9,8 +9,18 @@ import SimpleITK as sitk from unittest import mock import nibabel as nib -import torch import nrrd +from importlib.util import find_spec + +# Optional torch import +_spec = find_spec("torch") +if _spec is not None: + import torch + + HAS_TORCH = True +else: + torch = None + HAS_TORCH = False from panoptica.utils.input_check_and_conversion.sanity_checker import ( sanity_check_and_convert_to_array, @@ -324,6 +334,7 @@ def test_sanity_checker_without_package(self, *args): os.remove(test_nii_file) +@unittest.skipUnless(HAS_TORCH, "torch not available") class Test_Input_Sanity_Checker_Torch(unittest.TestCase): def setUp(self) -> None: os.environ["PANOPTICA_CITATION_REMINDER"] = "False" diff --git a/unit_tests/test_region_comprehensive.py b/unit_tests/test_region_comprehensive.py new file mode 100644 index 0000000..742d5cc --- /dev/null +++ b/unit_tests/test_region_comprehensive.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +""" +Comprehensive test suite for RegionBasedMatching implementation +""" + +import numpy as np +from panoptica.panoptica_evaluator import panoptic_evaluate +from panoptica.instance_matcher import RegionBasedMatching +from panoptica.instance_approximator import ConnectedComponentsInstanceApproximator +from panoptica.metrics import Metric +from panoptica.utils.constants import CCABackend +from panoptica.utils.processing_pair import SemanticPair + + +def test_scenario_1_basic(): + """Test basic case with non-overlapping regions""" + print("Test 1: Basic non-overlapping regions") + + gt = np.zeros((30, 30, 10), dtype=np.int32) + pred = np.zeros((30, 30, 10), dtype=np.int32) + + # GT regions + gt[5:15, 5:15, 2:8] = 1 + gt[20:25, 20:25, 2:8] = 2 + + # Pred regions - slightly offset + pred[6:16, 6:16, 3:9] = 1 + pred[19:24, 19:24, 3:9] = 2 + + return gt, pred + + +def test_scenario_2_overlapping(): + """Test case with overlapping predictions""" + print("Test 2: Overlapping predictions") + + gt = np.zeros((30, 30, 10), dtype=np.int32) + pred = np.zeros((30, 30, 10), dtype=np.int32) + + # GT regions + gt[5:15, 5:15, 2:8] = 1 + gt[20:25, 20:25, 2:8] = 2 + + # Overlapping predictions + pred[8:18, 8:18, 3:9] = 1 # Overlaps with both GT regions + pred[21:26, 21:26, 3:9] = 2 + + return gt, pred + + +def test_scenario_3_empty_prediction(): + """Test case with no predictions""" + print("Test 3: Empty predictions") + + gt = np.zeros((30, 30, 10), dtype=np.int32) + pred = np.zeros((30, 30, 10), dtype=np.int32) + + # Only GT regions, no predictions + gt[5:15, 5:15, 2:8] = 1 + gt[20:25, 20:25, 2:8] = 2 + + return gt, pred + + +def test_scenario_4_extra_predictions(): + """Test case with more predictions than GT regions""" + print("Test 4: Extra predictions") + + gt = np.zeros((40, 40, 10), dtype=np.int32) + pred = np.zeros((40, 40, 10), dtype=np.int32) + + # GT regions + gt[5:15, 5:15, 2:8] = 1 + + # Multiple predictions + pred[6:16, 6:16, 3:9] = 1 # Close to GT + pred[20:25, 20:25, 3:9] = 2 # Far from GT + pred[30:35, 30:35, 3:9] = 3 # Even farther + + return gt, pred + + +def run_test_scenario(gt, pred, scenario_name): + """Run a test scenario and return results""" + print(f"\n{scenario_name}") + print(f"GT unique values: {np.unique(gt)}") + print(f"Pred unique values: {np.unique(pred)}") + + try: + # Create components + matcher = RegionBasedMatching(cca_backend=CCABackend.scipy) + approximator = ConnectedComponentsInstanceApproximator() + semantic_pair = SemanticPair(prediction_arr=pred, reference_arr=gt) + + # Run evaluation + result = panoptic_evaluate( + input_pair=semantic_pair, + instance_approximator=approximator, + instance_matcher=matcher, + instance_metrics=[Metric.DSC, Metric.IOU], + global_metrics=[Metric.DSC], + verbose=False, + ) + + print(f"✅ {scenario_name} successful!") + print( + f" Pred instances: {result.num_pred_instances}, Ref instances: {result.num_ref_instances}" + ) + print(f" TP: {result.tp}, FP: {result.fp}, FN: {result.fn}") + + # Check individual metrics if available + if hasattr(result, "list_metrics") and result.list_metrics: + for metric, values in result.list_metrics.items(): + if values: # Only print if there are values + avg_val = np.mean(values) if values else 0 + print(f" {metric}: avg={avg_val:.3f}, values={len(values)}") + + return True + + except Exception as e: + print(f"❌ {scenario_name} failed: {e}") + import traceback + + traceback.print_exc() + return False + + +def main(): + """Run all test scenarios""" + print("🧪 Running comprehensive RegionBasedMatching tests...") + + scenarios = [ + (test_scenario_1_basic, "Test 1: Basic non-overlapping"), + (test_scenario_2_overlapping, "Test 2: Overlapping predictions"), + (test_scenario_3_empty_prediction, "Test 3: Empty predictions"), + (test_scenario_4_extra_predictions, "Test 4: Extra predictions"), + ] + + results = [] + for scenario_func, name in scenarios: + gt, pred = scenario_func() + success = run_test_scenario(gt, pred, name) + results.append(success) + + # Summary + passed = sum(results) + total = len(results) + print(f"\n📊 Test Summary: {passed}/{total} scenarios passed") + + if passed == total: + print("🎉 All comprehensive tests passed!") + return True + else: + print("💥 Some tests failed!") + return False + + +if __name__ == "__main__": + main() diff --git a/unit_tests/test_region_integration.py b/unit_tests/test_region_integration.py new file mode 100644 index 0000000..3b0a5d4 --- /dev/null +++ b/unit_tests/test_region_integration.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +""" +Integration test for RegionBasedMatching with full panoptic evaluation +""" + +import numpy as np +from panoptica.panoptica_evaluator import panoptic_evaluate +from panoptica.instance_matcher import RegionBasedMatching +from panoptica.instance_approximator import ConnectedComponentsInstanceApproximator +from panoptica.metrics import Metric +from panoptica.utils.constants import CCABackend +from panoptica.utils.processing_pair import SemanticPair + + +def create_test_data(): + """Create simple test data with ground truth and prediction instances""" + # Create a simple 3D volume with 2 GT regions + gt = np.zeros((50, 50, 20), dtype=np.int32) + pred = np.zeros((50, 50, 20), dtype=np.int32) + + # GT region 1: cube in corner + gt[10:20, 10:20, 5:15] = 1 + + # GT region 2: cube in opposite corner + gt[30:40, 30:40, 5:15] = 2 + + # Prediction region 1: slightly offset from GT region 1 + pred[12:22, 12:22, 6:16] = 1 + + # Prediction region 2: different location, should map to closest GT region + pred[25:35, 25:35, 6:16] = 2 + + return gt, pred + + +def test_region_integration(): + """Test RegionBasedMatching with full panoptic evaluation""" + print("Testing RegionBasedMatching with panoptic_evaluate...") + + # Create test data + gt, pred = create_test_data() + + print(f"GT shape: {gt.shape}, unique values: {np.unique(gt)}") + print(f"Pred shape: {pred.shape}, unique values: {np.unique(pred)}") + + # Create region-based matcher + matcher = RegionBasedMatching(cca_backend=CCABackend.scipy) + + # Create instance approximator + approximator = ConnectedComponentsInstanceApproximator() + + try: + # Create semantic pair + semantic_pair = SemanticPair(prediction_arr=pred, reference_arr=gt) + + # Run panoptic evaluation + result = panoptic_evaluate( + input_pair=semantic_pair, + instance_approximator=approximator, + instance_matcher=matcher, + instance_metrics=[Metric.DSC, Metric.IOU], + global_metrics=[Metric.DSC], + verbose=True, + ) + + print(f"\n✅ Integration test successful!") + print(f"Number of prediction instances: {result.num_pred_instances}") + print(f"Number of reference instances: {result.num_ref_instances}") + print(f"TP: {result.tp}") + print(f"FP: {result.fp}") + print(f"FN: {result.fn}") + print(f"Precision: {result.prec}") + print(f"Recall: {result.rec}") + print(f"RQ: {result.rq}") + + # Check if metrics are NaN as expected for region-based matching + if np.isnan(result.tp): + print("✅ Count metrics correctly set to NaN for region-based matching") + else: + print("⚠️ Expected TP to be NaN for region-based matching") + + # Check individual instance metrics + if hasattr(result, "list_metrics") and result.list_metrics: + print(f"\nInstance metrics:") + for metric, values in result.list_metrics.items(): + print(f" {metric}: {values}") + + return True + + except Exception as e: + print(f"❌ Error during integration test: {e}") + import traceback + + traceback.print_exc() + return False + + +if __name__ == "__main__": + success = test_region_integration() + if success: + print("\n🎉 All integration tests passed!") + else: + print("\n💥 Integration test failed!") diff --git a/unit_tests/test_region_matching.py b/unit_tests/test_region_matching.py new file mode 100644 index 0000000..332b522 --- /dev/null +++ b/unit_tests/test_region_matching.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +""" +Test script for the RegionBasedMatching implementation +""" + +import numpy as np +from panoptica.instance_matcher import RegionBasedMatching +from panoptica.utils.processing_pair import UnmatchedInstancePair +from panoptica.utils.constants import CCABackend + + +def create_test_data(): + """Create simple test data with ground truth and prediction instances""" + # Create a simple 3D volume with 2 GT regions + gt = np.zeros((50, 50, 20), dtype=np.int32) + pred = np.zeros((50, 50, 20), dtype=np.int32) + + # GT region 1: cube in corner + gt[10:20, 10:20, 5:15] = 1 + + # GT region 2: cube in opposite corner + gt[30:40, 30:40, 5:15] = 2 + + # Prediction region 1: slightly offset from GT region 1 + pred[12:22, 12:22, 6:16] = 1 + + # Prediction region 2: different location, should map to closest GT region + pred[25:35, 25:35, 6:16] = 2 + + return gt, pred + + +def test_region_based_matching(): + """Test the RegionBasedMatching algorithm""" + print("Testing RegionBasedMatching...") + + # Create test data + gt, pred = create_test_data() + + # Create unmatched instance pair + unmatched_pair = UnmatchedInstancePair(prediction_arr=pred, reference_arr=gt) + + print(f"Ground truth labels: {unmatched_pair.ref_labels}") + print(f"Prediction labels: {unmatched_pair.pred_labels}") + + # Create region-based matcher + matcher = RegionBasedMatching(cca_backend=CCABackend.scipy) + + # Perform matching + try: + labelmap = matcher._match_instances(unmatched_pair) + + print(f"Matching successful!") + print(f"Label map: {labelmap.get_one_to_one_dictionary()}") + + # Create matched instance pair + matched_pair = matcher.match_instances(unmatched_pair) + + print(f"Matched instances: {matched_pair.matched_instances}") + print(f"Prediction instances: {matched_pair.n_prediction_instance}") + print(f"Reference instances: {matched_pair.n_reference_instance}") + + return True + + except Exception as e: + print(f"Error during matching: {e}") + import traceback + + traceback.print_exc() + return False + + +if __name__ == "__main__": + success = test_region_based_matching() + if success: + print("\n✅ Region-based matching test passed!") + else: + print("\n❌ Region-based matching test failed!")