Visitar URL original
`InferenceRunner` + `analyze_videos`: Fix top-down analysis crashing for zero-detections and unreadable video handling by deruyter92 · Pull Request #3488 · DeepLabCut/DeepLabCut · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
64a2f4e
fix _generate_output_data crash on empty predictions
deruyter92 Sep 8, 2026
510090e
Allow create_df_from_prediction() to handle an empty predictions list.
deruyter92 Sep 8, 2026
4b45fb9
Treat zero predictions as a distinct warning case in `video_inference()`
deruyter92 Sep 8, 2026
c78f439
Don't write dataframe for empty predictions
deruyter92 Sep 9, 2026
16a2977
update test_videos: pop `"PAFinds"` before comparison
deruyter92 Sep 9, 2026
f534de6
update test_videos: assert no DF is written for empty predictions
deruyter92 Sep 9, 2026
624b1b1
Return empty-detection inference results
C-Achard Sep 9, 2026
c57dd35
Use CTD predictions for video output
C-Achard Sep 9, 2026
4e4dab7
Handle empty prediction outputs gracefully
C-Achard Sep 9, 2026
42098ac
Reuse shared frame key width helper
C-Achard Sep 9, 2026
292d068
Clarify video inference warnings
C-Achard Sep 9, 2026
d5ba290
Go back to ValueError for empty predictions
C-Achard Sep 9, 2026
bff4fea
Remove NaN fallback for empty video predictions
C-Achard Sep 9, 2026
06486de
Fix CTD export guard for empty predictions
C-Achard Sep 9, 2026
e456aeb
Clarify empty CTD prediction handling
C-Achard Sep 9, 2026
a162439
Centralize test fakes
C-Achard Sep 9, 2026
11ff4fd
Add test for async with no images
C-Achard Sep 9, 2026
a210b69
Clarify async inference test comment
C-Achard Sep 9, 2026
c7a8c34
Use shared frame key width helper
C-Achard Sep 9, 2026
8248a10
Merge branch 'dev' into jaap/fix-empty-video-predictions
C-Achard Sep 17, 2026
90849a5
Don't write pickle for unreadable video
deruyter92 Oct 22, 2026
4b8c0ca
Video inference: update warning for empty predictions
deruyter92 Oct 22, 2026
44918dd
InferenceRunner: don't emit results for producer exceptions
deruyter92 Oct 22, 2026
0e40ba8
update code comment
deruyter92 Oct 22, 2026
b0fbd36
fix ShelfWriter: report zero framens when empty
deruyter92 Oct 22, 2026
ce627ae
Skip unreadable videos with shelf writer + improve docstring and log …
deruyter92 Oct 22, 2026
4867b92
Merge branch 'dev' into jaap/fix-empty-video-predictions
deruyter92 Oct 22, 2026
89d06d0
Fix warning typo + update tests
deruyter92 Oct 22, 2026
a67e257
Remove unreachable CTD empty-export branch and reword FMPose error
deruyter92 Oct 22, 2026
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
34 changes: 27 additions & 7 deletions deeplabcut/pose_estimation_pytorch/apis/videos.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,13 @@ def video_inference(
if shelf_writer is not None:
shelf_writer.close()

if shelf_writer is None and len(predictions) != n_frames:
if shelf_writer is None and len(predictions) == 0 and n_frames > 0:
logging.warning(
"No predictions were produced for this video. This can happen if no "
"animals were detected in any frame. If that is unexpected, check "
"model performance and verify that the video can be read correctly."
)
elif shelf_writer is None and len(predictions) != n_frames:
tip_url = "https://deeplabcut.github.io/DeepLabCut/docs/recipes/io.html"
header = "#tips-on-video-re-encoding-and-preprocessing"
logging.warning(
Expand Down Expand Up @@ -718,11 +724,6 @@ def create_df_from_prediction(
output_prefix: str | Path,
save_as_csv: bool = False,
) -> pd.DataFrame:
pred_bodyparts = np.stack([p["bodyparts"][..., :3] for p in predictions])
pred_unique_bodyparts = None
if len(predictions) > 0 and "unique_bodyparts" in predictions[0]:
pred_unique_bodyparts = np.stack([p["unique_bodyparts"] for p in predictions])

output_h5 = Path(output_path) / f"{output_prefix}.h5"
output_pkl = Path(output_path) / f"{output_prefix}_full.pickle"

Expand All @@ -741,6 +742,18 @@ def create_df_from_prediction(
cols_names.insert(1, "individuals")

results_df_index = pd.MultiIndex.from_product(cols, names=cols_names)
if not predictions:
df = pd.DataFrame(index=range(0), columns=results_df_index)
Comment thread
deruyter92 marked this conversation as resolved.
Resolved
df.to_hdf(output_h5, key="df_with_missing", format="table", mode="w")
Comment thread
deruyter92 marked this conversation as resolved.
Resolved
if save_as_csv:
df.to_csv(output_h5.with_suffix(".csv"))
return df

pred_bodyparts = np.stack([p["bodyparts"][..., :3] for p in predictions])
pred_unique_bodyparts = None
if "unique_bodyparts" in predictions[0]:
pred_unique_bodyparts = np.stack([p["unique_bodyparts"] for p in predictions])

pred_bodyparts = pred_bodyparts[:, :n_individuals]
df = pd.DataFrame(
pred_bodyparts.reshape((len(pred_bodyparts), -1)),
Expand Down Expand Up @@ -881,11 +894,18 @@ def _generate_metadata(
return {"data": metadata}


def _frame_key_width(num_frames: int) -> int:
"""Returns the zero-fill width for frame keys."""
if num_frames <= 1:
return 1
return int(np.ceil(np.log10(num_frames)))


def _generate_output_data(
pose_config: dict,
predictions: list[dict[str, np.ndarray]],
) -> dict:
str_width = int(np.ceil(np.log10(len(predictions))))
str_width = _frame_key_width(len(predictions))
output = {
"metadata": {
"nms radius": pose_config.get("nmsradius"),
Expand Down
87 changes: 87 additions & 0 deletions tests/pose_estimation_pytorch/apis/test_videos.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
from pathlib import Path
from unittest.mock import Mock

import pandas as pd

import deeplabcut.pose_estimation_pytorch.apis.videos as videos

POSE_CFG = {
"all_joints": [[0], [1], [2]],
"all_joints_names": ["snout", "leftear", "rightear"],
"nmsradius": 5,
"minconfidence": 0.1,
"sigma": 1,
}


def test_generate_output_data_handles_empty_predictions():
output = videos._generate_output_data(POSE_CFG, [])

assert output == {
"metadata": {
"nms radius": 5,
"minimal confidence": 0.1,
"sigma": 1,
"PAFgraph": None,
"PAFinds": [],
"all_joints": [[0], [1], [2]],
"all_joints_names": ["snout", "leftear", "rightear"],
"nframes": 0,
"key_str_width": 1,
}
}
Comment thread
Copilot marked this conversation as resolved.
Resolved


def test_create_df_from_prediction_handles_empty_predictions(monkeypatch, tmp_path):
writes: list[tuple[Path, str, str, str]] = []

def _fake_to_hdf(self, path_or_buf, key, format, mode):
writes.append((Path(path_or_buf), key, format, mode))

monkeypatch.setattr(pd.DataFrame, "to_hdf", _fake_to_hdf)

df = videos.create_df_from_prediction(
predictions=[],
dlc_scorer="DLC_test",
multi_animal=False,
model_cfg={
"metadata": {
"bodyparts": ["snout", "leftear", "rightear"],
"unique_bodyparts": [],
"individuals": ["animal_0"],
}
},
output_path=tmp_path,
output_prefix="video",
save_as_csv=False,
)

assert df.empty
assert list(df.columns.names) == ["scorer", "bodyparts", "coords"]
assert writes == [(tmp_path / "video.h5", "df_with_missing", "table", "w")]


def test_video_inference_warns_distinctly_for_zero_predictions(monkeypatch, caplog):
class FakeVideoIterator:
def __init__(self, video_path, cropping=None):
self.video_path = video_path
self.fps = 25
self.dimensions = (640, 480)

def get_n_frames(self, robust=False):
return 3

def __iter__(self):
return iter(())

monkeypatch.setattr(videos, "VideoIterator", FakeVideoIterator)
pose_runner = Mock()
pose_runner.batch_size = 2
pose_runner.inference.return_value = []

with caplog.at_level("WARNING"):
predictions = videos.video_inference("video.mp4", pose_runner)

assert predictions == []
assert "No predictions were produced for this video." in caplog.text
assert "video is corrupted" not in caplog.text
Loading