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
268 lines
11 KiB
Python
268 lines
11 KiB
Python
# ------------------------------------------------------------------------
|
|
# RF-DETR
|
|
# Copyright (c) 2025 Roboflow. All Rights Reserved.
|
|
# Licensed under the Apache License, Version 2.0 [see LICENSE for details]
|
|
# ------------------------------------------------------------------------
|
|
"""Characterization tests for _build_train_resize_config."""
|
|
|
|
import pytest
|
|
|
|
from rfdetr.datasets.coco import _build_train_resize_config
|
|
|
|
|
|
class TestBuildTrainResizeConfigStructure:
|
|
"""Top-level structure is always a single-element list wrapping a OneOf."""
|
|
|
|
@pytest.mark.parametrize(
|
|
"scales,square",
|
|
[
|
|
pytest.param([640], True, id="square-single"),
|
|
pytest.param([480, 640], True, id="square-multi"),
|
|
pytest.param([640], False, id="nonsquare-single"),
|
|
pytest.param([480, 640], False, id="nonsquare-multi"),
|
|
],
|
|
)
|
|
def test_returns_single_element_list(self, scales, square):
|
|
result = _build_train_resize_config(scales, square=square)
|
|
assert isinstance(result, list)
|
|
assert len(result) == 1
|
|
|
|
@pytest.mark.parametrize(
|
|
"scales,square",
|
|
[
|
|
pytest.param([640], True, id="square-single"),
|
|
pytest.param([480, 640], True, id="square-multi"),
|
|
pytest.param([640], False, id="nonsquare-single"),
|
|
pytest.param([480, 640], False, id="nonsquare-multi"),
|
|
],
|
|
)
|
|
def test_top_level_is_oneof_with_two_branches(self, scales, square):
|
|
result = _build_train_resize_config(scales, square=square)
|
|
entry = result[0]
|
|
assert "OneOf" in entry
|
|
oneof = entry["OneOf"]
|
|
assert len(oneof["transforms"]) == 2
|
|
|
|
|
|
class TestBuildTrainResizeConfigSquareSingleScale:
|
|
"""Square=True, single scale — OneOf[Resize] + Sequential[..., OneOf[RandomSizedCrop]]."""
|
|
|
|
def test_option_a_is_oneof_wrapping_single_resize(self):
|
|
result = _build_train_resize_config([640], square=True)
|
|
option_a = result[0]["OneOf"]["transforms"][0]
|
|
assert option_a == {
|
|
"OneOf": {
|
|
"transforms": [{"Resize": {"height": 640, "width": 640}}],
|
|
}
|
|
}
|
|
|
|
def test_option_b_is_sequential_with_oneof_crop(self):
|
|
result = _build_train_resize_config([640], square=True)
|
|
option_b = result[0]["OneOf"]["transforms"][1]
|
|
assert option_b == {
|
|
"Sequential": {
|
|
"transforms": [
|
|
{"SmallestMaxSize": {"max_size": [400, 500, 600]}},
|
|
{
|
|
"OneOf": {
|
|
"transforms": [
|
|
{"RandomSizedCrop": {"min_max_height": [384, 600], "height": 640, "width": 640}},
|
|
],
|
|
}
|
|
},
|
|
]
|
|
}
|
|
}
|
|
|
|
def test_uses_correct_scale_value(self):
|
|
result = _build_train_resize_config([480], square=True)
|
|
option_a = result[0]["OneOf"]["transforms"][0]
|
|
assert option_a == {
|
|
"OneOf": {
|
|
"transforms": [{"Resize": {"height": 480, "width": 480}}],
|
|
}
|
|
}
|
|
|
|
|
|
class TestBuildTrainResizeConfigSquareMultiScale:
|
|
"""Square=True, multiple scales — OneOf[Resize] + Sequential[..., OneOf[RandomSizedCrop]]."""
|
|
|
|
def test_option_a_is_oneof_of_resizes(self):
|
|
result = _build_train_resize_config([480, 640], square=True)
|
|
option_a = result[0]["OneOf"]["transforms"][0]
|
|
assert option_a == {
|
|
"OneOf": {
|
|
"transforms": [
|
|
{"Resize": {"height": 480, "width": 480}},
|
|
{"Resize": {"height": 640, "width": 640}},
|
|
],
|
|
}
|
|
}
|
|
|
|
def test_option_b_is_sequential_with_oneof_crop(self):
|
|
result = _build_train_resize_config([480, 640], square=True)
|
|
option_b = result[0]["OneOf"]["transforms"][1]
|
|
assert option_b == {
|
|
"Sequential": {
|
|
"transforms": [
|
|
{"SmallestMaxSize": {"max_size": [400, 500, 600]}},
|
|
{
|
|
"OneOf": {
|
|
"transforms": [
|
|
{"RandomSizedCrop": {"min_max_height": [384, 600], "height": 480, "width": 480}},
|
|
{"RandomSizedCrop": {"min_max_height": [384, 600], "height": 640, "width": 640}},
|
|
],
|
|
}
|
|
},
|
|
]
|
|
}
|
|
}
|
|
|
|
def test_three_scales_produce_three_resize_options(self):
|
|
result = _build_train_resize_config([384, 512, 640], square=True)
|
|
option_a = result[0]["OneOf"]["transforms"][0]
|
|
assert len(option_a["OneOf"]["transforms"]) == 3
|
|
|
|
|
|
class TestBuildTrainResizeConfigNonSquareSingleScale:
|
|
"""Square=False, single scale — SmallestMaxSize uses scalar, default cap 1333."""
|
|
|
|
def test_option_a_uses_scalar_size(self):
|
|
result = _build_train_resize_config([640], square=False)
|
|
option_a = result[0]["OneOf"]["transforms"][0]
|
|
assert option_a == {
|
|
"Sequential": {
|
|
"transforms": [
|
|
{"SmallestMaxSize": {"max_size": 640}},
|
|
{"LongestMaxSize": {"max_size": 1333}},
|
|
]
|
|
}
|
|
}
|
|
|
|
def test_option_b_uses_scalar_size(self):
|
|
result = _build_train_resize_config([640], square=False)
|
|
option_b = result[0]["OneOf"]["transforms"][1]
|
|
assert option_b == {
|
|
"Sequential": {
|
|
"transforms": [
|
|
{"SmallestMaxSize": {"max_size": [400, 500, 600]}},
|
|
{
|
|
"OneOf": {
|
|
"transforms": [
|
|
{"RandomSizedCrop": {"min_max_height": [384, 600], "height": 640, "width": 640}},
|
|
]
|
|
}
|
|
},
|
|
]
|
|
}
|
|
}
|
|
|
|
def test_custom_max_size(self):
|
|
result = _build_train_resize_config([640], square=False, max_size=800)
|
|
option_a = result[0]["OneOf"]["transforms"][0]
|
|
assert option_a["Sequential"]["transforms"][1] == {"LongestMaxSize": {"max_size": 800}}
|
|
|
|
|
|
class TestBuildTrainResizeConfigNonSquareMultiScale:
|
|
"""Square=False, multiple scales — SmallestMaxSize uses list directly."""
|
|
|
|
def test_option_a_uses_list_size(self):
|
|
result = _build_train_resize_config([480, 640], square=False)
|
|
option_a = result[0]["OneOf"]["transforms"][0]
|
|
assert option_a == {
|
|
"Sequential": {
|
|
"transforms": [
|
|
{"SmallestMaxSize": {"max_size": [480, 640]}},
|
|
{"LongestMaxSize": {"max_size": 1333}},
|
|
]
|
|
}
|
|
}
|
|
|
|
def test_option_b_uses_list_size(self):
|
|
result = _build_train_resize_config([480, 640], square=False)
|
|
option_b = result[0]["OneOf"]["transforms"][1]
|
|
assert option_b == {
|
|
"Sequential": {
|
|
"transforms": [
|
|
{"SmallestMaxSize": {"max_size": [400, 500, 600]}},
|
|
{
|
|
"OneOf": {
|
|
"transforms": [
|
|
{"RandomSizedCrop": {"min_max_height": [384, 600], "height": 480, "width": 480}},
|
|
{"RandomSizedCrop": {"min_max_height": [384, 600], "height": 640, "width": 640}},
|
|
]
|
|
}
|
|
},
|
|
]
|
|
}
|
|
}
|
|
|
|
def test_custom_max_size_applies_to_option_a_only(self):
|
|
"""max_size caps option_a's long side; option_b now resizes the crop directly to the target (no cap step)."""
|
|
result = _build_train_resize_config([480, 640], square=False, max_size=1000)
|
|
option_a = result[0]["OneOf"]["transforms"][0]
|
|
option_b_steps = result[0]["OneOf"]["transforms"][1]["Sequential"]["transforms"]
|
|
assert option_a["Sequential"]["transforms"][1] == {"LongestMaxSize": {"max_size": 1000}}
|
|
assert not any("LongestMaxSize" in step for step in option_b_steps)
|
|
|
|
|
|
class TestBuildTrainResizeConfigNonSquareScaleJitter:
|
|
"""Non-square option_b must keep RandomSizedCrop (scale jitter), not a fixed RandomCrop.
|
|
|
|
Regression tests for https://github.com/roboflow/rf-detr/issues/1018 — PR #752 replaced RandomSizeCrop(384, 600)
|
|
with a fixed RandomCrop(384, 384), silently removing scale jitter from the non-square training pipeline.
|
|
|
|
The ``fix-resize-crop`` branch keeps RandomSizedCrop and removes the wasteful fixed-384 intermediate hop: the crop
|
|
now resizes directly to the target scale (per-scale ``OneOf``, mirroring the square path). ``min_max_height`` uses
|
|
``[384, 600]`` to match the full SmallestMaxSize range — when the image short side is 400, albumentations clamps
|
|
the crop to the image height (a full-image crop), which is the original DETR recipe behaviour and preserves
|
|
zoom-out diversity across the SmallestMaxSize range.
|
|
"""
|
|
|
|
@pytest.mark.parametrize(
|
|
"scales",
|
|
[
|
|
pytest.param([640], id="nonsquare-single"),
|
|
pytest.param([480, 640], id="nonsquare-multi"),
|
|
],
|
|
)
|
|
def test_option_b_crop_step_uses_random_sized_crop(self, scales):
|
|
"""Non-square option_b crop must use RandomSizedCrop, never fixed RandomCrop (issue #1018)."""
|
|
result = _build_train_resize_config(scales, square=False)
|
|
option_b = result[0]["OneOf"]["transforms"][1]
|
|
crop_step = option_b["Sequential"]["transforms"][1]
|
|
crop_variants = crop_step["OneOf"]["transforms"]
|
|
assert crop_variants and all(
|
|
"RandomSizedCrop" in entry and "RandomCrop" not in entry for entry in crop_variants
|
|
)
|
|
|
|
@pytest.mark.parametrize(
|
|
"scales",
|
|
[
|
|
pytest.param([640], id="nonsquare-single"),
|
|
pytest.param([480, 640], id="nonsquare-multi"),
|
|
],
|
|
)
|
|
def test_option_b_crop_uses_full_scale_jitter_range(self, scales):
|
|
"""RandomSizedCrop min_max_height matches SmallestMaxSize range [384, 600] for full zoom-out diversity."""
|
|
result = _build_train_resize_config(scales, square=False)
|
|
option_b = result[0]["OneOf"]["transforms"][1]
|
|
crop_variants = option_b["Sequential"]["transforms"][1]["OneOf"]["transforms"]
|
|
assert all(entry["RandomSizedCrop"]["min_max_height"] == [384, 600] for entry in crop_variants)
|
|
|
|
@pytest.mark.parametrize(
|
|
"scales,square",
|
|
[
|
|
pytest.param([640], True, id="square-single"),
|
|
pytest.param([480, 640], True, id="square-multi"),
|
|
],
|
|
)
|
|
def test_square_option_b_unchanged(self, scales, square):
|
|
"""Square path must still use RandomSizedCrop parameterized by scale."""
|
|
result = _build_train_resize_config(scales, square=square)
|
|
option_b = result[0]["OneOf"]["transforms"][1]
|
|
inner_transforms = option_b["Sequential"]["transforms"][1]["OneOf"]["transforms"]
|
|
for entry in inner_transforms:
|
|
assert "RandomSizedCrop" in entry
|
|
assert entry["RandomSizedCrop"]["min_max_height"] == [384, 600]
|