Repository: albumentations-team/albumentations

Issue:
`CropNonEmptyMaskIfExists` modifies first `mask` contained in `masks`
## 🐛 Bug

When applied to `masks`, `CropNonEmptyMaskIfExists` changes the first element of `masks` via the in-place comparison `|=` in `update_params`.

<!-- A clear and concise description of what the bug is. -->

## To Reproduce

Steps to reproduce the behavior:

Execute following code
```
import numpy as np
import albumentations as A

mask_1 = np.zeros([10, 10], dtype=np.uint8)
mask_1[0, 0] = 1
mask_2 = np.zeros([10, 10], dtype=np.uint8)
mask_2[0, 0] = 10
crop_1 = np.array([[1]])
crop_2 = np.array([[10]])
aug_1 = A.CropNonEmptyMaskIfExists(1, 1)

cropped = aug_1(image=mask_1, masks=[mask_1, mask_2])

np.testing.assert_array_equal(crop_1, cropped["masks"][0])
np.testing.assert_array_equal(crop_2, cropped["masks"][1])
```
<!-- If you have a code sample, error messages, stack traces, please provide it here as well -->

## Expected behavior

The above MWE passes as expected

<!-- A clear and concise description of what you expected to happen. -->

## Environment

 - Albumentations version (e.g., 0.1.8):
 - Python version (e.g., 3.7):
 - OS (e.g., Linux):
 - How you installed albumentations (`conda`, `pip`, source):
 - Any other relevant information:

## Additional context

<!-- Add any other context about the problem here. -->


Discussion:

Patch:
diff --git a/albumentations/augmentations/crops/transforms.py b/albumentations/augmentations/crops/transforms.py
index 150478a..24dffb6 100644
--- a/albumentations/augmentations/crops/transforms.py
+++ b/albumentations/augmentations/crops/transforms.py
@@ -202,7 +202,7 @@ class CropNonEmptyMaskIfExists(DualTransform):
             mask = self._preprocess_mask(kwargs["mask"])
         elif "masks" in kwargs and len(kwargs["masks"]):
             masks = kwargs["masks"]
-            mask = self._preprocess_mask(masks[0])
+            mask = self._preprocess_mask(np.copy(masks[0]))  # need copy as we perform in-place mod afterwards
             for m in masks[1:]:
                 mask |= self._preprocess_mask(m)
         else:


Test patch:
diff --git a/tests/test_transforms.py b/tests/test_transforms.py
index e371539..f83be9f 100644
--- a/tests/test_transforms.py
+++ b/tests/test_transforms.py
@@ -371,14 +371,20 @@ def test_crop_non_empty_mask():
             np.testing.assert_array_equal(augmented["image"], crop)
             np.testing.assert_array_equal(augmented["mask"], crop)
 
+    def _test_crops(masks, crops, aug, n=1):
+        for _ in range(n):
+            augmented = aug(image=masks[0], masks=masks)
+            for crop, augment in zip(crops, augmented["masks"]):
+                np.testing.assert_array_equal(augment, crop)
+
     # test general case
-    mask_1 = np.zeros([10, 10])
+    mask_1 = np.zeros([10, 10], dtype=np.uint8)  # uint8 required for passing mask_1 as `masks` (which uses bitwise or)
     mask_1[0, 0] = 1
     crop_1 = np.array([[1]])
     aug_1 = A.CropNonEmptyMaskIfExists(1, 1)
 
     # test empty mask
-    mask_2 = np.zeros([10, 10])
+    mask_2 = np.zeros([10, 10], dtype=np.uint8)  # uint8 required for passing mask_2 as `masks` (which uses bitwise or)
     crop_2 = np.array([[0]])
     aug_2 = A.CropNonEmptyMaskIfExists(1, 1)
 
@@ -411,6 +417,7 @@ def test_crop_non_empty_mask():
     _test_crop(mask_4, crop_4, aug_4, n=5)
     _test_crop(mask_5, crop_5, aug_5, n=1)
     _test_crop(mask_6, crop_6, aug_6, n=10)
+    _test_crops([mask_2, mask_1], [crop_2, crop_1], aug_1, n=1)
 
 
 @pytest.mark.parametrize("interpolation", [cv2.INTER_NEAREST, cv2.INTER_LINEAR, cv2.INTER_CUBIC])