Skip to content

Commit 92291ee

Browse files
authored
Fix resize boxes to visible area (#2372)
* Fix * Fix in resize_boxes_to_visible_area * Cleanup * Cleanup
1 parent 23352f5 commit 92291ee

File tree

4 files changed

+90
-14
lines changed

4 files changed

+90
-14
lines changed

.pre-commit-config.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ repos:
5151
files: setup.py
5252
- repo: https://github.com/astral-sh/ruff-pre-commit
5353
# Ruff version.
54-
rev: v0.9.7
54+
rev: v0.9.8
5555
hooks:
5656
# Run the linter.
5757
- id: ruff

albumentations/augmentations/dropout/functional.py

+6-11
Original file line numberDiff line numberDiff line change
@@ -242,14 +242,12 @@ def filter_keypoints_in_holes(keypoints: np.ndarray, holes: np.ndarray) -> np.nd
242242
return keypoints[valid_keypoints]
243243

244244

245+
@handle_empty_array("bboxes")
245246
def resize_boxes_to_visible_area(
246247
boxes: np.ndarray,
247248
hole_mask: np.ndarray,
248249
) -> np.ndarray:
249250
"""Resize boxes to their largest visible rectangular regions."""
250-
if len(boxes) == 0:
251-
return boxes
252-
253251
# Extract box coordinates
254252
x1 = boxes[:, 0].astype(int)
255253
y1 = boxes[:, 1].astype(int)
@@ -264,11 +262,6 @@ def resize_boxes_to_visible_area(
264262

265263
for i, (visible, box) in enumerate(zip(visible_areas, boxes)):
266264
if not visible.any():
267-
# Box is fully covered - handle directly
268-
new_box = box.copy()
269-
270-
new_box[2:] = new_box[:2] # collapse to point
271-
new_boxes.append(new_box)
272265
continue
273266

274267
# Find visible coordinates
@@ -278,16 +271,18 @@ def resize_boxes_to_visible_area(
278271
y_coords = np.nonzero(y_visible)[0]
279272
x_coords = np.nonzero(x_visible)[0]
280273

281-
# Create new box
282-
new_box = boxes[i].copy()
274+
# Update only the coordinate part of the box
275+
new_box = box.copy()
283276
new_box[0] = x1[i] + x_coords[0] # x_min
284277
new_box[1] = y1[i] + y_coords[0] # y_min
285278
new_box[2] = x1[i] + x_coords[-1] + 1 # x_max
286279
new_box[3] = y1[i] + y_coords[-1] + 1 # y_max
287280

288281
new_boxes.append(new_box)
289282

290-
return np.array(new_boxes)
283+
# Return empty array with correct shape if all boxes were removed
284+
285+
return np.array(new_boxes) if new_boxes else np.zeros((0, boxes.shape[1]), dtype=boxes.dtype)
291286

292287

293288
def filter_bboxes_by_holes(

tests/functional/test_dropout.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ def test_cutout_various_types_and_fills(dtype, max_value, shape, fill_type):
225225
(
226226
np.array([[5, 5, 8, 8]]),
227227
np.ones((100, 100), dtype=np.uint8), # Full image covered
228-
np.array([[5, 5, 5, 5]]) # Box collapses to point when fully covered
228+
np.zeros((0, 4), dtype=np.int32) # Empty array with correct shape
229229
),
230230
231231
# Test case 3: Box partially covered by hole
@@ -245,7 +245,6 @@ def test_cutout_various_types_and_fills(dtype, max_value, shape, fill_type):
245245
np.zeros((100, 100), dtype=np.uint8).copy(), # Start with all visible
246246
np.array([
247247
[0, 0, 10, 10], # Box 1: unchanged (no hole)
248-
[20, 20, 20, 20], # Box 2: collapsed (fully covered)
249248
[40, 40, 45, 50], # Box 3: width reduced (partially covered)
250249
])
251250
),

tests/test_bbox.py

+82
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
from albumentations.core.composition import BboxParams, Compose, ReplayCompose
2929
from albumentations.core.transforms_interface import BasicTransform, NoOp
3030

31+
from albumentations.augmentations.dropout.functional import resize_boxes_to_visible_area
32+
33+
3134

3235
@pytest.mark.parametrize(
3336
"bboxes, image_shape, expected",
@@ -2388,3 +2391,82 @@ def test_compose_max_accept_ratio_all_formats(bbox_format, bboxes, shape, max_ac
23882391

23892392
result = transform(**data)
23902393
np.testing.assert_array_almost_equal(result["bboxes"], expected, decimal=5)
2394+
2395+
2396+
def test_resize_boxes_to_visible_area_removes_fully_covered_boxes():
2397+
"""Test that resize_boxes_to_visible_area removes boxes with zero visible area."""
2398+
# Create bounding boxes with additional columns for encoded labels
2399+
# Format: [x_min, y_min, x_max, y_max, encoded_label_1, encoded_label_2]
2400+
boxes = np.array([
2401+
[10, 10, 30, 30, 1, 2], # Box 1 with encoded labels 1, 2 - will be fully covered
2402+
[40, 40, 60, 60, 3, 4], # Box 2 with encoded labels 3, 4 - will remain visible
2403+
], dtype=np.float32)
2404+
2405+
# Create a hole mask that completely covers the first box
2406+
hole_mask = np.zeros((100, 100), dtype=np.uint8)
2407+
hole_mask[10:30, 10:30] = 1 # Fully cover first box
2408+
2409+
# Update boxes using the function
2410+
updated_boxes = resize_boxes_to_visible_area(boxes, hole_mask)
2411+
2412+
# Assertions
2413+
assert len(updated_boxes) == 1, "Should remove fully covered boxes"
2414+
assert updated_boxes.shape == (1, 6), "Should preserve the shape with correct number of columns"
2415+
2416+
# The remaining box should be the second one
2417+
np.testing.assert_array_equal(updated_boxes[0, :4], boxes[1, :4], "Second box coordinates should be unchanged")
2418+
assert updated_boxes[0, 4] == 3, "Second box should preserve first encoded label"
2419+
assert updated_boxes[0, 5] == 4, "Second box should preserve second encoded label"
2420+
2421+
def test_resize_boxes_to_visible_area_with_partially_covered_boxes():
2422+
"""Test that resize_boxes_to_visible_area correctly handles partially covered boxes."""
2423+
# Create bounding boxes with additional columns for encoded labels
2424+
boxes = np.array([
2425+
[10, 10, 30, 30, 1, 2], # Box that will be partially covered
2426+
], dtype=np.float32)
2427+
2428+
# Create a hole mask that covers the left half of the box
2429+
hole_mask = np.zeros((100, 100), dtype=np.uint8)
2430+
hole_mask[10:30, 10:20] = 1 # Cover left half of the box
2431+
2432+
# Update boxes using the function
2433+
updated_boxes = resize_boxes_to_visible_area(boxes, hole_mask)
2434+
2435+
# Assertions
2436+
assert len(updated_boxes) == 1, "Should keep partially covered boxes"
2437+
assert updated_boxes.shape == (1, 6), "Should preserve the shape with correct number of columns"
2438+
assert updated_boxes[0, 0] > boxes[0, 0], "X min should increase (left part covered)"
2439+
assert updated_boxes[0, 2] == boxes[0, 2], "X max should remain the same"
2440+
assert updated_boxes[0, 4] == 1, "Should preserve first encoded label"
2441+
assert updated_boxes[0, 5] == 2, "Should preserve second encoded label"
2442+
2443+
def test_resize_boxes_to_visible_area_with_all_boxes_covered():
2444+
"""Test that resize_boxes_to_visible_area returns empty array when all boxes are covered."""
2445+
# Create bounding boxes with additional columns
2446+
boxes = np.array([
2447+
[10, 10, 30, 30, 1, 2], # Box that will be fully covered
2448+
], dtype=np.float32)
2449+
2450+
# Create a hole mask that completely covers the box
2451+
hole_mask = np.zeros((100, 100), dtype=np.uint8)
2452+
hole_mask[10:30, 10:30] = 1 # Fully cover the box
2453+
2454+
# Update boxes using the function
2455+
updated_boxes = resize_boxes_to_visible_area(boxes, hole_mask)
2456+
2457+
# Assertions
2458+
assert len(updated_boxes) == 0, "Should return empty array when all boxes are covered"
2459+
assert updated_boxes.shape == (0, 6), "Empty array should have correct shape with all columns"
2460+
2461+
def test_resize_boxes_to_visible_area_with_empty_input():
2462+
"""Test that resize_boxes_to_visible_area handles empty input correctly."""
2463+
# Empty boxes array with correct shape (0 boxes, 6 columns)
2464+
boxes = np.zeros((0, 6), dtype=np.float32)
2465+
hole_mask = np.zeros((100, 100), dtype=np.uint8)
2466+
2467+
# Update boxes using the function
2468+
updated_boxes = resize_boxes_to_visible_area(boxes, hole_mask)
2469+
2470+
# Assertions
2471+
assert len(updated_boxes) == 0, "Should return empty array"
2472+
assert updated_boxes.shape == (0, 6), "Should preserve the shape with correct number of columns"

0 commit comments

Comments
 (0)