Visitar URL original
Fix CTD empty conditions edge case (#3076) · DeepLabCut/DeepLabCut@382cdc7 · GitHub
Skip to content

Commit 382cdc7

Browse files
authored
Fix CTD empty conditions edge case (#3076)
* get_conditions_provider_for_video: load assemblies instead of full pickle * Fix CondProvider.load_conditions_pickle() The frames indexes start at 0 and go up to (n_frames - 1) * Fix FilterLowConfidencePoses * Fix create_video_with_all_detections (bboxes generated by CTD inference) * PadOutputs: normalize output format * Add CTD model to integration tests * Formatting * Trim superanimal_humanbody.yaml default project config * Trim superanimal_humanbody_colors * Correct get_checkpoint_epoch * Add rtmpose_x modelzoo model config * Add FilteredDetector * Add get_filtered_coco_detector_inference_runner() method * Add ScaleToUnitRange transform * Superanimal humanbody inference: use filtered detector runner * ModelZoo tab: make humanbody general case * get_super_animal_scorer(): add torchvision_detector_name arg * Remove superanimal_humanbody_video_inference.py module * Regularize get_super_animal_model_config_path() * Regularize load_super_animal_config() * Regularize download_super_animal_snapshot() * update_config(): superanimal_humanbody - compatible * Revert video_inference() * Revert create_df_from_prediction() * Restore CTDInferenceRunner * Remove TorchvisionDetectorInferenceRunner * Revert DetectorInferenceRunner * superanimal_analyze_images() - make humanbody compatible * Revert build_predictions_dataframe() * Revert get_inference_runners() * Revert detectors/fasterRCNN.py * Revert detectors/torchvision.py * Revert base Runner * Fix superanimal_humanbody unit test * Disable video adaptation for superanimal_humanbody * Fix testscript_superanimal_inference.py * Remove debug print * Black formatting * Black formatting * FilterLowConfidencePoses: fix default aggregate_func Replaced np.max by np.nanmax, to avoid excluding whole pose if some keypoint is Nan * ConditionalKeypointsToModelInputs - empty cond kpts case * TopDownCrop: crop to required size even with empty bboxes list * CTDInferenceRunner: shortcut predict() when empty conditions
1 parent c098ce0 commit 382cdc7

7 files changed

Lines changed: 94 additions & 33 deletions

File tree

‎deeplabcut/pose_estimation_pytorch/apis/ctd.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def get_conditions_provider_for_video(
7979
video = Path(video)
8080

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

8585
# Load h5 for single-animal projects

‎deeplabcut/pose_estimation_pytorch/data/ctd.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ def load_conditions_h5(
225225
Each array has shape (num_conditions, num_bodyparts, 3).
226226
If "images" is None: a list containing the conditions for each frame.
227227
"""
228+
228229
def _parse_row(df_row) -> np.ndarray:
229230
# Row to numpy and reshape
230231
pose = df_row.to_numpy().reshape((num_conditions, num_bodyparts, 3))
@@ -418,7 +419,7 @@ def load_conditions_pickle(filepath: str | Path) -> list[np.ndarray]:
418419
data = pickle.load(f)
419420

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

423424
parsed = []
424425
for i in range(n_frames):

‎deeplabcut/pose_estimation_pytorch/data/postprocessor.py‎

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ def build_bottom_up_postprocessor(
108108
"identity_scores": max_individuals,
109109
},
110110
pad_value=-1,
111+
expected_shapes={
112+
"bodyparts": (num_bodyparts, 3),
113+
"identity_scores": (num_bodyparts, max_individuals),
114+
},
111115
),
112116
]
113117

@@ -172,6 +176,11 @@ def build_top_down_postprocessor(
172176
"bbox_scores": max_individuals,
173177
},
174178
pad_value=-1,
179+
expected_shapes={
180+
"bodyparts": (num_bodyparts, 3),
181+
"bboxes": (4,),
182+
"bbox_scores": (), # scalar
183+
},
175184
),
176185
]
177186
)
@@ -271,26 +280,42 @@ def __init__(
271280
self,
272281
max_individuals: dict[str, int],
273282
pad_value: int,
283+
expected_shapes: dict[str, tuple[int, ...]],
274284
):
275285
self.max_individuals = max_individuals
276286
self.pad_value = pad_value
287+
self.expected_shapes = expected_shapes
277288

278289
def __call__(
279290
self, predictions: dict[str, np.ndarray], context: Context
280291
) -> tuple[dict[str, np.ndarray], Context]:
281292
for name in predictions:
282293
output = predictions[name]
283-
if isinstance(output, list):
284-
output = np.array(output)
294+
output = np.array(output) # Normalize all inputs to np.ndarray
295+
296+
expected_shape = self.expected_shapes.get(name, ())
297+
expected_ndim = 1 + len(
298+
expected_shape
299+
) # individuals_dimension + expected shape for single individual
300+
301+
# Special handling for empty arrays
302+
if len(output) == 0:
303+
output = np.empty((0, *expected_shape), dtype=float)
304+
elif output.ndim < expected_ndim:
305+
output = np.reshape(output, (len(output), *expected_shape))
285306

286307
if (
287308
name in self.max_individuals
288309
and len(output) < self.max_individuals[name]
289310
):
290311
pad_size = self.max_individuals[name] - len(output)
291312
tail_shape = output.shape[1:]
292-
padding = self.pad_value * np.ones((pad_size, *tail_shape))
293-
predictions[name] = np.concatenate([output, padding])
313+
padding = self.pad_value * np.ones(
314+
(pad_size, *tail_shape), dtype=output.dtype
315+
)
316+
output = np.concatenate([output, padding], axis=0)
317+
318+
predictions[name] = output
294319

295320
return predictions, context
296321

‎deeplabcut/pose_estimation_pytorch/data/preprocessor.py‎

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ class FilterLowConfidencePoses(Preprocessor):
370370
def __init__(
371371
self,
372372
confidence_threshold: float = 0.05,
373-
aggregate_func: Callable[[np.ndarray], float] = lambda arr: np.max(arr, axis=1),
373+
aggregate_func: Callable[[np.ndarray], float] = lambda arr: np.nanmax(arr, axis=1),
374374
) -> None:
375375
self.confidence_threshold = confidence_threshold
376376
self.aggregate_func = aggregate_func
@@ -382,7 +382,14 @@ def __call__(
382382
raise ValueError(f"Must include cond_kpts, found {context}")
383383

384384
keypoints = context["cond_kpts"]
385-
mask = self.aggregate_func(keypoints[:, :, 2]) >= self.confidence_threshold
385+
386+
if 0 in keypoints.shape:
387+
# No poses to filter; return early
388+
return image, context
389+
390+
confidences = keypoints[:, :, 2]
391+
aggregated_confidence = self.aggregate_func(confidences)
392+
mask = aggregated_confidence >= self.confidence_threshold
386393
context["cond_kpts"] = keypoints[mask]
387394

388395
return image, context
@@ -462,7 +469,9 @@ def __call__(
462469

463470
# can have no bounding boxes if detector made no detections
464471
if len(images) == 0:
465-
images = np.zeros((0, *image.shape))
472+
h, w = self.output_size[1], self.output_size[0] # output_size = (w, h)
473+
c = image.shape[2] if image.ndim == 3 else 1
474+
images = np.zeros((0, h, w, c), dtype=image.dtype)
466475
else:
467476
images = np.stack(images, axis=0)
468477

@@ -508,12 +517,11 @@ def __call__(
508517
self, image: np.ndarray, context: Context
509518
) -> tuple[np.ndarray, Context]:
510519
cond_keypoints = context[self.cond_kpt_key]
511-
if len(cond_keypoints) == 0:
512-
return image, context
513520

514521
rescaled = cond_keypoints.copy()
515-
rescaled[..., :2] = (
516-
rescaled[..., :2] - np.array(context["offsets"])[:, None]
517-
) / np.array(context["scales"])[:, None]
522+
if rescaled.size > 0: # only rescale if non-empty
523+
rescaled[..., :2] = (
524+
rescaled[..., :2] - np.array(context["offsets"])[:, None]
525+
) / np.array(context["scales"])[:, None]
518526
context["model_kwargs"] = {"cond_kpts": np.expand_dims(rescaled, axis=1)}
519527
return image, context

‎deeplabcut/pose_estimation_pytorch/runners/inference.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,12 @@ def predict(
581581
}
582582
]
583583
"""
584+
cond_kpts = kwargs.get("cond_kpts", None)
585+
if cond_kpts is not None and cond_kpts.shape[0] == 0:
586+
# No conditions, so just return an empty prediction list
587+
return []
588+
589+
# Normal prediction path
584590
if self.device and "cuda" in str(self.device):
585591
with torch.autocast(device_type=str(self.device)):
586592
outputs = self.model(inputs.to(self.device), **kwargs)

‎deeplabcut/utils/make_labeled_video.py‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,8 +1053,8 @@ def create_video(
10531053
fname=video,
10541054
sname=output_path,
10551055
codec=codec,
1056-
sw=bbox[1]-bbox[0] if display_cropped else "",
1057-
sh=bbox[3]-bbox[2] if display_cropped else "",
1056+
sw=bbox[1] - bbox[0] if display_cropped else "",
1057+
sh=bbox[3] - bbox[2] if display_cropped else "",
10581058
fps=fps,
10591059
)
10601060

@@ -1330,7 +1330,11 @@ def create_video_with_all_detections(
13301330
ind = frames.index(n)
13311331

13321332
# Draw bounding boxes of required and present
1333-
if plot_bboxes and "bboxes" in data[frame_names[ind]]:
1333+
if (
1334+
plot_bboxes
1335+
and "bboxes" in data[frame_names[ind]]
1336+
and "bbox_scores" in data[frame_names[ind]]
1337+
):
13341338
bboxes = data[frame_names[ind]]["bboxes"]
13351339
bbox_scores = data[frame_names[ind]]["bbox_scores"]
13361340
n_bboxes = bboxes.shape[0]

‎examples/testscript_pytorch_multi_animal.py‎

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515

1616
import deeplabcut.utils.auxiliaryfunctions as af
1717
from deeplabcut.compat import Engine
18-
from deeplabcut.pose_estimation_pytorch.config.utils import is_model_top_down
18+
from deeplabcut.pose_estimation_pytorch.config.utils import (
19+
is_model_top_down,
20+
is_model_cond_top_down,
21+
)
1922

2023
from utils import (
2124
cleanup,
@@ -38,6 +41,7 @@ def main(
3841
max_snapshots_to_keep: int = 5,
3942
device: str = "cpu",
4043
logger: dict | None = None,
44+
conditions_shuffle: int = 0,
4145
create_labeled_videos: bool = False,
4246
delete_after_test_run: bool = False,
4347
) -> None:
@@ -55,6 +59,23 @@ def main(
5559
if is_model_top_down(net_type):
5660
epochs_ = top_down_epochs
5761
try:
62+
pytorch_cfg_updates = {
63+
"data.conditions.snapshot_index": -1,
64+
"train_settings.display_iters": 50,
65+
"train_settings.epochs": epochs_,
66+
"train_settings.batch_size": batch_size,
67+
"runner.device": device,
68+
"runner.snapshots.save_epochs": save_epochs,
69+
"runner.snapshots.max_snapshots": max_snapshots_to_keep,
70+
"detector.train_settings.display_iters": 1,
71+
"detector.train_settings.epochs": detector_epochs,
72+
"detector.train_settings.batch_size": detector_batch_size,
73+
"detector.runner.snapshots.save_epochs": save_epochs,
74+
"detector.runner.snapshots.max_snapshots": max_snapshots_to_keep,
75+
"logger": logger,
76+
}
77+
if is_model_cond_top_down(net_type):
78+
pytorch_cfg_updates["data.conditions.shuffle"] = conditions_shuffle
5879
run(
5980
config_path=config_path,
6081
train_fraction=train_frac,
@@ -63,20 +84,7 @@ def main(
6384
videos=[str(project_path / "videos" / "video.mp4")],
6485
device=device,
6586
engine=engine,
66-
pytorch_cfg_updates={
67-
"train_settings.display_iters": 50,
68-
"train_settings.epochs": epochs_,
69-
"train_settings.batch_size": batch_size,
70-
"runner.device": device,
71-
"runner.snapshots.save_epochs": save_epochs,
72-
"runner.snapshots.max_snapshots": max_snapshots_to_keep,
73-
"detector.train_settings.display_iters": 1,
74-
"detector.train_settings.epochs": detector_epochs,
75-
"detector.train_settings.batch_size": detector_batch_size,
76-
"detector.runner.snapshots.save_epochs": save_epochs,
77-
"detector.runner.snapshots.max_snapshots": max_snapshots_to_keep,
78-
"logger": logger,
79-
},
87+
pytorch_cfg_updates=pytorch_cfg_updates,
8088
create_labeled_videos=create_labeled_videos,
8189
)
8290
except Exception as err:
@@ -96,8 +104,15 @@ def main(
96104
"project_name": "testscript-dev",
97105
"run_name": "test-logging",
98106
}
107+
net_types = [
108+
"top_down_resnet_50",
109+
"resnet_50",
110+
"dekr_w32",
111+
"rtmpose_m",
112+
"ctd_coam_w32",
113+
]
99114
main(
100-
net_types=["top_down_resnet_50", "resnet_50", "dekr_w32", "rtmpose_m"],
115+
net_types=net_types,
101116
params=SyntheticProjectParameters(
102117
multianimal=True,
103118
num_bodyparts=4,
@@ -115,6 +130,8 @@ def main(
115130
max_snapshots_to_keep=2,
116131
device="cpu", # "cpu", "cuda:0", "mps"
117132
logger=None,
133+
conditions_shuffle=net_types.index("resnet_50")
134+
+ 1, # shuffles start at index 1
118135
create_labeled_videos=True,
119136
delete_after_test_run=True,
120137
)

0 commit comments

Comments
 (0)