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
244 lines
9.0 KiB
Python
244 lines
9.0 KiB
Python
# ------------------------------------------------------------------------
|
|
# RF-DETR
|
|
# Copyright (c) 2025 Roboflow. All Rights Reserved.
|
|
# Licensed under the Apache License, Version 2.0 [see LICENSE for details]
|
|
# ------------------------------------------------------------------------
|
|
"""COCO benchmark coverage for short keypoint-preview training on a deterministic subset."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import torch
|
|
from torch.utils.data import Subset
|
|
|
|
from rfdetr import RFDETRKeypointPreview
|
|
from rfdetr.config import KeypointTrainConfig
|
|
from rfdetr.training import RFDETRDataModule, RFDETRModelModule, build_trainer
|
|
from rfdetr.utilities.reproducibility import seed_all
|
|
|
|
|
|
def _to_float(value: float | torch.Tensor) -> float:
|
|
return float(value.item()) if isinstance(value, torch.Tensor) else float(value)
|
|
|
|
|
|
def _build_subset_annotations(
|
|
payload: dict,
|
|
image_ids: list[int],
|
|
) -> dict:
|
|
image_id_set = set(image_ids)
|
|
images = [image for image in payload["images"] if int(image["id"]) in image_id_set]
|
|
annotations = [
|
|
annotation
|
|
for annotation in payload["annotations"]
|
|
if int(annotation["image_id"]) in image_id_set
|
|
and int(annotation.get("iscrowd", 0)) == 0
|
|
and int(annotation.get("num_keypoints", 0)) > 0
|
|
]
|
|
categories = [category for category in payload["categories"] if int(category["id"]) == 1]
|
|
return {
|
|
"info": payload.get("info", {}),
|
|
"licenses": payload.get("licenses", []),
|
|
"images": images,
|
|
"annotations": annotations,
|
|
"categories": categories,
|
|
}
|
|
|
|
|
|
def _build_coco_keypoint_subset_from_val(
|
|
*,
|
|
images_root: Path,
|
|
annotations_path: Path,
|
|
output_root: Path,
|
|
train_images: int,
|
|
val_images: int,
|
|
) -> Path:
|
|
with annotations_path.open(encoding="utf-8") as file:
|
|
payload = json.load(file)
|
|
|
|
person_image_ids = sorted(
|
|
{
|
|
int(annotation["image_id"])
|
|
for annotation in payload["annotations"]
|
|
if int(annotation.get("iscrowd", 0)) == 0 and int(annotation.get("num_keypoints", 0)) > 0
|
|
}
|
|
)
|
|
required = train_images + val_images
|
|
if len(person_image_ids) < required:
|
|
raise RuntimeError(f"Need at least {required} keypoint images, found {len(person_image_ids)}.")
|
|
|
|
train_ids = person_image_ids[:train_images]
|
|
val_ids = person_image_ids[train_images : train_images + val_images]
|
|
image_by_id = {int(image["id"]): image for image in payload["images"]}
|
|
|
|
train_dir = output_root / "train2017"
|
|
val_dir = output_root / "val2017"
|
|
annotations_dir = output_root / "annotations"
|
|
train_dir.mkdir(parents=True, exist_ok=True)
|
|
val_dir.mkdir(parents=True, exist_ok=True)
|
|
annotations_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
for image_id in train_ids:
|
|
file_name = str(image_by_id[image_id]["file_name"])
|
|
shutil.copy2(images_root / file_name, train_dir / file_name)
|
|
for image_id in val_ids:
|
|
file_name = str(image_by_id[image_id]["file_name"])
|
|
shutil.copy2(images_root / file_name, val_dir / file_name)
|
|
|
|
train_payload = _build_subset_annotations(payload, train_ids)
|
|
val_payload = _build_subset_annotations(payload, val_ids)
|
|
|
|
train_annotations = annotations_dir / "person_keypoints_train2017.json"
|
|
val_annotations = annotations_dir / "person_keypoints_val2017.json"
|
|
train_annotations.write_text(json.dumps(train_payload), encoding="utf-8")
|
|
val_annotations.write_text(json.dumps(val_payload), encoding="utf-8")
|
|
|
|
return output_root
|
|
|
|
|
|
def _build_subset_datamodule(
|
|
model: RFDETRKeypointPreview,
|
|
train_config: KeypointTrainConfig,
|
|
train_subset_size: int = 8,
|
|
val_subset_size: int = 4,
|
|
) -> RFDETRDataModule:
|
|
datamodule = RFDETRDataModule(model.model_config, train_config)
|
|
datamodule.setup("fit")
|
|
if datamodule._dataset_train is None or datamodule._dataset_val is None:
|
|
raise RuntimeError("Expected both training and validation datasets to be initialized.")
|
|
|
|
train_count = min(train_subset_size, len(datamodule._dataset_train))
|
|
val_count = min(val_subset_size, len(datamodule._dataset_val))
|
|
datamodule._dataset_train = Subset(datamodule._dataset_train, list(range(train_count)))
|
|
datamodule._dataset_val = Subset(datamodule._dataset_val, list(range(val_count)))
|
|
return datamodule
|
|
|
|
|
|
@pytest.mark.gpu
|
|
@pytest.mark.coco17
|
|
@pytest.mark.flaky(reruns=1, only_rerun="AssertionError")
|
|
def test_keypoint_training_subset_reports_loss_and_metric(
|
|
tmp_path: Path,
|
|
download_coco_val_keypoints: tuple[Path, Path],
|
|
) -> None:
|
|
"""Short deterministic fine-tuning should report finite loss and keypoint AP on the fixed subset."""
|
|
seed_all(7)
|
|
images_root, annotations_path = download_coco_val_keypoints
|
|
subset_root = _build_coco_keypoint_subset_from_val(
|
|
images_root=images_root,
|
|
annotations_path=annotations_path,
|
|
output_root=tmp_path / "coco_keypoint_subset",
|
|
train_images=64,
|
|
val_images=16,
|
|
)
|
|
train_config = KeypointTrainConfig(
|
|
dataset_file="coco",
|
|
dataset_dir=str(subset_root),
|
|
output_dir=str(tmp_path / "train_output"),
|
|
epochs=1,
|
|
batch_size=1,
|
|
num_workers=0,
|
|
grad_accum_steps=4,
|
|
use_ema=False,
|
|
run_test=False,
|
|
compute_val_loss=True,
|
|
multi_scale=False,
|
|
expanded_scales=False,
|
|
do_random_resize_via_padding=False,
|
|
tensorboard=False,
|
|
wandb=False,
|
|
mlflow=False,
|
|
clearml=False,
|
|
)
|
|
model = RFDETRKeypointPreview()
|
|
datamodule = _build_subset_datamodule(
|
|
model,
|
|
train_config,
|
|
train_subset_size=8,
|
|
val_subset_size=4,
|
|
)
|
|
|
|
module = RFDETRModelModule(model.model_config, train_config)
|
|
module.model.load_state_dict(model.model.model.state_dict())
|
|
|
|
trainer = build_trainer(
|
|
train_config,
|
|
model.model_config,
|
|
accelerator="gpu",
|
|
limit_train_batches=8,
|
|
limit_val_batches=4,
|
|
num_sanity_val_steps=0,
|
|
)
|
|
(pre_metrics,) = trainer.validate(module, datamodule=datamodule)
|
|
pre_loss = _to_float(pre_metrics["val/loss"])
|
|
pre_map = _to_float(pre_metrics["val/keypoint_map_50_95"])
|
|
assert torch.isfinite(torch.tensor(pre_loss)), f"Expected finite pre-training val/loss, got {pre_loss:.6f}"
|
|
assert torch.isfinite(torch.tensor(pre_map)), f"Expected finite pre-training keypoint AP, got {pre_map:.6f}"
|
|
assert 0.0 <= pre_map <= 1.0, f"Expected pre-training keypoint AP in [0, 1], got {pre_map:.6f}"
|
|
|
|
trainer.fit(module, datamodule=datamodule)
|
|
(post_metrics,) = trainer.validate(module, datamodule=datamodule)
|
|
post_loss = _to_float(post_metrics["val/loss"])
|
|
post_map = _to_float(post_metrics["val/keypoint_map_50_95"])
|
|
assert torch.isfinite(torch.tensor(post_loss)), f"Expected finite post-training val/loss, got {post_loss:.6f}"
|
|
assert torch.isfinite(torch.tensor(post_map)), f"Expected finite post-training keypoint AP, got {post_map:.6f}"
|
|
assert 0.0 <= post_map <= 1.0, f"Expected post-training keypoint AP in [0, 1], got {post_map:.6f}"
|
|
|
|
|
|
@pytest.mark.gpu
|
|
@pytest.mark.coco17
|
|
def test_keypoint_training_full_coco_release_qualification(
|
|
tmp_path: Path,
|
|
download_coco_val_keypoints: tuple[Path, Path],
|
|
) -> None:
|
|
"""Release smoke gate: train and validate keypoint preview on a bounded COCO subset."""
|
|
seed_all(7)
|
|
images_root, annotations_path = download_coco_val_keypoints
|
|
subset_root = _build_coco_keypoint_subset_from_val(
|
|
images_root=images_root,
|
|
annotations_path=annotations_path,
|
|
output_root=tmp_path / "full_coco_keypoint_subset",
|
|
train_images=8,
|
|
val_images=4,
|
|
)
|
|
train_config = KeypointTrainConfig(
|
|
dataset_file="coco",
|
|
dataset_dir=str(subset_root),
|
|
output_dir=str(tmp_path / "full_coco_keypoint_train"),
|
|
epochs=1,
|
|
batch_size=1,
|
|
num_workers=0,
|
|
grad_accum_steps=1,
|
|
use_ema=False,
|
|
run_test=False,
|
|
compute_val_loss=True,
|
|
tensorboard=False,
|
|
wandb=False,
|
|
mlflow=False,
|
|
clearml=False,
|
|
)
|
|
model = RFDETRKeypointPreview()
|
|
datamodule = RFDETRDataModule(model.model_config, train_config)
|
|
module = RFDETRModelModule(model.model_config, train_config)
|
|
module.model.load_state_dict(model.model.model.state_dict())
|
|
|
|
trainer = build_trainer(
|
|
train_config,
|
|
model.model_config,
|
|
accelerator="gpu",
|
|
limit_train_batches=1,
|
|
limit_val_batches=1,
|
|
num_sanity_val_steps=0,
|
|
)
|
|
trainer.fit(module, datamodule=datamodule)
|
|
(metrics,) = trainer.validate(module, datamodule=datamodule)
|
|
|
|
val_loss = _to_float(metrics["val/loss"])
|
|
keypoint_map = _to_float(metrics["val/keypoint_map_50_95"])
|
|
assert torch.isfinite(torch.tensor(val_loss)), f"Expected finite release val/loss, got {val_loss:.6f}"
|
|
assert torch.isfinite(torch.tensor(keypoint_map)), f"Expected finite release keypoint AP, got {keypoint_map:.6f}"
|
|
assert 0.0 <= keypoint_map <= 1.0, f"Expected release keypoint AP in [0, 1], got {keypoint_map:.6f}"
|