Repository: albumentations-team/albumentations

Issue:
min_visibility param does not work with A.Affine transform
## 🐛 Bug

 `min_visibility` param does not work with `A.Affine` transform
## To Reproduce

Steps to reproduce the behavior:

```
transform = A.Compose(
    [A.Affine(translate_px=-20, p=1.0)],
    bbox_params=A.BboxParams(format='pascal_voc', label_fields=['class_labels'], min_visibility=0.9)
)

img = np.zeros((500, 500, 3))
bboxes = np.array([[0, 0, 100, 100]])

transformed = transform(image=img, bboxes=bboxes, class_labels=['class_0'])

new_img = transformed['image']
new_bboxes = transformed['bboxes']

print(new_bboxes) # [(0.0, 0.0, 80.0, 80.0)]
```

new_bboxes = [(0.0, 0.0, 80.0, 80.0)], but I expect it should be empty


## Expected behavior
`new_bboxes` should be empty, because:
- the origin bbox area is 100x100 = 10000px
- the transformed bbox area is 80x80 = 6400px
- visibility = 6400/10000 = 0.64 < 0.9, then this bbox should be removed

## Environment

 - Albumentations version: 1.0.3
 - Python version: 3.9.13
 - OS: MacOS
 - How you installed albumentations: pip

## Additional context


Discussion:

Patch:
diff --git a/albumentations/augmentations/geometric/functional.py b/albumentations/augmentations/geometric/functional.py
index bc3782d..5690726 100644
--- a/albumentations/augmentations/geometric/functional.py
+++ b/albumentations/augmentations/geometric/functional.py
@@ -461,18 +461,12 @@ def perspective_bbox(
     for pt in points:
         pt = perspective_keypoint(pt.tolist() + [0, 0], height, width, matrix, max_width, max_height, keep_size)
         x, y = pt[:2]
-        x = np.clip(x, 0, width if keep_size else max_width)
-        y = np.clip(y, 0, height if keep_size else max_height)
         x1 = min(x1, x)
         x2 = max(x2, x)
         y1 = min(y1, y)
         y2 = max(y2, y)
 
-    x = np.clip([x1, x2], 0, width if keep_size else max_width)
-    y = np.clip([y1, y2], 0, height if keep_size else max_height)
-    return normalize_bbox(
-        (x[0], y[0], x[1], y[1]), height if keep_size else max_height, width if keep_size else max_width
-    )
+    return normalize_bbox((x1, y1, x2, y2), height if keep_size else max_height, width if keep_size else max_width)
 
 
 def rotation2DMatrixToEulerAngles(matrix: np.ndarray, y_up: bool = False) -> float:
@@ -575,8 +569,6 @@ def bbox_affine(
         ]
     )
     points = skimage.transform.matrix_transform(points, matrix.params)
-    points[:, 0] = np.clip(points[:, 0], 0, output_shape[1])
-    points[:, 1] = np.clip(points[:, 1], 0, output_shape[0])
     x_min = np.min(points[:, 0])
     x_max = np.max(points[:, 0])
     y_min = np.min(points[:, 1])


Test patch:
diff --git a/tests/test_transforms.py b/tests/test_transforms.py
index 8a1e75d..1921e1a 100644
--- a/tests/test_transforms.py
+++ b/tests/test_transforms.py
@@ -1188,3 +1188,40 @@ def test_rotate_equal(img, aug_cls, angle):
     diff = np.round(np.abs(res_a - res_b))
     assert diff[:, :2].max() <= 2
     assert (diff[:, -1] % 360).max() <= 1
+
+
+@pytest.mark.parametrize(
+    "get_transform",
+    [
+        lambda sign: A.Affine(translate_px=sign * 2),
+        lambda sign: A.ShiftScaleRotate(shift_limit=(sign * 0.02, sign * 0.02), scale_limit=0, rotate_limit=0),
+    ],
+)
+@pytest.mark.parametrize(
+    ["bboxes", "expected", "min_visibility", "sign"],
+    [
+        [[(0, 0, 10, 10, 1)], [], 0.9, -1],
+        [[(0, 0, 10, 10, 1)], [(0, 0, 8, 8, 1)], 0.6, -1],
+        [[(90, 90, 100, 100, 1)], [], 0.9, 1],
+        [[(90, 90, 100, 100, 1)], [(92, 92, 100, 100, 1)], 0.6, 1],
+    ],
+)
+def test_bbox_clipping(get_transform, image, bboxes, expected, min_visibility: float, sign: int):
+    transform = get_transform(sign)
+    transform.p = 1
+    transform = A.Compose([transform], bbox_params=A.BboxParams(format="pascal_voc", min_visibility=min_visibility))
+
+    res = transform(image=image, bboxes=bboxes)["bboxes"]
+    assert res == expected
+
+
+def test_bbox_clipping_perspective():
+    random.seed(0)
+    transform = A.Compose(
+        [A.Perspective(scale=(0.05, 0.05), p=1)], bbox_params=A.BboxParams(format="pascal_voc", min_visibility=0.6)
+    )
+
+    image = np.empty([1000, 1000, 3], dtype=np.uint8)
+    bboxes = np.array([[0, 0, 100, 100, 1]])
+    res = transform(image=image, bboxes=bboxes)["bboxes"]
+    assert len(res) == 0