Skip to content

Math/AveragePooling #403

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 37 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 133 additions & 21 deletions deeptrack/math.py
Original file line number Diff line number Diff line change
Expand Up @@ -1069,24 +1069,23 @@ def __init__(
super().__init__(ndimage.median_filter, size=ksize, **kwargs)


#TODO ***AL*** revise Pool - torch, typing, docstring, unit test
class Pool(Feature):
class Pool(Feature): # Deprecated, children will be independent in the future.
"""Downsamples the image by applying a function to local regions of the
image.

This class reduces the resolution of an image by dividing it into
non-overlapping blocks of size `ksize` and applying the specified pooling
function to each block. The result is a downsampled image where each pixel
value represents the result of the pooling function applied to the
corresponding block.
corresponding block. This pooling only works with numpy functions.

Parameters
----------
pooling_function: function
pooling_function: Numpy function
A function that is applied to each local region of the image.
DOES NOT NEED TO BE WRAPPED IN ANOTHER FUNCTION.
The `pooling_function` must accept the input image as a keyword argument
named `input`, as it is called via `utils.safe_call`.
The `pooling_function` must accept the input image as a keyword
argument named `input`, as it is called via `utils.safe_call`.
Examples include `np.mean`, `np.max`, `np.min`, etc.
ksize: int
Size of the pooling kernel.
Expand All @@ -1095,7 +1094,8 @@ class Pool(Feature):

Methods
-------
`get(image: np.ndarray | Image, ksize: int, **kwargs: Any) --> np.ndarray`
`get(image: NDArray,
ksize: int, **kwargs: Any) --> NDArray`
Applies the pooling function to the input image.

Examples
Expand Down Expand Up @@ -1152,17 +1152,17 @@ def __init__(

def get(
self: Pool,
image: np.ndarray | Image,
image: NDArray,
ksize: int,
**kwargs: Any,
) -> np.ndarray:
) -> NDArray:
"""Applies the pooling function to the input image.

This method applies the pooling function to the input image.
This method applies `pooling_function` to the input image.

Parameters
----------
image: np.ndarray
image: NDArray | torch.Tensor
The input image to pool.
ksize: int
Size of the pooling kernel.
Expand All @@ -1171,7 +1171,7 @@ def get(

Returns
-------
np.ndarray
NDArray | torch.Tensor
The pooled image.

"""
Expand All @@ -1188,15 +1188,20 @@ def get(
)


#TODO ***AL*** revise AveragePooling - torch, typing, docstring, unit test
class AveragePooling(Pool):
"""Apply average pooling to an image.

This class reduces the resolution of an image by dividing it into
non-overlapping blocks of size `ksize` and applying the average function to
each block. The result is a downsampled image where each pixel value
represents the average 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
`max` function to each block. The result is a downsampled image where each
pixel value represents the maximum value within the corresponding block of
the original image. This is useful for reducing the size of an image while
retaining the most significant features.

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.avg_pool2d`.

Parameters
----------
Expand All @@ -1221,8 +1226,9 @@ class AveragePooling(Pool):

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.

Expand All @@ -1235,7 +1241,9 @@ def __init__(
):
"""Initialize the parameters for average pooling.

This constructor initializes the parameters for average pooling.
This constructor initializes the parameters for average pooling and
checks whether to use the numpy or torch implementation, defaults to
numpy.

Parameters
----------
Expand All @@ -1248,6 +1256,110 @@ def __init__(

super().__init__(np.mean, 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.mean()` 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.mean for this class.
block_size=ksize,
**kwargs,
)

def _get_torch(
self,
image: torch.Tensor,
ksize: int=3,
**kwargs,
):
"""Method to perform average pooling with the torch backend enabled.

Returns the result of the image passed to a torch average
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.avg_pool2d(
expanded_image, kernel_size=ksize,
)
# Remove the expanded dim.
return pooled_image.squeeze(0)

return torch.nn.functional.avg_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 MaxPooling - torch, typing, docstring, unit test
class MaxPooling(Pool):
Expand Down
19 changes: 18 additions & 1 deletion deeptrack/tests/test_math.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,28 @@ def test_Blur(self):
#blurred_image = feature.resolve(input_image)
#self.assertTrue(xp.all(blurred_image == expected_output))

def test_AveragePooling(self):
input_image = np.array([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=float)
feature = math.AveragePooling(ksize=2)
pooled_image = feature.resolve(input_image)
self.assertTrue(np.all(pooled_image == [[3.5, 5.5]]))


# Extending the test and setting the backend to torch
@unittest.skipUnless(TORCH_AVAILABLE, "PyTorch is not installed.")
class TestMath_Torch(TestMath_Numpy):
BACKEND = "torch"
pass

def test_AveragePooling(self):
input_image = torch.tensor([[[ [1.0, 2.0, 3.0, 4.0],
[5.0, 6.0, 7.0, 8.0] ]]])
feature = math.AveragePooling(ksize=2)
pooled_image = feature(input_image, ksize=2)
expected = torch.tensor([[[[3.5, 5.5]]]])
self.assertEqual(pooled_image.shape, expected.shape)
self.assertTrue(torch.allclose(pooled_image, expected))




class TestMath(unittest.TestCase):
Expand All @@ -109,6 +125,7 @@ def test_AveragePooling(self):
pooled_image = feature.resolve(input_image)
self.assertTrue(np.all(pooled_image == [[3.5, 5.5]]))


def test_MaxPooling(self):
input_image = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
feature = math.MaxPooling(ksize=2)
Expand Down
Loading