Skip to content

Commit 77773ec

Browse files
committed
Use SuperAnimal detection fallback for Space
1 parent 2afeaf5 commit 77773ec

5 files changed

Lines changed: 122 additions & 17 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,8 @@ The `s1ckpt_inference.ckpt` checkpoint is downloaded automatically if missing.
163163
| | **Local** (`python app.py`) | **Hugging Face Space** |
164164
|--|--|--|
165165
| PRIMA device | GPU if available, else CPU | CPU only |
166-
| Detector | Detectron2 X-101-FPN | full-image crop fallback |
167-
| Default TTA iterations | 30 | 0 (PRIMA-only by default) |
166+
| Detector | Detectron2 X-101-FPN | DeepLabCut SuperAnimal detector |
167+
| Default TTA iterations | 30 | 30 |
168168
| Save `.obj` meshes | on | off |
169169
| Preload checkpoint at startup | off | on |
170170

@@ -194,7 +194,7 @@ The script rsyncs only the Git-tracked files needed by the Space from the
194194
working tree (not `git archive`) so image files are materialized before
195195
`git add` turns them into LFS blobs.
196196
During deployment, `detectron2` is removed from the Space `requirements.txt`;
197-
the app uses its full-image crop fallback on the CPU Space.
197+
the app uses the DeepLabCut SuperAnimal detector fallback on the CPU Space.
198198

199199
---
200200

app.py

Lines changed: 85 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
320398
def _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)

scripts/deploy_hf_space.sh

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ done
7070
README_FILE="${TMP}/README.md"
7171
REQ_FILE="${TMP}/requirements.txt"
7272

73-
echo "[deploy] Removing Detectron2 from Space requirements (app falls back to full-image crops) ..."
73+
echo "[deploy] Removing Detectron2 from Space requirements (app falls back to SuperAnimal detection) ..."
7474
grep -vE '^[[:space:]]*detectron2([[:space:]]|@|$)' "$REQ_FILE" > "${REQ_FILE}.tmp"
7575
mv "${REQ_FILE}.tmp" "$REQ_FILE"
7676

@@ -109,10 +109,37 @@ if [[ "$PUSH_URL" == https://huggingface.co/* && -z "${HF_TOKEN:-}" && -f "${HF_
109109
HF_TOKEN="$(<"${HF_HOME:-$HOME/.cache/huggingface}/token")"
110110
fi
111111
if [[ "$PUSH_URL" == https://huggingface.co/* && -n "${HF_TOKEN:-}" ]]; then
112-
PUSH_URL="${PUSH_URL/https:\/\/huggingface.co/https:\/\/hf_user:${HF_TOKEN}@huggingface.co}"
112+
export HF_TOKEN
113+
git config credential.helper "store --file=${TMP}/git-credentials"
114+
printf 'protocol=https\nhost=huggingface.co\nusername=hf_user\npassword=%s\n\n' "$HF_TOKEN" | git credential approve
115+
ASKPASS="${TMP}/git-askpass.sh"
116+
cat > "$ASKPASS" <<'SH'
117+
#!/usr/bin/env bash
118+
case "$1" in
119+
*Username*) printf '%s\n' 'hf_user' ;;
120+
*Password*) printf '%s\n' "${HF_TOKEN}" ;;
121+
*) printf '%s\n' "${HF_TOKEN}" ;;
122+
esac
123+
SH
124+
chmod 700 "$ASKPASS"
125+
export GIT_ASKPASS="$ASKPASS"
113126
fi
114127

115128
git remote add hf "$PUSH_URL"
129+
echo "[deploy] Uploading LFS objects to Hugging Face Space ..."
130+
mapfile -t LFS_OIDS < <(git lfs ls-files -l | awk '{print $1}')
131+
if [[ "${#LFS_OIDS[@]}" -gt 0 ]]; then
132+
if ! GIT_TERMINAL_PROMPT=0 git lfs push --object-id hf "${LFS_OIDS[@]}"; then
133+
echo "[deploy] ERROR: LFS upload failed. Ensure HF_TOKEN has write access to ${SPACE_URL}." >&2
134+
exit 1
135+
fi
136+
else
137+
echo "[deploy] No LFS objects found in this snapshot."
138+
fi
139+
116140
echo "[deploy] Force-pushing to Hugging Face Space ..."
117-
GIT_TERMINAL_PROMPT=0 git -c credential.helper= push hf HEAD:main --force
141+
# This deploy repo is freshly initialized, so older git-lfs pre-push hooks can
142+
# fail when they try to inspect the remote's previous main commit. LFS objects
143+
# are uploaded explicitly above; skip the hook for the Git ref update.
144+
GIT_TERMINAL_PROMPT=0 git push hf HEAD:main --force --no-verify
118145
echo "[deploy] Done."

scripts/local_infer.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ def main() -> int:
7171
model, model_cfg, renderer, cam_crop_to_full_fn, device = app._load_prima_model()
7272
print(f"[local_infer] device={device}")
7373

74-
print("[local_infer] Building detector (Detectron2 if installed, else fallback) ...")
74+
print("[local_infer] Building detector (Detectron2 if installed, else SuperAnimal fallback) ...")
7575
detector = app._build_detector()
76-
print(f"[local_infer] detector={'detectron2' if detector is not None else 'fallback'}")
76+
print(f"[local_infer] detector={'detectron2' if detector is not None else 'superanimal fallback'}")
7777

7878
print("[local_infer] Running inference ...")
7979
before, after, kpts, mesh_before, mesh_after = app._collect_animal_results(

scripts/run_local_demo_once.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ def main() -> int:
3737
model, cfg, renderer, device = app._load_prima_model()
3838
print(f" device={device}")
3939

40-
print("[2/4] Building detector (Detectron2 if installed, else full-image bbox) …")
40+
print("[2/4] Building detector (Detectron2 if installed, else SuperAnimal detector) …")
4141
det = app._build_detector()
42-
print(f" detector={'detectron2' if det is not None else 'full-image fallback'}")
42+
print(f" detector={'detectron2' if det is not None else 'superanimal fallback'}")
4343

4444
img = cv2.cvtColor(cv2.imread(str(img_path)), cv2.COLOR_BGR2RGB)
4545
print(f"[3/4] Running inference on {img_path.name} (TTA iterations=0) …")

0 commit comments

Comments
 (0)