Repository: albumentations-team/albumentations

Issue:
FromFloat transform doesn't serialize `dtype`
I found empirically that `FromFloat` doesn't quite survive the serialization-deserialization roundtrip. The serialized form has the `max_value` attribute *but not* `dtype`, and this is not sufficient to restore the expected behavior in the deserialized transform.

o4-mini elaborates below. (Not trying to be cute- just all I had time to do at the moment, regrets)

    You’re right – the serialized dict for FromFloat never includes the dtype parameter, so when you
    re-hydrate the transform it silently falls back to the default ("uint8"). Here’s what’s happening
    under the hood:

    • The base serializer (BasicTransform.to_dict_private) builds its state by

        1. taking all the init-args names (via introspection on your `__init__` signature, which includes
     `dtype`, `max_value`, and `p`)
        2. and then grabbing the *instance* attributes with those same names.
           • In your `__init__`, you take the `dtype` string and immediately convert it to a NumPy dtype
    into `self.dtype_value`, but you never assign back to `self.dtype`.
           • So when the serializer looks for an attribute called `dtype`, it doesn’t find it, and drops
    it. It *does* find and save `max_value`, but not `dtype`.
           • On deserialization, from_dict picks up only `max_value` (and `p`), and calls
    `FromFloat(**args)`. Since no `dtype` was passed in, you get `dtype="uint8"` again, even if you’d
    originally been using e.g. `"uint16"`.

    In short:
    – get_transform_init_args_names() knows about the dtype parameter
    – get_transform_init_args() only sees attributes on the instance, and you set only dtype_value, not
    dtype
    – so dtype never makes it into the serialized dict

    That’s the root cause. We’ll need to either store self.dtype (as a string) or override
    to_dict_private in FromFloat to explicitly write out self.dtype_value back under the key "dtype".


Discussion:
Thanks, will take a look.
@mlin Thank you, fixed



Patch:
diff --git a/albumentations/augmentations/other/type_transform.py b/albumentations/augmentations/other/type_transform.py
index 0dabe6aaa..7a3a14163 100644
--- a/albumentations/augmentations/other/type_transform.py
+++ b/albumentations/augmentations/other/type_transform.py
@@ -176,7 +176,7 @@ def __init__(
         p: float = 1.0,
     ):
         super().__init__(p=p)
-        self.dtype_value = np.dtype(dtype)
+        self.dtype = dtype
         self.max_value = max_value
 
     def apply(self, img: np.ndarray, **params: Any) -> np.ndarray:
@@ -190,7 +190,7 @@ def apply(self, img: np.ndarray, **params: Any) -> np.ndarray:
             np.ndarray: The image with the applied FromFloat transform.
 
         """
-        return from_float(img, self.dtype_value, self.max_value)
+        return from_float(img, np.dtype(self.dtype), self.max_value)
 
     def apply_to_images(self, images: np.ndarray, **params: Any) -> np.ndarray:
         """Apply the FromFloat transform to the input images.
@@ -200,7 +200,7 @@ def apply_to_images(self, images: np.ndarray, **params: Any) -> np.ndarray:
             **params (Any): Additional parameters (not used in this transform).
 
         """
-        return clip(np.rint(images * self.max_value), self.dtype_value, inplace=True)
+        return clip(np.rint(images * self.max_value), np.dtype(self.dtype), inplace=True)
 
     def apply_to_volume(self, volume: np.ndarray, **params: Any) -> np.ndarray:
         """Apply the FromFloat transform to the input volume.


Test patch:
diff --git a/tests/test_serialization.py b/tests/test_serialization.py
index a916daee9..935d6d173 100644
--- a/tests/test_serialization.py
+++ b/tests/test_serialization.py
@@ -812,3 +812,12 @@ def test_serialization_excludes_strict() -> None:
     transform = A.HorizontalFlip(strict=True)
     transform_dict = A.to_dict(transform)["transform"]
     assert "strict" not in transform_dict
+
+
+def test_serialization_from_float() -> None:
+    dtype = "uint8"
+    max_value = 137
+    transform = A.FromFloat(dtype=dtype, max_value=max_value)
+    transform_dict = A.to_dict(transform)["transform"]
+    assert transform_dict["dtype"] == dtype
+    assert transform_dict["max_value"] == max_value