Repository: huggingface/transformers
Pull request: Fix overriding Fast Image/Video Processors instance attributes affect other instances

# What does this PR do?

Fix an issue raised here https://github.com/huggingface/transformers/pull/39172#issuecomment-3052873789
After internal discussions with @ydshieh , we found that the issue is not due to class attributes, but to lists/dicts being passed around without being copied. 
In fact the same issue is also present in slow image processors, but we don't fix them here, as they all have their own attributes assignment logic, and are to be deprecated.

To reproduce the issue:
```python
>>> o = CLIPImageProcessorFast()
>>> o.image_mean[0]
0.48145466
>>> CLIPImageProcessorFast.image_mean
[0.48145466, 0.4578275, 0.40821073]
>>> o.image_mean[0] = 1
>>> CLIPImageProcessorFast.image_mean
[1, 0.4578275, 0.40821073]
>>>
```

Changed files:
filename: src/transformers/image_processing_utils_fast.py
patch: @@ -13,6 +13,7 @@
 # limitations under the License.
 
 from collections.abc import Iterable
+from copy import deepcopy
 from functools import lru_cache, partial
 from typing import Any, Optional, TypedDict, Union
 
@@ -229,7 +230,7 @@ def __init__(
             if kwarg is not None:
                 setattr(self, key, kwarg)
             else:
-                setattr(self, key, getattr(self, key, None))
+                setattr(self, key, deepcopy(getattr(self, key, None)))
 
         # get valid kwargs names
         self._valid_kwargs_names = list(self.valid_kwargs.__annotations__.keys())

filename: src/transformers/video_processing_utils.py
patch: @@ -13,10 +13,10 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import copy
 import json
 import os
 import warnings
+from copy import deepcopy
 from typing import Any, Optional, Union
 
 import numpy as np
@@ -202,7 +202,7 @@ def __init__(self, **kwargs: Unpack[VideosKwargs]) -> None:
             if kwargs.get(key) is not None:
                 setattr(self, key, kwargs[key])
             else:
-                setattr(self, key, getattr(self, key, None))
+                setattr(self, key, deepcopy(getattr(self, key, None)))
 
     def __call__(self, videos, **kwargs) -> BatchFeature:
         return self.preprocess(videos, **kwargs)
@@ -774,7 +774,7 @@ def to_dict(self) -> dict[str, Any]:
         Returns:
             `dict[str, Any]`: Dictionary of all the attributes that make up this video processor instance.
         """
-        output = copy.deepcopy(self.__dict__)
+        output = deepcopy(self.__dict__)
         output.pop("model_valid_processing_keys", None)
         output.pop("_valid_kwargs_names", None)
         output["video_processor_type"] = self.__class__.__name__

filename: tests/test_image_processing_common.py
patch: @@ -19,6 +19,7 @@
 import tempfile
 import time
 import warnings
+from copy import deepcopy
 
 import numpy as np
 import requests
@@ -559,6 +560,43 @@ def test_image_processor_preprocess_arguments(self):
         if not is_tested:
             self.skipTest(reason="No validation found for `preprocess` method")
 
+    def test_override_instance_attributes_does_not_affect_other_instances(self):
+        if self.fast_image_processing_class is None:
+            self.skipTest(
+                "Only testing fast image processor, as most slow processors break this test and are to be deprecated"
+            )
+
+        image_processing_class = self.fast_image_processing_class
+        image_processor_1 = image_processing_class()
+        image_processor_2 = image_processing_class()
+        if not (hasattr(image_processor_1, "size") and isinstance(image_processor_1.size, dict)) or not (
+            hasattr(image_processor_1, "image_mean") and isinstance(image_processor_1.image_mean, list)
+        ):
+            self.skipTest(
+                reason="Skipping test as the image processor does not have dict size or list image_mean attributes"
+            )
+
+        original_size_2 = deepcopy(image_processor_2.size)
+        for key in image_processor_1.size:
+            image_processor_1.size[key] = -1
+        modified_copied_size_1 = deepcopy(image_processor_1.size)
+
+        original_image_mean_2 = deepcopy(image_processor_2.image_mean)
+        image_processor_1.image_mean[0] = -1
+        modified_copied_image_mean_1 = deepcopy(image_processor_1.image_mean)
+
+        # check that the original attributes of the second instance are not affected
+        self.assertEqual(image_processor_2.size, original_size_2)
+        self.assertEqual(image_processor_2.image_mean, original_image_mean_2)
+
+        for key in image_processor_2.size:
+            image_processor_2.size[key] = -2
+        image_processor_2.image_mean[0] = -2
+
+        # check that the modified attributes of the first instance are not affected by the second instance
+        self.assertEqual(image_processor_1.size, modified_copied_size_1)
+        self.assertEqual(image_processor_1.image_mean, modified_copied_image_mean_1)
+
     @slow
     @require_torch_accelerator
     @require_vision
@@ -575,7 +613,6 @@ def test_can_compile_fast_image_processor(self):
 
         image_processor = torch.compile(image_processor, mode="reduce-overhead")
         output_compiled = image_processor(input_image, device=torch_device, return_tensors="pt")
-        print(output_eager.pixel_values.dtype, output_compiled.pixel_values.dtype)
         self._assert_slow_fast_tensors_equivalence(
             output_eager.pixel_values, output_compiled.pixel_values, atol=1e-4, rtol=1e-4, mean_atol=1e-5
         )

filename: tests/test_video_processing_common.py
patch: @@ -18,6 +18,7 @@
 import os
 import tempfile
 import warnings
+from copy import deepcopy
 
 import numpy as np
 from packaging import version
@@ -448,3 +449,40 @@ def test_video_processor_preprocess_arguments(self):
 
         if not is_tested:
             self.skipTest(reason="No validation found for `preprocess` method")
+
+    def test_override_instance_attributes_does_not_affect_other_instances(self):
+        if self.fast_video_processing_class is None:
+            self.skipTest(
+                "Only testing fast video processor, as most slow processors break this test and are to be deprecated"
+            )
+
+        video_processing_class = self.fast_video_processing_class
+        video_processor_1 = video_processing_class()
+        video_processor_2 = video_processing_class()
+        if not (hasattr(video_processor_1, "size") and isinstance(video_processor_1.size, dict)) or not (
+            hasattr(video_processor_1, "image_mean") and isinstance(video_processor_1.image_mean, list)
+        ):
+            self.skipTest(
+                reason="Skipping test as the image processor does not have dict size or list image_mean attributes"
+            )
+
+        original_size_2 = deepcopy(video_processor_2.size)
+        for key in video_processor_1.size:
+            video_processor_1.size[key] = -1
+        modified_copied_size_1 = deepcopy(video_processor_1.size)
+
+        original_image_mean_2 = deepcopy(video_processor_2.image_mean)
+        video_processor_1.image_mean[0] = -1
+        modified_copied_image_mean_1 = deepcopy(video_processor_1.image_mean)
+
+        # check that the original attributes of the second instance are not affected
+        self.assertEqual(video_processor_2.size, original_size_2)
+        self.assertEqual(video_processor_2.image_mean, original_image_mean_2)
+
+        for key in video_processor_2.size:
+            video_processor_2.size[key] = -2
+        video_processor_2.image_mean[0] = -2
+
+        # check that the modified attributes of the first instance are not affected by the second instance
+        self.assertEqual(video_processor_1.size, modified_copied_size_1)
+        self.assertEqual(video_processor_1.image_mean, modified_copied_image_mean_1)