From 067882bd18533be8b07251e152fbba60e4c196fb Mon Sep 17 00:00:00 2001 From: Alex <95913221+Pwhsky@users.noreply.github.com> Date: Wed, 6 Aug 2025 15:37:20 -0700 Subject: [PATCH 01/20] torch and numpy implemented like in maxpooling --- deeptrack/math.py | 131 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 120 insertions(+), 11 deletions(-) diff --git a/deeptrack/math.py b/deeptrack/math.py index 13af56cec..53f162ff0 100644 --- a/deeptrack/math.py +++ b/deeptrack/math.py @@ -1313,15 +1313,19 @@ def __init__( super().__init__(np.max, ksize=ksize, **kwargs) -#TODO ***AL*** revise MinPooling - torch, typing, docstring, unit test class MinPooling(Pool): """Apply min pooling to images. - This class reduces the resolution of an image by dividing it into - non-overlapping blocks of size `ksize` and applying the min function to - each block. The result is a downsampled image where each pixel value - represents the minimum value within the corresponding block of the - original image. + This class inherits from `Pool` to reduce the resolution of an image by + dividing it into non-overlapping blocks of size `ksize` and applying the + `min` function to each block. The result is a downsampled image where each + pixel value represents the minimum value within the corresponding block of + the original image. + + If the backend is numpy, the downsampling is performed using + `skimage.measure.block_reduce`. + If the backend is torch, the downsampling + is performed using `torch.nn.functional.max_pool2d`. Parameters ---------- @@ -1339,18 +1343,20 @@ class MinPooling(Pool): >>> input_image = np.random.rand(32, 32) Define a min pooling feature: - >>> min_pooling = dt.MinPooling(ksize=3) + >>> min_pooling = dt.MinPooling(ksize=4) >>> output_image = min_pooling(input_image) >>> print(output_image.shape) - (32, 32) + (8, 8) Notes ----- - Calling this feature returns a `np.ndarray` by default. If - `store_properties` is set to `True`, the returned array will be + Calling this feature returns a pooled image of the input, it will return + either numpy or torch depending on the backend. If `store_properties` is + set to `True` and the input is a numpy array, the returned array will be automatically wrapped in an `Image` object. This behavior is handled internally and does not affect the return type of the `get()` method. + """ def __init__( @@ -1360,7 +1366,8 @@ def __init__( ): """Initialize the parameters for min pooling. - This constructor initializes the parameters for min pooling. + This constructor initializes the parameters for max pooling and checks + whether to use the numpy or torch implementation, defaults to numpy. Parameters ---------- @@ -1373,6 +1380,108 @@ def __init__( super().__init__(np.min, ksize=ksize, **kwargs) +def _get_numpy( + self, + image: NDArray, + ksize: int=3, + **kwargs, + ): + """Method to perform average pooling with the numpy backend enabled. + + Returns the result of the image passed to the scikit image block_reduce + function with `np.min` as the pooling function. + + Parameters + ---------- + image: NDArray + Input image to be pooled. + ksize: int + Kernel size of the pooling operation. + + Returns + ------- + NDArray + The pooled image as a `NDArray`. + + """ + return utils.safe_call( + skimage.measure.block_reduce, + image=image, + func=self.pooling, # This will be np.min for this class. + block_size=ksize, + **kwargs, + ) + + def _get_torch( + self, + image: torch.Tensor, + ksize: int=3, + **kwargs, + ): + """Method to perform max pooling with the torch backend enabled. + + Returns the result of the image passed to a torch min pooling layer. + + Parameters + ---------- + image: torch.Tensor + Input image to be pooled. + ksize: int + Kernel size of the pooling operation. + + Returns + ------- + torch.Tensor + The pooled image as a `torch.Tensor`. + + """ + # If needed, expand tensor shape + if len(image.shape) == 2: + expanded_image = image.unsqueeze(0) + + pooled_image = torch.nn.functional.min_pool2d( + expanded_image, kernel_size=ksize, + ) + # Remove the expanded dim. + return pooled_image.squeeze(0) + + return torch.nn.functional.min_pool2d( + image, + kernel_size=ksize, + ) + + def get( + self, + image: NDArray | torch.Tensor, + ksize: int=3, + **kwargs, + ): + """Method to perform pooling with either torch or numpy backend. + + Checks the current backend and chooses the appropriate function to pool + the input image, either `_get_torch` or `_get_numpy`. + + Parameters + ---------- + image: NDArray | torch.Tensor + Input image to be pooled. + ksize: int + Kernel size of the pooling operation. + + Returns + ------- + NDArray | torch.Tensor + The pooled image as `NDArray` or `torch.Tensor` depending on + the backend. + + """ + + if self.get_backend() == "numpy": + return self._get_numpy(image, ksize, **kwargs,) + elif self.get_backend() == "torch": + return self._get_torch(image, ksize, **kwargs,) + else: + raise NotImplementedError(f"Backend {self.backend} not supported") #TODO ***AL*** revise MedianPooling - torch, typing, docstring, unit test class MedianPooling(Pool): From b90e77dd76adf39c7d75872c4489742739dfc667 Mon Sep 17 00:00:00 2001 From: Alex <95913221+Pwhsky@users.noreply.github.com> Date: Wed, 6 Aug 2025 15:42:28 -0700 Subject: [PATCH 02/20] tests for minpoolling --- deeptrack/tests/test_math.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/deeptrack/tests/test_math.py b/deeptrack/tests/test_math.py index 197afb404..d46873d6b 100644 --- a/deeptrack/tests/test_math.py +++ b/deeptrack/tests/test_math.py @@ -87,7 +87,27 @@ def test_Blur(self): @unittest.skipUnless(TORCH_AVAILABLE, "PyTorch is not installed.") class TestMath_Torch(TestMath_Numpy): BACKEND = "torch" - pass + + def test_MinPooling(self): + # (1, 1, 2, 4) + input_image = torch.tensor([[[ [1.0, 2.0, 3.0, 4.0], + [5.0, 6.0, 7.0, 8.0] ]]]) + feature = math.MinPooling(ksize=2) + pooled_image = feature(input_image, ksize=2) + expected = torch.tensor([[[[1.0, 3.0]]]]) + self.assertEqual(pooled_image.shape, expected.shape) + self.assertTrue(torch.allclose(pooled_image, expected)) + self.assertTrue(isinstance(pooled_image, torch.Tensor)) + + # (2, 4) + input_image = torch.tensor([ [1.0, 2.0, 3.0, 4.0], + [5.0, 6.0, 7.0, 8.0] ]) + feature = math.MinPooling(ksize=2) + pooled_image = feature(input_image, ksize=2) + expected = torch.tensor([[1.0, 3.0]]) + self.assertEqual(pooled_image.shape, expected.shape) + self.assertTrue(torch.allclose(pooled_image, expected)) + self.assertTrue(isinstance(pooled_image, torch.Tensor)) class TestMath(unittest.TestCase): From 52655d47aff8baeb08924996d4320ad47b69abe2 Mon Sep 17 00:00:00 2001 From: Alex <95913221+Pwhsky@users.noreply.github.com> Date: Wed, 6 Aug 2025 15:44:07 -0700 Subject: [PATCH 03/20] tests for numpy --- deeptrack/tests/test_math.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/deeptrack/tests/test_math.py b/deeptrack/tests/test_math.py index d46873d6b..9950ab80a 100644 --- a/deeptrack/tests/test_math.py +++ b/deeptrack/tests/test_math.py @@ -81,6 +81,13 @@ def test_Blur(self): #eature = math.Blur(filter_function=uniform_filter, size=2) #blurred_image = feature.resolve(input_image) #self.assertTrue(xp.all(blurred_image == expected_output)) + + def test_MinPooling(self): + input_image = np.array([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=float) + feature = math.MinPooling(ksize=2) + pooled_image = feature.resolve(input_image) + self.assertTrue(np.all(pooled_image == [[1.0, 3.0]])) + self.assertEqual(pooled_image.shape, (1, 2)) # Extending the test and setting the backend to torch From c40c2a1b1d20d563847ca398cc71d1f6c5fd737d Mon Sep 17 00:00:00 2001 From: Alex <95913221+Pwhsky@users.noreply.github.com> Date: Wed, 6 Aug 2025 15:46:33 -0700 Subject: [PATCH 04/20] u --- deeptrack/tests/test_math.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deeptrack/tests/test_math.py b/deeptrack/tests/test_math.py index 9950ab80a..1df982327 100644 --- a/deeptrack/tests/test_math.py +++ b/deeptrack/tests/test_math.py @@ -95,7 +95,7 @@ def test_MinPooling(self): class TestMath_Torch(TestMath_Numpy): BACKEND = "torch" - def test_MinPooling(self): + def test_MinPooling(self): # (1, 1, 2, 4) input_image = torch.tensor([[[ [1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0] ]]]) From d124d72dea4fc79508afbfe772dfdfc83bd14022 Mon Sep 17 00:00:00 2001 From: Alex <95913221+Pwhsky@users.noreply.github.com> Date: Wed, 6 Aug 2025 15:48:53 -0700 Subject: [PATCH 05/20] u --- deeptrack/math.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deeptrack/math.py b/deeptrack/math.py index 53f162ff0..a6cbf2b11 100644 --- a/deeptrack/math.py +++ b/deeptrack/math.py @@ -1380,7 +1380,7 @@ def __init__( super().__init__(np.min, ksize=ksize, **kwargs) -def _get_numpy( + def _get_numpy( self, image: NDArray, ksize: int=3, @@ -1475,7 +1475,7 @@ def get( the backend. """ - + if self.get_backend() == "numpy": return self._get_numpy(image, ksize, **kwargs,) elif self.get_backend() == "torch": From d49d8d344066d1dbbd1ca32581558e220b2f7398 Mon Sep 17 00:00:00 2001 From: Alex <95913221+Pwhsky@users.noreply.github.com> Date: Wed, 6 Aug 2025 15:55:26 -0700 Subject: [PATCH 06/20] added inverse max pooling to act as minpooling --- deeptrack/math.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/deeptrack/math.py b/deeptrack/math.py index a6cbf2b11..1823cf21b 100644 --- a/deeptrack/math.py +++ b/deeptrack/math.py @@ -1439,14 +1439,14 @@ def _get_torch( if len(image.shape) == 2: expanded_image = image.unsqueeze(0) - pooled_image = torch.nn.functional.min_pool2d( - expanded_image, kernel_size=ksize, + pooled_image = -torch.nn.functional.max_pool2d( + expanded_image*(-1), kernel_size=ksize, ) # Remove the expanded dim. return pooled_image.squeeze(0) - return torch.nn.functional.min_pool2d( - image, + return -torch.nn.functional.max_pool2d( + image*(-1), kernel_size=ksize, ) From 1c838e9b2ca9476f066906fff1d25427563d3780 Mon Sep 17 00:00:00 2001 From: Alex <95913221+Pwhsky@users.noreply.github.com> Date: Thu, 7 Aug 2025 09:28:43 -0700 Subject: [PATCH 07/20] clarified text regarding minpooling --- deeptrack/math.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/deeptrack/math.py b/deeptrack/math.py index 1823cf21b..c82d7e341 100644 --- a/deeptrack/math.py +++ b/deeptrack/math.py @@ -1325,7 +1325,8 @@ class MinPooling(Pool): If the backend is numpy, the downsampling is performed using `skimage.measure.block_reduce`. If the backend is torch, the downsampling - is performed using `torch.nn.functional.max_pool2d`. + is performed using the inverse of `torch.nn.functional.max_pool2d` by + changing the sign of the input. Parameters ---------- @@ -1366,7 +1367,7 @@ def __init__( ): """Initialize the parameters for min pooling. - This constructor initializes the parameters for max pooling and checks + This constructor initializes the parameters for min pooling and checks whether to use the numpy or torch implementation, defaults to numpy. Parameters @@ -1418,9 +1419,10 @@ def _get_torch( ksize: int=3, **kwargs, ): - """Method to perform max pooling with the torch backend enabled. - - Returns the result of the image passed to a torch min pooling layer. + """Method to perform min pooling with the torch backend enabled. + As torch does not contain a min pooling layer, in order to perform an + equivalent operation is to first multiply the image with `-1`, + perform max pooling and multiply the max pooled image with `-1`. Parameters ---------- From 60735fc0ea54b88d628183a0bab26309907b3c18 Mon Sep 17 00:00:00 2001 From: Alex <95913221+Pwhsky@users.noreply.github.com> Date: Mon, 11 Aug 2025 05:09:27 -0700 Subject: [PATCH 08/20] Implemented feedback from Mirja --- deeptrack/math.py | 12 +++++------- deeptrack/tests/test_math.py | 4 ++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/deeptrack/math.py b/deeptrack/math.py index c82d7e341..3bdfa1eeb 100644 --- a/deeptrack/math.py +++ b/deeptrack/math.py @@ -1322,11 +1322,10 @@ class MinPooling(Pool): pixel value represents the minimum value within the corresponding block of the original image. - If the backend is numpy, the downsampling is performed using + If the backend is numpy, the downsampling is performed using `skimage.measure.block_reduce`. - If the backend is torch, the downsampling - is performed using the inverse of `torch.nn.functional.max_pool2d` by - changing the sign of the input. + If the backend is torch, the downsampling is performed using the inverse + of `torch.nn.functional.max_pool2d` by changing the sign of the input. Parameters ---------- @@ -1357,7 +1356,6 @@ class MinPooling(Pool): automatically wrapped in an `Image` object. This behavior is handled internally and does not affect the return type of the `get()` method. - """ def __init__( @@ -1411,7 +1409,7 @@ def _get_numpy( func=self.pooling, # This will be np.min for this class. block_size=ksize, **kwargs, - ) + ) def _get_torch( self, @@ -1422,7 +1420,7 @@ def _get_torch( """Method to perform min pooling with the torch backend enabled. As torch does not contain a min pooling layer, in order to perform an equivalent operation is to first multiply the image with `-1`, - perform max pooling and multiply the max pooled image with `-1`. + perform max pooling and multiply the max pooled image with `-1`. Parameters ---------- diff --git a/deeptrack/tests/test_math.py b/deeptrack/tests/test_math.py index 1df982327..785b87e43 100644 --- a/deeptrack/tests/test_math.py +++ b/deeptrack/tests/test_math.py @@ -96,7 +96,7 @@ class TestMath_Torch(TestMath_Numpy): BACKEND = "torch" def test_MinPooling(self): - # (1, 1, 2, 4) + # input shape (1, 1, 2, 4) input_image = torch.tensor([[[ [1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0] ]]]) feature = math.MinPooling(ksize=2) @@ -106,7 +106,7 @@ def test_MinPooling(self): self.assertTrue(torch.allclose(pooled_image, expected)) self.assertTrue(isinstance(pooled_image, torch.Tensor)) - # (2, 4) + # input shape (2, 4) input_image = torch.tensor([ [1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0] ]) feature = math.MinPooling(ksize=2) From f025cc551b992a82c857636965c8a7a5375e7f74 Mon Sep 17 00:00:00 2001 From: Alex <95913221+Pwhsky@users.noreply.github.com> Date: Mon, 11 Aug 2025 20:39:26 +0200 Subject: [PATCH 09/20] typo --- deeptrack/math.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deeptrack/math.py b/deeptrack/math.py index 3bdfa1eeb..dc05f3b2a 100644 --- a/deeptrack/math.py +++ b/deeptrack/math.py @@ -1418,8 +1418,8 @@ def _get_torch( **kwargs, ): """Method to perform min pooling with the torch backend enabled. - As torch does not contain a min pooling layer, in order to perform an - equivalent operation is to first multiply the image with `-1`, + As torch does not contain a min pooling layer, the equivalent + operation is to first multiply the input image with `-1`, perform max pooling and multiply the max pooled image with `-1`. Parameters From f389ec95ec01edf67a2f306602ff1be34ec314a6 Mon Sep 17 00:00:00 2001 From: Alex <95913221+Pwhsky@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:09:02 +0200 Subject: [PATCH 10/20] Type hints --- deeptrack/math.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/deeptrack/math.py b/deeptrack/math.py index dc05f3b2a..22f204e58 100644 --- a/deeptrack/math.py +++ b/deeptrack/math.py @@ -1380,11 +1380,11 @@ def __init__( super().__init__(np.min, ksize=ksize, **kwargs) def _get_numpy( - self, - image: NDArray, + self: MinPooling, + image: NDArray[Any], ksize: int=3, - **kwargs, - ): + **kwargs: Any, + ) -> NDArray[Any]: """Method to perform average pooling with the numpy backend enabled. Returns the result of the image passed to the scikit image block_reduce @@ -1412,11 +1412,11 @@ def _get_numpy( ) def _get_torch( - self, + self: MinPooling, image: torch.Tensor, ksize: int=3, - **kwargs, - ): + **kwargs: Any, + ) -> torch.Tensor: """Method to perform min pooling with the torch backend enabled. As torch does not contain a min pooling layer, the equivalent operation is to first multiply the input image with `-1`, @@ -1455,7 +1455,7 @@ def get( image: NDArray | torch.Tensor, ksize: int=3, **kwargs, - ): + ) -> NDArray | torch.Tensor: """Method to perform pooling with either torch or numpy backend. Checks the current backend and chooses the appropriate function to pool From c02eabb0565c2ce1d0c67df7318ed3c015fe398a Mon Sep 17 00:00:00 2001 From: Alex <95913221+Pwhsky@users.noreply.github.com> Date: Wed, 3 Sep 2025 09:52:38 +0200 Subject: [PATCH 11/20] Implementing xp tests --- deeptrack/tests/test_math.py | 39 ++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/deeptrack/tests/test_math.py b/deeptrack/tests/test_math.py index 785b87e43..9c8a3ed20 100644 --- a/deeptrack/tests/test_math.py +++ b/deeptrack/tests/test_math.py @@ -83,10 +83,10 @@ def test_Blur(self): #self.assertTrue(xp.all(blurred_image == expected_output)) def test_MinPooling(self): - input_image = np.array([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=float) + input_image = xp.array([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=float) feature = math.MinPooling(ksize=2) pooled_image = feature.resolve(input_image) - self.assertTrue(np.all(pooled_image == [[1.0, 3.0]])) + self.assertTrue(xp.all(pooled_image == [[1.0, 3.0]])) self.assertEqual(pooled_image.shape, (1, 2)) @@ -94,27 +94,28 @@ def test_MinPooling(self): @unittest.skipUnless(TORCH_AVAILABLE, "PyTorch is not installed.") class TestMath_Torch(TestMath_Numpy): BACKEND = "torch" - - def test_MinPooling(self): + pass + + #def test_MinPooling(self): # input shape (1, 1, 2, 4) - input_image = torch.tensor([[[ [1.0, 2.0, 3.0, 4.0], + # input_image = torch.tensor([[[ [1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0] ]]]) - feature = math.MinPooling(ksize=2) - pooled_image = feature(input_image, ksize=2) - expected = torch.tensor([[[[1.0, 3.0]]]]) - self.assertEqual(pooled_image.shape, expected.shape) - self.assertTrue(torch.allclose(pooled_image, expected)) - self.assertTrue(isinstance(pooled_image, torch.Tensor)) + # feature = math.MinPooling(ksize=2) + # pooled_image = feature(input_image, ksize=2) + # expected = torch.tensor([[[[1.0, 3.0]]]]) + # self.assertEqual(pooled_image.shape, expected.shape) + # self.assertTrue(torch.allclose(pooled_image, expected)) + # self.assertTrue(isinstance(pooled_image, torch.Tensor)) # input shape (2, 4) - input_image = torch.tensor([ [1.0, 2.0, 3.0, 4.0], - [5.0, 6.0, 7.0, 8.0] ]) - feature = math.MinPooling(ksize=2) - pooled_image = feature(input_image, ksize=2) - expected = torch.tensor([[1.0, 3.0]]) - self.assertEqual(pooled_image.shape, expected.shape) - self.assertTrue(torch.allclose(pooled_image, expected)) - self.assertTrue(isinstance(pooled_image, torch.Tensor)) + # input_image = torch.tensor([ [1.0, 2.0, 3.0, 4.0], + # [5.0, 6.0, 7.0, 8.0] ]) + # feature = math.MinPooling(ksize=2) + # pooled_image = feature(input_image, ksize=2) + # expected = torch.tensor([[1.0, 3.0]]) + # self.assertEqual(pooled_image.shape, expected.shape) + # self.assertTrue(torch.allclose(pooled_image, expected)) + # self.assertTrue(isinstance(pooled_image, torch.Tensor)) class TestMath(unittest.TestCase): From 12bd13bfbfa194b9ee313bce9bef5169178bfce3 Mon Sep 17 00:00:00 2001 From: Alex <95913221+Pwhsky@users.noreply.github.com> Date: Wed, 3 Sep 2025 09:54:02 +0200 Subject: [PATCH 12/20] u --- deeptrack/tests/test_math.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deeptrack/tests/test_math.py b/deeptrack/tests/test_math.py index 9c8a3ed20..ba4c354d9 100644 --- a/deeptrack/tests/test_math.py +++ b/deeptrack/tests/test_math.py @@ -99,7 +99,7 @@ class TestMath_Torch(TestMath_Numpy): #def test_MinPooling(self): # input shape (1, 1, 2, 4) # input_image = torch.tensor([[[ [1.0, 2.0, 3.0, 4.0], - [5.0, 6.0, 7.0, 8.0] ]]]) + # [5.0, 6.0, 7.0, 8.0] ]]]) # feature = math.MinPooling(ksize=2) # pooled_image = feature(input_image, ksize=2) # expected = torch.tensor([[[[1.0, 3.0]]]]) From c80ec6e41b0450e7801bc10429abc94e27c93c1b Mon Sep 17 00:00:00 2001 From: Alex <95913221+Pwhsky@users.noreply.github.com> Date: Wed, 3 Sep 2025 10:03:45 +0200 Subject: [PATCH 13/20] u --- deeptrack/tests/test_math.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/deeptrack/tests/test_math.py b/deeptrack/tests/test_math.py index ba4c354d9..55f1e0280 100644 --- a/deeptrack/tests/test_math.py +++ b/deeptrack/tests/test_math.py @@ -83,7 +83,11 @@ def test_Blur(self): #self.assertTrue(xp.all(blurred_image == expected_output)) def test_MinPooling(self): - input_image = xp.array([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=float) + if BACKEND == "torch": + input_image = xp.tensor([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=float) + else: + input_image = xp.array([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=float) + feature = math.MinPooling(ksize=2) pooled_image = feature.resolve(input_image) self.assertTrue(xp.all(pooled_image == [[1.0, 3.0]])) From 03ed40c3c597ecdd3faa070d08ccca06d3ae67d6 Mon Sep 17 00:00:00 2001 From: Alex <95913221+Pwhsky@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:10:30 +0200 Subject: [PATCH 14/20] u --- deeptrack/tests/test_math.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/deeptrack/tests/test_math.py b/deeptrack/tests/test_math.py index 55f1e0280..99dfb823a 100644 --- a/deeptrack/tests/test_math.py +++ b/deeptrack/tests/test_math.py @@ -83,11 +83,8 @@ def test_Blur(self): #self.assertTrue(xp.all(blurred_image == expected_output)) def test_MinPooling(self): - if BACKEND == "torch": - input_image = xp.tensor([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=float) - else: - input_image = xp.array([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=float) - + + input_image = np.array([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=float) feature = math.MinPooling(ksize=2) pooled_image = feature.resolve(input_image) self.assertTrue(xp.all(pooled_image == [[1.0, 3.0]])) @@ -98,6 +95,7 @@ def test_MinPooling(self): @unittest.skipUnless(TORCH_AVAILABLE, "PyTorch is not installed.") class TestMath_Torch(TestMath_Numpy): BACKEND = "torch" + input_image = torch.tensor([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=float) pass #def test_MinPooling(self): From 0bef05ac49fa9115ab99d16fca0cee563316a4a6 Mon Sep 17 00:00:00 2001 From: Alex <95913221+Pwhsky@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:15:36 +0200 Subject: [PATCH 15/20] u --- deeptrack/tests/test_math.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deeptrack/tests/test_math.py b/deeptrack/tests/test_math.py index 99dfb823a..2e3df68e3 100644 --- a/deeptrack/tests/test_math.py +++ b/deeptrack/tests/test_math.py @@ -84,7 +84,7 @@ def test_Blur(self): def test_MinPooling(self): - input_image = np.array([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=float) + input_image = xp.asarray([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=float) feature = math.MinPooling(ksize=2) pooled_image = feature.resolve(input_image) self.assertTrue(xp.all(pooled_image == [[1.0, 3.0]])) @@ -95,7 +95,7 @@ def test_MinPooling(self): @unittest.skipUnless(TORCH_AVAILABLE, "PyTorch is not installed.") class TestMath_Torch(TestMath_Numpy): BACKEND = "torch" - input_image = torch.tensor([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=float) + # input_image = torch.tensor([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=float) pass #def test_MinPooling(self): From 33458569d7fcd34da7d01d36d44d3b17747deb9d Mon Sep 17 00:00:00 2001 From: Alex <95913221+Pwhsky@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:26:18 +0200 Subject: [PATCH 16/20] u --- deeptrack/tests/test_math.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/deeptrack/tests/test_math.py b/deeptrack/tests/test_math.py index 2e3df68e3..38ec94eed 100644 --- a/deeptrack/tests/test_math.py +++ b/deeptrack/tests/test_math.py @@ -87,7 +87,10 @@ def test_MinPooling(self): input_image = xp.asarray([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=float) feature = math.MinPooling(ksize=2) pooled_image = feature.resolve(input_image) - self.assertTrue(xp.all(pooled_image == [[1.0, 3.0]])) + + expected = xp.asarray([[1.0, 3.0]], dtype=float) + + self.assertTrue(xp.all(pooled_image == expected) self.assertEqual(pooled_image.shape, (1, 2)) From 2b5c83ac464bcd7281b6011da640b0aa0c2aed34 Mon Sep 17 00:00:00 2001 From: Alex <95913221+Pwhsky@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:28:14 +0200 Subject: [PATCH 17/20] u --- deeptrack/tests/test_math.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deeptrack/tests/test_math.py b/deeptrack/tests/test_math.py index 38ec94eed..5c6d36eed 100644 --- a/deeptrack/tests/test_math.py +++ b/deeptrack/tests/test_math.py @@ -90,7 +90,7 @@ def test_MinPooling(self): expected = xp.asarray([[1.0, 3.0]], dtype=float) - self.assertTrue(xp.all(pooled_image == expected) + self.assertTrue(xp.all(pooled_image == expected)) self.assertEqual(pooled_image.shape, (1, 2)) From 51d211b17de2ab21f8601bdea8779542bda3e801 Mon Sep 17 00:00:00 2001 From: Alex <95913221+Pwhsky@users.noreply.github.com> Date: Wed, 3 Sep 2025 13:02:20 +0200 Subject: [PATCH 18/20] Update test_math.py --- deeptrack/tests/test_math.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/deeptrack/tests/test_math.py b/deeptrack/tests/test_math.py index 5c6d36eed..41305c0da 100644 --- a/deeptrack/tests/test_math.py +++ b/deeptrack/tests/test_math.py @@ -98,9 +98,8 @@ def test_MinPooling(self): @unittest.skipUnless(TORCH_AVAILABLE, "PyTorch is not installed.") class TestMath_Torch(TestMath_Numpy): BACKEND = "torch" - # input_image = torch.tensor([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=float) pass - + # Old tests commented out #def test_MinPooling(self): # input shape (1, 1, 2, 4) # input_image = torch.tensor([[[ [1.0, 2.0, 3.0, 4.0], From a4d95bb87148f665cdc84a22d51d086ef6c9e39c Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Fri, 5 Sep 2025 14:34:12 +0200 Subject: [PATCH 19/20] Update math.py --- deeptrack/math.py | 155 +++++++++++++++++++++++----------------------- 1 file changed, 79 insertions(+), 76 deletions(-) diff --git a/deeptrack/math.py b/deeptrack/math.py index 22f204e58..c0d68e1c7 100644 --- a/deeptrack/math.py +++ b/deeptrack/math.py @@ -55,9 +55,9 @@ - `AveragePooling`: Apply average pooling to the image. -- `MaxPooling`: Apply max pooling to the image. +- `MaxPooling`: Apply max-pooling to the image. -- `MinPooling`: Apply min pooling to the image. +- `MinPooling`: Apply min-pooling to the image. - `MedianPooling`: Apply median pooling to the image. @@ -1251,7 +1251,7 @@ def __init__( #TODO ***AL*** revise MaxPooling - torch, typing, docstring, unit test class MaxPooling(Pool): - """Apply max pooling to images. + """Apply max-pooling to images. This class reduces the resolution of an image by dividing it into non-overlapping blocks of size `ksize` and applying the max function to @@ -1277,7 +1277,7 @@ class MaxPooling(Pool): Create an input image: >>> input_image = np.random.rand(32, 32) - Define a max pooling feature: + Define a max-pooling feature: >>> max_pooling = dt.MaxPooling(ksize=8) >>> output_image = max_pooling(input_image) >>> print(output_image.shape) @@ -1297,9 +1297,9 @@ def __init__( ksize: PropertyLike[int] = 3, **kwargs: Any, ): - """Initialize the parameters for max pooling. + """Initialize the parameters for max-pooling. - This constructor initializes the parameters for max pooling. + This constructor initializes the parameters for max-pooling. Parameters ---------- @@ -1314,48 +1314,42 @@ def __init__( class MinPooling(Pool): - """Apply min pooling to images. + """Apply min-pooling to images. - This class inherits from `Pool` to reduce the resolution of an image by - dividing it into non-overlapping blocks of size `ksize` and applying the - `min` function to each block. The result is a downsampled image where each - pixel value represents the minimum value within the corresponding block of - the original image. + `MinPooling` reduces the resolution of an image by dividing it into + non-overlapping blocks of size `ksize` and applying the `min` function to + each block. The result is a downsampled image where each pixel value + represents the minimum value within the corresponding block of the original + image. - If the backend is numpy, the downsampling is performed using + If the backend is NumPy, the downsampling is performed using `skimage.measure.block_reduce`. - If the backend is torch, the downsampling is performed using the inverse + + If the backend is PyTorch, the downsampling is performed using the inverse of `torch.nn.functional.max_pool2d` by changing the sign of the input. Parameters ---------- ksize: int Size of the pooling kernel. - **kwargs: dict + **kwargs: Any Additional parameters sent to the pooling function. Examples -------- >>> import deeptrack as dt - >>> import numpy as np Create an input image: + >>> import numpy as np + >>> >>> input_image = np.random.rand(32, 32) - Define a min pooling feature: + Define and use a min-pooling feature: >>> min_pooling = dt.MinPooling(ksize=4) >>> output_image = min_pooling(input_image) - >>> print(output_image.shape) + >>> output_image.shape (8, 8) - Notes - ----- - Calling this feature returns a pooled image of the input, it will return - either numpy or torch depending on the backend. If `store_properties` is - set to `True` and the input is a numpy array, the returned array will be - automatically wrapped in an `Image` object. This behavior is handled - internally and does not affect the return type of the `get()` method. - """ def __init__( @@ -1363,10 +1357,10 @@ def __init__( ksize: PropertyLike[int] = 3, **kwargs: Any, ): - """Initialize the parameters for min pooling. + """Initialize the parameters for min-pooling. - This constructor initializes the parameters for min pooling and checks - whether to use the numpy or torch implementation, defaults to numpy. + This constructor initializes the parameters for min-pooling and checks + whether to use the NumPy or PyTorch implementation, defaults to NumPy. Parameters ---------- @@ -1379,16 +1373,51 @@ def __init__( super().__init__(np.min, ksize=ksize, **kwargs) + def get( + self: MinPooling, + image: NDArray[Any] | torch.Tensor, + ksize: int=3, + **kwargs: Any, + ) -> NDArray[Any] | torch.Tensor: + """Min pooling of input. + + Checks the current backend and chooses the appropriate function to pool + the input image, either `._get_torch()` or `._get_numpy()`. + + Parameters + ---------- + image: array or tensor + Input array or tensor to be pooled. + ksize: int + Kernel size of the pooling operation. + + Returns + ------- + array or tensor + The pooled image as `NDArray` or `torch.Tensor` depending on the + backend. + + """ + + if self.get_backend() == "numpy": + return self._get_numpy(image, ksize, **kwargs) + + if self.get_backend() == "torch": + return self._get_torch(image, ksize, **kwargs) + + raise NotImplementedError(f"Backend {self.backend} not supported") + def _get_numpy( self: MinPooling, image: NDArray[Any], ksize: int=3, **kwargs: Any, ) -> NDArray[Any]: - """Method to perform average pooling with the numpy backend enabled. + """Min-pooling with the NumPy backend. - Returns the result of the image passed to the scikit image block_reduce - function with `np.min` as the pooling function. + Returns the result of the input array passed to the scikit + `image block_reduce()` function with `np.min()` as the pooling + function. Parameters ---------- @@ -1401,12 +1430,13 @@ def _get_numpy( ------- NDArray The pooled image as a `NDArray`. - + """ + return utils.safe_call( skimage.measure.block_reduce, image=image, - func=self.pooling, # This will be np.min for this class. + func=np.min, block_size=ksize, **kwargs, ) @@ -1417,15 +1447,16 @@ def _get_torch( ksize: int=3, **kwargs: Any, ) -> torch.Tensor: - """Method to perform min pooling with the torch backend enabled. - As torch does not contain a min pooling layer, the equivalent - operation is to first multiply the input image with `-1`, - perform max pooling and multiply the max pooled image with `-1`. + """Min-pooling with the PyTorch backend. + + As PyTorch does not have a min-pooling layer, the equivalent operation + is to first multiply the input tensor with `-1`, then perform + max-pooling, and finally multiply the max pooled tensor with `-1`. Parameters ---------- image: torch.Tensor - Input image to be pooled. + Input tensor to be pooled. ksize: int Kernel size of the pooling operation. @@ -1435,53 +1466,25 @@ def _get_torch( The pooled image as a `torch.Tensor`. """ - # If needed, expand tensor shape + + # If input tensor is 2D if len(image.shape) == 2: + # Add batch dimension for min-pooling expanded_image = image.unsqueeze(0) - pooled_image = -torch.nn.functional.max_pool2d( - expanded_image*(-1), kernel_size=ksize, + pooled_image = - torch.nn.functional.max_pool2d( + expanded_image * (-1), + kernel_size=ksize, ) - # Remove the expanded dim. + + # Remove the expanded dim return pooled_image.squeeze(0) return -torch.nn.functional.max_pool2d( - image*(-1), + image * (-1), kernel_size=ksize, ) - def get( - self, - image: NDArray | torch.Tensor, - ksize: int=3, - **kwargs, - ) -> NDArray | torch.Tensor: - """Method to perform pooling with either torch or numpy backend. - - Checks the current backend and chooses the appropriate function to pool - the input image, either `_get_torch` or `_get_numpy`. - - Parameters - ---------- - image: NDArray | torch.Tensor - Input image to be pooled. - ksize: int - Kernel size of the pooling operation. - - Returns - ------- - NDArray | torch.Tensor - The pooled image as `NDArray` or `torch.Tensor` depending on - the backend. - - """ - - if self.get_backend() == "numpy": - return self._get_numpy(image, ksize, **kwargs,) - elif self.get_backend() == "torch": - return self._get_torch(image, ksize, **kwargs,) - else: - raise NotImplementedError(f"Backend {self.backend} not supported") #TODO ***AL*** revise MedianPooling - torch, typing, docstring, unit test class MedianPooling(Pool): From 939fafe1c26c9e0e1d5e87087c895ae524e1df50 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Fri, 5 Sep 2025 14:34:14 +0200 Subject: [PATCH 20/20] Update test_math.py --- deeptrack/tests/test_math.py | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/deeptrack/tests/test_math.py b/deeptrack/tests/test_math.py index 41305c0da..94440e5c1 100644 --- a/deeptrack/tests/test_math.py +++ b/deeptrack/tests/test_math.py @@ -81,17 +81,16 @@ def test_Blur(self): #eature = math.Blur(filter_function=uniform_filter, size=2) #blurred_image = feature.resolve(input_image) #self.assertTrue(xp.all(blurred_image == expected_output)) - - def test_MinPooling(self): + def test_MinPooling(self): input_image = xp.asarray([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=float) feature = math.MinPooling(ksize=2) pooled_image = feature.resolve(input_image) expected = xp.asarray([[1.0, 3.0]], dtype=float) - self.assertTrue(xp.all(pooled_image == expected)) self.assertEqual(pooled_image.shape, (1, 2)) + self.assertTrue(xp.all(pooled_image == expected)) # Extending the test and setting the backend to torch @@ -99,27 +98,6 @@ def test_MinPooling(self): class TestMath_Torch(TestMath_Numpy): BACKEND = "torch" pass - # Old tests commented out - #def test_MinPooling(self): - # input shape (1, 1, 2, 4) - # input_image = torch.tensor([[[ [1.0, 2.0, 3.0, 4.0], - # [5.0, 6.0, 7.0, 8.0] ]]]) - # feature = math.MinPooling(ksize=2) - # pooled_image = feature(input_image, ksize=2) - # expected = torch.tensor([[[[1.0, 3.0]]]]) - # self.assertEqual(pooled_image.shape, expected.shape) - # self.assertTrue(torch.allclose(pooled_image, expected)) - # self.assertTrue(isinstance(pooled_image, torch.Tensor)) - - # input shape (2, 4) - # input_image = torch.tensor([ [1.0, 2.0, 3.0, 4.0], - # [5.0, 6.0, 7.0, 8.0] ]) - # feature = math.MinPooling(ksize=2) - # pooled_image = feature(input_image, ksize=2) - # expected = torch.tensor([[1.0, 3.0]]) - # self.assertEqual(pooled_image.shape, expected.shape) - # self.assertTrue(torch.allclose(pooled_image, expected)) - # self.assertTrue(isinstance(pooled_image, torch.Tensor)) class TestMath(unittest.TestCase):