From 68b3902dfbfbf43f60ad8d13663fec9e71ebf071 Mon Sep 17 00:00:00 2001 From: C-Achard Date: Fri, 13 Feb 2026 13:58:34 +0100 Subject: [PATCH 01/17] Add PyTorch support and refactor check_install Refactor check_install.py to support both PyTorch and TensorFlow backends and improve temporary file handling. Introduces TMP_DIR, MODELS_FOLDER, and separate run_pytorch_test/run_tensorflow_test helpers; PyTorch now exports and benchmarks an exported .pt checkpoint, TensorFlow model download logic is preserved. Replaces --nodisplay with --display, centralizes video download and assertions, tightens error handling for downloads, and ensures proper cleanup of temporary files. Also updates imports (urllib.error, export_modelzoo_model) and updates backend availability checks to require at least one backend. --- dlclive/check_install/check_install.py | 173 +++++++++++++++++-------- 1 file changed, 119 insertions(+), 54 deletions(-) mode change 100755 => 100644 dlclive/check_install/check_install.py diff --git a/dlclive/check_install/check_install.py b/dlclive/check_install/check_install.py old mode 100755 new mode 100644 index 8ed8bb3..65c492b --- a/dlclive/check_install/check_install.py +++ b/dlclive/check_install/check_install.py @@ -9,98 +9,163 @@ import shutil import urllib import warnings +import urllib.error from pathlib import Path from dlclibrary.dlcmodelzoo.modelzoo_download import download_huggingface_model -import dlclive +from dlclive.utils import download_file from dlclive.benchmark import benchmark_videos from dlclive.engine import Engine -from dlclive.utils import download_file, get_available_backends +from dlclive.modelzoo.pytorch_model_zoo_export import export_modelzoo_model +from dlclive.utils import get_available_backends MODEL_NAME = "superanimal_quadruped" SNAPSHOT_NAME = "snapshot-700000.pb" +TMP_DIR = Path(__file__).parent / "dlc-live-tmp" +MODELS_FOLDER = TMP_DIR / "test_models" +TORCH_MODEL = "resnet_50" +TORCH_CONFIG = { + "checkpoint": MODELS_FOLDER / f"exported_quadruped_{TORCH_MODEL}.pt", + "super_animal": "superanimal_quadruped", +} +TF_MODEL_DIR = TMP_DIR / "DLC_Dog_resnet_50_iteration-0_shuffle-0" -def main(): - parser = argparse.ArgumentParser( - description="Test DLC-Live installation by downloading and evaluating a demo DLC project!" - ) - parser.add_argument( - "--nodisplay", - action="store_false", - help="Run the test without displaying tracking", - ) - args = parser.parse_args() - display = args.nodisplay - - if not display: - print("Running without displaying video") +MODELS_FOLDER.mkdir(parents=True, exist_ok=True) - # make temporary directory - print("\nCreating temporary directory...\n") - tmp_dir = Path(dlclive.__file__).parent / "check_install" / "dlc-live-tmp" - tmp_dir.mkdir(mode=0o775, exist_ok=True) - video_file = str(tmp_dir / "dog_clip.avi") - model_dir = tmp_dir / "DLC_Dog_resnet_50_iteration-0_shuffle-0" +def run_pytorch_test(video_file: str, display: bool = False): + if Engine.PYTORCH not in get_available_backends(): + raise NotImplementedError( + "PyTorch backend is not available. Please ensure PyTorch is installed to run the PyTorch test." + ) + # Download model from the DeepLabCut Model Zoo + export_modelzoo_model( + export_path=TORCH_CONFIG["checkpoint"], + super_animal=TORCH_CONFIG["super_animal"], + model_name=TORCH_MODEL, + ) + assert TORCH_CONFIG["checkpoint"].exists(), ( + f"Failed to export {TORCH_CONFIG['super_animal']} model" + ) + assert TORCH_CONFIG["checkpoint"].stat().st_size > 0, ( + f"Exported {TORCH_CONFIG['super_animal']} model is empty" + ) + benchmark_videos( + model_path=str(TORCH_CONFIG["checkpoint"]), + model_type="pytorch", + video_path=video_file, + display=display, + resize=0.5, + pcutoff=0.25, + pixels=1000, + ) - # download dog test video from github: - # Use raw.githubusercontent.com for direct file access - if not Path(video_file).exists(): - print(f"Downloading Video to {video_file}") - url_link = "https://raw.githubusercontent.com/DeepLabCut/DeepLabCut-live/master/check_install/dog_clip.avi" - try: - download_file(url_link, video_file) - except (OSError, urllib.error.URLError) as e: - raise RuntimeError(f"Failed to download video file: {e}") from e - else: - print(f"Video file already exists at {video_file}, skipping download.") - # download model from the DeepLabCut Model Zoo +def run_tensorflow_test(video_file: str, display: bool = False): + if Engine.TENSORFLOW not in get_available_backends(): + raise NotImplementedError( + "TensorFlow backend is not available. Please ensure TensorFlow is installed to run the TensorFlow test." + ) + model_dir = TF_MODEL_DIR + model_dir.mkdir(parents=True, exist_ok=True) + assert model_dir.exists(), f"Model directory {model_dir} does not exist" if Path(model_dir / SNAPSHOT_NAME).exists(): print("Model already downloaded, using cached version") else: - print("Downloading superanimal_quadruped model from the DeepLabCut Model Zoo...") - download_huggingface_model(MODEL_NAME, model_dir) + print( + "Downloading superanimal_quadruped model from the DeepLabCut Model Zoo..." + ) + download_huggingface_model(MODEL_NAME, str(model_dir)) - # assert these things exist so we can give informative error messages - assert Path(video_file).exists(), f"Missing video file {video_file}" - assert Path(model_dir / SNAPSHOT_NAME).exists(), f"Missing model file {model_dir / SNAPSHOT_NAME}" + assert Path(model_dir / SNAPSHOT_NAME).exists(), ( + f"Missing model file {model_dir / SNAPSHOT_NAME}" + ) - # run benchmark videos - print("\n Running inference...\n") benchmark_videos( model_path=str(model_dir), - model_type="base" if Engine.from_model_path(model_dir) == Engine.TENSORFLOW else "pytorch", + model_type="base", video_path=video_file, display=display, resize=0.5, pcutoff=0.25, + pixels=1000, ) - # deleting temporary files - print("\n Deleting temporary files...\n") + +def main(): + tmp_dir = None try: - shutil.rmtree(tmp_dir) - except PermissionError: - warnings.warn( - f"Could not delete temporary directory {str(tmp_dir)} due to a permissions error.", - stacklevel=2, + parser = argparse.ArgumentParser( + description="Test DLC-Live installation by downloading and evaluating a demo DLC project!" ) + parser.add_argument( + "--display", + action="store_true", + help="Run the test and display tracking", + ) + args = parser.parse_args() + display = args.display + + if not display: + print("Running without displaying video") + + # make temporary directory + print("\nCreating temporary directory...\n") + tmp_dir = TMP_DIR + tmp_dir.mkdir(mode=0o775, exist_ok=True) + + video_file = str(tmp_dir / "dog_clip.avi") + + # download dog test video from github: + # Use raw.githubusercontent.com for direct file access + if not Path(video_file).exists(): + print(f"Downloading Video to {video_file}") + url_link = "https://raw.githubusercontent.com/DeepLabCut/DeepLabCut-live/master/check_install/dog_clip.avi" + try: + download_file(url_link, video_file) + except (urllib.error.URLError, IOError) as e: + raise RuntimeError(f"Failed to download video file: {e}") from e + else: + print(f"Video file already exists at {video_file}, skipping download.") + + # assert these things exist so we can give informative error messages + assert Path(video_file).exists(), f"Missing video file {video_file}" + + for backend in get_available_backends(): + if backend == Engine.PYTORCH: + print("\nRunning PyTorch test...\n") + run_pytorch_test(video_file, display=display) + elif backend == Engine.TENSORFLOW: + print("\nRunning TensorFlow test...\n") + run_tensorflow_test(video_file, display=display) + else: + warnings.warn( + f"Unrecognized backend {backend}, skipping...", UserWarning + ) + + finally: + # deleting temporary files + print("\n Deleting temporary files...\n") + try: + if tmp_dir is not None and tmp_dir.exists(): + shutil.rmtree(tmp_dir) + except PermissionError: + warnings.warn( + f"Could not delete temporary directory {str(tmp_dir)} due to a permissions error, but otherwise dlc-live seems to be working fine!" + ) - print("\nDone!\n") + print("\nDone!\n") if __name__ == "__main__": # Get available backends (emits a warning if neither TensorFlow nor PyTorch is installed) available_backends: list[Engine] = get_available_backends() print(f"Available backends: {[b.value for b in available_backends]}") - - # TODO: JR add support for PyTorch in check_install.py (requires some exported pytorch model to be downloaded) - if Engine.TENSORFLOW not in available_backends: + if len(available_backends) == 0: raise NotImplementedError( - "TensorFlow is not installed. Currently check_install.py only supports testing the TensorFlow installation." + "Neither TensorFlow nor PyTorch is installed. Please install at least one of these frameworks to run the installation test." ) main() From 89ba52e22bc8a756126b7b29de7e7438024f90b1 Mon Sep 17 00:00:00 2001 From: C-Achard Date: Fri, 13 Feb 2026 13:59:56 +0100 Subject: [PATCH 02/17] Add single_animal option to benchmark_videos Expose a single_animal flag on benchmark_videos (default False) and forward it to the underlying analysis call. This allows benchmarking to run in single-animal mode when using DeepLabCut-live exported models. --- dlclive/benchmark.py | 85 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 66 insertions(+), 19 deletions(-) diff --git a/dlclive/benchmark.py b/dlclive/benchmark.py index fd6dbb3..3eaf864 100644 --- a/dlclive/benchmark.py +++ b/dlclive/benchmark.py @@ -13,9 +13,10 @@ import sys import time import warnings -from pathlib import Path from typing import TYPE_CHECKING - +from pathlib import Path +import argparse +import os import colorcet as cc import cv2 import numpy as np @@ -24,14 +25,16 @@ from pip._internal.operations import freeze from tqdm import tqdm +from dlclive import DLCLive +from dlclive import VERSION from dlclive.engine import Engine from dlclive.utils import decode_fourcc -from .dlclive import DLCLive -from .version import VERSION - if TYPE_CHECKING: - import tensorflow + try: + import tensorflow + except ImportError: + tensorflow = None def download_benchmarking_data( @@ -56,16 +59,20 @@ def download_benchmarking_data( print(f"{zip_path} already exists. Skipping download.") else: + def show_progress(count, block_size, total_size): pbar.update(block_size) print(f"Downloading the benchmarking data from {url} ...") pbar = tqdm(unit="B", total=0, position=0, desc="Downloading") - filename, _ = urllib.request.urlretrieve(url, filename=zip_path, reporthook=show_progress) + filename, _ = urllib.request.urlretrieve( + url, filename=zip_path, reporthook=show_progress + ) pbar.close() print(f"Extracting {zip_path} to {target_dir} ...") + with zipfile.ZipFile(zip_path, "r") as zip_ref: with zipfile.ZipFile(zip_path, "r") as zip_ref: zip_ref.extractall(target_dir) @@ -88,6 +95,7 @@ def benchmark_videos( cmap="bmy", save_poses=False, save_video=False, + single_animal=False, ): """Analyze videos using DeepLabCut-live exported models. Analyze multiple videos and/or multiple options for the size of the video @@ -187,6 +195,7 @@ def benchmark_videos( for i in range(len(resize)): print(f"\nRun {i + 1} / {len(resize)}\n") + print(f"\nRun {i + 1} / {len(resize)}\n") this_inf_times, this_im_size, meta = benchmark( model_path=model_path, @@ -206,6 +215,7 @@ def benchmark_videos( save_poses=save_poses, save_video=save_video, save_dir=output, + single_animal=single_animal, ) inf_times.append(this_inf_times) @@ -271,7 +281,7 @@ def get_system_info() -> dict: dev_type = "GPU" dev = [torch.cuda.get_device_name(torch.cuda.current_device())] else: - from cpuinfo import get_cpu_info + from cpuinfo import get_cpu_info # noqa: F401 dev_type = "CPU" dev = get_cpu_info() @@ -289,6 +299,7 @@ def get_system_info() -> dict: } +def save_inf_times(sys_info, inf_times, im_size, model=None, meta=None, output=None): def save_inf_times(sys_info, inf_times, im_size, model=None, meta=None, output=None): """Save inference time data collected using :function:`benchmark` with system information to a pickle file. This is primarily used through :function:`benchmark_videos` @@ -358,6 +369,7 @@ def save_inf_times(sys_info, inf_times, im_size, model=None, meta=None, output=N return True + def benchmark( model_path: str, model_type: str, @@ -371,6 +383,8 @@ def benchmark( dynamic: tuple[bool, float, int] = (False, 0.5, 10), n_frames: int = 1000, print_rate: bool = False, + n_frames: int = 1000, + print_rate: bool = False, precision: str = "FP32", display: bool = True, pcutoff: float = 0.5, @@ -455,7 +469,10 @@ def benchmark( if not cap.isOpened(): print(f"Error: Could not open video file {video_path}") return - im_size = (int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))) + im_size = ( + int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), + int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)), + ) if pixels is not None: resize = np.sqrt(pixels / (im_size[0] * im_size[1])) @@ -512,7 +529,9 @@ def benchmark( frame_index = 0 total_n_frames = cap.get(cv2.CAP_PROP_FRAME_COUNT) - n_frames = int(n_frames if (n_frames > 0) and n_frames < total_n_frames else total_n_frames) + n_frames = int( + n_frames if (n_frames > 0) and n_frames < total_n_frames else total_n_frames + ) iterator = range(n_frames) if print_rate or display else tqdm(range(n_frames)) for _ in iterator: ret, frame = cap.read() @@ -527,6 +546,7 @@ def benchmark( start_time = time.perf_counter() if frame_index == 0: pose = dlc_live.init_inference(frame) # Loads model + pose = dlc_live.init_inference(frame) # Loads model else: pose = dlc_live.get_pose(frame) @@ -535,7 +555,9 @@ def benchmark( times.append(inf_time) if print_rate: - print(f"Inference rate = {1 / inf_time:.3f} FPS", end="\r", flush=True) + print( + "Inference rate = {:.3f} FPS".format(1 / inf_time), end="\r", flush=True + ) if save_video: draw_pose_and_write( @@ -548,14 +570,18 @@ def benchmark( display_radius=display_radius, draw_keypoint_names=draw_keypoint_names, vwriter=vwriter, + vwriter=vwriter, ) frame_index += 1 if print_rate: - print(f"Mean inference rate: {np.mean(1 / np.array(times)[1:]):.3f} FPS") + print( + "Mean inference rate: {:.3f} FPS".format(np.mean(1 / np.array(times)[1:])) + ) metadata = _get_metadata(video_path=video_path, cap=cap, dlc_live=dlc_live) + metadata = _get_metadata(video_path=video_path, cap=cap, dlc_live=dlc_live) cap.release() @@ -570,7 +596,9 @@ def benchmark( else: individuals = [] n_individuals = len(individuals) or 1 - save_poses_to_files(video_path, save_dir, n_individuals, bodyparts, poses, timestamp=timestamp) + save_poses_to_files( + video_path, save_dir, n_individuals, bodyparts, poses, timestamp=timestamp + ) return times, im_size, metadata @@ -583,6 +611,13 @@ def setup_video_writer( cmap: str, fps: float, frame_size: tuple[int, int], + video_path: str, + save_dir: str, + timestamp: str, + num_keypoints: int, + cmap: str, + fps: float, + frame_size: tuple[int, int], ): # Set colors and convert to RGB cmap_colors = getattr(cc, cmap) @@ -591,7 +626,9 @@ def setup_video_writer( # Define output video path video_path = Path(video_path) video_name = video_path.stem # filename without extension - output_video_path = Path(save_dir) / f"{video_name}_DLCLIVE_LABELLED_{timestamp}.mp4" + output_video_path = ( + Path(save_dir) / f"{video_name}_DLCLIVE_LABELLED_{timestamp}.mp4" + ) # Get video writer setup fourcc = cv2.VideoWriter_fourcc(*"mp4v") @@ -605,6 +642,7 @@ def setup_video_writer( return colors, vwriter + def draw_pose_and_write( frame: np.ndarray, pose: np.ndarray, @@ -621,7 +659,9 @@ def draw_pose_and_write( if resize is not None and resize != 1.0: # Resize the frame - frame = cv2.resize(frame, None, fx=resize, fy=resize, interpolation=cv2.INTER_LINEAR) + frame = cv2.resize( + frame, None, fx=resize, fy=resize, interpolation=cv2.INTER_LINEAR + ) # Scale pose coordinates pose = pose.copy() @@ -655,6 +695,7 @@ def draw_pose_and_write( vwriter.write(image=frame) +def _get_metadata(video_path: str, cap: cv2.VideoCapture, dlc_live: DLCLive): def _get_metadata(video_path: str, cap: cv2.VideoCapture, dlc_live: DLCLive): try: fourcc = decode_fourcc(cap.get(cv2.CAP_PROP_FOURCC)) @@ -692,7 +733,9 @@ def _get_metadata(video_path: str, cap: cv2.VideoCapture, dlc_live: DLCLive): return meta -def save_poses_to_files(video_path, save_dir, n_individuals, bodyparts, poses, timestamp): +def save_poses_to_files( + video_path, save_dir, n_individuals, bodyparts, poses, timestamp +): """ Saves the detected keypoint poses from the video to CSV and HDF5 files. @@ -713,7 +756,7 @@ def save_poses_to_files(video_path, save_dir, n_individuals, bodyparts, poses, t ------- None """ - import pandas as pd # noqa E402 + import pandas as pd # noqa: F401 base_filename = Path(video_path).stem save_dir = Path(save_dir) @@ -728,7 +771,8 @@ def save_poses_to_files(video_path, save_dir, n_individuals, bodyparts, poses, t else: individuals = [f"individual_{i}" for i in range(n_individuals)] pdindex = pd.MultiIndex.from_product( - [individuals, bodyparts, ["x", "y", "likelihood"]], names=["individuals", "bodyparts", "coords"] + [individuals, bodyparts, ["x", "y", "likelihood"]], + names=["individuals", "bodyparts", "coords"], ) pose_df = pd.DataFrame(flattened_poses, columns=pdindex) @@ -737,6 +781,7 @@ def save_poses_to_files(video_path, save_dir, n_individuals, bodyparts, poses, t pose_df.to_csv(csv_save_path, index=False) + def _create_poses_np_array(n_individuals: int, bodyparts: list, poses: list): # Create numpy array with poses: max_frame = max(p["frame"] for p in poses) @@ -749,7 +794,9 @@ def _create_poses_np_array(n_individuals: int, bodyparts: list, poses: list): if pose.ndim == 2: pose = pose[np.newaxis, :, :] padded_pose = np.full(pose_target_shape, np.nan) - slices = tuple(slice(0, min(pose.shape[i], pose_target_shape[i])) for i in range(3)) + slices = tuple( + slice(0, min(pose.shape[i], pose_target_shape[i])) for i in range(3) + ) padded_pose[slices] = pose[slices] poses_array[frame] = padded_pose From 4d8019e1f9fdac709d72aaf7345e9f3e8997819b Mon Sep 17 00:00:00 2001 From: C-Achard Date: Fri, 13 Feb 2026 14:07:12 +0100 Subject: [PATCH 03/17] Enable display for dlc-live-test in CI Update .github/workflows/testing.yml to run the Model Benchmark Test with --display instead of --nodisplay. This allows dlc-live-test to run tests that require a display (e.g., visual/benchmarking checks) in the CI workflow. --- .github/workflows/testing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 28d8b6c..159ec03 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -69,7 +69,7 @@ jobs: shell: bash - name: Run Model Benchmark Test - run: uv run dlc-live-test --nodisplay + run: uv run dlc-live-test --display - name: Run DLC Live Unit Tests run: uv run pytest From f3c8a761e49801920ab10a20e3f227fe7974acea Mon Sep 17 00:00:00 2001 From: C-Achard Date: Fri, 13 Feb 2026 14:07:34 +0100 Subject: [PATCH 04/17] Update testing.yml --- .github/workflows/testing.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 159ec03..62b5956 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -69,7 +69,7 @@ jobs: shell: bash - name: Run Model Benchmark Test - run: uv run dlc-live-test --display + run: uv run dlc-live-test - name: Run DLC Live Unit Tests run: uv run pytest From 2a215a923a69ccdbb04e0d4eccd7ba76e3ef03fe Mon Sep 17 00:00:00 2001 From: C-Achard Date: Fri, 13 Feb 2026 14:16:17 +0100 Subject: [PATCH 05/17] Improve backend test robustness and imports Refactor test startup and error handling for check_install and simplify a type-only import. - dlclive/benchmark.py: replace the try/except tensorflow import under TYPE_CHECKING with a direct import (type ignored) to simplify typing logic. - dlclive/check_install/check_install.py: - Defer importing export_modelzoo_model into run_pytorch_test to avoid importing heavy modules unless PyTorch test runs. - Move MODELS_FOLDER.mkdir to after temporary directory creation. - Add a --nodisplay flag and set default for --display to False so CLI can explicitly disable display. - Comment out resize parameters in test calls and remove an unnecessary model_dir.exists() assertion. - Wrap per-backend test runs in try/except, collect backend failures, allow other backends to continue, and raise an aggregated RuntimeError if all backend tests fail. These changes improve robustness when some backends fail or are unavailable and reduce unnecessary imports during initial checks. --- dlclive/benchmark.py | 5 +-- dlclive/check_install/check_install.py | 53 +++++++++++++++++++------- 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/dlclive/benchmark.py b/dlclive/benchmark.py index 3eaf864..2677e81 100644 --- a/dlclive/benchmark.py +++ b/dlclive/benchmark.py @@ -31,10 +31,7 @@ from dlclive.utils import decode_fourcc if TYPE_CHECKING: - try: - import tensorflow - except ImportError: - tensorflow = None + import tensorflow # type: ignore def download_benchmarking_data( diff --git a/dlclive/check_install/check_install.py b/dlclive/check_install/check_install.py index 65c492b..07f2a0e 100644 --- a/dlclive/check_install/check_install.py +++ b/dlclive/check_install/check_install.py @@ -17,7 +17,6 @@ from dlclive.utils import download_file from dlclive.benchmark import benchmark_videos from dlclive.engine import Engine -from dlclive.modelzoo.pytorch_model_zoo_export import export_modelzoo_model from dlclive.utils import get_available_backends MODEL_NAME = "superanimal_quadruped" @@ -32,10 +31,10 @@ } TF_MODEL_DIR = TMP_DIR / "DLC_Dog_resnet_50_iteration-0_shuffle-0" -MODELS_FOLDER.mkdir(parents=True, exist_ok=True) - def run_pytorch_test(video_file: str, display: bool = False): + from dlclive.modelzoo.pytorch_model_zoo_export import export_modelzoo_model + if Engine.PYTORCH not in get_available_backends(): raise NotImplementedError( "PyTorch backend is not available. Please ensure PyTorch is installed to run the PyTorch test." @@ -57,7 +56,7 @@ def run_pytorch_test(video_file: str, display: bool = False): model_type="pytorch", video_path=video_file, display=display, - resize=0.5, + # resize=0.5, pcutoff=0.25, pixels=1000, ) @@ -70,7 +69,6 @@ def run_tensorflow_test(video_file: str, display: bool = False): ) model_dir = TF_MODEL_DIR model_dir.mkdir(parents=True, exist_ok=True) - assert model_dir.exists(), f"Model directory {model_dir} does not exist" if Path(model_dir / SNAPSHOT_NAME).exists(): print("Model already downloaded, using cached version") else: @@ -88,7 +86,7 @@ def run_tensorflow_test(video_file: str, display: bool = False): model_type="base", video_path=video_file, display=display, - resize=0.5, + # resize=0.5, pcutoff=0.25, pixels=1000, ) @@ -103,8 +101,16 @@ def main(): parser.add_argument( "--display", action="store_true", + default=False, help="Run the test and display tracking", ) + parser.add_argument( + "--nodisplay", + action="store_false", + dest="display", + help=argparse.SUPPRESS, + ) + args = parser.parse_args() display = args.display @@ -115,6 +121,7 @@ def main(): print("\nCreating temporary directory...\n") tmp_dir = TMP_DIR tmp_dir.mkdir(mode=0o775, exist_ok=True) + MODELS_FOLDER.mkdir(parents=True, exist_ok=True) video_file = str(tmp_dir / "dog_clip.avi") @@ -132,19 +139,37 @@ def main(): # assert these things exist so we can give informative error messages assert Path(video_file).exists(), f"Missing video file {video_file}" + backend_failures = {} + any_backend_succeeded = False for backend in get_available_backends(): - if backend == Engine.PYTORCH: - print("\nRunning PyTorch test...\n") - run_pytorch_test(video_file, display=display) - elif backend == Engine.TENSORFLOW: - print("\nRunning TensorFlow test...\n") - run_tensorflow_test(video_file, display=display) - else: + try: + if backend == Engine.PYTORCH: + print("\nRunning PyTorch test...\n") + run_pytorch_test(video_file, display=display) + any_backend_succeeded = True + elif backend == Engine.TENSORFLOW: + print("\nRunning TensorFlow test...\n") + run_tensorflow_test(video_file, display=display) + any_backend_succeeded = True + else: + warnings.warn( + f"Unrecognized backend {backend}, skipping...", UserWarning + ) + except Exception as e: + backend_failures[backend] = e warnings.warn( - f"Unrecognized backend {backend}, skipping...", UserWarning + f"Error while running test for backend {backend}: {e}. " + "Continuing to test other available backends.", + UserWarning, ) + if not any_backend_succeeded and backend_failures: + failure_messages = "; ".join( + f"{b}: {exc}" for b, exc in backend_failures.items() + ) + raise RuntimeError(f"All backend tests failed. Details: {failure_messages}") + finally: # deleting temporary files print("\n Deleting temporary files...\n") From 4f946390b9c039deb989afb88419a3410148029f Mon Sep 17 00:00:00 2001 From: C-Achard Date: Tue, 17 Feb 2026 11:36:25 +0100 Subject: [PATCH 06/17] Refactor check_install tests and model paths Rename MODELS_FOLDER to MODELS_DIR and update references (TORCH_CONFIG checkpoint and TF_MODEL_DIR) for clearer naming. Change missing-backend errors from NotImplementedError to ImportError to better reflect installation issues. Simplify main(): consolidate arg parsing, consistently create TMP_DIR and MODELS_DIR, and add backend_results tracking to report per-backend SUCCESS/ERROR statuses with a printed summary. Improve error recording for backend failures and adjust cleanup check when removing the temporary directory. --- dlclive/check_install/check_install.py | 95 +++++++++++++++----------- 1 file changed, 57 insertions(+), 38 deletions(-) diff --git a/dlclive/check_install/check_install.py b/dlclive/check_install/check_install.py index 07f2a0e..9c63e6b 100644 --- a/dlclive/check_install/check_install.py +++ b/dlclive/check_install/check_install.py @@ -23,20 +23,20 @@ SNAPSHOT_NAME = "snapshot-700000.pb" TMP_DIR = Path(__file__).parent / "dlc-live-tmp" -MODELS_FOLDER = TMP_DIR / "test_models" +MODELS_DIR = TMP_DIR / "test_models" TORCH_MODEL = "resnet_50" TORCH_CONFIG = { - "checkpoint": MODELS_FOLDER / f"exported_quadruped_{TORCH_MODEL}.pt", + "checkpoint": MODELS_DIR / f"exported_quadruped_{TORCH_MODEL}.pt", "super_animal": "superanimal_quadruped", } -TF_MODEL_DIR = TMP_DIR / "DLC_Dog_resnet_50_iteration-0_shuffle-0" +TF_MODEL_DIR = MODELS_DIR / "DLC_Dog_resnet_50_iteration-0_shuffle-0" def run_pytorch_test(video_file: str, display: bool = False): from dlclive.modelzoo.pytorch_model_zoo_export import export_modelzoo_model if Engine.PYTORCH not in get_available_backends(): - raise NotImplementedError( + raise ImportError( "PyTorch backend is not available. Please ensure PyTorch is installed to run the PyTorch test." ) # Download model from the DeepLabCut Model Zoo @@ -64,7 +64,7 @@ def run_pytorch_test(video_file: str, display: bool = False): def run_tensorflow_test(video_file: str, display: bool = False): if Engine.TENSORFLOW not in get_available_backends(): - raise NotImplementedError( + raise ImportError( "TensorFlow backend is not available. Please ensure TensorFlow is installed to run the TensorFlow test." ) model_dir = TF_MODEL_DIR @@ -93,38 +93,39 @@ def run_tensorflow_test(video_file: str, display: bool = False): def main(): - tmp_dir = None - try: - parser = argparse.ArgumentParser( - description="Test DLC-Live installation by downloading and evaluating a demo DLC project!" - ) - parser.add_argument( - "--display", - action="store_true", - default=False, - help="Run the test and display tracking", - ) - parser.add_argument( - "--nodisplay", - action="store_false", - dest="display", - help=argparse.SUPPRESS, - ) + backend_results = {} + + parser = argparse.ArgumentParser( + description="Test DLC-Live installation by downloading and evaluating a demo DLC project!" + ) + parser.add_argument( + "--display", + action="store_true", + default=False, + help="Run the test and display tracking", + ) + parser.add_argument( + "--nodisplay", + action="store_false", + dest="display", + help=argparse.SUPPRESS, + ) - args = parser.parse_args() - display = args.display + args = parser.parse_args() + display = args.display - if not display: - print("Running without displaying video") + if not display: + print("Running without displaying video") - # make temporary directory - print("\nCreating temporary directory...\n") - tmp_dir = TMP_DIR - tmp_dir.mkdir(mode=0o775, exist_ok=True) - MODELS_FOLDER.mkdir(parents=True, exist_ok=True) + # make temporary directory + print("\nCreating temporary directory...\n") + tmp_dir = TMP_DIR + tmp_dir.mkdir(mode=0o775, exist_ok=True) + MODELS_DIR.mkdir(parents=True, exist_ok=True) - video_file = str(tmp_dir / "dog_clip.avi") + video_file = str(tmp_dir / "dog_clip.avi") + try: # download dog test video from github: # Use raw.githubusercontent.com for direct file access if not Path(video_file).exists(): @@ -148,15 +149,23 @@ def main(): print("\nRunning PyTorch test...\n") run_pytorch_test(video_file, display=display) any_backend_succeeded = True + backend_results["pytorch"] = ("SUCCESS", None) elif backend == Engine.TENSORFLOW: print("\nRunning TensorFlow test...\n") run_tensorflow_test(video_file, display=display) any_backend_succeeded = True + backend_results["tensorflow"] = ("SUCCESS", None) else: warnings.warn( f"Unrecognized backend {backend}, skipping...", UserWarning ) except Exception as e: + backend_name = ( + "pytorch" if backend == Engine.PYTORCH else + "tensorflow" if backend == Engine.TENSORFLOW else + str(backend) + ) + backend_results[backend_name] = ("ERROR", str(e)) backend_failures[backend] = e warnings.warn( f"Error while running test for backend {backend}: {e}. " @@ -164,17 +173,27 @@ def main(): UserWarning, ) - if not any_backend_succeeded and backend_failures: - failure_messages = "; ".join( - f"{b}: {exc}" for b, exc in backend_failures.items() - ) - raise RuntimeError(f"All backend tests failed. Details: {failure_messages}") + print("\n---\nBackend test summary:") + for name in ("tensorflow", "pytorch"): + status, _ = backend_results.get(name, ("SKIPPED", None)) + print(f"{name:<11} [{status}]") + print("---") + for name, (status, error) in backend_results.items(): + if status == "ERROR": + print(f"{name.capitalize()} error:\n{error}\n") + + if not any_backend_succeeded and backend_failures: + failure_messages = "; ".join( + f"{b}: {exc}" for b, exc in backend_failures.items() + ) + raise RuntimeError(f"All backend tests failed. Details: {failure_messages}") + finally: # deleting temporary files print("\n Deleting temporary files...\n") try: - if tmp_dir is not None and tmp_dir.exists(): + if tmp_dir.exists(): shutil.rmtree(tmp_dir) except PermissionError: warnings.warn( From 2b8a20ea3a12587522384842a1a99e28d4cc0aae Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 17 Feb 2026 12:08:27 +0100 Subject: [PATCH 07/17] Update dlclive/benchmark.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- dlclive/benchmark.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dlclive/benchmark.py b/dlclive/benchmark.py index 2677e81..37efe69 100644 --- a/dlclive/benchmark.py +++ b/dlclive/benchmark.py @@ -278,7 +278,7 @@ def get_system_info() -> dict: dev_type = "GPU" dev = [torch.cuda.get_device_name(torch.cuda.current_device())] else: - from cpuinfo import get_cpu_info # noqa: F401 + from cpuinfo import get_cpu_info dev_type = "CPU" dev = get_cpu_info() From e081e6f7ce8cef3b9f40e9c5f7c601a89de87dba Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 17 Feb 2026 12:10:26 +0100 Subject: [PATCH 08/17] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- dlclive/benchmark.py | 2 +- dlclive/check_install/check_install.py | 28 +++++++++++++------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/dlclive/benchmark.py b/dlclive/benchmark.py index 37efe69..c00ac57 100644 --- a/dlclive/benchmark.py +++ b/dlclive/benchmark.py @@ -753,7 +753,7 @@ def save_poses_to_files( ------- None """ - import pandas as pd # noqa: F401 + import pandas as pd base_filename = Path(video_path).stem save_dir = Path(save_dir) diff --git a/dlclive/check_install/check_install.py b/dlclive/check_install/check_install.py index 9c63e6b..ac06d04 100644 --- a/dlclive/check_install/check_install.py +++ b/dlclive/check_install/check_install.py @@ -173,20 +173,20 @@ def main(): UserWarning, ) - print("\n---\nBackend test summary:") - for name in ("tensorflow", "pytorch"): - status, _ = backend_results.get(name, ("SKIPPED", None)) - print(f"{name:<11} [{status}]") - print("---") - for name, (status, error) in backend_results.items(): - if status == "ERROR": - print(f"{name.capitalize()} error:\n{error}\n") - - if not any_backend_succeeded and backend_failures: - failure_messages = "; ".join( - f"{b}: {exc}" for b, exc in backend_failures.items() - ) - raise RuntimeError(f"All backend tests failed. Details: {failure_messages}") + print("\n---\nBackend test summary:") + for name in ("tensorflow", "pytorch"): + status, _ = backend_results.get(name, ("SKIPPED", None)) + print(f"{name:<11} [{status}]") + print("---") + for name, (status, error) in backend_results.items(): + if status == "ERROR": + print(f"{name.capitalize()} error:\n{error}\n") + + if not any_backend_succeeded and backend_failures: + failure_messages = "; ".join( + f"{b}: {exc}" for b, exc in backend_failures.items() + ) + raise RuntimeError(f"All backend tests failed. Details: {failure_messages}") finally: From f6a737d2174055f3ea556884fd76518b31f02f7a Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Thu, 19 Feb 2026 18:50:00 +0000 Subject: [PATCH 09/17] Update dlclive/check_install/check_install.py Co-authored-by: Jaap de Ruyter van Steveninck <32810691+deruyter92@users.noreply.github.com> --- dlclive/check_install/check_install.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dlclive/check_install/check_install.py b/dlclive/check_install/check_install.py index ac06d04..35cb730 100644 --- a/dlclive/check_install/check_install.py +++ b/dlclive/check_install/check_install.py @@ -200,7 +200,6 @@ def main(): f"Could not delete temporary directory {str(tmp_dir)} due to a permissions error, but otherwise dlc-live seems to be working fine!" ) - print("\nDone!\n") if __name__ == "__main__": From 6032bb54f1a5ea722e0d46118a6a075e4d4c1b24 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Fri, 20 Feb 2026 09:20:13 +0100 Subject: [PATCH 10/17] Apply suggestions from code review Co-authored-by: Jaap de Ruyter van Steveninck <32810691+deruyter92@users.noreply.github.com> --- dlclive/check_install/check_install.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/dlclive/check_install/check_install.py b/dlclive/check_install/check_install.py index 35cb730..327117d 100644 --- a/dlclive/check_install/check_install.py +++ b/dlclive/check_install/check_install.py @@ -45,12 +45,14 @@ def run_pytorch_test(video_file: str, display: bool = False): super_animal=TORCH_CONFIG["super_animal"], model_name=TORCH_MODEL, ) - assert TORCH_CONFIG["checkpoint"].exists(), ( - f"Failed to export {TORCH_CONFIG['super_animal']} model" - ) - assert TORCH_CONFIG["checkpoint"].stat().st_size > 0, ( + if not TORCH_CONFIG["checkpoint"].exists(): + raise FileNotFoundError( + f"Failed to export {TORCH_CONFIG['super_animal']} model" + ) + if TORCH_CONFIG["checkpoint"].stat().st_size == 0: + raise ValueError( f"Exported {TORCH_CONFIG['super_animal']} model is empty" - ) + ) benchmark_videos( model_path=str(TORCH_CONFIG["checkpoint"]), model_type="pytorch", @@ -139,7 +141,8 @@ def main(): print(f"Video file already exists at {video_file}, skipping download.") # assert these things exist so we can give informative error messages - assert Path(video_file).exists(), f"Missing video file {video_file}" + if not Path(video_file).exists(): + raise FileNotFoundError(f"Missing video file {video_file}") backend_failures = {} any_backend_succeeded = False @@ -197,7 +200,7 @@ def main(): shutil.rmtree(tmp_dir) except PermissionError: warnings.warn( - f"Could not delete temporary directory {str(tmp_dir)} due to a permissions error, but otherwise dlc-live seems to be working fine!" + f"Could not delete temporary directory {str(tmp_dir)} due to a permissions error." ) From dbf9840bbddd85e2cfa608d47abadd9372ca4998 Mon Sep 17 00:00:00 2001 From: C-Achard Date: Fri, 20 Feb 2026 09:23:14 +0100 Subject: [PATCH 11/17] Update check_install.py --- dlclive/check_install/check_install.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/dlclive/check_install/check_install.py b/dlclive/check_install/check_install.py index 327117d..d362f64 100644 --- a/dlclive/check_install/check_install.py +++ b/dlclive/check_install/check_install.py @@ -50,15 +50,12 @@ def run_pytorch_test(video_file: str, display: bool = False): f"Failed to export {TORCH_CONFIG['super_animal']} model" ) if TORCH_CONFIG["checkpoint"].stat().st_size == 0: - raise ValueError( - f"Exported {TORCH_CONFIG['super_animal']} model is empty" - ) + raise ValueError(f"Exported {TORCH_CONFIG['super_animal']} model is empty") benchmark_videos( model_path=str(TORCH_CONFIG["checkpoint"]), model_type="pytorch", video_path=video_file, display=display, - # resize=0.5, pcutoff=0.25, pixels=1000, ) @@ -88,7 +85,6 @@ def run_tensorflow_test(video_file: str, display: bool = False): model_type="base", video_path=video_file, display=display, - # resize=0.5, pcutoff=0.25, pixels=1000, ) @@ -96,7 +92,7 @@ def run_tensorflow_test(video_file: str, display: bool = False): def main(): backend_results = {} - + parser = argparse.ArgumentParser( description="Test DLC-Live installation by downloading and evaluating a demo DLC project!" ) @@ -164,9 +160,11 @@ def main(): ) except Exception as e: backend_name = ( - "pytorch" if backend == Engine.PYTORCH else - "tensorflow" if backend == Engine.TENSORFLOW else - str(backend) + "pytorch" + if backend == Engine.PYTORCH + else "tensorflow" + if backend == Engine.TENSORFLOW + else str(backend) ) backend_results[backend_name] = ("ERROR", str(e)) backend_failures[backend] = e @@ -191,7 +189,6 @@ def main(): ) raise RuntimeError(f"All backend tests failed. Details: {failure_messages}") - finally: # deleting temporary files print("\n Deleting temporary files...\n") @@ -204,7 +201,6 @@ def main(): ) - if __name__ == "__main__": # Get available backends (emits a warning if neither TensorFlow nor PyTorch is installed) available_backends: list[Engine] = get_available_backends() From 398eae2bcdced219b03c24b0b5988aa9bf86d336 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 10 Mar 2026 13:21:38 +0100 Subject: [PATCH 12/17] Fix rebase issues Clean up and minor fixes in dlclive/benchmark.py and dlclive/check_install/check_install.py. Changes include: reorder and consolidate imports; remove duplicated/extra blank lines and repeated statements (duplicate prints, duplicated init_inference call, duplicated function args); normalize long-line wrapping; switch some prints to f-strings; tighten exception handling for video download (catch OSError and URLError); add stacklevel to warnings.warn calls; and minor formatting improvements (single-line tuples, joined long expressions). These are non-functional refactors and small bug/safety fixes to reduce redundancy and improve diagnostics. --- dlclive/benchmark.py | 63 +++++--------------------- dlclive/check_install/check_install.py | 33 +++++--------- 2 files changed, 24 insertions(+), 72 deletions(-) diff --git a/dlclive/benchmark.py b/dlclive/benchmark.py index c00ac57..b18547b 100644 --- a/dlclive/benchmark.py +++ b/dlclive/benchmark.py @@ -13,10 +13,9 @@ import sys import time import warnings -from typing import TYPE_CHECKING from pathlib import Path -import argparse -import os +from typing import TYPE_CHECKING + import colorcet as cc import cv2 import numpy as np @@ -25,8 +24,7 @@ from pip._internal.operations import freeze from tqdm import tqdm -from dlclive import DLCLive -from dlclive import VERSION +from dlclive import VERSION, DLCLive from dlclive.engine import Engine from dlclive.utils import decode_fourcc @@ -56,20 +54,16 @@ def download_benchmarking_data( print(f"{zip_path} already exists. Skipping download.") else: - def show_progress(count, block_size, total_size): pbar.update(block_size) print(f"Downloading the benchmarking data from {url} ...") pbar = tqdm(unit="B", total=0, position=0, desc="Downloading") - filename, _ = urllib.request.urlretrieve( - url, filename=zip_path, reporthook=show_progress - ) + filename, _ = urllib.request.urlretrieve(url, filename=zip_path, reporthook=show_progress) pbar.close() print(f"Extracting {zip_path} to {target_dir} ...") - with zipfile.ZipFile(zip_path, "r") as zip_ref: with zipfile.ZipFile(zip_path, "r") as zip_ref: zip_ref.extractall(target_dir) @@ -192,7 +186,6 @@ def benchmark_videos( for i in range(len(resize)): print(f"\nRun {i + 1} / {len(resize)}\n") - print(f"\nRun {i + 1} / {len(resize)}\n") this_inf_times, this_im_size, meta = benchmark( model_path=model_path, @@ -296,7 +289,6 @@ def get_system_info() -> dict: } -def save_inf_times(sys_info, inf_times, im_size, model=None, meta=None, output=None): def save_inf_times(sys_info, inf_times, im_size, model=None, meta=None, output=None): """Save inference time data collected using :function:`benchmark` with system information to a pickle file. This is primarily used through :function:`benchmark_videos` @@ -366,7 +358,6 @@ def save_inf_times(sys_info, inf_times, im_size, model=None, meta=None, output=N return True - def benchmark( model_path: str, model_type: str, @@ -380,8 +371,6 @@ def benchmark( dynamic: tuple[bool, float, int] = (False, 0.5, 10), n_frames: int = 1000, print_rate: bool = False, - n_frames: int = 1000, - print_rate: bool = False, precision: str = "FP32", display: bool = True, pcutoff: float = 0.5, @@ -526,9 +515,7 @@ def benchmark( frame_index = 0 total_n_frames = cap.get(cv2.CAP_PROP_FRAME_COUNT) - n_frames = int( - n_frames if (n_frames > 0) and n_frames < total_n_frames else total_n_frames - ) + n_frames = int(n_frames if (n_frames > 0) and n_frames < total_n_frames else total_n_frames) iterator = range(n_frames) if print_rate or display else tqdm(range(n_frames)) for _ in iterator: ret, frame = cap.read() @@ -543,7 +530,6 @@ def benchmark( start_time = time.perf_counter() if frame_index == 0: pose = dlc_live.init_inference(frame) # Loads model - pose = dlc_live.init_inference(frame) # Loads model else: pose = dlc_live.get_pose(frame) @@ -552,9 +538,7 @@ def benchmark( times.append(inf_time) if print_rate: - print( - "Inference rate = {:.3f} FPS".format(1 / inf_time), end="\r", flush=True - ) + print(f"Inference rate = {1 / inf_time:.3f} FPS", end="\r", flush=True) if save_video: draw_pose_and_write( @@ -567,15 +551,12 @@ def benchmark( display_radius=display_radius, draw_keypoint_names=draw_keypoint_names, vwriter=vwriter, - vwriter=vwriter, ) frame_index += 1 if print_rate: - print( - "Mean inference rate: {:.3f} FPS".format(np.mean(1 / np.array(times)[1:])) - ) + print(f"Mean inference rate: {np.mean(1 / np.array(times)[1:]):.3f} FPS") metadata = _get_metadata(video_path=video_path, cap=cap, dlc_live=dlc_live) metadata = _get_metadata(video_path=video_path, cap=cap, dlc_live=dlc_live) @@ -593,9 +574,7 @@ def benchmark( else: individuals = [] n_individuals = len(individuals) or 1 - save_poses_to_files( - video_path, save_dir, n_individuals, bodyparts, poses, timestamp=timestamp - ) + save_poses_to_files(video_path, save_dir, n_individuals, bodyparts, poses, timestamp=timestamp) return times, im_size, metadata @@ -608,13 +587,6 @@ def setup_video_writer( cmap: str, fps: float, frame_size: tuple[int, int], - video_path: str, - save_dir: str, - timestamp: str, - num_keypoints: int, - cmap: str, - fps: float, - frame_size: tuple[int, int], ): # Set colors and convert to RGB cmap_colors = getattr(cc, cmap) @@ -623,9 +595,7 @@ def setup_video_writer( # Define output video path video_path = Path(video_path) video_name = video_path.stem # filename without extension - output_video_path = ( - Path(save_dir) / f"{video_name}_DLCLIVE_LABELLED_{timestamp}.mp4" - ) + output_video_path = Path(save_dir) / f"{video_name}_DLCLIVE_LABELLED_{timestamp}.mp4" # Get video writer setup fourcc = cv2.VideoWriter_fourcc(*"mp4v") @@ -639,7 +609,6 @@ def setup_video_writer( return colors, vwriter - def draw_pose_and_write( frame: np.ndarray, pose: np.ndarray, @@ -656,9 +625,7 @@ def draw_pose_and_write( if resize is not None and resize != 1.0: # Resize the frame - frame = cv2.resize( - frame, None, fx=resize, fy=resize, interpolation=cv2.INTER_LINEAR - ) + frame = cv2.resize(frame, None, fx=resize, fy=resize, interpolation=cv2.INTER_LINEAR) # Scale pose coordinates pose = pose.copy() @@ -692,7 +659,6 @@ def draw_pose_and_write( vwriter.write(image=frame) -def _get_metadata(video_path: str, cap: cv2.VideoCapture, dlc_live: DLCLive): def _get_metadata(video_path: str, cap: cv2.VideoCapture, dlc_live: DLCLive): try: fourcc = decode_fourcc(cap.get(cv2.CAP_PROP_FOURCC)) @@ -730,9 +696,7 @@ def _get_metadata(video_path: str, cap: cv2.VideoCapture, dlc_live: DLCLive): return meta -def save_poses_to_files( - video_path, save_dir, n_individuals, bodyparts, poses, timestamp -): +def save_poses_to_files(video_path, save_dir, n_individuals, bodyparts, poses, timestamp): """ Saves the detected keypoint poses from the video to CSV and HDF5 files. @@ -778,7 +742,6 @@ def save_poses_to_files( pose_df.to_csv(csv_save_path, index=False) - def _create_poses_np_array(n_individuals: int, bodyparts: list, poses: list): # Create numpy array with poses: max_frame = max(p["frame"] for p in poses) @@ -791,9 +754,7 @@ def _create_poses_np_array(n_individuals: int, bodyparts: list, poses: list): if pose.ndim == 2: pose = pose[np.newaxis, :, :] padded_pose = np.full(pose_target_shape, np.nan) - slices = tuple( - slice(0, min(pose.shape[i], pose_target_shape[i])) for i in range(3) - ) + slices = tuple(slice(0, min(pose.shape[i], pose_target_shape[i])) for i in range(3)) padded_pose[slices] = pose[slices] poses_array[frame] = padded_pose diff --git a/dlclive/check_install/check_install.py b/dlclive/check_install/check_install.py index d362f64..3bff5b3 100644 --- a/dlclive/check_install/check_install.py +++ b/dlclive/check_install/check_install.py @@ -8,16 +8,15 @@ import argparse import shutil import urllib -import warnings import urllib.error +import warnings from pathlib import Path from dlclibrary.dlcmodelzoo.modelzoo_download import download_huggingface_model -from dlclive.utils import download_file from dlclive.benchmark import benchmark_videos from dlclive.engine import Engine -from dlclive.utils import get_available_backends +from dlclive.utils import download_file, get_available_backends MODEL_NAME = "superanimal_quadruped" SNAPSHOT_NAME = "snapshot-700000.pb" @@ -46,9 +45,7 @@ def run_pytorch_test(video_file: str, display: bool = False): model_name=TORCH_MODEL, ) if not TORCH_CONFIG["checkpoint"].exists(): - raise FileNotFoundError( - f"Failed to export {TORCH_CONFIG['super_animal']} model" - ) + raise FileNotFoundError(f"Failed to export {TORCH_CONFIG['super_animal']} model") if TORCH_CONFIG["checkpoint"].stat().st_size == 0: raise ValueError(f"Exported {TORCH_CONFIG['super_animal']} model is empty") benchmark_videos( @@ -71,14 +68,10 @@ def run_tensorflow_test(video_file: str, display: bool = False): if Path(model_dir / SNAPSHOT_NAME).exists(): print("Model already downloaded, using cached version") else: - print( - "Downloading superanimal_quadruped model from the DeepLabCut Model Zoo..." - ) + print("Downloading superanimal_quadruped model from the DeepLabCut Model Zoo...") download_huggingface_model(MODEL_NAME, str(model_dir)) - assert Path(model_dir / SNAPSHOT_NAME).exists(), ( - f"Missing model file {model_dir / SNAPSHOT_NAME}" - ) + assert Path(model_dir / SNAPSHOT_NAME).exists(), f"Missing model file {model_dir / SNAPSHOT_NAME}" benchmark_videos( model_path=str(model_dir), @@ -131,7 +124,7 @@ def main(): url_link = "https://raw.githubusercontent.com/DeepLabCut/DeepLabCut-live/master/check_install/dog_clip.avi" try: download_file(url_link, video_file) - except (urllib.error.URLError, IOError) as e: + except (OSError, urllib.error.URLError) as e: raise RuntimeError(f"Failed to download video file: {e}") from e else: print(f"Video file already exists at {video_file}, skipping download.") @@ -155,9 +148,7 @@ def main(): any_backend_succeeded = True backend_results["tensorflow"] = ("SUCCESS", None) else: - warnings.warn( - f"Unrecognized backend {backend}, skipping...", UserWarning - ) + warnings.warn(f"Unrecognized backend {backend}, skipping...", UserWarning, stacklevel=2) except Exception as e: backend_name = ( "pytorch" @@ -172,6 +163,7 @@ def main(): f"Error while running test for backend {backend}: {e}. " "Continuing to test other available backends.", UserWarning, + stacklevel=2, ) print("\n---\nBackend test summary:") @@ -184,9 +176,7 @@ def main(): print(f"{name.capitalize()} error:\n{error}\n") if not any_backend_succeeded and backend_failures: - failure_messages = "; ".join( - f"{b}: {exc}" for b, exc in backend_failures.items() - ) + failure_messages = "; ".join(f"{b}: {exc}" for b, exc in backend_failures.items()) raise RuntimeError(f"All backend tests failed. Details: {failure_messages}") finally: @@ -197,7 +187,7 @@ def main(): shutil.rmtree(tmp_dir) except PermissionError: warnings.warn( - f"Could not delete temporary directory {str(tmp_dir)} due to a permissions error." + f"Could not delete temporary directory {str(tmp_dir)} due to a permissions error.", stacklevel=2 ) @@ -207,7 +197,8 @@ def main(): print(f"Available backends: {[b.value for b in available_backends]}") if len(available_backends) == 0: raise NotImplementedError( - "Neither TensorFlow nor PyTorch is installed. Please install at least one of these frameworks to run the installation test." + "Neither TensorFlow nor PyTorch is installed. " + "Please install at least one of these frameworks to run the installation test." ) main() From 22bebf07b85068823a9d432e8a9828cbd789ca4f Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 10 Mar 2026 13:31:02 +0100 Subject: [PATCH 13/17] Remove benchmark exports from __init__ Stop exporting benchmark_videos and download_benchmarking_data from dlclive.__init__.py and update tests to import them directly from dlclive.benchmark. Keeps the package __init__ smaller and avoids importing the benchmark module at package import time. --- dlclive/__init__.py | 1 - tests/test_benchmark_script.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/dlclive/__init__.py b/dlclive/__init__.py index 15d0731..13f0044 100644 --- a/dlclive/__init__.py +++ b/dlclive/__init__.py @@ -11,7 +11,6 @@ _AVAILABLE_BACKENDS = get_available_backends() -from dlclive.benchmark import benchmark_videos, download_benchmarking_data from dlclive.display import Display from dlclive.dlclive import DLCLive from dlclive.engine import Engine diff --git a/tests/test_benchmark_script.py b/tests/test_benchmark_script.py index 875e63d..64ae6e9 100644 --- a/tests/test_benchmark_script.py +++ b/tests/test_benchmark_script.py @@ -2,7 +2,7 @@ import pytest -from dlclive import benchmark_videos, download_benchmarking_data +from dlclive.benchmark import benchmark_videos, download_benchmarking_data from dlclive.engine import Engine From e0e315789e96b11f5c7989690d55cbbab70910f7 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 10 Mar 2026 13:33:20 +0100 Subject: [PATCH 14/17] Remove duplicate line --- dlclive/benchmark.py | 1 - 1 file changed, 1 deletion(-) diff --git a/dlclive/benchmark.py b/dlclive/benchmark.py index b18547b..2ef08bb 100644 --- a/dlclive/benchmark.py +++ b/dlclive/benchmark.py @@ -559,7 +559,6 @@ def benchmark( print(f"Mean inference rate: {np.mean(1 / np.array(times)[1:]):.3f} FPS") metadata = _get_metadata(video_path=video_path, cap=cap, dlc_live=dlc_live) - metadata = _get_metadata(video_path=video_path, cap=cap, dlc_live=dlc_live) cap.release() From 383f4422cbb1f916f4f30ad0d0e70e5dc68aecff Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 10 Mar 2026 14:09:44 +0100 Subject: [PATCH 15/17] Default single_animal to True and update exports Change default for benchmark_videos single_animal to True to reflect expected single-animal usage. Remove benchmark_videos/download_benchmarking_data from package __all__ to avoid exposing internal benchmarking helpers. Update CI test command to run dlc-live-test with --nodisplay so the workflow can run headless in CI environments. --- .github/workflows/testing.yml | 2 +- dlclive/__init__.py | 2 -- dlclive/benchmark.py | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 62b5956..28d8b6c 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -69,7 +69,7 @@ jobs: shell: bash - name: Run Model Benchmark Test - run: uv run dlc-live-test + run: uv run dlc-live-test --nodisplay - name: Run DLC Live Unit Tests run: uv run pytest diff --git a/dlclive/__init__.py b/dlclive/__init__.py index 13f0044..3209cbe 100644 --- a/dlclive/__init__.py +++ b/dlclive/__init__.py @@ -22,8 +22,6 @@ "Display", "Processor", "Engine", - "benchmark_videos", - "download_benchmarking_data", "VERSION", "__version__", ] diff --git a/dlclive/benchmark.py b/dlclive/benchmark.py index 2ef08bb..8c71b1b 100644 --- a/dlclive/benchmark.py +++ b/dlclive/benchmark.py @@ -86,7 +86,7 @@ def benchmark_videos( cmap="bmy", save_poses=False, save_video=False, - single_animal=False, + single_animal=True, ): """Analyze videos using DeepLabCut-live exported models. Analyze multiple videos and/or multiple options for the size of the video From cf3e57edf8c5acd5ac670f371bf56570b501dc45 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 10 Mar 2026 14:10:28 +0100 Subject: [PATCH 16/17] Use system temp dir and improve error handling Use tempfile.gettempdir() for TMP_DIR instead of a repo-relative folder and add the tempfile import. Replace an assertion checking for the model snapshot with an explicit FileNotFoundError to provide clearer failure semantics. Set the CLI --display option default to False and add a guard that raises a RuntimeError when no available backends are detected, giving a helpful message to the user. Minor whitespace/flow adjustments around backend iteration. --- dlclive/check_install/check_install.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/dlclive/check_install/check_install.py b/dlclive/check_install/check_install.py index 3bff5b3..00ade76 100644 --- a/dlclive/check_install/check_install.py +++ b/dlclive/check_install/check_install.py @@ -7,6 +7,7 @@ import argparse import shutil +import tempfile import urllib import urllib.error import warnings @@ -20,7 +21,7 @@ MODEL_NAME = "superanimal_quadruped" SNAPSHOT_NAME = "snapshot-700000.pb" -TMP_DIR = Path(__file__).parent / "dlc-live-tmp" +TMP_DIR = Path(tempfile.gettempdir()) / "dlc-live-tmp" MODELS_DIR = TMP_DIR / "test_models" TORCH_MODEL = "resnet_50" @@ -71,7 +72,8 @@ def run_tensorflow_test(video_file: str, display: bool = False): print("Downloading superanimal_quadruped model from the DeepLabCut Model Zoo...") download_huggingface_model(MODEL_NAME, str(model_dir)) - assert Path(model_dir / SNAPSHOT_NAME).exists(), f"Missing model file {model_dir / SNAPSHOT_NAME}" + if not Path(model_dir / SNAPSHOT_NAME).exists(): + raise FileNotFoundError(f"Missing model file {model_dir / SNAPSHOT_NAME}") benchmark_videos( model_path=str(model_dir), @@ -100,6 +102,7 @@ def main(): action="store_false", dest="display", help=argparse.SUPPRESS, + default=False, ) args = parser.parse_args() @@ -135,7 +138,15 @@ def main(): backend_failures = {} any_backend_succeeded = False - for backend in get_available_backends(): + available_backends = get_available_backends() + if not available_backends: + raise RuntimeError( + "No available backends to test. " + "Please ensure that at least one of the supported backends " + "(TensorFlow or PyTorch) is installed." + ) + + for backend in available_backends: try: if backend == Engine.PYTORCH: print("\nRunning PyTorch test...\n") From e86fb3427fdf4b0e0bfdd0402ea7c8f05db3c950 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 11 Mar 2026 08:50:22 +0100 Subject: [PATCH 17/17] Refactor backend tests and reporting Introduce BACKEND_TESTS and BACKEND_DISPLAY_NAMES mappings to dispatch backend-specific tests and print user-friendly names. Replace string keys with Engine enum keys in backend_results and backend_failures, add type annotations, and centralize test invocation to reduce duplication. Improve error handling and warnings (including stacklevel), unify summary output formatting, and clean up minor control-flow and temporary-directory messages --- dlclive/check_install/check_install.py | 69 ++++++++++++++------------ 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/dlclive/check_install/check_install.py b/dlclive/check_install/check_install.py index 00ade76..ab4c749 100644 --- a/dlclive/check_install/check_install.py +++ b/dlclive/check_install/check_install.py @@ -85,8 +85,19 @@ def run_tensorflow_test(video_file: str, display: bool = False): ) +BACKEND_TESTS = { + Engine.PYTORCH: run_pytorch_test, + Engine.TENSORFLOW: run_tensorflow_test, +} +BACKEND_DISPLAY_NAMES = { + Engine.PYTORCH: "PyTorch", + Engine.TENSORFLOW: "TensorFlow", +} + + def main(): - backend_results = {} + backend_results: dict[Engine, tuple[str, str | None]] = {} + backend_failures: dict[Engine, Exception] = {} parser = argparse.ArgumentParser( description="Test DLC-Live installation by downloading and evaluating a demo DLC project!" @@ -111,7 +122,6 @@ def main(): if not display: print("Running without displaying video") - # make temporary directory print("\nCreating temporary directory...\n") tmp_dir = TMP_DIR tmp_dir.mkdir(mode=0o775, exist_ok=True) @@ -132,10 +142,9 @@ def main(): else: print(f"Video file already exists at {video_file}, skipping download.") - # assert these things exist so we can give informative error messages if not Path(video_file).exists(): raise FileNotFoundError(f"Missing video file {video_file}") - backend_failures = {} + any_backend_succeeded = False available_backends = get_available_backends() @@ -147,28 +156,23 @@ def main(): ) for backend in available_backends: + test_func = BACKEND_TESTS.get(backend) + if test_func is None: + warnings.warn( + f"No test function defined for backend {backend}, skipping...", + UserWarning, + stacklevel=2, + ) + continue + try: - if backend == Engine.PYTORCH: - print("\nRunning PyTorch test...\n") - run_pytorch_test(video_file, display=display) - any_backend_succeeded = True - backend_results["pytorch"] = ("SUCCESS", None) - elif backend == Engine.TENSORFLOW: - print("\nRunning TensorFlow test...\n") - run_tensorflow_test(video_file, display=display) - any_backend_succeeded = True - backend_results["tensorflow"] = ("SUCCESS", None) - else: - warnings.warn(f"Unrecognized backend {backend}, skipping...", UserWarning, stacklevel=2) + print(f"\nRunning {BACKEND_DISPLAY_NAMES.get(backend, backend.value)} test...\n") + test_func(video_file, display=display) + any_backend_succeeded = True + backend_results[backend] = ("SUCCESS", None) + except Exception as e: - backend_name = ( - "pytorch" - if backend == Engine.PYTORCH - else "tensorflow" - if backend == Engine.TENSORFLOW - else str(backend) - ) - backend_results[backend_name] = ("ERROR", str(e)) + backend_results[backend] = ("ERROR", str(e)) backend_failures[backend] = e warnings.warn( f"Error while running test for backend {backend}: {e}. " @@ -178,16 +182,18 @@ def main(): ) print("\n---\nBackend test summary:") - for name in ("tensorflow", "pytorch"): - status, _ = backend_results.get(name, ("SKIPPED", None)) - print(f"{name:<11} [{status}]") + for backend in BACKEND_TESTS.keys(): + status, _ = backend_results.get(backend, ("SKIPPED", None)) + print(f"{backend.value:<11} [{status}]") print("---") - for name, (status, error) in backend_results.items(): + + for backend, (status, error) in backend_results.items(): if status == "ERROR": - print(f"{name.capitalize()} error:\n{error}\n") + backend_name = BACKEND_DISPLAY_NAMES.get(backend, backend.value) + print(f"{backend_name} error:\n{error}\n") if not any_backend_succeeded and backend_failures: - failure_messages = "; ".join(f"{b}: {exc}" for b, exc in backend_failures.items()) + failure_messages = "; ".join(f"{b}: {e}" for b, e in backend_failures.items()) raise RuntimeError(f"All backend tests failed. Details: {failure_messages}") finally: @@ -198,7 +204,8 @@ def main(): shutil.rmtree(tmp_dir) except PermissionError: warnings.warn( - f"Could not delete temporary directory {str(tmp_dir)} due to a permissions error.", stacklevel=2 + f"Could not delete temporary directory {str(tmp_dir)} due to a permissions error.", + stacklevel=2, )