diff --git a/packages/scratch-core/pyproject.toml b/packages/scratch-core/pyproject.toml index c3a3cf6f..4b5aed81 100644 --- a/packages/scratch-core/pyproject.toml +++ b/packages/scratch-core/pyproject.toml @@ -15,6 +15,7 @@ dependencies = [ "pillow>=12.0.0", "pydantic>=2.12.4", "returns>=0.26.0", + "scikit-image>=0.25.2", "scipy>=1.16.3", "surfalize~=0.16.6", "x3p @ git+https://github.com/giacomomarchioro/pyx3p.git#81c0f764cf321e56dc41e9e3c71d14e97d5bc3ae", diff --git a/packages/scratch-core/src/conversion/resample.py b/packages/scratch-core/src/conversion/resample.py index 7356876c..21211c6a 100644 --- a/packages/scratch-core/src/conversion/resample.py +++ b/packages/scratch-core/src/conversion/resample.py @@ -2,17 +2,17 @@ import numpy as np from numpy.typing import NDArray -from scipy import ndimage +from skimage.transform import resize from conversion.data_formats import Mark from container_models.scan_image import ScanImage from container_models.base import MaskArray -def resample_image_and_mask( - image: ScanImage, +def resample_scan_image_and_mask( + scan_image: ScanImage, mask: Optional[MaskArray] = None, - resample_factors: Optional[tuple[float, float]] = None, + factors: Optional[tuple[float, float]] = None, target_scale: float = 4e-6, only_downsample: bool = True, preserve_aspect_ratio: bool = True, @@ -20,46 +20,34 @@ def resample_image_and_mask( """ Resample the input image and optionally its corresponding mask. - If `only_downsample` is True and the current resolution is already coarser - than the target scale, no resampling is performed. If `resample_factors` are - provided, it overrides the target scale. + If `only_downsample` is True and the current resolution is already coarser than the target scale, + no resampling is performed. If `factors` are provided, it overrides the target scale. - The resampling factor determines how the image dimensions will change: - - factor < 1: upsampling (more pixels, finer resolution) - - factor > 1: downsampling (fewer pixels, coarser resolution) - - factor = 1: no change - - :param image: Input ScanImage to resample - :param mask: Corresponding mask array - :param resample_factors: Resampling factors - :param target_scale: Target scale (m) when resample_factors are not provided + :param scan_image: Input ScanImage to resample. + :param mask: Corresponding mask array. + :param factors: The multipliers for the scale of the X- and Y-axis. The formula used is `new_scale = factor * old_scale`. + :param target_scale: Target scale (in meters) when `factors` are not provided. :param preserve_aspect_ratio: Whether to preserve the aspect ratio of the image. - :param only_downsample: If True, only downsample data - + :param only_downsample: If True, only downsample data (default). If False, allow upsampling. :returns: Resampled ScanImage and MaskArray """ - if not resample_factors: - resample_factors = get_resampling_factors( - image.scale_x, - image.scale_y, - target_scale, + if not factors: + factors = _get_scaling_factors( + scales=(scan_image.scale_x, scan_image.scale_y), target_scale=target_scale ) if only_downsample: - resample_factors = clip_resample_factors( - resample_factors, preserve_aspect_ratio - ) - if resample_factors == (1, 1): - return image, mask - - image = resample_scan_image(image, resample_factors) + factors = _clip_factors(factors, preserve_aspect_ratio) + if np.allclose(factors, 1.0): + return scan_image, mask + image = _resample_scan_image(scan_image, factors=factors) if mask is not None: - mask = resample_mask(mask, resample_factors) + mask = _resample_image_array(mask, factors=factors) return image, mask def resample_mark(mark: Mark) -> Mark: """Resample a MarkImage so that the scale matches the scale specific for the mark type.""" - resampled_scan_image, _ = resample_image_and_mask( + resampled_scan_image, _ = resample_scan_image_and_mask( mark.scan_image, target_scale=mark.mark_type.scale, only_downsample=False, @@ -67,82 +55,68 @@ def resample_mark(mark: Mark) -> Mark: return mark.model_copy(update={"scan_image": resampled_scan_image}) -def resample_mask(mask: MaskArray, resample_factors: tuple[float, float]) -> MaskArray: - """Resample the provided mask array using the specified resampling factors.""" - return _resample_array(mask, resample_factors, order=0, mode="nearest") - +def _resample_scan_image(image: ScanImage, factors: tuple[float, float]) -> ScanImage: + """ + Resample the ScanImage object using the specified resampling factors. -def resample_scan_image( - image: ScanImage, resample_factors: tuple[float, float] -) -> ScanImage: - """Resample the ScanImage object using the specified resampling factors.""" - image_array_resampled = _resample_array( - image.data, resample_factors, order=1, mode="nearest" - ) + :param image: Input ScanImage to resample. + :param factors: The multipliers for the scale of the X- and Y-axis. + :returns: The resampled ScanImage. + """ + image_array_resampled = _resample_image_array(image.data, factors=factors) return ScanImage( data=image_array_resampled, - scale_x=image.scale_x * resample_factors[0], - scale_y=image.scale_y * resample_factors[1], + scale_x=image.scale_x * factors[0], + scale_y=image.scale_y * factors[1], ) -def _resample_array( +def _resample_image_array( array: NDArray, - resample_factors: tuple[float, float], - order: int, - mode: str, + factors: tuple[float, float], ) -> NDArray: """ - Resample an array using the specified resampling factors, order, and mode. + Resample an array using the specified resampling factors. - :param array: The array to resample. - :param resample_factors: The resampling factors for the x- and y-axis. - :param order: The order of the spline interpolation to use. - :param mode: The mode to use for handling boundaries. + For example, if the scale factor is 0.5, then the image output shape will be scaled by 1 / 0.5 = 2. - :returns: The resampled array. + :param array: The array containing the image data to resample. + :param factors: The multipliers for the scale of the X- and Y-axis. + :returns: A numpy array containing the resampled image data. """ - resample_factor_x, resample_factor_y = resample_factors - resampled = ndimage.zoom( - array, - (1 / resample_factor_y, 1 / resample_factor_x), - order=order, - mode=mode, + factor_x, factor_y = factors + resampled = resize( + image=array, + output_shape=(1 / factor_y * array.shape[0], 1 / factor_x * array.shape[1]), + mode="edge", + anti_aliasing=array.dtype != np.bool_ and all(factor > 1 for factor in factors), ) - return np.asarray(resampled).astype(array.dtype) + return np.asarray(resampled, dtype=array.dtype) -def get_resampling_factors( - scale_x: float, - scale_y: float, +def _get_scaling_factors( + scales: tuple[float, float], target_scale: float, ) -> tuple[float, float]: """ - Calculate resampling factors for x and y dimensions. + Calculate the multipliers for a target scale. - :param scale_x: Scale for the x-axis - :param scale_y: Scale for the y-axis - :param target_scale: Target pixel size (in meters). + :param scales: Current scales (= pixel size in meters per image dimension). + :param target_scale: Target scale (= pixel size in meters). - :returns: Resampling factors. + :returns: The computed multipliers. """ - resample_factor_x = target_scale / scale_x - resample_factor_y = target_scale / scale_y - return resample_factor_x, resample_factor_y + return target_scale / scales[0], target_scale / scales[1] -def clip_resample_factors( - resample_factors: tuple[float, float], +def _clip_factors( + factors: tuple[float, float], preserve_aspect_ratio: bool, ) -> tuple[float, float]: - """Clip the resample factors to minimum 1.0, while keeping the aspect ratio if `preserve_aspect_ratio` is True.""" + """Clip the scaling factors to minimum 1.0, while keeping the aspect ratio if `preserve_aspect_ratio` is True.""" if preserve_aspect_ratio: - # Scale both factors equally to preserve the aspect ratio - max_factor = max(resample_factors) - resample_factors = (max_factor, max_factor) + # Set the multipliers to equal values to preserve the aspect ratio + max_factor = max(factors) + factors = max_factor, max_factor - resample_factors = ( - max(resample_factors[0], 1.0), - max(resample_factors[1], 1.0), - ) - return resample_factors + return max(factors[0], 1.0), max(factors[1], 1.0) diff --git a/packages/scratch-core/src/image_generation/data_formats.py b/packages/scratch-core/src/image_generation/data_formats.py deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/scratch-core/tests/conversion/test_resample.py b/packages/scratch-core/tests/conversion/test_resample.py index 42f7fd80..ea6a3385 100644 --- a/packages/scratch-core/tests/conversion/test_resample.py +++ b/packages/scratch-core/tests/conversion/test_resample.py @@ -1,133 +1,158 @@ import numpy as np -import pytest +from unittest.mock import patch, MagicMock +from container_models.scan_image import ScanImage from conversion.data_formats import Mark from conversion.resample import ( - get_resampling_factors, - resample_image_and_mask, - clip_resample_factors, + resample_scan_image_and_mask, + _resample_scan_image, + _get_scaling_factors, + _clip_factors, + _resample_image_array, resample_mark, ) -from container_models.scan_image import ScanImage -from container_models.base import MaskArray -class TestGetResamplingFactors: - """Tests for get_resampling_factors function.""" +class TestGetScalingFactors: + def test_basic_calculation(self): + assert _get_scaling_factors((2e-6, 2e-6), 4e-6) == (2.0, 2.0) - def test_upsample_with_scales_bigger_than_target(self): - factor_x, factor_y = get_resampling_factors(10e-6, 10e-6, 5e-6) - assert factor_x == pytest.approx(0.5) - assert factor_y == pytest.approx(0.5) + def test_different_axes(self): + assert _get_scaling_factors((1e-6, 2e-6), 4e-6) == (4.0, 2.0) - def test_downsample_with_scales_smaller_than_target(self): - factor_x, factor_y = get_resampling_factors(1e-6, 1e-6, 4e-6) - assert factor_x == pytest.approx(4.0) - assert factor_y == pytest.approx(4.0) + def test_upsampling(self): + assert _get_scaling_factors((8e-6, 8e-6), 4e-6) == (0.5, 0.5) - def test_no_resampling_needed(self): - factor_x, factor_y = get_resampling_factors(4e-6, 4e-6, 4e-6) - assert factor_x == pytest.approx(1.0) - assert factor_y == pytest.approx(1.0) - def test_different_scales_lead_to_different_factors(self): - factor_x, factor_y = get_resampling_factors(2e-6, 4e-6, 4e-6) - assert factor_x == pytest.approx(2.0) - assert factor_y == pytest.approx(1.0) +class TestClipFactors: + def test_no_clipping_needed(self): + assert _clip_factors((2.0, 1.5), False) == (2.0, 1.5) + def test_clip_below_one(self): + assert _clip_factors((0.5, 2.0), False) == (1.0, 2.0) -class TestClipResampleFactors: - """Tests for clip_resample_factors function.""" + def test_preserve_aspect_ratio_clips_to_max(self): + assert _clip_factors((0.5, 2.0), True) == (2.0, 2.0) - def test_only_downsample_clamps_factors_below_1(self): - """Factors below 1 (upsampling) get clamped to 1.""" - result = clip_resample_factors((0.5, 0.5), preserve_aspect_ratio=False) - assert result == (1.0, 1.0) + def test_preserve_aspect_ratio_all_below_one(self): + assert _clip_factors((0.5, 0.8), True) == (1.0, 1.0) - def test_only_downsample_preserves_factors_above_1(self): - """Factors above 1 (downsampling) are preserved.""" - result = clip_resample_factors((4.0, 4.0), preserve_aspect_ratio=False) - assert result == (4.0, 4.0) - def test_only_downsample_clips_mixed_factors(self): - """Mixed factors: those below 1 get clamped, those above 1 preserved.""" - result = clip_resample_factors((0.5, 2.0), preserve_aspect_ratio=False) - assert result == (1.0, 2.0) +class TestResampleArray: + @patch("conversion.resample.resize") + def test_calculates_output_shape_correctly(self, mock_resize: MagicMock): + array = np.zeros((100, 200)) + mock_resize.return_value = np.zeros((50, 100)) - def test_preserve_aspect_ratio_with_only_downsample(self): - """Aspect ratio preserved first, then clamped if needed.""" - result = clip_resample_factors((0.5, 2.0), preserve_aspect_ratio=True) - assert result == (2.0, 2.0) + _resample_image_array(array, factors=(2.0, 2.0)) + call_args = mock_resize.call_args[1] + assert call_args["output_shape"] == (50.0, 100.0) -class TestResample: - """Tests for resample function.""" + @patch("conversion.resample.resize") + def test_anti_aliasing_on_upsampling(self, mock_resize: MagicMock): + array = np.zeros((100, 100)) + mock_resize.return_value = np.zeros((200, 200)) - def test_output_shape_matches_resample_size(self, scan_image: ScanImage): - """Output array shape matches original shape.""" - result, _ = resample_image_and_mask(scan_image, target_scale=4e-6) - assert result.data.shape == scan_image.data.shape + _resample_image_array(array, factors=(0.5, 0.5)) - def test_output_shape_matches_clamped_upsampled_size(self, scan_image: ScanImage): - """Output array shape matches expected size (unchanged since only_downsample is True).""" - result, _ = resample_image_and_mask( - scan_image, target_scale=1e-6, only_downsample=True - ) - assert result.data.shape == scan_image.data.shape + assert mock_resize.call_args[1]["anti_aliasing"] is False - def test_output_shape_matches_upsampled_size(self, scan_image: ScanImage): - """Output array shape matches expected upsampled size.""" - result, _ = resample_image_and_mask( - scan_image, target_scale=1e-6, only_downsample=False - ) - assert result.data.shape == tuple(i * 4 for i in scan_image.data.shape) + @patch("conversion.resample.resize") + def test_no_anti_aliasing_on_downsampling(self, mock_resize: MagicMock): + array = np.zeros((100, 100)) + mock_resize.return_value = np.zeros((50, 50)) - def test_scale_updated_according_to_target_scale(self, scan_image: ScanImage): - """Output scales are updated correctly.""" - result, _ = resample_image_and_mask(scan_image, target_scale=8e-6) - assert result.scale_x == pytest.approx(8e-6) - assert result.scale_y == pytest.approx(8e-6) + _resample_image_array(array, factors=(2.0, 2.0)) - def test_no_resampling_returns_original( - self, scan_image_replica: ScanImage, mask_array: MaskArray - ): - """When no resampling needed, returns original objects.""" - result, result_mask = resample_image_and_mask( - scan_image_replica, - mask=mask_array, - target_scale=0.5e-6, - only_downsample=True, - ) - assert result is scan_image_replica - assert result_mask is mask_array + assert mock_resize.call_args[1]["anti_aliasing"] is True + + +class TestResampleScanImage: + def test_updates_scales(self, scan_image_rectangular_with_nans: ScanImage): + with patch("conversion.resample._resample_image_array") as mock: + mock.return_value = np.zeros((50, 50)) + + result = _resample_scan_image(scan_image_rectangular_with_nans, (2.0, 2.0)) + + assert result.scale_x == scan_image_rectangular_with_nans.scale_x * 2.0 + assert result.scale_y == scan_image_rectangular_with_nans.scale_y * 2.0 - def test_mask_none_passthrough(self, scan_image: ScanImage): - """When mask is None, returns None.""" - _, result_mask = resample_image_and_mask( - scan_image, mask=None, target_scale=4e-6 - ) - assert result_mask is None - def test_mask_resampled_to_same_shape_as_image( - self, scan_image_replica: ScanImage, mask_array: MaskArray +class TestResampleImageAndMask: + def test_no_resampling_when_factors_close_to_one( + self, scan_image_rectangular_with_nans: ScanImage ): - """Mask is resampled to same shape as image.""" - result, result_mask = resample_image_and_mask( - scan_image_replica, mask=mask_array, target_scale=1e-6 + mask = np.ones((100, 100), dtype=np.bool_) + + result_img, result_mask = resample_scan_image_and_mask( + scan_image_rectangular_with_nans, mask, factors=(1.0, 1.0) ) - assert result_mask is not None - assert result_mask.shape == result.data.shape - def test_mask_stays_binary( - self, scan_image_replica: ScanImage, mask_array: MaskArray + assert result_img is scan_image_rectangular_with_nans + assert result_mask is mask + + def test_uses_explicit_factors(self, scan_image_rectangular_with_nans: ScanImage): + with patch("conversion.resample._get_scaling_factors") as mock: + resample_scan_image_and_mask( + scan_image_rectangular_with_nans, factors=(2.0, 2.0) + ) + mock.assert_not_called() + + def test_calculates_factors_when_not_provided( + self, scan_image_rectangular_with_nans: ScanImage + ): + with patch("conversion.resample._get_scaling_factors") as mock: + mock.return_value = (2.0, 2.0) + resample_scan_image_and_mask( + scan_image_rectangular_with_nans, target_scale=4e-6 + ) + mock.assert_called_once() + + def test_clips_when_only_downsample( + self, scan_image_rectangular_with_nans: ScanImage + ): + with patch("conversion.resample._clip_factors") as mock_clip: + mock_clip.return_value = (1.0, 1.0) + resample_scan_image_and_mask( + scan_image_rectangular_with_nans, + factors=(0.5, 0.5), + only_downsample=True, + ) + mock_clip.assert_called_once_with((0.5, 0.5), True) + + def test_no_clip_when_only_downsample_false( + self, scan_image_rectangular_with_nans: ScanImage + ): + with patch("conversion.resample._clip_factors") as mock_clip: + with patch("conversion.resample._resample_scan_image"): + resample_scan_image_and_mask( + scan_image_rectangular_with_nans, + factors=(0.5, 0.5), + only_downsample=False, + ) + mock_clip.assert_not_called() + + def test_resamples_mask_when_provided( + self, scan_image_rectangular_with_nans: ScanImage ): - """Mask values remain binary after resampling.""" - _, result_mask = resample_image_and_mask( - scan_image_replica, mask=mask_array, target_scale=1e-6 + mask = np.ones((100, 100), dtype=np.bool_) + + with patch("conversion.resample._resample_image_array") as mock: + mock.return_value = np.zeros((50, 50)) + + _, result_mask = resample_scan_image_and_mask( + scan_image_rectangular_with_nans, mask, factors=(2.0, 2.0) + ) + + assert mock.call_count == 2 # Once for image, once for mask + + def test_none_mask_stays_none(self, scan_image_rectangular_with_nans): + _, result_mask = resample_scan_image_and_mask( + scan_image_rectangular_with_nans, mask=None, factors=(2.0, 2.0) ) - assert result_mask is not None - unique_values = np.unique(result_mask) - assert all(v in [0, 1] for v in unique_values) + + assert result_mask is None class TestResampleMark: diff --git a/packages/scratch-core/tests/resources/baseline_images/surface_plot.npy b/packages/scratch-core/tests/resources/baseline_images/surface_plot.npy new file mode 100644 index 00000000..4046e900 Binary files /dev/null and b/packages/scratch-core/tests/resources/baseline_images/surface_plot.npy differ diff --git a/uv.lock b/uv.lock index a9f7f069..97c803cf 100644 --- a/uv.lock +++ b/uv.lock @@ -694,6 +694,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] +[[package]] +name = "imageio" +version = "2.37.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/606be632e37bf8d05b253e8626c2291d74c691ddc7bcdf7d6aaf33b32f6a/imageio-2.37.2.tar.gz", hash = "sha256:0212ef2727ac9caa5ca4b2c75ae89454312f440a756fcfc8ef1993e718f50f8a", size = 389600, upload-time = "2025-11-04T14:29:39.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/fe/301e0936b79bcab4cacc7548bf2853fc28dced0a578bab1f7ef53c9aa75b/imageio-2.37.2-py3-none-any.whl", hash = "sha256:ad9adfb20335d718c03de457358ed69f141021a333c40a53e57273d8a5bd0b9b", size = 317646, upload-time = "2025-11-04T14:29:37.948Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -881,6 +894,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" }, ] +[[package]] +name = "lazy-loader" +version = "0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6b/c875b30a1ba490860c93da4cabf479e03f584eba06fe5963f6f6644653d8/lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1", size = 15431, upload-time = "2024-04-05T13:03:12.261Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc", size = 12097, upload-time = "2024-04-05T13:03:10.514Z" }, +] + [[package]] name = "loguru" version = "0.7.3" @@ -1047,6 +1072,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, ] +[[package]] +name = "networkx" +version = "3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/fc/7b6fd4d22c8c4dc5704430140d8b3f520531d4fe7328b8f8d03f5a7950e8/networkx-3.6.tar.gz", hash = "sha256:285276002ad1f7f7da0f7b42f004bcba70d381e936559166363707fdad3d72ad", size = 2511464, upload-time = "2025-11-24T03:03:47.158Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c7/d64168da60332c17d24c0d2f08bdf3987e8d1ae9d84b5bbd0eec2eb26a55/networkx-3.6-py3-none-any.whl", hash = "sha256:cdb395b105806062473d3be36458d8f1459a4e4b98e236a66c3a48996e07684f", size = 2063713, upload-time = "2025-11-24T03:03:45.21Z" }, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -1837,6 +1871,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8b/c7/6c818dcac06844608244855753a0afb367365661b40fe6b3288bc26726a6/rust_just-1.43.1-py3-none-win_amd64.whl", hash = "sha256:28f0d898d3e04846348277a4d47231c949398102c2f6f452f52ba6506c9c7572", size = 1731800, upload-time = "2025-11-19T07:49:17.344Z" }, ] +[[package]] +name = "scikit-image" +version = "0.25.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "imageio" }, + { name = "lazy-loader" }, + { name = "networkx" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "scipy" }, + { name = "tifffile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/a8/3c0f256012b93dd2cb6fda9245e9f4bff7dc0486880b248005f15ea2255e/scikit_image-0.25.2.tar.gz", hash = "sha256:e5a37e6cd4d0c018a7a55b9d601357e3382826d3888c10d0213fc63bff977dde", size = 22693594, upload-time = "2025-02-18T18:05:24.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/7c/9814dd1c637f7a0e44342985a76f95a55dd04be60154247679fd96c7169f/scikit_image-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7efa888130f6c548ec0439b1a7ed7295bc10105458a421e9bf739b457730b6da", size = 13921841, upload-time = "2025-02-18T18:05:03.963Z" }, + { url = "https://files.pythonhosted.org/packages/84/06/66a2e7661d6f526740c309e9717d3bd07b473661d5cdddef4dd978edab25/scikit_image-0.25.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dd8011efe69c3641920614d550f5505f83658fe33581e49bed86feab43a180fc", size = 13196862, upload-time = "2025-02-18T18:05:06.986Z" }, + { url = "https://files.pythonhosted.org/packages/4e/63/3368902ed79305f74c2ca8c297dfeb4307269cbe6402412668e322837143/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28182a9d3e2ce3c2e251383bdda68f8d88d9fff1a3ebe1eb61206595c9773341", size = 14117785, upload-time = "2025-02-18T18:05:10.69Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/c3da56a145f52cd61a68b8465d6a29d9503bc45bc993bb45e84371c97d94/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8abd3c805ce6944b941cfed0406d88faeb19bab3ed3d4b50187af55cf24d147", size = 14977119, upload-time = "2025-02-18T18:05:13.871Z" }, + { url = "https://files.pythonhosted.org/packages/8a/97/5fcf332e1753831abb99a2525180d3fb0d70918d461ebda9873f66dcc12f/scikit_image-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:64785a8acefee460ec49a354706db0b09d1f325674107d7fa3eadb663fb56d6f", size = 12885116, upload-time = "2025-02-18T18:05:17.844Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/75e9f17e3670b5ed93c32456fda823333c6279b144cd93e2c03aa06aa472/scikit_image-0.25.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330d061bd107d12f8d68f1d611ae27b3b813b8cdb0300a71d07b1379178dd4cd", size = 13862801, upload-time = "2025-02-18T18:05:20.783Z" }, +] + [[package]] name = "scikit-learn" version = "1.7.2" @@ -1984,6 +2042,7 @@ dependencies = [ { name = "pillow" }, { name = "pydantic" }, { name = "returns" }, + { name = "scikit-image" }, { name = "scipy" }, { name = "surfalize" }, { name = "x3p" }, @@ -1996,6 +2055,7 @@ requires-dist = [ { name = "pillow", specifier = ">=12.0.0" }, { name = "pydantic", specifier = ">=2.12.4" }, { name = "returns", specifier = ">=0.26.0" }, + { name = "scikit-image", specifier = ">=0.25.2" }, { name = "scipy", specifier = ">=1.16.3" }, { name = "surfalize", specifier = "~=0.16.6" }, { name = "x3p", git = "https://github.com/giacomomarchioro/pyx3p.git" }, @@ -2114,6 +2174,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, ] +[[package]] +name = "tifffile" +version = "2025.10.16" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2d/b5/0d8f3d395f07d25ec4cafcdfc8cab234b2cc6bf2465e9d7660633983fe8f/tifffile-2025.10.16.tar.gz", hash = "sha256:425179ec7837ac0e07bc95d2ea5bea9b179ce854967c12ba07fc3f093e58efc1", size = 371848, upload-time = "2025-10-16T22:56:09.043Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/5e/56c751afab61336cf0e7aa671b134255a30f15f59cd9e04f59c598a37ff5/tifffile-2025.10.16-py3-none-any.whl", hash = "sha256:41463d979c1c262b0a5cdef2a7f95f0388a072ad82d899458b154a48609d759c", size = 231162, upload-time = "2025-10-16T22:56:07.214Z" }, +] + [[package]] name = "tornado" version = "6.5.2"