Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
eacbdaf
get_conditions_provider_for_video: load assemblies instead of full pi…
maximpavliv Jun 25, 2025
66f1da8
Fix CondProvider.load_conditions_pickle()
maximpavliv Jun 26, 2025
b5f341b
Fix FilterLowConfidencePoses
maximpavliv Jun 26, 2025
df805ec
Fix create_video_with_all_detections (bboxes generated by CTD inference)
maximpavliv Jun 26, 2025
8282f4d
PadOutputs: normalize output format
maximpavliv Jul 1, 2025
a098c63
Add CTD model to integration tests
maximpavliv Jun 25, 2025
2bd069f
Merge branch 'main' into maxim/add_ctd_integration_test
maximpavliv Jul 10, 2025
9f04c61
Formatting
maximpavliv Jul 11, 2025
2abf37c
Trim superanimal_humanbody.yaml default project config
maximpavliv Jul 21, 2025
a1a6be1
Trim superanimal_humanbody_colors
maximpavliv Jul 21, 2025
dfbce1d
Correct get_checkpoint_epoch
maximpavliv Jul 21, 2025
1432a73
Add rtmpose_x modelzoo model config
maximpavliv Jul 21, 2025
a4d74cc
Add FilteredDetector
maximpavliv Jul 21, 2025
0cbfe59
Add get_filtered_coco_detector_inference_runner() method
maximpavliv Jul 21, 2025
84b230e
Add ScaleToUnitRange transform
maximpavliv Jul 21, 2025
c4c1318
Superanimal humanbody inference: use filtered detector runner
maximpavliv Jul 21, 2025
dc511cd
ModelZoo tab: make humanbody general case
maximpavliv Jul 21, 2025
479cc66
get_super_animal_scorer(): add torchvision_detector_name arg
maximpavliv Jul 21, 2025
6a14584
Remove superanimal_humanbody_video_inference.py module
maximpavliv Jul 21, 2025
6774812
Regularize get_super_animal_model_config_path()
maximpavliv Jul 21, 2025
424ac92
Regularize load_super_animal_config()
maximpavliv Jul 21, 2025
04438f8
Regularize download_super_animal_snapshot()
maximpavliv Jul 21, 2025
256977b
update_config(): superanimal_humanbody - compatible
maximpavliv Jul 21, 2025
8d4cf20
Revert video_inference()
maximpavliv Jul 21, 2025
edf4f9d
Revert create_df_from_prediction()
maximpavliv Jul 21, 2025
cded581
Restore CTDInferenceRunner
maximpavliv Jul 21, 2025
7eaf923
Remove TorchvisionDetectorInferenceRunner
maximpavliv Jul 21, 2025
feb34d1
Revert DetectorInferenceRunner
maximpavliv Jul 21, 2025
711a47e
superanimal_analyze_images() - make humanbody compatible
maximpavliv Jul 21, 2025
28e4dd0
Revert build_predictions_dataframe()
maximpavliv Jul 21, 2025
193f935
Revert get_inference_runners()
maximpavliv Jul 21, 2025
613b2ac
Revert detectors/fasterRCNN.py
maximpavliv Jul 21, 2025
d848741
Revert detectors/torchvision.py
maximpavliv Jul 21, 2025
bba8689
Revert base Runner
maximpavliv Jul 21, 2025
25fa08d
Fix superanimal_humanbody unit test
maximpavliv Jul 31, 2025
425484b
Disable video adaptation for superanimal_humanbody
maximpavliv Jul 31, 2025
4b04013
Fix testscript_superanimal_inference.py
maximpavliv Jul 31, 2025
eea470e
Remove debug print
maximpavliv Jul 31, 2025
3d47a36
Black formatting
maximpavliv Jul 31, 2025
7b50466
Merge branch 'maxim/superanimal_humanbody_filtered_detector' into max…
maximpavliv Jul 31, 2025
0da592b
Black formatting
maximpavliv Jul 31, 2025
6c0dc12
FilterLowConfidencePoses: fix default aggregate_func
maximpavliv Aug 18, 2025
54b3b97
ConditionalKeypointsToModelInputs - empty cond kpts case
maximpavliv Aug 19, 2025
dbbd7d1
TopDownCrop: crop to required size even with empty bboxes list
maximpavliv Aug 19, 2025
62dda47
CTDInferenceRunner: shortcut predict() when empty conditions
maximpavliv Aug 19, 2025
e5ed8ee
Merge branch 'main' into maxim/cond_kpts_bug
maximpavliv Sep 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion deeplabcut/pose_estimation_pytorch/apis/ctd.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def get_conditions_provider_for_video(
video = Path(video)

# Load pickle for multi-animal projects
cond_file = video.parent / f"{video.stem}{cond_provider.scorer}_full.pickle"
cond_file = video.parent / f"{video.stem}{cond_provider.scorer}_assemblies.pickle"
if not cond_file.exists():

# Load h5 for single-animal projects
Expand Down
3 changes: 2 additions & 1 deletion deeplabcut/pose_estimation_pytorch/data/ctd.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ def load_conditions_h5(
Each array has shape (num_conditions, num_bodyparts, 3).
If "images" is None: a list containing the conditions for each frame.
"""

def _parse_row(df_row) -> np.ndarray:
# Row to numpy and reshape
pose = df_row.to_numpy().reshape((num_conditions, num_bodyparts, 3))
Expand Down Expand Up @@ -418,7 +419,7 @@ def load_conditions_pickle(filepath: str | Path) -> list[np.ndarray]:
data = pickle.load(f)

frames = [f for f in data.keys() if isinstance(f, int)]
n_frames = max(*frames)
n_frames = max(*frames) + 1

parsed = []
for i in range(n_frames):
Expand Down
33 changes: 29 additions & 4 deletions deeplabcut/pose_estimation_pytorch/data/postprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ def build_bottom_up_postprocessor(
"identity_scores": max_individuals,
},
pad_value=-1,
expected_shapes={
"bodyparts": (num_bodyparts, 3),
"identity_scores": (num_bodyparts, max_individuals),
},
),
]

Expand Down Expand Up @@ -172,6 +176,11 @@ def build_top_down_postprocessor(
"bbox_scores": max_individuals,
},
pad_value=-1,
expected_shapes={
"bodyparts": (num_bodyparts, 3),
"bboxes": (4,),
"bbox_scores": (), # scalar
},
),
]
)
Expand Down Expand Up @@ -271,26 +280,42 @@ def __init__(
self,
max_individuals: dict[str, int],
pad_value: int,
expected_shapes: dict[str, tuple[int, ...]],
):
self.max_individuals = max_individuals
self.pad_value = pad_value
self.expected_shapes = expected_shapes

def __call__(
self, predictions: dict[str, np.ndarray], context: Context
) -> tuple[dict[str, np.ndarray], Context]:
for name in predictions:
output = predictions[name]
if isinstance(output, list):
output = np.array(output)
output = np.array(output) # Normalize all inputs to np.ndarray

expected_shape = self.expected_shapes.get(name, ())
expected_ndim = 1 + len(
expected_shape
) # individuals_dimension + expected shape for single individual

# Special handling for empty arrays
if len(output) == 0:
output = np.empty((0, *expected_shape), dtype=float)
elif output.ndim < expected_ndim:
output = np.reshape(output, (len(output), *expected_shape))

if (
name in self.max_individuals
and len(output) < self.max_individuals[name]
):
pad_size = self.max_individuals[name] - len(output)
tail_shape = output.shape[1:]
padding = self.pad_value * np.ones((pad_size, *tail_shape))
predictions[name] = np.concatenate([output, padding])
padding = self.pad_value * np.ones(
(pad_size, *tail_shape), dtype=output.dtype
)
output = np.concatenate([output, padding], axis=0)

predictions[name] = output

return predictions, context

Expand Down
24 changes: 16 additions & 8 deletions deeplabcut/pose_estimation_pytorch/data/preprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ class FilterLowConfidencePoses(Preprocessor):
def __init__(
self,
confidence_threshold: float = 0.05,
aggregate_func: Callable[[np.ndarray], float] = lambda arr: np.max(arr, axis=1),
aggregate_func: Callable[[np.ndarray], float] = lambda arr: np.nanmax(arr, axis=1),
) -> None:
self.confidence_threshold = confidence_threshold
self.aggregate_func = aggregate_func
Expand All @@ -382,7 +382,14 @@ def __call__(
raise ValueError(f"Must include cond_kpts, found {context}")

keypoints = context["cond_kpts"]
mask = self.aggregate_func(keypoints[:, :, 2]) >= self.confidence_threshold

if 0 in keypoints.shape:
# No poses to filter; return early
return image, context

confidences = keypoints[:, :, 2]
aggregated_confidence = self.aggregate_func(confidences)
mask = aggregated_confidence >= self.confidence_threshold
context["cond_kpts"] = keypoints[mask]

return image, context
Expand Down Expand Up @@ -462,7 +469,9 @@ def __call__(

# can have no bounding boxes if detector made no detections
if len(images) == 0:
images = np.zeros((0, *image.shape))
h, w = self.output_size[1], self.output_size[0] # output_size = (w, h)
c = image.shape[2] if image.ndim == 3 else 1
images = np.zeros((0, h, w, c), dtype=image.dtype)
else:
images = np.stack(images, axis=0)

Expand Down Expand Up @@ -508,12 +517,11 @@ def __call__(
self, image: np.ndarray, context: Context
) -> tuple[np.ndarray, Context]:
cond_keypoints = context[self.cond_kpt_key]
if len(cond_keypoints) == 0:
return image, context

rescaled = cond_keypoints.copy()
rescaled[..., :2] = (
rescaled[..., :2] - np.array(context["offsets"])[:, None]
) / np.array(context["scales"])[:, None]
if rescaled.size > 0: # only rescale if non-empty
rescaled[..., :2] = (
rescaled[..., :2] - np.array(context["offsets"])[:, None]
) / np.array(context["scales"])[:, None]
context["model_kwargs"] = {"cond_kpts": np.expand_dims(rescaled, axis=1)}
return image, context
6 changes: 6 additions & 0 deletions deeplabcut/pose_estimation_pytorch/runners/inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,12 @@ def predict(
}
]
"""
cond_kpts = kwargs.get("cond_kpts", None)
if cond_kpts is not None and cond_kpts.shape[0] == 0:
# No conditions, so just return an empty prediction list
return []

# Normal prediction path
if self.device and "cuda" in str(self.device):
with torch.autocast(device_type=str(self.device)):
outputs = self.model(inputs.to(self.device), **kwargs)
Expand Down
10 changes: 7 additions & 3 deletions deeplabcut/utils/make_labeled_video.py
Original file line number Diff line number Diff line change
Expand Up @@ -1053,8 +1053,8 @@ def create_video(
fname=video,
sname=output_path,
codec=codec,
sw=bbox[1]-bbox[0] if display_cropped else "",
sh=bbox[3]-bbox[2] if display_cropped else "",
sw=bbox[1] - bbox[0] if display_cropped else "",
sh=bbox[3] - bbox[2] if display_cropped else "",
fps=fps,
)

Expand Down Expand Up @@ -1330,7 +1330,11 @@ def create_video_with_all_detections(
ind = frames.index(n)

# Draw bounding boxes of required and present
if plot_bboxes and "bboxes" in data[frame_names[ind]]:
if (
plot_bboxes
and "bboxes" in data[frame_names[ind]]
and "bbox_scores" in data[frame_names[ind]]
):
bboxes = data[frame_names[ind]]["bboxes"]
bbox_scores = data[frame_names[ind]]["bbox_scores"]
n_bboxes = bboxes.shape[0]
Expand Down
49 changes: 33 additions & 16 deletions examples/testscript_pytorch_multi_animal.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@

import deeplabcut.utils.auxiliaryfunctions as af
from deeplabcut.compat import Engine
from deeplabcut.pose_estimation_pytorch.config.utils import is_model_top_down
from deeplabcut.pose_estimation_pytorch.config.utils import (
is_model_top_down,
is_model_cond_top_down,
)

from utils import (
cleanup,
Expand All @@ -38,6 +41,7 @@ def main(
max_snapshots_to_keep: int = 5,
device: str = "cpu",
logger: dict | None = None,
conditions_shuffle: int = 0,
create_labeled_videos: bool = False,
delete_after_test_run: bool = False,
) -> None:
Expand All @@ -55,6 +59,23 @@ def main(
if is_model_top_down(net_type):
epochs_ = top_down_epochs
try:
pytorch_cfg_updates = {
"data.conditions.snapshot_index": -1,
"train_settings.display_iters": 50,
"train_settings.epochs": epochs_,
"train_settings.batch_size": batch_size,
"runner.device": device,
"runner.snapshots.save_epochs": save_epochs,
"runner.snapshots.max_snapshots": max_snapshots_to_keep,
"detector.train_settings.display_iters": 1,
"detector.train_settings.epochs": detector_epochs,
"detector.train_settings.batch_size": detector_batch_size,
"detector.runner.snapshots.save_epochs": save_epochs,
"detector.runner.snapshots.max_snapshots": max_snapshots_to_keep,
"logger": logger,
}
if is_model_cond_top_down(net_type):
pytorch_cfg_updates["data.conditions.shuffle"] = conditions_shuffle
run(
config_path=config_path,
train_fraction=train_frac,
Expand All @@ -63,20 +84,7 @@ def main(
videos=[str(project_path / "videos" / "video.mp4")],
device=device,
engine=engine,
pytorch_cfg_updates={
"train_settings.display_iters": 50,
"train_settings.epochs": epochs_,
"train_settings.batch_size": batch_size,
"runner.device": device,
"runner.snapshots.save_epochs": save_epochs,
"runner.snapshots.max_snapshots": max_snapshots_to_keep,
"detector.train_settings.display_iters": 1,
"detector.train_settings.epochs": detector_epochs,
"detector.train_settings.batch_size": detector_batch_size,
"detector.runner.snapshots.save_epochs": save_epochs,
"detector.runner.snapshots.max_snapshots": max_snapshots_to_keep,
"logger": logger,
},
pytorch_cfg_updates=pytorch_cfg_updates,
create_labeled_videos=create_labeled_videos,
)
except Exception as err:
Expand All @@ -96,8 +104,15 @@ def main(
"project_name": "testscript-dev",
"run_name": "test-logging",
}
net_types = [
"top_down_resnet_50",
"resnet_50",
"dekr_w32",
"rtmpose_m",
"ctd_coam_w32",
]
main(
net_types=["top_down_resnet_50", "resnet_50", "dekr_w32", "rtmpose_m"],
net_types=net_types,
params=SyntheticProjectParameters(
multianimal=True,
num_bodyparts=4,
Expand All @@ -115,6 +130,8 @@ def main(
max_snapshots_to_keep=2,
device="cpu", # "cpu", "cuda:0", "mps"
logger=None,
conditions_shuffle=net_types.index("resnet_50")
+ 1, # shuffles start at index 1
create_labeled_videos=True,
delete_after_test_run=True,
)
Loading