Skip to content

Commit

Permalink
Filter out bounding boxes with width or height below parameters (#1327)
Browse files Browse the repository at this point in the history
* Filter out bounding boxes with width or height below parameters

* Correct typo

* Include `min_width` and `min_height` in dictionary

* Test filtering by `min_width` and `min_height`

* Include `min_width` and `min_height` in docstring
  • Loading branch information
jangop committed Oct 25, 2022
1 parent cb39781 commit d09f6b0
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 2 deletions.
40 changes: 38 additions & 2 deletions albumentations/core/bbox_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ class BboxParams(Params):
visible area in pixels is less than this value will be removed. Default: 0.0.
min_visibility (float): minimum fraction of area for a bounding box
to remain this box in list. Default: 0.0.
min_width (float): Minimum width of a bounding box. All bounding boxes whose width is
less than this value will be removed. Default: 0.0.
min_height (float): Minimum height of a bounding box. All bounding boxes whose height is
less than this value will be removed. Default: 0.0.
check_each_transform (bool): if `True`, then bboxes will be checked after each dual transform.
Default: `True`
"""
Expand All @@ -62,11 +66,15 @@ def __init__(
label_fields: Optional[Sequence[str]] = None,
min_area: float = 0.0,
min_visibility: float = 0.0,
min_width: float = 0.0,
min_height: float = 0.0,
check_each_transform: bool = True,
):
super(BboxParams, self).__init__(format, label_fields)
self.min_area = min_area
self.min_visibility = min_visibility
self.min_width = min_width
self.min_height = min_height
self.check_each_transform = check_each_transform

def _to_dict(self) -> Dict[str, Any]:
Expand All @@ -75,6 +83,8 @@ def _to_dict(self) -> Dict[str, Any]:
{
"min_area": self.min_area,
"min_visibility": self.min_visibility,
"min_width": self.min_width,
"min_height": self.min_height,
"check_each_transform": self.check_each_transform,
}
)
Expand Down Expand Up @@ -112,7 +122,15 @@ def ensure_data_valid(self, data: Dict[str, Any]) -> None:

def filter(self, data: Sequence, rows: int, cols: int) -> List:
self.params: BboxParams
return filter_bboxes(data, rows, cols, min_area=self.params.min_area, min_visibility=self.params.min_visibility)
return filter_bboxes(
data,
rows,
cols,
min_area=self.params.min_area,
min_visibility=self.params.min_visibility,
min_width=self.params.min_width,
min_height=self.params.min_height,
)

def check(self, data: Sequence, rows: int, cols: int) -> None:
check_bboxes(data)
Expand Down Expand Up @@ -429,7 +447,13 @@ def check_bboxes(bboxes: Sequence[BoxType]) -> None:


def filter_bboxes(
bboxes: Sequence[BoxType], rows: int, cols: int, min_area: float = 0.0, min_visibility: float = 0.0
bboxes: Sequence[BoxType],
rows: int,
cols: int,
min_area: float = 0.0,
min_visibility: float = 0.0,
min_width: float = 0.0,
min_height: float = 0.0,
) -> List[BoxType]:
"""Remove bounding boxes that either lie outside of the visible area by more then min_visibility
or whose area in pixels is under the threshold set by `min_area`. Also it crops boxes to final image size.
Expand All @@ -441,20 +465,32 @@ def filter_bboxes(
min_area: Minimum area of a bounding box. All bounding boxes whose visible area in pixels.
is less than this value will be removed. Default: 0.0.
min_visibility: Minimum fraction of area for a bounding box to remain this box in list. Default: 0.0.
min_width: Minimum width of a bounding box. All bounding boxes whose width is
less than this value will be removed. Default: 0.0.
min_height: Minimum height of a bounding box. All bounding boxes whose height is
less than this value will be removed. Default: 0.0.
Returns:
List of bounding boxes.
"""
resulting_boxes: List[BoxType] = []
for bbox in bboxes:
# Calculate areas of bounding box before and after clipping.
transformed_box_area = calculate_bbox_area(bbox, rows, cols)
bbox, tail = cast(BoxType, tuple(np.clip(bbox[:4], 0, 1.0))), tuple(bbox[4:])
clipped_box_area = calculate_bbox_area(bbox, rows, cols)

# Calculate width and height of the clipped bounding box.
x_min, y_min, x_max, y_max = denormalize_bbox(bbox, rows, cols)[:4]
clipped_width, clipped_height = x_max - x_min, y_max - y_min

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
and clipped_width >= min_width
and clipped_height >= min_height
):
resulting_boxes.append(cast(BoxType, bbox + tail))
return resulting_boxes
Expand Down
34 changes: 34 additions & 0 deletions tests/test_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,40 @@ def test_filter_bboxes(bboxes, min_area, min_visibility, target):
assert filtered_bboxes == target


@pytest.mark.parametrize(
["bboxes", "img_width", "img_height", "min_width", "min_height", "target"],
[
[
[(0.1, 0.1, 0.9, 0.9), (0.1, 0.1, 0.2, 0.9), (0.1, 0.1, 0.9, 0.2), (0.1, 0.1, 0.2, 0.2)],
100,
100,
20,
20,
[(0.1, 0.1, 0.9, 0.9)],
],
[
[(0.1, 0.1, 0.9, 0.9), (0.1, 0.1, 0.2, 0.9), (0.1, 0.1, 0.9, 0.2), (0.1, 0.1, 0.2, 0.2)],
100,
100,
20,
0,
[(0.1, 0.1, 0.9, 0.9), (0.1, 0.1, 0.9, 0.2)],
],
[
[(0.1, 0.1, 0.9, 0.9), (0.1, 0.1, 0.2, 0.9), (0.1, 0.1, 0.9, 0.2), (0.1, 0.1, 0.2, 0.2)],
100,
100,
0,
20,
[(0.1, 0.1, 0.9, 0.9), (0.1, 0.1, 0.2, 0.9)],
],
],
)
def test_filter_bboxes_by_min_width_height(bboxes, img_width, img_height, min_width, min_height, target):
filtered_bboxes = filter_bboxes(bboxes, cols=img_width, rows=img_height, min_width=min_width, min_height=min_height)
assert filtered_bboxes == target


def test_fun_max_size():
target_width = 256

Expand Down

0 comments on commit d09f6b0

Please sign in to comment.