@@ -62,6 +62,7 @@ def test_segment(self):
6262
6363 def test_pose (self ):
6464 self .assertEqual (task_of_model_type ("yolov11-pose" ), TASK_POSE )
65+ self .assertEqual (task_of_model_type ("rfdetr-keypoint-preview" ), TASK_POSE )
6566
6667 def test_classify (self ):
6768 self .assertEqual (task_of_model_type ("yolov11-cls" ), TASK_CLS )
@@ -102,11 +103,19 @@ def test_detection_model_names(self):
102103 for name in ("RFDETRNano" , "RFDETRSmall" , "RFDETRMedium" , "RFDETRLarge" , "RFDETRXLarge" ):
103104 self .assertEqual (_detect_rfdetr_task ({"model_name" : name }), TASK_DET , name )
104105
105- def test_keypoint_model_name_returns_pose (self ):
106- # Keypoint checkpoints are unsupported; classifying them as pose lets the
107- # model_type task check reject them instead of uploading them as detection.
106+ def test_keypoint_model_names (self ):
108107 self .assertEqual (_detect_rfdetr_task ({"model_name" : "RFDETRKeypointPreview" }), TASK_POSE )
109108
109+ def test_keypoint_args_fallback (self ):
110+ # The deploy bundle from export_for_roboflow carries `args` but not
111+ # `model_name`; a non-empty `num_keypoints_per_class` marks a keypoint model.
112+ self .assertEqual (_detect_rfdetr_task ({"args" : SimpleNamespace (num_keypoints_per_class = [0 , 17 ])}), TASK_POSE )
113+ self .assertEqual (_detect_rfdetr_task ({"args" : {"num_keypoints_per_class" : [0 , 17 ]}}), TASK_POSE )
114+ # Empty / absent keypoint schema must NOT be treated as a keypoint model.
115+ self .assertEqual (
116+ _detect_rfdetr_task ({"args" : {"num_keypoints_per_class" : [], "segmentation_head" : False }}), TASK_DET
117+ )
118+
110119 def test_segmentation_head_fallback (self ):
111120 # Roboflow-hosted rf-detr .pt downloads lack `model_name` but always carry
112121 # `args.segmentation_head`. Cover both namespace and dict shapes.
@@ -454,6 +463,37 @@ def test_rfdetr_falls_back_to_discovered_checkpoint(self):
454463 finally :
455464 bundle .cleanup ()
456465
466+ def test_rfdetr_keypoint_exported_checkpoint_packages (self ):
467+ # The primary feature: an exported keypoint deploy-checkpoint (non-PTL shape,
468+ # args carries class_names + num_keypoints_per_class) packages successfully as
469+ # 'rfdetr-keypoint-preview'. num_keypoints_per_class marks it pose, which matches
470+ # the model_type's task, so it passes the task check and copies weights.pt.
471+ with tempfile .TemporaryDirectory () as tmp :
472+ model_dir = Path (tmp )
473+ (model_dir / "weights.pt" ).write_bytes (b"checkpoint" )
474+ torch = _fake_torch ({"args" : {"class_names" : ["goal" ], "num_keypoints_per_class" : [0 , 17 ]}})
475+ with _import_patch ({"torch" : torch }):
476+ bundle = package_custom_weights ("rfdetr-keypoint-preview" , str (model_dir ), filename = "weights.pt" )
477+ try :
478+ self .assertEqual (bundle .model_type , "rfdetr-keypoint-preview" )
479+ with zipfile .ZipFile (bundle .archive_path ) as archive :
480+ names = archive .namelist ()
481+ self .assertIn ("weights.pt" , names )
482+ self .assertIn ("class_names.txt" , names )
483+ finally :
484+ bundle .cleanup ()
485+
486+ def test_rfdetr_keypoint_checkpoint_rejected_as_detection_type (self ):
487+ # The same keypoint checkpoint uploaded under a detection model_type is a task
488+ # mismatch (pose != detect) and must be rejected, not silently packaged.
489+ with tempfile .TemporaryDirectory () as tmp :
490+ model_dir = Path (tmp )
491+ (model_dir / "weights.pt" ).write_bytes (b"checkpoint" )
492+ torch = _fake_torch ({"args" : {"class_names" : ["goal" ], "num_keypoints_per_class" : [0 , 17 ]}})
493+ with _import_patch ({"torch" : torch }):
494+ with self .assertRaises (TaskMismatchError ):
495+ package_custom_weights ("rfdetr-base" , str (model_dir ), filename = "weights.pt" )
496+
457497 def test_rfdetr_without_any_checkpoint_raises (self ):
458498 with tempfile .TemporaryDirectory () as tmp :
459499 torch = _fake_torch ({})
@@ -824,6 +864,7 @@ class RfdetrModelTypeToClassTest(unittest.TestCase):
824864 def test_representative_mappings (self ):
825865 self .assertEqual (_RFDETR_MODEL_TYPE_TO_CLASS ["rfdetr-seg-medium" ], "RFDETRSegMedium" )
826866 self .assertEqual (_RFDETR_MODEL_TYPE_TO_CLASS ["rfdetr-base" ], "RFDETRBase" )
867+ self .assertEqual (_RFDETR_MODEL_TYPE_TO_CLASS ["rfdetr-keypoint-preview" ], "RFDETRKeypointPreview" )
827868
828869 def test_keys_are_rfdetr_types_and_values_are_class_names (self ):
829870 for model_type , class_name in _RFDETR_MODEL_TYPE_TO_CLASS .items ():
@@ -878,8 +919,10 @@ def __init__(self, *, pretrain_weights):
878919 module = SimpleNamespace ()
879920 module .RFDETR = _RFDETR
880921 # The fallback resolves the subclass by name via _RFDETR_MODEL_TYPE_TO_CLASS,
881- # e.g. "rfdetr-seg-medium" -> getattr(rfdetr, "RFDETRSegMedium").
922+ # e.g. "rfdetr-seg-medium" -> getattr(rfdetr, "RFDETRSegMedium") and
923+ # "rfdetr-keypoint-preview" -> getattr(rfdetr, "RFDETRKeypointPreview").
882924 module .RFDETRSegMedium = _SizedModel
925+ module .RFDETRKeypointPreview = _SizedModel
883926 if capabilities :
884927 _RFDETR .export_for_roboflow = _StubBundleModel .export_for_roboflow # capability marker
885928 module ._calls = calls
@@ -910,13 +953,13 @@ def test_returns_module_when_capable(self):
910953class PackageRfdetrPtlTest (unittest .TestCase ):
911954 """PyTorch-Lightning rf-detr checkpoints are rebuilt via rfdetr into build_dir."""
912955
913- def _package (self , model_type , fake_rfdetr , * , segmentation_head = False ):
956+ def _package (self , model_type , fake_rfdetr , * , segmentation_head = False , num_keypoints_per_class = None ):
914957 with tempfile .TemporaryDirectory () as model_dir :
915958 (Path (model_dir ) / "checkpoint_best_ema.pth" ).write_bytes (b"raw-ptl" )
916- ckpt = {
917- "pytorch-lightning_version" : "2.1.0" ,
918- " args" : { "segmentation_head" : segmentation_head , "class_names" : [ "cat" , "dog" ]},
919- }
959+ args = {"segmentation_head" : segmentation_head , "class_names" : [ "cat" , "dog" ]}
960+ if num_keypoints_per_class is not None :
961+ args [ "num_keypoints_per_class" ] = num_keypoints_per_class
962+ ckpt = { "pytorch-lightning_version" : "2.1.0" , "args" : args }
920963 torch = _fake_torch (ckpt )
921964 with _import_patch ({"torch" : torch }), mock .patch .dict (sys .modules , {"rfdetr" : fake_rfdetr }):
922965 bundle = package_custom_weights (model_type , model_dir , filename = "checkpoint_best_ema.pth" )
@@ -944,6 +987,28 @@ def test_from_checkpoint_valueerror_falls_back_to_model_type(self):
944987 self .assertEqual (fake ._calls ["fallback_constructed" ], 1 )
945988 self .assertIn ("weights.pt" , names )
946989
990+ def test_keypoint_from_checkpoint_success_produces_bundle (self ):
991+ # A keypoint PTL checkpoint (args.num_keypoints_per_class marks pose, matching
992+ # the 'rfdetr-keypoint-preview' model_type) rebuilds via rfdetr.from_checkpoint.
993+ fake = _make_fake_rfdetr ()
994+ bundle , names = self ._package ("rfdetr-keypoint-preview" , fake , num_keypoints_per_class = [0 , 17 ])
995+ self .assertEqual (bundle .model_type , "rfdetr-keypoint-preview" )
996+ self .assertEqual (fake ._calls ["from_checkpoint" ], 1 )
997+ self .assertEqual (fake ._calls ["fallback_constructed" ], 0 )
998+ self .assertIn ("weights.pt" , names )
999+ self .assertIn ("class_names.txt" , names )
1000+
1001+ def test_keypoint_from_checkpoint_valueerror_falls_back_to_model_type (self ):
1002+ # When from_checkpoint can't infer the class, the fallback resolves the
1003+ # RFDETRKeypointPreview subclass from _RFDETR_MODEL_TYPE_TO_CLASS and rebuilds.
1004+ fake = _make_fake_rfdetr (from_checkpoint_raises = True )
1005+ bundle , names = self ._package ("rfdetr-keypoint-preview" , fake , num_keypoints_per_class = [0 , 17 ])
1006+ self .assertEqual (bundle .model_type , "rfdetr-keypoint-preview" )
1007+ self .assertEqual (fake ._calls ["from_checkpoint" ], 1 )
1008+ self .assertEqual (fake ._calls ["fallback_constructed" ], 1 )
1009+ self .assertIn ("weights.pt" , names )
1010+ self .assertIn ("class_names.txt" , names )
1011+
9471012 def test_ptl_path_raises_when_rfdetr_absent (self ):
9481013 with tempfile .TemporaryDirectory () as model_dir :
9491014 (Path (model_dir ) / "checkpoint_best_ema.pth" ).write_bytes (b"raw-ptl" )
0 commit comments