@@ -163,9 +163,9 @@ def resolve_detectron_device(self) -> str:
163163 ),
164164 description = (
165165 "**Hugging Face Space (cpu-basic)** — lightweight demo: **CPU-only** PRIMA inference. "
166- "The Space build skips Detectron2 and uses a full-image crop fallback. TTA is optional "
167- "(30 iterations by default, matching the local demo; set to 0 to skip). Mesh `.obj` export "
168- "is off by default to save time and disk."
166+ "The Space build skips Detectron2 and uses the DeepLabCut SuperAnimal detector for animal "
167+ "crops. TTA is optional (30 iterations by default, matching the local demo; set to 0 to "
168+ "skip). Mesh `.obj` export is off by default to save time and disk."
169169 ),
170170 interface_title = "PRIMA on Hugging Face — lightweight CPU demo" ,
171171)
@@ -298,7 +298,7 @@ def _build_detector(profile: Optional[DemoProfile] = None):
298298 import detectron2 .engine
299299 from detectron2 import model_zoo
300300 except Exception as e :
301- print (f"[warn] Detectron2 unavailable ({ type (e ).__name__ } : { e } ); using full-image fallback bbox ." )
301+ print (f"[warn] Detectron2 unavailable ({ type (e ).__name__ } : { e } ); using SuperAnimal detector fallback ." )
302302 return None
303303
304304 if profile is None :
@@ -317,6 +317,84 @@ def _build_detector(profile: Optional[DemoProfile] = None):
317317 return detector
318318
319319
320+ def _filter_superanimal_boxes (
321+ payload : Dict [str , Any ],
322+ det_thresh : float ,
323+ img_shape : Tuple [int , int ],
324+ ) -> Optional [np .ndarray ]:
325+ boxes = payload .get ("bboxes" )
326+ if boxes is None :
327+ return None
328+
329+ boxes = np .asarray (boxes , dtype = np .float32 ).reshape (- 1 , 4 )
330+ if len (boxes ) == 0 :
331+ return None
332+
333+ scores = payload .get ("bbox_scores" )
334+ if scores is None :
335+ scores = np .ones ((len (boxes ),), dtype = np .float32 )
336+ scores = np .asarray (scores , dtype = np .float32 ).reshape (- 1 )
337+ if len (scores ) != len (boxes ):
338+ scores = np .ones ((len (boxes ),), dtype = np .float32 )
339+
340+ h , w = img_shape
341+ valid = (
342+ (scores > float (det_thresh ))
343+ & np .isfinite (boxes ).all (axis = 1 )
344+ & (boxes [:, 2 ] > 0.0 )
345+ & (boxes [:, 3 ] > 0.0 )
346+ )
347+ if not np .any (valid ):
348+ return None
349+
350+ xywh = boxes [valid ]
351+ scores = scores [valid ]
352+ boxes = xywh .copy ()
353+ boxes [:, 2 ] = xywh [:, 0 ] + xywh [:, 2 ]
354+ boxes [:, 3 ] = xywh [:, 1 ] + xywh [:, 3 ]
355+ boxes [:, [0 , 2 ]] = np .clip (boxes [:, [0 , 2 ]], 0.0 , float (max (1 , w - 1 )))
356+ boxes [:, [1 , 3 ]] = np .clip (boxes [:, [1 , 3 ]], 0.0 , float (max (1 , h - 1 )))
357+ valid_size = (boxes [:, 2 ] > boxes [:, 0 ]) & (boxes [:, 3 ] > boxes [:, 1 ])
358+ boxes = boxes [valid_size ]
359+ scores = scores [valid_size ]
360+ if len (boxes ) == 0 :
361+ return None
362+ order = np .argsort (scores )[::- 1 ]
363+ return boxes [order ].astype (np .float32 , copy = False )
364+
365+
366+ def _detect_superanimal_boxes (img_rgb : np .ndarray , det_thresh : float ) -> Optional [np .ndarray ]:
367+ try :
368+ from deeplabcut .pose_estimation_pytorch .apis import superanimal_analyze_images
369+ except Exception as e :
370+ print (f"[warn] DeepLabCut SuperAnimal unavailable ({ type (e ).__name__ } : { e } ); no fallback bbox." )
371+ return None
372+
373+ with tempfile .TemporaryDirectory (prefix = "sa_detect_" ) as tmp_dir :
374+ img_path = os .path .join (tmp_dir , "image.png" )
375+ cv2 .imwrite (img_path , cv2 .cvtColor (img_rgb , cv2 .COLOR_RGB2BGR ))
376+
377+ dlc_device = "cuda" if torch .cuda .is_available () else "cpu"
378+ preds = superanimal_analyze_images (
379+ superanimal_name = SUPER_ANIMAL_ARGS .superanimal_name ,
380+ model_name = SUPER_ANIMAL_ARGS .superanimal_model_name ,
381+ detector_name = SUPER_ANIMAL_ARGS .superanimal_detector_name ,
382+ images = img_path ,
383+ max_individuals = SUPER_ANIMAL_ARGS .superanimal_max_individuals ,
384+ out_folder = tmp_dir ,
385+ progress_bar = False ,
386+ device = dlc_device ,
387+ pose_threshold = 0.1 ,
388+ bbox_threshold = float (det_thresh ),
389+ plot_skeleton = False ,
390+ )
391+
392+ payload = preds .get (img_path )
393+ if payload is None :
394+ return None
395+ return _filter_superanimal_boxes (payload , det_thresh , img_rgb .shape [:2 ])
396+
397+
320398def _load_model_and_detector_for_demo (checkpoint_path : str , profile : DemoProfile ):
321399 """Load PRIMA and Detectron2 once for the Gradio session (main thread only)."""
322400 model , model_cfg , renderer , cam_crop_to_full_fn , device = _load_prima_model (checkpoint_path )
@@ -331,8 +409,8 @@ def _detect_animal_boxes(
331409) -> Optional [np .ndarray ]:
332410 """Return Nx4 XYXY boxes or None if no animal detections."""
333411 if detector is None :
334- h , w = img_bgr . shape [: 2 ]
335- return np . array ([[ 0.0 , 0.0 , float ( max ( 1 , w - 1 )), float ( max ( 1 , h - 1 ))]], dtype = np . float32 )
412+ img_rgb = cv2 . cvtColor ( img_bgr , cv2 . COLOR_BGR2RGB )
413+ return _detect_superanimal_boxes ( img_rgb , det_thresh )
336414
337415 det_out = detector (img_bgr )
338416 det_instances = det_out ["instances" ]
@@ -403,7 +481,7 @@ def report(message: str) -> None:
403481 img_bgr = cv2 .cvtColor (img_rgb , cv2 .COLOR_RGB2BGR )
404482 if boxes is None :
405483 if detector is None :
406- report ("Detectron2 unavailable; using full-image crop ..." )
484+ report ("Detectron2 unavailable; detecting animals with SuperAnimal ..." )
407485 else :
408486 report ("Detecting animals with Detectron2..." )
409487 boxes = _detect_animal_boxes (detector , img_bgr , det_thresh )
0 commit comments