Files
roboflow--rf-detr/tests/models/test_transformer_keypoints.py
T
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

251 lines
9.6 KiB
Python

# ------------------------------------------------------------------------
# RF-DETR
# Copyright (c) 2025 Roboflow. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 [see LICENSE for details]
# ------------------------------------------------------------------------
"""Regression tests for GroupPose-oriented transformer streams."""
from types import SimpleNamespace
import torch
from torch import nn
from rfdetr.models.transformer import Transformer, TransformerDecoder, TransformerDecoderLayer, build_transformer
def _build_transformer_inputs(
batch_size: int = 2,
hidden_dim: int = 16,
num_levels: int = 2,
) -> tuple[list[torch.Tensor], list[torch.Tensor], list[torch.Tensor], torch.Tensor, torch.Tensor]:
"""Build minimal synthetic multi-scale inputs for `Transformer.forward`.
Args:
batch_size: Mini-batch size.
hidden_dim: Transformer and input channel size.
num_levels: Number of feature pyramid levels.
Returns:
srcs, masks, pos_embeds, refpoint_embed, query_feat.
"""
spatial_shapes = [(4, 4), (2, 2)]
srcs = [
torch.randn(batch_size, hidden_dim, spatial_shapes[idx][0], spatial_shapes[idx][1]) for idx in range(num_levels)
]
masks = [torch.zeros(batch_size, h, w, dtype=torch.bool) for h, w in spatial_shapes[:num_levels]]
pos_embeds = [
torch.randn(batch_size, hidden_dim, spatial_shapes[idx][0], spatial_shapes[idx][1]) for idx in range(num_levels)
]
refpoint_embed = torch.rand(6, 4)
query_feat = torch.randn(6, hidden_dim)
return srcs, masks, pos_embeds, refpoint_embed, query_feat
def test_transformer_keypoint_disabled_matches_default_contract() -> None:
"""Transformer without GroupPose should keep the 4-item return contract."""
srcs, masks, pos_embeds, refpoint_embed, query_feat = _build_transformer_inputs()
transformer = Transformer(
d_model=16,
num_queries=6,
num_decoder_layers=1,
sa_nhead=4,
ca_nhead=4,
num_feature_levels=2,
dec_n_points=1,
return_intermediate_dec=True,
lite_refpoint_refine=True,
use_grouppose_keypoints=False,
)
outputs = transformer(srcs, masks, pos_embeds, refpoint_embed, query_feat, cross_attn_srcs=None)
assert len(outputs) == 4, f"Expected 4 outputs, got {len(outputs)}"
hs, references, memory_ts, boxes_ts = outputs
assert hs is not None and references is not None
assert memory_ts is None and boxes_ts is None
def test_transformer_keypoint_enabled_shapes() -> None:
"""GroupPose path should emit keypoint hidden states and encoder keypoint slots."""
srcs, masks, pos_embeds, refpoint_embed, query_feat = _build_transformer_inputs()
transformer = Transformer(
d_model=16,
num_queries=6,
num_decoder_layers=1,
sa_nhead=4,
ca_nhead=4,
num_feature_levels=2,
dec_n_points=1,
return_intermediate_dec=True,
lite_refpoint_refine=True,
two_stage=True,
use_grouppose_keypoints=True,
num_keypoints_per_class=[17],
)
transformer.enc_out_class_embed = nn.ModuleList([nn.Linear(16, 2)])
transformer.enc_out_bbox_embed = nn.ModuleList([nn.Linear(16, 4)])
outputs = transformer(srcs, masks, pos_embeds, refpoint_embed, query_feat, cross_attn_srcs=None)
assert len(outputs) == 7, f"Expected 7 outputs, got {len(outputs)}"
hs, references, memory_ts, boxes_ts, keypoint_hs, enc_kp_predictions, keypoint_memory_ts = outputs
assert isinstance(hs, torch.Tensor)
assert hs.shape[-1] == 16
assert references.shape[-1] == 4
assert memory_ts is not None and boxes_ts is not None
assert keypoint_hs is not None
assert keypoint_hs.shape[3] == 17
assert enc_kp_predictions is not None
assert enc_kp_predictions.shape[-1] == 16
assert keypoint_memory_ts is not None
def test_build_transformer_defaults_inter_instance_keypoint_attention_to_config_default() -> None:
"""Older args objects without `inter_instance_kp_attn` should keep the preview topology default."""
args = SimpleNamespace(
hidden_dim=16,
sa_nheads=4,
ca_nheads=4,
num_queries=6,
dropout=0.0,
dim_feedforward=32,
dec_layers=1,
group_detr=1,
num_feature_levels=2,
dec_n_points=1,
lite_refpoint_refine=True,
decoder_norm="LN",
bbox_reparam=False,
use_grouppose_keypoints=True,
num_keypoints_per_class=[17],
)
transformer = build_transformer(args)
decoder_layer = transformer.decoder.layers[0]
assert isinstance(decoder_layer, TransformerDecoderLayer)
assert decoder_layer.enable_keypoint_processing
assert not decoder_layer.inter_instance_kp_attn
def test_keypoint_class_mask_person_only() -> None:
"""Person-only schema `[17]` should build a keypoint class mask with only self-class tokens."""
layer = TransformerDecoderLayer(
d_model=16,
sa_nhead=4,
ca_nhead=4,
dim_feedforward=32,
num_feature_levels=2,
enable_keypoint_processing=True,
grouppose_keypoint_dim_downscale=1,
keypoint_cross_attn=False,
inter_instance_kp_attn=False,
)
decoder = TransformerDecoder(
decoder_layer=layer,
num_layers=1,
return_intermediate=True,
d_model=16,
lite_refpoint_refine=True,
enable_keypoint_processing=True,
num_keypoints_per_class=[17],
grouppose_keypoint_dim_downscale=1,
)
assert decoder.keypoint_pos_embed is not None
assert decoder.keypoint_class_mask.shape == (18, 18)
assert decoder.keypoint_class_mask.dtype == torch.bool
assert not decoder.keypoint_class_mask.any()
def test_enc_keypoint_embed_eval_uses_only_head_zero() -> None:
"""Encoder keypoint path must use a single head (head 0) in eval mode.
Regression: ``group_detr = len(self.enc_out_keypoint_embed)`` without a
``self.training`` guard caused eval mode to split ``num_queries`` across all
group heads instead of routing every query through head 0. Fix:
``group_detr = len(...) if self.training else 1``.
Strategy: zero all head weights/biases; set head-0 last-layer bias to
``sentinel``. In eval mode every query routes through head 0, so
``kp_pred[..., 2:]`` (the pure-delta dims unaffected by ref_xy/wh) must all
equal ``sentinel``. In training mode only the first 1/group_detr queries go
through head 0 (the rest equal 0.0).
"""
group_detr = 3
num_queries = 6 # divisible by group_detr
hidden_dim = 16
batch_size = 1
sentinel = 50.0
srcs, masks, pos_embeds, _, _ = _build_transformer_inputs(batch_size=batch_size, hidden_dim=hidden_dim)
refpoint_embed = torch.rand(num_queries, 4)
query_feat = torch.randn(num_queries, hidden_dim)
transformer = Transformer(
d_model=hidden_dim,
num_queries=num_queries,
num_decoder_layers=1,
sa_nhead=4,
ca_nhead=4,
num_feature_levels=2,
dec_n_points=1,
return_intermediate_dec=True,
lite_refpoint_refine=True,
two_stage=True,
group_detr=group_detr,
use_grouppose_keypoints=True,
num_keypoints_per_class=[2],
)
transformer.enc_out_class_embed = nn.ModuleList([nn.Linear(hidden_dim, 2) for _ in range(group_detr)])
transformer.enc_out_bbox_embed = nn.ModuleList([nn.Linear(hidden_dim, 4) for _ in range(group_detr)])
# Zero all keypoint head weights and biases; give head 0 a distinctive output.
with torch.no_grad():
for _, head in enumerate(transformer.enc_out_keypoint_embed):
for layer in head.layers:
layer.weight.zero_()
layer.bias.zero_()
transformer.enc_out_keypoint_embed[0].layers[-1].bias.fill_(sentinel)
transformer.eval()
with torch.no_grad():
outputs = transformer(srcs, masks, pos_embeds, refpoint_embed, query_feat, cross_attn_srcs=None)
_, _, _, _, _, enc_kp_predictions, _ = outputs
assert enc_kp_predictions is not None, "enc_kp_predictions should not be None in keypoint mode"
# kp_pred = [kp_xy(2 dims), kp_delta[2:]]; dims 2: are pure MLP output unaffected by ref_xy/wh.
kp_beyond_xy = enc_kp_predictions[..., 2:]
assert (kp_beyond_xy == sentinel).all(), (
f"Eval mode must route all {num_queries} queries through head 0 (bias={sentinel}). "
f"Got min={kp_beyond_xy.min().item():.2f}, max={kp_beyond_xy.max().item():.2f}. "
"Bug: group_detr not guarded by self.training in enc_out_keypoint_embed loop."
)
def test_cross_attn_srcs_none_backward_compat() -> None:
"""`cross_attn_srcs=None` must remain equivalent to passing the primary feature stream."""
srcs, masks, pos_embeds, refpoint_embed, query_feat = _build_transformer_inputs()
transformer = Transformer(
d_model=16,
num_queries=6,
num_decoder_layers=1,
sa_nhead=4,
ca_nhead=4,
num_feature_levels=2,
dec_n_points=1,
return_intermediate_dec=True,
lite_refpoint_refine=True,
use_grouppose_keypoints=False,
)
outputs_default = transformer(srcs, masks, pos_embeds, refpoint_embed, query_feat, cross_attn_srcs=None)
outputs_explicit = transformer(srcs, masks, pos_embeds, refpoint_embed, query_feat, cross_attn_srcs=srcs)
assert len(outputs_default) == len(outputs_explicit) == 4
for default_part, explicit_part in zip(outputs_default, outputs_explicit):
if default_part is None:
assert explicit_part is None
else:
torch.testing.assert_close(default_part, explicit_part)