Repository: albumentations-team/albumentations

Issue:
bboxes with min_visibility are excluded
`min_visibility` bbox parameter works as non-strict limit to exclude bboxes. I.e. bboxes with visibility ratio == min_visibility  are excluded.
It not only contradicts to documentation ('min_visibility (float): Minimum fraction of area for a bounding box to remain this box in list.') but causes the problem that if I want to retain only not clipped bboxes, setting `min_visibility=1.0` results in all bboxes are filtered

Discussion:
Dipet: Please add this test to `test_bbox`
```
@pytest.mark.parametrize(
    ["transforms", "bboxes", "result_bboxes", "min_area", "min_visibility"],
    [[Crop(10, 10, 20, 20)], [[0, 0, 10, 10, 0]], [], 0, 0],
    [[Crop(0, 0, 90, 90)], [[0, 0, 90, 90, 0], [0, 0, 89, 89, 0]], [[0, 0, 89, 89, 0]], 0, 1],
    [[Crop(0, 0, 90, 90)], [[0, 0, 1, 10, 0], [0, 0, 1, 11, 0]], [[0, 0, 1, 10, 0], [0, 0, 1, 11, 0]], 10, 0],
)
def test_bbox_params_edges(transforms, bboxes, result_bboxes, min_area, min_visibility):
    image = np.empty([100, 100, 3], dtype=np.uint8)
    aug = Compose(transforms, bbox_params=BboxParams("pascal_voc", min_area=min_area, min_visibility=min_visibility))

    res = aug(image=image, bboxes=bboxes)["bboxes"]
    assert np.allclose(res, result_bboxes)
```
IlyaOvodov: Done. But had to change the 2nd test: rect(0,0,90,90) is not cropped as rects are considered as semi-open, only rect(0,0,91,91) is

Patch:
diff --git a/albumentations/augmentations/dropout/channel_dropout.py b/albumentations/augmentations/dropout/channel_dropout.py
index 45d42c7..48b4707 100644
--- a/albumentations/augmentations/dropout/channel_dropout.py
+++ b/albumentations/augmentations/dropout/channel_dropout.py
@@ -1,11 +1,10 @@
 import random
-from typing import Union, Tuple, Any, Mapping
+from typing import Any, Mapping, Tuple, Union
 
 import numpy as np
 
-from albumentations.core.transforms_interface import (
-    ImageOnlyTransform,
-)
+from albumentations.core.transforms_interface import ImageOnlyTransform
+
 from .functional import channel_dropout
 
 __all__ = ["ChannelDropout"]
diff --git a/albumentations/augmentations/dropout/cutout.py b/albumentations/augmentations/dropout/cutout.py
index 206f171..c781c97 100644
--- a/albumentations/augmentations/dropout/cutout.py
+++ b/albumentations/augmentations/dropout/cutout.py
@@ -1,12 +1,11 @@
 import random
 import warnings
-from typing import Union, Any, Dict, Tuple
+from typing import Any, Dict, Tuple, Union
 
 import numpy as np
 
-from albumentations.core.transforms_interface import (
-    ImageOnlyTransform,
-)
+from albumentations.core.transforms_interface import ImageOnlyTransform
+
 from .functional import cutout
 
 __all__ = ["Cutout"]
diff --git a/albumentations/augmentations/dropout/functional.py b/albumentations/augmentations/dropout/functional.py
index 759bc4d..d40b516 100644
--- a/albumentations/augmentations/dropout/functional.py
+++ b/albumentations/augmentations/dropout/functional.py
@@ -1,6 +1,7 @@
-from typing import List, Tuple, Union, Iterable
+from typing import Iterable, List, Tuple, Union
 
 import numpy as np
+
 from ..functional import preserve_shape
 
 __all__ = ["cutout", "channel_dropout"]
diff --git a/albumentations/augmentations/dropout/grid_dropout.py b/albumentations/augmentations/dropout/grid_dropout.py
index f5e7f93..96bb1e0 100644
--- a/albumentations/augmentations/dropout/grid_dropout.py
+++ b/albumentations/augmentations/dropout/grid_dropout.py
@@ -1,10 +1,10 @@
 import random
-from typing import Tuple, Iterable
+from typing import Iterable, Tuple
 
 import numpy as np
 
-from . import functional as F
 from ...core.transforms_interface import DualTransform
+from . import functional as F
 
 __all__ = ["GridDropout"]
 
diff --git a/albumentations/core/bbox_utils.py b/albumentations/core/bbox_utils.py
index d3b9790..152ef6a 100644
--- a/albumentations/core/bbox_utils.py
+++ b/albumentations/core/bbox_utils.py
@@ -458,13 +458,12 @@ def filter_bboxes(
         transformed_box_area = calculate_bbox_area(bbox, rows, cols)
         bbox, tail = tuple(np.clip(bbox[:4], 0, 1.0)), tuple(bbox[4:])
         clipped_box_area = calculate_bbox_area(bbox, rows, cols)
-        if not transformed_box_area or clipped_box_area / transformed_box_area <= min_visibility:
-            continue
-        else:
-            bbox = tuple(np.clip(bbox[:4], 0, 1.0))
-        if calculate_bbox_area(bbox, rows, cols) <= min_area:
-            continue
-        resulting_boxes.append(bbox + tail)
+        if (
+            clipped_box_area != 0  # to ensure transformed_box_area!=0 and to handle min_area=0 or min_visibility=0
+            and clipped_box_area >= min_area
+            and clipped_box_area / transformed_box_area >= min_visibility
+        ):
+            resulting_boxes.append(bbox + tail)
     return resulting_boxes
 
 


Test patch:
diff --git a/tests/test_bbox.py b/tests/test_bbox.py
index ba981d6..67c30e6 100644
--- a/tests/test_bbox.py
+++ b/tests/test_bbox.py
@@ -1,7 +1,7 @@
 import numpy as np
 import pytest
 
-from albumentations import RandomCrop, RandomResizedCrop, RandomSizedCrop, Rotate
+from albumentations import Crop, RandomCrop, RandomResizedCrop, RandomSizedCrop, Rotate
 from albumentations.core.bbox_utils import (
     calculate_bbox_area,
     convert_bbox_from_albumentations,
@@ -267,3 +267,18 @@ def test_crop_boxes_replay_compose():
     transformed2 = ReplayCompose.replay(transformed["replay"], **input_data)
 
     np.testing.assert_almost_equal(transformed["bboxes"], transformed2["bboxes"])
+
+
+@pytest.mark.parametrize(
+    ["transforms", "bboxes", "result_bboxes", "min_area", "min_visibility"],
+    [
+        [[Crop(10, 10, 20, 20)], [[0, 0, 10, 10, 0]], [], 0, 0],
+        [[Crop(0, 0, 90, 90)], [[0, 0, 91, 91, 0], [0, 0, 90, 90, 0]], [[0, 0, 90, 90, 0]], 0, 1],
+        [[Crop(0, 0, 90, 90)], [[0, 0, 1, 10, 0], [0, 0, 1, 11, 0]], [[0, 0, 1, 10, 0], [0, 0, 1, 11, 0]], 10, 0],
+    ],
+)
+def test_bbox_params_edges(transforms, bboxes, result_bboxes, min_area, min_visibility):
+    image = np.empty([100, 100, 3], dtype=np.uint8)
+    aug = Compose(transforms, bbox_params=BboxParams("pascal_voc", min_area=min_area, min_visibility=min_visibility))
+    res = aug(image=image, bboxes=bboxes)["bboxes"]
+    assert np.allclose(res, result_bboxes)