Files
wehub-resource-sync 16031aae96
CPU tests Workflow / Testing (ubuntu-latest, 3.12) (push) Failing after 1s
CPU tests Workflow / Testing (ubuntu-latest, 3.13) (push) Failing after 0s
Mypy Type Check / Type Check (push) Failing after 0s
Docs/Test WorkFlow / Test docs build (push) Failing after 1s
PR Conflict Labeler / labeling (push) Failing after 1s
Dependency resolution / Resolve [tflite] extra — Python 3.12 (push) Failing after 0s
Smoke Tests / try-all-models (ubuntu-latest, 3.10) (push) Failing after 0s
Smoke Tests / try-all-models (ubuntu-latest, 3.13) (push) Failing after 1s
CPU tests Workflow / build-pkg (push) Failing after 1s
CPU tests Workflow / Testing (ubuntu-latest, 3.10) (push) Failing after 0s
CPU tests Workflow / Testing (ubuntu-latest, 3.11) (push) Failing after 0s
Smoke Tests / try-all-models (macos-latest, 3.10) (push) Has been cancelled
Smoke Tests / try-all-models (macos-latest, 3.13) (push) Has been cancelled
Smoke Tests / try-all-models (windows-latest, 3.10) (push) Has been cancelled
Smoke Tests / try-all-models (windows-latest, 3.13) (push) Has been cancelled
CPU tests Workflow / Testing (macos-latest, 3.10) (push) Has been cancelled
CPU tests Workflow / Testing (macos-latest, 3.13) (push) Has been cancelled
CPU tests Workflow / Testing (windows-latest, 3.10) (push) Has been cancelled
CPU tests Workflow / Testing (windows-latest, 3.13) (push) Has been cancelled
CPU tests Workflow / testing-guardian (push) Has been cancelled
GPU tests Workflow / Testing (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:26:24 +08:00

171 lines
7.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ------------------------------------------------------------------------
# RF-DETR
# Copyright (c) 2025 Roboflow. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 [see LICENSE for details]
# ------------------------------------------------------------------------
"""Tests for PostProcess box clamping behaviour."""
import pytest
import torch
from rfdetr.models.postprocess import PostProcess
class TestGatherAndScaleBoxes:
"""Tests for :meth:`PostProcess._gather_and_scale_boxes`."""
def test_clamps_boxes_to_image_bounds(self):
"""Boxes that extrapolate beyond [0, 1] in normalized space are clamped to pixel-space image dimensions after
scaling."""
# Three synthetic boxes in cxcywh normalized coords:
# [0] cx=0.01, w=0.10 → x1 = (0.01 - 0.05) * 640 = -25.6 ← negative
# [1] cx=0.99, w=0.10 → x2 = (0.99 + 0.05) * 640 = 665.6 ← overflow
# [2] cx=0.50, w=0.20 → fully in-bounds
out_bbox = torch.tensor(
[
[
[0.01, 0.01, 0.10, 0.10], # negative x1, y1 after scale
[0.99, 0.99, 0.10, 0.10], # x2 > img_w, y2 > img_h after scale
[0.50, 0.50, 0.20, 0.20], # in-bounds control
]
]
) # shape (B=1, Q=3, 4)
topk_boxes = torch.tensor([[0, 1, 2]]) # select all three
target_sizes = torch.tensor([[480, 640]]) # (h, w)
boxes = PostProcess._gather_and_scale_boxes(out_bbox, topk_boxes, target_sizes)
img_h, img_w = 480, 640
# All coords must be >= 0
assert (boxes >= 0).all(), f"Negative coords present: {boxes[boxes < 0]}"
# x1, x2 must be <= image width
assert (boxes[..., 0] <= img_w).all()
assert (boxes[..., 2] <= img_w).all()
# y1, y2 must be <= image height
assert (boxes[..., 1] <= img_h).all()
assert (boxes[..., 3] <= img_h).all()
# Exact clamped values — bounds-only check cannot catch a clamp returning e.g. 1.0 instead of 0.0
# box [0]: x1_raw=-25.6, y1_raw=-19.2 → clamped to 0.0
assert boxes[0, 0, 0].item() == pytest.approx(0.0), "x1 of underflowing box must clamp to 0"
assert boxes[0, 0, 1].item() == pytest.approx(0.0), "y1 of underflowing box must clamp to 0"
# box [1]: x2_raw=665.6 → clamped to img_w=640.0; y2_raw=499.2 → clamped to img_h=480.0
assert boxes[0, 1, 2].item() == pytest.approx(640.0), "x2 of overflowing box must clamp to img_w"
assert boxes[0, 1, 3].item() == pytest.approx(480.0), "y2 of overflowing box must clamp to img_h"
def test_in_bounds_boxes_unchanged(self):
"""Boxes already within image bounds are not altered by clamping."""
out_bbox = torch.tensor(
[
[
[0.30, 0.30, 0.20, 0.20],
[0.70, 0.60, 0.30, 0.40],
]
]
)
topk_boxes = torch.tensor([[0, 1]])
target_sizes = torch.tensor([[480, 640]])
boxes = PostProcess._gather_and_scale_boxes(out_bbox, topk_boxes, target_sizes)
# Manually computed expected values (no clamping needed)
expected = torch.tensor(
[
[
[128.0, 96.0, 256.0, 192.0], # cx=0.30,cy=0.30,w=0.20,h=0.20
[352.0, 192.0, 544.0, 384.0], # cx=0.70,cy=0.60,w=0.30,h=0.40
]
]
)
assert torch.allclose(boxes, expected, atol=1e-4), (
f"In-bounds boxes were altered.\nExpected:\n{expected}\nGot:\n{boxes}"
)
def test_multiple_images_in_batch(self):
"""Clamping works correctly across a batch of mixed image sizes."""
out_bbox = torch.tensor(
[
[
[0.01, 0.50, 0.10, 0.20], # image 0: negative x1
],
[
[0.99, 0.50, 0.10, 0.20], # image 1: x2 overflow
],
]
)
topk_boxes = torch.tensor([[0], [0]])
target_sizes = torch.tensor(
[
[300, 400], # image 0: 400×300
[600, 800], # image 1: 800×600
]
)
boxes = PostProcess._gather_and_scale_boxes(out_bbox, topk_boxes, target_sizes)
# Image 0: all coords must be in [0, 400]×[0, 300]
assert (boxes[0, :, 0] >= 0).all(), "img0 x1: expected >= 0"
assert (boxes[0, :, 0] <= 400).all(), "img0 x1: expected <= img_w (400)"
assert (boxes[0, :, 1] >= 0).all(), "img0 y1: expected >= 0"
assert (boxes[0, :, 1] <= 300).all(), "img0 y1: expected <= img_h (300)"
assert (boxes[0, :, 2] >= 0).all(), "img0 x2: expected >= 0"
assert (boxes[0, :, 2] <= 400).all(), "img0 x2: expected <= img_w (400)"
assert (boxes[0, :, 3] >= 0).all(), "img0 y2: expected >= 0"
assert (boxes[0, :, 3] <= 300).all(), "img0 y2: expected <= img_h (300)"
# Image 1: all coords must be in [0, 800]×[0, 600]
assert (boxes[1, :, 0] >= 0).all(), "img1 x1: expected >= 0"
assert (boxes[1, :, 0] <= 800).all(), "img1 x1: expected <= img_w (800)"
assert (boxes[1, :, 1] >= 0).all(), "img1 y1: expected >= 0"
assert (boxes[1, :, 1] <= 600).all(), "img1 y1: expected <= img_h (600)"
assert (boxes[1, :, 2] >= 0).all(), "img1 x2: expected >= 0"
assert (boxes[1, :, 2] <= 800).all(), "img1 x2: expected <= img_w (800)"
assert (boxes[1, :, 3] >= 0).all(), "img1 y2: expected >= 0"
assert (boxes[1, :, 3] <= 600).all(), "img1 y2: expected <= img_h (600)"
class TestPostProcessForward:
"""Integration tests for :meth:`PostProcess.forward`."""
def test_forward_clamps_edge_boxes_to_bounds(self):
"""PostProcess.forward returns non-negative in-bounds boxes for edge-hugging predictions."""
postprocess = PostProcess(num_select=2)
outputs = {
"pred_logits": torch.tensor([[[10.0, -10.0], [9.0, -10.0]]]),
"pred_boxes": torch.tensor([[[0.01, 0.01, 0.10, 0.10], [0.99, 0.99, 0.10, 0.10]]]),
}
target_sizes = torch.tensor([[480, 640]])
results = postprocess(outputs, target_sizes)
boxes = results[0]["boxes"]
assert (boxes >= 0).all(), f"Negative coords present: {boxes[boxes < 0]}"
assert (boxes[..., 0] <= 640).all(), "x1 exceeds img_w (640)"
assert (boxes[..., 2] <= 640).all(), "x2 exceeds img_w (640)"
assert (boxes[..., 1] <= 480).all(), "y1 exceeds img_h (480)"
assert (boxes[..., 3] <= 480).all(), "y2 exceeds img_h (480)"
class TestPostProcessMasks:
"""Tests for :meth:`PostProcess._postprocess_masks` mask resizing."""
def test_chunked_upsample_preserves_shape_for_large_k(self):
"""Chunked upsampling of K=64 masks returns full-resolution boolean masks of shape [K, 1, H, W]."""
batch, num_queries, mask_h, mask_w = 1, 64, 16, 16
num_select, img_h, img_w = 64, 512, 512
out_masks = torch.randn(batch, num_queries, mask_h, mask_w)
scores = torch.rand(batch, num_select)
labels = torch.zeros(batch, num_select, dtype=torch.long)
boxes = torch.zeros(batch, num_select, 4)
topk_boxes = torch.arange(num_select).unsqueeze(0)
target_sizes = torch.tensor([[img_h, img_w]])
results = PostProcess._postprocess_masks(out_masks, scores, labels, boxes, topk_boxes, target_sizes)
masks = results[0]["masks"]
assert masks.shape == (num_select, 1, img_h, img_w)
assert masks.dtype == torch.bool