From 381630dfcb2661682a5e2e6096d2611a2801a3d6 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 18 Jun 2025 20:32:55 +0200 Subject: [PATCH 001/118] Update features.py --- deeptrack/features.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index a30d9892d..775cb8815 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -126,9 +126,10 @@ def merge_features( import itertools import operator import random -from typing import Any, Callable, Iterable, Literal +from typing import Any, Callable, Iterable, Literal, TYPE_CHECKING import numpy as np +from numpy.typing import NDArray import matplotlib.animation as animation import matplotlib.pyplot as plt from pint import Quantity @@ -137,12 +138,17 @@ def merge_features( from deeptrack import units from deeptrack.backend import config, xp from deeptrack.backend.core import DeepTrackNode -from deeptrack.backend.units import ConversionTable, create_context +from deeptrack.backend.units import ConversionTable from deeptrack.image import Image from deeptrack.properties import PropertyDict from deeptrack.sources import SourceItem from deeptrack.types import ArrayLike, PropertyLike + +if TYPE_CHECKING: + import torch + + MERGE_STRATEGY_OVERRIDE: int = 0 MERGE_STRATEGY_APPEND: int = 1 @@ -373,14 +379,21 @@ def device(self) -> str | torch.device: def __init__( self: Feature, - _input: Any = [], + _input: ( + NDArray + | list[NDArray] + | torch.Tensor + | list[torch.Tensor] + | Image + | list[Image] + ), **kwargs: dict[str, Any], ) -> None: """Initialize a new Feature instance. Parameters ---------- - _input: np.ndarray or list[np.ndarray] or Image or list of Images, optional + _input: np.ndarray or list[np.ndarray] or torch.Tensor or list[torch.Tensor] or Image or list[Images], optional The initial input(s) for the feature, often images or other data. If not provided, defaults to an empty list. **kwargs: dict of str to Any @@ -388,10 +401,10 @@ def __init__( stored in `self.properties`, allowing for dynamic or parameterized behavior. If not provided, defaults to an empty list. - + """ - # store backend on initialization + # Store backend on initialization. self._backend = config.get_backend() # Store the dtype and device on initialization. @@ -492,6 +505,7 @@ def __call__( The output of the feature or pipeline after execution. """ + with config.with_backend(self._backend): # If image_list is as Source, activate it. self._activate_sources(image_list) From c6132f482654b8ece9fc71ab3f698a181491f84b Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 18 Jun 2025 20:37:08 +0200 Subject: [PATCH 002/118] Update features.py --- deeptrack/features.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 775cb8815..8cbef4c07 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -138,7 +138,7 @@ def merge_features( from deeptrack import units from deeptrack.backend import config, xp from deeptrack.backend.core import DeepTrackNode -from deeptrack.backend.units import ConversionTable +from deeptrack.backend.units import ConversionTable, create_context from deeptrack.image import Image from deeptrack.properties import PropertyDict from deeptrack.sources import SourceItem From 4b68ce1ba69bea81520e6c0cb0e89d88eeac47d0 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 18 Jun 2025 20:41:03 +0200 Subject: [PATCH 003/118] Update features.py --- deeptrack/features.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 8cbef4c07..6bfe3c1a7 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -386,7 +386,7 @@ def __init__( | list[torch.Tensor] | Image | list[Image] - ), + ) = [], **kwargs: dict[str, Any], ) -> None: """Initialize a new Feature instance. From 21044e8388b58acdd7750b311ed5977ca0f2a207 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Sun, 22 Jun 2025 11:50:56 +0200 Subject: [PATCH 004/118] Update features.py --- deeptrack/features.py | 53 +++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 20 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 6bfe3c1a7..ae6ad2a9c 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -1804,16 +1804,16 @@ class DummyFeature(Feature): """A no-op feature that simply returns the input unchanged. This class can serve as a container for properties that don't directly - transform the data but need to be logically grouped. Since it inherits - transform the data but need to be logically grouped. Since it inherits - from `Feature`, any keyword arguments passed to the constructor are - stored as `Property` instances in `self.properties`, enabling dynamic - behavior or parameterization without performing any transformations - on the input data. + transform the data but need to be logically grouped. + + Since it inherits from `Feature`, any keyword arguments passed to the + constructor are stored as `Property` instances in `self.properties`, + enabling dynamic behavior or parameterization without performing any + transformations on the input data. Parameters ---------- - _input: np.ndarray or list np.ndarray or Image or list of Images, optional + _input: np.ndarray or list[np.ndarray] or torch.Tensor or list[torch.Tensor] or Image or list[Images], optional An optional input (image or list of images) that can be set for the feature. By default, an empty list. **kwargs: dict of str to Any @@ -1822,9 +1822,8 @@ class DummyFeature(Feature): Methods ------- - `get(image: np.ndarray | list np.ndarray | Image | list[Image], **kwargs: dict[str, Any]) -> Image | list[Image]` - Simply returns the input image(s) unchanged. - + `get(image: np.ndarray or list[np.ndarray] or torch.Tensor or list[torch.Tensor] or Image or list[Images], **kwargs: Any) -> np.ndarray or list[np.ndarray] or torch.Tensor or list[torch.Tensor] or Image or list[Images]` + It simply returns the input image(s) unchanged. Examples -------- @@ -1842,29 +1841,43 @@ class DummyFeature(Feature): >>> output_image = dummy_feature(dummy_image) Verify the output is identical to the input: - >>> print(np.array_equal(dummy_image, output_image)) + >>> np.array_equal(dummy_image, output_image) True Access the properties stored in DummyFeature: - >>> print(dummy_feature.properties["value"]()) + >>> dummy_feature.properties["value"]() 42 """ def get( self: Feature, - image: np.ndarray | list[np.ndarray] | Image | list[Image], + image: ( + NDArray + | list[NDArray] + | torch.Tensor + | list[torch.Tensor] + | Image + | list[Image] + ), **kwargs: Any, - )-> Image | list[Image]: + ) -> ( + NDArray + | list[NDArray] + | torch.Tensor + | list[torch.Tensor] + | Image + | list[Image] + ): """Return the input image or list of images unchanged. - This method simply returns the input without applying any transformation. + This method simply returns the input without any transformation. It adheres to the `Feature` interface by accepting additional keyword - arguments for consistency, although they are not used in this method. + arguments for consistency, although they are not used. Parameters ---------- - image: np.ndarray or list np.ndarray or Image or list of Image + image: np.ndarray or list[np.ndarray] or torch.Tensor or list[torch.Tensor] or Image or list[Images] The image or list of images to pass through without modification. **kwargs: Any Additional properties sampled from `self.properties` or passed @@ -1873,8 +1886,8 @@ def get( Returns ------- - Image or list of Images - The same `image` object that was passed in. + np.ndarray or list[np.ndarray] or torch.Tensor or list[torch.Tensor] or Image or list[Images] + The same image that was passed in. """ @@ -1930,7 +1943,7 @@ class Value(Feature): __distributed__: bool = False # Process as a single batch. def __init__( - self: Feature, + self: Feature, value: PropertyLike[float] = 0, **kwargs: dict[str, Any] ): From 0f55b17616309c7b60eef7308d9936a117b85404 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Sun, 22 Jun 2025 11:50:58 +0200 Subject: [PATCH 005/118] Update test_features.py --- deeptrack/tests/test_features.py | 78 ++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index f19d97207..e3037c40c 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -12,7 +12,14 @@ import numpy as np -from deeptrack import features, properties, scatterers, units, optics +from deeptrack import ( + features, + optics, + properties, + scatterers, + TORCH_AVAILABLE, + units, +) from deeptrack.image import Image from deeptrack.noises import Gaussian @@ -598,11 +605,10 @@ def get(self, image, **kwargs): + A.properties["addend"]()), ) ) - - def test_DummyFeature(self): - """Test that the DummyFeature correctly returns the value of its properties.""" + def test_DummyFeature(self): + # Test that DummyFeature properties are callable and can be updated. feature = features.DummyFeature(a=1, b=2, c=3) self.assertEqual(feature.a(), 1) @@ -618,6 +624,69 @@ def test_DummyFeature(self): feature.c.set_value(6) self.assertEqual(feature.c(), 6) + # Test that DummyFeature returns input unchanged and supports call + # syntax. + feature = features.DummyFeature() + input_array = np.random.rand(10, 10) + output_array = feature.get(input_array) + self.assertIs(output_array, input_array) + # For callability via __call__ (as per DeepTrack2) + output_array_call = feature(input_array) + self.assertIs(output_array_call, input_array) + + # Test with NumPy array + arr = np.zeros((3, 3)) + self.assertIs(feature.get(arr), arr) + self.assertIs(feature(arr), arr) + + # Test with list of NumPy arrays + arr_list = [np.ones((2, 2)), np.zeros((2, 2))] + self.assertEqual(feature.get(arr_list), arr_list) + self.assertEqual(feature(arr_list), arr_list) + + # Test with PyTorch + if TORCH_AVAILABLE: + import torch + + # Test with PyTorch tensor + tensor = torch.ones(4, 4) + self.assertIs(feature.get(tensor), tensor) + self.assertIs(feature(tensor), tensor) + + # Test with list of PyTorch tensors + tensor_list = [torch.zeros(2, 2), torch.ones(2, 2)] + self.assertEqual(feature.get(tensor_list), tensor_list) + self.assertEqual(feature(tensor_list), tensor_list) + + # Test with Image + img = Image(np.zeros((5, 5))) + self.assertIs(feature.get(img), img) + # feature(img) returns an array, not an Image. + self.assertTrue(np.array_equal(feature(img), img.data)) + # Note: Using feature.get(img) returns the Image object itself, + # while using feature(img) (i.e., calling the feature directly) + # returns the underlying NumPy array (img.data). This behavior + # is by design in DeepTrack2, where the __call__ method extracts + # the raw array from the Image to facilitate downstream processing + # with NumPy and similar libraries. Therefore, when testing or + # using features, always be mindful of whether you want the + # object (Image) or just its data (array). + + # Test with list of Image + img_list = [Image(np.ones((3, 3))), Image(np.zeros((3, 3)))] + self.assertEqual(feature.get(img_list), img_list) + # feature(img_list) returns a list of arrays, not a list of Images. + output = feature(img_list) + self.assertEqual(len(output), len(img_list)) + for arr, img in zip(output, img_list): + self.assertTrue(np.array_equal(arr, img.data)) + # Note: Calling feature(img_list) returns a list of NumPy arrays + # extracted from each Image in img_list, whereas feature.get(img_list) + # returns the original list of Image objects. This difference is + # intentional in DeepTrack2, where the __call__ method is designed to + # yield the underlying array data for easier interoperability with + # NumPy and downstream processing. + def test_Value(self): @@ -667,6 +736,7 @@ def test_FloorDivide(self): def test_Power(self): test_operator(self, operator.pow) + def test_LessThan(self): test_operator(self, operator.lt) From 017f21efa1f1eaa4f89a8dc759b1cec197b810c2 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Sun, 22 Jun 2025 12:00:51 +0200 Subject: [PATCH 006/118] Update features.py --- deeptrack/features.py | 57 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index ae6ad2a9c..2a42f02ad 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -130,8 +130,8 @@ def merge_features( import numpy as np from numpy.typing import NDArray -import matplotlib.animation as animation import matplotlib.pyplot as plt +from matplotlib import animation from pint import Quantity from scipy.spatial.distance import cdist @@ -145,6 +145,61 @@ def merge_features( from deeptrack.types import ArrayLike, PropertyLike +__all__ = [ + "Feature", + "StructuralFeature", + "Chain", + "Branch", + "DummyFeature", + "Value", + "ArithmeticOperationFeature", + "Add", + "Subtract", + "Multiply", + "Divide", + "FloorDivide", + "Power", + "LessThan", + "LessThanOrEquals", + "LessThanOrEqual", + "GreaterThan", + "GreaterThanOrEquals", + "GreaterThanOrEqual", + "Equals", + "Equal", + "Stack", + "Arguments", + "Probability", + "Repeat", + "Combine", + "Slice", + "Bind", + "BindResolve", + "BindUpdate", + "ConditionalSetProperty", + "ConditionalSetFeature", + "Lambda", + "Merge", + "OneOf", + "OneOfDict", + "LoadImage", + "SampleToMasks", + "AsType", + "ChannelFirst2d", + "Upscale", + "NonOverlapping", + "Store", + "Squeeze", + "Unsqueeze", + "ExpandDims", + "MoveAxis", + "Transpose", + "Permute", + "OneHot", + "TakeProperties", +] + + if TYPE_CHECKING: import torch From 3e4a546958db9576497b1160cb019f571fd18204 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 23 Jun 2025 09:03:52 +0200 Subject: [PATCH 007/118] Value Update test_features.py Update features.py Update test_features.py Update features.py --- deeptrack/features.py | 184 +++++++++++++++++-------------- deeptrack/tests/test_features.py | 31 +++++- 2 files changed, 134 insertions(+), 81 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 2a42f02ad..bbd8fdd8a 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -146,57 +146,57 @@ def merge_features( __all__ = [ - "Feature", - "StructuralFeature", - "Chain", - "Branch", + "Feature", # TODO + "StructuralFeature", # TODO + "Chain", # TODO + "Branch", # TODO "DummyFeature", "Value", - "ArithmeticOperationFeature", - "Add", - "Subtract", - "Multiply", - "Divide", - "FloorDivide", - "Power", - "LessThan", - "LessThanOrEquals", - "LessThanOrEqual", - "GreaterThan", - "GreaterThanOrEquals", - "GreaterThanOrEqual", - "Equals", - "Equal", - "Stack", - "Arguments", - "Probability", - "Repeat", - "Combine", - "Slice", - "Bind", - "BindResolve", - "BindUpdate", - "ConditionalSetProperty", - "ConditionalSetFeature", - "Lambda", - "Merge", - "OneOf", - "OneOfDict", - "LoadImage", - "SampleToMasks", - "AsType", - "ChannelFirst2d", - "Upscale", - "NonOverlapping", - "Store", - "Squeeze", - "Unsqueeze", - "ExpandDims", - "MoveAxis", - "Transpose", - "Permute", - "OneHot", - "TakeProperties", + "ArithmeticOperationFeature", # TODO + "Add", # TODO + "Subtract", # TODO + "Multiply", # TODO + "Divide", # TODO + "FloorDivide", # TODO + "Power", # TODO + "LessThan", # TODO + "LessThanOrEquals", # TODO + "LessThanOrEqual", # TODO + "GreaterThan", # TODO + "GreaterThanOrEquals", # TODO + "GreaterThanOrEqual", # TODO + "Equals", # TODO + "Equal", # TODO + "Stack", # TODO + "Arguments", # TODO + "Probability", # TODO + "Repeat", # TODO + "Combine", # TODO + "Slice", # TODO + "Bind", # TODO + "BindResolve", # TODO + "BindUpdate", # TODO + "ConditionalSetProperty", # TODO + "ConditionalSetFeature", # TODO + "Lambda", # TODO + "Merge", # TODO + "OneOf", # TODO + "OneOfDict", # TODO + "LoadImage", # TODO + "SampleToMasks", # TODO + "AsType", # TODO + "ChannelFirst2d", # TODO + "Upscale", # TODO + "NonOverlapping", # TODO + "Store", # TODO + "Squeeze", # TODO + "Unsqueeze", # TODO + "ExpandDims", # TODO + "MoveAxis", # TODO + "Transpose", # TODO + "Permute", # TODO + "OneHot", # TODO + "TakeProperties", # TODO ] @@ -530,7 +530,7 @@ def __call__( self: Feature, image_list: np.ndarray | list[np.ndarray] | Image | list[Image] = None, _ID: tuple[int, ...] = (), - **kwargs: dict[str, Any], + **kwargs: Any, ) -> Any: """Execute the feature or pipeline. @@ -1906,7 +1906,7 @@ class DummyFeature(Feature): """ def get( - self: Feature, + self: DummyFeature, image: ( NDArray | list[NDArray] @@ -1953,17 +1953,17 @@ class Value(Feature): """Represents a constant (per evaluation) value in a DeepTrack pipeline. This feature holds a constant value (e.g., a scalar or array) and supplies - it on demand to other parts of the pipeline. It does not transform the - input image but instead returns the stored value. + it on demand to other parts of the pipeline. + + Wen called with an image, it does not transform the input image but instead + returns the stored value. Parameters ---------- - value: PropertyLike[float], optional - The numerical value to store. Defaults to 0. If an `Image` is provided, - a warning is issued recommending conversion to a NumPy array for - The numerical value to store. Defaults to 0. If an `Image` is provided, - a warning is issued recommending conversion to a NumPy array for - performance reasons. + value: PropertyLike[float or array], optional + The numerical value to store. It defaults to 0. + If an `Image` is provided, a warning is issued recommending conversion + to a NumPy array or a PyTorch tensor for performance reasons. **kwargs: dict of str to Any Additional named properties passed to the `Feature` constructor. @@ -1979,42 +1979,65 @@ class Value(Feature): `get(image: Any, value: float, **kwargs: dict[str, Any]) -> float` Returns the stored value, ignoring the input image. - Examples -------- >>> import deeptrack as dt Initialize a constant value and retrieve it: >>> value = dt.Value(42) - >>> print(value()) + >>> value() 42 Override the value at call time: - >>> print(value(value=100)) + >>> value(value=100) 100 + Initialize a constant array value and retrieve it: + >>> import numpy as np + >>> + >>> arr_value = dt.Value(np.arange(4)) + >>> arr_value() + array([0, 1, 2, 3]) + + Override the array value at call time: + >>> arr_value(value=np.array([10, 20, 30, 40])) + array([10, 20, 30, 40]) + + Initialize a constant PyTorch tensor value and retrieve it: + >>> import torch + >>> + >>> tensor_value = dt.Value(torch.tensor([1., 2., 3.])) + >>> tensor_value() + tensor([1., 2., 3.]) + + Override the tensor value at call time: + >>> tensor_value(value=torch.tensor([10., 20., 30.])) + tensor([10., 20., 30.]) + """ __distributed__: bool = False # Process as a single batch. def __init__( - self: Feature, - value: PropertyLike[float] = 0, - **kwargs: dict[str, Any] + self: Value, + value: PropertyLike[float | ArrayLike] = 0, + **kwargs: Any, ): """Initialize the `Value` feature to store a constant value. This feature holds a constant numerical value and provides it to the - pipeline as needed. If an `Image` object is supplied, a warning is - issued to encourage converting it to a NumPy array for performance + pipeline as needed. + + If an `Image` object is supplied, a warning is issued to encourage + converting it to a NumPy array or a PyTorch tensor for performance optimization. Parameters ---------- - value: PropertyLike[float], optional + value: PropertyLike[float or array], optional The initial value to store. If an `Image` is provided, a warning is - raised. Defaults to 0. - **kwargs: dict of str to Any + raised. It defaults to 0. + **kwargs: Any Additional keyword arguments passed to the `Feature` constructor, such as custom properties or the feature name. @@ -2023,19 +2046,20 @@ def __init__( if isinstance(value, Image): import warnings warnings.warn( - "Setting dt.Value value as an Image object is likely to lead " - "to performance deterioration. Consider converting it to a " - "numpy array using np.array." + "Passing an Image object as the value to dt.Value may lead to " + "performance deterioration. Consider converting the Image to " + "a NumPy array with np.array(image), or to a PyTorch tensor " + "with torch.tensor(np.array(image))." ) super().__init__(value=value, **kwargs) def get( self: Feature, - image: Any, - value: float, - **kwargs: dict[str, Any] - ) -> float: + image: Any, + value: float | ArrayLike, + **kwargs: Any, + ) -> float | ArrayLike: """Return the stored value, ignoring the input image. The `get` method simply returns the stored numerical value, allowing @@ -2046,16 +2070,16 @@ def get( image: Any Input data typically processed by features. For `Value`, this is ignored and does not affect the output. - value: float + value: float or array The current value to return. This may be the initial value or an overridden value supplied during the method call. - **kwargs: dict of str to Any + **kwargs: Any Additional keyword arguments, which are ignored but included for consistency with the feature interface. Returns ------- - float + float or array The stored or overridden `value`, returned unchanged. """ diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index e3037c40c..70f38822c 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -689,19 +689,48 @@ def test_DummyFeature(self): def test_Value(self): - + # Scalar value tests value = features.Value(value=1) self.assertEqual(value(), 1) self.assertEqual(value.value(), 1) self.assertEqual(value(value=2), 2) + self.assertEqual(value(), 2) self.assertEqual(value.value(), 2) value = features.Value(value=lambda: 1) self.assertEqual(value(), 1) self.assertEqual(value.value(), 1) self.assertNotEqual(value(value=lambda: 2), 2) + self.assertNotEqual(value(), 2) self.assertNotEqual(value.value(), 2) + # NumPy array value tests + arr = np.arange(4) + value_arr = features.Value(value=arr) + self.assertTrue(np.array_equal(value_arr(), arr)) + self.assertTrue(np.array_equal(value_arr.value(), arr)) + # Override with a new array + override_arr = np.array([10, 20, 30, 40]) + self.assertTrue( + np.array_equal(value_arr(value=override_arr), override_arr) + ) + self.assertTrue(np.array_equal(value_arr(), override_arr)) + self.assertTrue(np.array_equal(value_arr.value(), override_arr)) + + # PyTorch tensor value tests + if TORCH_AVAILABLE: + import torch + + tensor = torch.tensor([1., 2., 3.]) + value_tensor = features.Value(value=tensor) + self.assertTrue(torch.equal(value_tensor(), tensor)) + self.assertTrue(torch.equal(value_tensor.value(), tensor)) + # Override with a new tensor + override_tensor = torch.tensor([10., 20., 30.]) + self.assertTrue(torch.equal(value_tensor(value=override_tensor), override_tensor)) + self.assertTrue(torch.equal(value_tensor(), override_tensor)) + self.assertTrue(torch.equal(value_tensor.value(), override_tensor)) + def test_ArithmeticOperationFeature(self): From 83bb4911320424595f655bd13d939c8ac0514422 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 23 Jun 2025 09:35:39 +0200 Subject: [PATCH 008/118] Update test_features.py --- deeptrack/tests/test_features.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index 70f38822c..6cd50d4af 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -739,7 +739,7 @@ def test_ArithmeticOperationFeature(self): input_values = [1, 2, 3, 4] expected_output = [11, 12, 13, 14] output = addition_feature(input_values) - self.assertEqual(output, expected_output) + self.assertEqual(output, expected_output) def test_Add(self): From 8851004153215a2bb262858d24d137c0c84d10b4 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 23 Jun 2025 09:59:40 +0200 Subject: [PATCH 009/118] remove __gpu_compatible__ --- deeptrack/elementwise.py | 2 -- deeptrack/features.py | 7 ------- deeptrack/optics.py | 8 -------- deeptrack/scatterers.py | 2 -- 4 files changed, 19 deletions(-) diff --git a/deeptrack/elementwise.py b/deeptrack/elementwise.py index a40c83a76..72427c6cd 100644 --- a/deeptrack/elementwise.py +++ b/deeptrack/elementwise.py @@ -145,8 +145,6 @@ class ElementwiseFeature(Feature): Returns the result of applying the function to the input array. """ - - __gpu_compatible__: bool = True def __init__( self: ElementwiseFeature, diff --git a/deeptrack/features.py b/deeptrack/features.py index bbd8fdd8a..332ff7f27 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -276,9 +276,6 @@ class Feature(DeepTrackNode): __conversion_table__: ConversionTable Defines the unit conversions used by the feature to convert its properties into the desired units. - __gpu_compatible__: bool - Indicates whether the feature can use GPU acceleration. When enabled, - GPU execution is triggered based on input size or backend settings. Methods ------- @@ -399,7 +396,6 @@ class Feature(DeepTrackNode): __distributed__ = True __property_memorability__ = 1 __conversion_table__ = ConversionTable() - __gpu_compatible__ = False _wrap_array_with_image: bool = False _float_dtype: str @@ -2111,8 +2107,6 @@ class ArithmeticOperationFeature(Feature): __distributed__: bool Indicates that this feature’s `get(...)` method processes the input as a whole (`False`) rather than distributing calls for individual items. - __gpu_compatible__: bool - Specifies that the feature is compatible with GPU processing (`True`). Methods ------- @@ -2138,7 +2132,6 @@ class ArithmeticOperationFeature(Feature): """ __distributed__: bool = False - __gpu_compatible__: bool = True def __init__( diff --git a/deeptrack/optics.py b/deeptrack/optics.py index b9885536d..9b29ca06c 100644 --- a/deeptrack/optics.py +++ b/deeptrack/optics.py @@ -972,8 +972,6 @@ class Fluorescence(Optics): Attributes ---------- - __gpu_compatible__: bool - Indicates whether the class supports GPU acceleration. NA: float Numerical aperture of the optical system. wavelength: float @@ -1018,8 +1016,6 @@ class Fluorescence(Optics): """ - __gpu_compatible__ = True - def get( self: Fluorescence, illuminated_volume: ArrayLike[complex], @@ -1198,8 +1194,6 @@ class Brightfield(Optics): Attributes ---------- - __gpu_compatible__: bool - Indicates whether the class supports GPU acceleration. __conversion_table__: ConversionTable Table used to convert properties of the feature to desired units. NA: float @@ -1247,8 +1241,6 @@ class Brightfield(Optics): """ - __gpu_compatible__ = True - __conversion_table__ = ConversionTable( working_distance=(u.meter, u.meter), ) diff --git a/deeptrack/scatterers.py b/deeptrack/scatterers.py index 1b45fc861..5646c64ec 100644 --- a/deeptrack/scatterers.py +++ b/deeptrack/scatterers.py @@ -791,8 +791,6 @@ class MieScatterer(Scatterer): """ - __gpu_compatible__ = True - __conversion_table__ = ConversionTable( radius=(u.meter, u.meter), polarization_angle=(u.radian, u.radian), From c806cfdce4b6d1f639bbec8da688b27b0e14dd98 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 23 Jun 2025 11:59:43 +0200 Subject: [PATCH 010/118] Update features.py --- deeptrack/features.py | 83 ++++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 332ff7f27..0b424d410 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -152,7 +152,7 @@ def merge_features( "Branch", # TODO "DummyFeature", "Value", - "ArithmeticOperationFeature", # TODO + "ArithmeticOperationFeature", "Add", # TODO "Subtract", # TODO "Multiply", # TODO @@ -524,7 +524,15 @@ def get( def __call__( self: Feature, - image_list: np.ndarray | list[np.ndarray] | Image | list[Image] = None, + image_list: ( + NDArray[Any] + | list[NDArray[Any]] + | torch.Tensor + | list[torch.Tensor] + | Image + | list[Image] + | None + ) = None, _ID: tuple[int, ...] = (), **kwargs: Any, ) -> Any: @@ -1864,16 +1872,16 @@ class DummyFeature(Feature): Parameters ---------- - _input: np.ndarray or list[np.ndarray] or torch.Tensor or list[torch.Tensor] or Image or list[Images], optional - An optional input (image or list of images) that can be set for - the feature. By default, an empty list. + _input: Any, optional + An optional input (typically an image or list of images) that can be + set for the feature. It defaults to an empty list []. **kwargs: dict of str to Any Additional keyword arguments are wrapped as `Property` instances and stored in `self.properties`. Methods ------- - `get(image: np.ndarray or list[np.ndarray] or torch.Tensor or list[torch.Tensor] or Image or list[Images], **kwargs: Any) -> np.ndarray or list[np.ndarray] or torch.Tensor or list[torch.Tensor] or Image or list[Images]` + `get(image: Any, **kwargs: Any) -> Any` It simply returns the input image(s) unchanged. Examples @@ -1903,23 +1911,9 @@ class DummyFeature(Feature): def get( self: DummyFeature, - image: ( - NDArray - | list[NDArray] - | torch.Tensor - | list[torch.Tensor] - | Image - | list[Image] - ), + image: Any, **kwargs: Any, - ) -> ( - NDArray - | list[NDArray] - | torch.Tensor - | list[torch.Tensor] - | Image - | list[Image] - ): + ) -> Any: """Return the input image or list of images unchanged. This method simply returns the input without any transformation. @@ -1928,8 +1922,9 @@ def get( Parameters ---------- - image: np.ndarray or list[np.ndarray] or torch.Tensor or list[torch.Tensor] or Image or list[Images] - The image or list of images to pass through without modification. + image: Any + The input (typically an image or list of images) to pass through + without modification. **kwargs: Any Additional properties sampled from `self.properties` or passed externally. These are unused here but provided for consistency @@ -1937,8 +1932,9 @@ def get( Returns ------- - np.ndarray or list[np.ndarray] or torch.Tensor or list[torch.Tensor] or Image or list[Images] - The same image that was passed in. + Any + The same input that was passed in (typically an image or list of + images). """ @@ -2051,7 +2047,7 @@ def __init__( super().__init__(value=value, **kwargs) def get( - self: Feature, + self: Value, image: Any, value: float | ArrayLike, **kwargs: Any, @@ -2088,7 +2084,10 @@ class ArithmeticOperationFeature(Feature): This feature performs an arithmetic operation (e.g., addition, subtraction, multiplication) on the input data. The inputs can be single values or lists - of values. If a list is passed, the operation is applied to each element. + of values. + + If a list is passed, the operation is applied to each element. + If both inputs are lists of different lengths, the shorter list is cycled. Parameters @@ -2097,7 +2096,7 @@ class ArithmeticOperationFeature(Feature): The arithmetic operation to apply, such as a built-in operator (`operator.add`, `operator.mul`) or a custom callable. value: float or int or list of float or int, optional - The second operand for the operation. Defaults to 0. If a list is + The second operand for the operation. It defaults to 0. If a list is provided, the operation will apply element-wise. **kwargs: dict of str to Any Additional keyword arguments passed to the parent `Feature`. @@ -2110,7 +2109,7 @@ class ArithmeticOperationFeature(Feature): Methods ------- - `get(image: Any | list of Any, value: float | int | list[float] | int, **kwargs: dict[str, Any]) -> list[Any]` + `get(image: Any | list of Any, value: float | int | list[float] | int, **kwargs: Any) -> list[Any]` Apply the arithmetic operation element-wise to the input data. Examples @@ -2133,33 +2132,35 @@ class ArithmeticOperationFeature(Feature): __distributed__: bool = False - def __init__( - self: Feature, + self: ArithmeticOperationFeature, op: Callable[[Any, Any], Any], - value: float | int | list[float | int] = 0, - **kwargs: dict[str, Any], + value: PropertyLike[float | int | list[float | int]] = 0, + **kwargs: Any, ): """Initialize the ArithmeticOperationFeature. Parameters ---------- op: Callable[[Any, Any], Any] - The arithmetic operation to apply, such as `operator.add`, `operator.mul`, - or any custom callable that takes two arguments. - value: float or int or list of float or int, optional + The arithmetic operation to apply, such as `operator.add`, + `operator.mul`, or any custom callable that takes two arguments and + returns a single output value. + value: PropertyLike[float or int or list of float or int], optional The second operand(s) for the operation. If a list is provided, the - operation is applied element-wise. Defaults to 0. + operation is applied element-wise. It defaults to 0. **kwargs: dict of str to Any - Additional keyword arguments passed to the parent `Feature` constructor. + Additional keyword arguments passed to the parent `Feature` + constructor. """ super().__init__(value=value, **kwargs) + self.op = op def get( - self: Feature, + self: ArithmeticOperationFeature, image: Any | list[Any], value: float | int | list[float | int], **kwargs: Any, @@ -2171,7 +2172,7 @@ def get( image: Any or list of Any The input data, either a single value or a list of values, to be transformed by the arithmetic operation. - value: float, int, or list of float or int + value: float or int or list of float or int The second operand(s) for the operation. If a single value is provided, it is broadcast to match the input size. If a list is provided, it will be cycled to match the length of the input list. From a1f71d71e4fa6cf934b2bea59c359786e8f3170c Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 23 Jun 2025 11:59:45 +0200 Subject: [PATCH 011/118] Update test_features.py --- deeptrack/tests/test_features.py | 77 +++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index 6cd50d4af..5d9dc8fa9 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -733,7 +733,7 @@ def test_Value(self): def test_ArithmeticOperationFeature(self): - + # Basic addition with lists addition_feature = \ features.ArithmeticOperationFeature(operator.add, value=10) input_values = [1, 2, 3, 4] @@ -741,6 +741,81 @@ def test_ArithmeticOperationFeature(self): output = addition_feature(input_values) self.assertEqual(output, expected_output) + # Scalar input and scalar value + output = addition_feature(5) + self.assertEqual(output, 15) + + # List input, scalar value (broadcast) + input_values = [10, 20, 30] + output = addition_feature(input_values) + self.assertEqual(output, [20, 30, 40]) + + # List input, list value (same length) + addition_feature = features.ArithmeticOperationFeature( + operator.add, value=[1, 2, 3], + ) + input_values = [10, 20, 30] + self.assertEqual(addition_feature(input_values), [11, 22, 33]) + + # List input, list value (different lengths, value list cycles) + addition_feature = features.ArithmeticOperationFeature( + operator.add, value=[1, 2], + ) + input_values = [10, 20, 30, 40, 50] + # value cycles as 1,2,1,2,1 + self.assertEqual(addition_feature(input_values), [11, 22, 31, 42, 51]) + + # NumPy array input, scalar value + addition_feature = features.ArithmeticOperationFeature( + operator.add, value=5, + ) + arr = np.array([1, 2, 3]) + self.assertEqual(addition_feature(arr.tolist()), [6, 7, 8]) + + # NumPy array input, NumPy array value + addition_feature = features.ArithmeticOperationFeature( + operator.add, value=[4, 5, 6], + ) + arr_input = [ + np.array([1, 2]), np.array([3, 4]), np.array([5, 6]), + ] + arr_value = [ + np.array([10, 20]), np.array([30, 40]), np.array([50, 60]), + ] + feature = features.ArithmeticOperationFeature( + lambda a, b: np.add(a, b), value=arr_value, + ) + for output, expected in zip( + feature(arr_input), + [np.array([11, 22]), np.array([33, 44]), np.array([55, 66])], + ): + self.assertTrue(np.array_equal(output, expected)) + + # PyTorch tensor input (if available) + if TORCH_AVAILABLE: + import torch + + addition_feature = features.ArithmeticOperationFeature( + lambda a, b: a + b, value=5, + ) + tensors = [torch.tensor(1), torch.tensor(2), torch.tensor(3)] + expected = [torch.tensor(6), torch.tensor(7), torch.tensor(8)] + output = addition_feature(tensors) + for out, exp in zip(output, expected): + self.assertTrue(torch.equal(out, exp)) + + # Tensor input, tensor value (elementwise) + t1 = [torch.tensor([1.0, 2.0]), torch.tensor([3.0, 4.0])] + t2 = [torch.tensor([10.0, 20.0]), torch.tensor([30.0, 40.0])] + feature = features.ArithmeticOperationFeature( + lambda a, b: a + b, value=t2, + ) + for output, expected in zip( + feature(t1), + [torch.tensor([11.0, 22.0]), torch.tensor([33.0, 44.0])], + ): + self.assertTrue(torch.equal(output, expected)) + def test_Add(self): test_operator(self, operator.add) From b1ca6ca0e5d1f739cfd9bacc636ab9a4af807fa6 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 23 Jun 2025 21:06:08 +0200 Subject: [PATCH 012/118] Update features.py --- deeptrack/features.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 0b424d410..70a440a2c 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -2239,8 +2239,8 @@ class Add(ArithmeticOperationFeature): def __init__( self: Feature, - value: PropertyLike[float] = 0, - **kwargs: dict[str, Any], + value: PropertyLike[float | int | list[float | int]] = 0, + **kwargs: Any, ): """Initialize the Add feature. From 0a78b0f45b5174790c64fc14d43d53390d2af9ad Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 23 Jun 2025 21:06:10 +0200 Subject: [PATCH 013/118] Update test_features.py --- deeptrack/tests/test_features.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index 5d9dc8fa9..2c39ff882 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -805,13 +805,13 @@ def test_ArithmeticOperationFeature(self): self.assertTrue(torch.equal(out, exp)) # Tensor input, tensor value (elementwise) - t1 = [torch.tensor([1.0, 2.0]), torch.tensor([3.0, 4.0])] - t2 = [torch.tensor([10.0, 20.0]), torch.tensor([30.0, 40.0])] + t_input = [torch.tensor([1.0, 2.0]), torch.tensor([3.0, 4.0])] + t_value = [torch.tensor([10.0, 20.0]), torch.tensor([30.0, 40.0])] feature = features.ArithmeticOperationFeature( - lambda a, b: a + b, value=t2, + lambda a, b: a + b, value=t_value, ) for output, expected in zip( - feature(t1), + feature(t_input), [torch.tensor([11.0, 22.0]), torch.tensor([33.0, 44.0])], ): self.assertTrue(torch.equal(output, expected)) From 2ea065bf67734e718a2e7ce4f7f8905ab9e670b0 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 23 Jun 2025 21:36:46 +0200 Subject: [PATCH 014/118] Update aberrations.py --- deeptrack/aberrations.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/deeptrack/aberrations.py b/deeptrack/aberrations.py index 6c9b41b3f..7256c3ceb 100644 --- a/deeptrack/aberrations.py +++ b/deeptrack/aberrations.py @@ -397,7 +397,7 @@ def get( n: int | list[int], m: int | list[int], coefficient: float | list[float], - **kwargs: dict[str, Any], + **kwargs: Any, ) -> np.ndarray: """Applies the Zernike phase aberration to the input pupil function. @@ -564,7 +564,7 @@ def __init__( self: "Piston", *args: tuple[Any, ...], coefficient: PropertyLike[float | list[float]] = 1, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> None: """Initializes the Piston class. @@ -623,7 +623,7 @@ def __init__( self: VerticalTilt, *args: tuple[Any, ...], coefficient: PropertyLike[float | list[float]] = 1, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> None: """Initializes the VerticalTilt class. @@ -682,7 +682,7 @@ def __init__( self: HorizontalTilt, *args: tuple[Any, ...], coefficient: PropertyLike[float | list[float]] = 1, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> None: """Initializes the HorizontalTilt class. @@ -743,7 +743,7 @@ def __init__( self: ObliqueAstigmatism, *args: tuple[Any, ...], coefficient: PropertyLike[float | list[float]] = 1, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> None: """Initializes the ObliqueAstigmatism class. @@ -802,7 +802,7 @@ def __init__( self: Defocus, *args: tuple[Any, ...], coefficient: PropertyLike[float | list[float]] = 1, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> None: """Initializes the Defocus class. @@ -861,7 +861,7 @@ def __init__( self: Astigmatism, *args: tuple[Any, ...], coefficient: PropertyLike[float | list[float]] = 1, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> None: """Initializes the Astigmatism class. @@ -909,7 +909,7 @@ def __init__( self: ObliqueTrefoil, *args: tuple[Any, ...], coefficient: PropertyLike[float | list[float]] = 1, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> None: super().__init__(*args, n=3, m=-3, coefficient=coefficient, **kwargs) @@ -930,7 +930,7 @@ def __init__( self: VerticalComa, *args: tuple[Any, ...], coefficient: PropertyLike[float | list[float]] = 1, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> None: super().__init__(*args, n=3, m=-1, coefficient=coefficient, **kwargs) @@ -951,7 +951,7 @@ def __init__( self: HorizontalComa, *args: tuple[Any, ...], coefficient: PropertyLike[float | list[float]] = 1, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> None: super().__init__(*args, n=3, m=1, coefficient=coefficient, **kwargs) @@ -972,7 +972,7 @@ def __init__( self: Trefoil, *args: tuple[Any, ...], coefficient: PropertyLike[float | list[float]] = 1, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> None: super().__init__(*args, n=3, m=3, coefficient=coefficient, **kwargs) @@ -993,6 +993,6 @@ def __init__( self: SphericalAberration, *args: tuple[Any, ...], coefficient: PropertyLike[float | list[float]] = 1, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> None: super().__init__(*args, n=4, m=0, coefficient=coefficient, **kwargs) From d1b8d34c86f6e98d1b3c1ad98d57ebca5a51afa5 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 23 Jun 2025 21:36:49 +0200 Subject: [PATCH 015/118] Update optics.py --- deeptrack/optics.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/deeptrack/optics.py b/deeptrack/optics.py index 9b29ca06c..b014855fd 100644 --- a/deeptrack/optics.py +++ b/deeptrack/optics.py @@ -89,7 +89,7 @@ def _create_volume( pad: int, output_region: Tuple[int, int, int, int], refractive_index_medium: float, - **kwargs: Dict[str, Any], + **kwargs: Any, ) -> np.ndarray Combines multiple scatterer objects into a single 3D volume for imaging. @@ -101,7 +101,7 @@ def _pad_volume( limits: np.ndarray, padding: Tuple[int, int, int, int], output_region: Tuple[int, int, int, int], - **kwargs: Dict[str, Any], + **kwargs: Any, ) -> Tuple[np.ndarray, np.ndarray] Pads a volume with zeros to avoid edge effects during imaging. @@ -205,7 +205,7 @@ def __init__( self: Microscope, sample: Feature, objective: Feature, - **kwargs: Dict[str, Any], + **kwargs: Any, ) -> None: """Initialize the `Microscope` instance. @@ -490,7 +490,7 @@ def __init__( pupil: Feature = None, illumination: Feature = None, upscale: int = 1, - **kwargs: Dict[str, Any], + **kwargs: Any, ) -> None: """Initialize the `Optics` instance. @@ -687,7 +687,7 @@ def _pupil( refractive_index_medium: float, include_aberration: bool = True, defocus: float | ArrayLike[float] = 0, - **kwargs: Dict[str, Any], + **kwargs: Any, ): """Calculates the pupil function at different focal points. @@ -791,7 +791,7 @@ def _pad_volume( limits: ArrayLike[int] = None, padding: ArrayLike[int] = None, output_region: ArrayLike[int] = None, - **kwargs: Dict[str, Any], + **kwargs: Any, ) -> tuple: """Pads the volume with zeros to avoid edge effects. @@ -885,7 +885,7 @@ def _pad_volume( def __call__( self: Optics, sample: Feature, - **kwargs: Dict[str, Any], + **kwargs: Any, ) -> Microscope: """Creates a Microscope instance with the given sample and optics. @@ -1250,7 +1250,7 @@ def get( illuminated_volume: ArrayLike[complex], limits: ArrayLike[int], fields: ArrayLike[complex], - **kwargs: Dict[str, Any], + **kwargs: Any, ) -> Image: """Simulates imaging with brightfield microscopy. @@ -1507,7 +1507,7 @@ def __init__( self: ISCAT, illumination_angle: float = np.pi, amp_factor: float = 1, - **kwargs: Dict[str, Any], + **kwargs: Any, ) -> None: """Initializes the ISCAT class. @@ -1617,7 +1617,7 @@ def get( illuminated_volume: ArrayLike[complex], limits: ArrayLike[int], fields: ArrayLike[complex], - **kwargs: Dict[str, Any], + **kwargs: Any, ) -> Image: """Retrieve the darkfield image of the illuminated volume. @@ -1698,7 +1698,7 @@ def __init__( constant: PropertyLike[float] = 0, vmin: PropertyLike[float] = 0, vmax: PropertyLike[float] = np.inf, - **kwargs: Dict[str, Any], + **kwargs: Any, ) -> None: """Initializes the IlluminationGradient class. @@ -1731,7 +1731,7 @@ def get( constant: float, vmin: float, vmax: float, - **kwargs: Dict[str, Any], + **kwargs: Any, ) -> ArrayLike[complex]: """Applies the gradient and constant offset to the amplitude of the field. @@ -1858,7 +1858,7 @@ def _create_volume( pad: tuple = (0, 0, 0, 0), output_region: tuple = (None, None, None, None), refractive_index_medium: float = 1.33, - **kwargs: Dict[str, Any], + **kwargs: Any, ) -> tuple: """Converts a list of scatterers into a volumetric representation. From 84fecbd9cbac0a936d9b7bf4e91262b195757556 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 23 Jun 2025 21:36:52 +0200 Subject: [PATCH 016/118] Update image.py --- deeptrack/image.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deeptrack/image.py b/deeptrack/image.py index 75fd0d7fe..20ecd4456 100644 --- a/deeptrack/image.py +++ b/deeptrack/image.py @@ -1004,7 +1004,7 @@ def __array_ufunc__( ufunc: np.ufunc, method: str, *inputs: tuple[Any, ...], - **kwargs: dict[str, Any], + **kwargs: Any, ) -> Image | tuple[Image, ...] | None: """Enable Image objects to use NumPy ufuncs. @@ -1211,7 +1211,7 @@ def __torch_function__(self, func, types, args=(), kwargs=None): def __array__( self: Image | np.ndarray, *args: tuple[Any, ...], - **kwargs: dict[str, Any], + **kwargs: Any, ) -> np.ndarray: """Convert the Image object to a NumPy array. From 6eafa18dcc74a97360df7e743c04f40890dade88 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 23 Jun 2025 21:36:54 +0200 Subject: [PATCH 017/118] Update holography.py --- deeptrack/holography.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/deeptrack/holography.py b/deeptrack/holography.py index db1a93fc7..994d362fb 100644 --- a/deeptrack/holography.py +++ b/deeptrack/holography.py @@ -188,7 +188,7 @@ def get( self: Rescale, image: Image | np.ndarray, rescale: float, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> Image | np.ndarray: """Rescales the image by subtracting the real part of the field before multiplication. @@ -199,7 +199,7 @@ def get( The image to rescale. rescale: float The rescaling factor. - **kwargs: dict of str to Any + **kwargs: Any Additional keyword arguments. Returns @@ -253,7 +253,7 @@ def get( self: FourierTransform, image: Image | np.ndarray, padding: int = 32, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> np.ndarray: """Computes the Fourier transform of the image. @@ -263,7 +263,7 @@ def get( The image to transform. padding: int, optional Number of pixels to pad symmetrically around the image (default is 32). - **kwargs: dict of str to Any + **kwargs: Any Returns ------- @@ -329,7 +329,7 @@ def get( self: InverseFourierTransform, image: Image | np.ndarray, padding: int = 32, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> Image | np.ndarray: """Computes the inverse Fourier transform and removes padding. @@ -340,7 +340,7 @@ def get( padding: int, optional Number of pixels removed symmetrically after inverse transformation (default is 32). - **kwargs: dict of str to Any + **kwargs: Any Returns ------- @@ -404,7 +404,7 @@ def get( Tz: np.ndarray, Tzinv: np.ndarray, i: int, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> Image | np.ndarray: """Applies the power of the propagation matrix to the image. @@ -419,7 +419,7 @@ def get( i: int Power of the propagation matrix to apply. Negative values apply the inverse. - **kwargs: dict of str to Any + **kwargs: Any Additional keyword arguments. Returns From addebc90da79b2d77a2906c6cbe4a580fe66c0e1 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 23 Jun 2025 21:36:59 +0200 Subject: [PATCH 018/118] Update features.py --- deeptrack/features.py | 489 ++++++++++++++++++++++++------------------ 1 file changed, 280 insertions(+), 209 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 70a440a2c..e78a03b1d 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -438,7 +438,7 @@ def __init__( | Image | list[Image] ) = [], - **kwargs: dict[str, Any], + **kwargs: Any, ) -> None: """Initialize a new Feature instance. @@ -447,7 +447,7 @@ def __init__( _input: np.ndarray or list[np.ndarray] or torch.Tensor or list[torch.Tensor] or Image or list[Images], optional The initial input(s) for the feature, often images or other data. If not provided, defaults to an empty list. - **kwargs: dict of str to Any + **kwargs: Any Keyword arguments that are wrapped into `Property` instances and stored in `self.properties`, allowing for dynamic or parameterized behavior. @@ -493,7 +493,7 @@ def __init__( def get( self: Feature, image: np.ndarray | list[np.ndarray] | Image | list[Image], - **kwargs: dict[str, Any], + **kwargs: Any, ) -> Image | list[Image]: """Transform an image [abstract method]. @@ -504,7 +504,7 @@ def get( ---------- image: np.ndarray or list of np.ndarray or Image or list of Images The image or list of images to transform. - **kwargs: dict of str to Any + **kwargs: Any The current value of all properties in `properties`, as well as any global arguments passed to the feature. @@ -525,7 +525,9 @@ def get( def __call__( self: Feature, image_list: ( - NDArray[Any] + Feature + | list[Feature] + | NDArray[Any] | list[NDArray[Any]] | torch.Tensor | list[torch.Tensor] @@ -551,7 +553,7 @@ def __call__( image_list: np.ndarrray or list[np.ndarrray] or Image or list of Images, optional The input to the feature or pipeline. If `None`, the feature uses previously set input values or propagates properties. - **kwargs: dict of str to Any + **kwargs: Any Additional parameters passed to the pipeline. These override properties with matching names. For example, calling `feature(x, value=4)` executes `feature` on the input `x` while @@ -1531,7 +1533,7 @@ def _process_output(self): def _image_wrapped_format_input( self: Feature, image_list: np.ndarray | list[np.ndarray] | Image | list[Image], - **kwargs: dict[str, Any], + **kwargs: Any, ) -> list[Image]: """Wraps input data as Image instances before processing. @@ -1548,7 +1550,7 @@ def _image_wrapped_format_input( def _no_wrap_format_input( self: Feature, image_list: np.ndarray | list[np.ndarray] | Image | list[Image], - **kwargs: dict[str, Any], + **kwargs: Any, ) -> list[Image]: """Processes input data without wrapping it as Image instances. @@ -1746,7 +1748,7 @@ class Chain(StructuralFeature): feature_2: Feature The second feature in the chain, which processes the output from `feature_1`. - **kwargs: dict of str to Any, optional + **kwargs: Any, optional Additional keyword arguments passed to the parent `StructuralFeature` (and, therefore, `Feature`). @@ -1793,7 +1795,7 @@ def __init__( self: Feature, feature_1: Feature, feature_2: Feature, - **kwargs: dict[str, Any], + **kwargs: Any, ): """Initialize the chain with two sub-features. @@ -1808,7 +1810,7 @@ def __init__( The first feature to be applied. feature_2: Feature The second feature, applied after `feature_1`. - **kwargs: dict of str to Any, optional + **kwargs: Any, optional Additional keyword arguments passed to the parent constructor (e.g., name, properties). @@ -1823,7 +1825,7 @@ def get( self: Feature, image: np.ndarray | list[np.ndarray] | Image | list[Image], _ID: tuple[int, ...] = (), - **kwargs: dict[str, Any], + **kwargs: Any, ) -> Image | list[Image]: """Apply the two features sequentially to the given input image(s). @@ -1836,9 +1838,9 @@ def get( The input data, which can be an `Image` or a list of `Image` objects, to transform sequentially. _ID: tuple of int, optional - A unique identifier for caching or parallel execution. Defaults to an - empty tuple. - **kwargs: dict of str to Any + A unique identifier for caching or parallel execution. It defaults + to an empty tuple. + **kwargs: Any Additional parameters passed to or sampled by the features. These are generally unused here, as each sub-feature fetches its required properties internally. @@ -1875,7 +1877,7 @@ class DummyFeature(Feature): _input: Any, optional An optional input (typically an image or list of images) that can be set for the feature. It defaults to an empty list []. - **kwargs: dict of str to Any + **kwargs: Any Additional keyword arguments are wrapped as `Property` instances and stored in `self.properties`. @@ -1956,7 +1958,7 @@ class Value(Feature): The numerical value to store. It defaults to 0. If an `Image` is provided, a warning is issued recommending conversion to a NumPy array or a PyTorch tensor for performance reasons. - **kwargs: dict of str to Any + **kwargs: Any Additional named properties passed to the `Feature` constructor. Attributes @@ -2098,7 +2100,7 @@ class ArithmeticOperationFeature(Feature): value: float or int or list of float or int, optional The second operand for the operation. It defaults to 0. If a list is provided, the operation will apply element-wise. - **kwargs: dict of str to Any + **kwargs: Any Additional keyword arguments passed to the parent `Feature`. Attributes @@ -2135,7 +2137,12 @@ class ArithmeticOperationFeature(Feature): def __init__( self: ArithmeticOperationFeature, op: Callable[[Any, Any], Any], - value: PropertyLike[float | int | list[float | int]] = 0, + value: PropertyLike[ + float + | int + | ArrayLike + | list[float | int | ArrayLike] + ] = 0, **kwargs: Any, ): """Initialize the ArithmeticOperationFeature. @@ -2146,10 +2153,10 @@ def __init__( The arithmetic operation to apply, such as `operator.add`, `operator.mul`, or any custom callable that takes two arguments and returns a single output value. - value: PropertyLike[float or int or list of float or int], optional + value: PropertyLike[float or int or array, or list of float or int or array], optional The second operand(s) for the operation. If a list is provided, the operation is applied element-wise. It defaults to 0. - **kwargs: dict of str to Any + **kwargs: Any Additional keyword arguments passed to the parent `Feature` constructor. @@ -2162,7 +2169,7 @@ def __init__( def get( self: ArithmeticOperationFeature, image: Any | list[Any], - value: float | int | list[float | int], + value: float | int | ArrayLike | list[float | int | ArrayLike], **kwargs: Any, ) -> list[Any]: """Apply the operation element-wise to the input data. @@ -2172,11 +2179,11 @@ def get( image: Any or list of Any The input data, either a single value or a list of values, to be transformed by the arithmetic operation. - value: float or int or list of float or int + value: float or int or array, or list of float or int or array The second operand(s) for the operation. If a single value is provided, it is broadcast to match the input size. If a list is provided, it will be cycled to match the length of the input list. - **kwargs: dict of str to Any + **kwargs: Any Additional parameters or property overrides. These are generally unused in this context but provided for compatibility with the `Feature` interface. @@ -2210,9 +2217,9 @@ class Add(ArithmeticOperationFeature): Parameters ---------- - value: PropertyLike[int or float], optional - The value to add to the input. Defaults to 0. - **kwargs: dict of str to Any + value: PropertyLike[int or float or array, or list of int or floar or array], optional + The value to add to the input. It defaults to 0. + **kwargs: Any Additional keyword arguments passed to the parent constructor. Examples @@ -2226,29 +2233,40 @@ class Add(ArithmeticOperationFeature): Alternatively, the pipeline can be created using operator overloading: >>> pipeline = dt.Value([1, 2, 3]) + 5 + >>> pipeline.resolve() + [6, 7, 8] Or: >>> pipeline = 5 + dt.Value([1, 2, 3]) + >>> pipeline.resolve() + [6, 7, 8] Or, more explicitly: >>> input_value = dt.Value([1, 2, 3]) >>> sum_feature = dt.Add(value=5) >>> pipeline = sum_feature(input_value) + >>> pipeline.resolve() + [6, 7, 8] """ def __init__( self: Feature, - value: PropertyLike[float | int | list[float | int]] = 0, + value: PropertyLike[ + float + | int + | ArrayLike[Any] + | list[float | int | ArrayLike[Any]] + ] = 0, **kwargs: Any, ): """Initialize the Add feature. Parameters ---------- - value: PropertyLike[float], optional - The value to add to the input. Defaults to 0. - **kwargs: dict of str to Any + value: PropertyLike[float or int or array, or list of float or int or array], optional + The value to add to the input. It defaults to 0. + **kwargs: Any Additional keyword arguments passed to the parent `Feature`. """ @@ -2263,9 +2281,9 @@ class Subtract(ArithmeticOperationFeature): Parameters ---------- - value: PropertyLike[int or float], optional - The value to subtract from the input. Defaults to 0. - **kwargs: dict of str to Any + value: PropertyLike[int or float or array, or list of int or floar or array], optional + The value to subtract from the input. It defaults to 0. + **kwargs: Any Additional keyword arguments passed to the parent constructor. Examples @@ -2292,16 +2310,21 @@ class Subtract(ArithmeticOperationFeature): def __init__( self: Feature, - value: PropertyLike[float] = 0, - **kwargs: dict[str, Any], + value: PropertyLike[ + float + | int + | ArrayLike[Any] + | list[float | int | ArrayLike[Any]] + ] = 0, + **kwargs: Any, ): """Initialize the Subtract feature. Parameters ---------- - value: PropertyLike[float], optional - The value to subtract from the input. Defaults to 0. - **kwargs: dict of str to Any + value: PropertyLike[float or int or array, or list of float or int or array], optional + The value to subtract from the input. it defaults to 0. + **kwargs: Any Additional keyword arguments passed to the parent `Feature`. """ @@ -2316,8 +2339,8 @@ class Multiply(ArithmeticOperationFeature): Parameters ---------- - value: PropertyLike[int or float], optional - The value to multiply the input. Defaults to 0. + value: PropertyLike[int or float or array, or list of int or floar or array], optional + The value to multiply the input. It defaults to 0. **kwargs: Any Additional keyword arguments passed to the parent constructor. @@ -2345,15 +2368,20 @@ class Multiply(ArithmeticOperationFeature): def __init__( self: Feature, - value: PropertyLike[float] = 0, - **kwargs: dict[str, Any], + value: PropertyLike[ + float + | int + | ArrayLike[Any] + | list[float | int | ArrayLike[Any]] + ] = 0, + **kwargs: Any, ): """Initialize the Multiply feature. Parameters ---------- value: PropertyLike[float], optional - The value to multiply the input. Defaults to 0. + The value to multiply the input. It defaults to 0. **kwargs: Any Additional keyword arguments. @@ -2369,8 +2397,8 @@ class Divide(ArithmeticOperationFeature): Parameters ---------- - value: PropertyLike[int or float], optional - The value to divide the input. Defaults to 0. + value: PropertyLike[int or float or array, or list of int or floar or array], optional + The value to divide the input. It defaults to 0. **kwargs: Any Additional keyword arguments passed to the parent constructor. @@ -2398,15 +2426,20 @@ class Divide(ArithmeticOperationFeature): def __init__( self: Feature, - value: PropertyLike[float] = 0, - **kwargs: dict[str, Any], + value: PropertyLike[ + float + | int + | ArrayLike[Any] + | list[float | int | ArrayLike[Any]] + ] = 0, + **kwargs: Any, ): """Initialize the Divide feature. Parameters ---------- - value: PropertyLike[float], optional - The value to divide the input. Defaults to 0. + value: PropertyLike[float or int or array, or list of float or int or array], optional + The value to divide the input. It defaults to 0. **kwargs: Any Additional keyword arguments. @@ -2426,8 +2459,8 @@ class FloorDivide(ArithmeticOperationFeature): Parameters ---------- - value: PropertyLike[int or float], optional - The value to floor-divide the input. Defaults to 0. + value: PropertyLike[int or float or array, or list of int or floar or array], optional + The value to floor-divide the input. It defaults to 0. **kwargs: Any Additional keyword arguments passed to the parent constructor. @@ -2455,15 +2488,20 @@ class FloorDivide(ArithmeticOperationFeature): def __init__( self: Feature, - value: PropertyLike[float] = 0, - **kwargs: dict[str, Any], + value: PropertyLike[ + float + | int + | ArrayLike[Any] + | list[float | int | ArrayLike[Any]] + ] = 0, + **kwargs: Any, ): """Initialize the FloorDivide feature. Parameters ---------- - value: PropertyLike[float], optional - The value to fllor-divide the input. Defaults to 0. + value: PropertyLike[float or int or array, or list of float or int or array], optional + The value to fllor-divide the input. It defaults to 0. **kwargs: Any Additional keyword arguments. @@ -2479,8 +2517,8 @@ class Power(ArithmeticOperationFeature): Parameters ---------- - value: PropertyLike[int or float], optional - The value to take the power of the input. Defaults to 0. + value: PropertyLike[int or float or array, or list of int or floar or array], optional + The value to take the power of the input. It defaults to 0. **kwargs: Any Additional keyword arguments passed to the parent constructor. @@ -2508,15 +2546,20 @@ class Power(ArithmeticOperationFeature): def __init__( self: Feature, - value: PropertyLike[float] = 0, - **kwargs: dict[str, Any], + value: PropertyLike[ + float + | int + | ArrayLike[Any] + | list[float | int | ArrayLike[Any]] + ] = 0, + **kwargs: Any, ): """Initialize the Power feature. Parameters ---------- - value: PropertyLike[float], optional - The value to take the power of the input. Defaults to 0. + value: PropertyLike[float or int or array, or list of float or int or array], optional + The value to take the power of the input. It defaults to 0. **kwargs: Any Additional keyword arguments. @@ -2532,8 +2575,8 @@ class LessThan(ArithmeticOperationFeature): Parameters ---------- - value: PropertyLike[int or float], optional - The value to compare (<) with the input. Defaults to 0. + value: PropertyLike[int or float or array, or list of int or floar or array], optional + The value to compare (<) with the input. It defaults to 0. **kwargs: Any Additional keyword arguments passed to the parent constructor. @@ -2561,15 +2604,20 @@ class LessThan(ArithmeticOperationFeature): def __init__( self: Feature, - value: PropertyLike[float] = 0, - **kwargs: dict[str, Any], + value: PropertyLike[ + float + | int + | ArrayLike[Any] + | list[float | int | ArrayLike[Any]] + ] = 0, + **kwargs: Any, ): """Initialize the LessThan feature. Parameters ---------- - value: PropertyLike[float], optional - The value to compare (<) with the input. Defaults to 0. + value: PropertyLike[float or int or array, or list of float or int or array], optional + The value to compare (<) with the input. It defaults to 0. **kwargs: Any Additional keyword arguments. @@ -2585,8 +2633,8 @@ class LessThanOrEquals(ArithmeticOperationFeature): Parameters ---------- - value: PropertyLike[int or float], optional - The value to compare (<=) with the input. Defaults to 0. + value: PropertyLike[int or float or array, or list of int or floar or array], optional + The value to compare (<=) with the input. It defaults to 0. **kwargs: Any Additional keyword arguments passed to the parent constructor. @@ -2614,15 +2662,20 @@ class LessThanOrEquals(ArithmeticOperationFeature): def __init__( self: Feature, - value: PropertyLike[float] = 0, - **kwargs: dict[str, Any], + value: PropertyLike[ + float + | int + | ArrayLike[Any] + | list[float | int | ArrayLike[Any]] + ] = 0, + **kwargs: Any, ): """Initialize the LessThanOrEquals feature. Parameters ---------- - value: PropertyLike[float], optional - The value to compare (<=) with the input. Defaults to 0. + value: PropertyLike[float or int or array, or list of float or int or array], optional + The value to compare (<=) with the input. It defaults to 0. **kwargs: Any Additional keyword arguments. @@ -2641,8 +2694,8 @@ class GreaterThan(ArithmeticOperationFeature): Parameters ---------- - value: PropertyLike[int or float], optional - The value to compare (>) with the input. Defaults to 0. + value: PropertyLike[int or float or array, or list of int or floar or array], optional + The value to compare (>) with the input. It defaults to 0. **kwargs: Any Additional keyword arguments passed to the parent constructor. @@ -2670,15 +2723,20 @@ class GreaterThan(ArithmeticOperationFeature): def __init__( self: Feature, - value: PropertyLike[float] = 0, - **kwargs: dict[str, Any], + value: PropertyLike[ + float + | int + | ArrayLike[Any] + | list[float | int | ArrayLike[Any]] + ] = 0, + **kwargs: Any, ): """Initialize the GreaterThan feature. Parameters ---------- - value: PropertyLike[float], optional - The value to compare (>) with the input. Defaults to 0. + value: PropertyLike[float or int or array, or list of float or int or array], optional + The value to compare (>) with the input. It defaults to 0. **kwargs: Any Additional keyword arguments. @@ -2694,8 +2752,8 @@ class GreaterThanOrEquals(ArithmeticOperationFeature): Parameters ---------- - value: PropertyLike[int or float], optional - The value to compare (<=) with the input. Defaults to 0. + value: PropertyLike[int or float or array, or list of int or floar or array], optional + The value to compare (<=) with the input. It defaults to 0. **kwargs: Any Additional keyword arguments passed to the parent constructor. @@ -2723,15 +2781,20 @@ class GreaterThanOrEquals(ArithmeticOperationFeature): def __init__( self: Feature, - value: PropertyLike[float] = 0, - **kwargs: dict[str, Any], + value: PropertyLike[ + float + | int + | ArrayLike[Any] + | list[float | int | ArrayLike[Any]] + ] = 0, + **kwargs: Any, ): """Initialize the GreaterThanOrEquals feature. Parameters ---------- - value: PropertyLike[float], optional - The value to compare (>=) with the input. Defaults to 0. + value: PropertyLike[float or int or array, or list of float or int or array], optional + The value to compare (>=) with the input. It defaults to 0. **kwargs: Any Additional keyword arguments. @@ -2751,8 +2814,8 @@ class Equals(ArithmeticOperationFeature): Parameters ---------- - value: PropertyLike[int or float], optional - The value to compare (==) with the input. Defaults to 0. + value: PropertyLike[int or float or array, or list of int or floar or array], optional + The value to compare (==) with the input. It defaults to 0. **kwargs: Any Additional keyword arguments passed to the parent constructor. @@ -2794,15 +2857,20 @@ class Equals(ArithmeticOperationFeature): def __init__( self: Feature, - value: PropertyLike[float] = 0, - **kwargs: dict[str, Any], + value: PropertyLike[ + float + | int + | ArrayLike[Any] + | list[float | int | ArrayLike[Any]] + ] = 0, + **kwargs: Any, ): """Initialize the Equals feature. Parameters ---------- - value: PropertyLike[float], optional - The value to compare (==) with the input. Defaults to 0. + value: PropertyLike[float or int or array, or list of float or int or array], optional + The value to compare (==) with the input. It defaults to 0. **kwargs: Any Additional keyword arguments. @@ -2833,7 +2901,7 @@ class Stack(Feature): ---------- value: PropertyLike[Any] The feature or data to stack with the input. - **kwargs: dict of str to Any + **kwargs: Any Additional arguments passed to the parent `Feature` class. Attributes @@ -2869,7 +2937,7 @@ class Stack(Feature): def __init__( self: Feature, value: PropertyLike[Any], - **kwargs: dict[str, Any], + **kwargs: Any, ): """Initialize the Stack feature. @@ -2877,7 +2945,7 @@ def __init__( ---------- value: PropertyLike[Any] The feature or data to stack with the input. - **kwargs: dict of str to Any + **kwargs: Any Additional arguments passed to the parent `Feature` class. """ @@ -2888,7 +2956,7 @@ def get( self: Feature, image: Any | list[Any], value: Any | list[Any], - **kwargs: dict[str, Any], + **kwargs: Any, ) -> list[Any]: """Concatenate the input with the value. @@ -2902,7 +2970,7 @@ def get( value: Any or list[Any] The feature or data to stack with the input. Can be a single element or a list. - **kwargs: dict of str to Any + **kwargs: Any Additional keyword arguments (not used here). Returns @@ -3072,7 +3140,7 @@ class Probability(StructuralFeature): The probability (between 0 and 1) of resolving the feature. *args: list[Any], optional Positional arguments passed to the parent `StructuralFeature` class. - **kwargs: dict of str to Any, optional + **kwargs: Any, optional Additional keyword arguments passed to the parent `StructuralFeature` class. @@ -3105,7 +3173,7 @@ def __init__( feature: Feature, probability: PropertyLike[float], *args: list[Any], - **kwargs: dict[str, Any], + **kwargs: Any, ): """Initialize the Probability feature. @@ -3117,7 +3185,7 @@ def __init__( The probability (between 0 and 1) of resolving the feature. *args: list[Any], optional Positional arguments passed to the parent `StructuralFeature` class. - **kwargs: dict of str to Any, optional + **kwargs: Any, optional Additional keyword arguments passed to the parent `StructuralFeature` class. """ @@ -3135,7 +3203,7 @@ def get( image: np.ndarray, probability: float, random_number: float, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> np.ndarray: """Resolve the feature if a random number is less than the probability. @@ -3148,7 +3216,7 @@ def get( random_number: float A random number sampled to determine whether to resolve the feature. - **kwargs: dict of str to Any + **kwargs: Any Additional arguments passed to the feature's `resolve` method. Returns @@ -3184,7 +3252,7 @@ class Repeat(Feature): The feature to be repeated. N: int The number of times to apply the feature in sequence. - **kwargs: dict of str to Any + **kwargs: Any Attributes ---------- @@ -3230,7 +3298,7 @@ def __init__( self: Feature, feature: Feature, N: int, - **kwargs: dict[str, Any], + **kwargs: Any, ): """Initialize the Repeat feature. @@ -3246,7 +3314,7 @@ def __init__( N: int The number of times to sequentially apply `feature`, passing the output of each iteration as the input to the next. - **kwargs: dict of str to Any + **kwargs: Any Keyword arguments that override properties dynamically at each iteration and are also passed to the parent `Feature` class. @@ -3260,7 +3328,7 @@ def get( image: Any, N: int, _ID: tuple[int, ...] = (), - **kwargs: dict[str, Any], + **kwargs: Any, ) -> Any: """Sequentially apply the feature `N` times. @@ -3278,7 +3346,7 @@ def get( _ID: tuple[int, ...], optional A unique identifier for tracking the iteration index, ensuring reproducibility, caching, and dynamic property updates. - **kwargs: dict of str to Any + **kwargs: Any Additional keyword arguments passed to the feature. Returns @@ -3314,7 +3382,7 @@ class Combine(StructuralFeature): features: list of Features A list of features to combine. Each feature will be resolved in the order they appear in the list. - **kwargs: dict of str to Any, optional + **kwargs: Any, optional Additional keyword arguments passed to the parent `StructuralFeature` class. @@ -3358,7 +3426,7 @@ def __init__( features: list of Features A list of features to combine. Each feature is added as a dependency to ensure proper execution in the computation graph. - **kwargs: dict of str to Any, optional + **kwargs: Any, optional Additional keyword arguments passed to the parent `StructuralFeature` class. @@ -3378,7 +3446,7 @@ def get( ---------- image_list: Any The input image or list of images to process. - **kwargs: dict of str to Any + **kwargs: Any Additional arguments passed to each feature's `resolve` method. Returns @@ -3404,7 +3472,7 @@ class Slice(Feature): slices: Iterable[int | slice | ...] The slicing instructions for each dimension. Each element corresponds to a dimension in the input image. - **kwargs: dict of str to Any + **kwargs: Any Additional keyword arguments passed to the parent `Feature` class. Methods @@ -3444,7 +3512,7 @@ def __init__( PropertyLike[int] | PropertyLike[slice] | PropertyLike[...] ] ], - **kwargs: dict[str, Any], + **kwargs: Any, ): """Initialize the Slice feature. @@ -3453,7 +3521,7 @@ def __init__( slices: list[int | slice | ...] or tuple[int | slice | ...] The slicing instructions for each dimension, specified as a list or tuple of integers, slice objects, or ellipses (`...`). - **kwargs: dict of str to Any + **kwargs: Any Additional keyword arguments passed to the parent `Feature` class. """ @@ -3464,7 +3532,7 @@ def get( self: Feature, image: np.ndarray, slices: tuple[Any, ...] | Any, - **kwargs: dict[str, Any], + **kwargs: Any, ): """Apply the specified slices to the input image. @@ -3476,7 +3544,7 @@ def get( The slicing instructions for the input image. Each element in the tuple corresponds to a dimension in the input image. If a single element is provided, it is converted to a tuple. - **kwargs: dict of str to Any + **kwargs: Any Additional keyword arguments (unused in this implementation). Returns @@ -3508,7 +3576,7 @@ class Bind(StructuralFeature): ---------- feature: Feature The child feature - **kwargs: dict of str to Any + **kwargs: Any Properties to send to child Methods @@ -3547,7 +3615,7 @@ def __init__( ---------- feature: Feature The child feature to bind. - **kwargs: dict of str to Any + **kwargs: Any Properties or arguments to pass to the child feature. """ @@ -3566,7 +3634,7 @@ def get( ---------- image: Any The input data or image to process. - **kwargs: dict of str to Any + **kwargs: Any Properties or arguments to pass to the child feature during resolution. @@ -3595,7 +3663,7 @@ class BindUpdate(StructuralFeature): ---------- feature: Feature The child feature to bind with specific arguments. - **kwargs: dict of str to Any + **kwargs: Any Properties to send to the child feature during updates. Methods @@ -3644,7 +3712,7 @@ def __init__( ---------- feature: Feature The child feature to bind with specific arguments. - **kwargs: dict of str to Any + **kwargs: Any Properties to send to the child feature during updates. Warnings @@ -3677,7 +3745,7 @@ def get( ---------- image: Any The input data or image to process. - **kwargs: dict of str to Any + **kwargs: Any Properties or arguments to pass to the child feature during resolution. @@ -3784,7 +3852,7 @@ def __init__( self: Feature, feature: Feature, condition: PropertyLike[str | bool] | None = None, - **kwargs: dict[str, Any], + **kwargs: Any, ): """Initialize the ConditionalSetProperty feature. @@ -3796,7 +3864,7 @@ def __init__( A boolean value or the name of a boolean property in the feature's property dictionary. If the condition evaluates to `True`, the specified properties are applied. - **kwargs: dict of str to Any + **kwargs: Any Properties to apply to the child feature if the condition is `True`. @@ -3812,7 +3880,7 @@ def get( self: Feature, image: Any, condition: str | bool, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> Any: """Resolve the child, conditionally applying specified properties. @@ -3878,7 +3946,7 @@ class ConditionalSetFeature(StructuralFeature): The name of the conditional property or a boolean value. If a string is provided, its value is retrieved from `kwargs` or `self.properties`. If not found, the default value is `True`. - **kwargs: dict of str to Any + **kwargs: Any Additional keyword arguments passed to the parent `StructuralFeature`. Attributes @@ -3950,7 +4018,7 @@ def __init__( on_false: Feature | None = None, on_true: Feature | None = None, condition: PropertyLike[str | bool] = True, - **kwargs: dict[str, Any], + **kwargs: Any, ): """Initialize the ConditionalSetFeature. @@ -3961,8 +4029,8 @@ def __init__( on_true: Feature, optional The feature to resolve if the condition evaluates to `True`. condition: str or bool, optional - The name of the property to listen to, or a boolean value. Defaults - to `"is_label"`. + The name of the property to listen to, or a boolean value. It + defaults to `"is_label"`. **kwargs:: dict of str to Any Additional keyword arguments for the parent `StructuralFeature`. @@ -3987,7 +4055,7 @@ def get( image: Any, *, condition: str | bool, - **kwargs: dict[str, Any], + **kwargs: Any, ): """Resolve the appropriate feature based on the condition. @@ -4080,7 +4148,7 @@ class Lambda(Feature): def __init__( self: Feature, function: Callable[..., Callable[[Image], Image]], - **kwargs: dict[str, Any], + **kwargs: Any, ): """Initialize the Lambda feature. @@ -4105,7 +4173,7 @@ def get( self: Feature, image: np.ndarray | Image, function: Callable[[Image], Image], - **kwargs: dict[str, Any], + **kwargs: Any, ) -> Image: """Apply the custom function to the input image. @@ -4120,7 +4188,7 @@ def get( function: Callable[[Image], Image] A callable function that takes an image and returns a transformed image. - **kwargs: dict of str to Any + **kwargs: Any Additional keyword arguments (unused in this implementation). Returns @@ -4221,7 +4289,7 @@ def get( self: Feature, list_of_images: list[np.ndarray] | list[Image], function: Callable[[list[np.ndarray] | list[Image]], np.ndarray | list[np.ndarray] | Image | list[Image]], - **kwargs: dict[str, Any], + **kwargs: Any, ) -> Image | list[Image]: """Apply the custom function to a list of images. @@ -4264,7 +4332,7 @@ class OneOf(Feature): key: int | None, optional The index of the feature to resolve from the collection. If not provided, a feature is selected randomly at each execution. - **kwargs: dict of str to Any + **kwargs: Any Additional keyword arguments passed to the parent `Feature` class. Attributes @@ -4309,7 +4377,7 @@ def __init__( self: Feature, collection: Iterable[Feature], key: int | None = None, - **kwargs: dict[str, Any], + **kwargs: Any, ): """Initialize the OneOf feature. @@ -4320,7 +4388,7 @@ def __init__( key: int | None, optional The index of the feature to resolve from the collection. If not provided, a feature is selected randomly at execution. - **kwargs: dict of str to Any + **kwargs: Any Additional keyword arguments passed to the parent `Feature` class. """ @@ -4365,7 +4433,7 @@ def get( image: Any, key: int, _ID: tuple[int, ...] = (), - **kwargs: dict[str, Any], + **kwargs: Any, ) -> Any: """Apply the selected feature to the input image. @@ -4377,7 +4445,7 @@ def get( The index of the feature to apply from the collection. _ID: tuple[int, ...], optional A unique identifier for caching and parallel processing. - **kwargs: dict of str to Any + **kwargs: Any Additional parameters passed to the selected feature. Returns @@ -4408,7 +4476,7 @@ class OneOfDict(Feature): key: Any | None, optional The key of the feature to resolve from the dictionary. If `None`, a random key is selected. - **kwargs: dict of str to Any + **kwargs: Any Additional parameters passed to the parent `Feature` class. Attributes @@ -4453,7 +4521,7 @@ def __init__( self: Feature, collection: dict[Any, Feature], key: Any | None = None, - **kwargs: dict[str, Any], + **kwargs: Any, ): """Initialize the OneOfDict feature. @@ -4464,7 +4532,7 @@ def __init__( key: Any | None, optional The key of the feature to resolve from the dictionary. If `None`, a random key is selected. - **kwargs: dict of str to Any + **kwargs: Any Additional parameters passed to the parent `Feature` class. """ @@ -4509,7 +4577,7 @@ def get( image: Any, key: Any, _ID: tuple[int, ...] = (), - **kwargs: dict[str, Any], + **kwargs: Any, )-> Any: """Resolve the selected feature and apply it to the input. @@ -4521,7 +4589,7 @@ def get( The key of the feature to apply from the dictionary. _ID: tuple[int, ...], optional A unique identifier for caching and parallel execution. - **kwargs: dict of str to Any + **kwargs: Any Additional parameters passed to the selected feature. Returns @@ -4549,17 +4617,18 @@ class LoadImage(Feature): The path(s) to the image(s) to load. Can be a single string or a list of strings. load_options: PropertyLike[dict[str, Any]], optional - Additional options passed to the file reader. Defaults to `None`. + Additional options passed to the file reader. It defaults to `None`. as_list: PropertyLike[bool], optional If `True`, the first dimension of the image will be treated as a list. - Defaults to `False`. + It defaults to `False`. ndim: PropertyLike[int], optional - Ensures the image has at least this many dimensions. Defaults to `3`. + Ensures the image has at least this many dimensions. It defaults to + `3`. to_grayscale: PropertyLike[bool], optional - If `True`, converts the image to grayscale. Defaults to `False`. + If `True`, converts the image to grayscale. It defaults to `False`. get_one_random: PropertyLike[bool], optional If `True`, extracts a single random image from a stack of images. Only - used when `as_list` is `True`. Defaults to `False`. + used when `as_list` is `True`. It defaults to `False`. Attributes ---------- @@ -4612,7 +4681,7 @@ def __init__( ndim: PropertyLike[int] = 3, to_grayscale: PropertyLike[bool] = False, get_one_random: PropertyLike[bool] = False, - **kwargs: dict[str, Any], + **kwargs: Any, ): """Initialize the LoadImage feature. @@ -4623,19 +4692,20 @@ def __init__( of strings. load_options: PropertyLike[dict[str, Any]], optional Additional options passed to the file reader (e.g., `mode` for OpenCV, - `allow_pickle` for NumPy). Defaults to `None`. + `allow_pickle` for NumPy). It defaults to `None`. as_list: PropertyLike[bool], optional If `True`, treats the first dimension of the image as a list of images. - Defaults to `False`. + It defaults to `False`. ndim: PropertyLike[int], optional Ensures the image has at least this many dimensions. If the loaded image - has fewer dimensions, extra dimensions are added. Defaults to `3`. + has fewer dimensions, extra dimensions are added. It defaults to + `3`. to_grayscale: PropertyLike[bool], optional - If `True`, converts the image to grayscale. Defaults to `False`. + If `True`, converts the image to grayscale. It defaults to `False`. get_one_random: PropertyLike[bool], optional If `True`, selects a single random image from a stack when `as_list=True`. - Defaults to `False`. - **kwargs: dict of str to Any + It defaults to `False`. + **kwargs: Any Additional keyword arguments passed to the parent `Feature` class, allowing further customization. @@ -4660,7 +4730,7 @@ def get( to_grayscale: bool, as_list: bool, get_one_random: bool, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> np.ndarray: """Load and process an image or a list of images from disk. @@ -4677,18 +4747,18 @@ def get( loads one image, while a list of paths loads multiple images. load_options: dict of str to Any, optional Additional options passed to the file reader (e.g., `allow_pickle` - for NumPy, `mode` for OpenCV). Defaults to `None`. + for NumPy, `mode` for OpenCV). It defaults to `None`. ndim: int Ensures the image has at least this many dimensions. If the loaded image has fewer dimensions, extra dimensions are added. to_grayscale: bool - If `True`, converts the image to grayscale. Defaults to `False`. + If `True`, converts the image to grayscale. It defaults to `False`. as_list: bool If `True`, treats the first dimension as a list of images instead of stacking them into a NumPy array. get_one_random: bool If `True`, selects a single random image from a multi-frame stack - when `as_list=True`. Defaults to `False`. + when `as_list=True`. It defaults to `False`. **kwargs: dict[str, Any] Additional keyword arguments. @@ -4892,7 +4962,7 @@ def get( self: Feature, image: np.ndarray | Image, transformation_function: Callable[[Image], Image], - **kwargs: dict[str, Any], + **kwargs: Any, ) -> Image: """Apply the transformation function to a single image. @@ -4917,7 +4987,7 @@ def get( def _process_and_get( self: Feature, images: list[np.ndarray] | np.ndarray | list[Image] | Image, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> Image | np.ndarray: """Process a list of images and generate a multi-layer mask. @@ -5072,7 +5142,7 @@ class AsType(Feature): Parameters ---------- dtype: PropertyLike[Any], optional - The desired data type for the image. Defaults to `"float64"`. + The desired data type for the image. It defaults to `"float64"`. **kwargs:: dict of str to Any Additional keyword arguments passed to the parent `Feature` class. @@ -5104,7 +5174,7 @@ class AsType(Feature): def __init__( self: Feature, dtype: PropertyLike[Any] = "float64", - **kwargs: dict[str, Any], + **kwargs: Any, ): """ Initialize the AsType feature. @@ -5112,7 +5182,7 @@ def __init__( Parameters ---------- dtype: PropertyLike[Any], optional - The desired data type for the image. Defaults to `"float64"`. + The desired data type for the image. It defaults to `"float64"`. **kwargs:: dict of str to Any Additional keyword arguments passed to the parent `Feature` class. @@ -5124,7 +5194,7 @@ def get( self: Feature, image: np.ndarray, dtype: str, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> np.ndarray: """Convert the data type of the input image. @@ -5158,7 +5228,7 @@ class ChannelFirst2d(Feature): Parameters ---------- axis: int, optional - The axis to move to the first position. Defaults to `-1` (last axis). + The axis to move to the first position. It defaults to `-1` (last axis). **kwargs:: dict of str to Any Additional keyword arguments passed to the parent `Feature` class. @@ -5198,7 +5268,7 @@ class ChannelFirst2d(Feature): def __init__( self: Feature, axis: int = -1, - **kwargs: dict[str, Any], + **kwargs: Any, ): """Initialize the ChannelFirst2d feature. @@ -5206,7 +5276,7 @@ def __init__( ---------- axis: int, optional The axis to move to the first position. - Defaults to `-1` (last axis). + It defaults to `-1` (last axis). **kwargs:: dict of str to Any Additional keyword arguments passed to the parent `Feature` class. @@ -5218,7 +5288,7 @@ def get( self: Feature, image: np.ndarray, axis: int, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> np.ndarray: """Rearrange the axes of an image to channel-first format. @@ -5280,8 +5350,8 @@ class Upscale(Feature): factor: int or tuple[int, int, int], optional The factor by which to upscale the simulation. If a single integer is provided, it is applied uniformly across all axes. If a tuple of three - integers is provided, each axis is scaled individually. Defaults to 1. - **kwargs: dict of str to Any + integers is provided, each axis is scaled individually. It defaults to 1. + **kwargs: Any Additional keyword arguments passed to the parent `Feature` class. Attributes @@ -5346,7 +5416,7 @@ def __init__( self: Feature, feature: Feature, factor: int | tuple[int, int, int] = 1, - **kwargs: dict[str, Any], + **kwargs: Any, ): """Initialize the Upscale feature. @@ -5358,8 +5428,8 @@ def __init__( The factor by which to upscale the simulation. If a single integer is provided, it is applied uniformly across all axes. If a tuple of three integers is provided, each axis is scaled individually. - Defaults to `1`. - **kwargs: dict of str to Any + It defaults to `1`. + **kwargs: Any Additional keyword arguments passed to the parent `Feature` class. """ @@ -5371,7 +5441,7 @@ def get( self: Feature, image: np.ndarray, factor: int | tuple[int, int, int], - **kwargs: dict[str, Any], + **kwargs: Any, ) -> np.ndarray: """Simulate the pipeline at a higher resolution and return result. @@ -5383,7 +5453,7 @@ def get( The factor by which to upscale the simulation. If a single integer is provided, it is applied uniformly across all axes. If a tuple of three integers is provided, each axis is scaled individually. - **kwargs: dict of str to Any + **kwargs: Any Additional keyword arguments passed to the feature. Returns @@ -5443,14 +5513,14 @@ class NonOverlapping(Feature): The feature that generates the list of volumes to place non-overlapping. min_distance: float, optional - The minimum distance between volumes in pixels. Defaults to `1`. + The minimum distance between volumes in pixels. It defaults to `1`. It can be negative to allow for partial overlap. max_attempts: int, optional The maximum number of attempts to place volumes without overlap. - Defaults to `5`. + It defaults to `5`. max_iters: int, optional The maximum number of resamplings. If this number is exceeded, a - new list of volumes is generated. Defaults to `100`. + new list of volumes is generated. It defaults to `100`. Attributes ---------- @@ -5564,7 +5634,7 @@ def __init__( min_distance: float = 1, max_attempts: int = 5, max_iters: int = 100, - **kwargs: dict[str, Any], + **kwargs: Any, ): """Initializes the NonOverlapping feature. @@ -5578,13 +5648,14 @@ def __init__( The feature that generates the list of volumes. min_distance: float, optional The minimum separation distance **between volume edges**, in - pixels. Defaults to `1`. Negative values allow for partial overlap. + pixels. It defaults to `1`. Negative values allow for partial + overlap. max_attempts: int, optional The maximum number of attempts to place the volumes without - overlap. Defaults to `5`. + overlap. It defaults to `5`. max_iters: int, optional The maximum number of resampling iterations per attempt. If - exceeded, a new list of volumes is generated. Defaults to `100`. + exceeded, a new list of volumes is generated. It defaults to `100`. """ @@ -5601,7 +5672,7 @@ def get( min_distance: float, max_attempts: int, max_iters: int, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> list[np.ndarray]: """Generates a list of non-overlapping 3D volumes within a defined field of view (FOV). @@ -6097,7 +6168,7 @@ class Store(Feature): key: Any The key used to identify the stored output. replace: bool, optional - If `True`, replaces the stored value with a new computation. Defaults + If `True`, replaces the stored value with a new computation. It defaults to `False`. **kwargs:: dict of str to Any Additional keyword arguments passed to the parent `Feature` class. @@ -6149,7 +6220,7 @@ def __init__( feature: Feature, key: Any, replace: bool = False, - **kwargs: dict[str, Any], + **kwargs: Any, ): """Initialize the Store feature. @@ -6161,7 +6232,7 @@ def __init__( The key used to identify the stored output. replace: bool, optional If `True`, replaces the stored value with a new computation. - Defaults to `False`. + It defaults to `False`. **kwargs:: dict of str to Any Additional keyword arguments passed to the parent `Feature` class. @@ -6176,7 +6247,7 @@ def get( _: Any, key: Any, replace: bool, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> Any: """Evaluate and store the feature output, or return the cached result. @@ -6219,7 +6290,7 @@ class Squeeze(Feature): Parameters ---------- axis: int or tuple[int, ...], optional - The axis or axes to squeeze. Defaults to `None`, squeezing all axes. + The axis or axes to squeeze. It defaults to `None`, squeezing all axes. **kwargs:: dict of str to Any Additional keyword arguments passed to the parent `Feature` class. @@ -6255,14 +6326,14 @@ class Squeeze(Feature): def __init__( self: Squeeze, axis: int | tuple[int, ...] | None = None, - **kwargs: dict[str, Any], + **kwargs: Any, ): """Initialize the Squeeze feature. Parameters ---------- axis: int or tuple[int, ...], optional - The axis or axes to squeeze. Defaults to `None`, which squeezes + The axis or axes to squeeze. It defaults to `None`, which squeezes all axes. **kwargs:: dict of str to Any Additional keyword arguments passed to the parent `Feature` class. @@ -6275,7 +6346,7 @@ def get( self: Squeeze, image: np.ndarray, axis: int | tuple[int, ...] | None = None, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> np.ndarray: """Squeeze the input image by removing singleton dimensions. @@ -6284,7 +6355,7 @@ def get( image: np.ndarray The input image to process. axis: int or tuple[int, ...], optional - The axis or axes to squeeze. Defaults to `None`, which squeezes + The axis or axes to squeeze. It defaults to `None`, which squeezes all axes. **kwargs:: dict of str to Any Additional keyword arguments (unused here). @@ -6310,7 +6381,7 @@ class Unsqueeze(Feature): ---------- axis: int or tuple[int, ...], optional The axis or axes where new singleton dimensions should be added. - Defaults to `None`, which adds a singleton dimension at the last axis. + It defaults to `None`, which adds a singleton dimension at the last axis. **kwargs:: dict of str to Any Additional keyword arguments passed to the parent `Feature` class. @@ -6346,7 +6417,7 @@ class Unsqueeze(Feature): def __init__( self: Unsqueeze, axis: int | tuple[int, ...] | None = -1, - **kwargs: dict[str, Any], + **kwargs: Any, ): """Initialize the Unsqueeze feature. @@ -6354,7 +6425,7 @@ def __init__( ---------- axis: int or tuple[int, ...], optional The axis or axes where new singleton dimensions should be added. - Defaults to -1, which adds a singleton dimension at the last axis. + It defaults to -1, which adds a singleton dimension at the last axis. **kwargs:: dict of str to Any Additional keyword arguments passed to the parent `Feature` class. @@ -6366,7 +6437,7 @@ def get( self: Unsqueeze, image: np.ndarray, axis: int | tuple[int, ...] | None = -1, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> np.ndarray: """Add singleton dimensions to the input image. @@ -6377,7 +6448,7 @@ def get( The input image to process. axis: int or tuple[int, ...], optional The axis or axes where new singleton dimensions should be added. - Defaults to -1, which adds a singleton dimension at the last axis. + It defaults to -1, which adds a singleton dimension at the last axis. **kwargs:: dict of str to Any Additional keyword arguments (unused here). @@ -6437,7 +6508,7 @@ def __init__( self: MoveAxis, source: int, destination: int, - **kwargs: dict[str, Any], + **kwargs: Any, ): """Initialize the MoveAxis feature. @@ -6459,7 +6530,7 @@ def get( image: np.ndarray, source: int, destination: int, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> np.ndarray: """Move the specified axis of the input image to a new position. @@ -6530,7 +6601,7 @@ class Transpose(Feature): def __init__( self: Transpose, axes: tuple[int, ...] | None = None, - **kwargs: dict[str, Any], + **kwargs: Any, ): """Initialize the Transpose feature. @@ -6550,7 +6621,7 @@ def get( self: Transpose, image: np.ndarray, axes: tuple[int, ...] | None = None, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> np.ndarray: """Transpose the axes of the input image. @@ -6618,7 +6689,7 @@ class OneHot(Feature): def __init__( self: OneHot, num_classes: int, - **kwargs: dict[str, Any], + **kwargs: Any, ): """Initialize the OneHot feature. @@ -6637,7 +6708,7 @@ def get( self: OneHot, image: np.ndarray, num_classes: int, - **kwargs: dict[str, Any], + **kwargs: Any, ) -> np.ndarray: """Convert the input array of labels into a one-hot encoded array. @@ -6735,7 +6806,7 @@ def __init__( self: TakeProperties, feature: Feature, *names: str, - **kwargs: dict[str, Any], + **kwargs: Any, ): """Initialize the TakeProperties feature. @@ -6745,7 +6816,7 @@ def __init__( The feature from which to extract properties. *names: str One or more names of the properties to extract. -= **kwargs: dict[str, Any], optional += **kwargs: Any, optional Additional keyword arguments passed to the parent `Feature` class. """ @@ -6758,7 +6829,7 @@ def get( image: Any, names: tuple[str, ...], _ID: tuple[int, ...] = (), - **kwargs: dict[str, Any], + **kwargs: Any, ) -> np.ndarray | tuple[np.ndarray, ...]: """Extract the specified properties from the feature pipeline. @@ -6773,8 +6844,8 @@ def get( The names of the properties to extract. _ID: tuple[int, ...], optional A unique identifier for the current computation, ensuring that - dependencies are correctly matched. Defaults to an empty tuple. - **kwargs: dict[str, Any], optional + dependencies are correctly matched. It defaults to an empty tuple. + **kwargs: Any, optional Additional keyword arguments (unused in this method). Returns From 15cdd3c47789e62a5eecb4aa0f79adcd294d67ea Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 23 Jun 2025 21:39:51 +0200 Subject: [PATCH 019/118] Update features.py --- deeptrack/features.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index e78a03b1d..daac37b7c 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -2251,7 +2251,7 @@ class Add(ArithmeticOperationFeature): """ def __init__( - self: Feature, + self: Add, value: PropertyLike[ float | int @@ -2309,7 +2309,7 @@ class Subtract(ArithmeticOperationFeature): """ def __init__( - self: Feature, + self: Subtract, value: PropertyLike[ float | int @@ -2367,7 +2367,7 @@ class Multiply(ArithmeticOperationFeature): """ def __init__( - self: Feature, + self: Multiply, value: PropertyLike[ float | int @@ -2425,7 +2425,7 @@ class Divide(ArithmeticOperationFeature): """ def __init__( - self: Feature, + self: Divide, value: PropertyLike[ float | int @@ -2487,7 +2487,7 @@ class FloorDivide(ArithmeticOperationFeature): """ def __init__( - self: Feature, + self: FloorDivide, value: PropertyLike[ float | int @@ -2545,7 +2545,7 @@ class Power(ArithmeticOperationFeature): """ def __init__( - self: Feature, + self: Power, value: PropertyLike[ float | int @@ -2603,7 +2603,7 @@ class LessThan(ArithmeticOperationFeature): """ def __init__( - self: Feature, + self: LessThan, value: PropertyLike[ float | int @@ -2661,7 +2661,7 @@ class LessThanOrEquals(ArithmeticOperationFeature): """ def __init__( - self: Feature, + self: LessThanOrEquals, value: PropertyLike[ float | int @@ -2722,7 +2722,7 @@ class GreaterThan(ArithmeticOperationFeature): """ def __init__( - self: Feature, + self: GreaterThan, value: PropertyLike[ float | int @@ -2780,7 +2780,7 @@ class GreaterThanOrEquals(ArithmeticOperationFeature): """ def __init__( - self: Feature, + self: GreaterThanOrEquals, value: PropertyLike[ float | int @@ -2856,7 +2856,7 @@ class Equals(ArithmeticOperationFeature): """ def __init__( - self: Feature, + self: Equals, value: PropertyLike[ float | int From ef0676130a0c35853296232b994b9023dad8e5bb Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 24 Jun 2025 09:59:39 +0200 Subject: [PATCH 020/118] Update test_features.py --- deeptrack/tests/test_features.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index 2c39ff882..d967dbcc2 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -859,8 +859,8 @@ def test_GreaterThanOrEquals(self): def test_Equals(self): """ - Notes - ----- + Important Notes + --------------- - Unlike other arithmetic operators, `Equals` does not define `__eq__` (`==`) and `__req__` (`==`) in `DeepTrackNode` and `Feature`, as this would affect Python’s built-in identity comparison. @@ -868,9 +868,8 @@ def test_Equals(self): expressions involving `Feature` instances but not for comparisons involving regular Python objects. - Always use `>>` to apply `Equals` correctly in a feature chain. - """ - + equals_feature = features.Equals(value=2) input_values = np.array([1, 2, 3]) output_values = equals_feature(input_values) From c8b3df67f53663e18dd7f0ccd142503a2e1dae7a Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 24 Jun 2025 09:59:41 +0200 Subject: [PATCH 021/118] Update features.py --- deeptrack/features.py | 152 +++++++++++++++++++++++++++++------------- 1 file changed, 107 insertions(+), 45 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index daac37b7c..840de666a 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -153,20 +153,20 @@ def merge_features( "DummyFeature", "Value", "ArithmeticOperationFeature", - "Add", # TODO - "Subtract", # TODO - "Multiply", # TODO - "Divide", # TODO - "FloorDivide", # TODO - "Power", # TODO - "LessThan", # TODO - "LessThanOrEquals", # TODO - "LessThanOrEqual", # TODO - "GreaterThan", # TODO - "GreaterThanOrEquals", # TODO - "GreaterThanOrEqual", # TODO - "Equals", # TODO - "Equal", # TODO + "Add", + "Subtract", + "Multiply", + "Divide", + "FloorDivide", + "Power", + "LessThan", + "LessThanOrEquals", + "LessThanOrEqual", + "GreaterThan", + "GreaterThanOrEquals", + "GreaterThanOrEqual", + "Equals", + "Equal", "Stack", # TODO "Arguments", # TODO "Probability", # TODO @@ -2297,14 +2297,20 @@ class Subtract(ArithmeticOperationFeature): Alternatively, the pipeline can be created using operator overloading: >>> pipeline = dt.Value([1, 2, 3]) - 2 + >>> pipeline.resolve() + [-1, 0, 1] Or: >>> pipeline = -2 + dt.Value([1, 2, 3]) + >>> pipeline.resolve() + [-1, 0, 1] Or, more explicitly: >>> input_value = dt.Value([1, 2, 3]) >>> sub_feature = dt.Subtract(value=2) >>> pipeline = sub_feature(input_value) + >>> pipeline.resolve() + [-1, 0, 1] """ @@ -2355,14 +2361,20 @@ class Multiply(ArithmeticOperationFeature): Alternatively, this pipeline can be created using: >>> pipeline = dt.Value([1, 2, 3]) * 5 + >>> pipeline.resolve() + [5, 10, 15] Or: >>> pipeline = 5 * dt.Value([1, 2, 3]) + >>> pipeline.resolve() + [5, 10, 15] Or, more explicitly: >>> input_value = dt.Value([1, 2, 3]) >>> mul_feature = dt.Multiply(value=5) >>> pipeline = mul_feature(input_value) + >>> pipeline.resolve() + [5, 10, 15] """ @@ -2380,7 +2392,7 @@ def __init__( Parameters ---------- - value: PropertyLike[float], optional + value: PropertyLike[float or int or array, or list of float or int or array], optional The value to multiply the input. It defaults to 0. **kwargs: Any Additional keyword arguments. @@ -2407,20 +2419,26 @@ class Divide(ArithmeticOperationFeature): >>> import deeptrack as dt Start by creating a pipeline using `Divide`: - >>> pipeline = Value([1, 2, 3]) >> Divide(value=5) + >>> pipeline = dt.Value([1, 2, 3]) >> dt.Divide(value=5) >>> pipeline.resolve() [0.2 0.4 0.6] Equivalently, this pipeline can be created using: - >>> pipeline = Value([1, 2, 3]) / 5 + >>> pipeline = dt.Value([1, 2, 3]) / 5 + >>> pipeline.resolve() + [0.2 0.4 0.6] Which is not equivalent to: - >>> pipeline = 5 / Value([1, 2, 3]) # Different result. + >>> pipeline = 5 / dt.Value([1, 2, 3]) # Different result + >>> pipeline.resolve() + [5.0, 2.5, 1.6666666666666667] Or, more explicitly: - >>> input_value = Value([1, 2, 3]) - >>> truediv_feature = Divide(value=5) + >>> input_value = dt.Value([1, 2, 3]) + >>> truediv_feature = dt.Divide(value=5) >>> pipeline = truediv_feature(input_value) + >>> pipeline.resolve() + [0.2 0.4 0.6] """ @@ -2471,18 +2489,24 @@ class FloorDivide(ArithmeticOperationFeature): Start by creating a pipeline using `FloorDivide`: >>> pipeline = dt.Value([-3, 3, 6]) >> dt.FloorDivide(value=5) >>> pipeline.resolve() - [0.2 0.4 0.6] + [-1, 0, 1] Equivalently, this pipeline can be created using: >>> pipeline = dt.Value([-3, 3, 6]) // 5 + >>> pipeline.resolve() + [-1, 0, 1] Which is not equivalent to: - >>> pipeline = 5 // dt.Value([-3, 3, 6]) # Different result. + >>> pipeline = 5 // dt.Value([-3, 3, 6]) # Different result + >>> pipeline.resolve() + [-2, 1, 0] Or, more explicitly: >>> input_value = dt.Value([-3, 3, 6]) >>> floordiv_feature = dt.FloorDivide(value=5) - >>> pipeline = feature(floordiv_input_value) + >>> pipeline = floordiv_feature(input_value) + >>> pipeline.resolve() + [-1, 0, 1] """ @@ -2533,14 +2557,20 @@ class Power(ArithmeticOperationFeature): Equivalently, this pipeline can be created using: >>> pipeline = dt.Value([1, 2, 3]) ** 3 + >>> pipeline.resolve() + [1, 8, 27] Which is not equivalent to: - >>> pipeline = 3 ** dt.Value([1, 2, 3]) # Different result. + >>> pipeline = 3 ** dt.Value([1, 2, 3]) # Different result + >>> pipeline.resolve() + [3, 9, 27] Or, more explicitly: >>> input_value = dt.Value([1, 2, 3]) - >>> pow_feature = Power(value=3) + >>> pow_feature = dt.Power(value=3) >>> pipeline = pow_feature(input_value) + >>> pipeline.resolve() + [1, 8, 27] """ @@ -2587,18 +2617,24 @@ class LessThan(ArithmeticOperationFeature): Start by creating a pipeline using `LessThan`: >>> pipeline = dt.Value([1, 2, 3]) >> dt.LessThan(value=2) >>> pipeline.resolve() - [True False False] + [True, False, False] Equivalently, this pipeline can be created using: >>> pipeline = dt.Value([1, 2, 3]) < 2 + >>> pipeline.resolve() + [True, False, False] Which is not equivalent to: - >>> pipeline = 2 < dt.Value([1, 2, 3]) # Different result. + >>> pipeline = 2 < dt.Value([1, 2, 3]) # Different result + >>> pipeline.resolve() + [False, False, True] Or, more explicitly: >>> input_value = dt.Value([1, 2, 3]) >>> lt_feature = dt.LessThan(value=2) >>> pipeline = lt_feature(input_value) + >>> pipeline.resolve() + [True, False, False] """ @@ -2645,18 +2681,24 @@ class LessThanOrEquals(ArithmeticOperationFeature): Start by creating a pipeline using `LessThanOrEquals`: >>> pipeline = dt.Value([1, 2, 3]) >> dt.LessThanOrEquals(value=2) >>> pipeline.resolve() - [True True False] + [True, True, False] Equivalently, this pipeline can be created using: >>> pipeline = dt.Value([1, 2, 3]) <= 2 + >>> pipeline.resolve() + [True, True, False] Which is not equivalent to: - >>> pipeline = 2 <= dt.Value([1, 2, 3]) # Different result. + >>> pipeline = 2 <= dt.Value([1, 2, 3]) # Different result + >>> pipeline.resolve() + [False, True, True] Or, more explicitly: >>> input_value = dt.Value([1, 2, 3]) >>> le_feature = dt.LessThanOrEquals(value=2) >>> pipeline = le_feature(input_value) + >>> pipeline.resolve() + [True, True, False] """ @@ -2706,18 +2748,24 @@ class GreaterThan(ArithmeticOperationFeature): Start by creating a pipeline using `GreaterThan`: >>> pipeline = dt.Value([1, 2, 3]) >> dt.GreaterThan(value=2) >>> pipeline.resolve() - [False False True] + [False, False, True] Equivalently, this pipeline can be created using: >>> pipeline = dt.Value([1, 2, 3]) > 2 + >>> pipeline.resolve() + [False, False, True] Which is not equivalent to: - >>> pipeline = 2 > dt.Value([1, 2, 3]) # Different result. + >>> pipeline = 2 > dt.Value([1, 2, 3]) # Different result + >>> pipeline.resolve() + [True, False, False] Or, most explicitly: >>> input_value = dt.Value([1, 2, 3]) >>> gt_feature = dt.GreaterThan(value=2) >>> pipeline = gt_feature(input_value) + >>> pipeline.resolve() + [False, False, True] """ @@ -2764,18 +2812,24 @@ class GreaterThanOrEquals(ArithmeticOperationFeature): Start by creating a pipeline using `GreaterThanOrEquals`: >>> pipeline = dt.Value([1, 2, 3]) >> dt.GreaterThanOrEquals(value=2) >>> pipeline.resolve() - [False True True] + [False, True, True] Equivalently, this pipeline can be created using: >>> pipeline = dt.Value([1, 2, 3]) >= 2 + >>> pipeline.resolve() + [False, True, True] Which is not equivalent to: - >>> pipeline = 2 >= dt.Value([1, 2, 3]) # Different result. + >>> pipeline = 2 >= dt.Value([1, 2, 3]) # Different result + >>> pipeline.resolve() + [True, True, False] Or, more explicitly: >>> input_value = dt.Value([1, 2, 3]) >>> ge_feature = dt.GreaterThanOrEquals(value=2) >>> pipeline = ge_feature(input_value) + >>> pipeline.resolve() + [False, True, True] """ @@ -2809,16 +2863,9 @@ def __init__( class Equals(ArithmeticOperationFeature): """Determine whether input is equal to a given value. - This feature performs element-wise comparison (==) between the input and a + This feature performs element-wise comparison between the input and a specified value. - Parameters - ---------- - value: PropertyLike[int or float or array, or list of int or floar or array], optional - The value to compare (==) with the input. It defaults to 0. - **kwargs: Any - Additional keyword arguments passed to the parent constructor. - Notes ----- - Unlike other arithmetic operators, `Equals` does not define `__eq__` @@ -2828,6 +2875,13 @@ class Equals(ArithmeticOperationFeature): expressions involving `Feature` instances but not for comparisons involving regular Python objects. - Always use `>>` to apply `Equals` correctly in a feature chain. + + Parameters + ---------- + value: PropertyLike[int or float or array, or list of int or floar or array], optional + The value to compare (==) with the input. It defaults to 0. + **kwargs: Any + Additional keyword arguments passed to the parent constructor. Examples -------- @@ -2836,11 +2890,19 @@ class Equals(ArithmeticOperationFeature): Start by creating a pipeline using `Equals`: >>> pipeline = dt.Value([1, 2, 3]) >> dt.Equals(value=2) >>> pipeline.resolve() - [False True False] + [False, True, False] + + Or: + >>> input_values = [1, 2, 3] + >>> eq_feature = dt.Equals(value=2) + >>> output_values = eq_feature(input_values) + >>> print(output_values) + [False, True, False] + + These are the **only correct ways** to apply `Equals` in a pipeline. - This is the **only correct way** to apply `Equals` in a feature pipeline. + The following approaches are **incorrect**: - ### Incorrect Approaches Using `==` directly on a `Feature` instance **does not work** because `Feature` does not override `__eq__`: >>> pipeline = dt.Value([1, 2, 3]) == 2 # Incorrect @@ -2870,7 +2932,7 @@ def __init__( Parameters ---------- value: PropertyLike[float or int or array, or list of float or int or array], optional - The value to compare (==) with the input. It defaults to 0. + The value to compare with the input. It defaults to 0. **kwargs: Any Additional keyword arguments. From 17e39211759dd86185976e12f2af6a59ea46226f Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 24 Jun 2025 10:03:56 +0200 Subject: [PATCH 022/118] Update test_features.py --- deeptrack/tests/test_features.py | 33 ++------------------------------ 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index d967dbcc2..03b689b81 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -159,7 +159,6 @@ def test_Feature_basics(self): self.assertIsInstance(F.properties['prop_str'](), str) self.assertEqual(F.properties['prop_str'](), 'a') - def test_Feature_properties_update(self): feature = features.DummyFeature( @@ -181,7 +180,6 @@ def test_Feature_properties_update(self): prop_dict_with_update = feature.properties() self.assertNotEqual(prop_dict, prop_dict_with_update) - def test_Feature_memorized(self): list_of_inputs = [] @@ -218,7 +216,6 @@ def get(self, input, **kwargs): feature([1]) self.assertEqual(len(list_of_inputs), 4) - def test_Feature_dependence(self): A = features.Value(lambda: np.random.rand()) @@ -262,7 +259,6 @@ def test_Feature_dependence(self): self.assertEqual(D(), C() + B()) self.assertEqual(E(), D() + C()) - def test_Feature_validation(self): class ConcreteFeature(features.Feature): @@ -283,7 +279,6 @@ def get(self, input, **kwargs): feature.prop.set_value(2) # Changes value. self.assertFalse(feature.is_valid()) - def test_Feature_store_properties_in_image(self): class FeatureAddValue(features.Feature): @@ -310,7 +305,6 @@ def get(self, image, value_to_add=0, **kwargs): output_image.get_property("value_to_add", get_one=False), [1, 1] ) - def test_Feature_with_dummy_property(self): class FeatureConcreteClass(features.Feature): @@ -327,7 +321,6 @@ def get(self, *args, **kwargs): output_image.get_property("dummy_property", get_one=False), ["foo"] ) - def test_Feature_plus_1(self): class FeatureAddValue(features.Feature): @@ -350,7 +343,6 @@ def get(self, image, value_to_add=0, **kwargs): output_image.get_property("value_to_add", get_one=True), 1 ) - def test_Feature_plus_2(self): class FeatureAddValue(features.Feature): @@ -377,7 +369,6 @@ def get(self, image, value_to_multiply=0, **kwargs): output_image21 = feature21.resolve(input_image) self.assertEqual(output_image21, 1) - def test_Feature_plus_3(self): class FeatureAppendImageOfShape(features.Feature): @@ -398,7 +389,6 @@ def get(self, *args, shape, **kwargs): self.assertEqual(output_image[0].shape, (1, 1)) self.assertEqual(output_image[1].shape, (2, 2)) - def test_Feature_arithmetic(self): inp = features.DummyFeature() @@ -411,7 +401,6 @@ def test_Feature_arithmetic(self): input_2 = [10, 20] self.assertListEqual(pipeline(input_2), [-input_2[0], -input_2[1]]) - def test_Features_chain_lambda(self): value = features.Value(value=1) @@ -424,7 +413,6 @@ def test_Features_chain_lambda(self): output_image = feature() self.assertEqual(output_image, 2) - def test_Feature_repeat(self): feature = features.Value(value=0) \ @@ -435,7 +423,6 @@ def test_Feature_repeat(self): output_image = feature() self.assertEqual(np.array(output_image), np.array(n)) - def test_Feature_repeat_random(self): feature = features.Value(value=0) >> ( @@ -450,7 +437,6 @@ def test_Feature_repeat_random(self): self.assertNotEqual(num_dups, len(values)) self.assertEqual(output_image, sum(values)) - def test_Feature_repeat_nested(self): value = features.Value(0) @@ -461,7 +447,6 @@ def test_Feature_repeat_nested(self): self.assertEqual(feature(), 15) - def test_Feature_repeat_nested_random_times(self): value = features.Value(0) @@ -476,7 +461,6 @@ def test_Feature_repeat_nested_random_times(self): feature.update() self.assertEqual(feature(), feature.feature_2.N() * 5) - def test_Feature_repeat_nested_random_addition(self): value = features.Value(0) @@ -503,7 +487,6 @@ def test_Feature_repeat_nested_random_addition(self): sum(added_values) - 3 * 4, feature() ) - def test_Feature_nested_Duplicate(self): A = features.DummyFeature( @@ -544,7 +527,6 @@ def test_Feature_nested_Duplicate(self): self.assertIn(c - b, range(0, 100)) self.assertIn(dl[ci] - c, range(0, 10)) - def test_Feature_outside_dependence(self): A = features.DummyFeature( @@ -912,6 +894,7 @@ def test_Stack(self): operator.__and__, ) + def test_Arguments_feature_passing(self): """Tests that arguments are correctly passed and updated in a feature pipeline.""" @@ -952,7 +935,6 @@ def test_Arguments_feature_passing(self): second_d = arguments.d.update()() self.assertNotEqual(first_d, second_d) # Check that values change - def test_Arguments(self): from tempfile import NamedTemporaryFile from PIL import Image as PIL_Image @@ -1131,7 +1113,6 @@ def test_Slice_constant(self): self.assertEqual(a22, input[2, 2]) self.assertEqual(a12, input[1, -1]) - def test_Slice_colon(self): input = np.arange(16).reshape((4, 4)) @@ -1153,7 +1134,6 @@ def test_Slice_colon(self): self.assertEqual(a2.tolist(), input[:, 2].tolist()) self.assertEqual(a3.tolist(), input[0:2, :].tolist()) - def test_Slice_ellipse(self): input = np.arange(16).reshape((4, 4)) @@ -1175,7 +1155,6 @@ def test_Slice_ellipse(self): self.assertEqual(a2.tolist(), input[:, ...].tolist()) self.assertEqual(a3.tolist(), input[0:2, ...].tolist()) - def test_Slice_static_dynamic(self): image = np.arange(27).reshape((3, 3, 3)) expected_output = image[:, 1:2, ::-2] @@ -1216,7 +1195,6 @@ def test_Bind(self): res = pipeline_with_small_input.update(input_value=10).resolve() self.assertEqual(res, 11) - def test_Bind_gaussian_noise(self): # Define the Gaussian noise feature and bind its properties gaussian_noise = Gaussian() @@ -1299,8 +1277,7 @@ def test_BindUpdate(self): res = pipeline_with_small_input.update(input_value=10).resolve() self.assertEqual(res, 11) - - + def test_BindUpdate_gaussian_noise(self): # Define the Gaussian noise feature and bind its properties gaussian_noise = Gaussian() @@ -1356,7 +1333,6 @@ def test_ConditionalSetProperty(self): clean_image = conditional_feature.update()(image, is_noisy=False) self.assertEqual(clean_image.std(), 0) - def test_ConditionalSetFeature(self): """Set up Gaussian noise features and test image before each test.""" @@ -1416,7 +1392,6 @@ def test_Lambda_dependence(self): B.key.set_value("c") self.assertEqual(B.prop(), 3) - def test_Lambda_dependence_twice(self): A = features.DummyFeature(a=1, b=2, c=3) @@ -1436,7 +1411,6 @@ def test_Lambda_dependence_twice(self): B.key.set_value("c") self.assertEqual(B.prop2(), 6) - def test_Lambda_dependence_other_feature(self): A = features.DummyFeature(a=1, b=2, c=3) @@ -1460,7 +1434,6 @@ def test_Lambda_dependence_other_feature(self): B.key.set_value("c") self.assertEqual(C.prop(), 12) - def test_Lambda_scaling(self): def scale_function_factory(scale=2): def scale_function(image): @@ -1571,7 +1544,6 @@ def test_OneOf_list(self): self.assertRaises(IndexError, lambda: values.update().resolve(key=3)) - def test_OneOf_tuple(self): values = features.OneOf( @@ -1844,7 +1816,6 @@ def test_Upscale(self): "The upscaled image should be similar to the original within a tolerance") - def test_NonOverlapping_resample_volume_position(self): nonOverlapping = features.NonOverlapping( From 83a3566da93bdff99eca02167734d9338c29e21f Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 24 Jun 2025 10:13:13 +0200 Subject: [PATCH 023/118] Update features.py --- deeptrack/features.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 840de666a..e921a24c3 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -2983,21 +2983,25 @@ class Stack(Feature): Start by creating a pipeline using `Stack`: >>> pipeline = dt.Value([1, 2, 3]) >> dt.Stack(value=[4, 5]) - >>> print(pipeline.resolve()) + >>> pipeline.resolve() [1, 2, 3, 4, 5] Equivalently, this pipeline can be created using: >>> pipeline = dt.Value([1, 2, 3]) & [4, 5] + >>> pipeline.resolve() + [1, 2, 3, 4, 5] Or: - >>> pipeline = [4, 5] & dt.Value([1, 2, 3]) # Different result. + >>> pipeline = [4, 5] & dt.Value([1, 2, 3]) # Different result + >>> pipeline.resolve() + [4, 5, 1, 2, 3] """ __distributed__: bool = False def __init__( - self: Feature, + self: Stack, value: PropertyLike[Any], **kwargs: Any, ): @@ -3015,7 +3019,7 @@ def __init__( super().__init__(value=value, **kwargs) def get( - self: Feature, + self: Stack, image: Any | list[Any], value: Any | list[Any], **kwargs: Any, From 158f247db7516e5653f862050d0e6f50b00c88d0 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 24 Jun 2025 10:13:15 +0200 Subject: [PATCH 024/118] Update test_features.py --- deeptrack/tests/test_features.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index 03b689b81..70ac5a12c 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -888,8 +888,16 @@ def test_Stack(self): {"value": np.random.rand(10, 10)}, ], lambda a, b: [ - *(a["value"] if isinstance(a["value"], list) else [a["value"]]), - *(b["value"] if isinstance(b["value"], list) else [b["value"]]), + *( + a["value"] + if isinstance(a["value"], list) + else [a["value"]] + ), + *( + b["value"] + if isinstance(b["value"], list) + else [b["value"]] + ), ], operator.__and__, ) From 00533aa46011d787f0fab7677d4a0e20925be2ec Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 24 Jun 2025 10:22:58 +0200 Subject: [PATCH 025/118] Update features.py --- deeptrack/features.py | 87 +++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 44 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index e921a24c3..50820b977 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -226,8 +226,7 @@ class Feature(DeepTrackNode): Parameters ---------- - _input: np.ndarray or list of np.ndarray or Image or list of Image, - optional. + _input: np.ndarray or Image or list[np.ndarray or Image], optional. A list of np.ndarray or `DeepTrackNode` objects or a single np.ndarray or an `Image` object representing the input data for the feature. This parameter specifies what the feature will process. If left empty, no @@ -502,7 +501,7 @@ def get( Parameters ---------- - image: np.ndarray or list of np.ndarray or Image or list of Images + image: np.ndarray or Image or list[np.ndarray or Image] The image or list of images to transform. **kwargs: Any The current value of all properties in `properties`, as well as any @@ -510,7 +509,7 @@ def get( Returns ------- - Image or list of Images + Image or list[Image] The transformed image or list of images. Raises @@ -550,7 +549,7 @@ def __call__( Parameters ---------- - image_list: np.ndarrray or list[np.ndarrray] or Image or list of Images, optional + image_list: np.ndarrray or Image or list[np.ndarrray or Image], optional The input to the feature or pipeline. If `None`, the feature uses previously set input values or propagates properties. **kwargs: Any @@ -753,7 +752,7 @@ def batch(self: Feature, batch_size: int = 32) -> tuple | list[Image]: Returns ------- - tuple or list of Images + tuple or list[Image] A tuple of stacked arrays (if the outputs are NumPy arrays or torch tensors) or a list of images if the outputs are not stackable. @@ -791,7 +790,7 @@ def action( Returns ------- - Image or list of Images + Image or list[Image] The resolved image or list of resolved images. """ @@ -989,7 +988,7 @@ def plot( Parameters ---------- - input_image: np.ndarray or list np.ndarray or Image or list of Image, optional + input_image: np.ndarray or Image or list[np.ndarray or Image], optional The input image or list of images passed as an argument to the `resolve` call. If `None`, uses previously set input values or propagates properties. resolve_kwargs: dict, optional @@ -1834,7 +1833,7 @@ def get( Parameters ---------- - image: np.ndarray or list np.ndarray or Image or list of Image + image: np.ndarray or Image or list[np.ndarray or Image] The input data, which can be an `Image` or a list of `Image` objects, to transform sequentially. _ID: tuple of int, optional @@ -1847,7 +1846,7 @@ def get( Returns ------- - Image or list of Images + Image or list[Image] The final output after `feature_1` and then `feature_2` have processed the input. @@ -1970,7 +1969,7 @@ class Value(Feature): Methods ------- - `get(image: Any, value: float, **kwargs: dict[str, Any]) -> float` + `get(image: Any, value: float, **kwargs: Any) -> float` Returns the stored value, ignoring the input image. Examples @@ -2097,7 +2096,7 @@ class ArithmeticOperationFeature(Feature): op: Callable[[Any, Any], Any] The arithmetic operation to apply, such as a built-in operator (`operator.add`, `operator.mul`) or a custom callable. - value: float or int or list of float or int, optional + value: float or int or list[float or int], optional The second operand for the operation. It defaults to 0. If a list is provided, the operation will apply element-wise. **kwargs: Any @@ -2111,7 +2110,7 @@ class ArithmeticOperationFeature(Feature): Methods ------- - `get(image: Any | list of Any, value: float | int | list[float] | int, **kwargs: Any) -> list[Any]` + `get(image: Any or list[Any], value: float or int or list[float or int], **kwargs: Any) -> list[Any]` Apply the arithmetic operation element-wise to the input data. Examples @@ -2153,7 +2152,7 @@ def __init__( The arithmetic operation to apply, such as `operator.add`, `operator.mul`, or any custom callable that takes two arguments and returns a single output value. - value: PropertyLike[float or int or array, or list of float or int or array], optional + value: PropertyLike[float or int or array or list[float or int or array]], optional The second operand(s) for the operation. If a list is provided, the operation is applied element-wise. It defaults to 0. **kwargs: Any @@ -2176,10 +2175,10 @@ def get( Parameters ---------- - image: Any or list of Any + image: Any or list[Any] The input data, either a single value or a list of values, to be transformed by the arithmetic operation. - value: float or int or array, or list of float or int or array + value: float or int or array or list[float or int or array] The second operand(s) for the operation. If a single value is provided, it is broadcast to match the input size. If a list is provided, it will be cycled to match the length of the input list. @@ -2190,7 +2189,7 @@ def get( Returns ------- - list of Any + list[Any] A list containing the results of applying the operation to the input data element-wise. @@ -2217,7 +2216,7 @@ class Add(ArithmeticOperationFeature): Parameters ---------- - value: PropertyLike[int or float or array, or list of int or floar or array], optional + value: PropertyLike[int or float or array or list[int or floar or array]], optional The value to add to the input. It defaults to 0. **kwargs: Any Additional keyword arguments passed to the parent constructor. @@ -2264,7 +2263,7 @@ def __init__( Parameters ---------- - value: PropertyLike[float or int or array, or list of float or int or array], optional + value: PropertyLike[float or int or array or list[float or int or array]], optional The value to add to the input. It defaults to 0. **kwargs: Any Additional keyword arguments passed to the parent `Feature`. @@ -2281,7 +2280,7 @@ class Subtract(ArithmeticOperationFeature): Parameters ---------- - value: PropertyLike[int or float or array, or list of int or floar or array], optional + value: PropertyLike[int or float or array or list[int or floar or array]], optional The value to subtract from the input. It defaults to 0. **kwargs: Any Additional keyword arguments passed to the parent constructor. @@ -2328,7 +2327,7 @@ def __init__( Parameters ---------- - value: PropertyLike[float or int or array, or list of float or int or array], optional + value: PropertyLike[float or int or array or list[float or int or array]], optional The value to subtract from the input. it defaults to 0. **kwargs: Any Additional keyword arguments passed to the parent `Feature`. @@ -2345,7 +2344,7 @@ class Multiply(ArithmeticOperationFeature): Parameters ---------- - value: PropertyLike[int or float or array, or list of int or floar or array], optional + value: PropertyLike[int or float or array or list[int or floar or array]], optional The value to multiply the input. It defaults to 0. **kwargs: Any Additional keyword arguments passed to the parent constructor. @@ -2392,7 +2391,7 @@ def __init__( Parameters ---------- - value: PropertyLike[float or int or array, or list of float or int or array], optional + value: PropertyLike[float or int or array or list[float or int or array]], optional The value to multiply the input. It defaults to 0. **kwargs: Any Additional keyword arguments. @@ -2409,7 +2408,7 @@ class Divide(ArithmeticOperationFeature): Parameters ---------- - value: PropertyLike[int or float or array, or list of int or floar or array], optional + value: PropertyLike[int or float or array or list[int or floar or array]], optional The value to divide the input. It defaults to 0. **kwargs: Any Additional keyword arguments passed to the parent constructor. @@ -2456,7 +2455,7 @@ def __init__( Parameters ---------- - value: PropertyLike[float or int or array, or list of float or int or array], optional + value: PropertyLike[float or int or array or list[float or int or array]], optional The value to divide the input. It defaults to 0. **kwargs: Any Additional keyword arguments. @@ -2477,7 +2476,7 @@ class FloorDivide(ArithmeticOperationFeature): Parameters ---------- - value: PropertyLike[int or float or array, or list of int or floar or array], optional + value: PropertyLike[int or float or array or list[int or floar or array]], optional The value to floor-divide the input. It defaults to 0. **kwargs: Any Additional keyword arguments passed to the parent constructor. @@ -2524,7 +2523,7 @@ def __init__( Parameters ---------- - value: PropertyLike[float or int or array, or list of float or int or array], optional + value: PropertyLike[float or int or array or list[float or int or array]], optional The value to fllor-divide the input. It defaults to 0. **kwargs: Any Additional keyword arguments. @@ -2541,7 +2540,7 @@ class Power(ArithmeticOperationFeature): Parameters ---------- - value: PropertyLike[int or float or array, or list of int or floar or array], optional + value: PropertyLike[int or float or array or list[int or floar or array]], optional The value to take the power of the input. It defaults to 0. **kwargs: Any Additional keyword arguments passed to the parent constructor. @@ -2588,7 +2587,7 @@ def __init__( Parameters ---------- - value: PropertyLike[float or int or array, or list of float or int or array], optional + value: PropertyLike[float or int or array or list[float or int or array]], optional The value to take the power of the input. It defaults to 0. **kwargs: Any Additional keyword arguments. @@ -2605,7 +2604,7 @@ class LessThan(ArithmeticOperationFeature): Parameters ---------- - value: PropertyLike[int or float or array, or list of int or floar or array], optional + value: PropertyLike[int or float or array or list[int or floar or array]], optional The value to compare (<) with the input. It defaults to 0. **kwargs: Any Additional keyword arguments passed to the parent constructor. @@ -2652,7 +2651,7 @@ def __init__( Parameters ---------- - value: PropertyLike[float or int or array, or list of float or int or array], optional + value: PropertyLike[float or int or array or list[float or int or array]], optional The value to compare (<) with the input. It defaults to 0. **kwargs: Any Additional keyword arguments. @@ -2669,7 +2668,7 @@ class LessThanOrEquals(ArithmeticOperationFeature): Parameters ---------- - value: PropertyLike[int or float or array, or list of int or floar or array], optional + value: PropertyLike[int or float or array or list[int or floar or array]], optional The value to compare (<=) with the input. It defaults to 0. **kwargs: Any Additional keyword arguments passed to the parent constructor. @@ -2716,7 +2715,7 @@ def __init__( Parameters ---------- - value: PropertyLike[float or int or array, or list of float or int or array], optional + value: PropertyLike[float or int or array or list[float or int or array]], optional The value to compare (<=) with the input. It defaults to 0. **kwargs: Any Additional keyword arguments. @@ -2736,7 +2735,7 @@ class GreaterThan(ArithmeticOperationFeature): Parameters ---------- - value: PropertyLike[int or float or array, or list of int or floar or array], optional + value: PropertyLike[int or float or array or list[int or floar or array]], optional The value to compare (>) with the input. It defaults to 0. **kwargs: Any Additional keyword arguments passed to the parent constructor. @@ -2783,7 +2782,7 @@ def __init__( Parameters ---------- - value: PropertyLike[float or int or array, or list of float or int or array], optional + value: PropertyLike[float or int or array or list[float or int or array]], optional The value to compare (>) with the input. It defaults to 0. **kwargs: Any Additional keyword arguments. @@ -2800,7 +2799,7 @@ class GreaterThanOrEquals(ArithmeticOperationFeature): Parameters ---------- - value: PropertyLike[int or float or array, or list of int or floar or array], optional + value: PropertyLike[int or float or array or list[int or floar or array]], optional The value to compare (<=) with the input. It defaults to 0. **kwargs: Any Additional keyword arguments passed to the parent constructor. @@ -2847,7 +2846,7 @@ def __init__( Parameters ---------- - value: PropertyLike[float or int or array, or list of float or int or array], optional + value: PropertyLike[float or int or array or list[float or int or array]], optional The value to compare (>=) with the input. It defaults to 0. **kwargs: Any Additional keyword arguments. @@ -2878,7 +2877,7 @@ class Equals(ArithmeticOperationFeature): Parameters ---------- - value: PropertyLike[int or float or array, or list of int or floar or array], optional + value: PropertyLike[int or float or array or list[int or floar or array]], optional The value to compare (==) with the input. It defaults to 0. **kwargs: Any Additional keyword arguments passed to the parent constructor. @@ -2931,7 +2930,7 @@ def __init__( Parameters ---------- - value: PropertyLike[float or int or array, or list of float or int or array], optional + value: PropertyLike[float or int or array or list[float or int or array]], optional The value to compare with the input. It defaults to 0. **kwargs: Any Additional keyword arguments. @@ -2974,7 +2973,7 @@ class Stack(Feature): Methods ------- - `get(image: Any, value: Any, **kwargs: dict[str, Any]) -> list[Any]` + `get(image: Any, value: Any, **kwargs: Any) -> list[Any]` Concatenate the input with the value. Examples @@ -3445,7 +3444,7 @@ class Combine(StructuralFeature): Parameters ---------- - features: list of Features + features: list[Feature] A list of features to combine. Each feature will be resolved in the order they appear in the list. **kwargs: Any, optional @@ -3489,7 +3488,7 @@ def __init__( Parameters ---------- - features: list of Features + features: list[Feature] A list of features to combine. Each feature is added as a dependency to ensure proper execution in the computation graph. **kwargs: Any, optional @@ -4361,7 +4360,7 @@ def get( Parameters ---------- - list_of_images: list[np.ndarray] or list[Image] + list_of_images: list[np.ndarray or Image] A list of images to be processed by the function. function: Callable[[list[np.ndarray] | list[Image]], np.ndarray | list[np.ndarray] | Image | list[Image]] The function that processes the list of images and returns either: @@ -4808,7 +4807,7 @@ def get( Parameters ---------- - path: str or list of str + path: str or list[str] The file path(s) to the image(s) to be loaded. A single string loads one image, while a list of paths loads multiple images. load_options: dict of str to Any, optional From 910dae0b63cbd03f09619fad3e7426891aedc703 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 24 Jun 2025 10:41:17 +0200 Subject: [PATCH 026/118] Update test_features.py --- deeptrack/tests/test_features.py | 66 ++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index 70ac5a12c..6466f25e9 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -902,6 +902,72 @@ def test_Stack(self): operator.__and__, ) + # Stack scalar with scalar + feature = features.Stack(value=2) + result = feature(1) + self.assertEqual(result, [1, 2]) + result = (1 & feature)() + self.assertEqual(result, [1, 1, 2]) + result = (feature & 1)() + self.assertEqual(result, [1, 2, 1]) + + # Stack scalar with list + feature = features.Stack(value=[3, 4]) + result = feature(2) + self.assertEqual(result, [2, 3, 4]) + + # Stack list with scalar + feature = features.Stack(value=5) + result = feature([1, 2, 3]) + self.assertEqual(result, [1, 2, 3, 5]) + + # Stack list with list + feature = features.Stack(value=[4, 5]) + result = feature([1, 2, 3]) + self.assertEqual(result, [1, 2, 3, 4, 5]) + + # Stack with empty lists + feature = features.Stack(value=[]) + result = feature([1, 2]) + self.assertEqual(result, [1, 2]) + + feature = features.Stack(value=[1, 2]) + result = feature([]) + self.assertEqual(result, [1, 2]) + + # Stack using Value feature + pipeline = features.Value([1, 2]) >> features.Stack(value=features.Value([3, 4])) + result = pipeline() + self.assertEqual(result, [1, 2, 3, 4]) + + # Stack using & operator (Value & list) + pipeline = features.Value([1, 2]) & [3, 4] + self.assertEqual(pipeline.resolve(), [1, 2, 3, 4]) + + # Stack using & operator (list & Value) + pipeline = [3, 4] & features.Value([1, 2]) + self.assertEqual(pipeline.resolve(), [3, 4, 1, 2]) + + # Stack NumPy arrays + arr1 = np.array([1, 2]) + arr2 = np.array([3, 4]) + feature = features.Stack(value=arr2) + result = feature(arr1) + self.assertEqual(len(result), 2) + self.assertTrue(np.array_equal(result[0], arr1)) + self.assertTrue(np.array_equal(result[1], arr2)) + + # Stack PyTorch tensors + if TORCH_AVAILABLE: + import torch + + t1 = torch.tensor([1, 2]) + t2 = torch.tensor([3, 4]) + feature = features.Stack(value=t2) + result = feature(t1) + self.assertEqual(len(result), 2) + self.assertTrue(torch.equal(result[0], t1)) + self.assertTrue(torch.equal(result[1], t2)) def test_Arguments_feature_passing(self): """Tests that arguments are correctly passed and updated in a feature pipeline.""" From 755e09b5974abdcffa042434b7592ea61c653b1d Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 24 Jun 2025 10:41:21 +0200 Subject: [PATCH 027/118] Update features.py --- deeptrack/features.py | 71 ++++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 50820b977..036438d41 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -416,7 +416,7 @@ def int_dtype(self) -> np.dtype | torch.dtype: def complex_dtype(self) -> np.dtype | torch.dtype: """The dtype of the complex numbers.""" return xp.get_complex_dtype(self._complex_dtype) - + @property def bool_dtype(self) -> np.dtype | torch.dtype: """The dtype of the boolean numbers.""" @@ -608,10 +608,8 @@ def __call__( return output - resolve = __call__ - def store_properties( self: Feature, toggle: bool = True, @@ -1252,7 +1250,7 @@ def __add__( """Adds another value or feature using '+'. """ - + return self >> Add(other) def __radd__( @@ -1262,7 +1260,7 @@ def __radd__( """Adds this feature to another value using right '+'. """ - + return Value(other) >> Add(self) def __sub__( @@ -1272,7 +1270,7 @@ def __sub__( """Subtracts another value or feature using '-'. """ - + return self >> Subtract(other) def __rsub__( @@ -1282,6 +1280,7 @@ def __rsub__( """Subtracts this feature from another value using right '-'. """ + return Value(other) >> Subtract(self) def __mul__( @@ -1291,7 +1290,7 @@ def __mul__( """Multiplies this feature with another value using '*'. """ - + return self >> Multiply(other) def __rmul__( @@ -1309,9 +1308,9 @@ def __truediv__( other: Any ) -> Feature: """Divides this feature by another value using '/'. - + """ - + return self >> Divide(other) def __rtruediv__( @@ -1319,9 +1318,9 @@ def __rtruediv__( other: Any ) -> Feature: """Divides another value by this feature using right '/'. - + """ - + return Value(other) >> Divide(self) def __floordiv__( @@ -1329,9 +1328,9 @@ def __floordiv__( other: Any ) -> Feature: """Performs floor division using '//'. - + """ - + return self >> FloorDivide(other) def __rfloordiv__( @@ -1339,9 +1338,9 @@ def __rfloordiv__( other: Any ) -> Feature: """Performs right floor division using '//'. - + """ - + return Value(other) >> FloorDivide(self) def __pow__( @@ -1349,9 +1348,9 @@ def __pow__( other: Any ) -> Feature: """Raises this feature to a power using '**'. - + """ - + return self >> Power(other) def __rpow__( @@ -1359,16 +1358,19 @@ def __rpow__( other: Any ) -> Feature: """Raises another value to this feature as a power using right '**'. - + """ - + return Value(other) >> Power(self) def __gt__( self: Feature, other: Any ) -> Feature: - """Checks if this feature is greater than another using '>'.""" + """Checks if this feature is greater than another using '>'. + + """ + return self >> GreaterThan(other) def __rgt__( @@ -1377,9 +1379,9 @@ def __rgt__( ) -> Feature: """Checks if another value is greater than this feature using right '>'. - + """ - + return Value(other) >> GreaterThan(self) def __lt__( @@ -1387,18 +1389,17 @@ def __lt__( other: Any ) -> Feature: """Checks if this feature is less than another using '<'. - + """ - + return self >> LessThan(other) def __rlt__( self: Feature, other: Any ) -> Feature: - """Checks if another value is less than this feature using - right '<'. - + """Checks if another value is less than this feature using right '<'. + """ return Value(other) >> LessThan(self) @@ -1408,9 +1409,9 @@ def __le__( other: Any ) -> Feature: """Checks if this feature is less than or equal to another using '<='. - + """ - + return self >> LessThanOrEquals(other) def __rle__( @@ -1419,9 +1420,9 @@ def __rle__( ) -> Feature: """Checks if another value is less than or equal to this feature using right '<='. - + """ - + return Value(other) >> LessThanOrEquals(self) def __ge__( @@ -1430,9 +1431,9 @@ def __ge__( ) -> Feature: """Checks if this feature is greater than or equal to another using '>='. - + """ - + return self >> GreaterThanOrEquals(other) def __rge__( @@ -1441,7 +1442,7 @@ def __rge__( ) -> Feature: """Checks if another value is greater than or equal to this feature using right '>='. - + """ return Value(other) >> GreaterThanOrEquals(self) @@ -1461,7 +1462,7 @@ def __and__( other: Any, ) -> Feature: """Stacks this feature with another using '&'. - + """ return self >> Stack(other) From 046b6e2552b9586923768059f5eab9d7acfce4cb Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 24 Jun 2025 10:43:37 +0200 Subject: [PATCH 028/118] Update features.py --- deeptrack/features.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 036438d41..a6652294d 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -167,7 +167,7 @@ def merge_features( "GreaterThanOrEqual", "Equals", "Equal", - "Stack", # TODO + "Stack", "Arguments", # TODO "Probability", # TODO "Repeat", # TODO From 0c83f7cf7911ad27fdd6fb1a557b0c3077a01607 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 24 Jun 2025 11:25:04 +0200 Subject: [PATCH 029/118] Update features.py --- deeptrack/features.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/deeptrack/features.py b/deeptrack/features.py index a6652294d..321753252 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -2996,6 +2996,23 @@ class Stack(Feature): >>> pipeline.resolve() [4, 5, 1, 2, 3] + Note + ---- + If a feature is called directly, its result is cached internally. This can + affect how it behaves when reused in chained pipelines. For exmaple: + >>> stack_feature = dt.Stack(value=2) + >>> _ = stack_feature(1) # Evaluate the feature and cache the output + >>> (1 & stack_feature)() + [1, 1, 2] + + To ensure consistent behavior when reusing a feature after calling it, + reset its state using instead: + >>> stack_feature = dt.Stack(value=2) + >>> _ = stack_feature(1) + >>> stack_feature.update() # clear cached state + >>> (1 & stack_feature)() + [1, 2] + """ __distributed__: bool = False From b2da19640185871e2f4e89c32bca93e93aacb679 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 24 Jun 2025 14:44:08 +0200 Subject: [PATCH 030/118] Update features.py --- deeptrack/features.py | 44 ++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 321753252..f509fdf59 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -3093,11 +3093,11 @@ class Arguments(Feature): Examples -------- >>> import deeptrack as dt - >>> from tempfile import NamedTemporaryFile - >>> from PIL import Image as PIL_Image - >>> import os Create a temporary image: + >>> import numpy as np + >>> import PIL, tempfile + >>> >>> test_image_array = (np.ones((50, 50)) * 128).astype(np.uint8) >>> temp_png = NamedTemporaryFile(suffix=".png", delete=False) >>> PIL_Image.fromarray(test_image_array).save(temp_png.name) @@ -3105,43 +3105,45 @@ class Arguments(Feature): A typical use-case is: >>> arguments = dt.Arguments(is_label=False) >>> image_pipeline = ( - ... dt.LoadImage(path=temp_png.name) >> - ... dt.Gaussian(sigma = (1 - arguments.is_label) * 5) + ... dt.LoadImage(path=temp_png.name) + ... >> dt.Gaussian(sigma=arguments.is_label) # Image with no noise ... ) >>> image_pipeline.bind_arguments(arguments) - - >>> image = image_pipeline() # Image with added noise. - >>> print(image.std()) - 5.041072178933536 + >>> + >>> image = image_pipeline() + >>> image.std() + 0.0 Change the argument: - >>> image = image_pipeline(is_label=True) # Image with no noise. - >>> print(image.std()) - 0.0 + >>> image = image_pipeline(is_label=True) # Image with added noise + >>> image.std() + 1.0104364326447652 Remove the temporary image: + >>> import os + >>> >>> os.remove(temp_png.name) For a non-mathematical dependence, create a local link to the property as follows: >>> arguments = dt.Arguments(is_label=False) >>> image_pipeline = ( - ... dt.LoadImage(path=temp_png.name) >> - ... dt.Gaussian( + ... dt.LoadImage(path=temp_png.name) + ... >> dt.Gaussian( ... is_label=arguments.is_label, - ... sigma=lambda is_label: 0 if is_label else 5 + ... sigma=lambda is_label: 1 if is_label else 0, ... ) ... ) >>> image_pipeline.bind_arguments(arguments) - Keep in mind that, if any dependent property is non-deterministic, they may + Keep in mind that, if any dependent property is non-deterministic, it may permanently change: - >>> arguments = dt.Arguments(noise_max_sigma=5) + >>> arguments = dt.Arguments(noise_max=1) >>> image_pipeline = ( - ... dt.LoadImage(path=temp_png.name) >> - ... dt.Gaussian( - ... noise_max_sigma=arguments.noise_max_sigma, - ... sigma=lambda noise_max_sigma: np.random.rand()*noise_max_sigma + ... dt.LoadImage(path=temp_png.name) + ... >> dt.Gaussian( + ... noise_max=arguments.noise_max, + ... sigma=lambda noise_max: np.random.rand() * noise_max, ... ) ... ) >>> image_pipeline.bind_arguments(arguments) From 573001575497e00d7654957a0df71e663cf0b6bf Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 24 Jun 2025 15:25:16 +0200 Subject: [PATCH 031/118] Update test_features.py --- deeptrack/tests/test_features.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index 6466f25e9..d8ab98110 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -906,10 +906,6 @@ def test_Stack(self): feature = features.Stack(value=2) result = feature(1) self.assertEqual(result, [1, 2]) - result = (1 & feature)() - self.assertEqual(result, [1, 1, 2]) - result = (feature & 1)() - self.assertEqual(result, [1, 2, 1]) # Stack scalar with list feature = features.Stack(value=[3, 4]) From 0c16b48a38144711ce9e5407afddf476d53f7586 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 24 Jun 2025 15:47:05 +0200 Subject: [PATCH 032/118] Update features.py --- deeptrack/features.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index f509fdf59..816ac0a5b 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -909,26 +909,33 @@ def bind_arguments( self: Feature, arguments: Feature, ) -> Feature: - """Binds another feature’s properties as arguments to this feature. + """Bind another feature’s properties as arguments to this feature. This method allows properties of `arguments` to be dynamically linked - to this feature, enabling shared configurations across multiple features. - It is commonly used in advanced feature pipelines. + to this feature, enabling shared configurations across multiple + features. It is commonly used in advanced feature pipelines. - See Also - -------- - features.Arguments - A utility that helps manage and propagate feature arguments efficiently. + This method is often used in combination with the `Arguments` Feature, + which provides a utility that helps manage and propagate feature + arguments efficiently. Parameters ---------- arguments: Feature - The feature whose properties will be bound as arguments to this feature. + The feature whose properties will be bound as arguments to this + feature. Returns ------- Feature The current feature instance with bound arguments. + + Examples + -------- + TODO method alone + + TODO use with Arguments + """ self.arguments = arguments From c8319d300289a1a51e3439cf3b9164af7383809a Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 24 Jun 2025 18:39:37 +0200 Subject: [PATCH 033/118] Update features.py --- deeptrack/features.py | 60 +++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 816ac0a5b..762dabd62 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -3085,17 +3085,17 @@ def get( class Arguments(Feature): """A convenience container for pipeline arguments. - The `Arguments` feature allows dynamic control of pipeline behavior by - providing a container for arguments that can be modified or overridden at - runtime. This is particularly useful when working with parameterized - pipelines, such as toggling behaviors based on whether an image is a label + The `Arguments` feature allows dynamic control of pipeline behavior by + providing a container for arguments that can be modified or overridden at + runtime. This is particularly useful when working with parametrized + pipelines, such as toggling behaviors based on whether an image is a label or a raw input. Methods ------- - `get(image: Any, **kwargs: dict[str, Any]) -> Any` - Passes the input image through unchanged, while allowing for property - overrides. + `get(image: Any, **kwargs: Any) -> Any` + It passes the input image through unchanged, while allowing for + property overrides. Examples -------- @@ -3106,8 +3106,8 @@ class Arguments(Feature): >>> import PIL, tempfile >>> >>> test_image_array = (np.ones((50, 50)) * 128).astype(np.uint8) - >>> temp_png = NamedTemporaryFile(suffix=".png", delete=False) - >>> PIL_Image.fromarray(test_image_array).save(temp_png.name) + >>> temp_png = tempfile.NamedTemporaryFile(suffix=".png", delete=False) + >>> PIL.Image.fromarray(test_image_array).save(temp_png.name) A typical use-case is: >>> arguments = dt.Arguments(is_label=False) @@ -3137,8 +3137,8 @@ class Arguments(Feature): >>> image_pipeline = ( ... dt.LoadImage(path=temp_png.name) ... >> dt.Gaussian( - ... is_label=arguments.is_label, - ... sigma=lambda is_label: 1 if is_label else 0, + ... local_is_label=arguments.is_label, + ... sigma=lambda local_is_label: 1 if local_is_label else 0, ... ) ... ) >>> image_pipeline.bind_arguments(arguments) @@ -3154,36 +3154,36 @@ class Arguments(Feature): ... ) ... ) >>> image_pipeline.bind_arguments(arguments) - >>> image_pipeline.store_properties() - + >>> image_pipeline.store_properties() # Store image properties + >>> >>> image = image_pipeline() - >>> print(image.get_property("sigma")) - 1.1838819055669947 + >>> image.std(), image.get_property("sigma") + (0.8464173007136401, 0.8423390304699889) - >>> image = image_pipeline(noise_max_sigma=0) - >>> print(image.get_property("sigma")) - 0.0 + >>> image = image_pipeline(noise_max=0) + >>> image.std(), image.get_property("sigma") + (0.0, 0.0) As with any feature, all arguments can be passed by deconstructing the properties dict: >>> arguments = dt.Arguments(is_label=False, noise_sigma=5) >>> image_pipeline = ( - ... dt.LoadImage(path=temp_png.name) >> - ... dt.Gaussian( + ... dt.LoadImage(path=temp_png.name) + ... >> dt.Gaussian( ... sigma=lambda is_label, noise_sigma: ( ... 0 if is_label else noise_sigma - ... ) - ... **arguments.properties + ... ), + ... **arguments.properties, ... ) ... ) >>> image_pipeline.bind_arguments(arguments) - - >>> image = image_pipeline() # Image with added noise. - >>> print(image.std()) + >>> + >>> image = image_pipeline() # Image with added noise + >>> image.std() 5.002151761964336 - >>> image = image_pipeline(is_label=True) # Raw image with no noise. - >>> print(image.std()) + >>> image = image_pipeline(is_label=True) # Raw image with no noise + >>> image.std() 0.0 """ @@ -3191,10 +3191,10 @@ class Arguments(Feature): def get( self: Feature, image: Any, - **kwargs: dict[str, Any] + **kwargs: Any, ) -> Any: - """Process the input image and allow property overrides. + """Return the input image and allow property overrides. This method does not modify the input image but provides a mechanism for overriding arguments dynamically during pipeline execution. @@ -3318,7 +3318,7 @@ def get( otherwise, it is the unchanged input image. """ - + if random_number < probability: image = self.feature.resolve(image, **kwargs) From 01fe26d9402b320df50aa1f0b86a586c0fc5d3c0 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 24 Jun 2025 18:53:02 +0200 Subject: [PATCH 034/118] Update test_features.py --- deeptrack/tests/test_features.py | 123 ++++++++++++++++--------------- 1 file changed, 64 insertions(+), 59 deletions(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index d8ab98110..50482ac4e 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -965,62 +965,23 @@ def test_Stack(self): self.assertTrue(torch.equal(result[0], t1)) self.assertTrue(torch.equal(result[1], t2)) - def test_Arguments_feature_passing(self): - """Tests that arguments are correctly passed and updated in a feature pipeline.""" - - # Define Arguments with static and dynamic values - arguments = features.Arguments( - a="foo", - b="bar", - c=lambda a, b: a + b, # "foobar" - d=np.random.rand, # Random float in [0, 1] - ) - - # First feature with dependencies on arguments - f1 = features.DummyFeature( - p1=arguments.a, # "foo" - p2=lambda p1: p1 + "baz" # "foobaz" - ) - - # Second feature dependent on the first - f2 = features.DummyFeature( - p1=f1.p2, # Should be "foobaz" - p2=arguments.d, # Random value - ) - - # Assertions - self.assertEqual(f1.properties['p1'](), "foo") # Check that p1 is set correctly - self.assertEqual(f1.properties['p2'](), "foobaz") # Check lambda evaluation - - self.assertEqual(f2.properties['p1'](), "foobaz") # Check dependency resolution - - # Ensure p2 in f2 is a valid float between 0 and 1 - self.assertTrue(0 <= f2.properties['p2']() <= 1) - - # Ensure `c` was computed correctly - self.assertEqual(arguments.c(), "foobar") # Should concatenate "foo" + "bar" - - # Test that d is dynamic (generates new values) - first_d = arguments.d.update()() - second_d = arguments.d.update()() - self.assertNotEqual(first_d, second_d) # Check that values change def test_Arguments(self): from tempfile import NamedTemporaryFile from PIL import Image as PIL_Image import os - """Creates a temporary test image.""" + # Create a temporary test image. test_image_array = (np.ones((50, 50)) * 128).astype(np.uint8) with NamedTemporaryFile(suffix=".png", delete=False) as temp_png: PIL_Image.fromarray(test_image_array).save(temp_png.name) - try: - """Tests pipeline behavior when toggling `is_label`.""" + try: # Ensure removal of test image. + # Test pipeline behavior when toggling `is_label`. arguments = features.Arguments(is_label=False) image_pipeline = ( - features.LoadImage(path=temp_png.name) >> - Gaussian(sigma=(1 - arguments.is_label) * 5) + features.LoadImage(path=temp_png.name) + >> Gaussian(sigma=(1 - arguments.is_label) * 5) ) image_pipeline.bind_arguments(arguments) @@ -1030,15 +991,15 @@ def test_Arguments(self): # Test raw image with `is_label=True` image = image_pipeline(is_label=True) - self.assertAlmostEqual(image.std(), 0.0, places=3) # No noise expected + self.assertAlmostEqual(image.std(), 0.0, places=3) # No noise - """Tests pipeline behavior with dynamically computed sigma.""" + # Test pipeline behavior with dynamically computed sigma. arguments = features.Arguments(is_label=False) image_pipeline = ( - features.LoadImage(path=temp_png.name) >> - Gaussian( + features.LoadImage(path=temp_png.name) + >> Gaussian( is_label=arguments.is_label, - sigma=lambda is_label: 0 if is_label else 5 + sigma=lambda is_label: 0 if is_label else 5, ) ) image_pipeline.bind_arguments(arguments) @@ -1049,15 +1010,16 @@ def test_Arguments(self): # Test raw image with `is_label=True` image = image_pipeline(is_label=True) - self.assertAlmostEqual(image.std(), 0.0, places=3) # No noise expected + self.assertAlmostEqual(image.std(), 0.0, places=3) # No noise - """Tests property storage and modification in the pipeline.""" + # Test property storage and modification in the pipeline. arguments = features.Arguments(noise_max_sigma=5) image_pipeline = ( - features.LoadImage(path=temp_png.name) >> - Gaussian( + features.LoadImage(path=temp_png.name) + >> Gaussian( noise_max_sigma=arguments.noise_max_sigma, - sigma=lambda noise_max_sigma: np.random.rand() * noise_max_sigma + sigma=lambda noise_max_sigma: + np.random.rand() * noise_max_sigma, ) ) image_pipeline.bind_arguments(arguments) @@ -1072,13 +1034,14 @@ def test_Arguments(self): image = image_pipeline(noise_max_sigma=0) self.assertEqual(image.get_property("sigma"), 0.0) - """Tests passing arguments dynamically using `**arguments.properties`.""" + # Test passing arguments dynamically using **arguments.properties. arguments = features.Arguments(is_label=False, noise_sigma=5) image_pipeline = ( features.LoadImage(path=temp_png.name) >> Gaussian( - sigma=lambda is_label, noise_sigma: 0 if is_label else noise_sigma, - **arguments.properties + sigma=lambda is_label, noise_sigma: + 0 if is_label else noise_sigma, + **arguments.properties, ) ) image_pipeline.bind_arguments(arguments) @@ -1089,12 +1052,54 @@ def test_Arguments(self): # Test raw image with `is_label=True` image = image_pipeline(is_label=True) - self.assertAlmostEqual(image.std(), 0.0, places=3) # No noise expected - + self.assertAlmostEqual(image.std(), 0.0, places=3) # No noise + + except Exception: + raise finally: if os.path.exists(temp_png.name): os.remove(temp_png.name) + def test_Arguments_feature_passing(self): + """Tests that arguments are correctly passed and updated in a feature pipeline.""" + + # Define Arguments with static and dynamic values + arguments = features.Arguments( + a="foo", + b="bar", + c=lambda a, b: a + b, # "foobar" + d=np.random.rand, # Random float in [0, 1] + ) + + # First feature with dependencies on arguments + f1 = features.DummyFeature( + p1=arguments.a, # "foo" + p2=lambda p1: p1 + "baz" # "foobaz" + ) + + # Second feature dependent on the first + f2 = features.DummyFeature( + p1=f1.p2, # Should be "foobaz" + p2=arguments.d, # Random value + ) + + # Assertions + self.assertEqual(f1.properties['p1'](), "foo") # Check that p1 is set correctly + self.assertEqual(f1.properties['p2'](), "foobaz") # Check lambda evaluation + + self.assertEqual(f2.properties['p1'](), "foobaz") # Check dependency resolution + + # Ensure p2 in f2 is a valid float between 0 and 1 + self.assertTrue(0 <= f2.properties['p2']() <= 1) + + # Ensure `c` was computed correctly + self.assertEqual(arguments.c(), "foobar") # Should concatenate "foo" + "bar" + + # Test that d is dynamic (generates new values) + first_d = arguments.d.update()() + second_d = arguments.d.update()() + self.assertNotEqual(first_d, second_d) # Check that values change + def test_Probability(self): np.random.seed(42) # Set seed for reproducibility From c9c418af04af5b5e0d04987fc0bc75de00fef3bc Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 24 Jun 2025 19:11:00 +0200 Subject: [PATCH 035/118] Update test_features.py --- deeptrack/tests/test_features.py | 45 ++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index 50482ac4e..c0a71bf6b 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -1061,7 +1061,8 @@ def test_Arguments(self): os.remove(temp_png.name) def test_Arguments_feature_passing(self): - """Tests that arguments are correctly passed and updated in a feature pipeline.""" + # Tests that arguments are correctly passed and updated. + # # Define Arguments with static and dynamic values arguments = features.Arguments( @@ -1074,7 +1075,7 @@ def test_Arguments_feature_passing(self): # First feature with dependencies on arguments f1 = features.DummyFeature( p1=arguments.a, # "foo" - p2=lambda p1: p1 + "baz" # "foobaz" + p2=lambda p1: p1 + "baz", # "foobaz" ) # Second feature dependent on the first @@ -1084,22 +1085,50 @@ def test_Arguments_feature_passing(self): ) # Assertions - self.assertEqual(f1.properties['p1'](), "foo") # Check that p1 is set correctly - self.assertEqual(f1.properties['p2'](), "foobaz") # Check lambda evaluation - - self.assertEqual(f2.properties['p1'](), "foobaz") # Check dependency resolution + self.assertEqual(f1.properties["p1"](), "foo") # Check that p1 is set + # correctly + self.assertEqual(f1.properties["p2"](), "foobaz") # Check lambda + # evaluation + self.assertEqual(f2.properties["p1"](), "foobaz") # Check dependency + # resolution # Ensure p2 in f2 is a valid float between 0 and 1 - self.assertTrue(0 <= f2.properties['p2']() <= 1) + self.assertTrue(0 <= f2.properties["p2"]() <= 1) # Ensure `c` was computed correctly - self.assertEqual(arguments.c(), "foobar") # Should concatenate "foo" + "bar" + self.assertEqual(arguments.c(), "foobar") # Should concatenate + # "foo" + "bar" # Test that d is dynamic (generates new values) first_d = arguments.d.update()() second_d = arguments.d.update()() self.assertNotEqual(first_d, second_d) # Check that values change + def test_Arguments_binding(self): + # Create a dynamic argument container + arguments = features.Arguments(x=10) + + # Create a simple pipeline: Value(100) + x + 1 + pipeline = ( + features.Value(100) + >> features.Add(value=arguments.x) + >> features.Add(1) + ) + + # Evaluate pipeline with default x=10 + result = pipeline() + self.assertEqual(result, 111) # 100 + 10 + 1 + + result_no_binding = pipeline(x=20) + self.assertEqual(result_no_binding, 111) # 100 + 10 + 1 + + # Bind the arguments to the pipeline + pipeline.bind_arguments(arguments) + + # Override x at runtime to 20 + result_binding = pipeline(x=20) + self.assertEqual(result_binding, 121) # 100 + 20 + 1 + def test_Probability(self): np.random.seed(42) # Set seed for reproducibility From d414d409e313a3cdb4997ef3e027133f1e5cb1c9 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 24 Jun 2025 19:28:33 +0200 Subject: [PATCH 036/118] Update features.py --- deeptrack/features.py | 45 +++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 762dabd62..b41443fc3 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -168,7 +168,7 @@ def merge_features( "Equals", "Equal", "Stack", - "Arguments", # TODO + "Arguments", "Probability", # TODO "Repeat", # TODO "Combine", # TODO @@ -1709,36 +1709,35 @@ def propagate_data_to_dependencies(feature: Feature, **kwargs: dict[str, Any]) - class StructuralFeature(Feature): - """Provides the structure of a feature set without input transformations. + """ + Provides the structure of a feature set without input transformations. + + A `StructuralFeature` does not modify the input data or introduce new + properties. Instead, it serves as a logical and organizational tool for + grouping, chaining, or structuring pipelines. - A `StructuralFeature` does not directly transform the input data or add new - properties. Instead, it is commonly used as a logical or organizational - tool to structure and manage feature sets within a pipeline. + This feature is typically used to: + - group or chain sub-features (e.g., `Chain`) + - apply conditional or sequential logic (e.g., `Probability`) + - organize pipelines without affecting data flow (e.g., `Combine`) - Since `StructuralFeature` does not override the `__init__` or `get` - methods, it inherits the behavior of the base `Feature` class. + `StructuralFeature` inherits all behavior from `Feature`, without + overriding `__init__` or `get`. Attributes ---------- - __property_verbosity__: int - Controls whether this feature’s properties are included in the output - image’s property list. A value of `2` means that this feature’s - properties are not included. - __distributed__: bool - Determines whether the feature’s `get` method is applied to each - element in the input list (`__distributed__ = True`) or to the entire - list as a whole (`__distributed__ = False`). - - Notes - ----- - Structural features are typically used for tasks like grouping or chaining - features, applying sequential or conditional logic, or structuring - pipelines without directly modifying the data. + __property_verbosity__ : int + Controls whether this feature's properties appear in the output image's + property list. A value of `2` hides them from output. + __distributed__ : bool + If `True`, applies `get` to each element in a list individually. + If `False`, processes the entire list as a single unit. Defaults to + `False`. """ - __property_verbosity__: int = 2 # Hide properties from logs or output. - __distributed__: bool = False # Process the entire image list in one call. + __property_verbosity__: int = 2 # Hide properties from logs or output + __distributed__: bool = False # Process the entire image list in one call class Chain(StructuralFeature): From 5179396109c78c644d1d59973ae69f28a7521775 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 24 Jun 2025 19:46:24 +0200 Subject: [PATCH 037/118] Update features.py --- deeptrack/features.py | 86 +++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 45 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index b41443fc3..080487632 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -147,9 +147,9 @@ def merge_features( __all__ = [ "Feature", # TODO - "StructuralFeature", # TODO - "Chain", # TODO - "Branch", # TODO + "StructuralFeature", + "Chain", + "Branch", "DummyFeature", "Value", "ArithmeticOperationFeature", @@ -1709,8 +1709,7 @@ def propagate_data_to_dependencies(feature: Feature, **kwargs: dict[str, Any]) - class StructuralFeature(Feature): - """ - Provides the structure of a feature set without input transformations. + """Provide the structure of a feature set without input transformations. A `StructuralFeature` does not modify the input data or introduce new properties. Instead, it serves as a logical and organizational tool for @@ -1743,9 +1742,13 @@ class StructuralFeature(Feature): class Chain(StructuralFeature): """Resolve two features sequentially. - This feature applies two features sequentially, passing the output of the - first feature as the input to the second. It enables building feature - chains that execute complex transformations by combining simple operations. + Applies two features sequentially: the output of `feature_1` is passed as + input to `feature_2`. This allows combining simple operations into complex + pipelines. + + This is equivalent to using the `>>` operator: + + >>> dt.Chain(A, B) ≡ A >> B Parameters ---------- @@ -1760,45 +1763,37 @@ class Chain(StructuralFeature): Methods ------- - `get(image: np.ndarray | list[np.ndarray] | Image | list[Image], _ID: tuple[int, ...], **kwargs: dict[str, Any]) -> Image | list[Image]` + `get(image: Any, _ID: tuple[int, ...], **kwargs: Any) -> Any | list[Any]` Apply the two features in sequence on the given input image. - Notes - ----- - This feature is used to combine simple operations into a pipeline without the - need for explicit function chaining. It is syntactic sugar for creating - sequential feature pipelines. - Examples -------- >>> import deeptrack as dt - >>> import numpy as np Create a feature chain where the first feature adds a constant offset, and the second feature multiplies the result by a constant: - >>> A = dt.Add(value=10) >>> M = dt.Multiply(value=0.5) - - Chain the features: - >>> chain = A >> M + >>> + >>> chain = A >> M Equivalent to: >>> chain = dt.Chain(A, M) Create a dummy image: - >>> dummy_image = np.ones((2, 4)) + >>> import numpy as np + >>> + >>> dummy_image = np.zeros((2, 4)) Apply the chained features: - >>> transformed_image = chain(dummy_image) - >>> print(transformed_image) - [[5.5 5.5 5.5 5.5] - [5.5 5.5 5.5 5.5]] + >>> chain(dummy_image) + array([[5., 5., 5., 5.], + [5., 5., 5., 5.]]) """ def __init__( - self: Feature, + self: Chain, feature_1: Feature, feature_2: Feature, **kwargs: Any, @@ -1815,10 +1810,10 @@ def __init__( feature_1: Feature The first feature to be applied. feature_2: Feature - The second feature, applied after `feature_1`. - **kwargs: Any, optional - Additional keyword arguments passed to the parent constructor (e.g., - name, properties). + The second feature, applied to the result of `feature_1`. + **kwargs: Any + Additional keyword arguments passed to the parent constructor + (e.g., name, properties). """ @@ -1829,33 +1824,34 @@ def __init__( def get( self: Feature, - image: np.ndarray | list[np.ndarray] | Image | list[Image], + image: Any | list[Any], _ID: tuple[int, ...] = (), **kwargs: Any, - ) -> Image | list[Image]: + ) -> Any | list[Any]: """Apply the two features sequentially to the given input image(s). - This method first applies `feature_1` to the input image(s) and then passes - the output through `feature_2`. + This method first applies `feature_1` to the input image(s) and then + passes the output through `feature_2`. Parameters ---------- - image: np.ndarray or Image or list[np.ndarray or Image] - The input data, which can be an `Image` or a list of `Image` objects, - to transform sequentially. - _ID: tuple of int, optional + image: Any or list[Any] + The input data to transform sequentially. Most typically, this is + a NumPy array, a PyTorch tensor, or an Image, or a list of the + same. + _ID: tuple[int, ...], optional A unique identifier for caching or parallel execution. It defaults to an empty tuple. **kwargs: Any - Additional parameters passed to or sampled by the features. These are - generally unused here, as each sub-feature fetches its required properties - internally. + Additional parameters passed to or sampled by the features. These + are generally unused here, as each sub-feature fetches its required + properties internally. Returns ------- - Image or list[Image] - The final output after `feature_1` and then `feature_2` have processed - the input. + Any or list[Any] + The final output after `feature_1` and then `feature_2` have + processed the input. """ @@ -2961,7 +2957,7 @@ class Stack(Feature): it is automatically converted into a list to maintain consistency in the output format. - If B is a feature, `Stack` can be visualized as:: + If B is a feature, `Stack` can be visualized as: >>> A >> Stack(B) = [*A(), *B()] From 62a450759dd5cbd7916d754e85e933d7778c87ee Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 24 Jun 2025 19:46:26 +0200 Subject: [PATCH 038/118] Update test_features.py --- deeptrack/tests/test_features.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index c0a71bf6b..e70c96576 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -564,7 +564,8 @@ def get(self, image, **kwargs): class Multiplication(features.Feature): """Simple feature that multiplies by a constant.""" def get(self, image, **kwargs): - # 'multiplier' is a property set via self.properties (default: 1). + # 'multiplier' is a property set via self.properties + # (default: 1). return image * self.properties.get("multiplier", 1)() A = Addition(addend=10) From 1b8a24c4f969959ff1216f8435063fecd903f4c0 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 25 Jun 2025 12:43:18 +0200 Subject: [PATCH 039/118] Update features.py --- deeptrack/features.py | 92 +++++++++++++++++++++++++++---------------- 1 file changed, 57 insertions(+), 35 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 080487632..91649d1b9 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -169,7 +169,7 @@ def merge_features( "Equal", "Stack", "Arguments", - "Probability", # TODO + "Probability", "Repeat", # TODO "Combine", # TODO "Slice", # TODO @@ -3184,7 +3184,7 @@ class Arguments(Feature): """ def get( - self: Feature, + self: Arguments, image: Any, **kwargs: Any, ) -> Any: @@ -3214,10 +3214,12 @@ def get( class Probability(StructuralFeature): """Resolve a feature with a certain probability. - This feature conditionally applies a given feature to an input image based - on a specified probability. A random number is sampled, and if it is less - than `probability`, the feature is resolved; otherwise, the input image - remains unchanged. + This feature conditionally applies a given feature to an input based on a + sampled uniform random number. If the sampled number is less than the + specified probability, the feature is resolved; otherwise, the input is + returned unchanged. + + To resample the decision, call `.update()` before evaluating the feature. Parameters ---------- @@ -3225,7 +3227,7 @@ class Probability(StructuralFeature): The feature to resolve conditionally. probability: PropertyLike[float] The probability (between 0 and 1) of resolving the feature. - *args: list[Any], optional + *args: Any, optional Positional arguments passed to the parent `StructuralFeature` class. **kwargs: Any, optional Additional keyword arguments passed to the parent `StructuralFeature` @@ -3233,87 +3235,107 @@ class Probability(StructuralFeature): Methods ------- - `get(image: np.ndarray, probability: float, random_number: float, **kwargs: dict[str, Any]) -> np.ndarray` + `get(image: Any, probability: float, random_number: float, **kwargs: Any) -> Any` Resolves the feature if the sampled random number is less than the specified probability. Examples -------- >>> import deeptrack as dt - >>> import numpy as np - In this example, the `Add` feature is applied to the input image with - a 70% chance. Define a feature and wrap it with `Probability`: + In this example, the `Add` feature is applied to the input image with a 70% + chance. + + Define a feature and wrap it with `Probability`: >>> add_feature = dt.Add(value=2) >>> probabilistic_feature = dt.Probability(add_feature, probability=0.7) Define an input image: - >>> input_image = np.ones((5, 5)) + >>> import numpy as np + >>> + >>> input_image = np.zeros((2, 3)) Apply the feature: + >>> probabilistic_feature.update() # Update the random number >>> output_image = probabilistic_feature(input_image) + With 70% probability, the output is: + >>> output_image + array([[2., 2., 2.], + [2., 2., 2.]]) + + With 30% probability, it remains: + >>> output_image + array([[0., 0., 0.], + [0., 0., 0.]]) + """ def __init__( - self: Feature, + self: Probability, feature: Feature, probability: PropertyLike[float], - *args: list[Any], + *args: Any, **kwargs: Any, ): """Initialize the Probability feature. + The random number is initialized when this feature is initialized. + It can be updated using the `update()` method. + Parameters ---------- feature: Feature The feature to resolve conditionally. probability: PropertyLike[float] The probability (between 0 and 1) of resolving the feature. - *args: list[Any], optional - Positional arguments passed to the parent `StructuralFeature` class. + *args: Any, optional + Positional arguments passed to the parent `StructuralFeature` + class. **kwargs: Any, optional - Additional keyword arguments passed to the parent `StructuralFeature` class. + Additional keyword arguments passed to the parent + `StructuralFeature` class. """ - + super().__init__( - *args, - probability=probability, - random_number=np.random.rand, + *args, + probability=probability, + random_number=np.random.rand, **kwargs, ) - self.feature = self.add_feature(feature) + self.feature = self.add_feature(feature) def get( - self: Feature, - image: np.ndarray, + self: Probability, + image: Any, probability: float, random_number: float, **kwargs: Any, - ) -> np.ndarray: - """Resolve the feature if a random number is less than the probability. + ) -> Any: + """Resolve the feature if random number is less than probability. Parameters ---------- - image: np.ndarray - The input image to process. + image: Any or list[Any] + The input to process. probability: float The probability (between 0 and 1) of resolving the feature. random_number: float - A random number sampled to determine whether to resolve the - feature. + A random number sampled to determine whether to resolve the + feature. It is initialized when this feature is initialized. + It can be updated using the `update()` method. **kwargs: Any - Additional arguments passed to the feature's `resolve` method. + Additional arguments passed to the feature's `resolve()` method. Returns ------- - np.ndarray - The processed image. If the feature is resolved, this is the output of the feature; - otherwise, it is the unchanged input image. + Any + The processed image. If the feature is resolved, this is the output + of the feature; otherwise, it is the unchanged input image. """ - + if random_number < probability: image = self.feature.resolve(image, **kwargs) From 66108d877df577aa994555d25ff05d1c27fd3eb9 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 25 Jun 2025 12:44:43 +0200 Subject: [PATCH 040/118] Update test_features.py --- deeptrack/tests/test_features.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index e70c96576..b14cca7b0 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -1139,7 +1139,7 @@ def test_Probability(self): feature = add_feature, probability=0.7 ) - + input_image = np.ones((5, 5)) applied_count = 0 @@ -1153,7 +1153,8 @@ def test_Probability(self): self.assertTrue(np.array_equal(output_image, input_image + 2)) observed_probability = applied_count / total_runs - self.assertTrue(0.65 <= observed_probability <= 0.75, f"Observed probability: {observed_probability}") + self.assertTrue(0.65 <= observed_probability <= 0.75, + f"Observed probability: {observed_probability}") def test_Repeat(self): From 5fbb7cd10225e5504192c80e05770eb763de69b4 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 25 Jun 2025 12:52:10 +0200 Subject: [PATCH 041/118] Update test_features.py --- deeptrack/tests/test_features.py | 50 +++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index b14cca7b0..c230a9bac 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -1132,30 +1132,66 @@ def test_Arguments_binding(self): def test_Probability(self): - np.random.seed(42) # Set seed for reproducibility + # Set seed for reproducibility of random trials + np.random.seed(42) + input_image = np.ones((5, 5)) add_feature = features.Add(value=2) + + # Helper: Check if feature was applied + def is_transformed(output): + return np.array_equal(output, input_image + 2) + + # 1. Test probabilistic application over many runs probabilistic_feature = features.Probability( - feature = add_feature, + feature=add_feature, probability=0.7 ) - input_image = np.ones((5, 5)) - applied_count = 0 total_runs = 300 for _ in range(total_runs): output_image = probabilistic_feature.update().resolve(input_image) - - if not np.array_equal(output_image, input_image): + if is_transformed(output_image): applied_count += 1 - self.assertTrue(np.array_equal(output_image, input_image + 2)) + else: + self.assertTrue(np.array_equal(output_image, input_image)) observed_probability = applied_count / total_runs self.assertTrue(0.65 <= observed_probability <= 0.75, f"Observed probability: {observed_probability}") + # 2. Edge case: probability = 0 (feature should never apply) + never_applied = features.Probability(feature=add_feature, + probability=0.0) + output = never_applied.update().resolve(input_image) + self.assertTrue(np.array_equal(output, input_image)) + + # 3. Edge case: probability = 1 (feature should always apply) + always_applied = features.Probability(feature=add_feature, + probability=1.0) + output = always_applied.update().resolve(input_image) + self.assertTrue(is_transformed(output)) + + # 4. Cached behavior: result is the same without update() + cached_feature = features.Probability(feature=add_feature, + probability=1.0) + output_1 = cached_feature.update().resolve(input_image) + output_2 = cached_feature.resolve(input_image) # same random number + self.assertTrue(np.array_equal(output_1, output_2)) + + # 5. Manual override: force behavior using random_number + manual = features.Probability(feature=add_feature, probability=0.5) + + # Should NOT apply (0.9 > 0.5) + output = manual.resolve(input_image, random_number=0.9) + self.assertTrue(np.array_equal(output, input_image)) + + # Should apply (0.1 < 0.5) + output = manual.resolve(input_image, random_number=0.1) + self.assertTrue(is_transformed(output)) + def test_Repeat(self): add_ten = features.Add(value=10) From 6c744fb1a300fac84302bc6d622e2bb035159d12 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 25 Jun 2025 12:56:11 +0200 Subject: [PATCH 042/118] Update features.py --- deeptrack/features.py | 35 +++++++++++++++-------------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 91649d1b9..137627d33 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -3227,9 +3227,9 @@ class Probability(StructuralFeature): The feature to resolve conditionally. probability: PropertyLike[float] The probability (between 0 and 1) of resolving the feature. - *args: Any, optional + *args: Any Positional arguments passed to the parent `StructuralFeature` class. - **kwargs: Any, optional + **kwargs: Any Additional keyword arguments passed to the parent `StructuralFeature` class. @@ -3289,10 +3289,10 @@ def __init__( The feature to resolve conditionally. probability: PropertyLike[float] The probability (between 0 and 1) of resolving the feature. - *args: Any, optional + *args: Any Positional arguments passed to the parent `StructuralFeature` class. - **kwargs: Any, optional + **kwargs: Any Additional keyword arguments passed to the parent `StructuralFeature` class. @@ -3382,21 +3382,16 @@ class Repeat(Feature): Define an `Add` feature that adds `10` to its input: >>> add_ten = dt.Add(value=10) - Apply this feature **3 times** using `Repeat`: + Apply this feature 3 times using `Repeat`: >>> pipeline = dt.Repeat(add_ten, N=3) Process an input list: - >>> print(pipeline.resolve([1, 2, 3])) + >>> pipeline.resolve([1, 2, 3]) [31, 32, 33] - Step-by-step breakdown: - - Iteration 1: `[1, 2, 3] + 10 → [11, 12, 13]` - - Iteration 2: `[11, 12, 13] + 10 → [21, 22, 23]` - - Iteration 3: `[21, 22, 23] + 10 → [31, 32, 33]` - Alternative shorthand using `^` operator: >>> pipeline = dt.Add(value=10) ^ 3 - >>> print(pipeline.resolve([1, 2, 3])) + >>> pipeline.resolve([1, 2, 3]) [31, 32, 33] """ @@ -3404,9 +3399,9 @@ class Repeat(Feature): __distributed__: bool = False def __init__( - self: Feature, - feature: Feature, - N: int, + self: Repeat, + feature: Feature, + N: int, **kwargs: Any, ): """Initialize the Repeat feature. @@ -3433,13 +3428,13 @@ def __init__( self.feature = self.add_feature(feature) def get( - self: Feature, + self: Repeat, image: Any, N: int, _ID: tuple[int, ...] = (), **kwargs: Any, ) -> Any: - """Sequentially apply the feature `N` times. + """Sequentially apply the feature N times. This method applies the feature `N` times, passing the output of each iteration as the input to the next. The `_ID` tuple is updated at @@ -3465,15 +3460,15 @@ def get( of the feature. """ - + for n in range(N): - index = _ID + (n,) # Track iteration index. + index = _ID + (n,) # Track iteration index image = self.feature( image, _ID=index, - replicate_index=index, # Pass replicate_index for legacy. + replicate_index=index, # Pass replicate_index for legacy ) return image From 5224632a532918d9f2dabdee03aa61e74f48784c Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 25 Jun 2025 12:56:39 +0200 Subject: [PATCH 043/118] Update test_features.py --- deeptrack/tests/test_features.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index c230a9bac..5f430c8ed 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -1203,8 +1203,8 @@ def test_Repeat(self): output_data = pipeline.resolve(input_data) - self.assertTrue(np.array_equal(output_data, expected_output), \ - f"Expected {expected_output}, got {output_data}") + self.assertTrue(np.array_equal(output_data, expected_output), + f"Expected {expected_output}, got {output_data}") pipeline_shorthand = features.Add(value=10) ^ 3 output_data_shorthand = pipeline_shorthand.resolve(input_data) From 4b127efaa39f988359a358c4b4c920daf9b6c299 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 25 Jun 2025 15:08:10 +0200 Subject: [PATCH 044/118] Update features.py --- deeptrack/features.py | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 137627d33..65f365c38 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -3342,8 +3342,8 @@ def get( return image -class Repeat(Feature): - """Applies a feature multiple times in sequence. +class Repeat(StructuralFeature): + """Apply a feature multiple times in sequence. The `Repeat` feature iteratively applies another feature, passing the output of each iteration as the input to the next. This enables chained @@ -3351,9 +3351,12 @@ class Repeat(Feature): number of repetitions is defined by `N`. Each iteration operates with its own set of properties, and the index of - the current iteration is accessible via `_ID` or `replicate_index`. - `_ID` is extended to include the current iteration index, ensuring - deterministic behavior when needed. + the current iteration is accessible via `_ID`. `_ID` is extended to include + the current iteration index, ensuring deterministic behavior when needed. + + This is equivalent to using the `^` operator: + + >>> dt.Repeat(A, 3) ≡ A ^ 3 Parameters ---------- @@ -3363,17 +3366,11 @@ class Repeat(Feature): The number of times to apply the feature in sequence. **kwargs: Any - Attributes - ---------- - __distributed__: bool - Always `False` for `Repeat`, since it processes sequentially rather - than distributing computation across inputs. - Methods ------- - `get(image: Any, N: int, _ID: tuple[int, ...], **kwargs: dict[str, Any]) -> Any` - Applies the feature `N` times in sequence, passing the output of each - iteration as the input to the next. + `get(image: Any, N: int, _ID: tuple[int, ...], **kwargs: Any) -> Any` + It applies the feature `N` times in sequence, passing the output of + each iteration as the input to the next. Examples -------- @@ -3396,8 +3393,6 @@ class Repeat(Feature): """ - __distributed__: bool = False - def __init__( self: Repeat, feature: Feature, @@ -3440,6 +3435,10 @@ def get( iteration as the input to the next. The `_ID` tuple is updated at each iteration, ensuring dynamic property updates and reproducibility. + Each iteration uses the output of the previous one. This makes `Repeat` + suitable for building recursive, cumulative, or progressive + transformations. + Parameters ---------- image: Any @@ -3461,6 +3460,9 @@ def get( """ + if not isinstance(N, int) or N < 0: + raise ValueError("N must be a non-negative integer.") + for n in range(N): index = _ID + (n,) # Track iteration index @@ -3468,7 +3470,7 @@ def get( image = self.feature( image, _ID=index, - replicate_index=index, # Pass replicate_index for legacy + replicate_index=index, # Legacy property ) return image From 0edf8fd305acb000ab9fc22e032dcde9b8d30dc5 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 25 Jun 2025 15:21:12 +0200 Subject: [PATCH 045/118] Update test_features.py --- deeptrack/tests/test_features.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index 5f430c8ed..ad032d007 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -1194,24 +1194,25 @@ def is_transformed(output): def test_Repeat(self): + # Define a simple feature and pipeline add_ten = features.Add(value=10) - pipeline = features.Repeat(add_ten, N=3) input_data = [1, 2, 3] expected_output = [31, 32, 33] + # Test standard Repeat behavior output_data = pipeline.resolve(input_data) + self.assertEqual(output_data, expected_output) - self.assertTrue(np.array_equal(output_data, expected_output), - f"Expected {expected_output}, got {output_data}") - + # Test shorthand syntax (^) produces same result pipeline_shorthand = features.Add(value=10) ^ 3 output_data_shorthand = pipeline_shorthand.resolve(input_data) + self.assertEqual(output_data_shorthand, expected_output) - self.assertTrue(np.array_equal(output_data_shorthand, expected_output), \ - f"Shorthand failed. Expected {expected_output}, \ - got {output_data_shorthand}") + # Test dynamic override of N + output_override = pipeline(input_data, N=2) + self.assertEqual(output_override, [21, 22, 23]) def test_Combine(self): From c8279355c562eaa08d652c1774a3292a6d710c2c Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 25 Jun 2025 15:21:14 +0200 Subject: [PATCH 046/118] Update features.py --- deeptrack/features.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 65f365c38..13d56c41c 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -170,7 +170,7 @@ def merge_features( "Stack", "Arguments", "Probability", - "Repeat", # TODO + "Repeat", "Combine", # TODO "Slice", # TODO "Bind", # TODO From 6f65d74c567560d944d35f3eaa048d69e3989e70 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 25 Jun 2025 15:22:28 +0200 Subject: [PATCH 047/118] Update features.py --- deeptrack/features.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/deeptrack/features.py b/deeptrack/features.py index 13d56c41c..536a1b574 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -532,6 +532,8 @@ def __call__( | list[torch.Tensor] | Image | list[Image] + | Any + | list[Any] | None ) = None, _ID: tuple[int, ...] = (), From 63093f91e66d5fb4f35e320e1f46c6437e35b3ba Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 25 Jun 2025 15:23:13 +0200 Subject: [PATCH 048/118] Update test_features.py --- deeptrack/tests/test_features.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index ad032d007..6d1fa15f3 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -1807,7 +1807,7 @@ def test_OneOfDict(self): output_image = controlled_feature.resolve(input_image) expected_output = input_image * 2 # The "multiply" feature should be applied self.assertTrue(np.array_equal(output_image, expected_output)) - + def test_LoadImage(self): from tempfile import NamedTemporaryFile From 3f8a9cf90650d82224cc89f9a72a5980d789f292 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 25 Jun 2025 15:49:14 +0200 Subject: [PATCH 049/118] Update features.py --- deeptrack/features.py | 46 +++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 536a1b574..1fd389454 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -3482,50 +3482,57 @@ class Combine(StructuralFeature): """Combine multiple features into a single feature. This feature sequentially resolves a list of features and returns their - results as a list. Each feature in the `features` parameter operates on - the same input, and their outputs are aggregated into a single list. + results as a list. Parameters ---------- features: list[Feature] A list of features to combine. Each feature will be resolved in the - order they appear in the list. - **kwargs: Any, optional + order they appear in the list and their outputs aggregated into a + single list to be returned. + **kwargs: Any Additional keyword arguments passed to the parent `StructuralFeature` class. Methods ------- - `get(image_list: Any, **kwargs: dict[str, Any]) -> list[Any]` + `get(image_list: Any, **kwargs: Any) -> list[Any]` Resolves each feature in the `features` list on the input image and returns their results as a list. Examples -------- >>> import deeptrack as dt - >>> import numpy as np - Define a list of features to combine `GaussianBlur` and `Add`: - >>> blur_feature = dt.GaussianBlur(sigma=2) - >>> add_feature = dt.Add(value=10) + Define a list of features: + >>> add_1 = dt.Add(value=1) + >>> add_2 = dt.Add(value=2) + >>> add_3 = dt.Add(value=3) Combine the features: - >>> combined_feature = dt.Combine([blur_feature, add_feature]) + >>> combined_feature = dt.Combine([add_1, add_2, add_3]) Define an input image: - >>> input_image = np.ones((10, 10)) + >>> import numpy as np + >>> + >>> input_image = np.zeros((2, 3)) Apply the combined feature: >>> output_list = combined_feature(input_image) + >>> output_list + [array([[1., 1., 1.], + [1., 1., 1.]]), + array([[2., 2., 2.], + [2., 2., 2.]]), + array([[3., 3., 3.], + [3., 3., 3.]])] """ - __distributed__: bool = False - def __init__( - self: Feature, - features: list[Feature], - **kwargs: dict[str, Any] + self: Feature, + features: list[Feature], + **kwargs: Any, ): """Initialize the Combine feature. @@ -3534,19 +3541,20 @@ def __init__( features: list[Feature] A list of features to combine. Each feature is added as a dependency to ensure proper execution in the computation graph. - **kwargs: Any, optional + **kwargs: Any Additional keyword arguments passed to the parent `StructuralFeature` class. """ super().__init__(**kwargs) + self.features = [self.add_feature(f) for f in features] def get( - self: Feature, + self: Feature, image_list: Any, - **kwargs: dict[str, Any] + **kwargs: Any, ) -> list[Any]: """Resolve each feature in the `features` list on the input image. From 3af492b03a7c430b10262eb0a3fbb69aaa3fa3db Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 25 Jun 2025 19:20:24 +0200 Subject: [PATCH 050/118] Update features.py --- deeptrack/features.py | 75 +++++++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 32 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 1fd389454..5d5c50c3e 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -171,7 +171,7 @@ def merge_features( "Arguments", "Probability", "Repeat", - "Combine", # TODO + "Combine", "Slice", # TODO "Bind", # TODO "BindResolve", # TODO @@ -1493,7 +1493,7 @@ def __getitem__( """Allows direct slicing of the feature's output. """ - + if not isinstance(slices, tuple): slices = (slices,) @@ -3481,22 +3481,22 @@ def get( class Combine(StructuralFeature): """Combine multiple features into a single feature. - This feature sequentially resolves a list of features and returns their - results as a list. + This feature applies a list of features to the same input and returns their + outputs as a list. It is useful for computing multiple parallel outputs + from the same data (e.g., branches in a feature graph). Parameters ---------- features: list[Feature] - A list of features to combine. Each feature will be resolved in the - order they appear in the list and their outputs aggregated into a - single list to be returned. + A list of features to combine. Each feature will be applied in order, + and their outputs collected into a list. **kwargs: Any Additional keyword arguments passed to the parent `StructuralFeature` class. Methods ------- - `get(image_list: Any, **kwargs: Any) -> list[Any]` + `get(image: Any, **kwargs: Any) -> list[Any]` Resolves each feature in the `features` list on the input image and returns their results as a list. @@ -3530,7 +3530,7 @@ class Combine(StructuralFeature): """ def __init__( - self: Feature, + self: Combine, features: list[Feature], **kwargs: Any, ): @@ -3552,15 +3552,15 @@ def __init__( self.features = [self.add_feature(f) for f in features] def get( - self: Feature, - image_list: Any, + self: Combine, + image: Any, **kwargs: Any, ) -> list[Any]: """Resolve each feature in the `features` list on the input image. Parameters ---------- - image_list: Any + image: Any The input image or list of images to process. **kwargs: Any Additional arguments passed to each feature's `resolve` method. @@ -3572,16 +3572,16 @@ def get( """ - return [f(image_list, **kwargs) for f in self.features] + return [f(image, **kwargs) for f in self.features] class Slice(Feature): - """Dynamically applies array indexing to input Image(s). + """Dynamically applies array indexing to inputs. - This feature allows **dynamic slicing** of an image using integer indices, - slice objects, or ellipses (`...`). While normal array indexing is preferred - for static cases, `Slice` is useful when the slicing parameters **must be - computed dynamically** based on other properties. + This feature allows dynamic slicing of an image using integer indices, + slice objects, or ellipses (`...`). While normal array indexing is + preferred for static cases, `Slice` is useful when the slicing parameters + *must be computed dynamically based on other properties. Parameters ---------- @@ -3599,22 +3599,31 @@ class Slice(Feature): Examples -------- >>> import deeptrack as dt - >>> import numpy as np - **Recommended Approach: Use Normal Indexing for Static Slicing** + Recommended approach: Use normal indexing for static slicing: + >>> import numpy as np + >>> >>> feature = dt.DummyFeature() - >>> static_slicing = feature[:, 1:2, ::-2] + >>> static_slicing = feature[0:2, ::2, :] >>> result = static_slicing.resolve(np.arange(27).reshape((3, 3, 3))) - >>> print(result) - - **Using `Slice` for Dynamic Slicing (when necessary)** - If slices depend on computed properties, use `Slice`: + >>> result + array([[[ 0, 1, 2], + [ 6, 7, 8]], + [[ 9, 10, 11], + [15, 16, 17]]]) + + Using `Slice` for dynamic slicing (when necessary when slices depend on + computed properties): >>> feature = dt.DummyFeature() >>> dynamic_slicing = feature >> dt.Slice( - ... slices=(slice(None), slice(1, 2), slice(None, None, -2)) + ... slices=(slice(0, 2), slice(None, None, 2), slice(None)) ... ) >>> result = dynamic_slicing.resolve(np.arange(27).reshape((3, 3, 3))) - >>> print(result) + >>> result + array([[[ 0, 1, 2], + [ 6, 7, 8]], + [[ 9, 10, 11], + [15, 16, 17]]]) In both cases, slices can be defined dynamically based on feature properties. @@ -3622,10 +3631,12 @@ class Slice(Feature): """ def __init__( - self: Feature, + self: Slice, slices: PropertyLike[ Iterable[ - PropertyLike[int] | PropertyLike[slice] | PropertyLike[...] + PropertyLike[int] + | PropertyLike[slice] + | PropertyLike[...] ] ], **kwargs: Any, @@ -3645,7 +3656,7 @@ def __init__( super().__init__(slices=slices, **kwargs) def get( - self: Feature, + self: Slice, image: np.ndarray, slices: tuple[Any, ...] | Any, **kwargs: Any, @@ -3671,10 +3682,10 @@ def get( """ try: - # Convert slices to a tuple if possible. + # Convert slices to a tuple if possible slices = tuple(slices) except ValueError: - # Leave slices as is if conversion fails. + # Leave slices as is if conversion fails pass return image[slices] From ae805eec566faa6cc45631a288d72a1dbbf061b5 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 25 Jun 2025 19:20:27 +0200 Subject: [PATCH 051/118] Update test_features.py --- deeptrack/tests/test_features.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index 6d1fa15f3..18f463de0 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -1224,17 +1224,17 @@ def test_Combine(self): input_image = np.ones((10, 10)) output_list = combined_feature.resolve(input_image) - self.assertTrue(isinstance(output_list, list), "Output should be a list") - self.assertTrue(len(output_list) == 2, "Output list should contain results of both features") + self.assertTrue(isinstance(output_list, list)) + self.assertTrue(len(output_list) == 2) for output in output_list: - self.assertTrue(output.shape == input_image.shape, "Output shape mismatch") + self.assertTrue(output.shape == input_image.shape) noisy_image = output_list[0] added_image = output_list[1] - self.assertFalse(np.all(noisy_image == 1), "Gaussian noise was not applied") - self.assertTrue(np.allclose(added_image, input_image + 10), "Add operation failed") + self.assertFalse(np.all(noisy_image == 1)) + self.assertTrue(np.allclose(added_image, input_image + 10)) def test_Slice_constant(self): From 7a5b83fe01364b2a5874bdd545f1b7836b96ac7c Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Fri, 27 Jun 2025 11:21:11 +0200 Subject: [PATCH 052/118] Update features.py --- deeptrack/features.py | 52 ++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 5d5c50c3e..455aafb55 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -1491,14 +1491,13 @@ def __getitem__( slices: Any, ) -> 'Feature': """Allows direct slicing of the feature's output. - + """ if not isinstance(slices, tuple): slices = (slices,) - # We make it a list to ensure that each element is sampled - # independently. + # Make it a list to ensure that each element is sampled independently. slices = list(slices) return self >> Slice(slices) @@ -3577,15 +3576,17 @@ def get( class Slice(Feature): """Dynamically applies array indexing to inputs. - + This feature allows dynamic slicing of an image using integer indices, - slice objects, or ellipses (`...`). While normal array indexing is - preferred for static cases, `Slice` is useful when the slicing parameters - *must be computed dynamically based on other properties. + slice objects, or ellipses (`...`). + + While normal array indexing is preferred for static cases, `Slice` is + useful when the slicing parameters must be computed dynamically based on + other properties. Parameters ---------- - slices: Iterable[int | slice | ...] + slices: tuple[int or slice or ellipsis] or list[int or slice or ellipsis] The slicing instructions for each dimension. Each element corresponds to a dimension in the input image. **kwargs: Any @@ -3593,7 +3594,7 @@ class Slice(Feature): Methods ------- - `get(image: np.ndarray, slices: tuple[int | slice | ...], **kwargs: dict[str, Any]) -> np.ndarray` + `get(image: array or list[array], slices: Iterable[int or slice or ellipsis], **kwargs: Any) -> array or list[array]` Applies the specified slices to the input image. Examples @@ -3632,20 +3633,14 @@ class Slice(Feature): def __init__( self: Slice, - slices: PropertyLike[ - Iterable[ - PropertyLike[int] - | PropertyLike[slice] - | PropertyLike[...] - ] - ], + slices: PropertyLike[Iterable[int | slice | Ellipsis]], **kwargs: Any, ): """Initialize the Slice feature. Parameters ---------- - slices: list[int | slice | ...] or tuple[int | slice | ...] + slices: Iterable[int or slice or ellipsis] The slicing instructions for each dimension, specified as a list or tuple of integers, slice objects, or ellipses (`...`). **kwargs: Any @@ -3657,27 +3652,28 @@ def __init__( def get( self: Slice, - image: np.ndarray, - slices: tuple[Any, ...] | Any, + image: ArrayLike[Any] | list[ArrayLike[Any]], + slices: slice | tuple[int | slice | Ellipsis, ...], **kwargs: Any, - ): + ) -> ArrayLike[Any] | list[ArrayLike[Any]]: """Apply the specified slices to the input image. Parameters ---------- - image: np.ndarray - The input image to be sliced. - slices: tuple[int | slice | ellipsis, ...] | int | slice | ellipsis - The slicing instructions for the input image. Each element in the - tuple corresponds to a dimension in the input image. If a single - element is provided, it is converted to a tuple. + image: array or list[array] + The input image(s) to be sliced. + slices: slice ellipsis or tuple[int or slice or ellipsis, ...] + The slicing instructions for the input image. Typically it is a + tuple. Each element in the tuple corresponds to a dimension in the + input image. If a single element is provided, it is converted to a + tuple. **kwargs: Any Additional keyword arguments (unused in this implementation). Returns ------- - np.ndarray - The sliced image. + array or list[array] + The sliced image(s). """ From 1cf1973736544e9585678e0945161fe43c4e7b41 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Fri, 27 Jun 2025 11:21:13 +0200 Subject: [PATCH 053/118] Update test_features.py --- deeptrack/tests/test_features.py | 59 +++++++++++++++++--------------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index 18f463de0..ec4002094 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -1238,43 +1238,45 @@ def test_Combine(self): def test_Slice_constant(self): - input = np.arange(9).reshape((3, 3)) + image = np.arange(9).reshape((3, 3)) A = features.DummyFeature() + A0 = A[0] + a0 = A0.resolve(image) + self.assertEqual(a0.tolist(), image[0].tolist()) + A1 = A[1] - A22 = A[2, 2] - A12 = A[1, lambda: -1] + a1 = A1.resolve(image) + self.assertEqual(a1.tolist(), image[1].tolist()) - a0 = A0.resolve(input) - a1 = A1.resolve(input) - a22 = A22.resolve(input) - a12 = A12.resolve(input) + A22 = A[2, 2] + a22 = A22.resolve(image) + self.assertEqual(a22, image[2, 2]) - self.assertEqual(a0.tolist(), input[0].tolist()) - self.assertEqual(a1.tolist(), input[1].tolist()) - self.assertEqual(a22, input[2, 2]) - self.assertEqual(a12, input[1, -1]) + A12 = A[1, lambda: -1] + a12 = A12.resolve(image) + self.assertEqual(a12, image[1, -1]) def test_Slice_colon(self): - input = np.arange(16).reshape((4, 4)) A = features.DummyFeature() A0 = A[0, :1] - A1 = A[1, lambda: 0 : lambda: 4 : lambda: 2] - A2 = A[lambda: slice(0, 4, 1), 2] - A3 = A[lambda: 0 : lambda: 2, :] - a0 = A0.resolve(input) - a1 = A1.resolve(input) - a2 = A2.resolve(input) - a3 = A3.resolve(input) - self.assertEqual(a0.tolist(), input[0, :1].tolist()) + + A1 = A[1, lambda: 0 : lambda: 4 : lambda: 2] + a1 = A1.resolve(input) self.assertEqual(a1.tolist(), input[1, 0:4:2].tolist()) + + A2 = A[lambda: slice(0, 4, 1), 2] + a2 = A2.resolve(input) self.assertEqual(a2.tolist(), input[:, 2].tolist()) + + A3 = A[lambda: 0 : lambda: 2, :] + a3 = A3.resolve(input) self.assertEqual(a3.tolist(), input[0:2, :].tolist()) def test_Slice_ellipse(self): @@ -1284,18 +1286,19 @@ def test_Slice_ellipse(self): A = features.DummyFeature() A0 = A[..., :1] - A1 = A[..., lambda: 0 : lambda: 4 : lambda: 2] - A2 = A[lambda: slice(0, 4, 1), ...] - A3 = A[lambda: 0 : lambda: 2, lambda: ...] - a0 = A0.resolve(input) - a1 = A1.resolve(input) - a2 = A2.resolve(input) - a3 = A3.resolve(input) - self.assertEqual(a0.tolist(), input[..., :1].tolist()) + + A1 = A[..., lambda: 0 : lambda: 4 : lambda: 2] + a1 = A1.resolve(input) self.assertEqual(a1.tolist(), input[..., 0:4:2].tolist()) + + A2 = A[lambda: slice(0, 4, 1), ...] + a2 = A2.resolve(input) self.assertEqual(a2.tolist(), input[:, ...].tolist()) + + A3 = A[lambda: 0 : lambda: 2, lambda: ...] + a3 = A3.resolve(input) self.assertEqual(a3.tolist(), input[0:2, ...].tolist()) def test_Slice_static_dynamic(self): From 27a76b13113993038fd7c838f063f5bba65d20f6 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Fri, 27 Jun 2025 11:21:42 +0200 Subject: [PATCH 054/118] Update features.py --- deeptrack/features.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 455aafb55..bc4cb4dc1 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -172,7 +172,7 @@ def merge_features( "Probability", "Repeat", "Combine", - "Slice", # TODO + "Slice", "Bind", # TODO "BindResolve", # TODO "BindUpdate", # TODO From 927f6ab3f3f0d1fd1797caebb1170c61e8a3f3ed Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Fri, 27 Jun 2025 11:28:07 +0200 Subject: [PATCH 055/118] Update features.py --- deeptrack/features.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index bc4cb4dc1..10b7b3670 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -6689,34 +6689,35 @@ class Transpose(Feature): axes: tuple[int, ...], optional A tuple specifying the permutation of the axes. If `None`, the axes are reversed by default. - **kwargs:: dict of str to Any + **kwargs: Any Additional keyword arguments passed to the parent `Feature` class. Methods ------- - `get(image: np.ndarray, axes: tuple[int, ...] | None, **kwargs: dict[str, Any]) -> np.ndarray` + `get(image: array or list[array], axes: tuple[int, ...] | None, **kwargs: Any) -> array or list[array]` Transpose the axes of the input image Examples -------- >>> import deeptrack as dt - >>> import numpy as np Create an input array: + >>> import numpy as np + >>> >>> input_image = np.random.rand(2, 3, 4) - >>> print(input_image.shape) + >>> input_image.shape (2, 3, 4) Apply a Transpose feature: >>> transpose_feature = dt.Transpose(axes=(1, 2, 0)) >>> output_image = transpose_feature(input_image) - >>> print(output_image.shape) + >>> output_image.shape (3, 4, 2) Without specifying axes: >>> transpose_feature = dt.Transpose() >>> output_image = transpose_feature(input_image) - >>> print(output_image.shape) + >>> output_image.shape (4, 3, 2) """ @@ -6733,7 +6734,7 @@ def __init__( axes: tuple[int, ...], optional A tuple specifying the permutation of the axes. If `None`, the axes are reversed by default. - **kwargs:: dict of str to Any + **kwargs:: Any Additional keyword arguments passed to the parent `Feature` class. """ @@ -6742,15 +6743,15 @@ def __init__( def get( self: Transpose, - image: np.ndarray, + image: ArrayLike[Any] | list[ArrayLike[Any]], axes: tuple[int, ...] | None = None, **kwargs: Any, - ) -> np.ndarray: + ) -> ArrayLike[Any] | list[ArrayLike[Any]]: """Transpose the axes of the input image. Parameters ---------- - image: np.ndarray + image: array or list[array] The input image to process. axes: tuple[int, ...], optional A tuple specifying the permutation of the axes. If `None`, the @@ -6760,12 +6761,12 @@ def get( Returns ------- - np.ndarray + array or list[array] The transposed image with rearranged axes. """ - return np.transpose(image, axes) + return xp.transpose(image, axes) Permute = Transpose From 7040b1c223cdccc2a9af7f52cf20ec57ae1652cd Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Fri, 27 Jun 2025 11:47:26 +0200 Subject: [PATCH 056/118] Update test_features.py --- deeptrack/tests/test_features.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index ec4002094..7225fa338 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -23,6 +23,9 @@ from deeptrack.image import Image from deeptrack.noises import Gaussian +if TORCH_AVAILABLE: + import torch + def grid_test_features( tester, feature_a, @@ -629,8 +632,6 @@ def test_DummyFeature(self): # Test with PyTorch if TORCH_AVAILABLE: - import torch - # Test with PyTorch tensor tensor = torch.ones(4, 4) self.assertIs(feature.get(tensor), tensor) @@ -702,8 +703,6 @@ def test_Value(self): # PyTorch tensor value tests if TORCH_AVAILABLE: - import torch - tensor = torch.tensor([1., 2., 3.]) value_tensor = features.Value(value=tensor) self.assertTrue(torch.equal(value_tensor(), tensor)) @@ -776,8 +775,6 @@ def test_ArithmeticOperationFeature(self): # PyTorch tensor input (if available) if TORCH_AVAILABLE: - import torch - addition_feature = features.ArithmeticOperationFeature( lambda a, b: a + b, value=5, ) @@ -956,8 +953,6 @@ def test_Stack(self): # Stack PyTorch tensors if TORCH_AVAILABLE: - import torch - t1 = torch.tensor([1, 2]) t2 = torch.tensor([3, 4]) feature = features.Stack(value=t2) @@ -2401,6 +2396,9 @@ def test_Transpose(self): output_image = transpose_feature(input_image) self.assertEqual(output_image.shape, (4, 3, 2)) + if TORCH_AVAILABLE: + pass + def test_OneHot(self): From bfd68521b48e0181a2b931513002082d40193011 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Fri, 27 Jun 2025 11:47:28 +0200 Subject: [PATCH 057/118] Update features.py --- deeptrack/features.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 10b7b3670..d63c837b5 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -6694,8 +6694,8 @@ class Transpose(Feature): Methods ------- - `get(image: array or list[array], axes: tuple[int, ...] | None, **kwargs: Any) -> array or list[array]` - Transpose the axes of the input image + `get(image: array, axes: tuple[int, ...] | None, **kwargs: Any) -> array` + Transpose the axes of the input image(s). Examples -------- @@ -6743,15 +6743,15 @@ def __init__( def get( self: Transpose, - image: ArrayLike[Any] | list[ArrayLike[Any]], + image: ArrayLike[Any], axes: tuple[int, ...] | None = None, **kwargs: Any, - ) -> ArrayLike[Any] | list[ArrayLike[Any]]: + ) -> ArrayLike[Any]: """Transpose the axes of the input image. Parameters ---------- - image: array or list[array] + image: array The input image to process. axes: tuple[int, ...], optional A tuple specifying the permutation of the axes. If `None`, the @@ -6761,7 +6761,7 @@ def get( Returns ------- - array or list[array] + array The transposed image with rearranged axes. """ From f90f2c113afcf65adea8b4e66a5b3dcf20218212 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Fri, 27 Jun 2025 12:32:57 +0200 Subject: [PATCH 058/118] Update features.py --- deeptrack/features.py | 87 +++++++++++++++++++++++++------------------ 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index d63c837b5..76f0fe6b0 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -123,6 +123,7 @@ def merge_features( from __future__ import annotations +import array_api_compat as apc import itertools import operator import random @@ -193,8 +194,8 @@ def merge_features( "Unsqueeze", # TODO "ExpandDims", # TODO "MoveAxis", # TODO - "Transpose", # TODO - "Permute", # TODO + "Transpose", + "Permute", "OneHot", # TODO "TakeProperties", # TODO ] @@ -1764,7 +1765,7 @@ class Chain(StructuralFeature): Methods ------- - `get(image: Any, _ID: tuple[int, ...], **kwargs: Any) -> Any | list[Any]` + `get(image: Any, _ID: tuple[int, ...], **kwargs: Any) -> Any` Apply the two features in sequence on the given input image. Examples @@ -1825,10 +1826,10 @@ def __init__( def get( self: Feature, - image: Any | list[Any], + image: Any, _ID: tuple[int, ...] = (), **kwargs: Any, - ) -> Any | list[Any]: + ) -> Any: """Apply the two features sequentially to the given input image(s). This method first applies `feature_1` to the input image(s) and then @@ -1836,10 +1837,9 @@ def get( Parameters ---------- - image: Any or list[Any] + image: Any The input data to transform sequentially. Most typically, this is - a NumPy array, a PyTorch tensor, or an Image, or a list of the - same. + a NumPy array, a PyTorch tensor, or an Image. _ID: tuple[int, ...], optional A unique identifier for caching or parallel execution. It defaults to an empty tuple. @@ -1850,7 +1850,7 @@ def get( Returns ------- - Any or list[Any] + Any The final output after `feature_1` and then `feature_2` have processed the input. @@ -1973,7 +1973,7 @@ class Value(Feature): Methods ------- - `get(image: Any, value: float, **kwargs: Any) -> float` + `get(image: Any, value: float, **kwargs: Any) -> float or array` Returns the stored value, ignoring the input image. Examples @@ -2054,9 +2054,9 @@ def __init__( def get( self: Value, image: Any, - value: float | ArrayLike, + value: float | ArrayLike[Any], **kwargs: Any, - ) -> float | ArrayLike: + ) -> float | ArrayLike[Any]: """Return the stored value, ignoring the input image. The `get` method simply returns the stored numerical value, allowing @@ -2114,7 +2114,7 @@ class ArithmeticOperationFeature(Feature): Methods ------- - `get(image: Any or list[Any], value: float or int or list[float or int], **kwargs: Any) -> list[Any]` + `get(image: Any, value: float or int or list[float or int], **kwargs: Any) -> list[Any]` Apply the arithmetic operation element-wise to the input data. Examples @@ -2171,7 +2171,7 @@ def __init__( def get( self: ArithmeticOperationFeature, - image: Any | list[Any], + image: Any, value: float | int | ArrayLike | list[float | int | ArrayLike], **kwargs: Any, ) -> list[Any]: @@ -6598,31 +6598,33 @@ class MoveAxis(Feature): Parameters ---------- source: int - The axis to move. + The source position of the axis to move. destination: int The destination position of the axis. - **kwargs:: dict of str to Any + **kwargs:: Any Additional keyword arguments passed to the parent `Feature` class. Methods ------- - `get(image: np.ndarray, source: int, destination: int, **kwargs: dict[str, Any]) -> np.ndarray` - Move the specified axis of the input image to a new position. + `get(image: array, source: int, destination: int, **kwargs: Any) -> array` + Move the specified axis of the input image to a new position. The input + and output array can be a NumPy array, a PyTorch tensor, or an Image. Examples -------- >>> import deeptrack as dt - >>> import numpy as np Create an input array: + >>> import numpy as np + >>> >>> input_image = np.random.rand(2, 3, 4) - >>> print(input_image.shape) + >>> input_image.shape (2, 3, 4) Apply a MoveAxis feature: >>> move_axis_feature = dt.MoveAxis(source=0, destination=2) >>> output_image = move_axis_feature(input_image) - >>> print(output_image.shape) + >>> output_image.shape (3, 4, 2) """ @@ -6641,7 +6643,7 @@ def __init__( The axis to move. destination: int The destination position of the axis. - **kwargs:: dict of str to Any + **kwargs: Any Additional keyword arguments passed to the parent `Feature` class. """ @@ -6650,31 +6652,41 @@ def __init__( def get( self: MoveAxis, - image: np.ndarray, + image: NDArray | torch.Tensor | Image, source: int, - destination: int, + destination: int, **kwargs: Any, - ) -> np.ndarray: + ) -> NDArray | torch.Tensor | Image: """Move the specified axis of the input image to a new position. Parameters ---------- - image: np.ndarray - The input image to process. + image: array + The input image to process. The input array can be a NumPy array, a + PyTorch tensor, or an Image. source: int The axis to move. destination: int The destination position of the axis. - **kwargs:: dict of str to Any + **kwargs: Any Additional keyword arguments (unused here). Returns ------- - np.ndarray + array The input image with the specified axis moved to the destination. + The output array can be a NumPy array, a PyTorch tensor, or an + Image. + """ - return np.moveaxis(image, source, destination) + if apc.is_torch_array(image): + axes = list(range(image.ndim)) + axis = axes.pop(source) + axes.insert(destination, axis) + return image.permute(*axes) + + return xp.moveaxis(image, source, destination) class Transpose(Feature): @@ -6695,7 +6707,8 @@ class Transpose(Feature): Methods ------- `get(image: array, axes: tuple[int, ...] | None, **kwargs: Any) -> array` - Transpose the axes of the input image(s). + Transpose the axes of the input image(s). The input and output array + can be a NumPy array, a PyTorch tensor, or an Image. Examples -------- @@ -6734,7 +6747,7 @@ def __init__( axes: tuple[int, ...], optional A tuple specifying the permutation of the axes. If `None`, the axes are reversed by default. - **kwargs:: Any + **kwargs: Any Additional keyword arguments passed to the parent `Feature` class. """ @@ -6743,16 +6756,17 @@ def __init__( def get( self: Transpose, - image: ArrayLike[Any], + image: NDArray | torch.Tensor | Image, axes: tuple[int, ...] | None = None, **kwargs: Any, - ) -> ArrayLike[Any]: + ) -> NDArray | torch.Tensor | Image: """Transpose the axes of the input image. Parameters ---------- image: array - The input image to process. + The input image to process. The input array can be a NumPy array, a + PyTorch tensor, or an Image. axes: tuple[int, ...], optional A tuple specifying the permutation of the axes. If `None`, the axes are reversed by default. @@ -6762,7 +6776,8 @@ def get( Returns ------- array - The transposed image with rearranged axes. + The transposed image with rearranged axes. The output array can be + a NumPy array, a PyTorch tensor, or an Image. """ From f25fc1ceed8a1f912d637bb65975dd16e07c5d46 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Fri, 27 Jun 2025 12:32:59 +0200 Subject: [PATCH 059/118] Update test_features.py --- deeptrack/tests/test_features.py | 52 ++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index 7225fa338..467495800 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -2376,28 +2376,74 @@ def test_Unsqueeze(self): def test_MoveAxis(self): - + ### Test with NumPy array input_image = np.random.rand(2, 3, 4) move_axis_feature = features.MoveAxis(source=0, destination=2) output_image = move_axis_feature(input_image) self.assertEqual(output_image.shape, (3, 4, 2)) + ### Test with Image + input_data = np.random.rand(2, 3, 4) + input_image = features.Image(input_data) - def test_Transpose(self): + move_axis_feature = features.MoveAxis(source=0, destination=2) + output_image = move_axis_feature(input_image) + self.assertEqual(output_image.shape, (3, 4, 2)) + + ### Test with PyTorch tensor (if available) + if TORCH_AVAILABLE: + input_tensor = torch.rand(2, 3, 4) + + move_axis_feature = features.MoveAxis(source=0, destination=2) + output_tensor = move_axis_feature(input_tensor) + print(output_tensor.shape) + self.assertEqual(output_tensor.shape, (3, 4, 2)) + + def test_Transpose(self): + ### Test with NumPy array input_image = np.random.rand(2, 3, 4) + # Explicit axes transpose_feature = features.Transpose(axes=(1, 2, 0)) output_image = transpose_feature(input_image) self.assertEqual(output_image.shape, (3, 4, 2)) + expected_output = np.transpose(input_image, (1, 2, 0)) + self.assertTrue(np.allclose(output_image, expected_output)) + # Reversed axes transpose_feature = features.Transpose() output_image = transpose_feature(input_image) self.assertEqual(output_image.shape, (4, 3, 2)) + expected_output = np.transpose(input_image) + self.assertTrue(np.allclose(output_image, expected_output)) + + ### Test with Image + input_data = np.random.rand(2, 3, 4) + input_image = features.Image(input_data) + + transpose_feature = features.Transpose(axes=(1, 2, 0)) + output_image = transpose_feature(input_image) + self.assertEqual(output_image.shape, (3, 4, 2)) + ### Test with PyTorch tensor (if available) if TORCH_AVAILABLE: - pass + input_tensor = torch.rand(2, 3, 4) + + # Explicit axes + transpose_feature = features.Transpose(axes=(1, 2, 0)) + output_tensor = transpose_feature(input_tensor) + self.assertEqual(output_tensor.shape, (3, 4, 2)) + expected_tensor = input_tensor.permute(1, 2, 0) + self.assertTrue(torch.allclose(output_tensor, expected_tensor)) + + # Reversed axes + transpose_feature = features.Transpose() + output_tensor = transpose_feature(input_tensor) + self.assertEqual(output_tensor.shape, (4, 3, 2)) + expected_tensor = input_tensor.permute(2, 1, 0) + self.assertTrue(torch.allclose(output_tensor, expected_tensor)) def test_OneHot(self): From 65773d0ee2e03373e5faf130f76643a6a32388f7 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Fri, 27 Jun 2025 12:39:49 +0200 Subject: [PATCH 060/118] Update features.py --- deeptrack/features.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 76f0fe6b0..8f0dc58fa 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -193,7 +193,7 @@ def merge_features( "Squeeze", # TODO "Unsqueeze", # TODO "ExpandDims", # TODO - "MoveAxis", # TODO + "MoveAxis", "Transpose", "Permute", "OneHot", # TODO From 155dcf35dd8bdba5bd58db8313355bf5f23d0616 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Fri, 27 Jun 2025 12:48:49 +0200 Subject: [PATCH 061/118] Update features.py --- deeptrack/features.py | 59 +++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 8f0dc58fa..0d69d4488 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -190,9 +190,9 @@ def merge_features( "Upscale", # TODO "NonOverlapping", # TODO "Store", # TODO - "Squeeze", # TODO - "Unsqueeze", # TODO - "ExpandDims", # TODO + "Squeeze", + "Unsqueeze", + "ExpandDims", "MoveAxis", "Transpose", "Permute", @@ -6494,7 +6494,7 @@ def get( class Unsqueeze(Feature): - """Unsqueezes the input image to the smallest possible dimension. + """Unsqueeze the input image to the smallest possible dimension. This feature adds new singleton dimensions to the input image at the specified axis or axes. If no axis is specified, it defaults to adding @@ -6503,36 +6503,38 @@ class Unsqueeze(Feature): Parameters ---------- axis: int or tuple[int, ...], optional - The axis or axes where new singleton dimensions should be added. - It defaults to `None`, which adds a singleton dimension at the last axis. - **kwargs:: dict of str to Any + The axis or axes where new singleton dimensions should be added. It + defaults to `None`, which adds a singleton dimension at the last axis. + **kwargs: Any Additional keyword arguments passed to the parent `Feature` class. Methods ------- - `get(image: np.ndarray, axis: int | tuple[int, ...] | None, **kwargs: dict[str, Any]) -> np.ndarray` - Add singleton dimensions to the input image. + `get(image: array, axis: int | tuple[int, ...] | None, **kwargs: Any) -> array` + Add singleton dimensions to the input image. The input and output + arrays can be a NumPy array, a PyTorch tensor, or an Image. Examples -------- >>> import deeptrack as dt - >>> import numpy as np Create an input array: + >>> import numpy as np + >>> >>> input_image = np.array([1, 2, 3]) - >>> print(input_image.shape) + >>> input_image.shape (3,) - Apply an Unsqueeze feature: + Apply Unsqueeze feature: >>> unsqueeze_feature = dt.Unsqueeze(axis=0) >>> output_image = unsqueeze_feature(input_image) - >>> print(output_image.shape) + >>> output_image.shape (1, 3) - Without specifying an axis: + Without specifying an axis, in unsqueezes the last dimension: >>> unsqueeze_feature = dt.Unsqueeze() >>> output_image = unsqueeze_feature(input_image) - >>> print(output_image.shape) + >>> output_image.shape (3, 1) """ @@ -6547,9 +6549,9 @@ def __init__( Parameters ---------- axis: int or tuple[int, ...], optional - The axis or axes where new singleton dimensions should be added. - It defaults to -1, which adds a singleton dimension at the last axis. - **kwargs:: dict of str to Any + The axis or axes where new singleton dimensions should be added. It + defaults to -1, which adds a singleton dimension at the last axis. + **kwargs:: Any Additional keyword arguments passed to the parent `Feature` class. """ @@ -6558,31 +6560,34 @@ def __init__( def get( self: Unsqueeze, - image: np.ndarray, + image: np.ndarray | torch.Tensor | Image, axis: int | tuple[int, ...] | None = -1, **kwargs: Any, - ) -> np.ndarray: + ) -> np.ndarray | torch.Tensor | Image: """Add singleton dimensions to the input image. Parameters ---------- - image: np.ndarray - The input image to process. + image: array + The input image to process. The input array can be a NumPy array, a + PyTorch tensor, or an Image. axis: int or tuple[int, ...], optional The axis or axes where new singleton dimensions should be added. - It defaults to -1, which adds a singleton dimension at the last axis. - **kwargs:: dict of str to Any + It defaults to -1, which adds a singleton dimension at the last + axis. + **kwargs: Any Additional keyword arguments (unused here). Returns ------- - np.ndarray - The input image with the specified singleton dimensions added. + array + The input image with the specified singleton dimensions added. The + output array can be a NumPy array, a PyTorch tensor, or an Image. """ - return np.expand_dims(image, axis=axis) + return xp.expand_dims(image, axis=axis) ExpandDims = Unsqueeze From 12a88e19f15a97db3d9e7b0b24a96dd5458243d1 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Fri, 27 Jun 2025 12:58:12 +0200 Subject: [PATCH 062/118] Update features.py --- deeptrack/features.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/deeptrack/features.py b/deeptrack/features.py index 0d69d4488..84ae0ae43 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -6587,6 +6587,13 @@ def get( """ + if apc.is_torch_array(image): + if isinstance(axis, int): + axis = (axis,) + for ax in sorted(axis): + image = image.unsqueeze(ax) + return image + return xp.expand_dims(image, axis=axis) From 4eb169c44dff4a449bf4842242862d67616232a5 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Fri, 27 Jun 2025 12:58:14 +0200 Subject: [PATCH 063/118] Update test_features.py --- deeptrack/tests/test_features.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index 467495800..72c93339a 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -2363,8 +2363,20 @@ def test_Squeeze(self): def test_Unsqueeze(self): + ### Test with NumPy array + input_image = np.array([1, 2, 3]) + + unsqueeze_feature = features.Unsqueeze(axis=0) + output_image = unsqueeze_feature(input_image) + self.assertEqual(output_image.shape, (1, 3)) + + unsqueeze_feature = features.Unsqueeze() + output_image = unsqueeze_feature(input_image) + self.assertEqual(output_image.shape, (3, 1)) - input_image = np.array([1, 2, 3]) # shape (3,) + ### Test with Image + input_data = np.array([1, 2, 3]) + input_image = features.Image(input_data) unsqueeze_feature = features.Unsqueeze(axis=0) output_image = unsqueeze_feature(input_image) @@ -2374,6 +2386,21 @@ def test_Unsqueeze(self): output_image = unsqueeze_feature(input_image) self.assertEqual(output_image.shape, (3, 1)) + ### Test with PyTorch tensor (if available) + if TORCH_AVAILABLE: + input_tensor = torch.tensor([1, 2, 3]) + + unsqueeze_feature = features.Unsqueeze(axis=0) + output_tensor = unsqueeze_feature(input_tensor) + self.assertEqual(output_tensor.shape, (1, 3)) + torch.testing.assert_close(output_tensor, + input_tensor.unsqueeze(0)) + + unsqueeze_feature = features.Unsqueeze() + output_tensor = unsqueeze_feature(input_tensor) + self.assertEqual(output_tensor.shape, (3, 1)) + torch.testing.assert_close(output_tensor, + input_tensor.unsqueeze(-1)) def test_MoveAxis(self): ### Test with NumPy array From e6734f091cb28566029e9171f1ca4cbd20652c8f Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Fri, 27 Jun 2025 13:01:10 +0200 Subject: [PATCH 064/118] Update features.py --- deeptrack/features.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 84ae0ae43..8008735d3 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -190,7 +190,7 @@ def merge_features( "Upscale", # TODO "NonOverlapping", # TODO "Store", # TODO - "Squeeze", + "Squeeze", # TODO "Unsqueeze", "ExpandDims", "MoveAxis", From 3dee646394ff484156165899eb6ad53fe1c80c74 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Fri, 27 Jun 2025 13:01:13 +0200 Subject: [PATCH 065/118] Update test_features.py --- deeptrack/tests/test_features.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index 72c93339a..ebf46c3c0 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -2374,6 +2374,11 @@ def test_Unsqueeze(self): output_image = unsqueeze_feature(input_image) self.assertEqual(output_image.shape, (3, 1)) + # Multiple axes + unsqueeze_feature = features.Unsqueeze(axis=(0, 2)) + output_image = unsqueeze_feature(input_image) + self.assertEqual(output_image.shape, (1, 3, 1)) + ### Test with Image input_data = np.array([1, 2, 3]) input_image = features.Image(input_data) @@ -2386,6 +2391,11 @@ def test_Unsqueeze(self): output_image = unsqueeze_feature(input_image) self.assertEqual(output_image.shape, (3, 1)) + # Multiple axes + unsqueeze_feature = features.Unsqueeze(axis=(0, 2)) + output_image = unsqueeze_feature(input_image) + self.assertEqual(output_image.shape, (1, 3, 1)) + ### Test with PyTorch tensor (if available) if TORCH_AVAILABLE: input_tensor = torch.tensor([1, 2, 3]) @@ -2402,6 +2412,13 @@ def test_Unsqueeze(self): torch.testing.assert_close(output_tensor, input_tensor.unsqueeze(-1)) + # Multiple axes + unsqueeze_feature = features.Unsqueeze(axis=(0, 2)) + output_tensor = unsqueeze_feature(input_tensor) + self.assertEqual(output_tensor.shape, (1, 3, 1)) + expected_tensor = input_tensor.unsqueeze(0).unsqueeze(2) + torch.testing.assert_close(output_tensor, expected_tensor) + def test_MoveAxis(self): ### Test with NumPy array input_image = np.random.rand(2, 3, 4) From a5a9736225fbe07c23edfcac28257a5b96bd43ac Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Fri, 27 Jun 2025 19:35:13 +0200 Subject: [PATCH 066/118] Update test_features.py --- deeptrack/tests/test_features.py | 64 +++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index ebf46c3c0..6dcb47d05 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -2350,16 +2350,75 @@ def test_Store(self): def test_Squeeze(self): + ### Test with NumPy array + input_image = np.array([[[[3], [2], [1]]], [[[1], [2], [3]]]]) + # shape: (2, 1, 3, 1) + + # Squeeze axis 1 + squeeze_feature = features.Squeeze(axis=1) + output_image = squeeze_feature(input_image) + self.assertEqual(output_image.shape, (2, 3, 1)) + expected_output = np.squeeze(input_image, axis=1) + np.testing.assert_array_equal(output_image, expected_output) + + # Squeeze all singleton dimensions + squeeze_feature = features.Squeeze() + output_image = squeeze_feature(input_image) + self.assertEqual(output_image.shape, (2, 3)) + expected_output = np.squeeze(input_image) + np.testing.assert_array_equal(output_image, expected_output) + + # Squeeze multiple axes + squeeze_feature = features.Squeeze(axis=(1, 3)) + output_image = squeeze_feature(input_image) + self.assertEqual(output_image.shape, (2, 3)) + expected_output = np.squeeze(np.squeeze(input_image, axis=3), axis=1) + np.testing.assert_array_equal(output_image, expected_output) - input_image = np.array([[[[3], [2], [1]]],[[[1], [2], [3]]]]) + ### Test with Image + input_data = np.array([[[[3], [2], [1]]], [[[1], [2], [3]]]]) + input_image = features.Image(input_data) squeeze_feature = features.Squeeze(axis=1) output_image = squeeze_feature(input_image) self.assertEqual(output_image.shape, (2, 3, 1)) + expected_output = np.squeeze(input_data, axis=1) + np.testing.assert_array_equal(output_image, expected_output) squeeze_feature = features.Squeeze() output_image = squeeze_feature(input_image) - self.assertEqual(output_image.shape, (2,3)) + self.assertEqual(output_image.shape, (2, 3)) + expected_output = np.squeeze(input_data) + np.testing.assert_array_equal(output_image, expected_output) + + squeeze_feature = features.Squeeze(axis=(1, 3)) + output_image = squeeze_feature(input_image) + self.assertEqual(output_image.shape, (2, 3)) + expected_output = np.squeeze(np.squeeze(input_data, axis=3), axis=1) + np.testing.assert_array_equal(output_image, expected_output) + + ### Test with PyTorch tensor (if available) + if TORCH_AVAILABLE: + input_tensor = torch.tensor([[[[3], [2], [1]]], [[[1], [2], [3]]]]) + # shape: (2, 1, 3, 1) + + squeeze_feature = features.Squeeze(axis=1) + output_tensor = squeeze_feature(input_tensor) + self.assertEqual(output_tensor.shape, (2, 3, 1)) + expected_tensor = input_tensor.squeeze(1) + torch.testing.assert_close(output_tensor, expected_tensor) + + squeeze_feature = features.Squeeze() + output_tensor = squeeze_feature(input_tensor) + self.assertEqual(output_tensor.shape, (2, 3)) + expected_tensor = input_tensor.squeeze() + torch.testing.assert_close(output_tensor, expected_tensor) + + squeeze_feature = features.Squeeze(axis=(1, 3)) + output_tensor = squeeze_feature(input_tensor) + self.assertEqual(output_tensor.shape, (2, 3)) + expected_tensor = input_tensor.squeeze(3).squeeze(1) + torch.testing.assert_close(output_tensor, expected_tensor) def test_Unsqueeze(self): @@ -2419,6 +2478,7 @@ def test_Unsqueeze(self): expected_tensor = input_tensor.unsqueeze(0).unsqueeze(2) torch.testing.assert_close(output_tensor, expected_tensor) + def test_MoveAxis(self): ### Test with NumPy array input_image = np.random.rand(2, 3, 4) From dcae336debd665fbc2a553cb38aaa4306ae4df76 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Fri, 27 Jun 2025 20:30:08 +0200 Subject: [PATCH 067/118] Update features.py --- deeptrack/features.py | 62 +++++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 8008735d3..b1ab4e03f 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -190,7 +190,7 @@ def merge_features( "Upscale", # TODO "NonOverlapping", # TODO "Store", # TODO - "Squeeze", # TODO + "Squeeze", "Unsqueeze", "ExpandDims", "MoveAxis", @@ -6414,34 +6414,36 @@ class Squeeze(Feature): ---------- axis: int or tuple[int, ...], optional The axis or axes to squeeze. It defaults to `None`, squeezing all axes. - **kwargs:: dict of str to Any + **kwargs: Any Additional keyword arguments passed to the parent `Feature` class. Methods ------- - `get(image: np.ndarray, axis: int | tuple[int, ...], **kwargs: dict[str, Any]) -> np.ndarray` - Squeeze the input image by removing singleton dimensions. + `get(image: array, axis: int | tuple[int, ...], **kwargs: Any) -> array` + Squeeze the input image by removing singleton dimensions. The input and + output arrays can be a NumPy array, a PyTorch tensor, or an Image. Examples -------- - >>> import numpy as np - >>> from deeptrack.features import Squeeze + >>> import deeptrack as dt Create an input array with extra dimensions: + >>> import numpy as np + >>> >>> input_image = np.array([[[[1], [2], [3]]]]) - >>> print(input_image.shape) + >>> input_image.shape (1, 1, 3, 1) Create a Squeeze feature: - >>> squeeze_feature = Squeeze(axis=0) + >>> squeeze_feature = dt.Squeeze(axis=0) >>> output_image = squeeze_feature(input_image) - >>> print(output_image.shape) + >>> output_image.shape (1, 3, 1) Without specifying an axis: - >>> squeeze_feature = Squeeze() + >>> squeeze_feature = dt.Squeeze() >>> output_image = squeeze_feature(input_image) - >>> print(output_image.shape) + >>> output_image.shape (3,) """ @@ -6457,8 +6459,8 @@ def __init__( ---------- axis: int or tuple[int, ...], optional The axis or axes to squeeze. It defaults to `None`, which squeezes - all axes. - **kwargs:: dict of str to Any + all singleton axes. + **kwargs: Any Additional keyword arguments passed to the parent `Feature` class. """ @@ -6467,30 +6469,41 @@ def __init__( def get( self: Squeeze, - image: np.ndarray, + image: NDArray | torch.Tensor | Image, axis: int | tuple[int, ...] | None = None, **kwargs: Any, - ) -> np.ndarray: + ) -> NDArray | torch.Tensor | Image: """Squeeze the input image by removing singleton dimensions. Parameters ---------- - image: np.ndarray - The input image to process. + image: array + The input image to process. The input array can be a NumPy array, a + PyTorch tensor, or an Image. axis: int or tuple[int, ...], optional The axis or axes to squeeze. It defaults to `None`, which squeezes - all axes. - **kwargs:: dict of str to Any + all singleton axes. + **kwargs: Any Additional keyword arguments (unused here). Returns ------- - np.ndarray - The squeezed image with reduced dimensions. + array + The squeezed image with reduced dimensions. The output array can be + a NumPy array, a PyTorch tensor, or an Image. """ - return np.squeeze(image, axis=axis) + if apc.is_torch_array(image): + if axis is None: + return image.squeeze() + if isinstance(axis, int): + return image.squeeze(axis) + for ax in sorted(axis, reverse=True): + image = image.squeeze(ax) + return image + + return xp.squeeze(image, axis=axis) class Unsqueeze(Feature): @@ -6885,8 +6898,11 @@ def get( if image.shape[-1] == 1: image = image[..., 0] + if apc.is_torch_array(image): + return torch.nn.functional.one_hot(image, num_classes=num_classes) + # Create the one-hot encoded array. - return np.eye(num_classes)[image] + return xp.eye(num_classes)[image] class TakeProperties(Feature): From c7985bfb9122931dfa43a30e81fe653a180d3800 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Fri, 27 Jun 2025 20:30:10 +0200 Subject: [PATCH 068/118] Update test_features.py --- deeptrack/tests/test_features.py | 1 + 1 file changed, 1 insertion(+) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index 6dcb47d05..25e2e5633 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -2377,6 +2377,7 @@ def test_Squeeze(self): ### Test with Image input_data = np.array([[[[3], [2], [1]]], [[[1], [2], [3]]]]) + # shape: (2, 1, 3, 1) input_image = features.Image(input_data) squeeze_feature = features.Squeeze(axis=1) From 0128f220267ced242bd2d838f019319e679a1d7b Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Sat, 28 Jun 2025 00:18:05 +0200 Subject: [PATCH 069/118] Update features.py --- deeptrack/features.py | 47 ++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index b1ab4e03f..3c53232c0 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -137,7 +137,7 @@ def merge_features( from scipy.spatial.distance import cdist from deeptrack import units -from deeptrack.backend import config, xp +from deeptrack.backend import config, TORCH_AVAILABLE, xp from deeptrack.backend.core import DeepTrackNode from deeptrack.backend.units import ConversionTable, create_context from deeptrack.image import Image @@ -145,6 +145,8 @@ def merge_features( from deeptrack.sources import SourceItem from deeptrack.types import ArrayLike, PropertyLike +if TORCH_AVAILABLE: + import torch __all__ = [ "Feature", # TODO @@ -196,7 +198,7 @@ def merge_features( "MoveAxis", "Transpose", "Permute", - "OneHot", # TODO + "OneHot", "TakeProperties", # TODO ] @@ -6823,30 +6825,32 @@ class OneHot(Feature): ---------- num_classes: int The total number of classes for the one-hot encoding. - **kwargs:: dict of str to Any + **kwargs: Any Additional keyword arguments passed to the parent `Feature` class. Methods ------- - `get(image: np.ndarray, num_classes: int, **kwargs: dict[str, Any]) -> np.ndarray` + `get(image: array, num_classes: int, **kwargs: Any) -> array` Convert the input array of class labels into a one-hot encoded array. + The input and output arrays can be a NumPy array, a PyTorch tensor, or + an Image. Examples -------- >>> import deeptrack as dt - >>> import numpy as np Create an input array of class labels: + >>> import numpy as np + >>> >>> input_data = np.array([0, 1, 2]) Apply a OneHot feature: >>> one_hot_feature = dt.OneHot(num_classes=3) - >>> one_hot_feature = dt.OneHot(num_classes=3) >>> one_hot_encoded = one_hot_feature.get(input_data, num_classes=3) - >>> print(one_hot_encoded) - [[1. 0. 0.] - [0. 1. 0.] - [0. 0. 1.]] + >>> one_hot_encoded + array([[1., 0., 0.], + [0., 1., 0.], + [0., 0., 1.]]) """ @@ -6861,7 +6865,7 @@ def __init__( ---------- num_classes: int The total number of classes for the one-hot encoding. - **kwargs:: dict of str to Any + **kwargs: Any Additional keyword arguments passed to the parent `Feature` class. """ @@ -6870,17 +6874,18 @@ def __init__( def get( self: OneHot, - image: np.ndarray, + image: NDArray | torch.Tensor | Image, num_classes: int, **kwargs: Any, - ) -> np.ndarray: + ) -> NDArray | torch.Tensor | Image: """Convert the input array of labels into a one-hot encoded array. Parameters ---------- - image: np.ndarray + image: array The input array of class labels. The last dimension should contain - integers representing class indices. + integers representing class indices. The input array can be a NumPy + array, a PyTorch tensor, or an Image. num_classes: int The total number of classes for the one-hot encoding. **kwargs: Any @@ -6888,9 +6893,11 @@ def get( Returns ------- - np.ndarray + array The one-hot encoded array. The last dimension is replaced with - one-hot vectors of length `num_classes`. + one-hot vectors of length `num_classes`. The output array can be a + NumPy array, a PyTorch tensor, or an Image. In all cases, it is of + data type float32 (e.g., np.float32 or torch.float32). """ @@ -6899,10 +6906,12 @@ def get( image = image[..., 0] if apc.is_torch_array(image): - return torch.nn.functional.one_hot(image, num_classes=num_classes) + return (torch.nn.functional + .one_hot(image, num_classes=num_classes) + .to(dtype=torch.float32)) # Create the one-hot encoded array. - return xp.eye(num_classes)[image] + return xp.eye(num_classes, dtype=np.float32)[image] class TakeProperties(Feature): From 2234f8dc34a3f1bd868e7e3135d08e43bc115175 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Sat, 28 Jun 2025 00:18:07 +0200 Subject: [PATCH 070/118] Update test_features.py --- deeptrack/tests/test_features.py | 45 ++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index 25e2e5633..760be1b28 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -2552,17 +2552,52 @@ def test_Transpose(self): def test_OneHot(self): - + ### Test with NumPy array input_image = np.array([0, 1, 2]) - one_hot_feature = features.OneHot(num_classes=3) - output_image = one_hot_feature.get(input_image, num_classes=3) + output_image = one_hot_feature(input_image) + expected_output = np.array([ [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0] - ]) - self.assertTrue(np.array_equal(output_image, expected_output)) + ], dtype=np.float32) + + self.assertEqual(output_image.shape, (3, 3)) + np.testing.assert_array_equal(output_image, expected_output) + + ### Test with singleton last dimension + input_image = np.array([[0], [1], [2]]) # shape (3, 1) + output_image = one_hot_feature(input_image) + self.assertEqual(output_image.shape, (3, 3)) + np.testing.assert_array_equal(output_image, expected_output) + + ### Test with Image + input_data = np.array([0, 1, 2]) + input_image = features.Image(input_data) + output_image = one_hot_feature(input_image) + self.assertEqual(output_image.shape, (3, 3)) + np.testing.assert_array_equal(output_image, expected_output) + + ### Test with PyTorch tensor (if available) + if TORCH_AVAILABLE: + input_tensor = torch.tensor([0, 1, 2]) + output_tensor = one_hot_feature(input_tensor) + + expected_tensor = torch.tensor([ + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0] + ], dtype=torch.float32) + + self.assertEqual(output_tensor.shape, (3, 3)) + torch.testing.assert_close(output_tensor, expected_tensor) + + # Test with singleton dimension + input_tensor = torch.tensor([[0], [1], [2]]) + output_tensor = one_hot_feature(input_tensor) + self.assertEqual(output_tensor.shape, (3, 3)) + torch.testing.assert_close(output_tensor, expected_tensor) def test_TakeProperties(self): From a23fa7cdbd888352d2f99bae08fa536b08e38378 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 30 Jun 2025 09:08:40 +0200 Subject: [PATCH 071/118] Update features.py --- deeptrack/features.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 3c53232c0..ec60f6bd8 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -3706,33 +3706,35 @@ class Bind(StructuralFeature): Methods ------- - `get(image: Any, **kwargs: dict[str, Any]) -> Any` - Resolves the child feature with the provided arguments. + `get(image: Any, **kwargs: Any) -> Any` + It resolves the child feature with the provided arguments. Examples -------- >>> import deeptrack as dt - >>> import numpy as np Start by creating a `Gaussian` feature: >>> gaussian_noise = dt.Gaussian() - Dynamically modify the behavior of the feature using `Bind`: - >>> bound_feature = dt.Bind(gaussian_noise, mu = -5, sigma=2) - + Create a test image: + >>> import numpy as np + >>> >>> input_image = np.zeros((512, 512)) + + Bind fixed values to the parameters: + >>> bound_feature = dt.Bind(gaussian_noise, mu=-5, sigma=2) + + Resolve the bound feature: >>> output_image = bound_feature.resolve(input_image) - >>> print(np.mean(output_image), np.std(output_image)) - -4.9954959040123152 1.9975296489398942 + >>> round(np.mean(output_image), 1), round(np.std(output_image), 1) + (-5.0, 2.0) """ - __distributed__: bool = False - def __init__( - self: Feature, - feature: Feature, - **kwargs: dict[str, Any] + self: Bind, + feature: Feature, + **kwargs: Any, ): """Initialize the Bind feature. @@ -3746,12 +3748,13 @@ def __init__( """ super().__init__(**kwargs) + self.feature = self.add_feature(feature) def get( - self: Feature, - image: Any, - **kwargs: dict[str, Any] + self: Bind, + image: Any, + **kwargs: Any, ) -> Any: """Resolve the child feature with the dynamically provided arguments. From 5c67108c92f2168e723dbefe3d7a078f062210d9 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 30 Jun 2025 09:08:43 +0200 Subject: [PATCH 072/118] Update test_features.py --- deeptrack/tests/test_features.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index 760be1b28..c6482f6f6 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -1315,21 +1315,15 @@ def test_Slice_static_dynamic(self): def test_Bind(self): - value = features.Value( - value=lambda input_value: input_value, - input_value=10, - ) value = features.Value( value=lambda input_value: input_value, input_value=10, ) pipeline = (value + 10) / value - - pipeline_with_small_input = features.Bind(pipeline, input_value=1) - res = pipeline.update().resolve() self.assertEqual(res, 2) + pipeline_with_small_input = features.Bind(pipeline, input_value=1) res = pipeline_with_small_input.update().resolve() self.assertEqual(res, 11) @@ -1342,7 +1336,7 @@ def test_Bind_gaussian_noise(self): bound_feature = features.Bind(gaussian_noise, mu=-5, sigma=2) # Create the input image - input_image = np.zeros((512, 512)) + input_image = np.zeros((128, 128)) # Resolve the feature to get the output image output_image = bound_feature.resolve(input_image) @@ -1352,10 +1346,8 @@ def test_Bind_gaussian_noise(self): output_std = np.std(output_image) # Assert that the mean and standard deviation are close to the bound values - self.assertAlmostEqual(output_mean, -5, delta=0.2, \ - msg="Mean is not within the expected range") - self.assertAlmostEqual(output_std, 2, delta=0.2, \ - msg="Standard deviation is not within the expected range") + self.assertAlmostEqual(output_mean, -5, delta=0.2) + self.assertAlmostEqual(output_std, 2, delta=0.2) def test_BindResolve(self): From fd5fc9fbebc280ab88a46f1d62782c8ca76f58f6 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 30 Jun 2025 11:39:54 +0200 Subject: [PATCH 073/118] Update test_features.py --- deeptrack/tests/test_features.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index c6482f6f6..7265790af 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -1426,11 +1426,9 @@ def test_BindUpdate_gaussian_noise(self): output_mean = np.mean(output_image) output_std = np.std(output_image) - # Assert that the mean and standard deviation are close to the bound values - self.assertAlmostEqual(output_mean, 5, \ - delta=0.2, msg="Mean is not within the expected range") - self.assertAlmostEqual(output_std, 3, \ - delta=0.2, msg="Standard deviation is not within the expected range") + # Assert mean and standard deviation close to the bound values + self.assertAlmostEqual(output_mean, 5) + self.assertAlmostEqual(output_std, 3) def test_ConditionalSetProperty(self): From 1714d5cec3d13b565edfa32aa0dc2656b3df0fe1 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 30 Jun 2025 11:39:56 +0200 Subject: [PATCH 074/118] Update features.py --- deeptrack/features.py | 120 ++++++++++++++++++++---------------------- 1 file changed, 57 insertions(+), 63 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index ec60f6bd8..9be3601d7 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -176,11 +176,11 @@ def merge_features( "Repeat", "Combine", "Slice", - "Bind", # TODO - "BindResolve", # TODO - "BindUpdate", # TODO - "ConditionalSetProperty", # TODO - "ConditionalSetFeature", # TODO + "Bind", + "BindResolve", + "BindUpdate", + "ConditionalSetProperty", + "ConditionalSetFeature", "Lambda", # TODO "Merge", # TODO "OneOf", # TODO @@ -2048,7 +2048,8 @@ def __init__( "Passing an Image object as the value to dt.Value may lead to " "performance deterioration. Consider converting the Image to " "a NumPy array with np.array(image), or to a PyTorch tensor " - "with torch.tensor(np.array(image))." + "with torch.tensor(np.array(image)).", + DeprecationWarning, ) super().__init__(value=value, **kwargs) @@ -3796,23 +3797,19 @@ class BindUpdate(StructuralFeature): Methods ------- - `get(image: Any, **kwargs: dict[str, Any]) -> Any` - Resolves the child feature with the provided arguments. + `get(image: Any, **kwargs: Any) -> Any` + It resolves the child feature with the provided arguments. Warnings -------- - This feature is deprecated and may be removed in a future release. - It is recommended to use `Bind` instead for equivalent functionality. - - Notes - ----- - The current implementation is not guaranteed to be exactly equivalent to - prior implementations. + Deprecation: This feature is deprecated and may be removed in a future + release. It is recommended to use `Bind` instead for equivalent + functionality. Further, the current implementation is not guaranteed to be + exactly equivalent to prior implementations. Examples -------- >>> import deeptrack as dt - >>> import numpy as np Start by creating a `Gaussian` feature: >>> gaussian_noise = dt.Gaussian() @@ -3820,19 +3817,19 @@ class BindUpdate(StructuralFeature): Dynamically modify the behavior of the feature using `BindUpdate`: >>> bound_feature = dt.BindUpdate(gaussian_noise, mu = 5, sigma=3) + >>> import numpy as np + >>> >>> input_image = np.zeros((512, 512)) >>> output_image = bound_feature.resolve(input_image) - >>> print(np.mean(output_image), np.std(output_image)) - 4.998501486851294 3.0020269383538176 + >>> round(np.mean(output_image), 1), round(np.std(output_image), 1) + (5.0, 3.0) """ - __distributed__: bool = False - def __init__( self: Feature, feature: Feature, - **kwargs: dict[str, Any] + **kwargs: Any, ): """Initialize the BindUpdate feature. @@ -3845,7 +3842,7 @@ def __init__( Warnings -------- - Emits a deprecation warning, encouraging the use of `Bind` instead. + It emits a deprecation warning, encouraging the use of `Bind` instead. """ @@ -3860,12 +3857,13 @@ def __init__( ) super().__init__(**kwargs) + self.feature = self.add_feature(feature) def get( - self: Feature, - image: Any, - **kwargs: dict[str, Any] + self: Feature, + image: Any, + **kwargs: Any, ) -> Any: """Resolve the child feature with the provided arguments. @@ -3896,64 +3894,60 @@ class ConditionalSetProperty(StructuralFeature): the given properties are applied; otherwise, the child feature remains unchanged. - **Note**: It is advisable to use `dt.Arguments` instead when possible, - since this feature **overwrites** properties, which may affect future - calls to the feature. + It is advisable to use `dt.Arguments` instead when possible, since this + feature overwrites properties, which may affect future calls to the + feature. + + If `condition` is a string, the condition must be explicitly passed when + resolving. + + The properties applied do not persist unless explicitly stored. Parameters ---------- feature: Feature The child feature whose properties will be modified conditionally. - condition: PropertyLike[str] or PropertyLike[bool] - Either a boolean value (`True`/`False`) or the name of a boolean + condition: PropertyLike[str or bool] or None + Either a boolean value (`True`, `False`) or the name of a boolean property in the feature’s property dictionary. If the condition evaluates to `True`, the specified properties are applied. - **kwargs: dict[str, Any] + **kwargs: Any The properties to be applied to the child feature if `condition` is `True`. - Attributes - ---------- - __distributed__: bool - Indicates whether this feature distributes computation across inputs. - Methods ------- - `get(image: Any, condition: str | bool, **kwargs: dict[str, Any]) -> Any` + `get(image: Any, condition: str or bool, **kwargs: Any) -> Any` Resolves the child feature, conditionally applying the specified properties. - Notes - ----- - - If `condition` is a string, the condition must be explicitly passed when - resolving. - - The properties applied **do not persist** unless explicitly stored. - Examples -------- >>> import deeptrack as dt + + Define an image: >>> import numpy as np + >>> + >>> image = np.ones((512, 512)) Define a `Gaussian` noise feature: >>> gaussian_noise = dt.Gaussian(sigma=0) --- Using a boolean condition --- - Apply `sigma=5` **only if** `condition=True`: + Apply `sigma=5` only if `condition=True`: >>> conditional_feature = dt.ConditionalSetProperty( - ... gaussian_noise, sigma=5 + ... gaussian_noise, sigma=5, ... ) - Define an image: - >>> image = np.ones((512, 512)) - Resolve with condition met: - >>> noisy_image = conditional_feature.update(image, condition=True) - >>> print(noisy_image.std()) # Should be ~5 - 4.987707046984823 + >>> noisy_image = conditional_feature(image, condition=True) + >>> round(noisy_image.std(), 1) + 5.0 Resolve without condition: - >>> clean_image = conditional_feature.update(image, condition=False) - >>> print(clean_image.std()) # Should be 0 + >>> conditional_feature.update() # Essential to reset the property + >>> clean_image = conditional_feature(image, condition=False) + >>> round(clean_image.std(), 1) 0.0 --- Using a string-based condition --- @@ -3963,18 +3957,17 @@ class ConditionalSetProperty(StructuralFeature): ... ) Resolve with condition met: - >>> noisy_image = conditional_feature.update(image, is_noisy=True) - >>> print(noisy_image.std()) # Should be ~5 - 5.006310381139811 + >>> noisy_image = conditional_feature(image, is_noisy=True) + >>> round(noisy_image.std(), 1) + 5.0 Resolve without condition: - >>> clean_image = conditional_feature.update(image, is_noisy=False) - >>> print(clean_image.std()) # Should be 0 + >>> conditional_feature.update() + >>> clean_image = conditional_feature(image, is_noisy=False) + >>> round(clean_image.std(), 1) 0.0 - - """ - __distributed__: bool = False + """ def __init__( self: Feature, @@ -3988,7 +3981,7 @@ def __init__( ---------- feature: Feature The child feature to conditionally modify. - condition: PropertyLike[str or bool] + condition: PropertyLike[str or bool] or None A boolean value or the name of a boolean property in the feature's property dictionary. If the condition evaluates to `True`, the specified properties are applied. @@ -4002,6 +3995,7 @@ def __init__( kwargs.setdefault(condition, True) super().__init__(condition=condition, **kwargs) + self.feature = self.add_feature(feature) def get( @@ -4020,7 +4014,7 @@ def get( A boolean value or the name of a boolean property in the feature's property dictionary. If the condition evaluates to `True`, the specified properties are applied. - **kwargs:: dict of str to Any + **kwargs:: Any Additional properties to apply to the child feature if the condition is `True`. From 3ef47fde874c0f726f1923707584651028872272 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 30 Jun 2025 11:45:15 +0200 Subject: [PATCH 075/118] Update test_features.py --- deeptrack/tests/test_features.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index 7265790af..af1c71679 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -1417,7 +1417,7 @@ def test_BindUpdate_gaussian_noise(self): bound_feature = features.BindUpdate(gaussian_noise, mu=5, sigma=3) # Create the input image - input_image = np.zeros((512, 512)) + input_image = np.zeros((128, 128)) # Resolve the feature to get the output image output_image = bound_feature.resolve(input_image) @@ -1432,13 +1432,12 @@ def test_BindUpdate_gaussian_noise(self): def test_ConditionalSetProperty(self): - """Test that ConditionalSetProperty correctly modifies properties based on condition.""" - """Set up a Gaussian feature and a test image before each test.""" + # Set up a Gaussian feature and a test image before each test. gaussian_noise = Gaussian(sigma=0) image = np.ones((128, 128)) - """Test that sigma is correctly applied when condition is a boolean.""" + # Test that sigma is correctly applied when condition is a boolean. conditional_feature = features.ConditionalSetProperty( gaussian_noise, sigma=5, ) @@ -1451,9 +1450,9 @@ def test_ConditionalSetProperty(self): clean_image = conditional_feature.update()(image, condition=False) self.assertEqual(clean_image.std(), 0) - """Test that sigma is correctly applied when condition is a string property.""" + # Test sigma is correctly applied when condition is string property. conditional_feature = features.ConditionalSetProperty( - gaussian_noise, sigma=5, condition="is_noisy" + gaussian_noise, sigma=5, condition="is_noisy", ) # Test with condition met (should apply sigma=5) @@ -1464,17 +1463,17 @@ def test_ConditionalSetProperty(self): clean_image = conditional_feature.update()(image, is_noisy=False) self.assertEqual(clean_image.std(), 0) - def test_ConditionalSetFeature(self): - """Set up Gaussian noise features and test image before each test.""" + def test_ConditionalSetFeature(self): + # Set up Gaussian noise features and test image before each test. true_feature = Gaussian(sigma=0) # Clean image (no noise) false_feature = Gaussian(sigma=5) # Noisy image (sigma=5) image = np.ones((512, 512)) - """Test using a direct boolean condition.""" + # Test using a direct boolean condition. conditional_feature = features.ConditionalSetFeature( on_true=true_feature, - on_false=false_feature + on_false=false_feature, ) # Default condition is True (no noise) @@ -1489,11 +1488,11 @@ def test_ConditionalSetFeature(self): clean_image = conditional_feature(image, condition=True) self.assertEqual(clean_image.std(), 0) - """Test using a string-based condition.""" + # Test using a string-based condition. conditional_feature = features.ConditionalSetFeature( on_true=true_feature, on_false=false_feature, - condition="is_noisy" + condition="is_noisy", ) # Condition is False (sigma=5) From 9f1f019c4c10bd54d43e8f44c7e4751a8bbe4a21 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 30 Jun 2025 12:47:26 +0200 Subject: [PATCH 076/118] Update features.py --- deeptrack/features.py | 65 ++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 38 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 9be3601d7..c3b7b604a 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -4046,7 +4046,7 @@ class ConditionalSetFeature(StructuralFeature): The `condition` parameter specifies either: - A boolean value (default is `True`). - - The name of a property to listen to. For example, if + - The name of a property to listen to. For example, if `condition="is_label"`, the selected feature can be toggled as follows: >>> feature.resolve(is_label=True) # Resolves `on_true` @@ -4071,20 +4071,19 @@ class ConditionalSetFeature(StructuralFeature): **kwargs: Any Additional keyword arguments passed to the parent `StructuralFeature`. - Attributes - ---------- - __distributed__: bool - Indicates whether this feature distributes computation across inputs. - Methods ------- - `get(image: Any, condition: str | bool, **kwargs: dict[str, Any]) -> Any` + `get(image: Any, condition: str or bool, **kwargs: Any) -> Any` Resolves the appropriate feature based on the condition. Examples -------- >>> import deeptrack as dt + + Define an image: >>> import numpy as np + >>> + >>> image = np.ones((512, 512)) Define two `Gaussian` noise features: >>> true_feature = dt.Gaussian(sigma=0) @@ -4092,26 +4091,23 @@ class ConditionalSetFeature(StructuralFeature): --- Using a boolean condition --- Combine the features into a conditional set feature. - If not provided explicitely, condition is assumed to be True: + If not provided explicitely, the condition is assumed to be True: >>> conditional_feature = dt.ConditionalSetFeature( - ... on_true=true_feature, - ... on_false=false_feature, + ... on_true=true_feature, + ... on_false=false_feature, ... ) - Define an image: - >>> image = np.ones((512, 512)) - - Resolve based on the condition: - >>> clean_image = conditional_feature(image) # If not specified, default is True - >>> print(clean_image.std()) # Should be 0 + Resolve based on the condition. If not specified, default is True: + >>> clean_image = conditional_feature(image) + >>> round(clean_image.std(), 1) 0.0 >>> noisy_image = conditional_feature(image, condition=False) - >>> print(noisy_image.std()) # Should be ~5 - 4.987707046984823 + >>> round(noisy_image.std(), 1) + 5.0 >>> clean_image = conditional_feature(image, condition=True) - >>> print(clean_image.std()) # Should be 0 + >>> round(clean_image.std(), 1) 0.0 --- Using a string-based condition --- @@ -4124,17 +4120,15 @@ class ConditionalSetFeature(StructuralFeature): Resolve based on the conditions: >>> noisy_image = conditional_feature(image, is_noisy=False) - >>> print(noisy_image.std()) # Should be ~5 - 5.006310381139811 + >>> round(noisy_image.std(), 1) + 5.0 >>> clean_image = conditional_feature(image, is_noisy=True) - >>> print(clean_image.std()) # Should be 0 + >>> round(clean_image.std(), 1) 0.0 """ - __distributed__: bool = False - def __init__( self: Feature, on_false: Feature | None = None, @@ -4152,8 +4146,8 @@ def __init__( The feature to resolve if the condition evaluates to `True`. condition: str or bool, optional The name of the property to listen to, or a boolean value. It - defaults to `"is_label"`. - **kwargs:: dict of str to Any + defaults to `True`. + **kwargs:: Any Additional keyword arguments for the parent `StructuralFeature`. """ @@ -4162,7 +4156,7 @@ def __init__( kwargs.setdefault(condition, True) super().__init__(condition=condition, **kwargs) - + # Add the child features to the dependency graph if provided. if on_true: self.add_feature(on_true) @@ -4189,7 +4183,7 @@ def get( The name of the conditional property or a boolean value. If a string is provided, it is looked up in `kwargs` to get the actual boolean value. - **kwargs:: dict of str to Any + **kwargs:: Any Additional keyword arguments to pass to the resolved feature. Returns @@ -4207,16 +4201,11 @@ def get( _condition = kwargs.get(condition, False) # Resolve the appropriate feature. - if _condition: - if self.on_true: - return self.on_true(image) - else: - return image - else: - if self.on_false: - return self.on_false(image) - else: - return image + if _condition and self.on_true: + return self.on_true(image) + if not _condition and self.on_false: + return self.on_false(image) + return image class Lambda(Feature): From ca3e44388f8dbb3c1cd7bdcf6a1ea42aa03af9d1 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 30 Jun 2025 12:47:28 +0200 Subject: [PATCH 077/118] Update test_features.py --- deeptrack/tests/test_features.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index af1c71679..12c9d76de 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -1427,8 +1427,8 @@ def test_BindUpdate_gaussian_noise(self): output_std = np.std(output_image) # Assert mean and standard deviation close to the bound values - self.assertAlmostEqual(output_mean, 5) - self.assertAlmostEqual(output_std, 3) + self.assertAlmostEqual(output_mean, 5, delta=0.5) + self.assertAlmostEqual(output_std, 3, delta=0.5) def test_ConditionalSetProperty(self): From b5ea8732a9aef440c6511bd9dc910882826a6068 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 30 Jun 2025 18:20:51 +0200 Subject: [PATCH 078/118] Update features.py --- deeptrack/features.py | 76 +++++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index c3b7b604a..40d8d12c8 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -181,7 +181,7 @@ def merge_features( "BindUpdate", "ConditionalSetProperty", "ConditionalSetFeature", - "Lambda", # TODO + "Lambda", "Merge", # TODO "OneOf", # TODO "OneOfDict", # TODO @@ -4209,12 +4209,12 @@ def get( class Lambda(Feature): - """Apply a user-defined function to each image in the input. + """Apply a user-defined function to the input. - This feature allows applying a custom function to individual images in the - input pipeline. The `function` parameter must be wrapped in an - **outer function** that can depend on other properties of the pipeline. - The **inner function** processes a single image. + This feature allows applying a custom function to individual inputs in the + input pipeline. The `function` parameter must be wrapped in an **outer + function** that can depend on other properties of the pipeline. + The **inner function** processes a single input. Parameters ---------- @@ -4227,7 +4227,7 @@ class Lambda(Feature): Methods ------- - `get(image: np.ndarray | Image, function: Callable[[Image], Image], **kwargs: dict[str, Any]) -> Image` + `get(image: Any, function: Callable[[Any], Any], **kwargs: Any) -> Any` Applies the custom function to the input image. Examples @@ -4244,36 +4244,40 @@ class Lambda(Feature): Create a `Lambda` feature that scales images by a factor of 5: >>> lambda_feature = dt.Lambda(function=scale_function_factory, scale=5) - Apply the feature to an image: - >>> input_image = np.ones((5, 5)) + Create an image: + >>> import numpy as np + >>> + >>> input_image = np.ones((2, 3)) + >>> input_image + array([[1., 1., 1.], + [1., 1., 1.]]) + + Apply the feature to the image: >>> output_image = lambda_feature(input_image) - >>> print(output_image) - [[5. 5. 5. 5. 5.] - [5. 5. 5. 5. 5.] - [5. 5. 5. 5. 5.] - [5. 5. 5. 5. 5.] - [5. 5. 5. 5. 5.]] - + >>> output_image + array([[5., 5., 5.], + [5., 5., 5.]]) + """ def __init__( self: Feature, - function: Callable[..., Callable[[Image], Image]], + function: Callable[..., Callable[[Any], Any]], **kwargs: Any, ): """Initialize the Lambda feature. - This feature applies a user-defined function to process an image. The + This feature applies a user-defined function to process an input. The `function` parameter must be a callable that returns another function, - where the inner function operates on the image. + where the inner function operates on the input. Parameters ---------- - function: Callable[..., Callable[[Image], Image]] + function: Callable[..., Callable[[Any], Any]] A callable that produces a function. The outer function can accept additional arguments from the pipeline, while the inner function - processes a single image. - **kwargs: dict[str, Any] + processes a single input. + **kwargs: Any Additional keyword arguments passed to the parent `Feature` class. """ @@ -4282,30 +4286,30 @@ def __init__( def get( self: Feature, - image: np.ndarray | Image, - function: Callable[[Image], Image], + image: Any, + function: Callable[[Any], Any], **kwargs: Any, - ) -> Image: - """Apply the custom function to the input image. + ) -> Any: + """Apply the custom function to the input. - This method applies a user-defined function to transform the input - image. The function should be a callable that takes an image as input - and returns a modified version of it. + This method applies a user-defined function to transform the input. The + function should be a callable that takes an input and returns a + modified version of it. Parameters ---------- - image: np.ndarray or Image - The input image to be processed. - function: Callable[[Image], Image] - A callable function that takes an image and returns a transformed - image. + image: Any + The input to be processed. + function: Callable[[Any], Any] + A callable function that takes an input and returns a transformed + output. **kwargs: Any Additional keyword arguments (unused in this implementation). Returns ------- - Image - The transformed image after applying the function. + Any + The transformed output after applying the function. """ From 9c7bb2939d4caec1171f86e911dd2b3683e1bbf2 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 30 Jun 2025 18:20:53 +0200 Subject: [PATCH 079/118] Update test_features.py --- deeptrack/tests/test_features.py | 57 ++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index 12c9d76de..887a2675c 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -1509,80 +1509,93 @@ def test_Lambda_dependence(self): B = features.DummyFeature( key="a", - prop=lambda key: A.a() if key == "a" - else (A.b() if key == "b" else A.c()), + prop=lambda key: A.a() if key == "a" + else (A.b() if key == "b" + else A.c()), ) B.update() self.assertEqual(B.prop(), 1) - B.key.set_value("a") - self.assertEqual(B.prop(), 1) + B.key.set_value("b") self.assertEqual(B.prop(), 2) + B.key.set_value("c") self.assertEqual(B.prop(), 3) + B.key.set_value("a") + self.assertEqual(B.prop(), 1) + def test_Lambda_dependence_twice(self): A = features.DummyFeature(a=1, b=2, c=3) B = features.DummyFeature( key="a", - prop=lambda key: A.a() if key == "a" - else (A.b() if key == "b" else A.c()), + prop=lambda key: A.a() if key == "a" + else (A.b() if key == "b" + else A.c()), prop2=lambda prop: prop * 2, ) B.update() self.assertEqual(B.prop2(), 2) - B.key.set_value("a") - self.assertEqual(B.prop2(), 2) + B.key.set_value("b") self.assertEqual(B.prop2(), 4) + B.key.set_value("c") self.assertEqual(B.prop2(), 6) + B.key.set_value("a") + self.assertEqual(B.prop2(), 2) + def test_Lambda_dependence_other_feature(self): A = features.DummyFeature(a=1, b=2, c=3) B = features.DummyFeature( key="a", - prop=lambda key: A.a() if key == "a" - else (A.b() if key == "b" else A.c()), + prop=lambda key: A.a() if key == "a" + else (A.b() if key == "b" + else A.c()), prop2=lambda prop: prop * 2, ) - C = features.DummyFeature(B_prop=B.prop2, + C = features.DummyFeature(B_prop=B.prop2, prop=lambda B_prop: B_prop * 2) C.update() self.assertEqual(C.prop(), 4) - B.key.set_value("a") - self.assertEqual(C.prop(), 4) + B.key.set_value("b") self.assertEqual(C.prop(), 8) + B.key.set_value("c") self.assertEqual(C.prop(), 12) + B.key.set_value("a") + self.assertEqual(C.prop(), 4) + def test_Lambda_scaling(self): def scale_function_factory(scale=2): def scale_function(image): return image * scale return scale_function - lambda_feature = features.Lambda(function=scale_function_factory, scale=5) + lambda_feature = features.Lambda( + function=scale_function_factory, + scale=5, + ) input_image = np.ones((5, 5)) - output_image = lambda_feature.resolve(input_image) + self.assertTrue(np.array_equal(output_image, np.ones((5, 5)) * 5)) - expected_output = np.ones((5, 5)) * 5 - self.assertTrue(np.array_equal(output_image, expected_output), "Arrays are not equal") - - lambda_feature = features.Lambda(function=scale_function_factory, scale=3) + lambda_feature = features.Lambda( + function=scale_function_factory, + scale=3, + ) output_image = lambda_feature.resolve(input_image) - - expected_output = np.ones((5, 5)) * 3 - self.assertTrue(np.array_equal(output_image, expected_output), "Arrays are not equal") + self.assertTrue(np.array_equal(output_image, np.ones((5, 5)) * 3)) def test_Merge(self): From ee903ab1807169e99d4ce76e8ab270b69f804e69 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 30 Jun 2025 21:27:39 +0200 Subject: [PATCH 080/118] Update features.py --- deeptrack/features.py | 121 +++++++++++++++++++++++------------------- 1 file changed, 66 insertions(+), 55 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 40d8d12c8..58c866dc7 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -182,8 +182,8 @@ def merge_features( "ConditionalSetProperty", "ConditionalSetFeature", "Lambda", - "Merge", # TODO - "OneOf", # TODO + "Merge", + "OneOf", "OneOfDict", # TODO "LoadImage", # TODO "SampleToMasks", # TODO @@ -1734,7 +1734,7 @@ class StructuralFeature(Feature): property list. A value of `2` hides them from output. __distributed__ : bool If `True`, applies `get` to each element in a list individually. - If `False`, processes the entire list as a single unit. Defaults to + If `False`, processes the entire list as a single unit. It defaults to `False`. """ @@ -4317,42 +4317,41 @@ def get( class Merge(Feature): - """Apply a custom function to a list of images. + """Apply a custom function to a list of inputs. - This feature allows applying a user-defined function to a list of images. + This feature allows applying a user-defined function to a list of inputs. The `function` parameter must be a callable that returns another function, where: - The **outer function** can depend on other properties in the pipeline. - - The **inner function** takes a list of images and returns a single - image or a list of images. + - The **inner function** takes a list of inputs and returns a single + outputs or a list of outputs. - **Note:** The function must be wrapped in an **outer layer** to enable - dependencies on other properties while ensuring correct execution. + The function must be wrapped in an outer layer to enable dependencies on + other properties while ensuring correct execution. Parameters ---------- - function: Callable[..., Callable[[list[np.ndarray] | list[Image]], np.ndarray | list[np.ndarray] | Image | list[Image]]] - A callable that produces a function. The **outer function** can depend - on other properties of the pipeline, while the **inner function** - processes a list of images and returns either a single image or a list - of images. - **kwargs: dict[str, Any] + function: Callable[..., Callable[[list[Any]], Any or list[Any]] + A callable that produces a function. The outer function can depend on + other properties of the pipeline, while the inner function processes a + list of inputs and returns either a single output or a list of outputs. + **kwargs: Any Additional parameters passed to the parent `Feature` class. Attributes ---------- __distributed__: bool Indicates whether this feature distributes computation across inputs. + It defaults to `False`. Methods ------- - `get(list_of_images: list[np.ndarray] | list[Image], function: Callable[[list[np.ndarray] | list[Image]], np.ndarray | list[np.ndarray] | Image | list[Image]], **kwargs: dict[str, Any]) -> Image | list[Image]` - Applies the custom function to the list of images. + `get(list_of_images: list[Any], function: Callable[[list[Any]], Any or list[Any]], **kwargs: Any) -> Any or list[Any]` + Applies the custom function to the list of inputs. Examples -------- >>> import deeptrack as dt - >>> import numpy as np Define a merge function that averages multiple images: >>> def merge_function_factory(): @@ -4363,16 +4362,17 @@ class Merge(Feature): Create a Merge feature: >>> merge_feature = dt.Merge(function=merge_function_factory) + Create some images: + >>> import numpy as np + >>> + >>> image_1 = np.ones((2, 3)) * 2 + >>> image_2 = np.ones((2, 3)) * 4 + Apply the feature to a list of images: - >>> image_1 = np.ones((5, 5)) * 2 - >>> image_2 = np.ones((5, 5)) * 4 >>> output_image = merge_feature([image_1, image_2]) - >>> print(output_image) - [[3. 3. 3. 3. 3.] - [3. 3. 3. 3. 3.] - [3. 3. 3. 3. 3.] - [3. 3. 3. 3. 3.] - [3. 3. 3. 3. 3.]] + >>> output_image + array([[3., 3., 3.], + [3., 3., 3.]]) """ @@ -4388,12 +4388,12 @@ def __init__( Parameters ---------- - function: Callable[..., Callable[list[np.ndarray] | [list[Image]], np.ndarray | list[np.ndarray] | Image | list[Image]]] + function: Callable[..., Callable[list[Any]], Any or list[Any]] A callable that returns a function for processing a list of images. - - The **outer function** can depend on other properties in the pipeline. - - The **inner function** takes a list of images as input and - returns either a single image or a list of images. - **kwargs: dict[str, Any] + The outer function can depend on other properties in the pipeline. + The inner function takes a list of inputs and returns either a + single output or a list of outputs. + **kwargs: Any Additional parameters passed to the parent `Feature` class. """ @@ -4406,17 +4406,16 @@ def get( function: Callable[[list[np.ndarray] | list[Image]], np.ndarray | list[np.ndarray] | Image | list[Image]], **kwargs: Any, ) -> Image | list[Image]: - """Apply the custom function to a list of images. + """Apply the custom function to a list of inputs. Parameters ---------- - list_of_images: list[np.ndarray or Image] - A list of images to be processed by the function. - function: Callable[[list[np.ndarray] | list[Image]], np.ndarray | list[np.ndarray] | Image | list[Image]] - The function that processes the list of images and returns either: - - A single transformed image (`Image`) - - A list of transformed images (`list[Image]`) - **kwargs: dict[str, Any] + list_of_images: list[Any] + A list of inputs to be processed by the function. + function: Callable[[list[Any]], Any | list[Any]] + The function that processes the list of images and returns either a + single transformed input or a list of transformed inputs. + **kwargs: Any Additional arguments (unused in this implementation). Returns @@ -4430,7 +4429,7 @@ def get( class OneOf(Feature): - """Resolves one feature from a given collection. + """Resolve one feature from a given collection. This feature selects and applies one of multiple features from a given collection. The default behavior selects a feature randomly, but this @@ -4454,18 +4453,18 @@ class OneOf(Feature): ---------- __distributed__: bool Indicates whether this feature distributes computation across inputs. + It defaults to `False`. Methods ------- - `_process_properties(propertydict: dict) -> dict` + `_process_properties(propertydict: dict[Property]) -> dict[Property]` Processes the properties to determine the selected feature index. - `get(image: Any, key: int, _ID: tuple[int, ...], **kwargs: dict[str, Any]) -> Any` - Applies the selected feature to the input image. + `get(image: Any, key: int, _ID: tuple[int, ...], **kwargs: Any) -> Any` + Applies the selected feature to the input. Examples -------- >>> import deeptrack as dt - >>> import numpy as np Define multiple features: >>> feature_1 = dt.Add(value=10) @@ -4474,15 +4473,25 @@ class OneOf(Feature): Create a `OneOf` feature that randomly selects a transformation: >>> one_of_feature = dt.OneOf([feature_1, feature_2]) - Apply it to an input image: + Create an input image: + >>> import numpy as np + >>> >>> input_image = np.array([1, 2, 3]) + + Apply the `OneOf` feature to the input image: >>> output_image = one_of_feature(input_image) - >>> print(output_image) # The output depends on the randomly selected feature. + >>> output_image # The output depends on the randomly selected feature. - Use a `key` to apply a specific feature: + Use `key` to apply a specific feature: >>> controlled_feature = dt.OneOf([feature_1, feature_2], key=0) >>> output_image = controlled_feature(input_image) - >>> print(output_image) # Adds 10 to each element. + >>> output_image + array([11, 12, 13]) + + >>> controlled_feature.key.set_value(1) + >>> output_image = controlled_feature(input_image) + >>> output_image + array([2, 4, 6]) """ @@ -4499,7 +4508,8 @@ def __init__( Parameters ---------- collection: Iterable[Feature] - A collection of features to choose from. It will be stored as a tuple. + A collection of features to choose from. It will be stored as a + tuple. key: int | None, optional The index of the feature to resolve from the collection. If not provided, a feature is selected randomly at execution. @@ -4509,28 +4519,29 @@ def __init__( """ super().__init__(key=key, **kwargs) + self.collection = tuple(collection) - + # Add all features in the collection as dependencies. for feature in self.collection: self.add_feature(feature) def _process_properties( - self: Feature, - propertydict: dict, - ) -> dict: + self: Feature, + propertydict: dict[Property], + ) -> dict[Property]: """Process the properties to determine the feature index. If `key` is not provided, a random feature index is assigned. Parameters ---------- - propertydict: dict + propertydict: dict[Property] The dictionary containing properties of the feature. Returns ------- - dict + dict[Property] The updated property dictionary with the `key` property set. """ From 4d94ed0b5a883dd892448b1cf32aec352770bb94 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 30 Jun 2025 21:27:42 +0200 Subject: [PATCH 081/118] Update test_features.py --- deeptrack/tests/test_features.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index 887a2675c..615923583 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -1609,9 +1609,12 @@ def merge_function(images): image_1 = np.ones((5, 5)) * 2 image_2 = np.ones((5, 5)) * 4 - expected_output = np.ones((5, 5)) * 3 output_image = merge_feature.resolve([image_1, image_2]) - self.assertIsNone(np.testing.assert_array_almost_equal(output_image, expected_output)) + self.assertIsNone( + np.testing.assert_array_almost_equal( + output_image, np.ones((5, 5)) * 3, + ) + ) image_1 = np.ones((5, 5)) * 2 image_2 = np.ones((3, 3)) * 4 @@ -1620,16 +1623,20 @@ def merge_function(images): image_1 = np.ones((5, 5)) * 2 output_image = merge_feature.resolve([image_1]) - self.assertIsNone(np.testing.assert_array_almost_equal(output_image, image_1)) + self.assertIsNone( + np.testing.assert_array_almost_equal( + output_image, image_1, + ) + ) def test_OneOf(self): - """Set up the features and input image for testing.""" + # Set up the features and input image for testing. feature_1 = features.Add(value=10) feature_2 = features.Multiply(value=2) input_image = np.array([1, 2, 3]) - """Test that OneOf applies one of the features randomly.""" + # Test that OneOf applies one of the features randomly. one_of_feature = features.OneOf([feature_1, feature_2]) output_image = one_of_feature.resolve(input_image) @@ -1638,14 +1645,16 @@ def test_OneOf(self): # - self.input_image * 2 (if feature_2 is chosen) expected_outputs = [ input_image + 10, - input_image * 2 + input_image * 2, ] self.assertTrue( - any(np.array_equal(output_image, expected) for expected in expected_outputs), - f"Output {output_image} did not match any expected transformations." + any( + np.array_equal(output_image, expected) + for expected in expected_outputs + ) ) - """Test that OneOf applies the selected feature when `key` is provided.""" + # Test that OneOf applies the selected feature when `key` is provided. controlled_feature = features.OneOf([feature_1, feature_2], key=0) output_image = controlled_feature.resolve(input_image) expected_output = input_image + 10 From a58bdc8340995a8498149f931ccf5b23c49f638b Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 30 Jun 2025 21:58:00 +0200 Subject: [PATCH 082/118] Update features.py --- deeptrack/features.py | 54 ++++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 58c866dc7..95535556e 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -184,7 +184,7 @@ def merge_features( "Lambda", "Merge", "OneOf", - "OneOfDict", # TODO + "OneOfDict", "LoadImage", # TODO "SampleToMasks", # TODO "AsType", # TODO @@ -4457,10 +4457,10 @@ class OneOf(Feature): Methods ------- - `_process_properties(propertydict: dict[Property]) -> dict[Property]` - Processes the properties to determine the selected feature index. + `_process_properties(propertydict: dict) -> dict` + It processes the properties to determine the selected feature index. `get(image: Any, key: int, _ID: tuple[int, ...], **kwargs: Any) -> Any` - Applies the selected feature to the input. + It applies the selected feature to the input. Examples -------- @@ -4480,7 +4480,7 @@ class OneOf(Feature): Apply the `OneOf` feature to the input image: >>> output_image = one_of_feature(input_image) - >>> output_image # The output depends on the randomly selected feature. + >>> output_image # The output depends on the randomly selected feature. Use `key` to apply a specific feature: >>> controlled_feature = dt.OneOf([feature_1, feature_2], key=0) @@ -4528,20 +4528,20 @@ def __init__( def _process_properties( self: Feature, - propertydict: dict[Property], - ) -> dict[Property]: + propertydict: dict, + ) -> dict: """Process the properties to determine the feature index. If `key` is not provided, a random feature index is assigned. Parameters ---------- - propertydict: dict[Property] + propertydict: dict The dictionary containing properties of the feature. Returns ------- - dict[Property] + dict The updated property dictionary with the `key` property set. """ @@ -4587,9 +4587,9 @@ def get( class OneOfDict(Feature): """Resolve one feature from a dictionary and apply it to an input. - This feature selects a feature from a dictionary and applies it to an input. - The selection is made randomly by default, but it can be controlled using - the `key` argument. + This feature selects a feature from a dictionary and applies it to an + input. The selection is made randomly by default, but it can be controlled + using the `key` argument. If `key` is not specified, a random key from the dictionary is selected, and the corresponding feature is applied. Otherwise, the feature mapped to @@ -4609,35 +4609,46 @@ class OneOfDict(Feature): ---------- __distributed__: bool Indicates whether this feature distributes computation across inputs. + It defaults to `False`. Methods ------- `_process_properties(propertydict: dict) -> dict` - Determines which feature to use based on `key`. - `get(image: Any, key: Any, _ID: tuple[int, ...], **kwargs: dict[str, Any]) -> Any` - Resolves the selected feature and applies it to the input image. + It determines which feature to use based on `key`. + `get(image: Any, key: Any, _ID: tuple[int, ...], **kwargs: Any) -> Any` + It resolves the selected feature and applies it to the input image. Examples -------- >>> import deeptrack as dt - >>> import numpy as np Define a dictionary of features: >>> features_dict = { ... "add": dt.Add(value=10), ... "multiply": dt.Multiply(value=2), ... } + + Create a `OneOfDict` feature that randomly selects a transformation: >>> one_of_dict_feature = dt.OneOfDict(features_dict) - Apply a randomly selected feature: + Creare an image: + >>> import numpy as np + >>> >>> input_image = np.array([1, 2, 3]) + + Apply a randomly selected feature to the image: >>> output_image = one_of_dict_feature(input_image) - >>> print(output_image) + >>> output_image # The output depends on the randomly selected feature. + + Potentially select a different feature: + >>> output_image = one_of_dict_feature.update()(input_image) + >>> output_image Use a specific key to apply a predefined feature: >>> controlled_feature = dt.OneOfDict(features_dict, key="add") >>> output_image = controlled_feature(input_image) - >>> print(output_image) # Adds 10 to each element. + >>> output_image + array([11, 12, 13]) """ @@ -4664,6 +4675,7 @@ def __init__( """ super().__init__(key=key, **kwargs) + self.collection = collection # Add all features in the dictionary as dependencies. @@ -4671,8 +4683,8 @@ def __init__( self.add_feature(feature) def _process_properties( - self: Feature, - propertydict: dict + self: Feature, + propertydict: dict, ) -> dict: """Determine which feature to apply based on the selected key. From 485fd1df8e598f431a674a1b326eec95d964cc5b Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 30 Jun 2025 21:58:02 +0200 Subject: [PATCH 083/118] Update test_features.py --- deeptrack/tests/test_features.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index 615923583..0f631d999 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -1727,7 +1727,6 @@ def test_OneOf_tuple(self): self.assertRaises(IndexError, lambda: values.update().resolve(key=3)) - def test_OneOf_set(self): values = features.OneOf( @@ -1795,26 +1794,24 @@ def test_OneOfDict(self): input_image = np.array([1, 2, 3]) - """Test that OneOfDict selects a feature randomly and applies it correctly.""" + # Test OneOfDict selects a feature randomly and applies it correctly. output_image = one_of_dict_feature.resolve(input_image) expected_outputs = [ input_image + 10, # "add" - input_image * 2, # "multiply" + input_image * 2, # "multiply" ] - self.assertTrue( - any(np.array_equal(output_image, expected) for expected in expected_outputs), - f"Output {output_image} did not match any expected transformations." - ) + self.assertTrue(any(np.array_equal(output_image, expected) + for expected in expected_outputs)) - """Test that OneOfDict selects the correct feature when a key is specified.""" + # Test OneOfDict selects the correct feature when a key is specified. controlled_feature = features.OneOfDict(features_dict, key="add") output_image = controlled_feature.resolve(input_image) - expected_output = input_image + 10 # The "add" feature should be applied + expected_output = input_image + 10 self.assertTrue(np.array_equal(output_image, expected_output)) controlled_feature = features.OneOfDict(features_dict, key="multiply") output_image = controlled_feature.resolve(input_image) - expected_output = input_image * 2 # The "multiply" feature should be applied + expected_output = input_image * 2 self.assertTrue(np.array_equal(output_image, expected_output)) From ad358a34f609da66668b84cb2c597b1159ea9b52 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 30 Jun 2025 22:28:21 +0200 Subject: [PATCH 084/118] Update test_features.py --- deeptrack/tests/test_features.py | 36 +++++++++++++++++++------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index 0f631d999..ee44ccfcd 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -1820,7 +1820,7 @@ def test_LoadImage(self): from PIL import Image as PIL_Image import os - """Create temporary image files in multiple formats for testing.""" + # Create temporary image files in multiple formats for testing. test_image_array = (np.random.rand(50, 50) * 255).astype(np.uint8) try: @@ -1833,39 +1833,45 @@ def test_LoadImage(self): # png_filename = temp_png.name with NamedTemporaryFile(suffix=".jpg", delete=False) as temp_jpg: - PIL_Image.fromarray(test_image_array).convert("RGB").save(temp_jpg.name) + PIL_Image.fromarray(test_image_array).convert("RGB") \ + .save(temp_jpg.name) # jpg_filename = temp_jpg.name - - """Test loading a .npy file.""" + # Test loading a .npy file. load_feature = features.LoadImage(path=temp_npy.name) loaded_image = load_feature.resolve() - self.assertEqual(loaded_image.shape[:2], test_image_array.shape[:2]) + self.assertEqual(loaded_image.shape[:2], + test_image_array.shape[:2]) - """Test loading a .png file.""" + # Test loading a .png file. load_feature = features.LoadImage(path=temp_png.name) loaded_image = load_feature.resolve() - self.assertEqual(loaded_image.shape[:2], test_image_array.shape[:2]) + self.assertEqual(loaded_image.shape[:2], + test_image_array.shape[:2]) - """Test loading a .jpg file.""" + # Test loading a .jpg file. load_feature = features.LoadImage(path=temp_jpg.name) loaded_image = load_feature.resolve() - self.assertEqual(loaded_image.shape[:2], test_image_array.shape[:2]) - - """Test loading an image and converting it to grayscale.""" - load_feature = features.LoadImage(path=temp_png.name, to_grayscale=True) + self.assertEqual(loaded_image.shape[:2], + test_image_array.shape[:2]) + + # Test loading an image and converting it to grayscale. + load_feature = features.LoadImage(path=temp_png.name, + to_grayscale=True) loaded_image = load_feature.resolve() - self.assertEqual(loaded_image.shape[-1], 1) + self.assertEqual(loaded_image.shape[-1], 1) - """Test ensuring a minimum number of dimensions.""" + # Test ensuring a minimum number of dimensions. load_feature = features.LoadImage(path=temp_png.name, ndim=4) loaded_image = load_feature.resolve() - self.assertGreaterEqual(len(loaded_image.shape), 4) + self.assertGreaterEqual(len(loaded_image.shape), 4) finally: for file in [temp_npy.name, temp_png.name, temp_jpg.name]: os.remove(file) + #TODO: Add a test for loading a list of images. + def test_SampleToMasks(self): # Parameters From 5610d400a4003c7925a0e677a20f5568233f5b21 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 30 Jun 2025 22:28:23 +0200 Subject: [PATCH 085/118] Update features.py --- deeptrack/features.py | 79 +++++++++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 95535556e..8ba399951 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -3100,7 +3100,7 @@ class Arguments(Feature): -------- >>> import deeptrack as dt - Create a temporary image: + Create a temporary image file: >>> import numpy as np >>> import PIL, tempfile >>> @@ -4772,10 +4772,11 @@ class LoadImage(Feature): ---------- __distributed__: bool Indicates whether this feature distributes computation across inputs. + It defaults to `False`. Methods ------- - `get(image: Any, path: str | list[str], load_options: dict[str, Any] | None, ndim: int, to_grayscale: bool, as_list: bool, get_one_random: bool, **kwargs: dict[str, Any]) -> np.ndarray` + `get(image: Any, path: str or list[str], load_options: dict[str, Any] | None, ndim: int, to_grayscale: bool, as_list: bool, get_one_random: bool, **kwargs: Any) -> array` Load the image(s) from disk and process them. Raises @@ -4786,27 +4787,43 @@ class LoadImage(Feature): Examples -------- >>> import deeptrack as dt - >>> import numpy as np - >>> from tempfile import NamedTemporaryFile Create a temporary image file: - >>> temp_file = NamedTemporaryFile(suffix=".npy", delete=False) - >>> np.save(temp_file.name, np.random.rand(100, 100)) + >>> import numpy as np + >>> import os, tempfile + >>> + >>> temp_file = tempfile.NamedTemporaryFile(suffix=".npy", delete=False) + >>> np.save(temp_file.name, np.random.rand(100, 100, 3)) Load the image using `LoadImage`: - >>> load_image_feature = dt.LoadImage(path=temp_file.name, to_grayscale=True) + >>> load_image_feature = dt.LoadImage(path=temp_file.name) >>> loaded_image = load_image_feature.resolve() Print image shape: - >>> print(loaded_image.shape) + >>> loaded_image.shape + (100, 100, 3) - If `to_grayscale=True`, the image is converted to grayscale (single channel). - If `ndim=4`, additional dimensions are added if necessary. + If `to_grayscale=True`, the image is converted to single channel: + >>> load_image_feature = dt.LoadImage( + ... path=temp_file.name, + ... to_grayscale=True, + ... ) + >>> loaded_image = load_image_feature.resolve() + >>> loaded_image.shape + (100, 100, 1) + + If `ndim=4`, additional dimensions are added if necessary: + >>> load_image_feature = dt.LoadImage( + ... path=temp_file.name, + ... ndim=4, + ... ) + >>> loaded_image = load_image_feature.resolve() + >>> loaded_image.shape + (2, 2, 3, 1) Cleanup the temporary file: - >>> import os >>> os.remove(temp_file.name) - + """ __distributed__: bool = False @@ -4826,23 +4843,23 @@ def __init__( Parameters ---------- path: PropertyLike[str or list[str]] - The path(s) to the image(s) to load. Can be a single string or a list - of strings. + The path(s) to the image(s) to load. Can be a single string or a + list of strings. load_options: PropertyLike[dict[str, Any]], optional - Additional options passed to the file reader (e.g., `mode` for OpenCV, - `allow_pickle` for NumPy). It defaults to `None`. + Additional options passed to the file reader (e.g., `mode` for + OpenCV, `allow_pickle` for NumPy). It defaults to `None`. as_list: PropertyLike[bool], optional - If `True`, treats the first dimension of the image as a list of images. - It defaults to `False`. + If `True`, treats the first dimension of the image as a list of + images. It defaults to `False`. ndim: PropertyLike[int], optional - Ensures the image has at least this many dimensions. If the loaded image - has fewer dimensions, extra dimensions are added. It defaults to - `3`. + Ensures the image has at least this many dimensions. If the loaded + image has fewer dimensions, extra dimensions are added. It defaults + to `3`. to_grayscale: PropertyLike[bool], optional If `True`, converts the image to grayscale. It defaults to `False`. get_one_random: PropertyLike[bool], optional - If `True`, selects a single random image from a stack when `as_list=True`. - It defaults to `False`. + If `True`, selects a single random image from a stack when + `as_list=True`. It defaults to `False`. **kwargs: Any Additional keyword arguments passed to the parent `Feature` class, allowing further customization. @@ -4869,7 +4886,7 @@ def get( as_list: bool, get_one_random: bool, **kwargs: Any, - ) -> np.ndarray: + ) -> NDArray | torch.Tensor: """Load and process an image or a list of images from disk. This method attempts to load an image using multiple file readers @@ -4897,14 +4914,15 @@ def get( get_one_random: bool If `True`, selects a single random image from a multi-frame stack when `as_list=True`. It defaults to `False`. - **kwargs: dict[str, Any] + **kwargs: Any Additional keyword arguments. Returns ------- - np.ndarray + array The loaded and processed image(s). If `as_list=True`, returns a - list of images; otherwise, returns a single NumPy array. + list of images; otherwise, returns a single NumPy array or PyTorch + tensor. Raises ------ @@ -4932,7 +4950,7 @@ def get( try: import PIL.Image - image = [PIL.Image.open(file, **load_options) + image = [PIL.Image.open(file, **load_options) for file in path] except (IOError, ImportError): import cv2 @@ -4959,7 +4977,7 @@ def get( try: import skimage - skimage.color.rgb2gray(image) + image = skimage.color.rgb2gray(image) except ValueError: import warnings @@ -4969,6 +4987,9 @@ def get( while ndim and image.ndim < ndim: image = np.expand_dims(image, axis=-1) + # Convert to PyTorch tensor if needed. + #TODO + return image From eb17babe7ef09e3e66019e28b263123ea9c147f1 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Mon, 30 Jun 2025 22:41:17 +0200 Subject: [PATCH 086/118] Update features.py --- deeptrack/features.py | 55 +++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 8ba399951..97f564048 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -5043,16 +5043,16 @@ class SampleToMasks(Feature): Examples ------- >>> import deeptrack as dt - >>> import matplotlib.pyplot as plt - >>> import numpy as np Define number of particles: >>> n_particles = 12 Define optics and particles: + >>> import numpy as np + >>> >>> optics = dt.Fluorescence(output_region=(0, 0, 64, 64)) >>> particle = dt.PointParticle( - >>> position=lambda: np.random.uniform(5, 55, size=2) + >>> position=lambda: np.random.uniform(5, 55, size=2), >>> ) >>> particles = particle ^ n_particles @@ -5061,7 +5061,7 @@ class SampleToMasks(Feature): >>> sim_mask_pip = particles >> dt.SampleToMasks( ... lambda: lambda particles: particles > 0, ... output_region=optics.output_region, - ... merge_method="or" + ... merge_method="or", ... ) >>> pipeline = sim_im_pip & sim_mask_pip >>> pipeline.store_properties() @@ -5073,12 +5073,14 @@ class SampleToMasks(Feature): >>> positions = np.array(image.get_property("position", get_one=False)) Visualize results: + >>> import matplotlib.pyplot as plt + >>> >>> plt.subplot(1, 2, 1) >>> plt.imshow(image, cmap="gray") >>> plt.title("Original Image") >>> plt.subplot(1, 2, 2) >>> plt.imshow(mask, cmap="gray") - >>> plt.scatter(positions[:,1], positions[:,0], c="r", marker="x", s = 10) + >>> plt.scatter(positions[:,1], positions[:,0], c="y", marker="x", s = 50) >>> plt.title("Mask") >>> plt.show() @@ -5300,39 +5302,40 @@ class AsType(Feature): Parameters ---------- - dtype: PropertyLike[Any], optional + dtype: PropertyLike[str], optional The desired data type for the image. It defaults to `"float64"`. - **kwargs:: dict of str to Any + **kwargs: Any Additional keyword arguments passed to the parent `Feature` class. Methods ------- - `get(image: np.ndarray, dtype: str, **kwargs: dict[str, Any]) -> np.ndarray` + `get(image: array, dtype: str, **kwargs: Any) -> array` Convert the data type of the input image. Examples -------- - >>> import numpy as np - >>> from deeptrack.features import AsType + >>> import deeptrack as dt Create an input array: + >>> import numpy as np + >>> >>> input_image = np.array([1.5, 2.5, 3.5]) Apply an AsType feature to convert to `int32`: - >>> astype_feature = AsType(dtype="int32") + >>> astype_feature = dt.AsType(dtype="int32") >>> output_image = astype_feature.get(input_image, dtype="int32") - >>> print(output_image) - [1 2 3] + >>> output_image + array([1, 2, 3], dtype=int32) Verify the data type: - >>> print(output_image.dtype) - int32 + >>> output_image.dtype + dtype('int32') """ def __init__( self: Feature, - dtype: PropertyLike[Any] = "float64", + dtype: PropertyLike[str] = "float64", **kwargs: Any, ): """ @@ -5340,9 +5343,9 @@ def __init__( Parameters ---------- - dtype: PropertyLike[Any], optional + dtype: PropertyLike[str], optional The desired data type for the image. It defaults to `"float64"`. - **kwargs:: dict of str to Any + **kwargs: Any Additional keyword arguments passed to the parent `Feature` class. """ @@ -5351,16 +5354,17 @@ def __init__( def get( self: Feature, - image: np.ndarray, + image: NDArray | torch.Tensor | Image, dtype: str, **kwargs: Any, - ) -> np.ndarray: + ) -> NDArray | torch.Tensor | Image: """Convert the data type of the input image. Parameters ---------- - image: np.ndarray - The input image to process. + image: array + The input image to process. It can be a NumPy array, a PyTorch + tensor, or an Image. dtype: str The desired data type for the image. **kwargs: Any @@ -5368,8 +5372,9 @@ def get( Returns ------- - np.ndarray - The input image converted to the specified data type. + array + The input image converted to the specified data type. It can be a + NumPy array, a PyTorch tensor, or an Image. """ @@ -5642,7 +5647,7 @@ def get( # Downscale the result to the original resolution. import skimage.measure - + image = skimage.measure.block_reduce( image, (factor[0], factor[1]) + (1,) * (image.ndim - 2), np.mean ) From 6ccdfac3d7919fd3fcff586abd7b01bed4ccb1bb Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 1 Jul 2025 08:05:30 +0200 Subject: [PATCH 087/118] Update test_features.py --- deeptrack/tests/test_features.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/deeptrack/tests/test_features.py b/deeptrack/tests/test_features.py index ee44ccfcd..4fb75ba42 100644 --- a/deeptrack/tests/test_features.py +++ b/deeptrack/tests/test_features.py @@ -1922,6 +1922,7 @@ def test_SampleToMasks(self): def test_AsType(self): + # Test for Numpy arrays. input_image = np.array([1.5, 2.5, 3.5]) data_types = ["float64", "int32", "uint16", "int16", "uint8", "int8"] @@ -1937,6 +1938,12 @@ def test_AsType(self): np.all(output_image == np.array([1, 2, 3], dtype=dtype)) ) + # Test for Image. + #TODO + + # Test for PyTorch tensors. + #TODO + def test_ChannelFirst2d(self): From 4b4d5476d3628582c8c06125cfb33f1c4d44636b Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 1 Jul 2025 18:46:20 +0200 Subject: [PATCH 088/118] Update features.py --- deeptrack/features.py | 164 ++++++++++++++++++++++++++++++------------ 1 file changed, 119 insertions(+), 45 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 97f564048..015453279 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -214,31 +214,56 @@ def merge_features( class Feature(DeepTrackNode): """Base feature class. - Features define the image generation process. All features operate on lists - of images. Most features, such as noise, apply some tranformation to all - images in the list. This transformation can be additive, such as adding - some Gaussian noise or a background illumination, or non-additive, such as - introducing Poisson noise or performing a low-pass filter. This - transformation is defined by the method `get(image, **kwargs)`, which all - implementations of the class `Feature` need to define. - - Whenever a Feature is initiated, all keyword arguments passed to the - constructor will be wrapped as a `Property`, and stored in the `properties` - attribute as a `PropertyDict`. When a Feature is resolved, the current - value of each property is sent as input to the get method. + Features define the image generation process. + + All features operate on lists of images. Most features, such as noise, + apply a tranformation to all images in the list. This transformation can be + additive, such as adding some Gaussian noise or a background illumination, + or non-additive, such as introducing Poisson noise or performing a low-pass + filter. This transformation is defined by the `get(image, **kwargs)` + method, which all implementations of the class `Feature` need to define. + This method operates on a single image at a time. + + Whenever a Feature is initialized, it wraps all keyword arguments passed to + the constructor as `Property` objects, and stored in the `properties` + attribute as a `PropertyDict`. + + When a Feature is resolved, the current value of each property is sent as + input to the get method. + + **Computational Backends and Data Types** + + This class also provides mechanisms for managing numerical types and + computational backends. + + Supported backends include NumPy and PyTorch. The active backend is + determined at initialization and stored in the `_backend` attribute, which + is used internally to control how computations are executed. The backend + can be switched using the `.numpy()` and `.torch()` methods. + + Numerical types used in computation (float, int, complex, and bool) can be + configured using the `.dtype()` method. The chosen types are retrieved + via the properties `float_dtype`, `int_dtype`, `complex_dtype`, and + `bool_dtype`. These are resolved dynamically using the backend's internal + type resolution system and are used in downstream computations. + + The computational device (e.g., "cpu" or a specific GPU) is managed through + the `.to()` method and accessed via the `device` property. This is + especially relevant for PyTorch backends, which support GPU acceleration. Parameters ---------- - _input: np.ndarray or Image or list[np.ndarray or Image], optional. - A list of np.ndarray or `DeepTrackNode` objects or a single np.ndarray - or an `Image` object representing the input data for the feature. This - parameter specifies what the feature will process. If left empty, no - initial input is set. - **kwargs: dict of str and Any + _input: Any, optional. + The input data for the feature. If left empty, no initial input is set. + It is most commonly a NumPy array, PyTorch tensor, or Image object, or + a list of NumPy arrays, PyTorch tensors, or Image objects; however, it + can be anything. + **kwargs: Any Keyword arguments to configure the feature. Each keyword argument is wrapped as a `Property` and added to the `properties` attribute, allowing dynamic sampling and parameterization during the feature's - execution. + execution. These properties are passed to the `get()` method when a + feature is resolved. Attributes ---------- @@ -248,6 +273,37 @@ class Feature(DeepTrackNode): dynamically sample values during pipeline execution. A sampled copy of this dictionary is passed to the `get` function and appended to the properties of the output image. + _input: DeepTrackNode + A node representing the input data for the feature. It is most commonly + a NumPy array, PyTorch tensor, or Image object, or a list of NumPy + arrays, PyTorch tensors, or Image objects; however, it can be anything. + It supports lazy evaluation and graph traversal. + _random_seed: DeepTrackNode + A node representing the feature’s random seed. This allows for + deterministic behavior when generating random elements, and ensures + reproducibility during evaluation. + arguments: Feature | None + An optional `Feature` whose properties are bound to this feature. This + allows dynamic property sharing and centralized parameter management + in complex pipelines. + __list_merge_strategy__: int + Specifies how the output of `.get(image, **kwargs)` is merged with the + current `_input`. Options include: + - `MERGE_STRATEGY_OVERRIDE` (0, default): `_input` is replaced by the + new output. + - `MERGE_STRATEGY_APPEND` (1): The output is appended to the end of + `_input`. + __distributed__: bool + Determines whether `.get(image, **kwargs)` is applied to each element + of the input list independently (`__distributed__ = True`) or to the + list as a whole (`__distributed__ = False`). + __conversion_table__: ConversionTable + Defines the unit conversions used by the feature to convert its + properties into the desired units. + _wrap_array_with_image: bool + Internal flag that determines whether arrays are wrapped as `Image` + instances during evaluation. When `True`, image metadata and properties + are preserved and propagated. It defaults to `False`. float_dtype: np.dtype The data type of the float numbers. int_dtype: np.dtype @@ -260,29 +316,14 @@ class Feature(DeepTrackNode): The device on which the feature is executed. _backend: Config The computational backend. - __list_merge_strategy__: int - Specifies how the output of `.get(image, **kwargs)` is merged with the - input list. Options include: - - `MERGE_STRATEGY_OVERRIDE` (0, default): The input list is replaced by - the new list. - - `MERGE_STRATEGY_APPEND` (1): The new list is appended to the end of - the input list. - __distributed__: bool - Determines whether `.get(image, **kwargs)` is applied to each element - of the input list independently (`__distributed__ = True`) or to the - list as a whole (`__distributed__ = False`). - __property_memorability__: int - Specifies whether to store the feature’s properties in the output - image. Properties with a memorability value of `1` or lower are stored - by default. - __conversion_table__: ConversionTable - Defines the unit conversions used by the feature to convert its - properties into the desired units. Methods ------- - `get(image: np.ndarray | list[np.ndarray] | Image | list[Image], **kwargs: Any) -> Image | list[Image]` - Abstract method that defines how the feature transforms the input. + `get(image: Any, **kwargs: Any) -> Any` + Abstract method that defines how the feature transforms the input. The + input is most commonly a NumPy array, PyTorch tensor, or Image object, + or a list of NumPy arrays, PyTorch tensors, or Image objects; however, + it can be anything. `__call__(image_list: np.ndarray | list[np.ndarray] | Image | list[Image] | None = None, _ID: tuple[int, ...] = (), **kwargs: Any) -> Any` Executes the feature or pipeline on the input and applies property overrides from `kwargs`. @@ -387,6 +428,10 @@ class Feature(DeepTrackNode): `_no_wrap_process_output(image_list: np.ndarray | list[np.ndarray] | Image | list[Image], **kwargs: Any) -> None` Processes the output of the feature. + Examples + -------- + TODO + """ properties: PropertyDict @@ -396,14 +441,15 @@ class Feature(DeepTrackNode): __list_merge_strategy__ = MERGE_STRATEGY_OVERRIDE __distributed__ = True - __property_memorability__ = 1 __conversion_table__ = ConversionTable() _wrap_array_with_image: bool = False + _float_dtype: str _int_dtype: str _complex_dtype: str _device: str | torch.device + _backend: Config @property def float_dtype(self) -> np.dtype | torch.dtype: @@ -3894,7 +3940,7 @@ class ConditionalSetProperty(StructuralFeature): the given properties are applied; otherwise, the child feature remains unchanged. - It is advisable to use `dt.Arguments` instead when possible, since this + It is advisable to use `Arguments` instead when possible, since this feature overwrites properties, which may affect future calls to the feature. @@ -3921,6 +3967,11 @@ class ConditionalSetProperty(StructuralFeature): Resolves the child feature, conditionally applying the specified properties. + Warnings + -------- + Deprecation: This feature is deprecated and may be removed in a future + release. It is recommended to use `Arguments` instead. + Examples -------- >>> import deeptrack as dt @@ -3970,7 +4021,7 @@ class ConditionalSetProperty(StructuralFeature): """ def __init__( - self: Feature, + self: ConditionalSetProperty, feature: Feature, condition: PropertyLike[str | bool] | None = None, **kwargs: Any, @@ -3991,6 +4042,14 @@ def __init__( """ + import warnings + + warnings.warn( + "ConditionalSetFeature is deprecated and may be removed in a " + "future release. Please use Arguments instead when possible.", + DeprecationWarning, + ) + if isinstance(condition, str): kwargs.setdefault(condition, True) @@ -3999,7 +4058,7 @@ def __init__( self.feature = self.add_feature(feature) def get( - self: Feature, + self: ConditionalSetProperty, image: Any, condition: str | bool, **kwargs: Any, @@ -4056,6 +4115,8 @@ class ConditionalSetFeature(StructuralFeature): Both `on_true` and `on_false` are updated during each call, even if only one is resolved. + It is advisable to use `Arguments` instead when possible. + Parameters ---------- on_false: Feature, optional @@ -4076,6 +4137,11 @@ class ConditionalSetFeature(StructuralFeature): `get(image: Any, condition: str or bool, **kwargs: Any) -> Any` Resolves the appropriate feature based on the condition. + Warnings + -------- + Deprecation: This feature is deprecated and may be removed in a future + release. It is recommended to use `Arguments` instead. + Examples -------- >>> import deeptrack as dt @@ -4130,7 +4196,7 @@ class ConditionalSetFeature(StructuralFeature): """ def __init__( - self: Feature, + self: ConditionalSetFeature, on_false: Feature | None = None, on_true: Feature | None = None, condition: PropertyLike[str | bool] = True, @@ -4152,6 +4218,14 @@ def __init__( """ + import warnings + + warnings.warn( + "ConditionalSetFeature is deprecated and may be removed in a " + "future release. Please use Arguments instead when possible.", + DeprecationWarning, + ) + if isinstance(condition, str): kwargs.setdefault(condition, True) @@ -4167,7 +4241,7 @@ def __init__( self.on_false = on_false def get( - self: Feature, + self: ConditionalSetFeature, image: Any, *, condition: str | bool, From 0cc01329c5f47f684328dc2898597ffbd1d06f0b Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 1 Jul 2025 18:56:48 +0200 Subject: [PATCH 089/118] Update features.py --- deeptrack/features.py | 67 ++++++++++++++++++------------------------- 1 file changed, 28 insertions(+), 39 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 015453279..18dd17e1b 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -322,8 +322,7 @@ class Feature(DeepTrackNode): `get(image: Any, **kwargs: Any) -> Any` Abstract method that defines how the feature transforms the input. The input is most commonly a NumPy array, PyTorch tensor, or Image object, - or a list of NumPy arrays, PyTorch tensors, or Image objects; however, - it can be anything. + but it can be anything. `__call__(image_list: np.ndarray | list[np.ndarray] | Image | list[Image] | None = None, _ID: tuple[int, ...] = (), **kwargs: Any) -> Any` Executes the feature or pipeline on the input and applies property overrides from `kwargs`. @@ -478,28 +477,22 @@ def device(self) -> str | torch.device: def __init__( self: Feature, - _input: ( - NDArray - | list[NDArray] - | torch.Tensor - | list[torch.Tensor] - | Image - | list[Image] - ) = [], + _input: Any = [], **kwargs: Any, - ) -> None: + ): """Initialize a new Feature instance. Parameters ---------- - _input: np.ndarray or list[np.ndarray] or torch.Tensor or list[torch.Tensor] or Image or list[Images], optional - The initial input(s) for the feature, often images or other data. - If not provided, defaults to an empty list. + _input: Any, optional + The initial input(s) for the feature. It is most commonly a NumPy + array, PyTorch tensor, or Image object, or a list of NumPy arrays, + PyTorch tensors, or Image objects; however, it can be anything. If + not provided, defaults to an empty list. **kwargs: Any Keyword arguments that are wrapped into `Property` instances and stored in `self.properties`, allowing for dynamic or parameterized - behavior. - If not provided, defaults to an empty list. + behavior. If not provided, it defaults to an empty list. """ @@ -540,25 +533,26 @@ def __init__( def get( self: Feature, - image: np.ndarray | list[np.ndarray] | Image | list[Image], + image: Any, **kwargs: Any, - ) -> Image | list[Image]: - """Transform an image [abstract method]. + ) -> Any: + """Transform an input (abstract method). Abstract method that defines how the feature transforms the input. The current value of all properties will be passed as keyword arguments. Parameters ---------- - image: np.ndarray or Image or list[np.ndarray or Image] - The image or list of images to transform. + image: Any + The input to transform. It is most commonly a NumPy array, PyTorch + tensor, or Image object, but it can be anything. **kwargs: Any The current value of all properties in `properties`, as well as any global arguments passed to the feature. Returns ------- - Image or list[Image] + Any The transformed image or list of images. Raises @@ -572,19 +566,7 @@ def get( def __call__( self: Feature, - image_list: ( - Feature - | list[Feature] - | NDArray[Any] - | list[NDArray[Any]] - | torch.Tensor - | list[torch.Tensor] - | Image - | list[Image] - | Any - | list[Any] - | None - ) = None, + image_list: Any = None, _ID: tuple[int, ...] = (), **kwargs: Any, ) -> Any: @@ -600,9 +582,12 @@ def __call__( Parameters ---------- - image_list: np.ndarrray or Image or list[np.ndarrray or Image], optional - The input to the feature or pipeline. If `None`, the feature uses - previously set input values or propagates properties. + image_list: Any, optional + The input to the feature or pipeline. It is most commonly a NumPy + array, PyTorch tensor, or Image object, or a list of NumPy arrays, + PyTorch tensors, or Image objects; however, it can be anything. It + defaults to `None`, in which case the feature uses the previous set + input values or propagates properties. **kwargs: Any Additional parameters passed to the pipeline. These override properties with matching names. For example, calling @@ -614,7 +599,11 @@ def __call__( ------- Any The output of the feature or pipeline after execution. - + + Examples + -------- + TODO: basic examples + examples with overwriting of features + """ with config.with_backend(self._backend): From 362ad856a5ddd3ebbebe771e1c8c5ac38c6b4845 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 1 Jul 2025 18:57:59 +0200 Subject: [PATCH 090/118] Update features.py --- deeptrack/features.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 18dd17e1b..f914499b6 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -323,8 +323,8 @@ class Feature(DeepTrackNode): Abstract method that defines how the feature transforms the input. The input is most commonly a NumPy array, PyTorch tensor, or Image object, but it can be anything. - `__call__(image_list: np.ndarray | list[np.ndarray] | Image | list[Image] | None = None, _ID: tuple[int, ...] = (), **kwargs: Any) -> Any` - Executes the feature or pipeline on the input and applies property + `__call__(image_list: Any = None, _ID: tuple[int, ...] = (), **kwargs: Any) -> Any` + It executes the feature or pipeline on the input and applies property overrides from `kwargs`. `store_properties(x: bool = True, recursive: bool = True) -> None` Controls whether the properties are stored in the output `Image` object. From 07383ce697c81194b7d97f8fe6fb482ce95f5577 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 1 Jul 2025 18:58:50 +0200 Subject: [PATCH 091/118] Update features.py --- deeptrack/features.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index f914499b6..9444a9d5e 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -137,7 +137,7 @@ def merge_features( from scipy.spatial.distance import cdist from deeptrack import units -from deeptrack.backend import config, TORCH_AVAILABLE, xp +from deeptrack.backend import config, Config, TORCH_AVAILABLE, xp from deeptrack.backend.core import DeepTrackNode from deeptrack.backend.units import ConversionTable, create_context from deeptrack.image import Image From 0be5371c2161c226bc7bec093dcb07fd39bcc076 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 1 Jul 2025 19:14:31 +0200 Subject: [PATCH 092/118] Update _config.py --- deeptrack/backend/_config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/deeptrack/backend/_config.py b/deeptrack/backend/_config.py index 4a578f149..3441e358b 100644 --- a/deeptrack/backend/_config.py +++ b/deeptrack/backend/_config.py @@ -144,6 +144,7 @@ __all__ = [ "config", + "Config", "OPENCV_AVAILABLE", "TORCH_AVAILABLE", "xp", From fffdf6d1fbd1b6cc164ed634ab92c8b6026d5170 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 1 Jul 2025 19:14:33 +0200 Subject: [PATCH 093/118] Update features.py --- deeptrack/features.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 9444a9d5e..81ac8f616 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -598,11 +598,30 @@ def __call__( Returns ------- Any - The output of the feature or pipeline after execution. + The output of the feature or pipeline after execution. This is + typically a NumPy array, PyTorch tensor, or Image object, or a list + of NumPy arrays, PyTorch tensors, or Image objects. Examples -------- - TODO: basic examples + examples with overwriting of features + >>> import deeptrack as dt + + Deafine a feature: + >>> feature = dt.Add(value=2) + + Call this feature with an input: + >>> import numpy as np + >>> + >>> feature(np.array([1, 2, 3])) + array([3, 4, 5]) + + Execute the feature with previously set input: + >>> feature() # Uses stored input + array([3, 4, 5]) + + Override a property: + >>> feature(np.array([1, 2, 3]), value=10) + array([11, 12, 13]) """ From df8330f6735b7854939653cd9102295deddd0af3 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 1 Jul 2025 19:19:37 +0200 Subject: [PATCH 094/118] Update features.py --- deeptrack/features.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 81ac8f616..25b60a168 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -629,7 +629,8 @@ def __call__( # If image_list is as Source, activate it. self._activate_sources(image_list) - # Potentially fragile. Maybe a special variable dt._last_input instead? + # Potentially fragile. + # Maybe a special variable dt._last_input instead? # If the input is not empty, set the value of the input. if ( image_list is not None @@ -639,29 +640,31 @@ def __call__( ): self._input.set_value(image_list, _ID=_ID) - # A dict to store the values of self.arguments before updating them. + # A dict to store values of self.arguments before updating them. original_values = {} - # If there are no self.arguments, instead propagate the values of the - # kwargs to all properties in the computation graph. + # If there are no self.arguments, instead propagate the values of + # the kwargs to all properties in the computation graph. if kwargs and self.arguments is None: propagate_data_to_dependencies(self, **kwargs) - # If there are self.arguments, update the values of self.arguments to - # match kwargs. + # If there are self.arguments, update the values of self.arguments + # to match kwargs. if isinstance(self.arguments, Feature): for key, value in kwargs.items(): if key in self.arguments.properties: original_values[key] = \ self.arguments.properties[key](_ID=_ID) - self.arguments.properties[key].set_value(value, _ID=_ID) + self.arguments.properties[key]\ + .set_value(value, _ID=_ID) - # This executes the feature. DeepTrackNode will determine if it needs - # to be recalculated. If it does, it will call the `action` method. + # This executes the feature. DeepTrackNode will determine if it + # needs to be recalculated. If it does, it will call the `action` + # method. output = super().__call__(_ID=_ID) - # If there are self.arguments, reset the values of self.arguments to - # their original values. + # If there are self.arguments, reset the values of self.arguments + # to their original values. for key, value in original_values.items(): self.arguments.properties[key].set_value(value, _ID=_ID) From 57e42ec8f7664b8f6987f7a01bfcfd3815f973bb Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 1 Jul 2025 19:43:23 +0200 Subject: [PATCH 095/118] Update features.py --- deeptrack/features.py | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 25b60a168..38b8bff42 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -323,7 +323,7 @@ class Feature(DeepTrackNode): Abstract method that defines how the feature transforms the input. The input is most commonly a NumPy array, PyTorch tensor, or Image object, but it can be anything. - `__call__(image_list: Any = None, _ID: tuple[int, ...] = (), **kwargs: Any) -> Any` + `__call__(image_list: Any, _ID: tuple[int, ...], **kwargs: Any) -> Any` It executes the feature or pipeline on the input and applies property overrides from `kwargs`. `store_properties(x: bool = True, recursive: bool = True) -> None` @@ -685,9 +685,44 @@ def store_properties( Parameters ---------- toggle: bool - If `True`, store properties. If `False`, do not store. + If `True` (default), store properties. If `False`, do not store. recursive: bool - If `True`, also set the same behavior for all dependent features. + If `True` (default), also set the same behavior for all dependent + features. If `False`, it does not. + + Examples + -------- + >>> import deeptrack as dt + + Create a feature and enable property storage: + >>> feature = dt.Add(value=2) + >>> feature.store_properties(True) + + Evaluate the feature and inspect the stored properties: + >>> import numpy as np + >>> + >>> output = feature(np.array([1, 2, 3])) + >>> isinstance(output, dt.Image) + True + >>> output.get_property("value") + 2 + + Disable property storage: + >>> feature.store_properties(False) + >>> output = feature(np.array([1, 2, 3])) + >>> isinstance(output, dt.Image) + False + + Apply recursively to a pipeline: + >>> feature1 = dt.Add(value=1) + >>> feature2 = dt.Multiply(value=2) + >>> pipeline = feature1 >> feature2 + >>> pipeline.store_properties(True, recursive=True) + >>> output = pipeline(np.array([1, 2])) + >>> output.get_property("value") + 1 + >>> output.get_property("value", get_one=False) + [1, 2] """ @@ -1562,7 +1597,7 @@ def __getitem__( return self >> Slice(slices) - # private properties to dispatch based on config + # Private properties to dispatch based on config. @property def _format_input(self): """Selects the appropriate input formatting function based on From e4e9750f61cbb3f412db84dc5ab3c528d890b6d3 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Tue, 1 Jul 2025 22:16:42 +0200 Subject: [PATCH 096/118] Update features.py --- deeptrack/features.py | 149 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 135 insertions(+), 14 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 38b8bff42..82c186825 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -743,15 +743,58 @@ def torch( Parameters ---------- device: torch.device, optional - The target device of the output (e.g., cpu or cuda). + The target device of the output (e.g., cpu or cuda). It defaults to + `None`. recursive: bool, optional - If `True`, also convert all dependent features. + If `True` (default), it also convert all dependent features. If + `False`, it does not. Returns ------- Feature self + Examples + -------- + >>> import deeptrack as dt + >>> import torch + + Create a feature and switch to the PyTorch backend: + >>> feature = dt.Multiply(value=2) + >>> feature.torch() + + Call the feature on a torch tensor: + >>> input_tensor = torch.tensor([1.0, 2.0, 3.0]) + >>> output = feature(input_tensor) + >>> output + tensor([2., 4., 6.]) + + Switch to GPU if available (CUDA): + >>> if torch.cuda.is_available(): + ... device = torch.device("cuda") + ... feature.torch(device=device) + ... output = feature(torch.tensor([1.0, 2.0, 3.0], device=device)) + ... output.device.type + 'cuda' + + Switch to GPU if available (MPS): + >>> if (torch.backends.mps.is_available() + ... and torch.backends.mps.is_built()): + ... device = torch.device("mps") + ... feature.torch(device=device) + ... output = feature(torch.tensor([1.0, 2.0, 3.0], device=device)) + ... output.device.type + 'mps' + + Apply recursively in a pipeline: + >>> f1 = dt.Add(value=1) + >>> f2 = dt.Multiply(value=2) + >>> pipeline = f1 >> f2 + >>> pipeline.torch() + >>> output = pipeline(torch.tensor([1.0, 2.0])) + >>> output + tensor([4., 6.]) + """ self._backend = "torch" @@ -763,19 +806,45 @@ def torch( self.invalidate() return self - def numpy(self: Feature, recursive: bool = True) -> Feature: + def numpy( + self: Feature, + recursive: bool = True, + ) -> Feature: """Set the backend to numpy. Parameters ---------- recursive: bool, optional - If `True`, also convert all dependent features. + If `True` (default), also convert all dependent features. Returns ------- Feature self + Examples + -------- + >>> import deeptrack as dt + >>> import numpy as np + + Create a feature and ensure it uses the NumPy backend: + >>> feature = dt.Add(value=5) + >>> feature.numpy() + + Evaluate the feature on a NumPy array: + >>> output = feature(np.array([1, 2, 3])) + >>> output + array([6, 7, 8]) + + Apply recursively in a pipeline: + >>> f1 = dt.Multiply(value=2) + >>> f2 = dt.Subtract(value=1) + >>> pipeline = f1 >> f2 + >>> pipeline.numpy() + >>> output = pipeline(np.array([1, 2, 3])) + >>> output + array([1, 3, 5]) + """ self._backend = "numpy" @@ -795,19 +864,45 @@ def dtype( ) -> None: """Set the dtype to be used during evaluation. - This alters the dtype used for array creation, but does not - automatically cast the type. + It alters the dtype used for array creation, but does not automatically + cast the type. Parameters ---------- float: str, optional - The float dtype to set. + The float dtype to set. It can be `"float32"`, `"float64"`, + `"default"`, or `None`. It defaults to `None`. int: str, optional - The int dtype to set. + The int dtype to set. It can be `"int16"`, `"int32"`, `"int64"`, + `"default"`, or `None`. It defaults to `None`. complex: str, optional - The complex dtype to set. + The complex dtype to set. It can be `"complex64"`, `"complex128"`, + `"default"`, or `None`. It defaults to `None`. bool: str, optional - The bool dtype to set. + The bool dtype to set. It cna be `"bool"`, `"default"`, or `None`. + It defaults to `None`. + + Examples + -------- + >>> import deeptrack as dt + + Set float and int data types for a feature: + >>> feature = dt.Multiply(value=2) + >>> feature.dtype(float="float32", int="int16") + >>> feature.float_dtype + dtype('float32') + >>> feature.int_dtype + dtype('int16') + + Use complex numbers in the feature: + >>> feature.dtype(complex="complex128") + >>> feature.complex_dtype + dtype('complex128') + + Reset float dtype to default: + >>> feature.dtype(float="default") + >>> feature.float_dtype # resolved from config + dtype('float64') # depending on backend config """ @@ -820,15 +915,41 @@ def dtype( if bool is not None: self._bool_dtype = bool - def to(self: Feature, device: str | torch.device): + def to( + self: Feature, + device: str | torch.device, + ) -> None: """Set the device to be used during evaluation. - If the backend is numpy, this can only be "cpu". - Parameters ---------- device: str or torch.device - The device to use. + The device to use. If the backend is numpy, this can only be "cpu". + + Examples + -------- + >>> import deeptrack as dt + >>> import torch + + Create a feature and assign a device (for torch backend): + >>> feature = dt.Add(value=1) + >>> feature.torch() + >>> feature.to(torch.device("cpu")) + >>> feature.device + device(type='cpu') + + Move the feature to GPU (if available): + >>> if torch.cuda.is_available(): + ... feature.to(torch.device("cuda")) + ... feature.device + device(type='cuda') + + Use Apple MPS device on Apple Silicon (if supported): + >>> if (torch.backends.mps.is_available() + ... and torch.backends.mps.is_built()): + ... feature.to(torch.device("mps")) + ... feature.device + device(type='mps') """ From 4355a71d929e7edf5740bc07a88135f9b7ae989a Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 2 Jul 2025 05:19:58 +0200 Subject: [PATCH 097/118] Update features.py --- deeptrack/features.py | 56 +++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 82c186825..6affc9ef9 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -955,7 +955,10 @@ def to( self._device = device - def batch(self: Feature, batch_size: int = 32) -> tuple | list[Image]: + def batch( + self: Feature, + batch_size: int = 32, + ) -> tuple: """Batch the feature. This method produces a batch of outputs by repeatedly calling @@ -964,14 +967,38 @@ def batch(self: Feature, batch_size: int = 32) -> tuple | list[Image]: Parameters ---------- batch_size: int - The number of times to sample or generate data. + The number of times to sample or generate data. It defaults to 32. Returns ------- - tuple or list[Image] - A tuple of stacked arrays (if the outputs are NumPy arrays or - torch tensors) or a list of images if the outputs are not - stackable. + tuple + A tuple where each element corresponds to one component of the + output. If the outputs are NumPy arrays or PyTorch tensors, each + element is a stacked array. + + Examples + -------- + >>> import deeptrack as dt + + Define a feature that adds a random value to a fixed array: + >>> import numpy as np + >>> + >>> feature = ( + ... dt.Value(value=np.array([[-1, 1]])) + ... >> dt.Add(value=lambda: np.random.rand()) + ... ) + + Evaluate the feature once: + >>> output = feature() + >>> output + array([[-0.77378939, 1.22621061]]) + + Generate a batch of outputs: + >>> batch = feature.batch(batch_size=3) + >>> batch + (array([[-0.2375814 , 1.7624186 ], + [-0.65764878, 1.34235122], + [-0.87449525, 1.12550475]]),) """ @@ -979,21 +1006,14 @@ def batch(self: Feature, batch_size: int = 32) -> tuple | list[Image]: results = list(zip(*results)) for idx, r in enumerate(results): - - if isinstance(r[0], np.ndarray): - results[idx] = np.stack(r) - else: - import torch - - if isinstance(r[0], torch.Tensor): - results[idx] = torch.stack(r) + results[idx] = xp.stack(r) return tuple(results) def action( self: Feature, _ID: tuple[int, ...] = (), - ) -> Image | list[Image]: + ) -> Any | list[Any]: """Core logic to create or transform the image. This method creates or transforms the input image by calling the @@ -1002,11 +1022,11 @@ def action( Parameters ---------- _ID: tuple of int - The unique identifier for the current execution. + The unique identifier for the current execution. It defaults to (). Returns ------- - Image or list[Image] + Any or list[Any] The resolved image or list of resolved images. """ @@ -1018,7 +1038,7 @@ def action( feature_input = self.properties(_ID=_ID).copy() # Call the _process_properties hook, default does nothing. - # For example, it can be used to ensure properties are formatted + # For example, it can be used to ensure properties are formatted # correctly or to rescale properties. feature_input = self._process_properties(feature_input) if _ID != (): From 55d678887b337287cd2792062a3be666e804a484 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 2 Jul 2025 05:22:10 +0200 Subject: [PATCH 098/118] Update features.py --- deeptrack/features.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 6affc9ef9..5f4a0150f 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -1014,20 +1014,24 @@ def action( self: Feature, _ID: tuple[int, ...] = (), ) -> Any | list[Any]: - """Core logic to create or transform the image. + """Core logic to create or transform the input. - This method creates or transforms the input image by calling the - `get()` method with the correct inputs. + This method creates or transforms the input by calling the `get()` + method with the correct inputs. Parameters ---------- - _ID: tuple of int + _ID: tuple[int], optional The unique identifier for the current execution. It defaults to (). Returns ------- Any or list[Any] - The resolved image or list of resolved images. + The resolved output or list of resolved outputs. + + Examples + -------- + TODO """ From a55e95f374655a7d8a493afa5c436212035bef9f Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 2 Jul 2025 05:45:57 +0200 Subject: [PATCH 099/118] Update features.py --- deeptrack/features.py | 155 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 125 insertions(+), 30 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 5f4a0150f..46d3d0fec 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -326,14 +326,21 @@ class Feature(DeepTrackNode): `__call__(image_list: Any, _ID: tuple[int, ...], **kwargs: Any) -> Any` It executes the feature or pipeline on the input and applies property overrides from `kwargs`. - `store_properties(x: bool = True, recursive: bool = True) -> None` - Controls whether the properties are stored in the output `Image` object. - `torch(dtype: torch.dtype | None = None, device: torch.device | None = None, permute_mode: str = "never") -> 'Feature'` - Converts the feature into a PyTorch-compatible feature. - `batch(batch_size: int = 32) -> tuple | list[Image]` - Batches the feature for repeated execution. - `action(_ID: tuple[int, ...] = ()) -> Image | list[Image]` - Core logic to create or transform the image. + `store_properties(toggle: bool, recursive: bool) -> Feature` + It controls whether the properties are stored in the output `Image` + object. + `torch(device: torch.device or None, recursive: bool) -> 'Feature'` + It sets the backend to torch. + `numpy(recursice: bool) -> Feature` + It set the backend to numpy. + `dtype(float: Literal["float32", "float64", "default"] or None, int: Literal["int16", "int32", "int64", "default"] or None, complex: Literal["complex64", "complex128", "default"] or None, bool: Literal["bool", "default"] or None) -> Feature` + It set the dtype to be used during evaluation. + `to(device: str or torch.device) -> Feature` + It set the device to be used during evaluation. + `batch(batch_size: int) -> tuple` + It batches the feature for repeated execution. + `action(_ID: tuple[int, ...]) -> Any | list[Any]` + Implement the core logic to create or transform the input(s). `update(**global_arguments: Any) -> Feature` Refreshes the feature to create a new image. `add_feature(feature: Feature) -> Feature` @@ -676,7 +683,7 @@ def store_properties( self: Feature, toggle: bool = True, recursive: bool = True, - ) -> None: + ) -> Feature: """Control whether to return an Image object. If selected `True`, the output of the evaluation of the feature is an @@ -690,6 +697,11 @@ def store_properties( If `True` (default), also set the same behavior for all dependent features. If `False`, it does not. + Returns + ------- + Feature + self + Examples -------- >>> import deeptrack as dt @@ -733,9 +745,11 @@ def store_properties( if isinstance(dependency, Feature): dependency.store_properties(toggle, recursive=False) + return self + def torch( self: Feature, - device: torch.device = None, + device: torch.device | None = None, recursive: bool = True, ) -> Feature: """Set the backend to torch. @@ -861,7 +875,7 @@ def dtype( int: Literal["int16", "int32", "int64", "default"] | None = None, complex: Literal["complex64", "complex128", "default"] | None = None, bool: Literal["bool", "default"] | None = None, - ) -> None: + ) -> Feature: """Set the dtype to be used during evaluation. It alters the dtype used for array creation, but does not automatically @@ -882,6 +896,11 @@ def dtype( The bool dtype to set. It cna be `"bool"`, `"default"`, or `None`. It defaults to `None`. + Returns + ------- + Feature + self + Examples -------- >>> import deeptrack as dt @@ -915,10 +934,12 @@ def dtype( if bool is not None: self._bool_dtype = bool + return self + def to( self: Feature, device: str | torch.device, - ) -> None: + ) -> Feature: """Set the device to be used during evaluation. Parameters @@ -926,6 +947,11 @@ def to( device: str or torch.device The device to use. If the backend is numpy, this can only be "cpu". + Returns + ------- + Feature + self + Examples -------- >>> import deeptrack as dt @@ -955,6 +981,8 @@ def to( self._device = device + return self + def batch( self: Feature, batch_size: int = 32, @@ -1016,10 +1044,43 @@ def action( ) -> Any | list[Any]: """Core logic to create or transform the input. - This method creates or transforms the input by calling the `get()` - method with the correct inputs. + This method is the central point where the feature's transformation is + actually executed. It retrieves the input data, evaluates the current + values of all properties, formats the input into a list of `Image` + objects, and applies the `get()` method to perform the desired + transformation. + + Depending on the configuration, the transformation can be applied to + each element of the input independently or to the full list at once. + The outputs are optionally post-processed, and then merged back into + the input according to the configured merge strategy. Parameters + + The behavior of this method is influenced by several class attributes: + + - `__distributed__`: If `True` (default), the `get()` method is applied + independently to each input in the input list. If `False`, the + `get()` method is applied to the entire list at once. + + - `__list_merge_strategy__`: Determines how the outputs returned by + `get()` are combined with the original inputs: + * `MERGE_STRATEGY_OVERRIDE` (default): The output replaces the + input. + * `MERGE_STRATEGY_APPEND`: The output is appended to the input + list. + + - `_wrap_array_with_image`: If `True`, input arrays are wrapped as + `Image` instances and their properties are preserved. Otherwise, + they are treated as raw arrays. + + - `_process_properties()`: This hook can be overridden to pre-process + properties before they are passed to `get()` (e.g., for unit + normalization). + + - `_process_output()`: Handles post-processing of the output images, + including appending feature properties and binding argument features. + ---------- _ID: tuple[int], optional The unique identifier for the current execution. It defaults to (). @@ -1027,11 +1088,37 @@ def action( Returns ------- Any or list[Any] - The resolved output or list of resolved outputs. + The resolved output or list of resolved outputs. If only a single + output is generated, the result is unwrapped for convenience. Examples -------- - TODO + >>> import deeptrack as dt + + Define a feature that adds a sampled value: + >>> import numpy as np + >>> + >>> feature = ( + ... dt.Value(value=np.array([1, 2, 3])) + ... >> dt.Add(value=0.5) + ... ) + + Execute core logic manually: + >>> output = feature.action() + >>> output + array([1.5, 2.5, 3.5]) + + Use a list of inputs: + >>> feature = ( + ... dt.Value(value=[ + ... np.array([1, 2, 3]), + ... np.array([4, 5, 6]), + ... ]) + ... >> dt.Add(value=0.5) + ... ) + >>> output = feature.action() + >>> output + [array([1.5, 2.5, 3.5]), array([4.5, 5.5, 6.5])] """ @@ -1188,23 +1275,27 @@ def _normalize( self: Feature, **properties: dict[str, Any], ) -> dict[str, Any]: - """Normalizes the properties. + """Normalize the properties. - This method handles all unit normalizations and conversions. For each class in - the method resolution order (MRO), it checks if the class has a - `__conversion_table__` attribute. If found, it calls the `convert` method of - the conversion table using the properties as arguments. + This method handles all unit normalizations and conversions. For each + class in the method resolution order (MRO), it checks if the class has + a `__conversion_table__` attribute. If found, it calls the `convert` + method of the conversion table using the properties as arguments. Parameters ---------- - **properties: dict of str to Any + **properties: dict[str, Any] The properties to be normalized and converted. Returns ------- - dict of str to Any + dict[str, Any] The normalized and converted properties. + Examples + -------- + TODO + """ for cl in type(self).mro(): @@ -1313,12 +1404,17 @@ def _process_properties( self: Feature, propertydict: dict[str, Any], ) -> dict[str, Any]: - """Preprocesses the input properties before calling `.get()`. + """Preprocess the input properties before calling `.get()`. This method acts as a preprocessing hook for subclasses, allowing them to modify or normalize input properties before the feature's main computation. + Notes: + - Calls `_normalize()` internally to standardize input properties. + - Subclasses may override this method to implement additional + preprocessing steps. + Parameters ---------- propertydict: dict[str, Any] @@ -1330,15 +1426,14 @@ def _process_properties( dict[str, Any] The processed property dictionary after normalization. - Notes - ----- - - Calls `_normalize()` internally to standardize input properties. - - Subclasses may override this method to implement additional - preprocessing steps. - + Examples + -------- + TODO + """ propertydict = self._normalize(**propertydict) + return propertydict def _activate_sources( From b247a6554684c7a3c4a621f62d1eea0c9775ddad Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 2 Jul 2025 05:53:47 +0200 Subject: [PATCH 100/118] Update features.py --- deeptrack/features.py | 66 +++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 46d3d0fec..a68a4103f 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -1840,38 +1840,36 @@ def __getitem__( # Private properties to dispatch based on config. @property def _format_input(self): - """Selects the appropriate input formatting function based on - configuration. + """Select the appropriate input formatting function for configuration. """ if self._wrap_array_with_image: return self._image_wrapped_format_input - else: - return self._no_wrap_format_input + + return self._no_wrap_format_input @property def _process_and_get(self): - """Selects the appropriate processing function based on configuration. - + """Select the appropriate processing function based on configuration. + """ if self._wrap_array_with_image: return self._image_wrapped_process_and_get - else: - return self._no_wrap_process_and_get + + return self._no_wrap_process_and_get @property def _process_output(self): - """Selects the appropriate output processing function based on - configuration. - + """Select the appropriate output processing function for configuration. + """ if self._wrap_array_with_image: return self._image_wrapped_process_output - else: - return self._no_wrap_process_output + + return self._no_wrap_process_output def _image_wrapped_format_input( self: Feature, @@ -1907,20 +1905,30 @@ def _no_wrap_format_input( return image_list - def _no_wrap_process_and_get( + def _image_wrapped_process_and_get( self: Feature, image_list: np.ndarray | list[np.ndarray] | Image | list[Image], **feature_input: dict[str, Any], ) -> list[Image]: - """Processes input data without additional wrapping and retrieves - results. + """Processes input data while maintaining Image properties. """ if self.__distributed__: # Call get on each image in list, and merge properties from # corresponding image - return [self.get(x, **feature_input) for x in image_list] + + results = [] + + for image in image_list: + output = self.get(image, **feature_input) + if not isinstance(output, Image): + output = Image(output) + + output.merge_properties_from(image) + results.append(output) + + return results else: # Call get on entire list. @@ -1929,32 +1937,25 @@ def _no_wrap_process_and_get( if not isinstance(new_list, list): new_list = [new_list] + for idx, image in enumerate(new_list): + if not isinstance(image, Image): + new_list[idx] = Image(image) return new_list - def _image_wrapped_process_and_get( + def _no_wrap_process_and_get( self: Feature, image_list: np.ndarray | list[np.ndarray] | Image | list[Image], **feature_input: dict[str, Any], ) -> list[Image]: - """Processes input data while maintaining Image properties. + """Processes input data without additional wrapping and retrieves + results. """ if self.__distributed__: # Call get on each image in list, and merge properties from # corresponding image - - results = [] - - for image in image_list: - output = self.get(image, **feature_input) - if not isinstance(output, Image): - output = Image(output) - - output.merge_properties_from(image) - results.append(output) - - return results + return [self.get(x, **feature_input) for x in image_list] else: # Call get on entire list. @@ -1963,9 +1964,6 @@ def _image_wrapped_process_and_get( if not isinstance(new_list, list): new_list = [new_list] - for idx, image in enumerate(new_list): - if not isinstance(image, Image): - new_list[idx] = Image(image) return new_list def _image_wrapped_process_output( From 2c61e9f14434858a95052c35e998f0227df93302 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 2 Jul 2025 06:28:34 +0200 Subject: [PATCH 101/118] Update features.py --- deeptrack/features.py | 222 +++++++++++++++++++++++++++++++++--------- 1 file changed, 176 insertions(+), 46 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index a68a4103f..6786e976a 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -1839,9 +1839,21 @@ def __getitem__( # Private properties to dispatch based on config. @property - def _format_input(self): + def _format_input(self: Feature) -> Callable[[Any], list[Any or Image]]: """Select the appropriate input formatting function for configuration. - + + Returns either `_image_wrapped_format_input` or + `_no_wrap_format_input`, depending on whether image metadata + (properties) should be preserved and processed downstream. + + This selection is controlled by the `_wrap_array_with_image` flag. + + Returns + ------- + Callable + A function that formats the input into a list of Image objects or + raw arrays, depending on the configuration. + """ if self._wrap_array_with_image: @@ -1850,9 +1862,23 @@ def _format_input(self): return self._no_wrap_format_input @property - def _process_and_get(self): + def _process_and_get(self: Feature) -> Callable[[Any], list[Any or Image]]: """Select the appropriate processing function based on configuration. + Returns a method that applies the feature’s transformation (`get`) to + the input data, either with or without wrapping and preserving `Image` + metadata. + + The decision is based on the `_wrap_array_with_image` flag: + - If `True`, returns `_image_wrapped_process_and_get` + - If `False`, returns `_no_wrap_process_and_get` + + Returns + ------- + Callable + A function that applies `.get()` to the input, either preserving + or ignoring metadata depending on configuration. + """ if self._wrap_array_with_image: @@ -1861,9 +1887,24 @@ def _process_and_get(self): return self._no_wrap_process_and_get @property - def _process_output(self): + def _process_output(self: Feature) -> Callable[[Any], None]: """Select the appropriate output processing function for configuration. + Returns a method that post-processes the outputs of the feature, + typically after the `get()` method has been called. The selected method + depends on whether the feature is configured to wrap outputs in `Image` + objects (`_wrap_array_with_image = True`). + + - If `True`, returns `_image_wrapped_process_output`, which appends + feature properties to each `Image`. + - If `False`, returns `_no_wrap_process_output`, which extracts raw + array values from any `Image` instances. + + Returns + ------- + Callable + A post-processing function for the feature output. + """ if self._wrap_array_with_image: @@ -1873,11 +1914,27 @@ def _process_output(self): def _image_wrapped_format_input( self: Feature, - image_list: np.ndarray | list[np.ndarray] | Image | list[Image], + image_list: np.ndarray | list[np.ndarray] | Image | list[Image] | None, **kwargs: Any, ) -> list[Image]: - """Wraps input data as Image instances before processing. - + """Wrap input data as Image instances before processing. + + This method ensures that all elements in the input are `Image` + objects. If any raw arrays are provided, they are wrapped in `Image`. + This allows features to propagate metadata and store properties in the + output. + + Parameters + ---------- + image_list: np.ndarray or list[np.ndarray] or Image or list[Image] or None + The input to the feature. If not a list, it is converted into a + single-element list. If `None`, it returns an empty list. + + Returns + ------- + list[Image] + A list where all items are instances of `Image`. + """ if image_list is None: @@ -1889,12 +1946,26 @@ def _image_wrapped_format_input( return [(Image(image)) for image in image_list] def _no_wrap_format_input( - self: Feature, - image_list: np.ndarray | list[np.ndarray] | Image | list[Image], + self: Feature, + image_list: Any, **kwargs: Any, - ) -> list[Image]: - """Processes input data without wrapping it as Image instances. - + ) -> list[Any]: + """Process input data without wrapping it as Image instances. + + This method returns the input list as-is (after ensuring it is a list). + It is used when metadata is not needed or performance is a concern. + + Parameters + ---------- + image_list: Any + The input to the feature. If not already a list, it is wrapped in + one. If `None`, it returns an empty list. + + Returns + ------- + list[Any] + A list of raw input elements, without any transformation. + """ if image_list is None: @@ -1907,16 +1978,36 @@ def _no_wrap_format_input( def _image_wrapped_process_and_get( self: Feature, - image_list: np.ndarray | list[np.ndarray] | Image | list[Image], + image_list: Image | list[Image] | Any | list[Any], **feature_input: dict[str, Any], ) -> list[Image]: """Processes input data while maintaining Image properties. - + + This method applies the `get()` method to the input while ensuring that + output values are wrapped as `Image` instances and preserve the + properties of the corresponding input images. + + If `__distributed__ = True`, `get()` is called separately for each + input image. If `False`, the full list is passed to `get()` at once. + + Parameters + ---------- + image_list: Image or list[Image] or Any or list[Any] + The input data to be processed. + **feature_input: dict[str, Any] + The keyword arguments containing the sampled properties to pass + to the `get()` method. + + Returns + ------- + list[Image] + The list of processed images, with properties preserved. + """ if self.__distributed__: - # Call get on each image in list, and merge properties from - # corresponding image + # Call get on each image in list, and merge properties from + # corresponding image. results = [] @@ -1930,69 +2021,108 @@ def _image_wrapped_process_and_get( return results - else: - # Call get on entire list. - new_list = self.get(image_list, **feature_input) + # ELse, call get on entire list. + new_list = self.get(image_list, **feature_input) - if not isinstance(new_list, list): - new_list = [new_list] + if not isinstance(new_list, list): + new_list = [new_list] - for idx, image in enumerate(new_list): - if not isinstance(image, Image): - new_list[idx] = Image(image) - return new_list + for idx, image in enumerate(new_list): + if not isinstance(image, Image): + new_list[idx] = Image(image) + return new_list def _no_wrap_process_and_get( self: Feature, - image_list: np.ndarray | list[np.ndarray] | Image | list[Image], + image_list: Any | list[Any], **feature_input: dict[str, Any], - ) -> list[Image]: - """Processes input data without additional wrapping and retrieves - results. - + ) -> list[Any]: + """Process input data without additional wrapping and retrieve results. + + This method applies the `get()` method to the input without wrapping + results in `Image` objects, and without propagating or merging metadata. + + If `__distributed__ = True`, `get()` is called separately for each + element in the input list. If `False`, the full list is passed to + `get()` at once. + + Parameters + ---------- + image_list: Any or list[Any] + The input data to be processed. + **feature_input: dict + The keyword arguments containing the sampled properties to pass + to the `get()` method. + + Returns + ------- + list[Any] + The list of processed outputs (raw arrays, tensors, etc.). + """ if self.__distributed__: - # Call get on each image in list, and merge properties from + # Call get on each image in list, and merge properties from # corresponding image + return [self.get(x, **feature_input) for x in image_list] - else: - # Call get on entire list. - new_list = self.get(image_list, **feature_input) + # Else, call get on entire list. + new_list = self.get(image_list, **feature_input) - if not isinstance(new_list, list): - new_list = [new_list] + if not isinstance(new_list, list): + new_list = [new_list] - return new_list + return new_list def _image_wrapped_process_output( self: Feature, - image_list: np.ndarray | list[np.ndarray] | Image | list[Image], + image_list: Image | list[Image] | Any | list[Any], feature_input: dict[str, Any], ) -> None: - """Appends feature properties and input data to each Image. - + """Append feature properties and input data to each Image. + + This method is called after `get()` when the feature is set to wrap + its outputs in `Image` instances. It appends the sampled properties + (from `feature_input`) to the metadata of each `Image`. If the feature + is bound to an `arguments` object, those properties are also appended. + + Parameters + ---------- + image_list: list[Image] + The output images from the feature. + feature_input: dict[str, Any] + The resolved property values used during this evaluation. + """ for index, image in enumerate(image_list): - if self.arguments: image.append(self.arguments.properties()) - image.append(feature_input) def _no_wrap_process_output( self: Feature, - image_list: np.ndarray | list[np.ndarray] | Image | list[Image], + image_list: Any | list[Any], feature_input: dict[str, Any], ) -> None: - """Extracts and updates raw values from Image instances. - + """Extract and update raw values from Image instances. + + This method is called after `get()` when the feature is not configured + to wrap outputs as `Image` instances. If any `Image` objects are + present in the output list, their underlying array values are extracted + using `.value` (i.e., `image._value`). + + Parameters + ---------- + image_list: list[Any] + The list of outputs returned by the feature. + feature_input: dict[str, Any] + The resolved property values used during this evaluation (unused). + """ for index, image in enumerate(image_list): - if isinstance(image, Image): image_list[index] = image._value From 434d661cd30e7788871d28f3667718c7c18a00d1 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 2 Jul 2025 06:29:53 +0200 Subject: [PATCH 102/118] Update features.py --- deeptrack/features.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 6786e976a..d11b16d02 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -425,10 +425,10 @@ class Feature(DeepTrackNode): Ensures the input is a list of Image. `_no_wrap_format_input(image_list: np.ndarray | list[np.ndarray] | Image | list[Image], **kwargs: Any) -> list[Image]` Ensures the input is a list of Image. - `_no_wrap_process_and_get(image_list: np.ndarray | list[np.ndarray] | Image | list[Image], **kwargs: Any) -> list[Image]` - Calls the `get` method according to the `__distributed__` attribute. `_image_wrapped_process_and_get(image_list: np.ndarray | list[np.ndarray] | Image | list[Image], **kwargs: Any) -> list[Image]` Calls the `get` method according to the `__distributed__` attribute. + `_no_wrap_process_and_get(image_list: np.ndarray | list[np.ndarray] | Image | list[Image], **kwargs: Any) -> list[Image]` + Calls the `get` method according to the `__distributed__` attribute. `_image_wrapped_process_output(image_list: np.ndarray | list[np.ndarray] | Image | list[Image], **kwargs: Any) -> None` Processes the output of the feature. `_no_wrap_process_output(image_list: np.ndarray | list[np.ndarray] | Image | list[Image], **kwargs: Any) -> None` From c356091dc4ff787cd043eed2163047c1f5f0dacd Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 2 Jul 2025 08:35:42 +0200 Subject: [PATCH 103/118] Update features.py --- deeptrack/features.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index d11b16d02..aa5913ea4 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -185,13 +185,13 @@ def merge_features( "Merge", "OneOf", "OneOfDict", - "LoadImage", # TODO - "SampleToMasks", # TODO - "AsType", # TODO - "ChannelFirst2d", # TODO - "Upscale", # TODO - "NonOverlapping", # TODO - "Store", # TODO + "LoadImage", # TODO **MG** + "SampleToMasks", # TODO **MG** + "AsType", # TODO **MG** + "ChannelFirst2d", # TODO **AL** + "Upscale", # TODO **AL** + "NonOverlapping", # TODO **AL** + "Store", # TODO **JH** "Squeeze", "Unsqueeze", "ExpandDims", @@ -199,7 +199,7 @@ def merge_features( "Transpose", "Permute", "OneHot", - "TakeProperties", # TODO + "TakeProperties", # TODO **JH** ] @@ -1708,8 +1708,8 @@ def __rpow__( return Value(other) >> Power(self) def __gt__( - self: Feature, - other: Any + self: Feature, + other: Any, ) -> Feature: """Checks if this feature is greater than another using '>'. From f790513be17f7d340a813194d4125bc8f928eecd Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 2 Jul 2025 08:50:48 +0200 Subject: [PATCH 104/118] Update features.py --- deeptrack/features.py | 132 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 127 insertions(+), 5 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index aa5913ea4..43e873c6f 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -1588,25 +1588,127 @@ def __rrshift__( return NotImplemented def __add__( - self: Feature, - other: Any + self: Feature, + other: Any, ) -> Feature: """Adds another value or feature using '+'. - + + This operator is shorthand for chaining with `dt.Add`. The expression: + + >>> feature + other + + is equivalent to: + + >>> feature >> dt.Add(value=other) + + Internally, this method constructs a new `Add` feature and uses the + right-shift operator (`>>`) to chain the current feature into it. + + Parameters + ---------- + other: Any + The value or `Feature` to be added. It is passed to `dt.Add` as + the `value` argument. + + Returns + ------- + Feature + A new feature that adds `other` to the output of `self`. + + Examples + -------- + >>> import deeptrack as dt + + Add a constant value to a static input: + >>> feature = dt.Value(value=[1, 2, 3]) + >>> pipeline = feature + 5 + >>> result = pipeline() + >>> result + [6, 7, 8] + + This is equivalent to: + >>> pipeline = f >> dt.Add(value=5) + + Add a dynamic feature that samples values at each call: + >>> import numpy as np + >>> + >>> noise = dt.Value(value=lambda: np.random.rand()) + >>> pipeline = feature + noise + >>> result = pipeline.update()() + >>> result + [1.325563919290048, 2.325563919290048, 3.325563919290048] + + This is equivalent to: + >>> pipeline = feature >> dt.Add(value=noise) + """ return self >> Add(other) def __radd__( - self: Feature, + self: Feature, other: Any ) -> Feature: """Adds this feature to another value using right '+'. - + + This operator is the right-hand version of `+`, enabling expressions + where the `Feature` appears on the right-hand side. The expression: + + >>> other + feature + + is equivalent to: + + >>> dt.Value(value=other) >> dt.Add(value=feature) + + Internally, this method constructs a `Value` feature from `other` and + chains it into an `Add` feature that adds the current feature as a + dynamic value. + + Parameters + ---------- + other: Any + A constant or `Feature` to which `self` will be added. It is + passed as the input to `Value`. + + Returns + ------- + Feature + A new feature that adds `self` to `other`. + + Examples + -------- + >>> import deeptrack as dt + + Add a feature to a constant: + >>> feature = dt.Value(value=[1, 2, 3]) + >>> pipeline = 5 + feature + >>> result = pipeline() + >>> result + [6, 7, 8] + + This is equivalent to: + >>> pipeline = dt.Value(value=5) >> dt.Add(value=feature) + + Add a feature to a dynamic value: + >>> import numpy as np + >>> + >>> noise = dt.Value(value=lambda: np.random.rand()) + >>> pipeline = noise + feature + >>> result = pipeline.update()() + >>> result + [1.5254613210875014, 2.5254613210875014, 3.5254613210875014] + + This is equivalent to: + >>> pipeline = ( + ... dt.Value(value=lambda: np.random.rand()) + ... >> dt.Add(value=feature) + ... ) + """ return Value(other) >> Add(self) + #TODO **MG** def __sub__( self: Feature, other: Any @@ -1617,6 +1719,7 @@ def __sub__( return self >> Subtract(other) + #TODO **MG** def __rsub__( self: Feature, other: Any @@ -1627,6 +1730,7 @@ def __rsub__( return Value(other) >> Subtract(self) + #TODO **MG** def __mul__( self: Feature, other: Any @@ -1637,6 +1741,7 @@ def __mul__( return self >> Multiply(other) + #TODO **MG** def __rmul__( self: Feature, other: Any @@ -1647,6 +1752,7 @@ def __rmul__( return Value(other) >> Multiply(self) + #TODO **AL** def __truediv__( self: Feature, other: Any @@ -1657,6 +1763,7 @@ def __truediv__( return self >> Divide(other) + #TODO **AL** def __rtruediv__( self: Feature, other: Any @@ -1667,6 +1774,7 @@ def __rtruediv__( return Value(other) >> Divide(self) + #TODO **AL** def __floordiv__( self: Feature, other: Any @@ -1677,6 +1785,7 @@ def __floordiv__( return self >> FloorDivide(other) + #TODO **AL** def __rfloordiv__( self: Feature, other: Any @@ -1687,6 +1796,7 @@ def __rfloordiv__( return Value(other) >> FloorDivide(self) + #TODO **JH** def __pow__( self: Feature, other: Any @@ -1697,6 +1807,7 @@ def __pow__( return self >> Power(other) + #TODO **JH** def __rpow__( self: Feature, other: Any @@ -1707,6 +1818,7 @@ def __rpow__( return Value(other) >> Power(self) + #TODO **JH** def __gt__( self: Feature, other: Any, @@ -1717,6 +1829,7 @@ def __gt__( return self >> GreaterThan(other) + #TODO **JH** def __rgt__( self: Feature, other: Any @@ -1728,6 +1841,7 @@ def __rgt__( return Value(other) >> GreaterThan(self) + #TODO **JH** def __lt__( self: Feature, other: Any @@ -1738,6 +1852,7 @@ def __lt__( return self >> LessThan(other) + #TODO **JH** def __rlt__( self: Feature, other: Any @@ -1748,6 +1863,7 @@ def __rlt__( return Value(other) >> LessThan(self) + #TODO **JH** def __le__( self: Feature, other: Any @@ -1758,6 +1874,7 @@ def __le__( return self >> LessThanOrEquals(other) + #TODO **JH** def __rle__( self: Feature, other: Any @@ -1769,6 +1886,7 @@ def __rle__( return Value(other) >> LessThanOrEquals(self) + #TODO **JH** def __ge__( self: Feature, other: Any @@ -1780,6 +1898,7 @@ def __ge__( return self >> GreaterThanOrEquals(other) + #TODO **JH** def __rge__( self: Feature, other: Any @@ -1791,6 +1910,7 @@ def __rge__( return Value(other) >> GreaterThanOrEquals(self) + #TODO **JH** def __xor__( self: Feature, other: Any, @@ -1801,6 +1921,7 @@ def __xor__( return Repeat(self, other) + #TODO **JH** def __and__( self: Feature, other: Any, @@ -1811,6 +1932,7 @@ def __and__( return self >> Stack(other) + #TODO **JH** def __rand__( self: Feature, other: Any, From c05ca7a147e5c22d9c7c23ca75983f802d315fa2 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 2 Jul 2025 09:10:00 +0200 Subject: [PATCH 105/118] Update features.py --- deeptrack/features.py | 65 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 43e873c6f..c6368a765 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -1557,7 +1557,70 @@ def __rshift__( other: Any, ) -> Feature: """Chains this feature with another feature or function using '>>'. - + + This operator enables pipeline-style chaining. The expression: + + >>> feature >> other + + creates a new pipeline where the output of `feature` is passed as + input to `other`. + + If `other` is a `Feature` or `DeepTrackNode`, this returns a + `Chain(feature, other)`. If `other` is a callable (e.g., a function), + it is wrapped using `dt.Lambda(lambda: other)` and chained + similarly. + + If `other` is neither a `DeepTrackNode` nor a callable, the operator + is not implemented and returns `NotImplemented`, which may lead to a + `TypeError` if no matching reverse operator is defined. + + Parameters + ---------- + other: Any + The feature, node, or callable to chain after `self`. + + Returns + ------- + Feature + A new chained feature combining `self` and `other`. + + Raises + ------ + TypeError + If `other` is not a `DeepTrackNode` or callable, the operator + returns `NotImplemented`, which may raise a `TypeError` if no + matching reverse operator is defined. + + Examples + -------- + >>> import deeptrack as dt + + Chain two features: + >>> feature1 = dt.Value(value=[1, 2, 3]) + >>> feature2 = dt.Add(value=1) + >>> pipeline = feature1 >> feature2 + >>> result = pipeline() + >>> result + [2, 3, 4] + + Chain with a callable (e.g., NumPy function): + >>> import numpy as np + >>> + >>> feature = dt.Value(value=np.array([1, 2, 3])) + >>> function = np.mean + >>> pipeline = feature >> function + >>> result = pipeline() + >>> result + 2.0 + + This is equivalent to: + >>> pipeline = feature >> dt.Lambda(lambda: function) + + Attempting to chain with an unsupported object raise a TypeError: + >>> feature >> "invalid" + ... + TypeError: unsupported operand type(s) for >>: 'Value' and 'str' + """ if isinstance(other, DeepTrackNode): From 9b42746c38e19f19520b6b4e7b582ec6e1f04305 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 2 Jul 2025 09:24:01 +0200 Subject: [PATCH 106/118] Update features.py --- deeptrack/features.py | 75 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index c6368a765..db5376de1 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -1568,7 +1568,9 @@ def __rshift__( If `other` is a `Feature` or `DeepTrackNode`, this returns a `Chain(feature, other)`. If `other` is a callable (e.g., a function), it is wrapped using `dt.Lambda(lambda: other)` and chained - similarly. + similarly. The lambda returns the function itself, which is then + automatically called with the upstream feature’s output during + evaluation. If `other` is neither a `DeepTrackNode` nor a callable, the operator is not implemented and returns `NotImplemented`, which may lead to a @@ -1616,7 +1618,10 @@ def __rshift__( This is equivalent to: >>> pipeline = feature >> dt.Lambda(lambda: function) - Attempting to chain with an unsupported object raise a TypeError: + The lambda returns the function object. During evaluation, DeepTrack + internally calls that function with the resolved output of `feature`. + + Attempting to chain with an unsupported object raises a TypeError: >>> feature >> "invalid" ... TypeError: unsupported operand type(s) for >>: 'Value' and 'str' @@ -1634,12 +1639,74 @@ def __rshift__( # The operator is not implemented for other inputs. return NotImplemented + #TODO **BM** TBE? note that it's never actually accessed def __rrshift__( self: Feature, other: Any, ) -> Feature: - """Chains another feature or function with this feature using '<<'. - + """Chains another feature or value with this feature using '>>'. + + This operator supports chaining when the `Feature` appears on the + right-hand side of a pipeline. The expression: + + >>> other >> feature + + triggers `feature.__rrshift__(other)` if `other` does not implement + `__rshift__`, or if its implementation returns `NotImplemented`. + + If `other` is a `Feature`, this is equivalent to: + + >>> dt.Chain(other, feature) + + If `other` is a raw value (e.g., a list or array), it is wrapped using + `dt.Value(value=other)` before chaining: + + >>> dt.Chain(dt.Value(value=other), feature) + + Parameters + ---------- + other: Any + The value or feature to be evaluated before this feature. + + Returns + ------- + Feature + A new chained feature where `other` is evaluated first. + + Raises + ------ + TypeError + If `other` is not a supported type, this method returns + `NotImplemented`, which may raise a `TypeError` if no matching + forward operator is defined. + + Examples + -------- + >>> import deeptrack as dt + + Chain a constant value into a feature: + >>> feature = dt.Add(value=1) + >>> pipeline = [1, 2, 3] >> feature + >>> result = pipeline() + >>> result + [2, 3, 4] + + This is equivalent to: + >>> pipeline = dt.Value(value=[1, 2, 3]) >> feature + + Chain two features (normally handled by __rshift__): + >>> feature1 = dt.Value(value=[1, 2, 3]) + >>> feature2 = dt.Add(value=1) + >>> pipeline = feature1 >> feature2 + >>> result = pipeline() + >>> result + [2, 3, 4] + + Attempting to chain an unsupported object raises a TypeError: + >>> object() >> feature + ... + TypeError: unsupported operand type(s) for >>: 'object' and 'Add' + """ if isinstance(other, Feature): From bb709db4074e9a4382a2a077c08768e7fd25d095 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 2 Jul 2025 10:03:29 +0200 Subject: [PATCH 107/118] Update features.py --- deeptrack/features.py | 53 ++++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index db5376de1..6bc3e613d 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -1639,7 +1639,6 @@ def __rshift__( # The operator is not implemented for other inputs. return NotImplemented - #TODO **BM** TBE? note that it's never actually accessed def __rrshift__( self: Feature, other: Any, @@ -1680,32 +1679,40 @@ def __rrshift__( `NotImplemented`, which may raise a `TypeError` if no matching forward operator is defined. - Examples - -------- - >>> import deeptrack as dt + Notes + ----- + This method enables chaining where a `Feature` appears on the + right-hand side of the `>>` operator. It is triggered when the + left-hand operand does not implement `__rshift__`, or when its + implementation returns `NotImplemented`. - Chain a constant value into a feature: - >>> feature = dt.Add(value=1) - >>> pipeline = [1, 2, 3] >> feature - >>> result = pipeline() - >>> result - [2, 3, 4] + This is particularly useful when chaining two `Feature` instances or + when the left-hand operand is a custom class designed to delegate + chaining behavior. For example: - This is equivalent to: - >>> pipeline = dt.Value(value=[1, 2, 3]) >> feature + >>> pipeline = dt.Value(value=[1, 2, 3]) >> dt.Add(value=1) - Chain two features (normally handled by __rshift__): - >>> feature1 = dt.Value(value=[1, 2, 3]) - >>> feature2 = dt.Add(value=1) - >>> pipeline = feature1 >> feature2 - >>> result = pipeline() - >>> result - [2, 3, 4] + In this case, if `dt.Value` does not handle `__rshift__`, Python will + fall back to calling `Add.__rrshift__(...)`, which constructs the + chain. - Attempting to chain an unsupported object raises a TypeError: - >>> object() >> feature - ... - TypeError: unsupported operand type(s) for >>: 'object' and 'Add' + However, this mechanism does **not** apply to built-in types like + `int`, `float`, or `list`. Due to limitations in Python's operator + overloading, expressions like: + + >>> 1 >> dt.Add(value=1) + >>> [1, 2, 3] >> dt.Add(value=1) + + will raise `TypeError`, because Python does not delegate to the + right-hand operand’s `__rrshift__` method for built-in types. + + To chain a raw value into a feature, wrap it explicitly using + `dt.Value`: + + >>> dt.Value(1) >> dt.Add(value=1) + + This is functionally equivalent and avoids the need for fallback + behavior. """ From d5920c86c4c260fffe5ea10d4a417235d2788221 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 2 Jul 2025 10:20:53 +0200 Subject: [PATCH 108/118] Update features.py --- deeptrack/features.py | 130 +++++++++++++++++++++--------------------- 1 file changed, 66 insertions(+), 64 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 6bc3e613d..7ea07a5ed 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -349,10 +349,12 @@ class Feature(DeepTrackNode): Sets the random seed for the feature, ensuring deterministic behavior. `bind_arguments(arguments: Feature) -> Feature` Binds another feature’s properties as arguments to this feature. - `_normalize(**properties: dict[str, Any]) -> dict[str, Any]` - Normalizes the properties of the feature. `plot(input_image: np.ndarray | list[np.ndarray] | Image | list[Image] | None = None, resolve_kwargs: dict | None = None, interval: float | None = None, **kwargs) -> Any` Visualizes the output of the feature. + + **Private and internal methods.** + `_normalize(**properties: dict[str, Any]) -> dict[str, Any]` + Normalizes the properties of the feature. `_process_properties(propertydict: dict[str, Any]) -> dict[str, Any]` Preprocesses the input properties before calling the `get` method. `_activate_sources(x: Any) -> None` @@ -364,57 +366,57 @@ class Feature(DeepTrackNode): `__next__() -> Any` Returns the next element in the feature. `__rshift__(other: Any) -> Feature` - Allows chaining of features. + It allows chaining of features. `__rrshift__(other: Any) -> Feature` - Allows right chaining of features. + It allows right chaining of features. `__add__(other: Any) -> Feature` - Overrides add operator. + It overrides add operator. `__radd__(other: Any) -> Feature` - Overrides right add operator. + It overrides right add operator. `__sub__(other: Any) -> Feature` - Overrides subtraction operator. + It overrides subtraction operator. `__rsub__(other: Any) -> Feature` - Overrides right subtraction operator. + It overrides right subtraction operator. `__mul__(other: Any) -> Feature` - Overrides multiplication operator. + It overrides multiplication operator. `__rmul__(other: Any) -> Feature` - Overrides right multiplication operator. + It overrides right multiplication operator. `__truediv__(other: Any) -> Feature` - Overrides division operator. + It overrides division operator. `__rtruediv__(other: Any) -> Feature` - Overrides right division operator. + It overrides right division operator. `__floordiv__(other: Any) -> Feature` - Overrides floor division operator. + It overrides floor division operator. `__rfloordiv__(other: Any) -> Feature` - Overrides right floor division operator. + It overrides right floor division operator. `__pow__(other: Any) -> Feature` - Overrides power operator. + It overrides power operator. `__rpow__(other: Any) -> Feature` - Overrides right power operator. + It overrides right power operator. `__gt__(other: Any) -> Feature` - Overrides greater than operator. + It overrides greater than operator. `__rgt__(other: Any) -> Feature` - Overrides right greater than operator. + It overrides right greater than operator. `__lt__(other: Any) -> Feature` - Overrides less than operator. + It overrides less than operator. `__rlt__(other: Any) -> Feature` - Overrides right less than operator. + It overrides right less than operator. `__le__(other: Any) -> Feature` - Overrides less than or equal to operator. + It overrides less than or equal to operator. `__rle__(other: Any) -> Feature` - Overrides right less than or equal to operator. + It overrides right less than or equal to operator. `__ge__(other: Any) -> Feature` - Overrides greater than or equal to operator. + It overrides greater than or equal to operator. `__rge__(other: Any) -> Feature` - Overrides right greater than or equal to operator. + It overrides right greater than or equal to operator. `__xor__(other: Any) -> Feature` - Overrides XOR operator. + It overrides XOR operator. `__and__(other: Feature) -> Feature` - Overrides AND operator. + It overrides AND operator. `__rand__(other: Feature) -> Feature` - Overrides right AND operator. + It overrides right AND operator. `__getitem__(key: Any) -> Feature` - Allows direct slicing of the data. + It allows direct slicing of the data. `_format_input(image_list: np.ndarray | list[np.ndarray] | Image | list[Image], **kwargs: Any) -> list[Image]` Formats the input data for the feature. `_process_and_get(image_list: np.ndarray | list[np.ndarray] | Image | list[Image], **kwargs: Any) -> list[Image]` @@ -1271,42 +1273,6 @@ def bind_arguments( return self - def _normalize( - self: Feature, - **properties: dict[str, Any], - ) -> dict[str, Any]: - """Normalize the properties. - - This method handles all unit normalizations and conversions. For each - class in the method resolution order (MRO), it checks if the class has - a `__conversion_table__` attribute. If found, it calls the `convert` - method of the conversion table using the properties as arguments. - - Parameters - ---------- - **properties: dict[str, Any] - The properties to be normalized and converted. - - Returns - ------- - dict[str, Any] - The normalized and converted properties. - - Examples - -------- - TODO - - """ - - for cl in type(self).mro(): - if hasattr(cl, "__conversion_table__"): - properties = cl.__conversion_table__.convert(**properties) - - for key, val in properties.items(): - if isinstance(val, Quantity): - properties[key] = val.magnitude - return properties - def plot( self: Feature, input_image: np.ndarray | list[np.ndarray] | Image | list[Image] = None, @@ -1400,6 +1366,42 @@ def plotter(frame=0): ), ) + def _normalize( + self: Feature, + **properties: dict[str, Any], + ) -> dict[str, Any]: + """Normalize the properties. + + This method handles all unit normalizations and conversions. For each + class in the method resolution order (MRO), it checks if the class has + a `__conversion_table__` attribute. If found, it calls the `convert` + method of the conversion table using the properties as arguments. + + Parameters + ---------- + **properties: dict[str, Any] + The properties to be normalized and converted. + + Returns + ------- + dict[str, Any] + The normalized and converted properties. + + Examples + -------- + TODO + + """ + + for cl in type(self).mro(): + if hasattr(cl, "__conversion_table__"): + properties = cl.__conversion_table__.convert(**properties) + + for key, val in properties.items(): + if isinstance(val, Quantity): + properties[key] = val.magnitude + return properties + def _process_properties( self: Feature, propertydict: dict[str, Any], From d28a82991729087aab1bdd470d74bfc9d127a37e Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 2 Jul 2025 10:28:21 +0200 Subject: [PATCH 109/118] Update features.py --- deeptrack/features.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 7ea07a5ed..4aff2f321 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -149,7 +149,7 @@ def merge_features( import torch __all__ = [ - "Feature", # TODO + "Feature", # TODO **GV** "StructuralFeature", "Chain", "Branch", @@ -1161,6 +1161,7 @@ def action( else: return image_list + # **GV** def update( self: Feature, **global_arguments: Any, @@ -1198,6 +1199,7 @@ def update( return self + # **GV** def add_feature( self: Feature, feature: Feature, @@ -1221,6 +1223,7 @@ def add_feature( return feature + # **GV** def seed( self: Feature, _ID: tuple[int, ...] = (), @@ -1236,6 +1239,7 @@ def seed( np.random.seed(self._random_seed(_ID=_ID)) + # **GV** def bind_arguments( self: Feature, arguments: Feature, @@ -1273,6 +1277,7 @@ def bind_arguments( return self + # **GV** def plot( self: Feature, input_image: np.ndarray | list[np.ndarray] | Image | list[Image] = None, @@ -1366,6 +1371,7 @@ def plotter(frame=0): ), ) + # **GV** def _normalize( self: Feature, **properties: dict[str, Any], @@ -1402,6 +1408,7 @@ class in the method resolution order (MRO), it checks if the class has properties[key] = val.magnitude return properties + # **GV** def _process_properties( self: Feature, propertydict: dict[str, Any], @@ -1438,6 +1445,7 @@ def _process_properties( return propertydict + # **GV** def _activate_sources( self: Feature, x: Any, @@ -1470,6 +1478,7 @@ def _activate_sources( if isinstance(source, SourceItem): source() + # **GV** def __getattr__( self: Feature, key: str, @@ -1534,6 +1543,7 @@ def __getattr__( raise AttributeError(f"'{self.__class__.__name__}' object has " "no attribute '{key}'") + # **GV** def __iter__( self: Feature, ) -> Iterable: @@ -1545,6 +1555,7 @@ def __iter__( while True: yield from next(self) + # **GV** def __next__( self: Feature, ) -> Any: @@ -2082,6 +2093,7 @@ def __rand__( return Value(other) >> Stack(self) + # **GV** def __getitem__( self: Feature, slices: Any, From 50491da873fedac10ab58e0e2d3eadcc249abbac Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 2 Jul 2025 14:32:05 +0200 Subject: [PATCH 110/118] Update features.py --- deeptrack/features.py | 74 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 62 insertions(+), 12 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 4aff2f321..59c610e05 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -361,10 +361,10 @@ class Feature(DeepTrackNode): Activates sources in the input data. `__getattr__(key: str) -> Any` Custom attribute access for the Feature class. - `__iter__() -> Iterable` - Iterates over the feature. - `__next__() -> Any` - Returns the next element in the feature. + `__iter__() -> Any` + It return the next element iterating over the feature. + `__next__() -> Iterable` + It returns an iterator for the feature. `__rshift__(other: Any) -> Feature` It allows chaining of features. `__rrshift__(other: Any) -> Feature` @@ -1543,24 +1543,74 @@ def __getattr__( raise AttributeError(f"'{self.__class__.__name__}' object has " "no attribute '{key}'") - # **GV** def __iter__( self: Feature, - ) -> Iterable: - """ Returns an infinite iterator that continuously yields feature - values. + ) -> Any: + """ Return infinite iterator that continuously yields feature values. + + This method allows a `Feature` instance to be used in a `for` loop or + as a generator. It repeatedly calls `__next__()` to resolve and yield + new values. + + Each iteration generates a fresh output by calling + `self.update().resolve()`, meaning it resamples all properties before + evaluation. + + Returns + ------- + Any + A newly generated output from the feature. + + Examples + -------- + >>> import deeptrack as dt + + Create a feature: + >>> import numpy as np + >>> + >>> feature = dt.Value(value=lambda: np.random.rand()) + + Use the feature in a loop: + >>> for i, sample in enumerate(feature): + ... print(sample) + ... if i == 2: + ... break + 0.43126475134786546 + 0.3270413736199965 + 0.6734339603677173 """ while True: yield from next(self) - # **GV** def __next__( self: Feature, - ) -> Any: - """Returns the next resolved feature in the sequence. - + ) -> Iterator: + """Return the next resolved feature in the sequence. + + This method is called by `next(feature)` and yields one new sample by + first resampling all properties via `update()` and then evaluating the + feature using `resolve()`. + + Returns + ------- + Iterable + An infinite generator that yields evaluated feature outputs. + + Examples + -------- + >>> import deeptrack as dt + + Create a feature: + >>> import numpy as np + >>> + >>> feature = dt.Value(value=lambda: np.random.rand()) + + Get a single sample multiple times: + >>> for _ in enumerate(3): + ... next(feature) + """ yield self.update().resolve() From a96969237a41e8149334ab65bc879b06812383f2 Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 2 Jul 2025 15:00:01 +0200 Subject: [PATCH 111/118] Update features.py --- deeptrack/features.py | 128 ++++++++++++++++++++++++++++++++---------- 1 file changed, 97 insertions(+), 31 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 59c610e05..1dc45d278 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -361,10 +361,10 @@ class Feature(DeepTrackNode): Activates sources in the input data. `__getattr__(key: str) -> Any` Custom attribute access for the Feature class. - `__iter__() -> Any` - It return the next element iterating over the feature. - `__next__() -> Iterable` + `__iter__() -> Feature` It returns an iterator for the feature. + `__next__() -> Any` + It return the next element iterating over the feature. `__rshift__(other: Any) -> Feature` It allows chaining of features. `__rrshift__(other: Any) -> Feature` @@ -1545,35 +1545,31 @@ def __getattr__( def __iter__( self: Feature, - ) -> Any: - """ Return infinite iterator that continuously yields feature values. - - This method allows a `Feature` instance to be used in a `for` loop or - as a generator. It repeatedly calls `__next__()` to resolve and yield - new values. + ) -> Feature: + """Return self as an iterator over feature values. - Each iteration generates a fresh output by calling - `self.update().resolve()`, meaning it resamples all properties before - evaluation. + This makes the `Feature` object compatible with Python's iterator + protocol. Each call to `next(feature)` generates a new output by + resampling its properties and resolving the pipeline. Returns ------- - Any - A newly generated output from the feature. + Feature + Returns self, which defines `__next__()` to yield outputs. Examples -------- >>> import deeptrack as dt - Create a feature: + Create feature: >>> import numpy as np >>> >>> feature = dt.Value(value=lambda: np.random.rand()) Use the feature in a loop: - >>> for i, sample in enumerate(feature): + >>> for sample in feature: ... print(sample) - ... if i == 2: + ... if sample > 0.5: ... break 0.43126475134786546 0.3270413736199965 @@ -1581,22 +1577,28 @@ def __iter__( """ - while True: - yield from next(self) + return self + + #TODO **BM** TBE? Previous implementation, not standard in Python + # while True: + # yield from next(self) def __next__( self: Feature, - ) -> Iterator: + ) -> Any: """Return the next resolved feature in the sequence. - This method is called by `next(feature)` and yields one new sample by - first resampling all properties via `update()` and then evaluating the - feature using `resolve()`. + This method allows a `Feature` to be used as an iterator that yields + a new result at each step. It is called automatically by `next(feature)` + or when used in iteration. + + Each call to `__next__()` triggers a resampling of all properties and + evaluation of the pipeline using `self.update().resolve()`. Returns ------- - Iterable - An infinite generator that yields evaluated feature outputs. + Any + A newly generated output from the feature. Examples -------- @@ -1607,13 +1609,16 @@ def __next__( >>> >>> feature = dt.Value(value=lambda: np.random.rand()) - Get a single sample multiple times: - >>> for _ in enumerate(3): - ... next(feature) + Get a single sample: + >>> next(feature) + 0.41251758103924216 """ - yield self.update().resolve() + return self.update().resolve() + + #TODO **BM** TBE? Previous implementation, not standard in Python + # yield self.update().resolve() def __rshift__( self: Feature, @@ -2143,13 +2148,74 @@ def __rand__( return Value(other) >> Stack(self) - # **GV** def __getitem__( self: Feature, slices: Any, - ) -> 'Feature': + ) -> Feature: """Allows direct slicing of the feature's output. + This operator enables syntax like: + + >>> feature[:, 0] + + to extract a slice from the output of the feature, just as you would + with a NumPy array or PyTorch tensor. + + Internally, this is equivalent to chaining with `dt.Slice`, and the + expression: + + >>> feature[slices] + + is equivalent to: + + >>> feature >> dt.Slice(slices) + + If the slice is not already a tuple (i.e., a single index or slice), + it is wrapped in one. The resulting tuple is converted to a list to + allow sampling of dynamic slices at runtime. + + Parameters + ---------- + slices: Any + The slice or index to apply to the feature output. Can be an int, + slice object, or a tuple of them. + + Returns + ------- + Feature + A new feature that applies slicing to the output of the current + feature. + + Examples + -------- + >>> import deeptrack as dt + + Create a feature: + >>> import numpy as np + >>> + >>> feature = dt.Value(value=np.arange(9).reshape(3, 3)) + >>> feature() + array([[0, 1, 2], + [3, 4, 5], + [6, 7, 8]]) + + Slice a row: + >>> sliced = feature[1] + >>> sliced() + array([3, 4, 5]) + + This is equivalent to: + >>> sliced = feature >> dt.Slice([1]) + + Slice with multiple axes: + >>> sliced = feature[1:, 1:] + >>> sliced() + array([[4, 5], + [7, 8]]) + + This is equivalent to: + >>> sliced = feature >> dt.Slice([slice(1, None), slice(1, None)]) + """ if not isinstance(slices, tuple): From 13205239809458cfeba00fc1a44cc21be98fb0bc Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Wed, 2 Jul 2025 15:09:09 +0200 Subject: [PATCH 112/118] Update features.py --- deeptrack/features.py | 102 +++++++++++++++++++++--------------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 1dc45d278..72de58575 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -417,24 +417,26 @@ class Feature(DeepTrackNode): It overrides right AND operator. `__getitem__(key: Any) -> Feature` It allows direct slicing of the data. - `_format_input(image_list: np.ndarray | list[np.ndarray] | Image | list[Image], **kwargs: Any) -> list[Image]` - Formats the input data for the feature. - `_process_and_get(image_list: np.ndarray | list[np.ndarray] | Image | list[Image], **kwargs: Any) -> list[Image]` - Calls the `get` method according to the `__distributed__` attribute. - `_process_output(image_list: np.ndarray | list[np.ndarray] | Image | list[Image], **kwargs: Any) -> None` - Processes the output of the feature. + `_format_input(image_list: Any, **kwargs: Any) -> list[Any or Image]` + It formats the input data for the feature. + `_process_and_get(image_list: Any, **kwargs: Any) -> list[Any or Image]` + It calls the `get` method according to the `__distributed__` attribute. + `_process_output(image_list: Any, **kwargs: Any) -> None` + It processes the output of the feature. `_image_wrapped_format_input(image_list: np.ndarray | list[np.ndarray] | Image | list[Image], **kwargs: Any) -> list[Image]` - Ensures the input is a list of Image. - `_no_wrap_format_input(image_list: np.ndarray | list[np.ndarray] | Image | list[Image], **kwargs: Any) -> list[Image]` - Ensures the input is a list of Image. + It ensures the input is a list of Image. + `_no_wrap_format_input(image_list: Any, **kwargs: Any) -> list[Any]` + It ensures the input is a list of Image. `_image_wrapped_process_and_get(image_list: np.ndarray | list[np.ndarray] | Image | list[Image], **kwargs: Any) -> list[Image]` - Calls the `get` method according to the `__distributed__` attribute. - `_no_wrap_process_and_get(image_list: np.ndarray | list[np.ndarray] | Image | list[Image], **kwargs: Any) -> list[Image]` - Calls the `get` method according to the `__distributed__` attribute. + It calls the `get()` method according to the `__distributed__` + attribute. + `_no_wrap_process_and_get(image_list: Any | list[Any], **kwargs: Any) -> list[Any]` + It calls the `get()` method according to the `__distributed__` + attribute. `_image_wrapped_process_output(image_list: np.ndarray | list[np.ndarray] | Image | list[Image], **kwargs: Any) -> None` - Processes the output of the feature. - `_no_wrap_process_output(image_list: np.ndarray | list[np.ndarray] | Image | list[Image], **kwargs: Any) -> None` - Processes the output of the feature. + It processes the output of the feature. + `_no_wrap_process_output(image_list: Any | list[Any], **kwargs: Any) -> None` + It processes the output of the feature. Examples -------- @@ -1328,48 +1330,46 @@ def plot( plt.imshow(output_image, **kwargs) return plt.gca() - else: - # Assume video - fig = plt.figure() - images = [] - plt.axis("off") - for image in output_image: - images.append([plt.imshow(image, **kwargs)]) - + # Assume video + fig = plt.figure() + images = [] + plt.axis("off") + for image in output_image: + images.append([plt.imshow(image, **kwargs)]) - if not interval: - if isinstance(output_image[0], Image): - interval = output_image[0].get_property("interval") or (1 / 30 * 1000) - else: - interval = (1 / 30 * 1000) + if not interval: + if isinstance(output_image[0], Image): + interval = output_image[0].get_property("interval") or (1 / 30 * 1000) + else: + interval = 1 / 30 * 1000 - anim = animation.ArtistAnimation( - fig, images, interval=interval, blit=True, repeat_delay=0 - ) + anim = animation.ArtistAnimation( + fig, images, interval=interval, blit=True, repeat_delay=0 + ) - try: - get_ipython # Throws NameError if not in Notebook - display(HTML(anim.to_jshtml())) - return anim + try: + get_ipython # Throws NameError if not in Notebook + display(HTML(anim.to_jshtml())) + return anim - except NameError: - # Not in an notebook - plt.show() + except NameError: + # Not in an notebook + plt.show() - except RuntimeError: - # In notebook, but animation failed - import ipywidgets as widgets + except RuntimeError: + # In notebook, but animation failed + import ipywidgets as widgets - def plotter(frame=0): - plt.imshow(output_image[frame][:, :, 0], **kwargs) - plt.show() + def plotter(frame=0): + plt.imshow(output_image[frame][:, :, 0], **kwargs) + plt.show() - return widgets.interact( - plotter, - frame=widgets.IntSlider( - value=0, min=0, max=len(images) - 1, step=1 - ), - ) + return widgets.interact( + plotter, + frame=widgets.IntSlider( + value=0, min=0, max=len(images) - 1, step=1 + ), + ) # **GV** def _normalize( @@ -2158,7 +2158,7 @@ def __getitem__( >>> feature[:, 0] - to extract a slice from the output of the feature, just as you would + to extract a slice from the output of the feature, just as one would with a NumPy array or PyTorch tensor. Internally, this is equivalent to chaining with `dt.Slice`, and the From adaa4262ef31e15de32269a82ee9202edb481c5d Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Thu, 3 Jul 2025 09:13:50 +0200 Subject: [PATCH 113/118] Update features.py --- deeptrack/features.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 72de58575..456ced4de 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -137,7 +137,7 @@ def merge_features( from scipy.spatial.distance import cdist from deeptrack import units -from deeptrack.backend import config, Config, TORCH_AVAILABLE, xp +from deeptrack.backend import config, TORCH_AVAILABLE, xp from deeptrack.backend.core import DeepTrackNode from deeptrack.backend.units import ConversionTable, create_context from deeptrack.image import Image @@ -459,7 +459,7 @@ class Feature(DeepTrackNode): _int_dtype: str _complex_dtype: str _device: str | torch.device - _backend: Config + _backend: Literal["numpy", "torch"] @property def float_dtype(self) -> np.dtype | torch.dtype: From 7d2379feb173ffae4865dc1f78fecebcdb82192c Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Thu, 3 Jul 2025 09:13:52 +0200 Subject: [PATCH 114/118] Update _config.py --- deeptrack/backend/_config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/deeptrack/backend/_config.py b/deeptrack/backend/_config.py index 3441e358b..4a578f149 100644 --- a/deeptrack/backend/_config.py +++ b/deeptrack/backend/_config.py @@ -144,7 +144,6 @@ __all__ = [ "config", - "Config", "OPENCV_AVAILABLE", "TORCH_AVAILABLE", "xp", From ce4c46390f73dedae29162a551d14e3b585ff962 Mon Sep 17 00:00:00 2001 From: mirjagranfors Date: Thu, 3 Jul 2025 17:04:08 +0200 Subject: [PATCH 115/118] update features/SampleToMasks --- deeptrack/features.py | 157 +++++++++++++++++++++++++++++------------- 1 file changed, 109 insertions(+), 48 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 456ced4de..fbb311513 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -5873,21 +5873,21 @@ def get( class SampleToMasks(Feature): """Creates a mask from a list of images. - This feature applies a transformation function to each input image and - merges the resulting masks into a single multi-layer image. Each input - image must have a `position` property that determines its placement within - the final mask. When used with scatterers, the `voxel_size` property must + This feature applies a transformation function to each input image and + merges the resulting masks into a single multi-layer image. Each input + image must have a `position` property that determines its placement within + the final mask. When used with scatterers, the `voxel_size` property must be provided for correct object sizing. Parameters ---------- transformation_function: Callable[[Image], Image] - A function that transforms each input image into a mask with + A function that transforms each input image into a mask with `number_of_masks` layers. number_of_masks: PropertyLike[int], optional The number of mask layers to generate. Default is 1. output_region: PropertyLike[tuple[int, int, int, int]], optional - The size and position of the output mask, typically aligned with + The size and position of the output mask, typically aligned with `optics.output_region`. merge_method: PropertyLike[str | Callable | list[str | Callable]], optional Method for merging individual masks into the final image. Can be: @@ -5902,9 +5902,16 @@ class SampleToMasks(Feature): Methods ------- - `get(image: np.ndarray | Image, transformation_function: Callable[[Image], Image], **kwargs: dict[str, Any]) -> Image` + `get( + image: np.ndarray | Image, + transformation_function: Callable[[Image], Image], + **kwargs: dict[str, Any] + ) -> Image` Applies the transformation function to the input image. - `_process_and_get(images: list[np.ndarray] | np.ndarray | list[Image] | Image, **kwargs: dict[str, Any]) -> Image | np.ndarray` + `_process_and_get( + images: list[np.ndarray] | np.ndarray | list[Image] | Image, + **kwargs: dict[str, Any] + ) -> Image | np.ndarray` Processes a list of images and generates a multi-layer mask. Returns @@ -5926,7 +5933,7 @@ class SampleToMasks(Feature): Define optics and particles: >>> import numpy as np - >>> + >>> >>> optics = dt.Fluorescence(output_region=(0, 0, 64, 64)) >>> particle = dt.PointParticle( >>> position=lambda: np.random.uniform(5, 55, size=2), @@ -5968,7 +5975,9 @@ def __init__( transformation_function: Callable[[Image], Image], number_of_masks: PropertyLike[int] = 1, output_region: PropertyLike[tuple[int, int, int, int]] = None, - merge_method: PropertyLike[str | Callable | list[str | Callable]] = "add", + merge_method: PropertyLike[ + str | Callable | list[str | Callable] + ] = "add", **kwargs: Any, ): """Initialize the SampleToMasks feature. @@ -5981,11 +5990,13 @@ def __init__( Number of mask layers. Default is 1. output_region: PropertyLike[tuple[int, int, int, int]], optional Output region of the mask. Default is None. - merge_method: PropertyLike[str | Callable | list[str | Callable]], optional + merge_method: PropertyLike[ + str | Callable | list[str | Callable] + ], optional Method to merge masks. Default is "add". **kwargs: dict[str, Any] Additional keyword arguments passed to the parent class. - + """ super().__init__( @@ -5998,7 +6009,7 @@ def __init__( def get( self: Feature, - image: np.ndarray | Image, + image: NDArray | torch.Tensor | Image, transformation_function: Callable[[Image], Image], **kwargs: Any, ) -> Image: @@ -6024,34 +6035,45 @@ def get( def _process_and_get( self: Feature, - images: list[np.ndarray] | np.ndarray | list[Image] | Image, + images: ( + list[NDArray] + | NDArray + | list[torch.Tensor] + | torch.Tensor + | list[Image] + | Image + ), **kwargs: Any, - ) -> Image | np.ndarray: + ) -> NDArray | torch.Tensor | Image: """Process a list of images and generate a multi-layer mask. Parameters ---------- - images: np.ndarray or list[np.ndarrray] or Image or list[Image] + images: np.ndarray or list[np.ndarrray] or torch.Tensor or + list[torch.tensor] or Image or list[Image] List of input images or a single image. **kwargs: dict[str, Any] - Additional parameters including `output_region`, `number_of_masks`, + Additional parameters including `output_region`, `number_of_masks`, and `merge_method`. Returns ------- - Image or np.ndarray + Image, np.ndarray, or torch.Tensor The final mask image. - + """ # Handle list of images. if isinstance(images, list) and len(images) != 1: list_of_labels = super()._process_and_get(images, **kwargs) if not self._wrap_array_with_image: - for idx, (label, image) in enumerate(zip(list_of_labels, - images)): - list_of_labels[idx] = \ - Image(label, copy=False).merge_properties_from(image) + for idx, (label, image) in enumerate( + zip(list_of_labels, images) + ): + list_of_labels[idx] = Image( + label, + copy=False + ).merge_properties_from(image) else: if isinstance(images, list): images = images[0] @@ -6060,7 +6082,12 @@ def _process_and_get( if "position" in prop: - inp = Image(np.array(images)) + if apc.is_torch_array(images): + inp = Image(images) + + else: + inp = Image(np.array(images)) + inp.append(prop) out = Image(self.get(inp, **kwargs)) out.merge_properties_from(inp) @@ -6068,42 +6095,77 @@ def _process_and_get( # Create an empty output image. output_region = kwargs["output_region"] - output = np.zeros( - ( + shape = ( output_region[2] - output_region[0], output_region[3] - output_region[1], kwargs["number_of_masks"], ) - ) + if apc.is_torch_array(images): + output = torch.zeros(shape) + else: + output = np.zeros(shape) from deeptrack.optics import _get_position # Merge masks into the output. for label in list_of_labels: position = _get_position(label) - p0 = np.round(position - output_region[0:2]) + p0 = xp.round(position - output_region[0:2]) - if np.any(p0 > output.shape[0:2]) or \ - np.any(p0 + label.shape[0:2] < 0): + if xp.any(p0 > output.shape[0:2]) or xp.any( + p0 + label.shape[0:2] < 0 + ): continue - crop_x = int(-np.min([p0[0], 0])) - crop_y = int(-np.min([p0[1], 0])) - crop_x_end = int( - label.shape[0] - - np.max([p0[0] + label.shape[0] - output.shape[0], 0]) - ) - crop_y_end = int( - label.shape[1] - - np.max([p0[1] + label.shape[1] - output.shape[1], 0]) - ) - - labelarg = label[crop_x:crop_x_end, crop_y:crop_y_end, :] + if apc.is_torch_array(images): + crop_x = int( + -torch.minimum(p0[0], torch.tensor(0, device=p0.device)) + ) + crop_y = int( + -torch.minimum(p0[1], torch.tensor(0, device=p0.device)) + ) + crop_x_end = int( + label.shape[0] - torch.max(torch.stack([ + p0[0] + label.shape[0] - output.shape[0], + torch.tensor(0) + ])) + ) + crop_y_end = int( + label.shape[1]- torch.max(torch.stack([ + p0[1] + label.shape[1] - output.shape[1], + torch.tensor(0) + ])) + ) + + labelarg = label[crop_x:crop_x_end, crop_y:crop_y_end, :] + + p0[0] = torch.max( + p0[0], torch.tensor(0, dtype=p0.dtype, device=p0.device) + ) + p0[1] = torch.max( + p0[1], torch.tensor(0, dtype=p0.dtype, device=p0.device) + ) + + p0 = p0.int() - p0[0] = np.max([p0[0], 0]) - p0[1] = np.max([p0[1], 0]) - - p0 = p0.astype(int) + else: + crop_x = int(-np.min([p0[0], 0])) + crop_y = int(-np.min([p0[1], 0])) + crop_x_end = int( + label.shape[0] + - np.max([p0[0] + label.shape[0] - output.shape[0], 0]) + ) + crop_y_end = int( + label.shape[1] + - np.max([p0[1] + label.shape[1] - output.shape[1], 0]) + ) + + labelarg = label[crop_x:crop_x_end, crop_y:crop_y_end, :] + + p0[0] = np.max([p0[0], 0]) + p0[1] = np.max([p0[1], 0]) + + p0 = p0.astype(int) output_slice = output[ p0[0] : p0[0] + labelarg.shape[0], @@ -6127,8 +6189,7 @@ def _process_and_get( elif merge == "overwrite": output_slice[ labelarg[..., label_index] != 0, label_index - ] = labelarg[labelarg[..., label_index] != 0, \ - label_index] + ] = labelarg[labelarg[..., label_index] != 0, label_index] output[ p0[0] : p0[0] + labelarg.shape[0], p0[1] : p0[1] + labelarg.shape[1], From 38ce4519a446014442f4d9ba55b92e7a1ee0acfe Mon Sep 17 00:00:00 2001 From: mirjagranfors Date: Thu, 3 Jul 2025 17:10:55 +0200 Subject: [PATCH 116/118] corrections of docs --- deeptrack/features.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index fbb311513..c1967c0e3 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -5903,15 +5903,22 @@ class SampleToMasks(Feature): Methods ------- `get( - image: np.ndarray | Image, + image: np.ndarray | torch.Tensor | Image, transformation_function: Callable[[Image], Image], **kwargs: dict[str, Any] ) -> Image` Applies the transformation function to the input image. `_process_and_get( - images: list[np.ndarray] | np.ndarray | list[Image] | Image, + images: ( + list[np.ndarray] + | np.ndarray + | list[torch.Tensor] + | torch.Tensor + | list[Image] + | Image + ), **kwargs: dict[str, Any] - ) -> Image | np.ndarray` + ) -> Image | np.ndarray | torch.Tensor` Processes a list of images and generates a multi-layer mask. Returns From f78b7c5c80e7f8f2c5df6b633d852d5a380c63fa Mon Sep 17 00:00:00 2001 From: Giovanni Volpe Date: Fri, 4 Jul 2025 10:29:51 +0200 Subject: [PATCH 117/118] Update features.py --- deeptrack/features.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index c1967c0e3..75a65a925 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -5979,11 +5979,30 @@ class SampleToMasks(Feature): def __init__( self: Feature, - transformation_function: Callable[[Image], Image], + transformation_function: PropertyLike[ + Callable[ + [ + NDArray + | list[NDArray] + | torch.Tensor + | list[torch.Tensor] + | Image + | list[Image] + ], + NDArray + | list[NDArray] + | torch.Tensor + | list[torch.Tensor] + | Image + | list[Image] + ], + ], number_of_masks: PropertyLike[int] = 1, - output_region: PropertyLike[tuple[int, int, int, int]] = None, + output_region: PropertyLike[tuple[int, int, int, int]] | None = None, merge_method: PropertyLike[ - str | Callable | list[str | Callable] + str + | Callable[[...], ...] + | list[str | Callable[[...], ...]] ] = "add", **kwargs: Any, ): @@ -5994,9 +6013,9 @@ def __init__( transformation_function: Callable[[Image], Image] Function to transform input images into masks. number_of_masks: PropertyLike[int], optional - Number of mask layers. Default is 1. + Number of mask layers. It defaults to 1. output_region: PropertyLike[tuple[int, int, int, int]], optional - Output region of the mask. Default is None. + Output region of the mask. It defaults to `None`. merge_method: PropertyLike[ str | Callable | list[str | Callable] ], optional From e8b7088a665552f6ab9a0a7258ec9923160beb2a Mon Sep 17 00:00:00 2001 From: mirjagranfors Date: Fri, 4 Jul 2025 10:53:09 +0200 Subject: [PATCH 118/118] fix typos --- deeptrack/features.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/deeptrack/features.py b/deeptrack/features.py index 75a65a925..04c4af649 100644 --- a/deeptrack/features.py +++ b/deeptrack/features.py @@ -186,7 +186,7 @@ def merge_features( "OneOf", "OneOfDict", "LoadImage", # TODO **MG** - "SampleToMasks", # TODO **MG** + "SampleToMasks", "AsType", # TODO **MG** "ChannelFirst2d", # TODO **AL** "Upscale", # TODO **AL** @@ -5871,7 +5871,7 @@ def get( class SampleToMasks(Feature): - """Creates a mask from a list of images. + """Create a mask from a list of images. This feature applies a transformation function to each input image and merges the resulting masks into a single multi-layer image. Each input @@ -5907,7 +5907,7 @@ class SampleToMasks(Feature): transformation_function: Callable[[Image], Image], **kwargs: dict[str, Any] ) -> Image` - Applies the transformation function to the input image. + Apply the transformation function to the input image. `_process_and_get( images: ( list[np.ndarray] @@ -5919,7 +5919,7 @@ class SampleToMasks(Feature): ), **kwargs: dict[str, Any] ) -> Image | np.ndarray | torch.Tensor` - Processes a list of images and generates a multi-layer mask. + Process a list of images and generate a multi-layer mask. Returns -------