From abd8f9e008098ccaa2e6d4fb5e930eeffc922434 Mon Sep 17 00:00:00 2001 From: Jaap de Ruyter van Steveninck <32810691+deruyter92@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:42:03 +0100 Subject: [PATCH 01/80] Update pre-commit config --- .pre-commit-config.yaml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6623a8593e..7e74fb5f0f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,13 +1,15 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v6.0.0 hooks: - id: check-added-large-files - id: check-yaml - id: check-toml - id: end-of-file-fixer - id: name-tests-test + args: [--pytest-test-first] - id: trailing-whitespace + - id: check-merge-conflict - repo: https://github.com/tox-dev/pyproject-fmt rev: v2.15.2 hooks: @@ -16,12 +18,11 @@ repos: rev: v0.25 hooks: - id: validate-pyproject - - repo: https://github.com/PyCQA/isort - rev: 5.12.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.0 hooks: - - id: isort - - repo: https://github.com/psf/black - rev: 22.3.0 - hooks: - - id: black - language_version: python3 + # Run the formatter. + - id: ruff-format + # Run the linter. + - id: ruff-check + args: [--fix,--unsafe-fixes] \ No newline at end of file From 9d8dfb294a061cc3bfa34dafe803d08eb788662d Mon Sep 17 00:00:00 2001 From: Jaap de Ruyter van Steveninck <32810691+deruyter92@users.noreply.github.com> Date: Mon, 23 Feb 2026 11:50:11 +0100 Subject: [PATCH 02/80] Update pyproject.toml with new linting --- pyproject.toml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7eee712b70..17d926534b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -109,10 +109,10 @@ Documentation = "https://deeplabcut.github.io/DeepLabCut/README.html" [dependency-groups] dev = [ - "black", "coverage", "pytest", "pytest-cov", + "ruff", ] [tool.setuptools] @@ -131,13 +131,14 @@ requires-dist = [] torch-backend = "auto" [tool.ruff] +lint.select = ["E", "F", "B", "I", "UP"] +lint.ignore = ["E741"] target-version = "py310" -line-length = 88 -[tool.ruff.lint] -select = [ "E", "F", "I" ] -ignore = [ - "E501", # line-too-long -] +fix = true +line-length = 120 + +[tool.ruff.lint.pydocstyle] +convention = "google" [tool.isort] multi_line_output = 3 From 3fcc0706894689dc8e2875a069f8ff8c2a56dcf5 Mon Sep 17 00:00:00 2001 From: Jaap de Ruyter van Steveninck <32810691+deruyter92@users.noreply.github.com> Date: Mon, 23 Feb 2026 12:11:01 +0100 Subject: [PATCH 03/80] add linting CI workflow --- .github/workflows/format.yml | 61 ++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 .github/workflows/format.yml diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 0000000000..8f1509a6d8 --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,61 @@ +name: pre-commit (PR only on changed files) + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + detect_changes: + runs-on: ubuntu-latest + outputs: + changed: ${{ steps.changed_files.outputs.changed }} + + steps: + - name: Checkout full history + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Detect changed files + id: changed_files + run: | + git fetch origin ${{ github.base_ref }} + CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD) + + { + echo "changed<> "$GITHUB_OUTPUT" + + - name: Show changed files + run: | + echo "Changed files:" + echo "${{ steps.changed_files.outputs.changed }}" + + precommit: + needs: detect_changes + runs-on: ubuntu-latest + if: ${{ needs.detect_changes.outputs.changed != '' }} + + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.head_ref }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install pre-commit + run: pip install pre-commit + + - name: Run pre-commit (CI check-only stage) on changed files + env: + CHANGED_FILES: ${{ needs.detect_changes.outputs.changed }} + run: | + mapfile -t files <<< "$CHANGED_FILES" + pre-commit run --hook-stage manual --files "${files[@]}" --show-diff-on-failure \ No newline at end of file From 8e65bd623b1fbdd2038296d3b5d29b03e3019a2e Mon Sep 17 00:00:00 2001 From: Jaap de Ruyter Date: Mon, 16 Mar 2026 10:17:42 +0100 Subject: [PATCH 04/80] Reformat codebase: `ruff format . --config pyproject.toml` --- deeplabcut/__init__.py | 4 +- deeplabcut/__main__.py | 3 +- deeplabcut/benchmark/__init__.py | 21 +- deeplabcut/benchmark/base.py | 28 +- deeplabcut/benchmark/benchmarks.py | 21 +- deeplabcut/benchmark/metrics.py | 20 +- deeplabcut/benchmark/mot.py | 33 +- deeplabcut/benchmark/utils.py | 3 +- deeplabcut/cli.py | 4 +- deeplabcut/compat.py | 20 +- deeplabcut/core/config.py | 5 +- deeplabcut/core/conversion_table.py | 1 + deeplabcut/core/crossvalutils.py | 51 +- deeplabcut/core/engine.py | 1 + deeplabcut/core/inferenceutils.py | 106 +- deeplabcut/core/metrics/api.py | 5 +- deeplabcut/core/metrics/bbox.py | 7 +- deeplabcut/core/metrics/distance_metrics.py | 16 +- deeplabcut/core/metrics/identity.py | 5 +- deeplabcut/core/metrics/matching.py | 6 +- deeplabcut/core/trackingutils.py | 66 +- deeplabcut/core/visualization.py | 12 +- deeplabcut/core/weight_init.py | 5 +- deeplabcut/create_project/add.py | 21 +- deeplabcut/create_project/demo_data.py | 4 +- deeplabcut/create_project/modelzoo.py | 52 +- deeplabcut/create_project/new.py | 20 +- deeplabcut/create_project/new_3d.py | 20 +- .../frame_extraction.py | 59 +- .../generate_training_dataset/metadata.py | 37 +- ...ple_individuals_trainingsetmanipulation.py | 92 +- .../trainingsetmanipulation.py | 153 +- deeplabcut/gui/components.py | 32 +- .../gui/displays/selected_shuffle_display.py | 18 +- .../gui/displays/shuffle_metadata_viewer.py | 2 +- deeplabcut/gui/launch_script.py | 1 + deeplabcut/gui/tabs/analyze_videos.py | 28 +- deeplabcut/gui/tabs/create_project.py | 57 +- .../gui/tabs/create_training_dataset.py | 73 +- deeplabcut/gui/tabs/create_videos.py | 36 +- deeplabcut/gui/tabs/evaluate_network.py | 33 +- deeplabcut/gui/tabs/extract_frames.py | 25 +- deeplabcut/gui/tabs/extract_outlier_frames.py | 16 +- deeplabcut/gui/tabs/label_frames.py | 11 +- deeplabcut/gui/tabs/modelzoo.py | 84 +- deeplabcut/gui/tabs/refine_tracklets.py | 4 +- deeplabcut/gui/tabs/train_network.py | 20 +- .../gui/tabs/unsupervised_id_tracking.py | 4 +- deeplabcut/gui/tabs/video_editor.py | 9 +- deeplabcut/gui/tracklet_toolbox.py | 137 +- deeplabcut/gui/widgets.py | 25 +- deeplabcut/gui/window.py | 95 +- deeplabcut/modelzoo/fmpose_3d/fmpose3d.py | 16 +- .../conversion_table/conversion_table.py | 9 +- .../datasets/base.py | 31 +- .../datasets/base_dlc.py | 9 +- .../datasets/coco.py | 16 +- .../datasets/ma_dlc.py | 25 +- .../datasets/ma_dlc_dataframe.py | 38 +- .../datasets/materialize.py | 133 +- .../datasets/multi.py | 47 +- .../datasets/single_dlc.py | 5 +- .../datasets/single_dlc_dataframe.py | 18 +- .../generalized_data_converter/utils.py | 10 +- deeplabcut/modelzoo/utils.py | 37 +- deeplabcut/modelzoo/video_inference.py | 74 +- deeplabcut/modelzoo/webapp/inference.py | 10 +- deeplabcut/modelzoo/weight_initialization.py | 1 + .../pose_estimation_3d/camera_calibration.py | 85 +- deeplabcut/pose_estimation_3d/plotting3D.py | 54 +- .../pose_estimation_3d/triangulation.py | 134 +- .../apis/analyze_images.py | 33 +- .../pose_estimation_pytorch/apis/ctd.py | 9 +- .../apis/evaluation.py | 73 +- .../pose_estimation_pytorch/apis/export.py | 9 +- .../apis/prune_paf_graph.py | 23 +- .../apis/tracking_dataset.py | 29 +- .../pose_estimation_pytorch/apis/tracklets.py | 12 +- .../pose_estimation_pytorch/apis/training.py | 21 +- .../pose_estimation_pytorch/apis/utils.py | 44 +- .../pose_estimation_pytorch/apis/videos.py | 38 +- .../apis/visualization.py | 65 +- .../config/make_pose_config.py | 25 +- .../pose_estimation_pytorch/config/utils.py | 29 +- .../pose_estimation_pytorch/data/base.py | 11 +- .../data/cocoloader.py | 20 +- .../pose_estimation_pytorch/data/collate.py | 11 +- .../pose_estimation_pytorch/data/ctd.py | 34 +- .../pose_estimation_pytorch/data/dataset.py | 68 +- .../pose_estimation_pytorch/data/dlcloader.py | 66 +- .../data/generative_sampling.py | 41 +- .../pose_estimation_pytorch/data/image.py | 18 +- .../data/postprocessor.py | 97 +- .../data/preprocessor.py | 65 +- .../pose_estimation_pytorch/data/snapshots.py | 8 +- .../data/transforms.py | 32 +- .../pose_estimation_pytorch/data/utils.py | 26 +- .../metrics/scoring.py | 8 +- .../models/backbones/base.py | 6 +- .../models/backbones/cond_prenet.py | 12 +- .../models/backbones/cspnext.py | 15 +- .../models/backbones/hrnet_coam.py | 28 +- .../models/backbones/resnet.py | 24 +- .../models/criterions/aggregators.py | 4 +- .../models/criterions/base.py | 4 +- .../models/criterions/dekr.py | 3 +- .../models/criterions/kl_discrete.py | 1 + .../models/criterions/utils.py | 8 +- .../models/detectors/base.py | 4 +- .../models/detectors/fasterRCNN.py | 4 +- .../models/detectors/torchvision.py | 1 + .../models/heads/base.py | 18 +- .../models/heads/dekr.py | 52 +- .../models/heads/dlcrnet.py | 40 +- .../models/heads/rtmcc_head.py | 1 + .../models/heads/simple_head.py | 8 +- .../pose_estimation_pytorch/models/model.py | 21 +- .../models/modules/__init__.py | 7 +- .../models/modules/coam_module.py | 82 +- .../models/modules/conv_block.py | 17 +- .../models/modules/conv_module.py | 39 +- .../models/modules/csp.py | 16 +- .../models/modules/gated_attention_unit.py | 13 +- .../models/modules/kpt_encoders.py | 29 +- .../models/modules/norm.py | 3 +- .../models/necks/layers.py | 14 +- .../models/necks/transformer.py | 30 +- .../models/necks/utils.py | 8 +- .../models/predictors/base.py | 4 +- .../models/predictors/dekr_predictor.py | 92 +- .../models/predictors/identity_predictor.py | 5 +- .../models/predictors/paf_predictor.py | 56 +- .../models/predictors/sim_cc.py | 11 +- .../models/predictors/single_predictor.py | 32 +- .../models/target_generators/dekr_targets.py | 30 +- .../models/target_generators/pafs_targets.py | 24 +- .../models/target_generators/sim_cc.py | 13 +- .../modelzoo/config.py | 22 +- .../modelzoo/inference.py | 5 +- .../modelzoo/memory_replay.py | 35 +- .../pose_estimation_pytorch/modelzoo/utils.py | 8 +- .../post_processing/identity.py | 5 +- .../match_predictions_to_gt.py | 12 +- .../post_processing/nms.py | 1 + .../pose_estimation_pytorch/registry.py | 34 +- .../pose_estimation_pytorch/runners/base.py | 3 +- .../pose_estimation_pytorch/runners/ctd.py | 2 + .../runners/dynamic_cropping.py | 38 +- .../runners/inference.py | 125 +- .../pose_estimation_pytorch/runners/logger.py | 18 +- .../runners/schedulers.py | 2 +- .../runners/shelving.py | 21 +- .../runners/snapshots.py | 17 +- .../pose_estimation_pytorch/runners/train.py | 50 +- deeplabcut/pose_estimation_pytorch/task.py | 1 + .../pose_estimation_tensorflow/__init__.py | 1 + .../pose_estimation_tensorflow/_tf_legacy.py | 3 +- .../backbones/efficientnet_builder.py | 16 +- .../backbones/efficientnet_model.py | 89 +- .../backbones/mobilenet.py | 61 +- .../backbones/mobilenet_v2.py | 25 +- .../core/evaluate.py | 187 +- .../core/evaluate_multianimal.py | 133 +- .../mo_extensions/front/tf/unravel_index.py | 16 +- .../core/openvino/session.py | 12 +- .../core/predict.py | 16 +- .../core/predict_multianimal.py | 4 +- .../pose_estimation_tensorflow/core/test.py | 4 +- .../pose_estimation_tensorflow/core/train.py | 50 +- .../core/train_multianimal.py | 15 +- .../datasets/augmentation.py | 3 +- .../datasets/pose_base.py | 4 +- .../datasets/pose_deterministic.py | 22 +- .../datasets/pose_imgaug.py | 65 +- .../datasets/pose_multianimal_imgaug.py | 162 +- .../datasets/pose_tensorpack.py | 26 +- .../pose_estimation_tensorflow/export.py | 39 +- .../lib/crossvalutils.py | 1 + .../lib/inferenceutils.py | 1 + .../lib/trackingutils.py | 1 + .../modelzoo/__init__.py | 2 +- .../modelzoo/api/spatiotemporal_adapt.py | 19 +- .../modelzoo/api/superanimal_inference.py | 40 +- .../pose_estimation_tensorflow/nnets/base.py | 35 +- .../nnets/conv_blocks.py | 23 +- .../nnets/efficientnet.py | 4 +- .../nnets/layers.py | 4 +- .../pose_estimation_tensorflow/nnets/multi.py | 71 +- .../nnets/resnet.py | 4 +- .../pose_estimation_tensorflow/nnets/utils.py | 54 +- .../predict_multianimal.py | 48 +- .../predict_videos.py | 220 +- .../pose_estimation_tensorflow/training.py | 34 +- .../util/logging.py | 1 + .../util/visualize.py | 5 +- .../pose_estimation_tensorflow/vis_dataset.py | 4 +- .../visualizemaps.py | 69 +- deeplabcut/pose_tracking_pytorch/apis.py | 4 +- .../pose_tracking_pytorch/create_dataset.py | 34 +- deeplabcut/pose_tracking_pytorch/inference.py | 4 +- .../model/backbones/vit_pytorch.py | 21 +- .../processor/processor.py | 10 +- .../pose_tracking_pytorch/solver/cosine_lr.py | 20 +- .../solver/make_optimizer.py | 8 +- .../pose_tracking_pytorch/solver/scheduler.py | 25 +- .../solver/scheduler_factory.py | 3 +- .../tracking_utils/reranking.py | 18 +- .../train_dlctransreid.py | 4 +- .../post_processing/analyze_skeleton.py | 15 +- deeplabcut/post_processing/filtering.py | 20 +- .../refine_training_dataset/outlier_frames.py | 138 +- deeplabcut/refine_training_dataset/stitch.py | 128 +- .../refine_training_dataset/tracklets.py | 76 +- deeplabcut/utils/auxfun_models.py | 16 +- deeplabcut/utils/auxfun_multianimal.py | 77 +- deeplabcut/utils/auxfun_videos.py | 73 +- deeplabcut/utils/auxiliaryfunctions.py | 137 +- deeplabcut/utils/auxiliaryfunctions_3d.py | 42 +- deeplabcut/utils/conversioncode.py | 41 +- deeplabcut/utils/frameselectiontools.py | 45 +- deeplabcut/utils/make_labeled_video.py | 150 +- deeplabcut/utils/multiprocessing.py | 13 +- deeplabcut/utils/plotting.py | 50 +- deeplabcut/utils/pseudo_label.py | 67 +- deeplabcut/utils/skeleton.py | 16 +- deeplabcut/utils/video_processor.py | 4 +- deeplabcut/utils/visualization.py | 52 +- docs/recipes/flip_and_rotate.ipynb | 1072 ++++---- docs/recipes/fmpose3d.ipynb | 684 ++--- examples/COLAB/COLAB_3miceDemo.ipynb | 36 +- .../COLAB/COLAB_BUCTD_and_CTD_tracking.ipynb | 10 +- examples/COLAB/COLAB_DEMO_SuperAnimal.ipynb | 19 +- .../COLAB/COLAB_DEMO_mouse_openfield.ipynb | 14 +- examples/COLAB/COLAB_DLC_ModelZoo.ipynb | 621 +++-- .../COLAB/COLAB_HumanPose_with_RTMPose.ipynb | 2260 ++++++++--------- .../COLAB/COLAB_YOURDATA_SuperAnimal.ipynb | 41 +- ..._YOURDATA_TrainNetwork_VideoAnalysis.ipynb | 10 +- examples/COLAB/COLAB_transformer_reID.ipynb | 1162 +++++---- examples/JUPYTER/Demo_3D_DeepLabCut.ipynb | 31 +- .../Demo_labeledexample_MouseReaching.ipynb | 20 +- .../Demo_labeledexample_Openfield.ipynb | 14 +- examples/JUPYTER/Demo_napari.ipynb | 38 +- examples/JUPYTER/Demo_yourowndata.ipynb | 27 +- .../Docker_TrainNetwork_VideoAnalysis.ipynb | 6 +- examples/testscript.py | 49 +- examples/testscript_3d.py | 11 +- .../testscript_deterministicwithResNet152.py | 6 +- examples/testscript_mobilenets.py | 39 +- examples/testscript_multianimal.py | 62 +- examples/testscript_openfielddata.py | 17 +- ...pt_openfielddata_augmentationcomparison.py | 11 +- examples/testscript_pretrained_models.py | 7 +- examples/testscript_pytorch_multi_animal.py | 4 +- examples/testscript_superanimal_adaptation.py | 5 +- ...t_superanimal_create_pretrained_project.py | 1 + examples/testscript_superanimal_inference.py | 7 +- ...estscript_superanimal_transfer_learning.py | 2 +- examples/testscript_transreid.py | 61 +- examples/utils.py | 32 +- setup.py | 1 + .../inferenceutils/test_map_computation.py | 36 +- tests/core/metrics/test_metrics_api.py | 6 +- .../metrics/test_metrics_identity_accuracy.py | 1 + .../metrics/test_metrics_map_computation.py | 20 +- .../metrics/test_metrics_rmse_computation.py | 85 +- .../test_video_set_configuration.py | 48 +- .../test_trainingset_manipulation.py | 11 +- .../test_trainset_metadata.py | 213 +- .../apis/test_apis_evaluate.py | 72 +- .../apis/test_apis_export.py | 13 +- .../apis/test_create_tracking_dataset.py | 4 +- .../config/test_config_utils.py | 3 +- .../config/test_make_pose_config.py | 21 +- .../data/test_data_ctd.py | 12 +- .../data/test_postprocessor.py | 41 +- .../data/test_preprocessor.py | 33 +- .../data/test_transforms.py | 18 +- .../data/test_utils.py | 1 + .../target_generators/test_heatmap_targets.py | 20 +- .../target_generators/test_plateau_targets.py | 25 +- .../modelzoo/test_fmpose_integration.py | 11 +- .../modelzoo/test_modelzoo_utils.py | 5 +- .../modelzoo/test_webapp.py | 25 +- .../other/test_api_utils.py | 6 +- .../other/test_custom_transforms.py | 4 +- .../other/test_dataset.py | 64 +- .../other/test_gaussian_targets.py | 8 +- .../other/test_heatmap_plateau_targets.py | 4 +- .../other/test_match_predictions_to_gt.py | 32 +- .../other/test_modelzoo.py | 1 + .../other/test_paf_targets.py | 8 +- .../other/test_pose_model.py | 12 +- .../other/test_seq_targets.py | 6 +- .../post_processing/test_identity.py | 3 +- .../test_postprocessing_nms.py | 3 +- .../runners/bottum_up.py | 15 +- .../runners/test_dynamic_cropper.py | 14 +- ...test_filtered_detector_inference_runner.py | 13 +- .../runners/test_logger.py | 1 + .../runners/test_runners.py | 4 +- .../runners/test_runners_inference.py | 13 +- .../runners/test_runners_train.py | 16 +- .../runners/test_schedulers.py | 23 +- .../runners/test_shelving.py | 9 +- .../runners/test_task.py | 3 +- tests/test_auxfun_models.py | 12 +- tests/test_auxfun_multianimal.py | 8 +- tests/test_auxiliaryfunctions.py | 26 +- tests/test_crossvalutils.py | 8 +- tests/test_dataset_augmentation.py | 8 +- tests/test_evaluate.py | 4 +- tests/test_frame_selection_tools.py | 3 +- tests/test_inferenceutils.py | 37 +- tests/test_pose_multianimal_imgaug.py | 26 +- tests/test_predict_multianimal.py | 8 +- tests/test_trackingutils.py | 36 +- tests/test_trainingsetmanipulation.py | 19 +- tests/test_triangulation.py | 4 +- tests/test_video.py | 8 +- testscript_cli.py | 5 +- 320 files changed, 5558 insertions(+), 9380 deletions(-) diff --git a/deeplabcut/__init__.py b/deeplabcut/__init__.py index 72dac1e3ce..557c0b8eee 100644 --- a/deeplabcut/__init__.py +++ b/deeplabcut/__init__.py @@ -26,9 +26,7 @@ ) from deeplabcut.gui.widgets import SkeletonBuilder except (ModuleNotFoundError, ImportError): - print( - "DLC loaded in light mode; you cannot use any GUI (labeling, relabeling and standalone GUI)" - ) + print("DLC loaded in light mode; you cannot use any GUI (labeling, relabeling and standalone GUI)") from deeplabcut.core.engine import Engine from deeplabcut.create_project import ( diff --git a/deeplabcut/__main__.py b/deeplabcut/__main__.py index 8d8c782a74..f0903fcf97 100644 --- a/deeplabcut/__main__.py +++ b/deeplabcut/__main__.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # + def main(): try: import PySide6 @@ -30,4 +31,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/deeplabcut/benchmark/__init__.py b/deeplabcut/benchmark/__init__.py index a5ddd3c4a9..935f853a2e 100644 --- a/deeplabcut/benchmark/__init__.py +++ b/deeplabcut/benchmark/__init__.py @@ -38,9 +38,7 @@ class needs to be a subclass of the ``benchmark.base.Benchmark`` not a subclass of ``benchmark.base.Benchmark``. """ if not issubclass(cls, Benchmark): - raise ValueError( - f"Can only register subclasses of {type(Benchmark)}, " f"but got {cls}." - ) + raise ValueError(f"Can only register subclasses of {type(Benchmark)}, but got {cls}.") __registry.append(cls) @@ -85,11 +83,14 @@ def evaluate( continue benchmark = benchmark_cls() for name in benchmark.names(): - if Result( - code=benchmark.code, - method_name=name, - benchmark_name=benchmark_cls.name, - ) in results: + if ( + Result( + code=benchmark.code, + method_name=name, + benchmark_name=benchmark_cls.name, + ) + in results + ): continue else: result = benchmark.evaluate(name, on_error=on_error) @@ -106,9 +107,7 @@ def savecache(results: ResultCollection): json.dump(results.todicts(), fh, indent=2) -def loadcache( - cache=CACHE, on_missing: Literal["raise", "ignore"] = "ignore" -) -> ResultCollection: +def loadcache(cache=CACHE, on_missing: Literal["raise", "ignore"] = "ignore") -> ResultCollection: if not os.path.exists(cache): if on_missing == "raise": raise FileNotFoundError(cache) diff --git a/deeplabcut/benchmark/base.py b/deeplabcut/benchmark/base.py index ef6894ac3b..41c2e44d81 100644 --- a/deeplabcut/benchmark/base.py +++ b/deeplabcut/benchmark/base.py @@ -62,10 +62,7 @@ def __init__(self): keys = ["code", "name", "keypoints", "ground_truth", "metadata"] for key in keys: if not hasattr(self, key): - raise NotImplementedError( - f"Subclass of abstract Benchmark class need " - f"to define the {key} property." - ) + raise NotImplementedError(f"Subclass of abstract Benchmark class need to define the {key} property.") def compute_pose_rmse(self, results_objects): return deeplabcut.benchmark.metrics.calc_rmse_from_obj( @@ -81,9 +78,7 @@ def evaluate(self, name: str, on_error="raise"): """Evaluate this benchmark with all registered methods.""" if name not in self.names(): - raise ValueError( - f"{name} is not registered. Valid names are {self.names()}" - ) + raise ValueError(f"{name} is not registered. Valid names are {self.names()}") if on_error not in ("ignore", "return", "raise"): raise ValueError(f"on_error got an undefined value: {on_error}") mean_avg_precision = float("nan") @@ -104,9 +99,7 @@ def evaluate(self, name: str, on_error="raise"): pass elif on_error == "raise": # raise the error and stop evaluation - raise BenchmarkEvaluationError( - f"Error during benchmark evaluation for model {name}" - ) from exception + raise BenchmarkEvaluationError(f"Error during benchmark evaluation for model {name}") from exception else: raise NotImplementedError() from exception return Result( @@ -122,9 +115,7 @@ def _validate_predictions(self, name: str, predictions: dict) -> dict: Checks that there is a prediction for each test image, and raises a warning if that is not the case. Returns only predictions made for test images. """ - test_images = deeplabcut.benchmark.metrics.load_test_images( - self.ground_truth, self.metadata - ) + test_images = deeplabcut.benchmark.metrics.load_test_images(self.ground_truth, self.metadata) missing_images = set(test_images) - set(predictions.keys()) if len(missing_images) > 0: warnings.warn( @@ -196,9 +187,9 @@ def primary_key_names(self): def toframe(self) -> pd.DataFrame: """Convert results to pandas dataframe""" - return pd.DataFrame( - [result.todict() for result in self.results.values()] - ).set_index(list(self.primary_key_names)) + return pd.DataFrame([result.todict() for result in self.results.values()]).set_index( + list(self.primary_key_names) + ) def add(self, result: Result): """Add a result to the collection.""" @@ -225,10 +216,7 @@ def __len__(self): def __contains__(self, other: Result): if not isinstance(other, Result): - raise ValueError( - f"{type(self)} can only store objects of type Result, " - f"but got {type(other)}." - ) + raise ValueError(f"{type(self)} can only store objects of type Result, but got {type(other)}.") return other.primary_key in self.results def __eq__(self, other): diff --git a/deeplabcut/benchmark/benchmarks.py b/deeplabcut/benchmark/benchmarks.py index 4068c29cf2..6a76bad957 100644 --- a/deeplabcut/benchmark/benchmarks.py +++ b/deeplabcut/benchmark/benchmarks.py @@ -45,9 +45,7 @@ class TriMouseBenchmark(deeplabcut.benchmark.base.Benchmark): "tailend", ) ground_truth = deeplabcut.benchmark.get_filepath("CollectedData_Daniel.h5") - metadata = deeplabcut.benchmark.get_filepath( - "Documentation_data-MultiMouse_70shuffle1.pickle" - ) + metadata = deeplabcut.benchmark.get_filepath("Documentation_data-MultiMouse_70shuffle1.pickle") num_animals = 3 @@ -81,9 +79,7 @@ class ParentingMouseBenchmark(deeplabcut.benchmark.base.Benchmark): ) ground_truth = deeplabcut.benchmark.get_filepath("CollectedData_Mostafizur.h5") - metadata = deeplabcut.benchmark.get_filepath( - "Documentation_data-CrackingParenting_70shuffle1.pickle" - ) + metadata = deeplabcut.benchmark.get_filepath("Documentation_data-CrackingParenting_70shuffle1.pickle") num_animals = 2 def compute_pose_map(self, results_objects): @@ -100,10 +96,7 @@ def _validate_predictions(self, name: str, predictions: dict) -> dict: """Fixes filenames for predictions made on old versions of the dataset""" return super()._validate_predictions( name, - { - k.replace("Dummy", "D").replace("Dead pup", "DP"): v - for k, v in predictions.items() - }, + {k.replace("Dummy", "D").replace("Dead pup", "DP"): v for k, v in predictions.items()}, ) @@ -134,9 +127,7 @@ class MarmosetBenchmark(deeplabcut.benchmark.base.Benchmark): "Body3", ) ground_truth = deeplabcut.benchmark.get_filepath("CollectedData_Mackenzie.h5") - metadata = deeplabcut.benchmark.get_filepath( - "Documentation_data-Marmoset_70shuffle1.pickle" - ) + metadata = deeplabcut.benchmark.get_filepath("Documentation_data-Marmoset_70shuffle1.pickle") num_animals = 2 @@ -151,9 +142,7 @@ class FishBenchmark(deeplabcut.benchmark.base.Benchmark): name = "fish" keypoints = ("tip", "gill", "peduncle", "caudaltip", "dfintip") ground_truth = deeplabcut.benchmark.get_filepath("CollectedData_Valentina.h5") - metadata = deeplabcut.benchmark.get_filepath( - "Documentation_data-Schooling_70shuffle1.pickle" - ) + metadata = deeplabcut.benchmark.get_filepath("Documentation_data-Schooling_70shuffle1.pickle") num_animals = 14 def compute_pose_rmse(self, results_objects): diff --git a/deeplabcut/benchmark/metrics.py b/deeplabcut/benchmark/metrics.py index e73eb4cba2..de0ee4161d 100644 --- a/deeplabcut/benchmark/metrics.py +++ b/deeplabcut/benchmark/metrics.py @@ -39,11 +39,7 @@ def _format_gt_data(h5file: str, test_indices: Optional[List[int]] = None): animals = _get_unique_level_values(df.columns, "individuals") kpts = _get_unique_level_values(df.columns, "bodyparts") try: - n_unique = len( - _get_unique_level_values( - df.xs("single", level="individuals", axis=1).columns, "bodyparts" - ) - ) + n_unique = len(_get_unique_level_values(df.xs("single", level="individuals", axis=1).columns, "bodyparts")) except KeyError: n_unique = 0 guarantee_multiindex_rows(df) @@ -98,9 +94,7 @@ def calc_prediction_errors(preds, gt): if visible.size and xy_pred_.size: # Pick the predictions closest to ground truth, # rather than the ones the model has most confident in. - neighbors = crossvalutils.find_closest_neighbors( - xy_gt_[visible], xy_pred_, k=3 - ) + neighbors = crossvalutils.find_closest_neighbors(xy_gt_[visible], xy_pred_, k=3) found = neighbors != -1 if ~np.any(found): continue @@ -178,16 +172,14 @@ def calc_map_from_obj( missing_images = set(test_images) - set(eval_results_obj.keys()) if len(missing_images) > 0: raise ValueError( - "Failed to compute the test mAP: there are test images missing from the" - f"prediction object: {missing_images}" + f"Failed to compute the test mAP: there are test images missing from theprediction object: {missing_images}" ) ground_truth = df_test.to_numpy().reshape((len(test_images), n_animals, -1, 2)) temp = np.ones((*ground_truth.shape[:3], 3)) temp[..., :2] = ground_truth assemblies_gt_test = { - test_images[i]: assembly - for i, assembly in inferenceutils._parse_ground_truth_data(temp).items() + test_images[i]: assembly for i, assembly in inferenceutils._parse_ground_truth_data(temp).items() } # TODO(stes): remove/rewrite @@ -233,9 +225,7 @@ def calc_rmse_from_obj( for ind in sorted(drop_kpts, reverse=True): kpts.pop(ind) - test_objects = { - k: v for k, v in eval_results_obj.items() if k in gt["annotations"].keys() - } + test_objects = {k: v for k, v in eval_results_obj.items() if k in gt["annotations"].keys()} if len(gt["annotations"]) != len(test_objects): gt_images = list(gt["annotations"].keys()) missing_images = [img for img in gt_images if img not in test_objects] diff --git a/deeplabcut/benchmark/mot.py b/deeplabcut/benchmark/mot.py index 8e1c3a39ee..ca37bd169f 100644 --- a/deeplabcut/benchmark/mot.py +++ b/deeplabcut/benchmark/mot.py @@ -53,12 +53,11 @@ def convert_bboxes_to_xywh(bboxes: NDArray, inplace: bool = False) -> NDArray: bboxes[:, 2] = w bboxes[:, 3] = h + _convert_bboxes_to_xywh = convert_bboxes_to_xywh -def reconstruct_bboxes_from_bodyparts( - data: pd.DataFrame, margin: float, to_xywh: bool = False -) -> NDArray: +def reconstruct_bboxes_from_bodyparts(data: pd.DataFrame, margin: float, to_xywh: bool = False) -> NDArray: """ Reconstructs bounding boxes from body part coordinates and likelihoods. @@ -103,9 +102,7 @@ def reconstruct_bboxes_from_bodyparts( return bboxes -def reconstruct_all_bboxes( - data: pd.DataFrame, margin: float, to_xywh: bool = False -) -> NDArray: +def reconstruct_all_bboxes(data: pd.DataFrame, margin: float, to_xywh: bool = False) -> NDArray: """ Reconstructs bounding boxes for multiple individuals from body part data. @@ -144,9 +141,7 @@ def reconstruct_all_bboxes( pass bboxes = np.full((len(animals), data.shape[0], 5), np.nan) for n, animal in enumerate(animals): - bboxes[n] = reconstruct_bboxes_from_bodyparts( - data.xs(animal, axis=1, level="individuals"), margin, to_xywh - ) + bboxes[n] = reconstruct_bboxes_from_bodyparts(data.xs(animal, axis=1, level="individuals"), margin, to_xywh) return bboxes @@ -168,7 +163,9 @@ def compute_mot_metrics( trackers_gt = func(df_gt, **kwargs) trackers = func(df, **kwargs) return _compute_mot_metrics( - trackers_gt, trackers, tracker_type, + trackers_gt, + trackers, + tracker_type, ) @@ -178,9 +175,7 @@ def _compute_mot_metrics( tracker_type: str = "bbox", ) -> mm.MOTAccumulator: if trackers_ground_truth.shape != trackers.shape: - raise ValueError( - "Dimensions mismatch. There must be as many `trackers_ground_truth` as there are `trackers`." - ) + raise ValueError("Dimensions mismatch. There must be as many `trackers_ground_truth` as there are `trackers`.") if tracker_type == "bbox": sl = slice(0, 4) @@ -214,20 +209,14 @@ def cost_func(ellipses_gt, ellipses_hyp): return acc -def print_all_metrics( - accumulators: list[mm.MOTAccumulator], all_params: list[str] | None = None -): +def print_all_metrics(accumulators: list[mm.MOTAccumulator], all_params: list[str] | None = None): if not all_params: names = [f"iter{i + 1}" for i in range(len(accumulators))] else: s = "_".join("{}" for _ in range(len(all_params[0]))) names = [s.format(*params.values()) for params in all_params] mh = mm.metrics.create() - summary = mh.compute_many( - accumulators, metrics=mm.metrics.motchallenge_metrics, names=names - ) - strsummary = mm.io.render_summary( - summary, formatters=mh.formatters, namemap=mm.io.motchallenge_metric_names - ) + summary = mh.compute_many(accumulators, metrics=mm.metrics.motchallenge_metrics, names=names) + strsummary = mm.io.render_summary(summary, formatters=mh.formatters, namemap=mm.io.motchallenge_metric_names) print(strsummary) return summary diff --git a/deeplabcut/benchmark/utils.py b/deeplabcut/benchmark/utils.py index bc1cd64d3c..627a4db566 100644 --- a/deeplabcut/benchmark/utils.py +++ b/deeplabcut/benchmark/utils.py @@ -10,8 +10,9 @@ # """Helper functions in this file are not affected by the main repositories -license. They are independent from the remainder of the benchmarking code. +license. They are independent from the remainder of the benchmarking code. """ + import importlib import os import pkgutil diff --git a/deeplabcut/cli.py b/deeplabcut/cli.py index 2a75c3a6bf..be8bff3b13 100644 --- a/deeplabcut/cli.py +++ b/deeplabcut/cli.py @@ -256,9 +256,7 @@ def train_network(_, *args, **kwargs): default=[1], help="Shuffle index of the training dataset. Default is set to 1.", ) -@click.option( - "-p", "--plot", "plotting", is_flag=True, help="Make plots. Default is False." -) +@click.option("-p", "--plot", "plotting", is_flag=True, help="Make plots. Default is False.") @click.pass_context def evaluate_network(_, config, **kwargs): """Evaluates a trained Feature detector model.\n diff --git a/deeplabcut/compat.py b/deeplabcut/compat.py index 08a42cc7e0..8ebd62bf89 100644 --- a/deeplabcut/compat.py +++ b/deeplabcut/compat.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Compatibility file for methods available with either PyTorch or Tensorflow""" + from __future__ import annotations from pathlib import Path @@ -526,13 +527,9 @@ def evaluate_network( ) ) if len(engines) == 0: - raise ValueError( - f"You must pass at least one shuffle to evaluate (had {list(Shuffles)})" - ) + raise ValueError(f"You must pass at least one shuffle to evaluate (had {list(Shuffles)})") elif len(engines) > 1: - raise ValueError( - f"All shuffles must have the same engine (found {list(engines)})" - ) + raise ValueError(f"All shuffles must have the same engine (found {list(engines)})") engine = engines.pop() if engine == Engine.TF: @@ -1536,8 +1533,7 @@ def convert_detections2tracklets( if greedy or calibrate or window_size: raise NotImplementedError( - f"The 'greedy', 'calibrate' and 'window_size' option are not yet " - f"implemented with {engine}" + f"The 'greedy', 'calibrate' and 'window_size' option are not yet implemented with {engine}" ) return convert_detections2tracklets( @@ -1686,9 +1682,7 @@ def visualize_locrefs( Returns: The figure and axis on which the image scoremap and locref field were plot. """ - return visualization.visualize_locrefs( - image, scmap, locref_x, locref_y, step=step, zoom_width=zoom_width - ) + return visualization.visualize_locrefs(image, scmap, locref_x, locref_y, step=step, zoom_width=zoom_width) def visualize_paf( @@ -1964,9 +1958,7 @@ def _gpu_to_use_to_device(gpu_to_use: int | None, device: str | None) -> str | N def _load_config(config: str) -> dict: config_path = Path(config) if not config_path.exists(): - raise FileNotFoundError( - f"Config {config} is not found. Please make sure that the file exists." - ) + raise FileNotFoundError(f"Config {config} is not found. Please make sure that the file exists.") with open(config, "r") as f: project_config = YAML(typ="safe", pure=True).load(f) diff --git a/deeplabcut/core/config.py b/deeplabcut/core/config.py index 1a638e48da..13e4fc6e46 100644 --- a/deeplabcut/core/config.py +++ b/deeplabcut/core/config.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Simple helper methods related to configuration files stored in yaml files""" + from __future__ import annotations from pathlib import Path @@ -43,9 +44,7 @@ def write_config(config_path: str | Path, config: dict, overwrite: bool = True) FileExistsError if overwrite=True and the file already exists """ if not overwrite and Path(config_path).exists(): - raise FileExistsError( - f"Cannot write to {config_path} - set overwrite=True to force" - ) + raise FileExistsError(f"Cannot write to {config_path} - set overwrite=True to force") with open(config_path, "w") as file: YAML().dump(config, file) diff --git a/deeplabcut/core/conversion_table.py b/deeplabcut/core/conversion_table.py index e5d9679fa9..faa01d82ba 100644 --- a/deeplabcut/core/conversion_table.py +++ b/deeplabcut/core/conversion_table.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Defines conversion tables mapping DeepLabCut project bodyparts to SA bodyparts""" + from __future__ import annotations from dataclasses import dataclass diff --git a/deeplabcut/core/crossvalutils.py b/deeplabcut/core/crossvalutils.py index e95b2c7591..8b76e6ef82 100644 --- a/deeplabcut/core/crossvalutils.py +++ b/deeplabcut/core/crossvalutils.py @@ -37,9 +37,7 @@ def _set_up_evaluation(data): params["num_joints"] = len(params["joint_names"]) partaffinityfield_graph = data["metadata"]["PAFgraph"] params["paf"] = np.arange(len(partaffinityfield_graph)) - params["paf_graph"] = params["paf_links"] = [ - partaffinityfield_graph[l] for l in params["paf"] - ] + params["paf_graph"] = params["paf_links"] = [partaffinityfield_graph[l] for l in params["paf"]] params["bpts"] = params["ibpts"] = range(params["num_joints"]) params["imnames"] = [fn for fn in list(data) if fn != "metadata"] return params @@ -56,9 +54,7 @@ def _unsorted_unique(array): return np.asarray(array)[np.sort(inds)] -def find_closest_neighbors( - query: np.ndarray, ref: np.ndarray, k: int = 3 -) -> np.ndarray: +def find_closest_neighbors(query: np.ndarray, ref: np.ndarray, k: int = 3) -> np.ndarray: """Greedy matching of predicted keypoints to ground truth keypoints Args: @@ -87,9 +83,7 @@ def find_closest_neighbors( return neighbors -def _calc_separability( - vals_left, vals_right, n_bins=101, metric="jeffries", max_sensitivity=False -): +def _calc_separability(vals_left, vals_right, n_bins=101, metric="jeffries", max_sensitivity=False): if metric not in ("jeffries", "auc"): raise ValueError("`metric` should be either 'jeffries' or 'auc'.") @@ -100,9 +94,7 @@ def _calc_separability( hist_right = hist_right / hist_right.sum() tpr = np.cumsum(hist_right) if metric == "jeffries": - sep = np.sqrt( - 2 * (1 - np.sum(np.sqrt(hist_left * hist_right))) - ) # Jeffries-Matusita distance + sep = np.sqrt(2 * (1 - np.sum(np.sqrt(hist_left * hist_right)))) # Jeffries-Matusita distance else: sep = np.trapz(np.cumsum(hist_left), tpr) if max_sensitivity: @@ -224,12 +216,7 @@ def _benchmark_paf_graphs( params = ass.metadata image_paths = params["imnames"] bodyparts = params["joint_names"] - idx = ( - data[image_paths[0]]["groundtruth"][2] - .unstack("coords") - .reindex(bodyparts, level="bodyparts") - .index - ) + idx = data[image_paths[0]]["groundtruth"][2].unstack("coords").reindex(bodyparts, level="bodyparts").index mask_multi = idx.get_level_values("individuals") != "single" if not mask_multi.all(): idx = idx.drop("single", level="individuals") @@ -267,12 +254,8 @@ def _benchmark_paf_graphs( # get the indices of the images in the training set dataset_idx = [data[image_name]["index"] for image_name in image_paths] for inds in split_inds: - ass_gt = { - k: v for k, v in ass_true_dict.items() if dataset_idx[k] in inds - } - ass_pred = { - k: v for k, v in ass.assemblies.items() if dataset_idx[k] in inds - } + ass_gt = {k: v for k, v in ass_true_dict.items() if dataset_idx[k] in inds} + ass_pred = {k: v for k, v in ass.assemblies.items() if dataset_idx[k] in inds} oks.append( evaluate_assembly( @@ -308,10 +291,7 @@ def _benchmark_paf_graphs( if n_dets: scores[i, 0] = 1 else: - animals = [ - np.c_[animal.data, np.ones(animal.data.shape[0]) * n] - for n, animal in enumerate(animals) - ] + animals = [np.c_[animal.data, np.ones(animal.data.shape[0]) * n] for n, animal in enumerate(animals)] hyp = np.concatenate(animals) hyp = hyp[~np.isnan(hyp).any(axis=1)] scores[i, 0] = max(0, (n_dets - hyp.shape[0]) / n_dets) @@ -362,12 +342,7 @@ def _get_n_best_paf_graphs( # Only 1 animal, let us return the full graph indices only return ([existing_edges], dict(zip(existing_edges, [0] * len(existing_edges)))) - scores, _ = zip( - *[ - _calc_separability(between_train[n], within_train[n], metric=metric) - for n in existing_edges - ] - ) + scores, _ = zip(*[_calc_separability(between_train[n], within_train[n], metric=metric) for n in existing_edges]) # Find minimal skeleton G = nx.Graph() @@ -424,9 +399,7 @@ def cross_validate_paf_graphs( metadata = pickle.load(file) params = _set_up_evaluation(data) - to_ignore = auxfun_multianimal.filter_unwanted_paf_connections( - cfg, params["paf_graph"] - ) + to_ignore = auxfun_multianimal.filter_unwanted_paf_connections(cfg, params["paf_graph"]) best_graphs = _get_n_best_paf_graphs( data, metadata, @@ -471,9 +444,7 @@ def cross_validate_paf_graphs( if not overwrite_config: shutil.copy(pose_config, pose_config.replace(".yaml", "_old.yaml")) inds = list(paf_inds[size_opt]) - auxiliaryfunctions.edit_config( - pose_config, {"paf_best": [int(ind) for ind in inds]} - ) + auxiliaryfunctions.edit_config(pose_config, {"paf_best": [int(ind) for ind in inds]}) if output_name: with open(output_name, "wb") as file: pickle.dump([results], file) diff --git a/deeplabcut/core/engine.py b/deeplabcut/core/engine.py index c6f07ca69d..dadad5871e 100644 --- a/deeplabcut/core/engine.py +++ b/deeplabcut/core/engine.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Defines the deep learning frameworks available""" + from __future__ import annotations from dataclasses import dataclass diff --git a/deeplabcut/core/inferenceutils.py b/deeplabcut/core/inferenceutils.py index 35eef50cae..4abeb85e87 100644 --- a/deeplabcut/core/inferenceutils.py +++ b/deeplabcut/core/inferenceutils.py @@ -61,9 +61,7 @@ def __init__(self, j1, j2, affinity=1): self._length = sqrt((j1.pos[0] - j2.pos[0]) ** 2 + (j1.pos[1] - j2.pos[1]) ** 2) def __repr__(self): - return ( - f"Link {self.idx}, affinity={self.affinity:.2f}, length={self.length:.2f}" - ) + return f"Link {self.idx}, affinity={self.affinity:.2f}, length={self.length:.2f}" @property def confidence(self): @@ -261,9 +259,7 @@ def __init__( self.max_overlap = max_overlap self._has_identity = "identity" in self[0] if identity_only and not self._has_identity: - warnings.warn( - "The network was not trained with identity; setting `identity_only` to False." - ) + warnings.warn("The network was not trained with identity; setting `identity_only` to False.") self.identity_only = identity_only & self._has_identity self.nan_policy = nan_policy self.force_fusion = force_fusion @@ -367,13 +363,9 @@ def calibrate(self, train_data_file): self.safe_edge = True except np.linalg.LinAlgError: # Covariance matrix estimation fails due to numerical singularities - warnings.warn( - "The assembler could not be robustly calibrated. Continuing without it..." - ) + warnings.warn("The assembler could not be robustly calibrated. Continuing without it...") - def calc_assembly_mahalanobis_dist( - self, assembly, return_proba=False, nan_policy="little" - ): + def calc_assembly_mahalanobis_dist(self, assembly, return_proba=False, nan_policy="little"): if self._kde is None: raise ValueError("Assembler should be calibrated first with training data.") @@ -452,9 +444,7 @@ def extract_best_links(self, joints_dict, costs, trees=None): aff[np.isnan(aff)] = 0 if trees: - vecs = np.vstack( - [[*det_s.pos, *det_t.pos] for det_s in dets_s for det_t in dets_t] - ) + vecs = np.vstack([[*det_s.pos, *det_t.pos] for det_s in dets_s for det_t in dets_t]) dists = [] for n, tree in enumerate(trees, start=1): d, _ = tree.query(vecs) @@ -463,15 +453,8 @@ def extract_best_links(self, joints_dict, costs, trees=None): aff *= w.reshape(aff.shape) if self.greedy: - conf = np.asarray( - [ - [det_s.confidence * det_t.confidence for det_t in dets_t] - for det_s in dets_s - ] - ) - rows, cols = np.where( - (conf >= self.pcutoff * self.pcutoff) & (aff >= self.min_affinity) - ) + conf = np.asarray([[det_s.confidence * det_t.confidence for det_t in dets_t] for det_s in dets_s]) + rows, cols = np.where((conf >= self.pcutoff * self.pcutoff) & (aff >= self.min_affinity)) candidates = sorted( zip(rows, cols, aff[rows, cols], lengths[rows, cols]), key=lambda x: x[2], @@ -487,18 +470,14 @@ def extract_best_links(self, joints_dict, costs, trees=None): if len(i_seen) == self.max_n_individuals: break else: # Optimal keypoint pairing - inds_s = sorted( - range(len(dets_s)), key=lambda x: dets_s[x].confidence, reverse=True - )[: self.max_n_individuals] - inds_t = sorted( - range(len(dets_t)), key=lambda x: dets_t[x].confidence, reverse=True - )[: self.max_n_individuals] - keep_s = [ - ind for ind in inds_s if dets_s[ind].confidence >= self.pcutoff + inds_s = sorted(range(len(dets_s)), key=lambda x: dets_s[x].confidence, reverse=True)[ + : self.max_n_individuals ] - keep_t = [ - ind for ind in inds_t if dets_t[ind].confidence >= self.pcutoff + inds_t = sorted(range(len(dets_t)), key=lambda x: dets_t[x].confidence, reverse=True)[ + : self.max_n_individuals ] + keep_s = [ind for ind in inds_s if dets_s[ind].confidence >= self.pcutoff] + keep_t = [ind for ind in inds_t if dets_t[ind].confidence >= self.pcutoff] aff = aff[np.ix_(keep_s, keep_t)] rows, cols = linear_sum_assignment(aff, maximize=True) for row, col in zip(rows, cols): @@ -537,9 +516,7 @@ def push_to_stack(i): if new_ind in assembled: continue if safe_edge: - d_old = self.calc_assembly_mahalanobis_dist( - assembly, nan_policy=nan_policy - ) + d_old = self.calc_assembly_mahalanobis_dist(assembly, nan_policy=nan_policy) success = assembly.add_link(best, store_dict=True) if not success: assembly._dict = dict() @@ -594,9 +571,7 @@ def build_assemblies(self, links): continue assembly = Assembly(self.n_multibodyparts) assembly.add_link(link) - self._fill_assembly( - assembly, lookup, assembled, self.safe_edge, self.nan_policy - ) + self._fill_assembly(assembly, lookup, assembled, self.safe_edge, self.nan_policy) for link in assembly._links: i, j = link.idx lookup[i].pop(j) @@ -608,10 +583,7 @@ def build_assemblies(self, links): n_extra = len(assemblies) - self.max_n_individuals if n_extra > 0: if self.safe_edge: - ds_old = [ - self.calc_assembly_mahalanobis_dist(assembly) - for assembly in assemblies - ] + ds_old = [self.calc_assembly_mahalanobis_dist(assembly) for assembly in assemblies] while len(assemblies) > self.max_n_individuals: ds = [] for i, j in itertools.combinations(range(len(assemblies)), 2): @@ -741,10 +713,7 @@ def _assemble(self, data_dict, ind_frame): for _, group in groups: ass = Assembly(self.n_multibodyparts) for joint in sorted(group, key=lambda x: x.confidence, reverse=True): - if ( - joint.confidence >= self.pcutoff - and joint.label < self.n_multibodyparts - ): + if joint.confidence >= self.pcutoff and joint.label < self.n_multibodyparts: ass.add_joint(joint) if len(ass): assemblies.append(ass) @@ -773,11 +742,7 @@ def _assemble(self, data_dict, ind_frame): assembled.update(assembled_) # Remove invalid assemblies - discarded = set( - joint - for joint in joints - if joint.idx not in assembled and np.isfinite(joint.confidence) - ) + discarded = set(joint for joint in joints if joint.idx not in assembled and np.isfinite(joint.confidence)) for assembly in assemblies[::-1]: if 0 < assembly.n_links < self.min_n_links or not len(assembly): for link in assembly._links: @@ -785,9 +750,7 @@ def _assemble(self, data_dict, ind_frame): assemblies.remove(assembly) if 0 < self.max_overlap < 1: # Non-maximum pose suppression if self._kde is not None: - scores = [ - -self.calc_assembly_mahalanobis_dist(ass) for ass in assemblies - ] + scores = [-self.calc_assembly_mahalanobis_dist(ass) for ass in assemblies] else: scores = [ass._affinity for ass in assemblies] lst = list(zip(scores, assemblies)) @@ -841,7 +804,6 @@ def assemble(self, chunk_size=1, n_processes=None): # work nicely with the GUI or interactive sessions. # In that case, we fall back to the serial assembly. if chunk_size == 0 or multiprocessing.get_start_method() == "spawn": - for i, data_dict in enumerate(tqdm(self)): assemblies, unique = self._assemble(data_dict, i) if assemblies: @@ -857,9 +819,7 @@ def wrapped(i): n_frames = len(self.metadata["imnames"]) with multiprocessing.Pool(n_processes) as p: with tqdm(total=n_frames) as pbar: - for i, (assemblies, unique) in p.imap_unordered( - wrapped, range(n_frames), chunksize=chunk_size - ): + for i, (assemblies, unique) in p.imap_unordered(wrapped, range(n_frames), chunksize=chunk_size): if assemblies: self.assemblies[i] = assemblies if unique is not None: @@ -878,9 +838,7 @@ def parse_metadata(data): params["joint_names"] = data["metadata"]["all_joints_names"] params["num_joints"] = len(params["joint_names"]) params["paf_graph"] = data["metadata"]["PAFgraph"] - params["paf"] = data["metadata"].get( - "PAFinds", np.arange(len(params["joint_names"])) - ) + params["paf"] = data["metadata"].get("PAFinds", np.arange(len(params["joint_names"]))) params["bpts"] = params["ibpts"] = range(params["num_joints"]) params["imnames"] = [fn for fn in list(data) if fn != "metadata"] return params @@ -973,11 +931,7 @@ def calc_object_keypoint_similarity( else: oks = [] xy_preds = [xy_pred] - combos = ( - pair - for l in range(len(symmetric_kpts)) - for pair in itertools.combinations(symmetric_kpts, l + 1) - ) + combos = (pair for l in range(len(symmetric_kpts)) for pair in itertools.combinations(symmetric_kpts, l + 1)) for pairs in combos: # Swap corresponding keypoints tmp = xy_pred.copy() @@ -1014,9 +968,7 @@ def match_assemblies( num_ground_truth = len(ground_truth) # Sort predictions by score - inds_pred = np.argsort( - [ins.affinity if ins.n_links else ins.confidence for ins in predictions] - )[::-1] + inds_pred = np.argsort([ins.affinity if ins.n_links else ins.confidence for ins in predictions])[::-1] predictions = np.asarray(predictions)[inds_pred] # indices of unmatched ground truth assemblies @@ -1122,9 +1074,7 @@ def find_outlier_assemblies(dict_of_assemblies, criterion="area", qs=(5, 95)): raise ValueError(f"Invalid criterion {criterion}.") if len(qs) != 2: - raise ValueError( - "Two percentiles (for lower and upper bounds) should be given." - ) + raise ValueError("Two percentiles (for lower and upper bounds) should be given.") tuples = [] for frame_ind, assemblies in dict_of_assemblies.items(): @@ -1228,9 +1178,7 @@ def evaluate_assembly_greedy( oks = np.asarray([match.oks for match in all_matched])[sorted_pred_indices] # Compute prediction and recall - p, r = _compute_precision_and_recall( - total_gt_assemblies, oks, oks_t, recall_thresholds - ) + p, r = _compute_precision_and_recall(total_gt_assemblies, oks, oks_t, recall_thresholds) precisions.append(p) recalls.append(r) @@ -1301,9 +1249,7 @@ def evaluate_assembly( precisions = [] recalls = [] for t in oks_thresholds: - p, r = _compute_precision_and_recall( - total_gt_assemblies, oks, t, recall_thresholds - ) + p, r = _compute_precision_and_recall(total_gt_assemblies, oks, t, recall_thresholds) precisions.append(p) recalls.append(r) diff --git a/deeplabcut/core/metrics/api.py b/deeplabcut/core/metrics/api.py index 8dded92143..348d8c7abf 100644 --- a/deeplabcut/core/metrics/api.py +++ b/deeplabcut/core/metrics/api.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """API methods to get metrics for deep learning models""" + from __future__ import annotations import numpy as np @@ -125,7 +126,9 @@ def compute_metrics( if compute_detection_rmse and not single_animal: det_rmse, det_rmse_p = distance_metrics.compute_detection_rmse( - data, pcutoff, data_unique=data_unique, + data, + pcutoff, + data_unique=data_unique, ) results["rmse_detections"] = det_rmse results["rmse_detections_pcutoff"] = det_rmse_p diff --git a/deeplabcut/core/metrics/bbox.py b/deeplabcut/core/metrics/bbox.py index 83478e55fa..534ceb3e67 100644 --- a/deeplabcut/core/metrics/bbox.py +++ b/deeplabcut/core/metrics/bbox.py @@ -13,6 +13,7 @@ Metrics are currently computed using pycocotools, which can be installed with `pypi` (see https://github.com/ppwwyyxx/cocoapi/tree/master). """ + from __future__ import annotations from unittest.mock import Mock, patch @@ -68,7 +69,11 @@ def compute_bbox_metrics( coco.dataset["annotations"] = [] coco.dataset["categories"] = [{"id": 1, "name": "animals", "supercategory": "obj"}] coco.dataset["images"] = [] - coco.dataset['info'] = {"description": "Generated by DeepLabCut","year": datetime.now().year,"date_created": datetime.now().strftime("%Y-%m-%d")} + coco.dataset["info"] = { + "description": "Generated by DeepLabCut", + "year": datetime.now().year, + "date_created": datetime.now().strftime("%Y-%m-%d"), + } predictions = [] for idx, (img, gt) in enumerate(ground_truth.items()): img_id = idx + 1 diff --git a/deeplabcut/core/metrics/distance_metrics.py b/deeplabcut/core/metrics/distance_metrics.py index 6c54817e7a..43a0dfc031 100644 --- a/deeplabcut/core/metrics/distance_metrics.py +++ b/deeplabcut/core/metrics/distance_metrics.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Implementations of methods to compute distance metrics such as RMSE or OKS""" + from __future__ import annotations import numpy as np @@ -276,10 +277,12 @@ def compute_rmse( if pixel_errors is not None: bpt_cutoffs = pcutoff if not isinstance(pcutoff, (int, float)): - bpt_cutoffs = pcutoff[:pixel_errors.shape[1]] + bpt_cutoffs = pcutoff[: pixel_errors.shape[1]] error, support, cutoff_error, cutoff_support = collect_pixel_errors( - pixel_errors, keypoint_scores, bpt_cutoffs, + pixel_errors, + keypoint_scores, + bpt_cutoffs, ) unique_pixel_errors, unique_keypoint_scores = None, None @@ -291,9 +294,11 @@ def compute_rmse( bpt_cutoffs = pcutoff if not isinstance(pcutoff, (int, float)): - bpt_cutoffs = pcutoff[-unique_pixel_errors.shape[1]:] + bpt_cutoffs = pcutoff[-unique_pixel_errors.shape[1] :] u_error, u_support, u_cutoff_error, u_cutoff_support = collect_pixel_errors( - unique_pixel_errors, unique_keypoint_scores, bpt_cutoffs, + unique_pixel_errors, + unique_keypoint_scores, + bpt_cutoffs, ) error += u_error support += u_support @@ -384,8 +389,7 @@ def compute_detection_rmse( if data_unique is not None: for image_gt, image_pred in data_unique: assert len(image_gt) <= 1 and len(image_pred) <= 1, ( - f"Unique GT an predictions must have length 0 or 1! Found {image_gt.shape}, " - f"{image_pred.shape}." + f"Unique GT an predictions must have length 0 or 1! Found {image_gt.shape}, {image_pred.shape}." ) if len(image_gt) == 1 and len(image_pred) == 1: diff --git a/deeplabcut/core/metrics/identity.py b/deeplabcut/core/metrics/identity.py index 1720bfdffa..92353db9da 100644 --- a/deeplabcut/core/metrics/identity.py +++ b/deeplabcut/core/metrics/identity.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Implementations of methods to compute identity prediction accuracy""" + from __future__ import annotations import numpy as np @@ -77,9 +78,7 @@ def compute_identity_scores( found = neighbors != -1 indices = np.flatnonzero(all_bpts == bpt) # Get the predicted identity of each bodypart by taking the argmax - ids[i, indices[indices_gt[found]], 1] = np.argmax( - bpt_id_scores[neighbors[found]], axis=1 - ) + ids[i, indices[indices_gt[found]], 1] = np.argmax(bpt_id_scores[neighbors[found]], axis=1) ids = ids.reshape((len(predictions), len(individuals), len(bodyparts), 2)) results = {} diff --git a/deeplabcut/core/metrics/matching.py b/deeplabcut/core/metrics/matching.py index 95b28ebe5b..8a49e69cad 100644 --- a/deeplabcut/core/metrics/matching.py +++ b/deeplabcut/core/metrics/matching.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Algorithms to match predictions to ground truth labels""" + from __future__ import annotations from dataclasses import dataclass @@ -69,10 +70,7 @@ def from_pose(cls, pose: np.ndarray) -> "PotentialMatch": assert len(pose.shape) == 2 # Must be pose for a single individual scores = pose[:, 2] if np.all(np.isnan(scores)): - raise ValueError( - "Cannot create a Match from a pose prediction where all scores are nan " - f"(pose={pose})" - ) + raise ValueError(f"Cannot create a Match from a pose prediction where all scores are nan (pose={pose})") return PotentialMatch(pose=pose, score=np.nanmean(scores).item()) diff --git a/deeplabcut/core/trackingutils.py b/deeplabcut/core/trackingutils.py index 084bdb4502..ee294bee39 100644 --- a/deeplabcut/core/trackingutils.py +++ b/deeplabcut/core/trackingutils.py @@ -43,11 +43,7 @@ def calc_iou(bbox1, bbox2): w = max(0, x2 - x1) h = max(0, y2 - y1) wh = w * h - return wh / ( - (bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1]) - + (bbox2[2] - bbox2[0]) * (bbox2[3] - bbox2[1]) - - wh - ) + return wh / ((bbox1[2] - bbox1[0]) * (bbox1[3] - bbox1[1]) + (bbox2[2] - bbox2[0]) * (bbox2[3] - bbox2[1]) - wh) class BaseTracker: @@ -110,12 +106,8 @@ def aspect_ratio(self): return max(self.width, self.height) / min(self.width, self.height) def calc_similarity_with(self, other_ellipse): - max_dist = max( - self.height, self.width, other_ellipse.height, other_ellipse.width - ) - dist = math.sqrt( - (self.x - other_ellipse.x) ** 2 + (self.y - other_ellipse.y) ** 2 - ) + max_dist = max(self.height, self.width, other_ellipse.height, other_ellipse.width) + dist = math.sqrt((self.x - other_ellipse.x) ** 2 + (self.y - other_ellipse.y) ** 2) if max_dist == 0: max_dist = 1 @@ -152,9 +144,7 @@ def draw(self, show_axes=True, ax=None, **kwargs): if show_axes: major = Line2D([-self.width / 2, self.width / 2], [0, 0], lw=3, zorder=3) minor = Line2D([0, 0], [-self.height / 2, self.height / 2], lw=3, zorder=3) - trans = ( - Affine2D().rotate(self.theta).translate(self.x, self.y) + ax.transData - ) + trans = Affine2D().rotate(self.theta).translate(self.x, self.y) + ax.transData major.set_transform(trans) minor.set_transform(trans) ax.add_artist(major) @@ -378,13 +368,9 @@ def convert_x_to_bbox(x, score=None): w = np.sqrt(x[2] * x[3]) h = x[2] / w if score is None: - return np.array( - [x[0] - w / 2.0, x[1] - h / 2.0, x[0] + w / 2.0, x[1] + h / 2.0] - ).reshape((1, 4)) + return np.array([x[0] - w / 2.0, x[1] - h / 2.0, x[0] + w / 2.0, x[1] + h / 2.0]).reshape((1, 4)) else: - return np.array( - [x[0] - w / 2.0, x[1] - h / 2.0, x[0] + w / 2.0, x[1] + h / 2.0, score] - ).reshape((1, 5)) + return np.array([x[0] - w / 2.0, x[1] - h / 2.0, x[0] + w / 2.0, x[1] + h / 2.0, score]).reshape((1, 5)) @staticmethod def convert_bbox_to_z(bbox): @@ -455,12 +441,8 @@ def track(self, poses, identities=None): cost *= match cost_matrix[i, j] = cost row_indices, col_indices = linear_sum_assignment(cost_matrix, maximize=True) - unmatched_detections = [ - i for i, _ in enumerate(ellipses) if i not in row_indices - ] - unmatched_trackers = [ - j for j, _ in enumerate(trackers) if j not in col_indices - ] + unmatched_detections = [i for i, _ in enumerate(ellipses) if i not in row_indices] + unmatched_trackers = [j for j, _ in enumerate(trackers) if j not in col_indices] matches = [] for row, col in zip(row_indices, col_indices): val = cost_matrix[row, col] @@ -503,13 +485,9 @@ def track(self, poses, identities=None): ret = [] for trk in reversed(self.trackers): d = trk.state - if (trk.time_since_update < 1) and ( - trk.hit_streak >= self.min_hits or self.n_frames <= self.min_hits - ): + if (trk.time_since_update < 1) and (trk.hit_streak >= self.min_hits or self.n_frames <= self.min_hits): ret.append( - np.concatenate((d, [trk.id, int(animalindex[i - 1])])).reshape( - 1, -1 - ) + np.concatenate((d, [trk.id, int(animalindex[i - 1])])).reshape(1, -1) ) # for DLC we also return the original animalid # +1 as MOT benchmark requires positive >> this is removed for DLC! i -= 1 @@ -558,9 +536,7 @@ def object_keypoint_similarity(x, y): xx = x[mask] yy = y[mask] dist = np.linalg.norm(xx - yy, axis=1) - scale = np.sqrt( - np.product(np.ptp(yy, axis=0)) - ) # square root of bounding box area + scale = np.sqrt(np.product(np.ptp(yy, axis=0))) # square root of bounding box area oks = np.exp(-0.5 * (dist / (0.05 * scale)) ** 2) return np.mean(oks) @@ -597,9 +573,7 @@ def track(self, poses): row_indices, col_indices = linear_sum_assignment(mat, maximize=False) unmatched_poses = [p for p, _ in enumerate(poses) if p not in row_indices] - unmatched_trackers = [ - t for t, _ in enumerate(poses_ref) if t not in col_indices - ] + unmatched_trackers = [t for t, _ in enumerate(poses_ref) if t not in col_indices] # Remove matched detections with low OKS # matches = [] # for row, col in zip(row_indices, col_indices): @@ -662,9 +636,7 @@ def track(self, dets): for ind in np.flatnonzero(empty)[::-1]: self.trackers.pop(ind) - matched, unmatched_dets, unmatched_trks = self.match_detections_to_trackers( - dets, trackers, self.iou_threshold - ) + matched, unmatched_dets, unmatched_trks = self.match_detections_to_trackers(dets, trackers, self.iou_threshold) # update matched trackers with assigned detections animalindex = [] @@ -686,13 +658,9 @@ def track(self, dets): ret = [] for trk in reversed(self.trackers): d = trk.state - if (trk.time_since_update < 1) and ( - trk.hit_streak >= self.min_hits or self.n_frames <= self.min_hits - ): + if (trk.time_since_update < 1) and (trk.hit_streak >= self.min_hits or self.n_frames <= self.min_hits): ret.append( - np.concatenate((d, [trk.id, int(animalindex[i - 1])])).reshape( - 1, -1 - ) + np.concatenate((d, [trk.id, int(animalindex[i - 1])])).reshape(1, -1) ) # for DLC we also return the original animalid # +1 as MOT benchmark requires positive >> this is removed for DLC! i -= 1 @@ -826,9 +794,7 @@ def reconstruct_all_ellipses(data, sd): return ellipses -def _track_individuals( - individuals, min_hits=1, max_age=5, similarity_threshold=0.6, track_method="ellipse" -): +def _track_individuals(individuals, min_hits=1, max_age=5, similarity_threshold=0.6, track_method="ellipse"): if track_method not in TRACK_METHODS: raise ValueError(f"Unknown {track_method} tracker.") diff --git a/deeplabcut/core/visualization.py b/deeplabcut/core/visualization.py index 1911a4f769..d415db8470 100644 --- a/deeplabcut/core/visualization.py +++ b/deeplabcut/core/visualization.py @@ -8,7 +8,8 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Visualization methods for """ +"""Visualization methods for""" + from __future__ import annotations from pathlib import Path @@ -171,12 +172,11 @@ def generate_model_output_plots( paf_colormap: The colormap to use for the PAF maps. output_suffix: The filename suffix for the maps to output. """ + def _filename(map_name) -> str: return f"{image_name}_{map_name}_{output_suffix}.png" - to_plot = [ - i for i, bpt in enumerate(bodypart_names) if bpt in bodyparts_to_plot - ] + to_plot = [i for i, bpt in enumerate(bodypart_names) if bpt in bodyparts_to_plot] if len(to_plot) > 1: map_ = scmap[:, :, to_plot].sum(axis=2) elif len(to_plot) == 1 and len(bodypart_names) > 1: @@ -217,9 +217,7 @@ def _filename(map_name) -> str: for n, edge in enumerate(paf_graph): if any(ind in to_plot for ind in edge): e0, e1 = edge - edge_list.append( - [(2 * n, 2 * n + 1), (bodypart_names[e0], bodypart_names[e1])] - ) + edge_list.append([(2 * n, 2 * n + 1), (bodypart_names[e0], bodypart_names[e1])]) if paf_all_in_one: inds = [elem[0] for elem in edge_list] diff --git a/deeplabcut/core/weight_init.py b/deeplabcut/core/weight_init.py index 8a6d374e8a..01fe499c51 100644 --- a/deeplabcut/core/weight_init.py +++ b/deeplabcut/core/weight_init.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Classes to configure how to initialize model weights""" + from __future__ import annotations import warnings @@ -61,8 +62,7 @@ def __post_init__(self): if self.with_decoder and self.conversion_array is None: raise ValueError( - f"You must specify a conversion_array to initialize decoder weights " - f"(``with_decoder=True``)." + f"You must specify a conversion_array to initialize decoder weights (``with_decoder=True``)." ) if self.bodyparts is not None and self.conversion_array is None: @@ -187,6 +187,7 @@ def build( The built WeightInitialization. """ from deeplabcut.modelzoo import build_weight_init + deprecation_warning = ( "The `WeightInitialization.build` is deprecated and will be removed in a " "future version of DeepLabCut. Please use `build_weight_init` from " diff --git a/deeplabcut/create_project/add.py b/deeplabcut/create_project/add.py index 9b83b12444..c3b56412be 100644 --- a/deeplabcut/create_project/add.py +++ b/deeplabcut/create_project/add.py @@ -10,9 +10,7 @@ # -def add_new_videos( - config, videos, copy_videos=False, coords=None, extract_frames=False -): +def add_new_videos(config, videos, copy_videos=False, coords=None, extract_frames=False): """ Add new videos to the config file at any stage of the project. @@ -101,10 +99,7 @@ def add_new_videos( subprocess.check_call("mklink %s %s" % (dst, src), shell=True) except (OSError, subprocess.CalledProcessError): - print( - "Symlink creation impossible (exFat architecture?): " - "copying the video instead." - ) + print("Symlink creation impossible (exFat architecture?): copying the video instead.") shutil.copy(os.fspath(src), os.fspath(dst)) print("{} copied to {}".format(src, dst)) videos = destinations @@ -133,13 +128,7 @@ def add_new_videos( videos_str = [str(video) for video in videos] auxiliaryfunctions.write_config(config, cfg) if extract_frames: - frame_extraction.extract_frames( - config, userfeedback=False, videos_list=videos_str - ) - print( - "New videos were added to the project and frames have been extracted for labeling!" - ) + frame_extraction.extract_frames(config, userfeedback=False, videos_list=videos_str) + print("New videos were added to the project and frames have been extracted for labeling!") else: - print( - "New videos were added to the project! Use the function 'extract_frames' to select frames for labeling." - ) + print("New videos were added to the project! Use the function 'extract_frames' to select frames for labeling.") diff --git a/deeplabcut/create_project/demo_data.py b/deeplabcut/create_project/demo_data.py index 9e53eb7770..0ab9b70bba 100644 --- a/deeplabcut/create_project/demo_data.py +++ b/deeplabcut/create_project/demo_data.py @@ -69,8 +69,6 @@ def transform_data(config): print("This is not an official demo dataset.") if "WILL BE AUTOMATICALLY UPDATED BY DEMO CODE" in cfg["video_sets"].keys(): - cfg["video_sets"][str(video_file)] = cfg["video_sets"].pop( - "WILL BE AUTOMATICALLY UPDATED BY DEMO CODE" - ) + cfg["video_sets"][str(video_file)] = cfg["video_sets"].pop("WILL BE AUTOMATICALLY UPDATED BY DEMO CODE") auxiliaryfunctions.write_config(config, cfg) diff --git a/deeplabcut/create_project/modelzoo.py b/deeplabcut/create_project/modelzoo.py index dd1f233531..8f86c50200 100644 --- a/deeplabcut/create_project/modelzoo.py +++ b/deeplabcut/create_project/modelzoo.py @@ -354,9 +354,7 @@ def create_pretrained_project_pytorch( detector_name = "fasterrcnn_resnet50_fpn_v2" if dataset not in get_available_datasets(): - raise ValueError( - f"Invalid dataset '{dataset}'. Available datasets are: {get_available_datasets()}" - ) + raise ValueError(f"Invalid dataset '{dataset}'. Available datasets are: {get_available_datasets()}") if net_name not in get_available_models(dataset): raise ValueError( @@ -433,16 +431,12 @@ def create_pretrained_project_pytorch( ) pytorch_config = add_metadata(config, pytorch_config, train_cfg_path) pytorch_config["resume_training_from"] = str(train_dir / new_snapshot_name) - pytorch_config["detector"]["resume_training_from"] = str( - train_dir / new_detector_name - ) + pytorch_config["detector"]["resume_training_from"] = str(train_dir / new_detector_name) write_config(train_cfg_path, pytorch_config) # Create test pose_cfg.yaml test_cfg_path = test_dir / "pose_cfg.yaml" - make_pytorch_test_config( - model_config=pytorch_config, test_config_path=test_cfg_path, save=True - ) + make_pytorch_test_config(model_config=pytorch_config, test_config_path=test_cfg_path, save=True) # Create inference_cfg.yaml if needed if multi_animal: @@ -469,9 +463,7 @@ def _create_inference_config(inference_cfg_path: str | Path, project_cfg: dict): topktoretain=len(project_cfg["individuals"]), withid=project_cfg.get("identity", False), ) - default_inf_path = ( - Path(auxiliaryfunctions.get_deeplabcut_path()) / "inference_cfg.yaml" - ) + default_inf_path = Path(auxiliaryfunctions.get_deeplabcut_path()) / "inference_cfg.yaml" MakeInference_yaml(inf_updates, inference_cfg_path, default_inf_path) @@ -554,9 +546,7 @@ def create_pretrained_project_tensorflow( if model in MODELOPTIONS: cwd = os.getcwd() - cfg = deeplabcut.create_new_project( - project, experimenter, videos, working_directory, copy_videos, videotype - ) + cfg = deeplabcut.create_new_project(project, experimenter, videos, working_directory, copy_videos, videotype) if trainFraction is not None: auxiliaryfunctions.edit_config(cfg, {"TrainingFraction": [trainFraction]}) @@ -635,16 +625,8 @@ def create_pretrained_project_tensorflow( modelfoldername = auxiliaryfunctions.get_model_folder( trainFraction=config["TrainingFraction"][0], shuffle=1, cfg=config ) - path_train_config = str( - os.path.join( - config["project_path"], Path(modelfoldername), "train", "pose_cfg.yaml" - ) - ) - path_test_config = str( - os.path.join( - config["project_path"], Path(modelfoldername), "test", "pose_cfg.yaml" - ) - ) + path_train_config = str(os.path.join(config["project_path"], Path(modelfoldername), "train", "pose_cfg.yaml")) + path_test_config = str(os.path.join(config["project_path"], Path(modelfoldername), "test", "pose_cfg.yaml")) # Download the weights and put then in appropriate directory print("Downloading weights...") @@ -666,9 +648,7 @@ def create_pretrained_project_tensorflow( # model_path = auxfun_models.check_for_weights(pose_cfg['net_type'], parent_path) # Updating training and test pose_cfg: - snapshotname = [fn for fn in os.listdir(train_dir) if ".meta" in fn][0].split( - ".meta" - )[0] + snapshotname = [fn for fn in os.listdir(train_dir) if ".meta" in fn][0].split(".meta")[0] dict2change = { "init_weights": str(os.path.join(train_dir, snapshotname)), "project_path": str(config["project_path"]), @@ -707,9 +687,7 @@ def create_pretrained_project_tensorflow( return "N/A", "N/A" -def _create_training_datasets_metadata( - config: dict, shuffle_dir_name: str, engine: Engine -): +def _create_training_datasets_metadata(config: dict, shuffle_dir_name: str, engine: Engine): # First create the metadata object metadata = TrainingDatasetMetadata.create(config) @@ -743,18 +721,12 @@ def _process_videos( if analyze_video: print("Analyzing video...") - deeplabcut.analyze_videos( - cfg_path, [video_dir], videotype=video_type, save_as_csv=True - ) + deeplabcut.analyze_videos(cfg_path, [video_dir], videotype=video_type, save_as_csv=True) if create_labeled_video: if filtered: deeplabcut.filterpredictions(cfg_path, [video_dir], video_type) print("Plotting results...") - deeplabcut.create_labeled_video( - cfg_path, [video_dir], video_type, draw_skeleton=True, filtered=filtered - ) - deeplabcut.plot_trajectories( - cfg_path, [video_dir], video_type, filtered=filtered - ) + deeplabcut.create_labeled_video(cfg_path, [video_dir], video_type, draw_skeleton=True, filtered=filtered) + deeplabcut.plot_trajectories(cfg_path, [video_dir], video_type, filtered=filtered) diff --git a/deeplabcut/create_project/new.py b/deeplabcut/create_project/new.py index ff7db0773a..94dd42d74b 100644 --- a/deeplabcut/create_project/new.py +++ b/deeplabcut/create_project/new.py @@ -151,10 +151,7 @@ def create_new_project( for i in paths: # Check if it is a folder if i.is_dir(): - vids_in_dir = [ - p for p in i.iterdir() - if str(p).lower().endswith(videotype) - ] + vids_in_dir = [p for p in i.iterdir() if str(p).lower().endswith(videotype)] if len(vids_in_dir) == 0: print("No videos found in", i) print( @@ -185,9 +182,7 @@ def create_new_project( if copy_videos: print("Copying the videos") for src, dst in zip(videos, destinations): - shutil.copy( - os.fspath(src), os.fspath(dst) - ) # https://www.python.org/dev/peps/pep-0519/ + shutil.copy(os.fspath(src), os.fspath(dst)) # https://www.python.org/dev/peps/pep-0519/ else: # creates the symlinks of the video and puts it in the videos directory. print("Attempting to create a symbolic link of the video ...") @@ -205,10 +200,7 @@ def create_new_project( subprocess.check_call("mklink %s %s" % (dst, src), shell=True) except (OSError, subprocess.CalledProcessError): - print( - "Symlink creation impossible (exFat architecture?): " - "copying the video instead." - ) + print("Symlink creation impossible (exFat architecture?): copying the video instead.") shutil.copy(os.fspath(src), os.fspath(dst)) print("{} copied to {}".format(src, dst)) videos = destinations @@ -247,11 +239,7 @@ def create_new_project( cfg_file, ruamelFile = auxiliaryfunctions.create_config_template(multianimal) cfg_file["multianimalproject"] = multianimal cfg_file["identity"] = False - cfg_file["individuals"] = ( - individuals - if individuals - else ["individual1", "individual2", "individual3"] - ) + cfg_file["individuals"] = individuals if individuals else ["individual1", "individual2", "individual3"] cfg_file["multianimalbodyparts"] = ["bodypart1", "bodypart2", "bodypart3"] cfg_file["uniquebodyparts"] = [] cfg_file["bodyparts"] = "MULTI!" diff --git a/deeplabcut/create_project/new_3d.py b/deeplabcut/create_project/new_3d.py index 7521e56126..78ed925ead 100644 --- a/deeplabcut/create_project/new_3d.py +++ b/deeplabcut/create_project/new_3d.py @@ -58,9 +58,7 @@ def create_new_project_3d(project, experimenter, num_cameras=2, working_director working_directory = "." wd = Path(working_directory).resolve() - project_name = "{pn}-{exp}-{date}-{triangulate}".format( - pn=project, exp=experimenter, date=date, triangulate="3d" - ) + project_name = "{pn}-{exp}-{date}-{triangulate}".format(pn=project, exp=experimenter, date=date, triangulate="3d") project_path = wd / project_name # Create project and sub-directories if not DEBUG and project_path.exists(): @@ -98,9 +96,7 @@ def create_new_project_3d(project, experimenter, num_cameras=2, working_director cfg_file_3d["markerColor"] = "r" cfg_file_3d["pcutoff"] = 0.4 cfg_file_3d["num_cameras"] = num_cameras - cfg_file_3d["camera_names"] = [ - str("camera-" + str(i)) for i in range(1, num_cameras + 1) - ] + cfg_file_3d["camera_names"] = [str("camera-" + str(i)) for i in range(1, num_cameras + 1)] cfg_file_3d["scorername_3d"] = "DLC_3D" cfg_file_3d["skeleton"] = [ @@ -113,19 +109,13 @@ def create_new_project_3d(project, experimenter, num_cameras=2, working_director for i in range(num_cameras): path = str( - "/home/mackenzie/DEEPLABCUT/DeepLabCut/2DprojectCam" - + str(i + 1) - + "-Mackenzie-2019-06-05/config.yaml" - ) - cfg_file_3d.insert( - len(cfg_file_3d), str("config_file_camera-" + str(i + 1)), path + "/home/mackenzie/DEEPLABCUT/DeepLabCut/2DprojectCam" + str(i + 1) + "-Mackenzie-2019-06-05/config.yaml" ) + cfg_file_3d.insert(len(cfg_file_3d), str("config_file_camera-" + str(i + 1)), path) for i in range(num_cameras): cfg_file_3d.insert(len(cfg_file_3d), str("shuffle_camera-" + str(i + 1)), 1) - cfg_file_3d.insert( - len(cfg_file_3d), str("trainingsetindex_camera-" + str(i + 1)), 0 - ) + cfg_file_3d.insert(len(cfg_file_3d), str("trainingsetindex_camera-" + str(i + 1)), 0) projconfigfile = os.path.join(str(project_path), "config.yaml") auxiliaryfunctions.write_config_3d(projconfigfile, cfg_file_3d) diff --git a/deeplabcut/generate_training_dataset/frame_extraction.py b/deeplabcut/generate_training_dataset/frame_extraction.py index fe807e3838..97b2c518dd 100755 --- a/deeplabcut/generate_training_dataset/frame_extraction.py +++ b/deeplabcut/generate_training_dataset/frame_extraction.py @@ -284,13 +284,9 @@ def extract_frames( # Check for variable correctness if start > 1 or stop > 1 or start < 0 or stop < 0 or start >= stop: - raise Exception( - "Erroneous start or stop values. Please correct it in the config file." - ) + raise Exception("Erroneous start or stop values. Please correct it in the config file.") if numframes2pick < 1 and not int(numframes2pick): - raise Exception( - "Perhaps consider extracting more, or a natural number of frames." - ) + raise Exception("Perhaps consider extracting more, or a natural number of frames.") if opencv: from deeplabcut.utils.auxfun_videos import VideoWriter @@ -340,12 +336,7 @@ def extract_frames( askuser = input( "The directory already contains some frames. Do you want to add to it?(yes/no): " ) - if not ( - askuser == "y" - or askuser == "yes" - or askuser == "Y" - or askuser == "Yes" - ): + if not (askuser == "y" or askuser == "yes" or askuser == "Y" or askuser == "Yes"): sys.exit("Delete the frames and try again later!") if crop == "GUI": @@ -371,13 +362,9 @@ def extract_frames( print("Extracting frames based on %s ..." % algo) if algo == "uniform": if opencv: - frames2pick = frameselectiontools.UniformFramescv2( - cap, numframes2pick, start, stop - ) + frames2pick = frameselectiontools.UniformFramescv2(cap, numframes2pick, start, stop) else: - frames2pick = frameselectiontools.UniformFrames( - clip, numframes2pick, start, stop - ) + frames2pick = frameselectiontools.UniformFrames(clip, numframes2pick, start, stop) elif algo == "kmeans": if opencv: frames2pick = frameselectiontools.KmeansbasedFrameselectioncv2( @@ -401,18 +388,16 @@ def extract_frames( ) else: print( - "Please implement this method yourself and send us a pull " - "request! Otherwise, choose 'uniform' or 'kmeans'." - ) + "Please implement this method yourself and send us a pull " + "request! Otherwise, choose 'uniform' or 'kmeans'." + ) frames2pick = [] if not len(frames2pick): print("Frame selection failed...") return [] - output_path = ( - Path(config).parents[0] / "labeled-data" / Path(video).stem - ) + output_path = Path(config).parents[0] / "labeled-data" / Path(video).stem output_path.mkdir(parents=True, exist_ok=True) is_valid = [] if opencv: @@ -421,12 +406,7 @@ def extract_frames( frame = cap.read_frame(crop=True) if frame is not None: image = img_as_ubyte(frame) - img_name = ( - str(output_path) - + "/img" - + str(index).zfill(indexlength) - + ".png" - ) + img_name = str(output_path) + "/img" + str(index).zfill(indexlength) + ".png" io.imsave(img_name, image) is_valid.append(True) else: @@ -437,12 +417,7 @@ def extract_frames( for index in frames2pick: try: image = img_as_ubyte(clip.get_frame(index * 1.0 / clip.fps)) - img_name = ( - str(output_path) - + "/img" - + str(index).zfill(indexlength) - + ".png" - ) + img_name = str(output_path) + "/img" + str(index).zfill(indexlength) + ".png" io.imsave(img_name, image) if np.var(image) == 0: # constant image print( @@ -469,9 +444,7 @@ def extract_frames( elif any(has_failed): print("Although most frames were extracted, some were invalid.") else: - print( - "Frames were successfully extracted, for the videos listed in the config.yaml file." - ) + print("Frames were successfully extracted, for the videos listed in the config.yaml file.") print( "\nYou can now label the frames using the function 'label_frames' " "(Note, you should label frames extracted from diverse videos (and many videos; we do not recommend training on single videos!))." @@ -499,9 +472,7 @@ def extract_frames( cams = cfg_3d["camera_names"] extCam_name = cams[extracted_cam] del cams[extracted_cam] - label_dirs = sorted( - glob.glob(os.path.join(labels_path, "*" + extCam_name + "*")) - ) + label_dirs = sorted(glob.glob(os.path.join(labels_path, "*" + extCam_name + "*"))) # select crop method crop_list = [] @@ -565,9 +536,7 @@ def extract_frames( ) else: io.imsave(img_name, image) - print( - "\n Done extracting matched frames. You can now begin labeling frames using the function label_frames\n" - ) + print("\n Done extracting matched frames. You can now begin labeling frames using the function label_frames\n") else: print( diff --git a/deeplabcut/generate_training_dataset/metadata.py b/deeplabcut/generate_training_dataset/metadata.py index a26a5eadda..52828e213c 100644 --- a/deeplabcut/generate_training_dataset/metadata.py +++ b/deeplabcut/generate_training_dataset/metadata.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """File containing methods to load and parse shuffle metadata""" + from __future__ import annotations import logging @@ -27,6 +28,7 @@ @dataclass(frozen=True) class DataSplit: """Class representing the metadata for a shuffle""" + train_indices: tuple[int, ...] test_indices: tuple[int, ...] @@ -39,14 +41,14 @@ def __post_init__(self) -> None: idx = np.array(indices) if not np.all(idx[:-1] < idx[1:]): raise RuntimeError( - f"The training and test indices in a data split must be sorted in " - f"strictly ascending order." + f"The training and test indices in a data split must be sorted in strictly ascending order." ) @dataclass(frozen=True) class ShuffleMetadata: """Class representing the metadata for a shuffle""" + name: str train_fraction: float index: int @@ -83,7 +85,7 @@ def load_split(self, cfg: dict, trainset_path: Path) -> "ShuffleMetadata": split=DataSplit( train_indices=tuple(sorted([int(idx) for idx in train_idx])), test_indices=tuple(sorted([int(idx) for idx in test_idx])), - ) + ), ) @@ -120,6 +122,7 @@ class TrainingDatasetMetadata: trainset_metadata = trainset_metadata.add(new_shuffle) trainset_metadata.save() # saves to disk """ + project_config: dict shuffles: tuple[ShuffleMetadata, ...] file_header: tuple[str] = ( @@ -161,9 +164,7 @@ def add( ValueError: if overwrite=False and there is already a shuffle with the given index in the metadata file. """ - existing_indices = [ - s.index for s in self.shuffles if s.train_fraction == shuffle.train_fraction - ] + existing_indices = [s.index for s in self.shuffles if s.train_fraction == shuffle.train_fraction] if shuffle.index in existing_indices: if not overwrite: raise RuntimeError( @@ -173,9 +174,7 @@ def add( ) existing_shuffles = [ - s - for s in self.shuffles - if (s.index != shuffle.index or s.train_fraction != shuffle.train_fraction) + s for s in self.shuffles if (s.index != shuffle.index or s.train_fraction != shuffle.train_fraction) ] shuffles = existing_shuffles + [shuffle] return TrainingDatasetMetadata( @@ -197,16 +196,10 @@ def get(self, trainset_index: int = 0, index: int = 0) -> ShuffleMetadata: """ train_fraction = self.project_config["TrainingFraction"][trainset_index] for shuffle in self.shuffles: - if ( - shuffle.train_fraction == train_fraction - and shuffle.index == index - ): + if shuffle.train_fraction == train_fraction and shuffle.index == index: return shuffle - raise ValueError( - f"Could not find a shuffle with trainingset fraction {train_fraction} and " - f"index {index}" - ) + raise ValueError(f"Could not find a shuffle with trainingset fraction {train_fraction} and index {index}") def save(self) -> None: """Saves the training dataset metadata to disk""" @@ -292,9 +285,7 @@ def create(config: str | Path | dict) -> TrainingDatasetMetadata: trainset_path = TrainingDatasetMetadata.path(cfg).parent if trainset_path.exists(): shuffle_docs = [ - f - for f in trainset_path.iterdir() - if re.match(r"Documentation_data-.+shuffle[0-9]+\.pickle", f.name) + f for f in trainset_path.iterdir() if re.match(r"Documentation_data-.+shuffle[0-9]+\.pickle", f.name) ] else: trainset_path.mkdir(parents=True) @@ -380,7 +371,7 @@ def update_metadata( split=DataSplit( train_indices=tuple(sorted([int(i) for i in train_indices])), test_indices=tuple(sorted([int(i) for i in test_indices])), - ) + ), ) metadata = metadata.add(shuffle=new_shuffle, overwrite=overwrite) metadata.save() @@ -414,9 +405,7 @@ def get_shuffle_engine( shuffle_metadata = metadata.get(trainingsetindex, shuffle) if modelprefix: # try to get the engine by checking which models folder exists - engines = find_engines_from_model_folders( - cfg, trainingsetindex, shuffle, modelprefix - ) + engines = find_engines_from_model_folders(cfg, trainingsetindex, shuffle, modelprefix) if len(engines) == 0: raise ValueError( f"Couldn't find any shuffles with trainingsetindex={trainingsetindex}, " diff --git a/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py b/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py index 0886a4f60e..34e9038cd5 100755 --- a/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py +++ b/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py @@ -55,9 +55,7 @@ def format_multianimal_training_data( n_individuals = individuals.unique().size mask_single = individuals.str.contains("single") n_animals = n_individuals - 1 if np.any(mask_single) else n_individuals - array = np.full( - (nrows, n_individuals, n_bodyparts, 3), fill_value=np.nan, dtype=np.float32 - ) + array = np.full((nrows, n_individuals, n_bodyparts, 3), fill_value=np.nan, dtype=np.float32) array[..., 0] = np.arange(n_bodyparts) temp = df.to_numpy() temp_multi = temp[:, ~mask_single].reshape((nrows, n_animals, -1, 2)) @@ -284,8 +282,7 @@ def create_multianimaltraining_dataset( if crop_sampling not in ("uniform", "keypoints", "density", "hybrid"): raise ValueError( - f"Invalid sampling {crop_sampling}. Must be " - f"either 'uniform', 'keypoints', 'density', or 'hybrid." + f"Invalid sampling {crop_sampling}. Must be either 'uniform', 'keypoints', 'density', or 'hybrid." ) # Loading metadata from config file: @@ -314,10 +311,7 @@ def create_multianimaltraining_dataset( if engine is None: engine = compat.get_project_engine(cfg) - if not ( - any(net in net_type for net in ("resnet", "eff", "dlc", "mob")) - or engine == Engine.PYTORCH - ): + if not (any(net in net_type for net in ("resnet", "eff", "dlc", "mob")) or engine == Engine.PYTORCH): raise ValueError(f"Unsupported network {net_type} for engine {engine}.") multi_stage = False @@ -339,9 +333,7 @@ def create_multianimaltraining_dataset( if paf_graph is None: # Automatically form a complete PAF graph n_bpts = len(multianimalbodyparts) - partaffinityfield_graph = [ - list(edge) for edge in combinations(range(n_bpts), 2) - ] + partaffinityfield_graph = [list(edge) for edge in combinations(range(n_bpts), 2)] n_edges_orig = len(partaffinityfield_graph) # If the graph is unnecessarily large (with 15+ keypoints by default), # we randomly prune it to a size guaranteeing an average node degree of 6; @@ -356,21 +348,14 @@ def create_multianimaltraining_dataset( # Use the skeleton defined in the config file skeleton = cfg["skeleton"] paf_graph = [ - sorted( - (multianimalbodyparts.index(bpt1), multianimalbodyparts.index(bpt2)) - ) - for bpt1, bpt2 in skeleton + sorted((multianimalbodyparts.index(bpt1), multianimalbodyparts.index(bpt2))) for bpt1, bpt2 in skeleton ] - print( - "Using `skeleton` from the config file as a paf_graph. Data-driven skeleton will not be computed." - ) + print("Using `skeleton` from the config file as a paf_graph. Data-driven skeleton will not be computed.") # Ignore possible connections between 'multi' and 'unique' body parts; # one can never be too careful... to_ignore = auxfun_multianimal.filter_unwanted_paf_connections(cfg, paf_graph) - partaffinityfield_graph = [ - edge for i, edge in enumerate(paf_graph) if i not in to_ignore - ] + partaffinityfield_graph = [edge for i, edge in enumerate(paf_graph) if i not in to_ignore] auxfun_multianimal.validate_paf_graph(cfg, partaffinityfield_graph) print("Utilizing the following graph:", partaffinityfield_graph) @@ -397,19 +382,11 @@ def create_multianimaltraining_dataset( splits.append((train_frac, shuffle, (train_inds, test_inds))) else: if len(trainIndices) != len(testIndices) != len(Shuffles): - raise ValueError( - "Number of Shuffles and train and test indexes should be equal." - ) + raise ValueError("Number of Shuffles and train and test indexes should be equal.") splits = [] - for shuffle, (train_inds, test_inds) in enumerate( - zip(trainIndices, testIndices) - ): - trainFraction = round( - len(train_inds) * 1.0 / (len(train_inds) + len(test_inds)), 2 - ) - print( - f"You passed a split with the following fraction: {int(100 * trainFraction)}%" - ) + for shuffle, (train_inds, test_inds) in enumerate(zip(trainIndices, testIndices)): + trainFraction = round(len(train_inds) * 1.0 / (len(train_inds) + len(test_inds)), 2) + print(f"You passed a split with the following fraction: {int(100 * trainFraction)}%") # Now that the training fraction is guaranteed to be correct, # the values added to pad the indices are removed. train_inds = np.asarray(train_inds) @@ -446,9 +423,7 @@ def create_multianimaltraining_dataset( ( datafilename, metadatafilename, - ) = auxiliaryfunctions.get_data_and_metadata_filenames( - trainingsetfolder, trainFraction, shuffle, cfg - ) + ) = auxiliaryfunctions.get_data_and_metadata_filenames(trainingsetfolder, trainFraction, shuffle, cfg) ################################################################################ # Saving metadata and data file (Pickle file) ################################################################################ @@ -487,15 +462,9 @@ def create_multianimaltraining_dataset( cfg, engine=engine, ) - auxiliaryfunctions.attempt_to_make_folder( - Path(config).parents[0] / modelfoldername, recursive=True - ) - auxiliaryfunctions.attempt_to_make_folder( - str(Path(config).parents[0] / modelfoldername / "train") - ) - auxiliaryfunctions.attempt_to_make_folder( - str(Path(config).parents[0] / modelfoldername / "test") - ) + auxiliaryfunctions.attempt_to_make_folder(Path(config).parents[0] / modelfoldername, recursive=True) + auxiliaryfunctions.attempt_to_make_folder(str(Path(config).parents[0] / modelfoldername / "train")) + auxiliaryfunctions.attempt_to_make_folder(str(Path(config).parents[0] / modelfoldername / "test")) path_train_config = str( os.path.join( @@ -529,11 +498,9 @@ def create_multianimaltraining_dataset( "dataset": datafilename, "engine": engine.aliases[0], "metadataset": metadatafilename, - "num_joints": len(multianimalbodyparts) - + len(uniquebodyparts), # cfg["uniquebodyparts"]), + "num_joints": len(multianimalbodyparts) + len(uniquebodyparts), # cfg["uniquebodyparts"]), "all_joints": [ - [i] - for i in range(len(multianimalbodyparts) + len(uniquebodyparts)) + [i] for i in range(len(multianimalbodyparts) + len(uniquebodyparts)) ], # cfg["uniquebodyparts"]))], "all_joints_names": jointnames, "init_weights": str(model_path), @@ -552,9 +519,7 @@ def create_multianimaltraining_dataset( "multi_step": [[1e-4, 7500], [5 * 1e-5, 12000], [1e-5, 200000]], "save_iters": 10000, "display_iters": 500, - "num_idchannel": ( - len(cfg["individuals"]) if cfg.get("identity", False) else 0 - ), + "num_idchannel": (len(cfg["individuals"]) if cfg.get("identity", False) else 0), "crop_size": list(crop_size), "crop_sampling": crop_sampling, } @@ -664,10 +629,7 @@ def convert_cropped_to_standard_dataset( videos_orig = cfg.pop("video_sets_original") is_cropped = cfg.pop("croppedtraining") if videos_orig is None or not is_cropped: - print( - "Labeled data do not appear to be cropped. " - "Project will remain unchanged..." - ) + print("Labeled data do not appear to be cropped. Project will remain unchanged...") return project_path = cfg["project_path"] @@ -705,9 +667,7 @@ def strip_cropped_image_name(path): file = file.split("c")[0] return os.path.join(head, file + "." + ext) - img_names_old = np.asarray( - [strip_cropped_image_name(img) for img in df_old.index.to_list()] - ) + img_names_old = np.asarray([strip_cropped_image_name(img) for img in df_old.index.to_list()]) df = merge_annotateddatasets(cfg, datasets_folder) img_names = df.index.to_numpy() train_idx = [] @@ -720,15 +680,9 @@ def strip_cropped_image_name(path): if filename.startswith("Docu"): with open(pickle_file, "rb") as f: _, train_inds, test_inds, train_frac = pickle.load(f) - train_inds_temp = np.flatnonzero( - np.isin(img_names, img_names_old[train_inds]) - ) - test_inds_temp = np.flatnonzero( - np.isin(img_names, img_names_old[test_inds]) - ) - train_inds, test_inds = pad_train_test_indices( - train_inds_temp, test_inds_temp, train_frac - ) + train_inds_temp = np.flatnonzero(np.isin(img_names, img_names_old[train_inds])) + test_inds_temp = np.flatnonzero(np.isin(img_names, img_names_old[test_inds])) + train_inds, test_inds = pad_train_test_indices(train_inds_temp, test_inds_temp, train_frac) train_idx.append(train_inds) test_idx.append(test_inds) diff --git a/deeplabcut/generate_training_dataset/trainingsetmanipulation.py b/deeplabcut/generate_training_dataset/trainingsetmanipulation.py index 5eac233786..8edbfdf433 100755 --- a/deeplabcut/generate_training_dataset/trainingsetmanipulation.py +++ b/deeplabcut/generate_training_dataset/trainingsetmanipulation.py @@ -51,11 +51,7 @@ def comparevideolistsanddatafolders(config): cfg = auxiliaryfunctions.read_config(config) videos = cfg["video_sets"].keys() video_names = [Path(i).stem for i in videos] - alldatafolders = [ - fn - for fn in os.listdir(Path(config).parent / "labeled-data") - if "_labeled" not in fn - ] + alldatafolders = [fn for fn in os.listdir(Path(config).parent / "labeled-data") if "_labeled" not in fn] print("Config file contains:", len(video_names)) print("Labeled-data contains:", len(alldatafolders)) @@ -92,9 +88,7 @@ def adddatasetstovideolistandviceversa(config): video_names = [Path(i).stem for i in videos] alldatafolders = [ - fn - for fn in os.listdir(Path(config).parent / "labeled-data") - if "_labeled" not in fn and not fn.startswith(".") + fn for fn in os.listdir(Path(config).parent / "labeled-data") if "_labeled" not in fn and not fn.startswith(".") ] print("Config file contains:", len(video_names)) @@ -125,9 +119,7 @@ def adddatasetstovideolistandviceversa(config): if found: video_path = os.path.join(cfg["project_path"], "videos", file) clip = VideoReader(video_path) - videos.update( - {video_path: {"crop": ", ".join(map(str, clip.get_bbox()))}} - ) + videos.update({video_path: {"crop": ", ".join(map(str, clip.get_bbox()))}}) auxiliaryfunctions.write_config(config, cfg) @@ -157,9 +149,7 @@ def dropduplicatesinannotatinfiles(config): if len(DC.index) < numimages: print("Dropped", numimages - len(DC.index)) DC.to_hdf(fn, key="df_with_missing", mode="w") - DC.to_csv( - os.path.join(str(folder), "CollectedData_" + cfg["scorer"] + ".csv") - ) + DC.to_csv(os.path.join(str(folder), "CollectedData_" + cfg["scorer"] + ".csv")) except FileNotFoundError: print("Attention:", folder, "does not appear to have labeled data!") @@ -198,9 +188,7 @@ def dropannotationfileentriesduetodeletedimages(config): dropped = True if dropped == True: DC.to_hdf(fn, key="df_with_missing", mode="w") - DC.to_csv( - os.path.join(str(folder), "CollectedData_" + cfg["scorer"] + ".csv") - ) + DC.to_csv(os.path.join(str(folder), "CollectedData_" + cfg["scorer"] + ".csv")) def dropimagesduetolackofannotation(config): @@ -233,9 +221,7 @@ def dropimagesduetolackofannotation(config): if imagename in annotatedimages: pass else: - fullpath = os.path.join( - cfg["project_path"], "labeled-data", folder, imagename - ) + fullpath = os.path.join(cfg["project_path"], "labeled-data", folder, imagename) if os.path.isfile(fullpath): print("Deleting", fullpath) os.remove(fullpath) @@ -281,9 +267,7 @@ def dropunlabeledframes(config): dropped = before_len - after_len if dropped: DC.to_hdf(h5file, key="df_with_missing", mode="w") - DC.to_csv( - os.path.join(str(folder), "CollectedData_" + cfg["scorer"] + ".csv") - ) + DC.to_csv(os.path.join(str(folder), "CollectedData_" + cfg["scorer"] + ".csv")) print("Dropped ", dropped, "entries in ", folder) @@ -346,16 +330,11 @@ def check_labels( videos = cfg["video_sets"].keys() video_names = [_robust_path_split(video)[1] for video in videos] - folders = [ - os.path.join(cfg["project_path"], "labeled-data", str(Path(i))) - for i in video_names - ] + folders = [os.path.join(cfg["project_path"], "labeled-data", str(Path(i))) for i in video_names] print("Creating images with labels by %s." % cfg["scorer"]) for folder in folders: try: - DataCombined = pd.read_hdf( - os.path.join(str(folder), "CollectedData_" + cfg["scorer"] + ".h5") - ) + DataCombined = pd.read_hdf(os.path.join(str(folder), "CollectedData_" + cfg["scorer"] + ".h5")) conversioncode.guarantee_multiindex_rows(DataCombined) if cfg.get("multianimalproject", False): color_by = "individual" if visualizeindividuals else "bodypart" @@ -375,9 +354,7 @@ def check_labels( except FileNotFoundError: print("Attention:", folder, "does not appear to have labeled data!") - print( - "If all the labels are ok, then use the function 'create_training_dataset' to create the training dataset!" - ) + print("If all the labels are ok, then use the function 'create_training_dataset' to create the training dataset!") def boxitintoacell(joints): @@ -532,9 +509,7 @@ def merge_annotateddatasets(cfg, trainingsetfolder_full): videos = cfg["video_sets"].keys() video_filenames = parse_video_filenames(videos) for filename in video_filenames: - file_path = os.path.join( - data_path / filename, f'CollectedData_{cfg["scorer"]}.h5' - ) + file_path = os.path.join(data_path / filename, f"CollectedData_{cfg['scorer']}.h5") try: data = pd.read_hdf(file_path) conversioncode.guarantee_multiindex_rows(data) @@ -569,15 +544,13 @@ def merge_annotateddatasets(cfg, trainingsetfolder_full): bodyparts = multianimalbodyparts + uniquebodyparts else: bodyparts = cfg["bodyparts"] - AnnotationData = AnnotationData.reindex( - bodyparts, axis=1, level=AnnotationData.columns.names.index("bodyparts") - ) + AnnotationData = AnnotationData.reindex(bodyparts, axis=1, level=AnnotationData.columns.names.index("bodyparts")) if AnnotationData.empty: logging.warning( "The annotated dataframe is empty after reindexing using config. " "Hint: are bodyparts correctly listed in the configuration?" ) - filename = os.path.join(trainingsetfolder_full, f'CollectedData_{cfg["scorer"]}') + filename = os.path.join(trainingsetfolder_full, f"CollectedData_{cfg['scorer']}") AnnotationData.to_hdf(filename + ".h5", key="df_with_missing", mode="w") AnnotationData.to_csv(filename + ".csv") # human readable. return AnnotationData @@ -688,12 +661,8 @@ def mergeandsplit(config, trainindex=0, uniform=True): scorer = cfg["scorer"] project_path = cfg["project_path"] # Create path for training sets & store data there - trainingsetfolder = auxiliaryfunctions.get_training_set_folder( - cfg - ) # Path concatenation OS platform independent - auxiliaryfunctions.attempt_to_make_folder( - Path(os.path.join(project_path, str(trainingsetfolder))), recursive=True - ) + trainingsetfolder = auxiliaryfunctions.get_training_set_folder(cfg) # Path concatenation OS platform independent + auxiliaryfunctions.attempt_to_make_folder(Path(os.path.join(project_path, str(trainingsetfolder))), recursive=True) fn = os.path.join(project_path, trainingsetfolder, "CollectedData_" + cfg["scorer"]) try: @@ -775,9 +744,7 @@ def to_matlab_cell(array): to_matlab_cell(data["joints"]), ) ) - matlab_data = np.asarray( - matlab_data, dtype=[("image", "O"), ("size", "O"), ("joints", "O")] - ) + matlab_data = np.asarray(matlab_data, dtype=[("image", "O"), ("size", "O"), ("joints", "O")]) return train_data, matlab_data @@ -997,9 +964,7 @@ def create_training_dataset( and not posecfg_template.endswith("superquadruped.yaml") and not posecfg_template.endswith("supertopview.yaml") ): - raise ValueError( - "posecfg_template argument must contain path to a pose_cfg.yaml file" - ) + raise ValueError("posecfg_template argument must contain path to a pose_cfg.yaml file") else: print("Reloading pose_cfg parameters from " + posecfg_template + "\n") from deeplabcut.utils.auxiliaryfunctions import read_plainconfig @@ -1056,12 +1021,7 @@ def create_training_dataset( elif engine == Engine.PYTORCH: pass else: - if ( - "resnet" in net_type - or "mobilenet" in net_type - or "efficientnet" in net_type - or "dlcrnet" in net_type - ): + if "resnet" in net_type or "mobilenet" in net_type or "efficientnet" in net_type or "dlcrnet" in net_type: pass else: raise ValueError("Invalid network type:", net_type) @@ -1080,9 +1040,7 @@ def create_training_dataset( if augmenter_type is None: # this could be in config.yaml for old projects! # updating variable if null/None! #backwardscompatability augmenter_type = default_augmenter - auxiliaryfunctions.edit_config( - config, {"default_augmenter": augmenter_type} - ) + auxiliaryfunctions.edit_config(config, {"default_augmenter": augmenter_type}) elif augmenter_type not in augmenters: # as the default augmenter might not be available for the given engine augmenter_type = default_augmenter @@ -1094,8 +1052,7 @@ def create_training_dataset( if augmenter_type not in augmenters: if engine != Engine.PYTORCH: raise ValueError( - f"Invalid augmenter type: {augmenter_type} (available: for " - f"engine={engine}: {augmenters})" + f"Invalid augmenter type: {augmenter_type} (available: for engine={engine}: {augmenters})" ) logging.info(f"Switching augmentation to {default_augmenter} for PyTorch") @@ -1137,28 +1094,18 @@ def create_training_dataset( ] else: if len(trainIndices) != len(testIndices) != len(Shuffles): - raise ValueError( - "Number of Shuffles and train and test indexes should be equal." - ) + raise ValueError("Number of Shuffles and train and test indexes should be equal.") splits = [] - for shuffle, (train_inds, test_inds) in enumerate( - zip(trainIndices, testIndices) - ): - trainFraction = round( - len(train_inds) * 1.0 / (len(train_inds) + len(test_inds)), 2 - ) - print( - f"You passed a split with the following fraction: {int(100 * trainFraction)}%" - ) + for shuffle, (train_inds, test_inds) in enumerate(zip(trainIndices, testIndices)): + trainFraction = round(len(train_inds) * 1.0 / (len(train_inds) + len(test_inds)), 2) + print(f"You passed a split with the following fraction: {int(100 * trainFraction)}%") # Now that the training fraction is guaranteed to be correct, # the values added to pad the indices are removed. train_inds = np.asarray(train_inds) train_inds = train_inds[train_inds != -1] test_inds = np.asarray(test_inds) test_inds = test_inds[test_inds != -1] - splits.append( - (trainFraction, Shuffles[shuffle], (train_inds, test_inds)) - ) + splits.append((trainFraction, Shuffles[shuffle], (train_inds, test_inds))) bodyparts = auxiliaryfunctions.get_bodyparts(cfg) nbodyparts = len(bodyparts) @@ -1175,12 +1122,7 @@ def create_training_dataset( askuser = input( "The model folder is already present. If you continue, it will overwrite the existing model (split). Do you want to continue?(yes/no): " ) - if ( - askuser == "no" - or askuser == "No" - or askuser == "N" - or askuser == "No" - ): + if askuser == "no" or askuser == "No" or askuser == "N" or askuser == "No": raise Exception( "Use the Shuffles argument as a list to specify a different shuffle index. Check out the help for more details." ) @@ -1192,19 +1134,13 @@ def create_training_dataset( ( datafilename, metadatafilename, - ) = auxiliaryfunctions.get_data_and_metadata_filenames( - trainingsetfolder, trainFraction, shuffle, cfg - ) + ) = auxiliaryfunctions.get_data_and_metadata_filenames(trainingsetfolder, trainFraction, shuffle, cfg) ################################################################################ # Saving data file (convert to training file for deeper cut (*.mat)) ################################################################################ - data, MatlabData = format_training_data( - Data, trainIndices, nbodyparts, project_path - ) - sio.savemat( - os.path.join(project_path, datafilename), {"dataset": MatlabData} - ) + data, MatlabData = format_training_data(Data, trainIndices, nbodyparts, project_path) + sio.savemat(os.path.join(project_path, datafilename), {"dataset": MatlabData}) ################################################################################ # Saving metadata (Pickle file) @@ -1236,15 +1172,9 @@ def create_training_dataset( cfg, engine=engine, ) - auxiliaryfunctions.attempt_to_make_folder( - Path(config).parents[0] / modelfoldername, recursive=True - ) - auxiliaryfunctions.attempt_to_make_folder( - str(Path(config).parents[0] / modelfoldername) + "/train" - ) - auxiliaryfunctions.attempt_to_make_folder( - str(Path(config).parents[0] / modelfoldername) + "/test" - ) + auxiliaryfunctions.attempt_to_make_folder(Path(config).parents[0] / modelfoldername, recursive=True) + auxiliaryfunctions.attempt_to_make_folder(str(Path(config).parents[0] / modelfoldername) + "/train") + auxiliaryfunctions.attempt_to_make_folder(str(Path(config).parents[0] / modelfoldername) + "/test") path_train_config = str( os.path.join( @@ -1409,17 +1339,11 @@ def is_valid_data_stem(stem: str) -> bool: shuffle_indices = [ int(p.stem.split("shuffle")[-1]) for p in trainset_folder.iterdir() - if ( - p.stem.startswith("Documentation_data") - and p.suffix == ".pickle" - and is_valid_data_stem(p.stem) - ) + if (p.stem.startswith("Documentation_data") and p.suffix == ".pickle" and is_valid_data_stem(p.stem)) ] if engine is not None: if train_fraction is None: - raise ValueError( - f"Must select {train_fraction} to filter shuffles by engine" - ) + raise ValueError(f"Must select {train_fraction} to filter shuffles by engine") shuffle_indices = [ idx @@ -1592,9 +1516,7 @@ def create_training_model_comparison( shuffle_list = [] for shuffle in range(num_shuffles): - trainIndices, testIndices = mergeandsplit( - config, trainindex=trainindex, uniform=True - ) + trainIndices, testIndices = mergeandsplit(config, trainindex=trainindex, uniform=True) for idx_net, net in enumerate(net_types): for idx_aug, aug in enumerate(augmenter_types): get_max_shuffle_idx = ( @@ -1808,10 +1730,7 @@ def _compute_padding( the number of padding indices to add to the test indices """ if train_fraction <= 0 or train_fraction >= 1: - raise ValueError( - f"The training fraction must satisfy 0 < TrainingFraction < 1, but " - f"{train_fraction} was found" - ) + raise ValueError(f"The training fraction must satisfy 0 < TrainingFraction < 1, but {train_fraction} was found") base_images = 100 train_step = int(round(round(train_fraction, 2) * base_images)) diff --git a/deeplabcut/gui/components.py b/deeplabcut/gui/components.py index 21e282bdb9..0a99c94c87 100644 --- a/deeplabcut/gui/components.py +++ b/deeplabcut/gui/components.py @@ -182,9 +182,7 @@ def _init_layout(self, hide_videotype: bool): self.root.video_files_.connect(self._update_video_selection) # Number of selected videos text - self.selected_videos_text = QtWidgets.QLabel( - "" - ) # updated when videos are selected + self.selected_videos_text = QtWidgets.QLabel("") # updated when videos are selected # Clear video selection self.clear_videos = QtWidgets.QPushButton("Clear selection") @@ -264,9 +262,7 @@ def _init_layout(self, margins, select_button_text): self.select_snapshot_button.clicked.connect(self.select_snapshot) # Selected snapshot text - self.selected_snapshot_text = QtWidgets.QLabel( - "" - ) # updated when snapshot is selected + self.selected_snapshot_text = QtWidgets.QLabel("") # updated when snapshot is selected # Clear snapshot selection self.clear_snapshot_button = QtWidgets.QPushButton("Clear selection") @@ -284,9 +280,7 @@ def _update_selected_snapshot_display(self): self.selected_snapshot_text.setText("") self.clear_snapshot_button.hide() else: - self.selected_snapshot_text.setText( - f"{os.path.basename(self.selected_snapshot)}" - ) + self.selected_snapshot_text.setText(f"{os.path.basename(self.selected_snapshot)}") self.clear_snapshot_button.show() def select_snapshot(self): @@ -334,9 +328,7 @@ def _init_layout(self): self.select_conditions_button.clicked.connect(self.select_conditions) # Selected conditions text - self.selected_conditions_text = QtWidgets.QLabel( - "" - ) # updated when conditions are selected + self.selected_conditions_text = QtWidgets.QLabel("") # updated when conditions are selected layout.addWidget(self.select_conditions_button) layout.addWidget(self.selected_conditions_text) @@ -350,9 +342,7 @@ def _shorten_path(path: str, max_length: int = 30) -> str: return "..." + path[-(max_length - 3) :] self.selected_conditions_text.setText( - "" - if self.selected_conditions is None - else f"{_shorten_path(self.selected_conditions)}" + "" if self.selected_conditions is None else f"{_shorten_path(self.selected_conditions)}" ) def select_conditions(self): @@ -397,9 +387,7 @@ def _is_model_bu(selected_conditions) -> bool: selected_conditions = None # When Canceling a file selection, Qt returns an empty string as selected file - self.selected_conditions = ( - str(os.path.abspath(selected_conditions)) if selected_conditions else None - ) + self.selected_conditions = str(os.path.abspath(selected_conditions)) if selected_conditions else None self._update_selected_conditions_display() @@ -456,9 +444,7 @@ def __init__( def _init_default_layout(self): # Add tab header - self.main_layout.addWidget( - _create_label_widget(self.h1_description, "font:bold;", (10, 10, 0, 10)) - ) + self.main_layout.addWidget(_create_label_widget(self.h1_description, "font:bold;", (10, 10, 0, 10))) # Add separating line self.separator = QtWidgets.QFrame() @@ -474,9 +460,7 @@ def _init_default_layout(self): class EditYamlButton(QtWidgets.QPushButton): - def __init__( - self, button_label: str, filepath: str, parent: QtWidgets.QWidget = None - ): + def __init__(self, button_label: str, filepath: str, parent: QtWidgets.QWidget = None): super().__init__(parent) self.filepath = filepath self.parent = parent diff --git a/deeplabcut/gui/displays/selected_shuffle_display.py b/deeplabcut/gui/displays/selected_shuffle_display.py index 13e6db1094..0c5925074a 100644 --- a/deeplabcut/gui/displays/selected_shuffle_display.py +++ b/deeplabcut/gui/displays/selected_shuffle_display.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Module to display information about the selected shuffle in the GUI""" + from __future__ import annotations from pathlib import Path @@ -21,6 +22,7 @@ class SelectedShuffleDisplay(QtWidgets.QWidget): """A widget displaying information about the selected shuffle""" + pose_cfg_signal = QtCore.Signal(dict) def __init__(self, root, row_margin: int = 25): @@ -66,9 +68,7 @@ def _update_display(self, new_index: int) -> None: try: pose_cfg_path = Path(self.root.pose_cfg_path) except ValueError as err: - self._set_text_error( - f"Failed to read shuffle {self._current_index} - check that it exists!" - ) + self._set_text_error(f"Failed to read shuffle {self._current_index} - check that it exists!") return except ModuleNotFoundError as err: # Loading a TF shuffle but TF is not installed @@ -85,9 +85,7 @@ def _update_display(self, new_index: int) -> None: return if not pose_cfg_path.exists(): - self._set_text_error( - f"The model configuration file {pose_cfg_path} was not created" - ) + self._set_text_error(f"The model configuration file {pose_cfg_path} was not created") return self._read_pose_config(pose_cfg_path) @@ -120,11 +118,7 @@ def _set_text_error(self, error: str) -> None: def _read_pose_config(self, pose_cfg_path: Path) -> None: pose_cfg = auxiliaryfunctions.read_plainconfig(str(pose_cfg_path)) - self._engine = ( - Engine.PYTORCH if "pytorch" in pose_cfg_path.stem.lower() else Engine.TF - ) + self._engine = Engine.PYTORCH if "pytorch" in pose_cfg_path.stem.lower() else Engine.TF self._net_type = pose_cfg.get("net_type", "UNKNOWN") - self._is_top_down = ( - self._engine == Engine.PYTORCH and pose_cfg.get("method").lower() == "td" - ) + self._is_top_down = self._engine == Engine.PYTORCH and pose_cfg.get("method").lower() == "td" self.pose_cfg = pose_cfg diff --git a/deeplabcut/gui/displays/shuffle_metadata_viewer.py b/deeplabcut/gui/displays/shuffle_metadata_viewer.py index b18aef85b4..220d4c2a20 100644 --- a/deeplabcut/gui/displays/shuffle_metadata_viewer.py +++ b/deeplabcut/gui/displays/shuffle_metadata_viewer.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Widget to display existing shuffles""" + from __future__ import annotations from PySide6 import QtWidgets @@ -39,7 +40,6 @@ def __init__(self, root: QtWidgets.QMainWindow, parent: QtWidgets.QWidget): inner_layout.setContentsMargins(0, 0, 0, 0) for line in self.file_content: - inner_layout.addWidget(QtWidgets.QLabel(line)) inner = QtWidgets.QFrame(scroll) diff --git a/deeplabcut/gui/launch_script.py b/deeplabcut/gui/launch_script.py index 6ada864102..65d51d2834 100644 --- a/deeplabcut/gui/launch_script.py +++ b/deeplabcut/gui/launch_script.py @@ -18,6 +18,7 @@ Licensed under GNU Lesser General Public License v3.0 """ + import sys import os import logging diff --git a/deeplabcut/gui/tabs/analyze_videos.py b/deeplabcut/gui/tabs/analyze_videos.py index 60971f4c3e..1ba43f7d4c 100644 --- a/deeplabcut/gui/tabs/analyze_videos.py +++ b/deeplabcut/gui/tabs/analyze_videos.py @@ -173,27 +173,17 @@ def _generate_layout_multianimal(self, layout): self.calibrate_assembly_checkbox = QtWidgets.QCheckBox("Calibrate assembly") self.calibrate_assembly_checkbox.setCheckState(Qt.Unchecked) - self.calibrate_assembly_checkbox.stateChanged.connect( - self.update_calibrate_assembly - ) + self.calibrate_assembly_checkbox.stateChanged.connect(self.update_calibrate_assembly) tmp_layout.addWidget(self.calibrate_assembly_checkbox, 0, 2) - self.assemble_with_ID_only_checkbox = QtWidgets.QCheckBox( - "Assemble with ID only" - ) + self.assemble_with_ID_only_checkbox = QtWidgets.QCheckBox("Assemble with ID only") self.assemble_with_ID_only_checkbox.setCheckState(Qt.Unchecked) - self.assemble_with_ID_only_checkbox.stateChanged.connect( - self.update_assemble_with_ID_only - ) + self.assemble_with_ID_only_checkbox.stateChanged.connect(self.update_assemble_with_ID_only) tmp_layout.addWidget(self.assemble_with_ID_only_checkbox, 0, 3) - self.create_detections_video_checkbox = QtWidgets.QCheckBox( - "Create video with all detections" - ) + self.create_detections_video_checkbox = QtWidgets.QCheckBox("Create video with all detections") self.create_detections_video_checkbox.setCheckState(Qt.Unchecked) - self.create_detections_video_checkbox.stateChanged.connect( - self.update_create_video_detections - ) + self.create_detections_video_checkbox.stateChanged.connect(self.update_create_video_detections) tmp_layout.addWidget(self.create_detections_video_checkbox, 0, 4) layout.addLayout(tmp_layout) @@ -324,9 +314,7 @@ def run_enabled(self): filter_data = self.filter_predictions.isChecked() videotype = self.video_selection_widget.videotype_widget.currentText() try: - create_video_all_detections = ( - self.create_detections_video_checkbox.isChecked() - ) + create_video_all_detections = self.create_detections_video_checkbox.isChecked() except AttributeError: create_video_all_detections = False if create_video_all_detections: @@ -352,9 +340,7 @@ def run_enabled(self): if self.plot_trajectories.isChecked(): bdpts = self.bodyparts_list_widget.selected_bodyparts - self.root.logger.debug( - f"Selected body parts for plot_trajectories: {bdpts}" - ) + self.root.logger.debug(f"Selected body parts for plot_trajectories: {bdpts}") deeplabcut.plot_trajectories( config, videos=videos, diff --git a/deeplabcut/gui/tabs/create_project.py b/deeplabcut/gui/tabs/create_project.py index f9724432e0..c06f1aa914 100644 --- a/deeplabcut/gui/tabs/create_project.py +++ b/deeplabcut/gui/tabs/create_project.py @@ -103,10 +103,7 @@ def _check_for_spaces(self, entry, text): if " " in text: msg = QtWidgets.QMessageBox() msg.setIcon(QtWidgets.QMessageBox.Warning) - msg.setText( - f"Spaces are not allowed in the {self.label_text} list. Use underscores " - f"instead." - ) + msg.setText(f"Spaces are not allowed in the {self.label_text} list. Use underscores instead.") msg.setWindowTitle("Warning") msg.exec_() entry.setText(entry.text().replace(" ", "_")) @@ -139,7 +136,6 @@ def _update_indices(self): class Switch(QtWidgets.QPushButton): - def __init__(self, on_text="Yes", off_text="No", width=80, parent=None): super().__init__(parent) self.on_text = on_text @@ -166,9 +162,7 @@ def paintEvent(self, event): pen.setWidth(2) painter.setPen(pen) - painter.drawRoundedRect( - QtCore.QRect(-width, -radius, 2 * width, 2 * radius), radius, radius - ) + painter.drawRoundedRect(QtCore.QRect(-width, -radius, 2 * width, 2 * radius), radius, radius) painter.setBrush(QBrush(bg_color)) sw_rect = QtCore.QRect(-radius, -radius, width + radius, 2 * radius) if not self.isChecked(): @@ -321,36 +315,24 @@ def lay_out_user_frame(self): # Connect the unique_toggle to the unique_bodyparts_list self.unique_toggle.toggled.connect( - lambda yes: self.unique_bodyparts_list.setVisible( - yes and self.madlc_toggle.isChecked() - ) + lambda yes: self.unique_bodyparts_list.setVisible(yes and self.madlc_toggle.isChecked()) ) # Connect 3d toggle to all other option visibility self.toggle_3d.toggled.connect(lambda yes: madlc_widget.setVisible(not yes)) self.toggle_3d.toggled.connect( - lambda checked_3d: unique_widget.setVisible( - not checked_3d and self.madlc_toggle.isChecked() - ) - ) - self.toggle_3d.toggled.connect( - lambda checked_3d: identity_widget.setVisible( - not checked_3d and self.madlc_toggle.isChecked() - ) + lambda checked_3d: unique_widget.setVisible(not checked_3d and self.madlc_toggle.isChecked()) ) self.toggle_3d.toggled.connect( - lambda checked_3d: self.bodypart_list.setVisible(not checked_3d) + lambda checked_3d: identity_widget.setVisible(not checked_3d and self.madlc_toggle.isChecked()) ) + self.toggle_3d.toggled.connect(lambda checked_3d: self.bodypart_list.setVisible(not checked_3d)) self.toggle_3d.toggled.connect( - lambda checked_3d: self.individuals_list.setVisible( - not checked_3d and self.madlc_toggle.isChecked() - ) + lambda checked_3d: self.individuals_list.setVisible(not checked_3d and self.madlc_toggle.isChecked()) ) self.toggle_3d.toggled.connect( lambda checked_3d: self.unique_bodyparts_list.setVisible( - not checked_3d - and self.madlc_toggle.isChecked() - and self.unique_toggle.isChecked() + not checked_3d and self.madlc_toggle.isChecked() and self.unique_toggle.isChecked() ) ) @@ -379,9 +361,7 @@ def build_toggle_widget( help_label = ClickableLabel(help_text, parent=self) help_label.setStyleSheet("text-decoration: underline; font-weight: bold;") help_label.setCursor(QtCore.Qt.PointingHandCursor) - help_label.signal.connect( - lambda: QDesktopServices.openUrl(QtCore.QUrl(docs_link)) - ) + help_label.signal.connect(lambda: QDesktopServices.openUrl(QtCore.QUrl(docs_link))) toggle_layout.addWidget(switch, alignment=QtCore.Qt.AlignLeft) toggle_layout.addWidget(toggle_label, alignment=QtCore.Qt.AlignLeft) @@ -423,14 +403,14 @@ def lay_out_video_frame(self): def browse_videos(self): options = QtWidgets.QFileDialog.Options() options |= QtWidgets.QFileDialog.DontUseNativeDialog - + if self.select_files_box.isChecked(): # Select individual video files video_types = [f"*.{ext.lower()}" for ext in DLCParams.VIDEOTYPES[1:]] + [ f"*.{ext.upper()}" for ext in DLCParams.VIDEOTYPES[1:] ] video_filter = f"Videos ({' '.join(video_types)})" - + files, _ = QtWidgets.QFileDialog.getOpenFileNames( self, "Select video files", @@ -438,7 +418,7 @@ def browse_videos(self): video_filter, options=options, ) - + if files: for video in files: self.video_frame.fancy_list.add_item(video) @@ -487,9 +467,7 @@ def finalize_project(self): self.video_frame.fancy_list.setStyleSheet("border: 1px solid red") return else: - self.video_frame.fancy_list.setStyleSheet( - self.video_frame.fancy_list._default_style - ) + self.video_frame.fancy_list.setStyleSheet(self.video_frame.fancy_list._default_style) to_copy = self.copy_box.isChecked() is_madlc = self.madlc_toggle.isChecked() config = create_new_project( @@ -511,10 +489,7 @@ def finalize_project(self): if len(individuals) > 0: updates["individuals"] = individuals - if ( - self.unique_toggle.isChecked() - and self.unique_bodyparts_list is not None - ): + if self.unique_toggle.isChecked() and self.unique_bodyparts_list is not None: unique_bodyparts = self.unique_bodyparts_list.get_entries() if len(unique_bodyparts) > 0: updates["uniquebodyparts"] = unique_bodyparts @@ -544,9 +519,7 @@ def finalize_project(self): self.close() def on_click(self): - dirname = QtWidgets.QFileDialog.getExistingDirectory( - self, "Please select a folder", self.loc_default - ) + dirname = QtWidgets.QFileDialog.getExistingDirectory(self, "Please select a folder", self.loc_default) if not dirname: return self.loc_default = dirname diff --git a/deeplabcut/gui/tabs/create_training_dataset.py b/deeplabcut/gui/tabs/create_training_dataset.py index 3f6db3ceef..f22ce90e83 100644 --- a/deeplabcut/gui/tabs/create_training_dataset.py +++ b/deeplabcut/gui/tabs/create_training_dataset.py @@ -81,14 +81,10 @@ def __init__(self, root, parent, h1_description): self.main_layout.addWidget(self.help_button, alignment=Qt.AlignLeft) def set_edit_table_visibility(self) -> None: - has_conversion_tables = bool( - self.root.cfg.get("SuperAnimalConversionTables", {}) - ) + has_conversion_tables = bool(self.root.cfg.get("SuperAnimalConversionTables", {})) is_pytorch_engine = self.root.engine == Engine.PYTORCH is_finetuning = self.weight_init_selector.with_decoder - self.mapping_button.setVisible( - has_conversion_tables & is_pytorch_engine & is_finetuning - ) + self.mapping_button.setVisible(has_conversion_tables & is_pytorch_engine & is_finetuning) def show_help_dialog(self): dialog = QtWidgets.QDialog(self) @@ -136,9 +132,7 @@ def _generate_layout_attributes(self, layout): self.net_choice.currentTextChanged.connect(self.log_net_choice) # Update Net types when selected weight init changes - self.weight_init_selector.weight_init_choice.currentTextChanged.connect( - lambda _: self.update_nets(None) - ) + self.weight_init_selector.weight_init_choice.currentTextChanged.connect(lambda _: self.update_nets(None)) self.weight_init_selector.weight_init_choice.currentTextChanged.connect( lambda _: self.set_edit_table_visibility() ) @@ -148,26 +142,18 @@ def _generate_layout_attributes(self, layout): self.detector_choice = QtWidgets.QComboBox() self.detector_choice.setMinimumWidth(200) self.update_detectors(engine=self.root.engine) - self.root.engine_change.connect( - lambda engine: self.update_detectors(engine=engine) - ) + self.root.engine_change.connect(lambda engine: self.update_detectors(engine=engine)) self.net_choice.currentTextChanged.connect( lambda new_net_choice: self.update_detectors(net_choice=new_net_choice) ) # Conditions selection for CTD models self.conditions_label = QtWidgets.QLabel("Conditions") - self.conditions_selection_widget = ConditionsSelectionWidget( - root=self.root, parent=self - ) + self.conditions_selection_widget = ConditionsSelectionWidget(root=self.root, parent=self) self.update_conditions(engine=self.root.engine) - self.root.engine_change.connect( - lambda engine: self.update_conditions(engine=engine) - ) + self.root.engine_change.connect(lambda engine: self.update_conditions(engine=engine)) self.net_choice.currentTextChanged.connect( - lambda new_net_choice: self.update_conditions( - engine=self.root.engine, net_choice=new_net_choice - ) + lambda new_net_choice: self.update_conditions(engine=self.root.engine, net_choice=new_net_choice) ) # Overwrite selection @@ -178,9 +164,7 @@ def _generate_layout_attributes(self, layout): "will overwrite the existing index. Be careful with this option as you " "might lose data." ) - self.overwrite.stateChanged.connect( - lambda s: self.root.logger.info(f"Overwrite: {s}") - ) + self.overwrite.stateChanged.connect(lambda s: self.root.logger.info(f"Overwrite: {s}")) # Use same data split as another shuffle self.data_split_selection = DataSplitSelector(self.root, self) @@ -274,11 +258,9 @@ def create_training_dataset(self): ) try: - weight_init = ( - self.weight_init_selector.get_super_animal_weight_init( - net_type, - detector_type, - ) + weight_init = self.weight_init_selector.get_super_animal_weight_init( + net_type, + detector_type, ) except ValueError as err: print(f"The training dataset could not be created: {err}.") @@ -341,9 +323,7 @@ def create_training_dataset(self): " Apple Silicon:\n" " pip install 'deeplabcut[apple_mchips]'" ) - msg = _create_message_box( - f"The training dataset could not be created.", info_text - ) + msg = _create_message_box(f"The training dataset could not be created.", info_text) msg.exec_() return @@ -359,10 +339,7 @@ def create_training_dataset(self): ) if self.root.is_multianimal: filenames[0] = filenames[0].replace("mat", "pickle") - if all( - os.path.exists(os.path.join(self.root.project_folder, file)) - for file in filenames - ): + if all(os.path.exists(os.path.join(self.root.project_folder, file)) for file in filenames): self.root.shuffle_created.emit(self.shuffle.value()) msg = _create_message_box( "The training dataset is successfully created.", @@ -390,9 +367,7 @@ def _confirm_overwrite(self, shuffle: int, existing_indices: list[int]) -> bool: whether the user confirmed overwriting the shuffle """ try: - engine = get_shuffle_engine( - self.root.cfg, self.root.trainingset_index, shuffle - ) + engine = get_shuffle_engine(self.root.cfg, self.root.trainingset_index, shuffle) engine_str = f" (with engine '{engine.aliases[0]}')" except ValueError: engine_str = "" @@ -400,18 +375,14 @@ def _confirm_overwrite(self, shuffle: int, existing_indices: list[int]) -> bool: conf = _create_confirmation_box( title=f"Are you sure you want to overwrite shuffle {shuffle}?", description=( - f"As shuffle {shuffle} already exists{engine_str}, " - f"the training-dataset files would be overwritten." + f"As shuffle {shuffle} already exists{engine_str}, the training-dataset files would be overwritten." ), ) result = conf.exec() if result != QtWidgets.QMessageBox.Yes: msg = _create_message_box( text="The training dataset was not be created.", - info_text=( - "You can create a shuffle with another index. Existing indices " - f"are {existing_indices}" - ), + info_text=(f"You can create a shuffle with another index. Existing indices are {existing_indices}"), ) msg.exec_() self.root.writer.write("Training dataset creation interrupted.") @@ -419,9 +390,7 @@ def _confirm_overwrite(self, shuffle: int, existing_indices: list[int]) -> bool: return True - def _build_ctd_conditions( - self, conditions_path: str | Path - ) -> Path | tuple[int, str]: + def _build_ctd_conditions(self, conditions_path: str | Path) -> Path | tuple[int, str]: """ Builds CTD conditions in appropriate format from path to conditions. Args: @@ -471,13 +440,7 @@ def update_nets(self, engine: Engine | None) -> None: nets = [ n for n in nets - if ( - n in net_filter - or ( - n.startswith(td_prefix) - and n[len(td_prefix) :] in net_filter - ) - ) + if (n in net_filter or (n.startswith(td_prefix) and n[len(td_prefix) :] in net_filter)) ] if default_net is None: diff --git a/deeplabcut/gui/tabs/create_videos.py b/deeplabcut/gui/tabs/create_videos.py index 9cdf92789e..2a60f944f4 100644 --- a/deeplabcut/gui/tabs/create_videos.py +++ b/deeplabcut/gui/tabs/create_videos.py @@ -55,9 +55,7 @@ def _set_page(self): self.main_layout.addLayout(tmp_layout) - self.main_layout.addWidget( - _create_label_widget("Video Parameters", "font:bold") - ) + self.main_layout.addWidget(_create_label_widget("Video Parameters", "font:bold")) self.layout_video_parameters = _create_vertical_layout() self._generate_layout_video_parameters(self.layout_video_parameters) self.main_layout.addLayout(self.layout_video_parameters) @@ -141,9 +139,7 @@ def _generate_layout_video_parameters(self, layout): # Filtered data self.use_filtered_data_checkbox = QtWidgets.QCheckBox("Use filtered data") self.use_filtered_data_checkbox.setCheckState(Qt.Unchecked) - self.use_filtered_data_checkbox.stateChanged.connect( - self.update_use_filtered_data - ) + self.use_filtered_data_checkbox.stateChanged.connect(self.update_use_filtered_data) tmp_layout.addWidget(self.use_filtered_data_checkbox) # Selector for p-cutoff @@ -159,8 +155,7 @@ def _generate_layout_video_parameters(self, layout): pcutoff_layout.addWidget(self.pcutoff_selector) pcutoff_widget.setLayout(pcutoff_layout) pcutoff_widget.setToolTip( - "This value sets the confidence threshold, above which predictions are " - "shown in the labeled videos." + "This value sets the confidence threshold, above which predictions are shown in the labeled videos." ) tmp_layout.addWidget(pcutoff_widget) @@ -171,13 +166,9 @@ def _generate_layout_video_parameters(self, layout): tmp_layout.addWidget(self.plot_trajectories) # High quality video - self.create_high_quality_video = QtWidgets.QCheckBox( - "High quality video (slow)" - ) + self.create_high_quality_video = QtWidgets.QCheckBox("High quality video (slow)") self.create_high_quality_video.setCheckState(Qt.Unchecked) - self.create_high_quality_video.stateChanged.connect( - self.update_high_quality_video - ) + self.create_high_quality_video.stateChanged.connect(self.update_high_quality_video) tmp_layout.addWidget(self.create_high_quality_video) nested_tmp_layout = _create_horizontal_layout(margins=(0, 0, 0, 0)) @@ -205,12 +196,8 @@ def update_plot_trajectory_choice(self, state): self.root.logger.info(f"Plot trajectories {s}.") def update_selected_bodyparts(self): - selected_bodyparts = [ - item.text() for item in self.bodyparts_list_widget.selectedItems() - ] - self.root.logger.info( - f"Selected bodyparts for plotting:\n\t{selected_bodyparts}" - ) + selected_bodyparts = [item.text() for item in self.bodyparts_list_widget.selectedItems()] + self.root.logger.info(f"Selected bodyparts for plotting:\n\t{selected_bodyparts}") self.bodyparts_to_use = selected_bodyparts def update_use_all_bodyparts(self, s): @@ -264,10 +251,7 @@ def create_videos(self): filtered = self.use_filtered_data_checkbox.isChecked() bodyparts = "all" - if ( - len(self.bodyparts_to_use) != 0 - and not self.plot_all_bodyparts.isChecked() - ): + if len(self.bodyparts_to_use) != 0 and not self.plot_all_bodyparts.isChecked(): self.update_selected_bodyparts() bodyparts = self.bodyparts_to_use @@ -287,9 +271,7 @@ def create_videos(self): if all(videos_created): self.root.writer.write("Labeled videos created.") else: - failed_videos = [ - video for success, video in zip(videos_created, videos) if not success - ] + failed_videos = [video for success, video in zip(videos_created, videos) if not success] failed_videos_str = ", ".join(failed_videos) self.root.writer.write(f"Failed to create videos from {failed_videos_str}.") diff --git a/deeplabcut/gui/tabs/evaluate_network.py b/deeplabcut/gui/tabs/evaluate_network.py index 9b46ee09ce..e3bcf402dc 100644 --- a/deeplabcut/gui/tabs/evaluate_network.py +++ b/deeplabcut/gui/tabs/evaluate_network.py @@ -85,9 +85,7 @@ def _set_page(self): self.edit_inferencecfg_btn.clicked.connect(self.open_inferencecfg_editor) if self.root.is_multianimal: - self.main_layout.addWidget( - self.edit_inferencecfg_btn, alignment=Qt.AlignRight - ) + self.main_layout.addWidget(self.edit_inferencecfg_btn, alignment=Qt.AlignRight) self.main_layout.addWidget(self.ev_nw_button, alignment=Qt.AlignRight) self.main_layout.addWidget(self.opt_button, alignment=Qt.AlignRight) @@ -133,27 +131,17 @@ def plot_maps(self): # Display all images dest_folder = os.path.join( self.root.project_folder, - str( - auxiliaryfunctions.get_evaluation_folder( - self.root.cfg["TrainingFraction"][0], shuffle, self.root.cfg - ) - ), + str(auxiliaryfunctions.get_evaluation_folder(self.root.cfg["TrainingFraction"][0], shuffle, self.root.cfg)), "maps", ) - image_paths = [ - os.path.join(dest_folder, file) - for file in os.listdir(dest_folder) - if file.endswith(".png") - ] + image_paths = [os.path.join(dest_folder, file) for file in os.listdir(dest_folder) if file.endswith(".png")] canvas = GridCanvas(image_paths, parent=self) canvas.show() def _generate_additional_attributes(self, layout): tmp_layout = _create_horizontal_layout(margins=(0, 0, 0, 0)) - self.plot_predictions = QtWidgets.QCheckBox( - "Plot predictions (as in standard DLC projects)" - ) + self.plot_predictions = QtWidgets.QCheckBox("Plot predictions (as in standard DLC projects)") self.plot_predictions.stateChanged.connect(self.update_plot_predictions) tmp_layout.addWidget(self.plot_predictions) @@ -188,9 +176,7 @@ def update_bodypart_choice(self, s): else: self.bodyparts_list_widget.setEnabled(True) self.bodyparts_list_widget.show() - self.root.logger.info( - f"Use selected bodyparts only: {self.bodyparts_list_widget.selected_bodyparts}" - ) + self.root.logger.info(f"Use selected bodyparts only: {self.bodyparts_list_widget.selected_bodyparts}") def evaluate_network(self): config = self.root.config @@ -199,8 +185,7 @@ def evaluate_network(self): bodyparts_to_use = "all" if ( - len(self.root.all_bodyparts) - != len(self.bodyparts_list_widget.selected_bodyparts) + len(self.root.all_bodyparts) != len(self.bodyparts_list_widget.selected_bodyparts) ) and not self.use_all_bodyparts.isChecked(): bodyparts_to_use = self.bodyparts_list_widget.selected_bodyparts @@ -225,11 +210,7 @@ def evaluate_network(self): trainFraction=project_cfg["TrainingFraction"][0], ) - image_dir = ( - Path(self.root.project_folder) - / eval_folder - / f"LabeledImages_{scorer}" - ) + image_dir = Path(self.root.project_folder) / eval_folder / f"LabeledImages_{scorer}" labeled_images = [str(p) for p in image_dir.rglob("*.png")] if len(labeled_images) > 0: _ = launch_napari(image_dir) diff --git a/deeplabcut/gui/tabs/extract_frames.py b/deeplabcut/gui/tabs/extract_frames.py index e2fbc0aaf4..3602456a08 100644 --- a/deeplabcut/gui/tabs/extract_frames.py +++ b/deeplabcut/gui/tabs/extract_frames.py @@ -131,25 +131,19 @@ def _generate_layout_attributes(self, layout): self.extraction_method_widget = QtWidgets.QComboBox() options = ["automatic", "manual"] self.extraction_method_widget.addItems(options) - self.extraction_method_widget.currentTextChanged.connect( - self.log_extraction_method - ) + self.extraction_method_widget.currentTextChanged.connect(self.log_extraction_method) # Frame extraction algorithm ext_algo_label = QtWidgets.QLabel("Extraction algorithm") self.extraction_algorithm_widget = QtWidgets.QComboBox() self.extraction_algorithm_widget.addItems(DLCParams.FRAME_EXTRACTION_ALGORITHMS) - self.extraction_algorithm_widget.currentTextChanged.connect( - self.log_extraction_algorithm - ) + self.extraction_algorithm_widget.currentTextChanged.connect(self.log_extraction_algorithm) # Frame cropping frame_crop_label = QtWidgets.QLabel("Frame cropping") self.frame_cropping_widget = QtWidgets.QComboBox() self.frame_cropping_widget.addItems(["disabled", "read from config", "GUI"]) - self.frame_cropping_widget.currentTextChanged.connect( - self.log_frame_cropping_choice - ) + self.frame_cropping_widget.currentTextChanged.connect(self.log_frame_cropping_choice) # Cluster step cluster_step_label = QtWidgets.QLabel("Cluster step") @@ -208,9 +202,7 @@ def extract_frames(self): return first_video = videos[0] if len(videos) > 1: - self.root.writer.write( - f"Only the first video ({first_video}) will be opened." - ) + self.root.writer.write(f"Only the first video ({first_video}) will be opened.") video_path_in_folder = self._check_symlink(first_video) _ = launch_napari(str(video_path_in_folder)) return @@ -258,19 +250,14 @@ def _show_success_message(self): return if len(failed) == 0: - message = ( - "Frame extraction failed. Please check your terminal output " - "for more information." - ) + message = "Frame extraction failed. Please check your terminal output for more information." elif all(failed): message = "Frame extraction failed. Video files must be corrupted." elif any(failed): message = "Although most frames were extracted, some were invalid." root_message = "failed to extract (some) frames" else: - message = ( - "Frames were successfully extracted, for the videos of interest." - ) + message = "Frames were successfully extracted, for the videos of interest." root_message = "successfully extracted frames" msg = QtWidgets.QMessageBox() diff --git a/deeplabcut/gui/tabs/extract_outlier_frames.py b/deeplabcut/gui/tabs/extract_outlier_frames.py index ac62911ae5..d4ed23d4bf 100644 --- a/deeplabcut/gui/tabs/extract_outlier_frames.py +++ b/deeplabcut/gui/tabs/extract_outlier_frames.py @@ -47,9 +47,7 @@ def _set_page(self): self._generate_multianimal_options(self.layout_attributes) self.main_layout.addLayout(self.layout_attributes) - self.main_layout.addWidget( - _create_label_widget("Frame extraction options", "font:bold") - ) + self.main_layout.addWidget(_create_label_widget("Frame extraction options", "font:bold")) self.layout_extraction_options = _create_horizontal_layout() self._generate_layout_extraction_options(self.layout_extraction_options) self.main_layout.addLayout(self.layout_extraction_options) @@ -67,9 +65,7 @@ def _set_page(self): self.merge_data_button.clicked.connect(self.merge_dataset) self.merge_data_button.setMinimumWidth(150) - self.main_layout.addWidget( - self.extract_outlierframes_button, alignment=Qt.AlignRight - ) + self.main_layout.addWidget(self.extract_outlierframes_button, alignment=Qt.AlignRight) self.main_layout.addWidget(self.label_outliers_button, alignment=Qt.AlignRight) self.main_layout.addWidget(self.merge_data_button, alignment=Qt.AlignRight) @@ -115,9 +111,7 @@ def _generate_layout_extraction_options(self, layout): self.outlier_algorithm_widget = QtWidgets.QComboBox() self.outlier_algorithm_widget.addItems(DLCParams.OUTLIER_EXTRACTION_ALGORITHMS) self.outlier_algorithm_widget.setMinimumWidth(200) - self.outlier_algorithm_widget.currentTextChanged.connect( - self.update_outlier_algorithm - ) + self.outlier_algorithm_widget.currentTextChanged.connect(self.update_outlier_algorithm) layout.addWidget(opt_text) layout.addWidget(self.outlier_algorithm_widget) @@ -126,9 +120,7 @@ def update_tracker_type(self, method): self.root.logger.info(f"Using {method.upper()} tracker") def update_outlier_algorithm(self, algorithm): - self.root.logger.info( - f"Using {algorithm.upper()} algorithm for frame extraction" - ) + self.root.logger.info(f"Using {algorithm.upper()} algorithm for frame extraction") def extract_outlier_frames(self): config = self.root.config diff --git a/deeplabcut/gui/tabs/label_frames.py b/deeplabcut/gui/tabs/label_frames.py index eb35f507b7..13b0de7056 100644 --- a/deeplabcut/gui/tabs/label_frames.py +++ b/deeplabcut/gui/tabs/label_frames.py @@ -22,10 +22,7 @@ from deeplabcut.utils.skeleton import SkeletonBuilder -def label_frames( - config_path: str | Path | None = None, - image_folder: str | None = None -): +def label_frames(config_path: str | Path | None = None, image_folder: str | None = None): """Launches the napari-deeplabcut labelling GUI. For more information on labelling data with napari-deeplabcut, see our docs: @@ -125,9 +122,7 @@ def label_frames(self): dialog = QtWidgets.QFileDialog(self) dialog.setFileMode(QtWidgets.QFileDialog.Directory) dialog.setViewMode(QtWidgets.QFileDialog.Detail) - dialog.setDirectory( - os.path.join(os.path.dirname(self.root.config), "labeled-data") - ) + dialog.setDirectory(os.path.join(os.path.dirname(self.root.config), "labeled-data")) if dialog.exec_(): folder = dialog.selectedFiles()[0] has_h5 = False @@ -145,4 +140,4 @@ def check_labels(self): _ = launch_napari(labeled_images, plugin="napari", stack=True) def build_skeleton(self, *args): - SkeletonBuilder(self.root.config) \ No newline at end of file + SkeletonBuilder(self.root.config) diff --git a/deeplabcut/gui/tabs/modelzoo.py b/deeplabcut/gui/tabs/modelzoo.py index 18200148c3..e74c43f7a1 100644 --- a/deeplabcut/gui/tabs/modelzoo.py +++ b/deeplabcut/gui/tabs/modelzoo.py @@ -87,9 +87,7 @@ def _set_page(self): button_layout.addStretch() self.main_layout.addWidget(_create_label_widget("Video Selection", "font:bold")) - self.video_selection_widget = VideoSelectionWidget( - self.root, self, hide_videotype=True - ) + self.video_selection_widget = VideoSelectionWidget(self.root, self, hide_videotype=True) self.main_layout.addWidget(self.video_selection_widget) self._build_common_attributes() @@ -241,27 +239,19 @@ def _build_common_attributes(self) -> None: self.model_combo.currentTextChanged.connect(self._update_pose_models) self.model_combo.currentTextChanged.connect(self._update_detectors) - self.model_combo.currentTextChanged.connect( - self._update_adaptation_detector_visibility - ) + self.model_combo.currentTextChanged.connect(self._update_adaptation_detector_visibility) def _add_tf_scales_row(self, layout: QtWidgets.QGridLayout): scales_label = QtWidgets.QLabel("Scale list") scales_label.setMinimumWidth(300) self.scales_line = QtWidgets.QLineEdit("", parent=self) self.scales_line.setMinimumWidth(500) - self.scales_line.setPlaceholderText( - "Optionally input a list of integer sizes separated by commas..." - ) + self.scales_line.setPlaceholderText("Optionally input a list of integer sizes separated by commas...") validator = RegExpValidator(self._val_pattern, self) validator.validationChanged.connect(self._handle_validation_change) self.scales_line.setValidator(validator) tooltip_label = QtWidgets.QLabel() - tooltip_label.setPixmap( - QPixmap( - os.path.join(BASE_DIR, "assets", "icons", "help2.png") - ).scaledToWidth(30) - ) + tooltip_label.setPixmap(QPixmap(os.path.join(BASE_DIR, "assets", "icons", "help2.png")).scaledToWidth(30)) tooltip_label.setToolTip( "Approximate animal sizes in pixels, for spatial pyramid search. If left " "blank, defaults to video height +/- 50 pixels" @@ -276,14 +266,10 @@ def _add_use_adaptation_row(self, layout: QtWidgets.QGridLayout, layout_row: int # --- Adaptation Checkbox with Help Button (TF section) --- self.adapt_checkbox = QtWidgets.QCheckBox("Use video adaptation") self.adapt_checkbox.setChecked(True) - self.adapt_checkbox.setStyleSheet( - "font-weight: bold; font-size: 16px; padding: 6px 12px;" - ) + self.adapt_checkbox.setStyleSheet("font-weight: bold; font-size: 16px; padding: 6px 12px;") # Add help button adapt_help_btn = QtWidgets.QToolButton() - adapt_help_btn.setIcon( - QIcon(os.path.join(BASE_DIR, "assets", "icons", "help2.png")) - ) + adapt_help_btn.setIcon(QIcon(os.path.join(BASE_DIR, "assets", "icons", "help2.png"))) adapt_help_btn.setIconSize(QSize(24, 24)) adapt_help_btn.setToolTip("What is video adaptation?") @@ -361,9 +347,7 @@ def _add_torch_adaptation_settings_row(self, layout: QtWidgets.QGridLayout): self.torch_adapt_epoch_spinbox.setRange(1, 50) self.torch_adapt_epoch_spinbox.setValue(4) self.torch_adapt_epoch_spinbox.setMaximumWidth(100) - self.adapt_det_epoch_label = QtWidgets.QLabel( - "Number of detector adaptation epochs" - ) + self.adapt_det_epoch_label = QtWidgets.QLabel("Number of detector adaptation epochs") self.adapt_det_epoch_label.setMinimumWidth(200) self.torch_adapt_det_epoch_spinbox = QtWidgets.QSpinBox() self.torch_adapt_det_epoch_spinbox.setRange(1, 50) @@ -371,9 +355,7 @@ def _add_torch_adaptation_settings_row(self, layout: QtWidgets.QGridLayout): self.torch_adapt_det_epoch_spinbox.setMaximumWidth(100) self.torch_adaptation_settings_row = QtWidgets.QHBoxLayout() self.torch_adaptation_settings_row.addWidget(pseudo_threshold_label) - self.torch_adaptation_settings_row.addWidget( - self.torch_pseudo_threshold_spinbox - ) + self.torch_adaptation_settings_row.addWidget(self.torch_pseudo_threshold_spinbox) self.torch_adaptation_settings_row.addSpacing(20) self.torch_adaptation_settings_row.addWidget(adapt_epoch_label) self.torch_adaptation_settings_row.addWidget(self.torch_adapt_epoch_spinbox) @@ -398,22 +380,14 @@ def _build_torch_attributes(self) -> None: def _adapt_checkbox_status_changed(self, state: int) -> None: if self.root.engine == Engine.TF: - set_layout_contents_visible( - self.tf_adaptation_settings_row, Qt.CheckState(state) == Qt.Checked - ) + set_layout_contents_visible(self.tf_adaptation_settings_row, Qt.CheckState(state) == Qt.Checked) elif self.root.engine == Engine.PYTORCH: - set_layout_contents_visible( - self.torch_adaptation_settings_row, Qt.CheckState(state) == Qt.Checked - ) + set_layout_contents_visible(self.torch_adaptation_settings_row, Qt.CheckState(state) == Qt.Checked) if Qt.CheckState(state) == Qt.Checked: - self._update_adaptation_detector_visibility( - self.model_combo.currentText() - ) + self._update_adaptation_detector_visibility(self.model_combo.currentText()) def select_folder(self): - dirname = QtWidgets.QFileDialog.getExistingDirectory( - self, "Please select a folder", self.root.project_folder - ) + dirname = QtWidgets.QFileDialog.getExistingDirectory(self, "Please select a folder", self.root.project_folder) if not dirname: return @@ -513,17 +487,14 @@ def signal_analysis_complete(self): for video_path in files: video_name = Path(video_path).stem - labeled_videos = list( - Path(output_folder).glob(f"{video_name}_*_labeled*.mp4") - ) + labeled_videos = list(Path(output_folder).glob(f"{video_name}_*_labeled*.mp4")) if labeled_videos: videos_created.extend([str(v) for v in labeled_videos]) # Show appropriate message if videos_created: msg = QtWidgets.QMessageBox( - text=f"SuperAnimal video inference complete!\n\nCreated labeled videos:\n" - + "\n".join(videos_created) + text=f"SuperAnimal video inference complete!\n\nCreated labeled videos:\n" + "\n".join(videos_created) ) msg.setIcon(QtWidgets.QMessageBox.Information) msg.exec_() @@ -560,10 +531,7 @@ def _gather_kwargs(self) -> dict: scales = [] scales_ = self.scales_line.text() if scales_: - if ( - self.scales_line.validator().validate(scales_, 0)[0] - == RegExpValidator.Acceptable - ): + if self.scales_line.validator().validate(scales_, 0)[0] == RegExpValidator.Acceptable: scales = list(map(int, scales_.split(","))) kwargs["scale_list"] = scales kwargs["video_adapt"] = self.adapt_checkbox.isChecked() @@ -571,7 +539,7 @@ def _gather_kwargs(self) -> dict: kwargs["adapt_iterations"] = self.adapt_iter_spinbox.value() else: kwargs["detector_name"] = self.detector_type_selector.currentText() - kwargs["video_adapt"] = (self.adapt_checkbox.isChecked()) + kwargs["video_adapt"] = self.adapt_checkbox.isChecked() kwargs["pseudo_threshold"] = self.pose_threshold_spinbox.value() kwargs["bbox_threshold"] = self.detector_threshold_spinbox.value() kwargs["detector_epochs"] = self.torch_adapt_det_epoch_spinbox.value() @@ -591,11 +559,7 @@ def _update_available_models(self, engine: Engine) -> None: set_combo_items( combo_box=self.model_combo, items=supermodels, - index=( - supermodels.index(current_dataset) - if current_dataset in supermodels - else 0 - ), + index=(supermodels.index(current_dataset) if current_dataset in supermodels else 0), ) def _update_pose_models(self, super_animal: str) -> None: @@ -605,11 +569,7 @@ def _update_pose_models(self, super_animal: str) -> None: set_combo_items( combo_box=self.net_type_selector, - items=( - ["dlcrnet"] - if self.root.engine == Engine.TF - else dlclibrary.get_available_models(super_animal) - ), + items=(["dlcrnet"] if self.root.engine == Engine.TF else dlclibrary.get_available_models(super_animal)), ) def _update_detectors(self, super_animal: str) -> None: @@ -624,15 +584,11 @@ def _update_detectors(self, super_animal: str) -> None: else: items = dlclibrary.get_available_detectors(super_animal) set_combo_items(combo_box=self.detector_type_selector, items=items) - set_layout_contents_visible( - self.detector_row, self.root.engine == Engine.PYTORCH - ) + set_layout_contents_visible(self.detector_row, self.root.engine == Engine.PYTORCH) def _update_adaptation_detector_visibility(self, superanimal: str): self.adapt_det_epoch_label.setVisible((superanimal != "superanimal_humanbody")) - self.torch_adapt_det_epoch_spinbox.setVisible( - (superanimal != "superanimal_humanbody") - ) + self.torch_adapt_det_epoch_spinbox.setVisible((superanimal != "superanimal_humanbody")) @Slot(Engine) def _on_engine_change(self, engine: Engine) -> None: diff --git a/deeplabcut/gui/tabs/refine_tracklets.py b/deeplabcut/gui/tabs/refine_tracklets.py index 74227b3512..004f4492f4 100644 --- a/deeplabcut/gui/tabs/refine_tracklets.py +++ b/deeplabcut/gui/tabs/refine_tracklets.py @@ -122,9 +122,7 @@ def _generate_layout_attributes(self, layout): layout.addWidget(self.num_animals_in_videos) def _generate_layout_refinement(self, layout): - section_title = _create_label_widget( - "Refinement Settings", "font:bold", (0, 50, 0, 0) - ) + section_title = _create_label_widget("Refinement Settings", "font:bold", (0, 50, 0, 0)) # Min swap length swap_length_label = QtWidgets.QLabel("Min swap length to highlight") diff --git a/deeplabcut/gui/tabs/train_network.py b/deeplabcut/gui/tabs/train_network.py index 212302eafd..5957d0673d 100644 --- a/deeplabcut/gui/tabs/train_network.py +++ b/deeplabcut/gui/tabs/train_network.py @@ -185,9 +185,7 @@ def _generate_layout_attributes(self) -> None: spin_box.setMinimum(attribute.min) spin_box.setMaximum(attribute.max) spin_box.setValue(attribute.default) - spin_box.valueChanged.connect( - lambda new_val: self.log_attribute_change(attribute, new_val) - ) + spin_box.valueChanged.connect(lambda new_val: self.log_attribute_change(attribute, new_val)) self._attribute_kwargs[engine][attribute.fn_key] = spin_box # Pad below to create spacing with other rows @@ -201,9 +199,7 @@ def _generate_layout_attributes(self) -> None: param_layout.addWidget(spin_box, row_index, 2 * j + 1) if row.show_when_cfg is not None: - self._rows_with_requirements.append( - (row.show_when_cfg, row_elements) - ) + self._rows_with_requirements.append((row.show_when_cfg, row_elements)) row_index += 1 @@ -230,14 +226,10 @@ def train_network(self): for k, spin_box in self._attribute_kwargs[self.root.engine].items(): kwargs[k] = int(spin_box.value()) if self.root.engine == Engine.PYTORCH: - snapshot_to_start_training_from = ( - self.snapshot_selection_widget.selected_snapshot - ) + snapshot_to_start_training_from = self.snapshot_selection_widget.selected_snapshot if snapshot_to_start_training_from is not None: kwargs["snapshot_path"] = snapshot_to_start_training_from - detector_to_start_training_from = ( - self.detector_snapshot_selection_widget.selected_snapshot - ) + detector_to_start_training_from = self.detector_snapshot_selection_widget.selected_snapshot if detector_to_start_training_from is not None: kwargs["detector_path"] = detector_to_start_training_from @@ -245,9 +237,7 @@ def train_network(self): msg = QtWidgets.QMessageBox() msg.setIcon(QtWidgets.QMessageBox.Information) msg.setText("The network is now trained and ready to evaluate.") - msg.setInformativeText( - "Use the function 'evaluate_network' to evaluate the network." - ) + msg.setInformativeText("Use the function 'evaluate_network' to evaluate the network.") msg.setWindowTitle("Info") msg.setMinimumWidth(900) diff --git a/deeplabcut/gui/tabs/unsupervised_id_tracking.py b/deeplabcut/gui/tabs/unsupervised_id_tracking.py index 5272be8d5e..044b0bdabc 100644 --- a/deeplabcut/gui/tabs/unsupervised_id_tracking.py +++ b/deeplabcut/gui/tabs/unsupervised_id_tracking.py @@ -128,9 +128,7 @@ def run_transformer(self): track_method=track_method, ) self.worker, self.thread = move_to_separate_thread(func) - self.worker.finished.connect( - lambda: self.run_transformer_button.setEnabled(True) - ) + self.worker.finished.connect(lambda: self.run_transformer_button.setEnabled(True)) self.worker.finished.connect(lambda: self.root._progress_bar.hide()) self.thread.start() self.run_transformer_button.setEnabled(False) diff --git a/deeplabcut/gui/tabs/video_editor.py b/deeplabcut/gui/tabs/video_editor.py index ecc0374c4d..e0d8c75d03 100644 --- a/deeplabcut/gui/tabs/video_editor.py +++ b/deeplabcut/gui/tabs/video_editor.py @@ -139,17 +139,12 @@ def rotate_videos(self): if self.files: for video in self.files: if self.video_rotation.currentText() == "specific angle": - auxfun_videos.rotate_video( - video, self.rotation_angle.value(), "Arbitrary" - ) + auxfun_videos.rotate_video(video, self.rotation_angle.value(), "Arbitrary") elif self.video_rotation.currentText() == "clockwise": - auxfun_videos.rotate_video( - video, 0, "Yes" - ) + auxfun_videos.rotate_video(video, 0, "Yes") else: self.root.logger.error("No videos selected...") - def trim_videos(self): start = time.strftime("%H:%M:%S", time.gmtime(self.video_start.value())) stop = time.strftime("%H:%M:%S", time.gmtime(self.video_stop.value())) diff --git a/deeplabcut/gui/tracklet_toolbox.py b/deeplabcut/gui/tracklet_toolbox.py index 9eb71dc145..b942ebf0af 100644 --- a/deeplabcut/gui/tracklet_toolbox.py +++ b/deeplabcut/gui/tracklet_toolbox.py @@ -49,18 +49,10 @@ def __init__(self, point, bodyParts, individual_names=None, likelihood=None): def connect(self): "connect to all the events we need" - self.cidpress = self.point.figure.canvas.mpl_connect( - "button_press_event", self.on_press - ) - self.cidrelease = self.point.figure.canvas.mpl_connect( - "button_release_event", self.on_release - ) - self.cidmotion = self.point.figure.canvas.mpl_connect( - "motion_notify_event", self.on_motion - ) - self.cidhover = self.point.figure.canvas.mpl_connect( - "motion_notify_event", self.on_hover - ) + self.cidpress = self.point.figure.canvas.mpl_connect("button_press_event", self.on_press) + self.cidrelease = self.point.figure.canvas.mpl_connect("button_release_event", self.on_release) + self.cidmotion = self.point.figure.canvas.mpl_connect("motion_notify_event", self.on_motion) + self.cidhover = self.point.figure.canvas.mpl_connect("motion_notify_event", self.on_hover) def on_press(self, event): """ @@ -299,17 +291,13 @@ def reconnect(self): class TrackletVisualizer: def __init__(self, manager, videoname, trail_len=50): self.manager = manager - self.cmap = plt.cm.get_cmap( - manager.cfg["colormap"], len(set(manager.tracklet2id)) - ) + self.cmap = plt.cm.get_cmap(manager.cfg["colormap"], len(set(manager.tracklet2id))) self.videoname = videoname self.video = VideoReader(videoname) self.nframes = len(self.video) # Take into consideration imprecise OpenCV estimation of total number of frames if abs(self.nframes - manager.nframes) >= 0.05 * manager.nframes: - print( - "Video duration and data length do not match. Continuing nonetheless..." - ) + print("Video duration and data length do not match. Continuing nonetheless...") self.trail_len = trail_len self.help_text = "" self.draggable = False @@ -364,9 +352,7 @@ def _prepare_canvas(self, manager, fig): self.scat = self.ax1.scatter([], [], s=self.dotsize**2, picker=True) self.scat.set_offsets(manager.xy[:, 0]) self.scat.set_color(self.colors) - self.trails = sum( - [self.ax1.plot([], [], "-", lw=2, c=c) for c in self.colors], [] - ) + self.trails = sum([self.ax1.plot([], [], "-", lw=2, c=c) for c in self.colors], []) self.lines_x = sum( [self.ax2.plot([], [], "-", lw=1, c=c, pickradius=5) for c in self.colors], [], @@ -378,10 +364,7 @@ def _prepare_canvas(self, manager, fig): self.vline_x = self.ax2.axvline(0, 0, 1, c="k", ls=":") self.vline_y = self.ax3.axvline(0, 0, 1, c="k", ls=":") - custom_lines = [ - plt.Line2D([0], [0], color=self.cmap(i), lw=4) - for i in range(len(manager.individuals)) - ] + custom_lines = [plt.Line2D([0], [0], color=self.cmap(i), lw=4) for i in range(len(manager.individuals))] self.leg = self.fig.legend( custom_lines, manager.individuals, @@ -396,9 +379,7 @@ def _prepare_canvas(self, manager, fig): line.set_picker(5) self.ax_slider = self.fig.add_axes([0.1, 0.1, 0.5, 0.03], facecolor="lightgray") - self.ax_slider2 = self.fig.add_axes( - [0.1, 0.05, 0.3, 0.03], facecolor="darkorange" - ) + self.ax_slider2 = self.fig.add_axes([0.1, 0.05, 0.3, 0.03], facecolor="darkorange") self.slider = Slider( self.ax_slider, "# Frame", @@ -463,18 +444,9 @@ def show(self, fig=None): def swap_tracklets(self, event): if self.swap_id1 is not None and self.swap_id2 is not None: - # Get tracklet indices for each individual - inds1 = [ - k - for k in range(len(self.manager.tracklet2id)) - if self.manager.tracklet2id[k] == self.swap_id1 - ] - inds2 = [ - k - for k in range(len(self.manager.tracklet2id)) - if self.manager.tracklet2id[k] == self.swap_id2 - ] + inds1 = [k for k in range(len(self.manager.tracklet2id)) if self.manager.tracklet2id[k] == self.swap_id1] + inds2 = [k for k in range(len(self.manager.tracklet2id)) if self.manager.tracklet2id[k] == self.swap_id2] print(f"Swapping tracklets {self.swap_id1} and {self.swap_id2}") @@ -499,9 +471,7 @@ def set_swap_id1(self, val): self.swap_id1 = int(val) print("ID 1 set.") else: - print( - f"Invalid ID. Please select a valid ID from the list of individuals: {set(self.manager.tracklet2id)}" - ) + print(f"Invalid ID. Please select a valid ID from the list of individuals: {set(self.manager.tracklet2id)}") self.swap_id1 = None def set_swap_id2(self, val): @@ -510,9 +480,7 @@ def set_swap_id2(self, val): self.swap_id2 = int(val) print("ID 2 set.") else: - print( - f"Invalid ID. Please select a valid ID from the list of individuals: {set(self.manager.tracklet2id)}" - ) + print(f"Invalid ID. Please select a valid ID from the list of individuals: {set(self.manager.tracklet2id)}") self.swap_id2 = None def terminate(self, event): @@ -531,12 +499,8 @@ def fill_shaded_areas(self): facecolor="darkgray", alpha=0.2, ) - trans = mtransforms.blended_transform_factory( - self.ax_slider.transData, self.ax_slider.transAxes - ) - self.ax_slider.vlines( - np.flatnonzero(mask), 0, 0.5, color="darkorange", transform=trans - ) + trans = mtransforms.blended_transform_factory(self.ax_slider.transData, self.ax_slider.transAxes) + self.ax_slider.vlines(np.flatnonzero(mask), 0, 0.5, color="darkorange", transform=trans) def toggle_draggable_points(self, *args): self.draggable = not self.draggable @@ -592,9 +556,7 @@ def save_coords(self): if not nrow.size: return nrow = nrow[0] - if not np.array_equal( - coords[nrow], dp.point.center - ): # Keypoint has been displaced + if not np.array_equal(coords[nrow], dp.point.center): # Keypoint has been displaced coords[nrow] = dp.point.center prob[ind] = 1 self.manager.xy[nonempty, self._curr_frame] = coords @@ -614,12 +576,8 @@ def flag_frame(self, *args): facecolor="darkgray", alpha=0.2, ) - trans = mtransforms.blended_transform_factory( - self.ax_slider.transData, self.ax_slider.transAxes - ) - self.ax_slider.vlines( - np.flatnonzero(mask), 0, 0.5, color="darkorange", transform=trans - ) + trans = mtransforms.blended_transform_factory(self.ax_slider.transData, self.ax_slider.transAxes) + self.ax_slider.vlines(np.flatnonzero(mask), 0, 0.5, color="darkorange", transform=trans) self.fig.canvas.draw_idle() def on_scroll(self, event): @@ -662,9 +620,9 @@ def on_press(self, event): if len(self.cuts) > 1: self.cuts.sort() if self.picked_pair: - self.manager.tracklet_swaps[self.picked_pair][self.cuts] = ( - ~self.manager.tracklet_swaps[self.picked_pair][self.cuts] - ) + self.manager.tracklet_swaps[self.picked_pair][self.cuts] = ~self.manager.tracklet_swaps[ + self.picked_pair + ][self.cuts] self.fill_shaded_areas() self.cuts = [] for line in self.ax_slider.lines: @@ -679,12 +637,7 @@ def on_press(self, event): except IndexError: pass else: # Smart point removal - i = np.nanargmin( - [ - self.calc_distance(*dp.point.center, event.xdata, event.ydata) - for dp in self.dps - ] - ) + i = np.nanargmin([self.calc_distance(*dp.point.center, event.xdata, event.ydata) for dp in self.dps]) closest_dp = self.dps[i] label = closest_dp.individual_names, closest_dp.bodyParts closest_dp.disconnect() @@ -718,14 +671,10 @@ def move_backward(self): def swap(self): if self.picked_pair: swap_inds = self.manager.get_swap_indices(*self.picked_pair) - inds = np.insert( - swap_inds, [0, len(swap_inds)], [0, self.manager.nframes - 1] - ) + inds = np.insert(swap_inds, [0, len(swap_inds)], [0, self.manager.nframes - 1]) if len(inds): ind = np.argmax(inds > self.curr_frame) - self.manager.swap_tracklets( - *self.picked_pair, range(inds[ind - 1], inds[ind] + 1) - ) + self.manager.swap_tracklets(*self.picked_pair, range(inds[ind - 1], inds[ind] + 1)) self.display_traces() self.slider.set_val(self.curr_frame) @@ -751,9 +700,7 @@ def on_pick(self, event): if self.picked: num_individual = self.leg.get_lines().index(artist) nrow = self.manager.tracklet2id.index(num_individual) - inds = [ - nrow + self.manager.to_num_bodypart(pick) for pick in self.picked - ] + inds = [nrow + self.manager.to_num_bodypart(pick) for pick in self.picked] xy = self.manager.xy[self.picked] p = self.manager.prob[self.picked] mask = np.zeros(xy.shape[1], dtype=bool) @@ -799,9 +746,7 @@ def on_click(self, event): self.clean_collections() def clean_collections(self): - for coll in ( - self.ax2.collections + self.ax3.collections + self.ax_slider.collections - ): + for coll in self.ax2.collections + self.ax3.collections + self.ax_slider.collections: coll.remove() def display_points(self, val): @@ -903,9 +848,7 @@ def export_to_training_data(self, pcutoff=0.1): # Save additional frames to the labeled-data directory strwidth = int(np.ceil(np.log10(self.nframes))) - tmpfolder = os.path.join( - self.manager.cfg["project_path"], "labeled-data", self.video.name - ) + tmpfolder = os.path.join(self.manager.cfg["project_path"], "labeled-data", self.video.name) if os.path.isdir(tmpfolder): print( "Frames from video", @@ -916,14 +859,8 @@ def export_to_training_data(self, pcutoff=0.1): attempt_to_make_folder(tmpfolder) index = [] for ind in inds: - imagename = os.path.join( - tmpfolder, "img" + str(ind).zfill(strwidth) + ".png" - ) - index.append( - tuple( - (os.path.join(*imagename.rsplit(os.path.sep, 3)[-3:])).split("\\") - ) - ) + imagename = os.path.join(tmpfolder, "img" + str(ind).zfill(strwidth) + ".png") + index.append(tuple((os.path.join(*imagename.rsplit(os.path.sep, 3)[-3:])).split("\\"))) if not os.path.isfile(imagename): self.video.set_to_frame(ind) frame = self.video.read_frame() @@ -932,9 +869,7 @@ def export_to_training_data(self, pcutoff=0.1): continue frame = frame.astype(np.ubyte) if self.manager.cfg["cropping"]: - x1, x2, y1, y2 = [ - int(self.manager.cfg[key]) for key in ("x1", "x2", "y1", "y2") - ] + x1, x2, y1, y2 = [int(self.manager.cfg[key]) for key in ("x1", "x2", "y1", "y2")] frame = frame[y1:y2, x1:x2] io.imsave(imagename, frame) @@ -948,14 +883,10 @@ def filter_low_prob(cols, prob): cols.loc[mask] = np.nan return cols - df = df.groupby(level="bodyparts", axis=1, group_keys=False).apply( - filter_low_prob, prob=pcutoff - ) + df = df.groupby(level="bodyparts", axis=1, group_keys=False).apply(filter_low_prob, prob=pcutoff) df.index = pd.MultiIndex.from_tuples(index) - machinefile = os.path.join( - tmpfolder, "machinelabels-iter" + str(self.manager.cfg["iteration"]) + ".h5" - ) + machinefile = os.path.join(tmpfolder, "machinelabels-iter" + str(self.manager.cfg["iteration"]) + ".h5") if os.path.isfile(machinefile): df_old = pd.read_hdf(machinefile) df_joint = pd.concat([df_old, df]) @@ -969,9 +900,7 @@ def filter_low_prob(cols, prob): # Merge with the already existing annotated data df.columns = df.columns.set_levels([self.manager.cfg["scorer"]], level="scorer") df.drop("likelihood", level="coords", axis=1, inplace=True) - output_path = os.path.join( - tmpfolder, f'CollectedData_{self.manager.cfg["scorer"]}.h5' - ) + output_path = os.path.join(tmpfolder, f"CollectedData_{self.manager.cfg['scorer']}.h5") if os.path.isfile(output_path): print( "A training dataset file is already found for this video. The refined machine labels are merged to this data!" diff --git a/deeplabcut/gui/widgets.py b/deeplabcut/gui/widgets.py index 085fdd1682..f6b17cc407 100644 --- a/deeplabcut/gui/widgets.py +++ b/deeplabcut/gui/widgets.py @@ -175,9 +175,7 @@ def toggle_select(self, state): class NavigationToolbar(NavigationToolbar2QT): - toolitems = [ - t for t in NavigationToolbar2QT.toolitems if t[0] in ("Home", "Pan", "Zoom") - ] + toolitems = [t for t in NavigationToolbar2QT.toolitems if t[0] in ("Home", "Pan", "Zoom")] def set_message(self, msg): pass @@ -389,9 +387,7 @@ def get_nested_key(cfg, keys): def edit_value(self, item): keys, value = self.walk_recursively_to_root(item) - if ( - "crop" not in keys - ): # 'crop' should not be cast, otherwise it is understood as a list + if "crop" not in keys: # 'crop' should not be cast, otherwise it is understood as a list value = self.cast_to_right_type(value) self.set_value(self.cfg, keys, value) @@ -441,10 +437,7 @@ class ConfigEditor(QtWidgets.QDialog): def __init__(self, config, parent=None): super(ConfigEditor, self).__init__(parent) self.config = config - if ( - config.endswith("config.yaml") - and not config.endswith("pytorch_config.yaml") - ): + if config.endswith("config.yaml") and not config.endswith("pytorch_config.yaml"): self.read_func = auxiliaryfunctions.read_config self.write_func = auxiliaryfunctions.write_config else: @@ -546,12 +539,8 @@ def __init__(self, config_path, parent=None): root = os.path.join(self.cfg["project_path"], "labeled-data") for dir_ in os.listdir(root): folder = os.path.join(root, dir_) - if os.path.isdir(folder) and not any( - folder.endswith(s) for s in ("cropped", "labeled") - ): - self.df = pd.read_hdf( - os.path.join(folder, f'CollectedData_{self.cfg["scorer"]}.h5') - ) + if os.path.isdir(folder) and not any(folder.endswith(s) for s in ("cropped", "labeled")): + self.df = pd.read_hdf(os.path.join(folder, f"CollectedData_{self.cfg['scorer']}.h5")) row, col = self.pick_labeled_frame() if "individuals" in self.df.columns.names: self.df = self.df.xs(col, axis=1, level="individuals") @@ -602,9 +591,7 @@ def __init__(self, config_path, parent=None): layout.addWidget(self.canvas) self.setLayout(layout) - self.lines = LineCollection( - self.segs, colors=mcolors.to_rgba(self.cfg["skeleton_color"]) - ) + self.lines = LineCollection(self.segs, colors=mcolors.to_rgba(self.cfg["skeleton_color"])) self.lines.set_picker(True) self._show() diff --git a/deeplabcut/gui/window.py b/deeplabcut/gui/window.py index c91d6050f6..a503923b87 100644 --- a/deeplabcut/gui/window.py +++ b/deeplabcut/gui/window.py @@ -45,17 +45,14 @@ warnings.filterwarnings( "ignore", message=r".*shibokensupport/signature/parser.py:269: RuntimeWarning: pyside_type_init:_resolve_value.*", - category=RuntimeWarning + category=RuntimeWarning, ) + def _check_for_updates(silent=True): try: - is_latest, latest_version = call_with_timeout( - utils.is_latest_deeplabcut_version, 5 - ) - is_latest_plugin, latest_plugin_version = call_with_timeout( - misc.is_latest_version, 5 - ) + is_latest, latest_version = call_with_timeout(utils.is_latest_deeplabcut_version, 5) + is_latest_plugin, latest_plugin_version = call_with_timeout(misc.is_latest_version, 5) except (URLError, TimeoutError): # Handle internet connectivity issues is_latest = is_latest_plugin = True @@ -315,8 +312,8 @@ def add_video_files(self, new_video_files): Emits a signal to notify about the updated set of files. """ new_video_files = set(new_video_files) - self.files.update(new_video_files) # Add new items to the existing set - self.video_files_.emit(self.files) # Emit the updated set of files + self.files.update(new_video_files) # Add new items to the existing set + self.video_files_.emit(self.files) # Emit the updated set of files self.logger.info(f"Videos added to analyze:\n{new_video_files}\nCurrent video files:\n{self.files}") def clear_video_files(self): @@ -328,9 +325,9 @@ def clear_video_files(self): self.logger.info("All video files have been cleared.") def window_set(self): - WINDOW_RESIZE_FACTOR=.8 + WINDOW_RESIZE_FACTOR = 0.8 DEFAULT_MINIMUM_WIDTH, DEFAULT_MINIMUM_HEIGHT = 800, 600 - + self.setWindowTitle("DeepLabCut") palette = QtGui.QPalette() @@ -370,9 +367,7 @@ def _generate_welcome_page(self): image_widget.setContentsMargins(0, 0, 0, 0) logo = os.path.join(BASE_DIR, "assets", "logo_transparent.png") pixmap = QtGui.QPixmap(logo) - image_widget.setPixmap( - pixmap.scaledToHeight(400, QtCore.Qt.SmoothTransformation) - ) + image_widget.setPixmap(pixmap.scaledToHeight(400, QtCore.Qt.SmoothTransformation)) self.layout.addWidget(image_widget) description = "DeepLabCut™ is an open source tool for markerless pose estimation of user-defined body parts with deep learning.\nA. and M.W. Mathis Labs | http://www.deeplabcut.org\n\n To get started, create a new project, load an existing one, or try one of our pretrained models from the Model Zoo." @@ -423,9 +418,7 @@ def create_actions(self, names): self.newAction = QAction(self) self.newAction.setText("&New Project...") - self.newAction.setIcon( - QIcon(os.path.join(BASE_DIR, "assets", "icons", names[0])) - ) + self.newAction.setIcon(QIcon(os.path.join(BASE_DIR, "assets", "icons", names[0]))) self.newAction.setShortcut("Ctrl+N") self.newAction.setStatusTip("Create a new project...") @@ -433,9 +426,7 @@ def create_actions(self, names): # Creating actions using the second constructor self.openAction = QAction("&Open...", self) - self.openAction.setIcon( - QIcon(os.path.join(BASE_DIR, "assets", "icons", names[1])) - ) + self.openAction.setIcon(QIcon(os.path.join(BASE_DIR, "assets", "icons", names[1]))) self.openAction.setShortcut("Ctrl+O") self.openAction.setStatusTip("Open a project...") self.openAction.triggered.connect(self._open_project) @@ -449,9 +440,7 @@ def create_actions(self, names): self.darkmodeAction.triggered.connect(self.darkmode) self.helpAction = QAction("&Help", self) - self.helpAction.setIcon( - QIcon(os.path.join(BASE_DIR, "assets", "icons", names[2])) - ) + self.helpAction.setIcon(QIcon(os.path.join(BASE_DIR, "assets", "icons", names[2]))) self.helpAction.setStatusTip("Ask for help...") self.helpAction.triggered.connect(self._ask_for_help) @@ -472,9 +461,7 @@ def create_menu_bar(self): self.file_menu.addAction(self.openAction) self.recentfiles_menu = self.file_menu.addMenu("Open Recent") - self.recentfiles_menu.triggered.connect( - lambda a: self._update_project_state(a.text(), True) - ) + self.recentfiles_menu.triggered.connect(lambda a: self._update_project_state(a.text(), True)) self.file_menu.addAction(self.saveAction) self.file_menu.addAction(self.exitAction) @@ -521,9 +508,7 @@ def _update_icon(engine: str): file = files("deeplabcut.gui.media") / f"dlc-{engine}.png" pixmap = QPixmap(str(file)) if not pixmap.isNull(): - engine_icon.setPixmap( - pixmap.scaled(56, 56, Qt.AspectRatioMode.KeepAspectRatio) - ) + engine_icon.setPixmap(pixmap.scaled(56, 56, Qt.AspectRatioMode.KeepAspectRatio)) _update_icon("pt" if self.engine == Engine.PYTORCH else "tf") @@ -560,9 +545,7 @@ def _update_project_state(self, config, loaded): def _ask_for_help(self): dlg = QMessageBox(self) dlg.setWindowTitle("Ask for help") - dlg.setText( - """Ask our community for help on the forum!""" - ) + dlg.setText("""Ask our community for help on the forum!""") _ = dlg.exec() def _learn_dlc(self): @@ -592,9 +575,7 @@ def _open_project(self): def _goto_superanimal(self): self.tab_widget = QtWidgets.QTabWidget() self.tab_widget.setContentsMargins(0, 20, 0, 0) - self.modelzoo = ModelZoo( - root=self, parent=None, h1_description="DeepLabCut - Model Zoo" - ) + self.modelzoo = ModelZoo(root=self, parent=None, h1_description="DeepLabCut - Model Zoo") self.tab_widget.addTab(self.modelzoo, "Model Zoo") self.setCentralWidget(self.tab_widget) @@ -628,15 +609,9 @@ def lightmode(self): def add_tabs(self): self.tab_widget = QtWidgets.QTabWidget() self.tab_widget.setContentsMargins(0, 20, 0, 0) - self.manage_project = ManageProject( - root=self, parent=None, h1_description="DeepLabCut - Manage Project" - ) - self.extract_frames = ExtractFrames( - root=self, parent=None, h1_description="DeepLabCut - Extract Frames" - ) - self.label_frames = LabelFrames( - root=self, parent=None, h1_description="DeepLabCut - Label Frames" - ) + self.manage_project = ManageProject(root=self, parent=None, h1_description="DeepLabCut - Manage Project") + self.extract_frames = ExtractFrames(root=self, parent=None, h1_description="DeepLabCut - Extract Frames") + self.label_frames = LabelFrames(root=self, parent=None, h1_description="DeepLabCut - Label Frames") self.create_training_dataset = CreateTrainingDataset( root=self, parent=None, @@ -652,9 +627,7 @@ def add_tabs(self): parent=None, h1_description="DeepLabCut - Evaluate Network", ) - self.analyze_videos = AnalyzeVideos( - root=self, parent=None, h1_description="DeepLabCut - Analyze Videos" - ) + self.analyze_videos = AnalyzeVideos(root=self, parent=None, h1_description="DeepLabCut - Analyze Videos") self.unsupervised_id_tracking = UnsupervizedIdTracking( root=self, parent=None, @@ -670,15 +643,9 @@ def add_tabs(self): parent=None, h1_description="DeepLabCut - Step 8. Extract outlier frames", ) - self.refine_tracklets = RefineTracklets( - root=self, parent=None, h1_description="DeepLabCut - Refine labels" - ) - self.modelzoo = ModelZoo( - root=self, parent=None, h1_description="DeepLabCut - Model Zoo" - ) - self.video_editor = VideoEditor( - root=self, parent=None, h1_description="DeepLabCut - Optional Video Editor" - ) + self.refine_tracklets = RefineTracklets(root=self, parent=None, h1_description="DeepLabCut - Refine labels") + self.modelzoo = ModelZoo(root=self, parent=None, h1_description="DeepLabCut - Model Zoo") + self.video_editor = VideoEditor(root=self, parent=None, h1_description="DeepLabCut - Optional Video Editor") self.tab_widget.addTab(self.manage_project, "Manage project") self.tab_widget.addTab(self.extract_frames, "Extract frames") @@ -687,21 +654,15 @@ def add_tabs(self): self.tab_widget.addTab(self.train_network, "Train network") self.tab_widget.addTab(self.evaluate_network, "Evaluate network") self.tab_widget.addTab(self.analyze_videos, "Analyze videos") - self.tab_widget.addTab( - self.unsupervised_id_tracking, "Unsupervised ID Tracking (*)" - ) + self.tab_widget.addTab(self.unsupervised_id_tracking, "Unsupervised ID Tracking (*)") self.tab_widget.addTab(self.create_videos, "Create videos") - self.tab_widget.addTab( - self.extract_outlier_frames, "Extract outlier frames (*)" - ) + self.tab_widget.addTab(self.extract_outlier_frames, "Extract outlier frames (*)") self.tab_widget.addTab(self.refine_tracklets, "Refine tracklets (*)") self.tab_widget.addTab(self.modelzoo, "Model Zoo") self.tab_widget.addTab(self.video_editor, "Video editor (*)") if not self.is_multianimal: - self.tab_widget.removeTab( - self.tab_widget.indexOf(self.unsupervised_id_tracking) - ) + self.tab_widget.removeTab(self.tab_widget.indexOf(self.unsupervised_id_tracking)) self.tab_widget.removeTab(self.tab_widget.indexOf(self.refine_tracklets)) self.setCentralWidget(self.tab_widget) @@ -722,9 +683,7 @@ def _attempt_attribute_update(widget_name, updated_value): try: widget = getattr(active_tab, widget_name) method = getattr(widget, widget_to_attribute_map[type(widget)]) - self.logger.debug( - f"Setting {widget_name}={updated_value} in tab '{tab_label}'" - ) + self.logger.debug(f"Setting {widget_name}={updated_value} in tab '{tab_label}'") method(updated_value) except AttributeError: pass diff --git a/deeplabcut/modelzoo/fmpose_3d/fmpose3d.py b/deeplabcut/modelzoo/fmpose_3d/fmpose3d.py index a7ce458ef4..c8ceb5ae80 100644 --- a/deeplabcut/modelzoo/fmpose_3d/fmpose3d.py +++ b/deeplabcut/modelzoo/fmpose_3d/fmpose3d.py @@ -19,10 +19,10 @@ def get_fmpose3d_inference_api( snapshot_path: str | None = None, device: str | None = None, config_kwargs: dict = {}, - ) -> FMPose3DInference: +) -> FMPose3DInference: """ Get a FMPose3DInference API for a given model type and snapshot path. - + Args: model_type: one of the supported model types: "fmpose3d_humans", "fmpose3d_animals", snapshot_path: The path to the snapshot file. If None, FMPose3D will download the default snapshot. @@ -30,7 +30,7 @@ def get_fmpose3d_inference_api( config_kwargs: Additional keyword arguments to pass to the FMPose3DConfig. Returns: FMPose3DInference: An FMPose3DInference API runner. - + Example Usages ```python # Initialize the API (downloads the default weights automatically from huggingface) @@ -47,10 +47,6 @@ def get_fmpose3d_inference_api( predictions_3d = fmpose.pose_3d(keypoints_2d=keypoints_2d) ``` """ - model_config = FMPose3DConfig(model_type=model_type, **config_kwargs) - fmpose3d_api = FMPose3DInference( - model_config, - model_weights_path=snapshot_path, - device=device - ) - return fmpose3d_api \ No newline at end of file + model_config = FMPose3DConfig(model_type=model_type, **config_kwargs) + fmpose3d_api = FMPose3DInference(model_config, model_weights_path=snapshot_path, device=device) + return fmpose3d_api diff --git a/deeplabcut/modelzoo/generalized_data_converter/conversion_table/conversion_table.py b/deeplabcut/modelzoo/generalized_data_converter/conversion_table/conversion_table.py index 06cc1224a3..7a883d78db 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/conversion_table/conversion_table.py +++ b/deeplabcut/modelzoo/generalized_data_converter/conversion_table/conversion_table.py @@ -21,9 +21,7 @@ def __init__(self, raw_table_dict): def convert(self, kpt): if kpt not in self.table_dict: - warnings.warn( - f"{kpt} is defined in src space but not appeared in the conversion table" - ) + warnings.warn(f"{kpt} is defined in src space but not appeared in the conversion table") return None else: return self.table_dict[kpt] @@ -78,7 +76,6 @@ def __init__(self, src_keypoints, table_path): self.table = {} for src_kpt in src_keypoints: for target_kpt in target_keypoints: - src_kpt_id = self._search(src_kpt) target_kpt_id = self._search(target_kpt) @@ -122,9 +119,7 @@ def check_inclusion(self): def convert(self, kpt): if kpt not in self.table: - warnings.warn( - f"{kpt} is defined in src space but not appeared in the conversion table" - ) + warnings.warn(f"{kpt} is defined in src space but not appeared in the conversion table") return None else: return self.table[kpt] diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/base.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/base.py index 8ea478ce00..5a41f81f7c 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/base.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/base.py @@ -73,9 +73,7 @@ def __init__(self): def _build_maps(self): self.datasetname2imageids[self.meta["dataset_name"]] = set() - total_annotations = ( - self.generic_train_annotations + self.generic_test_annotations - ) + total_annotations = self.generic_train_annotations + self.generic_test_annotations for anno in total_annotations: image_id = anno["image_id"] if image_id not in self.imageid2anno: @@ -107,7 +105,6 @@ def filter_by_pattern(self, pattern): for img in self.generic_train_images + self.generic_test_images: print(img["file_name"]) if pattern in img["file_name"]: - image_id = img["id"] keep_ids.append(image_id) @@ -139,9 +136,9 @@ def filter_by_pattern(self, pattern): self.generic_test_annotations = keep_test_annotations def summary(self): - print(f'Summary of dataset {self.meta["dataset_name"]}') + print(f"Summary of dataset {self.meta['dataset_name']}") print("-------------") - print(f'max num individuals is {self.meta["max_individuals"]}') + print(f"max num individuals is {self.meta['max_individuals']}") print(f"total keypoints : {len(self.meta['categories']['keypoints'])}") print(f"total train images : {len(self.generic_train_images)}") print(f"total test images : {len(self.generic_test_images)}") @@ -235,9 +232,7 @@ def _proj(self, annotations, conversion_table): src_kpt_name = master2src[master_kpt_name] src_kpt_id = kpt2index[src_kpt_name] - new_kpts[master_kpt_id * 3 : master_kpt_id * 3 + 3] = kpts[ - src_kpt_id * 3 : src_kpt_id * 3 + 3 - ] + new_kpts[master_kpt_id * 3 : master_kpt_id * 3 + 3] = kpts[src_kpt_id * 3 : src_kpt_id * 3 + 3] # skipping empty frames after conversion new_anno = copy.deepcopy(anno) @@ -255,9 +250,7 @@ def adjust_bbox_and_area(self): """ from .utils import calc_bboxes_from_keypoints - for annotation in ( - self.generic_train_annotations + self.generic_test_annotations - ): + for annotation in self.generic_train_annotations + self.generic_test_annotations: keypoints = annotation["keypoints"] bbox_margin = 20 @@ -292,21 +285,15 @@ def project_with_conversion_table(self, table_path="", table_dict=None): Replace the generic annotations with those that are in superset keypoint space """ - print(f'Converting {self.meta["dataset_name"]}') + print(f"Converting {self.meta['dataset_name']}") keypoints = self.get_keypoints() - self.conversion_table = get_conversion_table( - keypoints=keypoints, table_path=table_path, table_dict=table_dict - ) + self.conversion_table = get_conversion_table(keypoints=keypoints, table_path=table_path, table_dict=table_dict) - self.generic_train_annotations = self._proj( - self.generic_train_annotations, self.conversion_table - ) + self.generic_train_annotations = self._proj(self.generic_train_annotations, self.conversion_table) - self.generic_test_annotations = self._proj( - self.generic_test_annotations, self.conversion_table - ) + self.generic_test_annotations = self._proj(self.generic_test_annotations, self.conversion_table) # all category id fixed to 1. So that it does not conflict with the background # category id diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/base_dlc.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/base_dlc.py index d816449e4a..b33822bba6 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/base_dlc.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/base_dlc.py @@ -19,7 +19,6 @@ class BaseDLCPoseDataset(BasePoseDataset): - def __init__(self, proj_root, dataset_name, shuffle=1, modelprefix=""): super(BaseDLCPoseDataset, self).__init__() @@ -104,12 +103,8 @@ def populate_generic(self): print(f"Before checking trainset {self.meta['dataset_name']}") - self.whether_anno_image_match( - self.generic_train_images, self.generic_train_annotations - ) + self.whether_anno_image_match(self.generic_train_images, self.generic_train_annotations) print(f"Before checking testset {self.meta['dataset_name']}") - self.whether_anno_image_match( - self.generic_test_images, self.generic_test_annotations - ) + self.whether_anno_image_match(self.generic_test_images, self.generic_test_annotations) diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/coco.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/coco.py index 7350a5fec0..79e10e46a4 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/coco.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/coco.py @@ -35,14 +35,10 @@ def __init__( self.train_json_obj = ( self._load_json(train_filename) if shuffle is None - else self._load_json( - train_filename.replace(".json", f"_shuffle{shuffle}.json") - ) + else self._load_json(train_filename.replace(".json", f"_shuffle{shuffle}.json")) ) self.test_json_obj = ( - self._load_json("test.json") - if shuffle is None - else self._load_json(f"test_shuffle{shuffle}.json") + self._load_json("test.json") if shuffle is None else self._load_json(f"test_shuffle{shuffle}.json") ) self.populate_generic() @@ -79,12 +75,8 @@ def populate_generic(self): print(f"Before checking trainset {self.meta['dataset_name']}") - self.whether_anno_image_match( - self.generic_train_images, self.generic_train_annotations - ) + self.whether_anno_image_match(self.generic_train_images, self.generic_train_annotations) print(f"Before checking testset {self.meta['dataset_name']}") - self.whether_anno_image_match( - self.generic_test_images, self.generic_test_annotations - ) + self.whether_anno_image_match(self.generic_test_images, self.generic_test_annotations) diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc.py index 4a13c14f37..902312dac9 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc.py @@ -24,9 +24,7 @@ class MaDLCPoseDataset(BaseDLCPoseDataset): def __init__(self, proj_root, dataset_name, shuffle=1, modelprefix=""): - super(MaDLCPoseDataset, self).__init__( - proj_root, dataset_name, shuffle=shuffle, modelprefix=modelprefix - ) + super(MaDLCPoseDataset, self).__init__(proj_root, dataset_name, shuffle=shuffle, modelprefix=modelprefix) def _df2generic(self, df, image_id_offset=0): @@ -36,15 +34,10 @@ def _df2generic(self, df, image_id_offset=0): if "single" in individuals: unique_bpts.extend( - df.xs("single", level="individuals", axis=1) - .columns.get_level_values("bodyparts") - .unique() + df.xs("single", level="individuals", axis=1).columns.get_level_values("bodyparts").unique() ) multi_bpts = ( - df.xs(individuals[0], level="individuals", axis=1) - .columns.get_level_values("bodyparts") - .unique() - .tolist() + df.xs(individuals[0], level="individuals", axis=1).columns.get_level_values("bodyparts").unique().tolist() ) coco_categories = [] @@ -83,19 +76,11 @@ def _df2generic(self, df, image_id_offset=0): for individual_id, individual in enumerate(individuals): category_id = 0 try: - kpts = ( - data.xs(individual, level="individuals") - .to_numpy() - .reshape((-1, 2)) - ) + kpts = data.xs(individual, level="individuals").to_numpy().reshape((-1, 2)) except: # somehow there are duplicates. So only use the first occurrence data = data.iloc[0] - kpts = ( - data.xs(individual, level="individuals") - .to_numpy() - .reshape((-1, 2)) - ) + kpts = data.xs(individual, level="individuals").to_numpy().reshape((-1, 2)) keypoints = np.zeros((len(kpts), 3)) diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc_dataframe.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc_dataframe.py index 0278d39a54..348a687a86 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc_dataframe.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc_dataframe.py @@ -38,9 +38,7 @@ def merge_annotateddatasets(cfg): videos = cfg["video_sets"].keys() video_filenames = parse_video_filenames(videos) for filename in video_filenames: - file_path = os.path.join( - data_path / filename, f'CollectedData_{cfg["scorer"]}.h5' - ) + file_path = os.path.join(data_path / filename, f"CollectedData_{cfg['scorer']}.h5") try: data = pd.read_hdf(file_path) conversioncode.guarantee_multiindex_rows(data) @@ -75,15 +73,12 @@ def merge_annotateddatasets(cfg): bodyparts = multianimalbodyparts + uniquebodyparts else: bodyparts = cfg["bodyparts"] - AnnotationData = AnnotationData.reindex( - bodyparts, axis=1, level=AnnotationData.columns.names.index("bodyparts") - ) + AnnotationData = AnnotationData.reindex(bodyparts, axis=1, level=AnnotationData.columns.names.index("bodyparts")) return AnnotationData class MaDLCDataFrame(BasePoseDataset): - def __init__(self, proj_root, dataset_name): super(MaDLCDataFrame, self).__init__() assert proj_root != None and dataset_name != None @@ -133,15 +128,11 @@ def populate_generic(self): print(f"Before checking trainset {self.meta['dataset_name']}") - self.whether_anno_image_match( - self.generic_train_images, self.generic_train_annotations - ) + self.whether_anno_image_match(self.generic_train_images, self.generic_train_annotations) print(f"Before checking testset {self.meta['dataset_name']}") - self.whether_anno_image_match( - self.generic_test_images, self.generic_test_annotations - ) + self.whether_anno_image_match(self.generic_test_images, self.generic_test_annotations) def _df2generic(self, df, image_id_offset=0): @@ -151,15 +142,10 @@ def _df2generic(self, df, image_id_offset=0): if "single" in individuals: unique_bpts.extend( - df.xs("single", level="individuals", axis=1) - .columns.get_level_values("bodyparts") - .unique() + df.xs("single", level="individuals", axis=1).columns.get_level_values("bodyparts").unique() ) multi_bpts = ( - df.xs(individuals[0], level="individuals", axis=1) - .columns.get_level_values("bodyparts") - .unique() - .tolist() + df.xs(individuals[0], level="individuals", axis=1).columns.get_level_values("bodyparts").unique().tolist() ) coco_categories = [] @@ -198,19 +184,11 @@ def _df2generic(self, df, image_id_offset=0): for individual_id, individual in enumerate(individuals): category_id = 0 try: - kpts = ( - data.xs(individual, level="individuals") - .to_numpy() - .reshape((-1, 2)) - ) + kpts = data.xs(individual, level="individuals").to_numpy().reshape((-1, 2)) except: # somehow there are duplicates. So only use the first occurrence data = data.iloc[0] - kpts = ( - data.xs(individual, level="individuals") - .to_numpy() - .reshape((-1, 2)) - ) + kpts = data.xs(individual, level="individuals").to_numpy().reshape((-1, 2)) keypoints = np.zeros((len(kpts), 3)) diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/materialize.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/materialize.py index 63211b8f35..78156ad9f3 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/materialize.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/materialize.py @@ -44,10 +44,8 @@ def modify_train_test_cfg(config_path, shuffle=1, modelprefix=""): # use dlcr net # use gradient masking # set batch size as 8 - trainposeconfigfile, testposeconfigfile, snapshotfolder = ( - compat.return_train_network_path( - config_path, shuffle=shuffle, modelprefix=modelprefix, trainingsetindex=0 - ) + trainposeconfigfile, testposeconfigfile, snapshotfolder = compat.return_train_network_path( + config_path, shuffle=shuffle, modelprefix=modelprefix, trainingsetindex=0 ) train_cfg = auxiliaryfunctions.read_plainconfig(trainposeconfigfile) @@ -111,9 +109,7 @@ def __init__(self): corer2move2 = [50, 50] move2corner = True identity = False - self.cfg = { - k: v for k, v in vars().items() if "__" not in k and "self" not in k - } + self.cfg = {k: v for k, v in vars().items() if "__" not in k and "self" not in k} def create_cfg(self, proj_root, kwargs): self.cfg.update(kwargs) @@ -162,9 +158,7 @@ def __init__(self): corer2move2 = [50, 50] move2corner = True identity = False - self.cfg = { - k: v for k, v in vars().items() if "__" not in k and "self" not in k - } + self.cfg = {k: v for k, v in vars().items() if "__" not in k and "self" not in k} def create_cfg(self, proj_root, kwargs): self.cfg.update(kwargs) @@ -203,17 +197,12 @@ def _generic2madlc( scorer = "maDLC_scorer" # this line is taken from dlc's multi animal dataset creation function - train_fraction = round( - len(train_images) * 1.0 / (len(train_images) + len(test_images)), 2 - ) + train_fraction = round(len(train_images) * 1.0 / (len(train_images) + len(test_images)), 2) # need to fake a video path # let's use individual dataset names as fake video name # merged_dataset_name = '_'.join(meta['mat_datasets']) - video_sets = { - f"{dataset_name}.mp4": {"crop": "0, 400, 0, 400"} - for dataset_name in meta["mat_datasets"] - } + video_sets = {f"{dataset_name}.mp4": {"crop": "0, 400, 0, 400"} for dataset_name in meta["mat_datasets"]} modify_dict = dict( Task=meta["dataset_name"], @@ -236,9 +225,7 @@ def _generic2madlc( imageid2datasetname = meta["imageid2datasetname"] for dataset_name in meta["mat_datasets"]: - os.makedirs( - os.path.join(proj_root, "labeled-data", dataset_name), exist_ok=True - ) + os.makedirs(os.path.join(proj_root, "labeled-data", dataset_name), exist_ok=True) # also, to make sure the split is right, we will have to pass the right indices @@ -283,13 +270,8 @@ def _generic2madlc( temp_count = 0 for dataset_name, dataset in meta["mat_datasets"].items(): - - dataset_total_images = ( - dataset.generic_train_images + dataset.generic_test_images - ) - dataset_total_annotations = ( - dataset.generic_train_annotations + dataset.generic_test_annotations - ) + dataset_total_images = dataset.generic_train_images + dataset.generic_test_images + dataset_total_annotations = dataset.generic_train_annotations + dataset.generic_test_annotations dataset_index = [] @@ -321,30 +303,18 @@ def _generic2madlc( # need to be careful here to assign right keypoints to right people if coord[0] > 0 and coord[1] > 0: # leave them to NaN if values are 0 - df.loc[file_name][ - scorer, f"individual{individual_id}", kpt_name, "x" - ] = coord[0] - df.loc[file_name][ - scorer, f"individual{individual_id}", kpt_name, "y" - ] = coord[1] + df.loc[file_name][scorer, f"individual{individual_id}", kpt_name, "x"] = coord[0] + df.loc[file_name][scorer, f"individual{individual_id}", kpt_name, "y"] = coord[1] elif coord[2] == -1: - df.loc[file_name][ - scorer, f"individual{individual_id}", kpt_name, "x" - ] = -1 - df.loc[file_name][ - scorer, f"individual{individual_id}", kpt_name, "y" - ] = -1 + df.loc[file_name][scorer, f"individual{individual_id}", kpt_name, "x"] = -1 + df.loc[file_name][scorer, f"individual{individual_id}", kpt_name, "y"] = -1 df.to_hdf( - os.path.join( - proj_root, "labeled-data", dataset_name, f"CollectedData_{scorer}.h5" - ), + os.path.join(proj_root, "labeled-data", dataset_name, f"CollectedData_{scorer}.h5"), key="df_with_missing", mode="w", ) # paf_graph default as None. But I am not sure how to do better - create_multianimaltraining_dataset( - os.path.join(proj_root, "config.yaml"), paf_graph=None - ) + create_multianimaltraining_dataset(os.path.join(proj_root, "config.yaml"), paf_graph=None) # dlc's merge_annotation messes up my indices, so I will need to overwrite the documentation file # I could have done it in a more elegant way if I could modify part of DLC source code, but for backward compatibility reasons, overriding documentation is smarter @@ -355,9 +325,7 @@ def _generic2madlc( train_folder = os.path.join(proj_root, auxiliaryfunctions.GetTrainingSetFolder(cfg)) - datafilename, metafilename = auxiliaryfunctions.GetDataandMetaDataFilenames( - train_folder, train_fraction, 1, cfg - ) + datafilename, metafilename = auxiliaryfunctions.GetDataandMetaDataFilenames(train_folder, train_fraction, 1, cfg) modify_train_test_cfg(config_path) @@ -387,14 +355,10 @@ def _filter(image): pickle.dump(parent_trace, f) trainIndices = [ - idx - for idx, image in enumerate(dlc_df.index) - if get_filename(image).split(os.sep)[-1] in _filter_train_images + idx for idx, image in enumerate(dlc_df.index) if get_filename(image).split(os.sep)[-1] in _filter_train_images ] testIndices = [ - idx - for idx, image in enumerate(dlc_df.index) - if get_filename(image).split(os.sep)[-1] in _filter_test_images + idx for idx, image in enumerate(dlc_df.index) if get_filename(image).split(os.sep)[-1] in _filter_test_images ] with open(metafilename, "rb") as f: @@ -422,7 +386,6 @@ def _filter(image): print(f"overwriting data file {datafilename}") with open(os.path.join(proj_root, datafilename), "wb") as f: - pickle.dump(data, f, pickle.HIGHEST_PROTOCOL) @@ -447,17 +410,12 @@ def _generic2sdlc( bodyparts = meta["categories"]["keypoints"] scorer = "singleDLC_scorer" - train_fraction = round( - len(train_images) * 1.0 / (len(train_images) + len(test_images)), 2 - ) + train_fraction = round(len(train_images) * 1.0 / (len(train_images) + len(test_images)), 2) # need to fake a video path # let's use individual dataset names as fake video name - video_sets = { - f"{dataset_name}.mp4": {"crop": "0, 400, 0, 400"} - for dataset_name in meta["mat_datasets"].keys() - } + video_sets = {f"{dataset_name}.mp4": {"crop": "0, 400, 0, 400"} for dataset_name in meta["mat_datasets"].keys()} modify_dict = dict( Task=meta["dataset_name"], @@ -474,13 +432,9 @@ def _generic2sdlc( imageid2datasetname = meta["imageid2datasetname"] for dataset_name in meta["mat_datasets"]: - os.makedirs( - os.path.join(proj_root, "labeled-data", dataset_name), exist_ok=True - ) + os.makedirs(os.path.join(proj_root, "labeled-data", dataset_name), exist_ok=True) - columnindex = pd.MultiIndex.from_product( - [[scorer], bodyparts, ["x", "y"]], names=["scorer", "bodyparts", "coords"] - ) + columnindex = pd.MultiIndex.from_product([[scorer], bodyparts, ["x", "y"]], names=["scorer", "bodyparts", "coords"]) total_images = train_images + test_images total_annotations = train_annotations + test_annotations @@ -527,13 +481,8 @@ def _generic2sdlc( # so we know where to put the next annotation if there are multiple individuals in that image for dataset_name, dataset in meta["mat_datasets"].items(): - - dataset_total_images = ( - dataset.generic_train_images + dataset.generic_test_images - ) - dataset_total_annotations = ( - dataset.generic_train_annotations + dataset.generic_test_annotations - ) + dataset_total_images = dataset.generic_train_images + dataset.generic_test_images + dataset_total_annotations = dataset.generic_train_annotations + dataset.generic_test_annotations dataset_index = [] freq = {} @@ -563,7 +512,6 @@ def _generic2sdlc( # need to be careful here to assign right keypoints to right people if coord[0] > 0 and coord[1] > 0: - df.loc[file_name][scorer, kpt_name, "x"] = coord[0] df.loc[file_name][scorer, kpt_name, "y"] = coord[1] elif coord[2] == -1: @@ -573,9 +521,7 @@ def _generic2sdlc( df = df.dropna(how="all") df.to_hdf( - os.path.join( - proj_root, "labeled-data", dataset_name, f"CollectedData_{scorer}.h5" - ), + os.path.join(proj_root, "labeled-data", dataset_name, f"CollectedData_{scorer}.h5"), key="df_with_missing", mode="w", ) @@ -591,9 +537,7 @@ def _generic2sdlc( train_folder = os.path.join(proj_root, auxiliaryfunctions.GetTrainingSetFolder(cfg)) - datafilename, metafilename = auxiliaryfunctions.GetDataandMetaDataFilenames( - train_folder, train_fraction, 1, cfg - ) + datafilename, metafilename = auxiliaryfunctions.GetDataandMetaDataFilenames(train_folder, train_fraction, 1, cfg) modify_train_test_cfg(config_path) @@ -623,14 +567,10 @@ def _filter(image): pickle.dump(parent_trace, f) trainIndices = [ - idx - for idx, image in enumerate(dlc_df.index) - if get_filename(image).split(os.sep)[-1] in _filter_train_images + idx for idx, image in enumerate(dlc_df.index) if get_filename(image).split(os.sep)[-1] in _filter_train_images ] testIndices = [ - idx - for idx, image in enumerate(dlc_df.index) - if get_filename(image).split(os.sep)[-1] in _filter_test_images + idx for idx, image in enumerate(dlc_df.index) if get_filename(image).split(os.sep)[-1] in _filter_test_images ] with open(metafilename, "rb") as f: @@ -645,9 +585,7 @@ def _filter(image): # need to overwrite the true data file too nbodyparts = len(bodyparts) - data, MatlabData = format_single_training_data( - dlc_df, trainIndices, nbodyparts, cfg["project_path"] - ) + data, MatlabData = format_single_training_data(dlc_df, trainIndices, nbodyparts, cfg["project_path"]) print(f"overwriting data file {datafilename}") @@ -748,19 +686,10 @@ def _generic2coco( image["file_name"] = file_name lookuptable[dest] = src - train_annotations = [ - train_anno - for train_anno in train_annotations - if train_anno["image_id"] not in broken_links - ] - test_annotations = [ - test_anno - for test_anno in test_annotations - if test_anno["image_id"] not in broken_links - ] + train_annotations = [train_anno for train_anno in train_annotations if train_anno["image_id"] not in broken_links] + test_annotations = [test_anno for test_anno in test_annotations if test_anno["image_id"] not in broken_links] with open(os.path.join(proj_root, "annotations", "train.json"), "w") as f: - train_json_obj = dict( images=train_images, annotations=train_annotations, diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/multi.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/multi.py index 1b24a7a2b7..a904233354 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/multi.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/multi.py @@ -39,7 +39,6 @@ def __init__(self, dataset_name, datasets, table_path): names = [] for dataset in datasets: - # Must project datasets to same keypoint space before merging if table_path != None: dataset.project_with_conversion_table(table_path) @@ -70,12 +69,8 @@ def summary(self): print(f"Summary of dataset {self.dataset_name}") print("Decomposition of multi source datasets:") for dataset_name, dataset in self.name2genericdataset.items(): - n_images = len(dataset.generic_train_images) + len( - dataset.generic_test_images - ) - n_annotations = len(dataset.generic_train_annotations) + len( - dataset.generic_test_annotations - ) + n_images = len(dataset.generic_train_images) + len(dataset.generic_test_images) + n_annotations = len(dataset.generic_train_annotations) + len(dataset.generic_test_annotations) print(f"{dataset_name} has {n_images} images, {n_annotations} annotations") print(f"total train images : {len(self.train_images)}") @@ -144,30 +139,20 @@ def _update_imgids(self): total_number_images = 0 total_number_annotations = 0 for dataset in all_datasets: - total_number_images += len(dataset.generic_train_images) + len( - dataset.generic_test_images - ) - total_number_annotations += len(dataset.generic_train_annotations) + len( - dataset.generic_test_annotations - ) + total_number_images += len(dataset.generic_train_images) + len(dataset.generic_test_images) + total_number_annotations += len(dataset.generic_train_annotations) + len(dataset.generic_test_annotations) global_image_id_pool = set(range(total_number_images)) global_annotation_id_pool = set(range(total_number_annotations)) for dataset_name, dataset in self.name2genericdataset.items(): - local_image_id_map = defaultdict(int) local_anno_id_map = defaultdict(int) - traintest_images = ( - dataset.generic_train_images + dataset.generic_test_images - ) - traintest_annotations = ( - dataset.generic_train_annotations + dataset.generic_test_annotations - ) + traintest_images = dataset.generic_train_images + dataset.generic_test_images + traintest_annotations = dataset.generic_train_annotations + dataset.generic_test_annotations for img in traintest_images: - new_image_id = global_image_id_pool.pop() local_image_id_map[img["id"]] = new_image_id img["id"] = new_image_id @@ -204,7 +189,6 @@ def _merge_datasets(self, name2dataset): merged_test_annotations = [] for dataset_name, dataset in name2dataset.items(): - train_images = dataset.generic_train_images test_images = dataset.generic_test_images train_annotations = dataset.generic_train_annotations @@ -218,13 +202,9 @@ def _merge_datasets(self, name2dataset): print("Checking merged dataset") merged_traintest_images = merged_train_images + merged_test_images - merged_traintest_annotations = ( - merged_train_annotations + merged_test_annotations - ) + merged_traintest_annotations = merged_train_annotations + merged_test_annotations - self.whether_anno_image_match( - merged_traintest_images, merged_traintest_annotations - ) + self.whether_anno_image_match(merged_traintest_images, merged_traintest_annotations) return ( merged_train_images, @@ -236,22 +216,17 @@ def _merge_datasets(self, name2dataset): def __eq__(self, other_dataset): if isinstance(other_dataset, BasePoseDataset): - train_images1 = set(map(raw_2_imagename_with_id, self.train_images)) - train_images2 = set( - map(raw_2_imagename, other_dataset.generic_train_images) - ) + train_images2 = set(map(raw_2_imagename, other_dataset.generic_train_images)) test_images1 = set(map(raw_2_imagename_with_id, self.test_images)) test_images2 = set(map(raw_2_imagename, other_dataset.generic_test_images)) if train_images1 == train_images2 and test_images1 == test_images2: - print( - f'dataset {self.meta["dataset_name"]} and {other_dataset.meta["dataset_name"]} are equivalent' - ) + print(f"dataset {self.meta['dataset_name']} and {other_dataset.meta['dataset_name']} are equivalent") return True else: print( - f'dataset {self.meta["dataset_name"]} and {other_dataset.meta["dataset_name"]} are NOT equivalent' + f"dataset {self.meta['dataset_name']} and {other_dataset.meta['dataset_name']} are NOT equivalent" ) return False diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc.py index 8d7b419654..377a362883 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc.py @@ -29,9 +29,7 @@ class SingleDLCPoseDataset(BaseDLCPoseDataset): """ def __init__(self, proj_root, dataset_name, shuffle=1, modelprefix=""): - super(SingleDLCPoseDataset, self).__init__( - proj_root, dataset_name, shuffle=shuffle, modelprefix=modelprefix - ) + super(SingleDLCPoseDataset, self).__init__(proj_root, dataset_name, shuffle=shuffle, modelprefix=modelprefix) # overriding max_individuals self.meta["max_individuals"] = 1 @@ -107,7 +105,6 @@ def _df2generic(self, df, image_id_offset=0): "iscrowd": 0, } if np.sum(keypoints) != 0: - coco_annotations.append(annotation) # I think width and height are important diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc_dataframe.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc_dataframe.py index e6e8fd5828..8a47185e2d 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc_dataframe.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc_dataframe.py @@ -38,9 +38,7 @@ def merge_annotateddatasets(cfg): videos = cfg["video_sets"].keys() video_filenames = parse_video_filenames(videos) for filename in video_filenames: - file_path = os.path.join( - data_path / filename, f'CollectedData_{cfg["scorer"]}.h5' - ) + file_path = os.path.join(data_path / filename, f"CollectedData_{cfg['scorer']}.h5") try: data = pd.read_hdf(file_path) conversioncode.guarantee_multiindex_rows(data) @@ -75,15 +73,12 @@ def merge_annotateddatasets(cfg): bodyparts = multianimalbodyparts + uniquebodyparts else: bodyparts = cfg["bodyparts"] - AnnotationData = AnnotationData.reindex( - bodyparts, axis=1, level=AnnotationData.columns.names.index("bodyparts") - ) + AnnotationData = AnnotationData.reindex(bodyparts, axis=1, level=AnnotationData.columns.names.index("bodyparts")) return AnnotationData class SingleDLCDataFrame(BasePoseDataset): - def __init__(self, proj_root, dataset_name): super(SingleDLCDataFrame, self).__init__() self.meta["max_individuals"] = 1 @@ -134,15 +129,11 @@ def populate_generic(self): print(f"Before checking trainset {self.meta['dataset_name']}") - self.whether_anno_image_match( - self.generic_train_images, self.generic_train_annotations - ) + self.whether_anno_image_match(self.generic_train_images, self.generic_train_annotations) print(f"Before checking testset {self.meta['dataset_name']}") - self.whether_anno_image_match( - self.generic_test_images, self.generic_test_annotations - ) + self.whether_anno_image_match(self.generic_test_images, self.generic_test_annotations) def _df2generic(self, df, image_id_offset=0): @@ -215,7 +206,6 @@ def _df2generic(self, df, image_id_offset=0): "iscrowd": 0, } if np.sum(keypoints) != 0: - coco_annotations.append(annotation) # I think width and height are important diff --git a/deeplabcut/modelzoo/generalized_data_converter/utils.py b/deeplabcut/modelzoo/generalized_data_converter/utils.py index 3d9cfb4f45..948e49af53 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/utils.py +++ b/deeplabcut/modelzoo/generalized_data_converter/utils.py @@ -72,9 +72,7 @@ def create_dummy_config_file_from_h5( labeled_folders = [f.split("/")[-1] for f in pattern] - video_sets = { - f"{folder}.mp4": {"crop": "0, 400, 0, 400"} for folder in labeled_folders - } + video_sets = {f"{folder}.mp4": {"crop": "0, 400, 0, 400"} for folder in labeled_folders} # bodyparts = df[scorer]['bodyparts'] @@ -109,7 +107,6 @@ def create_dummy_config_file_from_pickle( cfg_template = SingleDLC_config() with open(reference_pickle, "rb") as f: - pickle_obj = pickle.load(f) # bodyparts = pickle_obj['keypoint_names'] @@ -145,7 +142,6 @@ def create_dummy_config_file_from_pickle( def create_video_h5_from_pickle(proj_root, cfg, reference_pickle, videopath): with open(reference_pickle, "rb") as f: - pickle_obj = pickle.load(f) # bodyparts = pickle_obj['keypoint_names'] @@ -189,9 +185,7 @@ def create_video_h5_from_pickle(proj_root, cfg, reference_pickle, videopath): df = pd.DataFrame(data, columns=columnindex, index=imagenames) for imagename, kpts in zip(imagenames, detections): - for kpt_id, kpt_name in enumerate(keypoint_names): - df.loc[imagename][scorer, kpt_name, "x"] = kpts[kpt_id, 0] df.loc[imagename][scorer, kpt_name, "y"] = kpts[kpt_id, 1] df.loc[imagename][scorer, kpt_name, "likelihood"] = kpts[kpt_id, 2] @@ -294,7 +288,6 @@ def customized_colormap(config_path): visited = set() for kpt_id in range(len(bodyparts)): - bodypart = bodyparts[kpt_id] if "left" in bodypart: ref_color = colors[kpt_id] @@ -320,5 +313,4 @@ def create_modelprefix(modelprefix): if __name__ == "__main__": - customized_colormap("hei") diff --git a/deeplabcut/modelzoo/utils.py b/deeplabcut/modelzoo/utils.py index 216c53a555..79d739d254 100644 --- a/deeplabcut/modelzoo/utils.py +++ b/deeplabcut/modelzoo/utils.py @@ -78,9 +78,7 @@ def get_super_animal_scorer( The DLC scorer name to use for the given SuperAnimal models. """ if detector_snapshot_path is not None and torchvision_detector_name is not None: - raise ValueError( - "Provide only one of `detector_snapshot_path` or `torchvision_detector_name`, not both." - ) + raise ValueError("Provide only one of `detector_snapshot_path` or `torchvision_detector_name`, not both.") super_animal_prefix = super_animal + "_" # Always use model name first model_name = Path(model_snapshot_path).stem @@ -218,25 +216,14 @@ def parse_project_model_name(superanimal_name: str) -> tuple[str, str]: dlc_root_path = get_deeplabcut_path() modelzoo_path = os.path.join(dlc_root_path, "modelzoo") - available_model_configs = glob( - os.path.join(modelzoo_path, "model_configs", "*.yaml") - ) - available_models = [ - os.path.splitext(os.path.basename(path))[0] for path in available_model_configs - ] + available_model_configs = glob(os.path.join(modelzoo_path, "model_configs", "*.yaml")) + available_models = [os.path.splitext(os.path.basename(path))[0] for path in available_model_configs] if model_name not in available_models: - raise ValueError( - f"Model {model_name} not found. Available models are: {available_models}" - ) + raise ValueError(f"Model {model_name} not found. Available models are: {available_models}") - available_project_configs = glob( - os.path.join(modelzoo_path, "project_configs", "*.yaml") - ) - available_projects = [ - os.path.splitext(os.path.basename(path))[0] - for path in available_project_configs - ] + available_project_configs = glob(os.path.join(modelzoo_path, "project_configs", "*.yaml")) + available_projects = [os.path.splitext(os.path.basename(path))[0] for path in available_project_configs] return project_name, model_name @@ -398,17 +385,11 @@ def get_superanimal_colormaps(): ) superanimal_colormaps = { - "superanimal_bird": ListedColormap( - list(superanimal_bird_colors), name="superanimal_bird" - ), + "superanimal_bird": ListedColormap(list(superanimal_bird_colors), name="superanimal_bird"), "superanimal_topviewmouse": ListedColormap( list(superanimal_topviewmouse_colors), name="superanimal_topviewmouse" ), - "superanimal_quadruped": ListedColormap( - list(superanimal_quadruped_colors), name="superanimal_quadruped" - ), - "superanimal_humanbody": ListedColormap( - list(superanimal_humanbody_colors), name="superanimal_humanbody" - ), + "superanimal_quadruped": ListedColormap(list(superanimal_quadruped_colors), name="superanimal_quadruped"), + "superanimal_humanbody": ListedColormap(list(superanimal_humanbody_colors), name="superanimal_humanbody"), } return superanimal_colormaps diff --git a/deeplabcut/modelzoo/video_inference.py b/deeplabcut/modelzoo/video_inference.py index 90e082880c..a0bb141567 100644 --- a/deeplabcut/modelzoo/video_inference.py +++ b/deeplabcut/modelzoo/video_inference.py @@ -320,9 +320,7 @@ def video_inference_superanimal( print(f"Running video inference on {videos} with {superanimal_name}_{model_name}") dlc_root_path = get_deeplabcut_path() modelzoo_path = os.path.join(dlc_root_path, "modelzoo") - available_architectures = json.load( - open(os.path.join(modelzoo_path, "models_to_framework.json"), "r") - ) + available_architectures = json.load(open(os.path.join(modelzoo_path, "models_to_framework.json"), "r")) framework = available_architectures[model_name] print(f"Using {framework} for model {model_name}") if framework == "tensorflow": @@ -332,9 +330,7 @@ def video_inference_superanimal( weight_folder = get_snapshot_folder_path() / f"{superanimal_name}_{model_name}" if not weight_folder.exists(): - download_huggingface_model( - superanimal_name, target_dir=str(weight_folder), rename_mapping=None - ) + download_huggingface_model(superanimal_name, target_dir=str(weight_folder), rename_mapping=None) if isinstance(videos, str): videos = [videos] @@ -354,9 +350,7 @@ def video_inference_superanimal( elif framework == "pytorch": torchvision_detector_name = None if superanimal_name != "superanimal_humanbody" and detector_name is None: - raise ValueError( - "You have to specify a detector_name when using the Pytorch framework." - ) + raise ValueError("You have to specify a detector_name when using the Pytorch framework.") elif superanimal_name == "superanimal_humanbody": if detector_name: torchvision_detector_name = detector_name @@ -373,11 +367,7 @@ def video_inference_superanimal( config = load_super_animal_config( super_animal=superanimal_name, model_name=model_name, - detector_name=( - detector_name - if superanimal_name != "superanimal_humanbody" - else None - ), + detector_name=(detector_name if superanimal_name != "superanimal_humanbody" else None), ) pose_model_path = customized_pose_checkpoint @@ -440,16 +430,11 @@ def video_inference_superanimal( print(f"{image_folder} exists, skipping the frame extraction") else: image_folder.mkdir() - print( - f"Video frames being extracted to {image_folder} for video " - f"adaptation." - ) + print(f"Video frames being extracted to {image_folder} for video adaptation.") video_to_frames(video_path, pseudo_dataset_folder, cropping=cropping) anno_folder = pseudo_dataset_folder / "annotations" - if (anno_folder / "train.json").exists() and ( - anno_folder / "test.json" - ).exists(): + if (anno_folder / "train.json").exists() and (anno_folder / "test.json").exists(): print( f"{anno_folder} exists, skipping the annotation construction. " f"Delete the folder if you want to re-construct pseudo annotations" @@ -483,9 +468,7 @@ def video_inference_superanimal( if superanimal_name != "superanimal_humanbody": detector_snapshot_prefix = f"snapshot-{detector_name}" - config["detector"]["runner"][ - "snapshot_prefix" - ] = detector_snapshot_prefix + config["detector"]["runner"]["snapshot_prefix"] = detector_snapshot_prefix # the model config's parameters need to be updated for adaptation training model_config_path = model_folder / "pytorch_config.yaml" @@ -496,21 +479,16 @@ def video_inference_superanimal( # get the current epoch of the pose model current_pose_epoch = get_checkpoint_epoch(pose_model_path) # update the checkpoint path with the current epoch, if the checkpoint does not exist, use the best checkpoint - adapted_pose_checkpoint = ( - model_folder - / f"{model_snapshot_prefix}-{current_pose_epoch + pose_epochs:03}.pt" - ) + adapted_pose_checkpoint = model_folder / f"{model_snapshot_prefix}-{current_pose_epoch + pose_epochs:03}.pt" if not Path(adapted_pose_checkpoint).exists(): adapted_pose_checkpoint = ( - model_folder - / f"{model_snapshot_prefix}-best-{current_pose_epoch + pose_epochs:03}.pt" + model_folder / f"{model_snapshot_prefix}-best-{current_pose_epoch + pose_epochs:03}.pt" ) if superanimal_name != "superanimal_humanbody": current_detector_epoch = get_checkpoint_epoch(detector_path) adapted_detector_checkpoint = ( - model_folder - / f"{detector_snapshot_prefix}-{current_detector_epoch + detector_epochs:03}.pt" + model_folder / f"{detector_snapshot_prefix}-{current_detector_epoch + detector_epochs:03}.pt" ) if not Path(adapted_detector_checkpoint).exists(): adapted_detector_checkpoint = ( @@ -519,8 +497,7 @@ def video_inference_superanimal( ) if ( - superanimal_name == "superanimal_humanbody" - or adapted_detector_checkpoint.exists() + superanimal_name == "superanimal_humanbody" or adapted_detector_checkpoint.exists() ) and adapted_pose_checkpoint.exists(): snapshots_msg = f"pose ({adapted_pose_checkpoint})" if superanimal_name != "superanimal_humanbody": @@ -537,13 +514,8 @@ def video_inference_superanimal( " (pose) save_epochs: 1\n" ) if superanimal_name != "superanimal_humanbody": - params_msg += ( - f" detector_epochs: {detector_epochs}\n" - " detector_save_epochs: 1\n" - ) - print( - "Running video adaptation with following parameters:\n" + params_msg - ) + params_msg += f" detector_epochs: {detector_epochs}\n detector_save_epochs: 1\n" + print("Running video adaptation with following parameters:\n" + params_msg) train_file = pseudo_dataset_folder / "annotations" / "train.json" with open(train_file, "r") as f: @@ -551,16 +523,11 @@ def video_inference_superanimal( annotations = temp_obj["annotations"] if len(annotations) == 0: - print( - f"No valid predictions from {str(video_path)}. Check the " - "quality of the video" - ) + print(f"No valid predictions from {str(video_path)}. Check the quality of the video") return if superanimal_name == "superanimal_humanbody": - print( - "Warning, with the superanimal_humanbody type, only the pose model is adapted" - ) + print("Warning, with the superanimal_humanbody type, only the pose model is adapted") adaptation_train( project_root=pseudo_dataset_folder, @@ -581,21 +548,16 @@ def video_inference_superanimal( ) # after video adaptation, re-update the adapted checkpoint path, if the checkpoint does not exist, use the best checkpoint - adapted_pose_checkpoint = ( - model_folder - / f"{model_snapshot_prefix}-{current_pose_epoch + pose_epochs:03}.pt" - ) + adapted_pose_checkpoint = model_folder / f"{model_snapshot_prefix}-{current_pose_epoch + pose_epochs:03}.pt" if not Path(adapted_pose_checkpoint).exists(): adapted_pose_checkpoint = ( - model_folder - / f"{model_snapshot_prefix}-best-{current_pose_epoch + pose_epochs:03}.pt" + model_folder / f"{model_snapshot_prefix}-best-{current_pose_epoch + pose_epochs:03}.pt" ) pose_model_path = adapted_pose_checkpoint if superanimal_name != "superanimal_humanbody": adapted_detector_checkpoint = ( - model_folder - / f"{detector_snapshot_prefix}-{current_detector_epoch + detector_epochs:03}.pt" + model_folder / f"{detector_snapshot_prefix}-{current_detector_epoch + detector_epochs:03}.pt" ) if not Path(adapted_detector_checkpoint).exists(): adapted_detector_checkpoint = ( diff --git a/deeplabcut/modelzoo/webapp/inference.py b/deeplabcut/modelzoo/webapp/inference.py index 806e7778f1..ae1ab4bb24 100644 --- a/deeplabcut/modelzoo/webapp/inference.py +++ b/deeplabcut/modelzoo/webapp/inference.py @@ -103,14 +103,8 @@ def predict(self, frames: Dict[str, np.array]): bbox_predictions = self.models.detector_runner.inference(images=input_images) input_images = list(zip(input_images, bbox_predictions)) predictions = self.models.pose_runner.inference(images=input_images) - predictions = [ - {("markers" if k == "bodyparts" else k): v for k, v in d.items()} - for d in predictions - ] - predictions = [ - {**item[1], "image_path": item[0]} - for item in zip(frames.keys(), predictions) - ] + predictions = [{("markers" if k == "bodyparts" else k): v for k, v in d.items()} for d in predictions] + predictions = [{**item[1], "image_path": item[0]} for item in zip(frames.keys(), predictions)] responses = { "joint_names": self.config["bodyparts"], "predictions": predictions, diff --git a/deeplabcut/modelzoo/weight_initialization.py b/deeplabcut/modelzoo/weight_initialization.py index 7751998696..e5bf7c7fae 100644 --- a/deeplabcut/modelzoo/weight_initialization.py +++ b/deeplabcut/modelzoo/weight_initialization.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Functions to build weight initialization parameters for SuperAnimal models""" + from pathlib import Path import deeplabcut.modelzoo.utils as utils diff --git a/deeplabcut/pose_estimation_3d/camera_calibration.py b/deeplabcut/pose_estimation_3d/camera_calibration.py index 10b89f1301..d3c1e90e0a 100644 --- a/deeplabcut/pose_estimation_3d/camera_calibration.py +++ b/deeplabcut/pose_estimation_3d/camera_calibration.py @@ -26,9 +26,7 @@ matplotlib_axes_logger.setLevel("ERROR") -def calibrate_cameras( - config, cbrow=8, cbcol=6, calibrate=False, alpha=0.4, search_window_size=(11, 11) -): +def calibrate_cameras(config, cbrow=8, cbcol=6, calibrate=False, alpha=0.4, search_window_size=(11, 11)): """This function extracts the corners points from the calibration images, calibrates the camera and stores the calibration files in the project folder (defined in the config file). Make sure you have around 20-60 pairs of calibration images. The function should be used iteratively to select the right set of calibration images. @@ -94,13 +92,9 @@ def calibrate_cameras( # update the variable snapshot* in config file according to the name of the cameras try: for i in range(len(cam_names)): - cfg_3d[str("config_file_" + cam_names[i])] = cfg_3d.pop( - str("config_file_camera-" + str(i + 1)) - ) + cfg_3d[str("config_file_" + cam_names[i])] = cfg_3d.pop(str("config_file_camera-" + str(i + 1))) for i in range(len(cam_names)): - cfg_3d[str("shuffle_" + cam_names[i])] = cfg_3d.pop( - str("shuffle_camera-" + str(i + 1)) - ) + cfg_3d[str("shuffle_" + cam_names[i])] = cfg_3d.pop(str("shuffle_camera-" + str(i + 1))) except: pass @@ -143,15 +137,11 @@ def calibrate_cameras( if ret == True: img_shape[cam] = gray.shape[::-1] objpoints[cam].append(objp) - corners = cv2.cornerSubPix( - gray, corners, search_window_size, (-1, -1), criteria - ) + corners = cv2.cornerSubPix(gray, corners, search_window_size, (-1, -1), criteria) imgpoints[cam].append(corners) # Draw the corners and store the images img = cv2.drawChessboardCorners(img, (cbcol, cbrow), corners, ret) - cv2.imwrite( - os.path.join(str(path_corners), filename + "_corner.jpg"), img - ) + cv2.imwrite(os.path.join(str(path_corners), filename + "_corner.jpg"), img) else: print("Corners not found for the image %s" % Path(fname).name) for new_cam in cam_names: @@ -200,17 +190,10 @@ def calibrate_cameras( # Compute mean re-projection errors for individual cameras mean_error = 0 for i in range(len(objpoints[cam])): - imgpoints_proj, _ = cv2.projectPoints( - objpoints[cam][i], rvecs[i], tvecs[i], mtx, dist - ) - error = cv2.norm(imgpoints[cam][i], imgpoints_proj, cv2.NORM_L2) / len( - imgpoints_proj - ) + imgpoints_proj, _ = cv2.projectPoints(objpoints[cam][i], rvecs[i], tvecs[i], mtx, dist) + error = cv2.norm(imgpoints[cam][i], imgpoints_proj, cv2.NORM_L2) / len(imgpoints_proj) mean_error += error - print( - "Mean re-projection error for %s images: %.3f pixels " - % (cam, mean_error / len(objpoints[cam])) - ) + print("Mean re-projection error for %s images: %.3f pixels " % (cam, mean_error / len(objpoints[cam]))) # Compute stereo calibration for each pair of cameras camera_pair = [[cam_names[0], cam_names[1]]] @@ -275,12 +258,8 @@ def calibrate_cameras( % str(os.path.join(path_camera_matrix)) ) - auxiliaryfunctions.write_pickle( - os.path.join(path_camera_matrix, "stereo_params.pickle"), stereo_params - ) - print( - "Camera calibration done! Use the function ``check_undistortion`` to check the check the calibration" - ) + auxiliaryfunctions.write_pickle(os.path.join(path_camera_matrix, "stereo_params.pickle"), stereo_params) + print("Camera calibration done! Use the function ``check_undistortion`` to check the check the calibration") else: print( "Corners extracted! You may check for the extracted corners in the directory %s and remove the pair of images where the corners are incorrectly detected. If all the corners are detected correctly with right order, then re-run the same function and use the flag ``calibrate=True``, to calbrate the camera." @@ -346,9 +325,7 @@ def check_undistortion(config, cbrow=8, cbcol=6, plot=True): gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) """ camera_pair = [[cam_names[0], cam_names[1]]] - stereo_params = auxiliaryfunctions.read_pickle( - os.path.join(path_camera_matrix, "stereo_params.pickle") - ) + stereo_params = auxiliaryfunctions.read_pickle(os.path.join(path_camera_matrix, "stereo_params.pickle")) for pair in camera_pair: map1_x, map1_y = cv2.initUndistortRectifyMap( @@ -377,17 +354,13 @@ def check_undistortion(config, cbrow=8, cbcol=6, plot=True): gray1 = cv2.cvtColor(img1, cv2.COLOR_BGR2GRAY) h, w = img1.shape[:2] _, corners1 = cv2.findChessboardCorners(gray1, (cbcol, cbrow), None) - corners_origin1 = cv2.cornerSubPix( - gray1, corners1, (11, 11), (-1, -1), criteria - ) + corners_origin1 = cv2.cornerSubPix(gray1, corners1, (11, 11), (-1, -1), criteria) # Remapping dataFrame_camera1_undistort im_remapped1 = cv2.remap(img1, map1_x, map1_y, cv2.INTER_LANCZOS4) imgpoints_proj_undistort = cv2.undistortPoints( src=corners_origin1, - cameraMatrix=stereo_params[pair[0] + "-" + pair[1]][ - "cameraMatrix1" - ], + cameraMatrix=stereo_params[pair[0] + "-" + pair[1]]["cameraMatrix1"], distCoeffs=stereo_params[pair[0] + "-" + pair[1]]["distCoeffs1"], P=stereo_params[pair[0] + "-" + pair[1]]["P1"], R=stereo_params[pair[0] + "-" + pair[1]]["R1"], @@ -405,17 +378,13 @@ def check_undistortion(config, cbrow=8, cbcol=6, plot=True): gray2 = cv2.cvtColor(img2, cv2.COLOR_BGR2GRAY) h, w = img2.shape[:2] _, corners2 = cv2.findChessboardCorners(gray2, (cbcol, cbrow), None) - corners_origin2 = cv2.cornerSubPix( - gray2, corners2, (11, 11), (-1, -1), criteria - ) + corners_origin2 = cv2.cornerSubPix(gray2, corners2, (11, 11), (-1, -1), criteria) # Remapping im_remapped2 = cv2.remap(img2, map2_x, map2_y, cv2.INTER_LANCZOS4) imgpoints_proj_undistort2 = cv2.undistortPoints( src=corners_origin2, - cameraMatrix=stereo_params[pair[0] + "-" + pair[1]][ - "cameraMatrix2" - ], + cameraMatrix=stereo_params[pair[0] + "-" + pair[1]]["cameraMatrix2"], distCoeffs=stereo_params[pair[0] + "-" + pair[1]]["distCoeffs2"], P=stereo_params[pair[0] + "-" + pair[1]]["P2"], R=stereo_params[pair[0] + "-" + pair[1]]["R2"], @@ -430,9 +399,7 @@ def check_undistortion(config, cbrow=8, cbcol=6, plot=True): cam1_undistort = np.array(cam1_undistort) cam2_undistort = np.array(cam2_undistort) print("All images are undistorted and stored in %s" % str(path_undistort)) - print( - "Use the function ``triangulate`` to undistort the dataframes and compute the triangulation" - ) + print("Use the function ``triangulate`` to undistort the dataframes and compute the triangulation") if plot == True: f1, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 10)) @@ -450,9 +417,7 @@ def check_undistortion(config, cbrow=8, cbcol=6, plot=True): # Plot the undistorted corner points f2, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 10)) - f2.suptitle( - "Undistorted corner points on camera-1 and camera-2", fontsize=25 - ) + f2.suptitle("Undistorted corner points on camera-1 and camera-2", fontsize=25) ax1.imshow(cv2.cvtColor(im_remapped1, cv2.COLOR_BGR2RGB)) ax2.imshow(cv2.cvtColor(im_remapped2, cv2.COLOR_BGR2RGB)) for i in range(0, cam1_undistort.shape[1]): @@ -475,14 +440,12 @@ def check_undistortion(config, cbrow=8, cbcol=6, plot=True): plt.savefig(os.path.join(str(path_undistort), "undistorted_points.png")) # Triangulate - triangulate = ( - auxiliaryfunctions_3d.compute_triangulation_calibration_images( - stereo_params[pair[0] + "-" + pair[1]], - cam1_undistort, - cam2_undistort, - path_undistort, - cfg_3d, - plot=True, - ) + triangulate = auxiliaryfunctions_3d.compute_triangulation_calibration_images( + stereo_params[pair[0] + "-" + pair[1]], + cam1_undistort, + cam2_undistort, + path_undistort, + cfg_3d, + plot=True, ) auxiliaryfunctions.write_pickle("triangulate.pickle", triangulate) diff --git a/deeplabcut/pose_estimation_3d/plotting3D.py b/deeplabcut/pose_estimation_3d/plotting3D.py index f534e74964..0232d62f34 100644 --- a/deeplabcut/pose_estimation_3d/plotting3D.py +++ b/deeplabcut/pose_estimation_3d/plotting3D.py @@ -169,12 +169,8 @@ def create_labeled_video_3d( pickle_file = triangulate_file.replace(string_to_remove, "_meta.pickle") metadata_ = auxiliaryfunctions_3d.LoadMetadata3d(pickle_file) - base_filename_cam1 = str(Path(file[1]).stem).split(videotype)[ - 0 - ] # required for searching the filtered file - base_filename_cam2 = str(Path(file[2]).stem).split(videotype)[ - 0 - ] # required for searching the filtered file + base_filename_cam1 = str(Path(file[1]).stem).split(videotype)[0] # required for searching the filtered file + base_filename_cam2 = str(Path(file[2]).stem).split(videotype)[0] # required for searching the filtered file cam1_view_video = file[1] cam2_view_video = file[2] cam1_scorer = metadata_["scorer_name"][cam_names[0]] @@ -199,9 +195,7 @@ def create_labeled_video_3d( glob.glob( os.path.join( path_h5_file, - str( - "*" + base_filename_cam1 + cam1_scorer + "*filtered.h5" - ), + str("*" + base_filename_cam1 + cam1_scorer + "*filtered.h5"), ) )[0] ) @@ -209,9 +203,7 @@ def create_labeled_video_3d( glob.glob( os.path.join( path_h5_file, - str( - "*" + base_filename_cam2 + cam2_scorer + "*filtered.h5" - ), + str("*" + base_filename_cam2 + cam2_scorer + "*filtered.h5"), ) )[0] ) @@ -228,29 +220,17 @@ def create_labeled_video_3d( ), ) except IndexError: - print( - "No filtered predictions found, the unfiltered predictions will be used instead." - ) + print("No filtered predictions found, the unfiltered predictions will be used instead.") df_cam1 = pd.read_hdf( - glob.glob( - os.path.join( - path_h5_file, str(base_filename_cam1 + cam1_scorer + "*.h5") - ) - )[0] + glob.glob(os.path.join(path_h5_file, str(base_filename_cam1 + cam1_scorer + "*.h5")))[0] ) df_cam2 = pd.read_hdf( - glob.glob( - os.path.join( - path_h5_file, str(base_filename_cam2 + cam2_scorer + "*.h5") - ) - )[0] + glob.glob(os.path.join(path_h5_file, str(base_filename_cam2 + cam2_scorer + "*.h5")))[0] ) df_3d = pd.read_hdf(triangulate_file) try: - num_animals = ( - df_3d.columns.get_level_values("individuals").unique().size - ) + num_animals = df_3d.columns.get_level_values("individuals").unique().size except KeyError: num_animals = 1 @@ -263,26 +243,14 @@ def create_labeled_video_3d( output_folder.mkdir(parents=True, exist_ok=True) # Flatten the list of bodyparts to connect - bodyparts2plot = list( - np.unique([val for sublist in bodyparts2connect for val in sublist]) - ) + bodyparts2plot = list(np.unique([val for sublist in bodyparts2connect for val in sublist])) # Format data mask2d = df_cam1.columns.get_level_values("bodyparts").isin(bodyparts2plot) - xy1 = ( - df_cam1.iloc[: len(df_3d)] - .loc[:, mask2d] - .to_numpy() - .reshape((len(df_3d), -1, 3)) - ) + xy1 = df_cam1.iloc[: len(df_3d)].loc[:, mask2d].to_numpy().reshape((len(df_3d), -1, 3)) visible1 = xy1[..., 2] >= pcutoff xy1[~visible1] = np.nan - xy2 = ( - df_cam2.iloc[: len(df_3d)] - .loc[:, mask2d] - .to_numpy() - .reshape((len(df_3d), -1, 3)) - ) + xy2 = df_cam2.iloc[: len(df_3d)].loc[:, mask2d].to_numpy().reshape((len(df_3d), -1, 3)) visible2 = xy2[..., 2] >= pcutoff xy2[~visible2] = np.nan mask = df_3d.columns.get_level_values("bodyparts").isin(bodyparts2plot) diff --git a/deeplabcut/pose_estimation_3d/triangulation.py b/deeplabcut/pose_estimation_3d/triangulation.py index a0ebf98a16..4a32bd80b3 100644 --- a/deeplabcut/pose_estimation_3d/triangulation.py +++ b/deeplabcut/pose_estimation_3d/triangulation.py @@ -98,10 +98,7 @@ def triangulate( # Check if the config file exists if not os.path.exists(snapshots[cam]): raise Exception( - str( - "It seems the file specified in the variable config_file_" - + str(cam) - ) + str("It seems the file specified in the variable config_file_" + str(cam)) + " does not exist. Please edit the config file with correct file path and retry." ) @@ -109,9 +106,7 @@ def triangulate( flag = False # assumes that video path is a list if isinstance(video_path, str) == True: flag = True - video_list = auxiliaryfunctions_3d.get_camerawise_videos( - video_path, cam_names, videotype=videotype - ) + video_list = auxiliaryfunctions_3d.get_camerawise_videos(video_path, cam_names, videotype=videotype) else: video_list = video_path @@ -132,30 +127,17 @@ def triangulate( dataname = [] for j in range(len(video_list[i])): # looping over cameras if cam_names[j] not in video_list[i][j]: - raise ValueError( - f"Camera name '{cam_names[j]}' " - f"not found in video list '{video_list[i][j]}'." - ) + raise ValueError(f"Camera name '{cam_names[j]}' not found in video list '{video_list[i][j]}'.") else: - print( - "Analyzing video %s using %s" - % (video_list[i][j], str("config_file_" + cam_names[j])) - ) + print("Analyzing video %s using %s" % (video_list[i][j], str("config_file_" + cam_names[j]))) config_2d = snapshots[cam_names[j]] cfg = auxiliaryfunctions.read_config(config_2d) # Get track_method and do related checks - track_method = auxfun_multianimal.get_track_method( - cfg, track_method=track_method - ) - if ( - len(cfg.get("multianimalbodyparts", [])) == 1 - and track_method != "box" - ): - warnings.warn( - "Switching to `box` tracker for single point tracking..." - ) + track_method = auxfun_multianimal.get_track_method(cfg, track_method=track_method) + if len(cfg.get("multianimalbodyparts", [])) == 1 and track_method != "box": + warnings.warn("Switching to `box` tracker for single point tracking...") track_method = "box" # Get track method suffix @@ -198,13 +180,9 @@ def triangulate( output_file + "_" + scorer_3d ) # Check if the videos are already analyzed for 3d if os.path.isfile(output_filename + ".h5"): - if save_as_csv is True and not os.path.exists( - output_filename + ".csv" - ): + if save_as_csv is True and not os.path.exists(output_filename + ".csv"): # In case user adds save_as_csv is True after triangulating - pd.read_hdf(output_filename + ".h5").to_csv( - str(output_filename + ".csv") - ) + pd.read_hdf(output_filename + ".h5").to_csv(str(output_filename + ".csv")) print( "Already analyzed...Checking the meta data for any change in the camera matrices and/or scorer names", @@ -219,17 +197,13 @@ def triangulate( path_undistort, _, ) = auxiliaryfunctions_3d.Foldernames3Dproject(cfg_3d) - path_stereo_file = os.path.join( - path_camera_matrix, "stereo_params.pickle" - ) + path_stereo_file = os.path.join(path_camera_matrix, "stereo_params.pickle") stereo_file = auxiliaryfunctions.read_pickle(path_stereo_file) cam_pair = str(cam_names[0] + "-" + cam_names[1]) is_video_analyzed = False # variable to keep track if the video was already analyzed # Check for the camera matrix for k in metadata_["stereo_matrix"].keys(): - if np.all( - metadata_["stereo_matrix"][k] == stereo_file[cam_pair][k] - ): + if np.all(metadata_["stereo_matrix"][k] == stereo_file[cam_pair][k]): pass else: run_triangulate = True @@ -239,9 +213,7 @@ def triangulate( cfg, shuffle, trainFraction, trainingsiterations="unknown" ) - if ( - metadata_["scorer_name"][cam_names[j]] == DLCscorer - ): # TODO: CHECK FOR BOTH? + if metadata_["scorer_name"][cam_names[j]] == DLCscorer: # TODO: CHECK FOR BOTH? is_video_analyzed = True elif metadata_["scorer_name"][cam_names[j]] == DLCscorerlegacy: is_video_analyzed = True @@ -251,11 +223,7 @@ def triangulate( if is_video_analyzed: print("This file is already analyzed!") - dataname.append( - os.path.join( - destfolder, vname + DLCscorer + tr_method_suffix + ".h5" - ) - ) + dataname.append(os.path.join(destfolder, vname + DLCscorer + tr_method_suffix + ".h5")) scorer_name[cam_names[j]] = DLCscorer else: # Analyze video if score name is different @@ -284,9 +252,7 @@ def triangulate( ) suffix += "_filtered" - dataname.append( - os.path.join(destfolder, vname + DLCscorer + suffix + ".h5") - ) + dataname.append(os.path.join(destfolder, vname + DLCscorer + suffix + ".h5")) else: # need to do the whole jam. DLCscorer = analyze_videos( @@ -313,9 +279,7 @@ def triangulate( destfolder=destfolder, ) suffix += "_filtered" - dataname.append( - os.path.join(destfolder, vname + DLCscorer + suffix + ".h5") - ) + dataname.append(os.path.join(destfolder, vname + DLCscorer + suffix + ".h5")) if run_triangulate: # if len(dataname)>0: @@ -326,9 +290,7 @@ def triangulate( dataFrame_camera2_undistort, stereomatrix, path_stereo_file, - ) = undistort_points( - config, dataname, str(cam_names[0] + "-" + cam_names[1]) - ) + ) = undistort_points(config, dataname, str(cam_names[0] + "-" + cam_names[1])) if len(dataFrame_camera1_undistort) != len(dataFrame_camera2_undistort): import warnings @@ -336,20 +298,14 @@ def triangulate( "The number of frames do not match in the two videos. Please make sure that your videos have same number of frames and then retry! Excluding the extra frames from the longer video." ) if len(dataFrame_camera1_undistort) > len(dataFrame_camera2_undistort): - dataFrame_camera1_undistort = dataFrame_camera1_undistort[ - : len(dataFrame_camera2_undistort) - ] + dataFrame_camera1_undistort = dataFrame_camera1_undistort[: len(dataFrame_camera2_undistort)] if len(dataFrame_camera2_undistort) > len(dataFrame_camera1_undistort): - dataFrame_camera2_undistort = dataFrame_camera2_undistort[ - : len(dataFrame_camera1_undistort) - ] + dataFrame_camera2_undistort = dataFrame_camera2_undistort[: len(dataFrame_camera1_undistort)] # raise Exception("The number of frames do not match in the two videos. Please make sure that your videos have same number of frames and then retry!") scorer_cam1 = dataFrame_camera1_undistort.columns.get_level_values(0)[0] scorer_cam2 = dataFrame_camera2_undistort.columns.get_level_values(0)[0] - bodyparts = dataFrame_camera1_undistort.columns.get_level_values( - "bodyparts" - ).unique() + bodyparts = dataFrame_camera1_undistort.columns.get_level_values("bodyparts").unique() P1 = stereomatrix["P1"] P2 = stereomatrix["P2"] @@ -360,12 +316,8 @@ def triangulate( num_frames = dataFrame_camera1_undistort.shape[0] ### Assign nan to [X,Y] of low likelihood predictions ### # Convert the data to a np array to easily mask out the low likelihood predictions - data_cam1_tmp = dataFrame_camera1_undistort.to_numpy().reshape( - (num_frames, -1, 3) - ) - data_cam2_tmp = dataFrame_camera2_undistort.to_numpy().reshape( - (num_frames, -1, 3) - ) + data_cam1_tmp = dataFrame_camera1_undistort.to_numpy().reshape((num_frames, -1, 3)) + data_cam2_tmp = dataFrame_camera2_undistort.to_numpy().reshape((num_frames, -1, 3)) # Assign [X,Y] = nan to low likelihood predictions data_cam1_tmp[data_cam1_tmp[..., 2] < pcutoff, :2] = np.nan data_cam2_tmp[data_cam2_tmp[..., 2] < pcutoff, :2] = np.nan @@ -381,19 +333,13 @@ def triangulate( if cfg.get("multianimalproject"): # Check individuals are the same in both views individuals_view1 = ( - dataFrame_camera1_undistort.columns.get_level_values("individuals") - .unique() - .to_list() + dataFrame_camera1_undistort.columns.get_level_values("individuals").unique().to_list() ) individuals_view2 = ( - dataFrame_camera2_undistort.columns.get_level_values("individuals") - .unique() - .to_list() + dataFrame_camera2_undistort.columns.get_level_values("individuals").unique().to_list() ) if individuals_view1 != individuals_view2: - raise ValueError( - "The individuals do not match between the two DataFrames" - ) + raise ValueError("The individuals do not match between the two DataFrames") # Cross-view match individuals _, voting = auxiliaryfunctions_3d.cross_view_match_dataframes( @@ -408,12 +354,12 @@ def triangulate( individuals = individuals_view1 # Reshape: (num_framex, num_individuals, num_bodyparts , 2) - all_points_cam1 = dataFrame_camera1_undistort.to_numpy().reshape( - (num_frames, len(individuals), -1, 3) - )[..., :2] - all_points_cam2 = dataFrame_camera2_undistort.to_numpy().reshape( - (num_frames, len(individuals), -1, 3) - )[..., :2] + all_points_cam1 = dataFrame_camera1_undistort.to_numpy().reshape((num_frames, len(individuals), -1, 3))[ + ..., :2 + ] + all_points_cam2 = dataFrame_camera2_undistort.to_numpy().reshape((num_frames, len(individuals), -1, 3))[ + ..., :2 + ] # Triangulate data triangulate = [] @@ -424,9 +370,7 @@ def triangulate( pts_indv_cam1 = all_points_cam1[:, i].reshape((-1, 2)).T pts_indv_cam2 = all_points_cam2[:, voting[i]].reshape((-1, 2)).T - indv_points_3d = auxiliaryfunctions_3d.triangulatePoints( - P1, P2, pts_indv_cam1, pts_indv_cam2 - ) + indv_points_3d = auxiliaryfunctions_3d.triangulatePoints(P1, P2, pts_indv_cam1, pts_indv_cam2) indv_points_3d = indv_points_3d[:3].T.reshape((num_frames, -1, 3)) @@ -488,9 +432,7 @@ def triangulate( if cfg.get("multianimalproject"): df_2d_view2 = pd.read_hdf(dataname[1]) individuals_order = [individuals[i] for i in list(voting.values())] - df_2d_view2 = auxfun_multianimal.reorder_individuals_in_df( - df_2d_view2, individuals_order - ) + df_2d_view2 = auxfun_multianimal.reorder_individuals_in_df(df_2d_view2, individuals_order) df_2d_view2.to_hdf( dataname[1], key="tracks", @@ -498,9 +440,7 @@ def triangulate( mode="w", ) - auxiliaryfunctions_3d.SaveMetadata3d( - str(output_filename) + "_meta.pickle", metadata - ) + auxiliaryfunctions_3d.SaveMetadata3d(str(output_filename) + "_meta.pickle", metadata) if save_as_csv: df_3d.to_csv(str(output_filename) + ".csv") @@ -570,13 +510,9 @@ def undistort_points(config, dataframe, camera_pair): ) for filename in dataframe: if not os.path.exists(filename): - raise FileNotFoundError( - f"Dataframe path '{filename}' could not be found in the filesystem." - ) + raise FileNotFoundError(f"Dataframe path '{filename}' could not be found in the filesystem.") if not os.path.exists(path_camera_matrix): - raise FileNotFoundError( - f"Camera matrix file '{path_camera_matrix}' could not be found in the filesystem." - ) + raise FileNotFoundError(f"Camera matrix file '{path_camera_matrix}' could not be found in the filesystem.") # Create an empty dataFrame to store the undistorted 2d coordinates and likelihood dataframe_cam1 = pd.read_hdf(dataframe[0]) dataframe_cam2 = pd.read_hdf(dataframe[1]) diff --git a/deeplabcut/pose_estimation_pytorch/apis/analyze_images.py b/deeplabcut/pose_estimation_pytorch/apis/analyze_images.py index a7cabc1a82..16014629b3 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/analyze_images.py +++ b/deeplabcut/pose_estimation_pytorch/apis/analyze_images.py @@ -186,9 +186,7 @@ def superanimal_analyze_images( config = modelzoo.load_super_animal_config( super_animal=superanimal_name, model_name=model_name, - detector_name=( - detector_name if superanimal_name != "superanimal_humanbody" else None - ), + detector_name=(detector_name if superanimal_name != "superanimal_humanbody" else None), ) elif isinstance(customized_model_config, (str, Path)): config = config_utils.read_config_as_dict(customized_model_config) @@ -319,9 +317,7 @@ def analyze_images( snapshot = get_model_snapshots(snapshot_index, train_folder, pose_task)[0] detector_snapshot = None if detector_snapshot_index is not None: - detector_snapshot = get_model_snapshots( - detector_snapshot_index, train_folder, Task.DETECT - )[0] + detector_snapshot = get_model_snapshots(detector_snapshot_index, train_folder, Task.DETECT)[0] # Load the BU model for the conditions provider cond_provider = None @@ -402,10 +398,7 @@ def analyze_images( bodyparts = model_cfg["metadata"]["bodyparts"] skeleton = None if plot_skeleton and len(cfg.get("skeleton", [])) > 0: - skeleton = [ - (bodyparts.index(bpt_0), bodyparts.index(bpt_1)) - for bpt_0, bpt_1 in cfg["skeleton"] - ] + skeleton = [(bodyparts.index(bpt_0), bodyparts.index(bpt_1)) for bpt_0, bpt_1 in cfg["skeleton"]] if pcutoff is None: pcutoff = cfg.get("pcutoff", 0.6) @@ -471,11 +464,7 @@ def analyze_image_folder( model_cfg = config_utils.read_config_as_dict(model_cfg) pose_task = Task(model_cfg["method"]) - if ( - pose_task == Task.TOP_DOWN - and detector_path is None - and filtered_detector_config is None - ): + if pose_task == Task.TOP_DOWN and detector_path is None and filtered_detector_config is None: raise ValueError( "A detector path or filtered_detector_config must be specified for image analysis using top-down models" f" Please specify the `detector_path` parameter or the `filtered_detector_config` parameter." @@ -507,10 +496,7 @@ def analyze_image_folder( image_paths = parse_images_and_image_folders(images, image_suffixes) if not image_paths: - logging.info( - f"No images found searching {images} for extensions {image_suffixes}. " - "Skipping analysis." - ) + logging.info(f"No images found searching {images} for extensions {image_suffixes}. Skipping analysis.") return {} pose_inputs = image_paths @@ -552,10 +538,7 @@ def analyze_image_folder( predictions = pose_runner.inference(pose_inputs) - return { - image_path: image_predictions - for image_path, image_predictions in zip(image_paths, predictions) - } + return {image_path: image_predictions for image_path, image_predictions in zip(image_paths, predictions)} def plot_images_coco( @@ -668,9 +651,7 @@ def plot_images_coco( for bbox in bboxes: # Draw bounding boxes around detected objects xmin, ymin, w, h = bbox - rect = plt.Rectangle( - (xmin, ymin), w, h, fill=False, edgecolor="blue", linewidth=2 - ) + rect = plt.Rectangle((xmin, ymin), w, h, fill=False, edgecolor="blue", linewidth=2) ax.add_patch(rect) image_name = image_path.split("/")[-1] diff --git a/deeplabcut/pose_estimation_pytorch/apis/ctd.py b/deeplabcut/pose_estimation_pytorch/apis/ctd.py index a1d80049fa..9e13fe3ce9 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/ctd.py +++ b/deeplabcut/pose_estimation_pytorch/apis/ctd.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Methods to help with conditional top-down models""" + from pathlib import Path import numpy as np @@ -44,8 +45,7 @@ def get_condition_provider( if isinstance(condition_cfg, (str, Path)): error_message = ( - "To run inference with CTD models, you must specify the BU model you " - "want to use to generate conditions.\n" + "To run inference with CTD models, you must specify the BU model you want to use to generate conditions.\n" ) + error_message raise ValueError(error_message) elif not isinstance(condition_cfg, dict): @@ -81,7 +81,6 @@ def get_conditions_provider_for_video( # Load pickle for multi-animal projects cond_file = video.parent / f"{video.stem}{cond_provider.scorer}_assemblies.pickle" if not cond_file.exists(): - # Load h5 for single-animal projects cond_file = video.parent / f"{video.stem}{cond_provider.scorer}.h5" if not cond_file.exists(): @@ -90,9 +89,7 @@ def get_conditions_provider_for_video( return CondFromFile(filepath=cond_file) -def load_conditions_for_evaluation( - loader: data.Loader, images: list[str] -) -> dict[str, np.ndarray]: +def load_conditions_for_evaluation(loader: data.Loader, images: list[str]) -> dict[str, np.ndarray]: """Loads the conditions needed to evaluate a CTD model Args: diff --git a/deeplabcut/pose_estimation_pytorch/apis/evaluation.py b/deeplabcut/pose_estimation_pytorch/apis/evaluation.py index f4f0ec91c5..f6edfac830 100755 --- a/deeplabcut/pose_estimation_pytorch/apis/evaluation.py +++ b/deeplabcut/pose_estimation_pytorch/apis/evaluation.py @@ -80,10 +80,7 @@ def predict( context = bbox_predictions else: ground_truth_bboxes = loader.ground_truth_bboxes(mode=mode) - context = [ - {"bboxes": ground_truth_bboxes[image]["bboxes"]} - for image in image_paths - ] + context = [{"bboxes": ground_truth_bboxes[image]["bboxes"]} for image in image_paths] elif loader.pose_task == Task.COND_TOP_DOWN: # Load conditions for context @@ -93,16 +90,11 @@ def predict( images_with_context = image_paths if context is not None: if len(context) != len(image_paths): - raise ValueError( - f"Missing context for some images: {len(context)} != {len(image_paths)}" - ) + raise ValueError(f"Missing context for some images: {len(context)} != {len(image_paths)}") images_with_context = list(zip(image_paths, context)) predictions = pose_runner.inference(images=tqdm(images_with_context)) - return { - image_path: image_predictions - for image_path, image_predictions in zip(image_paths, predictions) - } + return {image_path: image_predictions for image_path, image_predictions in zip(image_paths, predictions)} def evaluate( @@ -163,9 +155,7 @@ def evaluate( gt_unique, pred_unique, unique_idx = None, None, None if parameters.num_unique_bpts >= 1: gt_unique = loader.ground_truth_keypoints(mode, unique_bodypart=True) - pred_unique = { - filename: pred["unique_bodyparts"] for filename, pred in predictions.items() - } + pred_unique = {filename: pred["unique_bodyparts"] for filename, pred in predictions.items()} unique_idx = _get_keypoints_to_use(parameters.unique_bpts, comparison_bodyparts) # When `comparison_bodyparts` is used, check that the bodyparts used for evaluation @@ -214,9 +204,7 @@ def evaluate( ) if loader.model_cfg["metadata"]["with_identity"]: - pred_id_scores = { - filename: pred["identity_scores"] for filename, pred in predictions.items() - } + pred_id_scores = {filename: pred["identity_scores"] for filename, pred in predictions.items()} id_scores = metrics.compute_identity_scores( individuals=parameters.individuals, bodyparts=parameters.bodyparts, @@ -276,9 +264,7 @@ def visualize_predictions( image_paths = list(predictions.keys()) if num_samples and num_samples < len(image_paths): if random_select: - image_paths = np.random.choice( - image_paths, num_samples, replace=False - ).tolist() + image_paths = np.random.choice(image_paths, num_samples, replace=False).tolist() else: image_paths = image_paths[:num_samples] @@ -312,11 +298,7 @@ def visualize_predictions( if plot_bboxes: bboxes = predictions[image_path].get("bboxes", None) bbox_scores = predictions[image_path].get("bbox_scores", None) - bounding_boxes = ( - (bboxes, bbox_scores) - if bboxes is not None and bbox_scores is not None - else None - ) + bounding_boxes = (bboxes, bbox_scores) if bboxes is not None and bbox_scores is not None else None else: bounding_boxes = None @@ -557,10 +539,7 @@ def evaluate_snapshot( if pcutoff is None: pcutoff = cfg.get("pcutoff", 0.6) elif isinstance(pcutoff, dict): - pcutoff = [ - pcutoff.get(bpt, 0.6) - for bpt in eval_parameters.bodyparts + eval_parameters.unique_bpts - ] + pcutoff = [pcutoff.get(bpt, 0.6) for bpt in eval_parameters.bodyparts + eval_parameters.unique_bpts] _validate_pcutoff(parameters.bodyparts, parameters.unique_bpts, pcutoff) predictions = {} @@ -570,14 +549,8 @@ def evaluate_snapshot( "%Training dataset": loader.train_fraction, "Shuffle number": loader.shuffle, "Training epochs": snapshot.epochs, - "Detector epochs (TD only)": ( - -1 if detector_snapshot is None else detector_snapshot.epochs - ), - "pcutoff": ( - ", ".join([str(v) for v in pcutoff]) - if isinstance(pcutoff, list) - else pcutoff - ), + "Detector epochs (TD only)": (-1 if detector_snapshot is None else detector_snapshot.epochs), + "pcutoff": (", ".join([str(v) for v in pcutoff]) if isinstance(pcutoff, list) else pcutoff), } for split in ["train", "test"]: results, predictions_for_split = evaluate( @@ -634,9 +607,7 @@ def evaluate_snapshot( save_evaluation_results(df_scores, scores_filepath, show_errors, pcutoff) if per_keypoint_evaluation: - rmse_per_bpt_path = output_filename.with_name( - output_filename.stem + "-keypoint-results.csv" - ) + rmse_per_bpt_path = output_filename.with_name(output_filename.stem + "-keypoint-results.csv") save_rmse_per_bodypart(rmse_per_bodypart, rmse_per_bpt_path, show_errors) if plotting: @@ -650,16 +621,10 @@ def evaluate_snapshot( df_ground_truth = ensure_multianimal_df_format(loader.df) - bboxes_cutoff = ( - loader.model_cfg.get("detector", {}) - .get("model", {}) - .get("box_score_thresh", 0.6) - ) + bboxes_cutoff = loader.model_cfg.get("detector", {}).get("model", {}).get("box_score_thresh", 0.6) for mode in ["train", "test"]: - df_combined = predictions[mode].merge( - df_ground_truth, left_index=True, right_index=True - ) + df_combined = predictions[mode].merge(df_ground_truth, left_index=True, right_index=True) bboxes_split = bounding_boxes[mode] plot_evaluation_results( @@ -804,9 +769,7 @@ def evaluate_network( detector_snapshots = [None] if loader.pose_task == Task.TOP_DOWN: if detector_snapshot_index is not None: - det_snapshots = get_model_snapshots( - "all", loader.model_folder, Task.DETECT - ) + det_snapshots = get_model_snapshots("all", loader.model_folder, Task.DETECT) if len(det_snapshots) == 0: print( "The detector_snapshot_index was set to " @@ -866,9 +829,7 @@ def image_to_dlc_df_index(image: str) -> tuple[str, ...]: raise ValueError(f"Unexpected image filepath for a DLC project") -def save_evaluation_results( - df_scores: pd.DataFrame, scores_path: Path, print_results: bool, pcutoff: float -) -> None: +def save_evaluation_results(df_scores: pd.DataFrame, scores_path: Path, print_results: bool, pcutoff: float) -> None: """ Saves the evaluation results to a CSV file. Adds the evaluation results for the model to the combined results file, or creates it if it does not yet exist. @@ -889,9 +850,7 @@ def save_evaluation_results( # Update combined results combined_scores_path = scores_path.parent.parent / "CombinedEvaluation-results.csv" if combined_scores_path.exists(): - df_existing_results = pd.read_csv( - combined_scores_path, index_col=[0, 1, 2, 3, 4] - ) + df_existing_results = pd.read_csv(combined_scores_path, index_col=[0, 1, 2, 3, 4]) df_scores = df_scores.combine_first(df_existing_results) df_scores = df_scores.sort_index() diff --git a/deeplabcut/pose_estimation_pytorch/apis/export.py b/deeplabcut/pose_estimation_pytorch/apis/export.py index 09de76f369..44d78ea5f2 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/export.py +++ b/deeplabcut/pose_estimation_pytorch/apis/export.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Code to export DeepLabCut models for DLCLive inference""" + import copy from pathlib import Path @@ -88,9 +89,7 @@ def export_model( if snapshotindex is None: snapshotindex = loader.project_cfg["snapshotindex"] - snapshots = utils.get_model_snapshots( - snapshotindex, loader.model_folder, loader.pose_task - ) + snapshots = utils.get_model_snapshots(snapshotindex, loader.model_folder, loader.pose_task) if len(snapshots) == 0: raise ValueError( @@ -102,9 +101,7 @@ def export_model( if loader.pose_task == Task.TOP_DOWN and not without_detector: if detector_snapshot_index is None: detector_snapshot_index = loader.project_cfg["detector_snapshotindex"] - detector_snapshots = utils.get_model_snapshots( - detector_snapshot_index, loader.model_folder, Task.DETECT - ) + detector_snapshots = utils.get_model_snapshots(detector_snapshot_index, loader.model_folder, Task.DETECT) if len(detector_snapshots) == 0: raise ValueError( diff --git a/deeplabcut/pose_estimation_pytorch/apis/prune_paf_graph.py b/deeplabcut/pose_estimation_pytorch/apis/prune_paf_graph.py index a31a833982..5e125f90e8 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/prune_paf_graph.py +++ b/deeplabcut/pose_estimation_pytorch/apis/prune_paf_graph.py @@ -147,9 +147,7 @@ def benchmark_paf_graphs( print() # update the edges to keep in the PyTorch configuration file - loader.update_model_cfg( - {"model.heads.bodypart.predictor.edges_to_keep": best_edges} - ) + loader.update_model_cfg({"model.heads.bodypart.predictor.edges_to_keep": best_edges}) # update the edges indices test_config = loader.model_folder.parent / "test" / "pose_cfg.yaml" @@ -175,9 +173,7 @@ def _calc_separability( hist_right = hist_right / hist_right.sum() tpr = np.cumsum(hist_right) if metric == "jeffries": - sep = np.sqrt( - 2 * (1 - np.sum(np.sqrt(hist_left * hist_right))) - ) # Jeffries-Matusita distance + sep = np.sqrt(2 * (1 - np.sum(np.sqrt(hist_left * hist_right)))) # Jeffries-Matusita distance else: sep = np.trapz(np.cumsum(hist_left), tpr) if max_sensitivity: @@ -221,9 +217,7 @@ def compute_within_between_paf_costs( inds = np.flatnonzero(np.all(~np.isnan(coord_pred), axis=1)) inds_gt = np.flatnonzero(np.all(~np.isnan(coord_gt), axis=1)) if inds.size and inds_gt.size: - neighbors = find_closest_neighbors( - coord_gt[inds_gt], coord_pred[inds], k=3 - ) + neighbors = find_closest_neighbors(coord_gt[inds_gt], coord_pred[inds], k=3) found = neighbors != -1 lookup[i] = dict(zip(inds_gt[found], inds[neighbors[found]])) @@ -259,17 +253,10 @@ def get_n_best_paf_graphs( return_preds = model.heads.bodypart.predictor.return_preds model.heads.bodypart.predictor.return_preds = True - within_train, between_train = compute_within_between_paf_costs( - model, ground_truth, preprocessor, device - ) + within_train, between_train = compute_within_between_paf_costs(model, ground_truth, preprocessor, device) existing_edges = list(set(k for k, v in within_train.items() if v)) - scores, _ = zip( - *[ - _calc_separability(between_train[n], within_train[n], metric=metric) - for n in existing_edges - ] - ) + scores, _ = zip(*[_calc_separability(between_train[n], within_train[n], metric=metric) for n in existing_edges]) # Find minimal skeleton G = nx.Graph() diff --git a/deeplabcut/pose_estimation_pytorch/apis/tracking_dataset.py b/deeplabcut/pose_estimation_pytorch/apis/tracking_dataset.py index d2b5d35d2d..3d4af32cc2 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/tracking_dataset.py +++ b/deeplabcut/pose_estimation_pytorch/apis/tracking_dataset.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Code to create tracking datasets for ReID model training""" + from pathlib import Path from tqdm import tqdm @@ -59,8 +60,7 @@ def build_feature_extraction_runner( ) else: preprocessor = data.build_bottom_up_preprocessor( - loader.model_cfg["data"]["colormode"], - data.build_transforms(loader.model_cfg["data"]["inference"]) + loader.model_cfg["data"]["colormode"], data.build_transforms(loader.model_cfg["data"]["inference"]) ) postprocessor = postprocessing.ComposePostprocessor( @@ -91,9 +91,7 @@ def build_feature_extraction_runner( postprocessor=postprocessor, load_weights_only=loader.model_cfg["runner"].get("load_weights_only", None), ) - assert isinstance(runner, runners.PoseInferenceRunner), ( - f"Failed to build inference runner: got type {type(runner)}" - ) + assert isinstance(runner, runners.PoseInferenceRunner), f"Failed to build inference runner: got type {type(runner)}" # Set the model to output backbone features runner.model.output_features = True @@ -187,10 +185,15 @@ def create_tracking_dataset( test_cfg = read_config_as_dict(test_cfg_path) snapshot_index, detector_snapshot_index = utils.parse_snapshot_index_for_analysis( - loader.project_cfg, loader.model_cfg, None, None, + loader.project_cfg, + loader.model_cfg, + None, + None, ) snapshot = utils.get_model_snapshots( - snapshot_index, loader.model_folder, loader.pose_task, + snapshot_index, + loader.model_folder, + loader.pose_task, )[0] if cropping is None and loader.project_cfg.get("cropping", False): @@ -209,9 +212,7 @@ def create_tracking_dataset( batch_size = loader.project_cfg["batch_size"] device = utils.resolve_device(loader.model_cfg) - runner = build_feature_extraction_runner( - loader, snapshot.path, device, batch_size=batch_size - ) + runner = build_feature_extraction_runner(loader, snapshot.path, device, batch_size=batch_size) detector_runner = None detector_snapshot = None @@ -220,7 +221,9 @@ def create_tracking_dataset( detector_batch_size = loader.project_cfg.get("detector_batch_size", 1) detector_snapshot = utils.get_model_snapshots( - detector_snapshot_index, loader.model_folder, Task.DETECT, + detector_snapshot_index, + loader.model_folder, + Task.DETECT, )[0] detector_runner = utils.get_detector_inference_runner( model_config=loader.model_cfg, @@ -264,9 +267,7 @@ def create_tracking_dataset( output_filepath, num_frames=video.get_n_frames(robust=robust_nframes), ) - extract_features_for_video( - runner, video, shelf_writer, detector_runner=detector_runner - ) + extract_features_for_video(runner, video, shelf_writer, detector_runner=detector_runner) create_triplets_dataset( videos, diff --git a/deeplabcut/pose_estimation_pytorch/apis/tracklets.py b/deeplabcut/pose_estimation_pytorch/apis/tracklets.py index c3706c510b..5833edf86e 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/tracklets.py +++ b/deeplabcut/pose_estimation_pytorch/apis/tracklets.py @@ -94,9 +94,7 @@ def convert_detections2tracklets( ) if inference_cfg is None: - inference_cfg = auxfun_multianimal.read_inferencecfg( - model_dir / "test" / "inference_cfg.yaml", cfg - ) + inference_cfg = auxfun_multianimal.read_inferencecfg(model_dir / "test" / "inference_cfg.yaml", cfg) auxfun_multianimal.check_inferencecfg_sanity(cfg, inference_cfg) if len(cfg["multianimalbodyparts"]) == 1 and track_method != "box": @@ -159,9 +157,7 @@ def convert_detections2tracklets( print(f"Tracklets already computed at {track_filename}") print("Set overwrite = True to overwrite.") else: - assemblies_path = data_filename.with_stem( - data_filename.stem + "_assemblies" - ).with_suffix(".pickle") + assemblies_path = data_filename.with_stem(data_filename.stem + "_assemblies").with_suffix(".pickle") if not assemblies_path.exists(): raise FileNotFoundError( f"Could not find the assembles file {assemblies_path}. You're " @@ -278,9 +274,7 @@ def build_tracklets( unique_ids, idx = np.unique(animal_pose[:, 3], return_inverse=True) total_scores = np.bincount(idx, weights=animal_pose[:, 2]) softmax_id_scores = softmax(total_scores) - for pred_id, softmax_score in zip( - unique_ids.astype(int), softmax_id_scores - ): + for pred_id, softmax_score in zip(unique_ids.astype(int), softmax_id_scores): mat[row, pred_id] = softmax_score inds = linear_sum_assignment(mat, maximize=True) diff --git a/deeplabcut/pose_estimation_pytorch/apis/training.py b/deeplabcut/pose_estimation_pytorch/apis/training.py index 92de01c40d..720b786abc 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/training.py +++ b/deeplabcut/pose_estimation_pytorch/apis/training.py @@ -102,9 +102,7 @@ def train( logger = None if logger_config is not None: - logger = LOGGER.build( - {**logger_config, "model": model, "train_folder": loader.model_folder} - ) + logger = LOGGER.build({**logger_config, "model": model, "train_folder": loader.model_folder}) logger.log_config(run_config) if device is None: @@ -145,9 +143,7 @@ def train( logging.info(f" Validation: {inference_transform}") train_dataset = loader.create_dataset(transform=transform, mode="train", task=task) - valid_dataset = loader.create_dataset( - transform=inference_transform, mode="test", task=task - ) + valid_dataset = loader.create_dataset(transform=inference_transform, mode="test", task=task) collate_fn = None if collate_fn_cfg := run_config["data"]["train"].get("collate"): @@ -187,9 +183,7 @@ def train( "scale the learning rate by sqrt(batch_size) times).\n" ) - logging.info( - f"Using {len(train_dataset)} images and {len(valid_dataset)} for testing" - ) + logging.info(f"Using {len(train_dataset)} images and {len(valid_dataset)} for testing") if task == task.DETECT: logging.info("\nStarting object detector training...\n" + (50 * "-")) else: @@ -346,10 +340,7 @@ def train_network( # get the pose task pose_task = Task(loader.model_cfg.get("method", "bu")) - if ( - pose_task == Task.TOP_DOWN - and loader.model_cfg["detector"]["train_settings"]["epochs"] > 0 - ): + if pose_task == Task.TOP_DOWN and loader.model_cfg["detector"]["train_settings"]["epochs"] > 0: logger_config = None if loader.model_cfg.get("logger"): logger_config = copy.deepcopy(loader.model_cfg["logger"]) @@ -357,9 +348,7 @@ def train_network( detector_run_config = loader.model_cfg["detector"] detector_run_config["device"] = loader.model_cfg["device"] - detector_run_config["train_settings"]["weight_init"] = loader.model_cfg[ - "train_settings" - ].get("weight_init") + detector_run_config["train_settings"]["weight_init"] = loader.model_cfg["train_settings"].get("weight_init") train( loader=loader, run_config=detector_run_config, diff --git a/deeplabcut/pose_estimation_pytorch/apis/utils.py b/deeplabcut/pose_estimation_pytorch/apis/utils.py index e363ab8c19..a204ed71b5 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/utils.py +++ b/deeplabcut/pose_estimation_pytorch/apis/utils.py @@ -180,9 +180,7 @@ def get_model_snapshots( ValueError: If the index given is not valid ValueError: If index=="best" but there is no saved best model """ - snapshot_manager = TorchSnapshotManager( - model_folder=model_folder, snapshot_prefix=task.snapshot_prefix - ) + snapshot_manager = TorchSnapshotManager(model_folder=model_folder, snapshot_prefix=task.snapshot_prefix) if snapshot_filter is not None: all_snapshots = snapshot_manager.snapshots() snapshots = [s for s in all_snapshots if s.path.stem in snapshot_filter] @@ -202,11 +200,7 @@ def get_model_snapshots( snapshots = snapshot_manager.snapshots() elif isinstance(index, int): all_snapshots = snapshot_manager.snapshots() - if ( - len(all_snapshots) == 0 - or len(all_snapshots) <= index - or (index < 0 and len(all_snapshots) < -index) - ): + if len(all_snapshots) == 0 or len(all_snapshots) <= index or (index < 0 and len(all_snapshots) < -index): names = [s.path.name for s in all_snapshots] raise ValueError( f"Found {len(all_snapshots)} snapshots in {model_folder} (with names " @@ -279,9 +273,7 @@ def get_scorer_name( if snapshot_uid is None: if snapshot_index is None: - snapshot_index = auxiliaryfunctions.get_snapshot_index_for_scorer( - "snapshotindex", cfg["snapshotindex"] - ) + snapshot_index = auxiliaryfunctions.get_snapshot_index_for_scorer("snapshotindex", cfg["snapshotindex"]) if detector_index is None: detector_index = auxiliaryfunctions.get_snapshot_index_for_scorer( "detector_snapshotindex", cfg["detector_snapshotindex"] @@ -291,9 +283,7 @@ def get_scorer_name( detector_snapshot = None if detector_index is not None and pose_task == Task.TOP_DOWN: try: - detector_snapshot = get_model_snapshots( - detector_index, train_dir, Task.DETECT - )[0] + detector_snapshot = get_model_snapshots(detector_index, train_dir, Task.DETECT)[0] except ValueError: detector_snapshot = None @@ -335,9 +325,7 @@ def list_videos_in_folder( videos = [] for path in map(Path, data_path): if not path.exists(): - raise FileNotFoundError( - f"Could not find: {path}. Check access rights." - ) + raise FileNotFoundError(f"Could not find: {path}. Check access rights.") if path.is_dir(): videos.extend(f for f in path.iterdir() if f.is_file() and f.suffix.lower() in video_suffixes) @@ -388,9 +376,7 @@ def _image_names_to_df_index( """ if image_name_to_index is not None: - return pd.MultiIndex.from_tuples( - [image_name_to_index(image_name) for image_name in image_names] - ) + return pd.MultiIndex.from_tuples([image_name_to_index(image_name) for image_name in image_names]) else: return image_names @@ -430,9 +416,7 @@ def build_predictions_dataframe( for image_name, image_predictions in predictions.items(): image_data = image_predictions["bodyparts"][..., :3].reshape(-1) if "unique_bodyparts" in image_predictions: - image_data = np.concatenate( - [image_data, image_predictions["unique_bodyparts"][..., :3].reshape(-1)] - ) + image_data = np.concatenate([image_data, image_predictions["unique_bodyparts"][..., :3].reshape(-1)]) image_names.append(image_name) prediction_data.append(image_data) @@ -475,9 +459,7 @@ def build_bboxes_dict_for_dataframe( for image_name, image_predictions in predictions.items(): image_names.append(image_name) if "bboxes" in image_predictions and "bbox_scores" in image_predictions: - bboxes_data.append( - (image_predictions["bboxes"], image_predictions["bbox_scores"]) - ) + bboxes_data.append((image_predictions["bboxes"], image_predictions["bbox_scores"])) index = _image_names_to_df_index(image_names, image_name_to_index) @@ -498,7 +480,7 @@ def get_inference_runners( detector_path: str | Path | None = None, detector_transform: A.BaseCompose | None = None, dynamic: DynamicCropper | None = None, - inference_cfg:InferenceConfig | dict | None = None, + inference_cfg: InferenceConfig | dict | None = None, min_bbox_score: float | None = None, ) -> tuple[InferenceRunner, InferenceRunner | None]: """Builds the runners for pose estimation @@ -601,9 +583,7 @@ def get_inference_runners( if detector_path is not None: detector_path = str(detector_path) if detector_transform is None: - detector_transform = build_transforms( - model_config["detector"]["data"]["inference"] - ) + detector_transform = build_transforms(model_config["detector"]["data"]["inference"]) detector_config = model_config["detector"]["model"] if "pretrained" in detector_config: @@ -807,9 +787,7 @@ def get_filtered_coco_detector_inference_runner( if color_mode is None: missing.append("color_mode") if missing: - raise ValueError( - f"If `model_config` is not provided, you must explicitly specify: {', '.join(missing)}." - ) + raise ValueError(f"If `model_config` is not provided, you must explicitly specify: {', '.join(missing)}.") if device == "mps": device = "cpu" diff --git a/deeplabcut/pose_estimation_pytorch/apis/videos.py b/deeplabcut/pose_estimation_pytorch/apis/videos.py index 70b4d77c77..bd4cfe10b4 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/videos.py +++ b/deeplabcut/pose_estimation_pytorch/apis/videos.py @@ -213,9 +213,7 @@ def video_inference( if detector_runner is not None: print(f"Running detector with batch size {detector_runner.batch_size}") - bbox_predictions = detector_runner.inference( - images = GpuTqdm(video) if show_gpu_memory else tqdm(video) - ) + bbox_predictions = detector_runner.inference(images=GpuTqdm(video) if show_gpu_memory else tqdm(video)) video.set_context(bbox_predictions) print(f"Running pose prediction with batch size {pose_runner.batch_size}") @@ -223,8 +221,7 @@ def video_inference( shelf_writer.open() predictions = pose_runner.inference( - images = GpuTqdm(video) if show_gpu_memory else tqdm(video), - shelf_writer=shelf_writer + images=GpuTqdm(video) if show_gpu_memory else tqdm(video), shelf_writer=shelf_writer ) if shelf_writer is not None: shelf_writer.close() @@ -457,8 +454,7 @@ def analyze_videos( save_as_df = True if use_shelve: print( - "The ``use_shelve`` parameter cannot be used for single animal " - "projects. Setting ``use_shelve=False``." + "The ``use_shelve`` parameter cannot be used for single animal projects. Setting ``use_shelve=False``." ) use_shelve = False @@ -482,9 +478,7 @@ def analyze_videos( print(f"Creating a TopDownDynamicCropper with configuration {top_down_dynamic}") dynamic = TopDownDynamicCropper(**top_down_dynamic) - snapshot = utils.get_model_snapshots( - snapshot_index, loader.model_folder, loader.pose_task - )[0] + snapshot = utils.get_model_snapshots(snapshot_index, loader.model_folder, loader.pose_task)[0] # Load the BU model for the conditions provider cond_provider = None @@ -532,9 +526,7 @@ def analyze_videos( if detector_batch_size is None: detector_batch_size = loader.project_cfg.get("detector_batch_size", 1) - detector_snapshot = utils.get_model_snapshots( - detector_snapshot_index, loader.model_folder, Task.DETECT - )[0] + detector_snapshot = utils.get_model_snapshots(detector_snapshot_index, loader.model_folder, Task.DETECT)[0] print(f" -> Using detector {detector_snapshot.path}") detector_runner = utils.get_detector_inference_runner( model_config=loader.model_cfg, @@ -647,9 +639,7 @@ def analyze_videos( for i in range(num_frames): frame_data = full_data.get("frame" + str(i).zfill(str_width)) if frame_data is None: - pose = np.full( - (len(individuals), len(bodyparts), 3), np.nan - ) + pose = np.full((len(individuals), len(bodyparts), 3), np.nan) ctd_predictions.append(dict(bodyparts=pose)) continue @@ -848,9 +838,7 @@ def _validate_destfolder(destfolder: str | None) -> None: print(f"Creating the output folder {output_folder}") output_folder.mkdir(parents=True) - assert Path( - output_folder - ).is_dir(), f"Output folder must be a directory: you passed '{output_folder}'" + assert Path(output_folder).is_dir(), f"Output folder must be a directory: you passed '{output_folder}'" def _generate_metadata( @@ -870,8 +858,7 @@ def _generate_metadata( else: if not len(cropping) == 4: raise ValueError( - "The cropping parameters should be exactly 4 values: [x_min, x_max, " - f"y_min, y_max]. Found {cropping}" + f"The cropping parameters should be exactly 4 values: [x_min, x_max, y_min, y_max]. Found {cropping}" ) cropping_parameters = cropping @@ -912,10 +899,7 @@ def _generate_output_data( np.arange(len(pose_config.get("partaffinityfield_graph", []))), ), "all_joints": [[i] for i in range(len(pose_config["all_joints"]))], - "all_joints_names": [ - pose_config["all_joints_names"][i] - for i in range(len(pose_config["all_joints"])) - ], + "all_joints_names": [pose_config["all_joints_names"][i] for i in range(len(pose_config["all_joints"]))], "nframes": len(predictions), "key_str_width": str_width, } @@ -959,8 +943,6 @@ def _generate_output_data( if num_unique > 0: # needed for create_video_with_all_detections to display unique bpts num_assem, num_ind = id_scores.shape[1:] - output[key]["identity"] += [ - -1 * np.ones((num_assem, num_ind)) for i in range(num_unique) - ] + output[key]["identity"] += [-1 * np.ones((num_assem, num_ind)) for i in range(num_unique)] return output diff --git a/deeplabcut/pose_estimation_pytorch/apis/visualization.py b/deeplabcut/pose_estimation_pytorch/apis/visualization.py index de040527ed..788c4aae3f 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/visualization.py +++ b/deeplabcut/pose_estimation_pytorch/apis/visualization.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Methods to help with visualization of model outputs""" + from __future__ import annotations from pathlib import Path @@ -121,11 +122,7 @@ def create_labeled_images( xy = xy[mask] ax.scatter(xy[:, 0], xy[:, 1], **kwargs) if len(bones) > 0: - ax.add_collection( - collections.LineCollection( - bones, colors=skeleton_color, alpha=alpha_value - ) - ) + ax.add_collection(collections.LineCollection(bones, colors=skeleton_color, alpha=alpha_value)) # plot unique bodyparts if unique_pred is not None: @@ -154,9 +151,7 @@ def create_labeled_images( continue xmin, ymin, w, h = bbox - rect = plt.Rectangle( - (xmin, ymin), w, h, fill=False, edgecolor="green", linewidth=2 - ) + rect = plt.Rectangle((xmin, ymin), w, h, fill=False, edgecolor="green", linewidth=2) ax.add_patch(rect) # save predictions @@ -237,9 +232,7 @@ def extract_model_outputs( head: {name: output.cpu().numpy() for name, output in head_outputs.items()} for head, head_outputs in output.items() } - model_data.append( - dict(inputs=inputs.cpu().numpy(), context=context, outputs=output) - ) + model_data.append(dict(inputs=inputs.cpu().numpy(), context=context, outputs=output)) return model_data @@ -337,9 +330,7 @@ def extract_maps( if snapshot_index is None: snapshot_index = -1 - snapshots = utils.get_model_snapshots( - snapshot_index, loader.model_folder, loader.pose_task - ) + snapshots = utils.get_model_snapshots(snapshot_index, loader.model_folder, loader.pose_task) image_paths = loader.df.index if indices is not None: @@ -347,9 +338,7 @@ def extract_maps( if len(image_paths) > 0 and isinstance(image_paths[0], tuple): image_paths = [Path(*img_path) for img_path in image_paths] - image_paths = [ - (loader.project_path / img_path).resolve() for img_path in image_paths - ] + image_paths = [(loader.project_path / img_path).resolve() for img_path in image_paths] context = _get_context(image_paths, loader, detector_snapshot_index, device) train_idx = set(loader.split["train"]) @@ -373,17 +362,12 @@ def extract_maps( image_idx = indices[idx] # key can be just image_idx, or (image_idx, bbox_idx) for TD models - keys, images, outputs = _collect_model_outputs( - loader.pose_task, result, image_idx - ) + keys, images, outputs = _collect_model_outputs(loader.pose_task, result, image_idx) for key, image, output in zip(keys, images, outputs): parsed = _parse_model_outputs( image, output, - strides={ - k: runner.model.get_stride(k) - for k in runner.model.heads.keys() - }, + strides={k: runner.model.get_stride(k) for k in runner.model.heads.keys()}, denormalize_image=True, ) img_name = image_paths[idx].stem @@ -471,9 +455,7 @@ def extract_save_all_maps( detector_snapshot_index=detector_snapshot_index, modelprefix=modelprefix, ) - bpts_to_plot = auxiliaryfunctions.intersection_of_body_parts_and_ones_given_by_user( - cfg, comparison_bodyparts - ) + bpts_to_plot = auxiliaryfunctions.intersection_of_body_parts_and_ones_given_by_user(cfg, comparison_bodyparts) print("Saving plots...") for frac, values in maps.items(): @@ -535,9 +517,7 @@ def _get_context( det_snapshots = [] if detector_snapshot_index is not None: - det_snapshots = utils.get_model_snapshots( - detector_snapshot_index, loader.model_folder, Task.DETECT - ) + det_snapshots = utils.get_model_snapshots(detector_snapshot_index, loader.model_folder, Task.DETECT) if detector_snapshot_index is None or len(det_snapshots) == 0: if detector_snapshot_index is None: @@ -549,9 +529,7 @@ def _get_context( bboxes_train = loader.ground_truth_bboxes(mode="train") bboxes_test = loader.ground_truth_bboxes(mode="test") bboxes = {**bboxes_train, **bboxes_test} - return [ - dict(bboxes=bboxes[str(img_path)]["bboxes"]) for img_path in image_paths - ] + return [dict(bboxes=bboxes[str(img_path)]["bboxes"]) for img_path in image_paths] detector_runner = utils.get_detector_inference_runner( model_config=loader.model_cfg, @@ -599,12 +577,7 @@ def _collect_model_outputs( return ( [image_idx], [result["inputs"][0]], - [ - { - head: {k: v[0] for k, v in head_outputs.items()} - for head, head_outputs in result["outputs"].items() - } - ], + [{head: {k: v[0] for k, v in head_outputs.items()} for head, head_outputs in result["outputs"].items()}], ) @@ -640,17 +613,12 @@ def _parse_model_outputs( if "unique_bodypart" in outputs: heatmaps += [h for h in outputs["unique_bodypart"].get("heatmap", [])] - locrefs += [ - strides["unique_bodypart"] * m - for m in outputs["unique_bodypart"].get("locref", []) - ] + locrefs += [strides["unique_bodypart"] * m for m in outputs["unique_bodypart"].get("locref", [])] return image, heatmaps, locrefs, paf -def _prepare_maps_for_plotting( - maps: list[np.ndarray], image_size: tuple[int, int] -) -> np.ndarray | None: +def _prepare_maps_for_plotting(maps: list[np.ndarray], image_size: tuple[int, int]) -> np.ndarray | None: """Resizes all maps to the image size and concatenates them into a single array. Args: @@ -665,10 +633,7 @@ def _prepare_maps_for_plotting( img_w, img_h = image_size return np.stack( - [ - cv2.resize(map_, (img_w, img_h), interpolation=cv2.INTER_LINEAR) - for map_ in maps - ], + [cv2.resize(map_, (img_w, img_h), interpolation=cv2.INTER_LINEAR) for map_ in maps], axis=-1, ) diff --git a/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py b/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py index 6e3ef51604..c49c3823fa 100644 --- a/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py +++ b/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Methods to create the configuration files for PyTorch DeepLabCut models""" + from __future__ import annotations import copy @@ -198,9 +199,7 @@ def make_pytorch_pose_config( return pose_config -def _add_ctd_conditions( - model_cfg: dict, ctd_conditions: int | str | Path | tuple[int, str] | tuple[int, int] -): +def _add_ctd_conditions(model_cfg: dict, ctd_conditions: int | str | Path | tuple[int, str] | tuple[int, int]): """ Args: model_cfg: dict, contents of pytorch_config.yaml @@ -238,9 +237,7 @@ def _add_ctd_conditions( elif isinstance(ctd_conditions[1], str): conditions = {"shuffle": ctd_conditions[0], "snapshot": ctd_conditions[1]} else: - raise TypeError( - "Conditions snapshot must be of type int (index) or string (snapshot name)." - ) + raise TypeError("Conditions snapshot must be of type int (index) or string (snapshot name).") else: raise TypeError("Conditions ctd_conditions is of invalid type.") @@ -351,9 +348,7 @@ def make_basic_project_config( ) -def add_metadata( - project_config: dict, config: dict, pose_config_path: str | Path -) -> dict: +def add_metadata(project_config: dict, config: dict, pose_config_path: str | Path) -> dict: """Adds metadata to a pytorch pose configuration Args: @@ -423,9 +418,7 @@ def create_backbone_with_heatmap_model( bodypart_head_name = "head_topdown.yaml" # add a bodypart head - bodypart_head_config = read_config_as_dict( - configs_dir / "base" / bodypart_head_name - ) + bodypart_head_config = read_config_as_dict(configs_dir / "base" / bodypart_head_name) model_config["model"]["heads"] = { "bodypart": replace_default_values( bodypart_head_config, @@ -463,9 +456,7 @@ def create_backbone_with_paf_model( backbone_output_channels = model_config["model"]["backbone_output_channels"] # add a bodypart head - bodypart_head_config = read_config_as_dict( - configs_dir / "base" / f"head_bodyparts_with_paf.yaml" - ) + bodypart_head_config = read_config_as_dict(configs_dir / "base" / f"head_bodyparts_with_paf.yaml") model_config["model"]["heads"] = { "bodypart": replace_default_values( bodypart_head_config, @@ -574,9 +565,7 @@ def _get_paf_parameters( paf_graph_degree: int = 6, ) -> dict: """Gets values for PAF parameters from the project configuration""" - paf_graph = [ - [i, j] for i in range(len(bodyparts)) for j in range(i + 1, len(bodyparts)) - ] + paf_graph = [[i, j] for i in range(len(bodyparts)) for j in range(i + 1, len(bodyparts))] num_limbs = len(paf_graph) # If the graph is unnecessarily large (with 15+ keypoints by default), # we randomly prune it to a size guaranteeing an average node degree of 6; diff --git a/deeplabcut/pose_estimation_pytorch/config/utils.py b/deeplabcut/pose_estimation_pytorch/config/utils.py index b38d431074..bc1025e2cd 100644 --- a/deeplabcut/pose_estimation_pytorch/config/utils.py +++ b/deeplabcut/pose_estimation_pytorch/config/utils.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Util functions to create pytorch pose configuration files""" + from __future__ import annotations import copy @@ -63,8 +64,7 @@ def get_updated_value(variable: str) -> int | list[int]: var_name = var_parts[0] if updated_values[var_name] is None: raise ValueError( - f"Found {variable} in the configuration file, but there is no default " - f"value for this variable." + f"Found {variable} in the configuration file, but there is no default value for this variable." ) if len(var_parts) == 1: @@ -84,9 +84,7 @@ def get_updated_value(variable: str) -> int | list[int]: else: raise ValueError(f"Unknown operator for variable: {variable}") - raise ValueError( - f"Found {variable} in the configuration file, but cannot parse it." - ) + raise ValueError(f"Found {variable} in the configuration file, but cannot parse it.") updated_values = { "num_bodyparts": num_bodyparts, @@ -112,10 +110,7 @@ def get_updated_value(variable: str) -> int | list[int]: backbone_output_channels, **kwargs, ) - elif ( - isinstance(config[k], str) - and config[k].strip().split(" ")[0] in updated_values.keys() - ): + elif isinstance(config[k], str) and config[k].strip().split(" ")[0] in updated_values.keys(): config[k] = get_updated_value(config[k]) return config @@ -151,9 +146,7 @@ def update_config(config: dict, updates: dict, copy_original: bool = True) -> di return config -def update_config_by_dotpath( - config: dict, updates: dict, copy_original: bool = True -) -> dict: +def update_config_by_dotpath(config: dict, updates: dict, copy_original: bool = True) -> dict: """Updates items in the configuration file using dot notation for nested keys The configuration dict should only be composed of primitive Python types @@ -245,9 +238,7 @@ def available_models() -> list[str]: models.add("top_down_" + backbone) other_architectures = [ - p - for p in configs_folder_path.iterdir() - if p.is_dir() and not p.name in ("backbones", "base", "detectors") + p for p in configs_folder_path.iterdir() if p.is_dir() and not p.name in ("backbones", "base", "detectors") ] for folder in other_architectures: variants = [p.stem for p in folder.iterdir() if p.suffix == ".yaml"] @@ -260,9 +251,7 @@ def available_models() -> list[str]: def is_model_top_down(net_type: str) -> bool: """Checks whenever a given net_type is top-down or not""" if net_type not in available_models(): - raise ValueError( - f"Model {net_type} is not part of available models, which are {str(available_models())}" - ) + raise ValueError(f"Model {net_type} is not part of available models, which are {str(available_models())}") configs_dir = get_config_folder_path() backbones = load_backbones(configs_dir) @@ -285,9 +274,7 @@ def is_model_top_down(net_type: str) -> bool: def is_model_cond_top_down(net_type: str) -> bool: """Checks whether a given net_type is conditional top-down or not""" if net_type not in available_models(): - raise ValueError( - f"Model {net_type} is not part of available models, which are {str(available_models())}" - ) + raise ValueError(f"Model {net_type} is not part of available models, which are {str(available_models())}") if net_type.startswith("ctd_"): return True diff --git a/deeplabcut/pose_estimation_pytorch/data/base.py b/deeplabcut/pose_estimation_pytorch/data/base.py index df5c474c79..8d08ca990e 100644 --- a/deeplabcut/pose_estimation_pytorch/data/base.py +++ b/deeplabcut/pose_estimation_pytorch/data/base.py @@ -124,9 +124,7 @@ def image_filenames(self, mode: str = "train") -> list[str]: data = self._loaded_data[mode] return [image["file_name"] for image in data["images"]] - def ground_truth_keypoints( - self, mode: str = "train", unique_bodypart: bool = False - ) -> dict[str, np.ndarray]: + def ground_truth_keypoints(self, mode: str = "train", unique_bodypart: bool = False) -> dict[str, np.ndarray]: """ Creates a dictionary containing the ground truth data @@ -171,8 +169,7 @@ def ground_truth_keypoints( for image in data["images"]: image_path = image["file_name"] individual_keypoints = { - annotations[i]["individual"]: annotations[i]["keypoints"] - for i in img_to_ann_map[image["id"]] + annotations[i]["individual"]: annotations[i]["keypoints"] for i in img_to_ann_map[image["id"]] } gt_array = np.zeros((len(individuals), num_bodyparts, 3)) # Keep the shape of the ground truth @@ -296,9 +293,7 @@ def filter_annotations(annotations: list[dict], task: Task) -> list[dict]: filtered_annotations = [] for annotation in annotations: keypoints = annotation["keypoints"].reshape(-1, 3) - if task in (Task.DETECT, Task.TOP_DOWN) and ( - annotation["bbox"][2] <= 0 or annotation["bbox"][3] <= 0 - ): + if task in (Task.DETECT, Task.TOP_DOWN) and (annotation["bbox"][2] <= 0 or annotation["bbox"][3] <= 0): continue elif task != Task.DETECT and np.all(keypoints[:, :2] <= 0): continue diff --git a/deeplabcut/pose_estimation_pytorch/data/cocoloader.py b/deeplabcut/pose_estimation_pytorch/data/cocoloader.py index 1fc6ed6f05..3592eb4192 100644 --- a/deeplabcut/pose_estimation_pytorch/data/cocoloader.py +++ b/deeplabcut/pose_estimation_pytorch/data/cocoloader.py @@ -80,9 +80,7 @@ def get_dataset_parameters(self) -> PoseDatasetParameters: bodyparts=bodyparts, unique_bpts=[], individuals=[f"individual{i}" for i in range(num_individuals)], - with_center_keypoints=self.model_cfg.get( - "with_center_keypoints", False - ), + with_center_keypoints=self.model_cfg.get("with_center_keypoints", False), color_mode=self.model_cfg.get("color_mode", "RGB"), top_down_crop_size=(crop_w, crop_h), top_down_crop_margin=crop_margin, @@ -202,10 +200,7 @@ def validate_images(self, coco_json: dict) -> dict: image_ids.add(image["id"]) if len(missing_images) > 0: - warnings.warn( - f"There are {len(missing_images)} images that cannot be found (here" - " are some):" - ) + warnings.warn(f"There are {len(missing_images)} images that cannot be found (here are some):") for img_id, file_name in missing_images.items(): print(f" * {img_id}: {file_name}") @@ -226,8 +221,7 @@ def validate_images(self, coco_json: dict) -> dict: if len(coco_json["annotations"]) < len(validated_annotations): warnings.warn( - f"Found some annotations for which the image ID was not in the images." - f" Removing them from the dataset." + f"Found some annotations for which the image ID was not in the images. Removing them from the dataset." ) print(f" All annotations: {len(coco_json['annotations'])}") print(f" Annotations with correct image IDs: {len(validated_annotations)}") @@ -305,9 +299,7 @@ def get_project_parameters(train_json: dict) -> tuple[int, list[str]]: elif len(img_to_annotations) == 1: num_individuals = len(list(img_to_annotations.values())[0]) else: - num_individuals = max( - *[len(a_ids) for a_ids in img_to_annotations.values()] - ) + num_individuals = max(*[len(a_ids) for a_ids in img_to_annotations.values()]) return num_individuals, bodyparts @@ -349,9 +341,7 @@ def predictions_to_coco( if "bboxes" in pred: coco_pred["bbox"] = pred["bboxes"][idx].reshape(-1).tolist() if "bbox_scores" in pred: - coco_pred["bbox_scores"] = ( - pred["bbox_scores"][idx].reshape(-1).tolist() - ) + coco_pred["bbox_scores"] = pred["bbox_scores"][idx].reshape(-1).tolist() coco_predictions.append(coco_pred) diff --git a/deeplabcut/pose_estimation_pytorch/data/collate.py b/deeplabcut/pose_estimation_pytorch/data/collate.py index 701075ee53..2f1cc884b1 100644 --- a/deeplabcut/pose_estimation_pytorch/data/collate.py +++ b/deeplabcut/pose_estimation_pytorch/data/collate.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Custom collate functions""" + from __future__ import annotations from abc import ABC, abstractmethod @@ -118,7 +119,7 @@ def __init__( max_ratio: float = 2.0, multiple_of: int | None = None, to_square: bool = False, - **kwargs + **kwargs, ) -> None: super().__init__(**kwargs) self.min_scale = min_scale @@ -137,9 +138,7 @@ def _sample_scale(self) -> int | tuple[int, int]: scale = self.generator.uniform(self.min_scale, self.max_scale) if self.to_square: short_side = min(h, w) - size = int(round( - min(self.max_short_side, max(self.min_short_side, scale * short_side)) - )) + size = int(round(min(self.max_short_side, max(self.min_short_side, scale * short_side)))) if self.multiple_of is not None: size = _to_multiple(size, self.multiple_of) return size @@ -149,9 +148,7 @@ def _sample_scale(self) -> int | tuple[int, int]: if ratio > self.max_ratio: ratio = self.max_ratio - short_size = int( - round(min(self.max_short_side, max(self.min_short_side, scale * short))) - ) + short_size = int(round(min(self.max_short_side, max(self.min_short_side, scale * short)))) if h < w: h = short_size w = int(ratio * short_size) diff --git a/deeplabcut/pose_estimation_pytorch/data/ctd.py b/deeplabcut/pose_estimation_pytorch/data/ctd.py index 28cfee1d63..f599263e55 100644 --- a/deeplabcut/pose_estimation_pytorch/data/ctd.py +++ b/deeplabcut/pose_estimation_pytorch/data/ctd.py @@ -81,18 +81,11 @@ def get_loader_and_snapshot( snapshots = loader.snapshots() if len(snapshots) == 0: - raise ValueError( - f"No snapshots found for shuffle={shuffle} in {loader.model_folder}" - ) + raise ValueError(f"No snapshots found for shuffle={shuffle} in {loader.model_folder}") if snapshot_index > len(snapshots): - snapshot_str = "\n".join( - [f" {i}: {s.path.name}" for i, s in enumerate(snapshots)] - ) - raise ValueError( - f"Snapshot index {snapshot_index} is out of range. Existing " - f"snapshots: {snapshot_str}" - ) + snapshot_str = "\n".join([f" {i}: {s.path.name}" for i, s in enumerate(snapshots)]) + raise ValueError(f"Snapshot index {snapshot_index} is out of range. Existing snapshots: {snapshot_str}") bu_snapshot = snapshots[snapshot_index] @@ -129,10 +122,7 @@ def __init__( ) if not filepath.exists(): - raise ValueError( - "Conditions file {conditions_filepath} does not exist. Please check " - f"the given path." - ) + raise ValueError(f"Conditions file {{conditions_filepath}} does not exist. Please check the given path.") self.filepath = filepath @@ -170,8 +160,7 @@ def load_conditions( return self.load_conditions_pickle(self.filepath) raise ValueError( - f"Unknown file suffix {suffix}. Can only read conditions from HDF5 or JSON " - f"files. Received {self.filepath}." + f"Unknown file suffix {suffix}. Can only read conditions from HDF5 or JSON files. Received {self.filepath}." ) @staticmethod @@ -278,8 +267,7 @@ def _parse_row(df_row) -> np.ndarray: missing = image_set.difference(set(conditions.keys())) if len(missing) > 0: print( - f"Warning: did not find conditions for {len(missing)} of the {len(images)} " - f"images. Missing conditions:" + f"Warning: did not find conditions for {len(missing)} of the {len(images)} images. Missing conditions:" ) for img_path in missing: print(f" - {img_path}") @@ -356,8 +344,7 @@ def load_conditions_json( if images is None: if not isinstance(conditions, list): raise ValueError( - f"Conditions are expected to be of type list when `images=None`, " - f"got {type(conditions)}." + f"Conditions are expected to be of type list when `images=None`, got {type(conditions)}." ) parsed = [] @@ -378,9 +365,7 @@ def load_conditions_json( path_with_prefix_to_key = {} if path_prefix is not None: - path_with_prefix_to_key = { - str(Path(path_prefix) / k): k for k in conditions.keys() - } + path_with_prefix_to_key = {str(Path(path_prefix) / k): k for k in conditions.keys()} parsed = {} missing = [] @@ -400,8 +385,7 @@ def load_conditions_json( if len(missing) > 0: print( - f"Warning: did not find conditions for {len(missing)} of the " - f"{len(images)} images. Missing conditions:" + f"Warning: did not find conditions for {len(missing)} of the {len(images)} images. Missing conditions:" ) for img_path in missing: print(f" - {img_path}") diff --git a/deeplabcut/pose_estimation_pytorch/data/dataset.py b/deeplabcut/pose_estimation_pytorch/data/dataset.py index faf0201a73..ffa67ff20c 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dataset.py +++ b/deeplabcut/pose_estimation_pytorch/data/dataset.py @@ -89,12 +89,9 @@ class PoseDataset(Dataset): def __post_init__(self): self.image_path_id_map = map_image_path_to_id(self.images) self.annotation_idx_map = map_id_to_annotations(self.annotations) - self.img_id_to_index = { - img["id"]: index for index, img in enumerate(self.images) - } + self.img_id_to_index = {img["id"]: index for index, img in enumerate(self.images)} if self.task == Task.TOP_DOWN and ( - self.parameters.top_down_crop_size is None - or self.parameters.top_down_crop_margin is None + self.parameters.top_down_crop_size is None or self.parameters.top_down_crop_margin is None ): raise ValueError( "You must specify a ``top_down_crop_size`` and ``top_down_crop_margin``" @@ -106,10 +103,7 @@ def __post_init__(self): if self.task == Task.COND_TOP_DOWN: if self.ctd_config is None: - raise ValueError( - "Must specify a ``ctd_config`` in your PoseDatasetParameters for " - "CTD models." - ) + raise ValueError("Must specify a ``ctd_config`` in your PoseDatasetParameters for CTD models.") self.generative_sampler = GenerativeSampler( self.parameters.num_joints, @@ -200,9 +194,7 @@ def __getitem__(self, index: int) -> dict: # this is applying data augmentations before the cropping # though normalization should be applied after the cropping - transformed = self.apply_transform_all_keypoints( - image, keypoints, keypoints_unique, bboxes - ) + transformed = self.apply_transform_all_keypoints(image, keypoints, keypoints_unique, bboxes) image = transformed["image"] keypoints = transformed["keypoints"] keypoints_unique = transformed["keypoints_unique"] @@ -212,9 +204,7 @@ def __getitem__(self, index: int) -> dict: if self.task in (Task.TOP_DOWN, Task.COND_TOP_DOWN): if self.parameters.top_down_crop_size is None: - raise ValueError( - "You must specify a cropped image size for top-down models" - ) + raise ValueError("You must specify a cropped image size for top-down models") if len(bboxes) > 1 and self.task == Task.TOP_DOWN: raise ValueError( "There can only be one bbox per item in TD datasets, found " @@ -266,12 +256,8 @@ def __getitem__(self, index: int) -> dict: keypoints[:, :, 0] = (keypoints[:, :, 0] - offsets[0]) / scales[0] keypoints[:, :, 1] = (keypoints[:, :, 1] - offsets[1]) / scales[1] if self.task == Task.COND_TOP_DOWN: - synthesized_keypoints[:, 0] = ( - synthesized_keypoints[:, 0] - offsets[0] - ) / scales[0] - synthesized_keypoints[:, 1] = ( - synthesized_keypoints[:, 1] - offsets[1] - ) / scales[1] + synthesized_keypoints[:, 0] = (synthesized_keypoints[:, 0] - offsets[0]) / scales[0] + synthesized_keypoints[:, 1] = (synthesized_keypoints[:, 1] - offsets[1]) / scales[1] keypoints = safe_stack( [keypoints, synthesized_keypoints[None, ...]], (2, 1, self.parameters.num_joints, 3), @@ -282,9 +268,7 @@ def __getitem__(self, index: int) -> dict: bboxes[..., 1] = (bboxes[..., 1] - offsets[1]) / scales[1] bboxes[..., 2] = bboxes[..., 2] / scales[0] bboxes[..., 3] = bboxes[..., 3] / scales[1] - bboxes = np.clip( - bboxes, 0, self.parameters.top_down_crop_size[0] - 1 - ) # TODO: clip based on [x,y,x,y]? + bboxes = np.clip(bboxes, 0, self.parameters.top_down_crop_size[0] - 1) # TODO: clip based on [x,y,x,y]? # RandomBBoxTransform may move keypoints outside the cropped image oob_mask = out_of_bounds_keypoints(keypoints, self.td_crop_size) @@ -331,9 +315,7 @@ def _prepare_final_data_dict( "original_size": np.array(original_size), "offsets": np.array(offsets, dtype=int), "scales": np.array(scales, dtype=float), - "annotations": self._prepare_final_annotation_dict( - keypoints, keypoints_unique, bboxes, annotations_merged - ), + "annotations": self._prepare_final_annotation_dict(keypoints, keypoints_unique, bboxes, annotations_merged), "context": context, } @@ -367,18 +349,14 @@ def _prepare_final_annotation_dict( # we use ..., :3 to pass the visibility flag along return { - "keypoints": pad_to_length(keypoints[..., :3], num_animals, 0).astype( - np.single - ), + "keypoints": pad_to_length(keypoints[..., :3], num_animals, 0).astype(np.single), "keypoints_unique": keypoints_unique[..., :3].astype(np.single), "with_center_keypoints": self.parameters.with_center_keypoints, "area": pad_to_length(area, num_animals, 0).astype(np.single), "boxes": pad_to_length(bboxes, num_animals, 0).astype(np.single), "is_crowd": pad_to_length(is_crowd, num_animals, 0).astype(int), "labels": pad_to_length(labels, num_animals, -1).astype(int), - "individual_ids": pad_to_length(individual_ids, num_animals, -1).astype( - int - ), + "individual_ids": pad_to_length(individual_ids, num_animals, -1).astype(int), } def _get_data_based_on_task(self, index: int) -> tuple[str, list[dict], int]: @@ -434,29 +412,19 @@ def apply_transform_all_keypoints( "bboxes": (4,), } """ - class_labels = [ - f"individual{i}_{bpt}" - for i in range(len(keypoints)) - for bpt in self.parameters.bodyparts - ] + [f"unique_{bpt}" for bpt in self.parameters.unique_bpts] + class_labels = [f"individual{i}_{bpt}" for i in range(len(keypoints)) for bpt in self.parameters.bodyparts] + [ + f"unique_{bpt}" for bpt in self.parameters.unique_bpts + ] all_keypoints = keypoints.reshape(-1, 3) if self.parameters.num_unique_bpts > 0: all_keypoints = np.concatenate([all_keypoints, keypoints_unique], axis=0) - transformed = apply_transform( - self.transform, image, all_keypoints, bboxes, class_labels=class_labels - ) + transformed = apply_transform(self.transform, image, all_keypoints, bboxes, class_labels=class_labels) if self.parameters.num_unique_bpts > 0: - keypoints = transformed["keypoints"][ - : -self.parameters.num_unique_bpts - ].reshape(*keypoints.shape) - keypoints_unique = transformed["keypoints"][ - -self.parameters.num_unique_bpts : - ] - keypoints_unique = keypoints_unique.reshape( - self.parameters.num_unique_bpts, 3 - ) + keypoints = transformed["keypoints"][: -self.parameters.num_unique_bpts].reshape(*keypoints.shape) + keypoints_unique = transformed["keypoints"][-self.parameters.num_unique_bpts :] + keypoints_unique = keypoints_unique.reshape(self.parameters.num_unique_bpts, 3) else: keypoints = transformed["keypoints"].reshape(*keypoints.shape) keypoints_unique = np.zeros((0,)) diff --git a/deeplabcut/pose_estimation_pytorch/data/dlcloader.py b/deeplabcut/pose_estimation_pytorch/data/dlcloader.py index bd61c59f31..5a88bd752d 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dlcloader.py +++ b/deeplabcut/pose_estimation_pytorch/data/dlcloader.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Class implementing the Loader for DeepLabCut projects""" + from __future__ import annotations import logging @@ -70,12 +71,7 @@ def __init__( engine=Engine.PYTORCH, modelprefix=modelprefix, ) - model_config_path = ( - self._project_root - / self._model_folder - / "train" - / Engine.PYTORCH.pose_cfg_name - ) + model_config_path = self._project_root / self._model_folder / "train" / Engine.PYTORCH.pose_cfg_name super().__init__(self._project_root, self._project_root, model_config_path) # lazy-load split and DataFrames @@ -130,9 +126,7 @@ def train_fraction(self) -> float: @property def split(self) -> dict[str, list[int]]: if self._split is None: - self._split = self.load_split( - self._project_config, self._trainset_index, self.shuffle - ) + self._split = self.load_split(self._project_config, self._trainset_index, self.shuffle) return self._split @@ -259,10 +253,7 @@ def load_ground_truth( # load the full dataset file df = pd.read_hdf(trainset_dir / dataset_path) if not isinstance(df, pd.DataFrame): - raise ValueError( - f"The ground truth data in {trainset_dir} must contain a DataFrame! " - f"Found {df}" - ) + raise ValueError(f"The ground truth data in {trainset_dir} must contain a DataFrame! Found {df}") # load the data splits, check that there's nothing suspect dfs = self.split_data(df, self.split) @@ -306,21 +297,14 @@ def load_predictions( parameters: PoseDatasetParameters, ) -> pd.DataFrame: if bu_predictions is None: - pred_path = Path( - str(bu_snapshot).replace("dlc-models", "evaluation-results") - ).parent.parent + pred_path = Path(str(bu_snapshot).replace("dlc-models", "evaluation-results")).parent.parent cfg = af.read_config(pred_path.parent.parent.parent / "config.yaml") scorer = af.get_scorer_name( cfg=cfg, shuffle=int(re.search(r"shuffle(\d+)", str(bu_snapshot)).group(1)), - trainFraction=int( - re.search(r"trainset(\d+)", str(bu_snapshot)).group(1) - ) - / 100, + trainFraction=int(re.search(r"trainset(\d+)", str(bu_snapshot)).group(1)) / 100, engine=Engine.PYTORCH, - trainingsiterations=re.search( - r"snapshot-(.+)\.pth", str(bu_snapshot) - ).group(1), + trainingsiterations=re.search(r"snapshot-(.+)\.pth", str(bu_snapshot)).group(1), modelprefix="", ) @@ -341,9 +325,7 @@ def load_predictions( else: img_path = pred_path.parent.parent / Path(idx) - keypoints = dlc_preds.loc[idx].values.reshape( - -1, len(parameters.bodyparts), 3 - )[..., :2] + keypoints = dlc_preds.loc[idx].values.reshape(-1, len(parameters.bodyparts), 3)[..., :2] keypoints = keypoints[~np.isnan(keypoints).all(axis=-1).all(axis=-1)] cond_keypoints = np.zeros((*keypoints.shape[:-1], 3)) cond_keypoints[..., :2] = keypoints @@ -394,9 +376,7 @@ def to_coco( the coco format data """ with_individuals = "individuals" in df.columns.names - if not with_individuals and ( - len(parameters.individuals) > 1 or len(parameters.unique_bpts) > 0 - ): + if not with_individuals and (len(parameters.individuals) > 1 or len(parameters.unique_bpts) > 0): raise ValueError( "The DataFrame contains single-animal annotations (for a single, " "individual), but the parameters suggest this is a multi-animal project" @@ -491,15 +471,9 @@ def to_coco( def _add_bbox_annotations(coco_dict: dict) -> dict: for annotation in coco_dict.get("annotations", []): if "bbox" not in annotation: - image = [ - img - for img in coco_dict.get("images") - if img.get("id") == annotation.get("image_id") - ][0] + image = [img for img in coco_dict.get("images") if img.get("id") == annotation.get("image_id")][0] bbox = bbox_from_keypoints( - keypoints=np.array( - annotation["keypoints"] - ), # (..., num_keypoints, xy) + keypoints=np.array(annotation["keypoints"]), # (..., num_keypoints, xy) image_h=image.get("height"), image_w=image.get("width"), margin=20, @@ -545,9 +519,7 @@ def _load_mat_dataset( dlc_dataset: the dataset in a DLC-format DataFrame """ if not params.max_num_animals == 1: - raise RuntimeError( - f"Cannot load a multi-animal pose dataset from a `.mat` file ({file})" - ) + raise RuntimeError(f"Cannot load a multi-animal pose dataset from a `.mat` file ({file})") raw_data = sio.loadmat(str(file)) dataset = raw_data["dataset"] @@ -635,11 +607,7 @@ def _load_pickle_dataset( keypoints[idv_idx, bodypart, 0] = x keypoints[idv_idx, bodypart, 1] = y - elif ( - idv_idx == params.max_num_animals - and data_unique is not None - and keypoints_unique is None - ): + elif idv_idx == params.max_num_animals and data_unique is not None and keypoints_unique is None: keypoints_unique = np.zeros((params.num_unique_bpts, 2)) keypoints_unique.fill(np.nan) for joint_id, x, y in idv_bodyparts: @@ -708,14 +676,10 @@ def _validate_dataframes( extra_images = hdf_train_images - pickle_train_images if len(missing_images) > 0: error = True - logging.debug( - f"Found images in the dataset file which were not in H5: {missing_images}" - ) + logging.debug(f"Found images in the dataset file which were not in H5: {missing_images}") if len(extra_images) > 0: error = True - logging.debug( - f"Found images in the H5 file which were not in the dataset: {extra_images}" - ) + logging.debug(f"Found images in the H5 file which were not in the dataset: {extra_images}") # checks that the data is close for the similar images train_index = list(hdf_train_images.intersection(pickle_train_images)) diff --git a/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py b/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py index 84fe6ae01a..7d717894a9 100644 --- a/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py +++ b/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py @@ -40,6 +40,7 @@ kps_symmetry = [] kps_sigmas = np.array([1.] * num_kpts)/10.0 """ + from __future__ import annotations import math @@ -68,6 +69,7 @@ class GenSamplingConfig: miss_prob: The probability of applying a miss error. Miss error represents a large displacement from the GT keypoint position. """ + bbox_margin: int keypoint_sigmas: float | list[float] = 0.1 keypoints_symmetry: list[tuple[int, int]] | None = None @@ -171,7 +173,6 @@ def __call__( N = 500 # TODO: do not know how this is set for j in range(self.num_keypoints): - # source keypoint position candidates to generate error on that (gt, swap, inv, swap+inv) coord_list = [] # on top of gt @@ -207,9 +208,7 @@ def __call__( coord_list.append(np.empty([0, 2])) if pair_idx is not None: - swap_inv_coord = near_keypoints[ - near_keypoints[:, pair_idx, 2] > 0, pair_idx, :2 - ] + swap_inv_coord = near_keypoints[near_keypoints[:, pair_idx, 2] > 0, pair_idx, :2] coord_list.append(swap_inv_coord) else: coord_list.append(np.empty([0, 2])) @@ -234,11 +233,7 @@ def __call__( continue dist_mask = np.logical_and( dist_mask, - np.sqrt( - (tot_coord_list[i][0] - x) ** 2 - + (tot_coord_list[i][1] - y) ** 2 - ) - > r, + np.sqrt((tot_coord_list[i][0] - x) ** 2 + (tot_coord_list[i][1] - y) ** 2) > r, ) x = x[dist_mask].reshape(-1) @@ -265,11 +260,7 @@ def __call__( continue dist_mask = np.logical_and( dist_mask, - np.sqrt( - (tot_coord_list[i][0] - x) ** 2 - + (tot_coord_list[i][1] - y) ** 2 - ) - > ks_50_dist[j], + np.sqrt((tot_coord_list[i][0] - x) ** 2 + (tot_coord_list[i][1] - y) ** 2) > ks_50_dist[j], ) x = x[dist_mask].reshape(-1) y = y[dist_mask].reshape(-1) @@ -305,11 +296,7 @@ def __call__( continue dist_mask = np.logical_and( dist_mask, - np.sqrt( - (tot_coord_list[i][0] - x) ** 2 - + (tot_coord_list[i][1] - y) ** 2 - ) - > r, + np.sqrt((tot_coord_list[i][0] - x) ** 2 + (tot_coord_list[i][1] - y) ** 2) > r, ) x = x[dist_mask].reshape(-1) y = y[dist_mask].reshape(-1) @@ -327,9 +314,7 @@ def __call__( if swap_exist: swap_pt_list = [] for swap_idx in range(len(tot_coord_list)): - if swap_idx == 0 or swap_idx == len(coord_list[0]) + len( - coord_list[1] - ): + if swap_idx == 0 or swap_idx == len(coord_list[0]) + len(coord_list[1]): continue angle = np.random.uniform(0, 2 * math.pi, [N]) r = np.random.uniform(0, ks_50_dist[j], [N]) @@ -340,11 +325,7 @@ def __call__( if i == 0 or i == len(coord_list[0]) + len(coord_list[1]): dist_mask = np.logical_and( dist_mask, - np.sqrt( - (tot_coord_list[i][0] - x) ** 2 - + (tot_coord_list[i][1] - y) ** 2 - ) - > r, + np.sqrt((tot_coord_list[i][0] - x) ** 2 + (tot_coord_list[i][1] - y) ** 2) > r, ) x = x[dist_mask].reshape(-1) y = y[dist_mask].reshape(-1) @@ -374,11 +355,7 @@ def __call__( continue dist_mask = np.logical_and( dist_mask, - np.sqrt( - (tot_coord_list[i][0] - x) ** 2 - + (tot_coord_list[i][1] - y) ** 2 - ) - > r, + np.sqrt((tot_coord_list[i][0] - x) ** 2 + (tot_coord_list[i][1] - y) ** 2) > r, ) x = x[dist_mask].reshape(-1) diff --git a/deeplabcut/pose_estimation_pytorch/data/image.py b/deeplabcut/pose_estimation_pytorch/data/image.py index f62548c2ed..58070d3758 100644 --- a/deeplabcut/pose_estimation_pytorch/data/image.py +++ b/deeplabcut/pose_estimation_pytorch/data/image.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Classes and functions to manipulate images""" + from __future__ import annotations import copy @@ -118,10 +119,9 @@ def get_resize_preserve_ratio( h = max_long_side return h, w - + def scale_kpts( - keypoints: np.ndarray, kpt_scale: np.ndarray, kpt_offset: np.ndarray, - tgt_h: int, tgt_w: int + keypoints: np.ndarray, kpt_scale: np.ndarray, kpt_offset: np.ndarray, tgt_h: int, tgt_w: int ) -> np.ndarray: scaled_kpts = keypoints.copy() scaled_kpts[..., :2] = (scaled_kpts[..., :2] / kpt_scale) - kpt_offset @@ -135,9 +135,7 @@ def scale_kpts( h, w = get_resize_hw((oh, ow), tgt_short_side=size, max_long_side=max_size) tgt_h, tgt_w = size, size else: - h, w = get_resize_preserve_ratio( - oh, ow, size[0], size[1], max_long_side=max_size - ) + h, w = get_resize_preserve_ratio(oh, ow, size[0], size[1], max_long_side=max_size) tgt_h, tgt_w = size scale_x, scale_y = ow / w, oh / h @@ -181,7 +179,7 @@ def scale_kpts( cond_keypoints = context.get("cond_keypoints") if cond_keypoints is not None and len(cond_keypoints) > 0: context["cond_keypoints"] = scale_kpts(cond_keypoints, kpt_scale, kpt_offset, tgt_h, tgt_w) - + bbox_scale = np.array([scale_x, scale_y, scale_x, scale_y]) bbox_offset = np.array([offset_x, offset_y, 0, 0]) for bbox_key in ["boxes"]: @@ -189,7 +187,9 @@ def scale_kpts( if boxes is not None and len(boxes) > 0: scaled_boxes = (boxes / bbox_scale) - bbox_offset scaled_boxes = _compute_crop_bounds( - scaled_boxes, (tgt_h, tgt_w, 3), remove_empty=False, + scaled_boxes, + (tgt_h, tgt_w, 3), + remove_empty=False, ) anns[bbox_key] = scaled_boxes @@ -292,7 +292,7 @@ def top_down_crop( # crop the pixels we care about image_crop = np.zeros((h + pad_y, w + pad_x, c), dtype=image.dtype) - image_crop[pad_top:pad_top + h, pad_left:pad_left + w] = image[y1:y2, x1:x2] + image_crop[pad_top : pad_top + h, pad_left : pad_left + w] = image[y1:y2, x1:x2] # resize the cropped image image = cv2.resize(image_crop, (out_w, out_h), interpolation=cv2.INTER_LINEAR) diff --git a/deeplabcut/pose_estimation_pytorch/data/postprocessor.py b/deeplabcut/pose_estimation_pytorch/data/postprocessor.py index 7957f90a80..a808d93cd1 100644 --- a/deeplabcut/pose_estimation_pytorch/data/postprocessor.py +++ b/deeplabcut/pose_estimation_pytorch/data/postprocessor.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Post-process predictions made by models""" + from __future__ import annotations from abc import ABC, abstractmethod @@ -202,23 +203,23 @@ def build_detector_postprocessor( A default top-down Postprocessor """ components = [ - ConcatenateOutputs( - keys_to_concatenate={ - "bboxes": ("detection", "bboxes"), - "bbox_scores": ("detection", "scores"), - } - ), - TrimOutputs( - max_individuals={ - "bboxes": max_individuals, - "bbox_scores": max_individuals, - }, - ), - BboxToCoco(bounding_box_keys=["bboxes"]), - RescaleAndOffset( - keys_to_rescale=["bboxes"], - mode=RescaleAndOffset.Mode.BBOX_XYWH, - ) + ConcatenateOutputs( + keys_to_concatenate={ + "bboxes": ("detection", "bboxes"), + "bbox_scores": ("detection", "scores"), + } + ), + TrimOutputs( + max_individuals={ + "bboxes": max_individuals, + "bbox_scores": max_individuals, + }, + ), + BboxToCoco(bounding_box_keys=["bboxes"]), + RescaleAndOffset( + keys_to_rescale=["bboxes"], + mode=RescaleAndOffset.Mode.BBOX_XYWH, + ), ] if min_bbox_score is not None: components.append(RemoveLowConfidenceBoxes(min_bbox_score)) @@ -260,22 +261,15 @@ def __init__( f" when create_empty_outputs is true, found {self.empty_shapes}" ) - def __call__( - self, predictions: Any, context: Context - ) -> tuple[dict[str, np.ndarray], Context]: + def __call__(self, predictions: Any, context: Context) -> tuple[dict[str, np.ndarray], Context]: if len(predictions) == 0: - outputs = { - name: np.zeros((0, *self.empty_shapes[name])) - for name in self.keys_to_concatenate.keys() - } + outputs = {name: np.zeros((0, *self.empty_shapes[name])) for name in self.keys_to_concatenate.keys()} return outputs, context outputs = {} for output_name, head_key in self.keys_to_concatenate.items(): head_name, val_name = head_key - outputs[output_name] = np.concatenate( - [p[head_name][val_name] for p in predictions] - ) + outputs[output_name] = np.concatenate([p[head_name][val_name] for p in predictions]) return outputs, context @@ -293,17 +287,13 @@ def __init__( 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]: + def __call__(self, predictions: dict[str, np.ndarray], context: Context) -> tuple[dict[str, np.ndarray], Context]: for name in predictions: output = predictions[name] 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 + expected_ndim = 1 + len(expected_shape) # individuals_dimension + expected shape for single individual # Special handling for empty arrays if len(output) == 0: @@ -311,15 +301,10 @@ def __call__( 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] - ): + 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), dtype=output.dtype - ) + padding = self.pad_value * np.ones((pad_size, *tail_shape), dtype=output.dtype) output = np.concatenate([output, padding], axis=0) predictions[name] = output @@ -337,9 +322,7 @@ class TrimOutputs(Postprocessor): def __init__(self, max_individuals: dict[str, int]): self.max_individuals = max_individuals - def __call__( - self, predictions: dict[str, np.ndarray], context: Context - ) -> tuple[dict[str, np.ndarray], Context]: + def __call__(self, predictions: dict[str, np.ndarray], context: Context) -> tuple[dict[str, np.ndarray], Context]: for name in predictions: output = predictions[name] if len(output) > self.max_individuals[name]: @@ -380,9 +363,7 @@ def __init__( self.keys_to_rescale = keys_to_rescale self.mode = mode - def __call__( - self, predictions: dict[str, np.ndarray], context: Context - ) -> tuple[dict[str, np.ndarray], Context]: + def __call__(self, predictions: dict[str, np.ndarray], context: Context) -> tuple[dict[str, np.ndarray], Context]: if "scales" not in context and "offsets" not in context: # no rescaling needed return predictions, context @@ -428,13 +409,9 @@ def __call__( kpt_score_sums = np.sum(kpt_scores, axis=1) idv_scores = kpt_score_sums / num_valid_kpts - cond_kpt_scores = np.mean( - context["cond_kpts"][:, :, 2], axis=1 - ) + cond_kpt_scores = np.mean(context["cond_kpts"][:, :, 2], axis=1) - rescaled[:, :, 2] = (cond_kpt_scores * idv_scores).reshape( - -1, 1 - ) + rescaled[:, :, 2] = (cond_kpt_scores * idv_scores).reshape(-1, 1) updated_predictions[name] = rescaled else: @@ -453,9 +430,7 @@ def __init__(self, bbox_score_thresh: float): logging.info("utilizing low confidence bbox filtering") self.bbox_score_thresh = bbox_score_thresh - def __call__( - self, predictions: dict[str, np.ndarray], context: Context - ) -> tuple[dict[str, np.ndarray], Context]: + def __call__(self, predictions: dict[str, np.ndarray], context: Context) -> tuple[dict[str, np.ndarray], Context]: above_threshold = predictions["bbox_scores"] >= self.bbox_score_thresh keepers = np.where(above_threshold) if any(~above_threshold): @@ -471,9 +446,7 @@ def __init__(self, bounding_box_keys: list[str]) -> None: super().__init__() self.bounding_box_keys = bounding_box_keys - def __call__( - self, predictions: dict[str, np.ndarray], context: Context - ) -> tuple[dict[str, np.ndarray], Context]: + def __call__(self, predictions: dict[str, np.ndarray], context: Context) -> tuple[dict[str, np.ndarray], Context]: for bbox_key in self.bounding_box_keys: predictions[bbox_key][:, 2] -= predictions[bbox_key][:, 0] predictions[bbox_key][:, 3] -= predictions[bbox_key][:, 1] @@ -528,9 +501,7 @@ def __init__( self.pose_key = pose_key self.keep_id_maps = keep_id_maps - def __call__( - self, predictions: dict[str, np.ndarray], context: Context - ) -> tuple[dict[str, np.ndarray], Context]: + def __call__(self, predictions: dict[str, np.ndarray], context: Context) -> tuple[dict[str, np.ndarray], Context]: pose = predictions[self.pose_key] num_preds, num_keypoints, _ = pose.shape @@ -568,9 +539,7 @@ def __init__(self, identity_key: str, pose_key: str) -> None: self.identity_key = identity_key self.pose_key = pose_key - def __call__( - self, predictions: dict[str, np.ndarray], context: Context - ) -> tuple[dict[str, np.ndarray], Context]: + def __call__(self, predictions: dict[str, np.ndarray], context: Context) -> tuple[dict[str, np.ndarray], Context]: map_ = assign_identity(predictions["bodyparts"], predictions["identity_scores"]) predictions["bodyparts"] = predictions["bodyparts"][map_] predictions["identity_scores"] = predictions["identity_scores"][map_] diff --git a/deeplabcut/pose_estimation_pytorch/data/preprocessor.py b/deeplabcut/pose_estimation_pytorch/data/preprocessor.py index 5cb66bac9c..23bac25a5e 100644 --- a/deeplabcut/pose_estimation_pytorch/data/preprocessor.py +++ b/deeplabcut/pose_estimation_pytorch/data/preprocessor.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Helpers to run preprocess data before running inference""" + from __future__ import annotations from abc import ABC, abstractmethod @@ -54,9 +55,7 @@ def __call__(self, image: Image, context: Context) -> tuple[Image, Context]: pass -def build_bottom_up_preprocessor( - color_mode: str, transform: A.BaseCompose -) -> Preprocessor: +def build_bottom_up_preprocessor(color_mode: str, transform: A.BaseCompose) -> Preprocessor: """Creates a preprocessor for bottom-up pose estimation (or object detection) Creates a preprocessor that loads an image, runs some transform on it (such as @@ -239,9 +238,7 @@ def update_offset( ) @staticmethod - def update_scale( - scale: tuple[float, float], new_scale: tuple[float, float] - ) -> tuple[float, float]: + def update_scale(scale: tuple[float, float], new_scale: tuple[float, float]) -> tuple[float, float]: return scale[0] * new_scale[0], scale[1] * new_scale[1] @staticmethod @@ -257,20 +254,14 @@ def update_offsets_and_scales(context, new_offsets, new_scales) -> tuple: if isinstance(offsets, tuple): if isinstance(new_offsets, list): updated_offsets = [ - AugmentImage.update_offset(offsets, scales, new_offset) - for new_offset in new_offsets - ] - updated_scales = [ - AugmentImage.update_scale(scales, new_scale) - for new_scale in new_scales + AugmentImage.update_offset(offsets, scales, new_offset) for new_offset in new_offsets ] + updated_scales = [AugmentImage.update_scale(scales, new_scale) for new_scale in new_scales] else: if not len(offsets) == len(new_offsets): raise ValueError("Cannot rescale lists when not same length") - updated_offsets = AugmentImage.update_offset( - offsets, scales, new_offsets - ) + updated_offsets = AugmentImage.update_offset(offsets, scales, new_offsets) updated_scales = AugmentImage.update_scale(scales, new_scales) else: if isinstance(new_offsets, list): @@ -282,17 +273,13 @@ def update_offsets_and_scales(context, new_offsets, new_scales) -> tuple: for offset, scale, new_offset in zip(offsets, scales, new_offsets) ] updated_scales = [ - AugmentImage.update_scale(scale, new_scale) - for scale, new_scale in zip(scales, new_scales) + AugmentImage.update_scale(scale, new_scale) for scale, new_scale in zip(scales, new_scales) ] else: updated_offsets = [ - AugmentImage.update_offset(offset, scale, new_offsets) - for offset, scale in zip(offsets, scales) - ] - updated_scales = [ - AugmentImage.update_scale(scale, new_scales) for scale in scales + AugmentImage.update_offset(offset, scale, new_offsets) for offset, scale in zip(offsets, scales) ] + updated_scales = [AugmentImage.update_scale(scale, new_scales) for scale in scales] return updated_offsets, updated_scales def __call__(self, image: Image, context: Context) -> tuple[np.ndarray, Context]: @@ -375,9 +362,7 @@ def __init__( self.confidence_threshold = confidence_threshold self.aggregate_func = aggregate_func - def __call__( - self, image: np.ndarray, context: Context - ) -> tuple[np.ndarray, Context]: + def __call__(self, image: np.ndarray, context: Context) -> tuple[np.ndarray, Context]: if "cond_kpts" not in context: raise ValueError(f"Must include cond_kpts, found {context}") @@ -401,9 +386,7 @@ class FilterInvalidBoundingBoxes(Preprocessor): def __init__(self, min_area: int = 1) -> None: self.min_area = min_area - def __call__( - self, image: np.ndarray, context: Context - ) -> tuple[np.ndarray, Context]: + def __call__(self, image: np.ndarray, context: Context) -> tuple[np.ndarray, Context]: bboxes = context.get("bboxes", []) keypoints = context.get("cond_kpts", []) @@ -444,9 +427,7 @@ def __init__( self.margin = margin self.with_context = with_context - def __call__( - self, image: np.ndarray, context: Context - ) -> tuple[np.ndarray, Context]: + def __call__(self, image: np.ndarray, context: Context) -> tuple[np.ndarray, Context]: """TODO: numpy implementation""" if "bboxes" not in context: raise ValueError(f"Must include bboxes to CropDetections, found {context}") @@ -491,37 +472,29 @@ def __init__(self, cond_kpt_key: str = "cond_kpts", bbox_margin: int = 0) -> Non self.cond_kpt_key = cond_kpt_key self.bbox_margin = bbox_margin - def __call__( - self, image: np.ndarray, context: Context - ) -> tuple[np.ndarray, Context]: + def __call__(self, image: np.ndarray, context: Context) -> tuple[np.ndarray, Context]: """TODO: numpy implementation""" if "cond_kpts" not in context: - raise ValueError( - f"Must include cond kpts to ComputeBBoxes, found {context}" - ) + raise ValueError(f"Must include cond kpts to ComputeBBoxes, found {context}") h, w = image.shape[:2] context["bboxes"] = [ - bbox_from_keypoints(cond_kpts, h, w, self.bbox_margin) - for cond_kpts in context[self.cond_kpt_key] + bbox_from_keypoints(cond_kpts, h, w, self.bbox_margin) for cond_kpts in context[self.cond_kpt_key] ] return image, context class ConditionalKeypointsToModelInputs(Preprocessor): - def __init__(self, cond_kpt_key: str = "cond_kpts") -> None: self.cond_kpt_key = cond_kpt_key - def __call__( - self, image: np.ndarray, context: Context - ) -> tuple[np.ndarray, Context]: + def __call__(self, image: np.ndarray, context: Context) -> tuple[np.ndarray, Context]: cond_keypoints = context[self.cond_kpt_key] rescaled = cond_keypoints.copy() if rescaled.size > 0: # only rescale if non-empty - rescaled[..., :2] = ( - rescaled[..., :2] - np.array(context["offsets"])[:, None] - ) / np.array(context["scales"])[:, None] + 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 diff --git a/deeplabcut/pose_estimation_pytorch/data/snapshots.py b/deeplabcut/pose_estimation_pytorch/data/snapshots.py index bb9edff25a..2ef9ef2fae 100644 --- a/deeplabcut/pose_estimation_pytorch/data/snapshots.py +++ b/deeplabcut/pose_estimation_pytorch/data/snapshots.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Code to handle storing models""" + from __future__ import annotations import re @@ -38,7 +39,7 @@ def uid(self) -> str: def from_path(path: Path) -> "Snapshot": best = "-best" in path.stem # Use regex to extract epoch number more robustly - match = re.search(r'-(\d+)\.pt$', path.name) + match = re.search(r"-(\d+)\.pt$", path.name) if match: epochs = int(match.group(1)) else: @@ -65,6 +66,7 @@ def list_snapshots( trained for. If ``best_in_last=True`` and a best snapshot exists, it will be the last one in the list. """ + def _sort_key(snapshot: Snapshot) -> int: return snapshot.epochs @@ -72,9 +74,7 @@ def _sort_key_best_as_last(snapshot: Snapshot) -> tuple[int, int]: return 1 if snapshot.best else 0, snapshot.epochs pattern = r"^(" + snapshot_prefix + r"(-best)?-\d+\.pt)$" - snapshots = [ - Snapshot.from_path(f) for f in model_folder.iterdir() if re.match(pattern, f.name) - ] + snapshots = [Snapshot.from_path(f) for f in model_folder.iterdir() if re.match(pattern, f.name)] sort_key = _sort_key if best_in_last: diff --git a/deeplabcut/pose_estimation_pytorch/data/transforms.py b/deeplabcut/pose_estimation_pytorch/data/transforms.py index cb321a0aca..4d06175f8e 100644 --- a/deeplabcut/pose_estimation_pytorch/data/transforms.py +++ b/deeplabcut/pose_estimation_pytorch/data/transforms.py @@ -48,10 +48,7 @@ def build_transforms(augmentations: dict) -> A.BaseCompose: if symmetries is not None: transforms.append(HFlip(symmetries=symmetries, p=hflip_proba)) else: - warnings.warn( - "Be careful! Do not train pose models with horizontal flips if you have" - " symmetric keypoints!" - ) + warnings.warn("Be careful! Do not train pose models with horizontal flips if you have symmetric keypoints!") transforms.append(A.HorizontalFlip(p=hflip_proba)) if (affine := augmentations.get("affine")) is not None: @@ -139,18 +136,14 @@ def build_transforms(augmentations: dict) -> A.BaseCompose: transforms.append(build_auto_padding(**augmentations["auto_padding"])) if augmentations.get("normalize_images"): - transforms.append( - A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) - ) + transforms.append(A.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])) if augmentations.get("scale_to_unit_range"): transforms.append(ScaleToUnitRange()) return A.Compose( transforms, - keypoint_params=A.KeypointParams( - "xy", remove_invisible=False, label_fields=["class_labels"] - ), + keypoint_params=A.KeypointParams("xy", remove_invisible=False, label_fields=["class_labels"]), bbox_params=A.BboxParams(format="coco", label_fields=["bbox_labels"]), ) @@ -192,8 +185,7 @@ def build_auto_padding( } if border_mode not in border_modes: raise ValueError( - f"Unknown border mode for auto_padding: {border_mode} " - f"(valid values are: {border_modes.keys()})" + f"Unknown border mode for auto_padding: {border_mode} (valid values are: {border_modes.keys()})" ) return A.PadIfNeeded( @@ -238,10 +230,7 @@ def __init__(self, symmetries: list[tuple[int, int]], *args, **kwargs) -> None: self._symmetries[j] = i def apply_to_keypoints(self, keypoints, **params): - swapped_keypoints = [ - keypoints[self._symmetries.get(kpt_idx, kpt_idx)] - for kpt_idx in range(len(keypoints)) - ] + swapped_keypoints = [keypoints[self._symmetries.get(kpt_idx, kpt_idx)] for kpt_idx in range(len(keypoints))] return super().apply_to_keypoints(swapped_keypoints, **params) @@ -273,8 +262,7 @@ def __init__( self.max_shift = max(0.0, min(max_shift, 0.4)) if crop_sampling not in ("uniform", "keypoints", "density", "hybrid"): raise ValueError( - f"Invalid sampling {crop_sampling}. Must be " - f"either 'uniform', 'keypoints', 'density', or 'hybrid." + f"Invalid sampling {crop_sampling}. Must be either 'uniform', 'keypoints', 'density', or 'hybrid." ) self.crop_sampling = crop_sampling @@ -477,12 +465,8 @@ def __init__( self._neighbor_dist = 3 self._neighbor_dist_square = self._neighbor_dist**2 - def apply_to_keypoints( - self, keypoints: Sequence[float], random_state: int | None = None, **params - ) -> list[float]: - heatmaps = np.zeros( - (params["rows"], params["cols"], len(keypoints)), dtype=np.float32 - ) + def apply_to_keypoints(self, keypoints: Sequence[float], random_state: int | None = None, **params) -> list[float]: + heatmaps = np.zeros((params["rows"], params["cols"], len(keypoints)), dtype=np.float32) grid = np.mgrid[: params["rows"], : params["cols"]].transpose((1, 2, 0)) kpts = np.array([(k[1], k[0]) for k in keypoints]) valid_kpts = np.all(kpts > 0.0, axis=1) diff --git a/deeplabcut/pose_estimation_pytorch/data/utils.py b/deeplabcut/pose_estimation_pytorch/data/utils.py index 5abf776f6f..65c2d1ec4d 100644 --- a/deeplabcut/pose_estimation_pytorch/data/utils.py +++ b/deeplabcut/pose_estimation_pytorch/data/utils.py @@ -78,9 +78,7 @@ def bbox_from_keypoints( return bboxes -def merge_list_of_dicts( - list_of_dicts: list[dict], keys_to_include: list[str] -) -> dict[str, list]: +def merge_list_of_dicts(list_of_dicts: list[dict], keys_to_include: list[str]) -> dict[str, list]: """ Flattens a list of dictionaries into a dictionary with the lists concatenated. @@ -99,11 +97,7 @@ def merge_list_of_dicts( {"id": [0, 1], "num": [1, 10]} """ return reduce( - lambda acc, d: { - key: acc.get(key, []) + [value] - for key, value in d.items() - if key in keys_to_include - }, + lambda acc, d: {key: acc.get(key, []) + [value] for key, value in d.items() if key in keys_to_include}, list_of_dicts, defaultdict(list), ) @@ -196,9 +190,7 @@ def _crop_and_pad_image( return pad_image, (pad_h, pad_w) -def _crop_and_pad_keypoints( - keypoints: np.ndarray, coords: tuple[int, int], pad_size: tuple[int, int] -): +def _crop_and_pad_keypoints(keypoints: np.ndarray, coords: tuple[int, int], pad_size: tuple[int, int]): """ Adjust the keypoints after cropping and padding. @@ -242,9 +234,7 @@ def _crop_image_keypoints( """ cropped_image, pad_size = _crop_and_pad_image(image, coords, output_size) - cropped_keypoints = _crop_and_pad_keypoints( - keypoints, (coords[0][0], coords[1][0]), pad_size - ) + cropped_keypoints = _crop_and_pad_keypoints(keypoints, (coords[0][0], coords[1][0]), pad_size) offsets = (coords[0][0], coords[1][0]) scales = [ @@ -253,9 +243,7 @@ def _crop_image_keypoints( ] # TODO: Fix resizing, use OpenCV - cropped_resized_image = np.resize( - cropped_image, (*output_size, cropped_image.shape[2]) - ) + cropped_resized_image = np.resize(cropped_image, (*output_size, cropped_image.shape[2])) cropped_resized_keypoints = np.array(cropped_keypoints) * np.array(scales + [1]) @@ -443,9 +431,7 @@ def apply_transform( if transform: oob_mask = out_of_bounds_keypoints(keypoints, image.shape) - transformed = _apply_transform( - transform, image, keypoints, bboxes, class_labels - ) + transformed = _apply_transform(transform, image, keypoints, bboxes, class_labels) transformed["keypoints"] = np.array(transformed["keypoints"]) diff --git a/deeplabcut/pose_estimation_pytorch/metrics/scoring.py b/deeplabcut/pose_estimation_pytorch/metrics/scoring.py index 317bc87c13..95dd1c7126 100644 --- a/deeplabcut/pose_estimation_pytorch/metrics/scoring.py +++ b/deeplabcut/pose_estimation_pytorch/metrics/scoring.py @@ -18,9 +18,7 @@ from deeplabcut.utils.auxiliaryfunctions import read_config -def _match_identity_preds_to_gt( - config_path: str, full_pickle_path: str -) -> tuple[np.ndarray, list]: +def _match_identity_preds_to_gt(config_path: str, full_pickle_path: str) -> tuple[np.ndarray, list]: with open(full_pickle_path, "rb") as f: data = pickle.load(f) metadata = data.pop("metadata") @@ -54,9 +52,7 @@ def _match_identity_preds_to_gt( found = neighbors != -1 inds = np.flatnonzero(all_bpts == bpt) id_ = dict_["prediction"]["identity"][n_joint] - ids[i, inds[inds_gt[found]], 1] = np.argmax( - id_[neighbors[found]], axis=1 - ) + ids[i, inds[inds_gt[found]], 1] = np.argmax(id_[neighbors[found]], axis=1) ids = ids[:, :n_multibodyparts].reshape((len(data), len(cfg["individuals"]), -1, 2)) return ids, list(data) diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/base.py b/deeplabcut/pose_estimation_pytorch/models/backbones/base.py index bf2febe9ec..0b32314f16 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/base.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/base.py @@ -121,11 +121,7 @@ def download_weights(self, filename: str, force: bool = False) -> Path: logging.info(f"Downloading the pre-trained backbone to {model_path}") self.backbone_weight_folder.mkdir(exist_ok=True, parents=False) - output_path = Path( - hf_hub_download( - self.repo_id, filename, cache_dir=self.backbone_weight_folder - ) - ) + output_path = Path(hf_hub_download(self.repo_id, filename, cache_dir=self.backbone_weight_folder)) # resolve gets the actual path if the output path is a symlink output_path = output_path.resolve() diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py b/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py index f3e23731a4..4a13491a83 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py @@ -57,21 +57,15 @@ def __init__( self.cond_enc = kpt_encoder self.backbone = backbone - self.rgb_preNet = self._make_preNet( - num_inputs=3, num_outputs=3, input_image=True - ) - self.cond_preNet = self._make_preNet( - num_inputs=self.cond_enc.num_channels, num_outputs=3, input_image=False - ) + self.rgb_preNet = self._make_preNet(num_inputs=3, num_outputs=3, input_image=True) + self.cond_preNet = self._make_preNet(num_inputs=self.cond_enc.num_channels, num_outputs=3, input_image=False) self.init_weights() def _make_preNet(self, num_inputs, num_outputs, input_image=False): if not input_image: # cond preNet = nn.Sequential( - nn.Conv2d( - num_inputs, num_outputs, kernel_size=7, stride=1, padding="same" - ), + nn.Conv2d(num_inputs, num_outputs, kernel_size=7, stride=1, padding="same"), nn.BatchNorm2d(num_outputs), ) else: diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/cspnext.py b/deeplabcut/pose_estimation_pytorch/models/backbones/cspnext.py index 50718940b9..3450e1195b 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/cspnext.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/cspnext.py @@ -16,6 +16,7 @@ For more details about this architecture, see `RTMDet: An Empirical Study of Designing Real-Time Object Detectors`: https://arxiv.org/abs/1711.05101. """ + from dataclasses import dataclass import torch @@ -36,6 +37,7 @@ @dataclass(frozen=True) class CSPNeXtLayerConfig: """Configuration for a CSPNeXt layer""" + in_channels: int out_channels: int num_blocks: int @@ -79,7 +81,7 @@ class CSPNeXt(HuggingFaceWeightsMixin, BaseBackbone): CSPNeXtLayerConfig(256, 512, 6, True, False), CSPNeXtLayerConfig(512, 768, 3, True, False), CSPNeXtLayerConfig(768, 1024, 3, False, True), - ] + ], } def __init__( @@ -98,10 +100,7 @@ def __init__( ) -> None: super().__init__(stride=32, **kwargs) if arch not in self.ARCH: - raise ValueError( - f"Unknown `CSPNeXT` architecture: {arch}. Must be one of " - f"{self.ARCH.keys()}" - ) + raise ValueError(f"Unknown `CSPNeXT` architecture: {arch}. Must be one of {self.ARCH.keys()}") self.model_name = model_name self.layer_configs = self.ARCH[arch] @@ -136,7 +135,7 @@ def __init__( stride=1, norm_layer=norm_layer, activation_fn=activation_fn, - ) + ), ) self.layers = ["stem"] @@ -177,8 +176,8 @@ def __init__( activation_fn=activation_fn, ) stage.append(csp_layer) - self.add_module(f'stage{i + 1}', nn.Sequential(*stage)) - self.layers.append(f'stage{i + 1}') + self.add_module(f"stage{i + 1}", nn.Sequential(*stage)) + self.layers.append(f"stage{i + 1}") self.single_output = isinstance(out_indices, int) if self.single_output: diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py index a52a58001e..b9906dafab 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py @@ -80,11 +80,9 @@ def __init__( (int(img_size[0] / 32), int(img_size[1] / 32)), ] - assert not ( - set(coam_modules) & set(selfatt_coam_modules) - if selfatt_coam_modules - else set() - ), "CoAM and Self-Attention-CoAM cannot be used at the same time" + assert not (set(coam_modules) & set(selfatt_coam_modules) if selfatt_coam_modules else set()), ( + "CoAM and Self-Attention-CoAM cannot be used at the same time" + ) all_output_channels = [ self.model.stage2_cfg["num_channels"], @@ -116,10 +114,8 @@ def __init__( else: spat_dims_ = spat_dims[: selfatt_coam_pos + 1] channels = all_output_channels[coam_pos - 1] - self.selfatt_coam_stages[selfatt_coam_pos - 1] = ( - SelfAttentionModule_CoAM( - spat_dims=spat_dims_, channel_list=channels - ) + self.selfatt_coam_stages[selfatt_coam_pos - 1] = SelfAttentionModule_CoAM( + spat_dims=spat_dims_, channel_list=channels ) def stages(self, x, cond_hm) -> list[torch.Tensor]: @@ -134,10 +130,7 @@ def stages(self, x, cond_hm) -> list[torch.Tensor]: yl = self.model.stage2(xl) - xl = [ - t(yl[-1]) if not isinstance(t, nn.Identity) else yl[i] - for i, t in enumerate(self.model.transition2) - ] + xl = [t(yl[-1]) if not isinstance(t, nn.Identity) else yl[i] for i, t in enumerate(self.model.transition2)] if self.coam_stages[1]: xl = self.coam_stages[1](xl, cond_hm) @@ -146,10 +139,7 @@ def stages(self, x, cond_hm) -> list[torch.Tensor]: yl = self.model.stage3(xl) - xl = [ - t(yl[-1]) if not isinstance(t, nn.Identity) else yl[i] - for i, t in enumerate(self.model.transition3) - ] + xl = [t(yl[-1]) if not isinstance(t, nn.Identity) else yl[i] for i, t in enumerate(self.model.transition3)] if self.coam_stages[2]: xl = self.coam_stages[2](xl, cond_hm) @@ -195,9 +185,7 @@ def forward(self, x: torch.Tensor, cond_kpts: np.ndarray): y = self.stages(x, cond_hm) if self.model.incre_modules is not None: - raise NotImplementedError( - "Incremental HRNet modules not supported for HRNetCoAM" - ) + raise NotImplementedError("Incremental HRNet modules not supported for HRNetCoAM") x = [incre(f) for f, incre in zip(x, self.model.incre_modules)] return self.prepare_output(y) diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/resnet.py b/deeplabcut/pose_estimation_pytorch/models/backbones/resnet.py index 5103ae64a9..d8ba3904b6 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/resnet.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/resnet.py @@ -93,21 +93,11 @@ def __init__( self.interm_features = {} self.model.layer1[2].register_forward_hook(self._get_features("bank1")) self.model.layer2[2].register_forward_hook(self._get_features("bank2")) - self.conv_block1 = self._make_conv_block( - in_channels=512, out_channels=512, kernel_size=3, stride=2 - ) - self.conv_block2 = self._make_conv_block( - in_channels=512, out_channels=128, kernel_size=1, stride=1 - ) - self.conv_block3 = self._make_conv_block( - in_channels=256, out_channels=256, kernel_size=3, stride=2 - ) - self.conv_block4 = self._make_conv_block( - in_channels=256, out_channels=256, kernel_size=3, stride=2 - ) - self.conv_block5 = self._make_conv_block( - in_channels=256, out_channels=128, kernel_size=1, stride=1 - ) + self.conv_block1 = self._make_conv_block(in_channels=512, out_channels=512, kernel_size=3, stride=2) + self.conv_block2 = self._make_conv_block(in_channels=512, out_channels=128, kernel_size=1, stride=1) + self.conv_block3 = self._make_conv_block(in_channels=256, out_channels=256, kernel_size=3, stride=2) + self.conv_block4 = self._make_conv_block(in_channels=256, out_channels=256, kernel_size=3, stride=2) + self.conv_block5 = self._make_conv_block(in_channels=256, out_channels=128, kernel_size=1, stride=1) def _make_conv_block( self, @@ -118,9 +108,7 @@ def _make_conv_block( momentum: float = 0.001, # (1 - decay) ) -> torch.nn.Sequential: return nn.Sequential( - nn.Conv2d( - in_channels, out_channels, kernel_size=kernel_size, stride=stride - ), + nn.Conv2d(in_channels, out_channels, kernel_size=kernel_size, stride=stride), nn.BatchNorm2d(out_channels, momentum=momentum), nn.ReLU(), ) diff --git a/deeplabcut/pose_estimation_pytorch/models/criterions/aggregators.py b/deeplabcut/pose_estimation_pytorch/models/criterions/aggregators.py index 973cabfc63..b93c9021b0 100644 --- a/deeplabcut/pose_estimation_pytorch/models/criterions/aggregators.py +++ b/deeplabcut/pose_estimation_pytorch/models/criterions/aggregators.py @@ -25,7 +25,5 @@ def __init__(self, weights: dict[str, float]) -> None: self.weights = weights def forward(self, losses: dict[str, torch.Tensor]) -> torch.Tensor: - weighted_losses = [ - weight * losses[loss_name] for loss_name, weight in self.weights.items() - ] + weighted_losses = [weight * losses[loss_name] for loss_name, weight in self.weights.items()] return torch.mean(torch.stack(weighted_losses)) diff --git a/deeplabcut/pose_estimation_pytorch/models/criterions/base.py b/deeplabcut/pose_estimation_pytorch/models/criterions/base.py index 8520366b7f..02c6b54989 100644 --- a/deeplabcut/pose_estimation_pytorch/models/criterions/base.py +++ b/deeplabcut/pose_estimation_pytorch/models/criterions/base.py @@ -26,9 +26,7 @@ def __init__(self) -> None: super().__init__() @abstractmethod - def forward( - self, output: torch.Tensor, target: torch.Tensor, **kwargs - ) -> torch.Tensor: + def forward(self, output: torch.Tensor, target: torch.Tensor, **kwargs) -> torch.Tensor: """ Args: output: the output from which to compute the loss diff --git a/deeplabcut/pose_estimation_pytorch/models/criterions/dekr.py b/deeplabcut/pose_estimation_pytorch/models/criterions/dekr.py index ab18007884..1297b6aa7d 100644 --- a/deeplabcut/pose_estimation_pytorch/models/criterions/dekr.py +++ b/deeplabcut/pose_estimation_pytorch/models/criterions/dekr.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Loss criterions for DEKR models""" + from __future__ import annotations import torch @@ -56,7 +57,7 @@ def smooth_l1_loss(self, pred, gt): l1_loss = torch.abs(pred - gt) return torch.where( l1_loss < self.beta, - 0.5 * l1_loss ** 2 / self.beta, + 0.5 * l1_loss**2 / self.beta, l1_loss - 0.5 * self.beta, ) diff --git a/deeplabcut/pose_estimation_pytorch/models/criterions/kl_discrete.py b/deeplabcut/pose_estimation_pytorch/models/criterions/kl_discrete.py index e36bf78ae6..29d6e688af 100644 --- a/deeplabcut/pose_estimation_pytorch/models/criterions/kl_discrete.py +++ b/deeplabcut/pose_estimation_pytorch/models/criterions/kl_discrete.py @@ -13,6 +13,7 @@ Can be used for SimCC-type heads. Modified from the `mmpose` implementation. For more details, see . """ + import torch import torch.nn as nn import torch.nn.functional as F diff --git a/deeplabcut/pose_estimation_pytorch/models/criterions/utils.py b/deeplabcut/pose_estimation_pytorch/models/criterions/utils.py index 693ec74e09..4fc455c585 100644 --- a/deeplabcut/pose_estimation_pytorch/models/criterions/utils.py +++ b/deeplabcut/pose_estimation_pytorch/models/criterions/utils.py @@ -13,9 +13,7 @@ import torch -def count_nonzero_elems( - losses: torch.Tensor, weights: float | torch.Tensor, per_batch: bool = False -): +def count_nonzero_elems(losses: torch.Tensor, weights: float | torch.Tensor, per_batch: bool = False): """ Compute the number of elements in the loss function induced by `weights`. This is a torch implementation of https://github.com/tensorflow/tensorflow/blob/4dacf3f368eb7965e9b5c3bbdd5193986081c3b2/tensorflow/python/ops/losses/losses_impl.py#L89 @@ -37,9 +35,7 @@ def count_nonzero_elems( weights = torch.as_tensor(weights, dtype=torch.float32) # Check for non-zero weights and broadcast to match losses - present = torch.where( - weights == 0.0, torch.zeros_like(weights), torch.ones_like(weights) - ) + present = torch.where(weights == 0.0, torch.zeros_like(weights), torch.ones_like(weights)) present = present.expand_as(losses) # Reduce sum across the desired dimensions diff --git a/deeplabcut/pose_estimation_pytorch/models/detectors/base.py b/deeplabcut/pose_estimation_pytorch/models/detectors/base.py index 198c14ed0b..99df54e166 100644 --- a/deeplabcut/pose_estimation_pytorch/models/detectors/base.py +++ b/deeplabcut/pose_estimation_pytorch/models/detectors/base.py @@ -41,9 +41,7 @@ def _build_detector( detector: BaseDetector = build_from_cfg(cfg, **kwargs) if weight_init is not None and weight_init.detector_snapshot_path is not None: - logging.info( - f"Loading detector checkpoint from {weight_init.detector_snapshot_path}" - ) + logging.info(f"Loading detector checkpoint from {weight_init.detector_snapshot_path}") snapshot = torch.load(weight_init.detector_snapshot_path, map_location="cpu") detector.load_state_dict(snapshot["model"]) diff --git a/deeplabcut/pose_estimation_pytorch/models/detectors/fasterRCNN.py b/deeplabcut/pose_estimation_pytorch/models/detectors/fasterRCNN.py index edfdbe8a23..f71248d4e8 100644 --- a/deeplabcut/pose_estimation_pytorch/models/detectors/fasterRCNN.py +++ b/deeplabcut/pose_estimation_pytorch/models/detectors/fasterRCNN.py @@ -69,6 +69,4 @@ def __init__( # Modify the base predictor to output the correct number of classes num_classes = 2 in_features = self.model.roi_heads.box_predictor.cls_score.in_features - self.model.roi_heads.box_predictor = detection.faster_rcnn.FastRCNNPredictor( - in_features, num_classes - ) + self.model.roi_heads.box_predictor = detection.faster_rcnn.FastRCNNPredictor(in_features, num_classes) diff --git a/deeplabcut/pose_estimation_pytorch/models/detectors/torchvision.py b/deeplabcut/pose_estimation_pytorch/models/detectors/torchvision.py index 6c700377f7..37339e055a 100644 --- a/deeplabcut/pose_estimation_pytorch/models/detectors/torchvision.py +++ b/deeplabcut/pose_estimation_pytorch/models/detectors/torchvision.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Module to adapt torchvision detectors for DeepLabCut""" + from __future__ import annotations import torch diff --git a/deeplabcut/pose_estimation_pytorch/models/heads/base.py b/deeplabcut/pose_estimation_pytorch/models/heads/base.py index b0d0a8c49f..3c575a8e0b 100644 --- a/deeplabcut/pose_estimation_pytorch/models/heads/base.py +++ b/deeplabcut/pose_estimation_pytorch/models/heads/base.py @@ -77,21 +77,14 @@ def __init__( elif isinstance(weight_init, (str, dict)): self.weight_init = WEIGHT_INIT.build(weight_init) elif weight_init is not None: - raise ValueError( - f"Could not parse ``weight_init`` parameter: {weight_init}." - ) + raise ValueError(f"Could not parse ``weight_init`` parameter: {weight_init}.") if isinstance(criterion, dict): if aggregator is None: - raise ValueError( - f"When multiple criterions are defined, a loss aggregator must " - "also be given" - ) + raise ValueError(f"When multiple criterions are defined, a loss aggregator must also be given") else: if aggregator is not None: - raise ValueError( - f"Cannot use a loss aggregator with a single criterion" - ) + raise ValueError(f"Cannot use a loss aggregator with a single criterion") @abstractmethod def forward(self, x: torch.Tensor) -> dict[str, torch.Tensor]: @@ -128,10 +121,7 @@ def get_loss( key = [k for k in outputs.keys()][0] return {"total_loss": self.criterion(outputs[key], **targets[key])} - losses = { - name: criterion(outputs[name], **targets[name]) - for name, criterion in self.criterion.items() - } + losses = {name: criterion(outputs[name], **targets[name]) for name, criterion in self.criterion.items()} losses["total_loss"] = self.aggregator(losses) return losses diff --git a/deeplabcut/pose_estimation_pytorch/models/heads/dekr.py b/deeplabcut/pose_estimation_pytorch/models/heads/dekr.py index d61da6a4e9..32db064180 100644 --- a/deeplabcut/pose_estimation_pytorch/models/heads/dekr.py +++ b/deeplabcut/pose_estimation_pytorch/models/heads/dekr.py @@ -49,9 +49,7 @@ def __init__( weight_init: str | dict | BaseWeightInitializer | None = "dekr", stride: int | float = 1, # head stride - should always be 1 for DEKR ) -> None: - super().__init__( - stride, predictor, target_generator, criterion, aggregator, weight_init - ) + super().__init__(stride, predictor, target_generator, criterion, aggregator, weight_init) self.heatmap_head = DEKRHeatmap(**heatmap_config) self.offset_head = DEKROffset(**offset_config) self._init_weights() @@ -101,21 +99,13 @@ def __init__( super().__init__() self.bn_momentum = 0.1 self.inp_channels = channels[0] - self.num_joints_with_center = channels[ - 2 - ] # Should account for the center being a joint + self.num_joints_with_center = channels[2] # Should account for the center being a joint self.final_conv_kernel = final_conv_kernel - self.transition_heatmap = self._make_transition_for_head( - self.inp_channels, channels[1] - ) - self.head_heatmap = self._make_heatmap_head( - block, num_blocks, channels[1], dilation_rate - ) + self.transition_heatmap = self._make_transition_for_head(self.inp_channels, channels[1]) + self.head_heatmap = self._make_heatmap_head(block, num_blocks, channels[1], dilation_rate) - def _make_transition_for_head( - self, in_channels: int, out_channels: int - ) -> nn.Sequential: + def _make_transition_for_head(self, in_channels: int, out_channels: int) -> nn.Sequential: """Summary: Construct the transition layer for the head. @@ -154,9 +144,7 @@ def _make_heatmap_head( """ heatmap_head_layers = [] - feature_conv = self._make_layer( - block, num_channels, num_channels, num_blocks, dilation=dilation_rate - ) + feature_conv = self._make_layer(block, num_channels, num_channels, num_blocks, dilation=dilation_rate) heatmap_head_layers.append(feature_conv) heatmap_conv = nn.Conv2d( @@ -203,14 +191,10 @@ def _make_layer( stride=stride, bias=False, ), - nn.BatchNorm2d( - out_channels * block.expansion, momentum=self.bn_momentum - ), + nn.BatchNorm2d(out_channels * block.expansion, momentum=self.bn_momentum), ) - layers = [ - block(in_channels, out_channels, stride, downsample, dilation=dilation) - ] + layers = [block(in_channels, out_channels, stride, downsample, dilation=dilation)] in_channels = out_channels * block.expansion for _ in range(1, num_blocks): layers.append(block(in_channels, out_channels, dilation=dilation)) @@ -264,9 +248,7 @@ def __init__( self.dilation_rate = dilation_rate self.final_conv_kernel = final_conv_kernel - self.transition_offset = self._make_transition_for_head( - self.inp_channels, self.offset_channels - ) + self.transition_offset = self._make_transition_for_head(self.inp_channels, self.offset_channels) ( self.offset_feature_layers, self.offset_final_layer, @@ -319,24 +301,18 @@ def _make_layer( stride=stride, bias=False, ), - nn.BatchNorm2d( - out_channels * block.expansion, momentum=self.bn_momentum - ), + nn.BatchNorm2d(out_channels * block.expansion, momentum=self.bn_momentum), ) layers = [] - layers.append( - block(in_channels, out_channels, stride, downsample, dilation=dilation) - ) + layers.append(block(in_channels, out_channels, stride, downsample, dilation=dilation)) in_channels = out_channels * block.expansion for _ in range(1, num_blocks): layers.append(block(in_channels, out_channels, dilation=dilation)) return nn.Sequential(*layers) - def _make_transition_for_head( - self, in_channels: int, out_channels: int - ) -> nn.Sequential: + def _make_transition_for_head(self, in_channels: int, out_channels: int) -> nn.Sequential: """Summary: Create a transition layer for the head. @@ -417,9 +393,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: final_offset.append( self.offset_final_layer[j]( self.offset_feature_layers[j]( - offset_feature[ - :, j * self.offset_perkpt : (j + 1) * self.offset_perkpt - ] + offset_feature[:, j * self.offset_perkpt : (j + 1) * self.offset_perkpt] ) ) ) diff --git a/deeplabcut/pose_estimation_pytorch/models/heads/dlcrnet.py b/deeplabcut/pose_estimation_pytorch/models/heads/dlcrnet.py index 6eeaf68df0..03203fd892 100644 --- a/deeplabcut/pose_estimation_pytorch/models/heads/dlcrnet.py +++ b/deeplabcut/pose_estimation_pytorch/models/heads/dlcrnet.py @@ -51,9 +51,7 @@ def __init__( num_limbs = paf_config["channels"][-1] # Already has the 2x multiplier in_refined_channels = features_dim + num_keypoints + num_limbs if num_stages > 0: - heatmap_config["channels"][0] = paf_config["channels"][0] = ( - in_refined_channels - ) + heatmap_config["channels"][0] = paf_config["channels"][0] = in_refined_channels locref_config["channels"][0] = locref_config["channels"][-1] super().__init__( @@ -70,36 +68,22 @@ def __init__( self.paf_head = DeconvModule(**paf_config) - self.convt1 = self._make_layer_same_padding( - in_channels=in_channels, out_channels=num_keypoints - ) - self.convt2 = self._make_layer_same_padding( - in_channels=in_channels, out_channels=locref_config["channels"][-1] - ) - self.convt3 = self._make_layer_same_padding( - in_channels=in_channels, out_channels=num_limbs - ) - self.convt4 = self._make_layer_same_padding( - in_channels=in_channels, out_channels=features_dim - ) + self.convt1 = self._make_layer_same_padding(in_channels=in_channels, out_channels=num_keypoints) + self.convt2 = self._make_layer_same_padding(in_channels=in_channels, out_channels=locref_config["channels"][-1]) + self.convt3 = self._make_layer_same_padding(in_channels=in_channels, out_channels=num_limbs) + self.convt4 = self._make_layer_same_padding(in_channels=in_channels, out_channels=features_dim) self.hm_ref_layers = nn.ModuleList() self.paf_ref_layers = nn.ModuleList() for _ in range(num_stages): self.hm_ref_layers.append( - self._make_refinement_layer( - in_channels=in_refined_channels, out_channels=num_keypoints - ) + self._make_refinement_layer(in_channels=in_refined_channels, out_channels=num_keypoints) ) self.paf_ref_layers.append( - self._make_refinement_layer( - in_channels=in_refined_channels, out_channels=num_limbs - ) + self._make_refinement_layer(in_channels=in_refined_channels, out_channels=num_limbs) ) self._init_weights() - def _make_layer_same_padding( - self, in_channels: int, out_channels: int - ) -> nn.ConvTranspose2d: + def _make_layer_same_padding(self, in_channels: int, out_channels: int) -> nn.ConvTranspose2d: # FIXME There is no consensual solution to emulate TF behavior in pytorch # see https://github.com/pytorch/pytorch/issues/3867 return nn.ConvTranspose2d( @@ -122,9 +106,7 @@ def _make_refinement_layer(self, in_channels: int, out_channels: int) -> nn.Conv Returns: refinement_layer: the refinement layer. """ - return nn.Conv2d( - in_channels, out_channels, kernel_size=3, stride=1, padding="same" - ) + return nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=1, padding="same") def forward(self, x: torch.Tensor) -> dict[str, torch.Tensor]: if self.num_stages > 0: @@ -135,9 +117,7 @@ def forward(self, x: torch.Tensor) -> dict[str, torch.Tensor]: stage_in = stage2_in stage_paf_out = stage1_paf_out stage_hm_out = stage1_hm_out - for i, (hm_ref_layer, paf_ref_layer) in enumerate( - zip(self.hm_ref_layers, self.paf_ref_layers) - ): + for i, (hm_ref_layer, paf_ref_layer) in enumerate(zip(self.hm_ref_layers, self.paf_ref_layers)): pre_stage_hm_out = stage_hm_out stage_hm_out = hm_ref_layer(stage_in) stage_paf_out = paf_ref_layer(stage_in) diff --git a/deeplabcut/pose_estimation_pytorch/models/heads/rtmcc_head.py b/deeplabcut/pose_estimation_pytorch/models/heads/rtmcc_head.py index 6b99ec7308..96bd9b7e64 100644 --- a/deeplabcut/pose_estimation_pytorch/models/heads/rtmcc_head.py +++ b/deeplabcut/pose_estimation_pytorch/models/heads/rtmcc_head.py @@ -13,6 +13,7 @@ Based on the official ``mmpose`` RTMCC head implementation. For more information, see . """ + from __future__ import annotations import torch diff --git a/deeplabcut/pose_estimation_pytorch/models/heads/simple_head.py b/deeplabcut/pose_estimation_pytorch/models/heads/simple_head.py index 334e674237..e4b1865ea7 100644 --- a/deeplabcut/pose_estimation_pytorch/models/heads/simple_head.py +++ b/deeplabcut/pose_estimation_pytorch/models/heads/simple_head.py @@ -159,9 +159,7 @@ def __init__( head_stride = 1 self.deconv_layers = nn.Identity() if len(kernel_size) > 0: - self.deconv_layers = nn.Sequential( - *self._make_layers(in_channels, channels[1:], kernel_size, strides) - ) + self.deconv_layers = nn.Sequential(*self._make_layers(in_channels, channels[1:], kernel_size, strides)) for s in strides: head_stride *= s @@ -196,9 +194,7 @@ def _make_layers( """ layers = [] for out_channels, k, s in zip(out_channels, kernel_sizes, strides): - layers.append( - nn.ConvTranspose2d(in_channels, out_channels, kernel_size=k, stride=s) - ) + layers.append(nn.ConvTranspose2d(in_channels, out_channels, kernel_size=k, stride=s)) layers.append(nn.ReLU()) in_channels = out_channels return layers[:-1] diff --git a/deeplabcut/pose_estimation_pytorch/models/model.py b/deeplabcut/pose_estimation_pytorch/models/model.py index 4701f7efec..9c773467cb 100644 --- a/deeplabcut/pose_estimation_pytorch/models/model.py +++ b/deeplabcut/pose_estimation_pytorch/models/model.py @@ -58,10 +58,7 @@ def __init__( self.neck = neck self.output_features = False - self._strides = { - name: _model_stride(self.backbone.stride, head.stride) - for name, head in heads.items() - } + self._strides = {name: _model_stride(self.backbone.stride, head.stride) for name, head in heads.items()} def forward(self, x: torch.Tensor, **backbone_kwargs) -> dict[str, dict[str, torch.Tensor]]: """ @@ -120,8 +117,7 @@ def get_target( targets: dict of the targets for each model head group """ return { - name: head.target_generator(self._strides[name], outputs[name], labels) - for name, head in self.heads.items() + name: head.target_generator(self._strides[name], outputs[name], labels) for name, head in self.heads.items() } def get_predictions(self, outputs: dict[str, dict[str, torch.Tensor]]) -> dict: @@ -133,10 +129,7 @@ def get_predictions(self, outputs: dict[str, dict[str, torch.Tensor]]) -> dict: Returns: A dictionary containing the predictions of each head group """ - predictions = { - name: head.predictor(self._strides[name], outputs[name]) - for name, head in self.heads.items() - } + predictions = {name: head.predictor(self._strides[name], outputs[name]) for name, head in self.heads.items()} if self.output_features: predictions["backbone"] = outputs["backbone"] @@ -190,18 +183,14 @@ def build( criterions = {} for loss_name, criterion_cfg in head_cfg["criterion"].items(): weights[loss_name] = criterion_cfg.get("weight", 1.0) - criterion_cfg = { - k: v for k, v in criterion_cfg.items() if k != "weight" - } + criterion_cfg = {k: v for k, v in criterion_cfg.items() if k != "weight"} criterions[loss_name] = CRITERIONS.build(criterion_cfg) aggregator_cfg = {"type": "WeightedLossAggregator", "weights": weights} head_cfg["aggregator"] = LOSS_AGGREGATORS.build(aggregator_cfg) head_cfg["criterion"] = criterions - head_cfg["target_generator"] = TARGET_GENERATORS.build( - head_cfg["target_generator"] - ) + head_cfg["target_generator"] = TARGET_GENERATORS.build(head_cfg["target_generator"]) head_cfg["predictor"] = PREDICTORS.build(head_cfg["predictor"]) heads[name] = HEADS.build(head_cfg) diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/__init__.py b/deeplabcut/pose_estimation_pytorch/models/modules/__init__.py index d8d306b504..7efd11f82c 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/__init__.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/__init__.py @@ -16,15 +16,12 @@ from deeplabcut.pose_estimation_pytorch.models.modules.conv_module import ( HighResolutionModule, ) -from deeplabcut.pose_estimation_pytorch.models.modules.coam_module import ( - CoAMBlock, - SelfAttentionModule_CoAM -) +from deeplabcut.pose_estimation_pytorch.models.modules.coam_module import CoAMBlock, SelfAttentionModule_CoAM from deeplabcut.pose_estimation_pytorch.models.modules.kpt_encoders import ( BaseKeypointEncoder, ColoredKeypointEncoder, StackedKeypointEncoder, - KEYPOINT_ENCODERS + KEYPOINT_ENCODERS, ) from deeplabcut.pose_estimation_pytorch.models.modules.gated_attention_unit import ( GatedAttentionUnit, diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py b/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py index 45fd5fc68d..e5041922ec 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py @@ -21,9 +21,7 @@ class CoAMBlock(nn.Module): Conditional Attention Module (CoAM) block. """ - def __init__( - self, spat_dims, channel_list, cond_enc, n_heads=1, channel_only=False - ): + def __init__(self, spat_dims, channel_list, cond_enc, n_heads=1, channel_only=False): super(CoAMBlock, self).__init__() self.att_layers = [] self.spat_dims = spat_dims @@ -57,24 +55,14 @@ def forward(self, y_list, cond_hm): # modified from https://github.com/xmu-xiaoma666/External-Attention-pytorch/blob/master/model/attention/DANet.py class PositionAttentionModule(nn.Module): - def __init__( - self, d_model=512, d_cond=3, kernel_size=3, H=7, W=7, n_heads=1, self_att=False - ): + def __init__(self, d_model=512, d_cond=3, kernel_size=3, H=7, W=7, n_heads=1, self_att=False): super().__init__() - self.cnn = nn.Conv2d( - d_model, d_model, kernel_size=kernel_size, padding=(kernel_size - 1) // 2 - ) - self.pa = ScaledDotProductAttention( - in_dim_q=d_model, in_dim_k=d_model, d_k=d_model, d_v=d_model, h=n_heads - ) + self.cnn = nn.Conv2d(d_model, d_model, kernel_size=kernel_size, padding=(kernel_size - 1) // 2) + self.pa = ScaledDotProductAttention(in_dim_q=d_model, in_dim_k=d_model, d_k=d_model, d_v=d_model, h=n_heads) self.self_att = self_att if not self_att: - self.cnn_cond = nn.Conv2d( - d_cond, d_cond, kernel_size=kernel_size, padding=(kernel_size - 1) // 2 - ) - self.pa = ScaledDotProductAttention( - in_dim_q=d_cond, in_dim_k=d_model, d_k=d_model, d_v=d_model, h=n_heads - ) + self.cnn_cond = nn.Conv2d(d_cond, d_cond, kernel_size=kernel_size, padding=(kernel_size - 1) // 2) + self.pa = ScaledDotProductAttention(in_dim_q=d_cond, in_dim_k=d_model, d_k=d_model, d_v=d_model, h=n_heads) def forward(self, x, cond=None): bs, c, h, w = x.shape @@ -94,18 +82,12 @@ def forward(self, x, cond=None): class ChannelAttentionModule(nn.Module): - def __init__( - self, d_model=512, d_cond=3, kernel_size=3, H=7, W=7, n_heads=1, self_att=False - ): + def __init__(self, d_model=512, d_cond=3, kernel_size=3, H=7, W=7, n_heads=1, self_att=False): super().__init__() - self.cnn = nn.Conv2d( - d_model, d_model, kernel_size=kernel_size, padding=(kernel_size - 1) // 2 - ) + self.cnn = nn.Conv2d(d_model, d_model, kernel_size=kernel_size, padding=(kernel_size - 1) // 2) self.self_att = self_att if not self_att: - self.cnn_cond = nn.Conv2d( - d_cond, d_model, kernel_size=kernel_size, padding=(kernel_size - 1) // 2 - ) + self.cnn_cond = nn.Conv2d(d_cond, d_model, kernel_size=kernel_size, padding=(kernel_size - 1) // 2) self.pa = SimplifiedScaledDotProductAttention(H * W, h=n_heads) def forward(self, x, cond=None): @@ -272,9 +254,7 @@ def init_weights(self): if m.bias is not None: init.constant_(m.bias, 0) - def forward( - self, queries, keys, values, attention_mask=None, attention_weights=None - ): + def forward(self, queries, keys, values, attention_mask=None, attention_weights=None): """ Computes :param queries: Queries (b_s, nq, d_model) @@ -287,15 +267,9 @@ def forward( b_s, nq = queries.shape[:2] nk = keys.shape[1] - q = ( - self.fc_q(queries).view(b_s, nq, self.h, self.d_k).permute(0, 2, 1, 3) - ) # (b_s, h, nq, d_k) - k = ( - self.fc_k(keys).view(b_s, nk, self.h, self.d_k).permute(0, 2, 3, 1) - ) # (b_s, h, d_k, nk) - v = ( - self.fc_v(values).view(b_s, nk, self.h, self.d_v).permute(0, 2, 1, 3) - ) # (b_s, h, nk, d_v) + q = self.fc_q(queries).view(b_s, nq, self.h, self.d_k).permute(0, 2, 1, 3) # (b_s, h, nq, d_k) + k = self.fc_k(keys).view(b_s, nk, self.h, self.d_k).permute(0, 2, 3, 1) # (b_s, h, d_k, nk) + v = self.fc_v(values).view(b_s, nk, self.h, self.d_v).permute(0, 2, 1, 3) # (b_s, h, nk, d_v) att = torch.matmul(q, k) / np.sqrt(self.d_k) # (b_s, h, nq, nk) if attention_weights is not None: @@ -305,12 +279,7 @@ def forward( att = torch.softmax(att, -1) att = self.dropout(att) - out = ( - torch.matmul(att, v) - .permute(0, 2, 1, 3) - .contiguous() - .view(b_s, nq, self.h * self.d_v) - ) # (b_s, nq, h*d_v) + out = torch.matmul(att, v).permute(0, 2, 1, 3).contiguous().view(b_s, nq, self.h * self.d_v) # (b_s, nq, h*d_v) out = self.fc_o(out) # (b_s, nq, d_model) return out @@ -354,9 +323,7 @@ def init_weights(self): if m.bias is not None: init.constant_(m.bias, 0) - def forward( - self, queries, keys, values, attention_mask=None, attention_weights=None - ): + def forward(self, queries, keys, values, attention_mask=None, attention_weights=None): """ Computes :param queries: Queries (b_s, nq, d_model) @@ -369,15 +336,9 @@ def forward( b_s, nq = queries.shape[:2] nk = keys.shape[1] - q = queries.view(b_s, nq, self.h, self.d_k).permute( - 0, 2, 1, 3 - ) # (b_s, h, nq, d_k) - k = keys.view(b_s, nk, self.h, self.d_k).permute( - 0, 2, 3, 1 - ) # (b_s, h, d_k, nk) - v = values.view(b_s, nk, self.h, self.d_v).permute( - 0, 2, 1, 3 - ) # (b_s, h, nk, d_v) + q = queries.view(b_s, nq, self.h, self.d_k).permute(0, 2, 1, 3) # (b_s, h, nq, d_k) + k = keys.view(b_s, nk, self.h, self.d_k).permute(0, 2, 3, 1) # (b_s, h, d_k, nk) + v = values.view(b_s, nk, self.h, self.d_v).permute(0, 2, 1, 3) # (b_s, h, nk, d_v) att = torch.matmul(q, k) / np.sqrt(self.d_k) # (b_s, h, nq, nk) if attention_weights is not None: @@ -387,11 +348,6 @@ def forward( att = torch.softmax(att, -1) att = self.dropout(att) - out = ( - torch.matmul(att, v) - .permute(0, 2, 1, 3) - .contiguous() - .view(b_s, nq, self.h * self.d_v) - ) # (b_s, nq, h*d_v) + out = torch.matmul(att, v).permute(0, 2, 1, 3).contiguous().view(b_s, nq, self.h * self.d_v) # (b_s, nq, h*d_v) out = self.fc_o(out) # (b_s, nq, d_model) return out diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/conv_block.py b/deeplabcut/pose_estimation_pytorch/models/modules/conv_block.py index 72816bcbcf..1aa8d33450 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/conv_block.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/conv_block.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """The code is based on DEKR: https://github.com/HRNet/DEKR/tree/main""" + from __future__ import annotations from abc import ABC, abstractmethod @@ -179,12 +180,8 @@ def __init__( dilation=dilation, ) self.bn2 = nn.BatchNorm2d(out_channels, momentum=self.bn_momentum) - self.conv3 = nn.Conv2d( - out_channels, out_channels * self.expansion, kernel_size=1, bias=False - ) - self.bn3 = nn.BatchNorm2d( - out_channels * self.expansion, momentum=self.bn_momentum - ) + self.conv3 = nn.Conv2d(out_channels, out_channels * self.expansion, kernel_size=1, bias=False) + self.bn3 = nn.BatchNorm2d(out_channels * self.expansion, momentum=self.bn_momentum) self.relu = nn.ReLU(inplace=True) self.downsample = downsample self.stride = stride @@ -250,9 +247,7 @@ def __init__( deformable_groups: int = 1, ): super(AdaptBlock, self).__init__() - regular_matrix = torch.tensor( - [[-1, -1, -1, 0, 0, 0, 1, 1, 1], [-1, 0, 1, -1, 0, 1, -1, 0, 1]] - ) + regular_matrix = torch.tensor([[-1, -1, -1, 0, 0, 0, 1, 1, 1], [-1, 0, 1, -1, 0, 1, -1, 0, 1]]) self.register_buffer("regular_matrix", regular_matrix.float()) self.downsample = downsample self.transform_matrix_conv = nn.Conv2d(in_channels, 4, 3, 1, 1, bias=True) @@ -283,9 +278,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: N, _, H, W = x.shape transform_matrix = self.transform_matrix_conv(x) - transform_matrix = transform_matrix.permute(0, 2, 3, 1).reshape( - (N * H * W, 2, 2) - ) + transform_matrix = transform_matrix.permute(0, 2, 3, 1).reshape((N * H * W, 2, 2)) offset = torch.matmul(transform_matrix, self.regular_matrix) offset = offset - self.regular_matrix offset = offset.transpose(1, 2).reshape((N, H, W, 18)).permute(0, 3, 1, 2) diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/conv_module.py b/deeplabcut/pose_estimation_pytorch/models/modules/conv_module.py index 630eed830f..372a70f44f 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/conv_module.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/conv_module.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """The code is based on DEKR: https://github.com/HRNet/DEKR/tree/main""" + import logging from typing import List @@ -46,9 +47,7 @@ def __init__( multi_scale_output: bool = True, ): super(HighResolutionModule, self).__init__() - self._check_branches( - num_branches, block, num_blocks, num_inchannels, num_channels - ) + self._check_branches(num_branches, block, num_blocks, num_inchannels, num_channels) self.num_inchannels = num_inchannels self.fuse_method = fuse_method @@ -56,9 +55,7 @@ def __init__( self.multi_scale_output = multi_scale_output - self.branches = self._make_branches( - num_branches, block, num_blocks, num_channels - ) + self.branches = self._make_branches(num_branches, block, num_blocks, num_channels) self.fuse_layers = self._make_fuse_layers() self.relu = nn.ReLU(True) @@ -71,23 +68,17 @@ def _check_branches( num_channels: int, ): if num_branches != len(num_blocks): - error_msg = "NUM_BRANCHES({}) <> NUM_BLOCKS({})".format( - num_branches, len(num_blocks) - ) + error_msg = "NUM_BRANCHES({}) <> NUM_BLOCKS({})".format(num_branches, len(num_blocks)) logger.error(error_msg) raise ValueError(error_msg) if num_branches != len(num_channels): - error_msg = "NUM_BRANCHES({}) <> NUM_CHANNELS({})".format( - num_branches, len(num_channels) - ) + error_msg = "NUM_BRANCHES({}) <> NUM_CHANNELS({})".format(num_branches, len(num_channels)) logger.error(error_msg) raise ValueError(error_msg) if num_branches != len(num_inchannels): - error_msg = "NUM_BRANCHES({}) <> NUM_INCHANNELS({})".format( - num_branches, len(num_inchannels) - ) + error_msg = "NUM_BRANCHES({}) <> NUM_INCHANNELS({})".format(num_branches, len(num_inchannels)) logger.error(error_msg) raise ValueError(error_msg) @@ -100,11 +91,7 @@ def _make_one_branch( stride: int = 1, ) -> nn.Sequential: downsample = None - if ( - stride != 1 - or self.num_inchannels[branch_index] - != num_channels[branch_index] * block.expansion - ): + if stride != 1 or self.num_inchannels[branch_index] != num_channels[branch_index] * block.expansion: downsample = nn.Sequential( nn.Conv2d( self.num_inchannels[branch_index], @@ -113,9 +100,7 @@ def _make_one_branch( stride=stride, bias=False, ), - nn.BatchNorm2d( - num_channels[branch_index] * block.expansion, momentum=BN_MOMENTUM - ), + nn.BatchNorm2d(num_channels[branch_index] * block.expansion, momentum=BN_MOMENTUM), ) layers = [] @@ -129,15 +114,11 @@ def _make_one_branch( ) self.num_inchannels[branch_index] = num_channels[branch_index] * block.expansion for i in range(1, num_blocks[branch_index]): - layers.append( - block(self.num_inchannels[branch_index], num_channels[branch_index]) - ) + layers.append(block(self.num_inchannels[branch_index], num_channels[branch_index])) return nn.Sequential(*layers) - def _make_branches( - self, num_branches: int, block: BasicBlock, num_blocks: int, num_channels: int - ) -> nn.ModuleList: + def _make_branches(self, num_branches: int, block: BasicBlock, num_blocks: int, num_channels: int) -> nn.ModuleList: branches = [] for i in range(num_branches): diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/csp.py b/deeplabcut/pose_estimation_pytorch/models/modules/csp.py index 3099eebeeb..49548cf039 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/csp.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/csp.py @@ -13,6 +13,7 @@ Based on the building blocks used for the ``mmdetection`` CSPNeXt implementation. For more information, see . """ + import torch import torch.nn as nn @@ -23,9 +24,7 @@ def build_activation(activation_fn: str, *args, **kwargs) -> nn.Module: elif activation_fn == "ReLU": return nn.ReLU(*args, **kwargs) - raise NotImplementedError( - f"Unknown `CSPNeXT` activation: {activation_fn}. Must be one of 'SiLU', 'ReLU'" - ) + raise NotImplementedError(f"Unknown `CSPNeXT` activation: {activation_fn}. Must be one of 'SiLU', 'ReLU'") def build_norm(norm: str, *args, **kwargs) -> nn.Module: @@ -34,9 +33,7 @@ def build_norm(norm: str, *args, **kwargs) -> nn.Module: elif norm == "BN": return nn.BatchNorm2d(*args, **kwargs) - raise NotImplementedError( - f"Unknown `CSPNeXT` norm_layer: {norm}. Must be one of 'SyncBN', 'BN'" - ) + raise NotImplementedError(f"Unknown `CSPNeXT` norm_layer: {norm}. Must be one of 'SyncBN', 'BN'") class SPPBottleneck(nn.Module): @@ -69,12 +66,7 @@ def __init__( activation_fn=activation_fn, ) - self.poolings = nn.ModuleList( - [ - nn.MaxPool2d(kernel_size=ks, stride=1, padding=ks // 2) - for ks in kernel_sizes - ] - ) + self.poolings = nn.ModuleList([nn.MaxPool2d(kernel_size=ks, stride=1, padding=ks // 2) for ks in kernel_sizes]) conv2_channels = mid_channels * (len(kernel_sizes) + 1) self.conv2 = CSPConvModule( conv2_channels, diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/gated_attention_unit.py b/deeplabcut/pose_estimation_pytorch/models/modules/gated_attention_unit.py index fd12ee43d8..7f7ccc3105 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/gated_attention_unit.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/gated_attention_unit.py @@ -13,6 +13,7 @@ Based on the building blocks used for the ``mmdetection`` CSPNeXt implementation. For more information, see . """ + from __future__ import annotations import math @@ -36,17 +37,13 @@ def rope(x, dim): for i in spatial_shape: total_len *= i - position = torch.reshape( - torch.arange(total_len, dtype=torch.int, device=x.device), spatial_shape - ) + position = torch.reshape(torch.arange(total_len, dtype=torch.int, device=x.device), spatial_shape) for i in range(dim[-1] + 1, len(shape) - 1, 1): position = torch.unsqueeze(position, dim=-1) half_size = shape[-1] // 2 - freq_seq = -torch.arange(half_size, dtype=torch.int, device=x.device) / float( - half_size - ) + freq_seq = -torch.arange(half_size, dtype=torch.int, device=x.device) / float(half_size) inv_freq = 10000**-freq_seq sinusoid = position[..., None] * inv_freq[None, None, :] @@ -109,9 +106,7 @@ def __init__( self.e = int(in_token_dims * expansion_factor) if use_rel_bias: if attn_type == "self-attn": - self.w = nn.Parameter( - torch.rand([2 * num_token - 1], dtype=torch.float) - ) + self.w = nn.Parameter(torch.rand([2 * num_token - 1], dtype=torch.float)) else: self.a = nn.Parameter(torch.rand([1, s], dtype=torch.float)) self.b = nn.Parameter(torch.rand([1, s], dtype=torch.float)) diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py index 6fe530e488..27bfffecc4 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py @@ -160,9 +160,7 @@ class ColoredKeypointEncoder(BaseKeypointEncoder): Modified from BUCTD/data/JointsDataset, get_condition_image_colored """ - def __init__( - self, colors: list[tuple[int, int, int]] | None = None, **kwargs - ) -> None: + def __init__(self, colors: list[tuple[int, int, int]] | None = None, **kwargs) -> None: """ Args: colors: the color to use for each keypoint @@ -219,23 +217,14 @@ def _get_condition_matrix(zero_matrix, kpts): def _get_condition_matrix_optim(zero_matrix, kpts): x, y = np.array(kpts).T - mask = ( - (0 < x) - & (x < zero_matrix.shape[2]) - & (0 < y) - & (y < zero_matrix.shape[1]) - ) - colors_masked = np.repeat( - self.colors[:, None, :], len(zero_matrix), 1 - ) * np.repeat(mask[:, :, None], 3, 2) + mask = (0 < x) & (x < zero_matrix.shape[2]) & (0 < y) & (y < zero_matrix.shape[1]) + colors_masked = np.repeat(self.colors[:, None, :], len(zero_matrix), 1) * np.repeat(mask[:, :, None], 3, 2) kpt_indices = np.stack([x.T, y.T]).transpose(1, 2, 0) - batch_indices = np.repeat( - np.arange(len(zero_matrix))[:, None, None], self.num_joints, axis=1 - ) + batch_indices = np.repeat(np.arange(len(zero_matrix))[:, None, None], self.num_joints, axis=1) kpt_input = np.concatenate([batch_indices, kpt_indices], dtype=int, axis=2) - zero_matrix[ - kpt_input[..., 0], kpt_input[..., 2] - 1, kpt_input[..., 1] - 1 - ] = colors_masked.transpose(1, 0, 2) + zero_matrix[kpt_input[..., 0], kpt_input[..., 2] - 1, kpt_input[..., 1] - 1] = colors_masked.transpose( + 1, 0, 2 + ) return zero_matrix condition = _get_condition_matrix(zero_matrix, kpts) @@ -251,7 +240,5 @@ def _get_condition_matrix_optim(zero_matrix, kpts): def get_colors_from_cmap(self, cmap_name, num_colors): cmap = plt.get_cmap(cmap_name) colors_float = [cmap(i) for i in np.linspace(0, 256, num_colors, dtype=int)] - colors = [ - (int(r * 255), int(g * 255), int(b * 255)) for r, g, b, _ in colors_float - ] + colors = [(int(r * 255), int(g * 255), int(b * 255)) for r, g, b, _ in colors_float] return colors diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/norm.py b/deeplabcut/pose_estimation_pytorch/models/modules/norm.py index 1cbc0f4f3a..ecaa94541c 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/norm.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/norm.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Normalization layers""" + from __future__ import annotations import torch @@ -31,7 +32,7 @@ class ScaleNorm(nn.Module): def __init__(self, dim: int, eps: float = 1e-5): super().__init__() - self.scale = dim ** -0.5 + self.scale = dim**-0.5 self.eps = eps self.g = nn.Parameter(torch.ones(1)) diff --git a/deeplabcut/pose_estimation_pytorch/models/necks/layers.py b/deeplabcut/pose_estimation_pytorch/models/necks/layers.py index 6c3ce7d45d..c5ee550d50 100644 --- a/deeplabcut/pose_estimation_pytorch/models/necks/layers.py +++ b/deeplabcut/pose_estimation_pytorch/models/necks/layers.py @@ -157,12 +157,10 @@ def __init__( """ super().__init__() self.heads = heads - self.scale = (dim // heads) ** -0.5 if scale_with_head else dim ** -0.5 + self.scale = (dim // heads) ** -0.5 if scale_with_head else dim**-0.5 self.to_qkv = torch.nn.Linear(dim, dim * 3, bias=False) - self.to_out = torch.nn.Sequential( - torch.nn.Linear(dim, dim), torch.nn.Dropout(dropout) - ) + self.to_out = torch.nn.Sequential(torch.nn.Linear(dim, dim), torch.nn.Dropout(dropout)) self.num_keypoints = num_keypoints def forward(self, x: torch.Tensor, mask: torch.Tensor = None): @@ -259,16 +257,12 @@ def __init__( ), ) ), - Residual( - PreNorm(dim, FeedForward(dim, mlp_dim, dropout=dropout)) - ), + Residual(PreNorm(dim, FeedForward(dim, mlp_dim, dropout=dropout))), ] ) ) - def forward( - self, x: torch.Tensor, mask: torch.Tensor = None, pos: torch.Tensor = None - ): + def forward(self, x: torch.Tensor, mask: torch.Tensor = None, pos: torch.Tensor = None): """Forward pass through the TransformerLayer block. Args: diff --git a/deeplabcut/pose_estimation_pytorch/models/necks/transformer.py b/deeplabcut/pose_estimation_pytorch/models/necks/transformer.py index 7b34d49975..09dba734d1 100644 --- a/deeplabcut/pose_estimation_pytorch/models/necks/transformer.py +++ b/deeplabcut/pose_estimation_pytorch/models/necks/transformer.py @@ -91,13 +91,11 @@ def __init__( channels: int = 32, dropout: float = 0.0, emb_dropout: float = 0.0, - pos_embedding_type: str = "sine-full" + pos_embedding_type: str = "sine-full", ): super().__init__() - num_patches = (feature_size[0] // (patch_size[0])) * ( - feature_size[1] // (patch_size[1]) - ) + num_patches = (feature_size[0] // (patch_size[0])) * (feature_size[1] // (patch_size[1])) patch_dim = channels * patch_size[0] * patch_size[1] self.inplanes = 64 @@ -108,9 +106,7 @@ def __init__( self.pos_embedding_type = pos_embedding_type self.all_attn = self.pos_embedding_type == "sine-full" - self.keypoint_token = torch.nn.Parameter( - torch.zeros(1, self.num_keypoints, dim) - ) + self.keypoint_token = torch.nn.Parameter(torch.zeros(1, self.num_keypoints, dim)) h, w = ( feature_size[0] // (self.patch_size[0]), feature_size[1] // (self.patch_size[1]), @@ -156,9 +152,7 @@ def __init__( if apply_init: self.apply(self._init_weights) - def _make_position_embedding( - self, w: int, h: int, d_model: int, pe_type="learnable" - ): + def _make_position_embedding(self, w: int, h: int, d_model: int, pe_type="learnable"): """Create position embeddings for the transformer. Args: @@ -173,17 +167,11 @@ def _make_position_embedding( self.pe_w = w length = h * w if pe_type != "learnable": - self.pos_embedding = torch.nn.Parameter( - make_sine_position_embedding(h, w, d_model), requires_grad=False - ) + self.pos_embedding = torch.nn.Parameter(make_sine_position_embedding(h, w, d_model), requires_grad=False) else: - self.pos_embedding = torch.nn.Parameter( - torch.zeros(1, self.num_patches + self.num_keypoints, d_model) - ) + self.pos_embedding = torch.nn.Parameter(torch.zeros(1, self.num_patches + self.num_keypoints, d_model)) - def _make_layer( - self, block: torch.nn.Module, planes: int, blocks: int, stride: int = 1 - ) -> torch.nn.Sequential: + def _make_layer(self, block: torch.nn.Module, planes: int, blocks: int, stride: int = 1) -> torch.nn.Sequential: """Create a layer of the transformer encoder. Args: @@ -248,9 +236,7 @@ def forward(self, feature: torch.Tensor, mask=None) -> torch.Tensor: """ p = self.patch_size - x = rearrange( - feature, "b c (h p1) (w p2) -> b (h w) (p1 p2 c)", p1=p[0], p2=p[1] - ) + x = rearrange(feature, "b c (h p1) (w p2) -> b (h w) (p1 p2 c)", p1=p[0], p2=p[1]) x = self.patch_to_embedding(x) b, n, _ = x.shape diff --git a/deeplabcut/pose_estimation_pytorch/models/necks/utils.py b/deeplabcut/pose_estimation_pytorch/models/necks/utils.py index 028078b8ab..bbcf81a939 100644 --- a/deeplabcut/pose_estimation_pytorch/models/necks/utils.py +++ b/deeplabcut/pose_estimation_pytorch/models/necks/utils.py @@ -48,12 +48,8 @@ def make_sine_position_embedding( pos_x = x_embed[:, :, :, None] / dim_t pos_y = y_embed[:, :, :, None] / dim_t - pos_x = torch.stack( - (pos_x[:, :, :, 0::2].sin(), pos_x[:, :, :, 1::2].cos()), dim=4 - ).flatten(3) - pos_y = torch.stack( - (pos_y[:, :, :, 0::2].sin(), pos_y[:, :, :, 1::2].cos()), dim=4 - ).flatten(3) + pos_x = torch.stack((pos_x[:, :, :, 0::2].sin(), pos_x[:, :, :, 1::2].cos()), dim=4).flatten(3) + pos_y = torch.stack((pos_y[:, :, :, 0::2].sin(), pos_y[:, :, :, 1::2].cos()), dim=4).flatten(3) pos = torch.cat((pos_y, pos_x), dim=3).permute(0, 3, 1, 2) pos = pos.flatten(2).permute(0, 2, 1) diff --git a/deeplabcut/pose_estimation_pytorch/models/predictors/base.py b/deeplabcut/pose_estimation_pytorch/models/predictors/base.py index dc9b38aab6..c0b0950da5 100644 --- a/deeplabcut/pose_estimation_pytorch/models/predictors/base.py +++ b/deeplabcut/pose_estimation_pytorch/models/predictors/base.py @@ -47,9 +47,7 @@ def __init__(self): self.num_animals = None @abstractmethod - def forward( - self, stride: float, outputs: dict[str, torch.Tensor] - ) -> dict[str, torch.Tensor]: + def forward(self, stride: float, outputs: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: """Abstract method for the forward pass of the Predictor. Args: diff --git a/deeplabcut/pose_estimation_pytorch/models/predictors/dekr_predictor.py b/deeplabcut/pose_estimation_pytorch/models/predictors/dekr_predictor.py index 2b72cf7261..8c525464de 100644 --- a/deeplabcut/pose_estimation_pytorch/models/predictors/dekr_predictor.py +++ b/deeplabcut/pose_estimation_pytorch/models/predictors/dekr_predictor.py @@ -107,9 +107,7 @@ def __init__( self.nms_threshold = nms_threshold self.apply_pose_nms = apply_pose_nms - def forward( - self, stride: float, outputs: dict[str, torch.Tensor] - ) -> dict[str, torch.Tensor]: + def forward(self, stride: float, outputs: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: """Forward pass of DEKRPredictor. Args: @@ -147,30 +145,18 @@ def forward( poses = self._update_pose_with_heatmaps(poses, heatmaps[:, :-1]) if self.keypoint_score_type == "center": - score = ( - ctr_scores.unsqueeze(-1) - .expand(batch_size, -1, num_joints) - .unsqueeze(-1) - ) + score = ctr_scores.unsqueeze(-1).expand(batch_size, -1, num_joints).unsqueeze(-1) elif self.keypoint_score_type == "heatmap": score = self.get_heat_value(poses, heatmaps).unsqueeze(-1) elif self.keypoint_score_type == "combined": - center_score = ( - ctr_scores.unsqueeze(-1) - .expand(batch_size, -1, num_joints) - .unsqueeze(-1) - ) + center_score = ctr_scores.unsqueeze(-1).expand(batch_size, -1, num_joints).unsqueeze(-1) htmp_score = self.get_heat_value(poses, heatmaps).unsqueeze(-1) score = center_score * htmp_score else: raise ValueError(f"Unknown keypoint score type: {self.keypoint_score_type}") - poses[:, :, :, 0] = ( - poses[:, :, :, 0] * scale_factors[1] + 0.5 * scale_factors[1] - ) - poses[:, :, :, 1] = ( - poses[:, :, :, 1] * scale_factors[0] + 0.5 * scale_factors[0] - ) + poses[:, :, :, 0] = poses[:, :, :, 0] * scale_factors[1] + 0.5 * scale_factors[1] + poses[:, :, :, 1] = poses[:, :, :, 1] * scale_factors[0] + 0.5 * scale_factors[0] if self.clip_scores: score = torch.clip(score, min=0, max=1) @@ -181,9 +167,7 @@ def forward( return {"poses": poses_w_scores} - def get_locations( - self, height: int, width: int, device: torch.device - ) -> torch.Tensor: + def get_locations(self, height: int, width: int, device: torch.device) -> torch.Tensor: """Get locations for offsets. Args: @@ -245,11 +229,7 @@ def offset_to_pose(self, offsets: torch.Tensor) -> torch.Tensor: num_joints = int(num_offset / 2) reg_poses = self.get_reg_poses(offsets, num_joints) - reg_poses = ( - reg_poses.contiguous() - .view(batch_size, h * w, 2 * num_joints) - .permute(0, 2, 1) - ) + reg_poses = reg_poses.contiguous().view(batch_size, h * w, 2 * num_joints).permute(0, 2, 1) reg_poses = reg_poses.contiguous().view(batch_size, -1, h, w).contiguous() return reg_poses @@ -271,15 +251,11 @@ def max_pool(self, heatmap: torch.Tensor) -> torch.Tensor: pool2 = torch.nn.MaxPool2d(5, 1, 2) pool3 = torch.nn.MaxPool2d(7, 1, 3) map_size = (heatmap.shape[1] + heatmap.shape[2]) / 2.0 - maxm = pool2( - heatmap - ) # Here I think pool 2 is a good match for default 17 pos_dist_tresh + maxm = pool2(heatmap) # Here I think pool 2 is a good match for default 17 pos_dist_tresh return maxm - def get_top_values( - self, heatmap: torch.Tensor - ) -> tuple[torch.Tensor, torch.Tensor]: + def get_top_values(self, heatmap: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: """Get top values from the heatmap. Args: @@ -303,9 +279,7 @@ def get_top_values( return pos_ind, scores - def _update_pose_with_heatmaps( - self, _poses: torch.Tensor, kpt_heatmaps: torch.Tensor - ): + def _update_pose_with_heatmaps(self, _poses: torch.Tensor, kpt_heatmaps: torch.Tensor): """If a heatmap center is close enough from the regressed point, the final prediction is the center of this heatmap @@ -323,9 +297,7 @@ def _update_pose_with_heatmaps( x = ind % w y = (ind / w).long() - heats_ind = torch.stack( - (x, y), dim=3 - ) # (batch_size, num_keypoints, num_animals, 2) + heats_ind = torch.stack((x, y), dim=3) # (batch_size, num_keypoints, num_animals, 2) # Calculate differences between all pose-heat pairs # (batch_size, num_animals, num_keypoints, 1, 2) - (batch_size, 1, num_keypoints, num_animals, 2) @@ -333,29 +305,21 @@ def _update_pose_with_heatmaps( 1 ) # (batch_size, num_animals, num_keypoints, num_animals, 2) - pose_heat_dist = torch.norm( - pose_heat_diff, dim=-1 - ) # (batch_size, num_animals, num_keypoints, num_animals) + pose_heat_dist = torch.norm(pose_heat_diff, dim=-1) # (batch_size, num_animals, num_keypoints, num_animals) # Find closest heat point for each pose - keep_ind = torch.argmin( - pose_heat_dist, dim=-1 - ) # (batch_size, num_animals, num_keypoints) + keep_ind = torch.argmin(pose_heat_dist, dim=-1) # (batch_size, num_animals, num_keypoints) # Get minimum distances for filtering min_distances = torch.gather(pose_heat_dist, 3, keep_ind.unsqueeze(-1)).squeeze( -1 ) # (batch_size, num_animals, num_keypoints) - absorb_mask = ( - min_distances < self.max_absorb_distance - ) # (batch_size, num_animals, num_keypoints) + absorb_mask = min_distances < self.max_absorb_distance # (batch_size, num_animals, num_keypoints) # Create indices for gathering the correct heat points batch_indices = torch.arange(batch_size, device=poses.device).view(-1, 1, 1) - keypoint_indices = torch.arange(num_keypoints, device=poses.device).view( - 1, 1, -1 - ) + keypoint_indices = torch.arange(num_keypoints, device=poses.device).view(1, 1, -1) selected_heat_points = heats_ind[ batch_indices, keypoint_indices, keep_ind @@ -365,9 +329,7 @@ def _update_pose_with_heatmaps( return poses - def get_heat_value( - self, pose_coords: torch.Tensor, heatmaps: torch.Tensor - ) -> torch.Tensor: + def get_heat_value(self, pose_coords: torch.Tensor, heatmaps: torch.Tensor) -> torch.Tensor: """Get heat values for pose coordinates and heatmaps. Args: @@ -382,9 +344,7 @@ def get_heat_value( heat_values = predictor.get_heat_value(pose_coords, heatmaps) """ h, w = heatmaps.shape[2:] - heatmaps_nocenter = heatmaps[:, :-1].flatten( - 2, 3 - ) # (batch_size, num_joints, h*w) + heatmaps_nocenter = heatmaps[:, :-1].flatten(2, 3) # (batch_size, num_joints, h*w) # Predicted poses based on the offset can be outside the image x = torch.clamp(torch.floor(pose_coords[:, :, :, 0]), 0, w - 1).long() @@ -413,11 +373,7 @@ def pose_nms(self, poses: torch.Tensor) -> torch.Tensor: w = xy[..., 0].max(dim=-1)[0] - xy[..., 0].min(dim=-1)[0] h = xy[..., 1].max(dim=-1)[0] - xy[..., 1].min(dim=-1)[0] area = torch.clamp((w * w) + (h * h), min=1) - area = ( - area.unsqueeze(1) - .unsqueeze(3) - .expand(batch_size, num_people, num_people, num_joints) - ) + area = area.unsqueeze(1).unsqueeze(3).expand(batch_size, num_people, num_people, num_joints) # compute the difference between keypoints pose_diff = xy.unsqueeze(2) - xy.unsqueeze(1) @@ -432,13 +388,9 @@ def pose_nms(self, poses: torch.Tensor) -> torch.Tensor: nms_pose = pose_dist > self.nms_threshold # shape (b, num_people, num_people) # Upper triangular mask matrix to avoid double processing - triu_mask = torch.triu( - torch.ones(num_people, num_people, device=device), diagonal=1 - ).bool() + triu_mask = torch.triu(torch.ones(num_people, num_people, device=device), diagonal=1).bool() - suppress_pairs = nms_pose & triu_mask.unsqueeze( - 0 - ) # (batch_size, num_people, num_people) + suppress_pairs = nms_pose & triu_mask.unsqueeze(0) # (batch_size, num_people, num_people) # For each batch, determine which poses to suppress suppressed = suppress_pairs.any(dim=1) # (batch_size, num_people) @@ -447,9 +399,7 @@ def pose_nms(self, poses: torch.Tensor) -> torch.Tensor: # Indices for reordering batch_indices = torch.arange(batch_size, device=device).unsqueeze(1) - people_indices = ( - torch.arange(num_people, device=device).unsqueeze(0).expand(batch_size, -1) - ) + people_indices = torch.arange(num_people, device=device).unsqueeze(0).expand(batch_size, -1) # non-suppressed first, then suppressed sort_keys = kept.float() + (people_indices.float() + 1) / (num_people + 1) diff --git a/deeplabcut/pose_estimation_pytorch/models/predictors/identity_predictor.py b/deeplabcut/pose_estimation_pytorch/models/predictors/identity_predictor.py index 9f209df4e7..493d389386 100644 --- a/deeplabcut/pose_estimation_pytorch/models/predictors/identity_predictor.py +++ b/deeplabcut/pose_estimation_pytorch/models/predictors/identity_predictor.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Predictor to generate identity maps from head outputs""" + import torch import torch.nn as nn import torchvision.transforms.functional as F @@ -36,9 +37,7 @@ def __init__(self, apply_sigmoid: bool = True): self.apply_sigmoid = apply_sigmoid self.sigmoid = nn.Sigmoid() - def forward( - self, stride: float, outputs: dict[str, torch.Tensor] - ) -> dict[str, torch.Tensor]: + def forward(self, stride: float, outputs: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: """ Swaps the dimensions so the heatmap are (batch_size, h, w, num_individuals), optionally applies a sigmoid to the heatmaps, and rescales it to be the size diff --git a/deeplabcut/pose_estimation_pytorch/models/predictors/paf_predictor.py b/deeplabcut/pose_estimation_pytorch/models/predictors/paf_predictor.py index ce1bd6b305..900ec33bf4 100644 --- a/deeplabcut/pose_estimation_pytorch/models/predictors/paf_predictor.py +++ b/deeplabcut/pose_estimation_pytorch/models/predictors/paf_predictor.py @@ -109,9 +109,7 @@ def __init__( force_fusion=force_fusion, ) - def forward( - self, stride: float, outputs: dict[str, torch.Tensor] - ) -> dict[str, torch.Tensor]: + def forward(self, stride: float, outputs: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: """Forward pass of PartAffinityFieldPredictor. Gets predictions from model output. Args: @@ -141,21 +139,15 @@ def forward( # Filter predicted heatmaps with a 2D Gaussian kernel as in: # https://openaccess.thecvf.com/content_CVPR_2020/papers/Huang_The_Devil_Is_in_the_Details_Delving_Into_Unbiased_Data_CVPR_2020_paper.pdf - kernel = self.make_2d_gaussian_kernel( - sigma=self.sigma, size=self.nms_radius * 2 + 1 - )[None, None] + kernel = self.make_2d_gaussian_kernel(sigma=self.sigma, size=self.nms_radius * 2 + 1)[None, None] kernel = kernel.repeat(n_channels, 1, 1, 1).to(heatmaps.device) - heatmaps = F.conv2d( - heatmaps, kernel, stride=1, padding="same", groups=n_channels - ) + heatmaps = F.conv2d(heatmaps, kernel, stride=1, padding="same", groups=n_channels) peaks = self.find_local_peak_indices_maxpool_nms( heatmaps, self.nms_radius, threshold=0.01 ) # (n_peaks, 4) -> columns: (batch, part, height, width) if ~torch.any(peaks): - poses = -torch.ones( - (batch_size, self.num_animals, self.num_multibodyparts, 5) - ) + poses = -torch.ones((batch_size, self.num_animals, self.num_multibodyparts, 5)) results = dict(poses=poses) if self.return_preds: results["preds"] = ([dict(coordinates=[[]], costs=[])],) @@ -163,12 +155,8 @@ def forward( return results locrefs = locrefs.reshape(batch_size, n_channels, 2, height, width) - locrefs = ( - locrefs * self.locref_stdev - ) # (batch_size, num_joints, 2, height, width) - pafs = pafs.reshape( - batch_size, -1, 2, height, width - ) # (batch_size, num_edges, 2, height, width) + locrefs = locrefs * self.locref_stdev # (batch_size, num_joints, 2, height, width) + pafs = pafs.reshape(batch_size, -1, 2, height, width) # (batch_size, num_edges, 2, height, width) # Use only the minimal tree edges for efficiency graph = [self.graph[ind] for ind in self.edges_to_keep] @@ -209,9 +197,7 @@ def forward( return out @staticmethod - def find_local_peak_indices_maxpool_nms( - input_: torch.Tensor, radius: int, threshold: float - ) -> torch.Tensor: + def find_local_peak_indices_maxpool_nms(input_: torch.Tensor, radius: int, threshold: float) -> torch.Tensor: pooled = F.max_pool2d(input_, kernel_size=radius, stride=1, padding=radius // 2) maxima = input_ * torch.eq(input_, pooled).float() peak_indices = torch.nonzero(maxima >= threshold, as_tuple=False) @@ -304,12 +290,8 @@ def compute_edge_costs( batch_bodyparts = peak_bodyparts[batch_mask] # Masks of peaks that match each edge's source/dest bodypart for this batch - src_mask = batch_bodyparts.unsqueeze(0) == src_bodypart_id.unsqueeze( - 1 - ) # (n_edges, n_batch_peaks) - dst_mask = batch_bodyparts.unsqueeze(0) == dst_bodypart_id.unsqueeze( - 1 - ) # (n_edges, n_batch_peaks) + src_mask = batch_bodyparts.unsqueeze(0) == src_bodypart_id.unsqueeze(1) # (n_edges, n_batch_peaks) + dst_mask = batch_bodyparts.unsqueeze(0) == dst_bodypart_id.unsqueeze(1) # (n_edges, n_batch_peaks) # Valid src/dst peaks for each edge in this batch: (n_edges, n_batch_peaks, n_batch_peaks) valid_pairs = src_mask.unsqueeze(2) & dst_mask.unsqueeze(1) @@ -339,12 +321,8 @@ def compute_edge_costs( edge_idx = paf_limb_inds[edge_idx] # Map back to original PAF indices # Gather coordinates - src_coords = torch.stack( - [peak_rows[src_idx], peak_cols[src_idx]], dim=1 - ) # (found_pairs, 2) - dst_coords = torch.stack( - [peak_rows[dst_idx], peak_cols[dst_idx]], dim=1 - ) # (found_pairs, 2) + src_coords = torch.stack([peak_rows[src_idx], peak_cols[src_idx]], dim=1) # (found_pairs, 2) + dst_coords = torch.stack([peak_rows[dst_idx], peak_cols[dst_idx]], dim=1) # (found_pairs, 2) vecs_s = src_coords.float() # (found_pairs, 2) vecs_t = dst_coords.float() # (found_pairs, 2) @@ -372,9 +350,7 @@ def compute_edge_costs( ] # Integrate PAF along segment using trapezoidal rule - xy_reversed = torch.flip( - xy.float(), dims=[-1] - ) + xy_reversed = torch.flip(xy.float(), dims=[-1]) integ = torch.trapz(y, xy_reversed, dim=1) # (n_edges, 2) affinities = torch.norm(integ, dim=1) # (n_edges,) affinities = affinities / lengths @@ -401,9 +377,7 @@ def compute_edge_costs( # Run-length encode on (batch, limb) boundaries where (batch, limb) changes change = np.empty(batch_inds.size, dtype=bool) change[0] = True - change[1:] = (batch_inds[1:] != batch_inds[:-1]) | ( - edge_idx[1:] != edge_idx[:-1] - ) + change[1:] = (batch_inds[1:] != batch_inds[:-1]) | (edge_idx[1:] != edge_idx[:-1]) group_starts = np.flatnonzero(change) # Add sentinel end group_ends = np.r_[group_starts[1:], batch_inds.size] @@ -499,9 +473,7 @@ def compute_peaks_and_costs( batch_size, n_channels = heatmaps.shape[:2] n_bodyparts = n_channels - n_id_channels # Refine peak positions to input-image pixels - pos = self.calc_peak_locations( - locrefs, peak_inds_in_batch, strides - ) # (n_peaks, 2) + pos = self.calc_peak_locations(locrefs, peak_inds_in_batch, strides) # (n_peaks, 2) # Compute per-limb affinity matrices via PAF line integral costs = self.compute_edge_costs( diff --git a/deeplabcut/pose_estimation_pytorch/models/predictors/sim_cc.py b/deeplabcut/pose_estimation_pytorch/models/predictors/sim_cc.py index b36a9639ab..41a296b383 100644 --- a/deeplabcut/pose_estimation_pytorch/models/predictors/sim_cc.py +++ b/deeplabcut/pose_estimation_pytorch/models/predictors/sim_cc.py @@ -13,6 +13,7 @@ Based on the official ``mmpose`` SimCC codec and RTMCC head implementation. For more information, see . """ + from __future__ import annotations import numpy as np @@ -58,9 +59,7 @@ def __init__( self.sigma = np.array(sigma) self.decode_beta = decode_beta - def forward( - self, stride: float, outputs: dict[str, torch.Tensor] - ) -> dict[str, torch.Tensor]: + def forward(self, stride: float, outputs: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: x, y = outputs["x"].detach(), outputs["y"].detach() if self.normalize_outputs: @@ -70,9 +69,7 @@ def forward( x = x * (self.sigma[0] * self.decode_beta) y = y * (self.sigma[1] * self.decode_beta) - keypoints, scores = get_simcc_maximum( - x.cpu().numpy(), y.cpu().numpy(), self.apply_softmax - ) + keypoints, scores = get_simcc_maximum(x.cpu().numpy(), y.cpu().numpy(), self.apply_softmax) if keypoints.ndim == 2: keypoints = keypoints[None, :] @@ -169,7 +166,7 @@ def get_simcc_normalized(pred: torch.Tensor) -> torch.Tensor: mask = (pred.amax(dim=-1) > 1).reshape(b, k, 1) # Normalize the tensor using the maximum value - norm = (pred / pred.amax(dim=-1).reshape(b, k, 1)) + norm = pred / pred.amax(dim=-1).reshape(b, k, 1) # return the normalized tensor return torch.where(mask, norm, pred) diff --git a/deeplabcut/pose_estimation_pytorch/models/predictors/single_predictor.py b/deeplabcut/pose_estimation_pytorch/models/predictors/single_predictor.py index b2d6cff006..dfac614b7e 100644 --- a/deeplabcut/pose_estimation_pytorch/models/predictors/single_predictor.py +++ b/deeplabcut/pose_estimation_pytorch/models/predictors/single_predictor.py @@ -55,9 +55,7 @@ def __init__( self.location_refinement = location_refinement self.locref_std = locref_std - def forward( - self, stride: float, outputs: dict[str, torch.Tensor] - ) -> dict[str, torch.Tensor]: + def forward(self, stride: float, outputs: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: """Forward pass of SinglePredictor. Gets predictions from model output. Args: @@ -85,9 +83,7 @@ def forward( locrefs = None if self.location_refinement: locrefs = outputs["locref"] - locrefs = locrefs.permute(0, 2, 3, 1).reshape( - batch_size, height, width, num_joints, 2 - ) + locrefs = locrefs.permute(0, 2, 3, 1).reshape(batch_size, height, width, num_joints, 2) locrefs = locrefs * self.locref_std poses = self.get_pose_prediction(heatmaps, locrefs, scale_factors) @@ -97,9 +93,7 @@ def forward( return {"poses": poses} - def get_top_values( - self, heatmap: torch.Tensor - ) -> Tuple[torch.Tensor, torch.Tensor]: + def get_top_values(self, heatmap: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: """Get the top values from the heatmap. Args: @@ -119,9 +113,7 @@ def get_top_values( y, x = heatmap_top // nx, heatmap_top % nx return y, x - def get_pose_prediction( - self, heatmap: torch.Tensor, locref: torch.Tensor | None, scale_factors - ) -> torch.Tensor: + def get_pose_prediction(self, heatmap: torch.Tensor, locref: torch.Tensor | None, scale_factors) -> torch.Tensor: """Gets the pose prediction given the heatmaps and locref. Args: @@ -146,15 +138,11 @@ def get_pose_prediction( # Create batch and joint indices for indexing # batch_idx: [[0,0,0,...], [1,1,1,...], [2,2,2,...], ...] batch_idx = ( - torch.arange(batch_size, device=heatmap.device) - .unsqueeze(1) - .expand(-1, num_joints) + torch.arange(batch_size, device=heatmap.device).unsqueeze(1).expand(-1, num_joints) ) # (batch_size, num_joints) # joint_idx: [[0,1,2,...], [0,1,2,...], [0,1,2,...], ...] joint_idx = ( - torch.arange(num_joints, device=heatmap.device) - .unsqueeze(0) - .expand(batch_size, -1) + torch.arange(num_joints, device=heatmap.device).unsqueeze(0).expand(batch_size, -1) ) # (batch_size, num_joints) # Vectorized extraction of heatmap scores and locref offsets @@ -164,9 +152,7 @@ def get_pose_prediction( dz[:, 0, :, 2] = scores if locref is not None: - offsets = locref[ - batch_idx, y, x, joint_idx, : - ] # (batch_size, num_joints, 2) + offsets = locref[batch_idx, y, x, joint_idx, :] # (batch_size, num_joints, 2) dz[:, 0, :, :2] = offsets x, y = x.unsqueeze(1), y.unsqueeze(1) # x, y: (batch_size, 1, num_joints) @@ -174,8 +160,6 @@ def get_pose_prediction( x = x * scale_factors[1] + 0.5 * scale_factors[1] + dz[:, :, :, 0] y = y * scale_factors[0] + 0.5 * scale_factors[0] + dz[:, :, :, 1] - pose = torch.stack( - [x, y, dz[:, :, :, 2]], dim=-1 - ) # (batch_size, 1, num_joints, 3) + pose = torch.stack([x, y, dz[:, :, :, 2]], dim=-1) # (batch_size, 1, num_joints, 3) return pose diff --git a/deeplabcut/pose_estimation_pytorch/models/target_generators/dekr_targets.py b/deeplabcut/pose_estimation_pytorch/models/target_generators/dekr_targets.py index 5a21e49185..1bb8c6d668 100644 --- a/deeplabcut/pose_estimation_pytorch/models/target_generators/dekr_targets.py +++ b/deeplabcut/pose_estimation_pytorch/models/target_generators/dekr_targets.py @@ -29,9 +29,7 @@ class DEKRGenerator(BaseGenerator): https://github.com/HRNet/DEKR """ - def __init__( - self, num_joints: int, pos_dist_thresh: int, bg_weight: float = 0.1, **kwargs - ): + def __init__(self, num_joints: int, pos_dist_thresh: int, bg_weight: float = 0.1, **kwargs): """ Args: num_joints: number of keypoints @@ -85,9 +83,7 @@ def forward( coords = labels[self.label_keypoint_key].cpu().numpy() area = labels["area"].cpu().numpy() - assert ( - self.num_joints + 1 == coords.shape[2] - ), f"the number of joints should be {coords.shape}" + assert self.num_joints + 1 == coords.shape[2], f"the number of joints should be {coords.shape}" # TODO make it possible to differentiate between center sigma and other sigmas scale = max(1 / stride_x, 1 / stride_y) @@ -149,13 +145,9 @@ def forward( joint_rg = np.zeros((bb - aa, dd - cc)) for sy in range(aa, bb): for sx in range(cc, dd): - joint_rg[sy - aa, sx - cc] = dekr_heatmap_val( - sigma, sx, sy, x_sm, y_sm - ) + joint_rg[sy - aa, sx - cc] = dekr_heatmap_val(sigma, sx, sy, x_sm, y_sm) - heatmaps[b, idx, aa:bb, cc:dd] = np.maximum( - heatmaps[b, idx, aa:bb, cc:dd], joint_rg - ) + heatmaps[b, idx, aa:bb, cc:dd] = np.maximum(heatmaps[b, idx, aa:bb, cc:dd], joint_rg) heatmap_weights[b, idx, aa:bb, cc:dd] = 1.0 # OFFSET COMPUTATION @@ -178,21 +170,15 @@ def forward( offset_map[b, idx * 2, pos_y, pos_x] = offset_x offset_map[b, idx * 2 + 1, pos_y, pos_x] = offset_y # TODO find a decent constant make weights vary giving animal area - weight_map[b, idx * 2, pos_y, pos_x] = 1.0 / np.sqrt( - area[b, person_id] - ) - weight_map[ - b, idx * 2 + 1, pos_y, pos_x - ] = 1.0 / np.sqrt(area[b, person_id]) + weight_map[b, idx * 2, pos_y, pos_x] = 1.0 / np.sqrt(area[b, person_id]) + weight_map[b, idx * 2 + 1, pos_y, pos_x] = 1.0 / np.sqrt(area[b, person_id]) area_map[b, pos_y, pos_x] = area[b, person_id] heatmap_weights[heatmap_weights == 2] = self.bg_weight return { "heatmap": { "target": torch.tensor(heatmaps, device=outputs["heatmap"].device), - "weights": torch.tensor( - heatmap_weights, device=outputs["heatmap"].device - ), + "weights": torch.tensor(heatmap_weights, device=outputs["heatmap"].device), }, "offset": { "target": torch.tensor(offset_map, device=outputs["offset"].device), @@ -216,4 +202,4 @@ def dekr_heatmap_val(sigma: float, x: float, y: float, x0: float, y0: float) -> Returns: g: calculated heat value represents the intensity of the heat at a given position """ - return np.exp(-((x - x0) ** 2 + (y - y0) ** 2) / (2 * sigma ** 2)) + return np.exp(-((x - x0) ** 2 + (y - y0) ** 2) / (2 * sigma**2)) diff --git a/deeplabcut/pose_estimation_pytorch/models/target_generators/pafs_targets.py b/deeplabcut/pose_estimation_pytorch/models/target_generators/pafs_targets.py index 6c6d15b5a0..404a8db4cc 100644 --- a/deeplabcut/pose_estimation_pytorch/models/target_generators/pafs_targets.py +++ b/deeplabcut/pose_estimation_pytorch/models/target_generators/pafs_targets.py @@ -52,9 +52,7 @@ def forward( batch_size, _, height, width = outputs["heatmap"].shape coords = labels[self.label_keypoint_key].cpu().numpy() - paf_map = np.zeros( - (batch_size, height, width, self.num_limbs * 2), dtype=np.float32 - ) + paf_map = np.zeros((batch_size, height, width, self.num_limbs * 2), dtype=np.float32) grid = np.mgrid[:height, :width].transpose((1, 2, 0)) grid[:, :, 0] = grid[:, :, 0] * stride_y + stride_y / 2 grid[:, :, 1] = grid[:, :, 1] * stride_x + stride_x / 2 @@ -72,7 +70,7 @@ def forward( j2_x, j2_y = kpts_animal[bp2] vec_x = j2_x - j1_x vec_y = j2_y - j1_y - dist = sqrt(vec_x ** 2 + vec_y ** 2) + dist = sqrt(vec_x**2 + vec_y**2) if dist > 0: vec_x_norm = vec_x / dist vec_y_norm = vec_y / dist @@ -83,15 +81,9 @@ def forward( vec_ortho = j1_y * vec_x_norm - j1_x * vec_y_norm distance_along = vec_x_norm * x + vec_y_norm * y - distance_across = ( - ((y * vec_x_norm - x * vec_y_norm) - vec_ortho) - * 1.0 - / self.width - ) + distance_across = ((y * vec_x_norm - x * vec_y_norm) - vec_ortho) * 1.0 / self.width - mask1 = (distance_along >= min(vec)) & ( - distance_along <= max(vec) - ) + mask1 = (distance_along >= min(vec)) & (distance_along <= max(vec)) distance_across_abs = np.abs(distance_across) mask2 = distance_across_abs <= 1 mask = mask1 & mask2 @@ -100,10 +92,4 @@ def forward( paf_map[b, mask, l * 2 + 1] = vec_y_norm * temp paf_map = paf_map.transpose((0, 3, 1, 2)) - return { - "paf": { - "target": torch.tensor( - paf_map, device=outputs["paf"].device - ) - } - } + return {"paf": {"target": torch.tensor(paf_map, device=outputs["paf"].device)}} diff --git a/deeplabcut/pose_estimation_pytorch/models/target_generators/sim_cc.py b/deeplabcut/pose_estimation_pytorch/models/target_generators/sim_cc.py index f0060261ae..dbc68c2c31 100644 --- a/deeplabcut/pose_estimation_pytorch/models/target_generators/sim_cc.py +++ b/deeplabcut/pose_estimation_pytorch/models/target_generators/sim_cc.py @@ -13,6 +13,7 @@ Based on the official ``mmpose`` SimCC codec and RTMCC head implementation. For more information, see . """ + from __future__ import annotations from itertools import product @@ -76,9 +77,7 @@ def __init__( ) if self.smoothing_type == "gaussian" and self.label_smooth_weight > 0: - raise ValueError( - "Attribute `label_smooth_weight` is only " "used for `standard` mode." - ) + raise ValueError("Attribute `label_smooth_weight` is only used for `standard` mode.") if self.label_smooth_weight < 0.0 or self.label_smooth_weight > 1.0: raise ValueError("`label_smooth_weight` should be in range [0, 1]") @@ -139,9 +138,7 @@ def _generate_standard( W = np.around(w * self.simcc_split_ratio).astype(int) H = np.around(h * self.simcc_split_ratio).astype(int) - keypoints_split, keypoint_weights = self._map_coordinates( - keypoints, keypoints_visible - ) + keypoints_split, keypoint_weights = self._map_coordinates(keypoints, keypoints_visible) target_x = np.zeros((N, K, W), dtype=np.float32) target_y = np.zeros((N, K, H), dtype=np.float32) @@ -189,9 +186,7 @@ def _generate_gaussian( W = np.around(w * self.simcc_split_ratio).astype(int) H = np.around(h * self.simcc_split_ratio).astype(int) - keypoints_split, keypoint_weights = self._map_coordinates( - keypoints, keypoints_visible - ) + keypoints_split, keypoint_weights = self._map_coordinates(keypoints, keypoints_visible) target_x = np.zeros((N, K, W), dtype=np.float32) target_y = np.zeros((N, K, H), dtype=np.float32) diff --git a/deeplabcut/pose_estimation_pytorch/modelzoo/config.py b/deeplabcut/pose_estimation_pytorch/modelzoo/config.py index 9c2f1303fb..cda28f87fa 100644 --- a/deeplabcut/pose_estimation_pytorch/modelzoo/config.py +++ b/deeplabcut/pose_estimation_pytorch/modelzoo/config.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Methods to create the configuration files to fine-tune SuperAnimal models""" + from __future__ import annotations import os @@ -63,10 +64,7 @@ def make_super_animal_finetune_config( """ bodyparts = af.get_bodyparts(project_config) if weight_init.dataset is None: - raise ValueError( - "You must set the ``WeightInitialization.dataset`` when fine-tuning " - "SuperAnimal models." - ) + raise ValueError("You must set the ``WeightInitialization.dataset`` when fine-tuning SuperAnimal models.") if not weight_init.with_decoder: raise ValueError( @@ -133,9 +131,7 @@ def create_config_from_modelzoo( The generated pose configuration file. """ # load the model configuration - model_cfg = read_config_as_dict( - get_super_animal_model_config_path(model_name) - ) + model_cfg = read_config_as_dict(get_super_animal_model_config_path(model_name)) if detector_name is None: model_cfg["method"] = Task.BOTTOM_UP.aliases[0].lower() # Use default bottom-up image augmentation if no detector is given (the collate @@ -145,15 +141,11 @@ def create_config_from_modelzoo( model_cfg["data"]["train"] = aug["train"] else: model_cfg["method"] = Task.TOP_DOWN.aliases[0].lower() - model_cfg["detector"] = read_config_as_dict( - get_super_animal_model_config_path(detector_name) - ) + model_cfg["detector"] = read_config_as_dict(get_super_animal_model_config_path(detector_name)) # use SuperAnimal bodyparts if weight_init.memory_replay: - super_animal_project_config = read_config_as_dict( - get_super_animal_project_config_path(super_animal) - ) + super_animal_project_config = read_config_as_dict(get_super_animal_project_config_path(super_animal)) converted_bodyparts = super_animal_project_config["bodyparts"] model_cfg["net_type"] = model_name @@ -166,9 +158,7 @@ def create_config_from_modelzoo( "with_identity": False, } - model_cfg["model"] = config_utils.replace_default_values( - model_cfg["model"], num_bodyparts=len(converted_bodyparts) - ) + model_cfg["model"] = config_utils.replace_default_values(model_cfg["model"], num_bodyparts=len(converted_bodyparts)) model_cfg["train_settings"]["weight_init"] = weight_init.to_dict() model_cfg["inference"] = InferenceConfig().to_dict() diff --git a/deeplabcut/pose_estimation_pytorch/modelzoo/inference.py b/deeplabcut/pose_estimation_pytorch/modelzoo/inference.py index 15c5004264..5b9a2eed11 100644 --- a/deeplabcut/pose_estimation_pytorch/modelzoo/inference.py +++ b/deeplabcut/pose_estimation_pytorch/modelzoo/inference.py @@ -141,10 +141,7 @@ def _video_inference_superanimal( if isinstance(video_paths, str): video_paths = [video_paths] - dest_folder = ( - Path(video_paths[0]).parent if dest_folder is None - else Path(dest_folder) - ) + dest_folder = Path(video_paths[0]).parent if dest_folder is None else Path(dest_folder) dest_folder.mkdir(parents=True, exist_ok=True) if create_labeled_video: superanimal_colormaps = get_superanimal_colormaps() diff --git a/deeplabcut/pose_estimation_pytorch/modelzoo/memory_replay.py b/deeplabcut/pose_estimation_pytorch/modelzoo/memory_replay.py index 002f74bb20..90db53dcfa 100644 --- a/deeplabcut/pose_estimation_pytorch/modelzoo/memory_replay.py +++ b/deeplabcut/pose_estimation_pytorch/modelzoo/memory_replay.py @@ -60,9 +60,7 @@ def get_pose_predictions( The predictions made by the SuperAnimal model on each image in the images list. """ model_name = detector_snapshot_path.stem + "-" + model_snapshot_path.stem - predictions_folder = ( - loader.project_path / "memory_replay" / superanimal_name / model_name - ) + predictions_folder = loader.project_path / "memory_replay" / superanimal_name / model_name predictions_folder.mkdir(exist_ok=True, parents=True) predictions_file = predictions_folder / "pseudo-labels.json" @@ -102,11 +100,7 @@ def get_pose_predictions( # boxes and predicted bounding boxes - keep the larger of the two # bbox_predictions = detector_runner.inference(images=images_to_process) pose_inputs = [ - ( - str(loader.project_path / Path(image)), - {"bboxes": np.array(bboxes[image])} - ) - for image in images_to_process + (str(loader.project_path / Path(image)), {"bboxes": np.array(bboxes[image])}) for image in images_to_process ] predictions = pose_runner.inference(pose_inputs) @@ -202,9 +196,7 @@ def optimal_match(gts_list, preds_list): for i in range(num_gts): for j in range(num_preds): - cost_matrix[i, j] = distance.euclidean( - gts_list[i][..., :2].flatten(), preds_list[j][..., :2].flatten() - ) + cost_matrix[i, j] = distance.euclidean(gts_list[i][..., :2].flatten(), preds_list[j][..., :2].flatten()) row_ind, col_ind = linear_sum_assignment(cost_matrix) return col_ind @@ -248,9 +240,7 @@ def optimal_match(gts_list, preds_list): gts[idx]["keypoints"] = list(matched_gt.flatten()) # memory replay path - memory_replay_train_file_path = os.path.join( - source_dataset_folder, "annotations", "memory_replay_train.json" - ) + memory_replay_train_file_path = os.path.join(source_dataset_folder, "annotations", "memory_replay_train.json") # parse the GT to put the image paths back into OS-specific format for image in project_gt["images"]: @@ -294,18 +284,12 @@ def prepare_memory_replay( pose_threshold: The minimum score for a prediction to be used as a pseudo-label. """ cfg = af.read_config(config) - super_animal_cfg = af.read_plainconfig( - get_super_animal_project_config_path(super_animal=superanimal_name) - ) + super_animal_cfg = af.read_plainconfig(get_super_animal_project_config_path(super_animal=superanimal_name)) if "individuals" in cfg: - temp_dataset = MaDLCPoseDataset( - str(loader.project_path), "temp_dataset", shuffle=loader.shuffle - ) + temp_dataset = MaDLCPoseDataset(str(loader.project_path), "temp_dataset", shuffle=loader.shuffle) else: - temp_dataset = SingleDLCPoseDataset( - str(loader.project_path), "temp_dataset", shuffle=loader.shuffle - ) + temp_dataset = SingleDLCPoseDataset(str(loader.project_path), "temp_dataset", shuffle=loader.shuffle) memory_replay_folder = loader.model_folder / "memory_replay" temp_dataset.materialize( @@ -352,7 +336,10 @@ def prepare_memory_replay( ) dataset.materialize( - memory_replay_folder, framework="coco", deepcopy=False, no_image_copy=True, + memory_replay_folder, + framework="coco", + deepcopy=False, + no_image_copy=True, ) # then in this function, we do pseudo label to match prediction and gts to create diff --git a/deeplabcut/pose_estimation_pytorch/modelzoo/utils.py b/deeplabcut/pose_estimation_pytorch/modelzoo/utils.py index 3c8b09d14c..1b394513b9 100644 --- a/deeplabcut/pose_estimation_pytorch/modelzoo/utils.py +++ b/deeplabcut/pose_estimation_pytorch/modelzoo/utils.py @@ -29,9 +29,7 @@ def get_model_configs_folder_path() -> Path: def get_project_configs_folder_path() -> Path: """Returns: the folder containing the SuperAnimal project configuration files""" - return ( - Path(auxiliaryfunctions.get_deeplabcut_path()) / "modelzoo" / "project_configs" - ) + return Path(auxiliaryfunctions.get_deeplabcut_path()) / "modelzoo" / "project_configs" def get_snapshot_folder_path() -> Path: @@ -117,9 +115,7 @@ def load_super_animal_config( else: model_config["method"] = "TD" if super_animal != "superanimal_humanbody": - detector_cfg_path = get_super_animal_model_config_path( - model_name=detector_name - ) + detector_cfg_path = get_super_animal_model_config_path(model_name=detector_name) detector_cfg = read_config_as_dict(detector_cfg_path) model_config["detector"] = detector_cfg return model_config diff --git a/deeplabcut/pose_estimation_pytorch/post_processing/identity.py b/deeplabcut/pose_estimation_pytorch/post_processing/identity.py index 9f81ab4619..50ce049127 100644 --- a/deeplabcut/pose_estimation_pytorch/post_processing/identity.py +++ b/deeplabcut/pose_estimation_pytorch/post_processing/identity.py @@ -9,15 +9,14 @@ # Licensed under GNU Lesser General Public License v3.0 # """Functions to assign identity to predictions from an identity head""" + from __future__ import annotations import numpy as np from scipy.optimize import linear_sum_assignment -def assign_identity( - predictions: np.ndarray, identity_scores: np.ndarray -) -> np.ndarray: +def assign_identity(predictions: np.ndarray, identity_scores: np.ndarray) -> np.ndarray: """ Args: predictions: Pose predictions for an image, with shape (num_individuals, diff --git a/deeplabcut/pose_estimation_pytorch/post_processing/match_predictions_to_gt.py b/deeplabcut/pose_estimation_pytorch/post_processing/match_predictions_to_gt.py index b37bb92a90..ee679fe1e0 100644 --- a/deeplabcut/pose_estimation_pytorch/post_processing/match_predictions_to_gt.py +++ b/deeplabcut/pose_estimation_pytorch/post_processing/match_predictions_to_gt.py @@ -17,9 +17,7 @@ ) -def rmse_match_prediction_to_gt( - pred_kpts: np.ndarray, gt_kpts: np.ndarray -) -> np.ndarray: +def rmse_match_prediction_to_gt(pred_kpts: np.ndarray, gt_kpts: np.ndarray) -> np.ndarray: """ Hungarian algorithm predicted individuals to ground truth ones, using root mean squared error (rmse). The function provides a way to match predicted individuals to @@ -96,9 +94,7 @@ def rmse_match_prediction_to_gt( return np.array(col_ind) -def oks_match_prediction_to_gt( - pred_kpts: np.array, gt_kpts: np.array, individual_names: list -) -> np.array: +def oks_match_prediction_to_gt(pred_kpts: np.array, gt_kpts: np.array, individual_names: list) -> np.array: """Summary: Hungarian algorithm predicted individuals to ground truth ones, using object keypoint similarity (oks). Oks measures the accuracy of predicted keypoints compared to ground truth keypoints. @@ -141,9 +137,7 @@ def oks_match_prediction_to_gt( num_animals_gt -= 1 oks_matrix = np.zeros((num_animals_gt, num_animals)) - gt_kpts_without_ctr[ - gt_kpts_without_ctr < 0 - ] = np.nan # non visible keypoints should be nan to use calc_oks + gt_kpts_without_ctr[gt_kpts_without_ctr < 0] = np.nan # non visible keypoints should be nan to use calc_oks idx_gt = -1 for g in range(num_animals): if np.isnan(gt_kpts_without_ctr[g]).all(): diff --git a/deeplabcut/pose_estimation_pytorch/post_processing/nms.py b/deeplabcut/pose_estimation_pytorch/post_processing/nms.py index 05e1dce2cb..6bf9368d07 100644 --- a/deeplabcut/pose_estimation_pytorch/post_processing/nms.py +++ b/deeplabcut/pose_estimation_pytorch/post_processing/nms.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Methods for non-maximum suppression of detected poses.""" + import numpy as np from deeplabcut.core.inferenceutils import calc_object_keypoint_similarity diff --git a/deeplabcut/pose_estimation_pytorch/registry.py b/deeplabcut/pose_estimation_pytorch/registry.py index 5b1c0a9869..ae992773c9 100644 --- a/deeplabcut/pose_estimation_pytorch/registry.py +++ b/deeplabcut/pose_estimation_pytorch/registry.py @@ -13,9 +13,7 @@ from typing import Any, Dict, Optional -def build_from_cfg( - cfg: Dict, registry: "Registry", default_args: Optional[Dict] = None -) -> Any: +def build_from_cfg(cfg: Dict, registry: "Registry", default_args: Optional[Dict] = None) -> Any: """Builds a module from the configuration dictionary when it represents a class configuration, or call a function from the configuration dictionary when it represents a function configuration. @@ -57,19 +55,14 @@ def build_from_cfg( else: raise TypeError(f"type must be a str or valid type, but got {type(obj_type)}") try: - sig = inspect.signature( - obj_cls.__init__ if inspect.isclass(obj_cls) else obj_cls - ) - accepts_kwargs = any( - p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values() - ) + sig = inspect.signature(obj_cls.__init__ if inspect.isclass(obj_cls) else obj_cls) + accepts_kwargs = any(p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()) valid_params = { - p for p, param in sig.parameters.items() + p + for p, param in sig.parameters.items() if param.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD) and p != "self" } - filtered_args = { - k: v for k, v in args.items() if accepts_kwargs or k in valid_params - } + filtered_args = {k: v for k, v in args.items() if accepts_kwargs or k in valid_params} return obj_cls(**filtered_args) except Exception as e: # Normal TypeError does not print class name. @@ -131,10 +124,7 @@ def __contains__(self, key): return self.get(key) is not None def __repr__(self): - format_str = ( - self.__class__.__name__ + f"(name={self._name}, " - f"items={self._module_dict})" - ) + format_str = self.__class__.__name__ + f"(name={self._name}, items={self._module_dict})" return format_str @staticmethod @@ -250,9 +240,7 @@ def _add_children(self, registry): """ assert isinstance(registry, Registry) assert registry.scope is not None - assert ( - registry.scope not in self.children - ), f"scope {registry.scope} exists in {self.name} registry" + assert registry.scope not in self.children, f"scope {registry.scope} exists in {self.name} registry" self.children[registry.scope] = registry def _register_module(self, module, module_name=None, force=False): @@ -277,9 +265,7 @@ def _register_module(self, module, module_name=None, force=False): >>> assert registry.get("Model") == Model """ if not inspect.isclass(module) and not inspect.isfunction(module): - raise TypeError( - "module must be a class or a function, " f"but got {type(module)}" - ) + raise TypeError(f"module must be a class or a function, but got {type(module)}") if module_name is None: module_name = module.__name__ @@ -287,7 +273,7 @@ def _register_module(self, module, module_name=None, force=False): module_name = [module_name] for name in module_name: if not force and name in self._module_dict: - raise KeyError(f"{name} is already registered " f"in {self.name}") + raise KeyError(f"{name} is already registered in {self.name}") self._module_dict[name] = module def deprecated_register_module(self, cls=None, force=False): diff --git a/deeplabcut/pose_estimation_pytorch/runners/base.py b/deeplabcut/pose_estimation_pytorch/runners/base.py index f0b4dd735a..eb9adff7cf 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/base.py +++ b/deeplabcut/pose_estimation_pytorch/runners/base.py @@ -76,8 +76,7 @@ def __init__( if len(gpus) == 1: if device != "cuda": raise ValueError( - "When specifying a GPU index to train on, the device must be set " - f"to 'cuda'. Found {device}" + f"When specifying a GPU index to train on, the device must be set to 'cuda'. Found {device}" ) device = f"cuda:{gpus[0]}" diff --git a/deeplabcut/pose_estimation_pytorch/runners/ctd.py b/deeplabcut/pose_estimation_pytorch/runners/ctd.py index b53e319daa..715bcf79a3 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/ctd.py +++ b/deeplabcut/pose_estimation_pytorch/runners/ctd.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Configuration for CTD tracking""" + from dataclasses import dataclass @@ -38,6 +39,7 @@ class CTDTrackingConfig: threshold_nms: The OKS threshold for non-maximum suppression to remove duplicates poses when two CTD model predictions converge to a single animal. """ + bu_on_lost_idv: bool = True bu_min_frequency: int | None = None bu_max_frequency: int | None = 100 diff --git a/deeplabcut/pose_estimation_pytorch/runners/dynamic_cropping.py b/deeplabcut/pose_estimation_pytorch/runners/dynamic_cropping.py index 14a3cb314c..8f3eb6a113 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/dynamic_cropping.py +++ b/deeplabcut/pose_estimation_pytorch/runners/dynamic_cropping.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Modules to dynamically crop individuals out of videos to improve video analysis""" + import math from dataclasses import dataclass, field from typing import Optional @@ -55,6 +56,7 @@ class DynamicCropper: >>> predictions.append(pose) >>> """ + threshold: float margin: int _crop: tuple[int, int, int, int] | None = field(default=None, repr=False) @@ -76,10 +78,7 @@ def crop(self, image: torch.Tensor) -> torch.Tensor: height. """ if len(image) != 1: - raise RuntimeError( - "DynamicCropper can only be used with batch size 1 (found image " - f"shape: {image.shape})" - ) + raise RuntimeError(f"DynamicCropper can only be used with batch size 1 (found image shape: {image.shape})") if self._shape is None: self._shape = image.shape[3], image.shape[2] @@ -153,9 +152,7 @@ def reset(self) -> None: self._crop = None @staticmethod - def build( - dynamic: bool, threshold: float, margin: int - ) -> Optional["DynamicCropper"]: + def build(dynamic: bool, threshold: float, margin: int) -> Optional["DynamicCropper"]: """Builds the DynamicCropper based on the given parameters Args: @@ -301,10 +298,7 @@ def crop(self, image: torch.Tensor) -> torch.Tensor: `crop` was previously called with an image of a different W or H. """ if len(image) != 1: - raise RuntimeError( - "DynamicCropper can only be used with batch size 1 (found image " - f"shape: {image.shape})" - ) + raise RuntimeError(f"DynamicCropper can only be used with batch size 1 (found image shape: {image.shape})") if self._shape is None: self._shape = image.shape[3], image.shape[2] @@ -402,9 +396,7 @@ def num_patches(self) -> int: """Returns: the total number of patches created for an image.""" return self._patch_counts[0] * self._patch_counts[1] - def _prepare_bounding_box( - self, x1: int, y1: int, x2: int, y2: int - ) -> tuple[int, int, int, int]: + def _prepare_bounding_box(self, x1: int, y1: int, x2: int, y2: int) -> tuple[int, int, int, int]: """Prepares the bounding box for cropping. Adds a margin around the bounding box, then transforms it into the target aspect @@ -428,16 +420,18 @@ def _prepare_bounding_box( input_ratio = w / h if input_ratio > self._td_ratio: # h/w < h0/w0 => h' = w * h0/w0 - h = w / self._td_ratio + h = w / self._td_ratio elif input_ratio < self._td_ratio: # w/h < w0/h0 => w' = h * w0/h0 - w = h * self._td_ratio + w = h * self._td_ratio x1, y1 = int(round(cx - (w / 2))), int(round(cy - (h / 2))) w, h = max(int(w), self.min_bbox_size[0]), max(int(h), self.min_bbox_size[1]) return x1, y1, w, h def _crop_bounding_box( - self, image: torch.Tensor, bbox: tuple[int, int, int, int], + self, + image: torch.Tensor, + bbox: tuple[int, int, int, int], ) -> torch.Tensor: """Applies a top-down crop to an image given a bounding box. @@ -491,7 +485,7 @@ def _extract_best_patch(self, pose: torch.Tensor) -> torch.Tensor: # set the crop to the one used for the best patch self._crop = self._patches[best_patch] - return pose[best_patch:best_patch + 1] + return pose[best_patch : best_patch + 1] def generate_patches(self) -> list[tuple[int, int, int, int]]: """Generates patch coordinates for splitting an image. @@ -499,12 +493,8 @@ def generate_patches(self) -> list[tuple[int, int, int, int]]: Returns: A list of patch coordinates as tuples (x0, y0, x1, y1). """ - patch_xs = self.split_array( - self._shape[0], self._patch_counts[0], self._patch_overlap - ) - patch_ys = self.split_array( - self._shape[1], self._patch_counts[1], self._patch_overlap - ) + patch_xs = self.split_array(self._shape[0], self._patch_counts[0], self._patch_overlap) + patch_ys = self.split_array(self._shape[1], self._patch_counts[1], self._patch_overlap) patches = [] for y0, y1 in patch_ys: diff --git a/deeplabcut/pose_estimation_pytorch/runners/inference.py b/deeplabcut/pose_estimation_pytorch/runners/inference.py index 077d045434..370b12093e 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/inference.py +++ b/deeplabcut/pose_estimation_pytorch/runners/inference.py @@ -53,6 +53,7 @@ def _merge_defaults(cls, data: dict[str, Any]): defaults[k] = v return defaults + @dataclass class MultithreadingConfig: """ @@ -61,6 +62,7 @@ class MultithreadingConfig: queue_length: Number of batches to prefetch in async mode timeout: Timeout for queue operations in async mode """ + enabled: bool = True queue_length: int = 4 timeout: float = 30.0 @@ -72,6 +74,7 @@ def from_dict(cls, data: dict[str, Any]) -> "MultithreadingConfig": def to_dict(self) -> dict: return asdict(self) + @dataclass class CompileConfig: """ @@ -79,6 +82,7 @@ class CompileConfig: enabled: Whether to use torch.compile on the model during InferenceRunner initialization backed: torch.compile backend to use """ + enabled: bool = False backend: str = "inductor" @@ -89,12 +93,14 @@ def from_dict(cls, data: dict[str, Any]) -> "CompileConfig": def to_dict(self) -> dict: return asdict(self) + @dataclass class AutocastConfig: """ Parameters for the torch.autocast option: enabled: Whether to use torch.autocast when running inference """ + enabled: bool = False @classmethod @@ -104,12 +110,14 @@ def from_dict(cls, data: dict[str, Any]) -> "AutocastConfig": def to_dict(self) -> dict: return asdict(self) + @dataclass class InferenceConfig: """ Top-level inference configuration that mirrors the `inference` block in pytorch_config.yaml. """ + multithreading: MultithreadingConfig = field(default_factory=MultithreadingConfig) compile: CompileConfig = field(default_factory=CompileConfig) autocast: AutocastConfig = field(default_factory=AutocastConfig) @@ -228,9 +236,7 @@ def __init__( if self.inference_cfg.compile.enabled: try: - self.model = torch.compile( - self.model, backend=self.inference_cfg.compile.backend - ) + self.model = torch.compile(self.model, backend=self.inference_cfg.compile.backend) except Exception as e: warnings.warn( f"torch.compile failed with backend='{self.inference_cfg.compile.backend}', " @@ -252,9 +258,7 @@ def __init__( self._exception = None @abstractmethod - def predict( - self, inputs: torch.Tensor, **kwargs - ) -> list[dict[str, dict[str, np.ndarray]]]: + def predict(self, inputs: torch.Tensor, **kwargs) -> list[dict[str, dict[str, np.ndarray]]]: """Makes predictions from a model input and output Args: @@ -267,10 +271,7 @@ def predict( @torch.inference_mode() def inference( self, - images: ( - Iterable[str | Path | np.ndarray] - | Iterable[tuple[str | Path | np.ndarray, dict[str, Any]]] - ), + images: (Iterable[str | Path | np.ndarray] | Iterable[tuple[str | Path | np.ndarray, dict[str, Any]]]), shelf_writer: shelving.ShelfWriter | None = None, ) -> list[dict[str, np.ndarray]]: """Run model inference on the given dataset @@ -302,10 +303,7 @@ def inference( def _sequential_inference( self, - images: ( - Iterable[str | Path | np.ndarray] - | Iterable[tuple[str | Path | np.ndarray, dict[str, Any]]] - ), + images: (Iterable[str | Path | np.ndarray] | Iterable[tuple[str | Path | np.ndarray, dict[str, Any]]]), shelf_writer: shelving.ShelfWriter | None = None, ) -> list[dict[str, np.ndarray]]: """Original sequential inference implementation""" @@ -324,10 +322,7 @@ def _sequential_inference( def _async_inference( self, - images: ( - Iterable[str | Path | np.ndarray] - | Iterable[tuple[str | Path | np.ndarray, dict[str, Any]]] - ), + images: (Iterable[str | Path | np.ndarray] | Iterable[tuple[str | Path | np.ndarray, dict[str, Any]]]), shelf_writer: shelving.ShelfWriter | None = None, ) -> list[dict[str, np.ndarray]]: """Async inference with pipeline parallelism""" @@ -341,9 +336,7 @@ def _async_inference( self._predictions = [] # Start preprocessing thread - self._preprocessing_thread = threading.Thread( - target=self._preprocessing_worker, args=(images,) - ) + self._preprocessing_thread = threading.Thread(target=self._preprocessing_worker, args=(images,)) self._preprocessing_thread.start() results = [] @@ -416,10 +409,7 @@ def _prepare_inputs( elif isinstance(curr_v, torch.Tensor): curr_v = torch.cat([curr_v, v], dim=0) else: - raise ValueError( - f"model_kwargs {k} must be a numpy array or torch tensor - " - f"found '{type(v)}'." - ) + raise ValueError(f"model_kwargs {k} must be a numpy array or torch tensor - found '{type(v)}'.") self._model_kwargs[k] = curr_v self._contexts.append(context) @@ -440,10 +430,7 @@ def _process_full_batches(self) -> None: def _extract_results(self, shelf_writer: shelving.ShelfWriter) -> list: """Obtains results that were obtained from processing a batch.""" results = [] - while ( - len(self._image_batch_sizes) > 0 - and len(self._predictions) >= self._image_batch_sizes[0] - ): + while len(self._image_batch_sizes) > 0 and len(self._predictions) >= self._image_batch_sizes[0]: num_predictions = self._image_batch_sizes[0] image_predictions = self._predictions[:num_predictions] context = self._contexts[0] @@ -474,9 +461,7 @@ def _process_batch(self) -> None: called, otherwise this method will raise an error. """ batch = torch.stack(self._batch_list[: self.batch_size], dim=0) - model_kwargs = { - mk: v[: self.batch_size] for mk, v in self._model_kwargs.items() - } + model_kwargs = {mk: v[: self.batch_size] for mk, v in self._model_kwargs.items()} self._predictions += self.predict(batch, **model_kwargs) @@ -486,9 +471,7 @@ def _process_batch(self) -> None: self._model_kwargs = {} else: self._batch_list = self._batch_list[self.batch_size :] - self._model_kwargs = { - mk: v[self.batch_size :] for mk, v in self._model_kwargs.items() - } + self._model_kwargs = {mk: v[self.batch_size :] for mk, v in self._model_kwargs.items()} def _inputs_waiting_for_processing(self) -> bool: """Returns: Whether there are inputs which have not yet been processed""" @@ -517,7 +500,11 @@ def _safe_get(self) -> Any: return item except Empty: # check if producer is still running - if self._stop_event.is_set() or self._preprocessing_thread is None or not self._preprocessing_thread.is_alive(): + if ( + self._stop_event.is_set() + or self._preprocessing_thread is None + or not self._preprocessing_thread.is_alive() + ): return None continue @@ -534,9 +521,7 @@ def _preprocessing_worker(self, images: Iterable) -> None: # Process full batches and put them in the queue while len(self._batch_list) >= self.batch_size: batch = torch.stack(self._batch_list[: self.batch_size], dim=0) - model_kwargs = { - mk: v[: self.batch_size] for mk, v in self._model_kwargs.items() - } + model_kwargs = {mk: v[: self.batch_size] for mk, v in self._model_kwargs.items()} self._safe_put((batch, model_kwargs)) @@ -545,10 +530,7 @@ def _preprocessing_worker(self, images: Iterable) -> None: self._batch_list, self._model_kwargs = [], {} else: self._batch_list = self._batch_list[self.batch_size :] - self._model_kwargs = { - mk: v[self.batch_size :] - for mk, v in self._model_kwargs.items() - } + self._model_kwargs = {mk: v[self.batch_size :] for mk, v in self._model_kwargs.items()} # Process any remaining inputs if len(self._batch_list) > 0: @@ -566,10 +548,7 @@ def __del__(self): """Cleanup method to ensure threads are stopped""" if hasattr(self, "_stop_event"): self._stop_event.set() - if ( - hasattr(self, "_preprocessing_thread") - and self._preprocessing_thread is not None - ): + if hasattr(self, "_preprocessing_thread") and self._preprocessing_thread is not None: self._preprocessing_thread.join(timeout=1.0) @@ -585,14 +564,9 @@ def __init__( super().__init__(model, **kwargs) self.dynamic = dynamic if dynamic is not None and self.batch_size != 1: - raise ValueError( - "Dynamic cropping can only be used with batch size 1. Please set " - "your batch size to 1." - ) + raise ValueError("Dynamic cropping can only be used with batch size 1. Please set your batch size to 1.") - def predict( - self, inputs: torch.Tensor, **kwargs - ) -> list[dict[str, dict[str, np.ndarray]]]: + def predict(self, inputs: torch.Tensor, **kwargs) -> list[dict[str, dict[str, np.ndarray]]]: """Makes predictions from a model input and output Args: @@ -620,16 +594,11 @@ def predict( raw_predictions = self.model.get_predictions(outputs) if self.dynamic is not None: - raw_predictions["bodypart"]["poses"] = self.dynamic.update( - raw_predictions["bodypart"]["poses"] - ) + raw_predictions["bodypart"]["poses"] = self.dynamic.update(raw_predictions["bodypart"]["poses"]) predictions = [ { - head: { - pred_name: pred[b].cpu().numpy() - for pred_name, pred in head_outputs.items() - } + head: {pred_name: pred[b].cpu().numpy() for pred_name, pred in head_outputs.items()} for head, head_outputs in raw_predictions.items() } for b in range(batch_size) @@ -682,10 +651,7 @@ def __init__( @torch.inference_mode() def inference( self, - images: ( - Iterable[str | Path | np.ndarray] - | Iterable[tuple[str | Path | np.ndarray, dict[str, Any]]] - ), + images: (Iterable[str | Path | np.ndarray] | Iterable[tuple[str | Path | np.ndarray, dict[str, Any]]]), shelf_writer: shelving.ShelfWriter | None = None, ) -> list[dict[str, np.ndarray]]: """Run CTD model inference on the given dataset @@ -724,9 +690,7 @@ def inference( return results - def predict( - self, inputs: torch.Tensor, **kwargs - ) -> list[dict[str, dict[str, np.ndarray]]]: + def predict(self, inputs: torch.Tensor, **kwargs) -> list[dict[str, dict[str, np.ndarray]]]: """Makes predictions from a model input and output Args: @@ -757,10 +721,7 @@ def predict( predictions = [ { - head: { - pred_name: pred[b].cpu().numpy() - for pred_name, pred in head_outputs.items() - } + head: {pred_name: pred[b].cpu().numpy() for pred_name, pred in head_outputs.items()} for head, head_outputs in raw_predictions.items() } for b in range(len(inputs)) @@ -807,10 +768,7 @@ def add_conditions( def _ctd_tracking_inference( self, - images: ( - Iterable[str | Path | np.ndarray] - | Iterable[tuple[str | Path | np.ndarray, dict[str, Any]]] - ), + images: (Iterable[str | Path | np.ndarray] | Iterable[tuple[str | Path | np.ndarray, dict[str, Any]]]), shelf_writer: shelving.ShelfWriter | None = None, ) -> list[dict[str, np.ndarray]]: results = [] @@ -846,15 +804,8 @@ def _prepare_ctd_inputs(self, data) -> tuple[torch.Tensor, dict[str, Any]]: self._bu_age += 1 if ( self._prev_pose is None - or ( - self._missing_idvs - and self.tracking.bu_on_lost_idv - and self._bu_age >= self.tracking.bu_max_frequency - ) - or ( - self.tracking.bu_min_frequency is not None - and self._bu_age >= self.tracking.bu_min_frequency - ) + or (self._missing_idvs and self.tracking.bu_on_lost_idv and self._bu_age >= self.tracking.bu_max_frequency) + or (self.tracking.bu_min_frequency is not None and self._bu_age >= self.tracking.bu_min_frequency) ): self._bu_age = 0 inputs, context = self.add_conditions(data) @@ -996,9 +947,7 @@ def __init__(self, model: BaseDetector, **kwargs): """ super().__init__(model, **kwargs) - def predict( - self, inputs: torch.Tensor, **kwargs - ) -> list[dict[str, dict[str, np.ndarray]]]: + def predict(self, inputs: torch.Tensor, **kwargs) -> list[dict[str, dict[str, np.ndarray]]]: """Makes predictions from a model input and output Args: diff --git a/deeplabcut/pose_estimation_pytorch/runners/logger.py b/deeplabcut/pose_estimation_pytorch/runners/logger.py index 313da09127..c422d2b4a9 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/logger.py +++ b/deeplabcut/pose_estimation_pytorch/runners/logger.py @@ -131,9 +131,7 @@ def __init__(self, image_log_interval: int | None = None, *args, **kwargs): self._logged = {} self._denormalize = transforms.Compose( [ - transforms.Normalize( - mean=[0, 0, 0], std=[1 / 0.229, 1 / 0.224, 1 / 0.225] - ), + transforms.Normalize(mean=[0, 0, 0], std=[1 / 0.229, 1 / 0.224, 1 / 0.225]), transforms.Normalize(mean=[-0.485, -0.456, -0.406], std=[1, 1, 1]), ] ) @@ -203,9 +201,7 @@ def _prepare_image( # pytorch.org/vision/0.18/generated/torchvision.utils.draw_keypoints.html # pytorch.org/vision/0.17/generated/torchvision.utils.draw_keypoints.html keypoints[torch.any(torch.isnan(keypoints), dim=-1)] = -1 - image = draw_keypoints( - image, keypoints=keypoints[..., :2], colors="red", radius=5 - ) + image = draw_keypoints(image, keypoints=keypoints[..., :2], colors="red", radius=5) if bboxes is not None and len(bboxes) > 0: assert len(bboxes.shape) == 2 @@ -494,10 +490,7 @@ def _load_existing_data(self) -> None: metric_store.append(step_metrics) except Exception as e: - logging.warning( - f"Failed to load existing CSV data from {self.log_file}: {e}. " - "Starting with empty log." - ) + logging.warning(f"Failed to load existing CSV data from {self.log_file}: {e}. Starting with empty log.") return self._steps.extend(steps) self._metric_store.extend(metric_store) @@ -511,10 +504,7 @@ def _prepare_logs(self) -> list[list]: logs = [["step"] + metrics] for step, step_metrics in zip(self._steps, self._metric_store): # Convert None values to empty strings for proper CSV formatting - row = [step] + [ - "" if step_metrics.get(m) is None else step_metrics.get(m) - for m in metrics - ] + row = [step] + ["" if step_metrics.get(m) is None else step_metrics.get(m) for m in metrics] logs.append(row) return logs diff --git a/deeplabcut/pose_estimation_pytorch/runners/schedulers.py b/deeplabcut/pose_estimation_pytorch/runners/schedulers.py index 3b4e2b2cda..a3270e07fd 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/schedulers.py +++ b/deeplabcut/pose_estimation_pytorch/runners/schedulers.py @@ -127,4 +127,4 @@ def load_scheduler_state( # Update the learning rate for the optimizer based on the scheduler for group, resume_lr in zip(param_groups, resume_lrs): - group['lr'] = resume_lr + group["lr"] = resume_lr diff --git a/deeplabcut/pose_estimation_pytorch/runners/shelving.py b/deeplabcut/pose_estimation_pytorch/runners/shelving.py index 9d5edf83fd..b65922ddb0 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/shelving.py +++ b/deeplabcut/pose_estimation_pytorch/runners/shelving.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Modules used to read/write shelve data during video analysis in DeepLabCut 3.0""" + import pickle import shelve from abc import ABC @@ -87,9 +88,7 @@ class ShelfWriter(ShelfManager): filepath: The path to the shelf. """ - def __init__( - self, pose_cfg: dict, filepath: str | Path, num_frames: int | None = None - ): + def __init__(self, pose_cfg: dict, filepath: str | Path, num_frames: int | None = None): super().__init__(filepath, flag="c") self._pose_cfg = pose_cfg self._num_frames = num_frames @@ -141,9 +140,7 @@ def add_prediction( # needed for create_video_with_all_detections to display unique bpts num_unique = unique_bodyparts.shape[1] num_assem, num_ind = id_scores.shape[1:] - output["identity"] += [ - -1 * np.ones((num_assem, num_ind)) for i in range(num_unique) - ] + output["identity"] += [-1 * np.ones((num_assem, num_ind)) for i in range(num_unique)] self._db[key] = output self._frame_index += 1 @@ -173,9 +170,7 @@ def open(self) -> None: "PAFgraph": paf_graph, "PAFinds": self._pose_cfg.get("paf_best", np.arange(len(paf_graph))), "all_joints": [[i] for i in range(len(all_joints))], - "all_joints_names": [ - self._pose_cfg["all_joints_names"][i] for i in range(len(all_joints)) - ], + "all_joints_names": [self._pose_cfg["all_joints_names"][i] for i in range(len(all_joints))], "nframes": self._num_frames, "key_str_width": self._str_width, } @@ -195,9 +190,7 @@ class FeatureShelfWriter(ShelfWriter): filepath: The path to the shelf. """ - def __init__( - self, pose_cfg: dict, filepath: str | Path, num_frames: int | None = None - ): + def __init__(self, pose_cfg: dict, filepath: str | Path, num_frames: int | None = None): super().__init__(pose_cfg, filepath, num_frames) def add_prediction( @@ -220,9 +213,7 @@ def add_prediction( # bodyparts to shape (num_assemblies, num_bpts, xy) coordinates = bodyparts[:, :, :2] if features is None: - raise ValueError( - "Backbone features must be given to the FeatureShelfWriter" - ) + raise ValueError("Backbone features must be given to the FeatureShelfWriter") self._db[key] = dict(coordinates=coordinates, features=features) self._frame_index += 1 diff --git a/deeplabcut/pose_estimation_pytorch/runners/snapshots.py b/deeplabcut/pose_estimation_pytorch/runners/snapshots.py index a5e9f23cf6..b5347d8822 100755 --- a/deeplabcut/pose_estimation_pytorch/runners/snapshots.py +++ b/deeplabcut/pose_estimation_pytorch/runners/snapshots.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Code to handle storing models""" + from __future__ import annotations import warnings @@ -101,11 +102,7 @@ def update(self, epoch: int, state_dict: dict, last: bool = False) -> None: # Save the new best model save_path = self.snapshot_path(epoch, best=True) - parsed_state_dict = { - k: v - for k, v in state_dict.items() - if self.save_optimizer_state or k != "optimizer" - } + parsed_state_dict = {k: v for k, v in state_dict.items() if self.save_optimizer_state or k != "optimizer"} torch.save(parsed_state_dict, save_path) # Handle previous best model @@ -118,11 +115,7 @@ def update(self, epoch: int, state_dict: dict, last: bool = False) -> None: elif last or epoch % self.save_epochs == 0: # Save regular snapshot if needed save_path = self.snapshot_path(epoch=epoch) - parsed_state_dict = { - k: v - for k, v in state_dict.items() - if self.save_optimizer_state or k != "optimizer" - } + parsed_state_dict = {k: v for k, v in state_dict.items() if self.save_optimizer_state or k != "optimizer"} torch.save(parsed_state_dict, save_path) # Clean up old snapshots if needed @@ -167,9 +160,7 @@ def snapshots(self, best_in_last: bool = True) -> list[Snapshot]: trained for. If ``best_in_last=True`` and a best snapshot exists, it will be the last one in the list. """ - return list_snapshots( - self.model_folder, self.snapshot_prefix, best_in_last=best_in_last - ) + return list_snapshots(self.model_folder, self.snapshot_prefix, best_in_last=best_in_last) def snapshot_path(self, epoch: int, best: bool = False) -> Path: """ diff --git a/deeplabcut/pose_estimation_pytorch/runners/train.py b/deeplabcut/pose_estimation_pytorch/runners/train.py index f5d50003f5..4c5e634777 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/train.py +++ b/deeplabcut/pose_estimation_pytorch/runners/train.py @@ -92,9 +92,7 @@ def __init__( log_filename: str = "learning_stats.csv", load_weights_only: bool | None = None, ): - super().__init__( - model=model, device=device, gpus=gpus, snapshot_path=snapshot_path - ) + super().__init__(model=model, device=device, gpus=gpus, snapshot_path=snapshot_path) if isinstance(optimizer, dict): optimizer = build_optimizer(model, optimizer) if isinstance(scheduler, dict): @@ -151,9 +149,7 @@ def state_dict(self) -> dict: return state_dict_ @abstractmethod - def step( - self, batch: dict[str, Any], mode: str = "train" - ) -> dict[str, torch.Tensor]: + def step(self, batch: dict[str, Any], mode: str = "train") -> dict[str, torch.Tensor]: """Perform a single epoch gradient update or validation step Args: @@ -218,9 +214,7 @@ def fit( for e in range(self.starting_epoch + 1, epochs + 1): self.current_epoch = e self._metadata["epoch"] = e - train_loss = self._epoch( - train_loader, mode="train", display_iters=display_iters - ) + train_loss = self._epoch(train_loader, mode="train", display_iters=display_iters) if self.scheduler: self.scheduler.step() @@ -229,9 +223,7 @@ def fit( if e % self.eval_interval == 0: with torch.no_grad(): logging.info(f"Training for epoch {e} done, starting evaluation") - valid_loss = self._epoch( - valid_loader, mode="eval", display_iters=display_iters - ) + valid_loss = self._epoch(valid_loader, mode="eval", display_iters=display_iters) if self._print_valid_loss: msg += f", valid loss {float(valid_loss):.5f}" msg += self._gpu_usage_str() @@ -240,11 +232,7 @@ def fit( logging.info(msg) epoch_metrics = self._metadata.get("metrics") - if ( - e % self.eval_interval == 0 - and epoch_metrics is not None - and len(epoch_metrics) > 0 - ): + if e % self.eval_interval == 0 and epoch_metrics is not None and len(epoch_metrics) > 0: logging.info(f"Model performance:") line_length = max([len(name) for name in epoch_metrics.keys()]) + 2 for name, score in epoch_metrics.items(): @@ -346,9 +334,7 @@ def _load_scheduler_state_dict(self, load_state_dict: bool, snapshot: dict) -> N ) if not loaded_state_dict and self.starting_epoch > 0: - logging.info( - f"Setting the scheduler starting epoch to {self.starting_epoch}" - ) + logging.info(f"Setting the scheduler starting epoch to {self.starting_epoch}") self.scheduler.last_epoch = self.starting_epoch @@ -406,17 +392,13 @@ def load_snapshot( else: backbone_prefix = "backbone." backbone_weights = { - k[len(backbone_prefix) :]: v - for k, v in snapshot["model"].items() - if k.startswith(backbone_prefix) + k[len(backbone_prefix) :]: v for k, v in snapshot["model"].items() if k.startswith(backbone_prefix) } model.backbone.load_state_dict(backbone_weights) return snapshot - def step( - self, batch: dict[str, Any], mode: str = "train" - ) -> dict[str, torch.Tensor]: + def step(self, batch: dict[str, Any], mode: str = "train") -> dict[str, torch.Tensor]: """Perform a single epoch gradient update or validation step. Args: @@ -434,9 +416,7 @@ def step( } """ if mode not in ["train", "eval"]: - raise ValueError( - f"BottomUpSolver must be in train or eval mode, but {mode} was found." - ) + raise ValueError(f"BottomUpSolver must be in train or eval mode, but {mode} was found.") if mode == "train": self.optimizer.zero_grad() @@ -563,9 +543,7 @@ def __init__(self, model: BaseDetector, optimizer: torch.optim.Optimizer, **kwar self._pycoco_warning_displayed = False self._print_valid_loss = False - def step( - self, batch: dict[str, Any], mode: str = "train" - ) -> dict[str, torch.Tensor]: + def step(self, batch: dict[str, Any], mode: str = "train") -> dict[str, torch.Tensor]: """Perform a single epoch gradient update or validation step. Args: @@ -583,9 +561,7 @@ def step( } """ if mode not in ["train", "eval"]: - raise ValueError( - f"DetectorSolver must be in train or eval mode, but {mode} was found." - ) + raise ValueError(f"DetectorSolver must be in train or eval mode, but {mode} was found.") if mode == "train": self.optimizer.zero_grad() @@ -634,9 +610,7 @@ def _compute_epoch_metrics(self) -> dict[str, float]: try: return { f"metrics/test.{k}": v - for k, v in metrics.compute_bbox_metrics( - self._epoch_ground_truth, self._epoch_predictions - ).items() + for k, v in metrics.compute_bbox_metrics(self._epoch_ground_truth, self._epoch_predictions).items() } except ModuleNotFoundError: if not self._pycoco_warning_displayed: diff --git a/deeplabcut/pose_estimation_pytorch/task.py b/deeplabcut/pose_estimation_pytorch/task.py index 12a5049043..9104c711a5 100644 --- a/deeplabcut/pose_estimation_pytorch/task.py +++ b/deeplabcut/pose_estimation_pytorch/task.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Types of tasks that can be run by DeepLabCut pose estimation models""" + from __future__ import annotations from dataclasses import dataclass diff --git a/deeplabcut/pose_estimation_tensorflow/__init__.py b/deeplabcut/pose_estimation_tensorflow/__init__.py index e787ada14d..963368ac08 100644 --- a/deeplabcut/pose_estimation_tensorflow/__init__.py +++ b/deeplabcut/pose_estimation_tensorflow/__init__.py @@ -16,6 +16,7 @@ # Suppress tensorflow warning messages import tensorflow as tf + tf.compat.v1.logging.set_verbosity(tf.compat.v1.logging.ERROR) from deeplabcut.pose_estimation_tensorflow.config import * diff --git a/deeplabcut/pose_estimation_tensorflow/_tf_legacy.py b/deeplabcut/pose_estimation_tensorflow/_tf_legacy.py index 9f75c70257..8ed9128fa7 100644 --- a/deeplabcut/pose_estimation_tensorflow/_tf_legacy.py +++ b/deeplabcut/pose_estimation_tensorflow/_tf_legacy.py @@ -6,7 +6,8 @@ try: import tf_keras.src.legacy_tf_layers as legacy_tf_layers + sys.modules["tf_keras.legacy_tf_layers"] = legacy_tf_layers except ImportError: # Older tf-keras didn’t use src/, so nothing to do - pass \ No newline at end of file + pass diff --git a/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_builder.py b/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_builder.py index 307bf4adf1..42147d19f9 100644 --- a/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_builder.py +++ b/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_builder.py @@ -183,12 +183,8 @@ def efficientnet( def get_model_params(model_name, override_params): """Get the block args and global params for a given model.""" if model_name.startswith("efficientnet"): - width_coefficient, depth_coefficient, _, dropout_rate = efficientnet_params( - model_name - ) - blocks_args, global_params = efficientnet( - width_coefficient, depth_coefficient, dropout_rate - ) + width_coefficient, depth_coefficient, _, dropout_rate = efficientnet_params(model_name) + blocks_args, global_params = efficientnet(width_coefficient, depth_coefficient, dropout_rate) else: raise NotImplementedError("model name is not pre-defined: %s" % model_name) @@ -254,9 +250,7 @@ def build_model( return outputs, model.endpoints -def build_model_base( - images, model_name, use_batch_norm=False, drop_out=False, override_params=None -): +def build_model_base(images, model_name, use_batch_norm=False, drop_out=False, override_params=None): """A helper function to create a base model and return global_pool. Args: images: input images tensor. @@ -276,9 +270,7 @@ def build_model_base( with tf.compat.v1.variable_scope(model_name): model = efficientnet_model.Model(blocks_args, global_params) - features = model( - images, use_batch_norm=use_batch_norm, drop_out=drop_out, features_only=True - ) + features = model(images, use_batch_norm=use_batch_norm, drop_out=drop_out, features_only=True) features = tf.identity(features, "features") return features, model.endpoints diff --git a/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_model.py b/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_model.py index 5a063d2c17..c643f2d618 100644 --- a/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_model.py +++ b/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_model.py @@ -18,6 +18,7 @@ EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks. ICML'19, https://arxiv.org/abs/1905.11946 """ + import collections import math import numpy as np @@ -117,9 +118,7 @@ def round_filters(filters, global_params): # Make sure that round down does not go down by more than 10%. if new_filters < 0.9 * filters: new_filters += divisor - tf.compat.v1.logging.info( - "round_filter input={} output={}".format(orig_f, new_filters) - ) + tf.compat.v1.logging.info("round_filter input={} output={}".format(orig_f, new_filters)) return int(new_filters) @@ -158,9 +157,7 @@ def __init__(self, block_args, global_params): self._relu_fn = global_params.relu_fn or tf.nn.swish self._has_se = ( - global_params.use_se - and self._block_args.se_ratio is not None - and 0 < self._block_args.se_ratio <= 1 + global_params.use_se and self._block_args.se_ratio is not None and 0 < self._block_args.se_ratio <= 1 ) self.endpoints = None @@ -208,9 +205,7 @@ def _build(self): ) if self._has_se: - num_reduced_filters = max( - 1, int(self._block_args.input_filters * self._block_args.se_ratio) - ) + num_reduced_filters = max(1, int(self._block_args.input_filters * self._block_args.se_ratio)) # Squeeze and Excitation layer. self._se_reduce = tf.compat.v1.layers.Conv2D( num_reduced_filters, @@ -255,18 +250,12 @@ def _call_se(self, input_tensor): Returns: A output tensor, which should have the same shape as input. """ - se_tensor = tf.reduce_mean( - input_tensor=input_tensor, axis=self._spatial_dims, keepdims=True - ) + se_tensor = tf.reduce_mean(input_tensor=input_tensor, axis=self._spatial_dims, keepdims=True) se_tensor = self._se_expand(self._relu_fn(self._se_reduce(se_tensor))) - tf.compat.v1.logging.info( - "Built Squeeze and Excitation with tensor shape: %s" % (se_tensor.shape) - ) + tf.compat.v1.logging.info("Built Squeeze and Excitation with tensor shape: %s" % (se_tensor.shape)) return tf.sigmoid(se_tensor) * input_tensor - def call( - self, inputs, use_batch_norm=False, drop_out=False, drop_connect_rate=None - ): + def call(self, inputs, use_batch_norm=False, drop_out=False, drop_connect_rate=None): """Implementation of call(). Args: inputs: the inputs tensor. @@ -275,13 +264,9 @@ def call( Returns: A output tensor. """ - tf.compat.v1.logging.info( - "Block input: %s shape: %s" % (inputs.name, inputs.shape) - ) + tf.compat.v1.logging.info("Block input: %s shape: %s" % (inputs.name, inputs.shape)) if self._block_args.expand_ratio != 1: - x = self._relu_fn( - self._bn0(self._expand_conv(inputs), training=use_batch_norm) - ) + x = self._relu_fn(self._bn0(self._expand_conv(inputs), training=use_batch_norm)) else: x = inputs tf.compat.v1.logging.info("Expand: %s shape: %s" % (x.name, x.shape)) @@ -347,9 +332,7 @@ def _build(self): epsilon=self._batch_norm_epsilon, ) - def call( - self, inputs, use_batch_norm=False, drop_out=False, drop_connect_rate=None - ): + def call(self, inputs, use_batch_norm=False, drop_out=False, drop_connect_rate=None): """Implementation of call(). Args: inputs: the inputs tensor. @@ -358,13 +341,9 @@ def call( Returns: A output tensor. """ - tf.compat.v1.logging.info( - "Block input: %s shape: %s" % (inputs.name, inputs.shape) - ) + tf.compat.v1.logging.info("Block input: %s shape: %s" % (inputs.name, inputs.shape)) if self._block_args.expand_ratio != 1: - x = self._relu_fn( - self._bn0(self._expand_conv(inputs), training=use_batch_norm) - ) + x = self._relu_fn(self._bn0(self._expand_conv(inputs), training=use_batch_norm)) else: x = inputs tf.compat.v1.logging.info("Expand: %s shape: %s" % (x.name, x.shape)) @@ -422,12 +401,8 @@ def _build(self): assert block_args.num_repeat > 0 # Update block input and output filters based on depth multiplier. block_args = block_args._replace( - input_filters=round_filters( - block_args.input_filters, self._global_params - ), - output_filters=round_filters( - block_args.output_filters, self._global_params - ), + input_filters=round_filters(block_args.input_filters, self._global_params), + output_filters=round_filters(block_args.output_filters, self._global_params), num_repeat=round_repeats(block_args.num_repeat, self._global_params), ) @@ -436,9 +411,7 @@ def _build(self): self._blocks.append(conv_block(block_args, self._global_params)) if block_args.num_repeat > 1: # pylint: disable=protected-access - block_args = block_args._replace( - input_filters=block_args.output_filters, strides=[1, 1] - ) + block_args = block_args._replace(input_filters=block_args.output_filters, strides=[1, 1]) # pylint: enable=protected-access for _ in range(block_args.num_repeat - 1): self._blocks.append(conv_block(block_args, self._global_params)) @@ -460,9 +433,7 @@ def _build(self): data_format=self._global_params.data_format, use_bias=False, ) - self._bn0 = self._batch_norm( - axis=channel_axis, momentum=batch_norm_momentum, epsilon=batch_norm_epsilon - ) + self._bn0 = self._batch_norm(axis=channel_axis, momentum=batch_norm_momentum, epsilon=batch_norm_epsilon) # Head part. self._conv_head = tf.compat.v1.layers.Conv2D( @@ -473,13 +444,9 @@ def _build(self): padding="same", use_bias=False, ) - self._bn1 = self._batch_norm( - axis=channel_axis, momentum=batch_norm_momentum, epsilon=batch_norm_epsilon - ) + self._bn1 = self._batch_norm(axis=channel_axis, momentum=batch_norm_momentum, epsilon=batch_norm_epsilon) - self._avg_pooling = tf.keras.layers.GlobalAveragePooling2D( - data_format=self._global_params.data_format - ) + self._avg_pooling = tf.keras.layers.GlobalAveragePooling2D(data_format=self._global_params.data_format) if self._global_params.num_classes: self._fc = tf.compat.v1.layers.Dense( self._global_params.num_classes, @@ -506,21 +473,15 @@ def call(self, inputs, use_batch_norm=False, drop_out=False, features_only=None) self.endpoints = {} # Calls Stem layers with tf.compat.v1.variable_scope("stem"): - outputs = self._relu_fn( - self._bn0(self._conv_stem(inputs), training=use_batch_norm) - ) - tf.compat.v1.logging.info( - "Built stem layers with output shape: %s" % outputs.shape - ) + outputs = self._relu_fn(self._bn0(self._conv_stem(inputs), training=use_batch_norm)) + tf.compat.v1.logging.info("Built stem layers with output shape: %s" % outputs.shape) self.endpoints["stem"] = outputs # Calls blocks. reduction_idx = 0 for idx, block in enumerate(self._blocks): is_reduction = False - if (idx == len(self._blocks) - 1) or self._blocks[ - idx + 1 - ].block_args().strides[0] > 1: + if (idx == len(self._blocks) - 1) or self._blocks[idx + 1].block_args().strides[0] > 1: is_reduction = True reduction_idx += 1 @@ -528,9 +489,7 @@ def call(self, inputs, use_batch_norm=False, drop_out=False, features_only=None) drop_rate = self._global_params.drop_connect_rate if drop_rate: drop_rate *= float(idx) / len(self._blocks) - tf.compat.v1.logging.info( - "block_%s drop_connect_rate: %s" % (idx, drop_rate) - ) + tf.compat.v1.logging.info("block_%s drop_connect_rate: %s" % (idx, drop_rate)) outputs = block.call( outputs, use_batch_norm=use_batch_norm, @@ -550,9 +509,7 @@ def call(self, inputs, use_batch_norm=False, drop_out=False, features_only=None) if not features_only: # Calls final layers and returns logits. with tf.compat.v1.variable_scope("head"): - outputs = self._relu_fn( - self._bn1(self._conv_head(outputs), training=use_batch_norm) - ) + outputs = self._relu_fn(self._bn1(self._conv_head(outputs), training=use_batch_norm)) outputs = self._avg_pooling(outputs) if self._dropout: outputs = self._dropout(outputs, training=drop_out) diff --git a/deeplabcut/pose_estimation_tensorflow/backbones/mobilenet.py b/deeplabcut/pose_estimation_tensorflow/backbones/mobilenet.py index b4fe2f5e8f..40456ebd6b 100644 --- a/deeplabcut/pose_estimation_tensorflow/backbones/mobilenet.py +++ b/deeplabcut/pose_estimation_tensorflow/backbones/mobilenet.py @@ -57,15 +57,11 @@ def _set_arg_scope_defaults(defaults): @slim.add_arg_scope -def depth_multiplier( - output_params, multiplier, divisible_by=8, min_depth=8, **unused_kwargs -): +def depth_multiplier(output_params, multiplier, divisible_by=8, min_depth=8, **unused_kwargs): if "num_outputs" not in output_params: return d = output_params["num_outputs"] - output_params["num_outputs"] = _make_divisible( - d * multiplier, divisible_by, min_depth - ) + output_params["num_outputs"] = _make_divisible(d * multiplier, divisible_by, min_depth) _Op = collections.namedtuple("Op", ["op", "params", "multiplier_func"]) @@ -188,10 +184,11 @@ def mobilenet_base( # pylint: disable=invalid-name # c) set all defaults # d) set all extra overrides. # pylint: disable=g-backslash-continuation - with _scope_all(scope, default_scope="Mobilenet"), safe_arg_scope( - [slim.batch_norm], is_training=is_training - ), _set_arg_scope_defaults(conv_defs_defaults), _set_arg_scope_defaults( - conv_defs_overrides + with ( + _scope_all(scope, default_scope="Mobilenet"), + safe_arg_scope([slim.batch_norm], is_training=is_training), + _set_arg_scope_defaults(conv_defs_defaults), + _set_arg_scope_defaults(conv_defs_overrides), ): # The current_stride variable keeps track of the output stride of the # activations, i.e., the running product of convolution strides up to the @@ -266,9 +263,10 @@ def mobilenet_base( # pylint: disable=invalid-name @contextlib.contextmanager def _scope_all(scope, default_scope=None): - with tf.compat.v1.variable_scope( - scope, default_name=default_scope - ) as s, tf.compat.v1.name_scope(s.original_name_scope): + with ( + tf.compat.v1.variable_scope(scope, default_name=default_scope) as s, + tf.compat.v1.name_scope(s.original_name_scope), + ): yield s @@ -280,7 +278,7 @@ def mobilenet( reuse=None, scope="Mobilenet", base_only=False, - **mobilenet_args + **mobilenet_args, ): """Mobilenet model for classification, supports both V1 and V2. @@ -385,9 +383,7 @@ def global_pool(input_tensor, pool_op=tf.nn.avg_pool2d): ) else: kernel_size = [1, shape[1], shape[2], 1] - output = pool_op( - input_tensor, ksize=kernel_size, strides=[1, 1, 1, 1], padding="VALID" - ) + output = pool_op(input_tensor, ksize=kernel_size, strides=[1, 1, 1, 1], padding="VALID") # Recover output shape, for unknown shape. output.set_shape([None, 1, 1, None]) return output @@ -433,20 +429,19 @@ def training_scope( weight_intitializer = tf.compat.v1.truncated_normal_initializer(stddev=stddev) # Set weight_decay for weights in Conv and FC layers. - with slim.arg_scope( - [slim.conv2d, slim.fully_connected, slim.separable_conv2d], - weights_initializer=weight_intitializer, - normalizer_fn=slim.batch_norm, - ), slim.arg_scope( - [mobilenet_base, mobilenet], is_training=is_training - ), safe_arg_scope( - [slim.batch_norm], **batch_norm_params - ), safe_arg_scope( - [slim.dropout], is_training=is_training, keep_prob=dropout_keep_prob - ), slim.arg_scope( - [slim.conv2d], - weights_regularizer=tf.keras.regularizers.l2(0.5 * (weight_decay)), - ), slim.arg_scope( - [slim.separable_conv2d], weights_regularizer=None - ) as s: + with ( + slim.arg_scope( + [slim.conv2d, slim.fully_connected, slim.separable_conv2d], + weights_initializer=weight_intitializer, + normalizer_fn=slim.batch_norm, + ), + slim.arg_scope([mobilenet_base, mobilenet], is_training=is_training), + safe_arg_scope([slim.batch_norm], **batch_norm_params), + safe_arg_scope([slim.dropout], is_training=is_training, keep_prob=dropout_keep_prob), + slim.arg_scope( + [slim.conv2d], + weights_regularizer=tf.keras.regularizers.l2(0.5 * (weight_decay)), + ), + slim.arg_scope([slim.separable_conv2d], weights_regularizer=None) as s, + ): return s diff --git a/deeplabcut/pose_estimation_tensorflow/backbones/mobilenet_v2.py b/deeplabcut/pose_estimation_tensorflow/backbones/mobilenet_v2.py index f716ed6daa..bedbe4c96e 100644 --- a/deeplabcut/pose_estimation_tensorflow/backbones/mobilenet_v2.py +++ b/deeplabcut/pose_estimation_tensorflow/backbones/mobilenet_v2.py @@ -98,7 +98,7 @@ def mobilenet( min_depth=None, divisible_by=None, activation_fn=None, - **kwargs + **kwargs, ): """Creates mobilenet V2 network. @@ -139,10 +139,7 @@ def mobilenet( if conv_defs is None: conv_defs = V2_DEF if "multiplier" in kwargs: - raise ValueError( - "mobilenetv2 doesn't support generic " - 'multiplier parameter use "depth_multiplier" instead.' - ) + raise ValueError('mobilenetv2 doesn\'t support generic multiplier parameter use "depth_multiplier" instead.') if finegrain_classification_mode: conv_defs = copy.deepcopy(conv_defs) if depth_multiplier < 1: @@ -150,9 +147,7 @@ def mobilenet( if activation_fn: conv_defs = copy.deepcopy(conv_defs) defaults = conv_defs["defaults"] - conv_defaults = defaults[ - (slim.conv2d, slim.fully_connected, slim.separable_conv2d) - ] + conv_defaults = defaults[(slim.conv2d, slim.fully_connected, slim.separable_conv2d)] conv_defaults["activation_fn"] = activation_fn depth_args = {} @@ -170,7 +165,7 @@ def mobilenet( conv_defs=conv_defs, scope=scope, multiplier=depth_multiplier, - **kwargs + **kwargs, ) @@ -187,20 +182,14 @@ def wrapped_partial(func, *args, **kwargs): # 'finegrain_classification_mode' is set to True, which means the embedding # layer will not be shrunk when given a depth-multiplier < 1.0. mobilenet_v2_140 = wrapped_partial(mobilenet, depth_multiplier=1.4) -mobilenet_v2_050 = wrapped_partial( - mobilenet, depth_multiplier=0.50, finegrain_classification_mode=True -) -mobilenet_v2_035 = wrapped_partial( - mobilenet, depth_multiplier=0.35, finegrain_classification_mode=True -) +mobilenet_v2_050 = wrapped_partial(mobilenet, depth_multiplier=0.50, finegrain_classification_mode=True) +mobilenet_v2_035 = wrapped_partial(mobilenet, depth_multiplier=0.35, finegrain_classification_mode=True) @slim.add_arg_scope def mobilenet_base(input_tensor, depth_multiplier=1.0, **kwargs): """Creates base of the mobilenet (no pooling and no logits) .""" - return mobilenet( - input_tensor, depth_multiplier=depth_multiplier, base_only=True, **kwargs - ) + return mobilenet(input_tensor, depth_multiplier=depth_multiplier, base_only=True, **kwargs) def training_scope(**kwargs): diff --git a/deeplabcut/pose_estimation_tensorflow/core/evaluate.py b/deeplabcut/pose_estimation_tensorflow/core/evaluate.py index e575d82f56..455a23274b 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/evaluate.py +++ b/deeplabcut/pose_estimation_tensorflow/core/evaluate.py @@ -28,24 +28,18 @@ def pairwisedistances(DataCombined, scorer1, scorer2, pcutoff=-1, bodyparts=None if bodyparts is None: Pointwisesquareddistance = (DataCombined[scorer1] - DataCombined[scorer2]) ** 2 RMSE = np.sqrt( - Pointwisesquareddistance.xs("x", level=1, axis=1) - + Pointwisesquareddistance.xs("y", level=1, axis=1) + Pointwisesquareddistance.xs("x", level=1, axis=1) + Pointwisesquareddistance.xs("y", level=1, axis=1) ) # Euclidean distance (proportional to RMSE) return RMSE, RMSE[mask] else: - Pointwisesquareddistance = ( - DataCombined[scorer1][bodyparts] - DataCombined[scorer2][bodyparts] - ) ** 2 + Pointwisesquareddistance = (DataCombined[scorer1][bodyparts] - DataCombined[scorer2][bodyparts]) ** 2 RMSE = np.sqrt( - Pointwisesquareddistance.xs("x", level=1, axis=1) - + Pointwisesquareddistance.xs("y", level=1, axis=1) + Pointwisesquareddistance.xs("x", level=1, axis=1) + Pointwisesquareddistance.xs("y", level=1, axis=1) ) # Euclidean distance (proportional to RMSE) return RMSE, RMSE[mask] -def calculatepafdistancebounds( - config, shuffle=0, trainingsetindex=0, modelprefix="", numdigits=0, onlytrain=False -): +def calculatepafdistancebounds(config, shuffle=0, trainingsetindex=0, modelprefix="", numdigits=0, onlytrain=False): """ Returns distances along paf edges in train/test data @@ -82,11 +76,7 @@ def calculatepafdistancebounds( trainFraction = cfg["TrainingFraction"][trainingsetindex] modelfolder = os.path.join( cfg["project_path"], - str( - auxiliaryfunctions.get_model_folder( - trainFraction, shuffle, cfg, modelprefix=modelprefix - ) - ), + str(auxiliaryfunctions.get_model_folder(trainFraction, shuffle, cfg, modelprefix=modelprefix)), ) # Load meta data & annotations @@ -113,13 +103,8 @@ def calculatepafdistancebounds( # get the graph! partaffinityfield_graph = test_pose_cfg["partaffinityfield_graph"] - jointnames = [ - test_pose_cfg["all_joints_names"][i] - for i in range(len(test_pose_cfg["all_joints"])) - ] - path_inferencebounds_config = ( - Path(modelfolder) / "test" / "inferencebounds.yaml" - ) + jointnames = [test_pose_cfg["all_joints_names"][i] for i in range(len(test_pose_cfg["all_joints"]))] + path_inferencebounds_config = Path(modelfolder) / "test" / "inferencebounds.yaml" inferenceboundscfg = {} for pi, edge in enumerate(partaffinityfield_graph): j1, j2 = jointnames[edge[0]], jointnames[edge[1]] @@ -154,12 +139,8 @@ def calculatepafdistancebounds( edgeencoding = str(edge[0]) + "_" + str(edge[1]) inferenceboundscfg[edgeencoding] = {} if len(ds_within) > 0: - inferenceboundscfg[edgeencoding]["intra_max"] = str( - round(np.nanmax(ds_within), numdigits) - ) - inferenceboundscfg[edgeencoding]["intra_min"] = str( - round(np.nanmin(ds_within), numdigits) - ) + inferenceboundscfg[edgeencoding]["intra_max"] = str(round(np.nanmax(ds_within), numdigits)) + inferenceboundscfg[edgeencoding]["intra_min"] = str(round(np.nanmin(ds_within), numdigits)) else: inferenceboundscfg[edgeencoding]["intra_max"] = str( 1e5 @@ -168,30 +149,22 @@ def calculatepafdistancebounds( # NOTE: the inter-animal distances are currently not used, but are interesting to compare to intra_* if len(ds_across) > 0: - inferenceboundscfg[edgeencoding]["inter_max"] = str( - round(np.nanmax(ds_across), numdigits) - ) - inferenceboundscfg[edgeencoding]["inter_min"] = str( - round(np.nanmin(ds_across), numdigits) - ) + inferenceboundscfg[edgeencoding]["inter_max"] = str(round(np.nanmax(ds_across), numdigits)) + inferenceboundscfg[edgeencoding]["inter_min"] = str(round(np.nanmin(ds_across), numdigits)) else: inferenceboundscfg[edgeencoding]["inter_max"] = str( 1e5 ) # large number (larger than image diameters in typical experiments) inferenceboundscfg[edgeencoding]["inter_min"] = str(0) - auxiliaryfunctions.write_plainconfig( - str(path_inferencebounds_config), dict(inferenceboundscfg) - ) + auxiliaryfunctions.write_plainconfig(str(path_inferencebounds_config), dict(inferenceboundscfg)) return inferenceboundscfg else: print("You might as well bring owls to Athens.") return {} -def Plotting( - cfg, comparisonbodyparts, DLCscorer, trainIndices, DataCombined, foldername -): +def Plotting(cfg, comparisonbodyparts, DLCscorer, trainIndices, DataCombined, foldername): """Function used for plotting GT and predictions""" from deeplabcut.utils import visualization @@ -276,22 +249,14 @@ def return_evaluate_network_data( # Data=pd.read_hdf(os.path.join(cfg["project_path"],str(trainingsetfolder),'CollectedData_' + cfg["scorer"] + '.h5'),'df_with_missing') # Get list of body parts to evaluate network for - comparisonbodyparts = ( - auxiliaryfunctions.intersection_of_body_parts_and_ones_given_by_user( - cfg, comparisonbodyparts - ) - ) + comparisonbodyparts = auxiliaryfunctions.intersection_of_body_parts_and_ones_given_by_user(cfg, comparisonbodyparts) ################################################## # Load data... ################################################## trainFraction = cfg["TrainingFraction"][trainingsetindex] modelfolder = os.path.join( cfg["project_path"], - str( - auxiliaryfunctions.get_model_folder( - trainFraction, shuffle, cfg, modelprefix=modelprefix - ) - ), + str(auxiliaryfunctions.get_model_folder(trainFraction, shuffle, cfg, modelprefix=modelprefix)), ) path_train_config, path_test_config, _ = return_train_network_path( config=config, @@ -304,8 +269,7 @@ def return_evaluate_network_data( test_pose_cfg = load_config(str(path_test_config)) except FileNotFoundError: raise FileNotFoundError( - "It seems the model for shuffle %s and trainFraction %s does not exist." - % (shuffle, trainFraction) + "It seems the model for shuffle %s and trainFraction %s does not exist." % (shuffle, trainFraction) ) train_pose_cfg = load_config(str(path_train_config)) @@ -340,11 +304,7 @@ def return_evaluate_network_data( evaluationfolder = os.path.join( cfg["project_path"], - str( - auxiliaryfunctions.get_evaluation_folder( - trainFraction, shuffle, cfg, modelprefix=modelprefix - ) - ), + str(auxiliaryfunctions.get_evaluation_folder(trainFraction, shuffle, cfg, modelprefix=modelprefix)), ) Snapshots = auxiliaryfunctions.get_snapshots_from_folder( @@ -366,9 +326,7 @@ def return_evaluate_network_data( test_pose_cfg["init_weights"] = os.path.join( str(modelfolder), "train", snapshot_name ) # setting weights to corresponding snapshot. - trainingsiterations = (test_pose_cfg["init_weights"].split(os.sep)[-1]).split( - "-" - )[ + trainingsiterations = (test_pose_cfg["init_weights"].split(os.sep)[-1]).split("-")[ -1 ] # read how many training siterations that corresponds to. @@ -388,9 +346,7 @@ def return_evaluate_network_data( notanalyzed, resultsfilename, DLCscorer, - ) = auxiliaryfunctions.check_if_not_evaluated( - str(evaluationfolder), DLCscorer, DLCscorerlegacy, snapshot_name - ) + ) = auxiliaryfunctions.check_if_not_evaluated(str(evaluationfolder), DLCscorer, DLCscorerlegacy, snapshot_name) # resultsfilename=os.path.join(str(evaluationfolder),DLCscorer + '-' + str(Snapshots[snapindex])+ '.h5') # + '-' + str(snapshot)+ ' #'-' + Snapshots[snapindex]+ '.h5') print(resultsfilename) resultsfns.append(resultsfilename) @@ -408,12 +364,8 @@ def return_evaluate_network_data( testerror = np.nanmean(RMSE.iloc[testIndices].values.flatten()) trainerror = np.nanmean(RMSE.iloc[trainIndices].values.flatten()) - testerrorpcutoff = np.nanmean( - RMSEpcutoff.iloc[testIndices].values.flatten() - ) - trainerrorpcutoff = np.nanmean( - RMSEpcutoff.iloc[trainIndices].values.flatten() - ) + testerrorpcutoff = np.nanmean(RMSEpcutoff.iloc[testIndices].values.flatten()) + trainerrorpcutoff = np.nanmean(RMSEpcutoff.iloc[trainIndices].values.flatten()) if show_errors == True: print( "Results for", @@ -669,9 +621,7 @@ def evaluate_network( plotting = bool(plotting) if "TF_CUDNN_USE_AUTOTUNE" in os.environ: - del os.environ[ - "TF_CUDNN_USE_AUTOTUNE" - ] # was potentially set during training + del os.environ["TF_CUDNN_USE_AUTOTUNE"] # was potentially set during training tf.compat.v1.reset_default_graph() os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" # @@ -707,15 +657,11 @@ def evaluate_network( ) # Get list of body parts to evaluate network for - comparisonbodyparts = ( - auxiliaryfunctions.intersection_of_body_parts_and_ones_given_by_user( - cfg, comparisonbodyparts - ) + comparisonbodyparts = auxiliaryfunctions.intersection_of_body_parts_and_ones_given_by_user( + cfg, comparisonbodyparts ) # Make folder for evaluation - auxiliaryfunctions.attempt_to_make_folder( - str(cfg["project_path"] + "/evaluation-results/") - ) + auxiliaryfunctions.attempt_to_make_folder(str(cfg["project_path"] + "/evaluation-results/")) for shuffle in Shuffles: for trainFraction in TrainingFractions: ################################################## @@ -760,15 +706,9 @@ def evaluate_network( # Create folder structure to store results. evaluationfolder = os.path.join( cfg["project_path"], - str( - auxiliaryfunctions.get_evaluation_folder( - trainFraction, shuffle, cfg, modelprefix=modelprefix - ) - ), - ) - auxiliaryfunctions.attempt_to_make_folder( - evaluationfolder, recursive=True + str(auxiliaryfunctions.get_evaluation_folder(trainFraction, shuffle, cfg, modelprefix=modelprefix)), ) + auxiliaryfunctions.attempt_to_make_folder(evaluationfolder, recursive=True) Snapshots = auxiliaryfunctions.get_snapshots_from_folder( train_folder=Path(modelfolder) / "train", @@ -839,13 +779,9 @@ def evaluate_network( ) if notanalyzed: # Specifying state of model (snapshot / training state) - sess, inputs, outputs = predict.setup_pose_prediction( - test_pose_cfg - ) + sess, inputs, outputs = predict.setup_pose_prediction(test_pose_cfg) Numimages = len(Data.index) - PredicteData = np.zeros( - (Numimages, 3 * len(test_pose_cfg["all_joints_names"])) - ) + PredicteData = np.zeros((Numimages, 3 * len(test_pose_cfg["all_joints_names"]))) print("Running evaluation ...") for imageindex, imagename in tqdm(enumerate(Data.index)): image = imread( @@ -857,17 +793,11 @@ def evaluate_network( image_batch = data_to_input(image) # Compute prediction with the CNN - outputs_np = sess.run( - outputs, feed_dict={inputs: image_batch} - ) - scmap, locref = predict.extract_cnn_output( - outputs_np, test_pose_cfg - ) + outputs_np = sess.run(outputs, feed_dict={inputs: image_batch}) + scmap, locref = predict.extract_cnn_output(outputs_np, test_pose_cfg) # Extract maximum scoring location from the heatmap, assume 1 person - pose = predict.argmax_pose_predict( - scmap, locref, test_pose_cfg["stride"] - ) + pose = predict.argmax_pose_predict(scmap, locref, test_pose_cfg["stride"]) PredicteData[imageindex, :] = ( pose.flatten() ) # NOTE: thereby cfg_test['all_joints_names'] should be same order as bodyparts! @@ -884,18 +814,14 @@ def evaluate_network( ) # Saving results - DataMachine = pd.DataFrame( - PredicteData, columns=index, index=Data.index - ) + DataMachine = pd.DataFrame(PredicteData, columns=index, index=Data.index) DataMachine.to_hdf(resultsfilename, key="df_with_missing") print( "Analysis is done and the results are stored (see evaluation-results) for snapshot: ", snapshot_name, ) - DataCombined = pd.concat( - [Data.T, DataMachine.T], axis=0, sort=False - ).T + DataCombined = pd.concat([Data.T, DataMachine.T], axis=0, sort=False).T RMSE, RMSEpcutoff = pairwisedistances( DataCombined, @@ -905,15 +831,9 @@ def evaluate_network( comparisonbodyparts, ) testerror = np.nanmean(RMSE.iloc[testIndices].values.flatten()) - trainerror = np.nanmean( - RMSE.iloc[trainIndices].values.flatten() - ) - testerrorpcutoff = np.nanmean( - RMSEpcutoff.iloc[testIndices].values.flatten() - ) - trainerrorpcutoff = np.nanmean( - RMSEpcutoff.iloc[trainIndices].values.flatten() - ) + trainerror = np.nanmean(RMSE.iloc[trainIndices].values.flatten()) + testerrorpcutoff = np.nanmean(RMSEpcutoff.iloc[testIndices].values.flatten()) + trainerrorpcutoff = np.nanmean(RMSEpcutoff.iloc[trainIndices].values.flatten()) results = [ training_iterations, int(100 * trainFraction), @@ -927,13 +847,9 @@ def evaluate_network( final_result.append(results) if per_keypoint_evaluation: - df_keypoint_error = keypoint_error( - RMSE, RMSEpcutoff, trainIndices, testIndices - ) + df_keypoint_error = keypoint_error(RMSE, RMSEpcutoff, trainIndices, testIndices) kpt_filename = DLCscorer + "-keypoint-results.csv" - df_keypoint_error.to_csv( - Path(evaluationfolder) / kpt_filename - ) + df_keypoint_error.to_csv(Path(evaluationfolder) / kpt_filename) if show_errors: print( @@ -988,9 +904,7 @@ def evaluate_network( DataMachine = pd.read_hdf(resultsfilename) conversioncode.guarantee_multiindex_rows(DataMachine) if plotting: - DataCombined = pd.concat( - [Data.T, DataMachine.T], axis=0, sort=False - ).T + DataCombined = pd.concat([Data.T, DataMachine.T], axis=0, sort=False).T foldername = os.path.join( str(evaluationfolder), "LabeledImages_" + DLCscorer + "_" + snapshot_name, @@ -1009,9 +923,7 @@ def evaluate_network( foldername, ) else: - print( - "Plots already exist for this snapshot... Skipping to the next one." - ) + print("Plots already exist for this snapshot... Skipping to the next one.") if len(final_result) > 0: # Only append if results were calculated make_results_file(final_result, evaluationfolder, DLCscorer) @@ -1056,9 +968,7 @@ def make_results_file(final_result, evaluationfolder, DLCscorer): ## Also storing one "large" table with results: # note: evaluationfolder.parents[0] to get common folder above all shuffle evaluations. df = pd.DataFrame(final_result, columns=col_names) - output_path = os.path.join( - str(Path(evaluationfolder).parents[0]), "CombinedEvaluation-results.csv" - ) + output_path = os.path.join(str(Path(evaluationfolder).parents[0]), "CombinedEvaluation-results.csv") if os.path.exists(output_path): temp = pd.read_csv(output_path, index_col=0) df = pd.concat((temp, df)).reset_index(drop=True) @@ -1084,14 +994,9 @@ def get_available_requested_snapshots( missing_snapshots.append(snap) if len(snapshot_names) == 0: - raise ValueError( - f"None of the requested snapshots were found: \n{missing_snapshots}" - ) + raise ValueError(f"None of the requested snapshots were found: \n{missing_snapshots}") elif len(missing_snapshots) > 0: - print( - f"The following requested snapshots were not found and will be skipped:\n" - f"{missing_snapshots}" - ) + print(f"The following requested snapshots were not found and will be skipped:\n{missing_snapshots}") return snapshot_names @@ -1103,9 +1008,7 @@ def get_snapshots_by_index( """ Assume available_snapshots is ordered in ascending order. Returns snapshot names. """ - if isinstance(idx, int) and -len(available_snapshots) <= idx < len( - available_snapshots - ): + if isinstance(idx, int) and -len(available_snapshots) <= idx < len(available_snapshots): return [available_snapshots[idx]] elif idx == "all": return available_snapshots diff --git a/deeplabcut/pose_estimation_tensorflow/core/evaluate_multianimal.py b/deeplabcut/pose_estimation_tensorflow/core/evaluate_multianimal.py index bd0c4ed1df..7307daf9b7 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/evaluate_multianimal.py +++ b/deeplabcut/pose_estimation_tensorflow/core/evaluate_multianimal.py @@ -145,19 +145,11 @@ def evaluate_multianimal_full( conversioncode.guarantee_multiindex_rows(Data) # Get list of body parts to evaluate network for - comparisonbodyparts = ( - auxiliaryfunctions.intersection_of_body_parts_and_ones_given_by_user( - cfg, comparisonbodyparts - ) - ) - all_bpts = np.asarray( - len(cfg["individuals"]) * cfg["multianimalbodyparts"] + cfg["uniquebodyparts"] - ) + comparisonbodyparts = auxiliaryfunctions.intersection_of_body_parts_and_ones_given_by_user(cfg, comparisonbodyparts) + all_bpts = np.asarray(len(cfg["individuals"]) * cfg["multianimalbodyparts"] + cfg["uniquebodyparts"]) colors = visualization.get_cmap(len(comparisonbodyparts), name=cfg["colormap"]) # Make folder for evaluation - auxiliaryfunctions.attempt_to_make_folder( - str(cfg["project_path"] + "/evaluation-results/") - ) + auxiliaryfunctions.attempt_to_make_folder(str(cfg["project_path"] + "/evaluation-results/")) for shuffle in Shuffles: for trainFraction in TrainingFractions: ################################################## @@ -212,11 +204,7 @@ def evaluate_multianimal_full( # Create folder structure to store results. evaluationfolder = os.path.join( cfg["project_path"], - str( - auxiliaryfunctions.get_evaluation_folder( - trainFraction, shuffle, cfg, modelprefix=modelprefix - ) - ), + str(auxiliaryfunctions.get_evaluation_folder(trainFraction, shuffle, cfg, modelprefix=modelprefix)), ) auxiliaryfunctions.attempt_to_make_folder(evaluationfolder, recursive=True) @@ -241,8 +229,7 @@ def evaluate_multianimal_full( ) except IndexError as err: print( - "Failed to get snapshot_names for trainFraction=" - f"{trainFraction} and shuffle={shuffle}. Error:" + f"Failed to get snapshot_names for trainFraction={trainFraction} and shuffle={shuffle}. Error:" ) print(err) snapshot_names = [] @@ -318,26 +305,18 @@ def evaluate_multianimal_full( # Pass the image and the keypoints through the resizer; # this has no effect if no augmenters were added to it. keypoints = [GT.to_numpy().reshape((-1, 2)).astype(float)] - frame_, keypoints = pipeline( - images=[frame], keypoints=keypoints - ) + frame_, keypoints = pipeline(images=[frame], keypoints=keypoints) frame = frame_[0] GT[:] = keypoints[0].flatten() df = GT.unstack("coords").reindex(joints, level="bodyparts") # FIXME Is having an empty array vs nan really that necessary?! - groundtruthidentity = list( - df.index.get_level_values("individuals") - .to_numpy() - .reshape((-1, 1)) - ) + groundtruthidentity = list(df.index.get_level_values("individuals").to_numpy().reshape((-1, 1))) groundtruthcoordinates = list(df.values[:, np.newaxis]) for i, coords in enumerate(groundtruthcoordinates): if np.isnan(coords).any(): - groundtruthcoordinates[i] = np.empty( - (0, 2), dtype=float - ) + groundtruthcoordinates[i] = np.empty((0, 2), dtype=float) groundtruthidentity[i] = np.array([], dtype=str) # Form 2D array of shape (n_rows, 4) where the last dim is @@ -353,9 +332,7 @@ def evaluate_multianimal_full( ) temp["sample"] = 0 - peaks_gt = temp.loc[ - :, ["sample", "y", "x", "bodyparts"] - ].to_numpy() + peaks_gt = temp.loc[:, ["sample", "y", "x", "bodyparts"]].to_numpy() peaks_gt[:, 1:3] = (peaks_gt[:, 1:3] - stride // 2) / stride pred = predictma.predict_batched_peaks_and_costs( @@ -391,9 +368,7 @@ def evaluate_multianimal_full( # Pick the predictions closest to ground truth, # rather than the ones the model has most confident in xy_gt_values = xy_gt.iloc[inds_gt].values - neighbors = find_closest_neighbors( - xy_gt_values, xy, k=3 - ) + neighbors = find_closest_neighbors(xy_gt_values, xy, k=3) found = neighbors != -1 min_dists = np.linalg.norm( xy_gt_values[found] - xy[neighbors[found]], @@ -403,15 +378,11 @@ def evaluate_multianimal_full( sl = imageindex, inds[inds_gt[found]] dist[sl] = min_dists predicted_poses[sl] = xy[neighbors[found]] - conf[sl] = probs_pred[n_joint][ - neighbors[found] - ].squeeze() + conf[sl] = probs_pred[n_joint][neighbors[found]].squeeze() if plotting == "bodypart": temp_xy = GT.unstack("bodyparts")[joints].values - gt = temp_xy.reshape((-1, 2, temp_xy.shape[1])).T.swapaxes( - 1, 2 - ) + gt = temp_xy.reshape((-1, 2, temp_xy.shape[1])).T.swapaxes(1, 2) h, w, _ = np.shape(frame) fig.set_size_inches(w / 100, h / 100) ax.set_xlim(0, w) @@ -450,18 +421,10 @@ def evaluate_multianimal_full( names=df.index.names + ["coordinates"], ) - predicted_poses = np.concatenate( - (predicted_poses, np.expand_dims(conf, axis=-1)), axis=-1 - ) - predicted_poses = predicted_poses.reshape( - predicted_poses.shape[0], -1 - ) - df_predicted_poses = pd.DataFrame( - predicted_poses, columns=poses_multi_index - ) - write_poses_path = os.path.join( - evaluationfolder, f"predicted_poses_{training_iterations}.h5" - ) + predicted_poses = np.concatenate((predicted_poses, np.expand_dims(conf, axis=-1)), axis=-1) + predicted_poses = predicted_poses.reshape(predicted_poses.shape[0], -1) + df_predicted_poses = pd.DataFrame(predicted_poses, columns=poses_multi_index) + write_poses_path = os.path.join(evaluationfolder, f"predicted_poses_{training_iterations}.h5") df_predicted_poses.to_hdf(write_poses_path, key="df_with_missing") # Compute all distance statistics @@ -473,25 +436,19 @@ def evaluate_multianimal_full( names=["metrics"], axis=1, ) - df_joint = df_joint.reorder_levels( - list(np.roll(df_joint.columns.names, -1)), axis=1 - ) + df_joint = df_joint.reorder_levels(list(np.roll(df_joint.columns.names, -1)), axis=1) df_joint.sort_index( axis=1, level=["individuals", "bodyparts"], ascending=[True, True], inplace=True, ) - write_path = os.path.join( - evaluationfolder, f"dist_{training_iterations}.csv" - ) + write_path = os.path.join(evaluationfolder, f"dist_{training_iterations}.csv") df_joint.to_csv(write_path) # Calculate overall prediction error error = df_joint.xs("rmse", level="metrics", axis=1) - mask = ( - df_joint.xs("conf", level="metrics", axis=1) >= cfg["pcutoff"] - ) + mask = df_joint.xs("conf", level="metrics", axis=1) >= cfg["pcutoff"] error_masked = error[mask] error_train = np.nanmean(error.iloc[trainIndices]) error_train_cut = np.nanmean(error_masked.iloc[trainIndices]) @@ -529,41 +486,20 @@ def evaluate_multianimal_full( print(string.format(*results)) print("##########################################") - print( - "Average Euclidean distance to GT per individual (in pixels; test-only)" - ) - print( - error_masked.iloc[testIndices] - .groupby("individuals", axis=1) - .mean() - .mean() - .to_string() - ) - print( - "Average Euclidean distance to GT per bodypart (in pixels; test-only)" - ) - print( - error_masked.iloc[testIndices] - .groupby("bodyparts", axis=1) - .mean() - .mean() - .to_string() - ) + print("Average Euclidean distance to GT per individual (in pixels; test-only)") + print(error_masked.iloc[testIndices].groupby("individuals", axis=1).mean().mean().to_string()) + print("Average Euclidean distance to GT per bodypart (in pixels; test-only)") + print(error_masked.iloc[testIndices].groupby("bodyparts", axis=1).mean().mean().to_string()) PredicteData["metadata"] = { "nms radius": test_pose_cfg["nmsradius"], "minimal confidence": test_pose_cfg["minconfidence"], "sigma": test_pose_cfg.get("sigma", 1), "PAFgraph": test_pose_cfg["partaffinityfield_graph"], - "PAFinds": np.arange( - len(test_pose_cfg["partaffinityfield_graph"]) - ), - "all_joints": [ - [i] for i in range(len(test_pose_cfg["all_joints"])) - ], + "PAFinds": np.arange(len(test_pose_cfg["partaffinityfield_graph"])), + "all_joints": [[i] for i in range(len(test_pose_cfg["all_joints"]))], "all_joints_names": [ - test_pose_cfg["all_joints_names"][i] - for i in range(len(test_pose_cfg["all_joints"])) + test_pose_cfg["all_joints_names"][i] for i in range(len(test_pose_cfg["all_joints"])) ], "stride": test_pose_cfg.get("stride", 8), } @@ -580,9 +516,7 @@ def evaluate_multianimal_full( "trainFraction": trainFraction, } metadata = {"data": dictionary} - _ = auxfun_multianimal.SaveFullMultiAnimalData( - PredicteData, metadata, resultsfilename - ) + _ = auxfun_multianimal.SaveFullMultiAnimalData(PredicteData, metadata, resultsfilename) tf.compat.v1.reset_default_graph() @@ -634,10 +568,7 @@ def evaluate_multianimal_full( ax.set_ylim(0, h) ax.invert_yaxis() - gt = [ - s.to_numpy().reshape((-1, 2)) - for _, s in Data.loc[imname].groupby("individuals") - ] + gt = [s.to_numpy().reshape((-1, 2)) for _, s in Data.loc[imname].groupby("individuals")] coords_pred = [] coords_pred += [ass.xy for ass in v] probs_pred = [] @@ -670,12 +601,8 @@ def evaluate_multianimal_full( visualization.erase_artists(ax) df = results[1].copy() - df.loc(axis=0)[("mAP_train", "mean")] = [ - d[0]["mAP"] for d in results[2] - ] - df.loc(axis=0)[("mAR_train", "mean")] = [ - d[0]["mAR"] for d in results[2] - ] + df.loc(axis=0)[("mAP_train", "mean")] = [d[0]["mAP"] for d in results[2]] + df.loc(axis=0)[("mAR_train", "mean")] = [d[0]["mAR"] for d in results[2]] df.loc(axis=0)[("mAP_test", "mean")] = [d[1]["mAP"] for d in results[2]] df.loc(axis=0)[("mAR_test", "mean")] = [d[1]["mAR"] for d in results[2]] with open(data_path.replace("_full.", "_map."), "wb") as file: diff --git a/deeplabcut/pose_estimation_tensorflow/core/openvino/mo_extensions/front/tf/unravel_index.py b/deeplabcut/pose_estimation_tensorflow/core/openvino/mo_extensions/front/tf/unravel_index.py index f72dd5e289..a06885e2f8 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/openvino/mo_extensions/front/tf/unravel_index.py +++ b/deeplabcut/pose_estimation_tensorflow/core/openvino/mo_extensions/front/tf/unravel_index.py @@ -43,18 +43,10 @@ def replace_op(self, graph: Graph, node: Node): rows = Div(graph, dict(name=node.name + "/rows")).create_node([inp0, dim1]) - inp0 = Cast( - graph, dict(name=inp0.name + "/fp32", dst_type=np.float32) - ).create_node([inp0]) - dim1 = Cast( - graph, dict(name=dim1.name + "/fp32", dst_type=np.float32) - ).create_node([dim1]) + inp0 = Cast(graph, dict(name=inp0.name + "/fp32", dst_type=np.float32)).create_node([inp0]) + dim1 = Cast(graph, dict(name=dim1.name + "/fp32", dst_type=np.float32)).create_node([dim1]) cols = FloorMod(graph, dict(name=node.name + "/cols")).create_node([inp0, dim1]) - cols = Cast( - graph, dict(name=cols.name + "/i64", dst_type=np.int64) - ).create_node([cols]) + cols = Cast(graph, dict(name=cols.name + "/i64", dst_type=np.int64)).create_node([cols]) - concat = PackOp(graph, dict(name=node.name + "/merged", axis=0)).create_node( - [rows, cols] - ) + concat = PackOp(graph, dict(name=node.name + "/merged", axis=0)).create_node([rows, cols]) return [concat.id] diff --git a/deeplabcut/pose_estimation_tensorflow/core/openvino/session.py b/deeplabcut/pose_estimation_tensorflow/core/openvino/session.py index 9feb5e1042..09f23ea40a 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/openvino/session.py +++ b/deeplabcut/pose_estimation_tensorflow/core/openvino/session.py @@ -69,9 +69,7 @@ def _init_model(self, inp_h, inp_w): }, ) if "GPU" in self.device: - self.core.set_property( - "GPU", {"GPU_THROUGHPUT_STREAMS": "GPU_THROUGHPUT_AUTO"} - ) + self.core.set_property("GPU", {"GPU_THROUGHPUT_STREAMS": "GPU_THROUGHPUT_AUTO"}) compiled_model = self.core.compile_model(self.net, self.device) num_requests = compiled_model.get_property("OPTIMAL_NUMBER_OF_INFER_REQUESTS") @@ -85,9 +83,7 @@ def run(self, out_name, feed_dict): self._init_model(inp.shape[1], inp.shape[2]) batch_size = inp.shape[0] - batch_output = np.zeros( - [batch_size] + self.net.outputs[out_name].shape, dtype=np.float32 - ) + batch_output = np.zeros([batch_size] + self.net.outputs[out_name].shape, dtype=np.float32) def completion_callback(request, inp_id): output = next(iter(request.results.values())) @@ -137,9 +133,7 @@ def completion_callback(request, inp_id): if cfg["cropping"]: frame = frame[cfg["y1"] : cfg["y2"], cfg["x1"] : cfg["x2"]] - sess.infer_queue.start_async( - {sess.input_name: np.expand_dims(frame, axis=0)}, counter - ) + sess.infer_queue.start_async({sess.input_name: np.expand_dims(frame, axis=0)}, counter) counter += 1 diff --git a/deeplabcut/pose_estimation_tensorflow/core/predict.py b/deeplabcut/pose_estimation_tensorflow/core/predict.py index 252ef0aa31..7b653c76b3 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/predict.py +++ b/deeplabcut/pose_estimation_tensorflow/core/predict.py @@ -20,9 +20,7 @@ def setup_pose_prediction(cfg, allow_growth=False, collect_extra=False): tf.compat.v1.reset_default_graph() - inputs = tf.compat.v1.placeholder( - tf.float32, shape=[cfg["batch_size"], None, None, 3] - ) + inputs = tf.compat.v1.placeholder(tf.float32, shape=[cfg["batch_size"], None, None, 3]) net_heads = PoseNetFactory.create(cfg).test(inputs) extra_dict = {} outputs = [net_heads["part_prob"]] @@ -78,9 +76,7 @@ def argmax_pose_predict(scmap, offmat, stride): num_joints = scmap.shape[2] pose = [] for joint_idx in range(num_joints): - maxloc = np.unravel_index( - np.argmax(scmap[:, :, joint_idx]), scmap[:, :, joint_idx].shape - ) + maxloc = np.unravel_index(np.argmax(scmap[:, :, joint_idx]), scmap[:, :, joint_idx].shape) offset = np.array(offmat[maxloc][joint_idx])[::-1] pos_f8 = np.array(maxloc).astype("float") * stride + 0.5 * stride + offset pose.append(np.hstack((pos_f8[::-1], [scmap[maxloc][joint_idx]]))) @@ -190,9 +186,7 @@ def getposeNP(image, cfg, sess, inputs, outputs, outall=False): Ys = Y.swapaxes(0, 2).swapaxes(0, 1) Ps = P.swapaxes(0, 2).swapaxes(0, 1) - pose = np.empty( - (cfg["batch_size"], num_outputs * cfg["num_joints"] * 3), dtype=X.dtype - ) + pose = np.empty((cfg["batch_size"], num_outputs * cfg["num_joints"] * 3), dtype=X.dtype) pose[:, 0::3] = Xs.reshape(batchsize, -1) pose[:, 1::3] = Ys.reshape(batchsize, -1) pose[:, 2::3] = Ps.reshape(batchsize, -1) @@ -206,9 +200,7 @@ def getposeNP(image, cfg, sess, inputs, outputs, outall=False): ### Code for TF inference on GPU def setup_GPUpose_prediction(cfg, allow_growth=False): tf.compat.v1.reset_default_graph() - inputs = tf.compat.v1.placeholder( - tf.float32, shape=[cfg["batch_size"], None, None, 3] - ) + inputs = tf.compat.v1.placeholder(tf.float32, shape=[cfg["batch_size"], None, None, 3]) net_heads = PoseNetFactory.create(cfg).inference(inputs) outputs = [net_heads["pose"]] diff --git a/deeplabcut/pose_estimation_tensorflow/core/predict_multianimal.py b/deeplabcut/pose_estimation_tensorflow/core/predict_multianimal.py index c212c9380e..4efe107a9c 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/predict_multianimal.py +++ b/deeplabcut/pose_estimation_tensorflow/core/predict_multianimal.py @@ -259,9 +259,7 @@ def predict_batched_peaks_and_costs( def find_local_maxima(scmap, radius, threshold): - peak_idx = peak_local_max( - scmap, min_distance=radius, threshold_abs=threshold, exclude_border=False - ) + peak_idx = peak_local_max(scmap, min_distance=radius, threshold_abs=threshold, exclude_border=False) grid = np.zeros_like(scmap, dtype=bool) grid[tuple(peak_idx.T)] = True labels = measurements.label(grid)[0] diff --git a/deeplabcut/pose_estimation_tensorflow/core/test.py b/deeplabcut/pose_estimation_tensorflow/core/test.py index 06bed0af9e..fa3e7d9b0e 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/test.py +++ b/deeplabcut/pose_estimation_tensorflow/core/test.py @@ -77,9 +77,7 @@ def test_net(visualise, cache_scoremaps): out_fn = os.path.join(out_dir, raw_name + "_locreg" + ".mat") if cfg["location_refinement"]: - scipy.io.savemat( - out_fn, mdict={"locreg_pred": locref.astype("float32")} - ) + scipy.io.savemat(out_fn, mdict={"locreg_pred": locref.astype("float32")}) scipy.io.savemat("predictions.mat", mdict={"joints": predictions}) diff --git a/deeplabcut/pose_estimation_tensorflow/core/train.py b/deeplabcut/pose_estimation_tensorflow/core/train.py index 487df6b462..10a20c6986 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/train.py +++ b/deeplabcut/pose_estimation_tensorflow/core/train.py @@ -60,10 +60,7 @@ def get_batch_spec(cfg): def setup_preloading(batch_spec): - placeholders = { - name: tf.compat.v1.placeholder(tf.float32, shape=spec) - for (name, spec) in batch_spec.items() - } + placeholders = {name: tf.compat.v1.placeholder(tf.float32, shape=spec) for (name, spec) in batch_spec.items()} names = placeholders.keys() placeholders_list = list(placeholders.values()) @@ -101,16 +98,12 @@ def get_optimizer(loss_op, cfg): if "efficientnet" in cfg["net_type"]: print("Switching to cosine decay schedule with adam!") cfg["optimizer"] = "adam" - learning_rate = tf.compat.v1.train.cosine_decay( - cfg["lr_init"], tstep, cfg["decay_steps"], alpha=cfg["alpha_r"] - ) + learning_rate = tf.compat.v1.train.cosine_decay(cfg["lr_init"], tstep, cfg["decay_steps"], alpha=cfg["alpha_r"]) else: learning_rate = tf.compat.v1.placeholder(tf.float32, shape=[]) if cfg["optimizer"] == "sgd": - optimizer = tf.compat.v1.train.MomentumOptimizer( - learning_rate=learning_rate, momentum=0.9 - ) + optimizer = tf.compat.v1.train.MomentumOptimizer(learning_rate=learning_rate, momentum=0.9) elif cfg["optimizer"] == "adam": optimizer = tf.compat.v1.train.AdamOptimizer(learning_rate) else: @@ -124,22 +117,16 @@ def get_optimizer_with_freeze(loss_op, cfg): learning_rate = tf.compat.v1.placeholder(tf.float32, shape=[]) if cfg["optimizer"] == "sgd": - optimizer = tf.compat.v1.train.MomentumOptimizer( - learning_rate=learning_rate, momentum=0.9 - ) + optimizer = tf.compat.v1.train.MomentumOptimizer(learning_rate=learning_rate, momentum=0.9) elif cfg["optimizer"] == "adam": optimizer = tf.compat.v1.train.AdamOptimizer(learning_rate) else: raise ValueError("unknown optimizer {}".format(cfg["optimizer"])) train_unfrozen_op = slim.learning.create_train_op(loss_op, optimizer) - variables_unfrozen = tf.compat.v1.get_collection( - tf.compat.v1.GraphKeys.TRAINABLE_VARIABLES, "pose" - ) + variables_unfrozen = tf.compat.v1.get_collection(tf.compat.v1.GraphKeys.TRAINABLE_VARIABLES, "pose") - train_frozen_op = slim.learning.create_train_op( - loss_op, optimizer, variables_to_train=variables_unfrozen - ) + train_frozen_op = slim.learning.create_train_op(loss_op, optimizer, variables_to_train=variables_unfrozen) return learning_rate, train_unfrozen_op, train_frozen_op @@ -154,9 +141,7 @@ def train( allow_growth=True, ): start_path = os.getcwd() - os.chdir( - str(Path(config_yaml).parents[0]) - ) # switch to folder of config_yaml (for logging) + os.chdir(str(Path(config_yaml).parents[0])) # switch to folder of config_yaml (for logging) setup_logging() cfg = load_config(config_yaml) @@ -189,16 +174,11 @@ def train( if "resnet" in net_type: variables_to_restore = slim.get_variables_to_restore(include=["resnet_v1"]) elif "mobilenet" in net_type: - variables_to_restore = slim.get_variables_to_restore( - include=["MobilenetV2"] - ) + variables_to_restore = slim.get_variables_to_restore(include=["MobilenetV2"]) elif "efficientnet" in net_type: - variables_to_restore = slim.get_variables_to_restore( - include=["efficientnet"] - ) + variables_to_restore = slim.get_variables_to_restore(include=["efficientnet"]) variables_to_restore = { - var.op.name.replace("efficientnet/", "") - + "/ExponentialMovingAverage": var + var.op.name.replace("efficientnet/", "") + "/ExponentialMovingAverage": var for var in variables_to_restore } else: @@ -284,20 +264,14 @@ def train( current_lr = lr_gen.get_lr(it - start_iter) lr_dict = {learning_rate: current_lr} - [_, loss_val, summary] = sess.run( - [train_op, total_loss, merged_summaries], feed_dict=lr_dict - ) + [_, loss_val, summary] = sess.run([train_op, total_loss, merged_summaries], feed_dict=lr_dict) cum_loss += loss_val train_writer.add_summary(summary, it) if it % display_iters == 0 and it > start_iter: average_loss = cum_loss / display_iters cum_loss = 0.0 - logging.info( - "iteration: {} loss: {} lr: {}".format( - it, "{0:.4f}".format(average_loss), current_lr - ) - ) + logging.info("iteration: {} loss: {} lr: {}".format(it, "{0:.4f}".format(average_loss), current_lr)) lrf.write("{}, {:.5f}, {}\n".format(it, average_loss, current_lr)) lrf.flush() diff --git a/deeplabcut/pose_estimation_tensorflow/core/train_multianimal.py b/deeplabcut/pose_estimation_tensorflow/core/train_multianimal.py index 4d98c226a0..7623b7774f 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/train_multianimal.py +++ b/deeplabcut/pose_estimation_tensorflow/core/train_multianimal.py @@ -53,9 +53,7 @@ def train( start_path = os.getcwd() if modelfolder == "": - os.chdir( - str(Path(config_yaml).parents[0]) - ) # switch to folder of config_yaml (for logging) + os.chdir(str(Path(config_yaml).parents[0])) # switch to folder of config_yaml (for logging) else: os.chdir(modelfolder) @@ -136,16 +134,11 @@ def train( if "resnet" in net_type: variables_to_restore = slim.get_variables_to_restore(include=["resnet_v1"]) elif "mobilenet" in net_type: - variables_to_restore = slim.get_variables_to_restore( - include=["MobilenetV2"] - ) + variables_to_restore = slim.get_variables_to_restore(include=["MobilenetV2"]) elif "efficientnet" in net_type: - variables_to_restore = slim.get_variables_to_restore( - include=["efficientnet"] - ) + variables_to_restore = slim.get_variables_to_restore(include=["efficientnet"]) variables_to_restore = { - var.op.name.replace("efficientnet/", "") - + "/ExponentialMovingAverage": var + var.op.name.replace("efficientnet/", "") + "/ExponentialMovingAverage": var for var in variables_to_restore } else: diff --git a/deeplabcut/pose_estimation_tensorflow/datasets/augmentation.py b/deeplabcut/pose_estimation_tensorflow/datasets/augmentation.py index f2923a6347..0c93d4182a 100644 --- a/deeplabcut/pose_estimation_tensorflow/datasets/augmentation.py +++ b/deeplabcut/pose_estimation_tensorflow/datasets/augmentation.py @@ -82,8 +82,7 @@ def __init__( self.max_shift = max(0.0, min(max_shift, 0.4)) if crop_sampling not in ("uniform", "keypoints", "density", "hybrid"): raise ValueError( - f"Invalid sampling {crop_sampling}. Must be " - f"either 'uniform', 'keypoints', 'density', or 'hybrid." + f"Invalid sampling {crop_sampling}. Must be either 'uniform', 'keypoints', 'density', or 'hybrid." ) self.crop_sampling = crop_sampling diff --git a/deeplabcut/pose_estimation_tensorflow/datasets/pose_base.py b/deeplabcut/pose_estimation_tensorflow/datasets/pose_base.py index f48390bc26..6fcff78805 100644 --- a/deeplabcut/pose_estimation_tensorflow/datasets/pose_base.py +++ b/deeplabcut/pose_estimation_tensorflow/datasets/pose_base.py @@ -30,8 +30,6 @@ def sample_scale(self): np.random.seed(42) scale = self.cfg["global_scale"] if "scale_jitter_lo" in self.cfg and "scale_jitter_up" in self.cfg: - scale_jitter = np.random.uniform( - self.cfg["scale_jitter_lo"], self.cfg["scale_jitter_up"] - ) + scale_jitter = np.random.uniform(self.cfg["scale_jitter_lo"], self.cfg["scale_jitter_up"]) scale *= scale_jitter return scale diff --git a/deeplabcut/pose_estimation_tensorflow/datasets/pose_deterministic.py b/deeplabcut/pose_estimation_tensorflow/datasets/pose_deterministic.py index 146a288333..e54e69f8d3 100644 --- a/deeplabcut/pose_estimation_tensorflow/datasets/pose_deterministic.py +++ b/deeplabcut/pose_estimation_tensorflow/datasets/pose_deterministic.py @@ -34,9 +34,7 @@ def __init__(self, cfg): self.data = self.load_dataset() self.num_images = len(self.data) if self.cfg["mirror"]: - self.symmetric_joints = mirror_joints_map( - cfg["all_joints"], cfg["num_joints"] - ) + self.symmetric_joints = mirror_joints_map(cfg["all_joints"], cfg["num_joints"]) self.curr_img = 0 self.scale = cfg["global_scale"] self.locref_scale = 1.0 / cfg["locref_stdev"] @@ -156,10 +154,7 @@ def is_valid_size(self, image_size, scale): if "min_input_size" in self.cfg and "max_input_size" in self.cfg: input_width = image_size[2] * scale input_height = image_size[1] * scale - if ( - input_height < self.cfg["min_input_size"] - or input_width < self.cfg["min_input_size"] - ): + if input_height < self.cfg["min_input_size"] or input_width < self.cfg["min_input_size"]: return False if input_height * input_width > self.cfg["max_input_size"] ** 2: return False @@ -178,9 +173,7 @@ def make_batch(self, data_item, scale, mirror): if self.cfg["crop"]: # adapted cropping for DLC if np.random.rand() < self.cfg["cropratio"]: j = np.random.randint(np.shape(joints)[1]) - joints, image = crop_image( - joints, image, joints[0, j, 1], joints[0, j, 2], self.cfg - ) + joints, image = crop_image(joints, image, joints[0, j, 1], joints[0, j, 2], self.cfg) img = imresize(image, scale) if scale != 1 else image scaled_img_size = np.array(img.shape[0:2]) @@ -194,10 +187,7 @@ def make_batch(self, data_item, scale, mirror): stride = self.cfg["stride"] if mirror: joints = [ - self.mirror_joints( - person_joints, self.symmetric_joints, image.shape[1] - ) - for person_joints in joints + self.mirror_joints(person_joints, self.symmetric_joints, image.shape[1]) for person_joints in joints ] sm_size = np.ceil(scaled_img_size / (stride * 2)).astype(int) * 2 scaled_joints = [person_joints[:, 1:3] * scale for person_joints in joints] @@ -207,9 +197,7 @@ def make_batch(self, data_item, scale, mirror): part_score_weights, locref_targets, locref_mask, - ) = self.compute_target_part_scoremap( - joint_id, scaled_joints, data_item, sm_size, scale - ) + ) = self.compute_target_part_scoremap(joint_id, scaled_joints, data_item, sm_size, scale) batch.update( { diff --git a/deeplabcut/pose_estimation_tensorflow/datasets/pose_imgaug.py b/deeplabcut/pose_estimation_tensorflow/datasets/pose_imgaug.py index 081d3c7a83..570680949d 100644 --- a/deeplabcut/pose_estimation_tensorflow/datasets/pose_imgaug.py +++ b/deeplabcut/pose_estimation_tensorflow/datasets/pose_imgaug.py @@ -70,9 +70,7 @@ def __init__(self, cfg): cfg["motion_blur"] = cfg.get("motion_blur", True) if cfg["motion_blur"]: - cfg["motion_blur_params"] = dict( - cfg.get("motion_blur_params", {"k": 7, "angle": (-90, 90)}) - ) + cfg["motion_blur_params"] = dict(cfg.get("motion_blur_params", {"k": 7, "angle": (-90, 90)})) print("Batch Size is %d" % self.batch_size) @@ -181,9 +179,7 @@ def build_augmentation_pipeline(self, height=None, width=None, apply_prob=0.5): pipeline.add(sometimes(iaa.MotionBlur(**opts))) if cfg["covering"]: - pipeline.add( - sometimes(iaa.CoarseDropout(0.02, size_percent=0.3, per_channel=0.5)) - ) + pipeline.add(sometimes(iaa.CoarseDropout(0.02, size_percent=0.3, per_channel=0.5))) if cfg["elastic_transform"]: pipeline.add(sometimes(iaa.ElasticTransformation(sigma=5))) @@ -191,21 +187,9 @@ def build_augmentation_pipeline(self, height=None, width=None, apply_prob=0.5): if cfg.get("gaussian_noise", False): opt = cfg.get("gaussian_noise", False) if type(opt) == int or type(opt) == float: - pipeline.add( - sometimes( - iaa.AdditiveGaussianNoise( - loc=0, scale=(0.0, opt), per_channel=0.5 - ) - ) - ) + pipeline.add(sometimes(iaa.AdditiveGaussianNoise(loc=0, scale=(0.0, opt), per_channel=0.5))) else: - pipeline.add( - sometimes( - iaa.AdditiveGaussianNoise( - loc=0, scale=(0.0, 0.05 * 255), per_channel=0.5 - ) - ) - ) + pipeline.add(sometimes(iaa.AdditiveGaussianNoise(loc=0, scale=(0.0, 0.05 * 255), per_channel=0.5))) if cfg.get("grayscale", False): pipeline.add(sometimes(iaa.Grayscale(alpha=(0.5, 1.0)))) @@ -235,17 +219,11 @@ def get_aug_param(cfg_value): if cfg_cnt["histeq"]: opt = get_aug_param(cfg_cnt["histeq"]) - pipeline.add( - iaa.Sometimes( - cfg_cnt["histeqratio"], iaa.AllChannelsHistogramEqualization(**opt) - ) - ) + pipeline.add(iaa.Sometimes(cfg_cnt["histeqratio"], iaa.AllChannelsHistogramEqualization(**opt))) if cfg_cnt["clahe"]: opt = get_aug_param(cfg_cnt["clahe"]) - pipeline.add( - iaa.Sometimes(cfg_cnt["claheratio"], iaa.AllChannelsCLAHE(**opt)) - ) + pipeline.add(iaa.Sometimes(cfg_cnt["claheratio"], iaa.AllChannelsCLAHE(**opt))) if cfg_cnt["log"]: opt = get_aug_param(cfg_cnt["log"]) @@ -253,15 +231,11 @@ def get_aug_param(cfg_value): if cfg_cnt["linear"]: opt = get_aug_param(cfg_cnt["linear"]) - pipeline.add( - iaa.Sometimes(cfg_cnt["linearratio"], iaa.LinearContrast(**opt)) - ) + pipeline.add(iaa.Sometimes(cfg_cnt["linearratio"], iaa.LinearContrast(**opt))) if cfg_cnt["sigmoid"]: opt = get_aug_param(cfg_cnt["sigmoid"]) - pipeline.add( - iaa.Sometimes(cfg_cnt["sigmoidratio"], iaa.SigmoidContrast(**opt)) - ) + pipeline.add(iaa.Sometimes(cfg_cnt["sigmoidratio"], iaa.SigmoidContrast(**opt))) if cfg_cnt["gamma"]: opt = get_aug_param(cfg_cnt["gamma"]) @@ -332,9 +306,7 @@ def get_batch(self): im_file = data_item.im_path logging.debug("image %s", im_file) - image = imread( - os.path.join(self.cfg["project_path"], im_file), mode="skimage" - ) + image = imread(os.path.join(self.cfg["project_path"], im_file), mode="skimage") if self.has_gt: joints = data_item.joints @@ -370,18 +342,14 @@ def get_scmap_update(self, joint_ids, joints, data_items, sm_size, target_size): part_score_weight, locref_target, locref_mask, - ) = self.gaussian_scmap( - joint_ids[i], [joints[i]], data_items[i], sm_size, scale - ) + ) = self.gaussian_scmap(joint_ids[i], [joints[i]], data_items[i], sm_size, scale) else: ( part_score_target, part_score_weight, locref_target, locref_mask, - ) = self.compute_target_part_scoremap_numpy( - joint_ids[i], [joints[i]], data_items[i], sm_size, scale - ) + ) = self.compute_target_part_scoremap_numpy(joint_ids[i], [joints[i]], data_items[i], sm_size, scale) part_score_targets.append(part_score_target) part_score_weights.append(part_score_weight) locref_targets.append(locref_target) @@ -407,13 +375,12 @@ def next_batch(self): ) = self.get_batch() pipeline = self.build_augmentation_pipeline( - height=target_size[0], width=target_size[1], + height=target_size[0], + width=target_size[1], apply_prob=cfg.get("apply_prob", 0.5), ) - batch_images, batch_joints = pipeline( - images=batch_images, keypoints=batch_joints - ) + batch_images, batch_joints = pipeline(images=batch_images, keypoints=batch_joints) image_shape = np.array(batch_images).shape[1:3] @@ -526,9 +493,7 @@ def compute_scmap_weights(self, scmap_shape, joint_id, data_item): weights = np.ones(scmap_shape) return weights - def compute_target_part_scoremap_numpy( - self, joint_id, coords, data_item, size, scale - ): + def compute_target_part_scoremap_numpy(self, joint_id, coords, data_item, size, scale): dist_thresh = float(self.cfg["pos_dist_thresh"] * scale) dist_thresh_sq = dist_thresh**2 num_joints = self.cfg["num_joints"] diff --git a/deeplabcut/pose_estimation_tensorflow/datasets/pose_multianimal_imgaug.py b/deeplabcut/pose_estimation_tensorflow/datasets/pose_multianimal_imgaug.py index 9c0697bb80..679ee02ee3 100644 --- a/deeplabcut/pose_estimation_tensorflow/datasets/pose_multianimal_imgaug.py +++ b/deeplabcut/pose_estimation_tensorflow/datasets/pose_multianimal_imgaug.py @@ -41,12 +41,8 @@ def __init__(self, cfg): self._n_animals = 1 else: - self.main_cfg = auxiliaryfunctions.read_config( - os.path.join(self.cfg["project_path"], "config.yaml") - ) - animals, unique, multi = auxfun_multianimal.extractindividualsandbodyparts( - self.main_cfg - ) + self.main_cfg = auxiliaryfunctions.read_config(os.path.join(self.cfg["project_path"], "config.yaml")) + animals, unique, multi = auxfun_multianimal.extractindividualsandbodyparts(self.main_cfg) self._n_kpts = len(multi) + len(unique) self._n_animals = len(animals) @@ -112,14 +108,7 @@ def load_dataset(self): item.im_size = sample["size"] if "joints" in sample.keys(): Joints = sample["joints"] - if ( - np.size( - np.concatenate( - [Joints[person_id][:, 1:3] for person_id in Joints.keys()] - ) - ) - > 0 - ): + if np.size(np.concatenate([Joints[person_id][:, 1:3] for person_id in Joints.keys()])) > 0: item.joints = Joints else: has_gt = False # no animal has joints! @@ -131,9 +120,7 @@ def load_dataset(self): self.has_gt = has_gt return data - def _load_pseudo_data_from_h5( - self, cfg, threshold=0.5, mask_kpts_below_thresh=False - ): + def _load_pseudo_data_from_h5(self, cfg, threshold=0.5, mask_kpts_below_thresh=False): gt_file = cfg["pseudo_label"] assert os.path.exists(gt_file) path_ = Path(gt_file) @@ -157,9 +144,7 @@ def _load_pseudo_data_from_h5( if self.vid: item.im_size = self.video_image_size else: - item.im_size = read_image_shape_fast( - os.path.join(video_root, frame_name) - ) + item.im_size = read_image_shape_fast(os.path.join(video_root, frame_name)) item.joints = {} @@ -251,29 +236,15 @@ def build_augmentation_pipeline(self, apply_prob=0.5): else: pipeline.add(sometimes(iaa.MotionBlur(k=7, angle=(-90, 90)))) if cfg.get("covering", False): - pipeline.add( - sometimes(iaa.CoarseDropout((0, 0.02), size_percent=(0.01, 0.05))) - ) # , per_channel=0.5))) + pipeline.add(sometimes(iaa.CoarseDropout((0, 0.02), size_percent=(0.01, 0.05)))) # , per_channel=0.5))) if cfg.get("elastic_transform", False): pipeline.add(sometimes(iaa.ElasticTransformation(sigma=5))) if cfg.get("gaussian_noise", False): opt = cfg.get("gaussian_noise", False) if type(opt) == int or type(opt) == float: - pipeline.add( - sometimes( - iaa.AdditiveGaussianNoise( - loc=0, scale=(0.0, opt), per_channel=0.5 - ) - ) - ) + pipeline.add(sometimes(iaa.AdditiveGaussianNoise(loc=0, scale=(0.0, opt), per_channel=0.5))) else: - pipeline.add( - sometimes( - iaa.AdditiveGaussianNoise( - loc=0, scale=(0.0, 0.05 * 255), per_channel=0.5 - ) - ) - ) + pipeline.add(sometimes(iaa.AdditiveGaussianNoise(loc=0, scale=(0.0, 0.05 * 255), per_channel=0.5))) if cfg.get("grayscale", False): pipeline.add(sometimes(iaa.Grayscale(alpha=(0.5, 1.0)))) @@ -303,17 +274,11 @@ def get_aug_param(cfg_value): if cfg_cnt["histeq"]: opt = get_aug_param(cfg_cnt["histeq"]) - pipeline.add( - iaa.Sometimes( - cfg_cnt["histeqratio"], iaa.AllChannelsHistogramEqualization(**opt) - ) - ) + pipeline.add(iaa.Sometimes(cfg_cnt["histeqratio"], iaa.AllChannelsHistogramEqualization(**opt))) if cfg_cnt["clahe"]: opt = get_aug_param(cfg_cnt["clahe"]) - pipeline.add( - iaa.Sometimes(cfg_cnt["claheratio"], iaa.AllChannelsCLAHE(**opt)) - ) + pipeline.add(iaa.Sometimes(cfg_cnt["claheratio"], iaa.AllChannelsCLAHE(**opt))) if cfg_cnt["log"]: opt = get_aug_param(cfg_cnt["log"]) @@ -321,15 +286,11 @@ def get_aug_param(cfg_value): if cfg_cnt["linear"]: opt = get_aug_param(cfg_cnt["linear"]) - pipeline.add( - iaa.Sometimes(cfg_cnt["linearratio"], iaa.LinearContrast(**opt)) - ) + pipeline.add(iaa.Sometimes(cfg_cnt["linearratio"], iaa.LinearContrast(**opt))) if cfg_cnt["sigmoid"]: opt = get_aug_param(cfg_cnt["sigmoid"]) - pipeline.add( - iaa.Sometimes(cfg_cnt["sigmoidratio"], iaa.SigmoidContrast(**opt)) - ) + pipeline.add(iaa.Sometimes(cfg_cnt["sigmoidratio"], iaa.SigmoidContrast(**opt))) if cfg_cnt["gamma"]: opt = get_aug_param(cfg_cnt["gamma"]) @@ -359,9 +320,7 @@ def get_batch_from_video(self): if trim_ends is None: trim_ends = 0 # because of the existence of threshold, sampling population is adjusted to len(self.data) - img_idx = np.random.choice( - len(self.data) - trim_ends * 2, size=self.batch_size, replace=True - ) + img_idx = np.random.choice(len(self.data) - trim_ends * 2, size=self.batch_size, replace=True) for i in range(self.batch_size): index = img_idx[i] offset = trim_ends @@ -383,9 +342,7 @@ def get_batch_from_video(self): for n, x, y in joints.get(j, []): kpts[j * self._n_kpts + int(n)] = x, y - joint_id = [ - np.array(list(range(self._n_kpts))) for _ in range(self._n_animals) - ] + joint_id = [np.array(list(range(self._n_kpts))) for _ in range(self._n_animals)] joint_ids.append(joint_id) batch_joints.append(kpts) @@ -406,18 +363,14 @@ def get_batch(self): im_file = data_item.im_path logging.debug("image %s", im_file) - image = imread( - os.path.join(self.cfg["project_path"], im_file), mode="skimage" - ) + image = imread(os.path.join(self.cfg["project_path"], im_file), mode="skimage") if self.has_gt: joints = data_item.joints kpts = np.full((self._n_kpts * self._n_animals, 2), np.nan) for j in range(self._n_animals): for n, x, y in joints.get(j, []): kpts[j * self._n_kpts + int(n)] = x, y - joint_id = [ - np.array(list(range(self._n_kpts))) for _ in range(self._n_animals) - ] + joint_id = [np.array(list(range(self._n_kpts))) for _ in range(self._n_animals)] joint_ids.append(joint_id) batch_joints.append(kpts) @@ -447,9 +400,7 @@ def get_targetmaps_update( part_score_weight, locref_target, locref_mask, - ) = self.gaussian_scmap( - joint_ids[i], [joints[i]], data_items[i], sm_size, scale - ) + ) = self.gaussian_scmap(joint_ids[i], [joints[i]], data_items[i], sm_size, scale) else: ( part_score_target, @@ -458,9 +409,7 @@ def get_targetmaps_update( locref_mask, partaffinityfield_target, partaffinityfield_mask, - ) = self.compute_target_part_scoremap_numpy( - joint_ids[i], joints[i], data_items[i], sm_size, scale - ) + ) = self.compute_target_part_scoremap_numpy(joint_ids[i], joints[i], data_items[i], sm_size, scale) part_score_targets.append(part_score_target) part_score_weights.append(part_score_weight) @@ -484,9 +433,9 @@ def calc_target_and_scoremap_sizes(self): if not self.is_valid_size(target_size): target_size = self.default_size stride = self.cfg["stride"] - sm_size = np.ceil(target_size / (stride * self.cfg.get("smfactor", 2))).astype( - int - ) * self.cfg.get("smfactor", 2) + sm_size = np.ceil(target_size / (stride * self.cfg.get("smfactor", 2))).astype(int) * self.cfg.get( + "smfactor", 2 + ) if stride == 2: sm_size = np.ceil(target_size / 16).astype(int) sm_size *= 8 @@ -512,9 +461,7 @@ def next_batch(self, plotting=False): target_size, sm_size = self.calc_target_and_scoremap_sizes() scale = np.mean(target_size / self.default_size) augmentation.update_crop_size(self.pipeline, *target_size) - batch_images, batch_joints = self.pipeline( - images=batch_images, keypoints=batch_joints - ) + batch_images, batch_joints = self.pipeline(images=batch_images, keypoints=batch_joints) batch_images = np.asarray(batch_images) image_shape = batch_images.shape[1:3] # Discard keypoints whose coordinates lie outside the cropped image @@ -554,9 +501,7 @@ def next_batch(self, plotting=False): shape=batch_images[i].shape, ) im = kps.draw_on_image(batch_images[i]) - imageio.imwrite( - os.path.join(self.cfg["project_path"], str(i) + ".png"), im - ) + imageio.imwrite(os.path.join(self.cfg["project_path"], str(i) + ".png"), im) batch = {Batch.inputs: batch_images.astype(np.float64)} if self.has_gt: @@ -597,17 +542,13 @@ def compute_scmap_weights(self, scmap_shape, joint_id): cfg = self.cfg if cfg["weigh_only_present_joints"]: weights = np.zeros(scmap_shape) - for k, j_id in enumerate( - np.concatenate(joint_id) - ): # looping over all animals + for k, j_id in enumerate(np.concatenate(joint_id)): # looping over all animals weights[:, :, j_id] = 1.0 else: weights = np.ones(scmap_shape) return weights - def compute_target_part_scoremap_numpy( - self, joint_id, coords, data_item, size, scale - ): + def compute_target_part_scoremap_numpy(self, joint_id, coords, data_item, size, scale): stride = self.cfg["stride"] half_stride = stride // 2 dist_thresh = float(self.cfg["pos_dist_thresh"] * scale) @@ -638,9 +579,7 @@ def compute_target_part_scoremap_numpy( # Produce score maps and location refinement fields coords_sm = np.round((coords - half_stride) / stride).astype(int) mins = np.round(np.maximum(coords_sm - dist_thresh - 1, 0)).astype(int) - maxs = np.round( - np.minimum(coords_sm + dist_thresh + 1, [width - 1, height - 1]) - ).astype(int) + maxs = np.round(np.minimum(coords_sm + dist_thresh + 1, [width - 1, height - 1])).astype(int) dx = coords[:, 0] - xx * stride - half_stride dx_ = dx * locref_scale dy = coords[:, 1] - yy * stride - half_stride @@ -661,11 +600,7 @@ def compute_target_part_scoremap_numpy( if num_idchannel > 0: coordinateoffset = 0 # Find indices of individuals in joint_id - idx = [ - (i, id_) - for i, id_ in enumerate(data_item.joints) - if id_ < num_idchannel - ] + idx = [(i, id_) for i, id_ in enumerate(data_item.joints) if id_ < num_idchannel] for i, person_id in idx: joint_ids = joint_id[i] n_joints = joint_ids.size @@ -705,24 +640,15 @@ def compute_target_part_scoremap_numpy( d2mid = j_y * Dx - j_x * Dy # orthogonal direction distance_along = Dx * x + Dy * y - distance_across = ( - ((y * Dx - x * Dy) - d2mid) - * 1.0 - / self.cfg["pafwidth"] - * scale - ) + distance_across = ((y * Dx - x * Dy) - d2mid) * 1.0 / self.cfg["pafwidth"] * scale - mask1 = (distance_along >= d1lowerboundary) & ( - distance_along <= d1upperboundary - ) + mask1 = (distance_along >= d1lowerboundary) & (distance_along <= d1upperboundary) distance_across_abs = np.abs(distance_across) mask2 = distance_across_abs <= 1 mask = mask1 & mask2 temp = 1 - distance_across_abs[mask] if self.cfg["weigh_only_present_joints"]: - partaffinityfield_mask[mask, [l * 2 + 0, l * 2 + 1]] = ( - 1.0 - ) + partaffinityfield_mask[mask, [l * 2 + 0, l * 2 + 1]] = 1.0 partaffinityfield_map[mask, l * 2 + 0] = Dx * temp partaffinityfield_map[mask, l * 2 + 1] = Dy * temp @@ -754,9 +680,7 @@ def gaussian_scmap(self, joint_id, coords, data_item, size, scale): locref_scale = 1.0 / self.cfg["locref_stdev"] dist_thresh_sq = dist_thresh**2 - partaffinityfield_shape = np.concatenate( - [size, np.array([self.cfg["num_limbs"] * 2])] - ) + partaffinityfield_shape = np.concatenate([size, np.array([self.cfg["num_limbs"] * 2])]) partaffinityfield_map = np.zeros(partaffinityfield_shape) if self.cfg["weigh_only_present_joints"]: partaffinityfield_mask = np.zeros(partaffinityfield_shape) @@ -829,24 +753,14 @@ def gaussian_scmap(self, joint_id, coords, data_item, size, scale): d1upperboundary = max(d1) d2mid = j_y * Dx - j_x * Dy # orthogonal direction - distance_along = Dx * (x * stride + half_stride) + Dy * ( - y * stride + half_stride - ) + distance_along = Dx * (x * stride + half_stride) + Dy * (y * stride + half_stride) distance_across = ( - ( - ( - (y * stride + half_stride) * Dx - - (x * stride + half_stride) * Dy - ) - - d2mid - ) + (((y * stride + half_stride) * Dx - (x * stride + half_stride) * Dy) - d2mid) * 1.0 / self.cfg["pafwidth"] * scale ) - mask1 = (distance_along >= d1lowerboundary) & ( - distance_along <= d1upperboundary - ) + mask1 = (distance_along >= d1lowerboundary) & (distance_along <= d1upperboundary) mask2 = np.abs(distance_across) <= 1 # mask3 = ((x >= 0) & (x <= width-1)) # mask4 = ((y >= 0) & (y <= height-1)) @@ -855,12 +769,8 @@ def gaussian_scmap(self, joint_id, coords, data_item, size, scale): partaffinityfield_mask[mask, l * 2 + 0] = 1.0 partaffinityfield_mask[mask, l * 2 + 1] = 1.0 - partaffinityfield_map[mask, l * 2 + 0] = ( - Dx * (1 - abs(distance_across)) - )[mask] - partaffinityfield_map[mask, l * 2 + 1] = ( - Dy * (1 - abs(distance_across)) - )[mask] + partaffinityfield_map[mask, l * 2 + 0] = (Dx * (1 - abs(distance_across)))[mask] + partaffinityfield_map[mask, l * 2 + 1] = (Dy * (1 - abs(distance_across)))[mask] coordinateoffset += len(joint_ids) # keeping track of the blocks diff --git a/deeplabcut/pose_estimation_tensorflow/datasets/pose_tensorpack.py b/deeplabcut/pose_estimation_tensorflow/datasets/pose_tensorpack.py index d25e98a6a4..41633abc55 100644 --- a/deeplabcut/pose_estimation_tensorflow/datasets/pose_tensorpack.py +++ b/deeplabcut/pose_estimation_tensorflow/datasets/pose_tensorpack.py @@ -19,7 +19,6 @@ https://github.com/tensorpack/tensorpack """ - import multiprocessing import os @@ -211,9 +210,7 @@ def __init__(self, cfg): # Randomly applies gaussian blur to an image with a random window size # within the range [0, 2 * blur_max_window_size + 1] to augment training data cfg["blur_max_window_size"] = cfg.get("blur_max_window_size", 10) - cfg["blurratio"] = cfg.get( - "blurratio", 0.2 - ) # what is the fraction of training samples with blur augmentation? + cfg["blurratio"] = cfg.get("blurratio", 0.2) # what is the fraction of training samples with blur augmentation? # Whether image is RGB or RBG. If None, contrast augmentation uses the mean per-channel. cfg["is_rgb"] = cfg.get("is_rgb", True) @@ -261,9 +258,7 @@ def __init__(self, cfg): rgb=self.cfg["is_rgb"], clip=self.cfg["to_clip"], ) - self.saturation = Saturation( - self.cfg["saturation_max_dif"], rgb=self.cfg["is_rgb"] - ) + self.saturation = Saturation(self.cfg["saturation_max_dif"], rgb=self.cfg["is_rgb"]) self.gaussian_noise = GaussianNoise(sigma=self.cfg["noise_sigma"]) self.gaussian_blur = GaussianBlur(max_size=self.cfg["blur_max_window_size"]) self.augmentors = [ @@ -305,9 +300,7 @@ def augment(self, data): aug_img = img aug_coords = coords size = [aug_img.shape[0], aug_img.shape[1]] - aug_coords = [ - aug_coords.reshape(int(len(aug_coords[~np.isnan(aug_coords)]) / 2), 2) - ] + aug_coords = [aug_coords.reshape(int(len(aug_coords[~np.isnan(aug_coords)]) / 2), 2)] joint_id = data.joint_id return [joint_id, aug_img, aug_coords, data, size, scale] @@ -322,13 +315,9 @@ def get_dataflow(self, cfg): if num_processes <= 1: num_processes = 2 # recommended to use more than one process for training if os.name == "nt": - df2 = MultiProcessRunner( - df, num_proc=num_processes, num_prefetch=self.cfg["num_prefetch"] - ) + df2 = MultiProcessRunner(df, num_proc=num_processes, num_prefetch=self.cfg["num_prefetch"]) else: - df2 = MultiProcessRunnerZMQ( - df, num_proc=num_processes, hwm=self.cfg["num_prefetch"] - ) + df2 = MultiProcessRunnerZMQ(df, num_proc=num_processes, hwm=self.cfg["num_prefetch"]) return df2 def compute_target_part_scoremap(self, components): @@ -418,10 +407,7 @@ def is_valid_size(self, image_size, scale): if "min_input_size" in self.cfg and "max_input_size" in self.cfg: input_width = image_size[2] * scale input_height = image_size[1] * scale - if ( - input_height < self.cfg["min_input_size"] - or input_width < self.cfg["min_input_size"] - ): + if input_height < self.cfg["min_input_size"] or input_width < self.cfg["min_input_size"]: return False if input_height * input_width > self.cfg["max_input_size"] ** 2: return False diff --git a/deeplabcut/pose_estimation_tensorflow/export.py b/deeplabcut/pose_estimation_tensorflow/export.py index 50f9b6acbf..6e6709a206 100644 --- a/deeplabcut/pose_estimation_tensorflow/export.py +++ b/deeplabcut/pose_estimation_tensorflow/export.py @@ -114,11 +114,7 @@ def load_model(cfg, shuffle=1, trainingsetindex=0, TFGPUinference=True, modelpre train_fraction = cfg["TrainingFraction"][trainingsetindex] model_folder = os.path.join( cfg["project_path"], - str( - auxiliaryfunctions.get_model_folder( - train_fraction, shuffle, cfg, modelprefix=modelprefix - ) - ), + str(auxiliaryfunctions.get_model_folder(train_fraction, shuffle, cfg, modelprefix=modelprefix)), ) path_test_config = os.path.normpath(model_folder + "/test/pose_cfg.yaml") path_train_config = os.path.normpath(model_folder + "/train/pose_cfg.yaml") @@ -128,8 +124,7 @@ def load_model(cfg, shuffle=1, trainingsetindex=0, TFGPUinference=True, modelpre # dlc_cfg_train = load_config(str(path_train_config)) except FileNotFoundError: raise FileNotFoundError( - "It seems the model for shuffle %s and trainFraction %s does not exist." - % (shuffle, train_fraction) + "It seems the model for shuffle %s and trainFraction %s does not exist." % (shuffle, train_fraction) ) Snapshots = auxiliaryfunctions.get_snapshots_from_folder( @@ -137,9 +132,7 @@ def load_model(cfg, shuffle=1, trainingsetindex=0, TFGPUinference=True, modelpre ) if cfg["snapshotindex"] == "all": - print( - "Snapshotindex is set to 'all' in the config.yaml file. Changing snapshot index to -1!" - ) + print("Snapshotindex is set to 'all' in the config.yaml file. Changing snapshot index to -1!") snapshotindex = -1 else: snapshotindex = cfg["snapshotindex"] @@ -149,9 +142,7 @@ def load_model(cfg, shuffle=1, trainingsetindex=0, TFGPUinference=True, modelpre #################################### # Check if data already was generated: - dlc_cfg["init_weights"] = os.path.join( - model_folder, "train", Snapshots[snapshotindex] - ) + dlc_cfg["init_weights"] = os.path.join(model_folder, "train", Snapshots[snapshotindex]) trainingsiterations = (dlc_cfg["init_weights"].split(os.sep)[-1]).split("-")[-1] dlc_cfg["num_outputs"] = cfg.get("num_outputs", dlc_cfg.get("num_outputs", 1)) dlc_cfg["batch_size"] = None @@ -194,9 +185,7 @@ def tf_to_pb(sess, checkpoint, output, output_dir=None): If None, will export to the directory of the checkpoint file. """ - output_dir = ( - os.path.expanduser(output_dir) if output_dir else os.path.dirname(checkpoint) - ) + output_dir = os.path.expanduser(output_dir) if output_dir else os.path.dirname(checkpoint) ckpt_base = os.path.basename(checkpoint) # save graph to pbtxt file @@ -284,15 +273,11 @@ def export_model( cfg["project_path"] = os.path.dirname(os.path.realpath(cfg_path)) cfg["iteration"] = iteration if iteration is not None else cfg["iteration"] cfg["batch_size"] = cfg["batch_size"] if cfg["batch_size"] > 1 else 2 - cfg["snapshotindex"] = ( - snapshotindex if snapshotindex is not None else cfg["snapshotindex"] - ) + cfg["snapshotindex"] = snapshotindex if snapshotindex is not None else cfg["snapshotindex"] ### load model - sess, input, output, dlc_cfg = load_model( - cfg, shuffle, trainingsetindex, TFGPUinference, modelprefix - ) + sess, input, output, dlc_cfg = load_model(cfg, shuffle, trainingsetindex, TFGPUinference, modelprefix) ckpt = dlc_cfg["init_weights"] model_dir = os.path.dirname(ckpt) @@ -312,10 +297,7 @@ def export_model( if os.path.isdir(full_export_dir): if not overwrite: - raise FileExistsError( - "Export directory %s already exists. Terminating export..." - % full_export_dir - ) + raise FileExistsError("Export directory %s already exists. Terminating export..." % full_export_dir) else: os.mkdir(full_export_dir) @@ -340,10 +322,7 @@ def export_model( ### copy checkpoint to export directory ckpt_files = glob.glob(ckpt + "*") - ckpt_dest = [ - os.path.normpath(full_export_dir + "/" + os.path.basename(ckf)) - for ckf in ckpt_files - ] + ckpt_dest = [os.path.normpath(full_export_dir + "/" + os.path.basename(ckf)) for ckf in ckpt_files] for ckf, ckd in zip(ckpt_files, ckpt_dest): shutil.copy(ckf, ckd) diff --git a/deeplabcut/pose_estimation_tensorflow/lib/crossvalutils.py b/deeplabcut/pose_estimation_tensorflow/lib/crossvalutils.py index 50f31dddf6..773f1b61fe 100644 --- a/deeplabcut/pose_estimation_tensorflow/lib/crossvalutils.py +++ b/deeplabcut/pose_estimation_tensorflow/lib/crossvalutils.py @@ -9,4 +9,5 @@ # Licensed under GNU Lesser General Public License v3.0 # """Backwards compatibility""" + from deeplabcut.core.crossvalutils import * diff --git a/deeplabcut/pose_estimation_tensorflow/lib/inferenceutils.py b/deeplabcut/pose_estimation_tensorflow/lib/inferenceutils.py index 889311441c..7a75c61440 100644 --- a/deeplabcut/pose_estimation_tensorflow/lib/inferenceutils.py +++ b/deeplabcut/pose_estimation_tensorflow/lib/inferenceutils.py @@ -9,4 +9,5 @@ # Licensed under GNU Lesser General Public License v3.0 # """Backwards compatibility""" + from deeplabcut.core.inferenceutils import * diff --git a/deeplabcut/pose_estimation_tensorflow/lib/trackingutils.py b/deeplabcut/pose_estimation_tensorflow/lib/trackingutils.py index f769fa7238..5459f03236 100644 --- a/deeplabcut/pose_estimation_tensorflow/lib/trackingutils.py +++ b/deeplabcut/pose_estimation_tensorflow/lib/trackingutils.py @@ -9,4 +9,5 @@ # Licensed under GNU Lesser General Public License v3.0 # """Backwards compatibility""" + from deeplabcut.core.trackingutils import * diff --git a/deeplabcut/pose_estimation_tensorflow/modelzoo/__init__.py b/deeplabcut/pose_estimation_tensorflow/modelzoo/__init__.py index c1e9461785..70c734c462 100644 --- a/deeplabcut/pose_estimation_tensorflow/modelzoo/__init__.py +++ b/deeplabcut/pose_estimation_tensorflow/modelzoo/__init__.py @@ -8,4 +8,4 @@ # # Licensed under GNU Lesser General Public License v3.0 # -from .api import SpatiotemporalAdaptation \ No newline at end of file +from .api import SpatiotemporalAdaptation diff --git a/deeplabcut/pose_estimation_tensorflow/modelzoo/api/spatiotemporal_adapt.py b/deeplabcut/pose_estimation_tensorflow/modelzoo/api/spatiotemporal_adapt.py index dce93b01ed..b37e72bd3f 100644 --- a/deeplabcut/pose_estimation_tensorflow/modelzoo/api/spatiotemporal_adapt.py +++ b/deeplabcut/pose_estimation_tensorflow/modelzoo/api/spatiotemporal_adapt.py @@ -99,16 +99,10 @@ def __init__( dlc_root_path = get_deeplabcut_path() project_config = read_config( - os.path.join( - dlc_root_path, "modelzoo", "project_configs", f"{project_name}.yaml" - ) + os.path.join(dlc_root_path, "modelzoo", "project_configs", f"{project_name}.yaml") ) - model_config = read_config( - os.path.join( - dlc_root_path, "modelzoo", "model_configs", f"{model_name}.yaml" - ) - ) + model_config = read_config(os.path.join(dlc_root_path, "modelzoo", "model_configs", f"{model_name}.yaml")) joints = [i for i in range(len(project_config["bodyparts"]))] num_joints = len(joints) @@ -190,15 +184,12 @@ def adaptation_training(self, displayiters=500, saveiters=1000, **kwargs): vname = str(Path(self.video_path).stem) video_root = Path(self.video_path).parent - _, pseudo_label_path, _, _ = load_analyzed_data( - video_root, vname, DLCscorer, False, "" - ) + _, pseudo_label_path, _, _ = load_analyzed_data(video_root, vname, DLCscorer, False, "") if self.modelfolder != "": os.makedirs(self.modelfolder, exist_ok=True) self.adapt_iterations = kwargs.get("adapt_iterations", self.adapt_iterations) - self.train_without_project( pseudo_label_path, displayiters=displayiters, @@ -207,9 +198,7 @@ def adaptation_training(self, displayiters=500, saveiters=1000, **kwargs): ) def after_adapt_inference(self, create_labeled_video, **kwargs): - pattern = os.path.join( - self.modelfolder, f"snapshot-{self.adapt_iterations}.index" - ) + pattern = os.path.join(self.modelfolder, f"snapshot-{self.adapt_iterations}.index") ref_proj_config_path = "" files = glob.glob(pattern) diff --git a/deeplabcut/pose_estimation_tensorflow/modelzoo/api/superanimal_inference.py b/deeplabcut/pose_estimation_tensorflow/modelzoo/api/superanimal_inference.py index fd83ad187d..29b0946a66 100644 --- a/deeplabcut/pose_estimation_tensorflow/modelzoo/api/superanimal_inference.py +++ b/deeplabcut/pose_estimation_tensorflow/modelzoo/api/superanimal_inference.py @@ -152,10 +152,7 @@ def _video_inference( if multi_scale_batched_frames is None: multi_scale_batched_frames = [ - np.empty( - (batchsize, frame.shape[0], frame.shape[1], 3), dtype="ubyte" - ) - for frame in frames + np.empty((batchsize, frame.shape[0], frame.shape[1], 3), dtype="ubyte") for frame in frames ] for scale_id, frame in enumerate(frames): @@ -165,9 +162,7 @@ def _video_inference( preds = [] for scale_id, batched_frames in enumerate(multi_scale_batched_frames): # batch full, start true inferencing - D = predict.predict_batched_peaks_and_costs( - test_cfg, batched_frames, sess, inputs, outputs - ) + D = predict.predict_batched_peaks_and_costs(test_cfg, batched_frames, sess, inputs, outputs) preds.append(D) # only do this when animal is detected ind_start = inds[0] @@ -181,9 +176,7 @@ def _video_inference( else: pred = preds[scale_id][i] if pred != []: - pred = _project_pred_to_original_size( - pred, old_shape, frame_shapes[scale_id] - ) + pred = _project_pred_to_original_size(pred, old_shape, frame_shapes[scale_id]) PredicteData["frame" + str(ind).zfill(strwidth)].append(pred) @@ -218,9 +211,7 @@ def _video_inference( else: pred = preds[scale_id][i] if pred != []: - pred = _project_pred_to_original_size( - pred, old_shape, frame_shapes[scale_id] - ) + pred = _project_pred_to_original_size(pred, old_shape, frame_shapes[scale_id]) PredicteData["frame" + str(ind).zfill(strwidth)].append(pred) break @@ -240,13 +231,9 @@ def _video_inference( "minimal confidence": test_cfg.get("minconfidence", None), "sigma": test_cfg.get("sigma", 1), "PAFgraph": test_cfg.get("partaffinityfield_graph", None), - "PAFinds": test_cfg.get( - "paf_best", np.arange(len(test_cfg["partaffinityfield_graph"])) - ), + "PAFinds": test_cfg.get("paf_best", np.arange(len(test_cfg["partaffinityfield_graph"]))), "all_joints": [[i] for i in range(len(test_cfg["all_joints"]))], - "all_joints_names": [ - test_cfg["all_joints_names"][i] for i in range(len(test_cfg["all_joints"])) - ], + "all_joints_names": [test_cfg["all_joints_names"][i] for i in range(len(test_cfg["all_joints"]))], "nframes": nframes, } @@ -296,12 +283,7 @@ def video_inference( test_cfg = customized_test_config # add a temp folder for checkpoint - weight_folder = str( - Path(dlc_root_path) - / "modelzoo" - / "checkpoints" - / f"{project_name}_{model_name}" - ) + weight_folder = str(Path(dlc_root_path) / "modelzoo" / "checkpoints" / f"{project_name}_{model_name}") snapshots = glob.glob(os.path.join(weight_folder, "snapshot-*.index")) test_cfg["partaffinityfield_graph"] = [] test_cfg["partaffinityfield_predict"] = False @@ -310,9 +292,7 @@ def video_inference( test_cfg["init_weights"] = init_weights else: if len(snapshots) == 0: - raise FileNotFoundError( - f"Did not find any super animal snapshots in {weight_folder}" - ) + raise FileNotFoundError(f"Did not find any super animal snapshots in {weight_folder}") init_weights = os.path.abspath(snapshots[0]).replace(".index", "") test_cfg["init_weights"] = init_weights @@ -320,9 +300,7 @@ def video_inference( test_cfg["num_outputs"] = 1 test_cfg["batch_size"] = batchsize - sess, inputs, outputs = single_predict.setup_pose_prediction( - test_cfg, allow_growth=allow_growth - ) + sess, inputs, outputs = single_predict.setup_pose_prediction(test_cfg, allow_growth=allow_growth) DLCscorer = "DLC_" + Path(test_cfg["init_weights"]).stem videos = auxiliaryfunctions.get_list_of_videos(videos, videotype) diff --git a/deeplabcut/pose_estimation_tensorflow/nnets/base.py b/deeplabcut/pose_estimation_tensorflow/nnets/base.py index f396c98572..7208b0d36a 100644 --- a/deeplabcut/pose_estimation_tensorflow/nnets/base.py +++ b/deeplabcut/pose_estimation_tensorflow/nnets/base.py @@ -41,10 +41,7 @@ def add_part_loss(pred_layer): loss = {"part_loss": add_part_loss("part_pred")} total_loss = loss["part_loss"] - if ( - self.cfg["intermediate_supervision"] - and "efficientnet" not in self.cfg["net_type"] - ): + if self.cfg["intermediate_supervision"] and "efficientnet" not in self.cfg["net_type"]: loss["part_loss_interm"] = add_part_loss("part_pred_interm") total_loss += loss["part_loss_interm"] @@ -105,20 +102,14 @@ def prediction_layers( "locref_pred", n_joints * 2, ) - if ( - self.cfg["pairwise_predict"] - and "multi-animal" not in self.cfg["dataset_type"] - ): + if self.cfg["pairwise_predict"] and "multi-animal" not in self.cfg["dataset_type"]: out["pairwise_pred"] = prediction_layer( self.cfg, features, "pairwise_pred", n_joints * (n_joints - 1) * 2, ) - if ( - self.cfg["partaffinityfield_predict"] - and "multi-animal" in self.cfg["dataset_type"] - ): + if self.cfg["partaffinityfield_predict"] and "multi-animal" in self.cfg["dataset_type"]: out["pairwise_pred"] = prediction_layer( self.cfg, features, @@ -143,21 +134,13 @@ def inference(self, inputs): locref = tf.reshape(locref, (l_shape[0] * l_shape[1], -1, 2)) probs = tf.reshape(probs, (l_shape[0] * l_shape[1], -1)) maxloc = tf.argmax(input=probs, axis=0) - loc = tf.unravel_index( - maxloc, (tf.cast(l_shape[0], tf.int64), tf.cast(l_shape[1], tf.int64)) - ) + loc = tf.unravel_index(maxloc, (tf.cast(l_shape[0], tf.int64), tf.cast(l_shape[1], tf.int64))) maxloc = tf.reshape(maxloc, (1, -1)) - joints = tf.reshape( - tf.range(0, tf.cast(l_shape[2], dtype=tf.int64)), (1, -1) - ) + joints = tf.reshape(tf.range(0, tf.cast(l_shape[2], dtype=tf.int64)), (1, -1)) else: - l_shape = tf.shape( - input=probs - ) # batchsize times x times y times body parts - locref = tf.reshape( - locref, (l_shape[0], l_shape[1], l_shape[2], l_shape[3], 2) - ) + l_shape = tf.shape(input=probs) # batchsize times x times y times body parts + locref = tf.reshape(locref, (l_shape[0], l_shape[1], l_shape[2], l_shape[3], 2)) # turn into x times y time bs * bpts locref = tf.transpose(a=locref, perm=[1, 2, 0, 3, 4]) probs = tf.transpose(a=probs, perm=[1, 2, 0, 3]) @@ -171,9 +154,7 @@ def inference(self, inputs): maxloc, (tf.cast(l_shape[0], tf.int64), tf.cast(l_shape[1], tf.int64)) ) # tuple of max indices maxloc = tf.reshape(maxloc, (1, -1)) - joints = tf.reshape( - tf.range(0, tf.cast(l_shape[2] * l_shape[3], dtype=tf.int64)), (1, -1) - ) + joints = tf.reshape(tf.range(0, tf.cast(l_shape[2] * l_shape[3], dtype=tf.int64)), (1, -1)) # extract corresponding locref x and y as well as probability indices = tf.transpose(a=tf.concat([maxloc, joints], axis=0)) diff --git a/deeplabcut/pose_estimation_tensorflow/nnets/conv_blocks.py b/deeplabcut/pose_estimation_tensorflow/nnets/conv_blocks.py index 3f42784003..8dc6f3bcb9 100644 --- a/deeplabcut/pose_estimation_tensorflow/nnets/conv_blocks.py +++ b/deeplabcut/pose_estimation_tensorflow/nnets/conv_blocks.py @@ -14,6 +14,7 @@ # limitations under the License. # """Convolution blocks for mobilenet.""" + import contextlib import functools @@ -80,9 +81,10 @@ def _split_divisible(num, num_ways, divisible_by=8): @contextlib.contextmanager def _v1_compatible_scope_naming(scope): if scope is None: # Create uniqified separable blocks. - with tf.compat.v1.variable_scope( - None, default_name="separable" - ) as s, tf.compat.v1.name_scope(s.original_name_scope): + with ( + tf.compat.v1.variable_scope(None, default_name="separable") as s, + tf.compat.v1.name_scope(s.original_name_scope), + ): yield "" else: # We use scope_depthwise, scope_pointwise for compatibility with V1 ckpts. @@ -238,19 +240,16 @@ def expanded_conv( Raises: TypeError: on inval """ - with tf.compat.v1.variable_scope( - scope, default_name="expanded_conv" - ) as s, tf.compat.v1.name_scope(s.original_name_scope): + with ( + tf.compat.v1.variable_scope(scope, default_name="expanded_conv") as s, + tf.compat.v1.name_scope(s.original_name_scope), + ): prev_depth = input_tensor.get_shape().as_list()[3] if depthwise_location not in [None, "input", "output", "expansion"]: - raise TypeError( - "%r is unknown value for depthwise_location" % depthwise_location - ) + raise TypeError("%r is unknown value for depthwise_location" % depthwise_location) if use_explicit_padding: if padding != "SAME": - raise TypeError( - "`use_explicit_padding` should only be used with " '"SAME" padding.' - ) + raise TypeError('`use_explicit_padding` should only be used with "SAME" padding.') padding = "VALID" depthwise_func = functools.partial( slim.separable_conv2d, diff --git a/deeplabcut/pose_estimation_tensorflow/nnets/efficientnet.py b/deeplabcut/pose_estimation_tensorflow/nnets/efficientnet.py index 0c41b147c6..cbebf508fb 100644 --- a/deeplabcut/pose_estimation_tensorflow/nnets/efficientnet.py +++ b/deeplabcut/pose_estimation_tensorflow/nnets/efficientnet.py @@ -49,7 +49,5 @@ def get_net(self, inputs, use_batch_norm=False, use_drop_out=False): return self.prediction_layers(net) def test(self, inputs): - heads = self.get_net( - inputs, self.cfg["use_batch_norm"], self.cfg["use_drop_out"] - ) + heads = self.get_net(inputs, self.cfg["use_batch_norm"], self.cfg["use_drop_out"]) return self.add_inference_layers(heads) diff --git a/deeplabcut/pose_estimation_tensorflow/nnets/layers.py b/deeplabcut/pose_estimation_tensorflow/nnets/layers.py index 4bd530fc95..b0a7ff29d3 100644 --- a/deeplabcut/pose_estimation_tensorflow/nnets/layers.py +++ b/deeplabcut/pose_estimation_tensorflow/nnets/layers.py @@ -36,9 +36,7 @@ def prediction_layer(cfg, input, name, num_outputs): weights_regularizer=slim.l2_regularizer(cfg["weight_decay"]), ): with tf.compat.v1.variable_scope(name): - pred = slim.conv2d_transpose( - input, num_outputs, kernel_size=[3, 3], stride=2, scope="block4" - ) + pred = slim.conv2d_transpose(input, num_outputs, kernel_size=[3, 3], stride=2, scope="block4") return pred diff --git a/deeplabcut/pose_estimation_tensorflow/nnets/multi.py b/deeplabcut/pose_estimation_tensorflow/nnets/multi.py index f67504d091..096d575453 100644 --- a/deeplabcut/pose_estimation_tensorflow/nnets/multi.py +++ b/deeplabcut/pose_estimation_tensorflow/nnets/multi.py @@ -24,9 +24,7 @@ # Change the stride from 2 to 1 to get 16x downscaling instead of 32x. -mobilenet_v2.V2_DEF["spec"][14] = mobilenet.op( - conv_blocks.expanded_conv, stride=1, num_outputs=160 -) +mobilenet_v2.V2_DEF["spec"][14] = mobilenet.op(conv_blocks.expanded_conv, stride=1, num_outputs=160) net_funcs = { @@ -117,9 +115,7 @@ def extract_features(self, inputs): if "resnet" in net_type: net_fun = net_funcs[net_type] with slim.arg_scope(resnet_v1.resnet_arg_scope()): - net, end_points = net_fun( - im_centered, global_pool=False, output_stride=16, is_training=False - ) + net, end_points = net_fun(im_centered, global_pool=False, output_stride=16, is_training=False) elif "mobilenet" in net_type: net_fun = net_funcs[net_type] with slim.arg_scope(mobilenet_v2.training_scope()): @@ -153,15 +149,11 @@ def prediction_layers( if self.cfg["multi_stage"]: # MuNet! (multi_stage decoder + multi_fusion) # Defining multi_fusion backbone num_layers = re.findall("resnet_([0-9]*)", net_type)[0] - layer_name = ( - "resnet_v1_{}".format(num_layers) + "/block{}/unit_{}/bottleneck_v1" - ) + layer_name = "resnet_v1_{}".format(num_layers) + "/block{}/unit_{}/bottleneck_v1" mid_pt_block1 = layer_name.format(1, 3) mid_pt_block2 = layer_name.format(2, 3) - final_dims = tf.math.ceil( - tf.divide(input_shape[1:3], tf.convert_to_tensor(16)) - ) + final_dims = tf.math.ceil(tf.divide(input_shape[1:3], tf.convert_to_tensor(16))) interim_dims_s8 = tf.scalar_mul(2, final_dims) interim_dims_s8 = tf.cast(interim_dims_s8, tf.int32) @@ -269,30 +261,18 @@ def prediction_layers( ) if self.cfg["location_refinement"]: - out["locref"] = prediction_layer( - self.cfg, net, "locref_pred", self.cfg["num_joints"] * 2 - ) - if ( - self.cfg["pairwise_predict"] - and "multi-animal" not in self.cfg["dataset_type"] - ): + out["locref"] = prediction_layer(self.cfg, net, "locref_pred", self.cfg["num_joints"] * 2) + if self.cfg["pairwise_predict"] and "multi-animal" not in self.cfg["dataset_type"]: out["pairwise_pred"] = prediction_layer( self.cfg, net, "pairwise_pred", self.cfg["num_joints"] * (self.cfg["num_joints"] - 1) * 2, ) - if ( - self.cfg["partaffinityfield_predict"] - and "multi-animal" in self.cfg["dataset_type"] - ): - feature = slim.conv2d_transpose( - net, self.cfg.get("bank3", 128), kernel_size=[3, 3], stride=2 - ) + if self.cfg["partaffinityfield_predict"] and "multi-animal" in self.cfg["dataset_type"]: + feature = slim.conv2d_transpose(net, self.cfg.get("bank3", 128), kernel_size=[3, 3], stride=2) - stage1_paf_out = prediction_layer( - self.cfg, net, "pairwise_pred_s1", self.cfg["num_limbs"] * 2 - ) + stage1_paf_out = prediction_layer(self.cfg, net, "pairwise_pred_s1", self.cfg["num_limbs"] * 2) stage2_in = tf.concat([stage1_hm_out, stage1_paf_out, feature], 3) stage_input = stage2_in @@ -321,9 +301,7 @@ def prediction_layers( # stage_paf_output = stage_paf_output + pre_stage_paf_output stage_hm_output = stage_hm_output + pre_stage_hm_output - stage_input = tf.concat( - [stage_hm_output, stage_paf_output, feature], 3 - ) + stage_input = tf.concat([stage_hm_output, stage_paf_output, feature], 3) out["part_pred"] = prediction_layer_stage( self.cfg, @@ -340,9 +318,7 @@ def prediction_layers( ) if self.cfg["intermediate_supervision"]: - interm_name = layer_name.format( - 3, self.cfg["intermediate_supervision_layer"] - ) + interm_name = layer_name.format(3, self.cfg["intermediate_supervision_layer"]) block_interm_out = end_points[interm_name] out["part_pred_interm"] = prediction_layer( self.cfg, @@ -363,9 +339,7 @@ def prediction_layers( else: raise ValueError(f"Unknown network of type {net_type}") - final_dims = tf.math.ceil( - tf.divide(input_shape[1:3], tf.convert_to_tensor(value=16)) - ) + final_dims = tf.math.ceil(tf.divide(input_shape[1:3], tf.convert_to_tensor(value=16))) interim_dims = tf.scalar_mul(2, final_dims) interim_dims = tf.cast(interim_dims, tf.int32) bank_3 = end_points[mid_pt] @@ -375,9 +349,7 @@ def prediction_layers( [slim.conv2d], padding="SAME", normalizer_fn=None, - weights_regularizer=tf.keras.regularizers.l2( - 0.5 * (self.cfg["weight_decay"]) - ), + weights_regularizer=tf.keras.regularizers.l2(0.5 * (self.cfg["weight_decay"])), ): with tf.compat.v1.variable_scope("decoder_filters"): bank_3 = slim.conv2d( @@ -391,9 +363,7 @@ def prediction_layers( [slim.conv2d_transpose], padding="SAME", normalizer_fn=None, - weights_regularizer=tf.keras.regularizers.l2( - 0.5 * (self.cfg["weight_decay"]) - ), + weights_regularizer=tf.keras.regularizers.l2(0.5 * (self.cfg["weight_decay"])), ): with tf.compat.v1.variable_scope("upsampled_features"): upsampled_features = slim.conv2d_transpose( @@ -410,20 +380,13 @@ def prediction_layers( reuse, ) with tf.compat.v1.variable_scope(scope, reuse=reuse): - if ( - self.cfg["intermediate_supervision"] - and "efficientnet" not in net_type - ): + if self.cfg["intermediate_supervision"] and "efficientnet" not in net_type: if "mobilenet" in net_type: - feat = end_points[ - f"layer_{self.cfg['intermediate_supervision_layer']}" - ] + feat = end_points[f"layer_{self.cfg['intermediate_supervision_layer']}"] elif "resnet" in net_type: layer_name = "resnet_v1_{}/block{}/unit_{}/bottleneck_v1" num_layers = re.findall("resnet_([0-9]*)", net_type)[0] - interm_name = layer_name.format( - num_layers, 3, self.cfg["intermediate_supervision_layer"] - ) + interm_name = layer_name.format(num_layers, 3, self.cfg["intermediate_supervision_layer"]) feat = end_points[interm_name] else: return out diff --git a/deeplabcut/pose_estimation_tensorflow/nnets/resnet.py b/deeplabcut/pose_estimation_tensorflow/nnets/resnet.py index 68fab30ab8..1441799a18 100644 --- a/deeplabcut/pose_estimation_tensorflow/nnets/resnet.py +++ b/deeplabcut/pose_estimation_tensorflow/nnets/resnet.py @@ -63,9 +63,7 @@ def prediction_layers( if self.cfg["intermediate_supervision"]: layer_name = "resnet_v1_{}/block{}/unit_{}/bottleneck_v1" num_layers = re.findall("resnet_([0-9]*)", self.cfg["net_type"])[0] - interm_name = layer_name.format( - num_layers, 3, self.cfg["intermediate_supervision_layer"] - ) + interm_name = layer_name.format(num_layers, 3, self.cfg["intermediate_supervision_layer"]) block_interm_out = end_points[interm_name] out["part_pred_interm"] = prediction_layer( self.cfg, diff --git a/deeplabcut/pose_estimation_tensorflow/nnets/utils.py b/deeplabcut/pose_estimation_tensorflow/nnets/utils.py index bb080952df..24d2b0249d 100644 --- a/deeplabcut/pose_estimation_tensorflow/nnets/utils.py +++ b/deeplabcut/pose_estimation_tensorflow/nnets/utils.py @@ -52,9 +52,7 @@ def get_batch_spec(cfg): batch_spec[Batch.locref_mask] = [batch_size, None, None, num_joints * 2] if cfg["pairwise_predict"]: print("Getting specs", cfg["dataset_type"], num_limbs, num_joints) - if ( - "multi-animal" not in cfg["dataset_type"] - ): # this can be used for pairwise conditional + if "multi-animal" not in cfg["dataset_type"]: # this can be used for pairwise conditional batch_spec[Batch.pairwise_targets] = [ batch_size, None, @@ -105,16 +103,10 @@ def build_learning_rate( if lr_decay_type == "exponential": assert steps_per_epoch is not None decay_steps = steps_per_epoch * decay_epochs - lr = tf.compat.v1.train.exponential_decay( - initial_lr, global_step, decay_steps, decay_factor, staircase=True - ) + lr = tf.compat.v1.train.exponential_decay(initial_lr, global_step, decay_steps, decay_factor, staircase=True) elif lr_decay_type == "cosine": assert total_steps is not None - lr = ( - 0.5 - * initial_lr - * (1 + tf.cos(np.pi * tf.cast(global_step, tf.float32) / total_steps)) - ) + lr = 0.5 * initial_lr * (1 + tf.cos(np.pi * tf.cast(global_step, tf.float32) / total_steps)) elif lr_decay_type == "constant": lr = initial_lr else: @@ -123,11 +115,7 @@ def build_learning_rate( if warmup_epochs: tf.compat.v1.logging.info("Learning rate warmup_epochs: %d" % warmup_epochs) warmup_steps = int(warmup_epochs * steps_per_epoch) - warmup_lr = ( - initial_lr - * tf.cast(global_step, tf.float32) - / tf.cast(warmup_steps, tf.float32) - ) + warmup_lr = initial_lr * tf.cast(global_step, tf.float32) / tf.cast(warmup_steps, tf.float32) lr = tf.cond( pred=global_step < warmup_steps, true_fn=lambda: warmup_lr, @@ -137,25 +125,17 @@ def build_learning_rate( return lr -def build_optimizer( - learning_rate, optimizer_name="rmsprop", decay=0.9, epsilon=0.001, momentum=0.9 -): +def build_optimizer(learning_rate, optimizer_name="rmsprop", decay=0.9, epsilon=0.001, momentum=0.9): """Build optimizer.""" if optimizer_name == "sgd": tf.compat.v1.logging.info("Using SGD optimizer") - optimizer = tf.compat.v1.train.GradientDescentOptimizer( - learning_rate=learning_rate - ) + optimizer = tf.compat.v1.train.GradientDescentOptimizer(learning_rate=learning_rate) elif optimizer_name == "momentum": tf.compat.v1.logging.info("Using Momentum optimizer") - optimizer = tf.compat.v1.train.MomentumOptimizer( - learning_rate=learning_rate, momentum=momentum - ) + optimizer = tf.compat.v1.train.MomentumOptimizer(learning_rate=learning_rate, momentum=momentum) elif optimizer_name == "rmsprop": tf.compat.v1.logging.info("Using RMSProp optimizer") - optimizer = tf.compat.v1.train.RMSPropOptimizer( - learning_rate, decay, momentum, epsilon - ) + optimizer = tf.compat.v1.train.RMSPropOptimizer(learning_rate, decay, momentum, epsilon) else: tf.compat.v1.logging.fatal("Unknown optimizer:", optimizer_name) return optimizer @@ -177,17 +157,13 @@ def _cross_replica_average(t, num_shards_per_group): if num_shards_per_group > 1: if num_shards % num_shards_per_group != 0: raise ValueError( - "num_shards: %d mod shards_per_group: %d, should be 0" - % (num_shards, num_shards_per_group) + "num_shards: %d mod shards_per_group: %d, should be 0" % (num_shards, num_shards_per_group) ) num_groups = num_shards // num_shards_per_group group_assignment = [ - [x for x in range(num_shards) if x // num_shards_per_group == y] - for y in range(num_groups) + [x for x in range(num_shards) if x // num_shards_per_group == y] for y in range(num_groups) ] - return tpu_ops.cross_replica_sum(t, group_assignment) / tf.cast( - num_shards_per_group, t.dtype - ) + return tpu_ops.cross_replica_sum(t, group_assignment) / tf.cast(num_shards_per_group, t.dtype) def _moments(self, inputs, reduction_axes, keep_dims): """Compute the mean and variance: it overrides the original _moments.""" @@ -200,17 +176,13 @@ def _moments(self, inputs, reduction_axes, keep_dims): num_shards_per_group = 1 else: num_shards_per_group = max(8, num_shards // 8) - tf.compat.v1.logging.info( - "TpuBatchNormalization with num_shards_per_group %s", num_shards_per_group - ) + tf.compat.v1.logging.info("TpuBatchNormalization with num_shards_per_group %s", num_shards_per_group) if num_shards_per_group > 1: # Compute variance using: Var[X]= E[X^2] - E[X]^2. shard_square_of_mean = tf.math.square(shard_mean) shard_mean_of_square = shard_variance + shard_square_of_mean group_mean = self._cross_replica_average(shard_mean, num_shards_per_group) - group_mean_of_square = self._cross_replica_average( - shard_mean_of_square, num_shards_per_group - ) + group_mean_of_square = self._cross_replica_average(shard_mean_of_square, num_shards_per_group) group_variance = group_mean_of_square - tf.math.square(group_mean) return group_mean, group_variance return shard_mean, shard_variance diff --git a/deeplabcut/pose_estimation_tensorflow/predict_multianimal.py b/deeplabcut/pose_estimation_tensorflow/predict_multianimal.py index f9c943a4fa..54c9752ab7 100644 --- a/deeplabcut/pose_estimation_tensorflow/predict_multianimal.py +++ b/deeplabcut/pose_estimation_tensorflow/predict_multianimal.py @@ -101,9 +101,7 @@ def extract_bpt_feature_from_video( extra_dict, ) else: - raise NotImplementedError( - "Not implemented yet, please raise an GitHub issue if you need this." - ) + raise NotImplementedError("Not implemented yet, please raise an GitHub issue if you need this.") def AnalyzeMultiAnimalVideo( @@ -222,9 +220,7 @@ def AnalyzeMultiAnimalVideo( with open(metadata_path, "wb") as f: pickle.dump(metadata, f, pickle.HIGHEST_PROTOCOL) else: - _ = auxfun_multianimal.SaveFullMultiAnimalData( - PredicteData, metadata, dataname - ) + _ = auxfun_multianimal.SaveFullMultiAnimalData(PredicteData, metadata, dataname) def _get_features_dict(raw_coords, features, stride): @@ -233,18 +229,14 @@ def _get_features_dict(raw_coords, features, stride): convert_coord_from_img_space_to_feature_space, ) - coords_img_space = np.array( - [coord[:, :2] for coord in raw_coords] - ) # only first two columns are useful + coords_img_space = np.array([coord[:, :2] for coord in raw_coords]) # only first two columns are useful coords_feature_space = convert_coord_from_img_space_to_feature_space( coords_img_space, stride, ) - bpt_features = load_features_from_coord( - features.astype(np.float16), coords_feature_space - ) + bpt_features = load_features_from_coord(features.astype(np.float16), coords_feature_space) return {"features": bpt_features, "coordinates": coords_img_space} @@ -269,9 +261,7 @@ def GetPoseandCostsF_from_assemblies( cap.set_bbox(cfg["x1"], cfg["x2"], cfg["y1"], cfg["y2"]) nx, ny = cap.dimensions - frames = np.empty( - (batchsize, ny, nx, 3), dtype="ubyte" - ) # this keeps all frames in a batch + frames = np.empty((batchsize, ny, nx, 3), dtype="ubyte") # this keeps all frames in a batch pbar = tqdm(total=nframes) counter = 0 inds = [] @@ -350,13 +340,9 @@ def GetPoseandCostsF_from_assemblies( "minimal confidence": dlc_cfg["minconfidence"], "sigma": dlc_cfg.get("sigma", 1), "PAFgraph": dlc_cfg["partaffinityfield_graph"], - "PAFinds": dlc_cfg.get( - "paf_best", np.arange(len(dlc_cfg["partaffinityfield_graph"])) - ), + "PAFinds": dlc_cfg.get("paf_best", np.arange(len(dlc_cfg["partaffinityfield_graph"]))), "all_joints": [[i] for i in range(len(dlc_cfg["all_joints"]))], - "all_joints_names": [ - dlc_cfg["all_joints_names"][i] for i in range(len(dlc_cfg["all_joints"])) - ], + "all_joints_names": [dlc_cfg["all_joints_names"][i] for i in range(len(dlc_cfg["all_joints"]))], "nframes": nframes, } return PredicteData, nframes @@ -381,9 +367,7 @@ def GetPoseandCostsF( cap.set_bbox(cfg["x1"], cfg["x2"], cfg["y1"], cfg["y2"]) nx, ny = cap.dimensions - frames = np.empty( - (batchsize, ny, nx, 3), dtype="ubyte" - ) # this keeps all frames in a batch + frames = np.empty((batchsize, ny, nx, 3), dtype="ubyte") # this keeps all frames in a batch pbar = tqdm(total=nframes) counter = 0 inds = [] @@ -400,13 +384,9 @@ def GetPoseandCostsF( "minimal confidence": dlc_cfg["minconfidence"], "sigma": dlc_cfg.get("sigma", 1), "PAFgraph": dlc_cfg["partaffinityfield_graph"], - "PAFinds": dlc_cfg.get( - "paf_best", np.arange(len(dlc_cfg["partaffinityfield_graph"])) - ), + "PAFinds": dlc_cfg.get("paf_best", np.arange(len(dlc_cfg["partaffinityfield_graph"]))), "all_joints": [[i] for i in range(len(dlc_cfg["all_joints"]))], - "all_joints_names": [ - dlc_cfg["all_joints_names"][i] for i in range(len(dlc_cfg["all_joints"])) - ], + "all_joints_names": [dlc_cfg["all_joints_names"][i] for i in range(len(dlc_cfg["all_joints"]))], "nframes": nframes, } while cap.video.isOpened(): @@ -480,13 +460,9 @@ def GetPoseandCostsS(cfg, dlc_cfg, sess, inputs, outputs, cap, nframes, shelf_pa "minimal confidence": dlc_cfg["minconfidence"], "sigma": dlc_cfg.get("sigma", 1), "PAFgraph": dlc_cfg["partaffinityfield_graph"], - "PAFinds": dlc_cfg.get( - "paf_best", np.arange(len(dlc_cfg["partaffinityfield_graph"])) - ), + "PAFinds": dlc_cfg.get("paf_best", np.arange(len(dlc_cfg["partaffinityfield_graph"]))), "all_joints": [[i] for i in range(len(dlc_cfg["all_joints"]))], - "all_joints_names": [ - dlc_cfg["all_joints_names"][i] for i in range(len(dlc_cfg["all_joints"])) - ], + "all_joints_names": [dlc_cfg["all_joints_names"][i] for i in range(len(dlc_cfg["all_joints"]))], "nframes": nframes, } pbar = tqdm(total=nframes) diff --git a/deeplabcut/pose_estimation_tensorflow/predict_videos.py b/deeplabcut/pose_estimation_tensorflow/predict_videos.py index dd4c66c0e8..7c35013f9a 100644 --- a/deeplabcut/pose_estimation_tensorflow/predict_videos.py +++ b/deeplabcut/pose_estimation_tensorflow/predict_videos.py @@ -69,9 +69,7 @@ def create_tracking_dataset( try: from deeplabcut.pose_tracking_pytorch import create_triplets_dataset except ModuleNotFoundError: - raise ModuleNotFoundError( - "Unsupervised identity learning requires PyTorch. Please run `pip install torch`." - ) + raise ModuleNotFoundError("Unsupervised identity learning requires PyTorch. Please run `pip install torch`.") from deeplabcut.pose_estimation_tensorflow.predict_multianimal import ( extract_bpt_feature_from_video, @@ -100,19 +98,14 @@ def create_tracking_dataset( modelfolder = os.path.join( cfg["project_path"], - str( - auxiliaryfunctions.get_model_folder( - trainFraction, shuffle, cfg, modelprefix=modelprefix - ) - ), + str(auxiliaryfunctions.get_model_folder(trainFraction, shuffle, cfg, modelprefix=modelprefix)), ) path_test_config = Path(modelfolder) / "test" / "pose_cfg.yaml" try: dlc_cfg = load_config(str(path_test_config)) except FileNotFoundError: raise FileNotFoundError( - "It seems the model for shuffle %s and trainFraction %s does not exist." - % (shuffle, trainFraction) + "It seems the model for shuffle %s and trainFraction %s does not exist." % (shuffle, trainFraction) ) Snapshots = auxiliaryfunctions.get_snapshots_from_folder( @@ -134,9 +127,7 @@ def create_tracking_dataset( ################################################## # Check if data already was generated: - dlc_cfg["init_weights"] = os.path.join( - modelfolder, "train", Snapshots[snapshotindex] - ) + dlc_cfg["init_weights"] = os.path.join(modelfolder, "train", Snapshots[snapshotindex]) trainingsiterations = (dlc_cfg["init_weights"].split(os.sep)[-1]).split("-")[-1] # Update number of output and batchsize dlc_cfg["num_outputs"] = cfg.get("num_outputs", dlc_cfg.get("num_outputs", 1)) @@ -185,9 +176,7 @@ def create_tracking_dataset( xyz_labs = ["x", "y", "likelihood"] if TFGPUinference: - sess, inputs, outputs = predict.setup_GPUpose_prediction( - dlc_cfg, allow_growth=allow_growth - ) + sess, inputs, outputs = predict.setup_GPUpose_prediction(dlc_cfg, allow_growth=allow_growth) else: sess, inputs, outputs, extra_dict = predict.setup_pose_prediction( dlc_cfg, allow_growth=allow_growth, collect_extra=True @@ -492,11 +481,7 @@ def analyze_videos( modelfolder = os.path.join( cfg["project_path"], - str( - auxiliaryfunctions.get_model_folder( - trainFraction, shuffle, cfg, modelprefix=modelprefix - ) - ), + str(auxiliaryfunctions.get_model_folder(trainFraction, shuffle, cfg, modelprefix=modelprefix)), ) path_test_config = Path(modelfolder) / "test" / "pose_cfg.yaml" try: @@ -526,9 +511,7 @@ def analyze_videos( ################################################## # Check if data already was generated: - dlc_cfg["init_weights"] = os.path.join( - modelfolder, "train", Snapshots[snapshotindex] - ) + dlc_cfg["init_weights"] = os.path.join(modelfolder, "train", Snapshots[snapshotindex]) trainingsiterations = (dlc_cfg["init_weights"].split(os.sep)[-1]).split("-")[-1] # Update number of output and batchsize dlc_cfg["num_outputs"] = cfg.get("num_outputs", dlc_cfg.get("num_outputs", 1)) @@ -577,17 +560,11 @@ def analyze_videos( xyz_labs = ["x", "y", "likelihood"] if use_openvino: - sess, inputs, outputs = predict.setup_openvino_pose_prediction( - dlc_cfg, device=use_openvino - ) + sess, inputs, outputs = predict.setup_openvino_pose_prediction(dlc_cfg, device=use_openvino) elif TFGPUinference: - sess, inputs, outputs = predict.setup_GPUpose_prediction( - dlc_cfg, allow_growth=allow_growth - ) + sess, inputs, outputs = predict.setup_GPUpose_prediction(dlc_cfg, allow_growth=allow_growth) else: - sess, inputs, outputs = predict.setup_pose_prediction( - dlc_cfg, allow_growth=allow_growth - ) + sess, inputs, outputs = predict.setup_pose_prediction(dlc_cfg, allow_growth=allow_growth) pdindex = pd.MultiIndex.from_product( [[DLCscorer], dlc_cfg["all_joints_names"], xyz_labs], @@ -708,20 +685,14 @@ def checkcropping(cfg, cap): def GetPoseF(cfg, dlc_cfg, sess, inputs, outputs, cap, nframes, batchsize): """Batchwise prediction of pose""" - PredictedData = np.zeros( - (nframes, dlc_cfg["num_outputs"] * 3 * len(dlc_cfg["all_joints_names"])) - ) + PredictedData = np.zeros((nframes, dlc_cfg["num_outputs"] * 3 * len(dlc_cfg["all_joints_names"]))) batch_ind = 0 # keeps track of which image within a batch should be written to batch_num = 0 # keeps track of which batch you are at - ny, nx = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)), int( - cap.get(cv2.CAP_PROP_FRAME_WIDTH) - ) + ny, nx = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)), int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) if cfg["cropping"]: ny, nx = checkcropping(cfg, cap) - frames = np.empty( - (batchsize, ny, nx, 3), dtype="ubyte" - ) # this keeps all frames in a batch + frames = np.empty((batchsize, ny, nx, 3), dtype="ubyte") # this keeps all frames in a batch pbar = tqdm(total=nframes) counter = 0 step = max(10, int(nframes / 100)) @@ -733,9 +704,7 @@ def GetPoseF(cfg, dlc_cfg, sess, inputs, outputs, cap, nframes, batchsize): if ret: frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) if cfg["cropping"]: - frames[batch_ind] = img_as_ubyte( - frame[cfg["y1"] : cfg["y2"], cfg["x1"] : cfg["x2"]] - ) + frames[batch_ind] = img_as_ubyte(frame[cfg["y1"] : cfg["y2"], cfg["x1"] : cfg["x2"]]) else: frames[batch_ind] = img_as_ubyte(frame) inds.append(counter) @@ -765,9 +734,7 @@ def GetPoseS(cfg, dlc_cfg, sess, inputs, outputs, cap, nframes): if cfg["cropping"]: ny, nx = checkcropping(cfg, cap) - PredictedData = np.zeros( - (nframes, dlc_cfg["num_outputs"] * 3 * len(dlc_cfg["all_joints_names"])) - ) + PredictedData = np.zeros((nframes, dlc_cfg["num_outputs"] * 3 * len(dlc_cfg["all_joints_names"]))) pbar = tqdm(total=nframes) counter = 0 step = max(10, int(nframes / 100)) @@ -779,9 +746,7 @@ def GetPoseS(cfg, dlc_cfg, sess, inputs, outputs, cap, nframes): if ret: frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) if cfg["cropping"]: - frame = img_as_ubyte( - frame[cfg["y1"] : cfg["y2"], cfg["x1"] : cfg["x2"]] - ) + frame = img_as_ubyte(frame[cfg["y1"] : cfg["y2"], cfg["x1"] : cfg["x2"]]) else: frame = img_as_ubyte(frame) pose = predict.getpose(frame, dlc_cfg, sess, inputs, outputs) @@ -801,9 +766,7 @@ def GetPoseS_GTF(cfg, dlc_cfg, sess, inputs, outputs, cap, nframes): if cfg["cropping"]: ny, nx = checkcropping(cfg, cap) - pose_tensor = predict.extract_GPUprediction( - outputs, dlc_cfg - ) # extract_output_tensor(outputs, dlc_cfg) + pose_tensor = predict.extract_GPUprediction(outputs, dlc_cfg) # extract_output_tensor(outputs, dlc_cfg) PredictedData = np.zeros((nframes, 3 * len(dlc_cfg["all_joints_names"]))) pbar = tqdm(total=nframes) counter = 0 @@ -816,9 +779,7 @@ def GetPoseS_GTF(cfg, dlc_cfg, sess, inputs, outputs, cap, nframes): if ret: frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) if cfg["cropping"]: - frame = img_as_ubyte( - frame[cfg["y1"] : cfg["y2"], cfg["x1"] : cfg["x2"]] - ) + frame = img_as_ubyte(frame[cfg["y1"] : cfg["y2"], cfg["x1"] : cfg["x2"]]) else: frame = img_as_ubyte(frame) @@ -898,16 +859,12 @@ def getboundingbox(x, y, nx, ny, margin): return x1, x2, y1, y2 -def GetPoseDynamic( - cfg, dlc_cfg, sess, inputs, outputs, cap, nframes, detectiontreshold, margin -): +def GetPoseDynamic(cfg, dlc_cfg, sess, inputs, outputs, cap, nframes, detectiontreshold, margin): """Non batch wise pose estimation for video cap by dynamically cropping around previously detected parts.""" if cfg["cropping"]: ny, nx = checkcropping(cfg, cap) else: - ny, nx = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)), int( - cap.get(cv2.CAP_PROP_FRAME_WIDTH) - ) + ny, nx = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)), int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) x1, x2, y1, y2 = 0, nx, 0, ny detected = False # TODO: perform detection on resized image (For speed) @@ -925,9 +882,7 @@ def GetPoseDynamic( # print(counter,x1,x2,y1,y2,detected) originalframe = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) if cfg["cropping"]: - frame = img_as_ubyte( - originalframe[cfg["y1"] : cfg["y2"], cfg["x1"] : cfg["x2"]] - )[y1:y2, x1:x2] + frame = img_as_ubyte(originalframe[cfg["y1"] : cfg["y2"], cfg["x1"] : cfg["x2"]])[y1:y2, x1:x2] else: frame = img_as_ubyte(originalframe[y1:y2, x1:x2]) @@ -949,14 +904,10 @@ def GetPoseDynamic( ): # was detected in last frame and dyn. cropping was performed >> but object lost in cropped variant >> re-run on full frame! # print("looking again, lost!") if cfg["cropping"]: - frame = img_as_ubyte( - originalframe[cfg["y1"] : cfg["y2"], cfg["x1"] : cfg["x2"]] - ) + frame = img_as_ubyte(originalframe[cfg["y1"] : cfg["y2"], cfg["x1"] : cfg["x2"]]) else: frame = img_as_ubyte(originalframe) - pose = predict.getpose( - frame, dlc_cfg, sess, inputs, outputs - ).flatten() # no offset is necessary + pose = predict.getpose(frame, dlc_cfg, sess, inputs, outputs).flatten() # no offset is necessary x0, y0 = x1, y1 x1, x2, y1, y2 = 0, nx, 0, ny @@ -1001,9 +952,7 @@ def AnalyzeVideo( print("Loading ", video) cap = cv2.VideoCapture(video) if not cap.isOpened(): - raise IOError( - "Video could not be opened. Please check that the the file integrity." - ) + raise IOError("Video could not be opened. Please check that the the file integrity.") # https://docs.opencv.org/2.4/modules/highgui/doc/reading_and_writing_images_and_video.html#videocapture-get fps = cap.get(cv2.CAP_PROP_FPS) nframes = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) @@ -1064,13 +1013,9 @@ def AnalyzeVideo( PredictedData, nframes = GetPoseF(*args) else: if TFGPUinference: - PredictedData, nframes = GetPoseS_GTF( - cfg, dlc_cfg, sess, inputs, outputs, cap, nframes - ) + PredictedData, nframes = GetPoseS_GTF(cfg, dlc_cfg, sess, inputs, outputs, cap, nframes) else: - PredictedData, nframes = GetPoseS( - cfg, dlc_cfg, sess, inputs, outputs, cap, nframes - ) + PredictedData, nframes = GetPoseS(cfg, dlc_cfg, sess, inputs, outputs, cap, nframes) stop = time.time() if cfg["cropping"] == True: @@ -1110,9 +1055,7 @@ def AnalyzeVideo( return DLCscorer -def GetPosesofFrames( - cfg, dlc_cfg, sess, inputs, outputs, directory, framelist, nframes, batchsize -): +def GetPosesofFrames(cfg, dlc_cfg, sess, inputs, outputs, directory, framelist, nframes, batchsize): """Batchwise prediction of pose for frame list in directory""" from deeplabcut.utils.auxfun_videos import imread @@ -1128,9 +1071,7 @@ def GetPosesofFrames( ny, ) - PredictedData = np.zeros( - (nframes, dlc_cfg["num_outputs"] * 3 * len(dlc_cfg["all_joints_names"])) - ) + PredictedData = np.zeros((nframes, dlc_cfg["num_outputs"] * 3 * len(dlc_cfg["all_joints_names"]))) batch_ind = 0 # keeps track of which image within a batch should be written to batch_num = 0 # keeps track of which batch you are at if cfg["cropping"]: @@ -1143,12 +1084,7 @@ def GetPosesofFrames( pass else: raise Exception("Please check the order of cropping parameter!") - if ( - cfg["x1"] >= 0 - and cfg["x2"] < int(np.shape(im)[1]) - and cfg["y1"] >= 0 - and cfg["y2"] < int(np.shape(im)[0]) - ): + if cfg["x1"] >= 0 and cfg["x2"] < int(np.shape(im)[1]) and cfg["y1"] >= 0 and cfg["y2"] < int(np.shape(im)[0]): pass # good cropping box else: raise Exception("Please check the boundary of cropping!") @@ -1165,18 +1101,14 @@ def GetPosesofFrames( pbar.update(step) if cfg["cropping"]: - frame = img_as_ubyte( - im[cfg["y1"] : cfg["y2"], cfg["x1"] : cfg["x2"], :] - ) + frame = img_as_ubyte(im[cfg["y1"] : cfg["y2"], cfg["x1"] : cfg["x2"], :]) else: frame = img_as_ubyte(im) pose = predict.getpose(frame, dlc_cfg, sess, inputs, outputs) PredictedData[counter, :] = pose.flatten() else: - frames = np.empty( - (batchsize, ny, nx, 3), dtype="ubyte" - ) # this keeps all the frames of a batch + frames = np.empty((batchsize, ny, nx, 3), dtype="ubyte") # this keeps all the frames of a batch for counter, framename in enumerate(framelist): im = imread(os.path.join(directory, framename), mode="skimage") @@ -1184,31 +1116,23 @@ def GetPosesofFrames( pbar.update(step) if cfg["cropping"]: - frames[batch_ind] = img_as_ubyte( - im[cfg["y1"] : cfg["y2"], cfg["x1"] : cfg["x2"], :] - ) + frames[batch_ind] = img_as_ubyte(im[cfg["y1"] : cfg["y2"], cfg["x1"] : cfg["x2"], :]) else: frames[batch_ind] = img_as_ubyte(im) if batch_ind == batchsize - 1: pose = predict.getposeNP(frames, dlc_cfg, sess, inputs, outputs) - PredictedData[ - batch_num * batchsize : (batch_num + 1) * batchsize, : - ] = pose + PredictedData[batch_num * batchsize : (batch_num + 1) * batchsize, :] = pose batch_ind = 0 batch_num += 1 else: batch_ind += 1 - if ( - batch_ind > 0 - ): # take care of the last frames (the batch that might have been processed) + if batch_ind > 0: # take care of the last frames (the batch that might have been processed) pose = predict.getposeNP( frames, dlc_cfg, sess, inputs, outputs ) # process the whole batch (some frames might be from previous batch!) - PredictedData[ - batch_num * batchsize : batch_num * batchsize + batch_ind, : - ] = pose[:batch_ind, :] + PredictedData[batch_num * batchsize : batch_num * batchsize + batch_ind, :] = pose[:batch_ind, :] pbar.close() return PredictedData, nframes, nx, ny @@ -1278,19 +1202,14 @@ def analyze_time_lapse_frames( trainFraction = cfg["TrainingFraction"][trainingsetindex] modelfolder = os.path.join( cfg["project_path"], - str( - auxiliaryfunctions.get_model_folder( - trainFraction, shuffle, cfg, modelprefix=modelprefix - ) - ), + str(auxiliaryfunctions.get_model_folder(trainFraction, shuffle, cfg, modelprefix=modelprefix)), ) path_test_config = Path(modelfolder) / "test" / "pose_cfg.yaml" try: dlc_cfg = load_config(str(path_test_config)) except FileNotFoundError: raise FileNotFoundError( - "It seems the model for shuffle %s and trainFraction %s does not exist." - % (shuffle, trainFraction) + "It seems the model for shuffle %s and trainFraction %s does not exist." % (shuffle, trainFraction) ) Snapshots = auxiliaryfunctions.get_snapshots_from_folder( @@ -1312,9 +1231,7 @@ def analyze_time_lapse_frames( ################################################## # Check if data already was generated: - dlc_cfg["init_weights"] = os.path.join( - modelfolder, "train", Snapshots[snapshotindex] - ) + dlc_cfg["init_weights"] = os.path.join(modelfolder, "train", Snapshots[snapshotindex]) trainingsiterations = (dlc_cfg["init_weights"].split(os.sep)[-1]).split("-")[-1] # update batchsize (based on parameters in config.yaml) @@ -1410,13 +1327,9 @@ def analyze_time_lapse_frames( save_as_csv, ) print("The folder was analyzed. Now your research can truly start!") - print( - "If the tracking is not satisfactory for some frame, consider expanding the training set." - ) + print("If the tracking is not satisfactory for some frame, consider expanding the training set.") else: - print( - "No frames were found. Consider changing the path or the frametype." - ) + print("No frames were found. Consider changing the path or the frametype.") os.chdir(str(start_path)) @@ -1437,9 +1350,7 @@ def _convert_detections_to_tracklets( ) if track_method == "ctd": - raise ValueError( - "CTD tracking is only available for BUCTD models with the PyTorch engine." - ) + raise ValueError("CTD tracking is only available for BUCTD models with the PyTorch engine.") joints = data["metadata"]["all_joints_names"] partaffinityfield_graph = data["metadata"]["PAFgraph"] @@ -1475,7 +1386,7 @@ def _convert_detections_to_tracklets( greedy=greedy, pcutoff=inference_cfg.get("pcutoff", 0.1), min_affinity=inference_cfg.get("pafthreshold", 0.05), - min_n_links=inference_cfg["minimalnumberofconnections"] + min_n_links=inference_cfg["minimalnumberofconnections"], ) if calibrate: trainingsetfolder = auxiliaryfunctions.get_training_set_folder(cfg) @@ -1499,9 +1410,7 @@ def _convert_detections_to_tracklets( assemblies = assembly_builder.assemblies.get(i) if assemblies is None: continue - animals = np.stack( - [assembly_builder.data[:, :3] for assembly_builder in assemblies] - ) + animals = np.stack([assembly_builder.data[:, :3] for assembly_builder in assemblies]) if track_method == "box": xy = trackingutils.calc_bboxes_from_keypoints( animals, inference_cfg.get("boundingboxslack", 0) @@ -1631,19 +1540,14 @@ def convert_detections2tracklets( modelfolder = os.path.join( cfg["project_path"], - str( - auxiliaryfunctions.get_model_folder( - trainFraction, shuffle, cfg, modelprefix=modelprefix - ) - ), + str(auxiliaryfunctions.get_model_folder(trainFraction, shuffle, cfg, modelprefix=modelprefix)), ) path_test_config = Path(modelfolder) / "test" / "pose_cfg.yaml" try: dlc_cfg = load_config(str(path_test_config)) except FileNotFoundError: raise FileNotFoundError( - "It seems the model for shuffle %s and trainFraction %s does not exist." - % (shuffle, trainFraction) + "It seems the model for shuffle %s and trainFraction %s does not exist." % (shuffle, trainFraction) ) if "multi-animal" not in dlc_cfg["dataset_type"]: @@ -1675,9 +1579,7 @@ def convert_detections2tracklets( snapshotindex = cfg["snapshotindex"] print("Using %s" % Snapshots[snapshotindex], "for model", modelfolder) - dlc_cfg["init_weights"] = os.path.join( - modelfolder, "train", Snapshots[snapshotindex] - ) + dlc_cfg["init_weights"] = os.path.join(modelfolder, "train", Snapshots[snapshotindex]) trainingsiterations = (dlc_cfg["init_weights"].split(os.sep)[-1]).split("-")[-1] # Name for scorer: @@ -1712,9 +1614,7 @@ def convert_detections2tracklets( trackname = dataname.split(".h5")[0] + f"_{method}.pickle" # NOTE: If dataname line above is changed then line below is obsolete? # trackname = trackname.replace(videofolder, destfolder) - if ( - os.path.isfile(trackname) and not overwrite - ): # TODO: check if metadata are identical (same parameters!) + if os.path.isfile(trackname) and not overwrite: # TODO: check if metadata are identical (same parameters!) print("Tracklets already computed", trackname) print("Set overwrite = True to overwrite.") else: @@ -1726,9 +1626,7 @@ def convert_detections2tracklets( # TODO: adjust this for multi + unique bodyparts! # this is only for multianimal parts and uniquebodyparts as one (not one uniquebodyparts guy tracked etc. ) - bodypartlabels = [ - bpt for i, bpt in enumerate(all_jointnames) for _ in range(3) - ] + bodypartlabels = [bpt for i, bpt in enumerate(all_jointnames) for _ in range(3)] scorers = len(bodypartlabels) * [DLCscorer] xylvalue = int(len(bodypartlabels) / 3) * ["x", "y", "likelihood"] pdindex = pd.MultiIndex.from_arrays( @@ -1768,14 +1666,12 @@ def convert_detections2tracklets( min_affinity=inferencecfg.get("pafthreshold", 0.05), window_size=window_size, identity_only=identity_only, - min_n_links=inferencecfg["minimalnumberofconnections"] + min_n_links=inferencecfg["minimalnumberofconnections"], ) assemblies_filename = dataname.split(".h5")[0] + "_assemblies.pickle" if not os.path.exists(assemblies_filename) or overwrite: if calibrate: - trainingsetfolder = auxiliaryfunctions.get_training_set_folder( - cfg - ) + trainingsetfolder = auxiliaryfunctions.get_training_set_folder(cfg) train_data_file = os.path.join( cfg["project_path"], str(trainingsetfolder), @@ -1792,9 +1688,7 @@ def convert_detections2tracklets( except AttributeError: pass - if cfg[ - "uniquebodyparts" - ]: # Initialize storage of the 'single' individual track + if cfg["uniquebodyparts"]: # Initialize storage of the 'single' individual track tracklets["single"] = {} _single = {} for index, imname in enumerate(imnames): @@ -1819,9 +1713,7 @@ def convert_detections2tracklets( assemblies = assembly_builder.assemblies.get(index) if assemblies is None: continue - animals = np.stack( - [assembly_builder.data for assembly_builder in assemblies] - ) + animals = np.stack([assembly_builder.data for assembly_builder in assemblies]) if not identity_only: if track_method == "box": xy = trackingutils.calc_bboxes_from_keypoints( @@ -1833,17 +1725,13 @@ def convert_detections2tracklets( trackers = mot_tracker.track(xy) else: # Optimal identity assignment based on soft voting - mat = np.zeros( - (len(assemblies), inferencecfg["topktoretain"]) - ) + mat = np.zeros((len(assemblies), inferencecfg["topktoretain"])) for nrow, assembly in enumerate(assemblies): for k, v in assembly.soft_identity.items(): mat[nrow, k] = v inds = linear_sum_assignment(mat, maximize=True) trackers = np.c_[inds][:, ::-1] - trackingutils.fill_tracklets( - tracklets, trackers, animals, imname - ) + trackingutils.fill_tracklets(tracklets, trackers, animals, imname) tracklets["header"] = pdindex with open(trackname, "wb") as f: diff --git a/deeplabcut/pose_estimation_tensorflow/training.py b/deeplabcut/pose_estimation_tensorflow/training.py index 7883934422..5e882b828c 100644 --- a/deeplabcut/pose_estimation_tensorflow/training.py +++ b/deeplabcut/pose_estimation_tensorflow/training.py @@ -35,17 +35,9 @@ def return_train_network_path(config, shuffle=1, trainingsetindex=0, modelprefix modelfoldername = auxiliaryfunctions.get_model_folder( cfg["TrainingFraction"][trainingsetindex], shuffle, cfg, modelprefix=modelprefix ) - trainposeconfigfile = Path( - os.path.join( - cfg["project_path"], str(modelfoldername), "train", "pose_cfg.yaml" - ) - ) - testposeconfigfile = Path( - os.path.join(cfg["project_path"], str(modelfoldername), "test", "pose_cfg.yaml") - ) - snapshotfolder = Path( - os.path.join(cfg["project_path"], str(modelfoldername), "train") - ) + trainposeconfigfile = Path(os.path.join(cfg["project_path"], str(modelfoldername), "train", "pose_cfg.yaml")) + testposeconfigfile = Path(os.path.join(cfg["project_path"], str(modelfoldername), "test", "pose_cfg.yaml")) + snapshotfolder = Path(os.path.join(cfg["project_path"], str(modelfoldername), "train")) return trainposeconfigfile, testposeconfigfile, snapshotfolder @@ -176,24 +168,16 @@ def train_network( modelfoldername = auxiliaryfunctions.get_model_folder( cfg["TrainingFraction"][trainingsetindex], shuffle, cfg, modelprefix=modelprefix ) - poseconfigfile = Path( - os.path.join( - cfg["project_path"], str(modelfoldername), "train", "pose_cfg.yaml" - ) - ) + poseconfigfile = Path(os.path.join(cfg["project_path"], str(modelfoldername), "train", "pose_cfg.yaml")) if not poseconfigfile.is_file(): print("The training datafile ", poseconfigfile, " is not present.") - print( - "Probably, the training dataset for this specific shuffle index was not created." - ) + print("Probably, the training dataset for this specific shuffle index was not created.") print( "Try with a different shuffle/trainingsetfraction or use function 'create_training_dataset' to create a new trainingdataset with this shuffle index." ) else: # Set environment variables - if ( - autotune is not False - ): # see: https://github.com/tensorflow/tensorflow/issues/13317 + if autotune is not False: # see: https://github.com/tensorflow/tensorflow/issues/13317 os.environ["TF_CUDNN_USE_AUTOTUNE"] = "0" if gputouse is not None: os.environ["CUDA_VISIBLE_DEVICES"] = str(gputouse) @@ -246,11 +230,7 @@ def train_network( keepdeconvweights=keepdeconvweights, allow_growth=allow_growth, init_weights=init_weights, - remove_head=( - True - if superanimal_name != "" and superanimal_transfer_learning - else False - ), + remove_head=(True if superanimal_name != "" and superanimal_transfer_learning else False), ) # pass on path and file name for pose_cfg.yaml! elif "multi-animal" in cfg_dlc["dataset_type"]: diff --git a/deeplabcut/pose_estimation_tensorflow/util/logging.py b/deeplabcut/pose_estimation_tensorflow/util/logging.py index dc355b555d..42f0ab7ea2 100644 --- a/deeplabcut/pose_estimation_tensorflow/util/logging.py +++ b/deeplabcut/pose_estimation_tensorflow/util/logging.py @@ -15,6 +15,7 @@ Adapted from DeeperCut by Eldar Insafutdinov https://github.com/eldar/pose-tensorflow """ + import logging import os diff --git a/deeplabcut/pose_estimation_tensorflow/util/visualize.py b/deeplabcut/pose_estimation_tensorflow/util/visualize.py index 12fb8cd7b7..2a8fe7213c 100644 --- a/deeplabcut/pose_estimation_tensorflow/util/visualize.py +++ b/deeplabcut/pose_estimation_tensorflow/util/visualize.py @@ -33,10 +33,7 @@ def _npcircle(image, cx, cy, radius, color, transparency=0.0): y, x = np.ogrid[-radius:radius, -radius:radius] index = x**2 + y**2 <= radius**2 image[cy - radius : cy + radius, cx - radius : cx + radius][index] = ( - image[cy - radius : cy + radius, cx - radius : cx + radius][index].astype( - "float32" - ) - * transparency + image[cy - radius : cy + radius, cx - radius : cx + radius][index].astype("float32") * transparency + np.array(color).astype("float32") * (1.0 - transparency) ).astype("uint8") diff --git a/deeplabcut/pose_estimation_tensorflow/vis_dataset.py b/deeplabcut/pose_estimation_tensorflow/vis_dataset.py index 0ace2cd762..35349e9257 100644 --- a/deeplabcut/pose_estimation_tensorflow/vis_dataset.py +++ b/deeplabcut/pose_estimation_tensorflow/vis_dataset.py @@ -67,9 +67,7 @@ def display_dataset(): continue scmap_part = scmap[:, :, j] - scmap_part = imresize( - scmap_part, 8.0, interpolationmethod=cv2.INTER_NEAREST - ) + scmap_part = imresize(scmap_part, 8.0, interpolationmethod=cv2.INTER_NEAREST) scmap_part = np.lib.pad(scmap_part, ((4, 0), (4, 0)), "minimum") curr_plot.set_title("{}".format(j + 1)) diff --git a/deeplabcut/pose_estimation_tensorflow/visualizemaps.py b/deeplabcut/pose_estimation_tensorflow/visualizemaps.py index 111a695277..671e0455ce 100644 --- a/deeplabcut/pose_estimation_tensorflow/visualizemaps.py +++ b/deeplabcut/pose_estimation_tensorflow/visualizemaps.py @@ -107,9 +107,7 @@ def extract_maps( ) # Make folder for evaluation - auxiliaryfunctions.attempt_to_make_folder( - str(cfg["project_path"] + "/evaluation-results/") - ) + auxiliaryfunctions.attempt_to_make_folder(str(cfg["project_path"] + "/evaluation-results/")) Maps = {} for trainFraction in TrainingFractions: @@ -123,11 +121,7 @@ def extract_maps( modelfolder = os.path.join( cfg["project_path"], - str( - auxiliaryfunctions.get_model_folder( - trainFraction, shuffle, cfg, modelprefix=modelprefix - ) - ), + str(auxiliaryfunctions.get_model_folder(trainFraction, shuffle, cfg, modelprefix=modelprefix)), ) path_test_config = Path(modelfolder) / "test" / "pose_cfg.yaml" # Load meta data @@ -136,15 +130,12 @@ def extract_maps( trainIndices, testIndices, trainFraction, - ) = auxiliaryfunctions.load_metadata( - os.path.join(cfg["project_path"], metadatafn) - ) + ) = auxiliaryfunctions.load_metadata(os.path.join(cfg["project_path"], metadatafn)) try: dlc_cfg = load_config(str(path_test_config)) except FileNotFoundError: raise FileNotFoundError( - "It seems the model for shuffle %s and trainFraction %s does not exist." - % (shuffle, trainFraction) + "It seems the model for shuffle %s and trainFraction %s does not exist." % (shuffle, trainFraction) ) # change batch size, if it was edited during analysis! @@ -153,11 +144,7 @@ def extract_maps( # Create folder structure to store results. evaluationfolder = os.path.join( cfg["project_path"], - str( - auxiliaryfunctions.get_evaluation_folder( - trainFraction, shuffle, cfg, modelprefix=modelprefix - ) - ), + str(auxiliaryfunctions.get_evaluation_folder(trainFraction, shuffle, cfg, modelprefix=modelprefix)), ) auxiliaryfunctions.attempt_to_make_folder(evaluationfolder, recursive=True) @@ -172,25 +159,19 @@ def extract_maps( elif cfg["snapshotindex"] < len(Snapshots): snapindices = [cfg["snapshotindex"]] else: - print( - "Invalid choice, only -1 (last), any integer up to last, or all (as string)!" - ) + print("Invalid choice, only -1 (last), any integer up to last, or all (as string)!") ########################### RESCALING (to global scale) scale = dlc_cfg["global_scale"] if rescale else 1 Data *= scale - bptnames = [ - dlc_cfg["all_joints_names"][i] for i in range(len(dlc_cfg["all_joints"])) - ] + bptnames = [dlc_cfg["all_joints_names"][i] for i in range(len(dlc_cfg["all_joints"]))] for snapindex in snapindices: dlc_cfg["init_weights"] = os.path.join( str(modelfolder), "train", Snapshots[snapindex] ) # setting weights to corresponding snapshot. - trainingsiterations = (dlc_cfg["init_weights"].split(os.sep)[-1]).split( - "-" - )[ + trainingsiterations = (dlc_cfg["init_weights"].split(os.sep)[-1]).split("-")[ -1 ] # read how many training siterations that corresponds to. @@ -213,9 +194,7 @@ def extract_maps( DATA = {} for imageindex, imagename in tqdm(Indices): - image = imread( - os.path.join(cfg["project_path"], *imagename), mode="skimage" - ) + image = imread(os.path.join(cfg["project_path"], *imagename), mode="skimage") if scale != 1: image = imresize(image, scale) @@ -226,9 +205,7 @@ def extract_maps( outputs_np = sess.run(outputs, feed_dict={inputs: image_batch}) if cfg.get("multianimalproject", False): - scmap, locref, paf = predictma.extract_cnn_output( - outputs_np, dlc_cfg - ) + scmap, locref, paf = predictma.extract_cnn_output(outputs_np, dlc_cfg) pagraph = dlc_cfg["partaffinityfield_graph"] else: scmap, locref = predict.extract_cnn_output(outputs_np, dlc_cfg) @@ -273,9 +250,7 @@ def resize_all_maps(image, scmap, locref, paf): def _save_individual_subplots(fig, axes, labels, output_path): for ax, label in zip(axes, labels): - extent = ax.get_tightbbox(fig.canvas.renderer).transformed( - fig.dpi_scale_trans.inverted() - ) + extent = ax.get_tightbbox(fig.canvas.renderer).transformed(fig.dpi_scale_trans.inverted()) fig.savefig(output_path.format(bp=label), bbox_inches=extent) @@ -340,13 +315,9 @@ def extract_save_all_maps( from tqdm import tqdm cfg = read_config(config) - data = extract_maps( - config, shuffle, trainingsetindex, gputouse, rescale, Indices, modelprefix - ) + data = extract_maps(config, shuffle, trainingsetindex, gputouse, rescale, Indices, modelprefix) - comparisonbodyparts = intersection_of_body_parts_and_ones_given_by_user( - cfg, comparisonbodyparts - ) + comparisonbodyparts = intersection_of_body_parts_and_ones_given_by_user(cfg, comparisonbodyparts) print("Saving plots...") for frac, values in data.items(): @@ -376,18 +347,12 @@ def extract_save_all_maps( paf = None label = "train" if trainingframe else "test" imname = impath[-1] - scmap, (locref_x, locref_y), paf = resize_all_maps( - image, scmap, locref, paf - ) - to_plot = [ - i for i, bpt in enumerate(bptnames) if bpt in comparisonbodyparts - ] + scmap, (locref_x, locref_y), paf = resize_all_maps(image, scmap, locref, paf) + to_plot = [i for i, bpt in enumerate(bptnames) if bpt in comparisonbodyparts] list_of_inds = [] for n, edge in enumerate(pafgraph): if any(ind in to_plot for ind in edge): - list_of_inds.append( - [(2 * n, 2 * n + 1), (bptnames[edge[0]], bptnames[edge[1]])] - ) + list_of_inds.append([(2 * n, 2 * n + 1), (bptnames[edge[0]], bptnames[edge[1]])]) if len(to_plot) > 1: map_ = scmap[:, :, to_plot].sum(axis=2) locref_x_ = locref_x[:, :, to_plot].sum(axis=2) @@ -428,7 +393,7 @@ def extract_save_all_maps( fig3, _ = visualize_paf(image, paf[:, :, [inds]]) temp = dest_path.format( imname=imname, - map=f'paf_{"_".join(names)}', + map=f"paf_{'_'.join(names)}", label=label, shuffle=shuffle, frac=frac, diff --git a/deeplabcut/pose_tracking_pytorch/apis.py b/deeplabcut/pose_tracking_pytorch/apis.py index 07a9fc13d8..30f5ef528c 100644 --- a/deeplabcut/pose_tracking_pytorch/apis.py +++ b/deeplabcut/pose_tracking_pytorch/apis.py @@ -139,9 +139,7 @@ def transformer_reID( destfolder=destfolder, ) - transformer_checkpoint = os.path.join( - snapshotfolder, f"dlc_transreid_{train_epochs}.pth" - ) + transformer_checkpoint = os.path.join(snapshotfolder, f"dlc_transreid_{train_epochs}.pth") if not os.path.exists(transformer_checkpoint): raise FileNotFoundError(f"checkpoint {transformer_checkpoint} not found") diff --git a/deeplabcut/pose_tracking_pytorch/create_dataset.py b/deeplabcut/pose_tracking_pytorch/create_dataset.py index ad588f0da0..b1a2e5792c 100644 --- a/deeplabcut/pose_tracking_pytorch/create_dataset.py +++ b/deeplabcut/pose_tracking_pytorch/create_dataset.py @@ -47,22 +47,12 @@ def save_train_triplets(feature_fname, triplets, out_name): pos_frame = "frame" + str(pos_frame).zfill(zfill_width) neg_frame = "frame" + str(neg_frame).zfill(zfill_width) - if ( - anchor_frame in feature_dict - and pos_frame in feature_dict - and neg_frame in feature_dict - ): + if anchor_frame in feature_dict and pos_frame in feature_dict and neg_frame in feature_dict: # only try to find these features if they are in the dictionary - anchor_vec = query_feature_by_coord_in_img_space( - feature_dict, anchor_frame, anchor_coord - ) - pos_vec = query_feature_by_coord_in_img_space( - feature_dict, pos_frame, pos_coord - ) - neg_vec = query_feature_by_coord_in_img_space( - feature_dict, neg_frame, neg_coord - ) + anchor_vec = query_feature_by_coord_in_img_space(feature_dict, anchor_frame, anchor_coord) + pos_vec = query_feature_by_coord_in_img_space(feature_dict, pos_frame, pos_coord) + neg_vec = query_feature_by_coord_in_img_space(feature_dict, neg_frame, neg_coord) ret_vecs.append([anchor_vec, pos_vec, neg_vec]) @@ -73,15 +63,11 @@ def save_train_triplets(feature_fname, triplets, out_name): def create_train_using_pickle(feature_fname, path_to_pickle, out_name, n_triplets=1000): - triplets = generate_train_triplets_from_pickle( - path_to_pickle, n_triplets=n_triplets - ) + triplets = generate_train_triplets_from_pickle(path_to_pickle, n_triplets=n_triplets) save_train_triplets(feature_fname, triplets, out_name) -def create_triplets_dataset( - videos, dlcscorer, track_method, n_triplets=1000, destfolder=None -): +def create_triplets_dataset(videos, dlcscorer, track_method, n_triplets=1000, destfolder=None): # 1) reference to video folder and get the proper bpt_feature file for feature table # 2) get either the path to gt or the path to track pickle @@ -90,9 +76,7 @@ def create_triplets_dataset( videofolder = str(Path(video).parents[0]) if destfolder is None: destfolder = videofolder - feature_fname = os.path.join( - destfolder, vname + dlcscorer + "_bpt_features.pickle" - ) + feature_fname = os.path.join(destfolder, vname + dlcscorer + "_bpt_features.pickle") method = trackingutils.TRACK_METHODS[track_method] track_file = os.path.join(destfolder, vname + dlcscorer + f"{method}.pickle") @@ -104,6 +88,4 @@ def create_triplets_dataset( ) out_fname = os.path.join(destfolder, vname + dlcscorer + "_triplet_vector.npy") - create_train_using_pickle( - feature_fname, track_file, out_fname, n_triplets=n_triplets - ) + create_train_using_pickle(feature_fname, track_file, out_fname, n_triplets=n_triplets) diff --git a/deeplabcut/pose_tracking_pytorch/inference.py b/deeplabcut/pose_tracking_pytorch/inference.py index ddec2ed96e..0691de2ec3 100644 --- a/deeplabcut/pose_tracking_pytorch/inference.py +++ b/deeplabcut/pose_tracking_pytorch/inference.py @@ -30,9 +30,7 @@ def __init__(self, checkpoint): ckpt_dict = torch.load(self.checkpoint) - self.model = build_dlc_transformer( - cfg, ckpt_dict["feature_dim"], ckpt_dict["num_kpts"], inference_factory - ) + self.model = build_dlc_transformer(cfg, ckpt_dict["feature_dim"], ckpt_dict["num_kpts"], inference_factory) self.cos = nn.CosineSimilarity(dim=1, eps=1e-6) diff --git a/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py b/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py index c036503dc7..0d73b4a4a3 100644 --- a/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py +++ b/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -""" Vision Transformer (ViT) in PyTorch +"""Vision Transformer (ViT) in PyTorch A PyTorch implement of Vision Transformers as described in 'An Image Is Worth 16 x 16 Words: Transformers for Image Recognition at Scale' - https://arxiv.org/abs/2010.11929 @@ -30,6 +30,7 @@ Hacked together by / Copyright 2020 Ross Wightman """ + import math from functools import partial @@ -51,9 +52,7 @@ def drop_path(x, drop_prob: float = 0.0, training: bool = False): if drop_prob == 0.0 or not training: return x keep_prob = 1 - drop_prob - shape = (x.shape[0],) + (1,) * ( - x.ndim - 1 - ) # work with diff dim tensors, not just 2D ConvNets + shape = (x.shape[0],) + (1,) * (x.ndim - 1) # work with diff dim tensors, not just 2D ConvNets random_tensor = keep_prob + torch.rand(shape, dtype=x.dtype, device=x.device) random_tensor.floor_() # binarize output = x.div(keep_prob) * random_tensor @@ -120,11 +119,7 @@ def __init__( def forward(self, x): B, N, C = x.shape - qkv = ( - self.qkv(x) - .reshape(B, N, 3, self.num_heads, C // self.num_heads) - .permute(2, 0, 3, 1, 4) - ) + qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4) q, k, v = ( qkv[0], qkv[1], @@ -273,9 +268,7 @@ def get_classifier(self): def reset_classifier(self, num_classes, global_pool=""): self.num_classes = num_classes - self.fc = ( - nn.Linear(self.embed_dim, num_classes) if num_classes > 0 else nn.Identity() - ) + self.fc = nn.Linear(self.embed_dim, num_classes) if num_classes > 0 else nn.Identity() def forward_features(self, x): # x: inputs @@ -330,9 +323,7 @@ def load_param(self, model_path): if "distilled" in model_path: print("distill need to choose right cls token in the pth") v = torch.cat([v[:, 0:1], v[:, 2:]], dim=1) - v = resize_pos_embed( - v, self.pos_embed, self.patch_embed.num_y, self.patch_embed.num_x - ) + v = resize_pos_embed(v, self.pos_embed, self.patch_embed.num_y, self.patch_embed.num_x) try: self.state_dict()[k].copy_(v) except: diff --git a/deeplabcut/pose_tracking_pytorch/processor/processor.py b/deeplabcut/pose_tracking_pytorch/processor/processor.py index 622c8b44fa..26d0635f3d 100644 --- a/deeplabcut/pose_tracking_pytorch/processor/processor.py +++ b/deeplabcut/pose_tracking_pytorch/processor/processor.py @@ -195,9 +195,7 @@ def do_dlc_train( plot_dict["test_acc"] = test_acc_list plot_dict["epochs"] = epoch_list - with open( - os.path.join(ckpt_folder, "dlc_transreid_results.pickle"), "wb" - ) as handle: + with open(os.path.join(ckpt_folder, "dlc_transreid_results.pickle"), "wb") as handle: pickle.dump(plot_dict, handle, protocol=pickle.HIGHEST_PROTOCOL) @@ -255,8 +253,8 @@ def do_dlc_inference(cfg, model, triplet_loss, val_loader, num_query): np.save(f, features_list) with open("labels.npy", "wb") as f: np.save(f, labels_list) - print(f"validation loss {val_loss/len(val_loader)}") - print(f" acc {total_correct/total_n}") + print(f"validation loss {val_loss / len(val_loader)}") + print(f" acc {total_correct / total_n}") logger.info("Validation Results ") @@ -293,5 +291,5 @@ def do_dlc_pair_inference(cfg, model, val_loader, num_query): total_n += vec1_feat.shape[0] total_correct += calc_cos_correct(vec1_feat, gt1, vec2_feat, gt2) - print(f" acc {total_correct/total_n}") + print(f" acc {total_correct / total_n}") logger.info("Validation Results ") diff --git a/deeplabcut/pose_tracking_pytorch/solver/cosine_lr.py b/deeplabcut/pose_tracking_pytorch/solver/cosine_lr.py index f61d248ea0..e27d67a700 100644 --- a/deeplabcut/pose_tracking_pytorch/solver/cosine_lr.py +++ b/deeplabcut/pose_tracking_pytorch/solver/cosine_lr.py @@ -8,12 +8,13 @@ # # Licensed under GNU Lesser General Public License v3.0 # -""" Cosine Scheduler +"""Cosine Scheduler Cosine LR schedule with warmup, cycle/restarts, noise. Hacked together by / Copyright 2020 Ross Wightman """ + import logging import math import torch @@ -78,9 +79,7 @@ def __init__( self.warmup_prefix = warmup_prefix self.t_in_epochs = t_in_epochs if self.warmup_t: - self.warmup_steps = [ - (v - warmup_lr_init) / self.warmup_t for v in self.base_values - ] + self.warmup_steps = [(v - warmup_lr_init) / self.warmup_t for v in self.base_values] super().update_groups(self.warmup_lr_init) else: self.warmup_steps = [1 for _ in self.base_values] @@ -93,9 +92,7 @@ def _get_lr(self, t): t = t - self.warmup_t if self.t_mul != 1: - i = math.floor( - math.log(1 - t / self.t_initial * (1 - self.t_mul), self.t_mul) - ) + i = math.floor(math.log(1 - t / self.t_initial * (1 - self.t_mul), self.t_mul)) t_i = self.t_mul**i * self.t_initial t_curr = t - (1 - self.t_mul**i) / (1 - self.t_mul) * self.t_initial else: @@ -109,8 +106,7 @@ def _get_lr(self, t): if self.cycle_limit == 0 or (self.cycle_limit > 0 and i < self.cycle_limit): lrs = [ - lr_min - + 0.5 * (lr_max - lr_min) * (1 + math.cos(math.pi * t_curr / t_i)) + lr_min + 0.5 * (lr_max - lr_min) * (1 + math.cos(math.pi * t_curr / t_i)) for lr_max in lr_max_values ] else: @@ -137,8 +133,4 @@ def get_cycle_length(self, cycles=0): if self.t_mul == 1.0: return self.t_initial * cycles else: - return int( - math.floor( - -self.t_initial * (self.t_mul**cycles - 1) / (1 - self.t_mul) - ) - ) + return int(math.floor(-self.t_initial * (self.t_mul**cycles - 1) / (1 - self.t_mul))) diff --git a/deeplabcut/pose_tracking_pytorch/solver/make_optimizer.py b/deeplabcut/pose_tracking_pytorch/solver/make_optimizer.py index e0582b395d..076ea071b7 100644 --- a/deeplabcut/pose_tracking_pytorch/solver/make_optimizer.py +++ b/deeplabcut/pose_tracking_pytorch/solver/make_optimizer.py @@ -29,13 +29,9 @@ def make_easy_optimizer(cfg, model): params += [{"params": [value], "lr": lr, "weight_decay": weight_decay}] optimizer_name = cfg["optimizer_name"] if optimizer_name == "SGD": - optimizer = getattr(torch.optim, optimizer_name)( - params, momentum=cfg["momentum"] - ) + optimizer = getattr(torch.optim, optimizer_name)(params, momentum=cfg["momentum"]) elif optimizer_name == "AdamW": - optimizer = torch.optim.AdamW( - params, lr=cfg["base_lr"], weight_decay=cfg["weight_decay"] - ) + optimizer = torch.optim.AdamW(params, lr=cfg["base_lr"], weight_decay=cfg["weight_decay"]) else: optimizer = getattr(torch.optim, optimizer_name)(params) diff --git a/deeplabcut/pose_tracking_pytorch/solver/scheduler.py b/deeplabcut/pose_tracking_pytorch/solver/scheduler.py index 699f882b88..91d6a9915b 100644 --- a/deeplabcut/pose_tracking_pytorch/solver/scheduler.py +++ b/deeplabcut/pose_tracking_pytorch/solver/scheduler.py @@ -49,22 +49,13 @@ def __init__( if initialize: for i, group in enumerate(self.optimizer.param_groups): if param_group_field not in group: - raise KeyError( - f"{param_group_field} missing from param_groups[{i}]" - ) - group.setdefault( - self._initial_param_group_field, group[param_group_field] - ) + raise KeyError(f"{param_group_field} missing from param_groups[{i}]") + group.setdefault(self._initial_param_group_field, group[param_group_field]) else: for i, group in enumerate(self.optimizer.param_groups): if self._initial_param_group_field not in group: - raise KeyError( - f"{self._initial_param_group_field} missing from param_groups[{i}]" - ) - self.base_values = [ - group[self._initial_param_group_field] - for group in self.optimizer.param_groups - ] + raise KeyError(f"{self._initial_param_group_field} missing from param_groups[{i}]") + self.base_values = [group[self._initial_param_group_field] for group in self.optimizer.param_groups] self.metric = None # any point to having this for all? self.noise_range_t = noise_range_t self.noise_pct = noise_pct @@ -74,9 +65,7 @@ def __init__( self.update_groups(self.base_values) def state_dict(self) -> Dict[str, Any]: - return { - key: value for key, value in self.__dict__.items() if key != "optimizer" - } + return {key: value for key, value in self.__dict__.items() if key != "optimizer"} def load_state_dict(self, state_dict: Dict[str, Any]) -> None: self.__dict__.update(state_dict) @@ -123,8 +112,6 @@ def _add_noise(self, lrs, t): if abs(noise) < self.noise_pct: break else: - noise = ( - 2 * (torch.rand(1, generator=g).item() - 0.5) * self.noise_pct - ) + noise = 2 * (torch.rand(1, generator=g).item() - 0.5) * self.noise_pct lrs = [v + v * noise for v in lrs] return lrs diff --git a/deeplabcut/pose_tracking_pytorch/solver/scheduler_factory.py b/deeplabcut/pose_tracking_pytorch/solver/scheduler_factory.py index fcbd8270a7..c341180f87 100644 --- a/deeplabcut/pose_tracking_pytorch/solver/scheduler_factory.py +++ b/deeplabcut/pose_tracking_pytorch/solver/scheduler_factory.py @@ -16,9 +16,10 @@ # Hacked together by / Copyright 2020 Ross Wightman # https://github.com/rwightman/pytorch-image-models/blob/main/timm/scheduler/scheduler_factory.py # -""" Scheduler Factory +"""Scheduler Factory Hacked together by / Copyright 2020 Ross Wightman """ + from .cosine_lr import CosineLRScheduler diff --git a/deeplabcut/pose_tracking_pytorch/tracking_utils/reranking.py b/deeplabcut/pose_tracking_pytorch/tracking_utils/reranking.py index 957be31efe..d4b117f6f4 100644 --- a/deeplabcut/pose_tracking_pytorch/tracking_utils/reranking.py +++ b/deeplabcut/pose_tracking_pytorch/tracking_utils/reranking.py @@ -12,9 +12,7 @@ import torch -def re_ranking( - probFea, galFea, k1, k2, lambda_value, local_distmat=None, only_local=False -): +def re_ranking(probFea, galFea, k1, k2, lambda_value, local_distmat=None, only_local=False): """ probFea: all feature vectors of the query set (torch tensor) @@ -58,20 +56,16 @@ def re_ranking( k_reciprocal_expansion_index = k_reciprocal_index for j in range(len(k_reciprocal_index)): candidate = k_reciprocal_index[j] - candidate_forward_k_neigh_index = initial_rank[ - candidate, : int(np.around(k1 / 2)) + 1 - ] + candidate_forward_k_neigh_index = initial_rank[candidate, : int(np.around(k1 / 2)) + 1] candidate_backward_k_neigh_index = initial_rank[ candidate_forward_k_neigh_index, : int(np.around(k1 / 2)) + 1 ] fi_candidate = np.where(candidate_backward_k_neigh_index == candidate)[0] candidate_k_reciprocal_index = candidate_forward_k_neigh_index[fi_candidate] - if len( - np.intersect1d(candidate_k_reciprocal_index, k_reciprocal_index) - ) > 2 / 3 * len(candidate_k_reciprocal_index): - k_reciprocal_expansion_index = np.append( - k_reciprocal_expansion_index, candidate_k_reciprocal_index - ) + if len(np.intersect1d(candidate_k_reciprocal_index, k_reciprocal_index)) > 2 / 3 * len( + candidate_k_reciprocal_index + ): + k_reciprocal_expansion_index = np.append(k_reciprocal_expansion_index, candidate_k_reciprocal_index) k_reciprocal_expansion_index = np.unique(k_reciprocal_expansion_index) weight = np.exp(-original_dist[i, k_reciprocal_expansion_index]) diff --git a/deeplabcut/pose_tracking_pytorch/train_dlctransreid.py b/deeplabcut/pose_tracking_pytorch/train_dlctransreid.py index 88bad6a31c..d92a9ed1fe 100644 --- a/deeplabcut/pose_tracking_pytorch/train_dlctransreid.py +++ b/deeplabcut/pose_tracking_pytorch/train_dlctransreid.py @@ -14,9 +14,7 @@ try: import torch except ModuleNotFoundError: - raise ModuleNotFoundError( - "Unsupervised identity learning requires PyTorch. Please run `pip install torch`." - ) + raise ModuleNotFoundError("Unsupervised identity learning requires PyTorch. Please run `pip install torch`.") import numpy as np import os import glob diff --git a/deeplabcut/post_processing/analyze_skeleton.py b/deeplabcut/post_processing/analyze_skeleton.py index 7d8ab2fe92..559e4a767b 100644 --- a/deeplabcut/post_processing/analyze_skeleton.py +++ b/deeplabcut/post_processing/analyze_skeleton.py @@ -116,17 +116,10 @@ def calc_angle_between_vectors_of_points_2d(v1, v2): """ # Check data format - if ( - v1 is None - or v2 is None - or not isinstance(v1, np.ndarray) - or not isinstance(v2, np.ndarray) - ): + if v1 is None or v2 is None or not isinstance(v1, np.ndarray) or not isinstance(v2, np.ndarray): raise ValueError("Invalid format for input arguments") if len(v1) != len(v2): - raise ValueError( - "Input arrays should have the same length, instead: ", len(v1), len(v2) - ) + raise ValueError("Input arrays should have the same length, instead: ", len(v1), len(v2)) if not v1.shape[0] == 2 or not v2.shape[0] == 2: raise ValueError("Invalid shape for input arrays: ", v1.shape, v2.shape) @@ -160,9 +153,7 @@ def analyzebone(bp1, bp2): likelihood = np.min(likelihoods, 1) # Create dataframe and return - df = pd.DataFrame.from_dict( - dict(length=bone_length, orientation=bone_orientation, likelihood=likelihood) - ) + df = pd.DataFrame.from_dict(dict(length=bone_length, orientation=bone_orientation, likelihood=likelihood)) # df.index.name=name return df diff --git a/deeplabcut/post_processing/filtering.py b/deeplabcut/post_processing/filtering.py index 414f580ca6..cc29570789 100644 --- a/deeplabcut/post_processing/filtering.py +++ b/deeplabcut/post_processing/filtering.py @@ -46,9 +46,7 @@ def columnwise_spline_interp(data, max_gap=0): x = np.arange(nrows) for i in range(ncols): mask = valid[:, i] - if ( - np.sum(mask) > 3 - ): # Make sure there are enough points to fit the cubic spline + if np.sum(mask) > 3: # Make sure there are enough points to fit the cubic spline spl = CubicSpline(x[mask], temp[mask, i]) y = spl(x) if max_gap > 0: @@ -233,9 +231,7 @@ def filterpredictions( vname = Path(video).stem try: - df, filepath, _, _ = auxiliaryfunctions.load_analyzed_data( - destfolder, vname, DLCscorer, True, track_method - ) + df, filepath, _, _ = auxiliaryfunctions.load_analyzed_data(destfolder, vname, DLCscorer, True, track_method) print(f"Data from {vname} were already filtered. Skipping...") video_to_filtered_df[video] = df # Data has been filtered so continue to the next video @@ -259,12 +255,8 @@ def filterpredictions( placeholder = np.empty_like(temp) for i in range(temp.shape[1]): x, y, p = temp[:, i].T - meanx, _ = FitSARIMAXModel( - x, p, p_bound, alpha, ARdegree, MAdegree, False - ) - meany, _ = FitSARIMAXModel( - y, p, p_bound, alpha, ARdegree, MAdegree, False - ) + meanx, _ = FitSARIMAXModel(x, p, p_bound, alpha, ARdegree, MAdegree, False) + meany, _ = FitSARIMAXModel(y, p, p_bound, alpha, ARdegree, MAdegree, False) meanx[0] = x[0] meany[0] = y[0] placeholder[:, i] = np.c_[meanx, meany, p] @@ -276,9 +268,7 @@ def filterpredictions( elif filtertype == "median": data = df.copy() mask = data.columns.get_level_values("coords") != "likelihood" - data.loc[:, mask] = df.loc[:, mask].apply( - signal.medfilt, args=(windowlength,), axis=0 - ) + data.loc[:, mask] = df.loc[:, mask].apply(signal.medfilt, args=(windowlength,), axis=0) elif filtertype == "spline": data = df.copy() mask_data = data.columns.get_level_values("coords").isin(("x", "y")) diff --git a/deeplabcut/refine_training_dataset/outlier_frames.py b/deeplabcut/refine_training_dataset/outlier_frames.py index cf81be3e11..20ad322472 100644 --- a/deeplabcut/refine_training_dataset/outlier_frames.py +++ b/deeplabcut/refine_training_dataset/outlier_frames.py @@ -120,9 +120,7 @@ def find_outliers_in_raw_data( ) -def find_outliers_in_raw_detections( - pickled_data, algo="uncertain", threshold=0.1, kept_keypoints=None -): +def find_outliers_in_raw_detections(pickled_data, algo="uncertain", threshold=0.1, kept_keypoints=None): """ Find outlier frames from the raw detections of multiple animals. @@ -176,10 +174,7 @@ def get_frame_ind(s): return candidates, data -def _read_video_specific_cropping_margins( - config: str | Path | dict, - video_path: str | Path -) -> tuple[int, int]: +def _read_video_specific_cropping_margins(config: str | Path | dict, video_path: str | Path) -> tuple[int, int]: if isinstance(config, (str, Path)): config = auxiliaryfunctions.read_config(config) output_crop = config["video_sets"].get(str(video_path), {}).get("crop") @@ -394,9 +389,7 @@ def extract_outlier_frames( """ cfg = auxiliaryfunctions.read_config(config) - bodyparts = auxiliaryfunctions.intersection_of_body_parts_and_ones_given_by_user( - cfg, comparisonbodyparts - ) + bodyparts = auxiliaryfunctions.intersection_of_body_parts_and_ones_given_by_user(cfg, comparisonbodyparts) if not len(bodyparts): raise ValueError("No valid bodyparts were selected.") @@ -425,9 +418,7 @@ def extract_outlier_frames( df, dataname, _, _ = auxiliaryfunctions.load_analyzed_data( videofolder, vname, DLCscorer, track_method=track_method ) - metadata = auxiliaryfunctions.load_video_metadata( - videofolder, vname, DLCscorer - ) + metadata = auxiliaryfunctions.load_video_metadata(videofolder, vname, DLCscorer) nframes = len(df) startindex = max([int(np.floor(nframes * cfg["start"])), 0]) stopindex = min([int(np.ceil(nframes * cfg["stop"])), nframes]) @@ -456,16 +447,11 @@ def extract_outlier_frames( ind = df_temp.index[(sum_ > epsilon**2).any(axis=1)].tolist() Indices.extend(ind) elif outlieralgorithm == "fitting": - d, o = compute_deviations( - df_temp, dataname, p_bound, alpha, ARdegree, MAdegree - ) + d, o = compute_deviations(df_temp, dataname, p_bound, alpha, ARdegree, MAdegree) # Some heuristics for extracting frames based on distance: - ind = np.flatnonzero( - d > epsilon - ) # time points with at least average difference of epsilon + ind = np.flatnonzero(d > epsilon) # time points with at least average difference of epsilon if ( - len(ind) < cfg["numframes2pick"] * 2 - and len(d) > cfg["numframes2pick"] * 2 + len(ind) < cfg["numframes2pick"] * 2 and len(d) > cfg["numframes2pick"] * 2 ): # if too few points qualify, extract the most distant ones. ind = np.argsort(d)[::-1][: cfg["numframes2pick"] * 2] Indices.extend(ind) @@ -479,9 +465,7 @@ def extract_outlier_frames( coords=None, ) if added_video: - project_video_path = ( - Path(cfg["project_path"]) / "videos" / Path(video).name - ) + project_video_path = Path(cfg["project_path"]) / "videos" / Path(video).name _ = launch_napari([project_video_path, dataname]) return @@ -496,9 +480,7 @@ def extract_outlier_frames( raise Indices.extend(frames2use) else: - raise ValueError( - 'Expected list of frames2use for outlieralgorithm "list"!' - ) + raise ValueError('Expected list of frames2use for outlieralgorithm "list"!') else: raise ValueError(f"outlieralgorithm {outlieralgorithm} not recognized!") @@ -534,12 +516,7 @@ def extract_outlier_frames( else: askuser = "Ja" - if ( - askuser == "y" - or askuser == "yes" - or askuser == "Ja" - or askuser == "ha" - ): # multilanguage support :) + if askuser == "y" or askuser == "yes" or askuser == "Ja" or askuser == "ha": # multilanguage support :) # Now extract from those Indices! ExtractFramesbasedonPreselection( Indices, @@ -555,9 +532,7 @@ def extract_outlier_frames( copy_videos=copy_videos, ) else: - print( - "Nothing extracted, please change the parameters and start again..." - ) + print("Nothing extracted, please change the parameters and start again...") except FileNotFoundError as e: print(e) print( @@ -597,9 +572,7 @@ def FitSARIMAXModel(x, p, pcutoff, alpha, ARdegree, MAdegree, nforecast=0, disp= # mod = sm.tsa.ARIMA(Y, order=(ARdegree,0,MAdegree)) #order=(ARdegree,0,MAdegree) try: res = mod.fit(disp=disp) - except ( - ValueError - ): # https://groups.google.com/forum/#!topic/pystatsmodels/S_Fo53F25Rk (let's update to statsmodels 0.10.0 soon...) + except ValueError: # https://groups.google.com/forum/#!topic/pystatsmodels/S_Fo53F25Rk (let's update to statsmodels 0.10.0 soon...) startvalues = np.array([convertparms2start(pn) for pn in mod.param_names]) res = mod.fit(start_params=startvalues, disp=disp) except np.linalg.LinAlgError: @@ -622,9 +595,7 @@ def FitSARIMAXModel(x, p, pcutoff, alpha, ARdegree, MAdegree, nforecast=0, disp= return np.nan * np.zeros(len(Y)), np.nan * np.zeros((len(Y), 2)) -def compute_deviations( - Dataframe, dataname, p_bound, alpha, ARdegree, MAdegree, storeoutput=None -): +def compute_deviations(Dataframe, dataname, p_bound, alpha, ARdegree, MAdegree, storeoutput=None): """Fits Seasonal AutoRegressive Integrated Moving Average with eXogenous regressors model to data and computes confidence interval as well as mean fit.""" @@ -638,9 +609,7 @@ def compute_deviations( meanx, CIx = FitSARIMAXModel(x, p, p_bound, alpha, ARdegree, MAdegree) meany, CIy = FitSARIMAXModel(y, p, p_bound, alpha, ARdegree, MAdegree) distance = np.sqrt((x - meanx) ** 2 + (y - meany) ** 2) - significant = ( - (x < CIx[:, 0]) + (x > CIx[:, 1]) + (y < CIy[:, 0]) + (y > CIy[:, 1]) - ) + significant = (x < CIx[:, 0]) + (x > CIx[:, 1]) + (y < CIy[:, 0]) + (y > CIy[:, 1]) preds.append(np.c_[distance, significant, meanx, meany, CIx, CIy]) columns = Dataframe.columns @@ -745,9 +714,7 @@ def ExtractFramesbasedonPreselection( start = cfg["start"] stop = cfg["stop"] numframes2extract = cfg["numframes2pick"] - bodyparts = auxiliaryfunctions.intersection_of_body_parts_and_ones_given_by_user( - cfg, "all" - ) + bodyparts = auxiliaryfunctions.intersection_of_body_parts_and_ones_given_by_user(cfg, "all") videofolder = str(Path(video).parents[0]) vname = str(Path(video).stem) @@ -784,9 +751,7 @@ def ExtractFramesbasedonPreselection( if opencv: if coords is not None: vid.set_bbox(*coords) - frames2pick = frameselectiontools.UniformFramescv2( - vid, numframes2extract, start, stop, Index - ) + frames2pick = frameselectiontools.UniformFramescv2(vid, numframes2extract, start, stop, Index) else: if coords is not None: clip = clip.crop( @@ -795,9 +760,7 @@ def ExtractFramesbasedonPreselection( x1=coords[0], x2=coords[1], ) - frames2pick = frameselectiontools.UniformFrames( - clip, numframes2extract, start, stop, Index - ) + frames2pick = frameselectiontools.UniformFrames(clip, numframes2extract, start, stop, Index) elif extractionalgorithm == "kmeans": if opencv: if coords is not None: @@ -830,9 +793,7 @@ def ExtractFramesbasedonPreselection( ) else: - print( - "Please implement this method yourself! Currently the options are 'kmeans', 'jump', 'uniform'." - ) + print("Please implement this method yourself! Currently the options are 'kmeans', 'jump', 'uniform'.") frames2pick = [] # Extract frames + frames with plotted labels and store them in folder (with name derived from video name) nder labeled-data @@ -889,9 +850,7 @@ def ExtractFramesbasedonPreselection( pass if with_annotations: - machinefile = os.path.join( - tmpfolder, "machinelabels-iter" + str(cfg["iteration"]) + ".h5" - ) + machinefile = os.path.join(tmpfolder, "machinelabels-iter" + str(cfg["iteration"]) + ".h5") if isinstance(data, pd.DataFrame): df = data.loc[frames2pick] df.index = pd.MultiIndex.from_tuples( @@ -915,9 +874,7 @@ def ExtractFramesbasedonPreselection( for index in frames2pick ] ) - filename = os.path.join( - str(tmpfolder), f"CollectedData_{cfg['scorer']}.h5" - ) + filename = os.path.join(str(tmpfolder), f"CollectedData_{cfg['scorer']}.h5") try: df_temp = pd.read_hdf(filename, "df_with_missing") columns = df_temp.columns @@ -962,9 +919,7 @@ def ExtractFramesbasedonPreselection( conversioncode.guarantee_multiindex_rows(Data) DataCombined = pd.concat([Data, df]) # drop duplicate labels: - DataCombined = DataCombined[ - ~DataCombined.index.duplicated(keep="first") - ] + DataCombined = DataCombined[~DataCombined.index.duplicated(keep="first")] DataCombined.to_hdf(machinefile, key="df_with_missing", mode="w") DataCombined.to_csv( @@ -974,13 +929,8 @@ def ExtractFramesbasedonPreselection( df.to_hdf(machinefile, key="df_with_missing", mode="w") df.to_csv(os.path.join(tmpfolder, "machinelabels.csv")) - print( - r"The outlier frames are extracted. They are stored in the subdirectory labeled-data\%s." - % vname - ) - print( - "Once you extracted frames for all videos, use 'refine_labels' to manually correct the labels." - ) + print(r"The outlier frames are extracted. They are stored in the subdirectory labeled-data\%s." % vname) + print("Once you extracted frames for all videos, use 'refine_labels' to manually correct the labels.") else: print("No frames were extracted.") @@ -1002,13 +952,9 @@ def PlottingSingleFrame( from skimage import io imagename1 = os.path.join(tmpfolder, "img" + str(index).zfill(strwidth) + ".png") - imagename2 = os.path.join( - tmpfolder, "img" + str(index).zfill(strwidth) + "labeled.png" - ) + imagename2 = os.path.join(tmpfolder, "img" + str(index).zfill(strwidth) + "labeled.png") - if not os.path.isfile( - os.path.join(tmpfolder, "img" + str(index).zfill(strwidth) + ".png") - ): + if not os.path.isfile(os.path.join(tmpfolder, "img" + str(index).zfill(strwidth) + ".png")): plt.axis("off") image = img_as_ubyte(clip.get_frame(index * 1.0 / clip.fps)) io.imsave(imagename1, image) @@ -1021,9 +967,7 @@ def PlottingSingleFrame( bpts = Dataframe.columns.get_level_values("bodyparts") all_bpts = bpts.values[::3] - df_x, df_y, df_likelihood = Dataframe.values.reshape( - (Dataframe.shape[0], -1, 3) - ).T + df_x, df_y, df_likelihood = Dataframe.values.reshape((Dataframe.shape[0], -1, 3)).T bplist = bpts.unique().to_list() if Dataframe.columns.nlevels == 3: map2bp = list(range(len(all_bpts))) @@ -1069,13 +1013,9 @@ def PlottingSingleFramecv2( from skimage import io imagename1 = os.path.join(tmpfolder, "img" + str(index).zfill(strwidth) + ".png") - imagename2 = os.path.join( - tmpfolder, "img" + str(index).zfill(strwidth) + "labeled.png" - ) + imagename2 = os.path.join(tmpfolder, "img" + str(index).zfill(strwidth) + "labeled.png") - if not os.path.isfile( - os.path.join(tmpfolder, "img" + str(index).zfill(strwidth) + ".png") - ): + if not os.path.isfile(os.path.join(tmpfolder, "img" + str(index).zfill(strwidth) + ".png")): plt.axis("off") cap.set_to_frame(index) frame = cap.read_frame(crop=True) @@ -1093,9 +1033,7 @@ def PlottingSingleFramecv2( bpts = Dataframe.columns.get_level_values("bodyparts") all_bpts = bpts.values[::3] - df_x, df_y, df_likelihood = Dataframe.values.reshape( - (Dataframe.shape[0], -1, 3) - ).T + df_x, df_y, df_likelihood = Dataframe.values.reshape((Dataframe.shape[0], -1, 3)).T bplist = bpts.unique().to_list() if Dataframe.columns.nlevels == 3: map2bp = list(range(len(all_bpts))) @@ -1153,15 +1091,11 @@ def merge_datasets(config, forceiterate=None): bf = Path(str(config_path / "labeled-data")) allfolders = [ - os.path.join(bf, fn) - for fn in os.listdir(bf) - if "_labeled" not in fn and not fn.startswith(".") + os.path.join(bf, fn) for fn in os.listdir(bf) if "_labeled" not in fn and not fn.startswith(".") ] # exclude labeled data folders and temporary files flagged = False for findex, folder in enumerate(allfolders): - if os.path.isfile( - os.path.join(folder, "MachineLabelsRefine.h5") - ): # Folder that was manually refine... + if os.path.isfile(os.path.join(folder, "MachineLabelsRefine.h5")): # Folder that was manually refine... pass elif os.path.isfile( os.path.join(folder, "CollectedData_" + cfg["scorer"] + ".h5") @@ -1182,14 +1116,8 @@ def merge_datasets(config, forceiterate=None): auxiliaryfunctions.write_config(config, cfg) - print( - "Merged data sets and updated refinement iteration to " - + str(cfg["iteration"]) - + "." - ) - print( - "Now you can create a new training set for the expanded annotated images (use create_training_dataset)." - ) + print("Merged data sets and updated refinement iteration to " + str(cfg["iteration"]) + ".") + print("Now you can create a new training set for the expanded annotated images (use create_training_dataset).") else: print("Please label, or remove the un-corrected folders.") diff --git a/deeplabcut/refine_training_dataset/stitch.py b/deeplabcut/refine_training_dataset/stitch.py index 8496d3ed42..0aa76b484f 100644 --- a/deeplabcut/refine_training_dataset/stitch.py +++ b/deeplabcut/refine_training_dataset/stitch.py @@ -56,9 +56,7 @@ def __init__(self, data, inds): raise ValueError("Data must of shape (nframes, nbodyparts, 3 or 4)") if data.shape[0] != len(inds): - raise ValueError( - "Data and corresponding indices must have the same length." - ) + raise ValueError("Data and corresponding indices must have the same length.") self.data = data.astype(np.float64) self.inds = np.array(inds) @@ -102,10 +100,7 @@ def __contains__(self, other_tracklet): return np.isin(self.inds, other_tracklet.inds, assume_unique=True).any() def __repr__(self): - return ( - f"Tracklet of length {len(self)} from {self.start} to {self.end} " - f"with reliability {self.likelihood:.3f}" - ) + return f"Tracklet of length {len(self)} from {self.start} to {self.end} with reliability {self.likelihood:.3f}" @property def xy(self): @@ -125,9 +120,7 @@ def centroid(self): return self._centroid def _update_centroid(self): - like = ( - self.data[..., 2:3] + 1e-10 - ) # Avoid division by zero in very uncertain tracklets + like = self.data[..., 2:3] + 1e-10 # Avoid division by zero in very uncertain tracklets self._centroid = np.nansum(self.xy * like, axis=1) / np.nansum(like, axis=1) @property @@ -214,15 +207,9 @@ def calc_velocity(self, where="head", norm=True): speed rather than a 2D vector. """ if where == "tail": - vel = ( - np.diff(self.centroid[:3], axis=0) - / np.diff(self.inds[:3])[:, np.newaxis] - ) + vel = np.diff(self.centroid[:3], axis=0) / np.diff(self.inds[:3])[:, np.newaxis] elif where == "head": - vel = ( - np.diff(self.centroid[-3:], axis=0) - / np.diff(self.inds[-3:])[:, np.newaxis] - ) + vel = np.diff(self.centroid[-3:], axis=0) / np.diff(self.inds[-3:])[:, np.newaxis] else: raise ValueError(f"Unknown where={where}") if norm: @@ -273,13 +260,9 @@ def distance_to(self, other_tracklet): ) return np.sqrt(np.sum(dist**2, axis=1)).mean() elif self < other_tracklet: - return np.sqrt( - np.sum((self.centroid[-1] - other_tracklet.centroid[0]) ** 2) - ) + return np.sqrt(np.sum((self.centroid[-1] - other_tracklet.centroid[0]) ** 2)) else: - return np.sqrt( - np.sum((self.centroid[0] - other_tracklet.centroid[-1]) ** 2) - ) + return np.sqrt(np.sum((self.centroid[0] - other_tracklet.centroid[-1]) ** 2)) def motion_affinity_with(self, other_tracklet): """ @@ -292,15 +275,11 @@ def motion_affinity_with(self, other_tracklet): if time_gap > 0: if self < other_tracklet: d1 = self.centroid[-1] + time_gap * self.calc_velocity(norm=False) - d2 = other_tracklet.centroid[ - 0 - ] - time_gap * other_tracklet.calc_velocity("tail", False) + d2 = other_tracklet.centroid[0] - time_gap * other_tracklet.calc_velocity("tail", False) delta1 = other_tracklet.centroid[0] - d1 delta2 = self.centroid[-1] - d2 else: - d1 = other_tracklet.centroid[ - -1 - ] + time_gap * other_tracklet.calc_velocity(norm=False) + d1 = other_tracklet.centroid[-1] + time_gap * other_tracklet.calc_velocity(norm=False) d2 = self.centroid[0] - time_gap * self.calc_velocity("tail", False) delta1 = self.centroid[0] - d1 delta2 = other_tracklet.centroid[-1] - d2 @@ -500,13 +479,8 @@ def __init__( # Map each Tracklet to an entry and output nodes and vice versa, # which is convenient once the tracklets are stitched. - self._mapping = { - tracklet: {"in": f"{i}in", "out": f"{i}out"} - for i, tracklet in enumerate(self) - } - self._mapping_inv = { - label: k for k, v in self._mapping.items() for label in v.values() - } + self._mapping = {tracklet: {"in": f"{i}in", "out": f"{i}out"} for i, tracklet in enumerate(self)} + self._mapping_inv = {label: k for k, v in self._mapping.items() for label in v.values()} # Store tracklets and corresponding negatives (those that overlap in time) self._lu_overlap = defaultdict(list) @@ -532,9 +506,7 @@ def from_pickle( ): with open(pickle_file, "rb") as file: tracklets = pickle.load(file) - class_ = cls.from_dict_of_dict( - tracklets, n_tracks, min_length, split_tracklets, prestitch_residuals - ) + class_ = cls.from_dict_of_dict(tracklets, n_tracks, min_length, split_tracklets, prestitch_residuals) class_.filename = pickle_file return class_ @@ -567,9 +539,7 @@ def from_dict_of_dict( single = tracklet else: tracklets.append(Tracklet(data, inds)) - class_ = cls( - tracklets, n_tracks, min_length, split_tracklets, prestitch_residuals - ) + class_ = cls(tracklets, n_tracks, min_length, split_tracklets, prestitch_residuals) class_.header = header class_.single = single return class_ @@ -626,9 +596,7 @@ def mine(self, n_samples): if not overlapping_tracklets: continue # Pick the closest (spatially) overlapping tracklet - ind_min = np.argmin( - [tracklet.distance_to(t) for t in overlapping_tracklets] - ) + ind_min = np.argmin([tracklet.distance_to(t) for t in overlapping_tracklets]) overlapping_tracklet = overlapping_tracklets[ind_min] common_inds = set(tracklet.inds).intersection(overlapping_tracklet.inds) ind_anchor = np.random.choice(list(common_inds)) @@ -657,9 +625,7 @@ def build_graph( self.G = nx.DiGraph() self.G.add_node("source", demand=-self.n_tracks) self.G.add_node("sink", demand=self.n_tracks) - nodes_in, nodes_out = zip( - *[v.values() for k, v in self._mapping.items() if k in nodes] - ) + nodes_in, nodes_out = zip(*[v.values() for k, v in self._mapping.items() if k in nodes]) self.G.add_nodes_from(nodes_in, demand=1) self.G.add_nodes_from(nodes_out, demand=-1) self.G.add_edges_from(zip(nodes_in, nodes_out), capacity=1) @@ -706,43 +672,25 @@ def stitch(self, add_back_residuals=True): warnings.warn("No optimal solution found. Employing black magic...") # Let us prune the graph by removing all source and sink edges # but those connecting the `n_tracks` first and last tracklets. - in_to_keep = [ - self._mapping[first_tracklet]["in"] - for first_tracklet in self._first_tracklets - ] - out_to_keep = [ - self._mapping[last_tracklet]["out"] - for last_tracklet in self._last_tracklets - ] - in_to_remove = set( - node for _, node in self.G.out_edges("source") - ).difference(in_to_keep) - out_to_remove = set(node for node, _ in self.G.in_edges("sink")).difference( - out_to_keep - ) + in_to_keep = [self._mapping[first_tracklet]["in"] for first_tracklet in self._first_tracklets] + out_to_keep = [self._mapping[last_tracklet]["out"] for last_tracklet in self._last_tracklets] + in_to_remove = set(node for _, node in self.G.out_edges("source")).difference(in_to_keep) + out_to_remove = set(node for node, _ in self.G.in_edges("sink")).difference(out_to_keep) self.G.remove_edges_from(zip(["source"] * len(in_to_remove), in_to_remove)) self.G.remove_edges_from(zip(out_to_remove, ["sink"] * len(out_to_remove))) # Preflow push seems to work slightly better than shortest # augmentation path..., and is more computationally efficient. paths = [] - for path in nx.node_disjoint_paths( - self.G, "source", "sink", preflow_push, self.n_tracks - ): + for path in nx.node_disjoint_paths(self.G, "source", "sink", preflow_push, self.n_tracks): temp = set() for node in path[1:-1]: self.G.remove_node(node) temp.add(self._mapping_inv[node]) paths.append(list(temp)) incomplete_tracks = self.n_tracks - len(paths) - remaining_nodes = set( - self._mapping_inv[node] - for node in self.G - if node not in ("source", "sink") - ) + remaining_nodes = set(self._mapping_inv[node] for node in self.G if node not in ("source", "sink")) if len(remaining_nodes) > 0: - if ( - incomplete_tracks == 1 - ): # All remaining nodes must belong to the same track + if incomplete_tracks == 1: # All remaining nodes must belong to the same track # Verify whether there are overlapping tracklets for t1, t2 in combinations(remaining_nodes, 2): if t1 in t2: @@ -780,9 +728,7 @@ def stitch(self, add_back_residuals=True): finally: if self.paths is None: - raise ValueError( - f"Could not reconstruct {self.n_tracks} tracks from the tracklets given." - ) + raise ValueError(f"Could not reconstruct {self.n_tracks} tracks from the tracklets given.") self.tracks = np.asarray([sum(path) for path in self.paths if path]) if add_back_residuals: @@ -796,9 +742,7 @@ def _finalize_tracks(self): n_max = len(residuals) while n_attemps < n_max and residuals: for res in residuals[::-1]: - easy_fit = [ - i for i, track in enumerate(self.tracks) if res not in track - ] + easy_fit = [i for i, track in enumerate(self.tracks) if res not in track] if not easy_fit: residuals.remove(res) continue @@ -830,9 +774,7 @@ def _finalize_tracks(self): elif right_gap <= 3: dist = np.linalg.norm(track.centroid[e] - c1[1]) else: - dist = np.linalg.norm(track.centroid[s] - c1[0]) + np.linalg.norm( - track.centroid[e] - c1[1] - ) + dist = np.linalg.norm(track.centroid[s] - c1[0]) + np.linalg.norm(track.centroid[e] - c1[1]) dists.append((n, dist)) if not dists: continue @@ -924,15 +866,11 @@ def format_df(self, animal_names=None): [scorer, ["single"], bpts[-n_unique_bpts:], coords], names=["scorer", "individuals", "bodyparts", "coords"], ) - df2 = pd.DataFrame( - self.single.flat_data, columns=columns, index=self.single.inds - ) + df2 = pd.DataFrame(self.single.flat_data, columns=columns, index=self.single.inds) df = df.join(df2, how="outer") return df - def write_tracks( - self, output_name="", suffix="", animal_names=None, save_as_csv=False - ): + def write_tracks(self, output_name="", suffix="", animal_names=None, save_as_csv=False): df = self.format_df(animal_names) if not output_name: if suffix: @@ -1214,9 +1152,7 @@ def trans_weight_func(tracklet1, tracklet2, nframe, feature_dict): deeplabcut.utils.auxiliaryfunctions.attempt_to_make_folder(dest) vname = Path(video).stem - feature_dict_path = os.path.join( - dest, vname + DLCscorer + "_bpt_features.pickle" - ) + feature_dict_path = os.path.join(dest, vname + DLCscorer + "_bpt_features.pickle") # should only exist one if transformer_checkpoint: import dbm @@ -1224,9 +1160,7 @@ def trans_weight_func(tracklet1, tracklet2, nframe, feature_dict): try: feature_dict = shelve.open(feature_dict_path, flag="r") except dbm.error: - raise FileNotFoundError( - f"{feature_dict_path} does not exist. Did you run transformer_reID()?" - ) + raise FileNotFoundError(f"{feature_dict_path} does not exist. Did you run transformer_reID()?") dataname = os.path.join(dest, vname + DLCscorer + ".h5") @@ -1246,9 +1180,7 @@ def weight_func(t1, t2): if transformer_checkpoint: stitcher.build_graph( max_gap=max_gap, - weight_func=partial( - trans_weight_func, nframe=nframe, feature_dict=feature_dict - ), + weight_func=partial(trans_weight_func, nframe=nframe, feature_dict=feature_dict), ) else: stitcher.build_graph(max_gap=max_gap, weight_func=weight_func) diff --git a/deeplabcut/refine_training_dataset/tracklets.py b/deeplabcut/refine_training_dataset/tracklets.py index df8bf2ac20..f4d6a4e639 100644 --- a/deeplabcut/refine_training_dataset/tracklets.py +++ b/deeplabcut/refine_training_dataset/tracklets.py @@ -72,15 +72,11 @@ def _load_tracklets(self, tracklets, auto_fill): header = tracklets.pop("header") self.scorer = header.get_level_values("scorer").unique().to_list() bodyparts = header.get_level_values("bodyparts") - bodyparts_multi = [ - bp for bp in self.cfg["multianimalbodyparts"] if bp in bodyparts - ] + bodyparts_multi = [bp for bp in self.cfg["multianimalbodyparts"] if bp in bodyparts] bodyparts_single = self.cfg["uniquebodyparts"] mask_multi = bodyparts.isin(bodyparts_multi) mask_single = bodyparts.isin(bodyparts_single) - self.bodyparts = list(bodyparts[mask_multi]) * self.nindividuals + list( - bodyparts[mask_single] - ) + self.bodyparts = list(bodyparts[mask_multi]) * self.nindividuals + list(bodyparts[mask_single]) # Sort tracklets by length to prioritize greater continuity temp = sorted(tracklets.values(), key=len) @@ -105,9 +101,7 @@ def _load_tracklets(self, tracklets, auto_fill): np.nan, np.float16, ) - tracklets_single = np.full( - (self.nframes, len(bodyparts_single) * 3), np.nan, np.float16 - ) + tracklets_single = np.full((self.nframes, len(bodyparts_single) * 3), np.nan, np.float16) for _ in trange(len(tracklets_sorted)): tracklet = tracklets_sorted.pop() inds, temp = zip(*[(get_frame_ind(k), v) for k, v in tracklet.items()]) @@ -126,15 +120,11 @@ def _load_tracklets(self, tracklets, auto_fill): overwrite = has_data & ~is_free if overwrite.any(): rows, cols = np.nonzero(overwrite) - more_confident = ( - data_single[overwrite] > tracklets_single[inds[rows], cols] - )[2::3] + more_confident = (data_single[overwrite] > tracklets_single[inds[rows], cols])[2::3] idx = np.flatnonzero(more_confident) for i in idx: sl = slice(i * 3, i * 3 + 3) - tracklets_single[inds[rows[sl]], cols[sl]] = data_single[ - rows[sl], cols[sl] - ] + tracklets_single[inds[rows[sl]], cols[sl]] = data_single[rows[sl], cols[sl]] else: is_free = np.isnan(tracklets_multi[:, inds]) data_multi = data[:, mask_multi] @@ -149,21 +139,15 @@ def _load_tracklets(self, tracklets, auto_fill): current_mask = mask[ind] rows, cols = np.nonzero(current_mask) if rows.size: - tracklets_multi[ind, inds[rows], cols] = data_multi[ - current_mask - ] + tracklets_multi[ind, inds[rows], cols] = data_multi[current_mask] is_free[ind, current_mask] = False has_data[current_mask] = False if has_data.any(): # For the remaining data, overwrite where we are least confident remaining = data_multi[has_data].reshape((-1, 3)) - mask3d = np.broadcast_to( - has_data, (self.nindividuals,) + has_data.shape - ) + mask3d = np.broadcast_to(has_data, (self.nindividuals,) + has_data.shape) dims, rows, cols = np.nonzero(mask3d) - temp = tracklets_multi[dims, inds[rows], cols].reshape( - (self.nindividuals, -1, 3) - ) + temp = tracklets_multi[dims, inds[rows], cols].reshape((self.nindividuals, -1, 3)) diff = remaining - temp # Find keypoints closest to the remaining data # Use Manhattan distance to avoid overflow @@ -176,9 +160,7 @@ def _load_tracklets(self, tracklets, auto_fill): rows, cols = np.nonzero(has_data) for i, j in zip(idx, better): sl = slice(j * 3, j * 3 + 3) - tracklets_multi[i, inds[rows[sl]], cols[sl]] = ( - remaining.flat[sl] - ) + tracklets_multi[i, inds[rows[sl]], cols[sl]] = remaining.flat[sl] else: rows, cols = np.nonzero(has_data) n = np.argmin(overwrite_risk) @@ -207,12 +189,10 @@ def _load_tracklets(self, tracklets, auto_fill): self.prob = self.data[:, :, 2] # Map a tracklet # to the animal ID it belongs to or the bodypart # it corresponds to. - self.individuals = self.cfg["individuals"] + ( - ["single"] if len(self.cfg["uniquebodyparts"]) else [] - ) - self.tracklet2id = [ - i for i in range(0, self.nindividuals) for _ in bodyparts_multi - ] + [self.nindividuals] * len(bodyparts_single) + self.individuals = self.cfg["individuals"] + (["single"] if len(self.cfg["uniquebodyparts"]) else []) + self.tracklet2id = [i for i in range(0, self.nindividuals) for _ in bodyparts_multi] + [ + self.nindividuals + ] * len(bodyparts_single) bps = bodyparts_multi + bodyparts_single map_ = dict(zip(bps, range(len(bps)))) self.tracklet2bp = [map_[bp] for bp in self.bodyparts[::3]] @@ -227,11 +207,7 @@ def _load_tracklets(self, tracklets, auto_fill): for frame, data in tracklet.items(): i = get_frame_ind(frame) tracklets_raw[n, i] = data - self.data = ( - tracklets_raw.swapaxes(0, 1) - .reshape((self.nframes, -1, 3)) - .swapaxes(0, 1) - ) + self.data = tracklets_raw.swapaxes(0, 1).reshape((self.nframes, -1, 3)).swapaxes(0, 1) self.xy = self.data[:, :, :2] self.prob = self.data[:, :, 2] self.tracklet2id = self.tracklet2bp = [0] * self.data.shape[0] @@ -276,13 +252,9 @@ def load_tracklets_from_hdf(self, filename): self.prob = self.data[:, :, 2] individuals = idx.get_level_values("individuals") self.individuals = individuals.unique().to_list() - self.tracklet2id = individuals.map( - dict(zip(self.individuals, range(len(self.individuals)))) - ).tolist()[::3] + self.tracklet2id = individuals.map(dict(zip(self.individuals, range(len(self.individuals))))).tolist()[::3] bodyparts = self.bodyparts.unique() - self.tracklet2bp = self.bodyparts.map( - dict(zip(bodyparts, range(len(bodyparts)))) - ).tolist()[::3] + self.tracklet2bp = self.bodyparts.map(dict(zip(bodyparts, range(len(bodyparts))))).tolist()[::3] self._label_pairs = list(idx.droplevel(["scorer", "coords"]).unique()) self._xy = self.xy.copy() @@ -305,12 +277,8 @@ def get_non_nan_elements(self, at): return data[mask], mask, np.flatnonzero(mask) def swap_tracklets(self, track1, track2, inds): - self.xy[np.ix_([track1, track2], inds)] = self.xy[ - np.ix_([track2, track1], inds) - ] - self.prob[np.ix_([track1, track2], inds)] = self.prob[ - np.ix_([track2, track1], inds) - ] + self.xy[np.ix_([track1, track2], inds)] = self.xy[np.ix_([track2, track1], inds)] + self.prob[np.ix_([track1, track2], inds)] = self.prob[np.ix_([track2, track1], inds)] self.tracklet2bp[track1], self.tracklet2bp[track2] = ( self.tracklet2bp[track2], self.tracklet2bp[track1], @@ -318,12 +286,8 @@ def swap_tracklets(self, track1, track2, inds): def find_swapping_bodypart_pairs(self, force_find=False): if not self.swapping_pairs or force_find: - sub = ( - self.xy[:, np.newaxis] - self.xy - ) # Broadcasting for efficient subtraction of X and Y coordinates - with np.errstate( - invalid="ignore" - ): # Get rid of annoying warnings when comparing with NaNs + sub = self.xy[:, np.newaxis] - self.xy # Broadcasting for efficient subtraction of X and Y coordinates + with np.errstate(invalid="ignore"): # Get rid of annoying warnings when comparing with NaNs pos = sub > 0 neg = sub <= 0 down = neg[:, :, 1:] & pos[:, :, :-1] diff --git a/deeplabcut/utils/auxfun_models.py b/deeplabcut/utils/auxfun_models.py index b8c46e1f9c..05341c6ab5 100644 --- a/deeplabcut/utils/auxfun_models.py +++ b/deeplabcut/utils/auxfun_models.py @@ -80,9 +80,7 @@ def download_weights(modeltype, model_path): from io import BytesIO target_dir = model_path.parents[0] - neturls = auxiliaryfunctions.read_plainconfig( - target_dir / "pretrained_model_urls.yaml" - ) + neturls = auxiliaryfunctions.read_plainconfig(target_dir / "pretrained_model_urls.yaml") try: if "efficientnet" in modeltype: url = neturls["efficientnet"] @@ -134,11 +132,7 @@ def tarfilenamecutting(tarf): if modelname in neturls.keys(): url = neturls[modelname] response = urllib.request.urlopen(url) - print( - "Downloading the model from the DeepLabCut server @Harvard -> Go Crimson!!! {}....".format( - url - ) - ) + print("Downloading the model from the DeepLabCut server @Harvard -> Go Crimson!!! {}....".format(url)) total_size = int(response.getheader("Content-Length")) pbar = tqdm(unit="B", total=total_size, position=0) filename, _ = urllib.request.urlretrieve(url, reporthook=show_progress) @@ -146,11 +140,7 @@ def tarfilenamecutting(tarf): tar.extractall(target_dir, members=tarfilenamecutting(tar)) else: models = [ - fn - for fn in neturls.keys() - if "resnet_" not in fn - and "efficientnet" not in fn - and "mobilenet_" not in fn + fn for fn in neturls.keys() if "resnet_" not in fn and "efficientnet" not in fn and "mobilenet_" not in fn ] print("Model does not exist: ", modelname) print("Pick one of the following: ", models) diff --git a/deeplabcut/utils/auxfun_multianimal.py b/deeplabcut/utils/auxfun_multianimal.py index ef4fa7257e..506897f0e5 100644 --- a/deeplabcut/utils/auxfun_multianimal.py +++ b/deeplabcut/utils/auxfun_multianimal.py @@ -73,10 +73,7 @@ def get_track_method(cfg, track_method=""): if track_method != "": # check if it exists: if track_method not in TRACK_METHODS: - raise ValueError( - f"Invalid tracking method. Only {', '.join(TRACK_METHODS)} are " - "currently supported." - ) + raise ValueError(f"Invalid tracking method. Only {', '.join(TRACK_METHODS)} are currently supported.") return track_method else: # default track_method = cfg.get("default_track_method", "") @@ -86,9 +83,7 @@ def get_track_method(cfg, track_method=""): ) track_method = "ellipse" cfg["default_track_method"] = track_method - auxiliaryfunctions.write_config( - str(Path(cfg["project_path"]) / "config.yaml"), cfg - ) + auxiliaryfunctions.write_config(str(Path(cfg["project_path"]) / "config.yaml"), cfg) return track_method else: # no tracker for single-animal projects @@ -122,7 +117,7 @@ def validate_paf_graph(cfg, paf_graph): unconnected = set(range(len(multianimalbodyparts))).difference(connected) if unconnected and len(multianimalbodyparts) > 1: # for single bpt not important! raise ValueError( - f'Unconnected {", ".join(multianimalbodyparts[i] for i in unconnected)}. ' + f"Unconnected {', '.join(multianimalbodyparts[i] for i in unconnected)}. " f"For multi-animal projects, all multianimalbodyparts should be connected. " f"Ideally there should be at least one (multinode) path from each multianimalbodyparts to each other multianimalbodyparts. " ) @@ -130,9 +125,7 @@ def validate_paf_graph(cfg, paf_graph): def prune_paf_graph(list_of_edges, desired_n_edges=None, average_degree=None): if not (desired_n_edges or average_degree): - raise ValueError( - "Either `desired_n_edges` or `average_degree` must be specified." - ) + raise ValueError("Either `desired_n_edges` or `average_degree` must be specified.") G = nx.Graph(list_of_edges) n_edges = len(G.edges) @@ -161,9 +154,7 @@ def getpafgraph(cfg, printnames=True): Convention: multianimalbodyparts go first! """ - individuals, uniquebodyparts, multianimalbodyparts = extractindividualsandbodyparts( - cfg - ) + individuals, uniquebodyparts, multianimalbodyparts = extractindividualsandbodyparts(cfg) # Attention this order has to be consistent (for training set creation, training, inference etc.) bodypartnames = multianimalbodyparts + uniquebodyparts @@ -191,9 +182,7 @@ def getpafgraph(cfg, printnames=True): def graph2names(cfg, partaffinityfield_graph): - individuals, uniquebodyparts, multianimalbodyparts = extractindividualsandbodyparts( - cfg - ) + individuals, uniquebodyparts, multianimalbodyparts = extractindividualsandbodyparts(cfg) bodypartnames = multianimalbodyparts + uniquebodyparts for pair in partaffinityfield_graph: print(pair, bodypartnames[pair[0]], bodypartnames[pair[1]]) @@ -233,9 +222,7 @@ def returnlabelingdata(config): for folder in folders: print("Do you want to get the data for folder:", folder, "?") askuser = input("yes/no") - if ( - askuser == "y" or askuser == "yes" or askuser == "Ja" or askuser == "ha" - ): # multilanguage support :) + if askuser == "y" or askuser == "yes" or askuser == "Ja" or askuser == "ha": # multilanguage support :) fn = os.path.join(str(folder), "CollectedData_" + cfg["scorer"] + ".h5") Data = pd.read_hdf(fn) return Data @@ -275,9 +262,7 @@ def convert2_maDLC(config, userfeedback=True, forceindividual=None): video_names = [trainingsetmanipulation._robust_path_split(i)[1] for i in videos] folders = [Path(config).parent / "labeled-data" / Path(i) for i in video_names] - individuals, uniquebodyparts, multianimalbodyparts = extractindividualsandbodyparts( - cfg - ) + individuals, uniquebodyparts, multianimalbodyparts = extractindividualsandbodyparts(cfg) if forceindividual is None: if len(individuals) == 0: @@ -289,9 +274,7 @@ def convert2_maDLC(config, userfeedback=True, forceindividual=None): if forceindividual == "single": # no specific individual () if len(multianimalbodyparts) > 0: # there should be an individual name... - print( - "At least one individual should exist beyond 'single', as there are multianimalbodyparts..." - ) + print("At least one individual should exist beyond 'single', as there are multianimalbodyparts...") folders = [] for folder in folders: @@ -301,9 +284,7 @@ def convert2_maDLC(config, userfeedback=True, forceindividual=None): else: askuser = "yes" - if ( - askuser == "y" or askuser == "yes" or askuser == "Ja" or askuser == "ha" - ): # multilanguage support :) + if askuser == "y" or askuser == "yes" or askuser == "Ja" or askuser == "ha": # multilanguage support :) fn = os.path.join(str(folder), "CollectedData_" + cfg["scorer"]) Data = pd.read_hdf(fn + ".h5") conversioncode.guarantee_multiindex_rows(Data) @@ -314,16 +295,12 @@ def convert2_maDLC(config, userfeedback=True, forceindividual=None): # -> adding (single,bpt) for uniquebodyparts for j, bpt in enumerate(uniquebodyparts): index = pd.MultiIndex.from_arrays( - np.array( - [2 * [cfg["scorer"]], 2 * ["single"], 2 * [bpt], ["x", "y"]] - ), + np.array([2 * [cfg["scorer"]], 2 * ["single"], 2 * [bpt], ["x", "y"]]), names=["scorer", "individuals", "bodyparts", "coords"], ) if bpt in Data[cfg["scorer"]].keys(): - frame = pd.DataFrame( - Data[cfg["scorer"]][bpt].values, columns=index, index=imindex - ) + frame = pd.DataFrame(Data[cfg["scorer"]][bpt].values, columns=index, index=imindex) else: frame = pd.DataFrame( np.ones((len(imindex), 2)) * np.nan, @@ -354,9 +331,7 @@ def convert2_maDLC(config, userfeedback=True, forceindividual=None): ) if bpt in Data[cfg["scorer"]].keys(): - frame = pd.DataFrame( - Data[cfg["scorer"]][bpt].values, columns=index, index=imindex - ) + frame = pd.DataFrame(Data[cfg["scorer"]][bpt].values, columns=index, index=imindex) else: frame = pd.DataFrame( np.ones((len(imindex), 2)) * np.nan, @@ -386,9 +361,7 @@ def convert_single2multiplelegacyAM(config, userfeedback=True, target=None): video_names = [Path(i).stem for i in videos] folders = [Path(config).parent / "labeled-data" / Path(i) for i in video_names] - prefixes, uniquebodyparts, multianimalbodyparts = extractindividualsandbodyparts( - cfg - ) + prefixes, uniquebodyparts, multianimalbodyparts = extractindividualsandbodyparts(cfg) for folder in folders: if userfeedback == True: print("Do you want to convert the annotation file in folder:", folder, "?") @@ -396,17 +369,13 @@ def convert_single2multiplelegacyAM(config, userfeedback=True, target=None): else: askuser = "yes" - if ( - askuser == "y" or askuser == "yes" or askuser == "Ja" or askuser == "ha" - ): # multilanguage support :) + if askuser == "y" or askuser == "yes" or askuser == "Ja" or askuser == "ha": # multilanguage support :) fn = os.path.join(str(folder), "CollectedData_" + cfg["scorer"]) Data = pd.read_hdf(fn + ".h5") conversioncode.guarantee_multiindex_rows(Data) imindex = Data.index - if "individuals" in Data.columns.names and ( - target is None or target == "single" - ): + if "individuals" in Data.columns.names and (target is None or target == "single"): print("This is a multianimal data set, converting to single...", folder) for prfxindex, prefix in enumerate(prefixes): if prefix == "single": @@ -456,9 +425,7 @@ def convert_single2multiplelegacyAM(config, userfeedback=True, target=None): ) DataFrame.to_csv(fn + ".csv") elif target is None or target == "multi": - print( - "This is a single animal data set, converting to multi...", folder - ) + print("This is a single animal data set, converting to multi...", folder) for prfxindex, prefix in enumerate(prefixes): if prefix == "single": if cfg["uniquebodyparts"] != [None]: @@ -552,9 +519,7 @@ def form_default_inferencecfg(cfg): os.path.join(auxiliaryfunctions.get_deeplabcut_path(), "inference_cfg.yaml") ) # set project specific parameters: - inferencecfg["minimalnumberofconnections"] = ( - len(cfg["multianimalbodyparts"]) / 2 - ) # reasonable default + inferencecfg["minimalnumberofconnections"] = len(cfg["multianimalbodyparts"]) / 2 # reasonable default inferencecfg["topktoretain"] = len(cfg["individuals"]) return inferencecfg @@ -563,7 +528,7 @@ def check_inferencecfg_sanity(cfg, inferencecfg): template = form_default_inferencecfg(cfg) missing = [key for key in template if key not in inferencecfg] if missing: - raise KeyError(f'Keys {", ".join(missing)} are missing in the inferencecfg.') + raise KeyError(f"Keys {', '.join(missing)} are missing in the inferencecfg.") def read_inferencecfg(path_inference_config, cfg): @@ -572,7 +537,5 @@ def read_inferencecfg(path_inference_config, cfg): inferencecfg = auxiliaryfunctions.read_plainconfig(str(path_inference_config)) except FileNotFoundError: inferencecfg = form_default_inferencecfg(cfg) - auxiliaryfunctions.write_plainconfig( - str(path_inference_config), dict(inferencecfg) - ) + auxiliaryfunctions.write_plainconfig(str(path_inference_config), dict(inferencecfg)) return inferencecfg diff --git a/deeplabcut/utils/auxfun_videos.py b/deeplabcut/utils/auxfun_videos.py index 6a937d1263..60efcc1c98 100644 --- a/deeplabcut/utils/auxfun_videos.py +++ b/deeplabcut/utils/auxfun_videos.py @@ -66,9 +66,7 @@ def check_integrity_robust(self): while fr < numframes: success, frame = self.video.read() if not success or frame is None: - warnings.warn( - f"Opencv failed to load frame {fr}. Use ffmpeg to re-encode video file" - ) + warnings.warn(f"Opencv failed to load frame {fr}. Use ffmpeg to re-encode video file") fr += 1 @property @@ -85,9 +83,7 @@ def directory(self): @property def metadata(self): - return dict( - n_frames=len(self), fps=self.fps, width=self.width, height=self.height - ) + return dict(n_frames=len(self), fps=self.fps, width=self.width, height=self.height) def get_n_frames(self, robust=False): if not robust: @@ -98,21 +94,14 @@ def get_n_frames(self, robust=False): f"-select_streams v:0 -show_entries stream=nb_read_frames " f"-of default=nokey=1:noprint_wrappers=1" ) - output = subprocess.check_output( - command, shell=True, stderr=subprocess.STDOUT - ) + output = subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT) self._n_frames_robust = int(output) return self._n_frames_robust def calc_duration(self, robust=False): if robust: - command = ( - f'ffprobe -i "{self.video_path}" -show_entries ' - f'format=duration -v quiet -of csv="p=0"' - ) - output = subprocess.check_output( - command, shell=True, stderr=subprocess.STDOUT - ) + command = f'ffprobe -i "{self.video_path}" -show_entries format=duration -v quiet -of csv="p=0"' + output = subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT) return float(output) return len(self) / self.fps @@ -121,10 +110,7 @@ def set_to_frame(self, ind): raise ValueError("Index must be a positive integer.") last_frame = len(self) - 1 if ind > last_frame: - warnings.warn( - "Index exceeds the total number of frames. " - "Setting to last frame instead." - ) + warnings.warn("Index exceeds the total number of frames. Setting to last frame instead.") ind = last_frame self.video.set(cv2.CAP_PROP_POS_FRAMES, ind) @@ -161,9 +147,7 @@ def get_bbox(self, relative=False): def set_bbox(self, x1, x2, y1, y2, relative=False): if x2 <= x1 or y2 <= y1: - raise ValueError( - f"Coordinates look wrong... " f"Ensure {x1} < {x2} and {y1} < {y2}." - ) + raise ValueError(f"Coordinates look wrong... Ensure {x1} < {x2} and {y1} < {y2}.") if not relative: x1 /= self._width x2 /= self._width @@ -171,9 +155,7 @@ def set_bbox(self, x1, x2, y1, y2, relative=False): y2 /= self._height bbox = x1, x2, y1, y2 if any(coord > 1 for coord in bbox): - warnings.warn( - "Bounding box larger than the video... " "Clipping to video dimensions." - ) + warnings.warn("Bounding box larger than the video... Clipping to video dimensions.") bbox = tuple(map(lambda x: min(x, 1), bbox)) self._bbox = bbox @@ -204,9 +186,7 @@ def dimensions(self): def parse_metadata(self): self._n_frames = int(self.video.get(cv2.CAP_PROP_FRAME_COUNT)) if self._n_frames >= 1e9: - warnings.warn( - "The video has more than 10^9 frames, we recommend chopping it up." - ) + warnings.warn("The video has more than 10^9 frames, we recommend chopping it up.") self._width = int(self.video.get(cv2.CAP_PROP_FRAME_WIDTH)) self._height = int(self.video.get(cv2.CAP_PROP_FRAME_HEIGHT)) self._fps = round(self.video.get(cv2.CAP_PROP_FPS), 2) @@ -223,9 +203,7 @@ def __init__(self, video_path, codec="h264", dpi=100, fps=None): if fps: self.fps = fps - def shorten( - self, start, end, suffix="short", dest_folder=None, validate_inputs=True - ): + def shorten(self, start, end, suffix="short", dest_folder=None, validate_inputs=True): """ Shorten the video from start to end. @@ -251,10 +229,7 @@ def shorten( def validate_timestamp(stamp): if not isinstance(stamp, str): - raise ValueError( - "Timestamp should be a string formatted " - "as hours:minutes:seconds." - ) + raise ValueError("Timestamp should be a string formatted as hours:minutes:seconds.") time = datetime.datetime.strptime(stamp, "%H:%M:%S").time() # The above already raises a ValueError if formatting is wrong seconds = (time.hour * 60 + time.minute) * 60 + time.second @@ -266,10 +241,7 @@ def validate_timestamp(stamp): validate_timestamp(stamp) output_path = self.make_output_path(suffix, dest_folder) - command = ( - f'ffmpeg -n -i "{self.video_path}" -ss {start} -to {end} ' - f'-c:a copy "{output_path}"' - ) + command = f'ffmpeg -n -i "{self.video_path}" -ss {start} -to {end} -c:a copy "{output_path}"' subprocess.call(command, shell=True) return output_path @@ -327,9 +299,9 @@ def rotate(self, angle, rotatecw="Arbitrary", suffix="rotated", dest_folder=None command = f'ffmpeg -n -i "{self.video_path}" -vf ' if rotatecw == "Arbitrary": angle = np.deg2rad(angle) - command += f'rotate={angle} ' + command += f"rotate={angle} " elif rotatecw == "Yes": - command += 'transpose=1 ' + command += "transpose=1 " else: raise ValueError("Unknown rotation direction.") @@ -347,10 +319,7 @@ def rescale( dest_folder=None, ): output_path = self.make_output_path(suffix, dest_folder) - command = ( - f'ffmpeg -n -i "{self.video_path}" -filter:v ' - f'"scale={width}:{height}{{}}" -c:a copy "{output_path}"' - ) + command = f'ffmpeg -n -i "{self.video_path}" -filter:v "scale={width}:{height}{{}}" -c:a copy "{output_path}"' # Rotate, see: https://stackoverflow.com/questions/3937387/rotating-videos-with-ffmpeg # interesting option to just update metadata. if rotatecw == "Arbitrary": @@ -392,9 +361,7 @@ def imread(image_path, mode="skimage"): return img_as_ubyte(image) elif mode == "cv2": - return cv2.imread(image_path, cv2.IMREAD_UNCHANGED)[ - ..., ::-1 - ] # ~10% faster than using cv2.cvtColor + return cv2.imread(image_path, cv2.IMREAD_UNCHANGED)[..., ::-1] # ~10% faster than using cv2.cvtColor # https://docs.opencv.org/3.4.0/da/d54/group__imgproc__transform.html#ga5bb5a1fea74ea38e1a5445ca803ff121 @@ -407,9 +374,7 @@ def imresize(img, size=1.0, interpolationmethod=cv2.INTER_AREA): return img -def ShortenVideo( - vname, start="00:00:01", stop="00:01:00", outsuffix="short", outpath=None -): +def ShortenVideo(vname, start="00:00:01", stop="00:01:00", outsuffix="short", outpath=None): """ Auxiliary function to shorten video and output with outsuffix appended. to the same folder from start (hours:minutes:seconds) to stop (hours:minutes:seconds). @@ -504,9 +469,7 @@ def CropVideo( writer = VideoWriter(vname) if useGUI: - print( - "Please, select your coordinates (draw from top left to bottom right ...)" - ) + print("Please, select your coordinates (draw from top left to bottom right ...)") coords = draw_bbox(vname) if not coords: diff --git a/deeplabcut/utils/auxiliaryfunctions.py b/deeplabcut/utils/auxiliaryfunctions.py index 7910b968ac..0a93303b3f 100644 --- a/deeplabcut/utils/auxiliaryfunctions.py +++ b/deeplabcut/utils/auxiliaryfunctions.py @@ -17,6 +17,7 @@ https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS Licensed under GNU Lesser General Public License v3.0 """ + from __future__ import annotations import os @@ -224,10 +225,7 @@ def read_config(configname): write_config(configname, cfg) except Exception as err: if len(err.args) > 2: - if ( - err.args[2] - == "could not determine a constructor for the tag '!!python/tuple'" - ): + if err.args[2] == "could not determine a constructor for the tag '!!python/tuple'": with open(path, "r") as ymlfile: cfg = yaml.load(ymlfile, Loader=yaml.SafeLoader) write_config(configname, cfg) @@ -246,9 +244,7 @@ def write_config(configname, cfg): Write structured config file. """ with open(configname, "w") as cf: - cfg_file, ruamelFile = create_config_template( - cfg.get("multianimalproject", False) - ) + cfg_file, ruamelFile = create_config_template(cfg.get("multianimalproject", False)) for key in cfg.keys(): cfg_file[key] = cfg[key] @@ -296,10 +292,7 @@ def edit_config(configname, edits, output_name=""): try: write_plainconfig(output_name, cfg) except ruamel.yaml.representer.RepresenterError: - warnings.warn( - "Some edits could not be written. " - "The configuration file will be left unchanged." - ) + warnings.warn("Some edits could not be written. The configuration file will be left unchanged.") for key in edits: cfg.pop(key) write_plainconfig(output_name, cfg) @@ -360,9 +353,7 @@ def write_config_3d_template(projconfigfile, cfg_file_3d, ruamelFile_3d): def read_plainconfig(configname): if not os.path.exists(configname): - raise FileNotFoundError( - f"Config {configname} is not found. Please make sure that the file exists." - ) + raise FileNotFoundError(f"Config {configname} is not found. Please make sure that the file exists.") with open(configname) as file: return YAML().load(file) @@ -377,9 +368,7 @@ def attempt_to_make_folder(foldername, recursive=False): try: os.path.isdir(foldername) except TypeError: # https://www.python.org/dev/peps/pep-0519/ - foldername = os.fspath( - foldername - ) # https://github.com/DeepLabCut/DeepLabCut/issues/105 (windows) + foldername = os.fspath(foldername) # https://github.com/DeepLabCut/DeepLabCut/issues/105 (windows) if os.path.isdir(foldername): pass @@ -443,9 +432,7 @@ def get_list_of_videos( if in_random_order: from random import shuffle - shuffle( - videos - ) # this is useful so multiple nets can be used to analyze simultaneously + shuffle(videos) # this is useful so multiple nets can be used to analyze simultaneously else: videos.sort() @@ -481,9 +468,7 @@ def save_data(PredicteData, metadata, dataname, pdindex, imagenames, save_as_csv def save_metadata(metadatafilename, data, trainIndices, testIndices, trainFraction): with open(metadatafilename, "wb") as f: # Pickle the 'labeled-data' dictionary using the highest protocol available. - pickle.dump( - [data, trainIndices, testIndices, trainFraction], f, pickle.HIGHEST_PROTOCOL - ) + pickle.dump([data, trainIndices, testIndices, trainFraction], f, pickle.HIGHEST_PROTOCOL) def load_metadata(metadatafile): @@ -499,9 +484,7 @@ def load_metadata(metadatafile): def get_immediate_subdirectories(a_dir): """Get list of immediate subdirectories""" - return [ - name for name in os.listdir(a_dir) if os.path.isdir(os.path.join(a_dir, name)) - ] + return [name for name in os.listdir(a_dir) if os.path.isdir(os.path.join(a_dir, name))] def grab_files_in_folder(folder, ext="", relative=True): @@ -544,14 +527,8 @@ def filter_files_by_patterns( file for file in folder.iterdir() if file.is_file() - and ( - not start_patterns - or any(file.name.startswith(start) for start in start_patterns) - ) - and ( - not contain_patterns - or any(contain in file.name for contain in contain_patterns) - ) + and (not start_patterns or any(file.name.startswith(start) for start in start_patterns)) + and (not contain_patterns or any(contain in file.name for contain in contain_patterns)) and (not end_patterns or any(file.name.endswith(end) for end in end_patterns)) ] @@ -578,9 +555,7 @@ def get_training_set_folder(cfg: dict) -> Path: Task = cfg["Task"] date = cfg["date"] iterate = "iteration-" + str(cfg["iteration"]) - return Path( - os.path.join("training-datasets", iterate, "UnaugmentedDataSet_" + Task + date) - ) + return Path(os.path.join("training-datasets", iterate, "UnaugmentedDataSet_" + Task + date)) def get_data_and_metadata_filenames(trainingsetfolder, trainFraction, shuffle, cfg): @@ -597,13 +572,7 @@ def get_data_and_metadata_filenames(trainingsetfolder, trainFraction, shuffle, c ) datafn = os.path.join( str(trainingsetfolder), - cfg["Task"] - + "_" - + cfg["scorer"] - + str(int(100 * trainFraction)) - + "shuffle" - + str(shuffle) - + ".mat", + cfg["Task"] + "_" + cfg["scorer"] + str(int(100 * trainFraction)) + "shuffle" + str(shuffle) + ".mat", ) return datafn, metadatafn @@ -682,12 +651,7 @@ def get_evaluation_folder( modelprefix, eval_prefix, iterate, - Task - + date - + "-trainset" - + str(int(trainFraction * 100)) - + "shuffle" - + str(shuffle), + Task + date + "-trainset" + str(int(trainFraction * 100)) + "shuffle" + str(shuffle), ) @@ -699,9 +663,7 @@ def get_snapshots_from_folder(train_folder: Path) -> List[str]: Raises: FileNotFoundError: if no snapshot_names are found in the train_folder. """ - snapshot_names = [ - file.stem for file in train_folder.iterdir() if "index" in file.name - ] + snapshot_names = [file.stem for file in train_folder.iterdir() if "index" in file.name] if len(snapshot_names) == 0: raise FileNotFoundError( @@ -798,12 +760,8 @@ def get_scorer_name( date = cfg["date"] if trainingsiterations == "unknown": - snapshotindex = get_snapshot_index_for_scorer( - "snapshotindex", cfg["snapshotindex"] - ) - model_folder = get_model_folder( - trainFraction, shuffle, cfg, engine=engine, modelprefix=modelprefix - ) + snapshotindex = get_snapshot_index_for_scorer("snapshotindex", cfg["snapshotindex"]) + model_folder = get_model_folder(trainFraction, shuffle, cfg, engine=engine, modelprefix=modelprefix) train_folder = Path(cfg["project_path"]) / model_folder / "train" snapshot_names = get_snapshots_from_folder(train_folder) snapshot_name = snapshot_names[snapshotindex] @@ -812,11 +770,7 @@ def get_scorer_name( dlc_cfg = read_plainconfig( os.path.join( cfg["project_path"], - str( - get_model_folder( - trainFraction, shuffle, cfg, engine=engine, modelprefix=modelprefix - ) - ), + str(get_model_folder(trainFraction, shuffle, cfg, engine=engine, modelprefix=modelprefix)), "train", engine.pose_cfg_name, ) @@ -834,26 +788,14 @@ def get_scorer_name( else: raise ValueError(f"Failed to abbreviate network name: {dlc_cfg['net_type']}") - scorer = ( - "DLC_" - + netname - + "_" - + Task - + str(date) - + "shuffle" - + str(shuffle) - + "_" - + str(trainingsiterations) - ) + scorer = "DLC_" + netname + "_" + Task + str(date) + "shuffle" + str(shuffle) + "_" + str(trainingsiterations) # legacy scorername until DLC 2.1. (cfg['resnet'] is deprecated / which is why we get the resnet_xyz name from dlc_cfg! # scorer_legacy = 'DeepCut' + "_resnet" + str(cfg['resnet']) + "_" + Task + str(date) + 'shuffle' + str(shuffle) + '_' + str(trainingsiterations) scorer_legacy = scorer.replace("DLC", "DeepCut") return scorer, scorer_legacy -def check_if_post_processing( - folder, vname, DLCscorer, DLCscorerlegacy, suffix="filtered" -): +def check_if_post_processing(folder, vname, DLCscorer, DLCscorerlegacy, suffix="filtered"): """Checks if filtered/bone lengths were already calculated. If not, figures out if data was already analyzed (either with legacy scorer name or new one!)""" outdataname = os.path.join(folder, vname + DLCscorer + suffix + ".h5") @@ -936,16 +878,13 @@ def find_video_full_data(folder, videoname, scorer): end_patterns={"pickle"}, ) if not full_files: - raise FileNotFoundError( - f"No full data found in {folder} " - f"for video {videoname} and scorer {scorer}." - ) + raise FileNotFoundError(f"No full data found in {folder} for video {videoname} and scorer {scorer}.") return full_files[0] def find_video_metadata(folder, videoname: str, scorer: str): """For backward compatibility, let us search the substring 'meta'""" - + scorer_legacy = scorer.replace("DLC", "DeepCut") meta_files = filter_files_by_patterns( folder=folder, @@ -954,10 +893,7 @@ def find_video_metadata(folder, videoname: str, scorer: str): end_patterns={"pickle"}, ) if not meta_files: - raise FileNotFoundError( - f"No metadata found in {folder} " - f"for video {videoname} and scorer {scorer}." - ) + raise FileNotFoundError(f"No metadata found in {folder} for video {videoname} and scorer {scorer}.") return meta_files[0] @@ -971,7 +907,7 @@ def load_video_full_data(folder, videoname, scorer): def find_analyzed_data(folder, videoname: str, scorer: str, filtered=False, track_method=""): """Find potential data files from the hints given to the function.""" - + scorer_legacy = scorer.replace("DLC", "DeepCut") suffix = "_filtered" if filtered else "" tracker = TRACK_METHODS.get(track_method, "") @@ -979,9 +915,7 @@ def find_analyzed_data(folder, videoname: str, scorer: str, filtered=False, trac candidates = [] for file in grab_files_in_folder(folder, "h5"): stem = Path(file).stem.replace("_filtered", "") - starts_by_scorer = file.startswith(videoname + scorer) or file.startswith( - videoname + scorer_legacy - ) + starts_by_scorer = file.startswith(videoname + scorer) or file.startswith(videoname + scorer_legacy) if tracker: matches_tracker = stem.endswith(tracker) else: @@ -991,15 +925,14 @@ def find_analyzed_data(folder, videoname: str, scorer: str, filtered=False, trac starts_by_scorer, "skeleton" not in file, matches_tracker, - (filtered and "filtered" in file) - or (not filtered and "filtered" not in file), + (filtered and "filtered" in file) or (not filtered and "filtered" not in file), ) ): candidates.append(file) if not len(candidates): msg = ( - f'No {"un" if not filtered else ""}filtered data file found in {folder} ' + f"No {'un' if not filtered else ''}filtered data file found in {folder} " f"for video {videoname} and scorer {scorer}" ) if track_method: @@ -1009,19 +942,14 @@ def find_analyzed_data(folder, videoname: str, scorer: str, filtered=False, trac n_candidates = len(candidates) if n_candidates > 1: # This should not be happening anyway... - print( - f"{n_candidates} possible data files were found: {candidates}.\n" - f"Picking the first by default..." - ) + print(f"{n_candidates} possible data files were found: {candidates}.\nPicking the first by default...") filepath = str(Path(folder) / candidates[0]) scorer = scorer if scorer in filepath else scorer_legacy return filepath, scorer, suffix def load_analyzed_data(folder, videoname, scorer, filtered=False, track_method=""): - filepath, scorer, suffix = find_analyzed_data( - folder, videoname, scorer, filtered, track_method - ) + filepath, scorer, suffix = find_analyzed_data(folder, videoname, scorer, filtered, track_method) df = pd.read_hdf(filepath) return df, filepath, scorer, suffix @@ -1041,8 +969,7 @@ def load_detection_data(video, scorer, track_method): filepath = os.path.splitext(video)[0] + scorer + f"_{tracker}.pickle" if not os.path.isfile(filepath): raise FileNotFoundError( - f"No detection data found in {folder} for video {videoname}, " - f"scorer {scorer}, and tracker {track_method}" + f"No detection data found in {folder} for video {videoname}, scorer {scorer}, and tracker {track_method}" ) return read_pickle(filepath) @@ -1091,9 +1018,7 @@ def get_snapshot_index_for_scorer(name: str, index: int | str) -> int: GetVideoList = get_video_list GetTrainingSetFolder = get_training_set_folder GetDataandMetaDataFilenames = get_data_and_metadata_filenames -IntersectionofBodyPartsandOnesGivenbyUser = ( - intersection_of_body_parts_and_ones_given_by_user -) +IntersectionofBodyPartsandOnesGivenbyUser = intersection_of_body_parts_and_ones_given_by_user GetScorerName = get_scorer_name CheckifPostProcessing = check_if_post_processing CheckifNotAnalyzed = check_if_not_analyzed diff --git a/deeplabcut/utils/auxiliaryfunctions_3d.py b/deeplabcut/utils/auxiliaryfunctions_3d.py index 2483623d31..54ae08196f 100644 --- a/deeplabcut/utils/auxiliaryfunctions_3d.py +++ b/deeplabcut/utils/auxiliaryfunctions_3d.py @@ -36,9 +36,7 @@ def Foldernames3Dproject(cfg_3d): path_corners = os.path.join(cfg_3d["project_path"], "corners") path_camera_matrix = os.path.join(cfg_3d["project_path"], "camera_matrix") path_undistort = os.path.join(cfg_3d["project_path"], "undistortion") - path_removed_images = os.path.join( - cfg_3d["project_path"], "removed_calibration_images" - ) + path_removed_images = os.path.join(cfg_3d["project_path"], "removed_calibration_images") return ( img_path, @@ -128,10 +126,7 @@ def get_camerawise_videos(path, cam_names, videotype): vid = [] # Find videos only specific to the cam names - videos = [ - glob.glob(os.path.join(path, str("*" + cam_names[i] + "*" + videotype))) - for i in range(len(cam_names)) - ] + videos = [glob.glob(os.path.join(path, str("*" + cam_names[i] + "*" + videotype))) for i in range(len(cam_names))] videos = [y for x in videos for y in x] # Exclude the labeled video files @@ -143,10 +138,7 @@ def get_camerawise_videos(path, cam_names, videotype): video_list = [] cam = cam_names[0] # camera1 vid.append( - [ - name - for name in glob.glob(os.path.join(path, str("*" + cam + "*" + videotype))) - ] + [name for name in glob.glob(os.path.join(path, str("*" + cam + "*" + videotype)))] ) # all videos with cam # print("here is what I found",vid) for k in range(len(vid[0])): @@ -163,21 +155,15 @@ def get_camerawise_videos(path, cam_names, videotype): if suf == "": putativecam2name = os.path.join(path, pref + cam_names[1] + ending) else: - putativecam2name = os.path.join( - path, pref + cam_names[1] + suf + ending - ) + putativecam2name = os.path.join(path, pref + cam_names[1] + suf + ending) # print([os.path.join(path,pref+cam+suf+ending),putativecam2name]) if os.path.isfile(putativecam2name): # found a pair!!! - video_list.append( - [os.path.join(path, pref + cam + suf + ending), putativecam2name] - ) + video_list.append([os.path.join(path, pref + cam + suf + ending), putativecam2name]) return video_list -def Get_list_of_triangulated_and_videoFiles( - filepath, videotype, scorer_3d, cam_names, videofolder -): +def Get_list_of_triangulated_and_videoFiles(filepath, videotype, scorer_3d, cam_names, videofolder): """ Returns the list of triangulated h5 and the corresponding video files. """ @@ -196,19 +182,13 @@ def Get_list_of_triangulated_and_videoFiles( videofolder = filepath[0] cwd = os.getcwd() os.chdir(videofolder) - triangulated_file_list = [ - fn for fn in os.listdir(os.curdir) if (string_to_search in fn) - ] + triangulated_file_list = [fn for fn in os.listdir(os.curdir) if (string_to_search in fn)] video_list = get_camerawise_videos(videofolder, cam_names, videotype) os.chdir(cwd) triangulated_folder = videofolder else: - triangulated_file_list = [ - str(Path(fn).name) for fn in filepath if (string_to_search in fn) - ] - triangulated_folder = [ - str(Path(fn).parents[0]) for fn in filepath if (string_to_search in fn) - ] + triangulated_file_list = [str(Path(fn).name) for fn in filepath if (string_to_search in fn)] + triangulated_folder = [str(Path(fn).parents[0]) for fn in filepath if (string_to_search in fn)] triangulated_folder = triangulated_folder[0] if videofolder is None: @@ -258,9 +238,7 @@ def Get_list_of_triangulated_and_videoFiles( ) ) vfiles = get_camerawise_videos(videofolder, cam_names, videotype) - vfiles = [ - z for z in vfiles if prefix[j][0] in z[0] and suffix[j][0] in z[1] - ][0] + vfiles = [z for z in vfiles if prefix[j][0] in z[0] and suffix[j][0] in z[1]][0] file_list.append(triangulated_file + vfiles) return file_list diff --git a/deeplabcut/utils/conversioncode.py b/deeplabcut/utils/conversioncode.py index 84ee2a3fc6..5227151e7a 100644 --- a/deeplabcut/utils/conversioncode.py +++ b/deeplabcut/utils/conversioncode.py @@ -17,6 +17,7 @@ https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS Licensed under GNU Lesser General Public License v3.0 """ + import os import pandas as pd from deeplabcut.utils import auxiliaryfunctions @@ -68,9 +69,7 @@ def convertcsv2h5(config, userfeedback=True, scorer=None): askuser = "yes" if askuser in ("y", "yes", "Ja", "ha", "oui"): # multilanguage support :) - fn = os.path.join( - str(folder), "CollectedData_" + cfg["scorer"] + ".csv" - ) + fn = os.path.join(str(folder), "CollectedData_" + cfg["scorer"] + ".csv") # Determine whether the data are single- or multi-animal without loading into memory # simply by checking whether 'individuals' is in the second line of the CSV. with open(fn) as datafile: @@ -92,9 +91,7 @@ def convertcsv2h5(config, userfeedback=True, scorer=None): print("Attention:", folder, "does not appear to have labeled data!") -def adapt_labeled_data_to_new_project( - config_path, remove_old_bodyparts=False, other_scorer=False, userfeedback=False -): +def adapt_labeled_data_to_new_project(config_path, remove_old_bodyparts=False, other_scorer=False, userfeedback=False): """Given the config.yaml file, this function will convert the labels of an ancient project to a new project. For this, the labeled data must be in the project folder, under the labeled-data folder and with the same configuration as all deeplabcut projects. @@ -240,20 +237,12 @@ def analyze_videos_converth5_to_csv(video_folder, videotype=".mp4", listofvideos if listofvideos: # can also be called with a list of videos (from GUI) videos = video_folder # GUI gives a list of videos if len(videos) > 0: - h5_files = list( - auxiliaryfunctions.grab_files_in_folder( - Path(videos[0]).parent, "h5", relative=False - ) - ) + h5_files = list(auxiliaryfunctions.grab_files_in_folder(Path(videos[0]).parent, "h5", relative=False)) else: h5_files = [] else: - h5_files = list( - auxiliaryfunctions.grab_files_in_folder(video_folder, "h5", relative=False) - ) - videos = auxiliaryfunctions.grab_files_in_folder( - video_folder, videotype, relative=False - ) + h5_files = list(auxiliaryfunctions.grab_files_in_folder(video_folder, "h5", relative=False)) + videos = auxiliaryfunctions.grab_files_in_folder(video_folder, videotype, relative=False) _convert_h5_files_to("csv", None, h5_files, videos) @@ -288,20 +277,12 @@ def analyze_videos_converth5_to_nwb( if listofvideos: # can also be called with a list of videos (from GUI) videos = video_folder # GUI gives a list of videos if len(videos) > 0: - h5_files = list( - auxiliaryfunctions.grab_files_in_folder( - Path(videos[0]).parent, "h5", relative=False - ) - ) + h5_files = list(auxiliaryfunctions.grab_files_in_folder(Path(videos[0]).parent, "h5", relative=False)) else: h5_files = [] else: - h5_files = list( - auxiliaryfunctions.grab_files_in_folder(video_folder, "h5", relative=False) - ) - videos = auxiliaryfunctions.grab_files_in_folder( - video_folder, videotype, relative=False - ) + h5_files = list(auxiliaryfunctions.grab_files_in_folder(video_folder, "h5", relative=False)) + videos = auxiliaryfunctions.grab_files_in_folder(video_folder, videotype, relative=False) _convert_h5_files_to("nwb", config, h5_files, videos) @@ -318,9 +299,7 @@ def _convert_h5_files_to(filetype, config, h5_files, videos): try: from dlc2nwb.utils import convert_h5_to_nwb except ImportError: - raise ImportError( - "The package `dlc2nwb` is missing. Please run `pip install dlc2nwb`." - ) + raise ImportError("The package `dlc2nwb` is missing. Please run `pip install dlc2nwb`.") for video in videos: if "_labeled" in video: diff --git a/deeplabcut/utils/frameselectiontools.py b/deeplabcut/utils/frameselectiontools.py index 947d32d59e..3e5bb0b0b8 100644 --- a/deeplabcut/utils/frameselectiontools.py +++ b/deeplabcut/utils/frameselectiontools.py @@ -18,7 +18,6 @@ Licensed under GNU Lesser General Public License v3.0 """ - import math import cv2 @@ -89,9 +88,7 @@ def UniformFramescv2(cap, numframes2pick, start, stop, Index=None): if Index is None: if start == 0: - frames2pick = np.random.choice( - math.ceil(nframes * stop), size=numframes2pick, replace=False - ) + frames2pick = np.random.choice(math.ceil(nframes * stop), size=numframes2pick, replace=False) else: frames2pick = np.random.choice( range(math.floor(nframes * start), math.ceil(nframes * stop)), @@ -165,25 +162,17 @@ def KmeansbasedFrameselection( if color and ncolors > 1: DATA = np.zeros((nframes, nx * 3, ny)) for counter, index in tqdm(enumerate(Index)): - image = img_as_ubyte( - clipresized.get_frame(index * 1.0 / clipresized.fps) - ) - DATA[counter, :, :] = np.vstack( - [image[:, :, 0], image[:, :, 1], image[:, :, 2]] - ) + image = img_as_ubyte(clipresized.get_frame(index * 1.0 / clipresized.fps)) + DATA[counter, :, :] = np.vstack([image[:, :, 0], image[:, :, 1], image[:, :, 2]]) else: DATA = np.zeros((nframes, nx, ny)) for counter, index in tqdm(enumerate(Index)): if ncolors == 1: - DATA[counter, :, :] = img_as_ubyte( - clipresized.get_frame(index * 1.0 / clipresized.fps) - ) + DATA[counter, :, :] = img_as_ubyte(clipresized.get_frame(index * 1.0 / clipresized.fps)) else: # attention: averages over color channels to keep size small / perhaps you want to use color information? DATA[counter, :, :] = img_as_ubyte( np.array( - np.mean( - clipresized.get_frame(index * 1.0 / clipresized.fps), 2 - ), + np.mean(clipresized.get_frame(index * 1.0 / clipresized.fps), 2), dtype=np.uint8, ) ) @@ -192,9 +181,7 @@ def KmeansbasedFrameselection( data = DATA - DATA.mean(axis=0) data = data.reshape(nframes, -1) # stacking - kmeans = MiniBatchKMeans( - n_clusters=numframes2pick, tol=1e-3, batch_size=batchsize, max_iter=max_iter - ) + kmeans = MiniBatchKMeans(n_clusters=numframes2pick, tol=1e-3, batch_size=batchsize, max_iter=max_iter) kmeans.fit(data) frames2pick = [] for clusterid in range(numframes2pick): # pick one frame per cluster @@ -202,9 +189,7 @@ def KmeansbasedFrameselection( numimagesofcluster = len(clusterids) if numimagesofcluster > 0: - frames2pick.append( - Index[clusterids[np.random.randint(numimagesofcluster)]] - ) + frames2pick.append(Index[clusterids[np.random.randint(numimagesofcluster)]]) clipresized.close() del clipresized @@ -284,9 +269,7 @@ def KmeansbasedFrameselectioncv2( interpolation=cv2.INTER_NEAREST, ) ) # color trafo not necessary; lack thereof improves speed. - DATA[counter, :, :] = np.hstack( - [image[:, :, 0], image[:, :, 1], image[:, :, 2]] - ) + DATA[counter, :, :] = np.hstack([image[:, :, 0], image[:, :, 1], image[:, :, 2]]) else: for counter, index in tqdm(enumerate(Index)): cap.set_to_frame(index) # extract a particular frame @@ -317,9 +300,7 @@ def KmeansbasedFrameselectioncv2( interpolation=cv2.INTER_NEAREST, ) ) # color trafo not necessary; lack thereof improves speed. - DATA[counter, :, :] = np.hstack( - [image[:, :, 0], image[:, :, 1], image[:, :, 2]] - ) + DATA[counter, :, :] = np.hstack([image[:, :, 0], image[:, :, 1], image[:, :, 2]]) else: for counter, index in tqdm(enumerate(Index)): frame = cap.read_frame(crop=True) @@ -339,9 +320,7 @@ def KmeansbasedFrameselectioncv2( data = DATA - DATA.mean(axis=0) data = data.reshape(nframes, -1) # stacking - kmeans = MiniBatchKMeans( - n_clusters=numframes2pick, tol=1e-3, batch_size=batchsize, max_iter=max_iter - ) + kmeans = MiniBatchKMeans(n_clusters=numframes2pick, tol=1e-3, batch_size=batchsize, max_iter=max_iter) kmeans.fit(data) frames2pick = [] for clusterid in range(numframes2pick): # pick one frame per cluster @@ -349,9 +328,7 @@ def KmeansbasedFrameselectioncv2( numimagesofcluster = len(clusterids) if numimagesofcluster > 0: - frames2pick.append( - Index[clusterids[np.random.randint(numimagesofcluster)]] - ) + frames2pick.append(Index[clusterids[np.random.randint(numimagesofcluster)]]) # cap.release() >> still used in frame_extraction! return list(np.array(frames2pick)) else: diff --git a/deeplabcut/utils/make_labeled_video.py b/deeplabcut/utils/make_labeled_video.py index 44940f5635..1d536a1a47 100644 --- a/deeplabcut/utils/make_labeled_video.py +++ b/deeplabcut/utils/make_labeled_video.py @@ -20,6 +20,7 @@ Hao Wu, hwu01@g.harvard.edu contributed the original OpenCV class. Thanks! You can find the directory for your ffmpeg bindings by: "find / | grep ffmpeg" and then setting it. """ + from __future__ import annotations import argparse @@ -96,9 +97,7 @@ def CreateVideo( bpts = Dataframe.columns.get_level_values("bodyparts") all_bpts = bpts.values[::3] if draw_skeleton: - color_for_skeleton = ( - np.array(mcolors.to_rgba(skeleton_color))[:3] * 255 - ).astype(np.uint8) + color_for_skeleton = (np.array(mcolors.to_rgba(skeleton_color))[:3] * 255).astype(np.uint8) # recode the bodyparts2connect into indices for df_x and df_y for speed bpts2connect = get_segment_indices(bodyparts2connect, all_bpts) @@ -114,16 +113,8 @@ def CreateVideo( nframes = clip.nframes duration = nframes / fps - print( - "Duration of video [s]: {}, recorded with {} fps!".format( - round(duration, 2), round(fps, 2) - ) - ) - print( - "Overall # of frames: {} with cropped frame dimensions: {} {}".format( - nframes, nx, ny - ) - ) + print("Duration of video [s]: {}, recorded with {} fps!".format(round(duration, 2), round(fps, 2))) + print("Overall # of frames: {} with cropped frame dimensions: {} {}".format(nframes, nx, ny)) print("Generating frames and creating video.") df_x, df_y, df_likelihood = Dataframe.values.reshape((len(Dataframe), -1, 3)).T @@ -142,9 +133,7 @@ def CreateVideo( else: nindividuals = len(Dataframe.columns.get_level_values("individuals").unique()) map2bp = [bplist.index(bp) for bp in all_bpts] - nbpts_per_ind = ( - Dataframe.groupby(level="individuals", axis=1).size().values // 3 - ) + nbpts_per_ind = Dataframe.groupby(level="individuals", axis=1).size().values // 3 map2id = [] for i, j in enumerate(nbpts_per_ind): map2id.extend([i] * j) @@ -192,8 +181,7 @@ def CreateVideo( if draw_skeleton: for bpt1, bpt2 in bpts2connect: if np.all(df_likelihood[[bpt1, bpt2], index] > pcutoff) and not ( - np.any(np.isnan(df_x[[bpt1, bpt2], index])) - or np.any(np.isnan(df_y[[bpt1, bpt2], index])) + np.any(np.isnan(df_x[[bpt1, bpt2], index])) or np.any(np.isnan(df_y[[bpt1, bpt2], index])) ): rr, cc, val = line_aa( int(np.clip(df_y[bpt1, index], 0, ny - 1)), @@ -217,9 +205,7 @@ def CreateVideo( shape=(ny, nx), ) image[rr, cc] = color - rr, cc = disk( - (df_y[ind, index], df_x[ind, index]), dotsize, shape=(ny, nx) - ) + rr, cc = disk((df_y[ind, index], df_x[ind, index]), dotsize, shape=(ny, nx)) alpha = 1 if confidence_to_alpha is not None: alpha = confidence_to_alpha(df_likelihood[ind, index]) @@ -273,16 +259,8 @@ def CreateVideoSlow( nframes = clip.nframes duration = nframes / fps - print( - "Duration of video [s]: {}, recorded with {} fps!".format( - round(duration, 2), round(fps, 2) - ) - ) - print( - "Overall # of frames: {} with cropped frame dimensions: {} {}".format( - nframes, nx, ny - ) - ) + print("Duration of video [s]: {}, recorded with {} fps!".format(round(duration, 2), round(fps, 2))) + print("Overall # of frames: {} with cropped frame dimensions: {} {}".format(nframes, nx, ny)) print("Generating frames and creating video.") df_x, df_y, df_likelihood = Dataframe.values.reshape((len(Dataframe), -1, 3)).T if cropping and not displaycropped: @@ -303,9 +281,7 @@ def CreateVideoSlow( else: nindividuals = len(Dataframe.columns.get_level_values("individuals").unique()) map2bp = [bplist.index(bp) for bp in all_bpts] - nbpts_per_ind = ( - Dataframe.groupby(level="individuals", axis=1).size().values // 3 - ) + nbpts_per_ind = Dataframe.groupby(level="individuals", axis=1).size().values // 3 map2id = [] for i, j in enumerate(nbpts_per_ind): map2id.extend([i] * j) @@ -321,9 +297,7 @@ def CreateVideoSlow( nframes_digits = int(np.ceil(np.log10(nframes))) if nframes_digits > 9: - raise Exception( - "Your video has more than 10**9 frames, we recommend chopping it up." - ) + raise Exception("Your video has more than 10**9 frames, we recommend chopping it up.") if Frames2plot is None: Index = set(range(nframes)) @@ -405,9 +379,7 @@ def CreateVideoSlow( ax.set_ylim(0, ny) ax.axis("off") ax.invert_yaxis() - fig.subplots_adjust( - left=0, bottom=0, right=1, top=1, wspace=0, hspace=0 - ) + fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0) if save_frames: fig.savefig(imagename) writer.grab_frame() @@ -600,8 +572,8 @@ def create_labeled_video( max_workers (int | None): Maximum number of processes to use for multiprocessing. Set this parameter to limit the total RAM-usage of - simultaneous processes. Default: no maximum (i.e. number of spawned processes is based on the number of - cores and the number of input videos). + simultaneous processes. Default: no maximum (i.e. number of spawned processes is based on the number of + cores and the number of input videos). kwargs: additional arguments. For torch-based shuffles, can be used to specify: @@ -668,9 +640,7 @@ def create_labeled_video( else: cfg = auxiliaryfunctions.read_config(config) train_fraction = cfg["TrainingFraction"][trainingsetindex] - track_method = auxfun_multianimal.get_track_method( - cfg, track_method=track_method - ) + track_method = auxfun_multianimal.get_track_method(cfg, track_method=track_method) if pcutoff is None: pcutoff = cfg["pcutoff"] @@ -687,25 +657,13 @@ def create_labeled_video( modelprefix, engine=Engine.PYTORCH, ) - model_config_path = ( - Path(config).parent / model_folder / "train" / Engine.PYTORCH.pose_cfg_name - ) + model_config_path = Path(config).parent / model_folder / "train" / Engine.PYTORCH.pose_cfg_name if model_config_path.exists(): model_config = auxiliaryfunctions.read_plainconfig(str(model_config_path)) - if ( - model_config["train_settings"] - .get("weight_init", {}) - .get("memory_replay", False) - ): - superanimal_name = model_config["train_settings"]["weight_init"][ - "dataset" - ] + if model_config["train_settings"].get("weight_init", {}).get("memory_replay", False): + superanimal_name = model_config["train_settings"]["weight_init"]["dataset"] if bboxes_pcutoff is None: - bboxes_pcutoff = ( - model_config.get("detector", {}) - .get("model", {}) - .get("box_score_thresh", 0.6) - ) + bboxes_pcutoff = model_config.get("detector", {}).get("model", {}).get("box_score_thresh", 0.6) else: if bboxes_pcutoff is None: bboxes_pcutoff = 0.6 @@ -755,19 +713,13 @@ def create_labeled_video( "uniquebodyparts": uniquebodyparts, } else: - bodyparts = ( - auxiliaryfunctions.intersection_of_body_parts_and_ones_given_by_user( - cfg, displayedbodyparts - ) - ) + bodyparts = auxiliaryfunctions.intersection_of_body_parts_and_ones_given_by_user(cfg, displayedbodyparts) if draw_skeleton: bodyparts2connect = cfg["skeleton"] if displayedbodyparts != "all": bodyparts2connect = [ - pair - for pair in bodyparts2connect - if all(element in displayedbodyparts for element in pair) + pair for pair in bodyparts2connect if all(element in displayedbodyparts for element in pair) ] skeleton_color = cfg["skeleton_color"] else: @@ -812,7 +764,7 @@ def create_labeled_video( ) if get_start_method() == "fork": - n_workers = (max_workers or min(os.cpu_count(), len(Videos))) + n_workers = max_workers or min(os.cpu_count(), len(Videos)) with Pool(n_workers) as pool: results = pool.map(func, Videos) else: @@ -900,17 +852,13 @@ def proc_video( df, filepath, _, _ = auxiliaryfunctions.load_analyzed_data( destfolder, vname, DLCscorer, filtered, track_method ) - metadata = auxiliaryfunctions.load_video_metadata( - destfolder, vname, DLCscorer - ) + metadata = auxiliaryfunctions.load_video_metadata(destfolder, vname, DLCscorer) if cfg.get("multianimalproject", False): s = "_id" if color_by == "individual" else "_bp" else: s = "" - videooutname = filepath.replace( - ".h5", f"{s}_p{int(100 * pcutoff)}_labeled.mp4" - ) + videooutname = filepath.replace(".h5", f"{s}_p{int(100 * pcutoff)}_labeled.mp4") if os.path.isfile(videooutname) and not overwrite: print("Labeled video already created. Skipping...") return @@ -925,17 +873,11 @@ def proc_video( cropping = metadata["data"]["cropping"] [x1, x2, y1, y2] = metadata["data"]["cropping_parameters"] - labeled_bpts = [ - bp - for bp in df.columns.get_level_values("bodyparts").unique() - if bp in bodyparts - ] + labeled_bpts = [bp for bp in df.columns.get_level_values("bodyparts").unique() if bp in bodyparts] # The full data file is not created for single-animal TensorFlow models try: - full_data = auxiliaryfunctions.load_video_full_data( - destfolder, vname, DLCscorer - ) + full_data = auxiliaryfunctions.load_video_full_data(destfolder, vname, DLCscorer) frames_dict = { int(key.replace("frame", "")): value for key, value in full_data.items() @@ -943,9 +885,7 @@ def proc_video( } bboxes_list = None if "bboxes" in frames_dict.get(min(frames_dict.keys()), {}): - bboxes_list = [ - frames_dict[key] for key in sorted(frames_dict.keys()) - ] + bboxes_list = [frames_dict[key] for key in sorted(frames_dict.keys())] except FileNotFoundError: bboxes_list = None @@ -1159,9 +1099,7 @@ def create_video_with_keypoints_only( map_ = individuals.map(dict(zip(individual_names, range(n_individuals)))) cmap = plt.get_cmap(colormap, n_individuals) except KeyError as e: - raise Exception( - "Coloring by individuals is only valid for multi-animal data" - ) from e + raise Exception("Coloring by individuals is only valid for multi-animal data") from e else: raise ValueError(f"Invalid color_by={color_by}") @@ -1181,11 +1119,7 @@ def create_video_with_keypoints_only( ax.set_xlim(0, nx) ax.set_ylim(0, ny) ax.axis("off") - ax.add_patch( - plt.Rectangle( - (0, 0), 1, 1, facecolor=background_color, transform=ax.transAxes, zorder=-1 - ) - ) + ax.add_patch(plt.Rectangle((0, 0), 1, 1, facecolor=background_color, transform=ax.transAxes, zorder=-1)) ax.invert_yaxis() plt.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0) @@ -1295,21 +1229,15 @@ def create_video_with_all_detections( full_pickle = os.path.join(videofolder + DLCscorername + "_full.pickle") else: auxiliaryfunctions.attempt_to_make_folder(destfolder) - outputname = os.path.join( - destfolder, str(Path(video).stem) + DLCscorername + "_full.mp4" - ) - full_pickle = os.path.join( - destfolder, str(Path(video).stem) + DLCscorername + "_full.pickle" - ) + outputname = os.path.join(destfolder, str(Path(video).stem) + DLCscorername + "_full.mp4") + full_pickle = os.path.join(destfolder, str(Path(video).stem) + DLCscorername + "_full.pickle") if not (os.path.isfile(outputname)): video_name = str(Path(video).stem) print("Creating labeled video for ", video_name) h5file = full_pickle.replace("_full.pickle", ".h5") data, metadata = auxfun_multianimal.LoadFullMultiAnimalData(h5file) - data = dict( - data - ) # Cast to dict (making a copy) so items can safely be popped + data = dict(data) # Cast to dict (making a copy) so items can safely be popped x1, y1 = 0, 0 if cropping is not None: @@ -1357,11 +1285,7 @@ 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]] - and "bbox_scores" 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] @@ -1374,9 +1298,7 @@ def create_video_with_all_detections( confidence = bbox_scores[i] if confidence < bboxes_pcutoff: continue - rect_coords = rectangle_perimeter( - start=(y, x), extent=(h, w) - ) + rect_coords = rectangle_perimeter(start=(y, x), extent=(h, w)) set_color( frame, @@ -1473,9 +1395,7 @@ def _create_video_from_tracks(video, tracks, destfolder, output_name, pcutoff, s [os.remove(image) for image in os.listdir(destfolder) if "frame" in image] -def create_video_from_pickled_tracks( - video, pickle_file, destfolder="", output_name="", pcutoff=0.6 -): +def create_video_from_pickled_tracks(video, pickle_file, destfolder="", output_name="", pcutoff=0.6): if not destfolder: destfolder = os.path.splitext(video)[0] if not output_name: diff --git a/deeplabcut/utils/multiprocessing.py b/deeplabcut/utils/multiprocessing.py index 3515b73125..e193ea918e 100644 --- a/deeplabcut/utils/multiprocessing.py +++ b/deeplabcut/utils/multiprocessing.py @@ -17,6 +17,7 @@ https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS Licensed under GNU Lesser General Public License v3.0 """ + import multiprocessing @@ -30,18 +31,14 @@ def _wrapper(func, queue, *args, **kwargs): def call_with_timeout(func, timeout, *args, **kwargs): queue = multiprocessing.Queue() - process = multiprocessing.Process( - target=_wrapper, args=(func, queue, *args), kwargs=kwargs - ) + process = multiprocessing.Process(target=_wrapper, args=(func, queue, *args), kwargs=kwargs) process.start() process.join(timeout) if process.is_alive(): process.terminate() # Forcefully terminate the process process.join() - raise TimeoutError( - f"Function {func.__name__} did not complete within {timeout} seconds." - ) + raise TimeoutError(f"Function {func.__name__} did not complete within {timeout} seconds.") if not queue.empty(): result = queue.get() @@ -49,6 +46,4 @@ def call_with_timeout(func, timeout, *args, **kwargs): raise result # Re-raise the exception if it occurred in the function return result else: - raise TimeoutError( - f"Function {func.__name__} completed but did not return a result." - ) + raise TimeoutError(f"Function {func.__name__} completed but did not return a result.") diff --git a/deeplabcut/utils/plotting.py b/deeplabcut/utils/plotting.py index 27b4d49f87..464de8d131 100644 --- a/deeplabcut/utils/plotting.py +++ b/deeplabcut/utils/plotting.py @@ -17,6 +17,7 @@ https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS Licensed under GNU Lesser General Public License v3.0 """ + from __future__ import annotations import argparse @@ -95,12 +96,8 @@ def PlottingResults( with np.errstate(invalid="ignore"): for bpindex, bp in enumerate(bodyparts2plot): - if ( - bp in animal_bpts - ): # Avoid 'unique' bodyparts only present in the 'single' animal - prob = Dataframe.xs( - (bp, "likelihood"), level=(-2, -1), axis=1 - ).values.squeeze() + if bp in animal_bpts: # Avoid 'unique' bodyparts only present in the 'single' animal + prob = Dataframe.xs((bp, "likelihood"), level=(-2, -1), axis=1).values.squeeze() mask = prob < pcutoff temp_x = np.ma.array( Dataframe.xs((bp, "x"), level=(-2, -1), axis=1).values.squeeze(), @@ -152,17 +149,13 @@ def PlottingResults( bbox_inches="tight", dpi=resolution, ) - fig2.savefig( - os.path.join(tmpfolder, "plot" + suffix), bbox_inches="tight", dpi=resolution - ) + fig2.savefig(os.path.join(tmpfolder, "plot" + suffix), bbox_inches="tight", dpi=resolution) fig3.savefig( os.path.join(tmpfolder, "plot-likelihood" + suffix), bbox_inches="tight", dpi=resolution, ) - fig4.savefig( - os.path.join(tmpfolder, "hist" + suffix), bbox_inches="tight", dpi=resolution - ) + fig4.savefig(os.path.join(tmpfolder, "hist" + suffix), bbox_inches="tight", dpi=resolution) if showfigures: plt.show() @@ -292,17 +285,11 @@ def plot_trajectories( modelprefix=modelprefix, **kwargs, ) # automatically loads corresponding model (even training iteration based on snapshot index) - bodyparts = auxiliaryfunctions.intersection_of_body_parts_and_ones_given_by_user( - cfg, displayedbodyparts - ) - individuals = auxfun_multianimal.IntersectionofIndividualsandOnesGivenbyUser( - cfg, displayedindividuals - ) + bodyparts = auxiliaryfunctions.intersection_of_body_parts_and_ones_given_by_user(cfg, displayedbodyparts) + individuals = auxfun_multianimal.IntersectionofIndividualsandOnesGivenbyUser(cfg, displayedindividuals) Videos = auxiliaryfunctions.get_list_of_videos(videos, videotype) if not len(Videos): - print( - "No videos found. Make sure you passed a list of videos and that *videotype* is right." - ) + print("No videos found. Make sure you passed a list of videos and that *videotype* is right.") return failures, multianimal_errors = [], [] @@ -339,9 +326,7 @@ def plot_trajectories( if track_method != "": # In a multi animal scenario, show more verbose errors. try: - _ = auxiliaryfunctions.load_detection_data( - video, DLCscorer, track_method - ) + _ = auxiliaryfunctions.load_detection_data(video, DLCscorer, track_method) error_message = 'Call "deeplabcut.stitch_tracklets() prior to plotting the trajectories.' except FileNotFoundError as e: print(e) @@ -361,13 +346,10 @@ def plot_trajectories( verbose_error = "." print( f"Plots could not be created for {failed_videos}. " - f"Videos were not evaluated with the current scorer {DLCscorer}" - + verbose_error + f"Videos were not evaluated with the current scorer {DLCscorer}" + verbose_error ) else: - print( - 'Plots created! Please check the directory "plot-poses" within the video directory' - ) + print('Plots created! Please check the directory "plot-poses" within the video directory') def _plot_trajectories( @@ -398,11 +380,7 @@ def _plot_trajectories( dest_folder = os.path.join(vid_folder, "plot-poses", vname) auxiliaryfunctions.attempt_to_make_folder(dest_folder, recursive=True) # Keep only the individuals and bodyparts that were labeled - labeled_bpts = [ - bp - for bp in df.columns.get_level_values("bodyparts").unique() - if bp in bodyparts - ] + labeled_bpts = [bp for bp in df.columns.get_level_values("bodyparts").unique() if bp in bodyparts] # Either display the animals defined in the config if they are found # in the dataframe, or all the trajectories regardless of their names try: @@ -444,9 +422,7 @@ def _plot_paf_performance( if ax is None: fig, ax = plt.subplots(tight_layout=True, figsize=(3, 3)) sns.histplot(within, kde=kde, ax=ax, stat="probability", color=colors[0], bins=bins) - sns.histplot( - between, kde=kde, ax=ax, stat="probability", color=colors[1], bins=bins - ) + sns.histplot(between, kde=kde, ax=ax, stat="probability", color=colors[1], bins=bins) return ax diff --git a/deeplabcut/utils/pseudo_label.py b/deeplabcut/utils/pseudo_label.py index 31fb06f204..dac2f85d5e 100644 --- a/deeplabcut/utils/pseudo_label.py +++ b/deeplabcut/utils/pseudo_label.py @@ -57,9 +57,7 @@ def optimal_match(gts_list, preds_list): for i in range(num_gts): for j in range(num_preds): - cost_matrix[i, j] = distance.euclidean( - gts_list[i][..., :2].flatten(), preds_list[j][..., :2].flatten() - ) + cost_matrix[i, j] = distance.euclidean(gts_list[i][..., :2].flatten(), preds_list[j][..., :2].flatten()) row_ind, col_ind = linear_sum_assignment(cost_matrix) return col_ind @@ -125,9 +123,7 @@ def video_to_frames(input_video, output_folder, cropping: list[int] | None = Non # cv2.destroyAllWindows() -def plot_cost_matrix( - matrix, gt_keypoint_names, pred_keypoint_names, conversion_plot_out_path -): +def plot_cost_matrix(matrix, gt_keypoint_names, pred_keypoint_names, conversion_plot_out_path): matrix /= np.max(matrix) fig, ax = plt.subplots() @@ -185,9 +181,7 @@ def keypoint_matching( max_individuals = 1 memory_replay_folder = dlc_proj_root / "memory_replay" - temp_dataset.materialize( - str(memory_replay_folder), framework="coco", deepcopy=copy_images - ) + temp_dataset.materialize(str(memory_replay_folder), framework="coco", deepcopy=copy_images) # run inference on the train set config = modelzoo.load_super_animal_config( @@ -200,10 +194,12 @@ def keypoint_matching( # get the SuperAnimal detector and pose model snapshot paths pose_model_path = modelzoo.get_super_animal_snapshot_path( - dataset=superanimal_name, model_name=model_name, + dataset=superanimal_name, + model_name=model_name, ) detector_path = modelzoo.get_super_animal_snapshot_path( - dataset=superanimal_name, model_name=detector_name, + dataset=superanimal_name, + model_name=detector_name, ) config = update_config(config, max_individuals, device) @@ -254,9 +250,7 @@ def keypoint_matching( image_extensions = ["*.png", "*.jpg", "*.jpeg", "*.bmp", "*.gif", "*.tiff"] images_in_folder = [] for ext in image_extensions: - images_in_folder.extend( - glob.glob(os.path.join(memory_replay_folder, "images", ext)) - ) + images_in_folder.extend(glob.glob(os.path.join(memory_replay_folder, "images", ext))) corresponded_images = [] for image in images_in_folder: @@ -266,10 +260,7 @@ def keypoint_matching( corresponded_images.append(image_path) images = corresponded_images - bbox_gts = [ - {"bboxes": np.array(image_name_to_bbox[image.split(os.sep)[-1]])} - for image in images - ] + bbox_gts = [{"bboxes": np.array(image_name_to_bbox[image.split(os.sep)[-1]])} for image in images] pose_inputs = list(zip(images, bbox_gts)) @@ -322,13 +313,9 @@ def keypoint_matching( row_ind, column_ind = linear_sum_assignment(match_matrix * -1) keypoint_mapping_list = [] - conversion_matrix_out_path = os.path.join( - memory_replay_folder, "confusion_matrix.png" - ) + conversion_matrix_out_path = os.path.join(memory_replay_folder, "confusion_matrix.png") - plot_cost_matrix( - match_matrix, gt_keypoint_names, pred_keypoint_names, conversion_matrix_out_path - ) + plot_cost_matrix(match_matrix, gt_keypoint_names, pred_keypoint_names, conversion_matrix_out_path) for row, column in zip(row_ind, column_ind): pred_kpt_name = pred_keypoint_names[row] @@ -336,18 +323,14 @@ def keypoint_matching( count = match_dict[pred_kpt_name][anno_kpt_name] keypoint_mapping_list.append((pred_kpt_name, anno_kpt_name, count)) - keypoint_mapping_list = sorted( - keypoint_mapping_list, key=lambda x: x[2], reverse=True - ) + keypoint_mapping_list = sorted(keypoint_mapping_list, key=lambda x: x[2], reverse=True) names = [e[:2] for e in keypoint_mapping_list] conversion_table = {} for pred, anno in names: conversion_table[pred] = anno - conversion_table_out_path = os.path.join( - memory_replay_folder, "conversion_table.csv" - ) + conversion_table_out_path = os.path.join(memory_replay_folder, "conversion_table.csv") with open(conversion_table_out_path, "w") as f: out = "gt, MasterName\n" for name in pred_keypoint_names: @@ -412,9 +395,9 @@ def dlc3predictions_2_annotation_from_video( predictions, image_paths = predictions[::10], image_paths[::10] # Since the inference API does not return the image path, I assume the predictions are provided in the same order as the frames in the video. - assert len(image_paths) == len( - predictions - ), f"number of images must be equal to number of predictions. image_paths: {len(image_paths)} , predictions: {len(predictions)}" + assert len(image_paths) == len(predictions), ( + f"number of images must be equal to number of predictions. image_paths: {len(image_paths)} , predictions: {len(predictions)}" + ) new_predictions = [] num_kpts = len(bodyparts) @@ -422,7 +405,7 @@ def dlc3predictions_2_annotation_from_video( if not superanimal_name.startswith("superanimal_"): raise ValueError("not supporting non superanimal model video adaptation yet") - category_name = superanimal_name[len("superanimal_"):] + category_name = superanimal_name[len("superanimal_") :] categories = [ { "name": category_name, @@ -447,19 +430,9 @@ def dlc3predictions_2_annotation_from_video( # iterate through individuals if there are many - assert ( - len(prediction["bodyparts"]) - == len(prediction["bboxes"]) - == len(prediction["bbox_scores"]) - ) - for pose, bbox, bbox_score in zip( - prediction["bodyparts"], prediction["bboxes"], prediction["bbox_scores"] - ): - if ( - np.all(np.array(pose) <= 0) - or len(bbox) == 0 - or bbox_score < bbox_threshold - ): + assert len(prediction["bodyparts"]) == len(prediction["bboxes"]) == len(prediction["bbox_scores"]) + for pose, bbox, bbox_score in zip(prediction["bodyparts"], prediction["bboxes"], prediction["bbox_scores"]): + if np.all(np.array(pose) <= 0) or len(bbox) == 0 or bbox_score < bbox_threshold: continue imageid2annotations[image_id].append(pose) pose = np.array(pose) diff --git a/deeplabcut/utils/skeleton.py b/deeplabcut/utils/skeleton.py index fab48dfb01..7622bda82d 100644 --- a/deeplabcut/utils/skeleton.py +++ b/deeplabcut/utils/skeleton.py @@ -35,9 +35,7 @@ def read_config(configname): if not os.path.exists(configname): - raise FileNotFoundError( - f"Config {configname} is not found. Please make sure that the file exists." - ) + raise FileNotFoundError(f"Config {configname} is not found. Please make sure that the file exists.") with open(configname) as file: return YAML().load(file) @@ -57,12 +55,8 @@ def __init__(self, config_path): root = os.path.join(self.cfg["project_path"], "labeled-data") for dir_ in os.listdir(root): folder = os.path.join(root, dir_) - if os.path.isdir(folder) and not any( - folder.endswith(s) for s in ("cropped", "labeled") - ): - self.df = pd.read_hdf( - os.path.join(folder, f'CollectedData_{self.cfg["scorer"]}.h5') - ) + if os.path.isdir(folder) and not any(folder.endswith(s) for s in ("cropped", "labeled")): + self.df = pd.read_hdf(os.path.join(folder, f"CollectedData_{self.cfg['scorer']}.h5")) row, col = self.pick_labeled_frame() if "individuals" in self.df.columns.names: self.df = self.df.xs(col, axis=1, level="individuals") @@ -97,9 +91,7 @@ def __init__(self, config_path): pair_sorted = tuple(sorted(pair)) self.inds.add(pair_sorted) self.segs.add(tuple(map(tuple, self.xy[pair_sorted, :]))) - self.lines = LineCollection( - self.segs, colors=mcolors.to_rgba(self.cfg["skeleton_color"]) - ) + self.lines = LineCollection(self.segs, colors=mcolors.to_rgba(self.cfg["skeleton_color"])) self.lines.set_picker(True) self.show() diff --git a/deeplabcut/utils/video_processor.py b/deeplabcut/utils/video_processor.py index 5da9f7c54f..72851eb786 100644 --- a/deeplabcut/utils/video_processor.py +++ b/deeplabcut/utils/video_processor.py @@ -32,9 +32,7 @@ class VideoProcessor(object): sh and sw are the output height and width respectively. """ - def __init__( - self, fname="", sname="", nframes=-1, fps=None, codec="X264", sh="", sw="" - ): + def __init__(self, fname="", sname="", nframes=-1, fps=None, codec="X264", sh="", sw=""): self.fname = fname self.sname = sname self.nframes = nframes diff --git a/deeplabcut/utils/visualization.py b/deeplabcut/utils/visualization.py index d720e82fce..d50bb85e28 100644 --- a/deeplabcut/utils/visualization.py +++ b/deeplabcut/utils/visualization.py @@ -17,6 +17,7 @@ https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS Licensed under GNU Lesser General Public License v3.0 """ + from __future__ import annotations import os @@ -75,8 +76,7 @@ def make_labeled_image( for scorerindex, loopscorer in enumerate(Scorers): for bpindex, bp in enumerate(bodyparts): if np.isfinite( - DataCombined[loopscorer][bp]["y"].iloc[imagenr] - + DataCombined[loopscorer][bp]["x"].iloc[imagenr] + DataCombined[loopscorer][bp]["y"].iloc[imagenr] + DataCombined[loopscorer][bp]["x"].iloc[imagenr] ): y, x = ( int(DataCombined[loopscorer][bp]["y"].iloc[imagenr]), @@ -160,9 +160,7 @@ def make_multianimal_labeled_image( ax.imshow(frame, "gray") if bounding_boxes is not None: - for i, (bbox, bbox_score) in enumerate( - zip(bounding_boxes[0], bounding_boxes[1]) - ): + for i, (bbox, bbox_score) in enumerate(zip(bounding_boxes[0], bounding_boxes[1])): bbox_origin = (bbox[0], bbox[1]) (bbox_width, bbox_height) = (bbox[2], bbox[3]) if isinstance(bboxes_color, Colormap): @@ -283,9 +281,7 @@ def erase_artists(ax): def prepare_figure_axes(width, height, scale=1.0, dpi=100): - fig = plt.figure( - frameon=False, figsize=(width * scale / dpi, height * scale / dpi), dpi=dpi - ) + fig = plt.figure(frameon=False, figsize=(width * scale / dpi, height * scale / dpi), dpi=dpi) ax = fig.add_subplot(111) ax.axis("off") ax.set_xlim(0, width) @@ -335,9 +331,7 @@ def make_labeled_images_from_dataframe( bodypart_names = bodyparts.unique() nbodyparts = len(bodypart_names) bodyparts = bodyparts[::2] - draw_skeleton = ( - draw_skeleton and cfg["skeleton"] - ) # Only draw if a skeleton is defined + draw_skeleton = draw_skeleton and cfg["skeleton"] # Only draw if a skeleton is defined if color_by == "bodypart": map_ = bodyparts.map(dict(zip(bodypart_names, range(nbodyparts)))) @@ -353,9 +347,7 @@ def make_labeled_images_from_dataframe( cmap = get_cmap(nindividuals, cfg["colormap"]) colors = cmap(map_) except KeyError as e: - raise Exception( - "Coloring by individuals is only valid for multi-animal data" - ) from e + raise Exception("Coloring by individuals is only valid for multi-animal data") from e else: raise ValueError("`color_by` must be either `bodypart` or `individual`.") @@ -371,9 +363,7 @@ def make_labeled_images_from_dataframe( bones.extend(zip(match1, match2)) ind_bones = tuple(zip(*bones)) - images_list = [ - os.path.join(cfg["project_path"], *tuple_) for tuple_ in df.index.tolist() - ] + images_list = [os.path.join(cfg["project_path"], *tuple_) for tuple_ in df.index.tolist()] if not destfolder: destfolder = os.path.dirname(images_list[0]) tmpfolder = destfolder + "_labeled" @@ -430,9 +420,7 @@ def make_labeled_images_from_dataframe( for coord, c in zip(coords, colors): ax.plot(*coord, keypoint, ms=s, alpha=alpha, color=c) if ind_bones: - coll = LineCollection( - segs[ind], colors=cfg["skeleton_color"], alpha=alpha - ) + coll = LineCollection(segs[ind], colors=cfg["skeleton_color"], alpha=alpha) ax.add_collection(coll) imagename = os.path.basename(filename) fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0) @@ -505,9 +493,7 @@ def plot_evaluation_results( image_path = Path(project_root) / data_folder / video / image frame = auxfun_videos.imread(str(image_path), mode="skimage") - row_multi = row.loc[ - (slice(None), row.index.get_level_values("individuals") != "single") - ] + row_multi = row.loc[(slice(None), row.index.get_level_values("individuals") != "single")] individuals = len(row_multi.index.get_level_values("individuals").unique()) bodyparts = len(row_multi.index.get_level_values("bodyparts").unique()) df_gt = row_multi[scorer] @@ -523,7 +509,7 @@ def plot_evaluation_results( actual_size_pred = df_predictions.size expected_size_gt = individuals * bodyparts * 2 expected_size_pred = individuals * bodyparts * 3 - + print(f"Warning: DataFrame reshape failed for {image}") print(f" Expected: {individuals} individuals, {bodyparts} bodyparts") print(f" Ground truth: {actual_size_gt} elements (expected {expected_size_gt})") @@ -534,23 +520,13 @@ def plot_evaluation_results( bboxes = bounding_boxes.get(row_index) if plot_unique_bodyparts: - row_unique = row.loc[ - (slice(None), row.index.get_level_values("individuals") == "single") - ] + row_unique = row.loc[(slice(None), row.index.get_level_values("individuals") == "single")] unique_individuals = 1 - unique_bodyparts = len( - row_unique.index.get_level_values("bodyparts").unique() - ) + unique_bodyparts = len(row_unique.index.get_level_values("bodyparts").unique()) try: - unique_ground_truth = ( - row_unique[scorer] - .to_numpy() - .reshape((unique_individuals, unique_bodyparts, 2)) - ) + unique_ground_truth = row_unique[scorer].to_numpy().reshape((unique_individuals, unique_bodyparts, 2)) unique_predictions = ( - row_unique[model_name] - .to_numpy() - .reshape((unique_individuals, unique_bodyparts, 3)) + row_unique[model_name].to_numpy().reshape((unique_individuals, unique_bodyparts, 3)) ) except ValueError as e: # Handle cases where unique bodyparts reshape fails diff --git a/docs/recipes/flip_and_rotate.ipynb b/docs/recipes/flip_and_rotate.ipynb index 501b7969d9..c545e871be 100644 --- a/docs/recipes/flip_and_rotate.ipynb +++ b/docs/recipes/flip_and_rotate.ipynb @@ -47,8 +47,13 @@ "source": [ "import deeplabcut\n", "\n", - "project_folder = \"/home/user/projects/\" #the folder in which the DLC project will be created\n", - "deeplabcut.create_new_project(project='bat_augmentation_austin_2020_bat_data',experimenter='DLC',videos=['/home/user/dummyVideos/'],working_directory=project_folder)" + "project_folder = \"/home/user/projects/\" # the folder in which the DLC project will be created\n", + "deeplabcut.create_new_project(\n", + " project=\"bat_augmentation_austin_2020_bat_data\",\n", + " experimenter=\"DLC\",\n", + " videos=[\"/home/user/dummyVideos/\"],\n", + " working_directory=project_folder,\n", + ")" ] }, { @@ -76,11 +81,11 @@ }, "outputs": [], "source": [ - "#define config file\n", + "# define config file\n", "config_path = \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml\"\n", "\n", - "#import tools for modifying our config file\n", - "from deeplabcut.utils.auxiliaryfunctions import read_config, edit_config\n" + "# import tools for modifying our config file\n", + "from deeplabcut.utils.auxiliaryfunctions import read_config, edit_config" ] }, { @@ -102,7 +107,29 @@ "outputs": [], "source": [ "# replace the default list of bodyparts with a list of the parts that we have actually digitized\n", - "edit_config(config_path,{\"bodyparts\":['t3L', 'wstL', 't5L', 'elbL', 'shdL', 'ankL', 'nl', 'str', 'lmb', 'shdR', 'ankR', 'elbR', 'wstR', 't5R', 't3R', 'tail']})" + "edit_config(\n", + " config_path,\n", + " {\n", + " \"bodyparts\": [\n", + " \"t3L\",\n", + " \"wstL\",\n", + " \"t5L\",\n", + " \"elbL\",\n", + " \"shdL\",\n", + " \"ankL\",\n", + " \"nl\",\n", + " \"str\",\n", + " \"lmb\",\n", + " \"shdR\",\n", + " \"ankR\",\n", + " \"elbR\",\n", + " \"wstR\",\n", + " \"t5R\",\n", + " \"t3R\",\n", + " \"tail\",\n", + " ]\n", + " },\n", + ")" ] }, { @@ -115,9 +142,9 @@ }, "outputs": [], "source": [ - "#fetch the list of videos from an older project using the same videos\n", + "# fetch the list of videos from an older project using the same videos\n", "videolist = read_config(\"/home/user/projects/old_project-DLC-2022-08-03/config.yaml\")[\"video_sets\"]\n", - "edit_config(config_path,{'video_sets':videolist})" + "edit_config(config_path, {\"video_sets\": videolist})" ] }, { @@ -142,6 +169,7 @@ "source": [ "# Convert training data into the DeepLabCut format\n", "import deeplabcut\n", + "\n", "deeplabcut.convertcsv2h5(config_path, userfeedback=False)\n", "\n", "# Check labels (sanity check)\n", @@ -253,9 +281,11 @@ "import pandas as pd\n", "\n", "# Read the h5 file containing all the frames, (project_folder/training-datasets/iteration-0/UnaufmentedDataSet_project_folder/CollectedData_LabelerName.h5)\n", - "df = pd.read_hdf('/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/training-datasets/iteration-0/UnaugmentedDataSet_bat_augmentation_austin_2020_bat_dataAug18/CollectedData_DLC.h5')\n", + "df = pd.read_hdf(\n", + " \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/training-datasets/iteration-0/UnaugmentedDataSet_bat_augmentation_austin_2020_bat_dataAug18/CollectedData_DLC.h5\"\n", + ")\n", "\n", - "image_paths = df.index.to_list() # turn dataframe into list\n", + "image_paths = df.index.to_list() # turn dataframe into list\n", "\n", "# create empty lists for putting testing and training indices in\n", "test_inds = []\n", @@ -276,7 +306,7 @@ " trainIndices=[train_inds],\n", " testIndices=[test_inds],\n", " net_type=\"resnet_50\",\n", - " augmenter_type=\"../imagesaug\"\n", + " augmenter_type=\"../imagesaug\",\n", ")\n", "\n", "# train on half+ref, shuffle 2\n", @@ -296,7 +326,7 @@ " trainIndices=[train_inds],\n", " testIndices=[test_inds],\n", " net_type=\"resnet_50\",\n", - " augmenter_type=\"../imagesaug\"\n", + " augmenter_type=\"../imagesaug\",\n", ")\n", "\n", "# train on full, test data is OOD, shuffle 3\n", @@ -316,7 +346,7 @@ " trainIndices=[train_inds],\n", " testIndices=[test_inds],\n", " net_type=\"resnet_50\",\n", - " augmenter_type=\"../imagesaug\"\n", + " augmenter_type=\"../imagesaug\",\n", ")\n", "\n", "# train on full+ref, shuffle 4\n", @@ -338,7 +368,7 @@ " trainIndices=[train_inds],\n", " testIndices=[test_inds],\n", " net_type=\"resnet_50\",\n", - " augmenter_type=\"../imagesaug\"\n", + " augmenter_type=\"../imagesaug\",\n", ")" ] }, @@ -383,8 +413,11 @@ "outputs": [], "source": [ "import os\n", - "files = os.listdir(\"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/training-datasets/iteration-0/UnaugmentedDataSet_bat_augmentation_austin_2020_bat_dataAug18\")\n", - "print(*files,sep='\\n')" + "\n", + "files = os.listdir(\n", + " \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/training-datasets/iteration-0/UnaugmentedDataSet_bat_augmentation_austin_2020_bat_dataAug18\"\n", + ")\n", + "print(*files, sep=\"\\n\")" ] }, { @@ -426,46 +459,50 @@ "# sure deeplabcut is imported and the config_path defined\n", "import deeplabcut\n", "\n", - "config_path = '/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml'\n", + "config_path = \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml\"\n", "\n", "# we also need the package os for folder manipulation\n", "import os\n", + "\n", "# and shutil for copying files\n", "import shutil\n", "\n", - "#import tools for reading our config file\n", + "# import tools for reading our config file\n", "from deeplabcut.utils.auxiliaryfunctions import read_config\n", "\n", "# Number and name for our model folder\n", "model_number = 0\n", - "modelprefix_pre = 'data_augm'\n", - "daug_str = 'base'\n", + "modelprefix_pre = \"data_augm\"\n", + "daug_str = \"base\"\n", "\n", "# Get config as dict and associated paths\n", "cfg = read_config(config_path)\n", - "project_path = cfg[\"project_path\"] # or: os.path.dirname(config_path) #dlc_models_path = os.path.join(project_path, \"dlc-models\")\n", + "project_path = cfg[\n", + " \"project_path\"\n", + "] # or: os.path.dirname(config_path) #dlc_models_path = os.path.join(project_path, \"dlc-models\")\n", "training_datasets_path = os.path.join(project_path, \"training-datasets\")\n", "\n", "# Define shuffles\n", - "shuffles = [1,2,3,4]\n", + "shuffles = [1, 2, 3, 4]\n", "trainingsetindices = [0, 1, 2, 3]\n", "\n", "# Get train and test pose config file paths from base project, for each shuffle\n", "list_base_train_pose_config_file_paths = []\n", "list_base_test_pose_config_file_paths = []\n", "for shuffle_number, trainingsetindex in zip(shuffles, trainingsetindices):\n", - " base_train_pose_config_file_path_TEMP,\\\n", - " base_test_pose_config_file_path_TEMP,\\\n", - " _ = deeplabcut.return_train_network_path(config_path,\n", - " shuffle=shuffle_number,\n", - " trainingsetindex=trainingsetindex) # base_train_pose_config_file\n", + " base_train_pose_config_file_path_TEMP, base_test_pose_config_file_path_TEMP, _ = (\n", + " deeplabcut.return_train_network_path(config_path, shuffle=shuffle_number, trainingsetindex=trainingsetindex)\n", + " ) # base_train_pose_config_file\n", " list_base_train_pose_config_file_paths.append(base_train_pose_config_file_path_TEMP)\n", " list_base_test_pose_config_file_paths.append(base_test_pose_config_file_path_TEMP)\n", "\n", "# Create subdirs for this augmentation method\n", - "model_prefix = '_'.join([modelprefix_pre, \"{0:0=2d}\".format(model_number), daug_str]) # modelprefix_pre = aug_\n", + "model_prefix = \"_\".join([modelprefix_pre, \"{0:0=2d}\".format(model_number), daug_str]) # modelprefix_pre = aug_\n", "aug_project_path = os.path.join(project_path, model_prefix)\n", - "aug_dlc_models = os.path.join(aug_project_path, \"dlc-models\", )\n", + "aug_dlc_models = os.path.join(\n", + " aug_project_path,\n", + " \"dlc-models\",\n", + ")\n", "\n", "# make the folder for this modelprefix\n", "try:\n", @@ -475,25 +512,20 @@ " print(\"Skipping this one as it already exists\")\n", "\n", "# Copy base train pose config file to the directory of this augmentation method\n", - "for j, (shuffle, trainingsetindex) in enumerate(zip(shuffles,trainingsetindices)):\n", - " one_train_pose_config_file_path,\\\n", - " one_test_pose_config_file_path,\\\n", - " _ = deeplabcut.return_train_network_path(config_path,\n", - " shuffle=shuffle,\n", - " trainingsetindex=trainingsetindex,\n", - " modelprefix=model_prefix)\n", - " \n", + "for j, (shuffle, trainingsetindex) in enumerate(zip(shuffles, trainingsetindices)):\n", + " one_train_pose_config_file_path, one_test_pose_config_file_path, _ = deeplabcut.return_train_network_path(\n", + " config_path, shuffle=shuffle, trainingsetindex=trainingsetindex, modelprefix=model_prefix\n", + " )\n", + "\n", " # make train and test directories for this subdir\n", - " os.makedirs(str(os.path.dirname(one_train_pose_config_file_path))) # create parentdir 'train'\n", - " os.makedirs(str(os.path.dirname(one_test_pose_config_file_path))) # create parentdir 'test\n", - " \n", + " os.makedirs(str(os.path.dirname(one_train_pose_config_file_path))) # create parentdir 'train'\n", + " os.makedirs(str(os.path.dirname(one_test_pose_config_file_path))) # create parentdir 'test\n", + "\n", " # copy test and train config from base project to this subdir\n", " # copy base train config file\n", - " shutil.copyfile(list_base_train_pose_config_file_paths[j],\n", - " one_train_pose_config_file_path) \n", + " shutil.copyfile(list_base_train_pose_config_file_paths[j], one_train_pose_config_file_path)\n", " # copy base test config file\n", - " shutil.copyfile(list_base_test_pose_config_file_paths[j],\n", - " one_test_pose_config_file_path)\n" + " shutil.copyfile(list_base_test_pose_config_file_paths[j], one_test_pose_config_file_path)" ] }, { @@ -514,25 +546,27 @@ }, "outputs": [], "source": [ - "\n", - "model_prefix = 'data_augm_00_base'\n", + "model_prefix = \"data_augm_00_base\"\n", "\n", "## Initialise dict with additional edits to train config: optimizer\n", "train_edits_dict = {}\n", - "dict_optimizer = {'optimizer':'adam',\n", - " 'batch_size': 8, # the gpu I'm using has plenty of memory so batch size 8 makes sense\n", - " 'multi_step': [[1e-4, 7500], [5 * 1e-5, 12000], [1e-5, 150000]]} # if no yaml file passed, initialise as an empty dict\n", - "train_edits_dict.update({'optimizer': dict_optimizer['optimizer'], #'adam',\n", - " 'batch_size': dict_optimizer['batch_size'],\n", - " 'multi_step': dict_optimizer['multi_step']})\n", - "\n", - "for shuffle, trainingsetindex in zip(shuffles,trainingsetindices):\n", - " one_train_pose_config_file_path,\\\n", - " _,\\\n", - " _ = deeplabcut.return_train_network_path(config_path,\n", - " shuffle=shuffle,\n", - " trainingsetindex=trainingsetindex,\n", - " modelprefix=model_prefix)\n", + "dict_optimizer = {\n", + " \"optimizer\": \"adam\",\n", + " \"batch_size\": 8, # the gpu I'm using has plenty of memory so batch size 8 makes sense\n", + " \"multi_step\": [[1e-4, 7500], [5 * 1e-5, 12000], [1e-5, 150000]],\n", + "} # if no yaml file passed, initialise as an empty dict\n", + "train_edits_dict.update(\n", + " {\n", + " \"optimizer\": dict_optimizer[\"optimizer\"], #'adam',\n", + " \"batch_size\": dict_optimizer[\"batch_size\"],\n", + " \"multi_step\": dict_optimizer[\"multi_step\"],\n", + " }\n", + ")\n", + "\n", + "for shuffle, trainingsetindex in zip(shuffles, trainingsetindices):\n", + " one_train_pose_config_file_path, _, _ = deeplabcut.return_train_network_path(\n", + " config_path, shuffle=shuffle, trainingsetindex=trainingsetindex, modelprefix=model_prefix\n", + " )\n", "\n", " edit_config(str(one_train_pose_config_file_path), train_edits_dict)" ] @@ -556,16 +590,17 @@ "outputs": [], "source": [ "import deeplabcut\n", + "\n", "# define config path and model prefix\n", - "config_path='/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml'\n", - "model_prefix = 'data_augm_00_base'\n", + "config_path = \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml\"\n", + "model_prefix = \"data_augm_00_base\"\n", "\n", "# the computer I'm working on has several gpus, here I used the third one.\n", - "gputouse=3\n", + "gputouse = 3\n", "\n", "# define shuffles and trainingsetindices\n", - "shuffles = [1,2,3,4]\n", - "trainingsetindices = [0,1,2,3]\n", + "shuffles = [1, 2, 3, 4]\n", + "trainingsetindices = [0, 1, 2, 3]\n", "\n", "# loop over shuffles and train each\n", "for shuffle, trainingsetindex in zip(shuffles, trainingsetindices):\n", @@ -575,8 +610,8 @@ " modelprefix=model_prefix,\n", " gputouse=gputouse,\n", " trainingsetindex=trainingsetindex,\n", - " max_snapshots_to_keep=3, # training for 150000 iterations so let's save 50, 100, and 150.\n", - " saveiters=50000\n", + " max_snapshots_to_keep=3, # training for 150000 iterations so let's save 50, 100, and 150.\n", + " saveiters=50000,\n", " )" ] }, @@ -604,11 +639,11 @@ "# sure deeplabcut is imported and the config_path defined\n", "import deeplabcut\n", "\n", - "config_path = '/home/juser/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml'\n", + "config_path = \"/home/juser/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml\"\n", "\n", "from deeplabcut.utils.auxiliaryfunctions import read_config, edit_config\n", "\n", - "edit_config(config_path,{'snapshotindex':'all'})" + "edit_config(config_path, {\"snapshotindex\": \"all\"})" ] }, { @@ -629,13 +664,16 @@ "outputs": [], "source": [ "import deeplabcut\n", + "\n", "config_path = \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml\"\n", - "model_prefix = 'data_augm_00_base'\n", - "Shuffles = [1,2,3,4]\n", - "trainingsetindices = [0,1,2,3]\n", + "model_prefix = \"data_augm_00_base\"\n", + "Shuffles = [1, 2, 3, 4]\n", + "trainingsetindices = [0, 1, 2, 3]\n", "\n", - "for shuffle, trainingsetindex in zip(Shuffles,trainingsetindices):\n", - " deeplabcut.evaluate_network(config_path, modelprefix = model_prefix, Shuffles = [shuffle], trainingsetindex=trainingsetindex)" + "for shuffle, trainingsetindex in zip(Shuffles, trainingsetindices):\n", + " deeplabcut.evaluate_network(\n", + " config_path, modelprefix=model_prefix, Shuffles=[shuffle], trainingsetindex=trainingsetindex\n", + " )" ] }, { @@ -663,22 +701,25 @@ "import deeplabcut\n", "\n", "config_path = \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml\"\n", - "model_prefix = 'data_augm_00_base'\n", - "Shuffles = [1,2,3,4]\n", - "trainingsetindices = [0,1,2,3]\n", + "model_prefix = \"data_augm_00_base\"\n", + "Shuffles = [1, 2, 3, 4]\n", + "trainingsetindices = [0, 1, 2, 3]\n", "\n", "# We need pandas for creatig a nice list to parse\n", "import pandas as pd\n", "\n", "import sys\n", - "sys.path.append('..') #my python file for this function is stored in the parent folder as I'm running this\n", - "from getErrorDistribution import getErrorDistribution #import the getErrorDistribution function\n", + "\n", + "sys.path.append(\"..\") # my python file for this function is stored in the parent folder as I'm running this\n", + "from getErrorDistribution import getErrorDistribution # import the getErrorDistribution function\n", "import numpy as np\n", "\n", "# Read the h5 file containing all the frames, (project_folder/training-datasets/iteration-0/UnaufmentedDataSet_project_folder/CollectedData_LabelerName.h5)\n", - "df = pd.read_hdf('/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/training-datasets/iteration-0/UnaugmentedDataSet_bat_augmentation_austin_2020_bat_dataAug18/CollectedData_DLC.h5')\n", + "df = pd.read_hdf(\n", + " \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/training-datasets/iteration-0/UnaugmentedDataSet_bat_augmentation_austin_2020_bat_dataAug18/CollectedData_DLC.h5\"\n", + ")\n", "\n", - "image_paths = df.index.to_list() # turn dataframe into list\n", + "image_paths = df.index.to_list() # turn dataframe into list\n", "\n", "# get test indices\n", "test_inds = []\n", @@ -689,30 +730,25 @@ "error_distributions = []\n", "error_distributions_pcut = []\n", "\n", - "for shuffle, trainFractionIndex in zip(Shuffles,trainingsetindices):\n", - " error_distributions_temp = []\n", - " error_distributions_pcut_temp = []\n", - " for snapshot in [0,1,2]: #we saved three snapshots, one at 50k iteratinos, one at 100k, and one at 150k\n", - " (\n", - " ErrorDistribution_all,\n", - " ErrorDistribution_test,\n", - " ErrorDistribution_train,\n", - " ErrorDistributionPCutOff_all,\n", - " _,\n", - " _\n", - " ) = getErrorDistribution(\n", - " config_path,\n", - " shuffle=shuffle,\n", - " snapindex=snapshot,\n", - " trainFractionIndex = trainFractionIndex,\n", - " modelprefix = model_prefix\n", - " )\n", - " error_distributions_temp.append(ErrorDistribution_all.iloc[test_inds].values.flatten())\n", - " error_distributions_pcut_temp.append(ErrorDistributionPCutOff_all.iloc[test_inds].values.flatten())\n", - " error_distributions.append(error_distributions_temp)\n", - " error_distributions_pcut.append(error_distributions_pcut_temp)\n", - "error_distributionsb = np.array(error_distributions) # array with dimensions [shuffle, snapshot, frames]\n", - "error_distributions_pcut = np.array(error_distributions_pcut) # array with dimensions [shuffle, snapshot, frames]" + "for shuffle, trainFractionIndex in zip(Shuffles, trainingsetindices):\n", + " error_distributions_temp = []\n", + " error_distributions_pcut_temp = []\n", + " for snapshot in [0, 1, 2]: # we saved three snapshots, one at 50k iteratinos, one at 100k, and one at 150k\n", + " (ErrorDistribution_all, ErrorDistribution_test, ErrorDistribution_train, ErrorDistributionPCutOff_all, _, _) = (\n", + " getErrorDistribution(\n", + " config_path,\n", + " shuffle=shuffle,\n", + " snapindex=snapshot,\n", + " trainFractionIndex=trainFractionIndex,\n", + " modelprefix=model_prefix,\n", + " )\n", + " )\n", + " error_distributions_temp.append(ErrorDistribution_all.iloc[test_inds].values.flatten())\n", + " error_distributions_pcut_temp.append(ErrorDistributionPCutOff_all.iloc[test_inds].values.flatten())\n", + " error_distributions.append(error_distributions_temp)\n", + " error_distributions_pcut.append(error_distributions_pcut_temp)\n", + "error_distributionsb = np.array(error_distributions) # array with dimensions [shuffle, snapshot, frames]\n", + "error_distributions_pcut = np.array(error_distributions_pcut) # array with dimensions [shuffle, snapshot, frames]" ] }, { @@ -723,32 +759,39 @@ "source": [ "import matplotlib.pyplot as plt\n", "\n", - "fig, (ax1,ax2) = plt.subplots(1,2)\n", + "fig, (ax1, ax2) = plt.subplots(1, 2)\n", "fig.set_figheight(8)\n", "fig.set_figwidth(10)\n", "\n", - "for shuffle in [0,1,2,3]: #we start counting at 0, so for now, let's consider each index one less\n", - " ax1.errorbar(np.array([50,100,150])-1.5+shuffle,np.nanmean(error_distributions[shuffle,:],axis=1), np.nanstd(error_distributions[shuffle,:],axis=1)/len(test_inds)**.5)\n", - " ax2.errorbar(np.array([50,100,150])-1.5+shuffle,np.nanmean(error_distributions_pcut[shuffle,:],axis=1), np.nanstd(error_distributions_pcut[shuffle,:],axis=1)/len(test_inds)**.5)\n", + "for shuffle in [0, 1, 2, 3]: # we start counting at 0, so for now, let's consider each index one less\n", + " ax1.errorbar(\n", + " np.array([50, 100, 150]) - 1.5 + shuffle,\n", + " np.nanmean(error_distributions[shuffle, :], axis=1),\n", + " np.nanstd(error_distributions[shuffle, :], axis=1) / len(test_inds) ** 0.5,\n", + " )\n", + " ax2.errorbar(\n", + " np.array([50, 100, 150]) - 1.5 + shuffle,\n", + " np.nanmean(error_distributions_pcut[shuffle, :], axis=1),\n", + " np.nanstd(error_distributions_pcut[shuffle, :], axis=1) / len(test_inds) ** 0.5,\n", + " )\n", "\n", "ax1.set_xticks([50, 100, 150])\n", "ax2.set_xticks([50, 100, 150])\n", - "ax1.set_xlim([25,175])\n", - "ax2.set_xlim([25,175])\n", - "ax1.set_ylim([0,23])\n", - "ax2.set_ylim([0,23])\n", + "ax1.set_xlim([25, 175])\n", + "ax2.set_xlim([25, 175])\n", + "ax1.set_ylim([0, 23])\n", + "ax2.set_ylim([0, 23])\n", "ax1.set_title(\"Without P-cut\")\n", "ax2.set_title(\"With P-cut 0.6\")\n", "ax2.set_yticklabels([])\n", - "ax2.legend([\"half, OOD\",\"half, Ref\",\"full, OOD\", \"full, Ref\"])\n", + "ax2.legend([\"half, OOD\", \"half, Ref\", \"full, OOD\", \"full, Ref\"])\n", "\n", "# add a big axis, hide frame\n", "fig.add_subplot(111, frameon=False)\n", "## hide tick and tick label of the big axis\n", - "plt.tick_params(labelcolor='none', which='both', top=False, bottom=False, left=False, right=False)\n", + "plt.tick_params(labelcolor=\"none\", which=\"both\", top=False, bottom=False, left=False, right=False)\n", "plt.xlabel(\"Iterations (thousands)\")\n", - "plt.ylabel(\"Error (px)\")\n", - "\n" + "plt.ylabel(\"Error (px)\")" ] }, { @@ -780,13 +823,16 @@ "import numpy as np\n", "from scipy.stats import wilcoxon\n", "\n", - "p_value = np.empty((4,4))\n", - "p_value[:]=np.NaN\n", + "p_value = np.empty((4, 4))\n", + "p_value[:] = np.NaN\n", "\n", - "for i in [0,1,2,3]:\n", - " for j in [0,1,2,3]:\n", - " if j<=i: continue\n", - " _, p_value[i,j] = wilcoxon(x = error_distributions_pcut[i,-1,:], y = error_distributions_pcut[j,-1,:],nan_policy='omit')\n", + "for i in [0, 1, 2, 3]:\n", + " for j in [0, 1, 2, 3]:\n", + " if j <= i:\n", + " continue\n", + " _, p_value[i, j] = wilcoxon(\n", + " x=error_distributions_pcut[i, -1, :], y=error_distributions_pcut[j, -1, :], nan_policy=\"omit\"\n", + " )\n", "\n", "print(p_value)" ] @@ -812,22 +858,25 @@ "import deeplabcut\n", "\n", "config_path = \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml\"\n", - "model_prefix = 'data_augm_00_base'\n", - "Shuffles = [1,2,3,4]\n", - "trainingsetindices = [0,1,2,3]\n", + "model_prefix = \"data_augm_00_base\"\n", + "Shuffles = [1, 2, 3, 4]\n", + "trainingsetindices = [0, 1, 2, 3]\n", "\n", "# We need pandas for creatig a nice list to parse\n", "import pandas as pd\n", "\n", "import sys\n", - "sys.path.append('..') #my python file for this function is stored in the parent folder as I'm running this\n", - "from getErrorDistribution import getErrorDistribution #import the getErrorDistribution function\n", + "\n", + "sys.path.append(\"..\") # my python file for this function is stored in the parent folder as I'm running this\n", + "from getErrorDistribution import getErrorDistribution # import the getErrorDistribution function\n", "import numpy as np\n", "\n", "# Read the h5 file containing all the frames, (project_folder/training-datasets/iteration-0/UnaufmentedDataSet_project_folder/CollectedData_LabelerName.h5)\n", - "df = pd.read_hdf('/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/training-datasets/iteration-0/UnaugmentedDataSet_bat_augmentation_austin_2020_bat_dataAug18/CollectedData_DLC.h5')\n", + "df = pd.read_hdf(\n", + " \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/training-datasets/iteration-0/UnaugmentedDataSet_bat_augmentation_austin_2020_bat_dataAug18/CollectedData_DLC.h5\"\n", + ")\n", "\n", - "image_paths = df.index.to_list() # turn dataframe into list\n", + "image_paths = df.index.to_list() # turn dataframe into list\n", "\n", "# get test indices\n", "test_inds = []\n", @@ -838,38 +887,41 @@ "# this gives us the paths of our 27 test videos\n", "test_paths = list(set([image_paths[i][1] for i in test_inds]))\n", "\n", - "#%% sorted so that the corresponding videos have the same index in three lists (one per camera)\n", - "test_paths_cam1 = ['TS5-544-Cam1_2020-06-25_000099Track8_50_test',\n", - " 'TS5-544-Cam1_2020-06-25_000103Track3_50_test',\n", - " 'TS5-544-Cam1_2020-06-25_000104Track3_50_test',\n", - " 'TS5-544-Cam1_2020-06-25_000108Track6_50_test',\n", - " 'TS5-544-Cam1_2020-06-25_000123Track6_50_test',\n", - " 'TS5-544-Cam1_2020-06-25_000128Track2_50_test',\n", - " 'TS5-544-Cam1_2020-06-25_000134Track5_50_test'\n", - " ]\n", - "test_paths_cam2 = ['IL5-519-Cam2_2020-06-25_000099Track6_50_test',\n", - " 'IL5-519-Cam2_2020-06-25_000103Track3_50_test',\n", - " 'IL5-519-Cam2_2020-06-25_000104Track2_50_test',\n", - " 'IL5-519-Cam2_2020-06-25_000109Track1_50_test',\n", - " 'IL5-519-Cam2_2020-06-25_000124Track9_50_test',\n", - " 'IL5-519-Cam2_2020-06-25_000130Track2_50_test',\n", - " 'IL5-519-Cam2_2020-06-25_000136Track10_50_test'\n", - " ]\n", - "test_paths_cam3 = ['IL5-534-Cam3_2020-06-25_000095Track14_50_test',\n", - " 'IL5-534-Cam3_2020-06-25_000100Track4_50_test',\n", - " 'IL5-534-Cam3_2020-06-25_000101Track4_50_test',\n", - " 'IL5-534-Cam3_2020-06-25_000106Track3_50_test',\n", - " 'IL5-534-Cam3_2020-06-25_000122Track7_50_test',\n", - " 'IL5-534-Cam3_2020-06-25_000127Track4_50_test',\n", - " 'IL5-534-Cam3_2020-06-25_000133Track9_50_test'\n", - " ]\n", - "\n", - "nvideos = 7 # number of videos\n", + "# %% sorted so that the corresponding videos have the same index in three lists (one per camera)\n", + "test_paths_cam1 = [\n", + " \"TS5-544-Cam1_2020-06-25_000099Track8_50_test\",\n", + " \"TS5-544-Cam1_2020-06-25_000103Track3_50_test\",\n", + " \"TS5-544-Cam1_2020-06-25_000104Track3_50_test\",\n", + " \"TS5-544-Cam1_2020-06-25_000108Track6_50_test\",\n", + " \"TS5-544-Cam1_2020-06-25_000123Track6_50_test\",\n", + " \"TS5-544-Cam1_2020-06-25_000128Track2_50_test\",\n", + " \"TS5-544-Cam1_2020-06-25_000134Track5_50_test\",\n", + "]\n", + "test_paths_cam2 = [\n", + " \"IL5-519-Cam2_2020-06-25_000099Track6_50_test\",\n", + " \"IL5-519-Cam2_2020-06-25_000103Track3_50_test\",\n", + " \"IL5-519-Cam2_2020-06-25_000104Track2_50_test\",\n", + " \"IL5-519-Cam2_2020-06-25_000109Track1_50_test\",\n", + " \"IL5-519-Cam2_2020-06-25_000124Track9_50_test\",\n", + " \"IL5-519-Cam2_2020-06-25_000130Track2_50_test\",\n", + " \"IL5-519-Cam2_2020-06-25_000136Track10_50_test\",\n", + "]\n", + "test_paths_cam3 = [\n", + " \"IL5-534-Cam3_2020-06-25_000095Track14_50_test\",\n", + " \"IL5-534-Cam3_2020-06-25_000100Track4_50_test\",\n", + " \"IL5-534-Cam3_2020-06-25_000101Track4_50_test\",\n", + " \"IL5-534-Cam3_2020-06-25_000106Track3_50_test\",\n", + " \"IL5-534-Cam3_2020-06-25_000122Track7_50_test\",\n", + " \"IL5-534-Cam3_2020-06-25_000127Track4_50_test\",\n", + " \"IL5-534-Cam3_2020-06-25_000133Track9_50_test\",\n", + "]\n", + "\n", + "nvideos = 7 # number of videos\n", "\n", "# get test frame indexes per camera\n", - "test_inds_cam1 = [[],[],[],[],[],[],[]]\n", - "test_inds_cam2 = [[],[],[],[],[],[],[]]\n", - "test_inds_cam3 = [[],[],[],[],[],[],[]]\n", + "test_inds_cam1 = [[], [], [], [], [], [], []]\n", + "test_inds_cam2 = [[], [], [], [], [], [], []]\n", + "test_inds_cam3 = [[], [], [], [], [], [], []]\n", "\n", "for i, path in enumerate(image_paths):\n", " for j in range(nvideos):\n", @@ -882,67 +934,91 @@ "\n", "nshuffles = len(Shuffles)\n", "\n", - "#pre-allocate matrixes for mean values and standard errors\n", - "mean_cam1 = np.zeros([nshuffles,nvideos]) # shuffle x movie\n", - "mean_cam2 = np.zeros([nshuffles,nvideos])\n", - "mean_cam3 = np.zeros([nshuffles,nvideos])\n", + "# pre-allocate matrixes for mean values and standard errors\n", + "mean_cam1 = np.zeros([nshuffles, nvideos]) # shuffle x movie\n", + "mean_cam2 = np.zeros([nshuffles, nvideos])\n", + "mean_cam3 = np.zeros([nshuffles, nvideos])\n", "\n", - "ste_cam1 = np.zeros([nshuffles,nvideos])\n", - "ste_cam2 = np.zeros([nshuffles,nvideos])\n", - "ste_cam3 = np.zeros([nshuffles,nvideos])\n", + "ste_cam1 = np.zeros([nshuffles, nvideos])\n", + "ste_cam2 = np.zeros([nshuffles, nvideos])\n", + "ste_cam3 = np.zeros([nshuffles, nvideos])\n", "\n", - "meanPcut_cam1 = np.zeros([nshuffles,nvideos]) # shuffle x movie\n", - "meanPcut_cam2 = np.zeros([nshuffles,nvideos])\n", - "meanPcut_cam3 = np.zeros([nshuffles,nvideos])\n", + "meanPcut_cam1 = np.zeros([nshuffles, nvideos]) # shuffle x movie\n", + "meanPcut_cam2 = np.zeros([nshuffles, nvideos])\n", + "meanPcut_cam3 = np.zeros([nshuffles, nvideos])\n", "\n", - "stePcut_cam1 = np.zeros([nshuffles,nvideos])\n", - "stePcut_cam2 = np.zeros([nshuffles,nvideos])\n", - "stePcut_cam3 = np.zeros([nshuffles,nvideos])\n", + "stePcut_cam1 = np.zeros([nshuffles, nvideos])\n", + "stePcut_cam2 = np.zeros([nshuffles, nvideos])\n", + "stePcut_cam3 = np.zeros([nshuffles, nvideos])\n", "\n", "# %%\n", "\n", - "for i, shuffle in enumerate(Shuffles): \n", + "for i, shuffle in enumerate(Shuffles):\n", " trainFractionIndex = i\n", - " snapshot=-1\n", - " (\n", - " ErrorDistribution_all,\n", - " _,\n", - " _,\n", - " ErrorDistributionPCutOff_all,\n", - " _,\n", - " _\n", - " ) = getErrorDistribution(\n", + " snapshot = -1\n", + " (ErrorDistribution_all, _, _, ErrorDistributionPCutOff_all, _, _) = getErrorDistribution(\n", " config_path,\n", " shuffle=shuffle,\n", " snapindex=snapshot,\n", - " trainFractionIndex = trainFractionIndex,\n", - " modelprefix = model_prefix\n", + " trainFractionIndex=trainFractionIndex,\n", + " modelprefix=model_prefix,\n", " )\n", " for movie_number in range(7):\n", - "\n", - " meanPcut_cam1[i,movie_number] = np.nanmean(ErrorDistributionPCutOff_all.values[test_inds_cam1[movie_number]][:])\n", - " stePcut_cam1[i,movie_number] = np.nanstd(ErrorDistributionPCutOff_all.values[test_inds_cam1[movie_number]][:])/(ErrorDistribution_all.values[test_inds_cam1[movie_number]][:].size**.5)\n", - "\n", - " meanPcut_cam2[i,movie_number] = np.nanmean(ErrorDistributionPCutOff_all.values[test_inds_cam2[movie_number]][:])\n", - " stePcut_cam2[i,movie_number] = np.nanstd(ErrorDistributionPCutOff_all.values[test_inds_cam1[movie_number]][:])/(ErrorDistribution_all.values[test_inds_cam2[movie_number]][:].size**.5)\n", - "\n", - " meanPcut_cam3[i,movie_number] = np.nanmean(ErrorDistributionPCutOff_all.values[test_inds_cam3[movie_number]][:])\n", - " stePcut_cam3[i,movie_number] = np.nanstd(ErrorDistributionPCutOff_all.values[test_inds_cam1[movie_number]][:])/(ErrorDistribution_all.values[test_inds_cam3[movie_number]][:].size**.5)\n", - "\n", - "fig, (ax1,ax2,ax3) = plt.subplots(3,1)\n", + " meanPcut_cam1[i, movie_number] = np.nanmean(\n", + " ErrorDistributionPCutOff_all.values[test_inds_cam1[movie_number]][:]\n", + " )\n", + " stePcut_cam1[i, movie_number] = np.nanstd(\n", + " ErrorDistributionPCutOff_all.values[test_inds_cam1[movie_number]][:]\n", + " ) / (ErrorDistribution_all.values[test_inds_cam1[movie_number]][:].size ** 0.5)\n", + "\n", + " meanPcut_cam2[i, movie_number] = np.nanmean(\n", + " ErrorDistributionPCutOff_all.values[test_inds_cam2[movie_number]][:]\n", + " )\n", + " stePcut_cam2[i, movie_number] = np.nanstd(\n", + " ErrorDistributionPCutOff_all.values[test_inds_cam1[movie_number]][:]\n", + " ) / (ErrorDistribution_all.values[test_inds_cam2[movie_number]][:].size ** 0.5)\n", + "\n", + " meanPcut_cam3[i, movie_number] = np.nanmean(\n", + " ErrorDistributionPCutOff_all.values[test_inds_cam3[movie_number]][:]\n", + " )\n", + " stePcut_cam3[i, movie_number] = np.nanstd(\n", + " ErrorDistributionPCutOff_all.values[test_inds_cam1[movie_number]][:]\n", + " ) / (ErrorDistribution_all.values[test_inds_cam3[movie_number]][:].size ** 0.5)\n", + "\n", + "fig, (ax1, ax2, ax3) = plt.subplots(3, 1)\n", "fig.set_figheight(15)\n", "fig.set_figwidth(10)\n", "for i, shuffle in enumerate(Shuffles):\n", - " \n", " # to jitter the error bars to keep them from overlapping\n", - " movie_number = list(range(1,8))\n", - " movie_number = [x - 2/50 + shuffle/50 for x in movie_number]\n", - " \n", - " ax1.errorbar(movie_number,meanPcut_cam1[i,:], stePcut_cam1[i,:,])\n", + " movie_number = list(range(1, 8))\n", + " movie_number = [x - 2 / 50 + shuffle / 50 for x in movie_number]\n", + "\n", + " ax1.errorbar(\n", + " movie_number,\n", + " meanPcut_cam1[i, :],\n", + " stePcut_cam1[\n", + " i,\n", + " :,\n", + " ],\n", + " )\n", "\n", - " ax2.errorbar(movie_number,meanPcut_cam2[i,:], stePcut_cam2[i,:,])\n", + " ax2.errorbar(\n", + " movie_number,\n", + " meanPcut_cam2[i, :],\n", + " stePcut_cam2[\n", + " i,\n", + " :,\n", + " ],\n", + " )\n", "\n", - " ax3.errorbar(movie_number,meanPcut_cam3[i,:], stePcut_cam3[i,:,])\n", + " ax3.errorbar(\n", + " movie_number,\n", + " meanPcut_cam3[i, :],\n", + " stePcut_cam3[\n", + " i,\n", + " :,\n", + " ],\n", + " )\n", "\n", "ax1.set_ylim([0, 50])\n", "ax2.set_ylim([0, 50])\n", @@ -953,7 +1029,7 @@ "ax1.set_ylabel(\"Error (px\")\n", "ax2.set_ylabel(\"Error (px\")\n", "ax3.set_ylabel(\"Error (px\")\n", - "ax1.legend([\"half, OOD\",\"half, Ref\",\"full, OOD\", \"full, Ref\"])" + "ax1.legend([\"half, OOD\", \"half, Ref\", \"full, OOD\", \"full, Ref\"])" ] }, { @@ -989,18 +1065,20 @@ "# sure deeplabcut is imported and the config_path defined\n", "import deeplabcut\n", "\n", - "config_path = '/home/jusers/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml'\n", - "model_prefix = 'data_augm_00_base'\n", + "config_path = \"/home/jusers/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml\"\n", + "model_prefix = \"data_augm_00_base\"\n", "\n", "# we only want to plot the last snapshot (150k iterations)\n", "from deeplabcut.utils.auxiliaryfunctions import read_config, edit_config\n", "\n", - "edit_config(config_path,{'snapshotindex':-1})\n", + "edit_config(config_path, {\"snapshotindex\": -1})\n", "\n", "shuffle = 3\n", "trainingsetindex = 2\n", "\n", - "deeplabcut.evaluate_network(config_path, modelprefix = model_prefix, Shuffles = [shuffle], trainingsetindex=trainingsetindex, plotting=True)" + "deeplabcut.evaluate_network(\n", + " config_path, modelprefix=model_prefix, Shuffles=[shuffle], trainingsetindex=trainingsetindex, plotting=True\n", + ")" ] }, { @@ -1048,46 +1126,50 @@ "# sure deeplabcut is imported and the config_path defined\n", "import deeplabcut\n", "\n", - "config_path = '/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml'\n", + "config_path = \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml\"\n", "\n", "# we also need the package os for folder manipulation\n", "import os\n", + "\n", "# and shutil for copying files\n", "import shutil\n", "\n", - "#import tools for reading our config file\n", + "# import tools for reading our config file\n", "from deeplabcut.utils.auxiliaryfunctions import read_config\n", "\n", "# Number and name for our model folder\n", - "model_number = 1 # CHANGE\n", - "modelprefix_pre = 'data_augm'\n", - "daug_str = 'fliplr' # CHANGE\n", + "model_number = 1 # CHANGE\n", + "modelprefix_pre = \"data_augm\"\n", + "daug_str = \"fliplr\" # CHANGE\n", "\n", "# Get config as dict and associated paths\n", "cfg = read_config(config_path)\n", - "project_path = cfg[\"project_path\"] # or: os.path.dirname(config_path) #dlc_models_path = os.path.join(project_path, \"dlc-models\")\n", + "project_path = cfg[\n", + " \"project_path\"\n", + "] # or: os.path.dirname(config_path) #dlc_models_path = os.path.join(project_path, \"dlc-models\")\n", "training_datasets_path = os.path.join(project_path, \"training-datasets\")\n", "\n", "# Define shuffles\n", - "shuffles = [1,2,3,4]\n", + "shuffles = [1, 2, 3, 4]\n", "trainingsetindices = [0, 1, 2, 3]\n", "\n", "# Get train and test pose config file paths from base project, for each shuffle\n", "list_base_train_pose_config_file_paths = []\n", "list_base_test_pose_config_file_paths = []\n", "for shuffle_number, trainingsetindex in zip(shuffles, trainingsetindices):\n", - " base_train_pose_config_file_path_TEMP,\\\n", - " base_test_pose_config_file_path_TEMP,\\\n", - " _ = deeplabcut.return_train_network_path(config_path,\n", - " shuffle=shuffle_number,\n", - " trainingsetindex=trainingsetindex) # base_train_pose_config_file\n", + " base_train_pose_config_file_path_TEMP, base_test_pose_config_file_path_TEMP, _ = (\n", + " deeplabcut.return_train_network_path(config_path, shuffle=shuffle_number, trainingsetindex=trainingsetindex)\n", + " ) # base_train_pose_config_file\n", " list_base_train_pose_config_file_paths.append(base_train_pose_config_file_path_TEMP)\n", " list_base_test_pose_config_file_paths.append(base_test_pose_config_file_path_TEMP)\n", "\n", "# Create subdirs for this augmentation method\n", - "model_prefix = '_'.join([modelprefix_pre, \"{0:0=2d}\".format(model_number), daug_str]) # modelprefix_pre = aug_\n", + "model_prefix = \"_\".join([modelprefix_pre, \"{0:0=2d}\".format(model_number), daug_str]) # modelprefix_pre = aug_\n", "aug_project_path = os.path.join(project_path, model_prefix)\n", - "aug_dlc_models = os.path.join(aug_project_path, \"dlc-models\", )\n", + "aug_dlc_models = os.path.join(\n", + " aug_project_path,\n", + " \"dlc-models\",\n", + ")\n", "\n", "# make the folder for this modelprefix\n", "try:\n", @@ -1097,25 +1179,20 @@ " print(\"Skipping this one as it already exists\")\n", "\n", "# Copy base train pose config file to the directory of this augmentation method\n", - "for j, (shuffle, trainingsetindex) in enumerate(zip(shuffles,trainingsetindices)):\n", - " one_train_pose_config_file_path,\\\n", - " one_test_pose_config_file_path,\\\n", - " _ = deeplabcut.return_train_network_path(config_path,\n", - " shuffle=shuffle,\n", - " trainingsetindex=trainingsetindex,\n", - " modelprefix=model_prefix)\n", - " \n", + "for j, (shuffle, trainingsetindex) in enumerate(zip(shuffles, trainingsetindices)):\n", + " one_train_pose_config_file_path, one_test_pose_config_file_path, _ = deeplabcut.return_train_network_path(\n", + " config_path, shuffle=shuffle, trainingsetindex=trainingsetindex, modelprefix=model_prefix\n", + " )\n", + "\n", " # make train and test directories for this subdir\n", - " os.makedirs(str(os.path.dirname(one_train_pose_config_file_path))) # create parentdir 'train'\n", - " os.makedirs(str(os.path.dirname(one_test_pose_config_file_path))) # create parentdir 'test\n", - " \n", + " os.makedirs(str(os.path.dirname(one_train_pose_config_file_path))) # create parentdir 'train'\n", + " os.makedirs(str(os.path.dirname(one_test_pose_config_file_path))) # create parentdir 'test\n", + "\n", " # copy test and train config from base project to this subdir\n", " # copy base train config file\n", - " shutil.copyfile(list_base_train_pose_config_file_paths[j],\n", - " one_train_pose_config_file_path) \n", + " shutil.copyfile(list_base_train_pose_config_file_paths[j], one_train_pose_config_file_path)\n", " # copy base test config file\n", - " shutil.copyfile(list_base_test_pose_config_file_paths[j],\n", - " one_test_pose_config_file_path)" + " shutil.copyfile(list_base_test_pose_config_file_paths[j], one_test_pose_config_file_path)" ] }, { @@ -1168,32 +1245,35 @@ }, "outputs": [], "source": [ - "#import tools for changing our config file\n", + "# import tools for changing our config file\n", "from deeplabcut.utils.auxiliaryfunctions import edit_config\n", "\n", - "model_prefix = 'data_augm_01_fliplr'\n", + "model_prefix = \"data_augm_01_fliplr\"\n", "\n", "## Initialise dict with additional edits to train config: optimizer\n", "train_edits_dict = {}\n", - "dict_optimizer = {'optimizer':'adam',\n", - " 'batch_size': 8, # the gpu I'm using has plenty of memory so batch size 8 makes sense\n", - " 'multi_step': [[1e-4, 7500], [5 * 1e-5, 12000], [1e-5, 150000]]} # if no yaml file passed, initialise as an empty dict\n", - "train_edits_dict.update({'optimizer': dict_optimizer['optimizer'], #'adam',\n", - " 'batch_size': dict_optimizer['batch_size'],\n", - " 'multi_step': dict_optimizer['multi_step']})\n", + "dict_optimizer = {\n", + " \"optimizer\": \"adam\",\n", + " \"batch_size\": 8, # the gpu I'm using has plenty of memory so batch size 8 makes sense\n", + " \"multi_step\": [[1e-4, 7500], [5 * 1e-5, 12000], [1e-5, 150000]],\n", + "} # if no yaml file passed, initialise as an empty dict\n", + "train_edits_dict.update(\n", + " {\n", + " \"optimizer\": dict_optimizer[\"optimizer\"], #'adam',\n", + " \"batch_size\": dict_optimizer[\"batch_size\"],\n", + " \"multi_step\": dict_optimizer[\"multi_step\"],\n", + " }\n", + ")\n", "\n", "# Augmentation edits\n", "edits_dict = dict()\n", "edits_dict[\"symmetric_pairs\"] = (0, 14), (1, 12), (2, 13), (3, 11), (4, 9), (5, 10)\n", "edits_dict[\"fliplr\"] = True\n", "\n", - "for shuffle, trainingsetindex in zip(shuffles,trainingsetindices):\n", - " one_train_pose_config_file_path,\\\n", - " _,\\\n", - " _ = deeplabcut.return_train_network_path(config_path,\n", - " shuffle=shuffle,\n", - " trainingsetindex=trainingsetindex,\n", - " modelprefix=model_prefix)\n", + "for shuffle, trainingsetindex in zip(shuffles, trainingsetindices):\n", + " one_train_pose_config_file_path, _, _ = deeplabcut.return_train_network_path(\n", + " config_path, shuffle=shuffle, trainingsetindex=trainingsetindex, modelprefix=model_prefix\n", + " )\n", "\n", " edit_config(str(one_train_pose_config_file_path), edits_dict)\n", " edit_config(str(one_train_pose_config_file_path), train_edits_dict)" @@ -1218,16 +1298,17 @@ "outputs": [], "source": [ "import deeplabcut\n", + "\n", "# define config path and model prefix\n", - "config_path='/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml'\n", - "model_prefix = 'data_augm_01_flipr'\n", + "config_path = \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml\"\n", + "model_prefix = \"data_augm_01_flipr\"\n", "\n", "# the computer I'm working on has several gpus, here I used the third one.\n", - "gputouse=3\n", + "gputouse = 3\n", "\n", "# define shuffles and trainingsetindices\n", - "shuffles = [1,2,3,4]\n", - "trainingsetindices = [0,1,2,3]\n", + "shuffles = [1, 2, 3, 4]\n", + "trainingsetindices = [0, 1, 2, 3]\n", "\n", "# loop over shuffles and train each\n", "for shuffle, trainingsetindex in zip(shuffles, trainingsetindices):\n", @@ -1237,8 +1318,8 @@ " modelprefix=model_prefix,\n", " gputouse=gputouse,\n", " trainingsetindex=trainingsetindex,\n", - " max_snapshots_to_keep=3, # training for 150000 iterations so let's save 50, 100, and 150.\n", - " saveiters=50000\n", + " max_snapshots_to_keep=3, # training for 150000 iterations so let's save 50, 100, and 150.\n", + " saveiters=50000,\n", " )" ] }, @@ -1268,18 +1349,20 @@ "import deeplabcut\n", "\n", "config_path = \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml\"\n", - "model_prefix = 'data_augm_01_fliplr'\n", - "Shuffles = [1,2,3,4]\n", - "trainingsetindices = [0,1,2,3]\n", + "model_prefix = \"data_augm_01_fliplr\"\n", + "Shuffles = [1, 2, 3, 4]\n", + "trainingsetindices = [0, 1, 2, 3]\n", "\n", - "#import tools for modifying our config file\n", + "# import tools for modifying our config file\n", "from deeplabcut.utils.auxiliaryfunctions import edit_config\n", "\n", "# make sure we are testing all snapshots\n", - "edit_config(config_path,{'snapshotindex':'all'})\n", + "edit_config(config_path, {\"snapshotindex\": \"all\"})\n", "\n", - "for shuffle, trainingsetindex in zip(Shuffles,trainingsetindices):\n", - " deeplabcut.evaluate_network(config_path, modelprefix = model_prefix, Shuffles = [shuffle], trainingsetindex=trainingsetindex)" + "for shuffle, trainingsetindex in zip(Shuffles, trainingsetindices):\n", + " deeplabcut.evaluate_network(\n", + " config_path, modelprefix=model_prefix, Shuffles=[shuffle], trainingsetindex=trainingsetindex\n", + " )" ] }, { @@ -1293,23 +1376,26 @@ "import deeplabcut\n", "\n", "config_path = \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml\"\n", - "model_prefix_base = 'data_augm_00_base'\n", - "model_prefix_augm = 'data_augm_01_fliplr'\n", - "Shuffles = [4,3] # let's start with the refined un-augmented, i.e. shuffle 4\n", - "trainingsetindices = [3,2]\n", + "model_prefix_base = \"data_augm_00_base\"\n", + "model_prefix_augm = \"data_augm_01_fliplr\"\n", + "Shuffles = [4, 3] # let's start with the refined un-augmented, i.e. shuffle 4\n", + "trainingsetindices = [3, 2]\n", "\n", "# We need pandas for creatig a nice list to parse\n", "import pandas as pd\n", "\n", "import sys\n", - "sys.path.append('..') #my python file for this function is stored in the parent folder as I'm running this\n", - "from getErrorDistribution import getErrorDistribution #import the getErrorDistribution function\n", + "\n", + "sys.path.append(\"..\") # my python file for this function is stored in the parent folder as I'm running this\n", + "from getErrorDistribution import getErrorDistribution # import the getErrorDistribution function\n", "import numpy as np\n", "\n", "# Read the h5 file containing all the frames, (project_folder/training-datasets/iteration-0/UnaufmentedDataSet_project_folder/CollectedData_LabelerName.h5)\n", - "df = pd.read_hdf('/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/training-datasets/iteration-0/UnaugmentedDataSet_bat_augmentation_austin_2020_bat_dataAug18/CollectedData_DLC.h5')\n", + "df = pd.read_hdf(\n", + " \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/training-datasets/iteration-0/UnaugmentedDataSet_bat_augmentation_austin_2020_bat_dataAug18/CollectedData_DLC.h5\"\n", + ")\n", "\n", - "image_paths = df.index.to_list() # turn dataframe into list\n", + "image_paths = df.index.to_list() # turn dataframe into list\n", "\n", "# get test indices\n", "test_inds = []\n", @@ -1319,39 +1405,39 @@ "\n", "error_distributions_pcut = []\n", "\n", - "for shuffle, trainFractionIndex in zip(Shuffles,trainingsetindices):\n", - " error_distributions_pcut_temp = []\n", - " if shuffle == 4: model_prefix = model_prefix_base\n", - " elif shuffle == 3: model_prefix = model_prefix_augm\n", - " \n", - " for snapshot in [0,1,2]: #we saved three snapshots, one at 50k iteratinos, one at 100k, and one at 150k\n", - " (\n", - " _,\n", - " _,\n", - " _,\n", - " ErrorDistributionPCutOff_all,\n", - " _,\n", - " _\n", - " ) = getErrorDistribution(\n", + "for shuffle, trainFractionIndex in zip(Shuffles, trainingsetindices):\n", + " error_distributions_pcut_temp = []\n", + " if shuffle == 4:\n", + " model_prefix = model_prefix_base\n", + " elif shuffle == 3:\n", + " model_prefix = model_prefix_augm\n", + "\n", + " for snapshot in [0, 1, 2]: # we saved three snapshots, one at 50k iteratinos, one at 100k, and one at 150k\n", + " (_, _, _, ErrorDistributionPCutOff_all, _, _) = getErrorDistribution(\n", " config_path,\n", " shuffle=shuffle,\n", " snapindex=snapshot,\n", - " trainFractionIndex = trainFractionIndex,\n", - " modelprefix = model_prefix\n", - " )\n", - " error_distributions_pcut_temp.append(ErrorDistributionPCutOff_all.iloc[test_inds].values.flatten())\n", - " error_distributions_pcut.append(error_distributions_pcut_temp)\n", + " trainFractionIndex=trainFractionIndex,\n", + " modelprefix=model_prefix,\n", + " )\n", + " error_distributions_pcut_temp.append(ErrorDistributionPCutOff_all.iloc[test_inds].values.flatten())\n", + " error_distributions_pcut.append(error_distributions_pcut_temp)\n", "\n", - "error_distributions_pcut = np.array(error_distributions_pcut) # array with dimensions [shuffle, snapshot, frames]\n", + "error_distributions_pcut = np.array(error_distributions_pcut) # array with dimensions [shuffle, snapshot, frames]\n", "\n", "import matplotlib.pyplot as plt\n", + "\n", "plt.figure(figsize=(10, 5))\n", - "for shuffle in [0,1]: #we start counting at 0, so for now, let's consider each index one less\n", - " plt.errorbar(np.array([50,100,150])-1.5+shuffle,np.nanmean(error_distributions_pcut[shuffle,:],axis=1), np.nanstd(error_distributions_pcut[shuffle,:],axis=1)/len(test_inds)**.5)\n", + "for shuffle in [0, 1]: # we start counting at 0, so for now, let's consider each index one less\n", + " plt.errorbar(\n", + " np.array([50, 100, 150]) - 1.5 + shuffle,\n", + " np.nanmean(error_distributions_pcut[shuffle, :], axis=1),\n", + " np.nanstd(error_distributions_pcut[shuffle, :], axis=1) / len(test_inds) ** 0.5,\n", + " )\n", "\n", "plt.xticks([50, 100, 150])\n", - "plt.xlim([25,175])\n", - "plt.ylim([0,10])\n", + "plt.xlim([25, 175])\n", + "plt.ylim([0, 10])\n", "plt.title(\"Error with P-cut 0.6, comparing baseline to fliplr and 180 degrees rotation augmented\")\n", "\n", "plt.legend([\"Full, ref, baseline\", \"Full, OOD, fliplr\"])\n", @@ -1390,51 +1476,54 @@ }, "outputs": [], "source": [ - "\n", "# in case we restarted the kernel or something, let's make\n", "# sure deeplabcut is imported and the config_path defined\n", "import deeplabcut\n", "\n", - "config_path = '/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml'\n", + "config_path = \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml\"\n", "\n", "# we also need the package os for folder manipulation\n", "import os\n", + "\n", "# and shutil for copying files\n", "import shutil\n", "\n", - "#import tools for reading our config file\n", + "# import tools for reading our config file\n", "from deeplabcut.utils.auxiliaryfunctions import read_config\n", "\n", "# Number and name for our model folder\n", - "model_number = 3 # CHANGE\n", - "modelprefix_pre = 'data_augm'\n", - "daug_str = 'max_rotate' # CHANGE\n", + "model_number = 3 # CHANGE\n", + "modelprefix_pre = \"data_augm\"\n", + "daug_str = \"max_rotate\" # CHANGE\n", "\n", "# Get config as dict and associated paths\n", "cfg = read_config(config_path)\n", - "project_path = cfg[\"project_path\"] # or: os.path.dirname(config_path) #dlc_models_path = os.path.join(project_path, \"dlc-models\")\n", + "project_path = cfg[\n", + " \"project_path\"\n", + "] # or: os.path.dirname(config_path) #dlc_models_path = os.path.join(project_path, \"dlc-models\")\n", "training_datasets_path = os.path.join(project_path, \"training-datasets\")\n", "\n", "# Define shuffles\n", - "shuffles = [1,2,3,4]\n", + "shuffles = [1, 2, 3, 4]\n", "trainingsetindices = [0, 1, 2, 3]\n", "\n", "# Get train and test pose config file paths from base project, for each shuffle\n", "list_base_train_pose_config_file_paths = []\n", "list_base_test_pose_config_file_paths = []\n", "for shuffle_number, trainingsetindex in zip(shuffles, trainingsetindices):\n", - " base_train_pose_config_file_path_TEMP,\\\n", - " base_test_pose_config_file_path_TEMP,\\\n", - " _ = deeplabcut.return_train_network_path(config_path,\n", - " shuffle=shuffle_number,\n", - " trainingsetindex=trainingsetindex) # base_train_pose_config_file\n", + " base_train_pose_config_file_path_TEMP, base_test_pose_config_file_path_TEMP, _ = (\n", + " deeplabcut.return_train_network_path(config_path, shuffle=shuffle_number, trainingsetindex=trainingsetindex)\n", + " ) # base_train_pose_config_file\n", " list_base_train_pose_config_file_paths.append(base_train_pose_config_file_path_TEMP)\n", " list_base_test_pose_config_file_paths.append(base_test_pose_config_file_path_TEMP)\n", "\n", "# Create subdirs for this augmentation method\n", - "model_prefix = '_'.join([modelprefix_pre, \"{0:0=2d}\".format(model_number), daug_str]) # modelprefix_pre = aug_\n", + "model_prefix = \"_\".join([modelprefix_pre, \"{0:0=2d}\".format(model_number), daug_str]) # modelprefix_pre = aug_\n", "aug_project_path = os.path.join(project_path, model_prefix)\n", - "aug_dlc_models = os.path.join(aug_project_path, \"dlc-models\", )\n", + "aug_dlc_models = os.path.join(\n", + " aug_project_path,\n", + " \"dlc-models\",\n", + ")\n", "\n", "# make the folder for this modelprefix\n", "try:\n", @@ -1444,25 +1533,20 @@ " print(\"Skipping this one as it already exists\")\n", "\n", "# Copy base train pose config file to the directory of this augmentation method\n", - "for j, (shuffle, trainingsetindex) in enumerate(zip(shuffles,trainingsetindices)):\n", - " one_train_pose_config_file_path,\\\n", - " one_test_pose_config_file_path,\\\n", - " _ = deeplabcut.return_train_network_path(config_path,\n", - " shuffle=shuffle,\n", - " trainingsetindex=trainingsetindex,\n", - " modelprefix=model_prefix)\n", - " \n", + "for j, (shuffle, trainingsetindex) in enumerate(zip(shuffles, trainingsetindices)):\n", + " one_train_pose_config_file_path, one_test_pose_config_file_path, _ = deeplabcut.return_train_network_path(\n", + " config_path, shuffle=shuffle, trainingsetindex=trainingsetindex, modelprefix=model_prefix\n", + " )\n", + "\n", " # make train and test directories for this subdir\n", - " os.makedirs(str(os.path.dirname(one_train_pose_config_file_path))) # create parentdir 'train'\n", - " os.makedirs(str(os.path.dirname(one_test_pose_config_file_path))) # create parentdir 'test\n", - " \n", + " os.makedirs(str(os.path.dirname(one_train_pose_config_file_path))) # create parentdir 'train'\n", + " os.makedirs(str(os.path.dirname(one_test_pose_config_file_path))) # create parentdir 'test\n", + "\n", " # copy test and train config from base project to this subdir\n", " # copy base train config file\n", - " shutil.copyfile(list_base_train_pose_config_file_paths[j],\n", - " one_train_pose_config_file_path) \n", + " shutil.copyfile(list_base_train_pose_config_file_paths[j], one_train_pose_config_file_path)\n", " # copy base test config file\n", - " shutil.copyfile(list_base_test_pose_config_file_paths[j],\n", - " one_test_pose_config_file_path)\n" + " shutil.copyfile(list_base_test_pose_config_file_paths[j], one_test_pose_config_file_path)" ] }, { @@ -1475,19 +1559,25 @@ }, "outputs": [], "source": [ - "#import tools for changing our config file\n", + "# import tools for changing our config file\n", "from deeplabcut.utils.auxiliaryfunctions import edit_config\n", "\n", - "model_prefix = 'data_augm_03_max_rotate'\n", + "model_prefix = \"data_augm_03_max_rotate\"\n", "\n", "## Initialise dict with additional edits to train config: optimizer\n", "train_edits_dict = {}\n", - "dict_optimizer = {'optimizer':'adam',\n", - " 'batch_size': 8, # the gpu I'm using has plenty of memory so batch size 8 makes sense\n", - " 'multi_step': [[1e-4, 7500], [5 * 1e-5, 12000], [1e-5, 150000]]} # if no yaml file passed, initialise as an empty dict\n", - "train_edits_dict.update({'optimizer': dict_optimizer['optimizer'], #'adam',\n", - " 'batch_size': dict_optimizer['batch_size'],\n", - " 'multi_step': dict_optimizer['multi_step']})\n", + "dict_optimizer = {\n", + " \"optimizer\": \"adam\",\n", + " \"batch_size\": 8, # the gpu I'm using has plenty of memory so batch size 8 makes sense\n", + " \"multi_step\": [[1e-4, 7500], [5 * 1e-5, 12000], [1e-5, 150000]],\n", + "} # if no yaml file passed, initialise as an empty dict\n", + "train_edits_dict.update(\n", + " {\n", + " \"optimizer\": dict_optimizer[\"optimizer\"], #'adam',\n", + " \"batch_size\": dict_optimizer[\"batch_size\"],\n", + " \"multi_step\": dict_optimizer[\"multi_step\"],\n", + " }\n", + ")\n", "\n", "# Augmentation edits\n", "edits_dict = dict()\n", @@ -1495,16 +1585,13 @@ "edits_dict[\"fliplr\"] = True\n", "edits_dict[\"rotation\"] = 180\n", "\n", - "for shuffle, trainingsetindex in zip(shuffles,trainingsetindices):\n", - " one_train_pose_config_file_path,\\\n", - " _,\\\n", - " _ = deeplabcut.return_train_network_path(config_path,\n", - " shuffle=shuffle,\n", - " trainingsetindex=trainingsetindex,\n", - " modelprefix=model_prefix)\n", + "for shuffle, trainingsetindex in zip(shuffles, trainingsetindices):\n", + " one_train_pose_config_file_path, _, _ = deeplabcut.return_train_network_path(\n", + " config_path, shuffle=shuffle, trainingsetindex=trainingsetindex, modelprefix=model_prefix\n", + " )\n", "\n", " edit_config(str(one_train_pose_config_file_path), edits_dict)\n", - " edit_config(str(one_train_pose_config_file_path), train_edits_dict)\n" + " edit_config(str(one_train_pose_config_file_path), train_edits_dict)" ] }, { @@ -1526,16 +1613,17 @@ "outputs": [], "source": [ "import deeplabcut\n", + "\n", "# define config path and model prefix\n", - "config_path='/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml'\n", - "model_prefix = 'data_augm_03_max_rotate'\n", + "config_path = \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml\"\n", + "model_prefix = \"data_augm_03_max_rotate\"\n", "\n", "# the computer I'm working on has several gpus, here I used the third one.\n", - "gputouse=3\n", + "gputouse = 3\n", "\n", "# define shuffles and trainingsetindices\n", - "shuffles = [1,2,3,4]\n", - "trainingsetindices = [0,1,2,3]\n", + "shuffles = [1, 2, 3, 4]\n", + "trainingsetindices = [0, 1, 2, 3]\n", "\n", "# loop over shuffles and train each\n", "for shuffle, trainingsetindex in zip(shuffles, trainingsetindices):\n", @@ -1545,8 +1633,8 @@ " modelprefix=model_prefix,\n", " gputouse=gputouse,\n", " trainingsetindex=trainingsetindex,\n", - " max_snapshots_to_keep=3, # training for 150000 iterations so let's save 50, 100, and 150.\n", - " saveiters=50000\n", + " max_snapshots_to_keep=3, # training for 150000 iterations so let's save 50, 100, and 150.\n", + " saveiters=50000,\n", " )" ] }, @@ -1564,18 +1652,20 @@ "import deeplabcut\n", "\n", "config_path = \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml\"\n", - "model_prefix = 'data_augm_03_max_rotate'\n", - "Shuffles = [1,2,3,4]\n", - "trainingsetindices = [0,1,2,3]\n", + "model_prefix = \"data_augm_03_max_rotate\"\n", + "Shuffles = [1, 2, 3, 4]\n", + "trainingsetindices = [0, 1, 2, 3]\n", "\n", - "#import tools for modifying our config file\n", + "# import tools for modifying our config file\n", "from deeplabcut.utils.auxiliaryfunctions import edit_config\n", "\n", "# make sure we are testing all snapshots\n", - "edit_config(config_path,{'snapshotindex':'all'})\n", + "edit_config(config_path, {\"snapshotindex\": \"all\"})\n", "\n", - "for shuffle, trainingsetindex in zip(Shuffles,trainingsetindices):\n", - " deeplabcut.evaluate_network(config_path, modelprefix = model_prefix, Shuffles = [shuffle], trainingsetindex=trainingsetindex, gputouse=3)" + "for shuffle, trainingsetindex in zip(Shuffles, trainingsetindices):\n", + " deeplabcut.evaluate_network(\n", + " config_path, modelprefix=model_prefix, Shuffles=[shuffle], trainingsetindex=trainingsetindex, gputouse=3\n", + " )" ] }, { @@ -1595,23 +1685,26 @@ "import deeplabcut\n", "\n", "config_path = \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml\"\n", - "model_prefix_base = 'data_augm_00_base'\n", - "model_prefix_augm = 'data_augm_03_max_rotate'\n", - "Shuffles = [4,3] # let's start with the refined un-augmented, i.e. shuffle 4\n", - "trainingsetindices = [3,2]\n", + "model_prefix_base = \"data_augm_00_base\"\n", + "model_prefix_augm = \"data_augm_03_max_rotate\"\n", + "Shuffles = [4, 3] # let's start with the refined un-augmented, i.e. shuffle 4\n", + "trainingsetindices = [3, 2]\n", "\n", "# We need pandas for creatig a nice list to parse\n", "import pandas as pd\n", "\n", "import sys\n", - "sys.path.append('..') #my python file for this function is stored in the parent folder as I'm running this\n", - "from getErrorDistribution import getErrorDistribution #import the getErrorDistribution function\n", + "\n", + "sys.path.append(\"..\") # my python file for this function is stored in the parent folder as I'm running this\n", + "from getErrorDistribution import getErrorDistribution # import the getErrorDistribution function\n", "import numpy as np\n", "\n", "# Read the h5 file containing all the frames, (project_folder/training-datasets/iteration-0/UnaufmentedDataSet_project_folder/CollectedData_LabelerName.h5)\n", - "df = pd.read_hdf('/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/training-datasets/iteration-0/UnaugmentedDataSet_bat_augmentation_austin_2020_bat_dataAug18/CollectedData_DLC.h5')\n", + "df = pd.read_hdf(\n", + " \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/training-datasets/iteration-0/UnaugmentedDataSet_bat_augmentation_austin_2020_bat_dataAug18/CollectedData_DLC.h5\"\n", + ")\n", "\n", - "image_paths = df.index.to_list() # turn dataframe into list\n", + "image_paths = df.index.to_list() # turn dataframe into list\n", "\n", "# get test indices\n", "test_inds = []\n", @@ -1621,30 +1714,25 @@ "\n", "error_distributions_pcut = []\n", "\n", - "for shuffle, trainFractionIndex in zip(Shuffles,trainingsetindices):\n", - " error_distributions_pcut_temp = []\n", - " if shuffle == 4: model_prefix = model_prefix_base\n", - " elif shuffle == 3: model_prefix = model_prefix_augm\n", - " \n", - " for snapshot in [0,1,2]: #we saved three snapshots, one at 50k iteratinos, one at 100k, and one at 150k\n", - " (\n", - " _,\n", - " _,\n", - " _,\n", - " ErrorDistributionPCutOff_all,\n", - " _,\n", - " _\n", - " ) = getErrorDistribution(\n", + "for shuffle, trainFractionIndex in zip(Shuffles, trainingsetindices):\n", + " error_distributions_pcut_temp = []\n", + " if shuffle == 4:\n", + " model_prefix = model_prefix_base\n", + " elif shuffle == 3:\n", + " model_prefix = model_prefix_augm\n", + "\n", + " for snapshot in [0, 1, 2]: # we saved three snapshots, one at 50k iteratinos, one at 100k, and one at 150k\n", + " (_, _, _, ErrorDistributionPCutOff_all, _, _) = getErrorDistribution(\n", " config_path,\n", " shuffle=shuffle,\n", " snapindex=snapshot,\n", - " trainFractionIndex = trainFractionIndex,\n", - " modelprefix = model_prefix\n", - " )\n", - " error_distributions_pcut_temp.append(ErrorDistributionPCutOff_all.iloc[test_inds].values.flatten())\n", - " error_distributions_pcut.append(error_distributions_pcut_temp)\n", + " trainFractionIndex=trainFractionIndex,\n", + " modelprefix=model_prefix,\n", + " )\n", + " error_distributions_pcut_temp.append(ErrorDistributionPCutOff_all.iloc[test_inds].values.flatten())\n", + " error_distributions_pcut.append(error_distributions_pcut_temp)\n", "\n", - "error_distributions_pcut = np.array(error_distributions_pcut) # array with dimensions [shuffle, snapshot, frames]" + "error_distributions_pcut = np.array(error_distributions_pcut) # array with dimensions [shuffle, snapshot, frames]" ] }, { @@ -1657,12 +1745,16 @@ "\n", "plt.figure(figsize=(10, 5))\n", "\n", - "for shuffle in [0,1]: #we start counting at 0, so for now, let's consider each index one less\n", - " plt.errorbar(np.array([50,100,150])-1.5+shuffle,np.nanmean(error_distributions_pcut[shuffle,:],axis=1), np.nanstd(error_distributions_pcut[shuffle,:],axis=1)/len(test_inds)**.5)\n", + "for shuffle in [0, 1]: # we start counting at 0, so for now, let's consider each index one less\n", + " plt.errorbar(\n", + " np.array([50, 100, 150]) - 1.5 + shuffle,\n", + " np.nanmean(error_distributions_pcut[shuffle, :], axis=1),\n", + " np.nanstd(error_distributions_pcut[shuffle, :], axis=1) / len(test_inds) ** 0.5,\n", + " )\n", "\n", "plt.xticks([50, 100, 150])\n", - "plt.xlim([25,175])\n", - "plt.ylim([0,10])\n", + "plt.xlim([25, 175])\n", + "plt.ylim([0, 10])\n", "plt.title(\"Error with P-cut 0.6, comparing baseline to fliplr and 180 degrees rotation augmented\")\n", "\n", "plt.legend([\"Full, ref, baseline\", \"Full, OOD, fliplr_180_rotate\"])\n", @@ -1697,23 +1789,26 @@ "import deeplabcut\n", "\n", "config_path = \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml\"\n", - "model_prefix_base = 'data_augm_00_base'\n", - "model_prefix_augm = 'data_augm_03_max_rotate'\n", - "Shuffles = [4,3,3]\n", - "trainingsetindices = [3,2,2]\n", + "model_prefix_base = \"data_augm_00_base\"\n", + "model_prefix_augm = \"data_augm_03_max_rotate\"\n", + "Shuffles = [4, 3, 3]\n", + "trainingsetindices = [3, 2, 2]\n", "\n", "# We need pandas for creatig a nice list to parse\n", "import pandas as pd\n", "\n", "import sys\n", - "sys.path.append('..') #my python file for this function is stored in the parent folder as I'm running this\n", - "from getErrorDistribution import getErrorDistribution #import the getErrorDistribution function\n", + "\n", + "sys.path.append(\"..\") # my python file for this function is stored in the parent folder as I'm running this\n", + "from getErrorDistribution import getErrorDistribution # import the getErrorDistribution function\n", "import numpy as np\n", "\n", "# Read the h5 file containing all the frames, (project_folder/training-datasets/iteration-0/UnaufmentedDataSet_project_folder/CollectedData_LabelerName.h5)\n", - "df = pd.read_hdf('/home/juser/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/training-datasets/iteration-0/UnaugmentedDataSet_bat_augmentation_austin_2020_bat_dataAug18/CollectedData_DLC.h5')\n", + "df = pd.read_hdf(\n", + " \"/home/juser/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/training-datasets/iteration-0/UnaugmentedDataSet_bat_augmentation_austin_2020_bat_dataAug18/CollectedData_DLC.h5\"\n", + ")\n", "\n", - "image_paths = df.index.to_list() # turn dataframe into list\n", + "image_paths = df.index.to_list() # turn dataframe into list\n", "\n", "# get test indices\n", "test_inds = []\n", @@ -1724,38 +1819,41 @@ "# this gives us the paths of our 27 test videos\n", "test_paths = list(set([image_paths[i][1] for i in test_inds]))\n", "\n", - "#%% sorted so that the corresponding videos have the same index in three lists (one per camera)\n", - "test_paths_cam1 = ['TS5-544-Cam1_2020-06-25_000099Track8_50_test',\n", - " 'TS5-544-Cam1_2020-06-25_000103Track3_50_test',\n", - " 'TS5-544-Cam1_2020-06-25_000104Track3_50_test',\n", - " 'TS5-544-Cam1_2020-06-25_000108Track6_50_test',\n", - " 'TS5-544-Cam1_2020-06-25_000123Track6_50_test',\n", - " 'TS5-544-Cam1_2020-06-25_000128Track2_50_test',\n", - " 'TS5-544-Cam1_2020-06-25_000134Track5_50_test'\n", - " ]\n", - "test_paths_cam2 = ['IL5-519-Cam2_2020-06-25_000099Track6_50_test',\n", - " 'IL5-519-Cam2_2020-06-25_000103Track3_50_test',\n", - " 'IL5-519-Cam2_2020-06-25_000104Track2_50_test',\n", - " 'IL5-519-Cam2_2020-06-25_000109Track1_50_test',\n", - " 'IL5-519-Cam2_2020-06-25_000124Track9_50_test',\n", - " 'IL5-519-Cam2_2020-06-25_000130Track2_50_test',\n", - " 'IL5-519-Cam2_2020-06-25_000136Track10_50_test'\n", - " ]\n", - "test_paths_cam3 = ['IL5-534-Cam3_2020-06-25_000095Track14_50_test',\n", - " 'IL5-534-Cam3_2020-06-25_000100Track4_50_test',\n", - " 'IL5-534-Cam3_2020-06-25_000101Track4_50_test',\n", - " 'IL5-534-Cam3_2020-06-25_000106Track3_50_test',\n", - " 'IL5-534-Cam3_2020-06-25_000122Track7_50_test',\n", - " 'IL5-534-Cam3_2020-06-25_000127Track4_50_test',\n", - " 'IL5-534-Cam3_2020-06-25_000133Track9_50_test'\n", - " ]\n", - "\n", - "nvideos = 7 # number of videos\n", + "# %% sorted so that the corresponding videos have the same index in three lists (one per camera)\n", + "test_paths_cam1 = [\n", + " \"TS5-544-Cam1_2020-06-25_000099Track8_50_test\",\n", + " \"TS5-544-Cam1_2020-06-25_000103Track3_50_test\",\n", + " \"TS5-544-Cam1_2020-06-25_000104Track3_50_test\",\n", + " \"TS5-544-Cam1_2020-06-25_000108Track6_50_test\",\n", + " \"TS5-544-Cam1_2020-06-25_000123Track6_50_test\",\n", + " \"TS5-544-Cam1_2020-06-25_000128Track2_50_test\",\n", + " \"TS5-544-Cam1_2020-06-25_000134Track5_50_test\",\n", + "]\n", + "test_paths_cam2 = [\n", + " \"IL5-519-Cam2_2020-06-25_000099Track6_50_test\",\n", + " \"IL5-519-Cam2_2020-06-25_000103Track3_50_test\",\n", + " \"IL5-519-Cam2_2020-06-25_000104Track2_50_test\",\n", + " \"IL5-519-Cam2_2020-06-25_000109Track1_50_test\",\n", + " \"IL5-519-Cam2_2020-06-25_000124Track9_50_test\",\n", + " \"IL5-519-Cam2_2020-06-25_000130Track2_50_test\",\n", + " \"IL5-519-Cam2_2020-06-25_000136Track10_50_test\",\n", + "]\n", + "test_paths_cam3 = [\n", + " \"IL5-534-Cam3_2020-06-25_000095Track14_50_test\",\n", + " \"IL5-534-Cam3_2020-06-25_000100Track4_50_test\",\n", + " \"IL5-534-Cam3_2020-06-25_000101Track4_50_test\",\n", + " \"IL5-534-Cam3_2020-06-25_000106Track3_50_test\",\n", + " \"IL5-534-Cam3_2020-06-25_000122Track7_50_test\",\n", + " \"IL5-534-Cam3_2020-06-25_000127Track4_50_test\",\n", + " \"IL5-534-Cam3_2020-06-25_000133Track9_50_test\",\n", + "]\n", + "\n", + "nvideos = 7 # number of videos\n", "\n", "# get test frame indexes per camera\n", - "test_inds_cam1 = [[],[],[],[],[],[],[]]\n", - "test_inds_cam2 = [[],[],[],[],[],[],[]]\n", - "test_inds_cam3 = [[],[],[],[],[],[],[]]\n", + "test_inds_cam1 = [[], [], [], [], [], [], []]\n", + "test_inds_cam2 = [[], [], [], [], [], [], []]\n", + "test_inds_cam3 = [[], [], [], [], [], [], []]\n", "\n", "for i, path in enumerate(image_paths):\n", " for j in range(nvideos):\n", @@ -1768,70 +1866,96 @@ "\n", "nshuffles = len(Shuffles)\n", "\n", - "#pre-allocate matrixes for mean values and standard errors\n", - "mean_cam1 = np.zeros([nshuffles,nvideos]) # shuffle x movie\n", - "mean_cam2 = np.zeros([nshuffles,nvideos])\n", - "mean_cam3 = np.zeros([nshuffles,nvideos])\n", + "# pre-allocate matrixes for mean values and standard errors\n", + "mean_cam1 = np.zeros([nshuffles, nvideos]) # shuffle x movie\n", + "mean_cam2 = np.zeros([nshuffles, nvideos])\n", + "mean_cam3 = np.zeros([nshuffles, nvideos])\n", "\n", - "ste_cam1 = np.zeros([nshuffles,nvideos])\n", - "ste_cam2 = np.zeros([nshuffles,nvideos])\n", - "ste_cam3 = np.zeros([nshuffles,nvideos])\n", + "ste_cam1 = np.zeros([nshuffles, nvideos])\n", + "ste_cam2 = np.zeros([nshuffles, nvideos])\n", + "ste_cam3 = np.zeros([nshuffles, nvideos])\n", "\n", - "meanPcut_cam1 = np.zeros([nshuffles,nvideos]) # shuffle x movie\n", - "meanPcut_cam2 = np.zeros([nshuffles,nvideos])\n", - "meanPcut_cam3 = np.zeros([nshuffles,nvideos])\n", + "meanPcut_cam1 = np.zeros([nshuffles, nvideos]) # shuffle x movie\n", + "meanPcut_cam2 = np.zeros([nshuffles, nvideos])\n", + "meanPcut_cam3 = np.zeros([nshuffles, nvideos])\n", "\n", - "stePcut_cam1 = np.zeros([nshuffles,nvideos])\n", - "stePcut_cam2 = np.zeros([nshuffles,nvideos])\n", - "stePcut_cam3 = np.zeros([nshuffles,nvideos])\n", + "stePcut_cam1 = np.zeros([nshuffles, nvideos])\n", + "stePcut_cam2 = np.zeros([nshuffles, nvideos])\n", + "stePcut_cam3 = np.zeros([nshuffles, nvideos])\n", "\n", "# %%\n", "\n", "for i, shuffle in enumerate(Shuffles):\n", - " if shuffle == 4 or i == 2: model_prefix = model_prefix_base\n", - " elif shuffle == 3: model_prefix = model_prefix_augm \n", - "\n", - " trainFractionIndex = shuffle-1\n", - " snapshot=-1\n", - " (\n", - " ErrorDistribution_all,\n", - " _,\n", - " _,\n", - " ErrorDistributionPCutOff_all,\n", - " _,\n", - " _\n", - " ) = getErrorDistribution(\n", + " if shuffle == 4 or i == 2:\n", + " model_prefix = model_prefix_base\n", + " elif shuffle == 3:\n", + " model_prefix = model_prefix_augm\n", + "\n", + " trainFractionIndex = shuffle - 1\n", + " snapshot = -1\n", + " (ErrorDistribution_all, _, _, ErrorDistributionPCutOff_all, _, _) = getErrorDistribution(\n", " config_path,\n", " shuffle=shuffle,\n", " snapindex=snapshot,\n", - " trainFractionIndex = trainFractionIndex,\n", - " modelprefix = model_prefix\n", + " trainFractionIndex=trainFractionIndex,\n", + " modelprefix=model_prefix,\n", " )\n", " for movie_number in range(7):\n", - "\n", - " meanPcut_cam1[i,movie_number] = np.nanmean(ErrorDistributionPCutOff_all.values[test_inds_cam1[movie_number]][:])\n", - " stePcut_cam1[i,movie_number] = np.nanstd(ErrorDistributionPCutOff_all.values[test_inds_cam1[movie_number]][:])/(ErrorDistribution_all.values[test_inds_cam1[movie_number]][:].size**.5)\n", - "\n", - " meanPcut_cam2[i,movie_number] = np.nanmean(ErrorDistributionPCutOff_all.values[test_inds_cam2[movie_number]][:])\n", - " stePcut_cam2[i,movie_number] = np.nanstd(ErrorDistributionPCutOff_all.values[test_inds_cam1[movie_number]][:])/(ErrorDistribution_all.values[test_inds_cam2[movie_number]][:].size**.5)\n", - "\n", - " meanPcut_cam3[i,movie_number] = np.nanmean(ErrorDistributionPCutOff_all.values[test_inds_cam3[movie_number]][:])\n", - " stePcut_cam3[i,movie_number] = np.nanstd(ErrorDistributionPCutOff_all.values[test_inds_cam1[movie_number]][:])/(ErrorDistribution_all.values[test_inds_cam3[movie_number]][:].size**.5)\n", - "\n", - "fig, (ax1,ax2,ax3) = plt.subplots(3,1)\n", + " meanPcut_cam1[i, movie_number] = np.nanmean(\n", + " ErrorDistributionPCutOff_all.values[test_inds_cam1[movie_number]][:]\n", + " )\n", + " stePcut_cam1[i, movie_number] = np.nanstd(\n", + " ErrorDistributionPCutOff_all.values[test_inds_cam1[movie_number]][:]\n", + " ) / (ErrorDistribution_all.values[test_inds_cam1[movie_number]][:].size ** 0.5)\n", + "\n", + " meanPcut_cam2[i, movie_number] = np.nanmean(\n", + " ErrorDistributionPCutOff_all.values[test_inds_cam2[movie_number]][:]\n", + " )\n", + " stePcut_cam2[i, movie_number] = np.nanstd(\n", + " ErrorDistributionPCutOff_all.values[test_inds_cam1[movie_number]][:]\n", + " ) / (ErrorDistribution_all.values[test_inds_cam2[movie_number]][:].size ** 0.5)\n", + "\n", + " meanPcut_cam3[i, movie_number] = np.nanmean(\n", + " ErrorDistributionPCutOff_all.values[test_inds_cam3[movie_number]][:]\n", + " )\n", + " stePcut_cam3[i, movie_number] = np.nanstd(\n", + " ErrorDistributionPCutOff_all.values[test_inds_cam1[movie_number]][:]\n", + " ) / (ErrorDistribution_all.values[test_inds_cam3[movie_number]][:].size ** 0.5)\n", + "\n", + "fig, (ax1, ax2, ax3) = plt.subplots(3, 1)\n", "fig.set_figheight(15)\n", "fig.set_figwidth(10)\n", "for i, shuffle in enumerate(Shuffles):\n", - " \n", " # to jitter the error bars to keep them from overlapping\n", - " movie_number = list(range(1,8))\n", - " movie_number = [x - 2/50 + shuffle/50 for x in movie_number]\n", - " \n", - " ax1.errorbar(movie_number,meanPcut_cam1[i,:], stePcut_cam1[i,:,])\n", + " movie_number = list(range(1, 8))\n", + " movie_number = [x - 2 / 50 + shuffle / 50 for x in movie_number]\n", + "\n", + " ax1.errorbar(\n", + " movie_number,\n", + " meanPcut_cam1[i, :],\n", + " stePcut_cam1[\n", + " i,\n", + " :,\n", + " ],\n", + " )\n", "\n", - " ax2.errorbar(movie_number,meanPcut_cam2[i,:], stePcut_cam2[i,:,])\n", + " ax2.errorbar(\n", + " movie_number,\n", + " meanPcut_cam2[i, :],\n", + " stePcut_cam2[\n", + " i,\n", + " :,\n", + " ],\n", + " )\n", "\n", - " ax3.errorbar(movie_number,meanPcut_cam3[i,:], stePcut_cam3[i,:,])\n", + " ax3.errorbar(\n", + " movie_number,\n", + " meanPcut_cam3[i, :],\n", + " stePcut_cam3[\n", + " i,\n", + " :,\n", + " ],\n", + " )\n", "\n", "ax1.set_ylim([0, 50])\n", "ax2.set_ylim([0, 50])\n", diff --git a/docs/recipes/fmpose3d.ipynb b/docs/recipes/fmpose3d.ipynb index 35c634d199..212303e8e5 100644 --- a/docs/recipes/fmpose3d.ipynb +++ b/docs/recipes/fmpose3d.ipynb @@ -1,344 +1,346 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "(fmpose3d-recipe)=\n", - "# 3D Pose Estimation with FMPose3D\n", - "\n", - "## Overview\n", - "**[FMPose3D: monocular 3D pose estimation via flow matching](https://arxiv.org/abs/2602.05755)** by Ti Wang, Xiaohang Yu, and Mackenzie Weygandt Mathis.\n", - "\n", - "| [Paper](https://arxiv.org/abs/2602.05755) | [Project Page](https://xiu-cs.github.io/FMPose3D/) | [GitHub](https://github.com/AdaptiveMotorControlLab/FMPose3D) | [PyPI](https://pypi.org/project/fmpose3d/) |\n", - "\n", - "FMPose3D lifts 2D keypoints from a single image into 3D poses using **flow matching** — a generative technique based on ODE sampling. It generates multiple plausible 3D pose hypotheses in just a few steps, then aggregates them using a reprojection-based Bayesian module (RPEA) for accurate predictions, achieving state-of-the-art results on human and animal 3D pose benchmarks.\n", - "\n", - "\n", - "This recipe shows how to use FMPose3D in DeepLabCut for monocular 3D pose\n", - "estimation. Two pipelines are available:\n", - "\n", - "| Pipeline | 2D Estimator | Skeleton | Joints |\n", - "|----------|-------------|----------|--------|\n", - "| **Human** | HRNet + YOLO | H36M | 17 |\n", - "| **Animal** | DeepLabCut SuperAnimal | Animal3D | 26 |\n", - "\n", - "Model weights are hosted on HuggingFace Hub and downloaded automatically on\n", - "first use.\n", - "\n", - "```{admonition} Prerequisites\n", - ":class: note\n", - "\n", - "Install the `fmpose3d` package before running this notebook:\n", - "\n", - " pip install fmpose3d\n", - "\n", - "A GPU is recommended but not required — CPU inference works out of the box.\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Setup\n", - "\n", - "Import the DeepLabCut convenience wrapper and a few helpers." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "from mpl_toolkits.mplot3d import Axes3D\n", - "\n", - "from deeplabcut.modelzoo.fmpose_3d.fmpose3d import get_fmpose3d_inference_api" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Human Pose Estimation (end-to-end)\n", - "\n", - "The simplest way to get 3D human poses is the **end-to-end** pipeline.\n", - "`get_fmpose3d_inference_api` creates an inference object that handles\n", - "2D detection and 3D lifting in a single `predict` call. Weights are\n", - "downloaded automatically from HuggingFace on first use." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Create the human pose API (downloads weights on first call)\n", - "human_api = get_fmpose3d_inference_api(\n", - " model_type=\"fmpose3d_humans\",\n", - " device=\"cuda:0\", # use \"cpu\" if no GPU is available\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Run end-to-end inference on an image\n", - "image_path = \"path/to/your/image.jpg\" # replace with your image path\n", - "result = human_api.predict(source=image_path)\n", - "\n", - "print(\"3D poses (root-relative):\", result.poses_3d.shape) # (num_frames, 17, 3)\n", - "print(\"3D poses (world coords):\", result.poses_3d_world.shape) # (num_frames, 17, 3)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Accepted input sources\n", - "\n", - "`predict` (and `prepare_2d`) accept a variety of input types:\n", - "\n", - "- A **file path** (`str` or `Path`) to a single image\n", - "- A **directory** of images\n", - "- A **numpy array** — either a single frame `(H, W, C)` or a batch `(N, H, W, C)`\n", - "- A **list** of any of the above" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Animal Pose Estimation (end-to-end)\n", - "\n", - "Switching to the **animal** pipeline only requires changing `model_type`.\n", - "This pipeline uses DeepLabCut SuperAnimal for 2D detection and outputs\n", - "26-joint Animal3D skeletons." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Create the animal pose API\n", - "animal_api = get_fmpose3d_inference_api(\n", - " model_type=\"fmpose3d_animals\",\n", - " device=\"cuda:0\",\n", - ")\n", - "\n", - "# Run inference\n", - "animal_image_path = \"path/to/your/animal_image.jpg\"\n", - "animal_result = animal_api.predict(source=animal_image_path)\n", - "\n", - "print(\"3D poses:\", animal_result.poses_3d.shape) # (num_frames, 26, 3)\n", - "print(\"3D poses (regularized):\", animal_result.poses_3d_world.shape)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "```{note}\n", - "For animals, `poses_3d_world` contains **limb-regularized** poses (the\n", - "skeleton is rotated so that the average limb direction is vertical) rather\n", - "than a camera-to-world transform.\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Two-Step Inference (2D then 3D)\n", - "\n", - "For more control, you can run the 2D and 3D stages separately. This is\n", - "useful when you want to inspect or modify 2D keypoints before lifting." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "api = get_fmpose3d_inference_api(model_type=\"fmpose3d_animals\", device=\"cuda:0\")\n", - "\n", - "# Step 1: detect 2D keypoints\n", - "result_2d = api.prepare_2d(source=animal_image_path)\n", - "\n", - "print(\"2D keypoints:\", result_2d.keypoints.shape) # (num_persons, num_frames, J, 2)\n", - "print(\"Confidence scores:\", result_2d.scores.shape) # (num_persons, num_frames, J)\n", - "print(\"Image size (H, W):\", result_2d.image_size)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Step 2: lift 2D keypoints to 3D\n", - "result_3d = api.pose_3d(\n", - " keypoints_2d=result_2d.keypoints,\n", - " image_size=result_2d.image_size,\n", - ")\n", - "\n", - "print(\"Lifted 3D poses:\", result_3d.poses_3d.shape) # (num_frames, J, 3)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Lifting DeepLabCut 2D Predictions to 3D\n", - "\n", - "A common workflow is to use a DeepLabCut model you have already trained for\n", - "2D pose estimation, then lift those predictions to 3D with FMPose3D. The\n", - "example below runs DLC inference with `deeplabcut.analyze_images` and feeds\n", - "the resulting keypoints straight into the 3D lifter.\n", - "\n", - "```{admonition} Keypoint compatibility\n", - ":class: warning\n", - "\n", - "The FMPose3D lifter was trained on specific skeleton layouts (17 H36M joints\n", - "for humans, 26 Animal3D joints for animals). Your DLC model's bodyparts must\n", - "match one of these layouts for the lifted poses to be meaningful. If your\n", - "skeleton differs, you will need to select or re-order the relevant subset of\n", - "keypoints before calling `pose_3d`.\n", - "```" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "import deeplabcut\n", - "\n", - "# ── 1. Run DLC 2D inference ───────────────────────────────────────────────\n", - "# analyze_images returns a dict mapping each image path to its predictions.\n", - "# Each prediction contains a \"bodyparts\" array of shape\n", - "# (num_individuals, num_bodyparts, 3) where 3 = (x, y, likelihood).\n", - "\n", - "config_path = \"path/to/my_dlc_project/config.yaml\"\n", - "image_paths = [\"frame_001.png\", \"frame_002.png\", \"frame_003.png\"]\n", - "\n", - "predictions = deeplabcut.analyze_images(\n", - " config=config_path,\n", - " images=image_paths,\n", - " shuffle=1,\n", - " device=\"cuda:0\",\n", - ")\n", - "\n", - "# ── 2. Extract (x, y) keypoints from each frame ──────────────────────────\n", - "# Stack all frames into a single array and take only the first individual.\n", - "all_bodyparts = np.stack([\n", - " predictions[img][\"bodyparts\"][0] # first individual per frame\n", - " for img in image_paths\n", - "]) # shape: (num_frames, num_bodyparts, 3)\n", - "\n", - "keypoints_2d = all_bodyparts[:, :, :2] # drop likelihood → (num_frames, J, 2)\n", - "print(\"keypoints_2d shape:\", keypoints_2d.shape)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# ── 3. Lift DLC 2D keypoints to 3D ────────────────────────────────────────\n", - "# image_size = (height, width) of the frames the DLC model was run on.\n", - "import cv2\n", - "\n", - "sample_img = cv2.imread(image_paths[0])\n", - "image_size = sample_img.shape[:2] # (height, width)\n", - "\n", - "api = get_fmpose3d_inference_api(model_type=\"fmpose3d_animals\", device=\"cuda:0\")\n", - "result_3d = api.pose_3d(\n", - " keypoints_2d=keypoints_2d,\n", - " image_size=image_size,\n", - " seed=42, # for reproducible sampling\n", - ")\n", - "\n", - "print(\"3D poses (root-relative):\", result_3d.poses_3d.shape) # (num_frames, J, 3)\n", - "print(\"3D poses (post-processed):\", result_3d.poses_3d_world.shape)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "```{tip}\n", - "If you are working with video frames from `deeplabcut.analyze_videos`\n", - "instead of individual images, you can read `image_size` from the video:\n", - "\n", - " import cv2\n", - " cap = cv2.VideoCapture(\"path/to/video.mp4\")\n", - " image_size = (int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)),\n", - " int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)))\n", - " cap.release()\n", - "\n", - "You will also need to load the keypoints from the `.h5` file that\n", - "`analyze_videos` produces:\n", - "\n", - " import pandas as pd\n", - " df = pd.read_hdf(\"path/to/videoDLC_scorer.h5\")\n", - " scorer = df.columns.get_level_values(\"scorer\").unique()[0]\n", - " bodyparts = df[scorer].columns.get_level_values(\"bodyparts\").unique()\n", - " coords = df[scorer].values.reshape(len(df), len(bodyparts), 3)\n", - " keypoints_2d = coords[:, :, :2] # (num_frames, num_bodyparts, 2)\n", - "```" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Further Reading\n", - "\n", - "- [FMPose3D repository](https://github.com/AdaptiveMotorControlLab/FMPose3D)\n", - " — full API documentation and model details.\n", - "- [DeepLabCut Model Zoo](https://deeplabcut.github.io/DeepLabCut/docs/ModelZoo.html)\n", - " — other pre-trained models available in DeepLabCut." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "name": "python", - "version": "3.10.0" - } + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "(fmpose3d-recipe)=\n", + "# 3D Pose Estimation with FMPose3D\n", + "\n", + "## Overview\n", + "**[FMPose3D: monocular 3D pose estimation via flow matching](https://arxiv.org/abs/2602.05755)** by Ti Wang, Xiaohang Yu, and Mackenzie Weygandt Mathis.\n", + "\n", + "| [Paper](https://arxiv.org/abs/2602.05755) | [Project Page](https://xiu-cs.github.io/FMPose3D/) | [GitHub](https://github.com/AdaptiveMotorControlLab/FMPose3D) | [PyPI](https://pypi.org/project/fmpose3d/) |\n", + "\n", + "FMPose3D lifts 2D keypoints from a single image into 3D poses using **flow matching** — a generative technique based on ODE sampling. It generates multiple plausible 3D pose hypotheses in just a few steps, then aggregates them using a reprojection-based Bayesian module (RPEA) for accurate predictions, achieving state-of-the-art results on human and animal 3D pose benchmarks.\n", + "\n", + "\n", + "This recipe shows how to use FMPose3D in DeepLabCut for monocular 3D pose\n", + "estimation. Two pipelines are available:\n", + "\n", + "| Pipeline | 2D Estimator | Skeleton | Joints |\n", + "|----------|-------------|----------|--------|\n", + "| **Human** | HRNet + YOLO | H36M | 17 |\n", + "| **Animal** | DeepLabCut SuperAnimal | Animal3D | 26 |\n", + "\n", + "Model weights are hosted on HuggingFace Hub and downloaded automatically on\n", + "first use.\n", + "\n", + "```{admonition} Prerequisites\n", + ":class: note\n", + "\n", + "Install the `fmpose3d` package before running this notebook:\n", + "\n", + " pip install fmpose3d\n", + "\n", + "A GPU is recommended but not required — CPU inference works out of the box.\n", + "```" + ] }, - "nbformat": 4, - "nbformat_minor": 4 + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup\n", + "\n", + "Import the DeepLabCut convenience wrapper and a few helpers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from mpl_toolkits.mplot3d import Axes3D\n", + "\n", + "from deeplabcut.modelzoo.fmpose_3d.fmpose3d import get_fmpose3d_inference_api" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Human Pose Estimation (end-to-end)\n", + "\n", + "The simplest way to get 3D human poses is the **end-to-end** pipeline.\n", + "`get_fmpose3d_inference_api` creates an inference object that handles\n", + "2D detection and 3D lifting in a single `predict` call. Weights are\n", + "downloaded automatically from HuggingFace on first use." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Create the human pose API (downloads weights on first call)\n", + "human_api = get_fmpose3d_inference_api(\n", + " model_type=\"fmpose3d_humans\",\n", + " device=\"cuda:0\", # use \"cpu\" if no GPU is available\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Run end-to-end inference on an image\n", + "image_path = \"path/to/your/image.jpg\" # replace with your image path\n", + "result = human_api.predict(source=image_path)\n", + "\n", + "print(\"3D poses (root-relative):\", result.poses_3d.shape) # (num_frames, 17, 3)\n", + "print(\"3D poses (world coords):\", result.poses_3d_world.shape) # (num_frames, 17, 3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Accepted input sources\n", + "\n", + "`predict` (and `prepare_2d`) accept a variety of input types:\n", + "\n", + "- A **file path** (`str` or `Path`) to a single image\n", + "- A **directory** of images\n", + "- A **numpy array** — either a single frame `(H, W, C)` or a batch `(N, H, W, C)`\n", + "- A **list** of any of the above" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Animal Pose Estimation (end-to-end)\n", + "\n", + "Switching to the **animal** pipeline only requires changing `model_type`.\n", + "This pipeline uses DeepLabCut SuperAnimal for 2D detection and outputs\n", + "26-joint Animal3D skeletons." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Create the animal pose API\n", + "animal_api = get_fmpose3d_inference_api(\n", + " model_type=\"fmpose3d_animals\",\n", + " device=\"cuda:0\",\n", + ")\n", + "\n", + "# Run inference\n", + "animal_image_path = \"path/to/your/animal_image.jpg\"\n", + "animal_result = animal_api.predict(source=animal_image_path)\n", + "\n", + "print(\"3D poses:\", animal_result.poses_3d.shape) # (num_frames, 26, 3)\n", + "print(\"3D poses (regularized):\", animal_result.poses_3d_world.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```{note}\n", + "For animals, `poses_3d_world` contains **limb-regularized** poses (the\n", + "skeleton is rotated so that the average limb direction is vertical) rather\n", + "than a camera-to-world transform.\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Two-Step Inference (2D then 3D)\n", + "\n", + "For more control, you can run the 2D and 3D stages separately. This is\n", + "useful when you want to inspect or modify 2D keypoints before lifting." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "api = get_fmpose3d_inference_api(model_type=\"fmpose3d_animals\", device=\"cuda:0\")\n", + "\n", + "# Step 1: detect 2D keypoints\n", + "result_2d = api.prepare_2d(source=animal_image_path)\n", + "\n", + "print(\"2D keypoints:\", result_2d.keypoints.shape) # (num_persons, num_frames, J, 2)\n", + "print(\"Confidence scores:\", result_2d.scores.shape) # (num_persons, num_frames, J)\n", + "print(\"Image size (H, W):\", result_2d.image_size)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# Step 2: lift 2D keypoints to 3D\n", + "result_3d = api.pose_3d(\n", + " keypoints_2d=result_2d.keypoints,\n", + " image_size=result_2d.image_size,\n", + ")\n", + "\n", + "print(\"Lifted 3D poses:\", result_3d.poses_3d.shape) # (num_frames, J, 3)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Lifting DeepLabCut 2D Predictions to 3D\n", + "\n", + "A common workflow is to use a DeepLabCut model you have already trained for\n", + "2D pose estimation, then lift those predictions to 3D with FMPose3D. The\n", + "example below runs DLC inference with `deeplabcut.analyze_images` and feeds\n", + "the resulting keypoints straight into the 3D lifter.\n", + "\n", + "```{admonition} Keypoint compatibility\n", + ":class: warning\n", + "\n", + "The FMPose3D lifter was trained on specific skeleton layouts (17 H36M joints\n", + "for humans, 26 Animal3D joints for animals). Your DLC model's bodyparts must\n", + "match one of these layouts for the lifted poses to be meaningful. If your\n", + "skeleton differs, you will need to select or re-order the relevant subset of\n", + "keypoints before calling `pose_3d`.\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import deeplabcut\n", + "\n", + "# ── 1. Run DLC 2D inference ───────────────────────────────────────────────\n", + "# analyze_images returns a dict mapping each image path to its predictions.\n", + "# Each prediction contains a \"bodyparts\" array of shape\n", + "# (num_individuals, num_bodyparts, 3) where 3 = (x, y, likelihood).\n", + "\n", + "config_path = \"path/to/my_dlc_project/config.yaml\"\n", + "image_paths = [\"frame_001.png\", \"frame_002.png\", \"frame_003.png\"]\n", + "\n", + "predictions = deeplabcut.analyze_images(\n", + " config=config_path,\n", + " images=image_paths,\n", + " shuffle=1,\n", + " device=\"cuda:0\",\n", + ")\n", + "\n", + "# ── 2. Extract (x, y) keypoints from each frame ──────────────────────────\n", + "# Stack all frames into a single array and take only the first individual.\n", + "all_bodyparts = np.stack(\n", + " [\n", + " predictions[img][\"bodyparts\"][0] # first individual per frame\n", + " for img in image_paths\n", + " ]\n", + ") # shape: (num_frames, num_bodyparts, 3)\n", + "\n", + "keypoints_2d = all_bodyparts[:, :, :2] # drop likelihood → (num_frames, J, 2)\n", + "print(\"keypoints_2d shape:\", keypoints_2d.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# ── 3. Lift DLC 2D keypoints to 3D ────────────────────────────────────────\n", + "# image_size = (height, width) of the frames the DLC model was run on.\n", + "import cv2\n", + "\n", + "sample_img = cv2.imread(image_paths[0])\n", + "image_size = sample_img.shape[:2] # (height, width)\n", + "\n", + "api = get_fmpose3d_inference_api(model_type=\"fmpose3d_animals\", device=\"cuda:0\")\n", + "result_3d = api.pose_3d(\n", + " keypoints_2d=keypoints_2d,\n", + " image_size=image_size,\n", + " seed=42, # for reproducible sampling\n", + ")\n", + "\n", + "print(\"3D poses (root-relative):\", result_3d.poses_3d.shape) # (num_frames, J, 3)\n", + "print(\"3D poses (post-processed):\", result_3d.poses_3d_world.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```{tip}\n", + "If you are working with video frames from `deeplabcut.analyze_videos`\n", + "instead of individual images, you can read `image_size` from the video:\n", + "\n", + " import cv2\n", + " cap = cv2.VideoCapture(\"path/to/video.mp4\")\n", + " image_size = (int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)),\n", + " int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)))\n", + " cap.release()\n", + "\n", + "You will also need to load the keypoints from the `.h5` file that\n", + "`analyze_videos` produces:\n", + "\n", + " import pandas as pd\n", + " df = pd.read_hdf(\"path/to/videoDLC_scorer.h5\")\n", + " scorer = df.columns.get_level_values(\"scorer\").unique()[0]\n", + " bodyparts = df[scorer].columns.get_level_values(\"bodyparts\").unique()\n", + " coords = df[scorer].values.reshape(len(df), len(bodyparts), 3)\n", + " keypoints_2d = coords[:, :, :2] # (num_frames, num_bodyparts, 2)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Further Reading\n", + "\n", + "- [FMPose3D repository](https://github.com/AdaptiveMotorControlLab/FMPose3D)\n", + " — full API documentation and model details.\n", + "- [DeepLabCut Model Zoo](https://deeplabcut.github.io/DeepLabCut/docs/ModelZoo.html)\n", + " — other pre-trained models available in DeepLabCut." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 } diff --git a/examples/COLAB/COLAB_3miceDemo.ipynb b/examples/COLAB/COLAB_3miceDemo.ipynb index 427602ff4c..9152b43346 100644 --- a/examples/COLAB/COLAB_3miceDemo.ipynb +++ b/examples/COLAB/COLAB_3miceDemo.ipynb @@ -82,17 +82,17 @@ "from io import BytesIO\n", "from zipfile import ZipFile\n", "\n", - "url_record = 'https://zenodo.org/api/records/7883589'\n", + "url_record = \"https://zenodo.org/api/records/7883589\"\n", "response = requests.get(url_record)\n", "if response.status_code == 200:\n", - " file = response.json()['files'][0]\n", - " title = file['key']\n", + " file = response.json()[\"files\"][0]\n", + " title = file[\"key\"]\n", " print(f\"Downloading {title}...\")\n", - " with requests.get(file['links']['self'], stream=True) as r:\n", + " with requests.get(file[\"links\"][\"self\"], stream=True) as r:\n", " with ZipFile(BytesIO(r.content)) as zf:\n", - " zf.extractall(path='/content')\n", + " zf.extractall(path=\"/content\")\n", "else:\n", - " raise ValueError(f'The URL {url_record} could not be reached.')" + " raise ValueError(f\"The URL {url_record} could not be reached.\")" ] }, { @@ -119,7 +119,7 @@ "config_path = os.path.join(project_path, \"config.yaml\")\n", "video = os.path.join(project_path, \"videos\", \"videocompressed1.mp4\")\n", "\n", - "dlc.analyze_videos(config_path,[video], shuffle=0, videotype=\"mp4\",auto_track=False )" + "dlc.analyze_videos(config_path, [video], shuffle=0, videotype=\"mp4\", auto_track=False)" ] }, { @@ -144,10 +144,14 @@ "dlc.convert_detections2tracklets(\n", " config_path,\n", " [video],\n", - " videotype='mp4',\n", + " videotype=\"mp4\",\n", " shuffle=0,\n", " track_method=TRACK_METHOD,\n", - " ignore_bodyparts=[\"tail1\", \"tail2\", \"tailend\"], # Some body parts can optionally be ignored during tracking for better assembly (but they are used later)\n", + " ignore_bodyparts=[\n", + " \"tail1\",\n", + " \"tail2\",\n", + " \"tailend\",\n", + " ], # Some body parts can optionally be ignored during tracking for better assembly (but they are used later)\n", ")" ] }, @@ -171,7 +175,7 @@ "dlc.stitch_tracklets(\n", " config_path,\n", " [video],\n", - " videotype='mp4',\n", + " videotype=\"mp4\",\n", " shuffle=0,\n", " track_method=TRACK_METHOD,\n", " n_tracks=3,\n", @@ -195,17 +199,13 @@ }, "outputs": [], "source": [ - "#Filter the predictions to remove small jitter, if desired:\n", - "dlc.filterpredictions(config_path, \n", - " [video], \n", - " shuffle=0,\n", - " videotype='mp4', \n", - " track_method = TRACK_METHOD)\n", + "# Filter the predictions to remove small jitter, if desired:\n", + "dlc.filterpredictions(config_path, [video], shuffle=0, videotype=\"mp4\", track_method=TRACK_METHOD)\n", "\n", "dlc.create_labeled_video(\n", " config_path,\n", " [video],\n", - " videotype='mp4',\n", + " videotype=\"mp4\",\n", " shuffle=0,\n", " color_by=\"individual\",\n", " keypoints_only=False,\n", @@ -244,7 +244,7 @@ }, "outputs": [], "source": [ - "dlc.plot_trajectories(config_path, [video], shuffle=0,videotype='mp4', track_method=TRACK_METHOD)" + "dlc.plot_trajectories(config_path, [video], shuffle=0, videotype=\"mp4\", track_method=TRACK_METHOD)" ] } ], diff --git a/examples/COLAB/COLAB_BUCTD_and_CTD_tracking.ipynb b/examples/COLAB/COLAB_BUCTD_and_CTD_tracking.ipynb index e7e123ca23..81ef8b9062 100644 --- a/examples/COLAB/COLAB_BUCTD_and_CTD_tracking.ipynb +++ b/examples/COLAB/COLAB_BUCTD_and_CTD_tracking.ipynb @@ -481,7 +481,7 @@ " file = response.json()[\"files\"][0]\n", " title = file[\"key\"]\n", " print(f\"Downloading {title}...\")\n", - " with requests.get(file['links']['self'], stream=True) as r:\n", + " with requests.get(file[\"links\"][\"self\"], stream=True) as r:\n", " with ZipFile(BytesIO(r.content)) as zf:\n", " zf.extractall(path=download_path)\n", "else:\n", @@ -491,7 +491,7 @@ "# Check that the config was downloaded correctly\n", "print(f\"Config path: {config}\")\n", "if not Path(config).exists():\n", - " print(f\"Could not find config at {config}: check that the dataset was downloaded correctly!\")\n" + " print(f\"Could not find config at {config}: check that the dataset was downloaded correctly!\")" ] }, { @@ -550,7 +550,7 @@ "rng = np.random.default_rng(seed)\n", "\n", "train_indices = rng.choice(num_images, size=train_images, replace=False, shuffle=False).tolist()\n", - "test_indices = [idx for idx in range(num_images) if idx not in train_indices]\n" + "test_indices = [idx for idx in range(num_images) if idx not in train_indices]" ] }, { @@ -2114,7 +2114,7 @@ " file = response.json()[\"files\"][0]\n", " title = file[\"key\"]\n", " print(f\"Downloading {title}...\")\n", - " with requests.get(file['links']['self'], stream=True) as r:\n", + " with requests.get(file[\"links\"][\"self\"], stream=True) as r:\n", " with ZipFile(BytesIO(r.content)) as zf:\n", " zf.extractall(path=download_path)\n", "else:\n", @@ -2128,7 +2128,7 @@ "# Move the video to the final path\n", "shutil.move(src_video_path, video_path)\n", "if not Path(video_path).exists():\n", - " raise ValueError(\"Failed to move the video\")\n" + " raise ValueError(\"Failed to move the video\")" ] }, { diff --git a/examples/COLAB/COLAB_DEMO_SuperAnimal.ipynb b/examples/COLAB/COLAB_DEMO_SuperAnimal.ipynb index f6efebe8b0..2162f5a809 100644 --- a/examples/COLAB/COLAB_DEMO_SuperAnimal.ipynb +++ b/examples/COLAB/COLAB_DEMO_SuperAnimal.ipynb @@ -93,7 +93,7 @@ "\n", "uploaded = files.upload()\n", "for filepath, content in uploaded.items():\n", - " print(f'User uploaded file \"{filepath}\" with length {len(content)} bytes')\n", + " print(f'User uploaded file \"{filepath}\" with length {len(content)} bytes')\n", "\n", "video_path = Path(filepath).resolve()\n", "\n", @@ -121,10 +121,12 @@ }, "outputs": [], "source": [ - "superanimal_name = \"superanimal_quadruped\" #@param [\"superanimal_topviewmouse\", \"superanimal_quadruped\"]\n", - "model_name = \"hrnet_w32\" #@param [\"hrnet_w32\", \"resnet_50\"]\n", - "detector_name = \"fasterrcnn_resnet50_fpn_v2\" #@param [\"fasterrcnn_resnet50_fpn_v2\", \"fasterrcnn_mobilenet_v3_large_fpn\"]\n", - "pcutoff = 0.15 #@param {type:\"slider\", min:0, max:1, step:0.05}" + "superanimal_name = \"superanimal_quadruped\" # @param [\"superanimal_topviewmouse\", \"superanimal_quadruped\"]\n", + "model_name = \"hrnet_w32\" # @param [\"hrnet_w32\", \"resnet_50\"]\n", + "detector_name = (\n", + " \"fasterrcnn_resnet50_fpn_v2\" # @param [\"fasterrcnn_resnet50_fpn_v2\", \"fasterrcnn_mobilenet_v3_large_fpn\"]\n", + ")\n", + "pcutoff = 0.15 # @param {type:\"slider\", min:0, max:1, step:0.05}" ] }, { @@ -200,11 +202,14 @@ "view_video = open(labeled_video_path, \"rb\").read()\n", "\n", "data_url = \"data:video/mp4;base64,\" + b64encode(view_video).decode()\n", - "HTML(\"\"\"\n", + "HTML(\n", + " \"\"\"\n", "\n", - "\"\"\" % data_url)" + "\"\"\"\n", + " % data_url\n", + ")" ] } ], diff --git a/examples/COLAB/COLAB_DEMO_mouse_openfield.ipynb b/examples/COLAB/COLAB_DEMO_mouse_openfield.ipynb index a4d7543147..04aacb261c 100644 --- a/examples/COLAB/COLAB_DEMO_mouse_openfield.ipynb +++ b/examples/COLAB/COLAB_DEMO_mouse_openfield.ipynb @@ -119,31 +119,31 @@ "outputs": [], "source": [ "# Create a path variable that links to the config file:\n", - "path_config_file = '/content/cloned-DLC-repo/examples/openfield-Pranav-2018-10-30/config.yaml'\n", + "path_config_file = \"/content/cloned-DLC-repo/examples/openfield-Pranav-2018-10-30/config.yaml\"\n", "\n", "# Loading example data set:\n", "deeplabcut.load_demo_data(path_config_file)\n", "\n", - "# Automatically update some hyperparameters for training, \n", - "# here rotations to +/- 180 degrees. This can be helpful for optimizing performance. \n", + "# Automatically update some hyperparameters for training,\n", + "# here rotations to +/- 180 degrees. This can be helpful for optimizing performance.\n", "# see Primer -- Mathis et al. Neuron 2020\n", "from deeplabcut.core.config import read_config_as_dict\n", "import deeplabcut.pose_estimation_pytorch as dlc_torch\n", "\n", "loader = dlc_torch.DLCLoader(\n", - " config=path_config_file, \n", + " config=path_config_file,\n", " trainset_index=0,\n", " shuffle=1,\n", ")\n", "\n", - "# Get the pytorch config path \n", + "# Get the pytorch config path\n", "pytorch_config_path = loader.model_folder / \"pytorch_config.yaml\"\n", "\n", "model_cfg = read_config_as_dict(pytorch_config_path)\n", - "model_cfg['data'][\"train\"][\"affine\"][\"rotation\"]=180\n", + "model_cfg[\"data\"][\"train\"][\"affine\"][\"rotation\"] = 180\n", "\n", "# Save the modified config\n", - "dlc_torch.config.write_config(pytorch_config_path,model_cfg)" + "dlc_torch.config.write_config(pytorch_config_path, model_cfg)" ] }, { diff --git a/examples/COLAB/COLAB_DLC_ModelZoo.ipynb b/examples/COLAB/COLAB_DLC_ModelZoo.ipynb index 1c312afd22..108bd64dac 100644 --- a/examples/COLAB/COLAB_DLC_ModelZoo.ipynb +++ b/examples/COLAB/COLAB_DLC_ModelZoo.ipynb @@ -1,315 +1,312 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "view-in-github" - }, - "source": [ - "\"Open" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "RK255E7YoEIt" - }, - "source": [ - "# DeepLabCut Model Zoo user-contributed models\n", - "\n", - "🚨 **WARNING** -- This is using the old version from 2020-2023 with user-supplied models. Please see the SuperAnimal notebook if you want to use our Foundational Models for Quadrupeds or mice.\n", - "\n", - "![alt text](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1616492373700-PGOAC72IOB6AUE47VTJX/ke17ZwdGBToddI8pDm48kB8JrdUaZR-OSkKLqWQPp_YUqsxRUqqbr1mOJYKfIPR7LoDQ9mXPOjoJoqy81S2I8N_N4V1vUb5AoIIIbLZhVYwL8IeDg6_3B-BRuF4nNrNcQkVuAT7tdErd0wQFEGFSnBqyW03PFN2MN6T6ry5cmXqqA9xITfsbVGDrg_goIDasRCalqV8R3606BuxERAtDaQ/modelzoo.png?format=1000w)\n", - "\n", - "http://modelzoo.deeplabcut.org\n", - "\n", - "You can use this notebook to analyze videos with pretrained networks from our model zoo - NO local installation of DeepLabCut is needed!\n", - "\n", - "- **What you need:** a video of your favorite dog, cat, human, etc: check the list of currently available models here: http://modelzoo.deeplabcut.org\n", - "\n", - "- **What to do:** (1) in the top right corner, click \"CONNECT\". Then, just hit run (play icon) on each cell below and follow the instructions!\n", - "\n", - "## **Please consider giving back and labeling a little data to help make each network even better!**\n", - "\n", - "We have a WebApp, so no need to install anything, just a few clicks! We'd really appreciate your help!\n", - " \n", - "https://contrib.deeplabcut.org/\n", - "\n", - "\n", - "- **Note, if you performance is less that you would like:** firstly check the labeled_video parameters (i.e. \"pcutoff\" in the config.yaml file that will set the video plotting) - see the end of this notebook. You can also use the model in your own projects locally. Please be sure to cite the papers for the model, and http://modelzoo.deeplabcut.org (paper forthcoming!)\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "## **Let's get going: install DeepLabCut into COLAB:**\n", - "\n", - "*Also, be sure you are connected to a GPU: go to menu, click Runtime > Change Runtime Type > select \"GPU\"*\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Install the latest version of DeepLabCut\n", - "!pip install --pre \"deeplabcut[tf,modelzoo]\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "### Important - Restart the Runtime for the updated packages to be imported!\n", - "\n", - "PLEASE, click \"restart runtime\" from the output above before proceeding!" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "ZT4PwGSbYQEO" - }, - "source": [ - "## Now let's set the backend & import the DeepLabCut package\n", - "### (if colab is buggy/throws an error, just rerun this cell):" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "bvoiWefrYQEP" - }, - "outputs": [], - "source": [ - "import os\n", - "import deeplabcut" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "syweXs88tyuO" - }, - "source": [ - "## Next, run the cell below to upload your video file from your computer:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "7eqEZYs_CaLy" - }, - "outputs": [], - "source": [ - "from google.colab import files\n", - "\n", - "uploaded = files.upload()\n", - "for filepath, content in uploaded.items():\n", - " print(f'User uploaded file \"{filepath}\" with length {len(content)} bytes')\n", - "video_path = os.path.abspath(filepath)\n", - "\n", - "# If this cell fails (e.g., when using Safari in place of Google Chrome),\n", - "# manually upload your video via the Files menu to the left\n", - "# and define `video_path` yourself with right click > copy path on the video." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "YsaqOTkZtf-w" - }, - "source": [ - "## Select your model from the dropdown menu, then below (optionally) input the name you want for the project:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "Ih0t7lUjYQEd" - }, - "outputs": [], - "source": [ - "import ipywidgets as widgets\n", - "from IPython.display import display\n", - "\n", - "model_options = deeplabcut.create_project.modelzoo.Modeloptions\n", - "model_selection = widgets.Dropdown(\n", - " options=model_options,\n", - " value=model_options[0],\n", - " description=\"Choose a DLC ModelZoo model!\",\n", - " disabled=False\n", - ")\n", - "display(model_selection)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "UV0QXswGCFrI" - }, - "outputs": [], - "source": [ - "project_name = 'myDLC_modelZoo'\n", - "your_name = 'teamDLC'\n", - "model2use = model_selection.value\n", - "videotype = os.path.splitext(video_path)[-1].lstrip('.') #or MOV, or avi, whatever you uploaded!" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "JQxko-t3uMVO" - }, - "source": [ - "## Attention on this step !!\n", - "- Please note that for optimal performance your videos should contain frames that are around ~300-600 pixels (on one edge). If you have a larger video (like from an iPhone, first downsize by running this please! :)\n", - "\n", - "- Thus, if you're using an iPhone, or such, you'll need to downsample the video first by running the code below**\n", - "\n", - "(no need to edit it unless you want to change the size)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "WpAX3BKY94e0" - }, - "outputs": [], - "source": [ - "video_path = deeplabcut.DownSampleVideo(video_path, width=300)\n", - "print(video_path)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "KJm_Vbx-s5OY" - }, - "source": [ - "## Lastly, run the cell below to create a pretrained project, analyze your video with your selected pretrained network, plot trajectories, and create a labeled video!:\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "T9MGgAdIFKPY" - }, - "outputs": [], - "source": [ - "config_path, train_config_path = deeplabcut.create_pretrained_project(\n", - " project_name,\n", - " your_name,\n", - " [video_path],\n", - " videotype=videotype,\n", - " model=model2use,\n", - " analyzevideo=True,\n", - " createlabeledvideo=True,\n", - " copy_videos=True, #must leave copy_videos=True\n", - " engine=deeplabcut.Engine.TF,\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "WS-KxhBMvEBj" - }, - "source": [ - "Now, you can move this project from Colab (i.e. download it to your GoogleDrive), and use it like a normal standard project!\n", - "\n", - "You can analyze more videos, extract outliers, refine then, and/or then add new key points + label new frames, and retrain if desired. We hope this gives you a good launching point for your work!\n", - "\n", - "###Happy DeepLabCutting! Welcome to the Zoo :)\n", - "\n" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "KPOqiLmo6d7t" - }, - "source": [ - "## More advanced options:\n", - "\n", - "- If you would now like to customize the video/plots - i.e., color, dot size, threshold for the point to be plotted (pcutoff), please simply edit the \"config.yaml\" file by updating the values below:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "yGLNVK1q6rIp" - }, - "outputs": [], - "source": [ - "# Updating the plotting within the config.yaml file (without opening it ;):\n", - "edits = {\n", - " 'dotsize': 7, # size of the dots!\n", - " 'colormap': 'spring', # any matplotlib colormap!\n", - " 'pcutoff': 0.5, # the higher the more conservative the plotting!\n", - "}\n", - "deeplabcut.auxiliaryfunctions.edit_config(config_path, edits)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "Vlc0wZgB7R5e" - }, - "outputs": [], - "source": [ - "# re-create the labeled video (first you will need to delete in the folder to the LEFT!):\n", - "project_path = os.path.dirname(config_path)\n", - "full_video_path = os.path.join(\n", - " project_path,\n", - " 'videos',\n", - " os.path.basename(video_path),\n", - ")\n", - "\n", - "#filter predictions (should already be done above ;):\n", - "deeplabcut.filterpredictions(config_path, [full_video_path], videotype=videotype)\n", - "\n", - "#re-create the video with your edits!\n", - "deeplabcut.create_labeled_video(config_path, [full_video_path], videotype=videotype, filtered=True)" - ] - } - ], - "metadata": { - "colab": { - "include_colab_link": true, - "name": "Copy of COLAB_DLC_ModelZoo.ipynb", - "provenance": [], - "toc_visible": true - }, - "gpuClass": "standard", - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.7" - } + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "view-in-github" + }, + "source": [ + "\"Open" + ] }, - "nbformat": 4, - "nbformat_minor": 0 + { + "cell_type": "markdown", + "metadata": { + "id": "RK255E7YoEIt" + }, + "source": [ + "# DeepLabCut Model Zoo user-contributed models\n", + "\n", + "🚨 **WARNING** -- This is using the old version from 2020-2023 with user-supplied models. Please see the SuperAnimal notebook if you want to use our Foundational Models for Quadrupeds or mice.\n", + "\n", + "![alt text](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1616492373700-PGOAC72IOB6AUE47VTJX/ke17ZwdGBToddI8pDm48kB8JrdUaZR-OSkKLqWQPp_YUqsxRUqqbr1mOJYKfIPR7LoDQ9mXPOjoJoqy81S2I8N_N4V1vUb5AoIIIbLZhVYwL8IeDg6_3B-BRuF4nNrNcQkVuAT7tdErd0wQFEGFSnBqyW03PFN2MN6T6ry5cmXqqA9xITfsbVGDrg_goIDasRCalqV8R3606BuxERAtDaQ/modelzoo.png?format=1000w)\n", + "\n", + "http://modelzoo.deeplabcut.org\n", + "\n", + "You can use this notebook to analyze videos with pretrained networks from our model zoo - NO local installation of DeepLabCut is needed!\n", + "\n", + "- **What you need:** a video of your favorite dog, cat, human, etc: check the list of currently available models here: http://modelzoo.deeplabcut.org\n", + "\n", + "- **What to do:** (1) in the top right corner, click \"CONNECT\". Then, just hit run (play icon) on each cell below and follow the instructions!\n", + "\n", + "## **Please consider giving back and labeling a little data to help make each network even better!**\n", + "\n", + "We have a WebApp, so no need to install anything, just a few clicks! We'd really appreciate your help!\n", + " \n", + "https://contrib.deeplabcut.org/\n", + "\n", + "\n", + "- **Note, if you performance is less that you would like:** firstly check the labeled_video parameters (i.e. \"pcutoff\" in the config.yaml file that will set the video plotting) - see the end of this notebook. You can also use the model in your own projects locally. Please be sure to cite the papers for the model, and http://modelzoo.deeplabcut.org (paper forthcoming!)\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "## **Let's get going: install DeepLabCut into COLAB:**\n", + "\n", + "*Also, be sure you are connected to a GPU: go to menu, click Runtime > Change Runtime Type > select \"GPU\"*\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Install the latest version of DeepLabCut\n", + "!pip install --pre \"deeplabcut[tf,modelzoo]\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Important - Restart the Runtime for the updated packages to be imported!\n", + "\n", + "PLEASE, click \"restart runtime\" from the output above before proceeding!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZT4PwGSbYQEO" + }, + "source": [ + "## Now let's set the backend & import the DeepLabCut package\n", + "### (if colab is buggy/throws an error, just rerun this cell):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "bvoiWefrYQEP" + }, + "outputs": [], + "source": [ + "import os\n", + "import deeplabcut" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "syweXs88tyuO" + }, + "source": [ + "## Next, run the cell below to upload your video file from your computer:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7eqEZYs_CaLy" + }, + "outputs": [], + "source": [ + "from google.colab import files\n", + "\n", + "uploaded = files.upload()\n", + "for filepath, content in uploaded.items():\n", + " print(f'User uploaded file \"{filepath}\" with length {len(content)} bytes')\n", + "video_path = os.path.abspath(filepath)\n", + "\n", + "# If this cell fails (e.g., when using Safari in place of Google Chrome),\n", + "# manually upload your video via the Files menu to the left\n", + "# and define `video_path` yourself with right click > copy path on the video." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "YsaqOTkZtf-w" + }, + "source": [ + "## Select your model from the dropdown menu, then below (optionally) input the name you want for the project:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Ih0t7lUjYQEd" + }, + "outputs": [], + "source": [ + "import ipywidgets as widgets\n", + "from IPython.display import display\n", + "\n", + "model_options = deeplabcut.create_project.modelzoo.Modeloptions\n", + "model_selection = widgets.Dropdown(\n", + " options=model_options, value=model_options[0], description=\"Choose a DLC ModelZoo model!\", disabled=False\n", + ")\n", + "display(model_selection)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "UV0QXswGCFrI" + }, + "outputs": [], + "source": [ + "project_name = \"myDLC_modelZoo\"\n", + "your_name = \"teamDLC\"\n", + "model2use = model_selection.value\n", + "videotype = os.path.splitext(video_path)[-1].lstrip(\".\") # or MOV, or avi, whatever you uploaded!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "JQxko-t3uMVO" + }, + "source": [ + "## Attention on this step !!\n", + "- Please note that for optimal performance your videos should contain frames that are around ~300-600 pixels (on one edge). If you have a larger video (like from an iPhone, first downsize by running this please! :)\n", + "\n", + "- Thus, if you're using an iPhone, or such, you'll need to downsample the video first by running the code below**\n", + "\n", + "(no need to edit it unless you want to change the size)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "WpAX3BKY94e0" + }, + "outputs": [], + "source": [ + "video_path = deeplabcut.DownSampleVideo(video_path, width=300)\n", + "print(video_path)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KJm_Vbx-s5OY" + }, + "source": [ + "## Lastly, run the cell below to create a pretrained project, analyze your video with your selected pretrained network, plot trajectories, and create a labeled video!:\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "T9MGgAdIFKPY" + }, + "outputs": [], + "source": [ + "config_path, train_config_path = deeplabcut.create_pretrained_project(\n", + " project_name,\n", + " your_name,\n", + " [video_path],\n", + " videotype=videotype,\n", + " model=model2use,\n", + " analyzevideo=True,\n", + " createlabeledvideo=True,\n", + " copy_videos=True, # must leave copy_videos=True\n", + " engine=deeplabcut.Engine.TF,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WS-KxhBMvEBj" + }, + "source": [ + "Now, you can move this project from Colab (i.e. download it to your GoogleDrive), and use it like a normal standard project!\n", + "\n", + "You can analyze more videos, extract outliers, refine then, and/or then add new key points + label new frames, and retrain if desired. We hope this gives you a good launching point for your work!\n", + "\n", + "###Happy DeepLabCutting! Welcome to the Zoo :)\n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KPOqiLmo6d7t" + }, + "source": [ + "## More advanced options:\n", + "\n", + "- If you would now like to customize the video/plots - i.e., color, dot size, threshold for the point to be plotted (pcutoff), please simply edit the \"config.yaml\" file by updating the values below:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "yGLNVK1q6rIp" + }, + "outputs": [], + "source": [ + "# Updating the plotting within the config.yaml file (without opening it ;):\n", + "edits = {\n", + " \"dotsize\": 7, # size of the dots!\n", + " \"colormap\": \"spring\", # any matplotlib colormap!\n", + " \"pcutoff\": 0.5, # the higher the more conservative the plotting!\n", + "}\n", + "deeplabcut.auxiliaryfunctions.edit_config(config_path, edits)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Vlc0wZgB7R5e" + }, + "outputs": [], + "source": [ + "# re-create the labeled video (first you will need to delete in the folder to the LEFT!):\n", + "project_path = os.path.dirname(config_path)\n", + "full_video_path = os.path.join(\n", + " project_path,\n", + " \"videos\",\n", + " os.path.basename(video_path),\n", + ")\n", + "\n", + "# filter predictions (should already be done above ;):\n", + "deeplabcut.filterpredictions(config_path, [full_video_path], videotype=videotype)\n", + "\n", + "# re-create the video with your edits!\n", + "deeplabcut.create_labeled_video(config_path, [full_video_path], videotype=videotype, filtered=True)" + ] + } + ], + "metadata": { + "colab": { + "include_colab_link": true, + "name": "Copy of COLAB_DLC_ModelZoo.ipynb", + "provenance": [], + "toc_visible": true + }, + "gpuClass": "standard", + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.7.7" + } + }, + "nbformat": 4, + "nbformat_minor": 0 } diff --git a/examples/COLAB/COLAB_HumanPose_with_RTMPose.ipynb b/examples/COLAB/COLAB_HumanPose_with_RTMPose.ipynb index f3a13ac588..bc32907a05 100644 --- a/examples/COLAB/COLAB_HumanPose_with_RTMPose.ipynb +++ b/examples/COLAB/COLAB_HumanPose_with_RTMPose.ipynb @@ -1,1182 +1,1172 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "id": "t3P1R5BTwud1" - }, - "source": [ - "\"Open\n", - "\n", - "# DeepLabCut RTMPose human pose estimation demo" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "tJm8QpTzyAEe" - }, - "source": [ - "Some useful links:\n", - "\n", - "- DeepLabCut's GitHub: [github.com/DeepLabCut/DeepLabCut](https://github.com/DeepLabCut/DeepLabCut/tree/main)\n", - "- DeepLabCut's Documentation: [deeplabcut.github.io/DeepLabCut](https://deeplabcut.github.io/DeepLabCut/README.html)\n", - "\n", - "This notebook illustrates how to use the cloud to run pose estimation on humans using a pre-trained [RTMPose](https://arxiv.org/abs/2303.07399) model. **⚠️Note: It uses DeepLabCut's low-level interface, so may be suited for more experienced users.⚠️**\n", - "\n", - "RTMPose is a top-down pose estimation model, which means that bounding boxes must be obtained for individuals (which is usually done through an [object detection model](https://en.wikipedia.org/wiki/Object_detection)) before running pose estimation. We obtain bounding boxes using a pre-trained object detector provided by [`torchvision`](https://pytorch.org/vision/main/models.html#object-detection-instance-segmentation-and-person-keypoint-detection).\n", - "\n", - "## Selecting the Runtime and Installing DeepLabCut\n", - "\n", - "**First, go to \"Runtime\" ->\"change runtime type\"->select \"Python3\", and then select \"GPU\".**\n", - "\n", - "Next, we need to install DeepLabCut and its dependencies." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "Aj7Fgm0Xx_fS" - }, - "outputs": [], - "source": [ - "# this will take a couple of minutes to install all the dependencies!\n", - "!pip install --pre deeplabcut" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "twiCWHbgzbwH" - }, - "source": [ - "**(Be sure to click \"RESTART RUNTIME\" if it is displayed above before moving on !) You will see this button at the output of the cells above ^.**" - ] + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "t3P1R5BTwud1" + }, + "source": [ + "\"Open\n", + "\n", + "# DeepLabCut RTMPose human pose estimation demo" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "tJm8QpTzyAEe" + }, + "source": [ + "Some useful links:\n", + "\n", + "- DeepLabCut's GitHub: [github.com/DeepLabCut/DeepLabCut](https://github.com/DeepLabCut/DeepLabCut/tree/main)\n", + "- DeepLabCut's Documentation: [deeplabcut.github.io/DeepLabCut](https://deeplabcut.github.io/DeepLabCut/README.html)\n", + "\n", + "This notebook illustrates how to use the cloud to run pose estimation on humans using a pre-trained [RTMPose](https://arxiv.org/abs/2303.07399) model. **⚠️Note: It uses DeepLabCut's low-level interface, so may be suited for more experienced users.⚠️**\n", + "\n", + "RTMPose is a top-down pose estimation model, which means that bounding boxes must be obtained for individuals (which is usually done through an [object detection model](https://en.wikipedia.org/wiki/Object_detection)) before running pose estimation. We obtain bounding boxes using a pre-trained object detector provided by [`torchvision`](https://pytorch.org/vision/main/models.html#object-detection-instance-segmentation-and-person-keypoint-detection).\n", + "\n", + "## Selecting the Runtime and Installing DeepLabCut\n", + "\n", + "**First, go to \"Runtime\" ->\"change runtime type\"->select \"Python3\", and then select \"GPU\".**\n", + "\n", + "Next, we need to install DeepLabCut and its dependencies." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "Aj7Fgm0Xx_fS" + }, + "outputs": [], + "source": [ + "# this will take a couple of minutes to install all the dependencies!\n", + "!pip install --pre deeplabcut" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "twiCWHbgzbwH" + }, + "source": [ + "**(Be sure to click \"RESTART RUNTIME\" if it is displayed above before moving on !) You will see this button at the output of the cells above ^.**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "x6DugzWMzGoj" + }, + "source": [ + "## Importing Packages and Downloading Model Snapshots" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Y7jKbk_mzPJR" + }, + "source": [ + "Next, we'll need to import `deeplabcut`, `huggingface_hub` and other dependencies needed to run the demo." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "gbXwpGKXzF98", + "outputId": "d7cc8390-e76a-4cc6-b945-42f0951c8d01" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "x6DugzWMzGoj" - }, - "source": [ - "## Importing Packages and Downloading Model Snapshots" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Loading DLC 3.0.0rc10...\n", + "DLC loaded in light mode; you cannot use any GUI (labeling, relabeling and standalone GUI)\n" + ] + } + ], + "source": [ + "from pathlib import Path\n", + "\n", + "import deeplabcut.pose_estimation_pytorch as dlc_torch\n", + "import huggingface_hub\n", + "import matplotlib.collections as collections\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import torch\n", + "import torchvision.models.detection as detection\n", + "from PIL import Image\n", + "from tqdm import tqdm" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "6KWKmWRxzX5R" + }, + "source": [ + "We can now download the pre-trained RTMPose model weights with which we'll run pose estimation." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "L_V11iCszw3s", + "outputId": "8b010e6c-27f5-46ad-f713-2fd07effa3b1" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "Y7jKbk_mzPJR" - }, - "source": [ - "Next, we'll need to import `deeplabcut`, `huggingface_hub` and other dependencies needed to run the demo." - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.11/dist-packages/huggingface_hub/utils/_auth.py:94: UserWarning: \n", + "The secret `HF_TOKEN` does not exist in your Colab secrets.\n", + "To authenticate with the Hugging Face Hub, create a token in your settings tab (https://huggingface.co/settings/tokens), set it as secret in your Google Colab and restart your session.\n", + "You will be able to reuse this secret in all of your notebooks.\n", + "Please note that authentication is recommended but still optional to access public models or datasets.\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "# Folder in COLAB where snapshots will be saved\n", + "model_files = Path(\"hf_files\").resolve()\n", + "model_files.mkdir(exist_ok=True)\n", + "\n", + "# Download the snapshot and model configuration file\n", + "# This is generic code to download any snapshot from HuggingFace\n", + "# To download DeepLabCut SuperAnimal or Model Zoo models, check\n", + "# out dlclibrary!\n", + "path_model_config = Path(\n", + " huggingface_hub.hf_hub_download(\n", + " \"DeepLabCut/HumanBody\",\n", + " \"rtmpose-x_simcc-body7_pytorch_config.yaml\",\n", + " local_dir=model_files,\n", + " )\n", + ")\n", + "path_snapshot = Path(\n", + " huggingface_hub.hf_hub_download(\n", + " \"DeepLabCut/HumanBody\",\n", + " \"rtmpose-x_simcc-body7.pt\",\n", + " local_dir=model_files,\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "eEqukXXy0coy" + }, + "source": [ + "We'll now also define some parameters that we'll later use to plot predictions:\n", + "\n", + "- a colormap for the keypoints to plot\n", + "- a colormap for the limbs of the skeleton\n", + "- a skeleton for the model\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "id": "Tam4rfJK0c_b" + }, + "outputs": [], + "source": [ + "cmap_keypoints = plt.get_cmap(\"rainbow\")\n", + "cmap_skeleton = plt.get_cmap(\"rainbow_r\")\n", + "\n", + "bodyparts2connect = [\n", + " (\"right_ankle\", \"right_knee\"),\n", + " (\"right_knee\", \"right_hip\"),\n", + " (\"left_ankle\", \"left_knee\"),\n", + " (\"left_hip\", \"left_knee\"),\n", + " (\"left_hip\", \"right_hip\"),\n", + " (\"right_shoulder\", \"right_hip\"),\n", + " (\"left_shoulder\", \"left_hip\"),\n", + " (\"left_shoulder\", \"right_shoulder\"),\n", + " (\"left_shoulder\", \"left_elbow\"),\n", + " (\"right_shoulder\", \"right_elbow\"),\n", + " (\"left_elbow\", \"left_wrist\"),\n", + " (\"right_elbow\", \"right_wrist\"),\n", + " (\"right_eye\", \"left_ear\"),\n", + " (\"left_eye\", \"right_eye\"),\n", + " (\"left_eye\", \"left_ear\"),\n", + " (\"right_eye\", \"right_ear\"),\n", + " (\"left_ear\", \"left_shoulder\"),\n", + " (\"right_ear\", \"right_shoulder\"),\n", + " (\"left_shoulder\", \"left_elbow\"),\n", + " (\"right_shoulder\", \"right_elbow\"),\n", + "]\n", + "skeleton = [\n", + " [16, 14],\n", + " [14, 12],\n", + " [17, 15],\n", + " [15, 13],\n", + " [12, 13],\n", + " [6, 12],\n", + " [7, 13],\n", + " [6, 7],\n", + " [6, 8],\n", + " [7, 9],\n", + " [8, 10],\n", + " [9, 11],\n", + " [2, 3],\n", + " [1, 2],\n", + " [1, 3],\n", + " [2, 4],\n", + " [3, 5],\n", + " [4, 6],\n", + " [5, 7],\n", + "]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cCxkkd-b0EJq" + }, + "source": [ + "## Running Inference on Images" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "dotn_xN-05gh" + }, + "source": [ + "First, let's upload some images to run inference on. To do so, you can just run the cell below." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 92 }, + "id": "mZtikE1H0D34", + "outputId": "3d47314f-3ed0-40b2-e54d-2677feef9943" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "id": "gbXwpGKXzF98", - "outputId": "d7cc8390-e76a-4cc6-b945-42f0951c8d01", - "colab": { - "base_uri": "https://localhost:8080/" - } - }, - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Loading DLC 3.0.0rc10...\n", - "DLC loaded in light mode; you cannot use any GUI (labeling, relabeling and standalone GUI)\n" - ] - } + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " Upload widget is only available when the cell has been executed in the\n", + " current browser session. Please rerun this cell to enable.\n", + " \n", + " " ], - "source": [ - "from pathlib import Path\n", - "\n", - "import deeplabcut.pose_estimation_pytorch as dlc_torch\n", - "import huggingface_hub\n", - "import matplotlib.collections as collections\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "import torch\n", - "import torchvision.models.detection as detection\n", - "from PIL import Image\n", - "from tqdm import tqdm" + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": { - "id": "6KWKmWRxzX5R" - }, - "source": [ - "We can now download the pre-trained RTMPose model weights with which we'll run pose estimation." - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving taylor_swift.jpg to taylor_swift.jpg\n", + "User uploaded file 'taylor_swift.jpg' with length 46915 bytes\n" + ] + } + ], + "source": [ + "from google.colab import files\n", + "\n", + "# JPG or PNG is recommended:\n", + "uploaded = files.upload()\n", + "for filepath, content in uploaded.items():\n", + " print(f\"User uploaded file '{filepath}' with length {len(content)} bytes\")\n", + "\n", + "image_paths = [Path(filepath).resolve() for filepath in uploaded.keys()]\n", + "\n", + "# If this cell fails (e.g., when using Safari in place of Google Chrome),\n", + "# manually upload your image via the Files menu to the left and define\n", + "# `image_paths` yourself with right `click` > `copy path` on the image:\n", + "#\n", + "# image_paths = [\n", + "# Path(\"/path/to/my/image_000.png\"),\n", + "# Path(\"/path/to/my/image_001.png\"),\n", + "# ]" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "nj-HtOBSwtdk", + "outputId": "eb5f3b18-cc89-4dd1-a58e-6c39c62582af" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "id": "L_V11iCszw3s", - "outputId": "8b010e6c-27f5-46ad-f713-2fd07effa3b1", - "colab": { - "base_uri": "https://localhost:8080/" - } - }, - "outputs": [ - { - "output_type": "stream", - "name": "stderr", - "text": [ - "/usr/local/lib/python3.11/dist-packages/huggingface_hub/utils/_auth.py:94: UserWarning: \n", - "The secret `HF_TOKEN` does not exist in your Colab secrets.\n", - "To authenticate with the Hugging Face Hub, create a token in your settings tab (https://huggingface.co/settings/tokens), set it as secret in your Google Colab and restart your session.\n", - "You will be able to reuse this secret in all of your notebooks.\n", - "Please note that authentication is recommended but still optional to access public models or datasets.\n", - " warnings.warn(\n" - ] - } - ], - "source": [ - "# Folder in COLAB where snapshots will be saved\n", - "model_files = Path(\"hf_files\").resolve()\n", - "model_files.mkdir(exist_ok=True)\n", - "\n", - "# Download the snapshot and model configuration file\n", - "# This is generic code to download any snapshot from HuggingFace\n", - "# To download DeepLabCut SuperAnimal or Model Zoo models, check\n", - "# out dlclibrary!\n", - "path_model_config = Path(\n", - " huggingface_hub.hf_hub_download(\n", - " \"DeepLabCut/HumanBody\",\n", - " \"rtmpose-x_simcc-body7_pytorch_config.yaml\",\n", - " local_dir=model_files,\n", - " )\n", - ")\n", - "path_snapshot = Path(\n", - " huggingface_hub.hf_hub_download(\n", - " \"DeepLabCut/HumanBody\",\n", - " \"rtmpose-x_simcc-body7.pt\",\n", - " local_dir=model_files,\n", - " )\n", - ")" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Running object detection\n" + ] }, { - "cell_type": "markdown", - "metadata": { - "id": "eEqukXXy0coy" - }, - "source": [ - "We'll now also define some parameters that we'll later use to plot predictions:\n", - "\n", - "- a colormap for the keypoints to plot\n", - "- a colormap for the limbs of the skeleton\n", - "- a skeleton for the model\n" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 1/1 [00:00<00:00, 1.95it/s]\n" + ] }, { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "id": "Tam4rfJK0c_b" - }, - "outputs": [], - "source": [ - "cmap_keypoints = plt.get_cmap(\"rainbow\")\n", - "cmap_skeleton = plt.get_cmap(\"rainbow_r\")\n", - "\n", - "bodyparts2connect = [\n", - " (\"right_ankle\", \"right_knee\"),\n", - " (\"right_knee\", \"right_hip\"),\n", - " (\"left_ankle\", \"left_knee\"),\n", - " (\"left_hip\", \"left_knee\"),\n", - " (\"left_hip\", \"right_hip\"),\n", - " (\"right_shoulder\", \"right_hip\"),\n", - " (\"left_shoulder\", \"left_hip\"),\n", - " (\"left_shoulder\", \"right_shoulder\"),\n", - " (\"left_shoulder\", \"left_elbow\"),\n", - " (\"right_shoulder\", \"right_elbow\"),\n", - " (\"left_elbow\", \"left_wrist\"),\n", - " (\"right_elbow\", \"right_wrist\"),\n", - " (\"right_eye\", \"left_ear\"),\n", - " (\"left_eye\", \"right_eye\"),\n", - " (\"left_eye\", \"left_ear\"),\n", - " (\"right_eye\", \"right_ear\"),\n", - " (\"left_ear\", \"left_shoulder\"),\n", - " (\"right_ear\", \"right_shoulder\"),\n", - " (\"left_shoulder\", \"left_elbow\"),\n", - " (\"right_shoulder\", \"right_elbow\"),\n", - "]\n", - "skeleton = [\n", - " [16, 14],\n", - " [14, 12],\n", - " [17, 15],\n", - " [15, 13],\n", - " [12, 13],\n", - " [6, 12],\n", - " [7, 13],\n", - " [6, 7],\n", - " [6, 8],\n", - " [7, 9],\n", - " [8, 10],\n", - " [9, 11],\n", - " [2, 3],\n", - " [1, 2],\n", - " [1, 3],\n", - " [2, 4],\n", - " [3, 5],\n", - " [4, 6],\n", - " [5, 7],\n", - "]" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Running pose estimation\n" + ] }, { - "cell_type": "markdown", - "metadata": { - "id": "cCxkkd-b0EJq" - }, - "source": [ - "## Running Inference on Images" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "1it [00:00, 78.27it/s]\n" + ] }, { - "cell_type": "markdown", - "metadata": { - "id": "dotn_xN-05gh" - }, - "source": [ - "First, let's upload some images to run inference on. To do so, you can just run the cell below." - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving the predictions to a CSV file\n", + "Done!\n" + ] + } + ], + "source": [ + "# Define the device on which the models will run\n", + "device = \"cuda\" # e.g. cuda, cpu\n", + "\n", + "# The maximum number of detections to keep in an image\n", + "max_detections = 10\n", + "\n", + "#############################################\n", + "# Run a pretrained detector to get bounding boxes\n", + "\n", + "# Load the detector from torchvision\n", + "weights = detection.FasterRCNN_MobileNet_V3_Large_FPN_Weights.DEFAULT\n", + "detector = detection.fasterrcnn_mobilenet_v3_large_fpn(\n", + " weights=weights,\n", + " box_score_thresh=0.6,\n", + ")\n", + "detector.eval()\n", + "detector.to(device)\n", + "preprocess = weights.transforms()\n", + "\n", + "# The context is a list containing the bounding boxes predicted\n", + "# for each image; it will be given to the RTMPose model alongside\n", + "# the images.\n", + "context = []\n", + "\n", + "print(\"Running object detection\")\n", + "with torch.no_grad():\n", + " for image_path in tqdm(image_paths):\n", + " image = Image.open(image_path).convert(\"RGB\")\n", + " batch = [preprocess(image).to(device)]\n", + " predictions = detector(batch)[0]\n", + " bboxes = predictions[\"boxes\"].cpu().numpy()\n", + " labels = predictions[\"labels\"].cpu().numpy()\n", + "\n", + " # Obtain the bounding boxes predicted for humans\n", + " human_bboxes = [bbox for bbox, label in zip(bboxes, labels) if label == 1]\n", + "\n", + " # Convert bounding boxes to xywh format\n", + " bboxes = np.zeros((0, 4))\n", + " if len(human_bboxes) > 0:\n", + " bboxes = np.stack(human_bboxes)\n", + " bboxes[:, 2] -= bboxes[:, 0]\n", + " bboxes[:, 3] -= bboxes[:, 1]\n", + "\n", + " # Only keep the best N detections\n", + " bboxes = bboxes[:max_detections]\n", + "\n", + " context.append({\"bboxes\": bboxes})\n", + "\n", + "\n", + "#############################################\n", + "# Run inference on the images\n", + "pose_cfg = dlc_torch.config.read_config_as_dict(path_model_config)\n", + "runner = dlc_torch.get_pose_inference_runner(\n", + " pose_cfg,\n", + " snapshot_path=path_snapshot,\n", + " batch_size=16,\n", + " max_individuals=max_detections,\n", + ")\n", + "\n", + "print(\"Running pose estimation\")\n", + "predictions = runner.inference(tqdm(zip(image_paths, context)))\n", + "\n", + "\n", + "#############################################\n", + "# Create a DataFrame with the predictions, and save them to a CSV file.\n", + "print(\"Saving the predictions to a CSV file\")\n", + "df = dlc_torch.build_predictions_dataframe(\n", + " scorer=\"rtmpose-body7\",\n", + " predictions={img_path: img_predictions for img_path, img_predictions in zip(image_paths, predictions)},\n", + " parameters=dlc_torch.PoseDatasetParameters(\n", + " bodyparts=pose_cfg[\"metadata\"][\"bodyparts\"],\n", + " unique_bpts=pose_cfg[\"metadata\"][\"unique_bodyparts\"],\n", + " individuals=[f\"idv_{i}\" for i in range(max_detections)],\n", + " ),\n", + ")\n", + "\n", + "# Save to CSV\n", + "df.to_csv(\"image_predictions.csv\")\n", + "\n", + "print(\"Done!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "pWtdL4U52OBJ" + }, + "source": [ + "Finally, we can plot the predictions!" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 447 }, + "id": "3slKu6Lr2MUh", + "outputId": "ef7d938c-39fc-473a-9b88-6169cbfbc567" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "id": "mZtikE1H0D34", - "outputId": "3d47314f-3ed0-40b2-e54d-2677feef9943", - "colab": { - "base_uri": "https://localhost:8080/", - "height": 92 - } - }, - "outputs": [ - { - "output_type": "display_data", - "data": { - "text/plain": [ - "" - ], - "text/html": [ - "\n", - " \n", - " \n", - " Upload widget is only available when the cell has been executed in the\n", - " current browser session. Please rerun this cell to enable.\n", - " \n", - " " - ] - }, - "metadata": {} - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Saving taylor_swift.jpg to taylor_swift.jpg\n", - "User uploaded file 'taylor_swift.jpg' with length 46915 bytes\n" - ] - } - ], - "source": [ - "from google.colab import files\n", - "\n", - "#JPG or PNG is recommended:\n", - "uploaded = files.upload()\n", - "for filepath, content in uploaded.items():\n", - " print(f\"User uploaded file '{filepath}' with length {len(content)} bytes\")\n", - "\n", - "image_paths = [Path(filepath).resolve() for filepath in uploaded.keys()]\n", - "\n", - "# If this cell fails (e.g., when using Safari in place of Google Chrome),\n", - "# manually upload your image via the Files menu to the left and define\n", - "# `image_paths` yourself with right `click` > `copy path` on the image:\n", - "#\n", - "# image_paths = [\n", - "# Path(\"/path/to/my/image_000.png\"),\n", - "# Path(\"/path/to/my/image_001.png\"),\n", - "# ]\n" + "data": { + "image/png": "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\n", + "text/plain": [ + "
" ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "#############################################\n", + "# Unpack and plot predictions\n", + "plot_skeleton = True\n", + "plot_pose_markers = True\n", + "plot_bounding_boxes = True\n", + "marker_size = 12\n", + "\n", + "for image_path, image_predictions in zip(image_paths, predictions):\n", + " image = Image.open(image_path).convert(\"RGB\")\n", + "\n", + " pose = image_predictions[\"bodyparts\"]\n", + " bboxes = image_predictions[\"bboxes\"]\n", + " num_individuals, num_bodyparts = pose.shape[:2]\n", + "\n", + " fig, ax = plt.subplots(figsize=(8, 8))\n", + " ax.imshow(image)\n", + " ax.set_xlim(0, image.width)\n", + " ax.set_ylim(image.height, 0)\n", + " ax.axis(\"off\")\n", + " for idv_pose in pose:\n", + " if plot_skeleton:\n", + " bones = []\n", + " for bpt_1, bpt_2 in skeleton:\n", + " bones.append([idv_pose[bpt_1 - 1, :2], idv_pose[bpt_2 - 1, :2]])\n", + "\n", + " bone_colors = cmap_skeleton\n", + " if not isinstance(cmap_skeleton, str):\n", + " bone_colors = cmap_skeleton(np.linspace(0, 1, len(skeleton)))\n", + "\n", + " ax.add_collection(collections.LineCollection(bones, colors=bone_colors))\n", + "\n", + " if plot_pose_markers:\n", + " ax.scatter(\n", + " idv_pose[:, 0],\n", + " idv_pose[:, 1],\n", + " c=list(range(num_bodyparts)),\n", + " cmap=\"rainbow\",\n", + " s=marker_size,\n", + " )\n", + "\n", + " if plot_bounding_boxes:\n", + " for x, y, w, h in bboxes:\n", + " ax.plot(\n", + " [x, x + w, x + w, x, x],\n", + " [y, y, y + h, y + h, y],\n", + " c=\"r\",\n", + " )\n", + "\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "wO18A_3m5Spk" + }, + "source": [ + "## Running Inference on a Video\n", + "\n", + "Running pose inference on a video is very similar! First, upload a video to Google Drive." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 92 }, + "id": "d9a7gSe15bCa", + "outputId": "698b180c-cd8f-4d17-9c71-f8e58f93631b" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "id": "nj-HtOBSwtdk", - "outputId": "eb5f3b18-cc89-4dd1-a58e-6c39c62582af", - "colab": { - "base_uri": "https://localhost:8080/" - } - }, - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Running object detection\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - "100%|██████████| 1/1 [00:00<00:00, 1.95it/s]\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Running pose estimation\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - "1it [00:00, 78.27it/s]\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Saving the predictions to a CSV file\n", - "Done!\n" - ] - } + "data": { + "text/html": [ + "\n", + " \n", + " \n", + " Upload widget is only available when the cell has been executed in the\n", + " current browser session. Please rerun this cell to enable.\n", + " \n", + " " ], - "source": [ - "# Define the device on which the models will run\n", - "device = \"cuda\" # e.g. cuda, cpu\n", - "\n", - "# The maximum number of detections to keep in an image\n", - "max_detections = 10\n", - "\n", - "#############################################\n", - "# Run a pretrained detector to get bounding boxes\n", - "\n", - "# Load the detector from torchvision\n", - "weights = detection.FasterRCNN_MobileNet_V3_Large_FPN_Weights.DEFAULT\n", - "detector = detection.fasterrcnn_mobilenet_v3_large_fpn(\n", - " weights=weights, box_score_thresh=0.6,\n", - ")\n", - "detector.eval()\n", - "detector.to(device)\n", - "preprocess = weights.transforms()\n", - "\n", - "# The context is a list containing the bounding boxes predicted\n", - "# for each image; it will be given to the RTMPose model alongside\n", - "# the images.\n", - "context = []\n", - "\n", - "print(\"Running object detection\")\n", - "with torch.no_grad():\n", - " for image_path in tqdm(image_paths):\n", - " image = Image.open(image_path).convert(\"RGB\")\n", - " batch = [preprocess(image).to(device)]\n", - " predictions = detector(batch)[0]\n", - " bboxes = predictions[\"boxes\"].cpu().numpy()\n", - " labels = predictions[\"labels\"].cpu().numpy()\n", - "\n", - " # Obtain the bounding boxes predicted for humans\n", - " human_bboxes = [\n", - " bbox for bbox, label in zip(bboxes, labels) if label == 1\n", - " ]\n", - "\n", - " # Convert bounding boxes to xywh format\n", - " bboxes = np.zeros((0, 4))\n", - " if len(human_bboxes) > 0:\n", - " bboxes = np.stack(human_bboxes)\n", - " bboxes[:, 2] -= bboxes[:, 0]\n", - " bboxes[:, 3] -= bboxes[:, 1]\n", - "\n", - " # Only keep the best N detections\n", - " bboxes = bboxes[:max_detections]\n", - "\n", - " context.append({\"bboxes\": bboxes})\n", - "\n", - "\n", - "#############################################\n", - "# Run inference on the images\n", - "pose_cfg = dlc_torch.config.read_config_as_dict(path_model_config)\n", - "runner = dlc_torch.get_pose_inference_runner(\n", - " pose_cfg,\n", - " snapshot_path=path_snapshot,\n", - " batch_size=16,\n", - " max_individuals=max_detections,\n", - ")\n", - "\n", - "print(\"Running pose estimation\")\n", - "predictions = runner.inference(tqdm(zip(image_paths, context)))\n", - "\n", - "\n", - "#############################################\n", - "# Create a DataFrame with the predictions, and save them to a CSV file.\n", - "print(\"Saving the predictions to a CSV file\")\n", - "df = dlc_torch.build_predictions_dataframe(\n", - " scorer=\"rtmpose-body7\",\n", - " predictions={\n", - " img_path: img_predictions\n", - " for img_path, img_predictions in zip(image_paths, predictions)\n", - " },\n", - " parameters=dlc_torch.PoseDatasetParameters(\n", - " bodyparts=pose_cfg[\"metadata\"][\"bodyparts\"],\n", - " unique_bpts=pose_cfg[\"metadata\"][\"unique_bodyparts\"],\n", - " individuals=[f\"idv_{i}\" for i in range(max_detections)]\n", - " )\n", - ")\n", - "\n", - "# Save to CSV\n", - "df.to_csv(\"image_predictions.csv\")\n", - "\n", - "print(\"Done!\")" + "text/plain": [ + "" ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "cell_type": "markdown", - "metadata": { - "id": "pWtdL4U52OBJ" - }, - "source": [ - "Finally, we can plot the predictions!" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "id": "3slKu6Lr2MUh", - "outputId": "ef7d938c-39fc-473a-9b88-6169cbfbc567", - "colab": { - "base_uri": "https://localhost:8080/", - "height": 447 - } - }, - "outputs": [ - { - "output_type": "display_data", - "data": { - "text/plain": [ - "
" - ], - "image/png": "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\n" - }, - "metadata": {} - } - ], - "source": [ - "#############################################\n", - "# Unpack and plot predictions\n", - "plot_skeleton = True\n", - "plot_pose_markers = True\n", - "plot_bounding_boxes = True\n", - "marker_size = 12\n", - "\n", - "for image_path, image_predictions in zip(image_paths, predictions):\n", - " image = Image.open(image_path).convert(\"RGB\")\n", - "\n", - " pose = image_predictions[\"bodyparts\"]\n", - " bboxes = image_predictions[\"bboxes\"]\n", - " num_individuals, num_bodyparts = pose.shape[:2]\n", - "\n", - " fig, ax = plt.subplots(figsize=(8, 8))\n", - " ax.imshow(image)\n", - " ax.set_xlim(0, image.width)\n", - " ax.set_ylim(image.height, 0)\n", - " ax.axis(\"off\")\n", - " for idv_pose in pose:\n", - " if plot_skeleton:\n", - " bones = []\n", - " for bpt_1, bpt_2 in skeleton:\n", - " bones.append([idv_pose[bpt_1 - 1, :2], idv_pose[bpt_2 - 1, :2]])\n", - "\n", - " bone_colors = cmap_skeleton\n", - " if not isinstance(cmap_skeleton, str):\n", - " bone_colors = cmap_skeleton(np.linspace(0, 1, len(skeleton)))\n", - "\n", - " ax.add_collection(\n", - " collections.LineCollection(bones, colors=bone_colors)\n", - " )\n", - "\n", - " if plot_pose_markers:\n", - " ax.scatter(\n", - " idv_pose[:, 0],\n", - " idv_pose[:, 1],\n", - " c=list(range(num_bodyparts)),\n", - " cmap=\"rainbow\",\n", - " s=marker_size,\n", - " )\n", - "\n", - " if plot_bounding_boxes:\n", - " for x, y, w, h in bboxes:\n", - " ax.plot(\n", - " [x, x + w, x + w, x, x],\n", - " [y, y, y + h, y + h, y],\n", - " c=\"r\",\n", - " )\n", - "\n", - " plt.show()" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving taylor-dancing.mov to taylor-dancing.mov\n", + "User uploaded file 'taylor-dancing.mov' with length 1415324 bytes\n" + ] + } + ], + "source": [ + "from google.colab import files\n", + "\n", + "uploaded = files.upload()\n", + "for filepath, content in uploaded.items():\n", + " print(f\"User uploaded file '{filepath}' with length {len(content)} bytes\")\n", + "\n", + "\n", + "video_path = [Path(filepath).resolve() for filepath in uploaded.keys()][0]\n", + "\n", + "# If this cell fails (e.g., when using Safari in place of Google Chrome),\n", + "# manually upload your video via the Files menu to the left and define\n", + "# `video_path` yourself with right `click` > `copy path` on the video:\n", + "#\n", + "# video_path = Path(\"/path/to/my/video.mp4\")" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "I885B01359qu", + "outputId": "0affdeda-a10b-4849-b3cd-edf1cb202b52" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "wO18A_3m5Spk" - }, - "source": [ - "## Running Inference on a Video\n", - "\n", - "Running pose inference on a video is very similar! First, upload a video to Google Drive." - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Running object detection\n" + ] }, { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "id": "d9a7gSe15bCa", - "outputId": "698b180c-cd8f-4d17-9c71-f8e58f93631b", - "colab": { - "base_uri": "https://localhost:8080/", - "height": 92 - } - }, - "outputs": [ - { - "output_type": "display_data", - "data": { - "text/plain": [ - "" - ], - "text/html": [ - "\n", - " \n", - " \n", - " Upload widget is only available when the cell has been executed in the\n", - " current browser session. Please rerun this cell to enable.\n", - " \n", - " " - ] - }, - "metadata": {} - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Saving taylor-dancing.mov to taylor-dancing.mov\n", - "User uploaded file 'taylor-dancing.mov' with length 1415324 bytes\n" - ] - } - ], - "source": [ - "from google.colab import files\n", - "\n", - "uploaded = files.upload()\n", - "for filepath, content in uploaded.items():\n", - " print(f\"User uploaded file '{filepath}' with length {len(content)} bytes\")\n", - "\n", - "\n", - "video_path = [Path(filepath).resolve() for filepath in uploaded.keys()][0]\n", - "\n", - "# If this cell fails (e.g., when using Safari in place of Google Chrome),\n", - "# manually upload your video via the Files menu to the left and define\n", - "# `video_path` yourself with right `click` > `copy path` on the video:\n", - "#\n", - "# video_path = Path(\"/path/to/my/video.mp4\")\n" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + " 81%|████████▏ | 66/81 [00:02<00:00, 25.37it/s]\n" + ] }, { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "id": "I885B01359qu", - "outputId": "0affdeda-a10b-4849-b3cd-edf1cb202b52", - "colab": { - "base_uri": "https://localhost:8080/" - } - }, - "outputs": [ - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Running object detection\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - " 81%|████████▏ | 66/81 [00:02<00:00, 25.37it/s]\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Running pose estimation\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - " 81%|████████▏ | 66/81 [00:01<00:00, 53.25it/s]\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Saving the predictions to a CSV file\n", - "Done!\n" - ] - } - ], - "source": [ - "# Define the device on which the models will run\n", - "device = \"cuda\" # e.g. cuda, cpu\n", - "\n", - "# The maximum number of individuals to detect in an image\n", - "max_detections = 30\n", - "\n", - "\n", - "#############################################\n", - "# Create a video iterator\n", - "video = dlc_torch.VideoIterator(video_path)\n", - "\n", - "\n", - "#############################################\n", - "# Run a pretrained detector to get bounding boxes\n", - "\n", - "# Load the detector from torchvision\n", - "weights = detection.FasterRCNN_MobileNet_V3_Large_FPN_Weights.DEFAULT\n", - "detector = detection.fasterrcnn_mobilenet_v3_large_fpn(\n", - " weights=weights, box_score_thresh=0.6,\n", - ")\n", - "detector.eval()\n", - "detector.to(device)\n", - "preprocess = weights.transforms()\n", - "\n", - "# The context is a list containing the bounding boxes predicted for each frame\n", - "# in the video.\n", - "context = []\n", - "\n", - "print(\"Running object detection\")\n", - "with torch.no_grad():\n", - " for frame in tqdm(video):\n", - " batch = [preprocess(Image.fromarray(frame)).to(device)]\n", - " predictions = detector(batch)[0]\n", - " bboxes = predictions[\"boxes\"].cpu().numpy()\n", - " labels = predictions[\"labels\"].cpu().numpy()\n", - "\n", - " # Obtain the bounding boxes predicted for humans\n", - " human_bboxes = [\n", - " bbox for bbox, label in zip(bboxes, labels) if label == 1\n", - " ]\n", - "\n", - " # Convert bounding boxes to xywh format\n", - " bboxes = np.zeros((0, 4))\n", - " if len(human_bboxes) > 0:\n", - " bboxes = np.stack(human_bboxes)\n", - " bboxes[:, 2] -= bboxes[:, 0]\n", - " bboxes[:, 3] -= bboxes[:, 1]\n", - "\n", - " # Only keep the top N bounding boxes\n", - " bboxes = bboxes[:max_detections]\n", - "\n", - " context.append({\"bboxes\": bboxes})\n", - "\n", - "# Set the context for the video\n", - "video.set_context(context)\n", - "\n", - "\n", - "#############################################\n", - "# Run inference on the images (in this case a single image)\n", - "pose_cfg = dlc_torch.config.read_config_as_dict(path_model_config)\n", - "runner = dlc_torch.get_pose_inference_runner(\n", - " pose_cfg,\n", - " snapshot_path=path_snapshot,\n", - " batch_size=16,\n", - " max_individuals=max_detections,\n", - ")\n", - "\n", - "print(\"Running pose estimation\")\n", - "predictions = runner.inference(tqdm(video))\n", - "\n", - "\n", - "print(\"Saving the predictions to a CSV file\")\n", - "df = dlc_torch.build_predictions_dataframe(\n", - " scorer=\"rtmpose-body7\",\n", - " predictions={\n", - " idx: img_predictions\n", - " for idx, img_predictions in enumerate(predictions)\n", - " },\n", - " parameters=dlc_torch.PoseDatasetParameters(\n", - " bodyparts=pose_cfg[\"metadata\"][\"bodyparts\"],\n", - " unique_bpts=pose_cfg[\"metadata\"][\"unique_bodyparts\"],\n", - " individuals=[f\"idv_{i}\" for i in range(max_detections)]\n", - " )\n", - ")\n", - "df.to_csv(\"video_predictions.csv\")\n", - "\n", - "print(\"Done!\")" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Running pose estimation\n" + ] }, { - "cell_type": "markdown", - "metadata": { - "id": "altka3NGB_su" - }, - "source": [ - "Finally, we can plot the predictions on the video! The labeled video output is saved in the `\"video_predictions.mp4\"` file, and can be downloaded to be viewed." - ] + "name": "stderr", + "output_type": "stream", + "text": [ + " 81%|████████▏ | 66/81 [00:01<00:00, 53.25it/s]\n" + ] }, { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "id": "xRWxH0gO6oPg", - "outputId": "c2cc9025-7741-4403-d5cc-c62470a4ba74", - "colab": { - "base_uri": "https://localhost:8080/" - } - }, - "outputs": [ - { - "output_type": "stream", - "name": "stderr", - "text": [ - "/usr/local/lib/python3.11/dist-packages/deeplabcut/utils/make_labeled_video.py:146: FutureWarning: DataFrame.groupby with axis=1 is deprecated. Do `frame.T.groupby(...)` without axis instead.\n", - " Dataframe.groupby(level=\"individuals\", axis=1).size().values // 3\n" - ] - }, - { - "output_type": "stream", - "name": "stdout", - "text": [ - "Duration of video [s]: 1.57, recorded with 51.7 fps!\n", - "Overall # of frames: 81 with cropped frame dimensions: 828 768\n", - "Generating frames and creating video.\n" - ] - }, - { - "output_type": "stream", - "name": "stderr", - "text": [ - "100%|██████████| 66/66 [00:01<00:00, 35.27it/s]\n" - ] - } - ], - "source": [ - "from deeplabcut.utils.make_labeled_video import CreateVideo\n", - "from deeplabcut.utils.video_processor import VideoProcessorCV\n", - "\n", - "video_output_path = \"video_predictions.mp4\"\n", - "\n", - "clip = VideoProcessorCV(str(video_path), sname=video_output_path, codec=\"mp4v\")\n", - "CreateVideo(\n", - " clip,\n", - " df,\n", - " pcutoff=0.4,\n", - " dotsize=3,\n", - " colormap=\"rainbow\",\n", - " bodyparts2plot=pose_cfg[\"metadata\"][\"bodyparts\"],\n", - " trailpoints=0,\n", - " cropping=False,\n", - " x1=0,\n", - " x2=clip.w,\n", - " y1=0,\n", - " y2=clip.h,\n", - " bodyparts2connect=bodyparts2connect,\n", - " skeleton_color=\"w\",\n", - " draw_skeleton=True,\n", - " displaycropped=True,\n", - " color_by=\"bodypart\",\n", - ")" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Saving the predictions to a CSV file\n", + "Done!\n" + ] } - ], - "metadata": { - "accelerator": "GPU", + ], + "source": [ + "# Define the device on which the models will run\n", + "device = \"cuda\" # e.g. cuda, cpu\n", + "\n", + "# The maximum number of individuals to detect in an image\n", + "max_detections = 30\n", + "\n", + "\n", + "#############################################\n", + "# Create a video iterator\n", + "video = dlc_torch.VideoIterator(video_path)\n", + "\n", + "\n", + "#############################################\n", + "# Run a pretrained detector to get bounding boxes\n", + "\n", + "# Load the detector from torchvision\n", + "weights = detection.FasterRCNN_MobileNet_V3_Large_FPN_Weights.DEFAULT\n", + "detector = detection.fasterrcnn_mobilenet_v3_large_fpn(\n", + " weights=weights,\n", + " box_score_thresh=0.6,\n", + ")\n", + "detector.eval()\n", + "detector.to(device)\n", + "preprocess = weights.transforms()\n", + "\n", + "# The context is a list containing the bounding boxes predicted for each frame\n", + "# in the video.\n", + "context = []\n", + "\n", + "print(\"Running object detection\")\n", + "with torch.no_grad():\n", + " for frame in tqdm(video):\n", + " batch = [preprocess(Image.fromarray(frame)).to(device)]\n", + " predictions = detector(batch)[0]\n", + " bboxes = predictions[\"boxes\"].cpu().numpy()\n", + " labels = predictions[\"labels\"].cpu().numpy()\n", + "\n", + " # Obtain the bounding boxes predicted for humans\n", + " human_bboxes = [bbox for bbox, label in zip(bboxes, labels) if label == 1]\n", + "\n", + " # Convert bounding boxes to xywh format\n", + " bboxes = np.zeros((0, 4))\n", + " if len(human_bboxes) > 0:\n", + " bboxes = np.stack(human_bboxes)\n", + " bboxes[:, 2] -= bboxes[:, 0]\n", + " bboxes[:, 3] -= bboxes[:, 1]\n", + "\n", + " # Only keep the top N bounding boxes\n", + " bboxes = bboxes[:max_detections]\n", + "\n", + " context.append({\"bboxes\": bboxes})\n", + "\n", + "# Set the context for the video\n", + "video.set_context(context)\n", + "\n", + "\n", + "#############################################\n", + "# Run inference on the images (in this case a single image)\n", + "pose_cfg = dlc_torch.config.read_config_as_dict(path_model_config)\n", + "runner = dlc_torch.get_pose_inference_runner(\n", + " pose_cfg,\n", + " snapshot_path=path_snapshot,\n", + " batch_size=16,\n", + " max_individuals=max_detections,\n", + ")\n", + "\n", + "print(\"Running pose estimation\")\n", + "predictions = runner.inference(tqdm(video))\n", + "\n", + "\n", + "print(\"Saving the predictions to a CSV file\")\n", + "df = dlc_torch.build_predictions_dataframe(\n", + " scorer=\"rtmpose-body7\",\n", + " predictions={idx: img_predictions for idx, img_predictions in enumerate(predictions)},\n", + " parameters=dlc_torch.PoseDatasetParameters(\n", + " bodyparts=pose_cfg[\"metadata\"][\"bodyparts\"],\n", + " unique_bpts=pose_cfg[\"metadata\"][\"unique_bodyparts\"],\n", + " individuals=[f\"idv_{i}\" for i in range(max_detections)],\n", + " ),\n", + ")\n", + "df.to_csv(\"video_predictions.csv\")\n", + "\n", + "print(\"Done!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "altka3NGB_su" + }, + "source": [ + "Finally, we can plot the predictions on the video! The labeled video output is saved in the `\"video_predictions.mp4\"` file, and can be downloaded to be viewed." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { "colab": { - "gpuType": "T4", - "provenance": [], - "include_colab_link": true + "base_uri": "https://localhost:8080/" }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" + "id": "xRWxH0gO6oPg", + "outputId": "c2cc9025-7741-4403-d5cc-c62470a4ba74" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.11/dist-packages/deeplabcut/utils/make_labeled_video.py:146: FutureWarning: DataFrame.groupby with axis=1 is deprecated. Do `frame.T.groupby(...)` without axis instead.\n", + " Dataframe.groupby(level=\"individuals\", axis=1).size().values // 3\n" + ] }, - "language_info": { - "name": "python" + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Duration of video [s]: 1.57, recorded with 51.7 fps!\n", + "Overall # of frames: 81 with cropped frame dimensions: 828 768\n", + "Generating frames and creating video.\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 66/66 [00:01<00:00, 35.27it/s]\n" + ] } + ], + "source": [ + "from deeplabcut.utils.make_labeled_video import CreateVideo\n", + "from deeplabcut.utils.video_processor import VideoProcessorCV\n", + "\n", + "video_output_path = \"video_predictions.mp4\"\n", + "\n", + "clip = VideoProcessorCV(str(video_path), sname=video_output_path, codec=\"mp4v\")\n", + "CreateVideo(\n", + " clip,\n", + " df,\n", + " pcutoff=0.4,\n", + " dotsize=3,\n", + " colormap=\"rainbow\",\n", + " bodyparts2plot=pose_cfg[\"metadata\"][\"bodyparts\"],\n", + " trailpoints=0,\n", + " cropping=False,\n", + " x1=0,\n", + " x2=clip.w,\n", + " y1=0,\n", + " y2=clip.h,\n", + " bodyparts2connect=bodyparts2connect,\n", + " skeleton_color=\"w\",\n", + " draw_skeleton=True,\n", + " displaycropped=True,\n", + " color_by=\"bodypart\",\n", + ")" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "T4", + "include_colab_link": true, + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" }, - "nbformat": 4, - "nbformat_minor": 0 + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 } diff --git a/examples/COLAB/COLAB_YOURDATA_SuperAnimal.ipynb b/examples/COLAB/COLAB_YOURDATA_SuperAnimal.ipynb index c4d1fc575e..60e8081f2c 100644 --- a/examples/COLAB/COLAB_YOURDATA_SuperAnimal.ipynb +++ b/examples/COLAB/COLAB_YOURDATA_SuperAnimal.ipynb @@ -174,9 +174,11 @@ "source": [ "# @markdown ---\n", "# @markdown SuperAnimal Configurations\n", - "superanimal_name = \"superanimal_topviewmouse\" #@param [\"superanimal_topviewmouse\", \"superanimal_quadruped\"]\n", - "model_name = \"hrnet_w32\" #@param [\"hrnet_w32\", \"resnet_50\"]\n", - "detector_name = \"fasterrcnn_resnet50_fpn_v2\" #@param [\"fasterrcnn_resnet50_fpn_v2\", \"fasterrcnn_mobilenet_v3_large_fpn\"]\n", + "superanimal_name = \"superanimal_topviewmouse\" # @param [\"superanimal_topviewmouse\", \"superanimal_quadruped\"]\n", + "model_name = \"hrnet_w32\" # @param [\"hrnet_w32\", \"resnet_50\"]\n", + "detector_name = (\n", + " \"fasterrcnn_resnet50_fpn_v2\" # @param [\"fasterrcnn_resnet50_fpn_v2\", \"fasterrcnn_mobilenet_v3_large_fpn\"]\n", + ")\n", "\n", "# @markdown ---\n", "# @markdown What is the maximum number of animals you expect to have in an image\n", @@ -272,9 +274,11 @@ "source": [ "# @markdown ---\n", "# @markdown SuperAnimal Configurations\n", - "superanimal_name = \"superanimal_topviewmouse\" #@param [\"superanimal_topviewmouse\", \"superanimal_quadruped\"]\n", - "model_name = \"hrnet_w32\" #@param [\"hrnet_w32\", \"resnet_50\"]\n", - "detector_name = \"fasterrcnn_resnet50_fpn_v2\" #@param [\"fasterrcnn_resnet50_fpn_v2\", \"fasterrcnn_mobilenet_v3_large_fpn\"]\n", + "superanimal_name = \"superanimal_topviewmouse\" # @param [\"superanimal_topviewmouse\", \"superanimal_quadruped\"]\n", + "model_name = \"hrnet_w32\" # @param [\"hrnet_w32\", \"resnet_50\"]\n", + "detector_name = (\n", + " \"fasterrcnn_resnet50_fpn_v2\" # @param [\"fasterrcnn_resnet50_fpn_v2\", \"fasterrcnn_mobilenet_v3_large_fpn\"]\n", + ")\n", "\n", "# @markdown ---\n", "# @markdown What is the maximum number of animals you expect to have in an image\n", @@ -347,7 +351,7 @@ " bbox_threshold=0.9,\n", " detector_epochs=1,\n", " pose_epochs=1,\n", - " dest_folder=\"/content/\"\n", + " dest_folder=\"/content/\",\n", ")" ] }, @@ -397,7 +401,8 @@ "# authorization instructions:\n", "\n", "from google.colab import drive\n", - "drive.mount('/content/drive')" + "\n", + "drive.mount(\"/content/drive\")" ] }, { @@ -445,9 +450,11 @@ "source": [ "# @markdown ---\n", "# @markdown SuperAnimal Configurations\n", - "superanimal_name = \"superanimal_topviewmouse\" #@param [\"superanimal_topviewmouse\", \"superanimal_quadruped\"]\n", - "model_name = \"hrnet_w32\" #@param [\"hrnet_w32\", \"resnet_50\"]\n", - "detector_name = \"fasterrcnn_resnet50_fpn_v2\" #@param [\"fasterrcnn_resnet50_fpn_v2\", \"fasterrcnn_mobilenet_v3_large_fpn\"]" + "superanimal_name = \"superanimal_topviewmouse\" # @param [\"superanimal_topviewmouse\", \"superanimal_quadruped\"]\n", + "model_name = \"hrnet_w32\" # @param [\"hrnet_w32\", \"resnet_50\"]\n", + "detector_name = (\n", + " \"fasterrcnn_resnet50_fpn_v2\" # @param [\"fasterrcnn_resnet50_fpn_v2\", \"fasterrcnn_mobilenet_v3_large_fpn\"]\n", + ")" ] }, { @@ -661,7 +668,7 @@ "outputs": [], "source": [ "weight_init = build_weight_init(\n", - " cfg=auxiliaryfunctions.read_config(config_path), \n", + " cfg=auxiliaryfunctions.read_config(config_path),\n", " super_animal=superanimal_name,\n", " model_name=model_name,\n", " detector_name=detector_name,\n", @@ -859,7 +866,7 @@ "confusion_matrix_image = Image.open(confusion_matrix_path)\n", "\n", "plt.imshow(confusion_matrix_image)\n", - "plt.axis('off') # Hide the axes for better view\n", + "plt.axis(\"off\") # Hide the axes for better view\n", "plt.show()" ] }, @@ -918,9 +925,7 @@ "create_conversion_table(\n", " config=config_path,\n", " super_animal=superanimal_name,\n", - " project_to_super_animal=read_conversion_table_from_csv(\n", - " conversion_table_path\n", - " ),\n", + " project_to_super_animal=read_conversion_table_from_csv(conversion_table_path),\n", ")" ] }, @@ -947,7 +952,7 @@ "outputs": [], "source": [ "weight_init = build_weight_init(\n", - " cfg=auxiliaryfunctions.read_config(config_path), \n", + " cfg=auxiliaryfunctions.read_config(config_path),\n", " super_animal=superanimal_name,\n", " model_name=model_name,\n", " detector_name=detector_name,\n", @@ -1079,7 +1084,7 @@ "outputs": [], "source": [ "weight_init = build_weight_init(\n", - " cfg=auxiliaryfunctions.read_config(config_path), \n", + " cfg=auxiliaryfunctions.read_config(config_path),\n", " super_animal=superanimal_name,\n", " model_name=model_name,\n", " detector_name=detector_name,\n", diff --git a/examples/COLAB/COLAB_YOURDATA_TrainNetwork_VideoAnalysis.ipynb b/examples/COLAB/COLAB_YOURDATA_TrainNetwork_VideoAnalysis.ipynb index 71e4e88045..b53a2b7259 100644 --- a/examples/COLAB/COLAB_YOURDATA_TrainNetwork_VideoAnalysis.ipynb +++ b/examples/COLAB/COLAB_YOURDATA_TrainNetwork_VideoAnalysis.ipynb @@ -155,7 +155,7 @@ "source": [ "# PLEASE EDIT THIS:\n", "project_folder_name = \"MontBlanc-Daniel-2019-12-16\"\n", - "video_type = \"mp4\" #, mp4, MOV, or avi, whatever you uploaded!\n", + "video_type = \"mp4\" # , mp4, MOV, or avi, whatever you uploaded!\n", "\n", "# No need to edit this, we are going to assume you put videos you want to analyze\n", "# in the \"videos\" folder, but if this is NOT true, edit below:\n", @@ -167,7 +167,7 @@ "# if you want the output files in the same folder as the videos, set this to an empty string.\n", "destfolder = f\"/content/drive/My Drive/{project_folder_name}/labeled-videos\"\n", "\n", - "#No need to edit this, as you set it when you passed the ProjectFolderName (above):\n", + "# No need to edit this, as you set it when you passed the ProjectFolderName (above):\n", "path_config_file = f\"/content/drive/My Drive/{project_folder_name}/config.yaml\"\n", "print(path_config_file)\n", "\n", @@ -206,9 +206,7 @@ "# There are many more functions you can set here, including which network to use!\n", "# Check the docstring for `create_training_dataset` for all options you can use!\n", "\n", - "deeplabcut.create_training_dataset(\n", - " path_config_file, net_type=\"resnet_50\", engine=deeplabcut.Engine.PYTORCH\n", - ")" + "deeplabcut.create_training_dataset(path_config_file, net_type=\"resnet_50\", engine=deeplabcut.Engine.PYTORCH)" ] }, { @@ -289,7 +287,7 @@ "deeplabcut.evaluate_network(path_config_file, plotting=True)\n", "\n", "# Here you want to see a low pixel error! Of course, it can only be as\n", - "# good as the labeler, so be sure your labels are good!\n" + "# good as the labeler, so be sure your labels are good!" ] }, { diff --git a/examples/COLAB/COLAB_transformer_reID.ipynb b/examples/COLAB/COLAB_transformer_reID.ipynb index 008255692f..9125155333 100644 --- a/examples/COLAB/COLAB_transformer_reID.ipynb +++ b/examples/COLAB/COLAB_transformer_reID.ipynb @@ -1,634 +1,632 @@ { - "cells": [ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "colab_type": "text", + "id": "view-in-github" + }, + "source": [ + "\"Open" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "TGChzLdc-lUJ" + }, + "source": [ + "# Demo: How to use our Pose Transformer for unsupervised identity tracking of animals\n", + "![alt text](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1628250004229-KVYD7JJVHYEFDJ32L9VJ/DLClogo2021.jpg?format=1000w)\n", + "\n", + "https://github.com/DeepLabCut/DeepLabCut\n", + "\n", + "### This notebook illustrates how to use the transformer for a multi-animal DeepLabCut (maDLC) Demo tri-mouse project:\n", + "- load our mini-demo data that includes a pretrained model and unlabeled video.\n", + "- analyze a novel video.\n", + "- use the transformer to do unsupervised ID tracking.\n", + "- create quality check plots and video.\n", + "\n", + "### To create a full maDLC pipeline please see our full docs: https://deeplabcut.github.io/DeepLabCut/README.html\n", + "- Of interest is a full how-to for maDLC: https://deeplabcut.github.io/DeepLabCut/docs/maDLC_UserGuide.html\n", + "- a quick guide to maDLC: https://deeplabcut.github.io/DeepLabCut/docs/quick-start/tutorial_maDLC.html\n", + "- a demo COLAB for how to use maDLC on your own data: https://github.com/DeepLabCut/DeepLabCut/blob/main/examples/COLAB/COLAB_YOURDATA_maDLC_TrainNetwork_VideoAnalysis.ipynb\n", + "\n", + "### To get started, please go to \"Runtime\" ->\"change runtime type\"->select \"Python3\", and then select \"GPU\"\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "xOe2hvy85EVP" + }, + "source": [ + "‼️ **Attention: this demo is for maDLC, which is version 2.2**\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "NXmLeZBX45Oe" + }, + "outputs": [], + "source": [ + "# Install DLC version 2.2-2.3 (pre DLC3):\n", + "!pip install \"deeplabcut[tf]\"" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "id": "TlhrVFKN8euh" + }, + "outputs": [], + "source": [ + "import deeplabcut\n", + "import os" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Wid0GTGMAEnZ" + }, + "source": [ + "## Important - Restart the Runtime for the updated packages to be imported!\n", + "\n", + "PLEASE, click \"restart runtime\" from the output above before proceeding!\n", + "\n", + "No information needs edited in the cells below, you can simply click run on each:\n", + "\n", + "### Download our Demo Project from our server:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "PusLdqbqJi60", + "outputId": "dbe30821-d3a7-443f-de74-6cb0bee49aac" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "colab_type": "text", - "id": "view-in-github" - }, - "source": [ - "\"Open" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Downloading demo-me-2021-07-14.zip...\n" + ] + } + ], + "source": [ + "# Download our demo project:\n", + "import requests\n", + "from io import BytesIO\n", + "from zipfile import ZipFile\n", + "\n", + "url_record = \"https://zenodo.org/api/records/7883589\"\n", + "response = requests.get(url_record)\n", + "if response.status_code == 200:\n", + " file = response.json()[\"files\"][0]\n", + " title = file[\"key\"]\n", + " print(f\"Downloading {title}...\")\n", + " with requests.get(file[\"links\"][\"self\"], stream=True) as r:\n", + " with ZipFile(BytesIO(r.content)) as zf:\n", + " zf.extractall(path=\"/content\")\n", + "else:\n", + " raise ValueError(f\"The URL {url_record} could not be reached.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "8iXtySnQB0BE" + }, + "source": [ + "## Analyze a novel 3 mouse video with our maDLC DLCRNet, pretrained on 3 mice data\n", + "\n", + "In one step, since `auto_track=True` you extract detections and association costs, create tracklets, & stitch them. We can use this to compare to the transformer-guided tracking below.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "id": "odYrU3o8BSAr" + }, + "outputs": [], + "source": [ + "project_path = \"/content/demo-me-2021-07-14\"\n", + "config_path = os.path.join(project_path, \"config.yaml\")\n", + "video = os.path.join(project_path, \"videos\", \"videocompressed1.mp4\")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 520 }, + "id": "U_351Hkv81X-", + "outputId": "f7c30461-101f-47b6-c04f-15809aa5a4bb" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "TGChzLdc-lUJ" - }, - "source": [ - "# Demo: How to use our Pose Transformer for unsupervised identity tracking of animals\n", - "![alt text](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1628250004229-KVYD7JJVHYEFDJ32L9VJ/DLClogo2021.jpg?format=1000w)\n", - "\n", - "https://github.com/DeepLabCut/DeepLabCut\n", - "\n", - "### This notebook illustrates how to use the transformer for a multi-animal DeepLabCut (maDLC) Demo tri-mouse project:\n", - "- load our mini-demo data that includes a pretrained model and unlabeled video.\n", - "- analyze a novel video.\n", - "- use the transformer to do unsupervised ID tracking.\n", - "- create quality check plots and video.\n", - "\n", - "### To create a full maDLC pipeline please see our full docs: https://deeplabcut.github.io/DeepLabCut/README.html\n", - "- Of interest is a full how-to for maDLC: https://deeplabcut.github.io/DeepLabCut/docs/maDLC_UserGuide.html\n", - "- a quick guide to maDLC: https://deeplabcut.github.io/DeepLabCut/docs/quick-start/tutorial_maDLC.html\n", - "- a demo COLAB for how to use maDLC on your own data: https://github.com/DeepLabCut/DeepLabCut/blob/main/examples/COLAB/COLAB_YOURDATA_maDLC_TrainNetwork_VideoAnalysis.ipynb\n", - "\n", - "### To get started, please go to \"Runtime\" ->\"change runtime type\"->select \"Python3\", and then select \"GPU\"\n" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Using snapshot-20000 for model /content/demo-me-2021-07-14/dlc-models/iteration-0/demoJul14-trainset95shuffle0\n" + ] }, { - "cell_type": "markdown", - "metadata": { - "id": "xOe2hvy85EVP" - }, - "source": [ - "‼️ **Attention: this demo is for maDLC, which is version 2.2**\n" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.11/dist-packages/tensorflow/python/keras/engine/base_layer_v1.py:1694: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.\n", + " warnings.warn('`layer.apply` is deprecated and '\n" + ] }, { - "cell_type": "code", - "execution_count": null, - "metadata": { - "id": "NXmLeZBX45Oe" - }, - "outputs": [], - "source": [ - "# Install DLC version 2.2-2.3 (pre DLC3):\n", - "!pip install \"deeplabcut[tf]\"" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Activating extracting of PAFs\n", + "Starting to analyze % /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", + "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", + "Duration of video [s]: 77.67 , recorded with 30.0 fps!\n", + "Overall # of frames: 2330 found with (before cropping) frame dimensions: 640 480\n", + "Starting to extract posture from the video(s) with batchsize: 8\n" + ] }, { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "id": "TlhrVFKN8euh" - }, - "outputs": [], - "source": [ - "import deeplabcut\n", - "import os" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 2330/2330 [00:39<00:00, 58.83it/s]\n" + ] }, { - "cell_type": "markdown", - "metadata": { - "id": "Wid0GTGMAEnZ" - }, - "source": [ - "## Important - Restart the Runtime for the updated packages to be imported!\n", - "\n", - "PLEASE, click \"restart runtime\" from the output above before proceeding!\n", - "\n", - "No information needs edited in the cells below, you can simply click run on each:\n", - "\n", - "### Download our Demo Project from our server:" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Video Analyzed. Saving results in /content/demo-me-2021-07-14/videos...\n" + ] }, { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "PusLdqbqJi60", - "outputId": "dbe30821-d3a7-443f-de74-6cb0bee49aac" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Downloading demo-me-2021-07-14.zip...\n" - ] - } - ], - "source": [ - "# Download our demo project:\n", - "import requests\n", - "from io import BytesIO\n", - "from zipfile import ZipFile\n", - "\n", - "url_record = \"https://zenodo.org/api/records/7883589\"\n", - "response = requests.get(url_record)\n", - "if response.status_code == 200:\n", - " file = response.json()[\"files\"][0]\n", - " title = file[\"key\"]\n", - " print(f\"Downloading {title}...\")\n", - " with requests.get(file[\"links\"][\"self\"], stream=True) as r:\n", - " with ZipFile(BytesIO(r.content)) as zf:\n", - " zf.extractall(path=\"/content\")\n", - "else:\n", - " raise ValueError(f\"The URL {url_record} could not be reached.\")" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.11/dist-packages/deeplabcut/utils/auxfun_multianimal.py:83: UserWarning: default_track_method` is undefined in the config.yaml file and will be set to `ellipse`.\n", + " warnings.warn(\n" + ] }, { - "cell_type": "markdown", - "metadata": { - "id": "8iXtySnQB0BE" - }, - "source": [ - "## Analyze a novel 3 mouse video with our maDLC DLCRNet, pretrained on 3 mice data\n", - "\n", - "In one step, since `auto_track=True` you extract detections and association costs, create tracklets, & stitch them. We can use this to compare to the transformer-guided tracking below.\n" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Using snapshot-20000 for model /content/demo-me-2021-07-14/dlc-models/iteration-0/demoJul14-trainset95shuffle0\n", + "Processing... /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", + "Analyzing /content/demo-me-2021-07-14/videos/videocompressed1DLC_dlcrnetms5_demoJul14shuffle0_20000.h5\n" + ] }, { - "cell_type": "code", - "execution_count": 6, - "metadata": { - "id": "odYrU3o8BSAr" - }, - "outputs": [], - "source": [ - "project_path = \"/content/demo-me-2021-07-14\"\n", - "config_path = os.path.join(project_path, \"config.yaml\")\n", - "video = os.path.join(project_path, \"videos\", \"videocompressed1.mp4\")" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 2330/2330 [00:02<00:00, 1088.72it/s]\n", + "2330it [00:06, 342.29it/s] \n" + ] }, { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/", - "height": 520 - }, - "id": "U_351Hkv81X-", - "outputId": "f7c30461-101f-47b6-c04f-15809aa5a4bb" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Using snapshot-20000 for model /content/demo-me-2021-07-14/dlc-models/iteration-0/demoJul14-trainset95shuffle0\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/lib/python3.11/dist-packages/tensorflow/python/keras/engine/base_layer_v1.py:1694: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.\n", - " warnings.warn('`layer.apply` is deprecated and '\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Activating extracting of PAFs\n", - "Starting to analyze % /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", - "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", - "Duration of video [s]: 77.67 , recorded with 30.0 fps!\n", - "Overall # of frames: 2330 found with (before cropping) frame dimensions: 640 480\n", - "Starting to extract posture from the video(s) with batchsize: 8\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|██████████| 2330/2330 [00:39<00:00, 58.83it/s]\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Video Analyzed. Saving results in /content/demo-me-2021-07-14/videos...\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/lib/python3.11/dist-packages/deeplabcut/utils/auxfun_multianimal.py:83: UserWarning: default_track_method` is undefined in the config.yaml file and will be set to `ellipse`.\n", - " warnings.warn(\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Using snapshot-20000 for model /content/demo-me-2021-07-14/dlc-models/iteration-0/demoJul14-trainset95shuffle0\n", - "Processing... /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", - "Analyzing /content/demo-me-2021-07-14/videos/videocompressed1DLC_dlcrnetms5_demoJul14shuffle0_20000.h5\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|██████████| 2330/2330 [00:02<00:00, 1088.72it/s]\n", - "2330it [00:06, 342.29it/s] \n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The tracklets were created (i.e., under the hood deeplabcut.convert_detections2tracklets was run). Now you can 'refine_tracklets' in the GUI, or run 'deeplabcut.stitch_tracklets'.\n", - "Processing... /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|██████████| 4/4 [00:00<00:00, 1488.53it/s]\n", - "/usr/local/lib/python3.11/dist-packages/deeplabcut/refine_training_dataset/stitch.py:934: FutureWarning: Starting with pandas version 3.0 all arguments of to_hdf except for the argument 'path_or_buf' will be keyword-only.\n", - " df.to_hdf(output_name, \"tracks\", format=\"table\", mode=\"w\")\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The videos are analyzed. Time to assemble animals and track 'em... \n", - " Call 'create_video_with_all_detections' to check multi-animal detection quality before tracking.\n", - "If the tracking is not satisfactory for some videos, consider expanding the training set. You can use the function 'extract_outlier_frames' to extract a few representative outlier frames.\n" - ] - }, - { - "data": { - "application/vnd.google.colaboratory.intrinsic+json": { - "type": "string" - }, - "text/plain": [ - "'DLC_dlcrnetms5_demoJul14shuffle0_20000'" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "deeplabcut.analyze_videos(config_path,[video],\n", - " shuffle=0, videotype=\"mp4\",\n", - " auto_track=True)" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "The tracklets were created (i.e., under the hood deeplabcut.convert_detections2tracklets was run). Now you can 'refine_tracklets' in the GUI, or run 'deeplabcut.stitch_tracklets'.\n", + "Processing... /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n" + ] }, { - "cell_type": "markdown", - "metadata": { - "id": "zmdSLRTOER00" - }, - "source": [ - "### Next, you compute the local, spatio-temporal grouping and track body part assemblies frame-by-frame:" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 4/4 [00:00<00:00, 1488.53it/s]\n", + "/usr/local/lib/python3.11/dist-packages/deeplabcut/refine_training_dataset/stitch.py:934: FutureWarning: Starting with pandas version 3.0 all arguments of to_hdf except for the argument 'path_or_buf' will be keyword-only.\n", + " df.to_hdf(output_name, \"tracks\", format=\"table\", mode=\"w\")\n" + ] }, { - "cell_type": "markdown", - "metadata": { - "id": "F-d6kXqnGeUP" - }, - "source": [ - "## Create a pretty video output:" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "The videos are analyzed. Time to assemble animals and track 'em... \n", + " Call 'create_video_with_all_detections' to check multi-animal detection quality before tracking.\n", + "If the tracking is not satisfactory for some videos, consider expanding the training set. You can use the function 'extract_outlier_frames' to extract a few representative outlier frames.\n" + ] }, { - "cell_type": "code", - "execution_count": 8, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "aTRbuUQ1FBO0", - "outputId": "0d182f64-512d-463d-a997-226c7199b724" + "data": { + "application/vnd.google.colaboratory.intrinsic+json": { + "type": "string" }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Filtering with median model /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", - "Saving filtered csv poses!\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/lib/python3.11/dist-packages/deeplabcut/post_processing/filtering.py:298: FutureWarning: Starting with pandas version 3.0 all arguments of to_hdf except for the argument 'path_or_buf' will be keyword-only.\n", - " data.to_hdf(outdataname, \"df_with_missing\", format=\"table\", mode=\"w\")\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Starting to process video: /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", - "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4 and data.\n", - "Duration of video [s]: 77.67, recorded with 30.0 fps!\n", - "Overall # of frames: 2330 with cropped frame dimensions: 640 480\n", - "Generating frames and creating video.\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/lib/python3.11/dist-packages/deeplabcut/utils/make_labeled_video.py:140: FutureWarning: DataFrame.groupby with axis=1 is deprecated. Do `frame.T.groupby(...)` without axis instead.\n", - " Dataframe.groupby(level=\"individuals\", axis=1).size().values // 3\n", - "100%|██████████| 2330/2330 [00:31<00:00, 73.04it/s]\n" - ] - }, - { - "data": { - "text/plain": [ - "[True]" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "#Filter the predictions to remove small jitter, if desired:\n", - "deeplabcut.filterpredictions(config_path, [video], shuffle=0, videotype=\"mp4\")\n", - "deeplabcut.create_labeled_video(\n", - " config_path,\n", - " [video],\n", - " videotype=\"mp4\",\n", - " shuffle=0,\n", - " color_by=\"individual\",\n", - " keypoints_only=False,\n", - " draw_skeleton=True,\n", - " filtered=True,\n", - ")" + "text/plain": [ + "'DLC_dlcrnetms5_demoJul14shuffle0_20000'" ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "deeplabcut.analyze_videos(config_path, [video], shuffle=0, videotype=\"mp4\", auto_track=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "zmdSLRTOER00" + }, + "source": [ + "### Next, you compute the local, spatio-temporal grouping and track body part assemblies frame-by-frame:" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "F-d6kXqnGeUP" + }, + "source": [ + "## Create a pretty video output:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "aTRbuUQ1FBO0", + "outputId": "0d182f64-512d-463d-a997-226c7199b724" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "AYNlrgeNUG4U" - }, - "source": [ - "Now, on the left panel if you click the folder icon, you will see the project folder \"demo-me..\"; click on this and go into \"videos\" and you can find the \"..._id_labeled.mp4\" video, which you can double-click on to download and inspect!" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Filtering with median model /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", + "Saving filtered csv poses!\n" + ] }, { - "cell_type": "markdown", - "metadata": { - "id": "n7GWMBJUA9x5" - }, - "source": [ - "### Create Plots of your data:\n", - "\n", - "> after running, you can look in \"videos\", \"plot-poses\" to check out the trajectories! (sometimes you need to click the folder refresh icon to see it). Within the folder, for example, see plotmus1.png to vide the bodyparts over time vs. pixel position.\n", - "\n" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.11/dist-packages/deeplabcut/post_processing/filtering.py:298: FutureWarning: Starting with pandas version 3.0 all arguments of to_hdf except for the argument 'path_or_buf' will be keyword-only.\n", + " data.to_hdf(outdataname, \"df_with_missing\", format=\"table\", mode=\"w\")\n" + ] }, { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "7w9BDIA7BB_i", - "outputId": "a163087d-cbcb-4e4d-f461-2e24ed19a80b" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4 and data.\n", - "Plots created! Please check the directory \"plot-poses\" within the video directory\n" - ] - } - ], - "source": [ - "deeplabcut.plot_trajectories(config_path, [video], shuffle=0,videotype=\"mp4\")" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting to process video: /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", + "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4 and data.\n", + "Duration of video [s]: 77.67, recorded with 30.0 fps!\n", + "Overall # of frames: 2330 with cropped frame dimensions: 640 480\n", + "Generating frames and creating video.\n" + ] }, { - "cell_type": "markdown", - "metadata": { - "id": "l7BJQq7nxHVz" - }, - "source": [ - "# Transformer for reID\n", - "\n", - "while the tracking here is very good without using the transformer, we want to demo the workflow for you!" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.11/dist-packages/deeplabcut/utils/make_labeled_video.py:140: FutureWarning: DataFrame.groupby with axis=1 is deprecated. Do `frame.T.groupby(...)` without axis instead.\n", + " Dataframe.groupby(level=\"individuals\", axis=1).size().values // 3\n", + "100%|██████████| 2330/2330 [00:31<00:00, 73.04it/s]\n" + ] }, { - "cell_type": "code", - "execution_count": 10, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "5xlO6TVYxQWc", - "outputId": "a433221f-0390-4028-fe68-be0b90adad48" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Using snapshot-20000 for model /content/demo-me-2021-07-14/dlc-models/iteration-0/demoJul14-trainset95shuffle0\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/lib/python3.11/dist-packages/tensorflow/python/keras/engine/base_layer_v1.py:1694: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.\n", - " warnings.warn('`layer.apply` is deprecated and '\n", - "/usr/local/lib/python3.11/dist-packages/tensorflow/python/keras/engine/base_layer_v1.py:1694: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.\n", - " warnings.warn('`layer.apply` is deprecated and '\n", - "/usr/local/lib/python3.11/dist-packages/tensorflow/python/keras/engine/base_layer_v1.py:1694: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.\n", - " warnings.warn('`layer.apply` is deprecated and '\n", - "/usr/local/lib/python3.11/dist-packages/tensorflow/python/keras/engine/base_layer_v1.py:1694: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.\n", - " warnings.warn('`layer.apply` is deprecated and '\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Activating extracting of PAFs\n", - "Starting to analyze % /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", - "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", - "Duration of video [s]: 77.67 , recorded with 30.0 fps!\n", - "Overall # of frames: 2330 found with (before cropping) frame dimensions: 640 480\n", - "Starting to extract posture\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|██████████| 2330/2330 [01:18<00:00, 29.78it/s]\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "If the tracking is not satisfactory for some videos, consider expanding the training set. You can use the function 'extract_outlier_frames' to extract a few representative outlier frames.\n", - "Epoch 10, train acc: 0.61\n", - "Epoch 10, test acc 0.45\n", - "Epoch 20, train acc: 0.74\n", - "Epoch 20, test acc 0.65\n", - "Epoch 30, train acc: 0.78\n", - "Epoch 30, test acc 0.55\n", - "Epoch 40, train acc: 0.76\n", - "Epoch 40, test acc 0.50\n", - "Epoch 50, train acc: 0.85\n", - "Epoch 50, test acc 0.55\n", - "Epoch 60, train acc: 0.84\n", - "Epoch 60, test acc 0.60\n", - "Epoch 70, train acc: 0.85\n", - "Epoch 70, test acc 0.55\n", - "Epoch 80, train acc: 0.79\n", - "Epoch 80, test acc 0.55\n", - "Epoch 90, train acc: 0.88\n", - "Epoch 90, test acc 0.55\n", - "Epoch 100, train acc: 0.84\n", - "Epoch 100, test acc 0.55\n", - "loading params\n", - "Processing... /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "100%|██████████| 4/4 [00:00<00:00, 483.21it/s]\n", - "/usr/local/lib/python3.11/dist-packages/deeplabcut/refine_training_dataset/stitch.py:934: FutureWarning: Starting with pandas version 3.0 all arguments of to_hdf except for the argument 'path_or_buf' will be keyword-only.\n", - " df.to_hdf(output_name, \"tracks\", format=\"table\", mode=\"w\")\n" - ] - } - ], - "source": [ - "deeplabcut.transformer_reID(\n", - " config_path,\n", - " [video],\n", - " shuffle=0,\n", - " videotype=\"mp4\",\n", - " track_method=\"ellipse\",\n", - " n_triplets=100,\n", - ")" + "data": { + "text/plain": [ + "[True]" ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Filter the predictions to remove small jitter, if desired:\n", + "deeplabcut.filterpredictions(config_path, [video], shuffle=0, videotype=\"mp4\")\n", + "deeplabcut.create_labeled_video(\n", + " config_path,\n", + " [video],\n", + " videotype=\"mp4\",\n", + " shuffle=0,\n", + " color_by=\"individual\",\n", + " keypoints_only=False,\n", + " draw_skeleton=True,\n", + " filtered=True,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "AYNlrgeNUG4U" + }, + "source": [ + "Now, on the left panel if you click the folder icon, you will see the project folder \"demo-me..\"; click on this and go into \"videos\" and you can find the \"..._id_labeled.mp4\" video, which you can double-click on to download and inspect!" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "n7GWMBJUA9x5" + }, + "source": [ + "### Create Plots of your data:\n", + "\n", + "> after running, you can look in \"videos\", \"plot-poses\" to check out the trajectories! (sometimes you need to click the folder refresh icon to see it). Within the folder, for example, see plotmus1.png to vide the bodyparts over time vs. pixel position.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "7w9BDIA7BB_i", + "outputId": "a163087d-cbcb-4e4d-f461-2e24ed19a80b" + }, + "outputs": [ { - "cell_type": "markdown", - "metadata": { - "id": "uO_yoqN7xiBT" - }, - "source": [ - "now we can make another video with the transformer-guided tracking:\n" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4 and data.\n", + "Plots created! Please check the directory \"plot-poses\" within the video directory\n" + ] + } + ], + "source": [ + "deeplabcut.plot_trajectories(config_path, [video], shuffle=0, videotype=\"mp4\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "l7BJQq7nxHVz" + }, + "source": [ + "# Transformer for reID\n", + "\n", + "while the tracking here is very good without using the transformer, we want to demo the workflow for you!" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" }, + "id": "5xlO6TVYxQWc", + "outputId": "a433221f-0390-4028-fe68-be0b90adad48" + }, + "outputs": [ { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "MBMbRFEMxmi4", - "outputId": "5ca4357a-c8e1-46c6-ecad-141bfce48cc5" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4 and data.\n", - "Plots created! Please check the directory \"plot-poses\" within the video directory\n" - ] - } - ], - "source": [ - "deeplabcut.plot_trajectories(\n", - " config_path,\n", - " [video],\n", - " shuffle=0,\n", - " videotype=\"mp4\",\n", - " track_method=\"transformer\",\n", - ")" - ] + "name": "stdout", + "output_type": "stream", + "text": [ + "Using snapshot-20000 for model /content/demo-me-2021-07-14/dlc-models/iteration-0/demoJul14-trainset95shuffle0\n" + ] }, { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" - }, - "id": "vx3e-r1CoXaX", - "outputId": "46cdbd39-d1f6-4b78-abba-7e979740f2a2" - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Starting to process video: /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", - "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4 and data.\n", - "Duration of video [s]: 77.67, recorded with 30.0 fps!\n", - "Overall # of frames: 2330 with cropped frame dimensions: 640 480\n", - "Generating frames and creating video.\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/lib/python3.11/dist-packages/deeplabcut/utils/make_labeled_video.py:140: FutureWarning: DataFrame.groupby with axis=1 is deprecated. Do `frame.T.groupby(...)` without axis instead.\n", - " Dataframe.groupby(level=\"individuals\", axis=1).size().values // 3\n", - "100%|██████████| 2330/2330 [00:31<00:00, 73.75it/s]\n" - ] - }, - { - "data": { - "text/plain": [ - "[True]" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "deeplabcut.create_labeled_video(\n", - " config_path,\n", - " [video],\n", - " videotype=\"mp4\",\n", - " shuffle=0,\n", - " color_by=\"individual\",\n", - " keypoints_only=False,\n", - " draw_skeleton=True,\n", - " track_method=\"transformer\"\n", - ")" - ] + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.11/dist-packages/tensorflow/python/keras/engine/base_layer_v1.py:1694: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.\n", + " warnings.warn('`layer.apply` is deprecated and '\n", + "/usr/local/lib/python3.11/dist-packages/tensorflow/python/keras/engine/base_layer_v1.py:1694: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.\n", + " warnings.warn('`layer.apply` is deprecated and '\n", + "/usr/local/lib/python3.11/dist-packages/tensorflow/python/keras/engine/base_layer_v1.py:1694: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.\n", + " warnings.warn('`layer.apply` is deprecated and '\n", + "/usr/local/lib/python3.11/dist-packages/tensorflow/python/keras/engine/base_layer_v1.py:1694: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.\n", + " warnings.warn('`layer.apply` is deprecated and '\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Activating extracting of PAFs\n", + "Starting to analyze % /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", + "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", + "Duration of video [s]: 77.67 , recorded with 30.0 fps!\n", + "Overall # of frames: 2330 found with (before cropping) frame dimensions: 640 480\n", + "Starting to extract posture\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 2330/2330 [01:18<00:00, 29.78it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "If the tracking is not satisfactory for some videos, consider expanding the training set. You can use the function 'extract_outlier_frames' to extract a few representative outlier frames.\n", + "Epoch 10, train acc: 0.61\n", + "Epoch 10, test acc 0.45\n", + "Epoch 20, train acc: 0.74\n", + "Epoch 20, test acc 0.65\n", + "Epoch 30, train acc: 0.78\n", + "Epoch 30, test acc 0.55\n", + "Epoch 40, train acc: 0.76\n", + "Epoch 40, test acc 0.50\n", + "Epoch 50, train acc: 0.85\n", + "Epoch 50, test acc 0.55\n", + "Epoch 60, train acc: 0.84\n", + "Epoch 60, test acc 0.60\n", + "Epoch 70, train acc: 0.85\n", + "Epoch 70, test acc 0.55\n", + "Epoch 80, train acc: 0.79\n", + "Epoch 80, test acc 0.55\n", + "Epoch 90, train acc: 0.88\n", + "Epoch 90, test acc 0.55\n", + "Epoch 100, train acc: 0.84\n", + "Epoch 100, test acc 0.55\n", + "loading params\n", + "Processing... /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 4/4 [00:00<00:00, 483.21it/s]\n", + "/usr/local/lib/python3.11/dist-packages/deeplabcut/refine_training_dataset/stitch.py:934: FutureWarning: Starting with pandas version 3.0 all arguments of to_hdf except for the argument 'path_or_buf' will be keyword-only.\n", + " df.to_hdf(output_name, \"tracks\", format=\"table\", mode=\"w\")\n" + ] } - ], - "metadata": { - "accelerator": "GPU", + ], + "source": [ + "deeplabcut.transformer_reID(\n", + " config_path,\n", + " [video],\n", + " shuffle=0,\n", + " videotype=\"mp4\",\n", + " track_method=\"ellipse\",\n", + " n_triplets=100,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uO_yoqN7xiBT" + }, + "source": [ + "now we can make another video with the transformer-guided tracking:\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { "colab": { - "gpuType": "A100", - "include_colab_link": true, - "machine_shape": "hm", - "name": "COLAB_transformer_reID.ipynb", - "provenance": [] + "base_uri": "https://localhost:8080/" }, - "kernelspec": { - "display_name": "Python 3", - "name": "python3" + "id": "MBMbRFEMxmi4", + "outputId": "5ca4357a-c8e1-46c6-ecad-141bfce48cc5" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4 and data.\n", + "Plots created! Please check the directory \"plot-poses\" within the video directory\n" + ] + } + ], + "source": [ + "deeplabcut.plot_trajectories(\n", + " config_path,\n", + " [video],\n", + " shuffle=0,\n", + " videotype=\"mp4\",\n", + " track_method=\"transformer\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "vx3e-r1CoXaX", + "outputId": "46cdbd39-d1f6-4b78-abba-7e979740f2a2" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting to process video: /content/demo-me-2021-07-14/videos/videocompressed1.mp4\n", + "Loading /content/demo-me-2021-07-14/videos/videocompressed1.mp4 and data.\n", + "Duration of video [s]: 77.67, recorded with 30.0 fps!\n", + "Overall # of frames: 2330 with cropped frame dimensions: 640 480\n", + "Generating frames and creating video.\n" + ] }, - "language_info": { - "name": "python" + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/usr/local/lib/python3.11/dist-packages/deeplabcut/utils/make_labeled_video.py:140: FutureWarning: DataFrame.groupby with axis=1 is deprecated. Do `frame.T.groupby(...)` without axis instead.\n", + " Dataframe.groupby(level=\"individuals\", axis=1).size().values // 3\n", + "100%|██████████| 2330/2330 [00:31<00:00, 73.75it/s]\n" + ] + }, + { + "data": { + "text/plain": [ + "[True]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" } + ], + "source": [ + "deeplabcut.create_labeled_video(\n", + " config_path,\n", + " [video],\n", + " videotype=\"mp4\",\n", + " shuffle=0,\n", + " color_by=\"individual\",\n", + " keypoints_only=False,\n", + " draw_skeleton=True,\n", + " track_method=\"transformer\",\n", + ")" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "A100", + "include_colab_link": true, + "machine_shape": "hm", + "name": "COLAB_transformer_reID.ipynb", + "provenance": [] + }, + "kernelspec": { + "display_name": "Python 3", + "name": "python3" }, - "nbformat": 4, - "nbformat_minor": 0 + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 0 } diff --git a/examples/JUPYTER/Demo_3D_DeepLabCut.ipynb b/examples/JUPYTER/Demo_3D_DeepLabCut.ipynb index 42242b6664..0e12af0e2e 100644 --- a/examples/JUPYTER/Demo_3D_DeepLabCut.ipynb +++ b/examples/JUPYTER/Demo_3D_DeepLabCut.ipynb @@ -50,9 +50,9 @@ "metadata": {}, "outputs": [], "source": [ - "#Setup your project variables:\n", - "YourName = 'teamDLC'\n", - "YourExperimentName = 'testing'" + "# Setup your project variables:\n", + "YourName = \"teamDLC\"\n", + "YourExperimentName = \"testing\"" ] }, { @@ -75,7 +75,7 @@ } ], "source": [ - "config_path = deeplabcut.create_new_project_3d(YourExperimentName,YourName,num_cameras=2)" + "config_path = deeplabcut.create_new_project_3d(YourExperimentName, YourName, num_cameras=2)" ] }, { @@ -93,11 +93,11 @@ "metadata": {}, "outputs": [], "source": [ - "#If you're loading an already created project, just set the 3D Project config_path variable:\n", - "#import os\n", - "#from pathlib import Path\n", - "#config_path3d = os.path.join(os.getcwd(),'testing3D-DeepLabCutTeam-2019-06-05-3d/config.yaml')\n", - "#print(config_path3d)" + "# If you're loading an already created project, just set the 3D Project config_path variable:\n", + "# import os\n", + "# from pathlib import Path\n", + "# config_path3d = os.path.join(os.getcwd(),'testing3D-DeepLabCutTeam-2019-06-05-3d/config.yaml')\n", + "# print(config_path3d)" ] }, { @@ -151,7 +151,7 @@ "metadata": {}, "outputs": [], "source": [ - "deeplabcut.calibrate_cameras(config_path3d, cbrow =9,cbcol =6,calibrate=False,alpha=0.9)" + "deeplabcut.calibrate_cameras(config_path3d, cbrow=9, cbcol=6, calibrate=False, alpha=0.9)" ] }, { @@ -179,7 +179,7 @@ "metadata": {}, "outputs": [], "source": [ - "deeplabcut.calibrate_cameras(config_path3d, cbrow = 9,cbcol = 6, calibrate=True, alpha=0.9)" + "deeplabcut.calibrate_cameras(config_path3d, cbrow=9, cbcol=6, calibrate=True, alpha=0.9)" ] }, { @@ -198,6 +198,7 @@ "outputs": [], "source": [ "import matplotlib\n", + "\n", "%matplotlib inline\n", "\n", "deeplabcut.check_undistortion(config_path3d)" @@ -239,12 +240,12 @@ "metadata": {}, "outputs": [], "source": [ - "# Of course, this does not work on the demo calibration images, \n", + "# Of course, this does not work on the demo calibration images,\n", "# but when you are ready for your own dataset, edit and then run the following!\n", "\n", - "video_path = '/home/yourname/videoFolder'\n", + "video_path = \"/home/yourname/videoFolder\"\n", "\n", - "deeplabcut.triangulate(config_path3d,video_path, videotype='mp4')" + "deeplabcut.triangulate(config_path3d, video_path, videotype=\"mp4\")" ] }, { @@ -269,7 +270,7 @@ "metadata": {}, "outputs": [], "source": [ - "deeplabcut.create_labeled_video_3d(config_path,['triangulated_file_folder'],start=50,end=250, trailpoints=3)" + "deeplabcut.create_labeled_video_3d(config_path, [\"triangulated_file_folder\"], start=50, end=250, trailpoints=3)" ] } ], diff --git a/examples/JUPYTER/Demo_labeledexample_MouseReaching.ipynb b/examples/JUPYTER/Demo_labeledexample_MouseReaching.ipynb index 312e0a1505..141e4f583c 100644 --- a/examples/JUPYTER/Demo_labeledexample_MouseReaching.ipynb +++ b/examples/JUPYTER/Demo_labeledexample_MouseReaching.ipynb @@ -74,7 +74,7 @@ "# If this path does not point to the project from the URL below,\n", "# edit it to make sure it does:\n", "# https://github.com/DeepLabCut/DeepLabCut/tree/main/examples/Reaching-Mackenzie-2018-08-30\n", - "# \n", + "#\n", "# Example - Linux/OSX\n", "# path_config_file = \"/Users/john/DeepLabCut/examples/Reaching-Mackenzie-2018-08-30/config.yaml\"\n", "# Example - Windows\n", @@ -110,7 +110,7 @@ }, "outputs": [], "source": [ - "# Let's load some demo data, and create a training set \n", + "# Let's load some demo data, and create a training set\n", "# (note, this function is not used when you create your own project):\n", "\n", "deeplabcut.load_demo_data(path_config_file)" @@ -161,7 +161,7 @@ "# notice the variables \"save_epochs\" and \"displayiters\" that can be set in the function\n", "deeplabcut.train_network(path_config_file, shuffle=1, save_epochs=2, displayiters=10)\n", "\n", - "# you just need to run this until you get at least 1 snapshot, which is set by: \"save_epochs\" \n", + "# you just need to run this until you get at least 1 snapshot, which is set by: \"save_epochs\"\n", "# (so in this case you could stop after 2 epochs!) How do I stop? Click the STOP button!\n", "\n", "# To train until ~50 epochs on a CPU should be ~15 min\n", @@ -440,7 +440,7 @@ }, "outputs": [], "source": [ - "#Perhaps plot the labels to see how how all the frames are annotated (including the refined ones)\n", + "# Perhaps plot the labels to see how how all the frames are annotated (including the refined ones)\n", "deeplabcut.check_labels(path_config_file)\n", "# if they are off, you can load them in the labeling_gui to adjust!" ] @@ -479,12 +479,12 @@ "outputs": [], "source": [ "snapshot_path = ( # Edit me if needed! Select the path to the snapshot to continue training from!\n", - " Path(path_config_file).parent / \n", - " \"dlc-models-pytorch\" / \n", - " \"iteration-0\" / \n", - " \"ReachingAug30-trainset95shuffle1\" / \n", - " \"train\" / \n", - " \"snapshot-best-080.pt\"\n", + " Path(path_config_file).parent\n", + " / \"dlc-models-pytorch\"\n", + " / \"iteration-0\"\n", + " / \"ReachingAug30-trainset95shuffle1\"\n", + " / \"train\"\n", + " / \"snapshot-best-080.pt\"\n", ")\n", "\n", "deeplabcut.train_network(\n", diff --git a/examples/JUPYTER/Demo_labeledexample_Openfield.ipynb b/examples/JUPYTER/Demo_labeledexample_Openfield.ipynb index d4458f1111..24fd435af7 100644 --- a/examples/JUPYTER/Demo_labeledexample_Openfield.ipynb +++ b/examples/JUPYTER/Demo_labeledexample_Openfield.ipynb @@ -66,7 +66,7 @@ "# If this path does not point to the project from the URL below,\n", "# edit it to make sure it does:\n", "# https://github.com/DeepLabCut/DeepLabCut/tree/main/examples/openfield-Pranav-2018-10-30\n", - "# \n", + "#\n", "# Example - Linux/OSX\n", "# path_config_file = \"/Users/john/DeepLabCut/examples/openfield-Pranav-2018-10-30/config.yaml\"\n", "# Example - Windows\n", @@ -412,12 +412,12 @@ "outputs": [], "source": [ "snapshot_path = ( # Edit me if needed! Select the path to the snapshot to continue training from!\n", - " Path(path_config_file).parent / \n", - " \"dlc-models-pytorch\" / \n", - " \"iteration-0\" / \n", - " \"openfieldOct30-trainset95shuffle1\" / \n", - " \"train\" / \n", - " \"snapshot-best-080.pt\"\n", + " Path(path_config_file).parent\n", + " / \"dlc-models-pytorch\"\n", + " / \"iteration-0\"\n", + " / \"openfieldOct30-trainset95shuffle1\"\n", + " / \"train\"\n", + " / \"snapshot-best-080.pt\"\n", ")\n", "\n", "deeplabcut.train_network(\n", diff --git a/examples/JUPYTER/Demo_napari.ipynb b/examples/JUPYTER/Demo_napari.ipynb index 6ebbfd7a6c..a7d8f752df 100644 --- a/examples/JUPYTER/Demo_napari.ipynb +++ b/examples/JUPYTER/Demo_napari.ipynb @@ -75,17 +75,17 @@ }, "outputs": [], "source": [ - "task = \"Reaching\" # Enter the name of your experiment Task\n", - "experimenter = \"Mackenzie\" # Enter the name of the experimenter\n", + "task = \"Reaching\" # Enter the name of your experiment Task\n", + "experimenter = \"Mackenzie\" # Enter the name of the experimenter\n", "video = [\n", " \"/Users/mwmathis/Documents/DeepLabCut/examples/Reaching-Mackenzie-2018-08-30/videos/reachingvideo1.avi\"\n", - "] # Enter the paths of your videos OR FOLDER you want to grab frames from.\n", + "] # Enter the paths of your videos OR FOLDER you want to grab frames from.\n", "\n", - "path_config_file = deeplabcut.create_new_project(task, experimenter, video, copy_videos=True) \n", + "path_config_file = deeplabcut.create_new_project(task, experimenter, video, copy_videos=True)\n", "\n", "# NOTE: The function returns the path, where your project is.\n", "\n", - "# You could also enter this manually (e.g. if the project is already created and you \n", + "# You could also enter this manually (e.g. if the project is already created and you\n", "# want to pick up, where you stopped...): Enter the path of the config file that was\n", "# just created from the above step (check the folder)\n", "# path_config_file = \"/home/Mackenzie/Reaching/config.yaml\"" @@ -147,7 +147,7 @@ "# there are other ways to grab frames, such as uniformly; please see the paper:\n", "\n", "# AUTOMATIC:\n", - "deeplabcut.extract_frames(path_config_file) " + "deeplabcut.extract_frames(path_config_file)" ] }, { @@ -173,8 +173,8 @@ "# Attention: If you have not installed the napari-dlc plugin, do so now by running this cell:\n", "!pip install napari-deeplabcut\n", "\n", - "#if the plugin does not appear upon launch, consider running in the terminal the above command \n", - "#within the same conda env and then re-starting kernel in your notebook (Kernel > restart)." + "# if the plugin does not appear upon launch, consider running in the terminal the above command\n", + "# within the same conda env and then re-starting kernel in your notebook (Kernel > restart)." ] }, { @@ -190,6 +190,7 @@ "# napari will pop up! Please go to plugin > deeplabcut to start:\n", "%gui qt6\n", "import napari\n", + "\n", "napari.Viewer()" ] }, @@ -215,7 +216,7 @@ }, "outputs": [], "source": [ - "deeplabcut.check_labels(path_config_file) #this creates a subdirectory with the frames + your labels" + "deeplabcut.check_labels(path_config_file) # this creates a subdirectory with the frames + your labels" ] }, { @@ -258,7 +259,7 @@ "outputs": [], "source": [ "deeplabcut.create_training_dataset(path_config_file)\n", - "#remember, there are several networks you can pick, the default is resnet-50!" + "# remember, there are several networks you can pick, the default is resnet-50!" ] }, { @@ -334,9 +335,9 @@ }, "outputs": [], "source": [ - "videofile_path = ['videos/video3.avi','videos/video4.avi'] # Enter a folder OR a list of videos to analyze.\n", + "videofile_path = [\"videos/video3.avi\", \"videos/video4.avi\"] # Enter a folder OR a list of videos to analyze.\n", "\n", - "deeplabcut.analyze_videos(path_config_file,videofile_path, videotype='.avi')" + "deeplabcut.analyze_videos(path_config_file, videofile_path, videotype=\".avi\")" ] }, { @@ -370,7 +371,7 @@ }, "outputs": [], "source": [ - "deeplabcut.extract_outlier_frames(path_config_file,['/videos/video3.avi']) #pass a specific video" + "deeplabcut.extract_outlier_frames(path_config_file, [\"/videos/video3.avi\"]) # pass a specific video" ] }, { @@ -394,10 +395,11 @@ }, "outputs": [], "source": [ - "#now you can edit the \"machine-labeled file\" within napari; \n", - "#just again drop the file and images into the workspace after you load the plugin\n", + "# now you can edit the \"machine-labeled file\" within napari;\n", + "# just again drop the file and images into the workspace after you load the plugin\n", "%gui qt6\n", "import napari\n", + "\n", "napari.Viewer()" ] }, @@ -428,7 +430,7 @@ }, "outputs": [], "source": [ - "#NOW, merge this with your original data:\n", + "# NOW, merge this with your original data:\n", "\n", "deeplabcut.merge_datasets(path_config_file)" ] @@ -493,7 +495,7 @@ }, "outputs": [], "source": [ - "deeplabcut.create_labeled_video(path_config_file,videofile_path)" + "deeplabcut.create_labeled_video(path_config_file, videofile_path)" ] }, { @@ -518,7 +520,7 @@ "outputs": [], "source": [ "%matplotlib notebook #for making interactive plots.\n", - "deeplabcut.plot_trajectories(path_config_file,videofile_path)" + "deeplabcut.plot_trajectories(path_config_file, videofile_path)" ] } ], diff --git a/examples/JUPYTER/Demo_yourowndata.ipynb b/examples/JUPYTER/Demo_yourowndata.ipynb index 0596204849..5a7586c45b 100644 --- a/examples/JUPYTER/Demo_yourowndata.ipynb +++ b/examples/JUPYTER/Demo_yourowndata.ipynb @@ -78,12 +78,12 @@ }, "outputs": [], "source": [ - "task = \"Reaching\" # Enter the name of your experiment Task\n", - "experimenter = \"Mackenzie\" # Enter the name of the experimenter\n", + "task = \"Reaching\" # Enter the name of your experiment Task\n", + "experimenter = \"Mackenzie\" # Enter the name of the experimenter\n", "video = [\n", " \"videos/video1.avi\",\n", " \"videos/video2.avi\",\n", - "] # Enter the paths of your videos OR FOLDER you want to grab frames from.\n", + "] # Enter the paths of your videos OR FOLDER you want to grab frames from.\n", "\n", "path_config_file = deeplabcut.create_new_project(\n", " task,\n", @@ -92,7 +92,7 @@ " copy_videos=True,\n", ")\n", "\n", - "# NOTE: The function returns the path, where your project is. \n", + "# NOTE: The function returns the path, where your project is.\n", "# You could also enter this manually (e.g. if the project is already created and you want to pick up, where you stopped...)\n", "# Enter the path of the config file that was just created from the above step (check the folder):\n", "# path_config_file = \"/home/Mackenzie/Reaching/config.yaml\"" @@ -152,10 +152,10 @@ "outputs": [], "source": [ "%matplotlib inline\n", - "#there are other ways to grab frames, such as uniformly; please see the paper:\n", + "# there are other ways to grab frames, such as uniformly; please see the paper:\n", "\n", - "#AUTOMATIC:\n", - "deeplabcut.extract_frames(path_config_file) " + "# AUTOMATIC:\n", + "deeplabcut.extract_frames(path_config_file)" ] }, { @@ -189,6 +189,7 @@ "\n", "%gui qt6\n", "import napari\n", + "\n", "napari.Viewer()" ] }, @@ -214,7 +215,7 @@ }, "outputs": [], "source": [ - "deeplabcut.check_labels(path_config_file) # this creates a subdirectory with the frames + your labels" + "deeplabcut.check_labels(path_config_file) # this creates a subdirectory with the frames + your labels" ] }, { @@ -336,9 +337,9 @@ }, "outputs": [], "source": [ - "videofile_path = ['videos/video3.avi', 'videos/video4.avi'] # Enter a folder OR a list of videos to analyze.\n", + "videofile_path = [\"videos/video3.avi\", \"videos/video4.avi\"] # Enter a folder OR a list of videos to analyze.\n", "\n", - "deeplabcut.analyze_videos(path_config_file,videofile_path, videotype='.avi')" + "deeplabcut.analyze_videos(path_config_file, videofile_path, videotype=\".avi\")" ] }, { @@ -372,7 +373,7 @@ }, "outputs": [], "source": [ - "deeplabcut.extract_outlier_frames(path_config_file,['/videos/video3.avi']) #pass a specific video" + "deeplabcut.extract_outlier_frames(path_config_file, [\"/videos/video3.avi\"]) # pass a specific video" ] }, { @@ -427,7 +428,7 @@ }, "outputs": [], "source": [ - "#NOW, merge this with your original data:\n", + "# NOW, merge this with your original data:\n", "\n", "deeplabcut.merge_datasets(path_config_file)" ] @@ -512,7 +513,7 @@ }, "outputs": [], "source": [ - "deeplabcut.create_labeled_video(path_config_file,videofile_path)" + "deeplabcut.create_labeled_video(path_config_file, videofile_path)" ] }, { diff --git a/examples/JUPYTER/Docker_TrainNetwork_VideoAnalysis.ipynb b/examples/JUPYTER/Docker_TrainNetwork_VideoAnalysis.ipynb index d6699d623c..af707e668b 100644 --- a/examples/JUPYTER/Docker_TrainNetwork_VideoAnalysis.ipynb +++ b/examples/JUPYTER/Docker_TrainNetwork_VideoAnalysis.ipynb @@ -80,7 +80,7 @@ }, "outputs": [], "source": [ - "# GUIs don't work on in Docker (or the cloud), so label your data locally on your computer! \n", + "# GUIs don't work on in Docker (or the cloud), so label your data locally on your computer!\n", "# This notebook is for you to train and run video analysis!\n", "import os\n", "\n", @@ -120,7 +120,7 @@ "outputs": [], "source": [ "# change to yours!\n", - "path_config_file = '/home/mackenzie/DEEPLABCUT/DeepLabCut/examples/Reaching-Mackenzie-2018-08-30/config.yaml'" + "path_config_file = \"/home/mackenzie/DEEPLABCUT/DeepLabCut/examples/Reaching-Mackenzie-2018-08-30/config.yaml\"" ] }, { @@ -192,7 +192,7 @@ "# hits the end (default, 200 epochs).\n", "\n", "# If you end training before it hits the end, you will see what looks like\n", - "# an error message, but it's not an error - don't worry....\n" + "# an error message, but it's not an error - don't worry...." ] }, { diff --git a/examples/testscript.py b/examples/testscript.py index 9a61f8aafe..5ca32b85de 100644 --- a/examples/testscript.py +++ b/examples/testscript.py @@ -22,6 +22,7 @@ It produces nothing of interest scientifically. """ + import os import platform import random @@ -36,6 +37,7 @@ from deeplabcut.utils import auxiliaryfunctions import matplotlib + matplotlib.use("Agg") # Non-interactive backend, for CI/CD on Windows USE_SHELVE = random.choice([True, False]) @@ -50,11 +52,7 @@ print("Imported DLC!") basepath = os.path.dirname(os.path.realpath(__file__)) videoname = "reachingvideo1" - video = [ - os.path.join( - basepath, "Reaching-Mackenzie-2018-08-30", "videos", videoname + ".avi" - ) - ] + video = [os.path.join(basepath, "Reaching-Mackenzie-2018-08-30", "videos", videoname + ".avi")] # For testing a color video: # videoname='baby4hin2min' @@ -78,9 +76,7 @@ SAVE_ITER = 3 print("CREATING PROJECT") - path_config_file = deeplabcut.create_new_project( - task, scorer, video, copy_videos=True - ) + path_config_file = deeplabcut.create_new_project(task, scorer, video, copy_videos=True) cfg = deeplabcut.auxiliaryfunctions.read_config(path_config_file) cfg["numframes2pick"] = 5 @@ -138,7 +134,10 @@ print("CREATING TRAININGSET") deeplabcut.create_training_dataset( - path_config_file, net_type=NET, augmenter_type=augmenter_type, engine=engine, + path_config_file, + net_type=NET, + augmenter_type=augmenter_type, + engine=engine, ) # Check the training image paths are correctly stored as arrays of strings @@ -230,14 +229,10 @@ def make_frame(t): ) print("analyze again...") - deeplabcut.analyze_videos( - path_config_file, [newvideo], save_as_csv=True, destfolder=DESTFOLDER - ) + deeplabcut.analyze_videos(path_config_file, [newvideo], save_as_csv=True, destfolder=DESTFOLDER) print("CREATE VIDEO") - successful = deeplabcut.create_labeled_video( - path_config_file, [newvideo], destfolder=DESTFOLDER, save_frames=True - ) + successful = deeplabcut.create_labeled_video(path_config_file, [newvideo], destfolder=DESTFOLDER, save_frames=True) assert all(successful), f"Failed to create a labeled video!" print("Making plots") @@ -295,9 +290,7 @@ def make_frame(t): deeplabcut.merge_datasets(path_config_file) # iteration + 1 print("CREATING TRAININGSET") - deeplabcut.create_training_dataset( - path_config_file, net_type=NET, augmenter_type=augmenter_type2, engine=engine - ) + deeplabcut.create_training_dataset(path_config_file, net_type=NET, augmenter_type=augmenter_type2, engine=engine) cfg = deeplabcut.auxiliaryfunctions.read_config(path_config_file) posefile = os.path.join( @@ -334,9 +327,7 @@ def make_frame(t): ) except: # if ffmpeg is broken - newvideo2 = os.path.join( - cfg["project_path"], "videos", videoname + "short2.mp4" - ) + newvideo2 = os.path.join(cfg["project_path"], "videos", videoname + "short2.mp4") from moviepy.editor import VideoClip, VideoFileClip clip = VideoFileClip(video[0]) @@ -362,9 +353,7 @@ def make_frame(t): ) print("Extracting skeleton distances, filter and plot filtered output") - deeplabcut.analyzeskeleton( - path_config_file, [newvideo2], save_as_csv=True, destfolder=DESTFOLDER - ) + deeplabcut.analyzeskeleton(path_config_file, [newvideo2], save_as_csv=True, destfolder=DESTFOLDER) deeplabcut.filterpredictions(path_config_file, [newvideo2]) successful = deeplabcut.create_labeled_video( @@ -382,9 +371,7 @@ def make_frame(t): ) assert all(successful), f"Failed to create a labeled video!" - deeplabcut.plot_trajectories( - path_config_file, [newvideo2], destfolder=DESTFOLDER, filtered=True - ) + deeplabcut.plot_trajectories(path_config_file, [newvideo2], destfolder=DESTFOLDER, filtered=True) print("ALL DONE!!! - default cases without Tensorpack loader are functional.") @@ -422,9 +409,7 @@ def make_frame(t): deeplabcut.auxiliaryfunctions.write_plainconfig(posefile, DLC_config) print("TRAINING shuffle 2, with smaller allocated memory") - deeplabcut.train_network( - path_config_file, shuffle=2, allow_growth=True, maxiters=updated_max_iters - ) + deeplabcut.train_network(path_config_file, shuffle=2, allow_growth=True, maxiters=updated_max_iters) print("ANALYZING some individual frames") deeplabcut.analyze_time_lapse_frames( @@ -436,9 +421,7 @@ def make_frame(t): deeplabcut.export_model(path_config_file, shuffle=2, make_tar=False) print("Merging datasets...") - trainIndices, testIndices = deeplabcut.mergeandsplit( - path_config_file, trainindex=0, uniform=True - ) + trainIndices, testIndices = deeplabcut.mergeandsplit(path_config_file, trainindex=0, uniform=True) print("Creating two identical splits...") deeplabcut.create_training_dataset( diff --git a/examples/testscript_3d.py b/examples/testscript_3d.py index e9d7603b15..1475228172 100644 --- a/examples/testscript_3d.py +++ b/examples/testscript_3d.py @@ -20,6 +20,7 @@ This script tests various functionalities in an automatic way. It produces nothing of interest scientifically. """ + import os, deeplabcut import zipfile, urllib.request, shutil from datetime import datetime as dt @@ -121,9 +122,7 @@ cfg["skeleton"] = [["bodypart1", "bodypart2"], ["objectA", "bodypart3"]] deeplabcut.auxiliaryfunctions.write_config_3d(path_config_file, cfg) except: - raise ( - "Please delete the project and re-try." - ) # otherwise the cfg is an empty array! + raise ("Please delete the project and re-try.") # otherwise the cfg is an empty array! """ # Creating the name of the project @@ -138,7 +137,7 @@ os.chdir(os.path.join(project_name, "calibration_images")) - file_name = os.path.join(basepath,"stereo_example.zip") + file_name = os.path.join(basepath, "stereo_example.zip") with zipfile.ZipFile(file_name) as zf: zf.extractall() @@ -176,9 +175,7 @@ print("TRIANGULATING") video_dir = os.path.join(os.path.dirname(basepath), folder) - deeplabcut.auxiliaryfunctions.edit_config( - path_config_file, edits={"pcutoff": 0.1} - ) # otherwise get all-nan slices + deeplabcut.auxiliaryfunctions.edit_config(path_config_file, edits={"pcutoff": 0.1}) # otherwise get all-nan slices deeplabcut.triangulate(path_config_file, video_dir, save_as_csv=True) print("CREATING LABELED VIDEO 3-D") diff --git a/examples/testscript_deterministicwithResNet152.py b/examples/testscript_deterministicwithResNet152.py index 30e93d783d..819a79a795 100644 --- a/examples/testscript_deterministicwithResNet152.py +++ b/examples/testscript_deterministicwithResNet152.py @@ -50,11 +50,7 @@ print("Imported DLC!") basepath = os.path.dirname(os.path.abspath("testscript.py")) videoname = "reachingvideo1" -video = [ - os.path.join( - basepath, "Reaching-Mackenzie-2018-08-30", "videos", videoname + ".avi" - ) -] +video = [os.path.join(basepath, "Reaching-Mackenzie-2018-08-30", "videos", videoname + ".avi")] # to test destination folder: # dfolder=basepath diff --git a/examples/testscript_mobilenets.py b/examples/testscript_mobilenets.py index 72d94b1df5..2e43d91b93 100644 --- a/examples/testscript_mobilenets.py +++ b/examples/testscript_mobilenets.py @@ -22,6 +22,7 @@ It produces nothing of interest scientifically. """ + import os os.environ["DLClight"] = "True" @@ -31,9 +32,7 @@ import numpy as np -def Cuttrainingschedule( - path_config_file, shuffle, trainingsetindex=0, initweights="imagenet", lastvalue=10 -): +def Cuttrainingschedule(path_config_file, shuffle, trainingsetindex=0, initweights="imagenet", lastvalue=10): cfg = deeplabcut.auxiliaryfunctions.read_config(path_config_file) posefile = os.path.join( cfg["project_path"], @@ -82,11 +81,7 @@ def Cuttrainingschedule( print("Imported DLC!") basepath = os.path.dirname(os.path.realpath(__file__)) videoname = "reachingvideo1" - video = [ - os.path.join( - basepath, "Reaching-Mackenzie-2018-08-30", "videos", videoname + ".avi" - ) - ] + video = [os.path.join(basepath, "Reaching-Mackenzie-2018-08-30", "videos", videoname + ".avi")] # to test destination folder: dfolder = os.path.join(basepath, "OUT") @@ -96,9 +91,7 @@ def Cuttrainingschedule( augmenter_type = "tensorpack" # imgaug' print("CREATING PROJECT") - path_config_file = deeplabcut.create_new_project( - task, scorer, video, copy_videos=True - ) + path_config_file = deeplabcut.create_new_project(task, scorer, video, copy_videos=True) cfg = deeplabcut.auxiliaryfunctions.read_config(path_config_file) cfg["numframes2pick"] = 5 @@ -153,9 +146,7 @@ def Cuttrainingschedule( print("Plot labels...") deeplabcut.check_labels(path_config_file) - for shuffle, net_type in enumerate( - ["mobilenet_v2_0.35", "resnet_50"] - ): #'mobilenet_v2_1.0']): # 'resnet_50']): + for shuffle, net_type in enumerate(["mobilenet_v2_0.35", "resnet_50"]): #'mobilenet_v2_1.0']): # 'resnet_50']): """ if shuffle==0: keepdeconvweights=True @@ -164,9 +155,7 @@ def Cuttrainingschedule( """ print("CREATING TRAININGSET", net_type) if "resnet_50" == net_type: # this tests the default condition... - deeplabcut.create_training_dataset( - path_config_file, Shuffles=[shuffle], augmenter_type=augmenter_type - ) + deeplabcut.create_training_dataset(path_config_file, Shuffles=[shuffle], augmenter_type=augmenter_type) else: deeplabcut.create_training_dataset( path_config_file, @@ -242,9 +231,7 @@ def Cuttrainingschedule( print("RELABELING") DF = pd.read_hdf(file, "df_with_missing") DLCscorer = np.unique(DF.columns.get_level_values(0))[0] - DF.columns.set_levels( - [scorer.replace(DLCscorer, scorer)], level=0, inplace=True - ) + DF.columns.set_levels([scorer.replace(DLCscorer, scorer)], level=0, inplace=True) DF = DF.drop("likelihood", axis=1, level=2) DF.to_csv( os.path.join( @@ -270,17 +257,11 @@ def Cuttrainingschedule( deeplabcut.merge_datasets(path_config_file) print("CREATING TRAININGSET") - deeplabcut.create_training_dataset( - path_config_file, Shuffles=[shuffle], net_type=net_type - ) - Cuttrainingschedule( - path_config_file, shuffle, lastvalue=stoptrain, initweights="previteration" - ) + deeplabcut.create_training_dataset(path_config_file, Shuffles=[shuffle], net_type=net_type) + Cuttrainingschedule(path_config_file, shuffle, lastvalue=stoptrain, initweights="previteration") print("TRAINING from previous snapshot!!!!!") - deeplabcut.train_network( - path_config_file, shuffle=shuffle, keepdeconvweights=keepdeconvweights - ) + deeplabcut.train_network(path_config_file, shuffle=shuffle, keepdeconvweights=keepdeconvweights) print("ANALYZING some individual frames") deeplabcut.analyze_time_lapse_frames( diff --git a/examples/testscript_multianimal.py b/examples/testscript_multianimal.py index b7dea7f9a3..3ca2ce73fc 100644 --- a/examples/testscript_multianimal.py +++ b/examples/testscript_multianimal.py @@ -17,6 +17,7 @@ import pandas as pd import matplotlib + matplotlib.use("Agg") # Non-interactive backend, for CI/CD on Windows import deeplabcut @@ -50,14 +51,10 @@ DESTFOLDER = basepath video = "m3v1mp4" - video_path = os.path.join( - basepath, "openfield-Pranav-2018-10-30", "videos", video + ".mp4" - ) + video_path = os.path.join(basepath, "openfield-Pranav-2018-10-30", "videos", video + ".mp4") print("Creating project...") - config_path = deeplabcut.create_new_project( - TASK, SCORER, [video_path], copy_videos=True, multianimal=True - ) + config_path = deeplabcut.create_new_project(TASK, SCORER, [video_path], copy_videos=True, multianimal=True) print("Project created.") @@ -86,34 +83,23 @@ bodyparts_single, bodyparts_multi, ) = auxfun_multianimal.extractindividualsandbodyparts(cfg) - animals_id = [i for i in range(n_animals) for _ in bodyparts_multi] + [ - n_animals - ] * len(bodyparts_single) + animals_id = [i for i in range(n_animals) for _ in bodyparts_multi] + [n_animals] * len(bodyparts_single) map_ = dict(zip(range(len(animals)), animals)) individuals = [map_[ind] for ind in animals_id for _ in range(2)] scorer = [SCORER] * len(individuals) coords = ["x", "y"] * len(animals_id) - bodyparts = [ - bp for _ in range(n_animals) for bp in bodyparts_multi for _ in range(2) - ] + bodyparts = [bp for _ in range(n_animals) for bp in bodyparts_multi for _ in range(2)] bodyparts += [bp for bp in bodyparts_single for _ in range(2)] columns = pd.MultiIndex.from_arrays( [scorer, individuals, bodyparts, coords], names=["scorer", "individuals", "bodyparts", "coords"], ) - index = [ - os.path.join(rel_folder, image) - for image in auxiliaryfunctions.grab_files_in_folder(image_folder, "png") - ] - fake_data = np.tile( - np.repeat(50 * np.arange(len(animals_id)) + 50, 2), (len(index), 1) - ) + index = [os.path.join(rel_folder, image) for image in auxiliaryfunctions.grab_files_in_folder(image_folder, "png")] + fake_data = np.tile(np.repeat(50 * np.arange(len(animals_id)) + 50, 2), (len(index), 1)) df = pd.DataFrame(fake_data, index=index, columns=columns) output_path = os.path.join(image_folder, f"CollectedData_{SCORER}.csv") df.to_csv(output_path) - df.to_hdf( - output_path.replace("csv", "h5"), key="df_with_missing", format="table", mode="w" - ) + df.to_hdf(output_path.replace("csv", "h5"), key="df_with_missing", format="table", mode="w") print("Artificial data created.") print("Checking labels...") @@ -164,9 +150,7 @@ print("Network trained.") print("Evaluating network...") - deeplabcut.evaluate_network( - config_path, plotting=True, per_keypoint_evaluation=True - ) + deeplabcut.evaluate_network(config_path, plotting=True, per_keypoint_evaluation=True) print("Network evaluated....") @@ -205,9 +189,7 @@ print("Video created.") print("Convert detections to tracklets...") - deeplabcut.convert_detections2tracklets( - config_path, [new_video_path], "mp4", track_method=TESTTRACKER - ) + deeplabcut.convert_detections2tracklets(config_path, [new_video_path], "mp4", track_method=TESTTRACKER) print("Tracklets created...") h5path = os.path.splitext(new_video_path)[0] + scorer + "_el.h5" try: @@ -224,9 +206,7 @@ individuals = [map_[ind] for ind in animals_id for _ in range(3)] scorer = [SCORER] * len(individuals) coords = ["x", "y", "likelihood"] * len(animals_id) - bodyparts = [ - bp for _ in range(n_animals) for bp in bodyparts_multi for _ in range(3) - ] + bodyparts = [bp for _ in range(n_animals) for bp in bodyparts_multi for _ in range(3)] bodyparts += [bp for bp in bodyparts_single for _ in range(3)] columns = pd.MultiIndex.from_arrays( [scorer, individuals, bodyparts, coords], @@ -238,9 +218,7 @@ df.to_hdf(h5path, key="data") print("Plotting trajectories...") - deeplabcut.plot_trajectories( - config_path, [new_video_path], "mp4", track_method=TESTTRACKER - ) + deeplabcut.plot_trajectories(config_path, [new_video_path], "mp4", track_method=TESTTRACKER) print("Trajectory plotted.") print("Creating labeled video...") @@ -255,15 +233,11 @@ print("Labeled video created.") print("Filtering predictions...") - deeplabcut.filterpredictions( - config_path, [new_video_path], "mp4", track_method=TESTTRACKER - ) + deeplabcut.filterpredictions(config_path, [new_video_path], "mp4", track_method=TESTTRACKER) print("Predictions filtered.") print("Extracting outlier frames...") - deeplabcut.extract_outlier_frames( - config_path, [new_video_path], "mp4", automatic=True, track_method=TESTTRACKER - ) + deeplabcut.extract_outlier_frames(config_path, [new_video_path], "mp4", automatic=True, track_method=TESTTRACKER) print("Outlier frames extracted.") vname = Path(new_video_path).stem @@ -309,9 +283,7 @@ print("Network trained.") print("Evaluating network...") - deeplabcut.evaluate_network( - config_path, plotting=True, per_keypoint_evaluation=True - ) + deeplabcut.evaluate_network(config_path, plotting=True, per_keypoint_evaluation=True) print("Network evaluated....") @@ -331,9 +303,7 @@ deeplabcut.export_model(config_path, shuffle=1, make_tar=False) print("Merging datasets...") - trainIndices, testIndices = deeplabcut.mergeandsplit( - config_path, trainindex=0, uniform=True - ) + trainIndices, testIndices = deeplabcut.mergeandsplit(config_path, trainindex=0, uniform=True) print("Creating two identical splits...") deeplabcut.create_multianimaltraining_dataset( diff --git a/examples/testscript_openfielddata.py b/examples/testscript_openfielddata.py index 760464a24a..47d894ae34 100644 --- a/examples/testscript_openfielddata.py +++ b/examples/testscript_openfielddata.py @@ -29,15 +29,14 @@ The analysis of the video takes 41 seconds (batch size 32) and creating the frames 8 seconds (+ a few seconds for ffmpeg) to create the video. """ + import deeplabcut import os if __name__ == "__main__": # Loading example data set - path_config_file = os.path.join( - os.getcwd(), "openfield-Pranav-2018-10-30/config.yaml" - ) + path_config_file = os.path.join(os.getcwd(), "openfield-Pranav-2018-10-30/config.yaml") deeplabcut.load_demo_data(path_config_file) shuffle = 13 @@ -45,9 +44,7 @@ cfg = deeplabcut.auxiliaryfunctions.read_config(path_config_file) # example how to set pose config variables: - posefile, _, _ = deeplabcut.return_train_network_path( - path_config_file, shuffle=shuffle - ) + posefile, _, _ = deeplabcut.return_train_network_path(path_config_file, shuffle=shuffle) edits = {"save_iters": 15000, "display_iters": 1000, "multi_step": [[0.005, 15001]]} DLC_config = deeplabcut.auxiliaryfunctions.edit_config(posefile, edits) @@ -58,12 +55,8 @@ deeplabcut.evaluate_network(path_config_file, Shuffles=[shuffle], plotting=True) print("Analyze Video") - videofile_path = os.path.join( - os.getcwd(), "openfield-Pranav-2018-10-30", "videos", "m3v1mp4.mp4" - ) - deeplabcut.analyze_videos( - path_config_file, [videofile_path], shuffle=shuffle - ) # ,videotype='.mp4') + videofile_path = os.path.join(os.getcwd(), "openfield-Pranav-2018-10-30", "videos", "m3v1mp4.mp4") + deeplabcut.analyze_videos(path_config_file, [videofile_path], shuffle=shuffle) # ,videotype='.mp4') print("Create Labeled Video") deeplabcut.create_labeled_video( diff --git a/examples/testscript_openfielddata_augmentationcomparison.py b/examples/testscript_openfielddata_augmentationcomparison.py index c98b1944e4..9a3ef1fa11 100644 --- a/examples/testscript_openfielddata_augmentationcomparison.py +++ b/examples/testscript_openfielddata_augmentationcomparison.py @@ -12,7 +12,7 @@ # """ -This is a test script to compare the loaders and models. +This is a test script to compare the loaders and models. This script creates one identical splits for the openfield test dataset and trains it with imgaug (default), scalecrop and the tensorpack loader. We also compare 3 backbones (mobilenet, resnet, efficientnet) @@ -55,7 +55,6 @@ """ - import os os.environ["CUDA_VISIBLE_DEVICES"] = str(0) @@ -82,9 +81,7 @@ ) for idx, shuffle in enumerate(Shuffles): - posefile, _, _ = deeplabcut.return_train_network_path( - path_config_file, shuffle=shuffle - ) + posefile, _, _ = deeplabcut.return_train_network_path(path_config_file, shuffle=shuffle) # Setting specific parameters for training if idx % 3 == 0: # imgaug @@ -115,9 +112,7 @@ print("Analyze Video") - videofile_path = os.path.join( - os.getcwd(), "openfield-Pranav-2018-10-30", "videos", "m3v1mp4.mp4" - ) + videofile_path = os.path.join(os.getcwd(), "openfield-Pranav-2018-10-30", "videos", "m3v1mp4.mp4") deeplabcut.analyze_videos(path_config_file, [videofile_path], shuffle=shuffle) diff --git a/examples/testscript_pretrained_models.py b/examples/testscript_pretrained_models.py index 99bd3c0295..e183f0786a 100644 --- a/examples/testscript_pretrained_models.py +++ b/examples/testscript_pretrained_models.py @@ -12,6 +12,7 @@ Testscript human network """ + import os, subprocess, deeplabcut from pathlib import Path import pandas as pd @@ -23,11 +24,7 @@ basepath = os.path.dirname(os.path.abspath("testscript.py")) videoname = "reachingvideo1" -video = [ - os.path.join( - basepath, "Reaching-Mackenzie-2018-08-30", "videos", videoname + ".avi" - ) -] +video = [os.path.join(basepath, "Reaching-Mackenzie-2018-08-30", "videos", videoname + ".avi")] # legacy mode: """ diff --git a/examples/testscript_pytorch_multi_animal.py b/examples/testscript_pytorch_multi_animal.py index a62d370104..2d3ccce0e5 100644 --- a/examples/testscript_pytorch_multi_animal.py +++ b/examples/testscript_pytorch_multi_animal.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Testscript for single animal PyTorch projects""" + from __future__ import annotations from pathlib import Path @@ -132,8 +133,7 @@ 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 + conditions_shuffle=net_types.index("resnet_50") + 1, # shuffles start at index 1 create_labeled_videos=True, delete_after_test_run=True, ) diff --git a/examples/testscript_superanimal_adaptation.py b/examples/testscript_superanimal_adaptation.py index a4d0e15d9d..f235abf999 100644 --- a/examples/testscript_superanimal_adaptation.py +++ b/examples/testscript_superanimal_adaptation.py @@ -11,6 +11,7 @@ """ Test script for super animal adaptation """ + import deeplabcut import os @@ -18,9 +19,7 @@ if __name__ == "__main__": basepath = os.path.dirname(os.path.realpath(__file__)) videoname = "m3v1mp4" - video = os.path.join( - basepath, "openfield-Pranav-2018-10-30", "videos", videoname + ".mp4" - ) + video = os.path.join(basepath, "openfield-Pranav-2018-10-30", "videos", videoname + ".mp4") video = deeplabcut.ShortenVideo( video, start="00:00:00", diff --git a/examples/testscript_superanimal_create_pretrained_project.py b/examples/testscript_superanimal_create_pretrained_project.py index b61491f576..bc1234fbfb 100644 --- a/examples/testscript_superanimal_create_pretrained_project.py +++ b/examples/testscript_superanimal_create_pretrained_project.py @@ -12,6 +12,7 @@ Testscript for creating a pretrained project from a super animal model """ + import glob import shutil from pathlib import Path diff --git a/examples/testscript_superanimal_inference.py b/examples/testscript_superanimal_inference.py index c0a042e08a..2ec86c1f21 100644 --- a/examples/testscript_superanimal_inference.py +++ b/examples/testscript_superanimal_inference.py @@ -12,6 +12,7 @@ Testscript for super animal inference """ + import deeplabcut import os @@ -19,11 +20,7 @@ if __name__ == "__main__": basepath = os.path.dirname(os.path.realpath(__file__)) videoname = "reachingvideo1" - video = [ - os.path.join( - basepath, "Reaching-Mackenzie-2018-08-30", "videos", videoname + ".avi" - ) - ] + video = [os.path.join(basepath, "Reaching-Mackenzie-2018-08-30", "videos", videoname + ".avi")] print("testing superanimal_topviewmouse") superanimal_name = "superanimal_topviewmouse" diff --git a/examples/testscript_superanimal_transfer_learning.py b/examples/testscript_superanimal_transfer_learning.py index 887d7ee174..2fe20416b2 100644 --- a/examples/testscript_superanimal_transfer_learning.py +++ b/examples/testscript_superanimal_transfer_learning.py @@ -11,6 +11,7 @@ """ Test script for super animal adaptation """ + import os import deeplabcut @@ -18,7 +19,6 @@ print(deeplabcut.__file__) if __name__ == "__main__": - superanimal_name = "superanimal_topviewmouse" basepath = os.path.dirname(os.path.realpath(__file__)) config_path = os.path.join(basepath, "openfield-Pranav-2018-10-30", "config.yaml") diff --git a/examples/testscript_transreid.py b/examples/testscript_transreid.py index 6d2695dfc0..5d6e2ec3b6 100644 --- a/examples/testscript_transreid.py +++ b/examples/testscript_transreid.py @@ -43,14 +43,10 @@ DESTFOLDER = basepath video = "m3v1mp4" - video_path = os.path.join( - basepath, "openfield-Pranav-2018-10-30", "videos", video + ".mp4" - ) + video_path = os.path.join(basepath, "openfield-Pranav-2018-10-30", "videos", video + ".mp4") print("Creating project...") - config_path = deeplabcut.create_new_project( - TASK, SCORER, [video_path], copy_videos=True, multianimal=True - ) + config_path = deeplabcut.create_new_project(TASK, SCORER, [video_path], copy_videos=True, multianimal=True) print("Project created.") @@ -79,34 +75,23 @@ bodyparts_single, bodyparts_multi, ) = auxfun_multianimal.extractindividualsandbodyparts(cfg) - animals_id = [i for i in range(n_animals) for _ in bodyparts_multi] + [ - n_animals - ] * len(bodyparts_single) + animals_id = [i for i in range(n_animals) for _ in bodyparts_multi] + [n_animals] * len(bodyparts_single) map_ = dict(zip(range(len(animals)), animals)) individuals = [map_[ind] for ind in animals_id for _ in range(2)] scorer = [SCORER] * len(individuals) coords = ["x", "y"] * len(animals_id) - bodyparts = [ - bp for _ in range(n_animals) for bp in bodyparts_multi for _ in range(2) - ] + bodyparts = [bp for _ in range(n_animals) for bp in bodyparts_multi for _ in range(2)] bodyparts += [bp for bp in bodyparts_single for _ in range(2)] columns = pd.MultiIndex.from_arrays( [scorer, individuals, bodyparts, coords], names=["scorer", "individuals", "bodyparts", "coords"], ) - index = [ - os.path.join(rel_folder, image) - for image in auxiliaryfunctions.grab_files_in_folder(image_folder, "png") - ] - fake_data = np.tile( - np.repeat(50 * np.arange(len(animals_id)) + 50, 2), (len(index), 1) - ) + index = [os.path.join(rel_folder, image) for image in auxiliaryfunctions.grab_files_in_folder(image_folder, "png")] + fake_data = np.tile(np.repeat(50 * np.arange(len(animals_id)) + 50, 2), (len(index), 1)) df = pd.DataFrame(fake_data, index=index, columns=columns) output_path = os.path.join(image_folder, f"CollectedData_{SCORER}.csv") df.to_csv(output_path) - df.to_hdf( - output_path.replace("csv", "h5"), key="df_with_missing", format="table", mode="w" - ) + df.to_hdf(output_path.replace("csv", "h5"), key="df_with_missing", format="table", mode="w") print("Artificial data created.") print("Checking labels...") @@ -114,9 +99,7 @@ print("Labels checked.") print("Creating train dataset...") - deeplabcut.create_multianimaltraining_dataset( - config_path, net_type=NET, crop_size=(200, 200) - ) + deeplabcut.create_multianimaltraining_dataset(config_path, net_type=NET, crop_size=(200, 200)) print("Train dataset created.") # Check the training image paths are correctly stored as arrays of strings @@ -134,9 +117,7 @@ assert all(len(pickledata[i]["joints"]) == 3 for i in range(num_images)) print("Editing pose config...") - model_folder = auxiliaryfunctions.get_model_folder( - TRAIN_SIZE, 1, cfg, cfg["project_path"] - ) + model_folder = auxiliaryfunctions.get_model_folder(TRAIN_SIZE, 1, cfg, cfg["project_path"]) pose_config_path = os.path.join(model_folder, "train", "pose_cfg.yaml") edits = { "global_scale": 0.5, @@ -191,9 +172,7 @@ print("Video created.") print("Convert detections to tracklets...") - deeplabcut.convert_detections2tracklets( - config_path, [new_video_path], "mp4", track_method=TESTTRACKER - ) + deeplabcut.convert_detections2tracklets(config_path, [new_video_path], "mp4", track_method=TESTTRACKER) print("Tracklets created...") ### adding it here @@ -202,9 +181,7 @@ trainposeconfigfile, testposeconfigfile, snapshotfolder, - ) = deeplabcut.return_train_network_path( - config_path, shuffle=1, modelprefix=modelprefix, trainingsetindex=0 - ) + ) = deeplabcut.return_train_network_path(config_path, shuffle=1, modelprefix=modelprefix, trainingsetindex=0) print("Creating triplet dataset") @@ -230,9 +207,7 @@ ckpt_folder=snapshotfolder, ) - transformer_checkpoint = os.path.join( - snapshotfolder, f"dlc_transreid_{train_epochs}.pth" - ) + transformer_checkpoint = os.path.join(snapshotfolder, f"dlc_transreid_{train_epochs}.pth") print("Stitching tracklets based on transformer") @@ -245,9 +220,7 @@ ) print("Plotting trajectories...") - deeplabcut.plot_trajectories( - config_path, [new_video_path], "mp4", track_method=TESTTRACKER - ) + deeplabcut.plot_trajectories(config_path, [new_video_path], "mp4", track_method=TESTTRACKER) print("Trajectory plotted.") print("Creating labeled video...") @@ -262,15 +235,11 @@ print("Labeled video created.") print("Filtering predictions...") - deeplabcut.filterpredictions( - config_path, [new_video_path], "mp4", track_method=TESTTRACKER - ) + deeplabcut.filterpredictions(config_path, [new_video_path], "mp4", track_method=TESTTRACKER) print("Predictions filtered.") print("Extracting outlier frames...") - deeplabcut.extract_outlier_frames( - config_path, [new_video_path], "mp4", automatic=True, track_method=TESTTRACKER - ) + deeplabcut.extract_outlier_frames(config_path, [new_video_path], "mp4", automatic=True, track_method=TESTTRACKER) print("Outlier frames extracted.") vname = Path(new_video_path).stem diff --git a/examples/utils.py b/examples/utils.py index 656f15f300..385875d2c2 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -232,9 +232,7 @@ def gen_fake_image( x, y = int(bpt_data.x), int(bpt_data.y) xmin, xmax = max(0, x - radius), min(img_w - 1, x + radius) ymin, ymax = max(0, y - radius), min(img_h - 1, y + radius) - image_array[ymin:ymax, xmin:xmax, 2] = int( - 255 * (i + 1) / params.num_unique - ) + image_array[ymin:ymax, xmin:xmax, 2] = int(255 * (i + 1) / params.num_unique) img = Image.fromarray(image_array) img.save(project_root / Path(*row.name)) @@ -354,17 +352,11 @@ def run( times = [time.time()] log_step(f"Testing with net type {net_type}") log_step("Creating the training dataset") - deeplabcut.create_training_dataset( - str(config_path), net_type=net_type, engine=engine - ) - existing_shuffles = get_existing_shuffle_indices( - config_path, train_fraction=train_fraction, engine=engine - ) + deeplabcut.create_training_dataset(str(config_path), net_type=net_type, engine=engine) + existing_shuffles = get_existing_shuffle_indices(config_path, train_fraction=train_fraction, engine=engine) shuffle_index = existing_shuffles[-1] - log_step( - f"Starting training for train_frac {train_fraction}, shuffle {shuffle_index}" - ) + log_step(f"Starting training for train_frac {train_fraction}, shuffle {shuffle_index}") deeplabcut.train_network( config=str(config_path), shuffle=shuffle_index, @@ -375,9 +367,7 @@ def run( times.append(time.time()) log_step(f"Train time: {times[-1] - times[-2]} seconds") - log_step( - f"Starting evaluation for train_frac {train_fraction}, shuffle {shuffle_index}" - ) + log_step(f"Starting evaluation for train_frac {train_fraction}, shuffle {shuffle_index}") deeplabcut.evaluate_network( config=str(config_path), Shuffles=[shuffle_index], @@ -391,12 +381,8 @@ def run( if len(videos) > 0: log_step(f"Analyzing videos for {train_fraction}, shuffle {shuffle_index}") - video_kwargs = dict( - videos=videos, shuffle=shuffle_index, trainingsetindex=trainset_index - ) - deeplabcut.analyze_videos( - str(config_path), **video_kwargs, device=device, auto_track=False - ) + video_kwargs = dict(videos=videos, shuffle=shuffle_index, trainingsetindex=trainset_index) + deeplabcut.analyze_videos(str(config_path), **video_kwargs, device=device, auto_track=False) times.append(time.time()) log_step(f"Video analysis time: {times[-1] - times[-2]} seconds") log_step(f"Total test time: {times[-1] - times[0]} seconds") @@ -404,9 +390,7 @@ def run( cfg = af.read_config(config_path) if cfg.get("multianimalproject"): if create_labeled_videos: - deeplabcut.create_video_with_all_detections( - str(config_path), **video_kwargs - ) + deeplabcut.create_video_with_all_detections(str(config_path), **video_kwargs) # relaxed tracking parameters deeplabcut.convert_detections2tracklets( diff --git a/setup.py b/setup.py index 444f15f7e1..7f1e5e5229 100644 --- a/setup.py +++ b/setup.py @@ -6,6 +6,7 @@ https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS Licensed under GNU Lesser General Public License v3.0 """ + from setuptools import setup # All configuration is now in pyproject.toml. This file is kept for backward compatibility diff --git a/tests/core/inferenceutils/test_map_computation.py b/tests/core/inferenceutils/test_map_computation.py index 98a4358a42..5d67eee982 100644 --- a/tests/core/inferenceutils/test_map_computation.py +++ b/tests/core/inferenceutils/test_map_computation.py @@ -181,9 +181,7 @@ def test_random_map_computation(num_images, num_joints, max_error): pred_kpts = -np.ones((max_idv, num_joints, 3)) gt_kpts[:gt_idv] = 2 * np.ones((gt_idv, num_joints, 3)) - gt_kpts[:gt_idv, :, :2] = rng.integers( - low=0, high=1024, size=(gt_idv, num_joints, 2) - ) + gt_kpts[:gt_idv, :, :2] = rng.integers(low=0, high=1024, size=(gt_idv, num_joints, 2)) gt[f"img_{i}"] = gt_kpts # set scores @@ -192,18 +190,14 @@ def test_random_map_computation(num_images, num_joints, max_error): # predictions that are ground truth + error matched = min(gt_idv, pred_idv) if matched > 0: - error = rng.integers( - low=-max_error, high=max_error, size=(matched, num_joints, 2) - ) + error = rng.integers(low=-max_error, high=max_error, size=(matched, num_joints, 2)) matched_pred = gt_kpts[:matched, :, :2] + error pred_kpts[:matched, :, :2] = np.clip(matched_pred, 0, 1024) # random predictions unmatched = pred_idv - matched if unmatched > 0: - pred_kpts[matched:pred_idv, :, :2] = rng.integers( - low=0, high=1024, size=(unmatched, num_joints, 2) - ) + pred_kpts[matched:pred_idv, :, :2] = rng.integers(low=0, high=1024, size=(unmatched, num_joints, 2)) pred[f"img_{i}"] = pred_kpts @@ -227,9 +221,7 @@ def test_random_map_computation_with_missing_kpts(num_images, num_joints, max_er pred_kpts = -np.ones((max_idv, num_joints, 3)) gt_kpts[:gt_idv] = 2 * np.ones((gt_idv, num_joints, 3)) - gt_kpts[:gt_idv, :, :2] = rng.integers( - low=0, high=1024, size=(gt_idv, num_joints, 2) - ) + gt_kpts[:gt_idv, :, :2] = rng.integers(low=0, high=1024, size=(gt_idv, num_joints, 2)) gt[f"img_{i}"] = gt_kpts # drop some ground truth keypoints @@ -242,18 +234,14 @@ def test_random_map_computation_with_missing_kpts(num_images, num_joints, max_er # predictions that are ground truth + error matched = min(gt_idv, pred_idv) if matched > 0: - error = rng.integers( - low=-max_error, high=max_error, size=(matched, num_joints, 2) - ) + error = rng.integers(low=-max_error, high=max_error, size=(matched, num_joints, 2)) matched_pred = gt_kpts[:matched, :, :2] + error pred_kpts[:matched, :, :2] = np.clip(matched_pred, 0, 1024) # random predictions unmatched = pred_idv - matched if unmatched > 0: - pred_kpts[matched:pred_idv, :, :2] = rng.integers( - low=0, high=1024, size=(unmatched, num_joints, 2) - ) + pred_kpts[matched:pred_idv, :, :2] = rng.integers(low=0, high=1024, size=(unmatched, num_joints, 2)) pred[f"img_{i}"] = pred_kpts @@ -297,7 +285,8 @@ def _evaluate(gt: dict[str, np.ndarray], pred: dict[str, np.ndarray]): def _to_assemblies( - data: dict[str, np.ndarray], ground_truth: bool, + data: dict[str, np.ndarray], + ground_truth: bool, ) -> dict[str, list[inferenceutils.Assembly]]: images = list(data.keys()) raw_data = np.stack([data[i] for i in images], axis=0) @@ -310,10 +299,7 @@ def _to_assemblies( if ground_truth: raw_data[~mask, 2] = 1 - return { - images[i]: assembly - for i, assembly in inferenceutils._parse_ground_truth_data(raw_data).items() - } + return {images[i]: assembly for i, assembly in inferenceutils._parse_ground_truth_data(raw_data).items()} def _to_coco_ground_truth( @@ -374,9 +360,7 @@ def _to_coco_predictions( assert image_keypoints.shape[1] == num_joints img_id = path_to_id[path] - valid_predictions = [ - kpt for kpt in image_keypoints if np.any(np.all(~np.isnan(kpt), axis=-1)) - ] + valid_predictions = [kpt for kpt in image_keypoints if np.any(np.all(~np.isnan(kpt), axis=-1))] for kpts in valid_predictions: score = float(np.nanmean(kpts[:, 2]).item()) kpts = kpts.copy() diff --git a/tests/core/metrics/test_metrics_api.py b/tests/core/metrics/test_metrics_api.py index 5051794a13..3c14af14f1 100644 --- a/tests/core/metrics/test_metrics_api.py +++ b/tests/core/metrics/test_metrics_api.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """General tests for the metrics API""" + import numpy as np import pytest from numpy.testing import assert_almost_equal @@ -16,9 +17,7 @@ import deeplabcut.core.metrics as metrics -def _get_gt_and_pred_with_constant_err( - num_idv: int, num_bpt: int, error: float -) -> tuple[np.ndarray, np.ndarray]: +def _get_gt_and_pred_with_constant_err(num_idv: int, num_bpt: int, error: float) -> tuple[np.ndarray, np.ndarray]: gt = np.arange(num_idv * num_bpt * 3).astype(float).reshape((num_idv, num_bpt, 3)) gt[..., 2] = 2 predictions = gt.copy() @@ -109,4 +108,3 @@ def test_computing_metrics_single_animal(error): ) assert_almost_equal(results["rmse"], np.sqrt(2) * error) assert_almost_equal(results["rmse_pcutoff"], np.sqrt(2) * error) - diff --git a/tests/core/metrics/test_metrics_identity_accuracy.py b/tests/core/metrics/test_metrics_identity_accuracy.py index 29930f5afd..32ed30eb03 100644 --- a/tests/core/metrics/test_metrics_identity_accuracy.py +++ b/tests/core/metrics/test_metrics_identity_accuracy.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Tests for the scoring methods""" + import numpy as np import pytest diff --git a/tests/core/metrics/test_metrics_map_computation.py b/tests/core/metrics/test_metrics_map_computation.py index dc527280be..be36f37416 100644 --- a/tests/core/metrics/test_metrics_map_computation.py +++ b/tests/core/metrics/test_metrics_map_computation.py @@ -198,18 +198,14 @@ def test_random_map_computation(num_images, num_joints, max_error): # predictions that are ground truth + error matched = min(gt_idv, pred_idv) if matched > 0: - error = rng.integers( - low=-max_error, high=max_error, size=(matched, num_joints, 2) - ) + error = rng.integers(low=-max_error, high=max_error, size=(matched, num_joints, 2)) matched_pred = gt_kpts[:matched, :, :2] + error pred_kpts[:matched, :, :2] = np.clip(matched_pred, 0, 1024) # random predictions unmatched = pred_idv - matched if unmatched > 0: - pred_kpts[matched:, :, :2] = rng.integers( - low=0, high=1024, size=(unmatched, num_joints, 2) - ) + pred_kpts[matched:, :, :2] = rng.integers(low=0, high=1024, size=(unmatched, num_joints, 2)) pred[f"img_{i}"] = pred_kpts @@ -240,18 +236,14 @@ def test_random_map_computation_with_missing_kpts(num_images, num_joints, max_er # predictions that are ground truth + error matched = min(gt_idv, pred_idv) if matched > 0: - error = rng.integers( - low=-max_error, high=max_error, size=(matched, num_joints, 2) - ) + error = rng.integers(low=-max_error, high=max_error, size=(matched, num_joints, 2)) matched_pred = gt_kpts[:matched, :, :2] + error pred_kpts[:matched, :, :2] = np.clip(matched_pred, 0, 1024) # random predictions unmatched = pred_idv - matched if unmatched > 0: - pred_kpts[matched:, :, :2] = rng.integers( - low=0, high=1024, size=(unmatched, num_joints, 2) - ) + pred_kpts[matched:, :, :2] = rng.integers(low=0, high=1024, size=(unmatched, num_joints, 2)) pred[f"img_{i}"] = pred_kpts @@ -349,9 +341,7 @@ def _to_coco_predictions( assert image_keypoints.shape[1] == num_joints img_id = path_to_id[path] - valid_predictions = [ - kpt for kpt in image_keypoints if np.any(np.all(~np.isnan(kpt), axis=-1)) - ] + valid_predictions = [kpt for kpt in image_keypoints if np.any(np.all(~np.isnan(kpt), axis=-1))] for kpts in valid_predictions: score = float(np.nanmean(kpts[:, 2]).item()) kpts = kpts.copy() diff --git a/tests/core/metrics/test_metrics_rmse_computation.py b/tests/core/metrics/test_metrics_rmse_computation.py index 97379817bf..4b1bef589b 100644 --- a/tests/core/metrics/test_metrics_rmse_computation.py +++ b/tests/core/metrics/test_metrics_rmse_computation.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Tests RMSE computation""" + import numpy as np import pytest from numpy.testing import assert_almost_equal @@ -135,7 +136,7 @@ def test_rmse_with_nans(gt: list, pred: list, result: tuple[float, float]): [ # predicted pose [[12.0, 10.0, 0.9], [10.0, 10.0, 0.4], [10.0, 10.0, 0.9]], ], - None, # unique data + None, # unique data (1, 1), # error 2 on one, 0 on the other; only 2 valid GT ), ( @@ -147,7 +148,7 @@ def test_rmse_with_nans(gt: list, pred: list, result: tuple[float, float]): [[10.0, 10.0, 0.9], [50.0, 50.0, 0.9], [30.0, 30.0, 0.9]], [[40.0, 40.0, 0.9], [20.0, 20.0, 0.4], [60.0, 60.0, 0.9]], ], - None, # unique data + None, # unique data (0, 0), # all pose perfect ), ( @@ -159,7 +160,7 @@ def test_rmse_with_nans(gt: list, pred: list, result: tuple[float, float]): [[12.0, 10.0, 0.9], [52.0, 50.0, 0.9], [32.0, 30.0, 0.9]], [[42.0, 40.0, 0.9], [18.0, 20.0, 0.4], [62.0, 60.0, 0.9]], ], - None, # unique data + None, # unique data (2, 2), # pixel error of 2 on x-axis for all predictions ), ( @@ -171,7 +172,7 @@ def test_rmse_with_nans(gt: list, pred: list, result: tuple[float, float]): [[12.0, 10.0, 0.4], [50.0, 50.0, 0.9], [30.0, 30.0, 0.9]], [[40.0, 40.0, 0.9], [22.0, 20.0, 0.4], [62.0, 60.0, 0.4]], ], - None, # unique data + None, # unique data (1, 0), # error of 2 on half, 0 on the other half (with good conf) ), ( # more ground truth than detections @@ -184,7 +185,7 @@ def test_rmse_with_nans(gt: list, pred: list, result: tuple[float, float]): [[70.0, 70.0, 2], [80.0, 80.0, 2], [90.0, 90.0, 2]], [[40.0, 40.0, 2], [50.0, 50.0, 2], [60.0, 60.0, 2]], ], - None, # unique data + None, # unique data (0, 0), ), ( # more detections than GT @@ -197,40 +198,40 @@ def test_rmse_with_nans(gt: list, pred: list, result: tuple[float, float]): [[40.0, 40.0, 2], [50.0, 50.0, 2], [60.0, 60.0, 2]], [[70.0, 70.0, 2], [80.0, 80.0, 2], [90.0, 90.0, 2]], ], - None, # unique data + None, # unique data (0, 0), ), ( - [ # ground truth pose - [[10.0, 10.0, 2], [np.nan, np.nan, 0], [10.0, 10.0, 2]], - ], - [ # predicted pose - [[12.0, 10.0, 0.9], [10.0, 10.0, 0.4], [10.0, 10.0, 0.9]], - ], - ( # unique data - [[[20, 20, 2], [22, 23, 2]]], - [[[20, 20, 0.8], [22, 23, 0.7]]] - ), - (0.5, 0.5), # error 2 on one, 0 on the other; only 2 valid GT + [ # ground truth pose + [[10.0, 10.0, 2], [np.nan, np.nan, 0], [10.0, 10.0, 2]], + ], + [ # predicted pose + [[12.0, 10.0, 0.9], [10.0, 10.0, 0.4], [10.0, 10.0, 0.9]], + ], + ( # unique data + [[[20, 20, 2], [22, 23, 2]]], + [[[20, 20, 0.8], [22, 23, 0.7]]], + ), + (0.5, 0.5), # error 2 on one, 0 on the other; only 2 valid GT ), ( - [ # ground truth pose - [[10.0, 10.0, 2], [20.0, 20.0, 2], [30.0, 30.0, 2]], - [[40.0, 40.0, 2], [50.0, 50.0, 2], [60.0, 60.0, 2]], - ], - [ # predicted pose, perfect detections but misassembled - [[10.0, 10.0, 0.9], [50.0, 50.0, 0.9], [30.0, 30.0, 0.9]], - [[40.0, 40.0, 0.9], [20.0, 20.0, 0.4], [60.0, 60.0, 0.9]], - ], - ( # unique data - [], # missing ground truth for unique bodyparts - [[[20, 20, 0.8], [22, 23, 0.7]]] - ), - (0, 0), # all pose perfect + [ # ground truth pose + [[10.0, 10.0, 2], [20.0, 20.0, 2], [30.0, 30.0, 2]], + [[40.0, 40.0, 2], [50.0, 50.0, 2], [60.0, 60.0, 2]], + ], + [ # predicted pose, perfect detections but misassembled + [[10.0, 10.0, 0.9], [50.0, 50.0, 0.9], [30.0, 30.0, 0.9]], + [[40.0, 40.0, 0.9], [20.0, 20.0, 0.4], [60.0, 60.0, 0.9]], + ], + ( # unique data + [], # missing ground truth for unique bodyparts + [[[20, 20, 0.8], [22, 23, 0.7]]], + ), + (0, 0), # all pose perfect ), ], ) -def test_detection_rmse(gt: list, pred: list, data_unique:tuple[list, list]|None, result: tuple[float, float]): +def test_detection_rmse(gt: list, pred: list, data_unique: tuple[list, list] | None, result: tuple[float, float]): data = [(np.asarray(gt), np.asarray(pred))] data_unique = [(np.asarray(data_unique[0]), np.asarray(data_unique[1]))] if data_unique else None expected_rmse, expected_rmse_cutoff = result @@ -277,18 +278,18 @@ def test_detection_rmse(gt: list, pred: list, data_unique:tuple[list, list]|None ], ) def test_rmse_with_unique( - gt: list, - pred: list, - unique_gt: list, - unique_pred: list, - result: tuple[float, float] + gt: list, pred: list, unique_gt: list, unique_pred: list, result: tuple[float, float] ) -> None: data = [(np.asarray(gt), np.asarray(pred))] data_unique = [(np.asarray(unique_gt), np.asarray(unique_pred))] expected_rmse, expected_rmse_cutoff = result results = compute_rmse( - data, False, pcutoff=0.6, data_unique=data_unique, oks_bbox_margin=10.0, + data, + False, + pcutoff=0.6, + data_unique=data_unique, + oks_bbox_margin=10.0, ) rmse, rmse_cutoff = results["rmse"], results["rmse_pcutoff"] assert_almost_equal(rmse, expected_rmse) @@ -314,11 +315,7 @@ def test_rmse_with_unique( [[10.0, 10.0, 0.9], [10.0, 10.0, 0.9]], ], # 4 pixel error on 2 keypoints, 0 error on 5 keypoints - [ - (1.0, 0.0), - [2.0, 2.0, 0.0], - [0.0, 0.0] - ], + [(1.0, 0.0), [2.0, 2.0, 0.0], [0.0, 0.0]], ), ( [ # ground truth pose @@ -338,7 +335,7 @@ def test_rmse_with_unique( [ # errors: 3 with 0px, 1 with 1px, 2 with 2px, 2 with 4px => 13/8 (1.625, 1.625), [3.0, 2.0, 0.0], - [2.0, 1.0] + [2.0, 1.0], ], ), ], @@ -348,7 +345,7 @@ def test_rmse_per_bodypart_with_unique( pred: list, unique_gt: list, unique_pred: list, - result: tuple[tuple[float, float], list[float], list[float]] + result: tuple[tuple[float, float], list[float], list[float]], ) -> None: data = [(np.asarray(gt), np.asarray(pred))] data_unique = [(np.asarray(unique_gt), np.asarray(unique_pred))] diff --git a/tests/create_project/test_video_set_configuration.py b/tests/create_project/test_video_set_configuration.py index 7407fe32f4..0b03f6840d 100644 --- a/tests/create_project/test_video_set_configuration.py +++ b/tests/create_project/test_video_set_configuration.py @@ -18,11 +18,13 @@ import deeplabcut.create_project.new as new_module from deeplabcut.utils.auxfun_videos import VideoReader + @pytest.fixture def project_directory(tmpdir_factory) -> Path: proj_dir = Path(tmpdir_factory.mktemp("test-project")) return proj_dir + @pytest.fixture def mock_video_file(tmpdir_factory) -> Path: """Create a mock video file for testing""" @@ -45,19 +47,19 @@ def video_directory(tmpdir_factory) -> Path: """Create a directory with multiple video files""" video_dir = Path(tmpdir_factory.mktemp("some_videos")) video_dir.mkdir(exist_ok=True) - + # Create multiple video files with different extensions (video_dir / "video1.avi").write_bytes(b"fake video 1") (video_dir / "video2.mp4").write_bytes(b"fake video 2") (video_dir / "video3.mov").write_bytes(b"fake video 3") (video_dir / "not_a_video.txt").write_text("text file") - + return video_dir def test_project_directory_creation_basic( - tmpdir: Path, - mock_video_file: Path, + tmpdir: Path, + mock_video_file: Path, mock_video_reader: VideoReader, ): """Test that project directories are created correctly""" @@ -69,7 +71,7 @@ def test_project_directory_creation_basic( working_directory=str(tmpdir), copy_videos=False, ) - + project_path = Path(config_path).parent assert project_path.exists() assert (project_path / "videos").exists() @@ -78,7 +80,7 @@ def test_project_directory_creation_basic( assert (project_path / "dlc-models").exists() -@pytest.mark.parametrize('copy_videos', [True, False]) +@pytest.mark.parametrize("copy_videos", [True, False]) def test_single_video_file( tmpdir: Path, mock_video_file: Path, @@ -94,9 +96,9 @@ def test_single_video_file( working_directory=str(tmpdir), copy_videos=copy_videos, ) - + project_path = Path(config_path).parent - video_path = project_path / "videos" / 'test_video.avi' + video_path = project_path / "videos" / "test_video.avi" assert video_path.exists() or video_path.is_symlink() # Content should match @@ -104,7 +106,7 @@ def test_single_video_file( assert mock_video_file.read_bytes() == video_path.read_bytes() -@pytest.mark.parametrize('copy_videos', [True, False]) +@pytest.mark.parametrize("copy_videos", [True, False]) def test_video_directory( tmpdir: Path, video_directory: Path, @@ -121,16 +123,16 @@ def test_video_directory( videotype=".avi", copy_videos=copy_videos, ) - + project_path = Path(config_path).parent assert (project_path / "videos" / "video1.avi").exists() or (project_path / "videos" / "video1.avi").is_symlink() - + # Content should match if copy_videos: assert (project_path / "videos" / "video1.avi").read_bytes() == (video_directory / "video1.avi").read_bytes() -@pytest.mark.parametrize('copy_videos', [True, False]) +@pytest.mark.parametrize("copy_videos", [True, False]) def test_mixed_video_files_and_directories( tmpdir, mock_video_file: Path, @@ -148,7 +150,7 @@ def test_mixed_video_files_and_directories( videotype=".avi", copy_videos=copy_videos, ) - + project_path = Path(config_path).parent videos_dir = project_path / "videos" # Should have both the single file and files from directory @@ -163,7 +165,7 @@ def test_empty_video_directory( """Test handling of empty video directory""" empty_dir = tmpdir / "empty_videos" empty_dir.mkdir() - + with patch("deeplabcut.create_project.new.VideoReader", return_value=mock_video_reader): with warnings.catch_warnings(record=True) as w: result = new_module.create_new_project( @@ -192,10 +194,11 @@ def test_valid_video_included_in_config( working_directory=str(tmpdir), copy_videos=False, ) - + from deeplabcut.utils import auxiliaryfunctions + cfg = auxiliaryfunctions.read_config(config_path) - + assert "video_sets" in cfg assert len(cfg["video_sets"]) > 0 # Check that video path is in video_sets @@ -210,7 +213,7 @@ def test_invalid_video_removed_from_project( """Test that invalid videos are removed from the project""" # Mock VideoReader to raise IOError mock_reader = Mock(side_effect=IOError("Cannot open video")) - + with patch("deeplabcut.create_project.new.VideoReader", mock_reader): with warnings.catch_warnings(record=True): result = new_module.create_new_project( @@ -220,11 +223,11 @@ def test_invalid_video_removed_from_project( working_directory=str(tmpdir), copy_videos=False, ) - + # Should return "nothingcreated" when no valid videos assert result == "nothingcreated" - + def test_config_file_video_sets_format( tmpdir: Path, mock_video_file: Path, @@ -239,13 +242,14 @@ def test_config_file_video_sets_format( working_directory=str(tmpdir), copy_videos=False, ) - + from deeplabcut.utils import auxiliaryfunctions + cfg = auxiliaryfunctions.read_config(config_path) - + assert "video_sets" in cfg assert isinstance(cfg["video_sets"], dict) - + # Check format of video_sets entries for video_path, video_info in cfg["video_sets"].items(): assert isinstance(video_info, dict) diff --git a/tests/generate_training_dataset/test_trainingset_manipulation.py b/tests/generate_training_dataset/test_trainingset_manipulation.py index 867d0ea0e4..4a907a927e 100644 --- a/tests/generate_training_dataset/test_trainingset_manipulation.py +++ b/tests/generate_training_dataset/test_trainingset_manipulation.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Tests for deeplabcut/generate_training_dataset/metadata.py""" + from __future__ import annotations import pytest @@ -16,9 +17,7 @@ import deeplabcut.generate_training_dataset.trainingsetmanipulation as trainingsetmanipulation -@pytest.mark.parametrize( - "train_fraction", [1, 2, 5, 17, 24, 29, 34, 47, 50, 53, 61, 68, 75, 90, 95, 97, 99] -) +@pytest.mark.parametrize("train_fraction", [1, 2, 5, 17, 24, 29, 34, 47, 50, 53, 61, 68, 75, 90, 95, 97, 99]) @pytest.mark.parametrize("n_train", [1, 2, 3, 5, 7, 11, 37, 62, 153]) @pytest.mark.parametrize("n_test", [1, 2, 3, 5, 7, 13, 19, 85, 112]) def test_compute_padding(train_fraction: int, n_train: int, n_test: int) -> None: @@ -31,10 +30,8 @@ def test_compute_padding(train_fraction: int, n_train: int, n_test: int) -> None This was done locally, but as it's many many tests to run a subset was selected here """ train_frac = train_fraction / 100 - train_pad, test_pad = trainingsetmanipulation._compute_padding( - train_frac, n_train, n_test - ) + train_pad, test_pad = trainingsetmanipulation._compute_padding(train_frac, n_train, n_test) print() print(train_fraction, n_train, n_test, train_pad, test_pad) - frac = round((n_train + train_pad)/(n_train + n_test + train_pad + test_pad), 2) + frac = round((n_train + train_pad) / (n_train + n_test + train_pad + test_pad), 2) assert train_frac == frac diff --git a/tests/generate_training_dataset/test_trainset_metadata.py b/tests/generate_training_dataset/test_trainset_metadata.py index e6c150cdf4..4f75d80d7e 100644 --- a/tests/generate_training_dataset/test_trainset_metadata.py +++ b/tests/generate_training_dataset/test_trainset_metadata.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Tests for deeplabcut/generate_training_dataset/metadata.py""" + from __future__ import annotations import pickle @@ -79,13 +80,9 @@ def test_load_metadata(tmpdir, data: dict, load_splits: bool): for name, s in data["shuffles"].items(): split = data["splits"][s["split"]] train, test = split["train"], split["test"] - _create_doc_data( - cfg, trainset_dir, s["train_fraction"], s["index"], train, test - ) + _create_doc_data(cfg, trainset_dir, s["train_fraction"], s["index"], train, test) - trainset_meta = metadata.TrainingDatasetMetadata.load( - str(cfg_path), load_splits=load_splits - ) + trainset_meta = metadata.TrainingDatasetMetadata.load(str(cfg_path), load_splits=load_splits) for s in trainset_meta.shuffles: print(s) @@ -107,72 +104,61 @@ def test_load_metadata(tmpdir, data: dict, load_splits: bool): assert s_with_split.split.test_indices == tuple(split_idx["test"]) -@pytest.mark.parametrize("data", [ - { - "task": "ch", - "date": "Aug1", - "shuffles": (SHUFFLES[1], ), - "expected": { - "shuffles": { - SHUFFLES[1].name: { - "index": 1, "train_fraction": 0.5, "split": 1, "engine": "pytorch" - } +@pytest.mark.parametrize( + "data", + [ + { + "task": "ch", + "date": "Aug1", + "shuffles": (SHUFFLES[1],), + "expected": { + "shuffles": {SHUFFLES[1].name: {"index": 1, "train_fraction": 0.5, "split": 1, "engine": "pytorch"}}, }, - } - }, - { - "task": "t", - "date": "Jan1", - "shuffles": (SHUFFLES[1], SHUFFLES[3]), - "expected": { - "shuffles": { - SHUFFLES[1].name: { - "index": 1, "train_fraction": 0.5, "split": 1, "engine": "pytorch" - }, - SHUFFLES[3].name: { - "index": 3, - "train_fraction": 0.5, - "split": 1, - "engine": "tensorflow", + }, + { + "task": "t", + "date": "Jan1", + "shuffles": (SHUFFLES[1], SHUFFLES[3]), + "expected": { + "shuffles": { + SHUFFLES[1].name: {"index": 1, "train_fraction": 0.5, "split": 1, "engine": "pytorch"}, + SHUFFLES[3].name: { + "index": 3, + "train_fraction": 0.5, + "split": 1, + "engine": "tensorflow", + }, }, }, - } - }, - { - "task": "t", - "date": "Jan1", - "shuffles": (SHUFFLES[1], SHUFFLES[2]), - "expected": { - "shuffles": { - SHUFFLES[1].name: { - "index": 1, "train_fraction": 0.5, "split": 1, "engine": "pytorch" - }, - SHUFFLES[2].name: { - "index": 2, "train_fraction": 0.5, "split": 2, "engine": "pytorch" + }, + { + "task": "t", + "date": "Jan1", + "shuffles": (SHUFFLES[1], SHUFFLES[2]), + "expected": { + "shuffles": { + SHUFFLES[1].name: {"index": 1, "train_fraction": 0.5, "split": 1, "engine": "pytorch"}, + SHUFFLES[2].name: {"index": 2, "train_fraction": 0.5, "split": 2, "engine": "pytorch"}, }, }, }, - }, - { - "shuffles": (SHUFFLES[1], SHUFFLES[2], SHUFFLES[3]), - "expected": { - "shuffles": { - SHUFFLES[1].name: { - "index": 1, "train_fraction": 0.5, "split": 1, "engine": "pytorch" - }, - SHUFFLES[2].name: { - "index": 2, "train_fraction": 0.5, "split": 2, "engine": "pytorch" - }, - SHUFFLES[3].name: { - "index": 3, - "train_fraction": 0.5, - "split": 1, - "engine": "tensorflow", + { + "shuffles": (SHUFFLES[1], SHUFFLES[2], SHUFFLES[3]), + "expected": { + "shuffles": { + SHUFFLES[1].name: {"index": 1, "train_fraction": 0.5, "split": 1, "engine": "pytorch"}, + SHUFFLES[2].name: {"index": 2, "train_fraction": 0.5, "split": 2, "engine": "pytorch"}, + SHUFFLES[3].name: { + "index": 3, + "train_fraction": 0.5, + "split": 1, + "engine": "tensorflow", + }, }, }, }, - }, -]) + ], +) def test_save_metadata_simple(tmpdir, data): """Tests that saving the metadata creates the expected file""" cfg, cfg_path, trainset_dir, meta_path = _create_project_with_config(tmpdir) @@ -187,15 +173,18 @@ def test_save_metadata_simple(tmpdir, data): assert data["expected"] == meta -@pytest.mark.parametrize("shuffles", [ - [SHUFFLES[i] for i in indices] - for indices in [[1], [1, 2], [1, 2, 3], [1, 2, 4], [1, 3, 4], [1, 2, 3, 4]] -]) +@pytest.mark.parametrize( + "shuffles", + [[SHUFFLES[i] for i in indices] for indices in [[1], [1, 2], [1, 2, 3], [1, 2, 4], [1, 3, 4], [1, 2, 3, 4]]], +) def test_save_metadata(tmpdir, shuffles): """Tests that saving the metadata and reloading it leads to the same instance""" cfg, cfg_path, trainset_dir, meta_path = _create_project_with_config(tmpdir) for s in shuffles: - train, test = s.split.train_indices, s.split.test_indices, + train, test = ( + s.split.train_indices, + s.split.test_indices, + ) _create_doc_data(cfg, trainset_dir, s.train_fraction, s.index, train, test) trainset_meta = metadata.TrainingDatasetMetadata(cfg, tuple(shuffles)) @@ -220,7 +209,7 @@ def test_save_metadata(tmpdir, shuffles): def test_add_shuffle(tmpdir): """Tests that a shuffle can be added correctlt""" cfg, cfg_path, trainset_dir, meta_path = _create_project_with_config(tmpdir) - trainset_meta = metadata.TrainingDatasetMetadata(cfg, (SHUFFLES[1], )) + trainset_meta = metadata.TrainingDatasetMetadata(cfg, (SHUFFLES[1],)) trainset_meta_added = trainset_meta.add(SHUFFLES[2]) assert len(trainset_meta.shuffles) == 1 assert len(trainset_meta_added.shuffles) == 2 @@ -230,11 +219,11 @@ def test_add_shuffle(tmpdir): def test_add_shuffle_twice(tmpdir): """Tests that a shuffle can be added correctlt""" cfg, cfg_path, trainset_dir, meta_path = _create_project_with_config(tmpdir) - trainset_meta = metadata.TrainingDatasetMetadata(cfg, (SHUFFLES[1], )) + trainset_meta = metadata.TrainingDatasetMetadata(cfg, (SHUFFLES[1],)) trainset_meta_added = trainset_meta.add(SHUFFLES[2]) trainset_meta_added_2 = trainset_meta.add(SHUFFLES[2]) assert len(trainset_meta.shuffles) == 1 - assert trainset_meta.shuffles == (SHUFFLES[1], ) + assert trainset_meta.shuffles == (SHUFFLES[1],) assert len(trainset_meta_added.shuffles) == len(trainset_meta_added_2.shuffles) assert trainset_meta_added.shuffles == trainset_meta_added_2.shuffles @@ -249,31 +238,23 @@ def test_add_shuffle_sorts_to_correct_order(tmpdir): assert trainset_meta_added.shuffles == (SHUFFLES[1], SHUFFLES[2], SHUFFLES[3]) -@pytest.mark.parametrize("shuffles", [ - indices for indices in [[1], [1, 2], [1, 2, 3], [1, 2, 4], [1, 3, 4], [1, 2, 3, 4]] -]) +@pytest.mark.parametrize( + "shuffles", [indices for indices in [[1], [1, 2], [1, 2, 3], [1, 2, 4], [1, 3, 4], [1, 2, 3, 4]]] +) @pytest.mark.parametrize("shuffle_to_add", [1, 2, 3, 4]) def test_add_shuffle(tmpdir, shuffles, shuffle_to_add): - """Tests """ + """Tests""" cfg, cfg_path, trainset_dir, meta_path = _create_project_with_config(tmpdir) - trainset_meta = metadata.TrainingDatasetMetadata( - cfg, tuple([SHUFFLES[i] for i in shuffles]) - ) + trainset_meta = metadata.TrainingDatasetMetadata(cfg, tuple([SHUFFLES[i] for i in shuffles])) if shuffle_to_add in shuffles: with pytest.raises(RuntimeError): - trainset_meta_added = trainset_meta.add( - SHUFFLES[shuffle_to_add], overwrite=False - ) + trainset_meta_added = trainset_meta.add(SHUFFLES[shuffle_to_add], overwrite=False) - trainset_meta_added = trainset_meta.add( - SHUFFLES[shuffle_to_add], overwrite=True - ) + trainset_meta_added = trainset_meta.add(SHUFFLES[shuffle_to_add], overwrite=True) assert len(trainset_meta_added.shuffles) == len(shuffles) assert [s.index for s in trainset_meta_added.shuffles] == shuffles else: - trainset_meta_added = trainset_meta.add( - SHUFFLES[shuffle_to_add], overwrite=False - ) + trainset_meta_added = trainset_meta.add(SHUFFLES[shuffle_to_add], overwrite=False) indices = [s.index for s in trainset_meta_added.shuffles] assert len(trainset_meta_added.shuffles) == len(shuffles) + 1 assert indices == list(sorted(shuffles + [shuffle_to_add])) @@ -301,40 +282,31 @@ def test_data_split_equality(split1, split2, equal): @pytest.mark.parametrize("split_idx", [1, 4, 20, 1000]) @pytest.mark.parametrize("indices", [(2, 1), (10, 1), (1, 21, 20), (1, 2, 4, 3)]) @pytest.mark.parametrize("sorted_indices", [(1, 2), (10, 12), (3, 4), (1, 1000, 1200)]) -def test_data_split_requires_sorted( - split_idx: int, indices: tuple[int], sorted_indices: tuple[int] -): +def test_data_split_requires_sorted(split_idx: int, indices: tuple[int], sorted_indices: tuple[int]): """Tests that equality functions as expected for DataSplits""" with pytest.raises(RuntimeError): - metadata.DataSplit( - train_indices=tuple(indices), test_indices=tuple(sorted_indices) - ) + metadata.DataSplit(train_indices=tuple(indices), test_indices=tuple(sorted_indices)) with pytest.raises(RuntimeError): - metadata.DataSplit( - train_indices=tuple(sorted_indices), test_indices=tuple(indices) - ) + metadata.DataSplit(train_indices=tuple(sorted_indices), test_indices=tuple(indices)) with pytest.raises(RuntimeError): - metadata.DataSplit( - train_indices=tuple(indices), test_indices=tuple(indices) - ) + metadata.DataSplit(train_indices=tuple(indices), test_indices=tuple(indices)) + + metadata.DataSplit(train_indices=tuple(sorted_indices), test_indices=tuple(sorted_indices)) + - metadata.DataSplit( - train_indices=tuple(sorted_indices), test_indices=tuple(sorted_indices) - ) - - -@pytest.mark.parametrize("shuffles", [ - ( - {"idx": 3, "train": [1], "test": [2], "train_fraction": 0.5}, - ), - ( - {"idx": 1, "train": [1], "test": [2], "train_fraction": 0.5}, - {"idx": 5, "train": [1, 2, 3], "test": [4, 5], "train_fraction": 0.6}, - {"idx": 4, "train": [1, 3], "test": [2], "train_fraction": 0.66}, - ), -]) +@pytest.mark.parametrize( + "shuffles", + [ + ({"idx": 3, "train": [1], "test": [2], "train_fraction": 0.5},), + ( + {"idx": 1, "train": [1], "test": [2], "train_fraction": 0.5}, + {"idx": 5, "train": [1, 2, 3], "test": [4, 5], "train_fraction": 0.6}, + {"idx": 4, "train": [1, 3], "test": [2], "train_fraction": 0.66}, + ), + ], +) def test_create_metadata_from_shuffles(tmpdir, shuffles): """Tests that equality functions as expected for DataSplits""" cfg, cfg_path, trainset_dir, meta_path = _create_project_with_config(tmpdir) @@ -343,10 +315,7 @@ def test_create_metadata_from_shuffles(tmpdir, shuffles): doc = f"Documentation_data-ex_{s['train_fraction']}shuffle{s['idx']}.pickle" doc_path = trainset_dir.join(doc) with open(doc_path, "wb") as f: - pickle.dump( - [[], s["train"], s["test"], s['train_fraction']], f, - pickle.HIGHEST_PROTOCOL - ) + pickle.dump([[], s["train"], s["test"], s["train_fraction"]], f, pickle.HIGHEST_PROTOCOL) trainset_metadata = metadata.TrainingDatasetMetadata.create(cfg) print() @@ -401,9 +370,5 @@ def _create_doc_data( train_indices, test_indices, ) -> None: - _, doc_path = auxiliaryfunctions.get_data_and_metadata_filenames( - trainset_dir, train_frac, shuffle, cfg - ) - auxiliaryfunctions.save_metadata( - doc_path, {}, list(train_indices), list(test_indices), train_frac - ) + _, doc_path = auxiliaryfunctions.get_data_and_metadata_filenames(trainset_dir, train_frac, shuffle, cfg) + auxiliaryfunctions.save_metadata(doc_path, {}, list(train_indices), list(test_indices), train_frac) diff --git a/tests/pose_estimation_pytorch/apis/test_apis_evaluate.py b/tests/pose_estimation_pytorch/apis/test_apis_evaluate.py index b2de74ba83..99997fef63 100644 --- a/tests/pose_estimation_pytorch/apis/test_apis_evaluate.py +++ b/tests/pose_estimation_pytorch/apis/test_apis_evaluate.py @@ -28,7 +28,7 @@ [ (["nose", "left_ear"], [5, 10]), (["nose", "left_ear", "right_ear"], [2, 3, 4]), - ] + ], ) def test_evaluate_basic( num_individuals: int, @@ -54,14 +54,14 @@ def test_evaluate_basic( [ (["nose", "left_ear"], [5, 10]), (["nose", "left_ear", "right_ear"], [2, 3, 4]), - ] + ], ) @pytest.mark.parametrize( "unique_bodyparts, unique_error", [ (["top_left"], [2]), (["top_left", "bottom_right"], [2, 3]), - ] + ], ) def test_evaluate_with_unique_bodyparts( num_individuals: int, @@ -73,18 +73,13 @@ def test_evaluate_with_unique_bodyparts( print() num_images = 5 gt, pred = generate_data(num_images, num_individuals, len(bodyparts), error) - gt_unique, pred_unique = generate_data( - num_images, 1, len(unique_bodyparts), unique_error - ) + gt_unique, pred_unique = generate_data(num_images, 1, len(unique_bodyparts), unique_error) pose_runner = Mock() PREDICT.return_value = { - img: {"bodyparts": pose, "unique_bodyparts": pred_unique[img]} - for img, pose in pred.items() + img: {"bodyparts": pose, "unique_bodyparts": pred_unique[img]} for img, pose in pred.items() } - loader = build_mock_loader( - gt, num_individuals, bodyparts, gt_unique=gt_unique, unique=unique_bodyparts - ) + loader = build_mock_loader(gt, num_individuals, bodyparts, gt_unique=gt_unique, unique=unique_bodyparts) results, preds = apis.evaluate(pose_runner, loader, mode="test") idv_errors = np.tile(error, (num_individuals, 1)).reshape(-1) expected_rmse = np.mean(np.concatenate([idv_errors, unique_error])) @@ -103,8 +98,8 @@ class CompTestConfig: num_individuals: int = 1 bodyparts: tuple[str, ...] = ("nose", "left_ear") error: tuple[float, ...] = (5, 10) - unique_bodyparts: tuple[str, ...] = ("top_left", ) - unique_error: tuple[float, ...] = (2, ) + unique_bodyparts: tuple[str, ...] = ("top_left",) + unique_error: tuple[float, ...] = (2,) comparison_bodyparts: str | list[str] | None = None expected_error: float = (2 + 5 + 10) / 3 @@ -149,7 +144,7 @@ def num_unique(self) -> int: comparison_bodyparts=["nose", "left_ear", "a", "b"], expected_error=((7 * 5) + (7 * 10) + 3.0 + 4.0) / (7 + 7 + 2), ), - ] + ], ) def test_evaluate_with_comparison_bodyparts(cfg: CompTestConfig) -> None: print() @@ -159,8 +154,7 @@ def test_evaluate_with_comparison_bodyparts(cfg: CompTestConfig) -> None: pose_runner = Mock() PREDICT.return_value = { - img: {"bodyparts": pose, "unique_bodyparts": pred_unique[img]} - for img, pose in pred.items() + img: {"bodyparts": pose, "unique_bodyparts": pred_unique[img]} for img, pose in pred.items() } loader = build_mock_loader( gt, @@ -170,7 +164,10 @@ def test_evaluate_with_comparison_bodyparts(cfg: CompTestConfig) -> None: unique=cfg.unique_bodyparts, ) results, preds = apis.evaluate( - pose_runner, loader, mode="test", comparison_bodyparts=cfg.comparison_bodyparts, + pose_runner, + loader, + mode="test", + comparison_bodyparts=cfg.comparison_bodyparts, ) print(cfg) print("results", results) @@ -190,17 +187,17 @@ def image(self) -> str: return f"image_{self.img:04d}.png" def error(self) -> float: - return np.linalg.norm( - np.asarray(self.gt, dtype=float) - np.asarray(self.pred, dtype=float) - ).item() + return np.linalg.norm(np.asarray(self.gt, dtype=float) - np.asarray(self.pred, dtype=float)).item() @patch("deeplabcut.pose_estimation_pytorch.apis.evaluation.predict", PREDICT) @pytest.mark.parametrize( - "pcutoff", [0.4, 0.6, 0.8, [0.3, 0.5, 0.7]], + "pcutoff", + [0.4, 0.6, 0.8, [0.3, 0.5, 0.7]], ) @pytest.mark.parametrize( - "keypoints", [ + "keypoints", + [ [ KeypointData(img=0, idv=0, bodypart="a", gt=(10, 10), pred=(11, 10), score=0.7), KeypointData(img=0, idv=0, bodypart="b", gt=(20, 20), pred=(21, 20), score=0.7), @@ -214,7 +211,7 @@ def error(self) -> float: KeypointData(img=0, idv=1, bodypart="b", gt=(50, 20), pred=(49, 20), score=0.5), KeypointData(img=0, idv=1, bodypart="c", gt=(60, 20), pred=(58, 20), score=0.2), ], - ] + ], ) def test_evaluate_with_pcutoff( pcutoff: float | list[float], @@ -267,17 +264,14 @@ def test_evaluate_with_pcutoff( np.testing.assert_almost_equal(results["rmse"], np.mean(errors)) np.testing.assert_almost_equal(results["rmse_pcutoff"], np.mean(errors_cutoff)) if "rmse_detections" in results: - np.testing.assert_almost_equal( - results["rmse_detections"], np.mean(errors) - ) - np.testing.assert_almost_equal( - results["rmse_detections_pcutoff"], np.mean(errors_cutoff) - ) + np.testing.assert_almost_equal(results["rmse_detections"], np.mean(errors)) + np.testing.assert_almost_equal(results["rmse_detections_pcutoff"], np.mean(errors_cutoff)) @patch("deeplabcut.pose_estimation_pytorch.apis.evaluation.predict", PREDICT) @pytest.mark.parametrize( - "pcutoff", [ + "pcutoff", + [ 0.4, 0.6, 0.8, @@ -288,7 +282,8 @@ def test_evaluate_with_pcutoff( ], ) @pytest.mark.parametrize( - "keypoints", [ + "keypoints", + [ [ KeypointData(img=0, idv=0, bodypart="a", gt=(10, 10), pred=(11, 10), score=0.7), KeypointData(img=0, idv=0, bodypart="b", gt=(20, 20), pred=(21, 20), score=0.7), @@ -329,8 +324,8 @@ def test_evaluate_with_pcutoff( KeypointData(img=1, idv=-1, bodypart="u1", gt=(17, 32), pred=(58, 20), score=0.2), KeypointData(img=1, idv=-1, bodypart="u2", gt=(37, 4), pred=(3, 3), score=0.7), KeypointData(img=1, idv=-1, bodypart="u3", gt=(12, 6), pred=(20, 22), score=0.9), - ] - ] + ], + ], ) def test_evaluate_with_pcutoff_and_unique_bodyparts( pcutoff: float | list[float], @@ -385,8 +380,7 @@ def test_evaluate_with_pcutoff_and_unique_bodyparts( pose_runner = Mock() PREDICT.return_value = { - img: {"bodyparts": pose, "unique_bodyparts": pred_unique[img]} - for img, pose in pred.items() + img: {"bodyparts": pose, "unique_bodyparts": pred_unique[img]} for img, pose in pred.items() } loader = build_mock_loader(gt, num_idv, bodyparts, gt_unique, unique_bodyparts) results, preds = apis.evaluate(pose_runner, loader, mode="test", pcutoff=pcutoff) @@ -395,12 +389,8 @@ def test_evaluate_with_pcutoff_and_unique_bodyparts( np.testing.assert_almost_equal(results["rmse"], np.mean(errors)) np.testing.assert_almost_equal(results["rmse_pcutoff"], np.mean(errors_cutoff)) if "rmse_detections" in results: - np.testing.assert_almost_equal( - results["rmse_detections"], np.mean(errors) - ) - np.testing.assert_almost_equal( - results["rmse_detections_pcutoff"], np.mean(errors_cutoff) - ) + np.testing.assert_almost_equal(results["rmse_detections"], np.mean(errors)) + np.testing.assert_almost_equal(results["rmse_detections_pcutoff"], np.mean(errors_cutoff)) def generate_data( diff --git a/tests/pose_estimation_pytorch/apis/test_apis_export.py b/tests/pose_estimation_pytorch/apis/test_apis_export.py index 89b93e2ac3..66f3b5c695 100644 --- a/tests/pose_estimation_pytorch/apis/test_apis_export.py +++ b/tests/pose_estimation_pytorch/apis/test_apis_export.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Tests exporting models""" + import copy import shutil from pathlib import Path @@ -118,9 +119,7 @@ def _get_export_model_data( snapshot_path = model_dir / f"snapshot-detector-{i:03}.pt" torch.save(snapshot, snapshot_path) detector_data.append(snapshot) - detector_snapshots.append( - Snapshot(best=False, epochs=i, path=snapshot_path) - ) + detector_snapshots.append(Snapshot(best=False, epochs=i, path=snapshot_path)) mock_loader = _make_mock_loader( project_path=project_dir, @@ -264,9 +263,7 @@ def test_export_change_iteration(project_dir, task: Task, iteration: int): snapshot = snapshots[0] detector = None if task == Task.BOTTOM_UP else detector_snapshots[0] - loader_diff_iter = _get_export_model_data( - project_dir, 1, task, project_iteration=iteration - )[0] + loader_diff_iter = _get_export_model_data(project_dir, 1, task, project_iteration=iteration)[0] def get_mock_loader(config, *args, **kwargs): _loader = copy.deepcopy(mock_loader) @@ -291,9 +288,7 @@ def read_mock_config(*args, **kwargs): for loader in [mock_loader, loader_diff_iter]: dir_name = export.get_export_folder_name(loader) filename = export.get_export_filename(loader, snapshot, detector) - assert not ( - project_dir / "exported-models-pytorch" / dir_name / filename - ).exists() + assert not (project_dir / "exported-models-pytorch" / dir_name / filename).exists() # export data export.export_model(project_dir / "config.yaml", iteration=iteration) diff --git a/tests/pose_estimation_pytorch/apis/test_create_tracking_dataset.py b/tests/pose_estimation_pytorch/apis/test_create_tracking_dataset.py index 6ea3ea4540..dcf26f2b19 100644 --- a/tests/pose_estimation_pytorch/apis/test_create_tracking_dataset.py +++ b/tests/pose_estimation_pytorch/apis/test_create_tracking_dataset.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Tests method to create the tracking dataset in PyTorch""" + from pathlib import Path import torch @@ -71,6 +72,3 @@ def test_build_feature_extraction_runner(tmp_path_factory): device="cpu", batch_size=1, ) - - - diff --git a/tests/pose_estimation_pytorch/config/test_config_utils.py b/tests/pose_estimation_pytorch/config/test_config_utils.py index 1084e5b940..52f17f752e 100644 --- a/tests/pose_estimation_pytorch/config/test_config_utils.py +++ b/tests/pose_estimation_pytorch/config/test_config_utils.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Test util functions for config creation""" + import pytest import deeplabcut.pose_estimation_pytorch.config.utils as utils @@ -53,7 +54,7 @@ "a": [{"b": 3}], "b": [[{"b": 30}]], }, - ) + ), ], ) def test_replace_default_values_no_extras(data: dict): diff --git a/tests/pose_estimation_pytorch/config/test_make_pose_config.py b/tests/pose_estimation_pytorch/config/test_make_pose_config.py index 3da2f0f1f4..ad0b1c3fd4 100644 --- a/tests/pose_estimation_pytorch/config/test_make_pose_config.py +++ b/tests/pose_estimation_pytorch/config/test_make_pose_config.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Tests the pre-processors""" + import pytest import deeplabcut.utils.auxiliaryfunctions as af @@ -24,9 +25,7 @@ @pytest.mark.parametrize("bodyparts", [["nose"], ["nose", "ear", "eye"]]) -@pytest.mark.parametrize( - "net_type", ["resnet_50", "resnet_101", "hrnet_w18", "hrnet_w32", "hrnet_w48"] -) +@pytest.mark.parametrize("net_type", ["resnet_50", "resnet_101", "hrnet_w18", "hrnet_w32", "hrnet_w48"]) def test_make_single_animal_config(bodyparts: list[str], net_type: str): # Single animal projects can't have unique bodyparts project_config = _make_project_config( @@ -68,9 +67,7 @@ def test_make_single_animal_config(bodyparts: list[str], net_type: str): @pytest.mark.parametrize("bodyparts", [["nose"], ["nose", "ear", "eye"]]) @pytest.mark.parametrize("identity", [False, True]) @pytest.mark.parametrize("unique_bodyparts", [[], ["tail"]]) -@pytest.mark.parametrize( - "net_type", ["resnet_50", "resnet_101", "hrnet_w18", "hrnet_w32", "hrnet_w48"] -) +@pytest.mark.parametrize("net_type", ["resnet_50", "resnet_101", "hrnet_w18", "hrnet_w32", "hrnet_w48"]) def test_backbone_plus_paf_config( multianimal: bool, individuals: list[str], @@ -95,9 +92,7 @@ def test_backbone_plus_paf_config( ) pretty_print(pytorch_pose_config) - graph = [ - [i, j] for i in range(len(bodyparts)) for j in range(i + 1, len(bodyparts)) - ] + graph = [[i, j] for i in range(len(bodyparts)) for j in range(i + 1, len(bodyparts))] num_limbs = len(graph) * 2 # check heads are there @@ -148,9 +143,7 @@ def test_backbone_plus_paf_config( ) @pytest.mark.parametrize("individuals", [["single"], ["bugs", "daffy"]]) @pytest.mark.parametrize("bodyparts", [["nose"], ["nose", "ear", "eye"]]) -@pytest.mark.parametrize( - "net_type", ["resnet_50", "resnet_101", "hrnet_w18", "hrnet_w32", "hrnet_w48"] -) +@pytest.mark.parametrize("net_type", ["resnet_50", "resnet_101", "hrnet_w18", "hrnet_w32", "hrnet_w48"]) def test_top_down_config( detector: tuple[str, str], individuals: list[str], @@ -285,9 +278,7 @@ def test_make_dlcrnet_config( net_type=net_type, ) pretty_print(pytorch_pose_config) - paf_graph = [ - [i, j] for i in range(len(bodyparts)) for j in range(i + 1, len(bodyparts)) - ] + paf_graph = [[i, j] for i in range(len(bodyparts)) for j in range(i + 1, len(bodyparts))] num_limbs = len(paf_graph) # check heads are there diff --git a/tests/pose_estimation_pytorch/data/test_data_ctd.py b/tests/pose_estimation_pytorch/data/test_data_ctd.py index 5e51c3a86b..0739416c81 100644 --- a/tests/pose_estimation_pytorch/data/test_data_ctd.py +++ b/tests/pose_estimation_pytorch/data/test_data_ctd.py @@ -58,10 +58,7 @@ def test_ctd_load_json_containing_rel_paths( path_prefix = _to_windows_path(path_prefix) print(f" Converted {path_prefix}") - data = [ - (_to_windows_path(img), _to_windows_path(key), cond) - for img, key, cond in data - ] + data = [(_to_windows_path(img), _to_windows_path(key), cond) for img, key, cond in data] print(f"Images: {[d[0] for d in data]}") print(f"Condition keys: {[d[1] for d in data]}") print("---") @@ -152,8 +149,7 @@ def test_ctd_load_hdf_containing_rel_paths( idv_mask = ~np.all(keypoint_mask, axis=2) output_pose = [ - p[p_mask] if np.any(p_mask) else np.zeros((0, num_bodyparts, 3)) - for p, p_mask in zip(output_pose, idv_mask) + p[p_mask] if np.any(p_mask) else np.zeros((0, num_bodyparts, 3)) for p, p_mask in zip(output_pose, idv_mask) ] # generate columns for the dataframe @@ -174,9 +170,7 @@ def test_ctd_load_hdf_containing_rel_paths( conditions_filepath = tmp_folder / "conditions.h5" df.to_hdf(conditions_filepath, key="df_with_missing") - conditions = CondFromFile.load_conditions_h5( - conditions_filepath, images, path_prefix=path_prefix - ) + conditions = CondFromFile.load_conditions_h5(conditions_filepath, images, path_prefix=path_prefix) for idx, (img_path, img_index) in enumerate(data): assert img_path in conditions np.testing.assert_allclose(output_pose[idx], conditions[img_path]) diff --git a/tests/pose_estimation_pytorch/data/test_postprocessor.py b/tests/pose_estimation_pytorch/data/test_postprocessor.py index 77ed34f44e..70094d5947 100644 --- a/tests/pose_estimation_pytorch/data/test_postprocessor.py +++ b/tests/pose_estimation_pytorch/data/test_postprocessor.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Tests the pre-processors""" + import numpy as np import pytest @@ -232,13 +233,15 @@ def test_prepare_backbone_features(): features[0, 25, 20] = 2 features[0, 35, 30] = 3 - pose = np.array([ + pose = np.array( [ - [10.1, 15.1, 0.95], - [20.1, 25.1, 0.95], - [29.9, 34.9, 0.95], - ], - ]) + [ + [10.1, 15.1, 0.95], + [20.1, 25.1, 0.95], + [29.9, 34.9, 0.95], + ], + ] + ) predictions = [dict(backbone=dict(features=features), bodypart=dict(poses=pose))] context = dict(image_size=(img_w, img_h)) @@ -270,13 +273,15 @@ def test_prepare_top_down_backbone_features(): features[1, 0, 85, 20] = 12 features[1, 0, 75, 30] = 13 - pose_idv0 = np.array([ + pose_idv0 = np.array( [ - [10.1, 15.1, 0.95], - [20.1, 25.1, 0.95], - [29.9, 34.9, 0.95], - ], - ]) + [ + [10.1, 15.1, 0.95], + [20.1, 25.1, 0.95], + [29.9, 34.9, 0.95], + ], + ] + ) pose_idv1 = np.array( [ [ @@ -343,34 +348,34 @@ def test_remove_low_confidence_boxes(data): """Tests that RemoveLowConfidenceBoxes filters boxes below threshold""" postprocessor = RemoveLowConfidenceBoxes(bbox_score_thresh=data["threshold"]) context = {} - + # Handle empty input arrays with proper shape if len(data["bboxes"]) == 0: bboxes = np.empty((0, 4)) else: bboxes = np.array(data["bboxes"]) - + if len(data["bbox_scores"]) == 0: bbox_scores = np.empty((0,)) else: bbox_scores = np.array(data["bbox_scores"]) - + predictions = { "bboxes": bboxes, "bbox_scores": bbox_scores, } predictions, context = postprocessor(predictions, context=context) - + # Handle empty expected arrays with proper shape if len(data["expected_bboxes"]) == 0: expected_bboxes = np.empty((0, 4)) else: expected_bboxes = np.array(data["expected_bboxes"]) - + if len(data["expected_scores"]) == 0: expected_scores = np.empty((0,)) else: expected_scores = np.array(data["expected_scores"]) - + np.testing.assert_array_equal(predictions["bboxes"], expected_bboxes) np.testing.assert_array_equal(predictions["bbox_scores"], expected_scores) diff --git a/tests/pose_estimation_pytorch/data/test_preprocessor.py b/tests/pose_estimation_pytorch/data/test_preprocessor.py index 3e90c98ae7..9ef21749ca 100644 --- a/tests/pose_estimation_pytorch/data/test_preprocessor.py +++ b/tests/pose_estimation_pytorch/data/test_preprocessor.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Tests the pre-processors""" + import albumentations as A import numpy as np import pytest @@ -83,15 +84,9 @@ def test_augment_image_rescaling(data): # two well-defined individuals { "image_shape": (100, 100, 3), - "context": { - "cond_kpts": np.array( - [[[10, 10, 0.8], [20, 20, 0.8]], [[60, 60, 0.8], [70, 70, 0.8]]] - ) - }, + "context": {"cond_kpts": np.array([[[10, 10, 0.8], [20, 20, 0.8]], [[60, 60, 0.8], [70, 70, 0.8]]])}, "output_context": { - "cond_kpts": np.array( - [[[10, 10, 0.8], [20, 20, 0.8]], [[60, 60, 0.8], [70, 70, 0.8]]] - ), + "cond_kpts": np.array([[[10, 10, 0.8], [20, 20, 0.8]], [[60, 60, 0.8], [70, 70, 0.8]]]), "bboxes": [np.array([10, 10, 10, 10]), np.array([60, 60, 10, 10])], "offsets": [(10, 10), (60, 60)], "scales": [(0.1, 0.1), (0.1, 0.1)], @@ -100,11 +95,7 @@ def test_augment_image_rescaling(data): # one individual has 0 keypoints { "image_shape": (100, 100, 3), - "context": { - "cond_kpts": np.array( - [[[10, 10, 0.8], [20, 20, 0.8]], [[60, 60, 0.0], [70, 70, 0.0]]] - ) - }, + "context": {"cond_kpts": np.array([[[10, 10, 0.8], [20, 20, 0.8]], [[60, 60, 0.0], [70, 70, 0.0]]])}, "output_context": { "cond_kpts": np.array( [ @@ -119,11 +110,7 @@ def test_augment_image_rescaling(data): # one individual has only 1 keypoints { "image_shape": (100, 100, 3), - "context": { - "cond_kpts": np.array( - [[[10, 10, 0.8], [20, 20, 0.8]], [[60, 60, 0.0], [70, 70, 0.9]]] - ) - }, + "context": {"cond_kpts": np.array([[[10, 10, 0.8], [20, 20, 0.8]], [[60, 60, 0.0], [70, 70, 0.9]]])}, "output_context": { "cond_kpts": np.array( [ @@ -138,11 +125,7 @@ def test_augment_image_rescaling(data): # two individuals but one is low confidence { "image_shape": (100, 100, 3), - "context": { - "cond_kpts": np.array( - [[[10, 10, 0.8], [20, 20, 0.8]], [[60, 60, 0.01], [70, 70, 0.01]]] - ) - }, + "context": {"cond_kpts": np.array([[[10, 10, 0.8], [20, 20, 0.8]], [[60, 60, 0.01], [70, 70, 0.01]]])}, "output_context": { "cond_kpts": np.array( [ @@ -162,9 +145,7 @@ def test_conditional_top_down_preprocessor(data): output_img, output_context = ctd_preprocessor(input_img, context=data["context"]) for context_key in ["cond_kpts", "bboxes", "offsets", "scales"]: - assert deep_equal( - output_context[context_key], data["output_context"][context_key] - ) + assert deep_equal(output_context[context_key], data["output_context"][context_key]) def deep_equal(a, b): diff --git a/tests/pose_estimation_pytorch/data/test_transforms.py b/tests/pose_estimation_pytorch/data/test_transforms.py index 482120bdfa..9f264fa4fb 100644 --- a/tests/pose_estimation_pytorch/data/test_transforms.py +++ b/tests/pose_estimation_pytorch/data/test_transforms.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Tests the custom transforms""" + import random import albumentations as A @@ -141,7 +142,9 @@ def test_random_bbox_transform_does_not_modify_with_base_config(data: dict) -> N bbox_params=A.BboxParams(format="coco", label_fields=["bbox_labels"]), ) output = t( - image=np.zeros((h, w, c)), bboxes=bboxes, bbox_labels=np.zeros(len(bboxes)), + image=np.zeros((h, w, c)), + bboxes=bboxes, + bbox_labels=np.zeros(len(bboxes)), ) print("Output bounding boxes") for out_bbox in output["bboxes"]: @@ -207,7 +210,9 @@ def test_random_bbox_transform_scale(data: dict) -> None: bbox_params=A.BboxParams(format="coco", label_fields=["bbox_labels"]), ) output = t( - image=np.zeros((h, w, c)), bboxes=bboxes, bbox_labels=np.zeros(len(bboxes)), + image=np.zeros((h, w, c)), + bboxes=bboxes, + bbox_labels=np.zeros(len(bboxes)), ) print("Output bounding boxes") for out_bbox in output["bboxes"]: @@ -253,7 +258,9 @@ def test_random_bbox_transform_shift(data: dict) -> None: bbox_params=A.BboxParams(format="coco", label_fields=["bbox_labels"]), ) output = t( - image=np.zeros((h, w, c)), bboxes=bboxes, bbox_labels=np.zeros(len(bboxes)), + image=np.zeros((h, w, c)), + bboxes=bboxes, + bbox_labels=np.zeros(len(bboxes)), ) print("Output bounding boxes") for out_bbox in output["bboxes"]: @@ -277,7 +284,10 @@ def _set_random_seed(): def _gen_random_bboxes( - gen: np.random.Generator, num_bboxes: int, w: int, h: int, + gen: np.random.Generator, + num_bboxes: int, + w: int, + h: int, ) -> np.ndarray: image_wh = np.array([w, h]) bboxes = np.zeros((num_bboxes, 4)) diff --git a/tests/pose_estimation_pytorch/data/test_utils.py b/tests/pose_estimation_pytorch/data/test_utils.py index 3b24c4ba12..7bc1a3b3b6 100644 --- a/tests/pose_estimation_pytorch/data/test_utils.py +++ b/tests/pose_estimation_pytorch/data/test_utils.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Tests data utils""" + import numpy as np import pytest diff --git a/tests/pose_estimation_pytorch/models/target_generators/test_heatmap_targets.py b/tests/pose_estimation_pytorch/models/target_generators/test_heatmap_targets.py index d6641f265e..c46447dca1 100644 --- a/tests/pose_estimation_pytorch/models/target_generators/test_heatmap_targets.py +++ b/tests/pose_estimation_pytorch/models/target_generators/test_heatmap_targets.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Tests the heatmap target generators (plateau and gaussian)""" + import numpy as np import torch import pytest @@ -17,6 +18,7 @@ HeatmapGaussianGenerator, ) + @pytest.mark.parametrize( "data", [ @@ -56,7 +58,7 @@ [0.1054, 0.3247, 0.1054, 0.0036], [0.3247, 1.0, 0.3247, 0.0111], [0.1054, 0.3247, 0.1054, 0.0036], - [0.0036, 0.0111, 0.0036, 0.0001] + [0.0036, 0.0111, 0.0036, 0.0001], ], }, ], @@ -91,23 +93,17 @@ def test_gaussian_heatmap_generation_single_keypoint(data): "batch_size, num_keypoints, image_size", [(2, 2, (64, 64)), (1, 5, (48, 64)), (15, 50, (64, 48))], ) -def test_random_gaussian_target_generation( - batch_size: int, num_keypoints: int, image_size: tuple, num_animals=1 -): +def test_random_gaussian_target_generation(batch_size: int, num_keypoints: int, image_size: tuple, num_animals=1): # generate annotations annotations = { - "keypoints": torch.randint( - 1, min(image_size), (batch_size, num_animals, num_keypoints, 2) - ) + "keypoints": torch.randint(1, min(image_size), (batch_size, num_animals, num_keypoints, 2)) } # batch size, num animals, num keypoints, 2 for x,y # model stride 1 stride = 1 # generate predictions - predicted_heatmaps = { - "heatmap": torch.zeros((batch_size, num_keypoints, *image_size)) - } + predicted_heatmaps = {"heatmap": torch.zeros((batch_size, num_keypoints, *image_size))} # generate heatmap generator = HeatmapGaussianGenerator( @@ -117,9 +113,7 @@ def test_random_gaussian_target_generation( generate_locref=False, ) targets = generator(stride, predicted_heatmaps, annotations) - target_heatmap = targets["heatmap"]["target"].reshape( - batch_size, num_keypoints, image_size[0] * image_size[1] - ) + target_heatmap = targets["heatmap"]["target"].reshape(batch_size, num_keypoints, image_size[0] * image_size[1]) # get coords of max value of the heatmap gaus_max = torch.argmax(target_heatmap, dim=2) diff --git a/tests/pose_estimation_pytorch/models/target_generators/test_plateau_targets.py b/tests/pose_estimation_pytorch/models/target_generators/test_plateau_targets.py index 4aa7133a4d..e577959d45 100644 --- a/tests/pose_estimation_pytorch/models/target_generators/test_plateau_targets.py +++ b/tests/pose_estimation_pytorch/models/target_generators/test_plateau_targets.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Tests the heatmap target generators (plateau and gaussian)""" + import numpy as np import torch import pytest @@ -29,9 +30,9 @@ "out_shape": (3, 3), "centers": [(1, 1)], "expected_output": [ - [0., 1., 0.], - [1., 1., 1.], - [0., 1., 0.], + [0.0, 1.0, 0.0], + [1.0, 1.0, 1.0], + [0.0, 1.0, 0.0], ], }, { @@ -41,11 +42,11 @@ "out_shape": (5, 5), "centers": [[1, 1], [2, 2]], "expected_output": [ - [1., 1., 1., 0., 0.], - [1., 1., 1., 1., 0.], - [1., 1., 1., 1., 1.], - [0., 1., 1., 1., 0.], - [0., 0., 1., 0., 0.], + [1.0, 1.0, 1.0, 0.0, 0.0], + [1.0, 1.0, 1.0, 1.0, 0.0], + [1.0, 1.0, 1.0, 1.0, 1.0], + [0.0, 1.0, 1.0, 1.0, 0.0], + [0.0, 0.0, 1.0, 0.0, 0.0], ], }, { @@ -55,10 +56,10 @@ "out_shape": (4, 4), "centers": [[1, 1]], "expected_output": [ - [1., 1., 1., 0.], - [1., 1., 1., 1.], - [1., 1., 1., 0.], - [0., 1., 0., 0.], + [1.0, 1.0, 1.0, 0.0], + [1.0, 1.0, 1.0, 1.0], + [1.0, 1.0, 1.0, 0.0], + [0.0, 1.0, 0.0, 0.0], ], }, ], diff --git a/tests/pose_estimation_pytorch/modelzoo/test_fmpose_integration.py b/tests/pose_estimation_pytorch/modelzoo/test_fmpose_integration.py index aa3b25e94b..9de3184af7 100644 --- a/tests/pose_estimation_pytorch/modelzoo/test_fmpose_integration.py +++ b/tests/pose_estimation_pytorch/modelzoo/test_fmpose_integration.py @@ -35,21 +35,14 @@ def _has_network(host="huggingface.co", port=443, timeout=3) -> bool: _REPO_ROOT = pathlib.Path(__file__).resolve().parents[3] _EXAMPLE_IMAGE = ( - _REPO_ROOT - / "examples" - / "Reaching-Mackenzie-2018-08-30" - / "labeled-data" - / "reachingvideo1" - / "img005.png" + _REPO_ROOT / "examples" / "Reaching-Mackenzie-2018-08-30" / "labeled-data" / "reachingvideo1" / "img005.png" ) # --------------------------------------------------------------------------- # Lightweight: verifies the API object is constructed correctly # --------------------------------------------------------------------------- -@pytest.mark.parametrize( - "model_type", ["fmpose3d_humans", "fmpose3d_animals"] -) +@pytest.mark.parametrize("model_type", ["fmpose3d_humans", "fmpose3d_animals"]) def test_api_init(model_type): api = get_fmpose3d_inference_api(model_type, device="cpu") assert api is not None diff --git a/tests/pose_estimation_pytorch/modelzoo/test_modelzoo_utils.py b/tests/pose_estimation_pytorch/modelzoo/test_modelzoo_utils.py index 80cf03e58e..571dc9f5cd 100644 --- a/tests/pose_estimation_pytorch/modelzoo/test_modelzoo_utils.py +++ b/tests/pose_estimation_pytorch/modelzoo/test_modelzoo_utils.py @@ -15,9 +15,8 @@ # TODO: make a proper test incl. human model, bird model and that skips the require... at least once per week. -@pytest.mark.parametrize( - "super_animal", ["superanimal_quadruped", "superanimal_topviewmouse"] -) + +@pytest.mark.parametrize("super_animal", ["superanimal_quadruped", "superanimal_topviewmouse"]) @pytest.mark.parametrize("model_name", ["hrnet_w32"]) @pytest.mark.parametrize("detector_name", [None, "fasterrcnn_resnet50_fpn_v2"]) def test_get_config_model_paths(super_animal, model_name, detector_name): diff --git a/tests/pose_estimation_pytorch/modelzoo/test_webapp.py b/tests/pose_estimation_pytorch/modelzoo/test_webapp.py index 3eae9e6a11..6b7e33cde9 100644 --- a/tests/pose_estimation_pytorch/modelzoo/test_webapp.py +++ b/tests/pose_estimation_pytorch/modelzoo/test_webapp.py @@ -19,15 +19,12 @@ # TODO: make a proper test incl. human model, bird model and that skips the require... at least once per week. + @pytest.mark.parametrize("max_individuals", [1, 3]) -@pytest.mark.parametrize( - "project_name", ["superanimal_quadruped", "superanimal_topviewmouse"] -) +@pytest.mark.parametrize("project_name", ["superanimal_quadruped", "superanimal_topviewmouse"]) @pytest.mark.parametrize("pose_model_type", ["hrnet_w32"]) def test_class_init(project_name, pose_model_type, max_individuals): - inference_pipeline = SuperanimalPyTorchInference( - project_name, pose_model_type, max_individuals=max_individuals - ) + inference_pipeline = SuperanimalPyTorchInference(project_name, pose_model_type, max_individuals=max_individuals) assert isinstance(inference_pipeline.config, dict) assert inference_pipeline.config["metadata"]["bodyparts"] @@ -35,14 +32,10 @@ def test_class_init(project_name, pose_model_type, max_individuals): @pytest.mark.skip(reason="require-models") -@pytest.mark.parametrize( - "project_name", ["superanimal_quadruped", "superanimal_topviewmouse"] -) +@pytest.mark.parametrize("project_name", ["superanimal_quadruped", "superanimal_topviewmouse"]) @pytest.mark.parametrize("pose_model_type", ["hrnet_w32"]) def test_runner_init(project_name, pose_model_type): - inference_pipeline = SuperanimalPyTorchInference( - project_name, pose_model_type, max_individuals=1 - ) + inference_pipeline = SuperanimalPyTorchInference(project_name, pose_model_type, max_individuals=1) weight_folder = f"{auxiliaryfunctions.get_deeplabcut_path()}/modelzoo/checkpoints" snapshot_path = f"{weight_folder}/{project_name}_{pose_model_type}.pth" detector_path = f"{weight_folder}/{project_name}_fasterrcnn.pt" @@ -55,14 +48,10 @@ def test_runner_init(project_name, pose_model_type): @pytest.mark.skip(reason="require-models") @pytest.mark.parametrize("max_individuals", [10, 4, 1]) -@pytest.mark.parametrize( - "project_name", ["superanimal_quadruped", "superanimal_topviewmouse", "superanimal_humanbody"] -) +@pytest.mark.parametrize("project_name", ["superanimal_quadruped", "superanimal_topviewmouse", "superanimal_humanbody"]) @pytest.mark.parametrize("pose_model_type", ["hrnet_w32"]) def test_predict(project_name, pose_model_type, max_individuals): - inference_pipeline = SuperanimalPyTorchInference( - project_name, pose_model_type, max_individuals=max_individuals - ) + inference_pipeline = SuperanimalPyTorchInference(project_name, pose_model_type, max_individuals=max_individuals) image_path = "img0001.png" weight_folder = f"{auxiliaryfunctions.get_deeplabcut_path()}/modelzoo/checkpoints" snapshot_path = f"{weight_folder}/{project_name}_{pose_model_type}.pth" diff --git a/tests/pose_estimation_pytorch/other/test_api_utils.py b/tests/pose_estimation_pytorch/other/test_api_utils.py index bd2cf125bf..a53960c3eb 100644 --- a/tests/pose_estimation_pytorch/other/test_api_utils.py +++ b/tests/pose_estimation_pytorch/other/test_api_utils.py @@ -67,16 +67,14 @@ def test_build_transforms(transform_dict, size_image, num_keypoints, num_animals with pytest.raises(Exception): transformed = transform_bbox_aug(image=test_image) transformed = transform_bbox_aug(image=test_image, bboxes=bboxes.copy()) - transformed = transform_bbox_aug( - image=test_image, keypoints=keypoints.copy(), bboxes=bboxes.copy() - ) + transformed = transform_bbox_aug(image=test_image, keypoints=keypoints.copy(), bboxes=bboxes.copy()) transformed_with_bbox = transform_bbox_aug( image=test_image, keypoints=keypoints.copy(), bboxes=bboxes.copy(), bbox_labels=np.arange(num_animals), - class_labels=[0 for _ in range(len(keypoints))] + class_labels=[0 for _ in range(len(keypoints))], ) if "resize" in transform_dict.keys(): diff --git a/tests/pose_estimation_pytorch/other/test_custom_transforms.py b/tests/pose_estimation_pytorch/other/test_custom_transforms.py index 875e05e80a..28ddb2ffc7 100644 --- a/tests/pose_estimation_pytorch/other/test_custom_transforms.py +++ b/tests/pose_estimation_pytorch/other/test_custom_transforms.py @@ -18,9 +18,7 @@ def test_keypoint_aware_cropping(width, height): fake_image = np.empty((600, 600, 3)) fake_keypoints = [(i * 100, i * 100, 0, 0) for i in range(1, 6)] - aug = transforms.KeypointAwareCrop( - width=width, height=height, crop_sampling="density" - ) + aug = transforms.KeypointAwareCrop(width=width, height=height, crop_sampling="density") transformed = aug(image=fake_image, keypoints=fake_keypoints) assert transformed["image"].shape[:2] == (height, width) # Ensure at least a keypoint is visible in each crop diff --git a/tests/pose_estimation_pytorch/other/test_dataset.py b/tests/pose_estimation_pytorch/other/test_dataset.py index b9c091c0f6..221ec64f19 100644 --- a/tests/pose_estimation_pytorch/other/test_dataset.py +++ b/tests/pose_estimation_pytorch/other/test_dataset.py @@ -107,31 +107,26 @@ def test_iter_all_dataset_no_transform(batch_size): num_keypoints = dataset.parameters.num_joints for i, item in enumerate(dataloader): is_last_batch = i == (len(dataloader) - 1) - assert ( - set(item.keys()) == key_set - ), f"the key returned don't match the required ones: {item.keys()} != {key_set}" + assert set(item.keys()) == key_set, ( + f"the key returned don't match the required ones: {item.keys()} != {key_set}" + ) anno = item["annotations"] - assert ( - set(anno.keys()) == anno_key_set - ), "the annotation keys returned don't match the required ones" + assert set(anno.keys()) == anno_key_set, "the annotation keys returned don't match the required ones" - assert (len(item["image"].shape) == 4) and ( - (item["image"].shape[:2] == (batch_size, 3)) or is_last_batch - ), "image shape is not (batch_size, 3, h, w)" + assert (len(item["image"].shape) == 4) and ((item["image"].shape[:2] == (batch_size, 3)) or is_last_batch), ( + "image shape is not (batch_size, 3, h, w)" + ) b, _, h, w = item["image"].shape kpts, bboxes = anno["keypoints"], anno["boxes"] - assert ( - kpts.shape == (batch_size, max_num_animals, num_keypoints, 3) - or is_last_batch - ), "keypoints have the wrong shape" - assert ( - bboxes.shape == (batch_size, max_num_animals, 4) or is_last_batch - ), "boxes have the wrong shape" - assert ((bboxes[:, :, 0] + bboxes[:, :, 2]) <= w).all() and ( - (bboxes[:, :, 1] + bboxes[:, :, 3]) <= h - ).all(), "boxes don't seem to be un the format (x, y, w, h)" + assert kpts.shape == (batch_size, max_num_animals, num_keypoints, 3) or is_last_batch, ( + "keypoints have the wrong shape" + ) + assert bboxes.shape == (batch_size, max_num_animals, 4) or is_last_batch, "boxes have the wrong shape" + assert ((bboxes[:, :, 0] + bboxes[:, :, 2]) <= w).all() and ((bboxes[:, :, 1] + bboxes[:, :, 3]) <= h).all(), ( + "boxes don't seem to be un the format (x, y, w, h)" + ) def _generate_random_test_values_aug(min_exa): @@ -170,29 +165,22 @@ def test_iter_all_augmented_dataset(batch_size, x_size, y_size, exaggeration): num_keypoints = dataset.parameters.num_joints for i, item in enumerate(dataloader): is_last_batch = i == (len(dataloader) - 1) - assert ( - set(item.keys()) == key_set - ), f"the key returned don't match the required ones: {item.keys()} != {key_set}" + assert set(item.keys()) == key_set, ( + f"the key returned don't match the required ones: {item.keys()} != {key_set}" + ) anno = item["annotations"] - assert ( - set(anno.keys()) == anno_key_set - ), "the annotation keys returned don't match the required ones" + assert set(anno.keys()) == anno_key_set, "the annotation keys returned don't match the required ones" - assert (len(item["image"].shape) == 4) and ( - (item["image"].shape[:2] == (batch_size, 3)) or is_last_batch - ), "image shape is not (batch_size, 3, h, w)" + assert (len(item["image"].shape) == 4) and ((item["image"].shape[:2] == (batch_size, 3)) or is_last_batch), ( + "image shape is not (batch_size, 3, h, w)" + ) kpts, bboxes = anno["keypoints"], anno["boxes"] b, _, h, w = item["image"].shape assert (h == y_size) and (w == x_size) - assert ( - kpts.shape == (batch_size, max_num_animals, num_keypoints, 3) - or is_last_batch - ), "keypoints have the wrong shape" - assert ( - bboxes.shape == (batch_size, max_num_animals, 4) or is_last_batch - ), "boxes have the wrong shape" - assert ((bboxes[:, :, 0] + bboxes[:, :, 2]) <= w).all() and ( - (bboxes[:, :, 1] + bboxes[:, :, 3]) <= h - ).all() + assert kpts.shape == (batch_size, max_num_animals, num_keypoints, 3) or is_last_batch, ( + "keypoints have the wrong shape" + ) + assert bboxes.shape == (batch_size, max_num_animals, 4) or is_last_batch, "boxes have the wrong shape" + assert ((bboxes[:, :, 0] + bboxes[:, :, 2]) <= w).all() and ((bboxes[:, :, 1] + bboxes[:, :, 3]) <= h).all() diff --git a/tests/pose_estimation_pytorch/other/test_gaussian_targets.py b/tests/pose_estimation_pytorch/other/test_gaussian_targets.py index 57cb212fa8..1c202b1902 100644 --- a/tests/pose_estimation_pytorch/other/test_gaussian_targets.py +++ b/tests/pose_estimation_pytorch/other/test_gaussian_targets.py @@ -18,14 +18,10 @@ "batch_size, num_keypoints, image_size", [(2, 2, (64, 64)), (1, 5, (48, 64)), (15, 50, (64, 48))], ) -def test_gaussian_target_generation( - batch_size: int, num_keypoints: int, image_size: tuple, num_animals=1 -): +def test_gaussian_target_generation(batch_size: int, num_keypoints: int, image_size: tuple, num_animals=1): # generate annotations labels = { - "keypoints": torch.randint( - 1, min(image_size), (batch_size, num_animals, num_keypoints, 2) - ) + "keypoints": torch.randint(1, min(image_size), (batch_size, num_animals, num_keypoints, 2)) } # batch size, num animals, num keypoints, 2 for x,y # generate predictions stride = 1 diff --git a/tests/pose_estimation_pytorch/other/test_heatmap_plateau_targets.py b/tests/pose_estimation_pytorch/other/test_heatmap_plateau_targets.py index 2508d90860..f4099889df 100644 --- a/tests/pose_estimation_pytorch/other/test_heatmap_plateau_targets.py +++ b/tests/pose_estimation_pytorch/other/test_heatmap_plateau_targets.py @@ -52,9 +52,7 @@ def get_target( """ labels = { - "keypoints": torch.randint( - 1, min(image_size), (batch_size, num_animals, num_joints, 2) - ) + "keypoints": torch.randint(1, min(image_size), (batch_size, num_animals, num_joints, 2)) } # 2 for x,y coords stride = 1 prediction = { diff --git a/tests/pose_estimation_pytorch/other/test_match_predictions_to_gt.py b/tests/pose_estimation_pytorch/other/test_match_predictions_to_gt.py index 943e5c9882..a31451d681 100644 --- a/tests/pose_estimation_pytorch/other/test_match_predictions_to_gt.py +++ b/tests/pose_estimation_pytorch/other/test_match_predictions_to_gt.py @@ -67,9 +67,7 @@ def test_invalid_rmse(animals_and_keypoints_invalid: tuple) -> None: pred_kpts, gt_kpts, indv_names = animals_and_keypoints_invalid with pytest.raises(ValueError): - deeplabcut_torch_match_predictions_gt.rmse_match_prediction_to_gt( - pred_kpts, gt_kpts - ) + deeplabcut_torch_match_predictions_gt.rmse_match_prediction_to_gt(pred_kpts, gt_kpts) def test_invalid_oks(animals_and_keypoints_invalid: tuple) -> None: @@ -83,14 +81,10 @@ def test_invalid_oks(animals_and_keypoints_invalid: tuple) -> None: pred_kpts, gt_kpts, indv_names = animals_and_keypoints_invalid with pytest.raises(ValueError): - deeplabcut_torch_match_predictions_gt.oks_match_prediction_to_gt( - pred_kpts, gt_kpts, indv_names - ) + deeplabcut_torch_match_predictions_gt.oks_match_prediction_to_gt(pred_kpts, gt_kpts, indv_names) -def test_rmse_match_predictions_to_gt( - animals_and_keypoints: tuple, num_animals: int = 6 -) -> None: +def test_rmse_match_predictions_to_gt(animals_and_keypoints: tuple, num_animals: int = 6) -> None: """Summary: Test if rmse_match_prediction_to_gt function returns the expected shape output. @@ -100,16 +94,12 @@ def test_rmse_match_predictions_to_gt( """ pred_kpts, gt_kpts, indv_names = animals_and_keypoints - col_ind = deeplabcut_torch_match_predictions_gt.rmse_match_prediction_to_gt( - pred_kpts, gt_kpts - ) + col_ind = deeplabcut_torch_match_predictions_gt.rmse_match_prediction_to_gt(pred_kpts, gt_kpts) assert isinstance(col_ind, np.ndarray) assert col_ind.shape == (num_animals,) -def test_oks_match_predictions_to_gt( - animals_and_keypoints: tuple, num_animals: int = 6 -) -> None: +def test_oks_match_predictions_to_gt(animals_and_keypoints: tuple, num_animals: int = 6) -> None: """Summary: Test if oks_match_predictions_to_gt function returns the expected shape output. @@ -119,9 +109,7 @@ def test_oks_match_predictions_to_gt( """ pred_kpts, gt_kpts, indv_names = animals_and_keypoints - col_ind = deeplabcut_torch_match_predictions_gt.rmse_match_prediction_to_gt( - pred_kpts, gt_kpts - ) + col_ind = deeplabcut_torch_match_predictions_gt.rmse_match_prediction_to_gt(pred_kpts, gt_kpts) assert isinstance(col_ind, np.ndarray) assert col_ind.shape == (num_animals,) @@ -136,10 +124,6 @@ def test_extend_col_ind(animals_and_keypoints: tuple, num_animals: int = 6) -> N """ pred_kpts, gt_kpts, indv_names = animals_and_keypoints - col_ind = deeplabcut_torch_match_predictions_gt.rmse_match_prediction_to_gt( - pred_kpts, gt_kpts - ) - extended_array = deeplabcut_torch_match_predictions_gt.extend_col_ind( - col_ind, num_animals - ) + col_ind = deeplabcut_torch_match_predictions_gt.rmse_match_prediction_to_gt(pred_kpts, gt_kpts) + extended_array = deeplabcut_torch_match_predictions_gt.extend_col_ind(col_ind, num_animals) assert extended_array.shape == (num_animals,) diff --git a/tests/pose_estimation_pytorch/other/test_modelzoo.py b/tests/pose_estimation_pytorch/other/test_modelzoo.py index f4ed80f5c8..4edb980b3d 100644 --- a/tests/pose_estimation_pytorch/other/test_modelzoo.py +++ b/tests/pose_estimation_pytorch/other/test_modelzoo.py @@ -21,6 +21,7 @@ "examples", ) + # requires videos to be in the examples folder @pytest.mark.skip @pytest.mark.parametrize( diff --git a/tests/pose_estimation_pytorch/other/test_paf_targets.py b/tests/pose_estimation_pytorch/other/test_paf_targets.py index f01fc3275a..9865cf732c 100644 --- a/tests/pose_estimation_pytorch/other/test_paf_targets.py +++ b/tests/pose_estimation_pytorch/other/test_paf_targets.py @@ -18,13 +18,9 @@ "batch_size, num_keypoints, image_size", [(2, 2, (64, 64)), (1, 5, (48, 64)), (8, 50, (64, 48))], ) -def test_paf_target_generation( - batch_size: int, num_keypoints: int, image_size: tuple, num_animals=2 -): +def test_paf_target_generation(batch_size: int, num_keypoints: int, image_size: tuple, num_animals=2): labels = { - "keypoints": torch.randint( - 1, min(image_size), (batch_size, num_animals, num_keypoints, 2) - ) + "keypoints": torch.randint(1, min(image_size), (batch_size, num_animals, num_keypoints, 2)) } # 2 for x,y coords graph = [(i, j) for i in range(num_keypoints) for j in range(i + 1, num_keypoints)] prediction = { diff --git a/tests/pose_estimation_pytorch/other/test_pose_model.py b/tests/pose_estimation_pytorch/other/test_pose_model.py index 977cbbb88e..680e82604f 100644 --- a/tests/pose_estimation_pytorch/other/test_pose_model.py +++ b/tests/pose_estimation_pytorch/other/test_pose_model.py @@ -250,9 +250,7 @@ def test_head(head_dict, input_shape, num_keypoints): output_channels = num_keypoints + 1 head_dict["target_generator"]["num_joints"] = num_keypoints head_dict["heatmap_config"]["channels"][2] = num_keypoints + 1 - head_dict["offset_config"]["channels"][1] = ( - num_keypoints * head_dict["offset_config"]["num_offset_per_kpt"] - ) + head_dict["offset_config"]["channels"][1] = num_keypoints * head_dict["offset_config"]["num_offset_per_kpt"] head_dict["offset_config"]["channels"][2] = num_keypoints input_tensor = torch.zeros((1, input_channels, h, w)) @@ -263,18 +261,14 @@ def test_head(head_dict, input_shape, num_keypoints): criterions = {} for loss_name, criterion_cfg in head_dict["criterion"].items(): weights[loss_name] = criterion_cfg.get("weight", 1.0) - criterion_cfg = { - k: v for k, v in criterion_cfg.items() if k != "weight" - } + criterion_cfg = {k: v for k, v in criterion_cfg.items() if k != "weight"} criterions[loss_name] = CRITERIONS.build(criterion_cfg) aggregator_cfg = {"type": "WeightedLossAggregator", "weights": weights} head_dict["aggregator"] = LOSS_AGGREGATORS.build(aggregator_cfg) head_dict["criterion"] = criterions - head_dict["target_generator"] = TARGET_GENERATORS.build( - head_dict["target_generator"] - ) + head_dict["target_generator"] = TARGET_GENERATORS.build(head_dict["target_generator"]) head_dict["predictor"] = PREDICTORS.build(head_dict["predictor"]) head = dlc_models.HEADS.build(head_dict) diff --git a/tests/pose_estimation_pytorch/other/test_seq_targets.py b/tests/pose_estimation_pytorch/other/test_seq_targets.py index 82f931f520..c2816c8650 100644 --- a/tests/pose_estimation_pytorch/other/test_seq_targets.py +++ b/tests/pose_estimation_pytorch/other/test_seq_targets.py @@ -39,11 +39,7 @@ def test_sequential_generator(): } gen = TARGET_GENERATORS.build(cfg) - annotations = { - "keypoints": torch.randint( - 1, min(image_size), (batch_size, num_animals, num_keypoints, 2) - ) - } + annotations = {"keypoints": torch.randint(1, min(image_size), (batch_size, num_animals, num_keypoints, 2))} head_outputs = { "heatmap": torch.rand(batch_size, num_keypoints, 32, 32), "locref": torch.rand(batch_size, num_keypoints * 2, 32, 32), diff --git a/tests/pose_estimation_pytorch/post_processing/test_identity.py b/tests/pose_estimation_pytorch/post_processing/test_identity.py index 42ae454341..465f12fd01 100644 --- a/tests/pose_estimation_pytorch/post_processing/test_identity.py +++ b/tests/pose_estimation_pytorch/post_processing/test_identity.py @@ -8,7 +8,8 @@ # # Licensed under GNU Lesser General Public License v3.0 # -""" Tests identity matching """ +"""Tests identity matching""" + import numpy as np import pytest diff --git a/tests/pose_estimation_pytorch/post_processing/test_postprocessing_nms.py b/tests/pose_estimation_pytorch/post_processing/test_postprocessing_nms.py index bc5acd99e0..e6e98c7e6f 100644 --- a/tests/pose_estimation_pytorch/post_processing/test_postprocessing_nms.py +++ b/tests/pose_estimation_pytorch/post_processing/test_postprocessing_nms.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Tests pose NMS""" + import numpy as np import pytest @@ -94,7 +95,7 @@ 0.1, [False, True, True], # two valid poses, far apart, sorted by score, one suppressed ), - ] + ], ) def test_oks_nms_post_processing(poses, score_threshold, expected_kept): """Tests pose NMS""" diff --git a/tests/pose_estimation_pytorch/runners/bottum_up.py b/tests/pose_estimation_pytorch/runners/bottum_up.py index 821c11422d..bd9e10e50c 100644 --- a/tests/pose_estimation_pytorch/runners/bottum_up.py +++ b/tests/pose_estimation_pytorch/runners/bottum_up.py @@ -8,7 +8,8 @@ # # Licensed under GNU Lesser General Public License v3.0 # -""" Tests for the bottom-up pytorch runner """ +"""Tests for the bottom-up pytorch runner""" + from pathlib import Path from typing import Dict, Any @@ -69,9 +70,7 @@ def test_build_bottom_up_runner( criterion = WeightedAggregateLoss(head_criterions) get_optimizer = getattr(torch.optim, pytorch_cfg["optimizer"]["type"]) - optimizer = get_optimizer( - params=pose_model.parameters(), **pytorch_cfg["optimizer"]["params"] - ) + optimizer = get_optimizer(params=pose_model.parameters(), **pytorch_cfg["optimizer"]["params"]) predictor = PREDICTORS.build(dict(pytorch_cfg["model"]["predictor"])) @@ -79,12 +78,8 @@ def test_build_bottom_up_runner( if pytorch_cfg["scheduler"]["type"] == "LRListScheduler": _scheduler = LRListScheduler else: - _scheduler = getattr( - torch.optim.lr_scheduler, pytorch_cfg["scheduler"]["type"] - ) - scheduler = _scheduler( - optimizer=optimizer, **pytorch_cfg["scheduler"]["params"] - ) + _scheduler = getattr(torch.optim.lr_scheduler, pytorch_cfg["scheduler"]["type"]) + scheduler = _scheduler(optimizer=optimizer, **pytorch_cfg["scheduler"]["params"]) else: scheduler = None diff --git a/tests/pose_estimation_pytorch/runners/test_dynamic_cropper.py b/tests/pose_estimation_pytorch/runners/test_dynamic_cropper.py index a68107bf5e..2d90ecbba6 100644 --- a/tests/pose_estimation_pytorch/runners/test_dynamic_cropper.py +++ b/tests/pose_estimation_pytorch/runners/test_dynamic_cropper.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Tests the dynamic cropper""" + import pytest import numpy as np @@ -86,22 +87,23 @@ def test_dynamic_cropper_does_nothing_with_low_quality(threshold: float): [[float("nan"), float("nan"), float("nan")]], [[float("nan"), float("nan"), float("nan")]], ], - 0.15, 10, [0, 0, 64, 64] + 0.15, + 10, + [0, 0, 64, 64], ), ( [ [[20, 30, 0.8], [5, 12, 0.2]], [[40, 10, 0.2], [35, 15, 0.79]], ], - 0.15, 5, [0, 5, 45, 35] + 0.15, + 5, + [0, 5, 45, 35], ), ], ) def test_dynamic_cropper_basic_crop( - pose: list[list[float]], - threshold: float, - margin: int, - expected_crop: tuple[int, int, int, int] + pose: list[list[float]], threshold: float, margin: int, expected_crop: tuple[int, int, int, int] ) -> None: x0, y0, x1, y1 = expected_crop crop_w, crop_h = x1 - x0, y1 - y0 diff --git a/tests/pose_estimation_pytorch/runners/test_filtered_detector_inference_runner.py b/tests/pose_estimation_pytorch/runners/test_filtered_detector_inference_runner.py index 5d52050026..daf5218a74 100644 --- a/tests/pose_estimation_pytorch/runners/test_filtered_detector_inference_runner.py +++ b/tests/pose_estimation_pytorch/runners/test_filtered_detector_inference_runner.py @@ -16,7 +16,6 @@ def test_torchvision_detector(): """Test that the torchvision detector works with superanimal_humanbody""" for detector_name in TORCHVISION_DETECTORS: - # Load the superanimal_humanbody config superanimal_config = load_super_animal_config( super_animal="superanimal_humanbody", @@ -47,9 +46,7 @@ def test_torchvision_detector(): ) print("Filtered detector runner created successfully!") - print( - "\n✅ All tests passed! The torchvision detector integration is working correctly." - ) + print("\n✅ All tests passed! The torchvision detector integration is working correctly.") return True @@ -57,10 +54,6 @@ def test_torchvision_detector(): print("Testing superanimal_humanbody with torchvision detector...") success = test_torchvision_detector() if success: - print( - "\n✅ Test passed! The torchvision detector works with superanimal_humanbody" - ) + print("\n✅ Test passed! The torchvision detector works with superanimal_humanbody") else: - print( - "\n❌ Test failed! There's an issue with the torchvision detector integration" - ) + print("\n❌ Test failed! There's an issue with the torchvision detector integration") diff --git a/tests/pose_estimation_pytorch/runners/test_logger.py b/tests/pose_estimation_pytorch/runners/test_logger.py index d34decec13..fefe787f37 100644 --- a/tests/pose_estimation_pytorch/runners/test_logger.py +++ b/tests/pose_estimation_pytorch/runners/test_logger.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Tests loggers""" + from pathlib import Path from typing import Any diff --git a/tests/pose_estimation_pytorch/runners/test_runners.py b/tests/pose_estimation_pytorch/runners/test_runners.py index 3f2fc2e3da..c3afa93db5 100644 --- a/tests/pose_estimation_pytorch/runners/test_runners.py +++ b/tests/pose_estimation_pytorch/runners/test_runners.py @@ -35,6 +35,4 @@ def test_load_snapshot_weights_only_error(tmpdir_factory): runners.set_load_weights_only(False) with pytest.raises(pickle.UnpicklingError): - runners.Runner.load_snapshot( - snapshot_path, device="cpu", model=Mock(), weights_only=True - ) + runners.Runner.load_snapshot(snapshot_path, device="cpu", model=Mock(), weights_only=True) diff --git a/tests/pose_estimation_pytorch/runners/test_runners_inference.py b/tests/pose_estimation_pytorch/runners/test_runners_inference.py index e5b64f4e72..431b851d12 100644 --- a/tests/pose_estimation_pytorch/runners/test_runners_inference.py +++ b/tests/pose_estimation_pytorch/runners/test_runners_inference.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Tests inference runners""" + from unittest.mock import Mock, patch import numpy as np @@ -37,9 +38,7 @@ def test_load_weights_only_with_build_training_runner(task: Task, weights_only: ) if weights_only is None: weights_only = get_load_weights_only() - load.assert_called_once_with( - snapshot, map_location="cpu", weights_only=weights_only - ) + load.assert_called_once_with(snapshot, map_location="cpu", weights_only=weights_only) class MockInferenceRunner(inference.InferenceRunner): @@ -114,10 +113,7 @@ def test_mock_top_down(batch_size, detections_per_image): detections = np.zeros((0, 3, 1, 1)) # random shape when no detections else: detections = np.concatenate( - [ - (1_000_000 * (index + 1) + i) * np.ones((1, 3, h, w)) - for i in range(num_detections) - ], + [(1_000_000 * (index + 1) + i) * np.ones((1, 3, h, w)) for i in range(num_detections)], axis=0, ) @@ -181,7 +177,8 @@ def test_dynamic_pose_inference_calls_dynamic(): assert len(updated_pose) == 1 np.testing.assert_allclose( - updated_pose[0]["bodypart"]["poses"], pose_batch_updated[0].cpu().numpy(), + updated_pose[0]["bodypart"]["poses"], + pose_batch_updated[0].cpu().numpy(), ) diff --git a/tests/pose_estimation_pytorch/runners/test_runners_train.py b/tests/pose_estimation_pytorch/runners/test_runners_train.py index 0fff6e671d..1787a63e62 100644 --- a/tests/pose_estimation_pytorch/runners/test_runners_train.py +++ b/tests/pose_estimation_pytorch/runners/test_runners_train.py @@ -42,9 +42,7 @@ def test_load_weights_only_with_build_training_runner(task: Task, weights_only: device="cpu", snapshot_path="snapshot.pt", ) - load.assert_called_once_with( - "snapshot.pt", map_location="cpu", weights_only=weights_only - ) + load.assert_called_once_with("snapshot.pt", map_location="cpu", weights_only=weights_only) @dataclass @@ -104,9 +102,7 @@ def test_load_head_weights(tmp_path_factory, load_head_weights): ) original_state_dict = model.state_dict() - zero_state_dict = { - k: torch.zeros_like(v) for k, v in original_state_dict.items() - } + zero_state_dict = {k: torch.zeros_like(v) for k, v in original_state_dict.items()} load = Mock() load.return_value = dict(model=zero_state_dict) @@ -198,9 +194,7 @@ def test_resuming_training_scheduler_every_epoch( [expected_lr], # trains for 1 epoch snapshot_to_load=snapshot_to_load, ) - snapshot_to_load = dict( - metadata=dict(epoch=epoch + 1), scheduler=runner.scheduler.state_dict() - ) + snapshot_to_load = dict(metadata=dict(epoch=epoch + 1), scheduler=runner.scheduler.state_dict()) @patch("deeplabcut.pose_estimation_pytorch.runners.train.CSVLogger", Mock()) @@ -243,9 +237,7 @@ def test_resuming_training_scheduler_every_epoch( ), ], ) -def test_resuming_training_with_no_scheduler_state( - runner_cls, test_cfg: SchedulerTestConfig, resume_epoch: int -): +def test_resuming_training_with_no_scheduler_state(runner_cls, test_cfg: SchedulerTestConfig, resume_epoch: int): """ Without a scheduler config, there is no way to set the initial LR. All we can do is set the last_epoch value, and adjust correctly at milestones going forward. diff --git a/tests/pose_estimation_pytorch/runners/test_schedulers.py b/tests/pose_estimation_pytorch/runners/test_schedulers.py index d3b17fa16c..9b4b3351ce 100644 --- a/tests/pose_estimation_pytorch/runners/test_schedulers.py +++ b/tests/pose_estimation_pytorch/runners/test_schedulers.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Tests building schedulers from config""" + import random from dataclasses import dataclass @@ -38,10 +39,7 @@ def generate_random_lr_list(num_floats: int): @pytest.mark.parametrize( "milestones, lr_list", - [ - ([10, 430], [[0.05], [0.005]]), - (list(sorted(random.sample(range(0, 999), 2))), generate_random_lr_list(2)) - ] + [([10, 430], [[0.05], [0.005]]), (list(sorted(random.sample(range(0, 999), 2))), generate_random_lr_list(2))], ) def test_scheduler(milestones, lr_list): """Testing schedulers.py. @@ -94,10 +92,7 @@ class SchedulerTestConfig: TEST_SCHEDULERS = [ SchedulerTestConfig( - cfg=dict( - type="LRListScheduler", - params=dict(milestones=[2, 5], lr_list=[[0.5], [0.1]]) - ), + cfg=dict(type="LRListScheduler", params=dict(milestones=[2, 5], lr_list=[[0.5], [0.1]])), init_lr=1.0, expected_lrs=[1.0, 1.0, 0.5, 0.5, 0.5, 0.1, 0.1, 0.1], ), @@ -228,8 +223,16 @@ def test_two_stage_training(test_cfg: SchedulerTestConfig, middle_epoch: int) -> ), start_lr=1.0, expected_lrs=[ - [0.1], [0.1], [1.0], [1.0], [1.0], # ConstantLR - [1.0], [1.0], [0.1], [0.1], [0.01], # StepLR + [0.1], + [0.1], + [1.0], + [1.0], + [1.0], # ConstantLR + [1.0], + [1.0], + [0.1], + [0.1], + [0.01], # StepLR ], ), ], diff --git a/tests/pose_estimation_pytorch/runners/test_shelving.py b/tests/pose_estimation_pytorch/runners/test_shelving.py index 5ecb071b17..0697c048f5 100644 --- a/tests/pose_estimation_pytorch/runners/test_shelving.py +++ b/tests/pose_estimation_pytorch/runners/test_shelving.py @@ -9,6 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # """Tests for ShelfWriter / ShelfReader""" + from __future__ import annotations import numpy as np @@ -122,9 +123,7 @@ def test_metadata_nframes_updated_on_close(tmp_path): def test_unique_bodyparts_appended(tmp_path): num_assemblies, num_bpts, num_unique = 2, 3, 1 bp = _make_bodyparts(num_assemblies, num_bpts) - ubp = np.random.default_rng(1).random((num_assemblies, num_unique, 3)).astype( - np.float32 - ) + ubp = np.random.default_rng(1).random((num_assemblies, num_unique, 3)).astype(np.float32) writer = ShelfWriter(POSE_CFG, tmp_path / "shelf", num_frames=5) writer.open() @@ -145,9 +144,7 @@ def test_unique_bodyparts_appended(tmp_path): def test_identity_scores_stored(tmp_path): num_assemblies, num_bpts, num_individuals = 2, 3, 2 bp = _make_bodyparts(num_assemblies, num_bpts) - ids = np.random.default_rng(2).random( - (num_assemblies, num_bpts, num_individuals) - ).astype(np.float32) + ids = np.random.default_rng(2).random((num_assemblies, num_bpts, num_individuals)).astype(np.float32) writer = ShelfWriter(POSE_CFG, tmp_path / "shelf", num_frames=5) writer.open() diff --git a/tests/pose_estimation_pytorch/runners/test_task.py b/tests/pose_estimation_pytorch/runners/test_task.py index 7a0a9730c2..1d7ef3395f 100644 --- a/tests/pose_estimation_pytorch/runners/test_task.py +++ b/tests/pose_estimation_pytorch/runners/test_task.py @@ -8,7 +8,8 @@ # # Licensed under GNU Lesser General Public License v3.0 # -""" Tests the Task enum """ +"""Tests the Task enum""" + import pytest from deeplabcut.pose_estimation_pytorch.task import Task diff --git a/tests/test_auxfun_models.py b/tests/test_auxfun_models.py index 35213d35e0..5ed026c837 100644 --- a/tests/test_auxfun_models.py +++ b/tests/test_auxfun_models.py @@ -21,20 +21,14 @@ class CheckForWeightsTestCase(unittest.TestCase): def test_filepaths_for_modeltypes(self): with TemporaryDirectory() as tmpdir: - with patch( - "deeplabcut.utils.auxfun_models.download_weights" - ) as mocked_download: + with patch("deeplabcut.utils.auxfun_models.download_weights") as mocked_download: for modeltype, expected_path in MODELTYPE_FILEPATH_MAP.items(): actual_path = check_for_weights(modeltype, Path(tmpdir)) self.assertIn(str(expected_path), actual_path) if "efficientnet" in modeltype: - mocked_download.assert_called_with( - modeltype, tmpdir / expected_path.parent - ) + mocked_download.assert_called_with(modeltype, tmpdir / expected_path.parent) else: - mocked_download.assert_called_with( - modeltype, tmpdir / expected_path - ) + mocked_download.assert_called_with(modeltype, tmpdir / expected_path) def test_bad_modeltype(self): actual_path = check_for_weights("dummymodel", "nonexistentpath") diff --git a/tests/test_auxfun_multianimal.py b/tests/test_auxfun_multianimal.py index 1dbd67d2f6..91601d5927 100644 --- a/tests/test_auxfun_multianimal.py +++ b/tests/test_auxfun_multianimal.py @@ -44,9 +44,7 @@ def test_reorder_individuals_in_df(): individuals = df.columns.get_level_values("individuals").unique().to_list() # Generate a random permutation and reorder data. Ignore the unique bodypart - permutation_indices = random.sample( - range(len(individuals[:-1])), k=len(individuals[:-1]) - ) + permutation_indices = random.sample(range(len(individuals[:-1])), k=len(individuals[:-1])) permutation = [individuals[i] for i in permutation_indices] permutation.append("single") df_reordered = auxfun_multianimal.reorder_individuals_in_df(df, permutation) @@ -56,9 +54,7 @@ def test_reorder_individuals_in_df(): inverse_permutation_indices = np.argsort(permutation_indices).tolist() inverse_permutation = [individuals[i] for i in inverse_permutation_indices] inverse_permutation.append("single") - df_inverse_reordering = auxfun_multianimal.reorder_individuals_in_df( - df_reordered, inverse_permutation - ) + df_inverse_reordering = auxfun_multianimal.reorder_individuals_in_df(df_reordered, inverse_permutation) # Check pd.testing.assert_frame_equal(df, df_inverse_reordering) diff --git a/tests/test_auxiliaryfunctions.py b/tests/test_auxiliaryfunctions.py index 39f2fcd9c4..a7a24571b2 100644 --- a/tests/test_auxiliaryfunctions.py +++ b/tests/test_auxiliaryfunctions.py @@ -36,20 +36,14 @@ def _create_fake_file(filename): for ind, ext in enumerate(SUPPORTED_VIDEOS): # test if existing models are found: - assert auxiliaryfunctions.find_analyzed_data( - fake_folder, "video" + str(ind), SCORER - ) + assert auxiliaryfunctions.find_analyzed_data(fake_folder, "video" + str(ind), SCORER) # Test if nonexisting models are not found with pytest.raises(FileNotFoundError): - auxiliaryfunctions.find_analyzed_data( - fake_folder, "video" + str(ind), WRONG_SCORER - ) + auxiliaryfunctions.find_analyzed_data(fake_folder, "video" + str(ind), WRONG_SCORER) with pytest.raises(FileNotFoundError): - auxiliaryfunctions.find_analyzed_data( - fake_folder, "video" + str(ind), SCORER, filtered=True - ) + auxiliaryfunctions.find_analyzed_data(fake_folder, "video" + str(ind), SCORER, filtered=True) def test_get_list_of_videos(tmpdir_factory): @@ -165,21 +159,15 @@ def test_intersection_of_body_parts_and_ones_given_by_user( else: all_bodyparts = bodyparts - filtered_bpts = ( - auxiliaryfunctions.intersection_of_body_parts_and_ones_given_by_user( - cfg, comparisonbodyparts="all" - ) - ) + filtered_bpts = auxiliaryfunctions.intersection_of_body_parts_and_ones_given_by_user(cfg, comparisonbodyparts="all") print(all_bodyparts) print(filtered_bpts) assert len(all_bodyparts) == len(filtered_bpts) assert all([bpt in all_bodyparts for bpt in filtered_bpts]) - filtered_bpts = ( - auxiliaryfunctions.intersection_of_body_parts_and_ones_given_by_user( - cfg, - comparisonbodyparts=comparison_bpts, - ) + filtered_bpts = auxiliaryfunctions.intersection_of_body_parts_and_ones_given_by_user( + cfg, + comparisonbodyparts=comparison_bpts, ) print(filtered_bpts) assert len(expected_bpts) == len(filtered_bpts) diff --git a/tests/test_crossvalutils.py b/tests/test_crossvalutils.py index 3dcf1dcbe1..6cecdca53b 100644 --- a/tests/test_crossvalutils.py +++ b/tests/test_crossvalutils.py @@ -20,9 +20,7 @@ def test_get_n_best_paf_graphs(evaluation_data_and_metadata): data, metadata = evaluation_data_and_metadata params = crossvalutils._set_up_evaluation(data) n_graphs = 5 - paf_inds, dict_ = crossvalutils._get_n_best_paf_graphs( - data, metadata, params["paf_graph"], n_graphs=n_graphs - ) + paf_inds, dict_ = crossvalutils._get_n_best_paf_graphs(data, metadata, params["paf_graph"], n_graphs=n_graphs) assert len(paf_inds) == n_graphs assert len(dict_) == len(params["paf_graph"]) assert len(paf_inds[0]) == 11 @@ -66,9 +64,7 @@ def test_benchmark_paf_graphs(evaluation_data_and_metadata): ], } inference_cfg = {"topktoretain": 3, "pcutoff": 0.1, "pafthreshold": 0.1} - results = crossvalutils._benchmark_paf_graphs( - cfg, inference_cfg, data, [BEST_GRAPH] - ) + results = crossvalutils._benchmark_paf_graphs(cfg, inference_cfg, data, [BEST_GRAPH]) all_scores = results[0] assert len(all_scores) == 1 assert all_scores[0][1] == BEST_GRAPH diff --git a/tests/test_dataset_augmentation.py b/tests/test_dataset_augmentation.py index bd2f269c6e..5350791ea0 100644 --- a/tests/test_dataset_augmentation.py +++ b/tests/test_dataset_augmentation.py @@ -101,9 +101,7 @@ def test_keypoint_horizontal_flip( keypoints_aug = aug( images=[sample_image], keypoints=[sample_keypoints], - )[ - 1 - ][0] + )[1][0] temp = keypoints_aug.reshape((3, 12, 2)) for pair in pairs: temp[:, pair] = temp[:, pair[::-1]] @@ -127,9 +125,7 @@ def test_keypoint_horizontal_flip_with_nans( keypoints_aug = aug( images=[sample_image], keypoints=[sample_keypoints], - )[ - 1 - ][0] + )[1][0] temp = keypoints_aug.reshape((3, 12, 2)) for pair in pairs: temp[:, pair] = temp[:, pair[::-1]] diff --git a/tests/test_evaluate.py b/tests/test_evaluate.py index 0edf27feac..fbf50b6a86 100644 --- a/tests/test_evaluate.py +++ b/tests/test_evaluate.py @@ -43,9 +43,7 @@ def make_multi_animal_rmse_df( names=["scorer", "individuals", "bodyparts"], ) if error_data is None: - error_data = np.ones( - (len(train_indices) + len(test_indices), len(individuals) * len(bodyparts)) - ) + error_data = np.ones((len(train_indices) + len(test_indices), len(individuals) * len(bodyparts))) return pd.DataFrame(error_data, columns=columns) diff --git a/tests/test_frame_selection_tools.py b/tests/test_frame_selection_tools.py index ddee2346c7..6746a9bb65 100644 --- a/tests/test_frame_selection_tools.py +++ b/tests/test_frame_selection_tools.py @@ -8,7 +8,8 @@ # # Licensed under GNU Lesser General Public License v3.0 # -""" Tests for frame selection tools """ +"""Tests for frame selection tools""" + import math from unittest.mock import Mock import pytest diff --git a/tests/test_inferenceutils.py b/tests/test_inferenceutils.py index 5b4c126f58..1684095c41 100644 --- a/tests/test_inferenceutils.py +++ b/tests/test_inferenceutils.py @@ -36,9 +36,7 @@ def test_calc_object_keypoint_similarity(real_assemblies): xy1 = real_assemblies[0][0].xy xy2 = real_assemblies[0][1].xy assert inferenceutils.calc_object_keypoint_similarity(xy1, xy1, sigma) == 1 - assert np.isclose( - inferenceutils.calc_object_keypoint_similarity(xy1, xy2, sigma), 0 - ) + assert np.isclose(inferenceutils.calc_object_keypoint_similarity(xy1, xy2, sigma), 0) xy3 = xy1.copy() xy3[: len(xy3) // 2] = np.nan assert inferenceutils.calc_object_keypoint_similarity(xy3, xy1, sigma) == 0.5 @@ -51,19 +49,12 @@ def test_calc_object_keypoint_similarity(real_assemblies): symmetric_pair = [0, 11] xy4[symmetric_pair] = xy4[symmetric_pair[::-1]] assert inferenceutils.calc_object_keypoint_similarity(xy1, xy4, sigma) != 1 - assert ( - inferenceutils.calc_object_keypoint_similarity( - xy1, xy4, sigma, symmetric_kpts=[symmetric_pair] - ) - == 1 - ) + assert inferenceutils.calc_object_keypoint_similarity(xy1, xy4, sigma, symmetric_kpts=[symmetric_pair]) == 1 def test_match_assemblies(real_assemblies): assemblies = real_assemblies[0] - num_gt, matches = inferenceutils.match_assemblies( - assemblies, assemblies[::-1], 0.01 - ) + num_gt, matches = inferenceutils.match_assemblies(assemblies, assemblies[::-1], 0.01) assert len(assemblies) == len(matches) for m in matches: assert m.prediction is m.ground_truth @@ -78,9 +69,7 @@ def test_evaluate_assemblies(real_assemblies): assemblies = {i: real_assemblies[i] for i in range(3)} n_thresholds = 5 thresholds = np.linspace(0.5, 0.95, n_thresholds) - dict_ = inferenceutils.evaluate_assembly( - assemblies, assemblies, oks_thresholds=thresholds - ) + dict_ = inferenceutils.evaluate_assembly(assemblies, assemblies, oks_thresholds=thresholds) assert dict_["mAP"] == dict_["mAR"] == 1 assert len(dict_["precisions"]) == len(dict_["recalls"]) == n_thresholds assert dict_["precisions"].shape[1] == 101 @@ -172,9 +161,7 @@ def test_assembler(tmpdir_factory, real_assemblies): ass.assemble() assert not ass.unique assert len(ass.assemblies) == len(real_assemblies) - assert sum(1 for a in ass.assemblies.values() for _ in a) == sum( - 1 for a in real_assemblies.values() for _ in a - ) + assert sum(1 for a in ass.assemblies.values() for _ in a) == sum(1 for a in real_assemblies.values() for _ in a) output_dir = tmpdir_factory.mktemp("data") ass.to_h5(output_dir.join("fake.h5")) @@ -223,15 +210,9 @@ def test_assembler_with_unique_bodypart(real_assemblies_montblanc): ass.assemble(chunk_size=0) assert len(ass.assemblies) == len(real_assemblies_montblanc[0]) assert len(ass.unique) == len(real_assemblies_montblanc[1]) - assemblies = np.concatenate( - [ass.xy for assemblies in ass.assemblies.values() for ass in assemblies] - ) + assemblies = np.concatenate([ass.xy for assemblies in ass.assemblies.values() for ass in assemblies]) assemblies_gt = np.concatenate( - [ - ass.xy - for assemblies in real_assemblies_montblanc[0].values() - for ass in assemblies - ] + [ass.xy for assemblies in real_assemblies_montblanc[0].values() for ass in assemblies] ) np.testing.assert_equal(assemblies, assemblies_gt) @@ -270,9 +251,7 @@ def test_assembler_with_identity(tmpdir_factory, real_assemblies): ass.assemble() assert not ass.unique assert len(ass.assemblies) == len(real_assemblies) - assert sum(1 for a in ass.assemblies.values() for _ in a) == sum( - 1 for a in real_assemblies.values() for _ in a - ) + assert sum(1 for a in ass.assemblies.values() for _ in a) == sum(1 for a in real_assemblies.values() for _ in a) assert all(np.all(_.data[:, -1] != -1) for a in ass.assemblies.values() for _ in a) # Test now with identity only and ensure assemblies diff --git a/tests/test_pose_multianimal_imgaug.py b/tests/test_pose_multianimal_imgaug.py index 85008805df..5c7c8bf4fd 100644 --- a/tests/test_pose_multianimal_imgaug.py +++ b/tests/test_pose_multianimal_imgaug.py @@ -67,16 +67,8 @@ def test_get_batch(ma_dataset): for batch_size in 1, 4, 8, 16: ma_dataset.batch_size = batch_size batch_images, joint_ids, batch_joints, data_items = ma_dataset.get_batch() - assert ( - len(batch_images) - == len(joint_ids) - == len(batch_joints) - == len(data_items) - == batch_size - ) - for data_item, joint_id, batch_joint in zip( - data_items, joint_ids, batch_joints - ): + assert len(batch_images) == len(joint_ids) == len(batch_joints) == len(data_items) == batch_size + for data_item, joint_id, batch_joint in zip(data_items, joint_ids, batch_joints): assert len(data_item.joints) == len(joint_id) assert len(batch_joint) == len(np.concatenate(joint_id)) start = 0 @@ -102,19 +94,11 @@ def test_get_targetmaps(ma_dataset, num_idchannel): scale = np.mean(target_size / ma_dataset.default_size) maps = ma_dataset.get_targetmaps_update(*batch, sm_size, scale) assert all(len(map_) == ma_dataset.batch_size for map_ in maps.values()) - assert ( - maps[Batch.part_score_targets][0].shape - == maps[Batch.part_score_weights][0].shape - ) - assert ( - maps[Batch.part_score_targets][0].shape[2] - == ma_dataset.cfg["num_joints"] + num_idchannel - ) + assert maps[Batch.part_score_targets][0].shape == maps[Batch.part_score_weights][0].shape + assert maps[Batch.part_score_targets][0].shape[2] == ma_dataset.cfg["num_joints"] + num_idchannel assert maps[Batch.locref_targets][0].shape == maps[Batch.locref_mask][0].shape assert maps[Batch.locref_targets][0].shape[2] == 2 * ma_dataset.cfg["num_joints"] - assert ( - maps[Batch.pairwise_targets][0].shape == maps[Batch.pairwise_targets][0].shape - ) + assert maps[Batch.pairwise_targets][0].shape == maps[Batch.pairwise_targets][0].shape assert maps[Batch.pairwise_targets][0].shape[2] == 2 * ma_dataset.cfg["num_limbs"] diff --git a/tests/test_predict_multianimal.py b/tests/test_predict_multianimal.py index eb9bbd7d16..85582a0275 100644 --- a/tests/test_predict_multianimal.py +++ b/tests/test_predict_multianimal.py @@ -66,14 +66,10 @@ def test_association_costs(model_outputs, ground_truth_detections): costs_pred = preds["costs"] assert len(costs_pred) == len(costs_gt) eq = [ - np.array_equal(np.argmax(v["m1"], axis=0), np.argmax(costs_gt[k]["m1"], axis=0)) - for k, v in costs_pred.items() + np.array_equal(np.argmax(v["m1"], axis=0), np.argmax(costs_gt[k]["m1"], axis=0)) for k, v in costs_pred.items() ] assert sum(eq) == 60 # 6 arrays are unequal as cost computation was corrected - assert all( - np.allclose(v["distance"], costs_gt[k]["distance"], atol=1.5) - for k, v in costs_pred.items() - ) + assert all(np.allclose(v["distance"], costs_gt[k]["distance"], atol=1.5) for k, v in costs_pred.items()) def test_compute_peaks_and_costs_no_graph(model_outputs): diff --git a/tests/test_trackingutils.py b/tests/test_trackingutils.py index 984fcc2a76..7c7f51459d 100644 --- a/tests/test_trackingutils.py +++ b/tests/test_trackingutils.py @@ -21,9 +21,7 @@ def ellipse(): def test_ellipse(ellipse): assert ellipse.aspect_ratio == 2 - np.testing.assert_equal( - ellipse.contains_points(np.asarray([[0, 0], [10, 10]])), [True, False] - ) + np.testing.assert_equal(ellipse.contains_points(np.asarray([[0, 0], [10, 10]])), [True, False]) def test_ellipse_similarity(ellipse): @@ -73,12 +71,8 @@ def test_tracking_ellipse(real_assemblies, real_tracklets): trackers = mot_tracker.track(animals[..., :2]) trackingutils.fill_tracklets(tracklets, trackers, animals, ind) assert len(tracklets) == len(tracklets_ref) - assert [len(tracklet) for tracklet in tracklets.values()] == [ - len(tracklet) for tracklet in tracklets_ref.values() - ] - assert all( - t.shape[1] == 4 for tracklet in tracklets.values() for t in tracklet.values() - ) + assert [len(tracklet) for tracklet in tracklets.values()] == [len(tracklet) for tracklet in tracklets_ref.values()] + assert all(t.shape[1] == 4 for tracklet in tracklets.values() for t in tracklet.values()) def test_box_tracker(): @@ -105,12 +99,8 @@ def test_tracking_box(real_assemblies, real_tracklets): trackers = mot_tracker.track(bboxes) trackingutils.fill_tracklets(tracklets, trackers, animals, ind) assert len(tracklets) == len(tracklets_ref) - assert [len(tracklet) for tracklet in tracklets.values()] == [ - len(tracklet) for tracklet in tracklets_ref.values() - ] - assert all( - t.shape[1] == 4 for tracklet in tracklets.values() for t in tracklet.values() - ) + assert [len(tracklet) for tracklet in tracklets.values()] == [len(tracklet) for tracklet in tracklets_ref.values()] + assert all(t.shape[1] == 4 for tracklet in tracklets.values() for t in tracklet.values()) def test_tracking_montblanc( @@ -127,9 +117,7 @@ def test_tracking_montblanc( trackers = mot_tracker.track(animals[..., :2]) trackingutils.fill_tracklets(tracklets, trackers, animals, ind) assert len(tracklets) == len(tracklets_ref) - assert [len(tracklet) for tracklet in tracklets.values()] == [ - len(tracklet) for tracklet in tracklets_ref.values() - ] + assert [len(tracklet) for tracklet in tracklets.values()] == [len(tracklet) for tracklet in tracklets_ref.values()] for k, assemblies in tracklets.items(): ref = tracklets_ref[k] for ind, data in assemblies.items(): @@ -140,12 +128,8 @@ def test_tracking_montblanc( def test_calc_bboxes_from_keypoints(): # Test bounding box from a single keypoint xy = np.asarray([[[0, 0, 1]]]) - np.testing.assert_equal( - trackingutils.calc_bboxes_from_keypoints(xy, 10), [[-10, -10, 10, 10, 1]] - ) - np.testing.assert_equal( - trackingutils.calc_bboxes_from_keypoints(xy, 20, 10), [[-10, -20, 30, 20, 1]] - ) + np.testing.assert_equal(trackingutils.calc_bboxes_from_keypoints(xy, 10), [[-10, -10, 10, 10, 1]]) + np.testing.assert_equal(trackingutils.calc_bboxes_from_keypoints(xy, 20, 10), [[-10, -20, 30, 20, 1]]) width = 200 height = width * 2 @@ -160,9 +144,7 @@ def test_calc_bboxes_from_keypoints(): slack = 20 bboxes = trackingutils.calc_bboxes_from_keypoints(xyp, slack=slack) - np.testing.assert_equal( - bboxes, [[-slack, -slack, width + slack, height + slack, 0.5]] - ) + np.testing.assert_equal(bboxes, [[-slack, -slack, width + slack, height + slack, 0.5]]) offset = 50 bboxes = trackingutils.calc_bboxes_from_keypoints(xyp, offset=offset) diff --git a/tests/test_trainingsetmanipulation.py b/tests/test_trainingsetmanipulation.py index febe3568a4..97a8caf69a 100644 --- a/tests/test_trainingsetmanipulation.py +++ b/tests/test_trainingsetmanipulation.py @@ -62,23 +62,16 @@ def test_format_training_data(monkeypatch): "read_image_shape_fast", lambda _: fake_shape, ) - df = pd.read_hdf(os.path.join(TEST_DATA_DIR, "trimouse_calib.h5")).xs( - "mus1", level="individuals", axis=1 - ) + df = pd.read_hdf(os.path.join(TEST_DATA_DIR, "trimouse_calib.h5")).xs("mus1", level="individuals", axis=1) guarantee_multiindex_rows(df) train_inds = list(range(10)) _, data = format_training_data(df, train_inds, 12, "") assert len(data) == len(train_inds) # Check data comprise path, shape, and xy coordinates assert all(len(d) == 3 for d in data) - assert all( - (d[0].size == 3 and d[0].dtype.char == "U" and d[0][0, -1].endswith(".png")) - for d in data - ) + assert all((d[0].size == 3 and d[0].dtype.char == "U" and d[0][0, -1].endswith(".png")) for d in data) assert all(np.all(d[1] == np.array(fake_shape)[None]) for d in data) - assert all( - (d[2][0, 0].shape[1] == 3 and d[2][0, 0].dtype == np.int64) for d in data - ) + assert all((d[2][0, 0].shape[1] == 3 and d[2][0, 0].dtype == np.int64) for d in data) def test_format_multianimal_training_data(monkeypatch): @@ -97,11 +90,7 @@ def test_format_multianimal_training_data(monkeypatch): assert all(isinstance(d, dict) for d in data) assert all(len(d["image"]) == 3 for d in data) assert all(np.all(d["size"] == np.array(fake_shape)) for d in data) - assert all( - (xy.shape[1] == 3 and np.isfinite(xy).all()) - for d in data - for xy in d["joints"].values() - ) + assert all((xy.shape[1] == 3 and np.isfinite(xy).all()) for d in data for xy in d["joints"].values()) @pytest.mark.parametrize( diff --git a/tests/test_triangulation.py b/tests/test_triangulation.py index 0fcf2b058c..02bf503447 100644 --- a/tests/test_triangulation.py +++ b/tests/test_triangulation.py @@ -48,9 +48,7 @@ def test_undistort_views(n_view_pairs, is_multi, stereo_params): df = df.xs("bird1", level="individuals", axis=1) view_pairs = [(df, df) for _ in range(n_view_pairs)] - cam_params = { - f"camera-1-camera-{i}": stereo_params for i in range(2, n_view_pairs + 2) - } + cam_params = {f"camera-1-camera-{i}": stereo_params for i in range(2, n_view_pairs + 2)} dfs = triangulation._undistort_views(view_pairs, cam_params) assert len(dfs) == n_view_pairs assert all(len(pair) == 2 for pair in dfs) diff --git a/tests/test_video.py b/tests/test_video.py index 02b70e828f..442c3d0b62 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -57,9 +57,7 @@ def test_reader_wrong_fps(video_clip): def test_reader_duration(video_clip): - assert video_clip.calc_duration() == pytest.approx( - video_clip.calc_duration(robust=False), abs=0.01 - ) + assert video_clip.calc_duration() == pytest.approx(video_clip.calc_duration(robust=False), abs=0.01) def test_reader_set_frame(video_clip): @@ -93,9 +91,7 @@ def test_writer_bbox(video_clip): assert video_clip.get_bbox(relative=True) == (0, 1, 0, 1) -@pytest.mark.parametrize( - "start, end", [(0, 10), ("0:0", "0:10"), ("00:00:00", "00:00:10")] -) +@pytest.mark.parametrize("start, end", [(0, 10), ("0:0", "0:10"), ("00:00:00", "00:00:10")]) def test_writer_shorten_invalid_timestamps(video_clip, start, end): with pytest.raises(ValueError): video_clip.shorten(start, end) diff --git a/testscript_cli.py b/testscript_cli.py index 401a2cecd9..40b371878e 100644 --- a/testscript_cli.py +++ b/testscript_cli.py @@ -119,7 +119,10 @@ print("CREATING TRAININGSET") dlc.create_training_dataset( - path_config_file, net_type=net_type, augmenter_type=augmenter_type, engine=engine, + path_config_file, + net_type=net_type, + augmenter_type=augmenter_type, + engine=engine, ) print("TRAIN") From 9785d7af9a519cea54b9888778e09c47946a9ee4 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Mon, 16 Mar 2026 15:58:27 +0100 Subject: [PATCH 05/80] Update pre-commit hooks and add docformatter Bump pyproject-fmt to v2.18.1 and ruff-pre-commit to v0.15.6. Add docformatter (v1.7.7) with formatting args (--wrap-summaries/--wrap-descriptions=88, --in-place, --black). Keep ruff hooks (format + check) and existing validate-pyproject/trailing-whitespace hooks. --- .pre-commit-config.yaml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7e74fb5f0f..74e445484d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,18 +11,23 @@ repos: - id: trailing-whitespace - id: check-merge-conflict - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.15.2 + rev: v2.18.1 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject rev: v0.25 hooks: - id: validate-pyproject + - repo: https://github.com/PyCQA/docformatter + rev: v1.7.7 + hooks: + - id: docformatter + args: ["--wrap-summaries=88", "--wrap-descriptions=88", "--in-place", "--black"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.0 + rev: v0.15.6 hooks: # Run the formatter. - id: ruff-format # Run the linter. - id: ruff-check - args: [--fix,--unsafe-fixes] \ No newline at end of file + args: [--fix,--unsafe-fixes] From 3362a7c2370b61cb5dd48bbf821915f1ffa6e650 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Mon, 16 Mar 2026 15:59:18 +0100 Subject: [PATCH 06/80] Add Ruff JSON to Markdown report tool Introduce tools/ruff_report.py: a CLI script that runs `ruff` on given paths (using `--output-format=json --exit-zero`), parses the JSON results, and generates a readable Markdown report. The report groups issues by rule and file, includes a summary table, suggested triage order, table of contents, per-rule sections with detailed issue tables, and VS Code "Quick open" commands. The script also embeds short RULE_NOTES for common codes, accepts paths (default `.`) and `--output` (default `ruff-report.md`), and writes the generated Markdown to disk. --- tools/ruff_report.py | 151 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 tools/ruff_report.py diff --git a/tools/ruff_report.py b/tools/ruff_report.py new file mode 100644 index 0000000000..e66934a7a9 --- /dev/null +++ b/tools/ruff_report.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +"""Generate a readable Markdown report from Ruff JSON output. + +Usage: + python generate_ruff_report.py . --output ruff-report.md + python generate_ruff_report.py src tests --output lint/ruff-report.md +""" + +from __future__ import annotations + +import argparse +import collections +import json +import os +import subprocess +import sys +from collections.abc import Iterable +from pathlib import Path + +RULE_NOTES = { + "F401": "Unused import. Usually safe to delete; verify imports with side effects.", + "E501": "Line too long. Prefer wrapping expressions, splitting long strings/comments, or extracting variables.", + "E402": "Module import not at top of file. Move imports above executable code if possible.", + "F403": "`from x import *` makes names unclear. Replace with explicit imports.", + "F405": "Likely consequence of `import *`. Import the name explicitly.", + "F821": "Undefined name. Usually a real bug or missing import.", + "E722": "Bare `except:`. Catch `Exception` or a narrower exception type.", + "B904": "Inside `except`, use `raise ... from e` to preserve exception chaining.", + "B007": "Unused loop variable. Rename to `_` or use it.", + "UP031": "Old `%` formatting. Convert to f-strings or `.format()` where appropriate.", + "E721": "Avoid direct `type(x) == Y`; prefer `isinstance(x, Y)`.", + "B008": "Function call in default arg. Use `None` + initialize inside the function.", + "B023": "Function closes over loop variable. Bind it via default arg or helper.", + "B024": "ABC without abstract method. Add `@abstractmethod` or remove ABC intent.", + "F811": "Redefined while unused. Remove duplicate or rename.", + "B012": "Jump statement in `finally` can swallow exceptions. Restructure flow.", + "B016": "Raise an exception instance/class, not a literal.", + "B017": "Use a more specific exception with `assertRaises`.", + "B020": "Loop variable overrides iterator. Rename loop variables.", + "B027": "Empty method in ABC without abstract decorator. Add `@abstractmethod` or implement it.", +} + + +def run_ruff(paths: Iterable[str]) -> list[dict]: + cmd = ["ruff", "check", *paths, "--output-format=json", "--exit-zero"] + proc = subprocess.run(cmd, capture_output=True, text=True) + if proc.returncode not in (0, 1): + print(proc.stdout) + print(proc.stderr, file=sys.stderr) + raise SystemExit(f"Failed to run Ruff: {' '.join(cmd)}") + data = json.loads(proc.stdout or "[]") + if not isinstance(data, list): + raise SystemExit("Unexpected Ruff JSON output") + return data + + +def relpath(path: str) -> str: + try: + return os.path.relpath(path) + except Exception: + return path + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("paths", nargs="*", default=["."], help="Files/directories to scan") + parser.add_argument("--output", default="ruff-report.md", help="Markdown output path") + args = parser.parse_args() + + issues = run_ruff(args.paths) + + by_rule: dict[str, list[dict]] = collections.defaultdict(list) + for item in issues: + by_rule[item.get("code", "UNKNOWN")].append(item) + + out = Path(args.output) + out.parent.mkdir(parents=True, exist_ok=True) + + lines: list[str] = [] + lines.append("# Ruff manual-fix report\n") + lines.append(f"Generated from: `{', '.join(args.paths)}`\n") + lines.append(f"Total remaining issues: **{len(issues)}**\n") + + lines.append("## Summary\n") + lines.append("| Rule | Count | Note |") + lines.append("|---|---:|---|") + for rule, items in sorted(by_rule.items(), key=lambda kv: (-len(kv[1]), kv[0])): + note = RULE_NOTES.get(rule, "") + lines.append(f"| `{rule}` | {len(items)} | {note} |") + lines.append("") + + lines.append("## Suggested triage order\n") + preferred = ["F403", "F405", "F821", "E722", "B904", "E402", "F401", "E501"] + present = [r for r in preferred if r in by_rule] + if present: + for idx, rule in enumerate(present, 1): + lines.append(f"{idx}. `{rule}` — {RULE_NOTES.get(rule, '')}") + lines.append("") + + lines.append("## Table of contents by rule\n") + for rule, items in sorted(by_rule.items(), key=lambda kv: (-len(kv[1]), kv[0])): + anchor = rule.lower() + lines.append(f"- [{rule} ({len(items)})](#{anchor})") + lines.append("") + + for rule, items in sorted(by_rule.items(), key=lambda kv: (-len(kv[1]), kv[0])): + lines.append(f"## {rule}\n") + lines.append(f"Count: **{len(items)}** ") + if rule in RULE_NOTES: + lines.append(f"Hint: {RULE_NOTES[rule]} ") + lines.append("") + + file_groups: dict[str, list[dict]] = collections.defaultdict(list) + for item in items: + file_groups[relpath(item["filename"])].append(item) + + lines.append("### Files affected\n") + lines.append("| File | Count |") + lines.append("|---|---:|") + for filename, entries in sorted(file_groups.items(), key=lambda kv: (-len(kv[1]), kv[0])): + lines.append(f"| `{filename}` | {len(entries)} |") + lines.append("") + + lines.append("### Details\n") + for filename, entries in sorted(file_groups.items(), key=lambda kv: (-len(kv[1]), kv[0])): + lines.append(f"#### `{filename}` ({len(entries)})\n") + lines.append("| Line | Col | Message |") + lines.append("|---:|---:|---|") + for e in sorted( + entries, key=lambda x: (x.get("location", {}).get("row", 0), x.get("location", {}).get("column", 0)) + ): + loc = e.get("location", {}) + line = loc.get("row", "") + col = loc.get("column", "") + msg = (e.get("message", "") or "").replace("|", "\\|") + lines.append(f"| {line} | {col} | {msg} |") + lines.append("") + lines.append("Quick open commands:") + lines.append("") + lines.append("```powershell") + lines.append(f'code -g "{filename}:{entries[0].get("location", {}).get("row", 1)}"') + lines.append("```") + lines.append("") + + out.write_text("\n".join(lines), encoding="utf-8") + print(f"Wrote {out}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 6a140e8f6c03c48e9eec211f9c1b9b7d84397813 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 17 Mar 2026 14:16:33 +0100 Subject: [PATCH 07/80] Add tool to auto-fix E501 line-lengths Add tools/trim_lines.py: a command-line helper that finds files with Ruff E501 violations and attempts to reduce them using autopep8, then normalizes formatting with Ruff. It scans paths via `ruff check --output-format=json`, supports batching (50 files), a configurable `--line-length` (default 88), and a `--check` dry-run mode. The script prints invoked commands and outputs, runs autopep8 with selected fixes, then runs `ruff --fix --unsafe-fixes` and `ruff format`. Requires ruff and autopep8. --- tools/trim_lines.py | 95 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 tools/trim_lines.py diff --git a/tools/trim_lines.py b/tools/trim_lines.py new file mode 100644 index 0000000000..fd2d4bb82e --- /dev/null +++ b/tools/trim_lines.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Reduce Ruff E501 violations using autopep8, then normalize with Ruff. + +Usage: + python fix_e501_with_autopep8.py . --line-length 88 + python fix_e501_with_autopep8.py src tests --line-length 100 --check + +Requirements: + - ruff + - autopep8 +""" + +from __future__ import annotations + +import argparse +import json +import subprocess +import sys + + +def run(cmd: list[str], check: bool = True) -> subprocess.CompletedProcess: + print("+", " ".join(cmd)) + proc = subprocess.run(cmd, text=True, capture_output=True) + if proc.stdout: + print(proc.stdout) + if proc.stderr: + print(proc.stderr, file=sys.stderr) + if check and proc.returncode != 0: + raise SystemExit(proc.returncode) + return proc + + +def ruff_json(paths: list[str]) -> list[dict]: + proc = run(["ruff", "check", *paths, "--output-format=json", "--exit-zero"], check=False) + try: + data = json.loads(proc.stdout or "[]") + except json.JSONDecodeError as e: + raise SystemExit(f"Could not parse Ruff JSON: {e}") from e + if not isinstance(data, list): + raise SystemExit("Unexpected Ruff JSON output") + return data + + +def unique_e501_files(paths: list[str]) -> list[str]: + data = ruff_json(paths) + files = sorted({item["filename"] for item in data if item.get("code") == "E501"}) + return files + + +def chunked(items: list[str], size: int = 50): + for i in range(0, len(items), size): + yield items[i : i + size] + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("paths", nargs="*", default=["."], help="Files/directories to scan") + parser.add_argument("--line-length", type=int, default=88) + parser.add_argument("--check", action="store_true", help="Dry run; only show affected files") + args = parser.parse_args() + + files = unique_e501_files(args.paths) + if not files: + print("No E501 files found. Nothing to do.") + return 0 + + print(f"Found {len(files)} file(s) with E501.") + for f in files: + print(" -", f) + + if args.check: + return 0 + + for batch in chunked(files, 50): + run( + [ + "autopep8", + "--in-place", + "--aggressive", + f"--max-line-length={args.line_length}", + "--select=E501,W291,W292,W391", + *batch, + ] + ) + + run(["ruff", "check", *batch, "--fix", "--unsafe-fixes"], check=False) + run(["ruff", "format", *batch], check=False) + + after = len(unique_e501_files(args.paths)) + print(f"Remaining files with E501: {after}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 67eeb2b665501a0e908b4af1d580e29fd4bacc7b Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 17 Mar 2026 14:17:56 +0100 Subject: [PATCH 08/80] Run autopep8 script to fix errors automatically --- deeplabcut/__init__.py | 84 +- deeplabcut/benchmark/__init__.py | 4 +- deeplabcut/benchmark/base.py | 7 +- deeplabcut/benchmark/metrics.py | 7 +- deeplabcut/benchmark/utils.py | 2 +- deeplabcut/cli.py | 30 +- deeplabcut/compat.py | 4 +- deeplabcut/core/config.py | 4 +- deeplabcut/core/crossvalutils.py | 2 +- deeplabcut/core/inferenceutils.py | 5 +- deeplabcut/core/metrics/bbox.py | 5 +- deeplabcut/core/metrics/matching.py | 2 +- deeplabcut/core/visualization.py | 2 +- deeplabcut/core/weight_init.py | 14 +- deeplabcut/create_project/add.py | 12 +- deeplabcut/create_project/modelzoo.py | 14 +- deeplabcut/create_project/new.py | 35 +- deeplabcut/create_project/new_3d.py | 8 +- .../generate_training_dataset/__init__.py | 4 +- .../frame_extraction.py | 19 +- .../generate_training_dataset/metadata.py | 6 +- ...ple_individuals_trainingsetmanipulation.py | 21 +- .../trainingsetmanipulation.py | 44 +- deeplabcut/gui/components.py | 6 +- .../gui/displays/selected_shuffle_display.py | 5 +- .../gui/displays/shuffle_metadata_viewer.py | 2 +- deeplabcut/gui/launch_script.py | 8 +- deeplabcut/gui/tabs/analyze_videos.py | 16 +- deeplabcut/gui/tabs/create_project.py | 8 +- .../gui/tabs/create_training_dataset.py | 18 +- deeplabcut/gui/tabs/create_videos.py | 5 +- deeplabcut/gui/tabs/evaluate_network.py | 7 +- deeplabcut/gui/tabs/extract_frames.py | 11 +- deeplabcut/gui/tabs/extract_outlier_frames.py | 7 +- deeplabcut/gui/tabs/label_frames.py | 2 +- deeplabcut/gui/tabs/manage_project.py | 6 +- deeplabcut/gui/tabs/modelzoo.py | 24 +- deeplabcut/gui/tabs/open_project.py | 5 +- deeplabcut/gui/tabs/refine_tracklets.py | 10 +- deeplabcut/gui/tabs/train_network.py | 2 +- .../gui/tabs/unsupervised_id_tracking.py | 6 +- deeplabcut/gui/tabs/video_editor.py | 2 +- deeplabcut/gui/tracklet_toolbox.py | 18 +- deeplabcut/gui/utils.py | 7 +- deeplabcut/gui/widgets.py | 48 +- deeplabcut/gui/window.py | 45 +- deeplabcut/modelzoo/fmpose_3d/fmpose3d.py | 2 +- .../generalized_data_converter/__init__.py | 2 +- .../datasets/__init__.py | 6 +- .../datasets/base_dlc.py | 2 +- .../datasets/coco.py | 4 +- .../datasets/ma_dlc.py | 2 +- .../datasets/ma_dlc_dataframe.py | 6 +- .../datasets/materialize.py | 101 +- .../datasets/multi.py | 11 +- .../datasets/single_dlc.py | 2 +- .../datasets/single_dlc_dataframe.py | 4 +- .../datasets/utils.py | 4 +- .../generalized_data_converter/utils.py | 2 +- deeplabcut/modelzoo/utils.py | 6 +- deeplabcut/modelzoo/video_inference.py | 27 +- deeplabcut/modelzoo/webapp/inference.py | 3 +- .../pose_estimation_3d/camera_calibration.py | 28 +- deeplabcut/pose_estimation_3d/plotting3D.py | 13 +- .../pose_estimation_3d/triangulation.py | 21 +- .../pose_estimation_pytorch/__init__.py | 20 +- .../pose_estimation_pytorch/apis/__init__.py | 25 +- .../apis/analyze_images.py | 10 +- .../pose_estimation_pytorch/apis/ctd.py | 2 +- .../apis/evaluation.py | 6 +- .../apis/prune_paf_graph.py | 2 +- .../pose_estimation_pytorch/apis/tracklets.py | 19 +- .../pose_estimation_pytorch/apis/training.py | 4 +- .../pose_estimation_pytorch/apis/utils.py | 17 +- .../pose_estimation_pytorch/apis/videos.py | 2 +- .../config/__init__.py | 15 +- .../config/make_pose_config.py | 8 +- .../pose_estimation_pytorch/config/utils.py | 2 +- .../pose_estimation_pytorch/data/__init__.py | 10 +- .../pose_estimation_pytorch/data/base.py | 2 +- .../data/cocoloader.py | 8 +- .../pose_estimation_pytorch/data/collate.py | 3 +- .../pose_estimation_pytorch/data/ctd.py | 4 +- .../pose_estimation_pytorch/data/dlcloader.py | 5 +- .../data/generative_sampling.py | 2 +- .../data/postprocessor.py | 2 +- .../data/preprocessor.py | 4 +- .../pose_estimation_pytorch/data/snapshots.py | 8 +- .../data/transforms.py | 3 +- .../pose_estimation_pytorch/data/utils.py | 4 +- .../metrics/scoring.py | 3 +- .../models/backbones/__init__.py | 4 +- .../models/backbones/base.py | 2 +- .../models/backbones/cond_prenet.py | 2 +- .../models/backbones/hrnet_coam.py | 2 +- .../models/criterions/aggregators.py | 2 +- .../models/criterions/base.py | 2 +- .../models/criterions/dekr.py | 2 +- .../models/criterions/kl_discrete.py | 2 +- .../models/criterions/weighted.py | 4 +- .../models/detectors/base.py | 2 +- .../models/heads/base.py | 8 +- .../models/heads/dekr.py | 2 +- .../models/heads/rtmcc_head.py | 2 +- .../models/heads/simple_head.py | 2 +- .../models/heads/transformer.py | 2 +- .../pose_estimation_pytorch/models/model.py | 6 +- .../models/modules/__init__.py | 10 +- .../models/modules/coam_module.py | 11 +- .../models/modules/conv_block.py | 8 +- .../models/modules/conv_module.py | 11 +- .../models/modules/gated_attention_unit.py | 4 +- .../models/modules/kpt_encoders.py | 7 +- .../models/necks/__init__.py | 2 +- .../models/necks/base.py | 2 +- .../models/necks/layers.py | 2 +- .../models/necks/transformer.py | 13 +- .../models/predictors/base.py | 2 +- .../models/predictors/dekr_predictor.py | 2 +- .../models/predictors/identity_predictor.py | 2 +- .../models/predictors/paf_predictor.py | 9 +- .../models/predictors/sim_cc.py | 2 +- .../models/predictors/single_predictor.py | 6 +- .../models/target_generators/base.py | 2 +- .../models/target_generators/dekr_targets.py | 2 +- .../target_generators/heatmap_targets.py | 2 +- .../models/target_generators/pafs_targets.py | 2 +- .../models/target_generators/sim_cc.py | 2 +- .../models/weight_init.py | 2 +- .../modelzoo/inference.py | 12 +- .../modelzoo/memory_replay.py | 4 +- .../pose_estimation_pytorch/modelzoo/utils.py | 4 +- .../pose_estimation_pytorch/registry.py | 4 +- .../runners/__init__.py | 8 +- .../runners/dynamic_cropping.py | 4 +- .../runners/inference.py | 21 +- .../pose_estimation_pytorch/runners/logger.py | 12 +- .../runners/shelving.py | 8 +- .../runners/snapshots.py | 4 +- .../pose_estimation_pytorch/runners/train.py | 4 +- deeplabcut/pose_estimation_pytorch/utils.py | 3 - .../pose_estimation_tensorflow/__init__.py | 10 +- .../backbones/efficientnet_builder.py | 3 +- .../backbones/efficientnet_model.py | 7 +- .../backbones/mobilenet.py | 2 +- .../backbones/mobilenet_v2.py | 2 +- .../pose_estimation_tensorflow/config.py | 5 +- .../core/evaluate.py | 49 +- .../core/evaluate_multianimal.py | 20 +- .../mo_extensions/front/tf/unravel_index.py | 8 +- .../core/openvino/session.py | 4 +- .../core/predict.py | 2 + .../core/predict_multianimal.py | 2 +- .../pose_estimation_tensorflow/core/test.py | 11 +- .../pose_estimation_tensorflow/core/train.py | 8 +- .../core/train_multianimal.py | 31 +- .../datasets/__init__.py | 5 +- .../datasets/augmentation.py | 8 +- .../datasets/pose_base.py | 1 + .../datasets/pose_deterministic.py | 11 +- .../datasets/pose_imgaug.py | 18 +- .../datasets/pose_multianimal_imgaug.py | 15 +- .../datasets/pose_scalecrop.py | 2 +- .../datasets/pose_tensorpack.py | 14 +- .../datasets/utils.py | 3 +- .../pose_estimation_tensorflow/export.py | 5 +- .../modelzoo/api/spatiotemporal_adapt.py | 3 +- .../modelzoo/api/superanimal_inference.py | 20 +- .../nnets/__init__.py | 3 +- .../pose_estimation_tensorflow/nnets/base.py | 5 +- .../nnets/efficientnet.py | 4 +- .../nnets/layers.py | 1 - .../nnets/mobilenet.py | 6 +- .../pose_estimation_tensorflow/nnets/multi.py | 11 +- .../nnets/resnet.py | 6 +- .../pose_estimation_tensorflow/nnets/utils.py | 12 +- .../predict_multianimal.py | 5 +- .../predict_videos.py | 62 +- .../pose_estimation_tensorflow/training.py | 14 +- .../util/visualize.py | 2 +- .../pose_estimation_tensorflow/vis_dataset.py | 5 +- .../visualizemaps.py | 39 +- deeplabcut/pose_tracking_pytorch/__init__.py | 2 +- deeplabcut/pose_tracking_pytorch/apis.py | 3 +- .../pose_tracking_pytorch/config/__init__.py | 4 +- .../pose_tracking_pytorch/create_dataset.py | 7 +- .../pose_tracking_pytorch/datasets/dlc_vec.py | 2 +- .../datasets/make_dataloader.py | 1 + deeplabcut/pose_tracking_pytorch/inference.py | 6 +- .../pose_tracking_pytorch/model/__init__.py | 2 +- .../model/backbones/vit_pytorch.py | 16 +- .../pose_tracking_pytorch/model/make_model.py | 5 +- .../processor/__init__.py | 4 +- .../processor/processor.py | 39 +- .../pose_tracking_pytorch/solver/cosine_lr.py | 2 +- .../pose_tracking_pytorch/solver/scheduler.py | 6 +- .../tracking_utils/__init__.py | 2 +- .../tracking_utils/meter.py | 2 +- .../tracking_utils/metrics.py | 7 +- .../tracking_utils/reranking.py | 2 +- .../train_dlctransreid.py | 13 +- .../post_processing/analyze_skeleton.py | 15 +- deeplabcut/post_processing/filtering.py | 2 +- .../refine_training_dataset/__init__.py | 2 +- .../refine_training_dataset/outlier_frames.py | 36 +- deeplabcut/refine_training_dataset/stitch.py | 63 +- .../refine_training_dataset/tracklets.py | 10 +- deeplabcut/utils/auxfun_models.py | 15 +- deeplabcut/utils/auxfun_multianimal.py | 11 +- deeplabcut/utils/auxfun_videos.py | 37 +- deeplabcut/utils/auxiliaryfunctions.py | 27 +- deeplabcut/utils/auxiliaryfunctions_3d.py | 12 +- deeplabcut/utils/conversioncode.py | 5 +- deeplabcut/utils/make_labeled_video.py | 49 +- deeplabcut/utils/plotting.py | 6 +- deeplabcut/utils/pseudo_label.py | 39 +- deeplabcut/utils/skeleton.py | 4 +- deeplabcut/utils/video_processor.py | 4 +- deeplabcut/utils/visualization.py | 38 +- docker/deeplabcut_docker.py | 1 - docs/recipes/flip_and_rotate.ipynb | 78 +- docs/recipes/fmpose3d.ipynb | 2 - examples/COLAB/COLAB_3miceDemo.ipynb | 6 +- .../COLAB/COLAB_BUCTD_and_CTD_tracking.ipynb | 12 +- examples/COLAB/COLAB_DEMO_SuperAnimal.ipynb | 2 +- .../COLAB/COLAB_DEMO_mouse_openfield.ipynb | 2 +- examples/COLAB/COLAB_DLC_ModelZoo.ipynb | 1 + .../COLAB/COLAB_HumanPose_with_RTMPose.ipynb | 5 +- .../COLAB/COLAB_YOURDATA_SuperAnimal.ipynb | 6 +- examples/COLAB/COLAB_transformer_reID.ipynb | 8 +- examples/JUPYTER/Demo_3D_DeepLabCut.ipynb | 1 - examples/testscript.py | 10 +- examples/testscript_3d.py | 16 +- .../testscript_deterministicwithResNet152.py | 9 +- examples/testscript_mobilenets.py | 9 +- examples/testscript_multianimal.py | 4 +- examples/testscript_openfielddata.py | 3 +- ...pt_openfielddata_augmentationcomparison.py | 3 +- examples/testscript_pretrained_models.py | 7 +- examples/testscript_pytorch_multi_animal.py | 16 +- examples/testscript_pytorch_single_animal.py | 8 +- examples/testscript_superanimal_adaptation.py | 2 +- examples/testscript_superanimal_inference.py | 2 +- examples/testscript_transreid.py | 10 +- examples/utils.py | 9 +- ruff-report.md | 6934 +++++++++++++++++ tests/conftest.py | 7 +- .../inferenceutils/test_map_computation.py | 6 +- .../metrics/test_metrics_map_computation.py | 6 +- .../test_video_set_configuration.py | 3 +- .../test_trainset_metadata.py | 3 +- .../apis/test_apis_evaluate.py | 1 - .../apis/test_apis_export.py | 2 +- .../apis/test_tracklets.py | 2 +- .../data/test_data_ctd.py | 1 - .../data/test_preprocessor.py | 3 +- .../target_generators/test_heatmap_targets.py | 2 +- .../target_generators/test_plateau_targets.py | 2 +- .../modelzoo/test_webapp.py | 2 - .../other/test_data_helper.py | 4 +- .../other/test_heatmap_plateau_targets.py | 7 +- .../other/test_helper.py | 4 +- .../other/test_modelzoo.py | 2 +- .../other/test_pose_model.py | 2 +- .../runners/bottum_up.py | 13 +- .../runners/test_dynamic_cropper.py | 3 +- tests/test_auxfun_models.py | 2 +- tests/test_auxfun_multianimal.py | 4 +- tests/test_auxiliaryfunctions.py | 2 + tests/test_conversioncode.py | 2 + tests/test_crossvalutils.py | 4 +- tests/test_dataset_augmentation.py | 1 + tests/test_frame_selection_tools.py | 2 + tests/test_inferenceutils.py | 8 +- tests/test_pose_multianimal_imgaug.py | 6 +- tests/test_predict_multianimal.py | 2 +- tests/test_predict_supermodel.py | 1 + tests/test_stitcher.py | 2 +- tests/test_trackingutils.py | 1 + tests/test_trainingsetmanipulation.py | 17 +- tests/test_triangulation.py | 1 + tests/test_video.py | 3 +- tests/utils/test_multiprocessing.py | 4 +- testscript_cli.py | 12 +- tools/update_license_headers.py | 9 +- 285 files changed, 8241 insertions(+), 1305 deletions(-) create mode 100644 ruff-report.md diff --git a/deeplabcut/__init__.py b/deeplabcut/__init__.py index 557c0b8eee..abf01328d9 100644 --- a/deeplabcut/__init__.py +++ b/deeplabcut/__init__.py @@ -13,67 +13,61 @@ import os DEBUG = True and "DEBUG" in os.environ and os.environ["DEBUG"] -from deeplabcut.version import __version__, VERSION +from deeplabcut.version import VERSION, __version__ print(f"Loading DLC {VERSION}...") try: - from deeplabcut.gui.tracklet_toolbox import refine_tracklets from deeplabcut.gui.launch_script import launch_dlc from deeplabcut.gui.tabs.label_frames import ( label_frames, refine_labels, ) + from deeplabcut.gui.tracklet_toolbox import refine_tracklets from deeplabcut.gui.widgets import SkeletonBuilder except (ModuleNotFoundError, ImportError): print("DLC loaded in light mode; you cannot use any GUI (labeling, relabeling and standalone GUI)") from deeplabcut.core.engine import Engine from deeplabcut.create_project import ( + add_new_videos, create_new_project, create_new_project_3d, - add_new_videos, - load_demo_data, - create_pretrained_project, create_pretrained_human_project, + create_pretrained_project, + load_demo_data, ) from deeplabcut.generate_training_dataset import ( + adddatasetstovideolistandviceversa, check_labels, + comparevideolistsanddatafolders, + create_multianimaltraining_dataset, create_training_dataset, - extract_frames, - mergeandsplit, -) -from deeplabcut.generate_training_dataset import ( create_training_dataset_from_existing_split, create_training_model_comparison, - create_multianimaltraining_dataset, -) -from deeplabcut.generate_training_dataset import ( dropannotationfileentriesduetodeletedimages, - comparevideolistsanddatafolders, - dropimagesduetolackofannotation, - adddatasetstovideolistandviceversa, dropduplicatesinannotatinfiles, + dropimagesduetolackofannotation, dropunlabeledframes, + extract_frames, + mergeandsplit, ) - from deeplabcut.modelzoo.video_inference import video_inference_superanimal - from deeplabcut.utils import ( - create_labeled_video, - create_video_with_all_detections, - plot_trajectories, - auxiliaryfunctions, - convert2_maDLC, - convertcsv2h5, analyze_videos_converth5_to_csv, analyze_videos_converth5_to_nwb, auxfun_videos, + auxiliaryfunctions, + convert2_maDLC, + convertcsv2h5, + create_labeled_video, + create_video_with_all_detections, + plot_trajectories, ) try: from deeplabcut.pose_tracking_pytorch import transformer_reID -except ModuleNotFoundError as e: +except ModuleNotFoundError: import warnings warnings.warn( @@ -83,44 +77,40 @@ """ ) -from deeplabcut.utils.auxfun_videos import ( - ShortenVideo, - DownSampleVideo, - CropVideo, - check_video_integrity, -) - # Train, evaluate & predict functions / all require TF from deeplabcut.compat import ( - train_network, - return_train_network_path, - evaluate_network, - return_evaluate_network_data, - analyze_videos, - create_tracking_dataset, analyze_images, analyze_time_lapse_frames, + analyze_videos, convert_detections2tracklets, + create_tracking_dataset, + evaluate_network, + export_model, extract_maps, - visualize_scoremaps, + extract_save_all_maps, + return_evaluate_network_data, + return_train_network_path, + train_network, visualize_locrefs, visualize_paf, - extract_save_all_maps, - export_model, + visualize_scoremaps, ) - - from deeplabcut.pose_estimation_3d import ( calibrate_cameras, check_undistortion, - triangulate, create_labeled_video_3d, + triangulate, ) - -from deeplabcut.refine_training_dataset.stitch import stitch_tracklets +from deeplabcut.post_processing import analyzeskeleton, filterpredictions from deeplabcut.refine_training_dataset import ( extract_outlier_frames, - merge_datasets, find_outliers_in_raw_data, + merge_datasets, +) +from deeplabcut.refine_training_dataset.stitch import stitch_tracklets +from deeplabcut.utils.auxfun_videos import ( + CropVideo, + DownSampleVideo, + ShortenVideo, + check_video_integrity, ) -from deeplabcut.post_processing import filterpredictions, analyzeskeleton diff --git a/deeplabcut/benchmark/__init__.py b/deeplabcut/benchmark/__init__.py index 935f853a2e..e663705b8e 100644 --- a/deeplabcut/benchmark/__init__.py +++ b/deeplabcut/benchmark/__init__.py @@ -12,7 +12,7 @@ import json import os -from typing import Container +from collections.abc import Container from typing import Literal from deeplabcut.benchmark.base import Benchmark, Result, ResultCollection @@ -112,7 +112,7 @@ def loadcache(cache=CACHE, on_missing: Literal["raise", "ignore"] = "ignore") -> if on_missing == "raise": raise FileNotFoundError(cache) return ResultCollection() - with open(cache, "r") as fh: + with open(cache) as fh: try: data = json.load(fh) except json.decoder.JSONDecodeError as e: diff --git a/deeplabcut/benchmark/base.py b/deeplabcut/benchmark/base.py index 41c2e44d81..48a9850257 100644 --- a/deeplabcut/benchmark/base.py +++ b/deeplabcut/benchmark/base.py @@ -24,8 +24,7 @@ import abc import dataclasses import warnings -from typing import Iterable -from typing import Tuple +from collections.abc import Iterable import pandas as pd @@ -150,12 +149,12 @@ class Result: _primary_key = ("benchmark_name", "method_name", "benchmark_version") @property - def primary_key(self) -> Tuple[str]: + def primary_key(self) -> tuple[str]: """The primary key to uniquely identify this result.""" return tuple(getattr(self, k) for k in self._primary_key) @property - def primary_key_names(self) -> Tuple[str]: + def primary_key_names(self) -> tuple[str]: """Names of the primary keys""" return tuple(self._export_mapping.get(k) for k in self._primary_key) diff --git a/deeplabcut/benchmark/metrics.py b/deeplabcut/benchmark/metrics.py index de0ee4161d..4cf8e96709 100644 --- a/deeplabcut/benchmark/metrics.py +++ b/deeplabcut/benchmark/metrics.py @@ -23,17 +23,16 @@ import os import pickle from collections import defaultdict -from typing import List, Optional import numpy as np import pandas as pd import deeplabcut.benchmark.utils -from deeplabcut.core import inferenceutils, crossvalutils +from deeplabcut.core import crossvalutils, inferenceutils from deeplabcut.utils.conversioncode import guarantee_multiindex_rows -def _format_gt_data(h5file: str, test_indices: Optional[List[int]] = None): +def _format_gt_data(h5file: str, test_indices: list[int] | None = None): df = pd.read_hdf(h5file) animals = _get_unique_level_values(df.columns, "individuals") @@ -251,7 +250,7 @@ def calc_rmse_from_obj( return np.nanmean(errors[..., 0]) -def load_test_images(h5file: str, metadata: str) -> List[str]: +def load_test_images(h5file: str, metadata: str) -> list[str]: """ Returns the names of the test images for the benchmark, in the order corresponding to the test indices. diff --git a/deeplabcut/benchmark/utils.py b/deeplabcut/benchmark/utils.py index 627a4db566..c842ec67e2 100644 --- a/deeplabcut/benchmark/utils.py +++ b/deeplabcut/benchmark/utils.py @@ -19,7 +19,7 @@ import sys -class RedirectStdStreams(object): +class RedirectStdStreams: """Context manager for redirecting stdout and stderr Reference: https://stackoverflow.com/a/6796752 diff --git a/deeplabcut/cli.py b/deeplabcut/cli.py index be8bff3b13..ec2e024aec 100644 --- a/deeplabcut/cli.py +++ b/deeplabcut/cli.py @@ -26,7 +26,7 @@ def main(ctx, verbose): click.echo(main.get_help(ctx)) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("project") @click.argument("experimenter") @@ -81,7 +81,7 @@ def create_new_project(_, *args, **kwargs): new.create_new_project(*args, **kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @@ -120,7 +120,7 @@ def add_new_videos(_, *args, **kwargs): add.add_new_videos(*args, **kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("config") @click.argument("mode") @@ -167,7 +167,7 @@ def extract_frames(_, *args, **kwargs): frameExtraction.extract_frames(*args, **kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("config") @click.pass_context @@ -182,7 +182,7 @@ def label_frames(_, config): labelFrames.label_frames(config) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("config") @click.pass_context @@ -193,7 +193,7 @@ def check_labels(_, config): labelFrames.check_labels(config) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("config") @click.option( @@ -221,7 +221,7 @@ def create_training_dataset(_, *args, **kwargs): labelFrames.create_training_dataset(*args, **kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("config") @click.option( @@ -246,7 +246,7 @@ def train_network(_, *args, **kwargs): training.train_network(*args, **kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("config") @click.option( @@ -273,7 +273,7 @@ def evaluate_network(_, config, **kwargs): evaluate.evaluate_network(config, **kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @@ -320,7 +320,7 @@ def analyze_videos(_, *args, **kwargs): # predict.predict_video(config, video,**kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @@ -431,7 +431,7 @@ def extract_outlier_frames(_, *args, **kwargs): outlier_frames.extract_outlier_frames(*args, **kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("config") @click.pass_context @@ -450,7 +450,7 @@ def refine_labels(_, config): outlier_frames.refine_labels(config) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("config") @click.argument("videos", nargs=-1) @@ -499,7 +499,7 @@ def create_labeled_video(_, *args, **kwargs): make_labeled_video.create_labeled_video(*args, **kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("config") @click.argument("videos", nargs=-1) @@ -542,7 +542,7 @@ def plot_trajectories(_, *args, **kwargs): plotting.plot_trajectories(*args, **kwargs) -########################################################################################################################### +########################################################################## @main.command(context_settings=CONTEXT_SETTINGS) @click.argument("cfg-path", nargs=1, type=click.STRING) @click.option( @@ -645,4 +645,4 @@ def export_model(_, *args, **kwargs): export_model(*args, **kwargs) -########################################################################################################################### +########################################################################## diff --git a/deeplabcut/compat.py b/deeplabcut/compat.py index 8ebd62bf89..2cc5510864 100644 --- a/deeplabcut/compat.py +++ b/deeplabcut/compat.py @@ -12,8 +12,8 @@ from __future__ import annotations +from collections.abc import Iterable from pathlib import Path -from typing import Iterable import numpy as np from ruamel.yaml import YAML @@ -1960,7 +1960,7 @@ def _load_config(config: str) -> dict: if not config_path.exists(): raise FileNotFoundError(f"Config {config} is not found. Please make sure that the file exists.") - with open(config, "r") as f: + with open(config) as f: project_config = YAML(typ="safe", pure=True).load(f) return project_config diff --git a/deeplabcut/core/config.py b/deeplabcut/core/config.py index 13e4fc6e46..061a73e461 100644 --- a/deeplabcut/core/config.py +++ b/deeplabcut/core/config.py @@ -12,8 +12,8 @@ from __future__ import annotations +from collections.abc import Callable from pathlib import Path -from typing import Callable from ruamel.yaml import YAML @@ -26,7 +26,7 @@ def read_config_as_dict(config_path: str | Path) -> dict: Returns: The configuration file with pure Python classes """ - with open(config_path, "r") as f: + with open(config_path) as f: cfg = YAML(typ="safe", pure=True).load(f) return cfg diff --git a/deeplabcut/core/crossvalutils.py b/deeplabcut/core/crossvalutils.py index 8b76e6ef82..0e6a324c28 100644 --- a/deeplabcut/core/crossvalutils.py +++ b/deeplabcut/core/crossvalutils.py @@ -24,8 +24,8 @@ from tqdm import tqdm from deeplabcut.core.inferenceutils import ( - _parse_ground_truth_data, Assembler, + _parse_ground_truth_data, evaluate_assembly, ) from deeplabcut.utils import auxfun_multianimal, auxiliaryfunctions diff --git a/deeplabcut/core/inferenceutils.py b/deeplabcut/core/inferenceutils.py index 4abeb85e87..ebb82047ca 100644 --- a/deeplabcut/core/inferenceutils.py +++ b/deeplabcut/core/inferenceutils.py @@ -17,9 +17,10 @@ import pickle import warnings from collections import defaultdict +from collections.abc import Iterable from dataclasses import dataclass from math import erf, sqrt -from typing import Any, Iterable, Tuple +from typing import Any import networkx as nx import numpy as np @@ -41,7 +42,7 @@ def _conv_square_to_condensed_indices(ind_row, ind_col, n): return n * ind_col - ind_col * (ind_col + 1) // 2 + ind_row - 1 - ind_col -Position = Tuple[float, float] +Position = tuple[float, float] @dataclass(frozen=True) diff --git a/deeplabcut/core/metrics/bbox.py b/deeplabcut/core/metrics/bbox.py index 534ceb3e67..428ea0fc72 100644 --- a/deeplabcut/core/metrics/bbox.py +++ b/deeplabcut/core/metrics/bbox.py @@ -16,9 +16,8 @@ from __future__ import annotations -from unittest.mock import Mock, patch - from datetime import datetime +from unittest.mock import Mock, patch import numpy as np @@ -27,7 +26,7 @@ from pycocotools.cocoeval import COCOeval with_pycocotools = True -except ModuleNotFoundError as err: +except ModuleNotFoundError: with_pycocotools = False diff --git a/deeplabcut/core/metrics/matching.py b/deeplabcut/core/metrics/matching.py index 8a49e69cad..296affb2e5 100644 --- a/deeplabcut/core/metrics/matching.py +++ b/deeplabcut/core/metrics/matching.py @@ -66,7 +66,7 @@ def match(self, gt: np.ndarray, oks: float) -> None: self.oks = oks @classmethod - def from_pose(cls, pose: np.ndarray) -> "PotentialMatch": + def from_pose(cls, pose: np.ndarray) -> PotentialMatch: assert len(pose.shape) == 2 # Must be pose for a single individual scores = pose[:, 2] if np.all(np.isnan(scores)): diff --git a/deeplabcut/core/visualization.py b/deeplabcut/core/visualization.py index d415db8470..4a01080c96 100644 --- a/deeplabcut/core/visualization.py +++ b/deeplabcut/core/visualization.py @@ -211,7 +211,7 @@ def _filename(map_name) -> str: if paf is not None: if paf_graph is None: - raise ValueError(f"When plotting the PAF, you must pass the ``paf_graph``") + raise ValueError("When plotting the PAF, you must pass the ``paf_graph``") edge_list = [] for n, edge in enumerate(paf_graph): diff --git a/deeplabcut/core/weight_init.py b/deeplabcut/core/weight_init.py index 01fe499c51..758da538b8 100644 --- a/deeplabcut/core/weight_init.py +++ b/deeplabcut/core/weight_init.py @@ -62,19 +62,19 @@ def __post_init__(self): if self.with_decoder and self.conversion_array is None: raise ValueError( - f"You must specify a conversion_array to initialize decoder weights (``with_decoder=True``)." + "You must specify a conversion_array to initialize decoder weights (``with_decoder=True``)." ) if self.bodyparts is not None and self.conversion_array is None: raise ValueError( - f"Specifying bodyparts should only be done when `with_decoder=True` and" - f" the conversion array is specified." + "Specifying bodyparts should only be done when `with_decoder=True` and" + " the conversion array is specified." ) if self.conversion_array is not None and self.bodyparts is not None: if not len(self.conversion_array) == len(self.bodyparts): raise ValueError( - f"There must be the same number of elements in the bodyparts list " + "There must be the same number of elements in the bodyparts list " "and conv. array; found {self.bodyparts}, {self.conversion_array}" ) @@ -100,7 +100,7 @@ def to_dict(self) -> dict: return data @staticmethod - def from_dict(data: dict) -> "WeightInitialization": + def from_dict(data: dict) -> WeightInitialization: if "snapshot_path" not in data: return WeightInitialization.from_dict_legacy(data) @@ -123,7 +123,7 @@ def from_dict(data: dict) -> "WeightInitialization": ) @staticmethod - def from_dict_legacy(data: dict) -> "WeightInitialization": + def from_dict_legacy(data: dict) -> WeightInitialization: """Deals with weight initialization that were created before 3.0.0rc5""" import deeplabcut.pose_estimation_pytorch.modelzoo.utils as utils @@ -156,7 +156,7 @@ def build( memory_replay: bool = False, customized_pose_checkpoint: str | None = None, customized_detector_checkpoint: str | None = None, - ) -> "WeightInitialization": + ) -> WeightInitialization: """Builds a WeightInitialization for a project `WeightInitialization.build` is deprecated and will be removed in a future diff --git a/deeplabcut/create_project/add.py b/deeplabcut/create_project/add.py index c3b56412be..165ab4e522 100644 --- a/deeplabcut/create_project/add.py +++ b/deeplabcut/create_project/add.py @@ -49,9 +49,9 @@ def add_new_videos(config, videos, copy_videos=False, coords=None, extract_frame import shutil from pathlib import Path + from deeplabcut.generate_training_dataset import frame_extraction from deeplabcut.utils import auxiliaryfunctions from deeplabcut.utils.auxfun_videos import VideoReader - from deeplabcut.generate_training_dataset import frame_extraction # Read the config file cfg = auxiliaryfunctions.read_config(config) @@ -74,7 +74,7 @@ def add_new_videos(config, videos, copy_videos=False, coords=None, extract_frame destinations = [video_path.joinpath(vp.name) for vp in videos] if copy_videos: - for src, dst in zip(videos, destinations): + for src, dst in zip(videos, destinations, strict=False): if dst.exists(): pass else: @@ -84,7 +84,7 @@ def add_new_videos(config, videos, copy_videos=False, coords=None, extract_frame else: # creates the symlinks of the video and puts it in the videos directory. print("Attempting to create a symbolic link of the video ...") - for src, dst in zip(videos, destinations): + for src, dst in zip(videos, destinations, strict=False): if dst.exists(): print(f"Video {dst} already exists. Skipping...") continue @@ -92,16 +92,16 @@ def add_new_videos(config, videos, copy_videos=False, coords=None, extract_frame src = str(src) dst = str(dst) os.symlink(src, dst) - print("Created the symlink of {} to {}".format(src, dst)) + print(f"Created the symlink of {src} to {dst}") except OSError: try: import subprocess - subprocess.check_call("mklink %s %s" % (dst, src), shell=True) + subprocess.check_call(f"mklink {dst} {src}", shell=True) except (OSError, subprocess.CalledProcessError): print("Symlink creation impossible (exFat architecture?): copying the video instead.") shutil.copy(os.fspath(src), os.fspath(dst)) - print("{} copied to {}".format(src, dst)) + print(f"{src} copied to {dst}") videos = destinations if copy_videos: diff --git a/deeplabcut/create_project/modelzoo.py b/deeplabcut/create_project/modelzoo.py index 8f86c50200..b3aeefeab4 100644 --- a/deeplabcut/create_project/modelzoo.py +++ b/deeplabcut/create_project/modelzoo.py @@ -15,8 +15,8 @@ import yaml from dlclibrary import get_available_detectors from dlclibrary.dlcmodelzoo.modelzoo_download import ( - download_huggingface_model, MODELOPTIONS, + download_huggingface_model, get_available_datasets, get_available_models, ) @@ -25,9 +25,9 @@ from deeplabcut.core.config import read_config_as_dict, write_config from deeplabcut.core.engine import Engine from deeplabcut.generate_training_dataset.metadata import ( - TrainingDatasetMetadata, - ShuffleMetadata, DataSplit, + ShuffleMetadata, + TrainingDatasetMetadata, ) from deeplabcut.generate_training_dataset.trainingsetmanipulation import ( MakeInference_yaml, @@ -358,12 +358,16 @@ def create_pretrained_project_pytorch( if net_name not in get_available_models(dataset): raise ValueError( - f"Invalid net_name '{net_name}' for dataset {dataset}. The following net types are available: {get_available_models(dataset)}" + f"Invalid net_name '{net_name}' for dataset {dataset}. The following net types are available: { + get_available_models(dataset) + }" ) if detector_name not in get_available_detectors(dataset): raise ValueError( - f"Invalid detector_name '{detector_name}' for dataset {dataset}. The following detectors are available: {get_available_detectors(dataset)}" + f"Invalid detector_name '{detector_name}' for dataset {dataset}. The following detectors are available: { + get_available_detectors(dataset) + }" ) # Create project diff --git a/deeplabcut/create_project/new.py b/deeplabcut/create_project/new.py index 94dd42d74b..2b03e8a48d 100644 --- a/deeplabcut/create_project/new.py +++ b/deeplabcut/create_project/new.py @@ -105,6 +105,7 @@ def create_new_project( Users must format paths with either: r'C:\ OR 'C:\\ <- i.e. a double backslash \ \ ) """ from datetime import datetime as dt + from deeplabcut.utils import auxiliaryfunctions months_3letter = { @@ -130,12 +131,12 @@ def create_new_project( if working_directory is None: working_directory = "." wd = Path(working_directory).resolve() - project_name = "{pn}-{exp}-{date}".format(pn=project, exp=experimenter, date=date) + project_name = f"{project}-{experimenter}-{date}" project_path = wd / project_name # Create project and sub-directories if not DEBUG and project_path.exists(): - print('Project "{}" already exists!'.format(project_path)) + print(f'Project "{project_path}" already exists!') return os.path.join(str(project_path), "config.yaml") video_path = project_path / "videos" data_path = project_path / "labeled-data" @@ -143,9 +144,10 @@ def create_new_project( results_path = project_path / "dlc-models" for p in [video_path, data_path, shuffles_path, results_path]: p.mkdir(parents=True, exist_ok=DEBUG) - print('Created "{}"'.format(p)) + print(f'Created "{p}"') - # Add all videos in the folder. Multiple folders can be passed in a list, similar to the video files. Folders and video files can also be passed! + # Add all videos in the folder. Multiple folders can be passed in a list, + # similar to the video files. Folders and video files can also be passed! collected_videos = [] paths = [Path(p) for p in videos] for i in paths: @@ -181,28 +183,28 @@ def create_new_project( destinations = [video_path.joinpath(vp.name) for vp in videos] if copy_videos: print("Copying the videos") - for src, dst in zip(videos, destinations): + for src, dst in zip(videos, destinations, strict=False): shutil.copy(os.fspath(src), os.fspath(dst)) # https://www.python.org/dev/peps/pep-0519/ else: # creates the symlinks of the video and puts it in the videos directory. print("Attempting to create a symbolic link of the video ...") - for src, dst in zip(videos, destinations): + for src, dst in zip(videos, destinations, strict=False): if dst.exists() and not DEBUG: - raise FileExistsError("Video {} exists already!".format(dst)) + raise FileExistsError(f"Video {dst} exists already!") try: src = str(src) dst = str(dst) os.symlink(src, dst) - print("Created the symlink of {} to {}".format(src, dst)) + print(f"Created the symlink of {src} to {dst}") except OSError: try: import subprocess - subprocess.check_call("mklink %s %s" % (dst, src), shell=True) + subprocess.check_call(f"mklink {dst} {src}", shell=True) except (OSError, subprocess.CalledProcessError): print("Symlink creation impossible (exFat architecture?): copying the video instead.") shutil.copy(os.fspath(src), os.fspath(dst)) - print("{} copied to {}".format(src, dst)) + print(f"{src} copied to {dst}") videos = destinations if copy_videos: @@ -213,7 +215,8 @@ def create_new_project( for video in videos: print(video) try: - # For windows os.path.realpath does not work and does not link to the real video. [old: rel_video_path = os.path.realpath(video)] + # For windows os.path.realpath does not work and does not link to the real + # video. [old: rel_video_path = os.path.realpath(video)] rel_video_path = str(Path.resolve(Path(video))) except: rel_video_path = os.readlink(str(video)) @@ -221,8 +224,8 @@ def create_new_project( try: vid = VideoReader(rel_video_path) video_sets[rel_video_path] = {"crop": ", ".join(map(str, vid.get_bbox()))} - except IOError: - warnings.warn("Cannot open the video file! Skipping to the next one...") + except OSError: + warnings.warn("Cannot open the video file! Skipping to the next one...", stacklevel=2) os.remove(video) # Removing the video or link from the project if not len(video_sets): @@ -230,7 +233,8 @@ def create_new_project( shutil.rmtree(project_path, ignore_errors=True) warnings.warn( "No valid videos were found. The project was not created... " - "Verify the video files and re-create the project." + "Verify the video files and re-create the project.", + stacklevel=2, ) return "nothingcreated" @@ -302,7 +306,6 @@ def create_new_project( print('Generated "{}"'.format(project_path / "config.yaml")) print( - "\nA new project with name %s is created at %s and a configurable file (config.yaml) is stored there. Change the parameters in this file to adapt to your project's needs.\n Once you have changed the configuration file, use the function 'extract_frames' to select frames for labeling.\n. [OPTIONAL] Use the function 'add_new_videos' to add new videos to your project (at any stage)." - % (project_name, str(wd)) + f"\nA new project with name {project_name} is created at {str(wd)} and a configurable file (config.yaml) is stored there. Change the parameters in this file to adapt to your project's needs.\n Once you have changed the configuration file, use the function 'extract_frames' to select frames for labeling.\n. [OPTIONAL] Use the function 'add_new_videos' to add new videos to your project (at any stage)." ) return projconfigfile diff --git a/deeplabcut/create_project/new_3d.py b/deeplabcut/create_project/new_3d.py index 78ed925ead..474c968df9 100644 --- a/deeplabcut/create_project/new_3d.py +++ b/deeplabcut/create_project/new_3d.py @@ -46,6 +46,7 @@ def create_new_project_3d(project, experimenter, num_cameras=2, working_director """ from datetime import datetime as dt + from deeplabcut.utils import auxiliaryfunctions date = dt.today() @@ -62,7 +63,7 @@ def create_new_project_3d(project, experimenter, num_cameras=2, working_director project_path = wd / project_name # Create project and sub-directories if not DEBUG and project_path.exists(): - print('Project "{}" already exists!'.format(project_path)) + print(f'Project "{project_path}" already exists!') return camera_matrix_path = project_path / "camera_matrix" @@ -79,7 +80,7 @@ def create_new_project_3d(project, experimenter, num_cameras=2, working_director path_removed_images, ]: p.mkdir(parents=True, exist_ok=DEBUG) - print('Created "{}"'.format(p)) + print(f'Created "{p}"') # Create config file cfg_file_3d, ruamelFile_3d = auxiliaryfunctions.create_config_template_3d() @@ -122,7 +123,6 @@ def create_new_project_3d(project, experimenter, num_cameras=2, working_director print('Generated "{}"'.format(project_path / "config.yaml")) print( - "\nA new project with name %s is created at %s and a configurable file (config.yaml) is stored there. If you have not calibrated the cameras, then use the function 'calibrate_camera' to start calibrating the camera otherwise use the function ``triangulate`` to triangulate the dataframe" - % (project_name, wd) + f"\nA new project with name {project_name} is created at {wd} and a configurable file (config.yaml) is stored there. If you have not calibrated the cameras, then use the function 'calibrate_camera' to start calibrating the camera otherwise use the function ``triangulate`` to triangulate the dataframe" ) return projconfigfile diff --git a/deeplabcut/generate_training_dataset/__init__.py b/deeplabcut/generate_training_dataset/__init__.py index 60eac17c1d..7729536aba 100644 --- a/deeplabcut/generate_training_dataset/__init__.py +++ b/deeplabcut/generate_training_dataset/__init__.py @@ -11,10 +11,10 @@ from deeplabcut.generate_training_dataset.frame_extraction import * -from deeplabcut.generate_training_dataset.trainingsetmanipulation import * -from deeplabcut.generate_training_dataset.multiple_individuals_trainingsetmanipulation import * from deeplabcut.generate_training_dataset.metadata import ( DataSplit, ShuffleMetadata, TrainingDatasetMetadata, ) +from deeplabcut.generate_training_dataset.multiple_individuals_trainingsetmanipulation import * +from deeplabcut.generate_training_dataset.trainingsetmanipulation import * diff --git a/deeplabcut/generate_training_dataset/frame_extraction.py b/deeplabcut/generate_training_dataset/frame_extraction.py index 97b2c518dd..33412bfd19 100755 --- a/deeplabcut/generate_training_dataset/frame_extraction.py +++ b/deeplabcut/generate_training_dataset/frame_extraction.py @@ -31,7 +31,7 @@ def select_cropping_area(config, videos=None): cfg : dict Updated project configuration """ - from deeplabcut.utils import auxiliaryfunctions, auxfun_videos + from deeplabcut.utils import auxfun_videos, auxiliaryfunctions cfg = auxiliaryfunctions.read_config(config) if videos is None: @@ -251,16 +251,17 @@ def extract_frames( extracted_cam=0, ) """ + import glob import os - import sys import re - import glob - import numpy as np + import sys from pathlib import Path + + import numpy as np from skimage import io from skimage.util import img_as_ubyte - from deeplabcut.utils import frameselectiontools - from deeplabcut.utils import auxiliaryfunctions + + from deeplabcut.utils import auxiliaryfunctions, frameselectiontools config_file = Path(config).resolve() cfg = auxiliaryfunctions.read_config(config_file) @@ -359,7 +360,7 @@ def extract_frames( else: coords = None - print("Extracting frames based on %s ..." % algo) + print(f"Extracting frames based on {algo} ...") if algo == "uniform": if opencv: frames2pick = frameselectiontools.UniformFramescv2(cap, numframes2pick, start, stop) @@ -462,7 +463,7 @@ def extract_frames( videos = [v for v in videos if v in videos_list] project_path = Path(config).parents[0] labels_path = os.path.join(project_path, "labeled-data/") - video_dir = os.path.join(project_path, "videos/") + os.path.join(project_path, "videos/") try: cfg_3d = auxiliaryfunctions.read_config(config3d) except: @@ -494,7 +495,7 @@ def extract_frames( coords = None crop_list.append(coords) - for coords, dirPath in zip(crop_list, label_dirs): + for coords, dirPath in zip(crop_list, label_dirs, strict=False): extracted_images = glob.glob(os.path.join(dirPath, "*png")) imgPattern = re.compile("[0-9]{1,10}") diff --git a/deeplabcut/generate_training_dataset/metadata.py b/deeplabcut/generate_training_dataset/metadata.py index 52828e213c..3354b60eb9 100644 --- a/deeplabcut/generate_training_dataset/metadata.py +++ b/deeplabcut/generate_training_dataset/metadata.py @@ -41,7 +41,7 @@ def __post_init__(self) -> None: idx = np.array(indices) if not np.all(idx[:-1] < idx[1:]): raise RuntimeError( - f"The training and test indices in a data split must be sorted in strictly ascending order." + "The training and test indices in a data split must be sorted in strictly ascending order." ) @@ -55,7 +55,7 @@ class ShuffleMetadata: engine: Engine split: DataSplit | None - def load_split(self, cfg: dict, trainset_path: Path) -> "ShuffleMetadata": + def load_split(self, cfg: dict, trainset_path: Path) -> ShuffleMetadata: """Loads the data split for this shuffle Args: @@ -243,7 +243,7 @@ def load( cfg = config metadata_path = TrainingDatasetMetadata.path(cfg) - with open(metadata_path, "r") as file: + with open(metadata_path) as file: metadata = YAML(typ="safe", pure=True).load(file) shuffles = [] diff --git a/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py b/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py index 34e9038cd5..25c4cbcbd9 100755 --- a/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py +++ b/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py @@ -25,19 +25,19 @@ from deeplabcut.core.engine import Engine from deeplabcut.core.weight_init import WeightInitialization from deeplabcut.generate_training_dataset import ( - merge_annotateddatasets, - read_image_shape_fast, - SplitTrials, - MakeTrain_pose_yaml, - MakeTest_pose_yaml, MakeInference_yaml, + MakeTest_pose_yaml, + MakeTrain_pose_yaml, + SplitTrials, + merge_annotateddatasets, pad_train_test_indices, + read_image_shape_fast, validate_shuffles, ) from deeplabcut.utils import ( - auxiliaryfunctions, auxfun_models, auxfun_multianimal, + auxiliaryfunctions, ) @@ -275,6 +275,7 @@ def create_multianimaltraining_dataset( warnings.warn( "`windows2linux` has no effect since 2.2.0.4 and will be removed in 2.2.1.", FutureWarning, + stacklevel=2, ) if len(crop_size) != 2 or not all(isinstance(v, int) for v in crop_size): @@ -321,7 +322,7 @@ def create_multianimaltraining_dataset( num_layers = re.findall("dlcr([0-9]*)", net_type)[0] if num_layers == "": num_layers = 50 - net_type = "resnet_{}".format(num_layers) + net_type = f"resnet_{num_layers}" multi_stage = True dataset_type = "multi-animal-imgaug" @@ -384,7 +385,7 @@ def create_multianimaltraining_dataset( if len(trainIndices) != len(testIndices) != len(Shuffles): raise ValueError("Number of Shuffles and train and test indexes should be equal.") splits = [] - for shuffle, (train_inds, test_inds) in enumerate(zip(trainIndices, testIndices)): + for shuffle, (train_inds, test_inds) in enumerate(zip(trainIndices, testIndices, strict=False)): trainFraction = round(len(train_inds) * 1.0 / (len(train_inds) + len(test_inds)), 2) print(f"You passed a split with the following fraction: {int(100 * trainFraction)}%") # Now that the training fraction is guaranteed to be correct, @@ -619,9 +620,11 @@ def convert_cropped_to_standard_dataset( delete_crops=True, back_up=True, ): - import pandas as pd import pickle import shutil + + import pandas as pd + from deeplabcut.generate_training_dataset import trainingsetmanipulation from deeplabcut.utils import read_plainconfig, write_config diff --git a/deeplabcut/generate_training_dataset/trainingsetmanipulation.py b/deeplabcut/generate_training_dataset/trainingsetmanipulation.py index 8edbfdf433..0e16f049b8 100755 --- a/deeplabcut/generate_training_dataset/trainingsetmanipulation.py +++ b/deeplabcut/generate_training_dataset/trainingsetmanipulation.py @@ -10,30 +10,28 @@ # from __future__ import annotations -import math import logging +import math import os import os.path import warnings - -from functools import lru_cache +from functools import cache from pathlib import Path -from PIL import Image -from typing import List import numpy as np import pandas as pd import yaml +from PIL import Image import deeplabcut.compat as compat import deeplabcut.generate_training_dataset.metadata as metadata from deeplabcut.core.engine import Engine from deeplabcut.core.weight_init import WeightInitialization from deeplabcut.utils import ( - auxiliaryfunctions, - conversioncode, auxfun_models, auxfun_multianimal, + auxiliaryfunctions, + conversioncode, ) from deeplabcut.utils.auxfun_videos import VideoReader @@ -186,7 +184,7 @@ def dropannotationfileentriesduetodeletedimages(config): print("Dropping...", imagename) DC = DC.drop(imagename) dropped = True - if dropped == True: + if dropped: DC.to_hdf(fn, key="df_with_missing", mode="w") DC.to_csv(os.path.join(str(folder), "CollectedData_" + cfg["scorer"] + ".csv")) @@ -276,7 +274,7 @@ def dropunlabeledframes(config): def check_labels( config, - Labels=["+", ".", "x"], + Labels=None, scale=1, dpi=100, draw_skeleton=True, @@ -326,12 +324,14 @@ def check_labels( from deeplabcut.utils import visualization + if Labels is None: + Labels = ["+", ".", "x"] cfg = auxiliaryfunctions.read_config(config) videos = cfg["video_sets"].keys() video_names = [_robust_path_split(video)[1] for video in videos] folders = [os.path.join(cfg["project_path"], "labeled-data", str(Path(i))) for i in video_names] - print("Creating images with labels by %s." % cfg["scorer"]) + print("Creating images with labels by {}.".format(cfg["scorer"])) for folder in folders: try: DataCombined = pd.read_hdf(os.path.join(str(folder), "CollectedData_" + cfg["scorer"] + ".h5")) @@ -447,12 +447,12 @@ def _robust_path_split(path): elif len(splits) == 2: parent, file = splits else: - raise ("Unknown filepath split for path {}".format(path)) + raise (f"Unknown filepath split for path {path}") filename, ext = os.path.splitext(file) return parent, filename, ext -def parse_video_filenames(videos: List[str]) -> List[str]: +def parse_video_filenames(videos: list[str]) -> list[str]: """Parses the names of all videos listed in a project's ``config.yaml`` file Goes through the paths all videos listed for a project, and removes entries with a @@ -678,7 +678,7 @@ def mergeandsplit(config, trainindex=0, uniform=True): conversioncode.guarantee_multiindex_rows(Data) Data = Data[scorer] # extract labeled data - if uniform == True: + if uniform: TrainingFraction = cfg["TrainingFraction"] trainFraction = TrainingFraction[trainindex] trainIndices, testIndices = SplitTrials( @@ -701,7 +701,7 @@ def mergeandsplit(config, trainindex=0, uniform=True): return trainIndices, testIndices -@lru_cache(maxsize=None) +@cache def read_image_shape_fast(path): # Blazing fast and does not load the image into memory with Image.open(path) as img: @@ -946,11 +946,12 @@ def create_training_dataset( warnings.warn( "`windows2linux` has no effect since 2.2.0.4 and will be removed in 2.2.1.", FutureWarning, + stacklevel=2, ) # Loading metadata from config file: cfg = auxiliaryfunctions.read_config(config) - dlc_root_path = auxiliaryfunctions.get_deeplabcut_path() + auxiliaryfunctions.get_deeplabcut_path() if superanimal_name != "": raise ValueError( @@ -1096,7 +1097,7 @@ def create_training_dataset( if len(trainIndices) != len(testIndices) != len(Shuffles): raise ValueError("Number of Shuffles and train and test indexes should be equal.") splits = [] - for shuffle, (train_inds, test_inds) in enumerate(zip(trainIndices, testIndices)): + for shuffle, (train_inds, test_inds) in enumerate(zip(trainIndices, testIndices, strict=False)): trainFraction = round(len(train_inds) * 1.0 / (len(train_inds) + len(test_inds)), 2) print(f"You passed a split with the following fraction: {int(100 * trainFraction)}%") # Now that the training fraction is guaranteed to be correct, @@ -1297,7 +1298,7 @@ def get_existing_shuffle_indices( cfg: dict | str | Path, train_fraction: float | None = None, engine: Engine | None = None, -) -> List[int]: +) -> list[int]: """ Args: cfg: The content of a project configuration file, or the path to the project @@ -1396,8 +1397,8 @@ def create_training_model_comparison( config, trainindex=0, num_shuffles=1, - net_types=["resnet_50"], - augmenter_types=["imgaug"], + net_types=None, + augmenter_types=None, userfeedback=False, windows2linux=False, ): @@ -1487,12 +1488,17 @@ def create_training_model_comparison( of how to use ``shuffle_list``. """ # read cfg file + if augmenter_types is None: + augmenter_types = ["imgaug"] + if net_types is None: + net_types = ["resnet_50"] cfg = auxiliaryfunctions.read_config(config) if windows2linux: warnings.warn( "`windows2linux` has no effect since 2.2.0.4 and will be removed in 2.2.1.", FutureWarning, + stacklevel=2, ) # create log file diff --git a/deeplabcut/gui/components.py b/deeplabcut/gui/components.py index 0a99c94c87..6f93bf88f5 100644 --- a/deeplabcut/gui/components.py +++ b/deeplabcut/gui/components.py @@ -11,6 +11,7 @@ from __future__ import annotations import os +from pathlib import Path from PySide6 import QtWidgets from PySide6.QtCore import Qt, Slot @@ -19,7 +20,6 @@ from deeplabcut.core.config import read_config_as_dict from deeplabcut.gui.dlc_params import DLCParams from deeplabcut.gui.widgets import ConfigEditor -from pathlib import Path def _create_label_widget( @@ -236,7 +236,7 @@ def update_videos(self): def clear_selected_videos(self): self.root.clear_video_files() - self.root.logger.info(f"Cleared selected videos") + self.root.logger.info("Cleared selected videos") class SnapshotSelectionWidget(QtWidgets.QWidget): @@ -377,7 +377,7 @@ def _is_model_bu(selected_conditions) -> bool: if selected_filter.startswith(snapshots_label) and selected_conditions: if not _is_model_bu(selected_conditions): msg = _create_message_box( - f"Invalid conditions", + "Invalid conditions", ( f"The selected snapshot ({selected_conditions}) cannot be " "used as conditions because it is not a Bottom-Up model." diff --git a/deeplabcut/gui/displays/selected_shuffle_display.py b/deeplabcut/gui/displays/selected_shuffle_display.py index 0c5925074a..3ce7bf1d27 100644 --- a/deeplabcut/gui/displays/selected_shuffle_display.py +++ b/deeplabcut/gui/displays/selected_shuffle_display.py @@ -11,6 +11,7 @@ """Module to display information about the selected shuffle in the GUI""" from __future__ import annotations + from pathlib import Path import PySide6.QtCore as QtCore @@ -67,7 +68,7 @@ def _update_display(self, new_index: int) -> None: try: pose_cfg_path = Path(self.root.pose_cfg_path) - except ValueError as err: + except ValueError: self._set_text_error(f"Failed to read shuffle {self._current_index} - check that it exists!") return except ModuleNotFoundError as err: @@ -98,7 +99,7 @@ def _set_text(self) -> None: text = f"net type: {self._net_type} | engine: {engine_str}" if self._engine == Engine.PYTORCH and self._is_top_down: - text += f" | top-down" + text += " | top-down" style = f"margin: 0px 0px {self._row_margin}px 0px;" if self._engine != self.root.engine: diff --git a/deeplabcut/gui/displays/shuffle_metadata_viewer.py b/deeplabcut/gui/displays/shuffle_metadata_viewer.py index 220d4c2a20..911e91a2c2 100644 --- a/deeplabcut/gui/displays/shuffle_metadata_viewer.py +++ b/deeplabcut/gui/displays/shuffle_metadata_viewer.py @@ -57,7 +57,7 @@ def _load_metadata(cfg: dict) -> list[str]: trainset_meta = metadata.TrainingDatasetMetadata.create(cfg) trainset_meta.save() - with open(metadata_path, "r") as file: + with open(metadata_path) as file: raw_metadata = file.read() return raw_metadata.split("\n") diff --git a/deeplabcut/gui/launch_script.py b/deeplabcut/gui/launch_script.py index 65d51d2834..698070c0e0 100644 --- a/deeplabcut/gui/launch_script.py +++ b/deeplabcut/gui/launch_script.py @@ -19,16 +19,16 @@ """ -import sys import os -import logging +import sys import PySide6.QtWidgets as QtWidgets import qdarkstyle -from deeplabcut.gui import BASE_DIR from PySide6.QtCore import Qt from PySide6.QtGui import QIcon, QPixmap +from deeplabcut.gui import BASE_DIR + def launch_dlc(): app = QtWidgets.QApplication(sys.argv) @@ -41,7 +41,7 @@ def launch_dlc(): splash.show() stylefile = os.path.join(BASE_DIR, "style.qss") - with open(stylefile, "r") as f: + with open(stylefile) as f: app.setStyleSheet(f.read()) dark_stylesheet = qdarkstyle.load_stylesheet_pyside2() diff --git a/deeplabcut/gui/tabs/analyze_videos.py b/deeplabcut/gui/tabs/analyze_videos.py index 1ba43f7d4c..52e9194d98 100644 --- a/deeplabcut/gui/tabs/analyze_videos.py +++ b/deeplabcut/gui/tabs/analyze_videos.py @@ -9,30 +9,30 @@ # Licensed under GNU Lesser General Public License v3.0 # from functools import partial + from PySide6 import QtWidgets from PySide6.QtCore import Qt -from deeplabcut.gui.utils import move_to_separate_thread -from deeplabcut.gui.widgets import ConfigEditor +import deeplabcut from deeplabcut.gui.components import ( - DefaultTab, BodypartListWidget, + DefaultTab, ShuffleSpinBox, VideoSelectionWidget, _create_grid_layout, - _create_label_widget, _create_horizontal_layout, + _create_label_widget, _create_vertical_layout, ) - -import deeplabcut -from deeplabcut.utils.auxiliaryfunctions import edit_config +from deeplabcut.gui.utils import move_to_separate_thread +from deeplabcut.gui.widgets import ConfigEditor from deeplabcut.utils import auxfun_multianimal +from deeplabcut.utils.auxiliaryfunctions import edit_config class AnalyzeVideos(DefaultTab): def __init__(self, root, parent, h1_description): - super(AnalyzeVideos, self).__init__(root, parent, h1_description) + super().__init__(root, parent, h1_description) self._set_page() diff --git a/deeplabcut/gui/tabs/create_project.py b/deeplabcut/gui/tabs/create_project.py index c06f1aa914..9c2fe96c40 100644 --- a/deeplabcut/gui/tabs/create_project.py +++ b/deeplabcut/gui/tabs/create_project.py @@ -17,12 +17,12 @@ from deeplabcut.create_project import create_new_project, create_new_project_3d from deeplabcut.gui import BASE_DIR from deeplabcut.gui.dlc_params import DLCParams -from deeplabcut.gui.widgets import ClickableLabel, ItemSelectionFrame from deeplabcut.gui.tabs.docs import ( URL_3D, URL_MA_CONFIGURE, URL_USE_GUIDE_SCENARIO, ) +from deeplabcut.gui.widgets import ClickableLabel, ItemSelectionFrame from deeplabcut.utils import auxiliaryfunctions @@ -30,7 +30,7 @@ class DynamicTextList(QtWidgets.QWidget): """Dynamically add text entries""" def __init__(self, label_text="bodyparts", parent=None): - super(DynamicTextList, self).__init__(parent) + super().__init__(parent) self.label_text = label_text self.layout = QtWidgets.QVBoxLayout(self) self.layout.setContentsMargins(0, 0, 0, 0) @@ -180,7 +180,7 @@ class ProjectCreator(QtWidgets.QDialog): """Project creation dialog""" def __init__(self, parent): - super(ProjectCreator, self).__init__(parent) + super().__init__(parent) self.parent = parent self.setWindowTitle("New Project") self.setModal(True) @@ -509,7 +509,7 @@ def finalize_project(self): self.parent.load_config(config) self.parent._update_project_state(config=config, loaded=True) except FileExistsError: - print('Project "{}" already exists!'.format(self.proj_default)) + print(f'Project "{self.proj_default}" already exists!') return msg = QtWidgets.QMessageBox(text="New project created") diff --git a/deeplabcut/gui/tabs/create_training_dataset.py b/deeplabcut/gui/tabs/create_training_dataset.py index f22ce90e83..2d75960588 100644 --- a/deeplabcut/gui/tabs/create_training_dataset.py +++ b/deeplabcut/gui/tabs/create_training_dataset.py @@ -11,8 +11,8 @@ from __future__ import annotations import os -from pathlib import Path import re +from pathlib import Path import dlclibrary from PySide6 import QtWidgets @@ -25,14 +25,14 @@ from deeplabcut.generate_training_dataset import get_existing_shuffle_indices from deeplabcut.generate_training_dataset.metadata import get_shuffle_engine from deeplabcut.gui.components import ( + ConditionsSelectionWidget, DefaultTab, ShuffleSpinBox, - ConditionsSelectionWidget, + _create_confirmation_box, _create_grid_layout, _create_label_widget, - set_combo_items, _create_message_box, - _create_confirmation_box, + set_combo_items, ) from deeplabcut.gui.displays.shuffle_metadata_viewer import ShuffleMetadataViewer from deeplabcut.gui.dlc_params import DLCParams @@ -40,8 +40,8 @@ from deeplabcut.modelzoo import build_weight_init from deeplabcut.pose_estimation_pytorch import ( available_models, - is_model_top_down, is_model_cond_top_down, + is_model_top_down, ) from deeplabcut.utils.auxiliaryfunctions import ( get_data_and_metadata_filenames, @@ -51,7 +51,7 @@ class CreateTrainingDataset(DefaultTab): def __init__(self, root, parent, h1_description): - super(CreateTrainingDataset, self).__init__(root, parent, h1_description) + super().__init__(root, parent, h1_description) self.model_comparison = False @@ -217,7 +217,7 @@ def create_training_dataset(self): return else: msg = _create_message_box( - f"The training dataset could not be created.", + "The training dataset could not be created.", ( f"Shuffle {shuffle} already exists - you can create a new " "training dataset with an unused shuffle index (existing " @@ -306,7 +306,7 @@ def create_training_dataset(self): ) except ValueError as err: msg = _create_message_box( - f"The training dataset could not be created.", + "The training dataset could not be created.", str(err), ) msg.exec_() @@ -323,7 +323,7 @@ def create_training_dataset(self): " Apple Silicon:\n" " pip install 'deeplabcut[apple_mchips]'" ) - msg = _create_message_box(f"The training dataset could not be created.", info_text) + msg = _create_message_box("The training dataset could not be created.", info_text) msg.exec_() return diff --git a/deeplabcut/gui/tabs/create_videos.py b/deeplabcut/gui/tabs/create_videos.py index 2a60f944f4..2bd846f9a5 100644 --- a/deeplabcut/gui/tabs/create_videos.py +++ b/deeplabcut/gui/tabs/create_videos.py @@ -11,6 +11,7 @@ from PySide6 import QtWidgets from PySide6.QtCore import Qt +import deeplabcut from deeplabcut.gui.components import ( BodypartListWidget, DefaultTab, @@ -21,12 +22,10 @@ _create_vertical_layout, ) -import deeplabcut - class CreateVideos(DefaultTab): def __init__(self, root, parent, h1_description): - super(CreateVideos, self).__init__(root, parent, h1_description) + super().__init__(root, parent, h1_description) self.bodyparts_to_use = self.root.all_bodyparts self._set_page() diff --git a/deeplabcut/gui/tabs/evaluate_network.py b/deeplabcut/gui/tabs/evaluate_network.py index e3bcf402dc..1c6ff500db 100644 --- a/deeplabcut/gui/tabs/evaluate_network.py +++ b/deeplabcut/gui/tabs/evaluate_network.py @@ -11,18 +11,18 @@ from __future__ import annotations import os +from pathlib import Path + import matplotlib.image as mpimg from matplotlib.backends.backend_qt5agg import ( FigureCanvasQTAgg as FigureCanvas, ) from matplotlib.figure import Figure -from pathlib import Path from PySide6 import QtWidgets from PySide6.QtCore import Qt, Slot import deeplabcut from deeplabcut.core.engine import Engine -from deeplabcut.gui.displays.selected_shuffle_display import SelectedShuffleDisplay from deeplabcut.gui.components import ( BodypartListWidget, DefaultTab, @@ -31,6 +31,7 @@ _create_label_widget, _create_vertical_layout, ) +from deeplabcut.gui.displays.selected_shuffle_display import SelectedShuffleDisplay from deeplabcut.gui.widgets import ConfigEditor, launch_napari from deeplabcut.utils import auxiliaryfunctions @@ -55,7 +56,7 @@ def __init__(self, image_paths, parent=None): class EvaluateNetwork(DefaultTab): def __init__(self, root, parent, h1_description): - super(EvaluateNetwork, self).__init__(root, parent, h1_description) + super().__init__(root, parent, h1_description) self.bodyparts_to_use = self.root.all_bodyparts diff --git a/deeplabcut/gui/tabs/extract_frames.py b/deeplabcut/gui/tabs/extract_frames.py index 3602456a08..40beaf5366 100644 --- a/deeplabcut/gui/tabs/extract_frames.py +++ b/deeplabcut/gui/tabs/extract_frames.py @@ -10,21 +10,20 @@ # from functools import partial from pathlib import Path -from typing import Union from PySide6 import QtWidgets from PySide6.QtCore import Qt -from deeplabcut.gui.dlc_params import DLCParams +from deeplabcut.generate_training_dataset import extract_frames from deeplabcut.gui.components import ( DefaultTab, VideoSelectionWidget, _create_grid_layout, _create_label_widget, ) +from deeplabcut.gui.dlc_params import DLCParams from deeplabcut.gui.utils import move_to_separate_thread from deeplabcut.gui.widgets import launch_napari -from deeplabcut.generate_training_dataset import extract_frames def select_cropping_area(config, videos=None): @@ -48,8 +47,8 @@ def select_cropping_area(config, videos=None): cfg : dict Updated project configuration """ - from deeplabcut.utils import auxiliaryfunctions from deeplabcut.gui.widgets import FrameCropper + from deeplabcut.utils import auxiliaryfunctions cfg = auxiliaryfunctions.read_config(config) if videos is None: @@ -83,7 +82,7 @@ def select_cropping_area(config, videos=None): class ExtractFrames(DefaultTab): def __init__(self, root, parent, h1_description): - super(ExtractFrames, self).__init__(root, parent, h1_description) + super().__init__(root, parent, h1_description) self.worker = None self.thread = None self._set_page() @@ -268,7 +267,7 @@ def _show_success_message(self): msg.exec_() self.root.writer.write(root_message) - def _check_symlink(self, video_path: Union[str, Path]) -> Path: + def _check_symlink(self, video_path: str | Path) -> Path: """Checks that a video is in the DeepLabCut 'videos' folder This is required before launching manual frame extraction. When users select diff --git a/deeplabcut/gui/tabs/extract_outlier_frames.py b/deeplabcut/gui/tabs/extract_outlier_frames.py index d4ed23d4bf..c43a4bae4e 100644 --- a/deeplabcut/gui/tabs/extract_outlier_frames.py +++ b/deeplabcut/gui/tabs/extract_outlier_frames.py @@ -11,7 +11,7 @@ from PySide6 import QtWidgets from PySide6.QtCore import Qt -from deeplabcut.gui.dlc_params import DLCParams +import deeplabcut from deeplabcut.gui.components import ( DefaultTab, ShuffleSpinBox, @@ -19,14 +19,13 @@ _create_horizontal_layout, _create_label_widget, ) +from deeplabcut.gui.dlc_params import DLCParams from deeplabcut.gui.widgets import launch_napari -import deeplabcut - class ExtractOutlierFrames(DefaultTab): def __init__(self, root, parent, h1_description): - super(ExtractOutlierFrames, self).__init__(root, parent, h1_description) + super().__init__(root, parent, h1_description) self.filelist = [] self._set_page() diff --git a/deeplabcut/gui/tabs/label_frames.py b/deeplabcut/gui/tabs/label_frames.py index 13b0de7056..509b04fcd3 100644 --- a/deeplabcut/gui/tabs/label_frames.py +++ b/deeplabcut/gui/tabs/label_frames.py @@ -100,7 +100,7 @@ def label_frames(config_path: str | Path | None = None, image_folder: str | None class LabelFrames(DefaultTab): def __init__(self, root, parent, h1_description): - super(LabelFrames, self).__init__(root, parent, h1_description) + super().__init__(root, parent, h1_description) self._set_page() diff --git a/deeplabcut/gui/tabs/manage_project.py b/deeplabcut/gui/tabs/manage_project.py index 932adcf827..b4d05e7d2a 100644 --- a/deeplabcut/gui/tabs/manage_project.py +++ b/deeplabcut/gui/tabs/manage_project.py @@ -9,16 +9,18 @@ # Licensed under GNU Lesser General Public License v3.0 # import os + from PySide6.QtCore import Qt from PySide6.QtWidgets import ( - QPushButton, QFileDialog, QLabel, QLineEdit, + QPushButton, ) + from deeplabcut.create_project import add_new_videos -from deeplabcut.gui.dlc_params import DLCParams from deeplabcut.gui.components import DefaultTab, _create_horizontal_layout +from deeplabcut.gui.dlc_params import DLCParams from deeplabcut.gui.widgets import ConfigEditor diff --git a/deeplabcut/gui/tabs/modelzoo.py b/deeplabcut/gui/tabs/modelzoo.py index e74c43f7a1..cbff89e2e0 100644 --- a/deeplabcut/gui/tabs/modelzoo.py +++ b/deeplabcut/gui/tabs/modelzoo.py @@ -15,21 +15,19 @@ import dlclibrary from PySide6 import QtWidgets -from PySide6.QtCore import QRegularExpression, Qt, QTimer, Signal, Slot, QSize +from PySide6.QtCore import QRegularExpression, QSize, Qt, QTimer, Signal, Slot from PySide6.QtGui import QIcon, QPixmap, QRegularExpressionValidator -import cv2 -import torch import deeplabcut from deeplabcut.core.engine import Engine from deeplabcut.gui import BASE_DIR from deeplabcut.gui.components import ( - _create_grid_layout, - _create_label_widget, DefaultTab, VideoSelectionWidget, - set_layout_contents_visible, + _create_grid_layout, + _create_label_widget, set_combo_items, + set_layout_contents_visible, ) from deeplabcut.gui.utils import move_to_separate_thread from deeplabcut.gui.widgets import ClickableLabel @@ -102,7 +100,9 @@ def _set_page(self): self.main_layout.addWidget(self.help_button, alignment=Qt.AlignLeft) self.go_to_button = QtWidgets.QPushButton("Read Documentation") - # go to url https://deeplabcut.github.io/DeepLabCut/docs/ModelZoo.html#about-the-superanimal-models when button is clicked + # go to url + # https://deeplabcut.github.io/DeepLabCut/docs/ModelZoo.html#about-the-superanimal-models + # when button is clicked self.go_to_button.clicked.connect( lambda: webbrowser.open( "https://deeplabcut.github.io/DeepLabCut/docs/ModelZoo.html#about-the-superanimal-models" @@ -458,7 +458,7 @@ def run_video_inference_superanimal(self): self.thread.start() else: print(f"Calling video_inference_superanimal with kwargs={kwargs}") - results = deeplabcut.video_inference_superanimal( + deeplabcut.video_inference_superanimal( files, supermodel_name, dest_folder=self._destfolder, @@ -494,13 +494,13 @@ def signal_analysis_complete(self): # Show appropriate message if videos_created: msg = QtWidgets.QMessageBox( - text=f"SuperAnimal video inference complete!\n\nCreated labeled videos:\n" + "\n".join(videos_created) + text="SuperAnimal video inference complete!\n\nCreated labeled videos:\n" + "\n".join(videos_created) ) msg.setIcon(QtWidgets.QMessageBox.Information) msg.exec_() else: msg = QtWidgets.QMessageBox( - text=f"SuperAnimal video inference complete, but no labeled videos were created." + text="SuperAnimal video inference complete, but no labeled videos were created." ) msg.setIcon(QtWidgets.QMessageBox.Warning) msg.exec_() @@ -587,8 +587,8 @@ def _update_detectors(self, super_animal: str) -> None: set_layout_contents_visible(self.detector_row, self.root.engine == Engine.PYTORCH) def _update_adaptation_detector_visibility(self, superanimal: str): - self.adapt_det_epoch_label.setVisible((superanimal != "superanimal_humanbody")) - self.torch_adapt_det_epoch_spinbox.setVisible((superanimal != "superanimal_humanbody")) + self.adapt_det_epoch_label.setVisible(superanimal != "superanimal_humanbody") + self.torch_adapt_det_epoch_spinbox.setVisible(superanimal != "superanimal_humanbody") @Slot(Engine) def _on_engine_change(self, engine: Engine) -> None: diff --git a/deeplabcut/gui/tabs/open_project.py b/deeplabcut/gui/tabs/open_project.py index 42985be676..c21cfc78cc 100644 --- a/deeplabcut/gui/tabs/open_project.py +++ b/deeplabcut/gui/tabs/open_project.py @@ -10,14 +10,13 @@ # import os -from PySide6 import QtWidgets, QtCore +from PySide6 import QtCore, QtWidgets from PySide6.QtGui import QIcon -from PySide6.QtWidgets import QCheckBox class OpenProject(QtWidgets.QDialog): def __init__(self, parent): - super(OpenProject, self).__init__(parent) + super().__init__(parent) self.setWindowTitle("Load Existing Project") diff --git a/deeplabcut/gui/tabs/refine_tracklets.py b/deeplabcut/gui/tabs/refine_tracklets.py index 004f4492f4..32c60d041c 100644 --- a/deeplabcut/gui/tabs/refine_tracklets.py +++ b/deeplabcut/gui/tabs/refine_tracklets.py @@ -10,10 +10,12 @@ # import os from pathlib import Path + from PySide6 import QtWidgets from PySide6.QtCore import Qt -from deeplabcut.gui.widgets import ConfigEditor +import deeplabcut +from deeplabcut.core import trackingutils from deeplabcut.gui.components import ( DefaultTab, ShuffleSpinBox, @@ -22,15 +24,13 @@ _create_horizontal_layout, _create_label_widget, ) - -import deeplabcut -from deeplabcut.core import trackingutils +from deeplabcut.gui.widgets import ConfigEditor from deeplabcut.utils.auxiliaryfunctions import GetScorerName class RefineTracklets(DefaultTab): def __init__(self, root, parent, h1_description): - super(RefineTracklets, self).__init__(root, parent, h1_description) + super().__init__(root, parent, h1_description) self._set_page() @property diff --git a/deeplabcut/gui/tabs/train_network.py b/deeplabcut/gui/tabs/train_network.py index 5957d0673d..c18e09676f 100644 --- a/deeplabcut/gui/tabs/train_network.py +++ b/deeplabcut/gui/tabs/train_network.py @@ -49,7 +49,7 @@ class TrainAttributeRow: class TrainNetwork(DefaultTab): def __init__(self, root, parent, h1_description): - super(TrainNetwork, self).__init__(root, parent, h1_description) + super().__init__(root, parent, h1_description) self._shuffle: ShuffleSpinBox = ShuffleSpinBox(root=self.root, parent=self) self._shuffle_display = SelectedShuffleDisplay(self.root) diff --git a/deeplabcut/gui/tabs/unsupervised_id_tracking.py b/deeplabcut/gui/tabs/unsupervised_id_tracking.py index 044b0bdabc..e8d57f027b 100644 --- a/deeplabcut/gui/tabs/unsupervised_id_tracking.py +++ b/deeplabcut/gui/tabs/unsupervised_id_tracking.py @@ -9,9 +9,11 @@ # Licensed under GNU Lesser General Public License v3.0 # from functools import partial + from PySide6 import QtWidgets from PySide6.QtCore import Qt +import deeplabcut from deeplabcut.gui.components import ( DefaultTab, ShuffleSpinBox, @@ -21,12 +23,10 @@ ) from deeplabcut.gui.utils import move_to_separate_thread -import deeplabcut - class UnsupervizedIdTracking(DefaultTab): def __init__(self, root, parent, h1_description): - super(UnsupervizedIdTracking, self).__init__(root, parent, h1_description) + super().__init__(root, parent, h1_description) self._set_page() diff --git a/deeplabcut/gui/tabs/video_editor.py b/deeplabcut/gui/tabs/video_editor.py index e0d8c75d03..dc7e8472e2 100644 --- a/deeplabcut/gui/tabs/video_editor.py +++ b/deeplabcut/gui/tabs/video_editor.py @@ -25,7 +25,7 @@ class VideoEditor(DefaultTab): def __init__(self, root, parent, h1_description): - super(VideoEditor, self).__init__(root, parent, h1_description) + super().__init__(root, parent, h1_description) self._set_page() diff --git a/deeplabcut/gui/tracklet_toolbox.py b/deeplabcut/gui/tracklet_toolbox.py index b942ebf0af..ec76577f22 100644 --- a/deeplabcut/gui/tracklet_toolbox.py +++ b/deeplabcut/gui/tracklet_toolbox.py @@ -8,20 +8,22 @@ # # Licensed under GNU Lesser General Public License v3.0 # +from threading import Event + import matplotlib.patches as patches import matplotlib.pyplot as plt import matplotlib.transforms as mtransforms import numpy as np import pandas as pd -from threading import Event +from matplotlib.path import Path +from matplotlib.widgets import Button, CheckButtons, LassoSelector, Slider, TextBox +from PySide6.QtCore import QMutex +from PySide6.QtWidgets import QMessageBox + from deeplabcut.gui.utils import move_to_separate_thread from deeplabcut.refine_training_dataset.tracklets import TrackletManager from deeplabcut.utils.auxfun_videos import VideoReader from deeplabcut.utils.auxiliaryfunctions import attempt_to_make_folder -from matplotlib.path import Path -from matplotlib.widgets import Slider, LassoSelector, Button, CheckButtons, TextBox -from PySide6.QtWidgets import QMessageBox -from PySide6.QtCore import QMutex class DraggablePoint: @@ -767,7 +769,7 @@ def display_traces(self, only_picked=True): inds = self.picked + list(self.picked_pair) else: inds = self.manager.swapping_bodyparts - for n, (line_x, line_y) in enumerate(zip(self.lines_x, self.lines_y)): + for n, (line_x, line_y) in enumerate(zip(self.lines_x, self.lines_y, strict=False)): if n in inds: line_x.set_data(self.manager.times, self.manager.xy[n, :, 0]) line_y.set_data(self.manager.times, self.manager.xy[n, :, 1]) @@ -839,6 +841,7 @@ def save(self, *args): def export_to_training_data(self, pcutoff=0.1): import os + from skimage import io inds = self.manager.find_edited_frames() @@ -907,7 +910,8 @@ def filter_low_prob(cols, prob): ) df_orig = pd.read_hdf(output_path) df_joint = pd.concat([df, df_orig]) - # Now drop redundant ones keeping the first one [this will make sure that the refined machine file gets preference] + # Now drop redundant ones keeping the first one [this will make sure that + # the refined machine file gets preference] df_joint = df_joint[~df_joint.index.duplicated(keep="first")] df_joint.sort_index(inplace=True) df_joint.to_hdf(output_path, key="df_with_missing", mode="w") diff --git a/deeplabcut/gui/utils.py b/deeplabcut/gui/utils.py index 1de8de9c24..852175c7fa 100644 --- a/deeplabcut/gui/utils.py +++ b/deeplabcut/gui/utils.py @@ -8,10 +8,10 @@ # # Licensed under GNU Lesser General Public License v3.0 # -from typing import Callable, Tuple +import re +from collections.abc import Callable from PySide6 import QtCore -import re class Worker(QtCore.QObject): @@ -57,7 +57,7 @@ def stop_thread(): return worker, thread -def parse_version(version: str) -> Tuple[int, int, int]: +def parse_version(version: str) -> tuple[int, int, int]: """ Parses a version string into a tuple of (major, minor, patch). """ @@ -71,6 +71,7 @@ def parse_version(version: str) -> Tuple[int, int, int]: def is_latest_deeplabcut_version(): import json import urllib.request + from deeplabcut import VERSION url = "https://pypi.org/pypi/deeplabcut/json" diff --git a/deeplabcut/gui/widgets.py b/deeplabcut/gui/widgets.py index f6b17cc407..69cbc0b0da 100644 --- a/deeplabcut/gui/widgets.py +++ b/deeplabcut/gui/widgets.py @@ -11,22 +11,24 @@ import ast import os import warnings +from queue import Queue import matplotlib.colors as mcolors import napari import numpy as np import pandas as pd -from matplotlib.collections import LineCollection -from matplotlib.path import Path from matplotlib.backends.backend_qt5agg import ( - NavigationToolbar2QT, FigureCanvasQTAgg as FigureCanvas, ) +from matplotlib.backends.backend_qt5agg import ( + NavigationToolbar2QT, +) +from matplotlib.collections import LineCollection from matplotlib.figure import Figure -from matplotlib.widgets import RectangleSelector, Button, LassoSelector -from queue import Queue +from matplotlib.path import Path +from matplotlib.widgets import Button, LassoSelector, RectangleSelector from PySide6 import QtCore, QtWidgets -from PySide6.QtGui import QStandardItemModel, QStandardItem, QCursor, QAction +from PySide6.QtGui import QAction, QCursor, QStandardItem, QStandardItemModel from scipy.spatial import cKDTree as KDTree from skimage import io @@ -73,7 +75,7 @@ def resetView(self): class DragDropListView(QtWidgets.QListView): def __init__(self, parent=None): - super(DragDropListView, self).__init__(parent) + super().__init__(parent) self.parent = parent self.setAcceptDrops(True) self.setDropIndicatorShown(True) @@ -128,7 +130,7 @@ def dropEvent(self, event): class ItemSelectionFrame(QtWidgets.QFrame): def __init__(self, items, parent=None): - super(ItemSelectionFrame, self).__init__(parent) + super().__init__(parent) self.setFrameShape(self.Shape.StyledPanel) self.setLineWidth(0) @@ -181,7 +183,7 @@ def set_message(self, msg): pass def release_zoom(self, event): - super(NavigationToolbar, self).release_zoom(event) + super().release_zoom(event) self.zoom() @@ -201,7 +203,7 @@ class StreamReceiver(QtCore.QThread): new_text = QtCore.Signal(str) def __init__(self, queue): - super(StreamReceiver, self).__init__() + super().__init__() self.queue = queue def run(self): @@ -214,7 +216,7 @@ class ClickableLabel(QtWidgets.QLabel): signal = QtCore.Signal() def __init__(self, text="", color="turquoise", parent=None): - super(ClickableLabel, self).__init__(text, parent) + super().__init__(text, parent) self._default_style = self.styleSheet() self.color = color self.setStyleSheet(f"color: {self.color}") @@ -235,7 +237,7 @@ class ItemCreator(QtWidgets.QDialog): created = QtCore.Signal(QtWidgets.QTreeWidgetItem) def __init__(self, parent=None): - super(ItemCreator, self).__init__(parent) + super().__init__(parent) self.parent = parent vbox = QtWidgets.QVBoxLayout(self) self.field1 = QtWidgets.QLineEdit(self) @@ -263,7 +265,7 @@ def form_item(self): # TODO Insert skeleton link class ContextMenu(QtWidgets.QMenu): def __init__(self, parent): - super(ContextMenu, self).__init__(parent) + super().__init__(parent) self.parent = parent self.current_item = parent.tree.currentItem() insert = QAction("Insert", self) @@ -287,7 +289,7 @@ def fix_path(self): class DictViewer(QtWidgets.QWidget): def __init__(self, cfg, filename="", parent=None): - super(DictViewer, self).__init__(parent) + super().__init__(parent) self.cfg = cfg self.filename = filename self.parent = parent @@ -435,7 +437,7 @@ def add_row(self, key, val, tree_widget): class ConfigEditor(QtWidgets.QDialog): def __init__(self, config, parent=None): - super(ConfigEditor, self).__init__(parent) + super().__init__(parent) self.config = config if config.endswith("config.yaml") and not config.endswith("pytorch_config.yaml"): self.read_func = auxiliaryfunctions.read_config @@ -470,12 +472,12 @@ def keyPressEvent(self, e): def accept(self): self.write_func(self.config, self.cfg) - super(ConfigEditor, self).accept() + super().accept() class FrameCropper(QtWidgets.QDialog): def __init__(self, video, parent=None): - super(FrameCropper, self).__init__(parent) + super().__init__(parent) self.clip = VideoWriter(video) self.fig = Figure() @@ -530,7 +532,7 @@ def display_help(self, *args): class SkeletonBuilder(QtWidgets.QDialog): def __init__(self, config_path, parent=None): - super(SkeletonBuilder, self).__init__(parent) + super().__init__(parent) self.config_path = config_path self.cfg = auxiliaryfunctions.read_config(config_path) # Find uncropped labeled data @@ -550,13 +552,14 @@ def __init__(self, config_path, parent=None): found = True break if self.df is None: - raise IOError("No labeled data were found.") + raise OSError("No labeled data were found.") self.bpts = self.df.columns.get_level_values("bodyparts").unique() if not found: warnings.warn( f"A fully labeled animal could not be found. " - f"{', '.join(self.bpts[missing])} will need to be manually connected in the config.yaml." + f"{', '.join(self.bpts[missing])} will need to be manually connected in the config.yaml.", + stacklevel=2, ) self.tree = KDTree(self.xy) # Handle image previously annotated on a different platform @@ -639,7 +642,8 @@ def export(self, *args): unconnected = [i for i in range(len(self.xy)) if i not in inds_flat] if len(unconnected): warnings.warn( - f"You didn't connect all the bodyparts (which is fine!). This is just a note to let you know." + "You didn't connect all the bodyparts (which is fine!). This is just a note to let you know.", + stacklevel=2, ) self.cfg["skeleton"] = [tuple(self.bpts[list(pair)]) for pair in self.inds] auxiliaryfunctions.write_config(self.config_path, self.cfg) @@ -658,7 +662,7 @@ def on_select(self, verts): for lst in inds: if len(lst) and lst[0] not in inds_unique: inds_unique.append(lst[0]) - for pair in zip(inds_unique, inds_unique[1:]): + for pair in zip(inds_unique, inds_unique[1:], strict=False): pair_sorted = tuple(sorted(pair)) self.inds.add(pair_sorted) self.segs.add(tuple(map(tuple, self.xy[pair_sorted, :]))) diff --git a/deeplabcut/gui/window.py b/deeplabcut/gui/window.py index a503923b87..9ab2ff52e6 100644 --- a/deeplabcut/gui/window.py +++ b/deeplabcut/gui/window.py @@ -8,39 +8,38 @@ # # Licensed under GNU Lesser General Public License v3.0 # -import os import logging +import os import subprocess import sys +import warnings from functools import cached_property +from importlib.resources import files from pathlib import Path -from typing import List from urllib.error import URLError -import warnings + import qdarkstyle -from importlib.resources import files +from napari_deeplabcut import misc +from PySide6 import QtCore, QtGui, QtWidgets +from PySide6.QtCore import Qt, QTimer +from PySide6.QtGui import QAction, QIcon, QPixmap +from PySide6.QtWidgets import ( + QComboBox, + QLabel, + QMainWindow, + QMenu, + QMessageBox, + QSizePolicy, + QWidget, +) import deeplabcut -from deeplabcut import auxiliaryfunctions, VERSION, compat +from deeplabcut import VERSION, auxiliaryfunctions, compat from deeplabcut.core.engine import Engine from deeplabcut.gui import BASE_DIR, components, utils from deeplabcut.gui.tabs import * from deeplabcut.gui.widgets import StreamReceiver, StreamWriter from deeplabcut.utils.multiprocessing import call_with_timeout -from napari_deeplabcut import misc -from PySide6.QtWidgets import ( - QMessageBox, - QMenu, - QWidget, - QMainWindow, - QComboBox, - QLabel, - QSizePolicy, -) -from PySide6 import QtCore -from PySide6.QtGui import QIcon, QAction, QPixmap -from PySide6 import QtWidgets, QtGui -from PySide6.QtCore import Qt, QTimer warnings.filterwarnings( "ignore", @@ -59,7 +58,7 @@ def _check_for_updates(silent=True): if is_latest and is_latest_plugin: if not silent: msg = QtWidgets.QMessageBox( - text=f"DeepLabCut is up-to-date", + text="DeepLabCut is up-to-date", ) msg.exec_() else: @@ -94,7 +93,7 @@ class MainWindow(QMainWindow): shuffle_created = QtCore.Signal(int) def __init__(self, app): - super(MainWindow, self).__init__() + super().__init__() self.app = app screen_size = app.screens()[0].size() self.screen_width = screen_size.width() @@ -229,14 +228,14 @@ def is_multianimal(self) -> bool: return bool(self.cfg.get("multianimalproject")) @property - def all_bodyparts(self) -> List: + def all_bodyparts(self) -> list: if self.is_multianimal: return self.cfg.get("multianimalbodyparts") else: return self.cfg["bodyparts"] @property - def all_individuals(self) -> List: + def all_individuals(self) -> list: if self.is_multianimal: return self.cfg.get("individuals") else: diff --git a/deeplabcut/modelzoo/fmpose_3d/fmpose3d.py b/deeplabcut/modelzoo/fmpose_3d/fmpose3d.py index c8ceb5ae80..1bbcac3531 100644 --- a/deeplabcut/modelzoo/fmpose_3d/fmpose3d.py +++ b/deeplabcut/modelzoo/fmpose_3d/fmpose3d.py @@ -8,8 +8,8 @@ """ from fmpose3d import ( - FMPose3DInference, FMPose3DConfig, + FMPose3DInference, SupportedModel, ) diff --git a/deeplabcut/modelzoo/generalized_data_converter/__init__.py b/deeplabcut/modelzoo/generalized_data_converter/__init__.py index fb1e45d7ba..1c56abbdbf 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/__init__.py +++ b/deeplabcut/modelzoo/generalized_data_converter/__init__.py @@ -8,4 +8,4 @@ # # Licensed under GNU Lesser General Public License v3.0 # -from .utils import add_skeleton, customized_colormap, create_modelprefix +from .utils import add_skeleton, create_modelprefix, customized_colormap diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/__init__.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/__init__.py index 47e42a1bd7..0032f808f3 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/__init__.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/__init__.py @@ -8,10 +8,10 @@ # # Licensed under GNU Lesser General Public License v3.0 # -from .ma_dlc import MaDLCPoseDataset -from .multi import MultiSourceDataset from .coco import COCOPoseDataset +from .ma_dlc import MaDLCPoseDataset +from .ma_dlc_dataframe import MaDLCDataFrame from .materialize import mat_func_factory +from .multi import MultiSourceDataset from .single_dlc import SingleDLCPoseDataset from .single_dlc_dataframe import SingleDLCDataFrame -from .ma_dlc_dataframe import MaDLCDataFrame diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/base_dlc.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/base_dlc.py index b33822bba6..b262980979 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/base_dlc.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/base_dlc.py @@ -20,7 +20,7 @@ class BaseDLCPoseDataset(BasePoseDataset): def __init__(self, proj_root, dataset_name, shuffle=1, modelprefix=""): - super(BaseDLCPoseDataset, self).__init__() + super().__init__() assert proj_root != None and dataset_name != None diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/coco.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/coco.py index 79e10e46a4..2c6200cc93 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/coco.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/coco.py @@ -24,7 +24,7 @@ def __init__( shuffle=None, ): - super(COCOPoseDataset, self).__init__() + super().__init__() self.meta["dataset_name"] = dataset_name self.meta["proj_root"] = proj_root @@ -45,7 +45,7 @@ def __init__( def _load_json(self, json_fn): path = os.path.join(self.proj_root, "annotations", json_fn) - with open(path, "r") as f: + with open(path) as f: json_obj = json.load(f) return json_obj diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc.py index 902312dac9..c88350d4a5 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc.py @@ -24,7 +24,7 @@ class MaDLCPoseDataset(BaseDLCPoseDataset): def __init__(self, proj_root, dataset_name, shuffle=1, modelprefix=""): - super(MaDLCPoseDataset, self).__init__(proj_root, dataset_name, shuffle=shuffle, modelprefix=modelprefix) + super().__init__(proj_root, dataset_name, shuffle=shuffle, modelprefix=modelprefix) def _df2generic(self, df, image_id_offset=0): diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc_dataframe.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc_dataframe.py index 348a687a86..d6ce29a41a 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc_dataframe.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc_dataframe.py @@ -80,8 +80,8 @@ def merge_annotateddatasets(cfg): class MaDLCDataFrame(BasePoseDataset): def __init__(self, proj_root, dataset_name): - super(MaDLCDataFrame, self).__init__() - assert proj_root != None and dataset_name != None + super().__init__() + assert proj_root is not None and dataset_name is not None self.proj_root = proj_root self.dataset_name = dataset_name self.meta["dataset_name"] = dataset_name @@ -181,7 +181,7 @@ def _df2generic(self, df, image_id_offset=0): image_id += 1 - for individual_id, individual in enumerate(individuals): + for _individual_id, individual in enumerate(individuals): category_id = 0 try: kpts = data.xs(individual, level="individuals").to_numpy().reshape((-1, 2)) diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/materialize.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/materialize.py index 78156ad9f3..60961c7579 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/materialize.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/materialize.py @@ -72,43 +72,11 @@ def default(self, obj): elif isinstance(obj, np.ndarray): return obj.tolist() else: - return super(NpEncoder, self).default(obj) + return super().default(obj) class SingleDLC_config: def __init__(self): - Task = "" # could be dataset name - project_path = "" - scorer = "" # random stuff - date = "" # random stuff - video_sets = "" # has to be used for labeled data - skeleton = "" # could be arbitrary - bodyparts = "" # either single or multi - start = 0 # not sure - stop = 1 # not sure - numframes2pick = 42 # does not matter - skeleton_color = "black" - pcutoff = 0.6 - dotsize = 8 - alphavalue = 0.7 - colormap = "rainbow" - TrainingFraction = "" # need to be filled correctly - iteration = 0 - default_net_type = "resnet_50" - default_augmenter = "imgaug" - snapshotindex = -1 - batch_size = 8 - cropping = False - croppedtraining = False - multianimalproject = False - uniquebodyparts = [] - x1 = 0 - x2 = 640 - y1 = 277 - y2 = 624 - corer2move2 = [50, 50] - move2corner = True - identity = False self.cfg = {k: v for k, v in vars().items() if "__" not in k and "self" not in k} def create_cfg(self, proj_root, kwargs): @@ -124,40 +92,6 @@ def __init__(self): Some variables can be configured by the user later """ - Task = "" # could be dataset name - project_path = "" - scorer = "" # random stuff - date = "" # random stuff - video_sets = "" # has to be used for labeled data - individuals = "" # number of individuals - multianimalbodyparts = "" # keypoints - skeleton = "" # could be arbitrary - bodyparts = "" # either single or multi - start = 0 # not sure - stop = 1 # not sure - numframes2pick = 42 # does not matter - skeleton_color = "black" - pcutoff = 0.6 - dotsize = 8 - alphavalue = 0.7 - colormap = "rainbow" - TrainingFraction = "" # need to be filled correctly - iteration = 0 - default_net_type = "resnet_50" - default_augmenter = "multi-animal-imgaug" - snapshotindex = -1 - batch_size = 8 - cropping = False - croppedtraining = True - multianimalproject = True - uniquebodyparts = [] - x1 = 0 - x2 = 640 - y1 = 277 - y2 = 624 - corer2move2 = [50, 50] - move2corner = True - identity = False self.cfg = {k: v for k, v in vars().items() if "__" not in k and "self" not in k} def create_cfg(self, proj_root, kwargs): @@ -236,17 +170,16 @@ def _generic2madlc( # it's important to put train first so the train_fraction parameter can work correctly total_images = train_images + test_images - total_annotations = train_annotations + test_annotations + train_annotations + test_annotations # DLC uses relative dest as index into dataframe imageid2relativedest = {} - count = 0 for image in total_images: image_id = image["id"] file_name = image["file_name"] image_name = file_name.split(os.sep)[-1] pre, suffix = image_name.split(".") - if append_image_id == True: + if append_image_id: dest_image_name = f"{pre}_{image_id}.{suffix}" else: dest_image_name = image_name @@ -261,14 +194,13 @@ def _generic2madlc( else: try: os.symlink(file_name, dest) - except Exception as e: + except Exception: pass relative_dest = os.path.join("labeled-data", dataset_name, dest_image_name) imageid2relativedest[image_id] = relative_dest - temp_count = 0 for dataset_name, dataset in meta["mat_datasets"].items(): dataset_total_images = dataset.generic_train_images + dataset.generic_test_images dataset_total_annotations = dataset.generic_train_annotations + dataset.generic_test_annotations @@ -317,7 +249,9 @@ def _generic2madlc( create_multianimaltraining_dataset(os.path.join(proj_root, "config.yaml"), paf_graph=None) # dlc's merge_annotation messes up my indices, so I will need to overwrite the documentation file - # I could have done it in a more elegant way if I could modify part of DLC source code, but for backward compatibility reasons, overriding documentation is smarter + # I could have done it in a more elegant way if I could modify part of DLC + # source code, but for backward compatibility reasons, overriding + # documentation is smarter config_path = os.path.join(proj_root, "config.yaml") @@ -372,7 +306,7 @@ def _filter(image): # need to overwrite the data pickle file too - nbodyparts = len(bodyparts) + len(bodyparts) if "individuals" not in dlc_df.columns.names: old_idx = dlc_df.columns.to_frame() @@ -437,15 +371,15 @@ def _generic2sdlc( columnindex = pd.MultiIndex.from_product([[scorer], bodyparts, ["x", "y"]], names=["scorer", "bodyparts", "coords"]) total_images = train_images + test_images - total_annotations = train_annotations + test_annotations + train_annotations + test_annotations # DLC uses relative dest as index imageid2relativedest = {} for image in total_images: imageid = image["id"] - filename = image["file_name"] - datasetname = imageid2datasetname[imageid] + image["file_name"] + imageid2datasetname[imageid] count = 0 for image in total_images: image_id = image["id"] @@ -454,7 +388,7 @@ def _generic2sdlc( image_name = file_name.split(os.sep)[-1] pre, suffix = image_name.split(".") - if append_image_id == True: + if append_image_id: dest_image_name = f"{pre}_{image_id}.{suffix}" else: dest_image_name = image_name @@ -485,9 +419,8 @@ def _generic2sdlc( dataset_total_annotations = dataset.generic_train_annotations + dataset.generic_test_annotations dataset_index = [] - freq = {} for image in dataset_total_images: - filename = image["file_name"] + image["file_name"] image_id = image["id"] relative_dest = imageid2relativedest[image_id] @@ -500,7 +433,7 @@ def _generic2sdlc( df = pd.DataFrame(raw_data, columns=columnindex, index=dataset_index) - for idx, anno in enumerate(dataset_total_annotations): + for _idx, anno in enumerate(dataset_total_annotations): keypoints = np.array(anno["keypoints"]) image_id = anno["image_id"] @@ -529,7 +462,9 @@ def _generic2sdlc( create_training_dataset(os.path.join(proj_root, "config.yaml")) # dlc's merge_annotation messes up my indices, so I will need to overwrite the documentation file - # I could have done it in a more elegant way if I could modify part of DLC source code, but for backward compatibility reasons, overriding documentation is smarter + # I could have done it in a more elegant way if I could modify part of DLC + # source code, but for backward compatibility reasons, overriding + # documentation is smarter config_path = os.path.join(proj_root, "config.yaml") @@ -636,7 +571,7 @@ def _generic2coco( annotation["iscrowd"] = 0 keypoints = annotation["keypoints"] - for kpt_id, kpt_name in enumerate(meta["categories"]["keypoints"]): + for kpt_id, _kpt_name in enumerate(meta["categories"]["keypoints"]): coord = keypoints[3 * kpt_id : 3 * kpt_id + 3] if coord[0] < 0 or coord[1] < 0: coord[2] = -1 diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/multi.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/multi.py index a904233354..d89d4c8dd3 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/multi.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/multi.py @@ -40,7 +40,7 @@ def __init__(self, dataset_name, datasets, table_path): names = [] for dataset in datasets: # Must project datasets to same keypoint space before merging - if table_path != None: + if table_path is not None: dataset.project_with_conversion_table(table_path) name = dataset.meta["dataset_name"] names.append(name) @@ -82,7 +82,8 @@ def _build_maps(self): species_set = set() for dataset_name, dataset in self.name2genericdataset.items(): - # I could of course do this during merge to save compute, but doing it here makes the logic cleaner to understand + # I could of course do this during merge to save compute, but doing it + # here makes the logic cleaner to understand total_images = dataset.generic_train_images + dataset.generic_test_images for image in total_images: @@ -118,7 +119,7 @@ def whether_anno_image_match(self, images, annotations): print("images-annotations", image_ids - annotation_image_ids) print("annotations-images", annotation_image_ids - image_ids) - warnings.warn("annotation and image ids do not match") + warnings.warn("annotation and image ids do not match", stacklevel=2) # This is constrain is too hard # assert len(annotation_image_ids - image_ids) == 0, "You can't have annotation on non-existed images" @@ -169,7 +170,7 @@ def _update_imgids(self): from functools import reduce count = 0 - for k, v in dataset_id_pool.items(): + for _k, v in dataset_id_pool.items(): count += len(v) print("size of the summation", count) union = reduce(set.union, dataset_id_pool.values()) @@ -188,7 +189,7 @@ def _merge_datasets(self, name2dataset): merged_train_annotations = [] merged_test_annotations = [] - for dataset_name, dataset in name2dataset.items(): + for _dataset_name, dataset in name2dataset.items(): train_images = dataset.generic_train_images test_images = dataset.generic_test_images train_annotations = dataset.generic_train_annotations diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc.py index 377a362883..c6bff8bee4 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc.py @@ -29,7 +29,7 @@ class SingleDLCPoseDataset(BaseDLCPoseDataset): """ def __init__(self, proj_root, dataset_name, shuffle=1, modelprefix=""): - super(SingleDLCPoseDataset, self).__init__(proj_root, dataset_name, shuffle=shuffle, modelprefix=modelprefix) + super().__init__(proj_root, dataset_name, shuffle=shuffle, modelprefix=modelprefix) # overriding max_individuals self.meta["max_individuals"] = 1 diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc_dataframe.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc_dataframe.py index 8a47185e2d..e99dce2991 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc_dataframe.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc_dataframe.py @@ -80,9 +80,9 @@ def merge_annotateddatasets(cfg): class SingleDLCDataFrame(BasePoseDataset): def __init__(self, proj_root, dataset_name): - super(SingleDLCDataFrame, self).__init__() + super().__init__() self.meta["max_individuals"] = 1 - assert proj_root != None and dataset_name != None + assert proj_root is not None and dataset_name is not None self.proj_root = proj_root self.dataset_name = dataset_name self.meta["dataset_name"] = dataset_name diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/utils.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/utils.py index d04e92201a..3bc48c63d2 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/utils.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/utils.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -from functools import lru_cache +from functools import cache import numpy as np from PIL import Image @@ -32,7 +32,7 @@ def calc_bboxes_from_keypoints(data, slack=0, offset=0, clip=False): return bboxes -@lru_cache(maxsize=None) +@cache def read_image_shape_fast(path): # Blazing fast and does not load the image into memory with Image.open(path) as img: diff --git a/deeplabcut/modelzoo/generalized_data_converter/utils.py b/deeplabcut/modelzoo/generalized_data_converter/utils.py index 948e49af53..012e770128 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/utils.py +++ b/deeplabcut/modelzoo/generalized_data_converter/utils.py @@ -16,10 +16,10 @@ import numpy as np import pandas as pd -from deeplabcut.utils import auxiliaryfunctions from deeplabcut.modelzoo.generalized_data_converter.datasets.materialize import ( SingleDLC_config, ) +from deeplabcut.utils import auxiliaryfunctions def threshold_kpts(config_path, h5path, threshold_mean=0.9, threshold_min=0.1): diff --git a/deeplabcut/modelzoo/utils.py b/deeplabcut/modelzoo/utils.py index 79d739d254..8fba813507 100644 --- a/deeplabcut/modelzoo/utils.py +++ b/deeplabcut/modelzoo/utils.py @@ -181,7 +181,7 @@ def read_conversion_table_from_csv(csv_path): df = df.dropna() df[0] = df[0].str.replace(r"\s+", "", regex=True) df[1] = df[1].str.replace(r"\s+", "", regex=True) - _map = dict(zip(df[0], df[1])) + _map = dict(zip(df[0], df[1], strict=False)) return _map @@ -200,6 +200,7 @@ def parse_project_model_name(superanimal_name: str) -> tuple[str, str]: warnings.warn( f"{superanimal_name} is deprecated and will be removed in a future version. Use {superanimal_name}_model_suffix instead.", DeprecationWarning, + stacklevel=2, ) superanimal_name = "superanimal_quadruped_hrnetw32" @@ -207,6 +208,7 @@ def parse_project_model_name(superanimal_name: str) -> tuple[str, str]: warnings.warn( f"{superanimal_name} is deprecated and will be removed in a future version. Use {superanimal_name}_model_suffix instead.", DeprecationWarning, + stacklevel=2, ) superanimal_name = "superanimal_topviewmouse_dlcrnet" @@ -223,7 +225,7 @@ def parse_project_model_name(superanimal_name: str) -> tuple[str, str]: raise ValueError(f"Model {model_name} not found. Available models are: {available_models}") available_project_configs = glob(os.path.join(modelzoo_path, "project_configs", "*.yaml")) - available_projects = [os.path.splitext(os.path.basename(path))[0] for path in available_project_configs] + [os.path.splitext(os.path.basename(path))[0] for path in available_project_configs] return project_name, model_name diff --git a/deeplabcut/modelzoo/video_inference.py b/deeplabcut/modelzoo/video_inference.py index a0bb141567..d64a61eb10 100644 --- a/deeplabcut/modelzoo/video_inference.py +++ b/deeplabcut/modelzoo/video_inference.py @@ -13,7 +13,6 @@ import json import os from pathlib import Path -from typing import Optional, Union import torch from dlclibrary.dlcmodelzoo.modelzoo_download import download_huggingface_model @@ -54,13 +53,13 @@ def get_checkpoint_epoch(checkpoint_path): def video_inference_superanimal( - videos: Union[str, list], + videos: str | list, superanimal_name: str, model_name: str, detector_name: str | None = None, - scale_list: Optional[list] = None, + scale_list: list | None = None, videotype: str = ".mp4", - dest_folder: Optional[str] = None, + dest_folder: str | None = None, cropping: list[int] | None = None, video_adapt: bool = False, plot_trajectories: bool = False, @@ -74,10 +73,10 @@ def video_inference_superanimal( pose_epochs: int = 4, max_individuals: int = 10, video_adapt_batch_size: int = 8, - device: Optional[str] = "auto", - customized_pose_checkpoint: Optional[str] = None, - customized_detector_checkpoint: Optional[str] = None, - customized_model_config: Optional[str] = None, + device: str | None = "auto", + customized_pose_checkpoint: str | None = None, + customized_detector_checkpoint: str | None = None, + customized_model_config: str | None = None, plot_bboxes: bool = True, create_labeled_video: bool = True, ): @@ -320,7 +319,7 @@ def video_inference_superanimal( print(f"Running video inference on {videos} with {superanimal_name}_{model_name}") dlc_root_path = get_deeplabcut_path() modelzoo_path = os.path.join(dlc_root_path, "modelzoo") - available_architectures = json.load(open(os.path.join(modelzoo_path, "models_to_framework.json"), "r")) + available_architectures = json.load(open(os.path.join(modelzoo_path, "models_to_framework.json"))) framework = available_architectures[model_name] print(f"Using {framework} for model {model_name}") if framework == "tensorflow": @@ -448,7 +447,7 @@ def video_inference_superanimal( pseudo_anno_dir = Path(dest_folder) pseudo_anno_name = f"{video_path.stem}_{dlc_scorer}_before_adapt.json" - with open(pseudo_anno_dir / pseudo_anno_name, "r") as f: + with open(pseudo_anno_dir / pseudo_anno_name) as f: predictions = json.load(f) # make sure we tune parameters inside this function such as pseudo @@ -478,7 +477,8 @@ def video_inference_superanimal( # get the current epoch of the pose model current_pose_epoch = get_checkpoint_epoch(pose_model_path) - # update the checkpoint path with the current epoch, if the checkpoint does not exist, use the best checkpoint + # update the checkpoint path with the current epoch, if the checkpoint + # does not exist, use the best checkpoint adapted_pose_checkpoint = model_folder / f"{model_snapshot_prefix}-{current_pose_epoch + pose_epochs:03}.pt" if not Path(adapted_pose_checkpoint).exists(): adapted_pose_checkpoint = ( @@ -518,7 +518,7 @@ def video_inference_superanimal( print("Running video adaptation with following parameters:\n" + params_msg) train_file = pseudo_dataset_folder / "annotations" / "train.json" - with open(train_file, "r") as f: + with open(train_file) as f: temp_obj = json.load(f) annotations = temp_obj["annotations"] @@ -547,7 +547,8 @@ def video_inference_superanimal( skip_detector=(superanimal_name == "superanimal_humanbody"), ) - # after video adaptation, re-update the adapted checkpoint path, if the checkpoint does not exist, use the best checkpoint + # after video adaptation, re-update the adapted checkpoint path, if the + # checkpoint does not exist, use the best checkpoint adapted_pose_checkpoint = model_folder / f"{model_snapshot_prefix}-{current_pose_epoch + pose_epochs:03}.pt" if not Path(adapted_pose_checkpoint).exists(): adapted_pose_checkpoint = ( diff --git a/deeplabcut/modelzoo/webapp/inference.py b/deeplabcut/modelzoo/webapp/inference.py index ae1ab4bb24..0e6b6eab05 100644 --- a/deeplabcut/modelzoo/webapp/inference.py +++ b/deeplabcut/modelzoo/webapp/inference.py @@ -8,7 +8,6 @@ # # Licensed under GNU Lesser General Public License v3.0 # -from typing import Dict import numpy as np @@ -96,7 +95,7 @@ def initialize_models(self, pose_model_path: str, detector_model_path: str): def config(self): return self._config - def predict(self, frames: Dict[str, np.array]): + def predict(self, frames: dict[str, np.array]): input_images = np.array(list(frames.values()), dtype=float) diff --git a/deeplabcut/pose_estimation_3d/camera_calibration.py b/deeplabcut/pose_estimation_3d/camera_calibration.py index d3c1e90e0a..84bf270557 100644 --- a/deeplabcut/pose_estimation_3d/camera_calibration.py +++ b/deeplabcut/pose_estimation_3d/camera_calibration.py @@ -20,8 +20,7 @@ import numpy as np from matplotlib.axes._axes import _log as matplotlib_axes_logger -from deeplabcut.utils import auxiliaryfunctions -from deeplabcut.utils import auxiliaryfunctions_3d +from deeplabcut.utils import auxiliaryfunctions, auxiliaryfunctions_3d matplotlib_axes_logger.setLevel("ERROR") @@ -134,7 +133,7 @@ def calibrate_cameras(config, cbrow=8, cbcol=6, calibrate=False, alpha=0.4, sear ) # (8,6) pattern (dimensions = common points of black squares) # If found, add object points, image points (after refining them) - if ret == True: + if ret: img_shape[cam] = gray.shape[::-1] objpoints[cam].append(objp) corners = cv2.cornerSubPix(gray, corners, search_window_size, (-1, -1), criteria) @@ -143,7 +142,7 @@ def calibrate_cameras(config, cbrow=8, cbcol=6, calibrate=False, alpha=0.4, sear img = cv2.drawChessboardCorners(img, (cbcol, cbrow), corners, ret) cv2.imwrite(os.path.join(str(path_corners), filename + "_corner.jpg"), img) else: - print("Corners not found for the image %s" % Path(fname).name) + print(f"Corners not found for the image {Path(fname).name}") for new_cam in cam_names: remove_fname = Path(fname).name.replace(cam, new_cam) os.rename( @@ -161,7 +160,7 @@ def calibrate_cameras(config, cbrow=8, cbcol=6, calibrate=False, alpha=0.4, sear ) # Perform calibration for each cameras and store the matrices as a pickle file - if calibrate == True: + if calibrate: # Calibrating each camera for cam in cam_names: ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( @@ -183,8 +182,7 @@ def calibrate_cameras(config, cbrow=8, cbcol=6, calibrate=False, alpha=0.4, sear ), ) print( - "Saving intrinsic camera calibration matrices for %s as a pickle file in %s" - % (cam, os.path.join(path_camera_matrix)) + f"Saving intrinsic camera calibration matrices for {cam} as a pickle file in {os.path.join(path_camera_matrix)}" ) # Compute mean re-projection errors for individual cameras @@ -193,12 +191,12 @@ def calibrate_cameras(config, cbrow=8, cbcol=6, calibrate=False, alpha=0.4, sear imgpoints_proj, _ = cv2.projectPoints(objpoints[cam][i], rvecs[i], tvecs[i], mtx, dist) error = cv2.norm(imgpoints[cam][i], imgpoints_proj, cv2.NORM_L2) / len(imgpoints_proj) mean_error += error - print("Mean re-projection error for %s images: %.3f pixels " % (cam, mean_error / len(objpoints[cam]))) + print(f"Mean re-projection error for {cam} images: {mean_error / len(objpoints[cam]):.3f} pixels ") # Compute stereo calibration for each pair of cameras camera_pair = [[cam_names[0], cam_names[1]]] for pair in camera_pair: - print("Computing stereo calibration for " % pair) + print("Computing stereo calibration for ") ( retval, cameraMatrix1, @@ -254,16 +252,14 @@ def calibrate_cameras(config, cbrow=8, cbcol=6, calibrate=False, alpha=0.4, sear } print( - "Saving the stereo parameters for every pair of cameras as a pickle file in %s" - % str(os.path.join(path_camera_matrix)) + f"Saving the stereo parameters for every pair of cameras as a pickle file in {str(os.path.join(path_camera_matrix))}" ) auxiliaryfunctions.write_pickle(os.path.join(path_camera_matrix, "stereo_params.pickle"), stereo_params) print("Camera calibration done! Use the function ``check_undistortion`` to check the check the calibration") else: print( - "Corners extracted! You may check for the extracted corners in the directory %s and remove the pair of images where the corners are incorrectly detected. If all the corners are detected correctly with right order, then re-run the same function and use the flag ``calibrate=True``, to calbrate the camera." - % str(path_corners) + f"Corners extracted! You may check for the extracted corners in the directory {str(path_corners)} and remove the pair of images where the corners are incorrectly detected. If all the corners are detected correctly with right order, then re-run the same function and use the flag ``calibrate=True``, to calbrate the camera." ) @@ -398,10 +394,10 @@ def check_undistortion(config, cbrow=8, cbcol=6, plot=True): cam1_undistort = np.array(cam1_undistort) cam2_undistort = np.array(cam2_undistort) - print("All images are undistorted and stored in %s" % str(path_undistort)) + print(f"All images are undistorted and stored in {str(path_undistort)}") print("Use the function ``triangulate`` to undistort the dataframes and compute the triangulation") - if plot == True: + if plot: f1, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 10)) f1.suptitle( str("Original Image: Views from " + pair[0] + " and " + pair[1]), @@ -412,7 +408,7 @@ def check_undistortion(config, cbrow=8, cbcol=6, plot=True): ax1.imshow(cv2.cvtColor(img1, cv2.COLOR_BGR2RGB)) ax2.imshow(cv2.cvtColor(img2, cv2.COLOR_BGR2RGB)) - norm = mcolors.Normalize(vmin=0.0, vmax=cam1_undistort.shape[1]) + mcolors.Normalize(vmin=0.0, vmax=cam1_undistort.shape[1]) plt.savefig(os.path.join(str(path_undistort), "Original_Image.png")) # Plot the undistorted corner points diff --git a/deeplabcut/pose_estimation_3d/plotting3D.py b/deeplabcut/pose_estimation_3d/plotting3D.py index 0232d62f34..b104116305 100644 --- a/deeplabcut/pose_estimation_3d/plotting3D.py +++ b/deeplabcut/pose_estimation_3d/plotting3D.py @@ -130,7 +130,7 @@ def create_labeled_video_3d( >>> deeplabcut.create_labeled_video_3d(config,['/data/project1/videos'],start=100, end=500,view=[30,90],xlim=[-12,12],ylim=[15,25],zlim=[20,30]) """ - start_path = os.getcwd() + os.getcwd() # Read the config file and related variables cfg_3d = auxiliaryfunctions.read_config(config) @@ -176,12 +176,7 @@ def create_labeled_video_3d( cam1_scorer = metadata_["scorer_name"][cam_names[0]] cam2_scorer = metadata_["scorer_name"][cam_names[1]] print( - "Creating 3D video from %s and %s using %s" - % ( - Path(cam1_view_video).name, - Path(cam2_view_video).name, - Path(triangulate_file).name, - ) + f"Creating 3D video from {Path(cam1_view_video).name} and {Path(cam2_view_video).name} using {Path(triangulate_file).name}" ) # Read the video files and corresponfing h5 files @@ -262,7 +257,7 @@ def create_labeled_video_3d( bodyparts2connect, bpts, ) - ind_links = tuple(zip(*links)) + ind_links = tuple(zip(*links, strict=False)) if color_by == "bodypart": color = plt.cm.get_cmap(cmap, len(bodyparts2plot)) @@ -325,7 +320,7 @@ def create_labeled_video_3d( frame_cam1 = vid_cam1.read_frame() frame_cam2 = vid_cam2.read_frame() if frame_cam1 is None or frame_cam2 is None: - raise IOError("A video frame is empty.") + raise OSError("A video frame is empty.") im1.set_data(frame_cam1) im2.set_data(frame_cam2) diff --git a/deeplabcut/pose_estimation_3d/triangulation.py b/deeplabcut/pose_estimation_3d/triangulation.py index 4a32bd80b3..296e0ddcd8 100644 --- a/deeplabcut/pose_estimation_3d/triangulation.py +++ b/deeplabcut/pose_estimation_3d/triangulation.py @@ -11,13 +11,13 @@ import os from pathlib import Path + import cv2 import numpy as np import pandas as pd -from deeplabcut.utils import auxfun_multianimal, auxiliaryfunctions -from deeplabcut.utils import auxiliaryfunctions_3d from deeplabcut.core.trackingutils import TRACK_METHODS +from deeplabcut.utils import auxfun_multianimal, auxiliaryfunctions, auxiliaryfunctions_3d def triangulate( @@ -104,7 +104,7 @@ def triangulate( # flag to check if the video_path variable is a string or a list of list flag = False # assumes that video path is a list - if isinstance(video_path, str) == True: + if isinstance(video_path, str): flag = True video_list = auxiliaryfunctions_3d.get_camerawise_videos(video_path, cam_names, videotype=videotype) else: @@ -129,7 +129,7 @@ def triangulate( if cam_names[j] not in video_list[i][j]: raise ValueError(f"Camera name '{cam_names[j]}' not found in video list '{video_list[i][j]}'.") else: - print("Analyzing video %s using %s" % (video_list[i][j], str("config_file_" + cam_names[j]))) + print("Analyzing video {} using {}".format(video_list[i][j], str("config_file_" + cam_names[j]))) config_2d = snapshots[cam_names[j]] cfg = auxiliaryfunctions.read_config(config_2d) @@ -146,7 +146,7 @@ def triangulate( shuffle = cfg_3d[str("shuffle_" + cam_names[j])] trainingsetindex = cfg_3d[str("trainingsetindex_" + cam_names[j])] trainFraction = cfg["TrainingFraction"][trainingsetindex] - if flag == True: + if flag: video = os.path.join(video_path, video_list[i][j]) else: video_path = str(Path(video_list[i][j]).parents[0]) @@ -295,17 +295,18 @@ def triangulate( import warnings warnings.warn( - "The number of frames do not match in the two videos. Please make sure that your videos have same number of frames and then retry! Excluding the extra frames from the longer video." + "The number of frames do not match in the two videos. Please make sure that your videos have same number of frames and then retry! Excluding the extra frames from the longer video.", + stacklevel=2, ) if len(dataFrame_camera1_undistort) > len(dataFrame_camera2_undistort): dataFrame_camera1_undistort = dataFrame_camera1_undistort[: len(dataFrame_camera2_undistort)] if len(dataFrame_camera2_undistort) > len(dataFrame_camera1_undistort): dataFrame_camera2_undistort = dataFrame_camera2_undistort[: len(dataFrame_camera1_undistort)] # raise Exception("The number of frames do not match in the two videos. Please make sure that your videos have same number of frames and then retry!") - scorer_cam1 = dataFrame_camera1_undistort.columns.get_level_values(0)[0] - scorer_cam2 = dataFrame_camera2_undistort.columns.get_level_values(0)[0] + dataFrame_camera1_undistort.columns.get_level_values(0)[0] + dataFrame_camera2_undistort.columns.get_level_values(0)[0] - bodyparts = dataFrame_camera1_undistort.columns.get_level_values("bodyparts").unique() + dataFrame_camera1_undistort.columns.get_level_values("bodyparts").unique() P1 = stereomatrix["P1"] P2 = stereomatrix["P2"] @@ -471,7 +472,7 @@ def _undistort_points(points, mat, coeffs, p, r): def _undistort_views(df_view_pairs, stereo_params): df_views_undist = [] - for df_view_pair, camera_pair in zip(df_view_pairs, stereo_params): + for df_view_pair, camera_pair in zip(df_view_pairs, stereo_params, strict=False): params = stereo_params[camera_pair] dfs = [] for i, df_view in enumerate(df_view_pair, start=1): diff --git a/deeplabcut/pose_estimation_pytorch/__init__.py b/deeplabcut/pose_estimation_pytorch/__init__.py index b6e0f0ec9f..09d8c1f839 100644 --- a/deeplabcut/pose_estimation_pytorch/__init__.py +++ b/deeplabcut/pose_estimation_pytorch/__init__.py @@ -10,13 +10,14 @@ # import deeplabcut.pose_estimation_pytorch.config as config from deeplabcut.pose_estimation_pytorch.apis import ( + VideoIterator, analyze_image_folder, analyze_images, analyze_videos, build_predictions_dataframe, + convert_detections2tracklets, create_labeled_images, create_tracking_dataset, - convert_detections2tracklets, evaluate, evaluate_network, extract_maps, @@ -28,42 +29,41 @@ train, train_network, video_inference, - VideoIterator, visualize_predictions, ) from deeplabcut.pose_estimation_pytorch.config import ( available_detectors, available_models, - is_model_top_down, is_model_cond_top_down, + is_model_top_down, ) from deeplabcut.pose_estimation_pytorch.data import ( - build_transforms, - COCOLoader, COLLATE_FUNCTIONS, + COCOLoader, DLCLoader, GenerativeSampler, GenSamplingConfig, - list_snapshots, Loader, PoseDataset, PoseDatasetParameters, Snapshot, + build_transforms, + list_snapshots, ) from deeplabcut.pose_estimation_pytorch.runners import ( - build_inference_runner, - build_training_runner, DetectorInferenceRunner, DetectorTrainingRunner, DynamicCropper, - get_load_weights_only, InferenceRunner, PoseInferenceRunner, PoseTrainingRunner, - set_load_weights_only, TopDownDynamicCropper, TorchSnapshotManager, TrainingRunner, + build_inference_runner, + build_training_runner, + get_load_weights_only, + set_load_weights_only, ) from deeplabcut.pose_estimation_pytorch.task import Task from deeplabcut.pose_estimation_pytorch.utils import fix_seeds diff --git a/deeplabcut/pose_estimation_pytorch/apis/__init__.py b/deeplabcut/pose_estimation_pytorch/apis/__init__.py index de20610fcf..e33c98629b 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/__init__.py +++ b/deeplabcut/pose_estimation_pytorch/apis/__init__.py @@ -12,43 +12,38 @@ from deeplabcut.pose_estimation_pytorch.apis.analyze_images import ( analyze_image_folder, analyze_images, - analyze_image_folder, superanimal_analyze_images, ) -from deeplabcut.pose_estimation_pytorch.apis.videos import ( - analyze_videos, - video_inference, - VideoIterator, -) -from deeplabcut.pose_estimation_pytorch.apis.tracklets import ( - convert_detections2tracklets, -) from deeplabcut.pose_estimation_pytorch.apis.evaluation import ( - predict, evaluate, evaluate_network, + predict, visualize_predictions, ) from deeplabcut.pose_estimation_pytorch.apis.export import export_model from deeplabcut.pose_estimation_pytorch.apis.tracking_dataset import ( create_tracking_dataset, ) +from deeplabcut.pose_estimation_pytorch.apis.tracklets import ( + convert_detections2tracklets, +) from deeplabcut.pose_estimation_pytorch.apis.training import ( train, train_network, ) from deeplabcut.pose_estimation_pytorch.apis.utils import ( + build_predictions_dataframe, get_detector_inference_runner, get_inference_runners, get_pose_inference_runner, ) +from deeplabcut.pose_estimation_pytorch.apis.videos import ( + VideoIterator, + analyze_videos, + video_inference, +) from deeplabcut.pose_estimation_pytorch.apis.visualization import ( create_labeled_images, extract_maps, extract_save_all_maps, ) -from deeplabcut.pose_estimation_pytorch.apis.utils import ( - build_predictions_dataframe, - get_detector_inference_runner, - get_pose_inference_runner, -) diff --git a/deeplabcut/pose_estimation_pytorch/apis/analyze_images.py b/deeplabcut/pose_estimation_pytorch/apis/analyze_images.py index 16014629b3..1ce8fb3762 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/analyze_images.py +++ b/deeplabcut/pose_estimation_pytorch/apis/analyze_images.py @@ -30,14 +30,14 @@ from deeplabcut.modelzoo.utils import get_superanimal_colormaps from deeplabcut.pose_estimation_pytorch.apis.ctd import get_condition_provider from deeplabcut.pose_estimation_pytorch.apis.utils import ( - get_detector_inference_runner, build_predictions_dataframe, + get_detector_inference_runner, + get_filtered_coco_detector_inference_runner, get_model_snapshots, get_pose_inference_runner, get_scorer_name, get_scorer_uid, parse_snapshot_index_for_analysis, - get_filtered_coco_detector_inference_runner, ) from deeplabcut.pose_estimation_pytorch.data.ctd import CondFromModel from deeplabcut.pose_estimation_pytorch.modelzoo.utils import update_config @@ -467,7 +467,7 @@ def analyze_image_folder( if pose_task == Task.TOP_DOWN and detector_path is None and filtered_detector_config is None: raise ValueError( "A detector path or filtered_detector_config must be specified for image analysis using top-down models" - f" Please specify the `detector_path` parameter or the `filtered_detector_config` parameter." + " Please specify the `detector_path` parameter or the `filtered_detector_config` parameter." ) if max_individuals is None: @@ -479,7 +479,7 @@ def analyze_image_folder( if pose_task == Task.COND_TOP_DOWN and cond_provider is None: raise ValueError( "A conditions provider must be specified for image analysis when using cond-top-down models" - f" Please specify the `cond_provider` parameter." + " Please specify the `cond_provider` parameter." ) pose_runner = get_pose_inference_runner( @@ -574,7 +574,7 @@ def plot_images_coco( Raises: ValueError: if a top-down model configuration is given but detector_path is None """ - with open(data_json_path, "r") as f: + with open(data_json_path) as f: obj = json.load(f) coco_images = obj["images"] diff --git a/deeplabcut/pose_estimation_pytorch/apis/ctd.py b/deeplabcut/pose_estimation_pytorch/apis/ctd.py index 9e13fe3ce9..b482f4d453 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/ctd.py +++ b/deeplabcut/pose_estimation_pytorch/apis/ctd.py @@ -100,7 +100,7 @@ def load_conditions_for_evaluation(loader: data.Loader, images: list[str]) -> di The conditions for the images. """ if loader.pose_task != Task.COND_TOP_DOWN: - raise ValueError(f"Conditions can only be loaded for CTD models") + raise ValueError("Conditions can only be loaded for CTD models") # load the conditions config condition_cfg = loader.model_cfg["inference"].get("conditions") diff --git a/deeplabcut/pose_estimation_pytorch/apis/evaluation.py b/deeplabcut/pose_estimation_pytorch/apis/evaluation.py index f6edfac830..7b9d393ce4 100755 --- a/deeplabcut/pose_estimation_pytorch/apis/evaluation.py +++ b/deeplabcut/pose_estimation_pytorch/apis/evaluation.py @@ -11,8 +11,8 @@ from __future__ import annotations import argparse +from collections.abc import Iterable from pathlib import Path -from typing import Iterable import albumentations as A import matplotlib.pyplot as plt @@ -26,13 +26,13 @@ from deeplabcut.core.weight_init import WeightInitialization from deeplabcut.pose_estimation_pytorch import utils from deeplabcut.pose_estimation_pytorch.apis.utils import ( + build_bboxes_dict_for_dataframe, build_predictions_dataframe, ensure_multianimal_df_format, get_inference_runners, get_model_snapshots, get_scorer_name, get_scorer_uid, - build_bboxes_dict_for_dataframe, ) from deeplabcut.pose_estimation_pytorch.data import DLCLoader, Loader from deeplabcut.pose_estimation_pytorch.data.dataset import PoseDatasetParameters @@ -826,7 +826,7 @@ def image_to_dlc_df_index(image: str) -> tuple[str, ...]: if len(image_path.parts) >= 3 and image_path.parts[-3] == "labeled-data": return Path(image_path).parts[-3:] - raise ValueError(f"Unexpected image filepath for a DLC project") + raise ValueError("Unexpected image filepath for a DLC project") def save_evaluation_results(df_scores: pd.DataFrame, scores_path: Path, print_results: bool, pcutoff: float) -> None: diff --git a/deeplabcut/pose_estimation_pytorch/apis/prune_paf_graph.py b/deeplabcut/pose_estimation_pytorch/apis/prune_paf_graph.py index 5e125f90e8..6214ab3f86 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/prune_paf_graph.py +++ b/deeplabcut/pose_estimation_pytorch/apis/prune_paf_graph.py @@ -69,7 +69,7 @@ def benchmark_paf_graphs( model.eval() if not isinstance(predictor, predictors.PartAffinityFieldPredictor): - raise ValueError(f"Predictor should be a PartAffinityFieldPredictor.") + raise ValueError("Predictor should be a PartAffinityFieldPredictor.") if verbose: print("-------------------------------------------------") diff --git a/deeplabcut/pose_estimation_pytorch/apis/tracklets.py b/deeplabcut/pose_estimation_pytorch/apis/tracklets.py index 5833edf86e..fbb1e7c3d9 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/tracklets.py +++ b/deeplabcut/pose_estimation_pytorch/apis/tracklets.py @@ -12,7 +12,6 @@ import pickle import warnings from pathlib import Path -from typing import Dict, List, Optional, Union import numpy as np import pandas as pd @@ -20,29 +19,29 @@ from scipy.special import softmax from tqdm import tqdm -import deeplabcut.utils.auxiliaryfunctions as auxiliaryfunctions import deeplabcut.utils.auxfun_multianimal as auxfun_multianimal +import deeplabcut.utils.auxiliaryfunctions as auxiliaryfunctions from deeplabcut.core import trackingutils from deeplabcut.core.engine import Engine from deeplabcut.core.inferenceutils import Assembly -from deeplabcut.pose_estimation_pytorch.data.dlcloader import DLCLoader from deeplabcut.pose_estimation_pytorch.apis.utils import ( get_scorer_name, list_videos_in_folder, parse_snapshot_index_for_analysis, ) +from deeplabcut.pose_estimation_pytorch.data.dlcloader import DLCLoader def convert_detections2tracklets( config: str, - videos: Union[str, List[str]], - videotype: Optional[str] = None, + videos: str | list[str], + videotype: str | None = None, shuffle: int = 1, trainingsetindex: int = 0, overwrite: bool = False, - destfolder: Optional[str] = None, - ignore_bodyparts: Optional[List[str]] = None, - inferencecfg: Optional[dict] = None, + destfolder: str | None = None, + ignore_bodyparts: list[str] | None = None, + inferencecfg: dict | None = None, modelprefix="", greedy: bool = False, # TODO(niels): implement greedy assembly during video analysis calibrate: bool = False, # TODO(niels): implement assembly calibration during video analysis @@ -306,8 +305,8 @@ def _create_tracklets_header(joints, dlc_scorer): def _conv_predictions_to_assemblies( - image_names: List[str], predictions: Dict[str, np.ndarray] -) -> Dict[int, List[Assembly]]: + image_names: list[str], predictions: dict[str, np.ndarray] +) -> dict[int, list[Assembly]]: """ Converts predictions to an assemblies dictionary predictions shape (num_animals, num_keypoints, 2 or 3) diff --git a/deeplabcut/pose_estimation_pytorch/apis/training.py b/deeplabcut/pose_estimation_pytorch/apis/training.py index 720b786abc..962b3d8ed0 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/training.py +++ b/deeplabcut/pose_estimation_pytorch/apis/training.py @@ -22,10 +22,10 @@ import deeplabcut.pose_estimation_pytorch.utils as utils from deeplabcut.core.weight_init import WeightInitialization from deeplabcut.pose_estimation_pytorch.data import ( - build_transforms, COCOLoader, DLCLoader, Loader, + build_transforms, ) from deeplabcut.pose_estimation_pytorch.data.collate import COLLATE_FUNCTIONS from deeplabcut.pose_estimation_pytorch.models import DETECTORS, PoseModel @@ -34,8 +34,8 @@ ) from deeplabcut.pose_estimation_pytorch.runners import build_training_runner from deeplabcut.pose_estimation_pytorch.runners.logger import ( - destroy_file_logging, LOGGER, + destroy_file_logging, setup_file_logging, ) from deeplabcut.pose_estimation_pytorch.task import Task diff --git a/deeplabcut/pose_estimation_pytorch/apis/utils.py b/deeplabcut/pose_estimation_pytorch/apis/utils.py index a204ed71b5..1eec687b4a 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/utils.py +++ b/deeplabcut/pose_estimation_pytorch/apis/utils.py @@ -12,20 +12,19 @@ import logging import random +from collections.abc import Callable from pathlib import Path -from typing import Callable import albumentations as A import numpy as np import pandas as pd - from torchvision.models import detection from torchvision.models.detection import ( - fasterrcnn_resnet50_fpn, - fasterrcnn_mobilenet_v3_large_fpn, - FasterRCNN_ResNet50_FPN_Weights, - FasterRCNN_ResNet50_FPN_V2_Weights, FasterRCNN_MobileNet_V3_Large_FPN_Weights, + FasterRCNN_ResNet50_FPN_V2_Weights, + FasterRCNN_ResNet50_FPN_Weights, + fasterrcnn_mobilenet_v3_large_fpn, + fasterrcnn_resnet50_fpn, ) from deeplabcut.core.config import read_config_as_dict @@ -42,8 +41,8 @@ ) from deeplabcut.pose_estimation_pytorch.data.preprocessor import ( build_bottom_up_preprocessor, - build_top_down_preprocessor, build_conditional_top_down_preprocessor, + build_top_down_preprocessor, ) from deeplabcut.pose_estimation_pytorch.data.transforms import build_transforms from deeplabcut.pose_estimation_pytorch.models import DETECTORS, PoseModel @@ -51,13 +50,13 @@ FilteredDetector, ) from deeplabcut.pose_estimation_pytorch.runners import ( - build_inference_runner, CTDTrackingConfig, DetectorInferenceRunner, DynamicCropper, InferenceRunner, PoseInferenceRunner, TopDownDynamicCropper, + build_inference_runner, ) from deeplabcut.pose_estimation_pytorch.runners.inference import InferenceConfig from deeplabcut.pose_estimation_pytorch.runners.snapshots import ( @@ -185,7 +184,7 @@ def get_model_snapshots( all_snapshots = snapshot_manager.snapshots() snapshots = [s for s in all_snapshots if s.path.stem in snapshot_filter] if len(snapshots) != len(snapshot_filter): - print(f"Warning: could not find all `snapshots_to_evaluate`.") + print("Warning: could not find all `snapshots_to_evaluate`.") print(f" Requested snapshots: {snapshot_filter}") print(f" Found snapshots: {[s.path.stem for s in all_snapshots]}") print(f" Snapshots returned: {[s.path.stem for s in snapshots]}") diff --git a/deeplabcut/pose_estimation_pytorch/apis/videos.py b/deeplabcut/pose_estimation_pytorch/apis/videos.py index bd4cfe10b4..424af46f72 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/videos.py +++ b/deeplabcut/pose_estimation_pytorch/apis/videos.py @@ -43,7 +43,7 @@ from deeplabcut.pose_estimation_pytorch.runners.inference import InferenceConfig from deeplabcut.pose_estimation_pytorch.task import Task from deeplabcut.refine_training_dataset.stitch import stitch_tracklets -from deeplabcut.utils import auxiliaryfunctions, VideoReader +from deeplabcut.utils import VideoReader, auxiliaryfunctions class VideoIterator(VideoReader): diff --git a/deeplabcut/pose_estimation_pytorch/config/__init__.py b/deeplabcut/pose_estimation_pytorch/config/__init__.py index 2c3d7039ca..5ff726c436 100644 --- a/deeplabcut/pose_estimation_pytorch/config/__init__.py +++ b/deeplabcut/pose_estimation_pytorch/config/__init__.py @@ -8,6 +8,12 @@ # # Licensed under GNU Lesser General Public License v3.0 # +# For backwards compatibility +from deeplabcut.core.config import ( + pretty_print, + read_config_as_dict, + write_config, +) from deeplabcut.pose_estimation_pytorch.config.make_pose_config import ( make_basic_project_config, make_pytorch_pose_config, @@ -16,15 +22,8 @@ from deeplabcut.pose_estimation_pytorch.config.utils import ( available_detectors, available_models, - is_model_top_down, is_model_cond_top_down, + is_model_top_down, update_config, update_config_by_dotpath, ) - -# For backwards compatibility -from deeplabcut.core.config import ( - read_config_as_dict, - write_config, - pretty_print, -) diff --git a/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py b/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py index c49c3823fa..64aee73944 100644 --- a/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py +++ b/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py @@ -26,7 +26,7 @@ ) from deeplabcut.pose_estimation_pytorch.runners.inference import InferenceConfig from deeplabcut.pose_estimation_pytorch.task import Task -from deeplabcut.utils import auxiliaryfunctions, auxfun_multianimal +from deeplabcut.utils import auxfun_multianimal, auxiliaryfunctions def make_pytorch_pose_config( @@ -221,12 +221,12 @@ def _add_ctd_conditions(model_cfg: dict, ctd_conditions: int | str | Path | tupl if not ctd_conditions.exists(): raise FileNotFoundError(f"Invalid path: {ctd_conditions}") if ctd_conditions.suffix not in (".h5", ".json"): - raise ValueError(f"Invalid conditions file extension.") + raise ValueError("Invalid conditions file extension.") conditions = str(ctd_conditions.resolve()) elif isinstance(ctd_conditions, tuple): if len(ctd_conditions) != 2: - raise ValueError(f"Invalid conditions tuple length.") + raise ValueError("Invalid conditions tuple length.") if not isinstance(ctd_conditions[0], int): raise TypeError("Conditions shuffle number must be of type int.") if isinstance(ctd_conditions[1], int): @@ -456,7 +456,7 @@ def create_backbone_with_paf_model( backbone_output_channels = model_config["model"]["backbone_output_channels"] # add a bodypart head - bodypart_head_config = read_config_as_dict(configs_dir / "base" / f"head_bodyparts_with_paf.yaml") + bodypart_head_config = read_config_as_dict(configs_dir / "base" / "head_bodyparts_with_paf.yaml") model_config["model"]["heads"] = { "bodypart": replace_default_values( bodypart_head_config, diff --git a/deeplabcut/pose_estimation_pytorch/config/utils.py b/deeplabcut/pose_estimation_pytorch/config/utils.py index bc1025e2cd..4a17e7ae34 100644 --- a/deeplabcut/pose_estimation_pytorch/config/utils.py +++ b/deeplabcut/pose_estimation_pytorch/config/utils.py @@ -238,7 +238,7 @@ def available_models() -> list[str]: models.add("top_down_" + backbone) other_architectures = [ - p for p in configs_folder_path.iterdir() if p.is_dir() and not p.name in ("backbones", "base", "detectors") + p for p in configs_folder_path.iterdir() if p.is_dir() and p.name not in ("backbones", "base", "detectors") ] for folder in other_architectures: variants = [p.stem for p in folder.iterdir() if p.suffix == ".yaml"] diff --git a/deeplabcut/pose_estimation_pytorch/data/__init__.py b/deeplabcut/pose_estimation_pytorch/data/__init__.py index f21800860e..6d0c4e4556 100644 --- a/deeplabcut/pose_estimation_pytorch/data/__init__.py +++ b/deeplabcut/pose_estimation_pytorch/data/__init__.py @@ -11,26 +11,26 @@ from deeplabcut.pose_estimation_pytorch.data.base import Loader from deeplabcut.pose_estimation_pytorch.data.cocoloader import COCOLoader from deeplabcut.pose_estimation_pytorch.data.collate import COLLATE_FUNCTIONS -from deeplabcut.pose_estimation_pytorch.data.dlcloader import DLCLoader from deeplabcut.pose_estimation_pytorch.data.dataset import ( - PoseDatasetParameters, PoseDataset, + PoseDatasetParameters, ) +from deeplabcut.pose_estimation_pytorch.data.dlcloader import DLCLoader from deeplabcut.pose_estimation_pytorch.data.generative_sampling import ( GenerativeSampler, GenSamplingConfig, ) from deeplabcut.pose_estimation_pytorch.data.image import top_down_crop from deeplabcut.pose_estimation_pytorch.data.postprocessor import ( + Postprocessor, build_bottom_up_postprocessor, build_detector_postprocessor, build_top_down_postprocessor, - Postprocessor, ) from deeplabcut.pose_estimation_pytorch.data.preprocessor import ( + Preprocessor, build_bottom_up_preprocessor, build_top_down_preprocessor, - Preprocessor, ) -from deeplabcut.pose_estimation_pytorch.data.snapshots import list_snapshots, Snapshot +from deeplabcut.pose_estimation_pytorch.data.snapshots import Snapshot, list_snapshots from deeplabcut.pose_estimation_pytorch.data.transforms import build_transforms diff --git a/deeplabcut/pose_estimation_pytorch/data/base.py b/deeplabcut/pose_estimation_pytorch/data/base.py index 8d08ca990e..1b06cd62b0 100644 --- a/deeplabcut/pose_estimation_pytorch/data/base.py +++ b/deeplabcut/pose_estimation_pytorch/data/base.py @@ -25,7 +25,7 @@ from deeplabcut.pose_estimation_pytorch.data.generative_sampling import ( GenSamplingConfig, ) -from deeplabcut.pose_estimation_pytorch.data.snapshots import list_snapshots, Snapshot +from deeplabcut.pose_estimation_pytorch.data.snapshots import Snapshot, list_snapshots from deeplabcut.pose_estimation_pytorch.data.utils import ( _compute_crop_bounds, bbox_from_keypoints, diff --git a/deeplabcut/pose_estimation_pytorch/data/cocoloader.py b/deeplabcut/pose_estimation_pytorch/data/cocoloader.py index 3592eb4192..57051b7de8 100644 --- a/deeplabcut/pose_estimation_pytorch/data/cocoloader.py +++ b/deeplabcut/pose_estimation_pytorch/data/cocoloader.py @@ -112,7 +112,7 @@ def load_json(project_root: str | Path, filename: str) -> dict: if not os.path.exists(json_path): raise FileNotFoundError(f"File {json_path} does not exist.") - with open(json_path, "r") as f: + with open(json_path) as f: json_obj = json.load(f) if not isinstance(json_obj, dict): @@ -150,8 +150,8 @@ def validate_categories(coco_json: dict) -> dict: if len(coco_json["categories"]) > 1: warnings.warn( - f"Found more than 1 category in the project. This is currently not" - f" supported in DeepLabCut. All annotations will be given category 1" + "Found more than 1 category in the project. This is currently not" + " supported in DeepLabCut. All annotations will be given category 1" ) if cat_0: @@ -221,7 +221,7 @@ def validate_images(self, coco_json: dict) -> dict: if len(coco_json["annotations"]) < len(validated_annotations): warnings.warn( - f"Found some annotations for which the image ID was not in the images. Removing them from the dataset." + "Found some annotations for which the image ID was not in the images. Removing them from the dataset." ) print(f" All annotations: {len(coco_json['annotations'])}") print(f" Annotations with correct image IDs: {len(validated_annotations)}") diff --git a/deeplabcut/pose_estimation_pytorch/data/collate.py b/deeplabcut/pose_estimation_pytorch/data/collate.py index 2f1cc884b1..bf72a5bd25 100644 --- a/deeplabcut/pose_estimation_pytorch/data/collate.py +++ b/deeplabcut/pose_estimation_pytorch/data/collate.py @@ -18,8 +18,7 @@ from torch.utils.data import default_collate from deeplabcut.pose_estimation_pytorch.data.image import resize_and_random_crop -from deeplabcut.pose_estimation_pytorch.registry import build_from_cfg, Registry - +from deeplabcut.pose_estimation_pytorch.registry import Registry, build_from_cfg COLLATE_FUNCTIONS = Registry("collate_functions", build_func=build_from_cfg) diff --git a/deeplabcut/pose_estimation_pytorch/data/ctd.py b/deeplabcut/pose_estimation_pytorch/data/ctd.py index f599263e55..72c3fe2584 100644 --- a/deeplabcut/pose_estimation_pytorch/data/ctd.py +++ b/deeplabcut/pose_estimation_pytorch/data/ctd.py @@ -122,7 +122,7 @@ def __init__( ) if not filepath.exists(): - raise ValueError(f"Conditions file {{conditions_filepath}} does not exist. Please check the given path.") + raise ValueError("Conditions file {conditions_filepath} does not exist. Please check the given path.") self.filepath = filepath @@ -337,7 +337,7 @@ def load_conditions_json( A dictionary mapping image paths to condition arrays. Each array has shape (num_conditions, num_bodyparts, 3). """ - with open(filepath, "r") as f: + with open(filepath) as f: conditions = json.load(f) # Parse list and return diff --git a/deeplabcut/pose_estimation_pytorch/data/dlcloader.py b/deeplabcut/pose_estimation_pytorch/data/dlcloader.py index 5a88bd752d..4c713e8970 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dlcloader.py +++ b/deeplabcut/pose_estimation_pytorch/data/dlcloader.py @@ -26,8 +26,7 @@ from deeplabcut.pose_estimation_pytorch.data.base import Loader from deeplabcut.pose_estimation_pytorch.data.dataset import PoseDatasetParameters from deeplabcut.pose_estimation_pytorch.data.snapshots import Snapshot -from deeplabcut.pose_estimation_pytorch.data.utils import bbox_from_keypoints -from deeplabcut.pose_estimation_pytorch.data.utils import read_image_shape_fast +from deeplabcut.pose_estimation_pytorch.data.utils import bbox_from_keypoints, read_image_shape_fast class DLCLoader(Loader): @@ -724,7 +723,7 @@ def _validate_dataframes( ) if error and strict: - raise ValueError(f"Found errors when validating the dataset") + raise ValueError("Found errors when validating the dataset") return dfs diff --git a/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py b/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py index 7d717894a9..1026550d34 100644 --- a/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py +++ b/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py @@ -45,7 +45,7 @@ import math import random -from dataclasses import dataclass, asdict +from dataclasses import dataclass import numpy as np diff --git a/deeplabcut/pose_estimation_pytorch/data/postprocessor.py b/deeplabcut/pose_estimation_pytorch/data/postprocessor.py index a808d93cd1..117bddd259 100644 --- a/deeplabcut/pose_estimation_pytorch/data/postprocessor.py +++ b/deeplabcut/pose_estimation_pytorch/data/postprocessor.py @@ -12,10 +12,10 @@ from __future__ import annotations +import logging from abc import ABC, abstractmethod from enum import Enum from typing import Any -import logging import numpy as np diff --git a/deeplabcut/pose_estimation_pytorch/data/preprocessor.py b/deeplabcut/pose_estimation_pytorch/data/preprocessor.py index 23bac25a5e..0c53e0a9c0 100644 --- a/deeplabcut/pose_estimation_pytorch/data/preprocessor.py +++ b/deeplabcut/pose_estimation_pytorch/data/preprocessor.py @@ -13,8 +13,9 @@ from __future__ import annotations from abc import ABC, abstractmethod +from collections.abc import Callable from pathlib import Path -from typing import Any, TypeVar, Callable +from typing import Any, TypeVar import albumentations as A import numpy as np @@ -23,7 +24,6 @@ from deeplabcut.pose_estimation_pytorch.data.image import load_image, top_down_crop from deeplabcut.pose_estimation_pytorch.data.utils import bbox_from_keypoints - Image = TypeVar("Image", torch.Tensor, np.ndarray, str, Path) Context = TypeVar("Context", dict[str, Any], None) diff --git a/deeplabcut/pose_estimation_pytorch/data/snapshots.py b/deeplabcut/pose_estimation_pytorch/data/snapshots.py index 2ef9ef2fae..a4a6a7646a 100644 --- a/deeplabcut/pose_estimation_pytorch/data/snapshots.py +++ b/deeplabcut/pose_estimation_pytorch/data/snapshots.py @@ -13,13 +13,9 @@ from __future__ import annotations import re -import warnings -from dataclasses import dataclass, field +from dataclasses import dataclass from pathlib import Path -import numpy as np -import torch - @dataclass(frozen=True) class Snapshot: @@ -36,7 +32,7 @@ def uid(self) -> str: return str(self.epochs) @staticmethod - def from_path(path: Path) -> "Snapshot": + def from_path(path: Path) -> Snapshot: best = "-best" in path.stem # Use regex to extract epoch number more robustly match = re.search(r"-(\d+)\.pt$", path.name) diff --git a/deeplabcut/pose_estimation_pytorch/data/transforms.py b/deeplabcut/pose_estimation_pytorch/data/transforms.py index 4d06175f8e..d3d7fb745e 100644 --- a/deeplabcut/pose_estimation_pytorch/data/transforms.py +++ b/deeplabcut/pose_estimation_pytorch/data/transforms.py @@ -11,7 +11,8 @@ from __future__ import annotations import warnings -from typing import Any, Iterable, Sequence +from collections.abc import Iterable, Sequence +from typing import Any import albumentations as A import cv2 diff --git a/deeplabcut/pose_estimation_pytorch/data/utils.py b/deeplabcut/pose_estimation_pytorch/data/utils.py index 65c2d1ec4d..5bdc91bb9c 100644 --- a/deeplabcut/pose_estimation_pytorch/data/utils.py +++ b/deeplabcut/pose_estimation_pytorch/data/utils.py @@ -12,7 +12,7 @@ import warnings from collections import defaultdict -from functools import reduce, lru_cache +from functools import cache, reduce from pathlib import Path import albumentations as A @@ -20,7 +20,7 @@ from PIL import Image -@lru_cache(maxsize=None) +@cache def read_image_shape_fast(path: str | Path) -> tuple[int, int, int]: """Blazing fast and does not load the image into memory""" with Image.open(path) as img: diff --git a/deeplabcut/pose_estimation_pytorch/metrics/scoring.py b/deeplabcut/pose_estimation_pytorch/metrics/scoring.py index 95dd1c7126..c830974ef3 100644 --- a/deeplabcut/pose_estimation_pytorch/metrics/scoring.py +++ b/deeplabcut/pose_estimation_pytorch/metrics/scoring.py @@ -10,8 +10,9 @@ # from __future__ import annotations -import numpy as np import pickle + +import numpy as np from sklearn.metrics import accuracy_score from deeplabcut.core.crossvalutils import find_closest_neighbors diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/__init__.py b/deeplabcut/pose_estimation_pytorch/models/backbones/__init__.py index 8879d8b244..5e88ef236d 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/__init__.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/__init__.py @@ -12,8 +12,8 @@ BACKBONES, BaseBackbone, ) +from deeplabcut.pose_estimation_pytorch.models.backbones.cond_prenet import CondPreNet from deeplabcut.pose_estimation_pytorch.models.backbones.cspnext import CSPNeXt from deeplabcut.pose_estimation_pytorch.models.backbones.hrnet import HRNet -from deeplabcut.pose_estimation_pytorch.models.backbones.resnet import ResNet, DLCRNet from deeplabcut.pose_estimation_pytorch.models.backbones.hrnet_coam import HRNetCoAM -from deeplabcut.pose_estimation_pytorch.models.backbones.cond_prenet import CondPreNet +from deeplabcut.pose_estimation_pytorch.models.backbones.resnet import DLCRNet, ResNet diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/base.py b/deeplabcut/pose_estimation_pytorch/models/backbones/base.py index 0b32314f16..59c0827730 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/base.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/base.py @@ -19,7 +19,7 @@ import torch.nn as nn from huggingface_hub import hf_hub_download -from deeplabcut.pose_estimation_pytorch.registry import build_from_cfg, Registry +from deeplabcut.pose_estimation_pytorch.registry import Registry, build_from_cfg BACKBONES = Registry("backbones", build_func=build_from_cfg) diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py b/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py index 4a13491a83..6f9ec2fe19 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py @@ -17,8 +17,8 @@ BaseBackbone, ) from deeplabcut.pose_estimation_pytorch.models.modules import ( # ColoredKeypointEncoder,; StackedKeypointEncoder, - BaseKeypointEncoder, KEYPOINT_ENCODERS, + BaseKeypointEncoder, ) diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py index b9906dafab..24bcca4691 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py @@ -14,9 +14,9 @@ from deeplabcut.pose_estimation_pytorch.models.backbones.base import BACKBONES from deeplabcut.pose_estimation_pytorch.models.backbones.hrnet import HRNet from deeplabcut.pose_estimation_pytorch.models.modules import ( # ColoredKeypointEncoder,; StackedKeypointEncoder, + KEYPOINT_ENCODERS, BaseKeypointEncoder, CoAMBlock, - KEYPOINT_ENCODERS, SelfAttentionModule_CoAM, ) diff --git a/deeplabcut/pose_estimation_pytorch/models/criterions/aggregators.py b/deeplabcut/pose_estimation_pytorch/models/criterions/aggregators.py index b93c9021b0..98d538a753 100644 --- a/deeplabcut/pose_estimation_pytorch/models/criterions/aggregators.py +++ b/deeplabcut/pose_estimation_pytorch/models/criterions/aggregators.py @@ -13,8 +13,8 @@ import torch from deeplabcut.pose_estimation_pytorch.models.criterions.base import ( - BaseLossAggregator, LOSS_AGGREGATORS, + BaseLossAggregator, ) diff --git a/deeplabcut/pose_estimation_pytorch/models/criterions/base.py b/deeplabcut/pose_estimation_pytorch/models/criterions/base.py index 02c6b54989..25e6003904 100644 --- a/deeplabcut/pose_estimation_pytorch/models/criterions/base.py +++ b/deeplabcut/pose_estimation_pytorch/models/criterions/base.py @@ -15,7 +15,7 @@ import torch import torch.nn as nn -from deeplabcut.pose_estimation_pytorch.registry import build_from_cfg, Registry +from deeplabcut.pose_estimation_pytorch.registry import Registry, build_from_cfg LOSS_AGGREGATORS = Registry("loss_aggregators", build_func=build_from_cfg) CRITERIONS = Registry("criterions", build_func=build_from_cfg) diff --git a/deeplabcut/pose_estimation_pytorch/models/criterions/dekr.py b/deeplabcut/pose_estimation_pytorch/models/criterions/dekr.py index 1297b6aa7d..6e0343d813 100644 --- a/deeplabcut/pose_estimation_pytorch/models/criterions/dekr.py +++ b/deeplabcut/pose_estimation_pytorch/models/criterions/dekr.py @@ -15,8 +15,8 @@ import torch from deeplabcut.pose_estimation_pytorch.models.criterions.base import ( - BaseCriterion, CRITERIONS, + BaseCriterion, ) diff --git a/deeplabcut/pose_estimation_pytorch/models/criterions/kl_discrete.py b/deeplabcut/pose_estimation_pytorch/models/criterions/kl_discrete.py index 29d6e688af..44b5cb648a 100644 --- a/deeplabcut/pose_estimation_pytorch/models/criterions/kl_discrete.py +++ b/deeplabcut/pose_estimation_pytorch/models/criterions/kl_discrete.py @@ -19,8 +19,8 @@ import torch.nn.functional as F from deeplabcut.pose_estimation_pytorch.models.criterions.base import ( - BaseCriterion, CRITERIONS, + BaseCriterion, ) diff --git a/deeplabcut/pose_estimation_pytorch/models/criterions/weighted.py b/deeplabcut/pose_estimation_pytorch/models/criterions/weighted.py index 65d5f8f425..5576e79d49 100644 --- a/deeplabcut/pose_estimation_pytorch/models/criterions/weighted.py +++ b/deeplabcut/pose_estimation_pytorch/models/criterions/weighted.py @@ -10,15 +10,13 @@ # from __future__ import annotations -import numpy as np import torch import torch.nn as nn -import torch.nn.functional as F from deeplabcut.pose_estimation_pytorch.models.criterions import utils from deeplabcut.pose_estimation_pytorch.models.criterions.base import ( - BaseCriterion, CRITERIONS, + BaseCriterion, ) diff --git a/deeplabcut/pose_estimation_pytorch/models/detectors/base.py b/deeplabcut/pose_estimation_pytorch/models/detectors/base.py index 99df54e166..bf7157514e 100644 --- a/deeplabcut/pose_estimation_pytorch/models/detectors/base.py +++ b/deeplabcut/pose_estimation_pytorch/models/detectors/base.py @@ -17,7 +17,7 @@ import torch.nn as nn from deeplabcut.core.weight_init import WeightInitialization -from deeplabcut.pose_estimation_pytorch.registry import build_from_cfg, Registry +from deeplabcut.pose_estimation_pytorch.registry import Registry, build_from_cfg def _build_detector( diff --git a/deeplabcut/pose_estimation_pytorch/models/heads/base.py b/deeplabcut/pose_estimation_pytorch/models/heads/base.py index 3c575a8e0b..612f7fceaf 100644 --- a/deeplabcut/pose_estimation_pytorch/models/heads/base.py +++ b/deeplabcut/pose_estimation_pytorch/models/heads/base.py @@ -22,10 +22,10 @@ from deeplabcut.pose_estimation_pytorch.models.predictors import BasePredictor from deeplabcut.pose_estimation_pytorch.models.target_generators import BaseGenerator from deeplabcut.pose_estimation_pytorch.models.weight_init import ( - BaseWeightInitializer, WEIGHT_INIT, + BaseWeightInitializer, ) -from deeplabcut.pose_estimation_pytorch.registry import build_from_cfg, Registry +from deeplabcut.pose_estimation_pytorch.registry import Registry, build_from_cfg HEADS = Registry("heads", build_func=build_from_cfg) @@ -81,10 +81,10 @@ def __init__( if isinstance(criterion, dict): if aggregator is None: - raise ValueError(f"When multiple criterions are defined, a loss aggregator must also be given") + raise ValueError("When multiple criterions are defined, a loss aggregator must also be given") else: if aggregator is not None: - raise ValueError(f"Cannot use a loss aggregator with a single criterion") + raise ValueError("Cannot use a loss aggregator with a single criterion") @abstractmethod def forward(self, x: torch.Tensor) -> dict[str, torch.Tensor]: diff --git a/deeplabcut/pose_estimation_pytorch/models/heads/dekr.py b/deeplabcut/pose_estimation_pytorch/models/heads/dekr.py index 32db064180..64f4b137bc 100644 --- a/deeplabcut/pose_estimation_pytorch/models/heads/dekr.py +++ b/deeplabcut/pose_estimation_pytorch/models/heads/dekr.py @@ -17,7 +17,7 @@ BaseCriterion, BaseLossAggregator, ) -from deeplabcut.pose_estimation_pytorch.models.heads.base import BaseHead, HEADS +from deeplabcut.pose_estimation_pytorch.models.heads.base import HEADS, BaseHead from deeplabcut.pose_estimation_pytorch.models.modules.conv_block import ( AdaptBlock, BaseBlock, diff --git a/deeplabcut/pose_estimation_pytorch/models/heads/rtmcc_head.py b/deeplabcut/pose_estimation_pytorch/models/heads/rtmcc_head.py index 96bd9b7e64..cb195fd10d 100644 --- a/deeplabcut/pose_estimation_pytorch/models/heads/rtmcc_head.py +++ b/deeplabcut/pose_estimation_pytorch/models/heads/rtmcc_head.py @@ -24,8 +24,8 @@ BaseLossAggregator, ) from deeplabcut.pose_estimation_pytorch.models.heads.base import ( - BaseHead, HEADS, + BaseHead, ) from deeplabcut.pose_estimation_pytorch.models.modules import ( GatedAttentionUnit, diff --git a/deeplabcut/pose_estimation_pytorch/models/heads/simple_head.py b/deeplabcut/pose_estimation_pytorch/models/heads/simple_head.py index e4b1865ea7..09ebaa16de 100644 --- a/deeplabcut/pose_estimation_pytorch/models/heads/simple_head.py +++ b/deeplabcut/pose_estimation_pytorch/models/heads/simple_head.py @@ -18,8 +18,8 @@ BaseLossAggregator, ) from deeplabcut.pose_estimation_pytorch.models.heads.base import ( - BaseHead, HEADS, + BaseHead, WeightConversionMixin, ) from deeplabcut.pose_estimation_pytorch.models.predictors import BasePredictor diff --git a/deeplabcut/pose_estimation_pytorch/models/heads/transformer.py b/deeplabcut/pose_estimation_pytorch/models/heads/transformer.py index 0dd014fed5..4cf0276651 100644 --- a/deeplabcut/pose_estimation_pytorch/models/heads/transformer.py +++ b/deeplabcut/pose_estimation_pytorch/models/heads/transformer.py @@ -16,7 +16,7 @@ from torch import nn as nn from deeplabcut.pose_estimation_pytorch.models.criterions import BaseCriterion -from deeplabcut.pose_estimation_pytorch.models.heads import BaseHead, HEADS +from deeplabcut.pose_estimation_pytorch.models.heads import HEADS, BaseHead from deeplabcut.pose_estimation_pytorch.models.predictors import BasePredictor from deeplabcut.pose_estimation_pytorch.models.target_generators import BaseGenerator diff --git a/deeplabcut/pose_estimation_pytorch/models/model.py b/deeplabcut/pose_estimation_pytorch/models/model.py index 9c773467cb..c5f7decf8e 100644 --- a/deeplabcut/pose_estimation_pytorch/models/model.py +++ b/deeplabcut/pose_estimation_pytorch/models/model.py @@ -22,8 +22,8 @@ CRITERIONS, LOSS_AGGREGATORS, ) -from deeplabcut.pose_estimation_pytorch.models.heads import BaseHead, HEADS -from deeplabcut.pose_estimation_pytorch.models.necks import BaseNeck, NECKS +from deeplabcut.pose_estimation_pytorch.models.heads import HEADS, BaseHead +from deeplabcut.pose_estimation_pytorch.models.necks import NECKS, BaseNeck from deeplabcut.pose_estimation_pytorch.models.predictors import PREDICTORS from deeplabcut.pose_estimation_pytorch.models.target_generators import ( TARGET_GENERATORS, @@ -153,7 +153,7 @@ def build( cfg: dict, weight_init: None | WeightInitialization = None, pretrained_backbone: bool = False, - ) -> "PoseModel": + ) -> PoseModel: """ Args: cfg: The configuration of the model to build. diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/__init__.py b/deeplabcut/pose_estimation_pytorch/models/modules/__init__.py index 7efd11f82c..4a83694122 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/__init__.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/__init__.py @@ -8,6 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # +from deeplabcut.pose_estimation_pytorch.models.modules.coam_module import CoAMBlock, SelfAttentionModule_CoAM from deeplabcut.pose_estimation_pytorch.models.modules.conv_block import ( AdaptBlock, BasicBlock, @@ -16,15 +17,14 @@ from deeplabcut.pose_estimation_pytorch.models.modules.conv_module import ( HighResolutionModule, ) -from deeplabcut.pose_estimation_pytorch.models.modules.coam_module import CoAMBlock, SelfAttentionModule_CoAM +from deeplabcut.pose_estimation_pytorch.models.modules.gated_attention_unit import ( + GatedAttentionUnit, +) from deeplabcut.pose_estimation_pytorch.models.modules.kpt_encoders import ( + KEYPOINT_ENCODERS, BaseKeypointEncoder, ColoredKeypointEncoder, StackedKeypointEncoder, - KEYPOINT_ENCODERS, -) -from deeplabcut.pose_estimation_pytorch.models.modules.gated_attention_unit import ( - GatedAttentionUnit, ) from deeplabcut.pose_estimation_pytorch.models.modules.norm import ( ScaleNorm, diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py b/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py index e5041922ec..dfc522ab80 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py @@ -9,11 +9,10 @@ # Licensed under GNU Lesser General Public License v3.0 # import numpy as np - import torch import torch.nn as nn -from torch.nn import init import torchvision.transforms.functional as TF +from torch.nn import init class CoAMBlock(nn.Module): @@ -22,7 +21,7 @@ class CoAMBlock(nn.Module): """ def __init__(self, spat_dims, channel_list, cond_enc, n_heads=1, channel_only=False): - super(CoAMBlock, self).__init__() + super().__init__() self.att_layers = [] self.spat_dims = spat_dims self.cond_enc = cond_enc @@ -187,7 +186,7 @@ def forward(self, input): class SelfAttentionModule_CoAM(nn.Module): def __init__(self, spat_dims, channel_list): - super(SelfAttentionModule_CoAM, self).__init__() + super().__init__() self.att_layers = [] for i in range(len(spat_dims)): att_layer = SelfDAModule( @@ -220,7 +219,7 @@ def __init__(self, in_dim_q, in_dim_k, d_k, d_v, h, dropout=0.1, rev=False): :param d_v: Dimensionality of values :param h: Number of heads """ - super(ScaledDotProductAttention, self).__init__() + super().__init__() # 'rev': condition is key/value and orig. feature map is query if rev: @@ -297,7 +296,7 @@ def __init__(self, d_model, h, dropout=0.1): :param d_v: Dimensionality of values :param h: Number of heads """ - super(SimplifiedScaledDotProductAttention, self).__init__() + super().__init__() self.d_model = d_model self.d_k = d_model // h diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/conv_block.py b/deeplabcut/pose_estimation_pytorch/models/modules/conv_block.py index 1aa8d33450..4e5c27de22 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/conv_block.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/conv_block.py @@ -18,7 +18,7 @@ import torch.nn as nn import torchvision.ops as ops -from deeplabcut.pose_estimation_pytorch.registry import build_from_cfg, Registry +from deeplabcut.pose_estimation_pytorch.registry import Registry, build_from_cfg BLOCKS = Registry("blocks", build_func=build_from_cfg) @@ -88,7 +88,7 @@ def __init__( downsample: nn.Module | None = None, dilation: int = 1, ): - super(BasicBlock, self).__init__() + super().__init__() self.conv1 = nn.Conv2d( in_channels, out_channels, @@ -167,7 +167,7 @@ def __init__( downsample: nn.Module | None = None, dilation: int = 1, ): - super(Bottleneck, self).__init__() + super().__init__() self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False) self.bn1 = nn.BatchNorm2d(out_channels, momentum=self.bn_momentum) self.conv2 = nn.Conv2d( @@ -246,7 +246,7 @@ def __init__( dilation: int = 1, deformable_groups: int = 1, ): - super(AdaptBlock, self).__init__() + super().__init__() regular_matrix = torch.tensor([[-1, -1, -1, 0, 0, 0, 1, 1, 1], [-1, 0, 1, -1, 0, 1, -1, 0, 1]]) self.register_buffer("regular_matrix", regular_matrix.float()) self.downsample = downsample diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/conv_module.py b/deeplabcut/pose_estimation_pytorch/models/modules/conv_module.py index 372a70f44f..24778c44a6 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/conv_module.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/conv_module.py @@ -11,7 +11,6 @@ """The code is based on DEKR: https://github.com/HRNet/DEKR/tree/main""" import logging -from typing import List import torch.nn as nn @@ -46,7 +45,7 @@ def __init__( fuse_method: str, multi_scale_output: bool = True, ): - super(HighResolutionModule, self).__init__() + super().__init__() self._check_branches(num_branches, block, num_blocks, num_inchannels, num_channels) self.num_inchannels = num_inchannels @@ -68,17 +67,17 @@ def _check_branches( num_channels: int, ): if num_branches != len(num_blocks): - error_msg = "NUM_BRANCHES({}) <> NUM_BLOCKS({})".format(num_branches, len(num_blocks)) + error_msg = f"NUM_BRANCHES({num_branches}) <> NUM_BLOCKS({len(num_blocks)})" logger.error(error_msg) raise ValueError(error_msg) if num_branches != len(num_channels): - error_msg = "NUM_BRANCHES({}) <> NUM_CHANNELS({})".format(num_branches, len(num_channels)) + error_msg = f"NUM_BRANCHES({num_branches}) <> NUM_CHANNELS({len(num_channels)})" logger.error(error_msg) raise ValueError(error_msg) if num_branches != len(num_inchannels): - error_msg = "NUM_BRANCHES({}) <> NUM_INCHANNELS({})".format(num_branches, len(num_inchannels)) + error_msg = f"NUM_BRANCHES({num_branches}) <> NUM_INCHANNELS({len(num_inchannels)})" logger.error(error_msg) raise ValueError(error_msg) @@ -195,7 +194,7 @@ def _make_fuse_layers(self) -> nn.ModuleList: def get_num_inchannels(self) -> int: return self.num_inchannels - def forward(self, x) -> List: + def forward(self, x) -> list: """Forward pass through the HighResolutionModule. Args: diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/gated_attention_unit.py b/deeplabcut/pose_estimation_pytorch/models/modules/gated_attention_unit.py index 7f7ccc3105..f6c6f161fa 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/gated_attention_unit.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/gated_attention_unit.py @@ -18,10 +18,10 @@ import math +import timm.layers as timm_layers import torch import torch.nn as nn import torch.nn.functional as F -import timm.layers as timm_layers from deeplabcut.pose_estimation_pytorch.models.modules.norm import ScaleNorm @@ -91,7 +91,7 @@ def __init__( use_rel_bias=True, pos_enc=False, ): - super(GatedAttentionUnit, self).__init__() + super().__init__() self.s = s self.num_token = num_token self.use_rel_bias = use_rel_bias diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py index 27bfffecc4..17c6d87521 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py @@ -13,14 +13,11 @@ from abc import ABC, abstractmethod import cv2 -import numpy as np -import torch -import torchvision.transforms.functional as TF import matplotlib.pyplot as plt +import numpy as np -from deeplabcut.pose_estimation_pytorch.registry import Registry, build_from_cfg from deeplabcut.pose_estimation_pytorch.data.utils import out_of_bounds_keypoints - +from deeplabcut.pose_estimation_pytorch.registry import Registry, build_from_cfg KEYPOINT_ENCODERS = Registry("kpt_encoders", build_func=build_from_cfg) diff --git a/deeplabcut/pose_estimation_pytorch/models/necks/__init__.py b/deeplabcut/pose_estimation_pytorch/models/necks/__init__.py index 5b3823ab6b..1462f9b213 100644 --- a/deeplabcut/pose_estimation_pytorch/models/necks/__init__.py +++ b/deeplabcut/pose_estimation_pytorch/models/necks/__init__.py @@ -8,5 +8,5 @@ # # Licensed under GNU Lesser General Public License v3.0 # -from deeplabcut.pose_estimation_pytorch.models.necks.base import BaseNeck, NECKS +from deeplabcut.pose_estimation_pytorch.models.necks.base import NECKS, BaseNeck from deeplabcut.pose_estimation_pytorch.models.necks.transformer import Transformer diff --git a/deeplabcut/pose_estimation_pytorch/models/necks/base.py b/deeplabcut/pose_estimation_pytorch/models/necks/base.py index 201456a5d4..c4eea8234d 100644 --- a/deeplabcut/pose_estimation_pytorch/models/necks/base.py +++ b/deeplabcut/pose_estimation_pytorch/models/necks/base.py @@ -12,7 +12,7 @@ import torch -from deeplabcut.pose_estimation_pytorch.registry import build_from_cfg, Registry +from deeplabcut.pose_estimation_pytorch.registry import Registry, build_from_cfg NECKS = Registry("necks", build_func=build_from_cfg) diff --git a/deeplabcut/pose_estimation_pytorch/models/necks/layers.py b/deeplabcut/pose_estimation_pytorch/models/necks/layers.py index c5ee550d50..e1916f9973 100644 --- a/deeplabcut/pose_estimation_pytorch/models/necks/layers.py +++ b/deeplabcut/pose_estimation_pytorch/models/necks/layers.py @@ -11,7 +11,7 @@ import torch import torch.nn.functional as F -from einops import rearrange, repeat +from einops import rearrange class Residual(torch.nn.Module): diff --git a/deeplabcut/pose_estimation_pytorch/models/necks/transformer.py b/deeplabcut/pose_estimation_pytorch/models/necks/transformer.py index 09dba734d1..f199aadd92 100644 --- a/deeplabcut/pose_estimation_pytorch/models/necks/transformer.py +++ b/deeplabcut/pose_estimation_pytorch/models/necks/transformer.py @@ -8,13 +8,12 @@ # # Licensed under GNU Lesser General Public License v3.0 # -from typing import Tuple import torch from einops import rearrange, repeat from timm.layers import trunc_normal_ -from deeplabcut.pose_estimation_pytorch.models.necks.base import BaseNeck, NECKS +from deeplabcut.pose_estimation_pytorch.models.necks.base import NECKS, BaseNeck from deeplabcut.pose_estimation_pytorch.models.necks.layers import TransformerLayer from deeplabcut.pose_estimation_pytorch.models.necks.utils import ( make_sine_position_embedding, @@ -79,15 +78,15 @@ class Transformer(BaseNeck): def __init__( self, *, - feature_size: Tuple[int, int], - patch_size: Tuple[int, int], + feature_size: tuple[int, int], + patch_size: tuple[int, int], num_keypoints: int, dim: int, depth: int, heads: int, mlp_dim: int = 3, apply_init: bool = False, - heatmap_size: Tuple[int, int] = (64, 64), + heatmap_size: tuple[int, int] = (64, 64), channels: int = 32, dropout: float = 0.0, emb_dropout: float = 0.0, @@ -165,7 +164,7 @@ def _make_position_embedding(self, w: int, h: int, d_model: int, pe_type="learna with torch.no_grad(): self.pe_h = h self.pe_w = w - length = h * w + h * w if pe_type != "learnable": self.pos_embedding = torch.nn.Parameter(make_sine_position_embedding(h, w, d_model), requires_grad=False) else: @@ -199,7 +198,7 @@ def _make_layer(self, block: torch.nn.Module, planes: int, blocks: int, stride: layers = [] layers.append(block(self.inplanes, planes, stride, downsample)) self.inplanes = planes * block.expansion - for i in range(1, blocks): + for _i in range(1, blocks): layers.append(block(self.inplanes, planes)) return torch.nn.Sequential(*layers) diff --git a/deeplabcut/pose_estimation_pytorch/models/predictors/base.py b/deeplabcut/pose_estimation_pytorch/models/predictors/base.py index c0b0950da5..42c9fb5864 100644 --- a/deeplabcut/pose_estimation_pytorch/models/predictors/base.py +++ b/deeplabcut/pose_estimation_pytorch/models/predictors/base.py @@ -15,7 +15,7 @@ import torch from torch import nn -from deeplabcut.pose_estimation_pytorch.registry import build_from_cfg, Registry +from deeplabcut.pose_estimation_pytorch.registry import Registry, build_from_cfg PREDICTORS = Registry("predictors", build_func=build_from_cfg) diff --git a/deeplabcut/pose_estimation_pytorch/models/predictors/dekr_predictor.py b/deeplabcut/pose_estimation_pytorch/models/predictors/dekr_predictor.py index 8c525464de..bc96097d6b 100644 --- a/deeplabcut/pose_estimation_pytorch/models/predictors/dekr_predictor.py +++ b/deeplabcut/pose_estimation_pytorch/models/predictors/dekr_predictor.py @@ -15,8 +15,8 @@ import torch.nn.functional as F from deeplabcut.pose_estimation_pytorch.models.predictors import ( - BasePredictor, PREDICTORS, + BasePredictor, ) diff --git a/deeplabcut/pose_estimation_pytorch/models/predictors/identity_predictor.py b/deeplabcut/pose_estimation_pytorch/models/predictors/identity_predictor.py index 493d389386..35461e6af0 100644 --- a/deeplabcut/pose_estimation_pytorch/models/predictors/identity_predictor.py +++ b/deeplabcut/pose_estimation_pytorch/models/predictors/identity_predictor.py @@ -15,8 +15,8 @@ import torchvision.transforms.functional as F from deeplabcut.pose_estimation_pytorch.models.predictors.base import ( - BasePredictor, PREDICTORS, + BasePredictor, ) diff --git a/deeplabcut/pose_estimation_pytorch/models/predictors/paf_predictor.py b/deeplabcut/pose_estimation_pytorch/models/predictors/paf_predictor.py index 900ec33bf4..c67223a4fb 100644 --- a/deeplabcut/pose_estimation_pytorch/models/predictors/paf_predictor.py +++ b/deeplabcut/pose_estimation_pytorch/models/predictors/paf_predictor.py @@ -10,17 +10,18 @@ # from __future__ import annotations +from collections import defaultdict + import numpy as np import torch import torch.nn.functional as F from numpy.typing import NDArray -from collections import defaultdict +from deeplabcut.core import inferenceutils from deeplabcut.pose_estimation_pytorch.models.predictors.base import ( - BasePredictor, PREDICTORS, + BasePredictor, ) -from deeplabcut.core import inferenceutils Graph = list[tuple[int, int]] @@ -384,7 +385,7 @@ def compute_edge_costs( # Build an index dict of slices for group lookup by (batch, limb) batch_groups = defaultdict(list) # (batch)->list of (limb, start, end) - for st, en in zip(group_starts, group_ends): + for st, en in zip(group_starts, group_ends, strict=False): b = batch_inds[st] k = edge_idx[st] batch_groups[b].append((k, st, en)) diff --git a/deeplabcut/pose_estimation_pytorch/models/predictors/sim_cc.py b/deeplabcut/pose_estimation_pytorch/models/predictors/sim_cc.py index 41a296b383..6bfe68b488 100644 --- a/deeplabcut/pose_estimation_pytorch/models/predictors/sim_cc.py +++ b/deeplabcut/pose_estimation_pytorch/models/predictors/sim_cc.py @@ -20,8 +20,8 @@ import torch from deeplabcut.pose_estimation_pytorch.models.predictors.base import ( - BasePredictor, PREDICTORS, + BasePredictor, ) diff --git a/deeplabcut/pose_estimation_pytorch/models/predictors/single_predictor.py b/deeplabcut/pose_estimation_pytorch/models/predictors/single_predictor.py index dfac614b7e..c379a7f213 100644 --- a/deeplabcut/pose_estimation_pytorch/models/predictors/single_predictor.py +++ b/deeplabcut/pose_estimation_pytorch/models/predictors/single_predictor.py @@ -10,13 +10,11 @@ # from __future__ import annotations -from typing import Tuple - import torch from deeplabcut.pose_estimation_pytorch.models.predictors.base import ( - BasePredictor, PREDICTORS, + BasePredictor, ) @@ -93,7 +91,7 @@ def forward(self, stride: float, outputs: dict[str, torch.Tensor]) -> dict[str, return {"poses": poses} - def get_top_values(self, heatmap: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: + def get_top_values(self, heatmap: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: """Get the top values from the heatmap. Args: diff --git a/deeplabcut/pose_estimation_pytorch/models/target_generators/base.py b/deeplabcut/pose_estimation_pytorch/models/target_generators/base.py index 6842277331..d4097356a2 100644 --- a/deeplabcut/pose_estimation_pytorch/models/target_generators/base.py +++ b/deeplabcut/pose_estimation_pytorch/models/target_generators/base.py @@ -15,7 +15,7 @@ import torch import torch.nn as nn -from deeplabcut.pose_estimation_pytorch.registry import build_from_cfg, Registry +from deeplabcut.pose_estimation_pytorch.registry import Registry, build_from_cfg TARGET_GENERATORS = Registry("target_generators", build_func=build_from_cfg) diff --git a/deeplabcut/pose_estimation_pytorch/models/target_generators/dekr_targets.py b/deeplabcut/pose_estimation_pytorch/models/target_generators/dekr_targets.py index 1bb8c6d668..402f449aaf 100644 --- a/deeplabcut/pose_estimation_pytorch/models/target_generators/dekr_targets.py +++ b/deeplabcut/pose_estimation_pytorch/models/target_generators/dekr_targets.py @@ -14,8 +14,8 @@ import torch from deeplabcut.pose_estimation_pytorch.models.target_generators.base import ( - BaseGenerator, TARGET_GENERATORS, + BaseGenerator, ) diff --git a/deeplabcut/pose_estimation_pytorch/models/target_generators/heatmap_targets.py b/deeplabcut/pose_estimation_pytorch/models/target_generators/heatmap_targets.py index 3c8d8e2f61..3a47093905 100644 --- a/deeplabcut/pose_estimation_pytorch/models/target_generators/heatmap_targets.py +++ b/deeplabcut/pose_estimation_pytorch/models/target_generators/heatmap_targets.py @@ -17,8 +17,8 @@ import torch from deeplabcut.pose_estimation_pytorch.models.target_generators.base import ( - BaseGenerator, TARGET_GENERATORS, + BaseGenerator, ) diff --git a/deeplabcut/pose_estimation_pytorch/models/target_generators/pafs_targets.py b/deeplabcut/pose_estimation_pytorch/models/target_generators/pafs_targets.py index 404a8db4cc..1ac22d9e8a 100644 --- a/deeplabcut/pose_estimation_pytorch/models/target_generators/pafs_targets.py +++ b/deeplabcut/pose_estimation_pytorch/models/target_generators/pafs_targets.py @@ -16,8 +16,8 @@ import torch from deeplabcut.pose_estimation_pytorch.models.target_generators.base import ( - BaseGenerator, TARGET_GENERATORS, + BaseGenerator, ) diff --git a/deeplabcut/pose_estimation_pytorch/models/target_generators/sim_cc.py b/deeplabcut/pose_estimation_pytorch/models/target_generators/sim_cc.py index dbc68c2c31..cfb4426052 100644 --- a/deeplabcut/pose_estimation_pytorch/models/target_generators/sim_cc.py +++ b/deeplabcut/pose_estimation_pytorch/models/target_generators/sim_cc.py @@ -22,8 +22,8 @@ import torch from deeplabcut.pose_estimation_pytorch.models.target_generators.base import ( - BaseGenerator, TARGET_GENERATORS, + BaseGenerator, ) diff --git a/deeplabcut/pose_estimation_pytorch/models/weight_init.py b/deeplabcut/pose_estimation_pytorch/models/weight_init.py index 9d2a4d539f..c660c5b294 100644 --- a/deeplabcut/pose_estimation_pytorch/models/weight_init.py +++ b/deeplabcut/pose_estimation_pytorch/models/weight_init.py @@ -6,7 +6,7 @@ import torch.nn as nn -from deeplabcut.pose_estimation_pytorch.registry import build_from_cfg, Registry +from deeplabcut.pose_estimation_pytorch.registry import Registry, build_from_cfg def _build_weight_init(cfg: str | dict, **kwargs) -> BaseWeightInitializer: diff --git a/deeplabcut/pose_estimation_pytorch/modelzoo/inference.py b/deeplabcut/pose_estimation_pytorch/modelzoo/inference.py index 5b9a2eed11..b5985fd704 100644 --- a/deeplabcut/pose_estimation_pytorch/modelzoo/inference.py +++ b/deeplabcut/pose_estimation_pytorch/modelzoo/inference.py @@ -14,15 +14,15 @@ import numpy as np from deeplabcut.modelzoo.utils import get_super_animal_scorer, get_superanimal_colormaps -from deeplabcut.pose_estimation_pytorch.apis.videos import ( - create_df_from_prediction, - video_inference, - VideoIterator, -) from deeplabcut.pose_estimation_pytorch.apis.utils import ( + get_filtered_coco_detector_inference_runner, get_inference_runners, get_pose_inference_runner, - get_filtered_coco_detector_inference_runner, +) +from deeplabcut.pose_estimation_pytorch.apis.videos import ( + VideoIterator, + create_df_from_prediction, + video_inference, ) from deeplabcut.pose_estimation_pytorch.modelzoo.utils import ( raise_warning_if_called_directly, diff --git a/deeplabcut/pose_estimation_pytorch/modelzoo/memory_replay.py b/deeplabcut/pose_estimation_pytorch/modelzoo/memory_replay.py index 90db53dcfa..c43456ed90 100644 --- a/deeplabcut/pose_estimation_pytorch/modelzoo/memory_replay.py +++ b/deeplabcut/pose_estimation_pytorch/modelzoo/memory_replay.py @@ -67,7 +67,7 @@ def get_pose_predictions( # COCO-format annotations file containing predictions made by the SuperAnimal model sa_predictions = {} if predictions_file.exists(): - with open(predictions_file, "r") as f: + with open(predictions_file) as f: raw_sa_predictions = json.load(f) # parse predictions to convert lists to numpy arrays @@ -142,7 +142,7 @@ def prepare_memory_replay_dataset( # Contains the ground truth annotations for the DeepLabCut project # .../dlc-models-pytorch/.../...shuffle0/train/memory_replay/annotations/train.json - with open(source_dataset_folder / "annotations" / train_file, "r") as f: + with open(source_dataset_folder / "annotations" / train_file) as f: project_gt = json.load(f) # parse the GT so that image paths are in the format (no matter the OS): diff --git a/deeplabcut/pose_estimation_pytorch/modelzoo/utils.py b/deeplabcut/pose_estimation_pytorch/modelzoo/utils.py index 1b394513b9..207b460865 100644 --- a/deeplabcut/pose_estimation_pytorch/modelzoo/utils.py +++ b/deeplabcut/pose_estimation_pytorch/modelzoo/utils.py @@ -164,7 +164,7 @@ def get_gpu_memory_map(): def select_device(): if torch.cuda.is_available(): - return torch.device(f"cuda:0") + return torch.device("cuda:0") else: return torch.device("cpu") @@ -174,7 +174,7 @@ def raise_warning_if_called_directly(): caller_frame = inspect.getouterframes(current_frame, 2) caller_name = caller_frame[1].filename - if not "pose_estimation_" in caller_name: + if "pose_estimation_" not in caller_name: warnings.warn( f"{caller_name} is intended for internal use only and should not be called directly.", UserWarning, diff --git a/deeplabcut/pose_estimation_pytorch/registry.py b/deeplabcut/pose_estimation_pytorch/registry.py index ae992773c9..6d9f549103 100644 --- a/deeplabcut/pose_estimation_pytorch/registry.py +++ b/deeplabcut/pose_estimation_pytorch/registry.py @@ -10,10 +10,10 @@ # import inspect from functools import partial -from typing import Any, Dict, Optional +from typing import Any -def build_from_cfg(cfg: Dict, registry: "Registry", default_args: Optional[Dict] = None) -> Any: +def build_from_cfg(cfg: dict, registry: "Registry", default_args: dict | None = None) -> Any: """Builds a module from the configuration dictionary when it represents a class configuration, or call a function from the configuration dictionary when it represents a function configuration. diff --git a/deeplabcut/pose_estimation_pytorch/runners/__init__.py b/deeplabcut/pose_estimation_pytorch/runners/__init__.py index 6d3aa5fac7..fff68b2d50 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/__init__.py +++ b/deeplabcut/pose_estimation_pytorch/runners/__init__.py @@ -10,10 +10,10 @@ # from deeplabcut.pose_estimation_pytorch.runners.base import ( + Runner, attempt_snapshot_load, - get_load_weights_only, fix_snapshot_metadata, - Runner, + get_load_weights_only, set_load_weights_only, ) from deeplabcut.pose_estimation_pytorch.runners.ctd import CTDTrackingConfig @@ -22,16 +22,16 @@ TopDownDynamicCropper, ) from deeplabcut.pose_estimation_pytorch.runners.inference import ( - build_inference_runner, DetectorInferenceRunner, InferenceRunner, PoseInferenceRunner, + build_inference_runner, ) from deeplabcut.pose_estimation_pytorch.runners.logger import LOGGER from deeplabcut.pose_estimation_pytorch.runners.snapshots import TorchSnapshotManager from deeplabcut.pose_estimation_pytorch.runners.train import ( - build_training_runner, DetectorTrainingRunner, PoseTrainingRunner, TrainingRunner, + build_training_runner, ) diff --git a/deeplabcut/pose_estimation_pytorch/runners/dynamic_cropping.py b/deeplabcut/pose_estimation_pytorch/runners/dynamic_cropping.py index 8f3eb6a113..e1746f1c70 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/dynamic_cropping.py +++ b/deeplabcut/pose_estimation_pytorch/runners/dynamic_cropping.py @@ -113,7 +113,7 @@ def update(self, pose: torch.Tensor) -> torch.Tensor: The pose, with coordinates updated to the full image space. """ if self._shape is None: - raise RuntimeError(f"You must call `crop` before calling `update`.") + raise RuntimeError("You must call `crop` before calling `update`.") # offset the pose to the original image space offset_x, offset_y = 0, 0 @@ -338,7 +338,7 @@ def update(self, pose: torch.Tensor) -> torch.Tensor: The pose, with coordinates updated to the full image space. """ if self._shape is None: - raise RuntimeError(f"You must call `crop` before calling `update`.") + raise RuntimeError("You must call `crop` before calling `update`.") # check whether this was a patched crop batch_size = pose.shape[0] diff --git a/deeplabcut/pose_estimation_pytorch/runners/inference.py b/deeplabcut/pose_estimation_pytorch/runners/inference.py index 370b12093e..aa8c9989f1 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/inference.py +++ b/deeplabcut/pose_estimation_pytorch/runners/inference.py @@ -10,13 +10,14 @@ # from __future__ import annotations +import threading import warnings from abc import ABCMeta, abstractmethod -from dataclasses import dataclass, field, asdict +from collections.abc import Iterable +from dataclasses import asdict, dataclass, field from pathlib import Path -from typing import Any, Generic, Iterable -import threading -from queue import Queue, Empty, Full +from queue import Empty, Full, Queue +from typing import Any, Generic import numpy as np import torch @@ -68,7 +69,7 @@ class MultithreadingConfig: timeout: float = 30.0 @classmethod - def from_dict(cls, data: dict[str, Any]) -> "MultithreadingConfig": + def from_dict(cls, data: dict[str, Any]) -> MultithreadingConfig: return cls(**_merge_defaults(cls, data or {})) def to_dict(self) -> dict: @@ -87,7 +88,7 @@ class CompileConfig: backend: str = "inductor" @classmethod - def from_dict(cls, data: dict[str, Any]) -> "CompileConfig": + def from_dict(cls, data: dict[str, Any]) -> CompileConfig: return cls(**_merge_defaults(cls, data or {})) def to_dict(self) -> dict: @@ -104,7 +105,7 @@ class AutocastConfig: enabled: bool = False @classmethod - def from_dict(cls, data: dict[str, Any]) -> "AutocastConfig": + def from_dict(cls, data: dict[str, Any]) -> AutocastConfig: return cls(**_merge_defaults(cls, data or {})) def to_dict(self) -> dict: @@ -124,7 +125,7 @@ class InferenceConfig: conditions: dict | None = None @classmethod - def from_dict(cls, data: dict[str, Any] | None) -> "InferenceConfig": + def from_dict(cls, data: dict[str, Any] | None) -> InferenceConfig: """ Build an InferenceConfig from a dict, supporting: - nested dictionaries @@ -1035,8 +1036,8 @@ def build_inference_runner( if task == Task.DETECT: if dynamic is not None: raise ValueError( - f"The DynamicCropper can only be used for pose estimation; not object " - f"detection. Please turn off dynamic cropping." + "The DynamicCropper can only be used for pose estimation; not object " + "detection. Please turn off dynamic cropping." ) return DetectorInferenceRunner(**kwargs) diff --git a/deeplabcut/pose_estimation_pytorch/runners/logger.py b/deeplabcut/pose_estimation_pytorch/runners/logger.py index c422d2b4a9..6d73af4a66 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/logger.py +++ b/deeplabcut/pose_estimation_pytorch/runners/logger.py @@ -14,15 +14,15 @@ import logging from abc import ABC, abstractmethod from pathlib import Path -from typing import Any, Optional +from typing import Any import numpy as np import torch import torchvision.transforms as transforms import torchvision.transforms.functional as F +import yaml from torch.utils.data import DataLoader from torchvision.utils import draw_bounding_boxes, draw_keypoints -import yaml try: import wandb @@ -80,7 +80,7 @@ def log_config(self, config: dict = None) -> None: """ @abstractmethod - def log(self, metrics: dict[str, Any], step: Optional[int] = None) -> None: + def log(self, metrics: dict[str, Any], step: int | None = None) -> None: """Logs data from a training run Args: @@ -317,7 +317,7 @@ def _save_wandb_info(self): logging.info(f"WandB run info saved to {output_path}") - def log(self, metrics: dict[str, Any], step: Optional[int] = None) -> None: + def log(self, metrics: dict[str, Any], step: int | None = None) -> None: """Logs metrics from runs Args: @@ -413,7 +413,7 @@ def __init__(self, train_folder: str, log_filename: str) -> None: if self.log_file.exists(): self._load_existing_data() - def log(self, metrics: dict[str, Any], step: Optional[int] = None) -> None: + def log(self, metrics: dict[str, Any], step: int | None = None) -> None: """Logs metrics from runs Args: @@ -454,7 +454,7 @@ def _load_existing_data(self) -> None: """Loads existing CSV data if the log file exists""" logging.info(f"Loading existing CSV data from {self.log_file}") try: - with open(self.log_file, "r", newline="") as f: + with open(self.log_file, newline="") as f: reader = csv.DictReader(f) # Update logged metrics from header diff --git a/deeplabcut/pose_estimation_pytorch/runners/shelving.py b/deeplabcut/pose_estimation_pytorch/runners/shelving.py index b65922ddb0..5d218429f9 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/shelving.py +++ b/deeplabcut/pose_estimation_pytorch/runners/shelving.py @@ -51,7 +51,7 @@ def close(self) -> None: def keys(self) -> list[str]: if not self._open: - raise ValueError(f"You must call open() before reading keys!") + raise ValueError("You must call open() before reading keys!") return [k for k in self._db] @@ -69,7 +69,7 @@ def __getitem__(self, item: str) -> dict: The item. """ if not self._open: - raise ValueError(f"You must call open() before reading data!") + raise ValueError("You must call open() before reading data!") return self._db[item] @@ -113,7 +113,7 @@ def add_prediction( identity_scores: The predicted identities, if there are any. """ if not self._open: - raise ValueError(f"You must call open() before adding data!") + raise ValueError("You must call open() before adding data!") key = "frame" + str(self._frame_index).zfill(self._str_width) @@ -206,7 +206,7 @@ def add_prediction( features: The features for the bodyparts. """ if not self._open: - raise ValueError(f"You must call open() before adding data!") + raise ValueError("You must call open() before adding data!") key = "frame" + str(self._frame_index).zfill(self._str_width) diff --git a/deeplabcut/pose_estimation_pytorch/runners/snapshots.py b/deeplabcut/pose_estimation_pytorch/runners/snapshots.py index b5347d8822..bb094218f3 100755 --- a/deeplabcut/pose_estimation_pytorch/runners/snapshots.py +++ b/deeplabcut/pose_estimation_pytorch/runners/snapshots.py @@ -19,7 +19,7 @@ import numpy as np import torch -from deeplabcut.pose_estimation_pytorch.data.snapshots import list_snapshots, Snapshot +from deeplabcut.pose_estimation_pytorch.data.snapshots import Snapshot, list_snapshots @dataclass @@ -72,7 +72,7 @@ class TorchSnapshotManager: _key: str = field(init=False) def __post_init__(self): - assert self.max_snapshots > 0, f"max_snapshots must be a positive integer" + assert self.max_snapshots > 0, "max_snapshots must be a positive integer" self._key = f"metrics/{self.key_metric}" def update(self, epoch: int, state_dict: dict, last: bool = False) -> None: diff --git a/deeplabcut/pose_estimation_pytorch/runners/train.py b/deeplabcut/pose_estimation_pytorch/runners/train.py index 4c5e634777..4220054724 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/train.py +++ b/deeplabcut/pose_estimation_pytorch/runners/train.py @@ -27,9 +27,9 @@ from deeplabcut.pose_estimation_pytorch.models.detectors import BaseDetector from deeplabcut.pose_estimation_pytorch.models.model import PoseModel from deeplabcut.pose_estimation_pytorch.runners.base import ( - attempt_snapshot_load, ModelType, Runner, + attempt_snapshot_load, ) from deeplabcut.pose_estimation_pytorch.runners.logger import ( BaseLogger, @@ -233,7 +233,7 @@ def fit( epoch_metrics = self._metadata.get("metrics") if e % self.eval_interval == 0 and epoch_metrics is not None and len(epoch_metrics) > 0: - logging.info(f"Model performance:") + logging.info("Model performance:") line_length = max([len(name) for name in epoch_metrics.keys()]) + 2 for name, score in epoch_metrics.items(): logging.info(f" {(name + ':').ljust(line_length)}{score:6.2f}") diff --git a/deeplabcut/pose_estimation_pytorch/utils.py b/deeplabcut/pose_estimation_pytorch/utils.py index 7ccacaa904..b2891bf92c 100644 --- a/deeplabcut/pose_estimation_pytorch/utils.py +++ b/deeplabcut/pose_estimation_pytorch/utils.py @@ -12,13 +12,10 @@ import os import random -from pathlib import Path import numpy as np import torch -from deeplabcut.utils.auxiliaryfunctions import read_plainconfig - def create_folder(path_to_folder): """Creates all folders contained in the path. diff --git a/deeplabcut/pose_estimation_tensorflow/__init__.py b/deeplabcut/pose_estimation_tensorflow/__init__.py index 963368ac08..31282b7a4f 100644 --- a/deeplabcut/pose_estimation_tensorflow/__init__.py +++ b/deeplabcut/pose_estimation_tensorflow/__init__.py @@ -12,19 +12,19 @@ # Licensed under GNU Lesser General Public License v3.0 # -from . import _tf_legacy - # Suppress tensorflow warning messages import tensorflow as tf +from . import _tf_legacy + tf.compat.v1.logging.set_verbosity(tf.compat.v1.logging.ERROR) from deeplabcut.pose_estimation_tensorflow.config import * -from deeplabcut.pose_estimation_tensorflow.datasets import * -from deeplabcut.pose_estimation_tensorflow.default_config import * from deeplabcut.pose_estimation_tensorflow.core.evaluate import * -from deeplabcut.pose_estimation_tensorflow.core.train import * from deeplabcut.pose_estimation_tensorflow.core.test import * +from deeplabcut.pose_estimation_tensorflow.core.train import * +from deeplabcut.pose_estimation_tensorflow.datasets import * +from deeplabcut.pose_estimation_tensorflow.default_config import * from deeplabcut.pose_estimation_tensorflow.export import export_model from deeplabcut.pose_estimation_tensorflow.models import * from deeplabcut.pose_estimation_tensorflow.nnets import * diff --git a/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_builder.py b/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_builder.py index 42147d19f9..85af4d31c6 100644 --- a/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_builder.py +++ b/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_builder.py @@ -16,6 +16,7 @@ import functools import os import re + import tensorflow as tf import deeplabcut.pose_estimation_tensorflow.backbones.efficientnet_model as efficientnet_model @@ -41,7 +42,7 @@ def efficientnet_params(model_name): return params_dict[model_name] -class BlockDecoder(object): +class BlockDecoder: """Block Decoder for readability.""" def _decode_block_string(self, block_string): diff --git a/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_model.py b/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_model.py index c643f2d618..d39dc9f578 100644 --- a/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_model.py +++ b/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_model.py @@ -21,6 +21,7 @@ import collections import math + import numpy as np import tensorflow as tf @@ -118,7 +119,7 @@ def round_filters(filters, global_params): # Make sure that round down does not go down by more than 10%. if new_filters < 0.9 * filters: new_filters += divisor - tf.compat.v1.logging.info("round_filter input={} output={}".format(orig_f, new_filters)) + tf.compat.v1.logging.info(f"round_filter input={orig_f} output={new_filters}") return int(new_filters) @@ -142,7 +143,7 @@ def __init__(self, block_args, global_params): block_args: BlockArgs, arguments to create a Block. global_params: GlobalParams, a set of global parameters. """ - super(MBConvBlock, self).__init__() + super().__init__() self._block_args = block_args self._batch_norm_momentum = global_params.batch_norm_momentum self._batch_norm_epsilon = global_params.batch_norm_epsilon @@ -377,7 +378,7 @@ def __init__(self, blocks_args=None, global_params=None): Raises: ValueError: when blocks_args is not specified as a list. """ - super(Model, self).__init__() + super().__init__() if not isinstance(blocks_args, list): raise ValueError("blocks_args should be a list.") self._global_params = global_params diff --git a/deeplabcut/pose_estimation_tensorflow/backbones/mobilenet.py b/deeplabcut/pose_estimation_tensorflow/backbones/mobilenet.py index 40456ebd6b..c382404dfd 100644 --- a/deeplabcut/pose_estimation_tensorflow/backbones/mobilenet.py +++ b/deeplabcut/pose_estimation_tensorflow/backbones/mobilenet.py @@ -72,7 +72,7 @@ def op(opfunc, multiplier_func=depth_multiplier, **params): return _Op(opfunc, params=params, multiplier_func=multiplier) -class NoOpScope(object): +class NoOpScope: """No-op context manager.""" def __enter__(self): diff --git a/deeplabcut/pose_estimation_tensorflow/backbones/mobilenet_v2.py b/deeplabcut/pose_estimation_tensorflow/backbones/mobilenet_v2.py index bedbe4c96e..59e27744e9 100644 --- a/deeplabcut/pose_estimation_tensorflow/backbones/mobilenet_v2.py +++ b/deeplabcut/pose_estimation_tensorflow/backbones/mobilenet_v2.py @@ -29,8 +29,8 @@ import tensorflow as tf import tf_slim as slim -from deeplabcut.pose_estimation_tensorflow.nnets import conv_blocks as ops from deeplabcut.pose_estimation_tensorflow.backbones import mobilenet as lib +from deeplabcut.pose_estimation_tensorflow.nnets import conv_blocks as ops op = lib.op diff --git a/deeplabcut/pose_estimation_tensorflow/config.py b/deeplabcut/pose_estimation_tensorflow/config.py index d36bf01a01..2bbf60d53f 100644 --- a/deeplabcut/pose_estimation_tensorflow/config.py +++ b/deeplabcut/pose_estimation_tensorflow/config.py @@ -36,7 +36,7 @@ def _merge_a_into_b(a, b): try: _merge_a_into_b(a[k], b[k]) except: - print("Error under config key: {}".format(k)) + print(f"Error under config key: {k}") raise else: b[k] = v @@ -46,7 +46,7 @@ def cfg_from_file(filename): """ Load a config from file filename and merge it into the default options. """ - with open(filename, "r") as f: + with open(filename) as f: yaml_cfg = yaml.load(f, Loader=yaml.SafeLoader) # Update the snapshot path to the corresponding path! @@ -56,6 +56,7 @@ def cfg_from_file(filename): # reloading defaults, as they can bleed over from a previous run otherwise import importlib + from . import default_config importlib.reload(default_config) diff --git a/deeplabcut/pose_estimation_tensorflow/core/evaluate.py b/deeplabcut/pose_estimation_tensorflow/core/evaluate.py index 455a23274b..556dbf2140 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/evaluate.py +++ b/deeplabcut/pose_estimation_tensorflow/core/evaluate.py @@ -13,7 +13,6 @@ import argparse import os from pathlib import Path -from typing import List, Union import numpy as np import pandas as pd @@ -58,8 +57,9 @@ def calculatepafdistancebounds(config, shuffle=0, trainingsetindex=0, modelprefi """ import os - from deeplabcut.utils import auxiliaryfunctions, auxfun_multianimal + from deeplabcut.pose_estimation_tensorflow.config import load_config + from deeplabcut.utils import auxfun_multianimal, auxiliaryfunctions # Read file path for pose_config file. >> pass it on cfg = auxiliaryfunctions.read_config(config) @@ -106,7 +106,7 @@ def calculatepafdistancebounds(config, shuffle=0, trainingsetindex=0, modelprefi jointnames = [test_pose_cfg["all_joints_names"][i] for i in range(len(test_pose_cfg["all_joints"]))] path_inferencebounds_config = Path(modelfolder) / "test" / "inferencebounds.yaml" inferenceboundscfg = {} - for pi, edge in enumerate(partaffinityfield_graph): + for _pi, edge in enumerate(partaffinityfield_graph): j1, j2 = jointnames[edge[0]], jointnames[edge[1]] ds_within = [] ds_across = [] @@ -269,7 +269,7 @@ def return_evaluate_network_data( test_pose_cfg = load_config(str(path_test_config)) except FileNotFoundError: raise FileNotFoundError( - "It seems the model for shuffle %s and trainFraction %s does not exist." % (shuffle, trainFraction) + f"It seems the model for shuffle {shuffle} and trainFraction {trainFraction} does not exist." ) train_pose_cfg = load_config(str(path_train_config)) @@ -279,7 +279,7 @@ def return_evaluate_network_data( ) ########################### RESCALING (to global scale) - if rescale == True: + if rescale: scale = test_pose_cfg["global_scale"] print("Rescaling Data to ", scale) Data = ( @@ -347,7 +347,9 @@ def return_evaluate_network_data( resultsfilename, DLCscorer, ) = auxiliaryfunctions.check_if_not_evaluated(str(evaluationfolder), DLCscorer, DLCscorerlegacy, snapshot_name) - # resultsfilename=os.path.join(str(evaluationfolder),DLCscorer + '-' + str(Snapshots[snapindex])+ '.h5') # + '-' + str(snapshot)+ ' #'-' + Snapshots[snapindex]+ '.h5') + # resultsfilename=os.path.join(str(evaluationfolder),DLCscorer + '-' + + # str(Snapshots[snapindex])+ '.h5') # + '-' + str(snapshot)+ ' #'-' + + # Snapshots[snapindex]+ '.h5') print(resultsfilename) resultsfns.append(resultsfilename) if not returnjustfns: @@ -366,7 +368,7 @@ def return_evaluate_network_data( trainerror = np.nanmean(RMSE.iloc[trainIndices].values.flatten()) testerrorpcutoff = np.nanmean(RMSEpcutoff.iloc[testIndices].values.flatten()) trainerrorpcutoff = np.nanmean(RMSEpcutoff.iloc[trainIndices].values.flatten()) - if show_errors == True: + if show_errors: print( "Results for", trainingsiterations, @@ -406,7 +408,7 @@ def return_evaluate_network_data( results.append(r) else: print("Model not trained/evaluated!") - if fulldata == True: + if fulldata: DATA.append( [ DataMachine, @@ -427,7 +429,7 @@ def return_evaluate_network_data( if returnjustfns: return resultsfns else: - if fulldata == True: + if fulldata: return DATA, results else: return results @@ -436,8 +438,8 @@ def return_evaluate_network_data( def keypoint_error( df_error: pd.DataFrame, df_error_p_cutoff: pd.DataFrame, - train_indices: List[int], - test_indices: List[int], + train_indices: list[int], + test_indices: list[int], ) -> pd.DataFrame: """Computes the RMSE error for each bodypart @@ -484,7 +486,7 @@ def keypoint_error( def evaluate_network( config, - Shuffles=[1], + Shuffles=None, trainingsetindex=0, plotting=False, show_errors=True, @@ -493,7 +495,7 @@ def evaluate_network( rescale=False, modelprefix="", per_keypoint_evaluation: bool = False, - snapshots_to_evaluate: List[str] = None, + snapshots_to_evaluate: list[str] = None, ): """Evaluates the network. @@ -584,6 +586,8 @@ def evaluate_network( Note: This defaults to standard plotting for single-animal projects. """ + if Shuffles is None: + Shuffles = [1] if plotting not in (True, False, "bodypart", "individual"): raise ValueError(f"Unknown value for `plotting`={plotting}") @@ -610,12 +614,13 @@ def evaluate_network( snapshots_to_evaluate=snapshots_to_evaluate, ) else: - from deeplabcut.utils.auxfun_videos import imread, imresize - from deeplabcut.pose_estimation_tensorflow.core import predict + import tensorflow as tf + from deeplabcut.pose_estimation_tensorflow.config import load_config + from deeplabcut.pose_estimation_tensorflow.core import predict from deeplabcut.pose_estimation_tensorflow.datasets.utils import data_to_input from deeplabcut.utils import auxiliaryfunctions, conversioncode - import tensorflow as tf + from deeplabcut.utils.auxfun_videos import imread, imresize # If a string was passed in, auto-convert to True for backward compatibility plotting = bool(plotting) @@ -977,9 +982,9 @@ def make_results_file(final_result, evaluationfolder, DLCscorer): def get_available_requested_snapshots( - requested_snapshots: List[str], - available_snapshots: List[str], -) -> List[str]: + requested_snapshots: list[str], + available_snapshots: list[str], +) -> list[str]: """ Intersects the requested snapshot names with the available snapshots. @@ -1002,9 +1007,9 @@ def get_available_requested_snapshots( def get_snapshots_by_index( - idx: Union[int, str], - available_snapshots: List[str], -) -> List[str]: + idx: int | str, + available_snapshots: list[str], +) -> list[str]: """ Assume available_snapshots is ordered in ascending order. Returns snapshot names. """ diff --git a/deeplabcut/pose_estimation_tensorflow/core/evaluate_multianimal.py b/deeplabcut/pose_estimation_tensorflow/core/evaluate_multianimal.py index 7307daf9b7..4692ce677d 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/evaluate_multianimal.py +++ b/deeplabcut/pose_estimation_tensorflow/core/evaluate_multianimal.py @@ -9,25 +9,25 @@ # Licensed under GNU Lesser General Public License v3.0 # -import imgaug.augmenters as iaa import os import pickle from pathlib import Path + +import imgaug.augmenters as iaa import numpy as np import pandas as pd from tqdm import tqdm -from typing import List from deeplabcut.core import crossvalutils from deeplabcut.core.crossvalutils import find_closest_neighbors +from deeplabcut.pose_estimation_tensorflow.config import load_config from deeplabcut.pose_estimation_tensorflow.core.evaluate import ( - make_results_file, - keypoint_error, get_available_requested_snapshots, get_snapshots_by_index, + keypoint_error, + make_results_file, ) from deeplabcut.pose_estimation_tensorflow.training import return_train_network_path -from deeplabcut.pose_estimation_tensorflow.config import load_config from deeplabcut.utils import visualization @@ -96,21 +96,23 @@ def evaluate_multianimal_full( gputouse=None, modelprefix="", per_keypoint_evaluation: bool = False, - snapshots_to_evaluate: List[str] = None, + snapshots_to_evaluate: list[str] = None, ): + import tensorflow as tf + from deeplabcut.pose_estimation_tensorflow.core import ( predict, + ) + from deeplabcut.pose_estimation_tensorflow.core import ( predict_multianimal as predictma, ) from deeplabcut.utils import ( - auxiliaryfunctions, auxfun_multianimal, auxfun_videos, + auxiliaryfunctions, conversioncode, ) - import tensorflow as tf - if "TF_CUDNN_USE_AUTOTUNE" in os.environ: del os.environ["TF_CUDNN_USE_AUTOTUNE"] # was potentially set during training diff --git a/deeplabcut/pose_estimation_tensorflow/core/openvino/mo_extensions/front/tf/unravel_index.py b/deeplabcut/pose_estimation_tensorflow/core/openvino/mo_extensions/front/tf/unravel_index.py index a06885e2f8..8983bf89ae 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/openvino/mo_extensions/front/tf/unravel_index.py +++ b/deeplabcut/pose_estimation_tensorflow/core/openvino/mo_extensions/front/tf/unravel_index.py @@ -9,14 +9,14 @@ # Licensed under GNU Lesser General Public License v3.0 # import numpy as np +from openvino.tools.mo.front.common.partial_infer.utils import int64_array from openvino.tools.mo.front.common.replacement import FrontReplacementOp from openvino.tools.mo.graph.graph import Graph, Node -from openvino.tools.mo.ops.const import Const -from openvino.tools.mo.ops.strided_slice import StridedSlice -from openvino.tools.mo.front.common.partial_infer.utils import int64_array -from openvino.tools.mo.ops.elementwise import FloorMod, Div from openvino.tools.mo.ops.Cast import Cast +from openvino.tools.mo.ops.const import Const +from openvino.tools.mo.ops.elementwise import Div, FloorMod from openvino.tools.mo.ops.pack import PackOp +from openvino.tools.mo.ops.strided_slice import StridedSlice class UnravelIndex(FrontReplacementOp): diff --git a/deeplabcut/pose_estimation_tensorflow/core/openvino/session.py b/deeplabcut/pose_estimation_tensorflow/core/openvino/session.py index 09f23ea40a..d09d2f357a 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/openvino/session.py +++ b/deeplabcut/pose_estimation_tensorflow/core/openvino/session.py @@ -11,12 +11,12 @@ import os import subprocess +import cv2 import numpy as np from tqdm import tqdm -import cv2 try: - from openvino.runtime import Core, AsyncInferQueue + from openvino.runtime import AsyncInferQueue, Core is_openvino_available = True except ImportError: diff --git a/deeplabcut/pose_estimation_tensorflow/core/predict.py b/deeplabcut/pose_estimation_tensorflow/core/predict.py index 7b653c76b3..6c9d374808 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/predict.py +++ b/deeplabcut/pose_estimation_tensorflow/core/predict.py @@ -14,7 +14,9 @@ import numpy as np import tensorflow as tf + from deeplabcut.pose_estimation_tensorflow.nnets.factory import PoseNetFactory + from .openvino.session import OpenVINOSession diff --git a/deeplabcut/pose_estimation_tensorflow/core/predict_multianimal.py b/deeplabcut/pose_estimation_tensorflow/core/predict_multianimal.py index 4efe107a9c..126753f1d9 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/predict_multianimal.py +++ b/deeplabcut/pose_estimation_tensorflow/core/predict_multianimal.py @@ -12,8 +12,8 @@ import numpy as np import tensorflow as tf -from skimage.feature import peak_local_max from scipy.ndimage import measurements +from skimage.feature import peak_local_max def extract_cnn_output(outputs_np, cfg): diff --git a/deeplabcut/pose_estimation_tensorflow/core/test.py b/deeplabcut/pose_estimation_tensorflow/core/test.py index fa3e7d9b0e..27bb23a35c 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/test.py +++ b/deeplabcut/pose_estimation_tensorflow/core/test.py @@ -21,14 +21,15 @@ import scipy.ndimage from deeplabcut.pose_estimation_tensorflow.config import load_config -from deeplabcut.pose_estimation_tensorflow.datasets.factory import PoseDatasetFactory from deeplabcut.pose_estimation_tensorflow.datasets import Batch +from deeplabcut.pose_estimation_tensorflow.datasets.factory import PoseDatasetFactory +from deeplabcut.pose_estimation_tensorflow.util import visualize + from .predict import ( - setup_pose_prediction, - extract_cnn_output, argmax_pose_predict, + extract_cnn_output, + setup_pose_prediction, ) -from deeplabcut.pose_estimation_tensorflow.util import visualize def test_net(visualise, cache_scoremaps): @@ -50,7 +51,7 @@ def test_net(visualise, cache_scoremaps): predictions = np.zeros((num_images,), dtype=np.object) for k in range(num_images): - print("processing image {}/{}".format(k, num_images - 1)) + print(f"processing image {k}/{num_images - 1}") batch = dataset.next_batch() diff --git a/deeplabcut/pose_estimation_tensorflow/core/train.py b/deeplabcut/pose_estimation_tensorflow/core/train.py index 10a20c6986..7eb517463d 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/train.py +++ b/deeplabcut/pose_estimation_tensorflow/core/train.py @@ -34,7 +34,7 @@ from deeplabcut.utils import auxfun_models -class LearningRate(object): +class LearningRate: def __init__(self, cfg): self.steps = cfg["multi_step"] self.current_step = 0 @@ -205,7 +205,7 @@ def train( info = build_info.build_info if not info["is_cuda_build"]: # Apple Silicon is not built with CUDA - warnings.warn("Switching to Adam, as SGD crashes on Apple Silicon.") + warnings.warn("Switching to Adam, as SGD crashes on Apple Silicon.", stacklevel=2) cfg["optimizer"] = "adam" cfg["lr_init"] = 5e-4 cfg["multi_step"] = [[1e-4, 7500], [5e-5, 12000], [1e-5, 200000]] @@ -271,8 +271,8 @@ def train( if it % display_iters == 0 and it > start_iter: average_loss = cum_loss / display_iters cum_loss = 0.0 - logging.info("iteration: {} loss: {} lr: {}".format(it, "{0:.4f}".format(average_loss), current_lr)) - lrf.write("{}, {:.5f}, {}\n".format(it, average_loss, current_lr)) + logging.info("iteration: {} loss: {} lr: {}".format(it, f"{average_loss:.4f}", current_lr)) + lrf.write(f"{it}, {average_loss:.5f}, {current_lr}\n") lrf.flush() # Save snapshot diff --git a/deeplabcut/pose_estimation_tensorflow/core/train_multianimal.py b/deeplabcut/pose_estimation_tensorflow/core/train_multianimal.py index 7623b7774f..b51353667f 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/train_multianimal.py +++ b/deeplabcut/pose_estimation_tensorflow/core/train_multianimal.py @@ -18,16 +18,16 @@ import tf_slim as slim from deeplabcut.pose_estimation_tensorflow.config import load_config -from deeplabcut.pose_estimation_tensorflow.datasets import PoseDatasetFactory -from deeplabcut.pose_estimation_tensorflow.nnets import PoseNetFactory -from deeplabcut.pose_estimation_tensorflow.nnets.utils import get_batch_spec -from deeplabcut.pose_estimation_tensorflow.util.logging import setup_logging from deeplabcut.pose_estimation_tensorflow.core.train import ( + LearningRate, + get_optimizer, setup_preloading, start_preloading, - get_optimizer, - LearningRate, ) +from deeplabcut.pose_estimation_tensorflow.datasets import PoseDatasetFactory +from deeplabcut.pose_estimation_tensorflow.nnets import PoseNetFactory +from deeplabcut.pose_estimation_tensorflow.nnets.utils import get_batch_spec +from deeplabcut.pose_estimation_tensorflow.util.logging import setup_logging from deeplabcut.utils import auxfun_models @@ -89,7 +89,8 @@ def train( if ( cfg["partaffinityfield_predict"] and "multi-animal" in cfg["dataset_type"] - ): # the PAF code currently just hijacks the pairwise net stuff (for the batch feeding via Batch.pairwise_targets: 5) + # the PAF code currently just hijacks the pairwise net stuff (for the batch feeding via Batch.pairwise_targets: 5) + ): print("Activating limb prediction...") cfg["pairwise_predict"] = True @@ -222,10 +223,10 @@ def train( logging.info( "iteration: {} loss: {} scmap loss: {} locref loss: {} limb loss: {} lr: {}".format( it, - "{0:.4f}".format(cumloss / display_iters), - "{0:.4f}".format(partloss / display_iters), - "{0:.4f}".format(locrefloss / display_iters), - "{0:.4f}".format(pwloss / display_iters), + f"{cumloss / display_iters:.4f}", + f"{partloss / display_iters:.4f}", + f"{locrefloss / display_iters:.4f}", + f"{pwloss / display_iters:.4f}", current_lr, ) ) @@ -234,10 +235,10 @@ def train( lrf.write( "iteration: {}, loss: {}, scmap loss: {}, locref loss: {}, limb loss: {}, lr: {}\n".format( it, - "{0:.4f}".format(cumloss / display_iters), - "{0:.4f}".format(partloss / display_iters), - "{0:.4f}".format(locrefloss / display_iters), - "{0:.4f}".format(pwloss / display_iters), + f"{cumloss / display_iters:.4f}", + f"{partloss / display_iters:.4f}", + f"{locrefloss / display_iters:.4f}", + f"{pwloss / display_iters:.4f}", current_lr, ) ) diff --git a/deeplabcut/pose_estimation_tensorflow/datasets/__init__.py b/deeplabcut/pose_estimation_tensorflow/datasets/__init__.py index c1a58a6b25..011f512d35 100644 --- a/deeplabcut/pose_estimation_tensorflow/datasets/__init__.py +++ b/deeplabcut/pose_estimation_tensorflow/datasets/__init__.py @@ -11,13 +11,12 @@ from .factory import PoseDatasetFactory from .pose_deterministic import DeterministicPoseDataset -from .pose_scalecrop import ScalecropPoseDataset from .pose_imgaug import ImgaugPoseDataset -from .pose_tensorpack import TensorpackPoseDataset from .pose_multianimal_imgaug import MAImgaugPoseDataset +from .pose_scalecrop import ScalecropPoseDataset +from .pose_tensorpack import TensorpackPoseDataset from .utils import Batch - __all__ = [ "PoseDatasetFactory", "DeterministicPoseDataset", diff --git a/deeplabcut/pose_estimation_tensorflow/datasets/augmentation.py b/deeplabcut/pose_estimation_tensorflow/datasets/augmentation.py index 0c93d4182a..a10dcd7462 100644 --- a/deeplabcut/pose_estimation_tensorflow/datasets/augmentation.py +++ b/deeplabcut/pose_estimation_tensorflow/datasets/augmentation.py @@ -8,18 +8,18 @@ # # Licensed under GNU Lesser General Public License v3.0 # + import imgaug.augmenters as iaa import numpy as np from imgaug import KeypointsOnImage from scipy.spatial.distance import pdist, squareform -from typing import List, Union, Tuple class KeypointFliplr(iaa.Fliplr): def __init__( self, - keypoints: List[str], - symmetric_pairs: List[Union[Tuple, List]], + keypoints: list[str], + symmetric_pairs: list[tuple | list], p: float = 1.0, ): super().__init__(p=p) @@ -72,7 +72,7 @@ def __init__( "density" (weighing preferentially dense regions of keypoints), or "hybrid" (alternating randomly between "uniform" and "density"). """ - super(KeypointAwareCropToFixedSize, self).__init__( + super().__init__( width, height, name="kptscrop", diff --git a/deeplabcut/pose_estimation_tensorflow/datasets/pose_base.py b/deeplabcut/pose_estimation_tensorflow/datasets/pose_base.py index 6fcff78805..b0a5606cd7 100644 --- a/deeplabcut/pose_estimation_tensorflow/datasets/pose_base.py +++ b/deeplabcut/pose_estimation_tensorflow/datasets/pose_base.py @@ -11,6 +11,7 @@ import abc + import numpy as np diff --git a/deeplabcut/pose_estimation_tensorflow/datasets/pose_deterministic.py b/deeplabcut/pose_estimation_tensorflow/datasets/pose_deterministic.py index e54e69f8d3..8f67c10cba 100644 --- a/deeplabcut/pose_estimation_tensorflow/datasets/pose_deterministic.py +++ b/deeplabcut/pose_estimation_tensorflow/datasets/pose_deterministic.py @@ -11,26 +11,29 @@ import logging -import numpy as np import os + +import numpy as np import scipy.io as sio + from deeplabcut.utils.auxfun_videos import imread, imresize from deeplabcut.utils.conversioncode import robust_split_path + from .factory import PoseDatasetFactory from .pose_base import BasePoseDataset from .utils import ( + Batch, DataItem, - mirror_joints_map, crop_image, - Batch, data_to_input, + mirror_joints_map, ) @PoseDatasetFactory.register("deterministic") class DeterministicPoseDataset(BasePoseDataset): def __init__(self, cfg): - super(DeterministicPoseDataset, self).__init__(cfg) + super().__init__(cfg) self.data = self.load_dataset() self.num_images = len(self.data) if self.cfg["mirror"]: diff --git a/deeplabcut/pose_estimation_tensorflow/datasets/pose_imgaug.py b/deeplabcut/pose_estimation_tensorflow/datasets/pose_imgaug.py index 570680949d..413d544084 100644 --- a/deeplabcut/pose_estimation_tensorflow/datasets/pose_imgaug.py +++ b/deeplabcut/pose_estimation_tensorflow/datasets/pose_imgaug.py @@ -21,19 +21,21 @@ import imgaug.augmenters as iaa import numpy as np import scipy.io as sio + from deeplabcut.pose_estimation_tensorflow.datasets import augmentation from deeplabcut.utils.auxfun_videos import imread from deeplabcut.utils.conversioncode import robust_split_path + from .factory import PoseDatasetFactory from .pose_base import BasePoseDataset -from .utils import DataItem, Batch +from .utils import Batch, DataItem @PoseDatasetFactory.register("default") @PoseDatasetFactory.register("imgaug") class ImgaugPoseDataset(BasePoseDataset): def __init__(self, cfg): - super(ImgaugPoseDataset, self).__init__(cfg) + super().__init__(cfg) self._n_kpts = len(cfg["all_joints_names"]) self.data = self.load_dataset() self.batch_size = cfg.get("batch_size", 1) @@ -139,7 +141,9 @@ def load_dataset(self): return data def build_augmentation_pipeline(self, height=None, width=None, apply_prob=0.5): - sometimes = lambda aug: iaa.Sometimes(apply_prob, aug) + def sometimes(aug): + return iaa.Sometimes(apply_prob, aug) + pipeline = iaa.Sequential(random_order=False) cfg = self.cfg @@ -386,7 +390,7 @@ def next_batch(self): batch_joints_valid = [] joint_ids_valid = [] - for joints, ids in zip(batch_joints, joint_ids): + for joints, ids in zip(batch_joints, joint_ids, strict=False): # invisible joints are represented by nans mask = ~np.isnan(joints[:, 0]) joints = joints[mask, :] @@ -466,10 +470,10 @@ def gaussian_scmap(self, joint_id, coords, data_item, size, scale): for k, j_id in enumerate(joint_id[person_id]): joint_pt = coords[person_id][k, :] j_x = np.asarray(joint_pt[0]).item() - j_x_sm = round((j_x - self.half_stride) / self.stride) + round((j_x - self.half_stride) / self.stride) j_y = np.asarray(joint_pt[1]).item() - j_y_sm = round((j_y - self.half_stride) / self.stride) - map_j = grid.copy() + round((j_y - self.half_stride) / self.stride) + grid.copy() # Distance between the joint point and each coordinate dist = np.linalg.norm(grid - (j_y, j_x), axis=2) ** 2 scmap_j = np.exp(-dist / (2 * (std**2))) diff --git a/deeplabcut/pose_estimation_tensorflow/datasets/pose_multianimal_imgaug.py b/deeplabcut/pose_estimation_tensorflow/datasets/pose_multianimal_imgaug.py index 679ee02ee3..0456e57890 100644 --- a/deeplabcut/pose_estimation_tensorflow/datasets/pose_multianimal_imgaug.py +++ b/deeplabcut/pose_estimation_tensorflow/datasets/pose_multianimal_imgaug.py @@ -13,28 +13,29 @@ import logging import os import pickle +from math import sqrt +from pathlib import Path + import imageio import imgaug.augmenters as iaa import numpy as np import pandas as pd from imgaug.augmentables import Keypoint, KeypointsOnImage + from deeplabcut.generate_training_dataset import read_image_shape_fast from deeplabcut.pose_estimation_tensorflow.datasets import augmentation from deeplabcut.pose_estimation_tensorflow.datasets.factory import PoseDatasetFactory from deeplabcut.pose_estimation_tensorflow.datasets.pose_base import BasePoseDataset -from deeplabcut.pose_estimation_tensorflow.datasets.utils import DataItem, Batch -from deeplabcut.utils import auxiliaryfunctions, auxfun_multianimal -from deeplabcut.utils.auxfun_videos import imread -from deeplabcut.utils.auxfun_videos import VideoReader +from deeplabcut.pose_estimation_tensorflow.datasets.utils import Batch, DataItem +from deeplabcut.utils import auxfun_multianimal, auxiliaryfunctions +from deeplabcut.utils.auxfun_videos import VideoReader, imread from deeplabcut.utils.conversioncode import robust_split_path -from pathlib import Path -from math import sqrt @PoseDatasetFactory.register("multi-animal-imgaug") class MAImgaugPoseDataset(BasePoseDataset): def __init__(self, cfg): - super(MAImgaugPoseDataset, self).__init__(cfg) + super().__init__(cfg) if cfg.get("pseudo_label", ""): self._n_kpts = len(cfg["all_joints_names"]) diff --git a/deeplabcut/pose_estimation_tensorflow/datasets/pose_scalecrop.py b/deeplabcut/pose_estimation_tensorflow/datasets/pose_scalecrop.py index b17474a67b..ff921a35ef 100644 --- a/deeplabcut/pose_estimation_tensorflow/datasets/pose_scalecrop.py +++ b/deeplabcut/pose_estimation_tensorflow/datasets/pose_scalecrop.py @@ -17,7 +17,7 @@ @PoseDatasetFactory.register("scalecrop") class ScalecropPoseDataset(DeterministicPoseDataset): def __init__(self, cfg): - super(ScalecropPoseDataset, self).__init__(cfg) + super().__init__(cfg) self.cfg["deterministic"] = False self.max_input_sizesquare = cfg.get("max_input_size", 1500) ** 2 self.min_input_sizesquare = cfg.get("min_input_size", 64) ** 2 diff --git a/deeplabcut/pose_estimation_tensorflow/datasets/pose_tensorpack.py b/deeplabcut/pose_estimation_tensorflow/datasets/pose_tensorpack.py index 41633abc55..c107d2e3ce 100644 --- a/deeplabcut/pose_estimation_tensorflow/datasets/pose_tensorpack.py +++ b/deeplabcut/pose_estimation_tensorflow/datasets/pose_tensorpack.py @@ -25,25 +25,26 @@ import cv2 import numpy as np import scipy.io as sio -from deeplabcut.utils.conversioncode import robust_split_path from numpy import array as arr from tensorpack.dataflow.base import RNGDataFlow from tensorpack.dataflow.common import MapData from tensorpack.dataflow.imgaug import ( Brightness, Contrast, + GaussianBlur, + GaussianNoise, RandomResize, Rotation, Saturation, - GaussianNoise, - GaussianBlur, ) from tensorpack.dataflow.imgaug.crop import RandomCropRandomShape from tensorpack.dataflow.imgaug.meta import RandomApplyAug from tensorpack.dataflow.imgaug.transform import CropTransform -from tensorpack.dataflow.parallel import MultiProcessRunnerZMQ, MultiProcessRunner +from tensorpack.dataflow.parallel import MultiProcessRunner, MultiProcessRunnerZMQ from tensorpack.utils.utils import get_rng +from deeplabcut.utils.conversioncode import robust_split_path + from .factory import PoseDatasetFactory from .pose_base import BasePoseDataset from .utils import Batch, data_to_input @@ -236,7 +237,7 @@ def __init__(self, cfg): cfg["cropratio"] = cfg.get("cropratio", 0.4) - super(TensorpackPoseDataset, self).__init__(cfg) + super().__init__(cfg) self.scaling = RandomResize( xrange=( self.cfg["scale_jitter_lo"] * self.cfg["global_scale"], @@ -416,13 +417,12 @@ def is_valid_size(self, image_size, scale): def make_batch(self, components): data_item = DataItem.from_dict(components[0]) - mirror = components[2] + components[2] part_score_targets = components[3] part_score_weights = components[4] locref_targets = components[5] locref_mask = components[6] - im_file = data_item.im_path # logging.debug('image %s', im_file) # print('image: {}'.format(im_file)) # logging.debug('mirror %r', mirror) diff --git a/deeplabcut/pose_estimation_tensorflow/datasets/utils.py b/deeplabcut/pose_estimation_tensorflow/datasets/utils.py index ded75db5be..82ec06667b 100644 --- a/deeplabcut/pose_estimation_tensorflow/datasets/utils.py +++ b/deeplabcut/pose_estimation_tensorflow/datasets/utils.py @@ -8,9 +8,10 @@ # # Licensed under GNU Lesser General Public License v3.0 # -import numpy as np from enum import Enum +import numpy as np + class Batch(Enum): inputs = 0 diff --git a/deeplabcut/pose_estimation_tensorflow/export.py b/deeplabcut/pose_estimation_tensorflow/export.py index 6e6709a206..5a38577a82 100644 --- a/deeplabcut/pose_estimation_tensorflow/export.py +++ b/deeplabcut/pose_estimation_tensorflow/export.py @@ -15,13 +15,12 @@ import tarfile from pathlib import Path -import numpy as np import ruamel.yaml import tensorflow as tf -from deeplabcut.utils import auxiliaryfunctions from deeplabcut.pose_estimation_tensorflow.config import load_config from deeplabcut.pose_estimation_tensorflow.core import predict +from deeplabcut.utils import auxiliaryfunctions def create_deploy_config_template(): @@ -72,7 +71,7 @@ def write_deploy_config(configname, cfg): cfg_file[key] = cfg[key] # Adding default value for variable skeleton and skeleton_color for backward compatibility. - if not "skeleton" in cfg.keys(): + if "skeleton" not in cfg.keys(): cfg_file["skeleton"] = [] cfg_file["skeleton_color"] = "black" ruamelFile.dump(cfg_file, cf) diff --git a/deeplabcut/pose_estimation_tensorflow/modelzoo/api/spatiotemporal_adapt.py b/deeplabcut/pose_estimation_tensorflow/modelzoo/api/spatiotemporal_adapt.py index b37e72bd3f..a0472dd56f 100644 --- a/deeplabcut/pose_estimation_tensorflow/modelzoo/api/spatiotemporal_adapt.py +++ b/deeplabcut/pose_estimation_tensorflow/modelzoo/api/spatiotemporal_adapt.py @@ -10,9 +10,8 @@ # import glob import os -import io from pathlib import Path -import yaml + from deeplabcut.pose_estimation_tensorflow.modelzoo.api.superanimal_inference import ( video_inference, ) diff --git a/deeplabcut/pose_estimation_tensorflow/modelzoo/api/superanimal_inference.py b/deeplabcut/pose_estimation_tensorflow/modelzoo/api/superanimal_inference.py index 29b0946a66..c06cd1a5f7 100644 --- a/deeplabcut/pose_estimation_tensorflow/modelzoo/api/superanimal_inference.py +++ b/deeplabcut/pose_estimation_tensorflow/modelzoo/api/superanimal_inference.py @@ -50,7 +50,7 @@ def get_multi_scale_frames(frame, scale_list): def _project_pred_to_original_size(pred, old_shape, new_shape): old_h, old_w, _ = old_shape new_h, new_w, _ = new_shape - ratio_h, ratio_w = old_h / new_h, old_w / new_w + ratio_h, _ratio_w = old_h / new_h, old_w / new_w coordinate = pred["coordinates"][0] confidence = pred["confidence"] @@ -87,7 +87,7 @@ def _average_multiple_scale_preds( continue coordinates = pred["coordinates"][0] confidence = pred["confidence"] - for i, (coords, conf) in enumerate(zip(coordinates, confidence)): + for i, (coords, conf) in enumerate(zip(coordinates, confidence, strict=False)): if not np.any(coords): continue xyp[scale_id, i, :2] = coords @@ -122,8 +122,10 @@ def _video_inference( cap, nframes, batchsize, - scale_list=[], + scale_list=None, ): + if scale_list is None: + scale_list = [] strwidth = int(np.ceil(np.log10(nframes))) # width for strings batch_ind = 0 # keeps track of which image within a batch should be written to @@ -244,7 +246,7 @@ def video_inference( videos, project_name, model_name, - scale_list=[], + scale_list=None, videotype="avi", destfolder=None, batchsize=1, @@ -253,6 +255,8 @@ def video_inference( init_weights="", customized_test_config="", ): + if scale_list is None: + scale_list = [] dlc_root_path = auxiliaryfunctions.get_deeplabcut_path() if customized_test_config == "": @@ -384,7 +388,7 @@ def video_inference( "cropping_parameters": coords, } metadata = {"data": dictionary} - print("Saving results in %s..." % (destfolder)) + print(f"Saving results in {destfolder}...") metadata_path = dataname.split(".h5")[0] + "_meta.pickle" @@ -408,7 +412,7 @@ def video_inference( keypoints = dict_["coordinates"][0] confidence = dict_["confidence"] temp = np.full((len(keypoints), 3), np.nan) - for n, (xy, c) in enumerate(zip(keypoints, confidence)): + for n, (xy, c) in enumerate(zip(keypoints, confidence, strict=False)): if xy.size and c.size: temp[n, :2] = xy temp[n, 2] = c @@ -423,7 +427,7 @@ def _video_inference_superanimal( videos, project_name, model_name, - scale_list=[], + scale_list=None, videotype=".mp4", video_adapt=False, plot_trajectories=True, @@ -494,6 +498,8 @@ def _video_inference_superanimal( SpatiotemporalAdaptation, ) + if scale_list is None: + scale_list = [] superanimal_name = project_name + "_" + model_name for video in videos: modelfolder = Path(video).parent / f"{Path(video).stem}_video_adaptation" diff --git a/deeplabcut/pose_estimation_tensorflow/nnets/__init__.py b/deeplabcut/pose_estimation_tensorflow/nnets/__init__.py index 6cd0a13cbd..ab0aca78fb 100644 --- a/deeplabcut/pose_estimation_tensorflow/nnets/__init__.py +++ b/deeplabcut/pose_estimation_tensorflow/nnets/__init__.py @@ -9,13 +9,12 @@ # Licensed under GNU Lesser General Public License v3.0 # -from .factory import PoseNetFactory from .efficientnet import PoseEfficientNet +from .factory import PoseNetFactory from .mobilenet import PoseMobileNet from .multi import PoseMultiNet from .resnet import PoseResnet - __all__ = [ "PoseNetFactory", "PoseEfficientNet", diff --git a/deeplabcut/pose_estimation_tensorflow/nnets/base.py b/deeplabcut/pose_estimation_tensorflow/nnets/base.py index 7208b0d36a..55433acb66 100644 --- a/deeplabcut/pose_estimation_tensorflow/nnets/base.py +++ b/deeplabcut/pose_estimation_tensorflow/nnets/base.py @@ -9,9 +9,12 @@ # Licensed under GNU Lesser General Public License v3.0 # import abc + import tensorflow as tf -from deeplabcut.pose_estimation_tensorflow.datasets import Batch + from deeplabcut.pose_estimation_tensorflow.core import predict_multianimal +from deeplabcut.pose_estimation_tensorflow.datasets import Batch + from .layers import prediction_layer from .utils import make_2d_gaussian_kernel diff --git a/deeplabcut/pose_estimation_tensorflow/nnets/efficientnet.py b/deeplabcut/pose_estimation_tensorflow/nnets/efficientnet.py index cbebf508fb..ca359be93e 100644 --- a/deeplabcut/pose_estimation_tensorflow/nnets/efficientnet.py +++ b/deeplabcut/pose_estimation_tensorflow/nnets/efficientnet.py @@ -17,7 +17,9 @@ """ import tensorflow as tf + import deeplabcut.pose_estimation_tensorflow.backbones.efficientnet_builder as eff + from .base import BasePoseNet from .factory import PoseNetFactory @@ -25,7 +27,7 @@ @PoseNetFactory.register("efficientnet") class PoseEfficientNet(BasePoseNet): def __init__(self, cfg): - super(PoseEfficientNet, self).__init__(cfg) + super().__init__(cfg) if "use_batch_norm" not in self.cfg: self.cfg["use_batch_norm"] = False if "use_drop_out" not in self.cfg: diff --git a/deeplabcut/pose_estimation_tensorflow/nnets/layers.py b/deeplabcut/pose_estimation_tensorflow/nnets/layers.py index b0a7ff29d3..fc0d4d2c0d 100644 --- a/deeplabcut/pose_estimation_tensorflow/nnets/layers.py +++ b/deeplabcut/pose_estimation_tensorflow/nnets/layers.py @@ -11,7 +11,6 @@ import tensorflow as tf import tf_slim as slim - # FIXME Fix wrong scope with Keras layers # def prediction_layer(cfg, input, name, num_outputs): # with tf.compat.v1.variable_scope(name): diff --git a/deeplabcut/pose_estimation_tensorflow/nnets/mobilenet.py b/deeplabcut/pose_estimation_tensorflow/nnets/mobilenet.py index b78a379993..1d4f4adafb 100644 --- a/deeplabcut/pose_estimation_tensorflow/nnets/mobilenet.py +++ b/deeplabcut/pose_estimation_tensorflow/nnets/mobilenet.py @@ -23,12 +23,12 @@ import tf_slim as slim from deeplabcut.pose_estimation_tensorflow.backbones import mobilenet_v2 + from .base import BasePoseNet from .factory import PoseNetFactory from .layers import prediction_layer from .utils import wrapper - networks = { "mobilenet_v2_1.0": (mobilenet_v2.mobilenet_base, mobilenet_v2.training_scope), "mobilenet_v2_0.75": ( @@ -61,7 +61,7 @@ @PoseNetFactory.register("mobilenet") class PoseMobileNet(BasePoseNet): def __init__(self, cfg): - super(PoseMobileNet, self).__init__(cfg) + super().__init__(cfg) def extract_features(self, inputs): net_fun, net_arg_scope = networks[self.cfg["net_type"]] @@ -78,7 +78,7 @@ def prediction_layers( scope="pose", reuse=None, ): - out = super(PoseMobileNet, self).prediction_layers( + out = super().prediction_layers( features, scope, reuse, diff --git a/deeplabcut/pose_estimation_tensorflow/nnets/multi.py b/deeplabcut/pose_estimation_tensorflow/nnets/multi.py index 096d575453..76fe20ab4a 100644 --- a/deeplabcut/pose_estimation_tensorflow/nnets/multi.py +++ b/deeplabcut/pose_estimation_tensorflow/nnets/multi.py @@ -10,19 +10,20 @@ # import re + import tensorflow as tf import tf_slim as slim from tf_slim.nets import resnet_v1 import deeplabcut.pose_estimation_tensorflow.backbones.efficientnet_builder as eff +from deeplabcut.pose_estimation_tensorflow.backbones import mobilenet, mobilenet_v2 from deeplabcut.pose_estimation_tensorflow.nnets import conv_blocks -from deeplabcut.pose_estimation_tensorflow.backbones import mobilenet_v2, mobilenet + from .base import BasePoseNet from .factory import PoseNetFactory from .layers import prediction_layer_stage from .utils import wrapper - # Change the stride from 2 to 1 to get 16x downscaling instead of 32x. mobilenet_v2.V2_DEF["spec"][14] = mobilenet.op(conv_blocks.expanded_conv, stride=1, num_outputs=160) @@ -104,7 +105,7 @@ def prediction_layer(cfg, input, name, num_outputs): @PoseNetFactory.register("multi") class PoseMultiNet(BasePoseNet): def __init__(self, cfg): - super(PoseMultiNet, self).__init__(cfg) + super().__init__(cfg) multi_stage = self.cfg.get("multi_stage", False) # Multi stage is currently only implemented for resnets self.cfg["multi_stage"] = multi_stage and "resnet" in self.cfg["net_type"] @@ -149,7 +150,7 @@ def prediction_layers( if self.cfg["multi_stage"]: # MuNet! (multi_stage decoder + multi_fusion) # Defining multi_fusion backbone num_layers = re.findall("resnet_([0-9]*)", net_type)[0] - layer_name = "resnet_v1_{}".format(num_layers) + "/block{}/unit_{}/bottleneck_v1" + layer_name = f"resnet_v1_{num_layers}" + "/block{}/unit_{}/bottleneck_v1" mid_pt_block1 = layer_name.format(1, 3) mid_pt_block2 = layer_name.format(2, 3) @@ -374,7 +375,7 @@ def prediction_layers( scope="block4", ) net = tf.concat([bank_3, upsampled_features], 3) - out = super(PoseMultiNet, self).prediction_layers( + out = super().prediction_layers( net, scope, reuse, diff --git a/deeplabcut/pose_estimation_tensorflow/nnets/resnet.py b/deeplabcut/pose_estimation_tensorflow/nnets/resnet.py index 1441799a18..b97bb130b9 100644 --- a/deeplabcut/pose_estimation_tensorflow/nnets/resnet.py +++ b/deeplabcut/pose_estimation_tensorflow/nnets/resnet.py @@ -13,6 +13,7 @@ # import re + import tensorflow as tf import tf_slim as slim from tf_slim.nets import resnet_v1 @@ -21,7 +22,6 @@ from .factory import PoseNetFactory from .layers import prediction_layer - net_funcs = { "resnet_50": resnet_v1.resnet_v1_50, "resnet_101": resnet_v1.resnet_v1_101, @@ -32,7 +32,7 @@ @PoseNetFactory.register("resnet") class PoseResnet(BasePoseNet): def __init__(self, cfg): - super(PoseResnet, self).__init__(cfg) + super().__init__(cfg) def extract_features(self, inputs): net_fun = net_funcs[self.cfg["net_type"]] @@ -53,7 +53,7 @@ def prediction_layers( scope="pose", reuse=None, ): - out = super(PoseResnet, self).prediction_layers( + out = super().prediction_layers( features, scope, reuse, diff --git a/deeplabcut/pose_estimation_tensorflow/nnets/utils.py b/deeplabcut/pose_estimation_tensorflow/nnets/utils.py index 24d2b0249d..11cb7b214b 100644 --- a/deeplabcut/pose_estimation_tensorflow/nnets/utils.py +++ b/deeplabcut/pose_estimation_tensorflow/nnets/utils.py @@ -15,11 +15,13 @@ # import functools + import numpy as np import tensorflow as tf -from deeplabcut.pose_estimation_tensorflow.datasets import Batch -from tensorflow.python.tpu.ops import tpu_ops from tensorflow.python.tpu import tpu_function +from tensorflow.python.tpu.ops import tpu_ops + +from deeplabcut.pose_estimation_tensorflow.datasets import Batch def wrapper(func, *args, **kwargs): @@ -147,7 +149,7 @@ class TpuBatchNormalization(tf.compat.v1.layers.BatchNormalization): def __init__(self, fused=False, **kwargs): if fused in (True, None): raise ValueError("TpuBatchNormalization does not support fused=True.") - super(TpuBatchNormalization, self).__init__(fused=fused, **kwargs) + super().__init__(fused=fused, **kwargs) @staticmethod def _cross_replica_average(t, num_shards_per_group): @@ -167,7 +169,7 @@ def _cross_replica_average(t, num_shards_per_group): def _moments(self, inputs, reduction_axes, keep_dims): """Compute the mean and variance: it overrides the original _moments.""" - shard_mean, shard_variance = super(TpuBatchNormalization, self)._moments( + shard_mean, shard_variance = super()._moments( inputs, reduction_axes, keep_dims=keep_dims ) @@ -192,7 +194,7 @@ class BatchNormalization(tf.compat.v1.layers.BatchNormalization): """Fixed default name of BatchNormalization to match TpuBatchNormalization.""" def __init__(self, name="tpu_batch_normalization", **kwargs): - super(BatchNormalization, self).__init__(name=name, **kwargs) + super().__init__(name=name, **kwargs) def drop_connect(inputs, is_training, drop_connect_rate): diff --git a/deeplabcut/pose_estimation_tensorflow/predict_multianimal.py b/deeplabcut/pose_estimation_tensorflow/predict_multianimal.py index 54c9752ab7..df6a8b9daf 100644 --- a/deeplabcut/pose_estimation_tensorflow/predict_multianimal.py +++ b/deeplabcut/pose_estimation_tensorflow/predict_multianimal.py @@ -21,9 +21,8 @@ from tqdm import tqdm from deeplabcut.pose_estimation_tensorflow.core import predict_multianimal as predict -from deeplabcut.utils import auxiliaryfunctions, auxfun_multianimal +from deeplabcut.utils import auxfun_multianimal, auxiliaryfunctions from deeplabcut.utils.auxfun_videos import VideoWriter -import pickle def extract_bpt_feature_from_video( @@ -225,8 +224,8 @@ def AnalyzeMultiAnimalVideo( def _get_features_dict(raw_coords, features, stride): from deeplabcut.pose_tracking_pytorch import ( - load_features_from_coord, convert_coord_from_img_space_to_feature_space, + load_features_from_coord, ) coords_img_space = np.array([coord[:, :2] for coord in raw_coords]) # only first two columns are useful diff --git a/deeplabcut/pose_estimation_tensorflow/predict_videos.py b/deeplabcut/pose_estimation_tensorflow/predict_videos.py index 7c35013f9a..0ccb2a48ad 100644 --- a/deeplabcut/pose_estimation_tensorflow/predict_videos.py +++ b/deeplabcut/pose_estimation_tensorflow/predict_videos.py @@ -31,17 +31,15 @@ from skimage.util import img_as_ubyte from tqdm import tqdm -from deeplabcut.core import trackingutils, inferenceutils +from deeplabcut.core import inferenceutils, trackingutils from deeplabcut.pose_estimation_tensorflow.config import load_config from deeplabcut.pose_estimation_tensorflow.core import predict - -from deeplabcut.refine_training_dataset.stitch import stitch_tracklets -from deeplabcut.utils import auxiliaryfunctions, auxfun_multianimal, auxfun_models from deeplabcut.pose_estimation_tensorflow.core.openvino.session import ( GetPoseF_OV, is_openvino_available, ) - +from deeplabcut.refine_training_dataset.stitch import stitch_tracklets +from deeplabcut.utils import auxfun_models, auxfun_multianimal, auxiliaryfunctions #################################################### # Loading data, and defining model folder @@ -75,7 +73,9 @@ def create_tracking_dataset( extract_bpt_feature_from_video, ) - # allow_growth must be true here because tensorflow does not automatically free gpu memory and setting it as false occupies all gpu memory so that pytorch cannot kick in + # allow_growth must be true here because tensorflow does not automatically + # free gpu memory and setting it as false occupies all gpu memory so that + # pytorch cannot kick in allow_growth = True if "TF_CUDNN_USE_AUTOTUNE" in os.environ: @@ -105,7 +105,7 @@ def create_tracking_dataset( dlc_cfg = load_config(str(path_test_config)) except FileNotFoundError: raise FileNotFoundError( - "It seems the model for shuffle %s and trainFraction %s does not exist." % (shuffle, trainFraction) + f"It seems the model for shuffle {shuffle} and trainFraction {trainFraction} does not exist." ) Snapshots = auxiliaryfunctions.get_snapshots_from_folder( @@ -120,7 +120,7 @@ def create_tracking_dataset( else: snapshotindex = cfg["snapshotindex"] - print("Using %s" % Snapshots[snapshotindex], "for model", modelfolder) + print(f"Using {Snapshots[snapshotindex]}", "for model", modelfolder) ################################################## # Load and setup CNN part detector @@ -182,7 +182,7 @@ def create_tracking_dataset( dlc_cfg, allow_growth=allow_growth, collect_extra=True ) - pdindex = pd.MultiIndex.from_product( + pd.MultiIndex.from_product( [[DLCscorer], dlc_cfg["all_joints_names"], xyz_labs], names=["scorer", "bodyparts", "coords"], ) @@ -488,8 +488,7 @@ def analyze_videos( dlc_cfg = load_config(str(path_test_config)) except FileNotFoundError: raise FileNotFoundError( - "It seems the model for iteration %s and shuffle %s and trainFraction %s does not exist." - % (iteration, shuffle, trainFraction) + f"It seems the model for iteration {iteration} and shuffle {shuffle} and trainFraction {trainFraction} does not exist." ) Snapshots = auxiliaryfunctions.get_snapshots_from_folder( @@ -504,7 +503,7 @@ def analyze_videos( else: snapshotindex = cfg["snapshotindex"] - print("Using %s" % Snapshots[snapshotindex], "for model", modelfolder) + print(f"Using {Snapshots[snapshotindex]}", "for model", modelfolder) ################################################## # Load and setup CNN part detector @@ -662,8 +661,9 @@ def analyze_videos( def checkcropping(cfg, cap): print( - "Cropping based on the x1 = %s x2 = %s y1 = %s y2 = %s. You can adjust the cropping coordinates in the config.yaml file." - % (cfg["x1"], cfg["x2"], cfg["y1"], cfg["y2"]) + "Cropping based on the x1 = {} x2 = {} y1 = {} y2 = {}. You can adjust the cropping coordinates in the config.yaml file.".format( + cfg["x1"], cfg["x2"], cfg["y1"], cfg["y2"] + ) ) nx = cfg["x2"] - cfg["x1"] ny = cfg["y2"] - cfg["y1"] @@ -823,7 +823,7 @@ def GetPoseF_GTF(cfg, dlc_cfg, sess, inputs, outputs, cap, nframes, batchsize): ret, frame = cap.read() counter += 1 if not ret: - warnings.warn(f"Could not decode frame #{counter}.") + warnings.warn(f"Could not decode frame #{counter}.", stacklevel=2) continue if cfg["cropping"]: @@ -909,7 +909,7 @@ def GetPoseDynamic(cfg, dlc_cfg, sess, inputs, outputs, cap, nframes, detectiont frame = img_as_ubyte(originalframe) pose = predict.getpose(frame, dlc_cfg, sess, inputs, outputs).flatten() # no offset is necessary - x0, y0 = x1, y1 + _x0, _y0 = x1, y1 x1, x2, y1, y2 = 0, nx, 0, ny detected = False @@ -952,7 +952,7 @@ def AnalyzeVideo( print("Loading ", video) cap = cv2.VideoCapture(video) if not cap.isOpened(): - raise IOError("Video could not be opened. Please check that the the file integrity.") + raise OSError("Video could not be opened. Please check that the the file integrity.") # https://docs.opencv.org/2.4/modules/highgui/doc/reading_and_writing_images_and_video.html#videocapture-get fps = cap.get(cv2.CAP_PROP_FPS) nframes = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) @@ -1018,7 +1018,7 @@ def AnalyzeVideo( PredictedData, nframes = GetPoseS(cfg, dlc_cfg, sess, inputs, outputs, cap, nframes) stop = time.time() - if cfg["cropping"] == True: + if cfg["cropping"]: coords = [cfg["x1"], cfg["x2"], cfg["y1"], cfg["y2"]] else: coords = [0, nx, 0, ny] @@ -1076,8 +1076,9 @@ def GetPosesofFrames(cfg, dlc_cfg, sess, inputs, outputs, directory, framelist, batch_num = 0 # keeps track of which batch you are at if cfg["cropping"]: print( - "Cropping based on the x1 = %s x2 = %s y1 = %s y2 = %s. You can adjust the cropping coordinates in the config.yaml file." - % (cfg["x1"], cfg["x2"], cfg["y1"], cfg["y2"]) + "Cropping based on the x1 = {} x2 = {} y1 = {} y2 = {}. You can adjust the cropping coordinates in the config.yaml file.".format( + cfg["x1"], cfg["x2"], cfg["y1"], cfg["y2"] + ) ) nx, ny = cfg["x2"] - cfg["x1"], cfg["y2"] - cfg["y1"] if nx > 0 and ny > 0: @@ -1209,7 +1210,7 @@ def analyze_time_lapse_frames( dlc_cfg = load_config(str(path_test_config)) except FileNotFoundError: raise FileNotFoundError( - "It seems the model for shuffle %s and trainFraction %s does not exist." % (shuffle, trainFraction) + f"It seems the model for shuffle {shuffle} and trainFraction {trainFraction} does not exist." ) Snapshots = auxiliaryfunctions.get_snapshots_from_folder( @@ -1224,7 +1225,7 @@ def analyze_time_lapse_frames( else: snapshotindex = cfg["snapshotindex"] - print("Using %s" % Snapshots[snapshotindex], "for model", modelfolder) + print(f"Using {Snapshots[snapshotindex]}", "for model", modelfolder) ################################################## # Load and setup CNN part detector @@ -1267,7 +1268,7 @@ def analyze_time_lapse_frames( # Loading the images ################################################## # checks if input is a directory - if os.path.isdir(directory) == True: + if os.path.isdir(directory): """ Analyzes all the frames in the directory. """ @@ -1296,7 +1297,7 @@ def analyze_time_lapse_frames( ) stop = time.time() - if cfg["cropping"] == True: + if cfg["cropping"]: coords = [cfg["x1"], cfg["x2"], cfg["y1"], cfg["y2"]] else: coords = [0, nx, 0, ny] @@ -1316,7 +1317,7 @@ def analyze_time_lapse_frames( } metadata = {"data": dictionary} - print("Saving results in %s..." % (directory)) + print(f"Saving results in {directory}...") auxiliaryfunctions.save_data( PredictedData[:nframes, :], @@ -1523,7 +1524,7 @@ def convert_detections2tracklets( track_method = auxfun_multianimal.get_track_method(cfg, track_method=track_method) if len(cfg["multianimalbodyparts"]) == 1 and track_method != "box": - warnings.warn("Switching to `box` tracker for single point tracking...") + warnings.warn("Switching to `box` tracker for single point tracking...", stacklevel=2) track_method = "box" cfg["default_track_method"] = track_method auxiliaryfunctions.write_config(config, cfg) @@ -1547,7 +1548,7 @@ def convert_detections2tracklets( dlc_cfg = load_config(str(path_test_config)) except FileNotFoundError: raise FileNotFoundError( - "It seems the model for shuffle %s and trainFraction %s does not exist." % (shuffle, trainFraction) + f"It seems the model for shuffle {shuffle} and trainFraction {trainFraction} does not exist." ) if "multi-animal" not in dlc_cfg["dataset_type"]: @@ -1560,7 +1561,7 @@ def convert_detections2tracklets( auxfun_multianimal.check_inferencecfg_sanity(cfg, inferencecfg) if len(cfg["multianimalbodyparts"]) == 1 and track_method != "box": - warnings.warn("Switching to `box` tracker for single point tracking...") + warnings.warn("Switching to `box` tracker for single point tracking...", stacklevel=2) track_method = "box" # Also ensure `boundingboxslack` is greater than zero, otherwise overlap # between trackers cannot be evaluated, resulting in empty tracklets. @@ -1578,7 +1579,7 @@ def convert_detections2tracklets( else: snapshotindex = cfg["snapshotindex"] - print("Using %s" % Snapshots[snapshotindex], "for model", modelfolder) + print(f"Using {Snapshots[snapshotindex]}", "for model", modelfolder) dlc_cfg["init_weights"] = os.path.join(modelfolder, "train", Snapshots[snapshotindex]) trainingsiterations = (dlc_cfg["init_weights"].split(os.sep)[-1]).split("-")[-1] @@ -1625,7 +1626,8 @@ def convert_detections2tracklets( numjoints = len(all_jointnames) # TODO: adjust this for multi + unique bodyparts! - # this is only for multianimal parts and uniquebodyparts as one (not one uniquebodyparts guy tracked etc. ) + # this is only for multianimal parts and uniquebodyparts as one (not one + # uniquebodyparts guy tracked etc. ) bodypartlabels = [bpt for i, bpt in enumerate(all_jointnames) for _ in range(3)] scorers = len(bodypartlabels) * [DLCscorer] xylvalue = int(len(bodypartlabels) / 3) * ["x", "y", "likelihood"] diff --git a/deeplabcut/pose_estimation_tensorflow/training.py b/deeplabcut/pose_estimation_tensorflow/training.py index 5e882b828c..2e964fb321 100644 --- a/deeplabcut/pose_estimation_tensorflow/training.py +++ b/deeplabcut/pose_estimation_tensorflow/training.py @@ -149,12 +149,12 @@ def train_network( if allow_growth: os.environ["TF_FORCE_GPU_ALLOW_GROWTH"] = "true" - import tensorflow as tf - # reload logger. import importlib import logging + import tensorflow as tf + importlib.reload(logging) logging.shutdown() @@ -185,15 +185,17 @@ def train_network( cfg_dlc = auxiliaryfunctions.read_plainconfig(poseconfigfile) if superanimal_name != "": - from deeplabcut.modelzoo.utils import parse_available_supermodels + import glob + from dlclibrary.dlcmodelzoo.modelzoo_download import ( - download_huggingface_model, MODELOPTIONS, + download_huggingface_model, ) - import glob + + from deeplabcut.modelzoo.utils import parse_available_supermodels dlc_root_path = auxiliaryfunctions.get_deeplabcut_path() - supermodels = parse_available_supermodels() + parse_available_supermodels() weight_folder = str( Path(dlc_root_path) / "pose_estimation_tensorflow" diff --git a/deeplabcut/pose_estimation_tensorflow/util/visualize.py b/deeplabcut/pose_estimation_tensorflow/util/visualize.py index 2a8fe7213c..c84a11ed07 100644 --- a/deeplabcut/pose_estimation_tensorflow/util/visualize.py +++ b/deeplabcut/pose_estimation_tensorflow/util/visualize.py @@ -18,9 +18,9 @@ import math +import cv2 import matplotlib.pyplot as plt import numpy as np -import cv2 from deeplabcut.utils.auxfun_videos import imresize diff --git a/deeplabcut/pose_estimation_tensorflow/vis_dataset.py b/deeplabcut/pose_estimation_tensorflow/vis_dataset.py index 35349e9257..a3414a50f3 100644 --- a/deeplabcut/pose_estimation_tensorflow/vis_dataset.py +++ b/deeplabcut/pose_estimation_tensorflow/vis_dataset.py @@ -15,10 +15,9 @@ import logging +import cv2 import matplotlib.pyplot as plt import numpy as np -import cv2 - from deeplabcut.pose_estimation_tensorflow.config import load_config from deeplabcut.pose_estimation_tensorflow.datasets import ( @@ -70,7 +69,7 @@ def display_dataset(): scmap_part = imresize(scmap_part, 8.0, interpolationmethod=cv2.INTER_NEAREST) scmap_part = np.lib.pad(scmap_part, ((4, 0), (4, 0)), "minimum") - curr_plot.set_title("{}".format(j + 1)) + curr_plot.set_title(f"{j + 1}") curr_plot.imshow(img) curr_plot.hold(True) curr_plot.imshow(scmap_part, alpha=0.5) diff --git a/deeplabcut/pose_estimation_tensorflow/visualizemaps.py b/deeplabcut/pose_estimation_tensorflow/visualizemaps.py index 671e0455ce..0f9d64cedb 100644 --- a/deeplabcut/pose_estimation_tensorflow/visualizemaps.py +++ b/deeplabcut/pose_estimation_tensorflow/visualizemaps.py @@ -9,13 +9,14 @@ # Licensed under GNU Lesser General Public License v3.0 # import os + import matplotlib.pyplot as plt from skimage.transform import resize + from deeplabcut.core.visualization import ( - form_figure, # for backwards compatibility - visualize_scoremaps, visualize_locrefs, visualize_paf, + visualize_scoremaps, ) @@ -57,20 +58,23 @@ def extract_maps( >>> deeplabcut.extract_maps(configfile,0,Indices=[0,103]) """ - from deeplabcut.utils.auxfun_videos import imread, imresize + from pathlib import Path + + import numpy as np + import pandas as pd + import tensorflow as tf + from tqdm import tqdm + + from deeplabcut.pose_estimation_tensorflow.config import load_config from deeplabcut.pose_estimation_tensorflow.core import ( predict, + ) + from deeplabcut.pose_estimation_tensorflow.core import ( predict_multianimal as predictma, ) - from deeplabcut.pose_estimation_tensorflow.config import load_config from deeplabcut.pose_estimation_tensorflow.datasets.utils import data_to_input from deeplabcut.utils import auxiliaryfunctions - from tqdm import tqdm - import tensorflow as tf - - import pandas as pd - from pathlib import Path - import numpy as np + from deeplabcut.utils.auxfun_videos import imread, imresize tf.compat.v1.reset_default_graph() os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2" # @@ -135,7 +139,7 @@ def extract_maps( dlc_cfg = load_config(str(path_test_config)) except FileNotFoundError: raise FileNotFoundError( - "It seems the model for shuffle %s and trainFraction %s does not exist." % (shuffle, trainFraction) + f"It seems the model for shuffle {shuffle} and trainFraction {trainFraction} does not exist." ) # change batch size, if it was edited during analysis! @@ -171,7 +175,7 @@ def extract_maps( dlc_cfg["init_weights"] = os.path.join( str(modelfolder), "train", Snapshots[snapindex] ) # setting weights to corresponding snapshot. - trainingsiterations = (dlc_cfg["init_weights"].split(os.sep)[-1]).split("-")[ + (dlc_cfg["init_weights"].split(os.sep)[-1]).split("-")[ -1 ] # read how many training siterations that corresponds to. @@ -184,7 +188,7 @@ def extract_maps( # Specifying state of model (snapshot / training state) sess, inputs, outputs = predict.setup_pose_prediction(dlc_cfg) Numimages = len(Data.index) - PredicteData = np.zeros((Numimages, 3 * len(dlc_cfg["all_joints_names"]))) + np.zeros((Numimages, 3 * len(dlc_cfg["all_joints_names"]))) print("Analyzing data...") if Indices is None: Indices = enumerate(Data.index) @@ -249,7 +253,7 @@ def resize_all_maps(image, scmap, locref, paf): def _save_individual_subplots(fig, axes, labels, output_path): - for ax, label in zip(axes, labels): + for ax, label in zip(axes, labels, strict=False): extent = ax.get_tightbbox(fig.canvas.renderer).transformed(fig.dpi_scale_trans.inverted()) fig.savefig(output_path.format(bp=label), bbox_inches=extent) @@ -306,13 +310,14 @@ def extract_save_all_maps( """ + from tqdm import tqdm + from deeplabcut.utils.auxiliaryfunctions import ( - read_config, attempt_to_make_folder, get_evaluation_folder, intersection_of_body_parts_and_ones_given_by_user, + read_config, ) - from tqdm import tqdm cfg = read_config(config) data = extract_maps(config, shuffle, trainingsetindex, gputouse, rescale, Indices, modelprefix) @@ -408,7 +413,7 @@ def extract_save_all_maps( fig3, _ = visualize_paf(image, paf[:, :, inds], colors=colors) temp = dest_path.format( imname=imname, - map=f"paf", + map="paf", label=label, shuffle=shuffle, frac=frac, diff --git a/deeplabcut/pose_tracking_pytorch/__init__.py b/deeplabcut/pose_tracking_pytorch/__init__.py index 54b4f0d0ee..f5e3b9925a 100644 --- a/deeplabcut/pose_tracking_pytorch/__init__.py +++ b/deeplabcut/pose_tracking_pytorch/__init__.py @@ -9,7 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # +from .apis import transformer_reID from .create_dataset import * from .tracking_utils.preprocessing import * from .train_dlctransreid import train_tracking_transformer -from .apis import transformer_reID diff --git a/deeplabcut/pose_tracking_pytorch/apis.py b/deeplabcut/pose_tracking_pytorch/apis.py index 30f5ef528c..a37baa8e50 100644 --- a/deeplabcut/pose_tracking_pytorch/apis.py +++ b/deeplabcut/pose_tracking_pytorch/apis.py @@ -89,8 +89,9 @@ def transformer_reID( -------- """ - import deeplabcut import os + + import deeplabcut from deeplabcut.utils import auxiliaryfunctions # calling create_tracking_dataset, train_tracking_transformer, stitch_tracklets diff --git a/deeplabcut/pose_tracking_pytorch/config/__init__.py b/deeplabcut/pose_tracking_pytorch/config/__init__.py index fcd59e9fce..be04343413 100644 --- a/deeplabcut/pose_tracking_pytorch/config/__init__.py +++ b/deeplabcut/pose_tracking_pytorch/config/__init__.py @@ -10,12 +10,12 @@ # import os + from deeplabcut.utils.auxiliaryfunctions import ( - read_plainconfig, get_deeplabcut_path, + read_plainconfig, ) - dlcparent_path = get_deeplabcut_path() reid_config = os.path.join(dlcparent_path, "reid_cfg.yaml") cfg = read_plainconfig(reid_config) diff --git a/deeplabcut/pose_tracking_pytorch/create_dataset.py b/deeplabcut/pose_tracking_pytorch/create_dataset.py index b1a2e5792c..e7e3e77522 100644 --- a/deeplabcut/pose_tracking_pytorch/create_dataset.py +++ b/deeplabcut/pose_tracking_pytorch/create_dataset.py @@ -9,13 +9,16 @@ # Licensed under GNU Lesser General Public License v3.0 # -import numpy as np import os import pickle import shelve +from pathlib import Path + +import numpy as np + from deeplabcut.core import trackingutils from deeplabcut.refine_training_dataset.stitch import TrackletStitcher -from pathlib import Path + from .tracking_utils.preprocessing import query_feature_by_coord_in_img_space np.random.seed(0) diff --git a/deeplabcut/pose_tracking_pytorch/datasets/dlc_vec.py b/deeplabcut/pose_tracking_pytorch/datasets/dlc_vec.py index fa1512924a..ae707771ba 100644 --- a/deeplabcut/pose_tracking_pytorch/datasets/dlc_vec.py +++ b/deeplabcut/pose_tracking_pytorch/datasets/dlc_vec.py @@ -9,8 +9,8 @@ # Licensed under GNU Lesser General Public License v3.0 # -from torch.utils.data import Dataset import numpy as np +from torch.utils.data import Dataset class TripletDataset(Dataset): diff --git a/deeplabcut/pose_tracking_pytorch/datasets/make_dataloader.py b/deeplabcut/pose_tracking_pytorch/datasets/make_dataloader.py index e96c9fa481..660b960003 100644 --- a/deeplabcut/pose_tracking_pytorch/datasets/make_dataloader.py +++ b/deeplabcut/pose_tracking_pytorch/datasets/make_dataloader.py @@ -10,6 +10,7 @@ # from torch.utils.data import DataLoader + from .dlc_vec import TripletDataset diff --git a/deeplabcut/pose_tracking_pytorch/inference.py b/deeplabcut/pose_tracking_pytorch/inference.py index 0691de2ec3..debea750a4 100644 --- a/deeplabcut/pose_tracking_pytorch/inference.py +++ b/deeplabcut/pose_tracking_pytorch/inference.py @@ -9,18 +9,18 @@ # Licensed under GNU Lesser General Public License v3.0 # +import numpy as np import torch import torch.nn as nn -import numpy as np + from deeplabcut.pose_tracking_pytorch.config import cfg from deeplabcut.pose_tracking_pytorch.model import build_dlc_transformer from deeplabcut.pose_tracking_pytorch.model.backbones import dlc_base_kpt_TransReID +from deeplabcut.pose_tracking_pytorch.processor import default_device from deeplabcut.pose_tracking_pytorch.tracking_utils import ( query_feature_by_coord_in_img_space, ) -from deeplabcut.pose_tracking_pytorch.processor import default_device - inference_factory = {"dlc_transreid": dlc_base_kpt_TransReID} diff --git a/deeplabcut/pose_tracking_pytorch/model/__init__.py b/deeplabcut/pose_tracking_pytorch/model/__init__.py index cd526a115f..573d1ecd61 100644 --- a/deeplabcut/pose_tracking_pytorch/model/__init__.py +++ b/deeplabcut/pose_tracking_pytorch/model/__init__.py @@ -9,4 +9,4 @@ # Licensed under GNU Lesser General Public License v3.0 # -from .make_model import make_dlc_model, build_dlc_transformer +from .make_model import build_dlc_transformer, make_dlc_model diff --git a/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py b/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py index 0d73b4a4a3..9d1b808b0a 100644 --- a/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py +++ b/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py @@ -63,7 +63,7 @@ class DropPath(nn.Module): """Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks).""" def __init__(self, drop_prob=None): - super(DropPath, self).__init__() + super().__init__() self.drop_prob = drop_prob def forward(self, x): @@ -272,7 +272,7 @@ def reset_classifier(self, num_classes, global_pool=""): def forward_features(self, x): # x: inputs - B = x.shape[0] + x.shape[0] # (B, 12, 768) x = self.kpt_embed(x) @@ -329,9 +329,9 @@ def load_param(self, model_path): except: print("===========================ERROR=========================") print( - "shape do not match in k :{}: param_dict{} vs self.state_dict(){}".format( - k, v.shape, self.state_dict()[k].shape - ) + f"shape do not match in k :{k}: param_dict{v.shape} vs self.state_dict(){ + self.state_dict()[k].shape + }" ) @@ -345,9 +345,9 @@ def resize_pos_embed(posemb, posemb_new, height, width): gs_old = int(math.sqrt(len(posemb_grid))) print( - "Resized position embedding from size:{} to size: {} with height:{} width: {}".format( - posemb.shape, posemb_new.shape, height, width - ) + f"Resized position embedding from size:{posemb.shape} to size: {posemb_new.shape} with height:{height} width: { + width + }" ) posemb_grid = posemb_grid.reshape(1, gs_old, gs_old, -1).permute(0, 3, 1, 2) posemb_grid = F.interpolate(posemb_grid, size=(height, width), mode="bilinear") diff --git a/deeplabcut/pose_tracking_pytorch/model/make_model.py b/deeplabcut/pose_tracking_pytorch/model/make_model.py index 9ef55029ad..51142aa16d 100644 --- a/deeplabcut/pose_tracking_pytorch/model/make_model.py +++ b/deeplabcut/pose_tracking_pytorch/model/make_model.py @@ -11,12 +11,13 @@ import torch import torch.nn as nn + from .backbones.vit_pytorch import dlc_base_kpt_TransReID class build_dlc_transformer(nn.Module): def __init__(self, cfg, in_chans, kpt_num, factory): - super(build_dlc_transformer, self).__init__() + super().__init__() self.cos_layer = cfg["cos_layer"] self.in_planes = 128 self.kpt_num = kpt_num @@ -52,7 +53,7 @@ def load_param(self, trained_path): param_dict = torch.load(trained_path, map_location=device) for i in param_dict: self.state_dict()[i.replace("module.", "")].copy_(param_dict[i]) - print("Loading pretrained model from {}".format(trained_path)) + print(f"Loading pretrained model from {trained_path}") __factory_T_type = { diff --git a/deeplabcut/pose_tracking_pytorch/processor/__init__.py b/deeplabcut/pose_tracking_pytorch/processor/__init__.py index d2253dbf25..2a49144cdf 100644 --- a/deeplabcut/pose_tracking_pytorch/processor/__init__.py +++ b/deeplabcut/pose_tracking_pytorch/processor/__init__.py @@ -10,8 +10,8 @@ # from .processor import ( - do_dlc_train, + default_device, do_dlc_inference, do_dlc_pair_inference, - default_device, + do_dlc_train, ) diff --git a/deeplabcut/pose_tracking_pytorch/processor/processor.py b/deeplabcut/pose_tracking_pytorch/processor/processor.py index 26d0635f3d..3d88ea9ad6 100644 --- a/deeplabcut/pose_tracking_pytorch/processor/processor.py +++ b/deeplabcut/pose_tracking_pytorch/processor/processor.py @@ -11,14 +11,16 @@ import logging import os +import pickle import time + +import numpy as np import torch +import torch.distributed as dist import torch.nn as nn + from ..tracking_utils.meter import AverageMeter from ..tracking_utils.metrics import R1_mAP_eval -import torch.distributed as dist -import pickle -import numpy as np def dist(a, b): @@ -126,13 +128,8 @@ def do_dlc_train( if (n_iter + 1) % log_period == 0: logger.info( - "Epoch[{}] Iteration[{}/{}] Loss: {:.3f}, , Base Lr: {:.2e}".format( - epoch, - (n_iter + 1), - len(train_loader), - loss_meter.avg, - scheduler._get_lr(epoch)[0], - ) + f"Epoch[{epoch}] Iteration[{n_iter + 1}/{len(train_loader)}] Loss: { + loss_meter.avg:.3f}, , Base Lr: {scheduler._get_lr(epoch)[0]:.2e}" ) end_time = time.time() @@ -144,12 +141,11 @@ def do_dlc_train( pass else: logger.info( - "Epoch {} done. Time per batch: {:.3f}[s] Speed: {:.1f}[samples/s]".format( - epoch, time_per_batch, train_loader.batch_size / time_per_batch - ) + f"Epoch {epoch} done. Time per batch: {time_per_batch:.3f}[s] Speed: { + train_loader.batch_size / time_per_batch:.1f}[samples/s]" ) - model_name = f"dlc_transreid" + model_name = "dlc_transreid" if epoch % checkpoint_period == 0: torch.save( @@ -158,7 +154,7 @@ def do_dlc_train( "num_kpts": num_kpts, "feature_dim": feature_dim, }, - os.path.join(ckpt_folder, model_name + "_{}.pth".format(epoch)), + os.path.join(ckpt_folder, model_name + f"_{epoch}.pth"), ) if epoch % eval_period == 0: @@ -180,7 +176,7 @@ def do_dlc_train( total_n += anchor_feat.shape[0] total_correct += calc_correct(anchor_feat, pos_feat, neg_feat) - logger.info("Validation Results - Epoch: {}".format(epoch)) + logger.info(f"Validation Results - Epoch: {epoch}") # print (f'validation loss {val_loss/len(val_loader)}') test_acc = total_correct / total_n @@ -210,7 +206,7 @@ def do_dlc_inference(cfg, model, triplet_loss, val_loader, num_query): if device: if torch.cuda.device_count() > 1: - print("Using {} GPUs for inference".format(torch.cuda.device_count())) + print(f"Using {torch.cuda.device_count()} GPUs for inference") model = nn.DataParallel(model) model.to(device) @@ -221,7 +217,7 @@ def do_dlc_inference(cfg, model, triplet_loss, val_loader, num_query): labels_list = [] total_n = 0.0 total_correct = 0.0 - for n_iter, (anchor, pos, neg) in enumerate(val_loader): + for _n_iter, (anchor, pos, neg) in enumerate(val_loader): with torch.no_grad(): anchor = anchor.to(device) pos = pos.to(device) @@ -233,7 +229,7 @@ def do_dlc_inference(cfg, model, triplet_loss, val_loader, num_query): features_list.append(pos_feat.cpu().detach().numpy()) features_list.append(neg_feat.cpu().detach().numpy()) - for i in range(neg.shape[0]): + for _i in range(neg.shape[0]): labels_list.append(0) labels_list.append(1) total_n += anchor_feat.shape[0] @@ -269,16 +265,15 @@ def do_dlc_pair_inference(cfg, model, val_loader, num_query): if device and torch.cuda.is_available(): if torch.cuda.device_count() > 1: - print("Using {} GPUs for inference".format(torch.cuda.device_count())) + print(f"Using {torch.cuda.device_count()} GPUs for inference") model = nn.DataParallel(model) model.to(device) model.eval() - val_loss = 0.0 total_n = 0.0 total_correct = 0.0 - for n_iter, ((vec1, gt1), (vec2, gt2)) in enumerate(val_loader): + for _n_iter, ((vec1, gt1), (vec2, gt2)) in enumerate(val_loader): with torch.no_grad(): gt1 = gt1.to(device) gt2 = gt2.to(device) diff --git a/deeplabcut/pose_tracking_pytorch/solver/cosine_lr.py b/deeplabcut/pose_tracking_pytorch/solver/cosine_lr.py index e27d67a700..69091c5a33 100644 --- a/deeplabcut/pose_tracking_pytorch/solver/cosine_lr.py +++ b/deeplabcut/pose_tracking_pytorch/solver/cosine_lr.py @@ -17,11 +17,11 @@ import logging import math + import torch from .scheduler import Scheduler - _logger = logging.getLogger(__name__) diff --git a/deeplabcut/pose_tracking_pytorch/solver/scheduler.py b/deeplabcut/pose_tracking_pytorch/solver/scheduler.py index 91d6a9915b..e724034922 100644 --- a/deeplabcut/pose_tracking_pytorch/solver/scheduler.py +++ b/deeplabcut/pose_tracking_pytorch/solver/scheduler.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -from typing import Dict, Any +from typing import Any import torch @@ -64,10 +64,10 @@ def __init__( self.noise_seed = noise_seed if noise_seed is not None else 42 self.update_groups(self.base_values) - def state_dict(self) -> Dict[str, Any]: + def state_dict(self) -> dict[str, Any]: return {key: value for key, value in self.__dict__.items() if key != "optimizer"} - def load_state_dict(self, state_dict: Dict[str, Any]) -> None: + def load_state_dict(self, state_dict: dict[str, Any]) -> None: self.__dict__.update(state_dict) def get_epoch_values(self, epoch: int): diff --git a/deeplabcut/pose_tracking_pytorch/tracking_utils/__init__.py b/deeplabcut/pose_tracking_pytorch/tracking_utils/__init__.py index d3a9fbccb8..1ee383d0e4 100644 --- a/deeplabcut/pose_tracking_pytorch/tracking_utils/__init__.py +++ b/deeplabcut/pose_tracking_pytorch/tracking_utils/__init__.py @@ -9,7 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # from .preprocessing import ( - load_features_from_coord, convert_coord_from_img_space_to_feature_space, + load_features_from_coord, query_feature_by_coord_in_img_space, ) diff --git a/deeplabcut/pose_tracking_pytorch/tracking_utils/meter.py b/deeplabcut/pose_tracking_pytorch/tracking_utils/meter.py index c26c07655b..655cfd2d27 100644 --- a/deeplabcut/pose_tracking_pytorch/tracking_utils/meter.py +++ b/deeplabcut/pose_tracking_pytorch/tracking_utils/meter.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -class AverageMeter(object): +class AverageMeter: """Computes and stores the average and current value""" def __init__(self): diff --git a/deeplabcut/pose_tracking_pytorch/tracking_utils/metrics.py b/deeplabcut/pose_tracking_pytorch/tracking_utils/metrics.py index cc1c73c270..37eab5fc38 100644 --- a/deeplabcut/pose_tracking_pytorch/tracking_utils/metrics.py +++ b/deeplabcut/pose_tracking_pytorch/tracking_utils/metrics.py @@ -8,8 +8,9 @@ # # Licensed under GNU Lesser General Public License v3.0 # -import torch import numpy as np +import torch + from ..tracking_utils.reranking import re_ranking @@ -47,7 +48,7 @@ def eval_func(distmat, q_pids, g_pids, q_camids, g_camids, max_rank=50): # 4 1 2 3 if num_g < max_rank: max_rank = num_g - print("Note: number of gallery samples is quite small, got {}".format(num_g)) + print(f"Note: number of gallery samples is quite small, got {num_g}") indices = np.argsort(distmat, axis=1) # 0, 2, 1, 3 # 1, 2, 3, 0 @@ -101,7 +102,7 @@ def eval_func(distmat, q_pids, g_pids, q_camids, g_camids, max_rank=50): class R1_mAP_eval: def __init__(self, num_query, max_rank=50, feat_norm=True, reranking=False): - super(R1_mAP_eval, self).__init__() + super().__init__() self.num_query = num_query self.max_rank = max_rank self.feat_norm = feat_norm diff --git a/deeplabcut/pose_tracking_pytorch/tracking_utils/reranking.py b/deeplabcut/pose_tracking_pytorch/tracking_utils/reranking.py index d4b117f6f4..e4117b7da6 100644 --- a/deeplabcut/pose_tracking_pytorch/tracking_utils/reranking.py +++ b/deeplabcut/pose_tracking_pytorch/tracking_utils/reranking.py @@ -39,7 +39,7 @@ def re_ranking(probFea, galFea, k1, k2, lambda_value, local_distmat=None, only_l distmat.addmm_(1, -2, feat, feat.t()) original_dist = distmat.cpu().numpy() del feat - if not local_distmat is None: + if local_distmat is not None: original_dist = original_dist + local_distmat gallery_num = original_dist.shape[0] original_dist = np.transpose(original_dist / np.max(original_dist, axis=0)) diff --git a/deeplabcut/pose_tracking_pytorch/train_dlctransreid.py b/deeplabcut/pose_tracking_pytorch/train_dlctransreid.py index d92a9ed1fe..0d5861ad72 100644 --- a/deeplabcut/pose_tracking_pytorch/train_dlctransreid.py +++ b/deeplabcut/pose_tracking_pytorch/train_dlctransreid.py @@ -15,18 +15,21 @@ import torch except ModuleNotFoundError: raise ModuleNotFoundError("Unsupervised identity learning requires PyTorch. Please run `pip install torch`.") -import numpy as np -import os import glob -from deeplabcut.utils import auxiliaryfunctions +import os from pathlib import Path + +import numpy as np + +from deeplabcut.utils import auxiliaryfunctions + from .config import cfg from .datasets import make_dlc_dataloader +from .loss import easy_triplet_loss from .model import make_dlc_model +from .processor import do_dlc_train from .solver import make_easy_optimizer from .solver.scheduler_factory import create_scheduler -from .loss import easy_triplet_loss -from .processor import do_dlc_train def set_seed(seed): diff --git a/deeplabcut/post_processing/analyze_skeleton.py b/deeplabcut/post_processing/analyze_skeleton.py index 559e4a767b..59820dcaa3 100644 --- a/deeplabcut/post_processing/analyze_skeleton.py +++ b/deeplabcut/post_processing/analyze_skeleton.py @@ -14,14 +14,15 @@ """ import argparse +import os from math import atan2, degrees from pathlib import Path -import os + import numpy as np import pandas as pd from scipy.spatial import distance -from deeplabcut.utils import auxiliaryfunctions, auxfun_multianimal +from deeplabcut.utils import auxfun_multianimal, auxiliaryfunctions # utility functions @@ -56,7 +57,7 @@ def calc_distance_between_points_two_vectors_2d(v1, v2): raise ValueError("Error: input arrays should have the same length") # Calculate distance - dist = [distance.euclidean(p1, p2) for p1, p2 in zip(v1, v2)] + dist = [distance.euclidean(p1, p2) for p1, p2 in zip(v1, v2, strict=False)] return dist @@ -259,7 +260,7 @@ def analyzeskeleton( Videos = auxiliaryfunctions.get_list_of_videos(videos, videotype) for video in Videos: - print("Processing %s" % (video)) + print(f"Processing {video}") if destfolder is None: destfolder = str(Path(video).parents[0]) @@ -273,7 +274,7 @@ def analyzeskeleton( video_to_skeleton_df[video] = None continue - output_name = filepath.replace(".h5", f"_skeleton.h5") + output_name = filepath.replace(".h5", "_skeleton.h5") if os.path.isfile(output_name): print(f"Skeleton in video {vname} already processed. Skipping...") video_to_skeleton_df[video] = pd.read_hdf(output_name, "df_with_missing") @@ -285,11 +286,11 @@ def analyzeskeleton( temp = df_.droplevel(["scorer", "individuals"], axis=1) if animal_name != "single": for bp1, bp2 in cfg["skeleton"]: - name = "{}_{}_{}".format(animal_name, bp1, bp2) + name = f"{animal_name}_{bp1}_{bp2}" bones[name] = analyzebone(temp[bp1], temp[bp2]) else: for bp1, bp2 in cfg["skeleton"]: - name = "{}_{}".format(bp1, bp2) + name = f"{bp1}_{bp2}" bones[name] = analyzebone(df[scorer][bp1], df[scorer][bp2]) skeleton = pd.concat(bones, axis=1) diff --git a/deeplabcut/post_processing/filtering.py b/deeplabcut/post_processing/filtering.py index cc29570789..c1041b384e 100644 --- a/deeplabcut/post_processing/filtering.py +++ b/deeplabcut/post_processing/filtering.py @@ -18,7 +18,7 @@ from scipy.interpolate import CubicSpline from deeplabcut.refine_training_dataset.outlier_frames import FitSARIMAXModel -from deeplabcut.utils import auxiliaryfunctions, auxfun_multianimal +from deeplabcut.utils import auxfun_multianimal, auxiliaryfunctions def columnwise_spline_interp(data, max_gap=0): diff --git a/deeplabcut/refine_training_dataset/__init__.py b/deeplabcut/refine_training_dataset/__init__.py index 6c04417f93..9dc09adde2 100644 --- a/deeplabcut/refine_training_dataset/__init__.py +++ b/deeplabcut/refine_training_dataset/__init__.py @@ -10,5 +10,5 @@ # -from deeplabcut.refine_training_dataset.tracklets import * from deeplabcut.refine_training_dataset.outlier_frames import * +from deeplabcut.refine_training_dataset.tracklets import * diff --git a/deeplabcut/refine_training_dataset/outlier_frames.py b/deeplabcut/refine_training_dataset/outlier_frames.py index 20ad322472..fbcb53dd8e 100644 --- a/deeplabcut/refine_training_dataset/outlier_frames.py +++ b/deeplabcut/refine_training_dataset/outlier_frames.py @@ -15,7 +15,6 @@ import pickle import re from pathlib import Path -from typing import List, Optional import matplotlib.pyplot as plt import numpy as np @@ -25,11 +24,11 @@ from deeplabcut.core import inferenceutils from deeplabcut.utils import ( - auxiliaryfunctions, auxfun_multianimal, + auxiliaryfunctions, conversioncode, - visualization, frameselectiontools, + visualization, ) from deeplabcut.utils.auxfun_videos import VideoWriter @@ -104,7 +103,7 @@ def find_outliers_in_raw_data( assemblies[k] = ass inds = inferenceutils.find_outlier_assemblies(assemblies, qs=percentiles) else: - raise IOError(f"Raw data file {pickle_file} could not be parsed.") + raise OSError(f"Raw data file {pickle_file} could not be parsed.") cfg = auxiliaryfunctions.read_config(config) ExtractFramesbasedonPreselection( @@ -147,7 +146,7 @@ def find_outliers_in_raw_detections(pickled_data, algo="uncertain", threshold=0. Indices of video frames containing potential outliers """ if algo != "uncertain": - raise ValueError(f"Only method 'uncertain' is currently supported.") + raise ValueError("Only method 'uncertain' is currently supported.") try: _ = pickled_data.pop("metadata") @@ -473,7 +472,7 @@ def extract_outlier_frames( if frames2use is not None: try: frames2use = np.array(frames2use).astype("int") - except ValueError() as e: + except ValueError(): print( "Could not cast frames2use into np array, please check that frames2use is a simply a list of integers!" ) @@ -557,11 +556,13 @@ def convertparms2start(pn): def FitSARIMAXModel(x, p, pcutoff, alpha, ARdegree, MAdegree, nforecast=0, disp=False): # Seasonal Autoregressive Integrated Moving-Average with eXogenous regressors (SARIMAX) - # see http://www.statsmodels.org/stable/statespace.html#seasonal-autoregressive-integrated-moving-average-with-exogenous-regressors-sarimax + # see + # http://www.statsmodels.org/stable/statespace.html#seasonal-autoregressive-integrated-moving-average-with-exogenous-regressors-sarimax Y = x.copy() Y[p < pcutoff] = np.nan # Set uncertain estimates to nan (modeled as missing data) if np.sum(np.isfinite(Y)) > 10: - # SARIMAX implementation has better prediction models than simple ARIMAX (however we do not use the seasonal etc. parameters!) + # SARIMAX implementation has better prediction models than simple ARIMAX + # (however we do not use the seasonal etc. parameters!) mod = sm.tsa.statespace.SARIMAX( Y.flatten(), order=(ARdegree, 0, MAdegree), @@ -572,7 +573,8 @@ def FitSARIMAXModel(x, p, pcutoff, alpha, ARdegree, MAdegree, nforecast=0, disp= # mod = sm.tsa.ARIMA(Y, order=(ARdegree,0,MAdegree)) #order=(ARdegree,0,MAdegree) try: res = mod.fit(disp=disp) - except ValueError: # https://groups.google.com/forum/#!topic/pystatsmodels/S_Fo53F25Rk (let's update to statsmodels 0.10.0 soon...) + # https://groups.google.com/forum/#!topic/pystatsmodels/S_Fo53F25Rk (let's update to statsmodels 0.10.0 soon...) + except ValueError: startvalues = np.array([convertparms2start(pn) for pn in mod.param_names]) res = mod.fit(start_params=startvalues, disp=disp) except np.linalg.LinAlgError: @@ -650,7 +652,7 @@ def attempt_to_add_video( config: str, video: str, copy_videos: bool, - coords: Optional[List], + coords: list | None, ) -> bool: """ Add new videos to the config file at any stage of the project. @@ -688,7 +690,7 @@ def attempt_to_add_video( # can we make a catch here? - in fact we should drop indices from DataCombined # if they are in CollectedData.. [ideal behavior; currently pretty unlikely] print( - f"AUTOMATIC ADDING OF VIDEO TO CONFIG FILE FAILED! You need to " + "AUTOMATIC ADDING OF VIDEO TO CONFIG FILE FAILED! You need to " "do this manually for including it in the config.yaml file!" ) print("Videopath:", video, "Coordinates for cropping:", coords) @@ -716,7 +718,7 @@ def ExtractFramesbasedonPreselection( numframes2extract = cfg["numframes2pick"] bodyparts = auxiliaryfunctions.intersection_of_body_parts_and_ones_given_by_user(cfg, "all") - videofolder = str(Path(video).parents[0]) + str(Path(video).parents[0]) vname = str(Path(video).stem) tmpfolder = os.path.join(cfg["project_path"], "labeled-data", vname) if os.path.isdir(tmpfolder): @@ -796,7 +798,8 @@ def ExtractFramesbasedonPreselection( print("Please implement this method yourself! Currently the options are 'kmeans', 'jump', 'uniform'.") frames2pick = [] - # Extract frames + frames with plotted labels and store them in folder (with name derived from video name) nder labeled-data + # Extract frames + frames with plotted labels and store them in folder + # (with name derived from video name) nder labeled-data print("Let's select frames indices:", frames2pick) colors = visualization.get_cmap(len(bodyparts), cfg["colormap"]) strwidth = int(np.ceil(np.log10(nframes))) # width for strings @@ -838,7 +841,8 @@ def ExtractFramesbasedonPreselection( clip.close() del clip - # Extract annotations based on DeepLabCut and store in the folder (with name derived from video name) under labeled-data + # Extract annotations based on DeepLabCut and store in the folder (with + # name derived from video name) under labeled-data if len(frames2pick) > 0: added_video = attempt_to_add_video( config=config, @@ -929,7 +933,7 @@ def ExtractFramesbasedonPreselection( df.to_hdf(machinefile, key="df_with_missing", mode="w") df.to_csv(os.path.join(tmpfolder, "machinelabels.csv")) - print(r"The outlier frames are extracted. They are stored in the subdirectory labeled-data\%s." % vname) + print(rf"The outlier frames are extracted. They are stored in the subdirectory labeled-data\{vname}.") print("Once you extracted frames for all videos, use 'refine_labels' to manually correct the labels.") else: print("No frames were extracted.") @@ -1094,7 +1098,7 @@ def merge_datasets(config, forceiterate=None): os.path.join(bf, fn) for fn in os.listdir(bf) if "_labeled" not in fn and not fn.startswith(".") ] # exclude labeled data folders and temporary files flagged = False - for findex, folder in enumerate(allfolders): + for _findex, folder in enumerate(allfolders): if os.path.isfile(os.path.join(folder, "MachineLabelsRefine.h5")): # Folder that was manually refine... pass elif os.path.isfile( diff --git a/deeplabcut/refine_training_dataset/stitch.py b/deeplabcut/refine_training_dataset/stitch.py index 0aa76b484f..3dea23d3c6 100644 --- a/deeplabcut/refine_training_dataset/stitch.py +++ b/deeplabcut/refine_training_dataset/stitch.py @@ -8,36 +8,35 @@ # # Licensed under GNU Lesser General Public License v3.0 # -from typing import List, Optional - -import matplotlib.pyplot as plt -import networkx as nx -import numpy as np import os -import pandas as pd import pickle import re -import scipy.linalg.interpolative as sli import shelve import warnings from collections import defaultdict - -import deeplabcut -from deeplabcut.utils.auxfun_videos import VideoWriter from functools import partial -from deeplabcut.core.trackingutils import ( - calc_iou, - TRACK_METHODS, -) -from deeplabcut.utils import auxiliaryfunctions, auxfun_multianimal from itertools import combinations, cycle -from networkx.algorithms.flow import preflow_push from pathlib import Path + +import matplotlib.pyplot as plt +import networkx as nx +import numpy as np +import pandas as pd +import scipy.linalg.interpolative as sli +from networkx.algorithms.flow import preflow_push from scipy.linalg import hankel from scipy.spatial.distance import directed_hausdorff from scipy.stats import mode from tqdm import trange +import deeplabcut +from deeplabcut.core.trackingutils import ( + TRACK_METHODS, + calc_iou, +) +from deeplabcut.utils import auxfun_multianimal, auxiliaryfunctions +from deeplabcut.utils.auxfun_videos import VideoWriter + class Tracklet: def __init__(self, data, inds): @@ -60,7 +59,7 @@ def __init__(self, data, inds): self.data = data.astype(np.float64) self.inds = np.array(inds) - monotonically_increasing = all(a < b for a, b in zip(inds, inds[1:])) + monotonically_increasing = all(a < b for a, b in zip(inds, inds[1:], strict=False)) if not monotonically_increasing: idx = np.argsort(inds, kind="mergesort") # For stable sort with duplicates self.inds = self.inds[idx] @@ -464,7 +463,7 @@ def __init__( self.residuals.append(t) if not len(self.tracklets): - raise IOError("Tracklets are empty.") + raise OSError("Tracklets are empty.") if prestitch_residuals: self._prestitch_residuals(5) # Hard-coded but found to work very well @@ -524,7 +523,7 @@ def from_dict_of_dict( single = None for k, dict_ in dict_of_dict.items(): try: - inds, data = zip(*[(cls.get_frame_ind(k), v) for k, v in dict_.items()]) + inds, data = zip(*[(cls.get_frame_ind(k), v) for k, v in dict_.items()], strict=False) except ValueError: continue inds = np.asarray(inds) @@ -562,7 +561,7 @@ def split_tracklet(tracklet, inds): idx = sorted(set(np.searchsorted(tracklet.inds, inds))) inds_new = np.split(tracklet.inds, idx) data_new = np.split(tracklet.data, idx) - return [Tracklet(data, inds) for data, inds in zip(data_new, inds_new)] + return [Tracklet(data, inds) for data, inds in zip(data_new, inds_new, strict=False)] @property def n_frames(self): @@ -625,12 +624,12 @@ def build_graph( self.G = nx.DiGraph() self.G.add_node("source", demand=-self.n_tracks) self.G.add_node("sink", demand=self.n_tracks) - nodes_in, nodes_out = zip(*[v.values() for k, v in self._mapping.items() if k in nodes]) + nodes_in, nodes_out = zip(*[v.values() for k, v in self._mapping.items() if k in nodes], strict=False) self.G.add_nodes_from(nodes_in, demand=1) self.G.add_nodes_from(nodes_out, demand=-1) - self.G.add_edges_from(zip(nodes_in, nodes_out), capacity=1) - self.G.add_edges_from(zip(["source"] * n_nodes, nodes_in), capacity=1) - self.G.add_edges_from(zip(nodes_out, ["sink"] * n_nodes), capacity=1) + self.G.add_edges_from(zip(nodes_in, nodes_out, strict=False), capacity=1) + self.G.add_edges_from(zip(["source"] * n_nodes, nodes_in, strict=False), capacity=1) + self.G.add_edges_from(zip(nodes_out, ["sink"] * n_nodes, strict=False), capacity=1) if weight_func is None: weight_func = self.calculate_edge_weight for i in trange(n_nodes): @@ -669,15 +668,15 @@ def stitch(self, add_back_residuals=True): _, self.flow = nx.capacity_scaling(self.G) self.paths = self.reconstruct_paths() except nx.exception.NetworkXUnfeasible: - warnings.warn("No optimal solution found. Employing black magic...") + warnings.warn("No optimal solution found. Employing black magic...", stacklevel=2) # Let us prune the graph by removing all source and sink edges # but those connecting the `n_tracks` first and last tracklets. in_to_keep = [self._mapping[first_tracklet]["in"] for first_tracklet in self._first_tracklets] out_to_keep = [self._mapping[last_tracklet]["out"] for last_tracklet in self._last_tracklets] in_to_remove = set(node for _, node in self.G.out_edges("source")).difference(in_to_keep) out_to_remove = set(node for node, _ in self.G.in_edges("sink")).difference(out_to_keep) - self.G.remove_edges_from(zip(["source"] * len(in_to_remove), in_to_remove)) - self.G.remove_edges_from(zip(out_to_remove, ["sink"] * len(out_to_remove))) + self.G.remove_edges_from(zip(["source"] * len(in_to_remove), in_to_remove, strict=False)) + self.G.remove_edges_from(zip(out_to_remove, ["sink"] * len(out_to_remove), strict=False)) # Preflow push seems to work slightly better than shortest # augmentation path..., and is more computationally efficient. paths = [] @@ -724,7 +723,7 @@ def stitch(self, add_back_residuals=True): paths += self.reconstruct_paths() self.paths = paths if len(self.paths) != self.n_tracks: - warnings.warn(f"Only {len(self.paths)} tracks could be reconstructed.") + warnings.warn(f"Only {len(self.paths)} tracks could be reconstructed.", stacklevel=2) finally: if self.paths is None: @@ -913,7 +912,7 @@ def plot_paths(self, colormap="Set2"): for path in self.paths: length = len(path) colors = plt.get_cmap(colormap, length)(range(length)) - for tracklet, color in zip(path, colors): + for tracklet, color in zip(path, colors, strict=False): tracklet.plot(color=color, ax=ax) def plot_tracks(self, colormap="viridis"): @@ -926,7 +925,7 @@ def plot_tracks(self, colormap="viridis"): if loc != "bottom": spine.set_visible(False) colors = plt.get_cmap(colormap, self.n_tracks)(range(self.n_tracks)) - for track, color in zip(self.tracks, colors): + for track, color in zip(self.tracks, colors, strict=False): track.plot(color=color, ax=ax) def plot_tracklets(self, colormap="Paired"): @@ -948,7 +947,7 @@ def plot_tracklets(self, colormap="Paired"): tracklet2lines[tracklet] = lines for line in lines: line2tracklet[line] = tracklet - for i, (x, y) in zip(tracklet.inds, tracklet.centroid): + for i, (x, y) in zip(tracklet.inds, tracklet.centroid, strict=False): all_points[i][(x, y)] = color def reconstruct_paths(self): @@ -976,7 +975,7 @@ def stitch_tracklets( shuffle=1, trainingsetindex=0, n_tracks=None, - animal_names: Optional[List[str]] = None, + animal_names: list[str] | None = None, min_length=10, split_tracklets=True, prestitch_residuals=True, diff --git a/deeplabcut/refine_training_dataset/tracklets.py b/deeplabcut/refine_training_dataset/tracklets.py index f4d6a4e639..6ae76f845d 100644 --- a/deeplabcut/refine_training_dataset/tracklets.py +++ b/deeplabcut/refine_training_dataset/tracklets.py @@ -8,13 +8,15 @@ # # Licensed under GNU Lesser General Public License v3.0 # -import numpy as np -import pandas as pd import pickle import re + +import numpy as np +import pandas as pd +from tqdm import trange + from deeplabcut.post_processing import columnwise_spline_interp from deeplabcut.utils import auxiliaryfunctions -from tqdm import trange class TrackletManager: @@ -81,7 +83,7 @@ def _load_tracklets(self, tracklets, auto_fill): # Sort tracklets by length to prioritize greater continuity temp = sorted(tracklets.values(), key=len) if not len(temp): - raise IOError("Tracklets are empty.") + raise OSError("Tracklets are empty.") get_frame_ind = lambda s: int(re.findall(r"\d+", s)[0]) diff --git a/deeplabcut/utils/auxfun_models.py b/deeplabcut/utils/auxfun_models.py index 05341c6ab5..5aeb98820d 100644 --- a/deeplabcut/utils/auxfun_models.py +++ b/deeplabcut/utils/auxfun_models.py @@ -20,8 +20,8 @@ import os from pathlib import Path -from deeplabcut.utils import auxiliaryfunctions +from deeplabcut.utils import auxiliaryfunctions # This dictionary maps the model types to the file locations where the models exist. MODEL_BASE_PATH = Path("pose_estimation_tensorflow") / "models" / "pretrained" @@ -75,8 +75,8 @@ def download_weights(modeltype, model_path): """ Downloads the ImageNet pretrained weights for ResNets, MobileNets et al. from TensorFlow... """ - import urllib import tarfile + import urllib from io import BytesIO target_dir = model_path.parents[0] @@ -87,7 +87,7 @@ def download_weights(modeltype, model_path): url = url + modeltype.replace("_", "-") + ".tar.gz" else: url = neturls[modeltype] - print("Downloading a ImageNet-pretrained model from {}....".format(url)) + print(f"Downloading a ImageNet-pretrained model from {url}....") response = urllib.request.urlopen(url) with tarfile.open(fileobj=BytesIO(response.read()), mode="r:gz") as tar: tar.extractall(path=target_dir) @@ -100,8 +100,9 @@ def download_model(modelname, target_dir): """ Downloads a DeepLabCut Model Zoo Project """ - import urllib.request import tarfile + import urllib.request + from tqdm import tqdm def show_progress(count, block_size, total_size): @@ -132,7 +133,7 @@ def tarfilenamecutting(tarf): if modelname in neturls.keys(): url = neturls[modelname] response = urllib.request.urlopen(url) - print("Downloading the model from the DeepLabCut server @Harvard -> Go Crimson!!! {}....".format(url)) + print(f"Downloading the model from the DeepLabCut server @Harvard -> Go Crimson!!! {url}....") total_size = int(response.getheader("Content-Length")) pbar = tqdm(unit="B", total=total_size, position=0) filename, _ = urllib.request.urlretrieve(url, reporthook=show_progress) @@ -153,7 +154,9 @@ def set_visible_devices(gputouse: int): n_devices = len(physical_devices) if gputouse >= n_devices: raise ValueError( - f"There are {n_devices} available GPUs: {physical_devices}\nPlease choose `gputouse` in {list(range(n_devices))}." + f"There are {n_devices} available GPUs: {physical_devices}\nPlease choose `gputouse` in { + list(range(n_devices)) + }." ) tf.config.set_visible_devices(physical_devices[gputouse], "GPU") diff --git a/deeplabcut/utils/auxfun_multianimal.py b/deeplabcut/utils/auxfun_multianimal.py index 506897f0e5..d55baef648 100644 --- a/deeplabcut/utils/auxfun_multianimal.py +++ b/deeplabcut/utils/auxfun_multianimal.py @@ -31,9 +31,9 @@ import numpy as np import pandas as pd -from deeplabcut.utils import auxiliaryfunctions, conversioncode -from deeplabcut.generate_training_dataset import trainingsetmanipulation from deeplabcut.core.trackingutils import TRACK_METHODS +from deeplabcut.generate_training_dataset import trainingsetmanipulation +from deeplabcut.utils import auxiliaryfunctions, conversioncode def reorder_individuals_in_df(df: pd.DataFrame, order: list) -> pd.DataFrame: @@ -79,7 +79,8 @@ def get_track_method(cfg, track_method=""): track_method = cfg.get("default_track_method", "") if not track_method: warnings.warn( - "default_track_method` is undefined in the config.yaml file and will be set to `ellipse`." + "default_track_method` is undefined in the config.yaml file and will be set to `ellipse`.", + stacklevel=2, ) track_method = "ellipse" cfg["default_track_method"] = track_method @@ -278,7 +279,7 @@ def convert2_maDLC(config, userfeedback=True, forceindividual=None): folders = [] for folder in folders: - if userfeedback == True: + if userfeedback: print("Do you want to convert the annotation file in folder:", folder, "?") askuser = input("yes/no") else: @@ -363,7 +364,7 @@ def convert_single2multiplelegacyAM(config, userfeedback=True, target=None): prefixes, uniquebodyparts, multianimalbodyparts = extractindividualsandbodyparts(cfg) for folder in folders: - if userfeedback == True: + if userfeedback: print("Do you want to convert the annotation file in folder:", folder, "?") askuser = input("yes/no") else: diff --git a/deeplabcut/utils/auxfun_videos.py b/deeplabcut/utils/auxfun_videos.py index 60efcc1c98..c9ea80065e 100644 --- a/deeplabcut/utils/auxfun_videos.py +++ b/deeplabcut/utils/auxfun_videos.py @@ -19,16 +19,16 @@ Licensed under GNU Lesser General Public License v3.0 """ -import skimage.color -from skimage import io -from skimage.util import img_as_ubyte -import cv2 import datetime -import numpy as np import os import subprocess import warnings +import cv2 +import numpy as np +import skimage.color +from skimage import io +from skimage.util import img_as_ubyte # more videos are in principle covered, as OpenCV is used and allows many formats. SUPPORTED_VIDEOS = "avi", "mp4", "mov", "mpeg", "mpg", "mpv", "mkv", "flv", "qt", "yuv" @@ -41,7 +41,7 @@ def __init__(self, video_path): self.video_path = video_path self.video = cv2.VideoCapture(video_path) if not self.video.isOpened(): - raise IOError("Video could not be opened; it may be corrupted.") + raise OSError("Video could not be opened; it may be corrupted.") self.parse_metadata() self._bbox = 0, 1, 0, 1 self._n_frames_robust = None @@ -58,7 +58,7 @@ def check_integrity(self): command = f'ffmpeg -v error -i "{self.video_path}" -f null - 2>"{dest}"' subprocess.call(command, shell=True) if os.path.getsize(dest) != 0: - warnings.warn(f'Video contains errors. See "{dest}" for a detailed report.') + warnings.warn(f'Video contains errors. See "{dest}" for a detailed report.', stacklevel=2) def check_integrity_robust(self): numframes = self.video.get(cv2.CAP_PROP_FRAME_COUNT) @@ -66,7 +66,7 @@ def check_integrity_robust(self): while fr < numframes: success, frame = self.video.read() if not success or frame is None: - warnings.warn(f"Opencv failed to load frame {fr}. Use ffmpeg to re-encode video file") + warnings.warn(f"Opencv failed to load frame {fr}. Use ffmpeg to re-encode video file", stacklevel=2) fr += 1 @property @@ -110,7 +110,7 @@ def set_to_frame(self, ind): raise ValueError("Index must be a positive integer.") last_frame = len(self) - 1 if ind > last_frame: - warnings.warn("Index exceeds the total number of frames. Setting to last frame instead.") + warnings.warn("Index exceeds the total number of frames. Setting to last frame instead.", stacklevel=2) ind = last_frame self.video.set(cv2.CAP_PROP_POS_FRAMES, ind) @@ -155,7 +155,7 @@ def set_bbox(self, x1, x2, y1, y2, relative=False): y2 /= self._height bbox = x1, x2, y1, y2 if any(coord > 1 for coord in bbox): - warnings.warn("Bounding box larger than the video... Clipping to video dimensions.") + warnings.warn("Bounding box larger than the video... Clipping to video dimensions.", stacklevel=2) bbox = tuple(map(lambda x: min(x, 1), bbox)) self._bbox = bbox @@ -186,7 +186,7 @@ def dimensions(self): def parse_metadata(self): self._n_frames = int(self.video.get(cv2.CAP_PROP_FRAME_COUNT)) if self._n_frames >= 1e9: - warnings.warn("The video has more than 10^9 frames, we recommend chopping it up.") + warnings.warn("The video has more than 10^9 frames, we recommend chopping it up.", stacklevel=2) self._width = int(self.video.get(cv2.CAP_PROP_FRAME_WIDTH)) self._height = int(self.video.get(cv2.CAP_PROP_FRAME_HEIGHT)) self._fps = round(self.video.get(cv2.CAP_PROP_FPS), 2) @@ -197,7 +197,7 @@ def close(self): class VideoWriter(VideoReader): def __init__(self, video_path, codec="h264", dpi=100, fps=None): - super(VideoWriter, self).__init__(video_path) + super().__init__(video_path) self.codec = codec self.dpi = dpi if fps: @@ -269,9 +269,12 @@ def split(self, n_splits, suffix="split", dest_folder=None): raise ValueError("The video should at least be split in half.") chunk_dur = self.calc_duration() / n_splits splits = np.arange(n_splits + 1) * chunk_dur - time_formatter = lambda val: str(datetime.timedelta(seconds=val)) + + def time_formatter(val): + return str(datetime.timedelta(seconds=val)) + clips = [] - for n, (start, end) in enumerate(zip(splits, splits[1:]), start=1): + for n, (start, end) in enumerate(zip(splits, splits[1:], strict=False), start=1): clips.append( self.shorten( time_formatter(start), @@ -326,7 +329,7 @@ def rescale( angle = np.deg2rad(angle) command = command.format(f", rotate={angle}") elif rotatecw == "Yes": - command = command.format(f", transpose=1") + command = command.format(", transpose=1") else: command = command.format("") subprocess.call(command, shell=True) @@ -581,7 +584,7 @@ def rotate_video(vname, angle, rotatecw="Arbitrary", outsuffix="rotated", outpat def draw_bbox(video): import matplotlib.pyplot as plt - from matplotlib.widgets import RectangleSelector, Button + from matplotlib.widgets import Button, RectangleSelector clip = VideoWriter(video) frame = None @@ -613,7 +616,7 @@ def display_help(*args): help_button = Button(ax_help, "Help") help_button.on_clicked(display_help) - rs = RectangleSelector( + RectangleSelector( ax, line_select_callback, minspanx=5, diff --git a/deeplabcut/utils/auxiliaryfunctions.py b/deeplabcut/utils/auxiliaryfunctions.py index 0a93303b3f..5d20b52be7 100644 --- a/deeplabcut/utils/auxiliaryfunctions.py +++ b/deeplabcut/utils/auxiliaryfunctions.py @@ -21,13 +21,10 @@ from __future__ import annotations import os -import typing import pickle import warnings from pathlib import Path -from typing import List -import numpy as np import pandas as pd import ruamel.yaml.representer import yaml @@ -35,7 +32,7 @@ from deeplabcut.core.engine import Engine from deeplabcut.core.trackingutils import TRACK_METHODS -from deeplabcut.utils import auxfun_videos, auxfun_multianimal +from deeplabcut.utils import auxfun_multianimal, auxfun_videos def create_config_template(multianimal=False): @@ -206,7 +203,7 @@ def read_config(configname): path = Path(configname) if os.path.exists(path): try: - with open(path, "r") as f: + with open(path) as f: cfg = ruamelFile.load(f) curr_dir = str(Path(configname).parent.resolve()) @@ -226,7 +223,7 @@ def read_config(configname): except Exception as err: if len(err.args) > 2: if err.args[2] == "could not determine a constructor for the tag '!!python/tuple'": - with open(path, "r") as ymlfile: + with open(path) as ymlfile: cfg = yaml.load(ymlfile, Loader=yaml.SafeLoader) write_config(configname, cfg) else: @@ -249,7 +246,7 @@ def write_config(configname, cfg): cfg_file[key] = cfg[key] # Adding default value for variable skeleton and skeleton_color for backward compatibility. - if not "skeleton" in cfg.keys(): + if "skeleton" not in cfg.keys(): cfg_file["skeleton"] = [] cfg_file["skeleton_color"] = "black" # Use a very large width so long strings (e.g., file paths or keys with spaces) @@ -292,14 +289,14 @@ def edit_config(configname, edits, output_name=""): try: write_plainconfig(output_name, cfg) except ruamel.yaml.representer.RepresenterError: - warnings.warn("Some edits could not be written. The configuration file will be left unchanged.") + warnings.warn("Some edits could not be written. The configuration file will be left unchanged.", stacklevel=2) for key in edits: cfg.pop(key) write_plainconfig(output_name, cfg) return cfg -def get_bodyparts(cfg: dict) -> typing.List[str]: +def get_bodyparts(cfg: dict) -> list[str]: """ Args: cfg: a project configuration file @@ -317,7 +314,7 @@ def get_bodyparts(cfg: dict) -> typing.List[str]: return cfg["bodyparts"] -def get_unique_bodyparts(cfg: dict) -> typing.List[str]: +def get_unique_bodyparts(cfg: dict) -> list[str]: """ Args: cfg: a project configuration file @@ -392,10 +389,10 @@ def write_pickle(filename, data): def get_list_of_videos( - videos: typing.Union[typing.List[str], str], - videotype: typing.Union[typing.List[str], str] = "", + videos: list[str] | str, + videotype: list[str] | str = "", in_random_order: bool = True, -) -> typing.List[str]: +) -> list[str]: """Returns list of videos of videotype "videotype" in folder videos or for list of videos. @@ -499,7 +496,7 @@ def filter_files_by_patterns( start_patterns: set[str] | None = None, contain_patterns: set[str] | None = None, end_patterns: set[str] | None = None, -) -> List[Path]: +) -> list[Path]: """ Filters files in a folder based on start, contain, and end patterns. @@ -655,7 +652,7 @@ def get_evaluation_folder( ) -def get_snapshots_from_folder(train_folder: Path) -> List[str]: +def get_snapshots_from_folder(train_folder: Path) -> list[str]: """ Returns an ordered list of existing snapshot names in the train folder, sorted by increasing training iterations. diff --git a/deeplabcut/utils/auxiliaryfunctions_3d.py b/deeplabcut/utils/auxiliaryfunctions_3d.py index 54ae08196f..f563c9bbd0 100644 --- a/deeplabcut/utils/auxiliaryfunctions_3d.py +++ b/deeplabcut/utils/auxiliaryfunctions_3d.py @@ -91,7 +91,7 @@ def compute_triangulation_calibration_images( triangulate = np.asanyarray(triangulate) # Plotting - if plot == True: + if plot: col = colormap(np.linspace(0, 1, triangulate.shape[0])) fig = plt.figure() ax = fig.add_subplot(111, projection="3d") @@ -134,7 +134,7 @@ def get_camerawise_videos(path, cam_names, videotype): file_to_exclude = str("labeled" + videotype) else: file_to_exclude = str("labeled." + videotype) - videos = [v for v in videos if os.path.isfile(v) and not (file_to_exclude in v)] + videos = [v for v in videos if os.path.isfile(v) and file_to_exclude not in v] video_list = [] cam = cam_names[0] # camera1 vid.append( @@ -203,7 +203,8 @@ def Get_list_of_triangulated_and_videoFiles(filepath, videotype, scorer_3d, cam_ if filename[i][0] == "_" or filename[i][0] == "-": filename[i] = filename[i][1:] - # Get the suffix and prefix of the video filenames so that they can be used for matching the triangulated file names. + # Get the suffix and prefix of the video filenames so that they can be + # used for matching the triangulated file names. for i in range(len(video_list)): pre = [ str(Path(video_list[i][0]).stem).split(cam_names[0])[0], @@ -225,7 +226,8 @@ def Get_list_of_triangulated_and_videoFiles(filepath, videotype, scorer_3d, cam_ suffix.append(suf) prefix.append(pre) - # Match the suffix and prefix with the triangulated file name and return the list with triangulated file and corresponding video files. + # Match the suffix and prefix with the triangulated file name and return + # the list with triangulated file and corresponding video files. for k in range(len(filename)): for j in range(len(prefix)): if (prefix[j][0] in filename[k] and prefix[j][1] in filename[k]) and ( @@ -314,7 +316,7 @@ def _associate_paired_view_tracks(tracklets1, tracklets2, F): costs[i, j] = cost match_inds = linear_sum_assignment(np.abs(costs)) - voting = dict(zip(*match_inds)) + voting = dict(zip(*match_inds, strict=False)) return costs, voting diff --git a/deeplabcut/utils/conversioncode.py b/deeplabcut/utils/conversioncode.py index 5227151e7a..ab0345dca6 100644 --- a/deeplabcut/utils/conversioncode.py +++ b/deeplabcut/utils/conversioncode.py @@ -19,11 +19,12 @@ """ import os -import pandas as pd -from deeplabcut.utils import auxiliaryfunctions from itertools import islice from pathlib import Path +import pandas as pd + +from deeplabcut.utils import auxiliaryfunctions SUPPORTED_FILETYPES = "csv", "nwb" diff --git a/deeplabcut/utils/make_labeled_video.py b/deeplabcut/utils/make_labeled_video.py index 1d536a1a47..d2a524297a 100644 --- a/deeplabcut/utils/make_labeled_video.py +++ b/deeplabcut/utils/make_labeled_video.py @@ -30,10 +30,10 @@ # Dependencies #################################################### import os.path +from collections.abc import Callable, Iterable from functools import partial -from multiprocessing import get_start_method, Pool +from multiprocessing import Pool, get_start_method from pathlib import Path -from typing import Callable, Iterable, List, Optional, Union import matplotlib.colors as mcolors import matplotlib.pyplot as plt @@ -42,7 +42,7 @@ from matplotlib import patches from matplotlib.animation import FFMpegWriter from matplotlib.collections import LineCollection -from skimage.draw import disk, line_aa, set_color, rectangle_perimeter +from skimage.draw import disk, line_aa, rectangle_perimeter, set_color from skimage.util import img_as_ubyte from tqdm import trange @@ -63,7 +63,8 @@ def get_segment_indices(bodyparts2connect, all_bpts): *( np.flatnonzero(all_bpts == bpt1), np.flatnonzero(all_bpts == bpt2), - ) + ), + strict=False, ) ) return bpts2connect @@ -113,8 +114,8 @@ def CreateVideo( nframes = clip.nframes duration = nframes / fps - print("Duration of video [s]: {}, recorded with {} fps!".format(round(duration, 2), round(fps, 2))) - print("Overall # of frames: {} with cropped frame dimensions: {} {}".format(nframes, nx, ny)) + print(f"Duration of video [s]: {round(duration, 2)}, recorded with {round(fps, 2)} fps!") + print(f"Overall # of frames: {nframes} with cropped frame dimensions: {nx} {ny}") print("Generating frames and creating video.") df_x, df_y, df_likelihood = Dataframe.values.reshape((len(Dataframe), -1, 3)).T @@ -259,8 +260,8 @@ def CreateVideoSlow( nframes = clip.nframes duration = nframes / fps - print("Duration of video [s]: {}, recorded with {} fps!".format(round(duration, 2), round(fps, 2))) - print("Overall # of frames: {} with cropped frame dimensions: {} {}".format(nframes, nx, ny)) + print(f"Duration of video [s]: {round(duration, 2)}, recorded with {round(fps, 2)} fps!") + print(f"Overall # of frames: {nframes} with cropped frame dimensions: {nx} {ny}") print("Generating frames and creating video.") df_x, df_y, df_likelihood = Dataframe.values.reshape((len(Dataframe), -1, 3)).T if cropping and not displaycropped: @@ -385,7 +386,7 @@ def CreateVideoSlow( writer.grab_frame() ax.clear() - print("Labeled video {} successfully created.".format(videooutname)) + print(f"Labeled video {videooutname} successfully created.") plt.switch_backend(prev_backend) @@ -414,16 +415,16 @@ def create_labeled_video( track_method: str = "", superanimal_name: str = "", pcutoff: float | None = None, - skeleton: list = [], + skeleton: list = None, skeleton_color: str = "white", dotsize: int = 8, colormap: str = "rainbow", alphavalue: float = 0.5, overwrite: bool = False, - confidence_to_alpha: Union[bool, Callable[[float], float]] = False, + confidence_to_alpha: bool | Callable[[float], float] = False, plot_bboxes: bool = True, bboxes_pcutoff: float | None = None, - max_workers: Optional[int] = None, + max_workers: int | None = None, **kwargs, ): """Labels the bodyparts in a video. @@ -629,6 +630,8 @@ def create_labeled_video( videotype='mp4', ) """ + if skeleton is None: + skeleton = [] if config == "": if pcutoff is None: pcutoff = 0.6 @@ -802,7 +805,7 @@ def proc_video( video, init_weights="", pcutoff: float | None = None, - confidence_to_alpha: Optional[Callable[[float], float]] = None, + confidence_to_alpha: Callable[[float], float] | None = None, plot_bboxes: bool = True, bboxes_pcutoff: float = 0.6, ): @@ -829,7 +832,7 @@ def proc_video( auxiliaryfunctions.attempt_to_make_folder(destfolder) os.chdir(destfolder) # THE VIDEO IS STILL IN THE VIDEO FOLDER - print("Starting to process video: {}".format(video)) + print(f"Starting to process video: {video}") vname = str(Path(video).stem) if init_weights != "": @@ -847,7 +850,7 @@ def proc_video( print(f"Labeled video {vname} already created.") return True else: - print("Loading {} and data.".format(video)) + print(f"Loading {video} and data.") try: df, filepath, _, _ = auxiliaryfunctions.load_analyzed_data( destfolder, vname, DLCscorer, filtered, track_method @@ -1089,14 +1092,14 @@ def create_video_with_keypoints_only( xyp = df.values.reshape((n_frames, -1, 3)) if color_by == "bodypart": - map_ = bodyparts.map(dict(zip(bodypart_names, range(n_bodyparts)))) + map_ = bodyparts.map(dict(zip(bodypart_names, range(n_bodyparts), strict=False))) cmap = plt.get_cmap(colormap, n_bodyparts) elif color_by == "individual": try: individuals = df.columns.get_level_values("individuals")[::3] individual_names = individuals.unique().to_list() n_individuals = len(individual_names) - map_ = individuals.map(dict(zip(individual_names, range(n_individuals)))) + map_ = individuals.map(dict(zip(individual_names, range(n_individuals), strict=False))) cmap = plt.get_cmap(colormap, n_individuals) except KeyError as e: raise Exception("Coloring by individuals is only valid for multi-animal data") from e @@ -1113,7 +1116,7 @@ def create_video_with_keypoints_only( scat.set_offsets(coords) colors = cmap(map_) scat.set_color(colors) - segs = coords[tuple(zip(*tuple(ind_links))), :].swapaxes(0, 1) if ind_links else [] + segs = coords[tuple(zip(*tuple(ind_links), strict=False)), :].swapaxes(0, 1) if ind_links else [] coll = LineCollection(segs, colors=skeleton_color, alpha=alpha) ax.add_collection(coll) ax.set_xlim(0, nx) @@ -1131,7 +1134,7 @@ def create_video_with_keypoints_only( coords[xyp[index, :, 2] < pcutoff] = np.nan scat.set_offsets(coords) if ind_links: - segs = coords[tuple(zip(*tuple(ind_links))), :].swapaxes(0, 1) + segs = coords[tuple(zip(*tuple(ind_links), strict=False)), :].swapaxes(0, 1) coll.set_segments(segs) writer.grab_frame() plt.close(fig) @@ -1145,10 +1148,10 @@ def create_video_with_all_detections( shuffle=1, trainingsetindex=0, displayedbodyparts="all", - cropping: Optional[List[int]] = None, + cropping: list[int] | None = None, destfolder=None, modelprefix="", - confidence_to_alpha: Union[bool, Callable[[float], float]] = False, + confidence_to_alpha: bool | Callable[[float], float] = False, plot_bboxes: bool = True, **kwargs, ): @@ -1225,7 +1228,7 @@ def create_video_with_all_detections( videofolder = os.path.splitext(video)[0] if destfolder is None: - outputname = "{}_full.mp4".format(videofolder + DLCscorername) + outputname = f"{videofolder + DLCscorername}_full.mp4" full_pickle = os.path.join(videofolder + DLCscorername + "_full.pickle") else: auxiliaryfunctions.attempt_to_make_folder(destfolder) @@ -1408,7 +1411,7 @@ def create_video_from_pickled_tracks(video, pickle_file, destfolder="", output_n def _get_default_conf_to_alpha( confidence_to_alpha: bool, pcutoff: float, -) -> Optional[Callable[[float], float]]: +) -> Callable[[float], float] | None: """Creates the default confidence_to_alpha function""" if not confidence_to_alpha: return None diff --git a/deeplabcut/utils/plotting.py b/deeplabcut/utils/plotting.py index 464de8d131..6453af5837 100644 --- a/deeplabcut/utils/plotting.py +++ b/deeplabcut/utils/plotting.py @@ -22,20 +22,20 @@ import argparse import os -import pickle -import pandas as pd #################################################### # Dependencies #################################################### import os.path +import pickle from pathlib import Path import matplotlib.pyplot as plt import numpy as np +import pandas as pd from deeplabcut.core import crossvalutils -from deeplabcut.utils import auxiliaryfunctions, auxfun_multianimal, visualization +from deeplabcut.utils import auxfun_multianimal, auxiliaryfunctions, visualization def Histogram(vector, color, bins, ax=None, linewidth=1.0): diff --git a/deeplabcut/utils/pseudo_label.py b/deeplabcut/utils/pseudo_label.py index dac2f85d5e..d9a42fc915 100644 --- a/deeplabcut/utils/pseudo_label.py +++ b/deeplabcut/utils/pseudo_label.py @@ -50,7 +50,6 @@ def xywh2xyxy(bbox): def optimal_match(gts_list, preds_list): - arranged_preds_list = [] num_gts = len(gts_list) num_preds = len(preds_list) cost_matrix = np.zeros((num_gts, num_preds)) @@ -98,7 +97,7 @@ def video_to_frames(input_video, output_folder, cropping: list[int] | None = Non # Create the output folder if it doesn't exist video = cv2.VideoCapture(str(input_video)) # Get the frames per second (fps) of the video - fps = int(video.get(cv2.CAP_PROP_FPS)) + int(video.get(cv2.CAP_PROP_FPS)) # Initialize a frame counter frame_count = 0 while True: @@ -127,7 +126,7 @@ def plot_cost_matrix(matrix, gt_keypoint_names, pred_keypoint_names, conversion_ matrix /= np.max(matrix) fig, ax = plt.subplots() - heatmap = ax.pcolor(matrix, cmap=plt.cm.Blues, vmin=0, vmax=1) + ax.pcolor(matrix, cmap=plt.cm.Blues, vmin=0, vmax=1) ax.set_xticks(np.arange(matrix.shape[1]) + 0.5, minor=False) ax.set_yticks(np.arange(matrix.shape[0]) + 0.5, minor=False) ax.set_xlim(0, int(matrix.shape[1])) @@ -216,7 +215,7 @@ def keypoint_matching( detector_path=detector_path, ) - with open(train_file_path, "r") as f: + with open(train_file_path) as f: train_obj = json.load(f) images = train_obj["images"] @@ -262,7 +261,7 @@ def keypoint_matching( images = corresponded_images bbox_gts = [{"bboxes": np.array(image_name_to_bbox[image.split(os.sep)[-1]])} for image in images] - pose_inputs = list(zip(images, bbox_gts)) + pose_inputs = list(zip(images, bbox_gts, strict=False)) # pose inference should return meta data for pseudo labeling predictions = pose_runner.inference(pose_inputs) @@ -273,7 +272,7 @@ def keypoint_matching( assert len(images) == len(predictions) image_name_to_pred = {} - for image_path, prediction in zip(images, predictions): + for image_path, prediction in zip(images, predictions, strict=False): name = image_path.split(os.sep)[-1] image_name_to_pred[name] = prediction @@ -304,7 +303,7 @@ def keypoint_matching( pair_distance = cdist(matched_pred, matched_gt) row_ind, column_ind = linear_sum_assignment(pair_distance) - for row, column in zip(row_ind, column_ind): + for row, column in zip(row_ind, column_ind, strict=False): pred_kpt_name = pred_keypoint_names[row] anno_kpt_name = gt_keypoint_names[column] match_matrix[row][column] += 1 @@ -317,7 +316,7 @@ def keypoint_matching( plot_cost_matrix(match_matrix, gt_keypoint_names, pred_keypoint_names, conversion_matrix_out_path) - for row, column in zip(row_ind, column_ind): + for row, column in zip(row_ind, column_ind, strict=False): pred_kpt_name = pred_keypoint_names[row] anno_kpt_name = gt_keypoint_names[column] count = match_dict[pred_kpt_name][anno_kpt_name] @@ -394,13 +393,15 @@ def dlc3predictions_2_annotation_from_video( # skipping every 10 frames should speed up and not impact the performance predictions, image_paths = predictions[::10], image_paths[::10] - # Since the inference API does not return the image path, I assume the predictions are provided in the same order as the frames in the video. - assert len(image_paths) == len(predictions), ( - f"number of images must be equal to number of predictions. image_paths: {len(image_paths)} , predictions: {len(predictions)}" - ) - new_predictions = [] + # Since the inference API does not return the image path, I assume the + # predictions are provided in the same order as the frames in the video. + assert len(image_paths) == len( + predictions + ), f"number of images must be equal to number of predictions. image_paths: {len(image_paths)} , predictions: { + len(predictions) + }" - num_kpts = len(bodyparts) + len(bodyparts) if not superanimal_name.startswith("superanimal_"): raise ValueError("not supporting non superanimal model video adaptation yet") @@ -417,7 +418,7 @@ def dlc3predictions_2_annotation_from_video( assert len(predictions) == len(image_paths) imageid2annotations = defaultdict(list) - for image_id, (prediction, image_path) in enumerate(zip(predictions, image_paths)): + for image_id, (prediction, image_path) in enumerate(zip(predictions, image_paths, strict=False)): image_obj = cv2.imread(image_path) height, width, channels = image_obj.shape imagename = image_path.split(os.sep)[-1] @@ -431,7 +432,9 @@ def dlc3predictions_2_annotation_from_video( # iterate through individuals if there are many assert len(prediction["bodyparts"]) == len(prediction["bboxes"]) == len(prediction["bbox_scores"]) - for pose, bbox, bbox_score in zip(prediction["bodyparts"], prediction["bboxes"], prediction["bbox_scores"]): + for pose, bbox, bbox_score in zip( + prediction["bodyparts"], prediction["bboxes"], prediction["bbox_scores"], strict=False + ): if np.all(np.array(pose) <= 0) or len(bbox) == 0 or bbox_score < bbox_threshold: continue imageid2annotations[image_id].append(pose) @@ -444,7 +447,7 @@ def dlc3predictions_2_annotation_from_video( # by default all visible pose[:, -1] = 2 - bbox_confidence = bbox[-1] + bbox[-1] keypoints = list(pose.reshape(-1)) keypoints = [float(num) for num in keypoints] @@ -471,8 +474,6 @@ def dlc3predictions_2_annotation_from_video( train_obj = {"images": images, "annotations": annotations, "categories": categories} - test_annotations = [] - # just use the first 10 image annotations for test test_obj = { "images": images[:10], diff --git a/deeplabcut/utils/skeleton.py b/deeplabcut/utils/skeleton.py index 7622bda82d..77635d4c74 100644 --- a/deeplabcut/utils/skeleton.py +++ b/deeplabcut/utils/skeleton.py @@ -66,7 +66,7 @@ def __init__(self, config_path): found = True break if self.df is None: - raise IOError("No labeled data were found.") + raise OSError("No labeled data were found.") self.bpts = self.df.columns.get_level_values("bodyparts").unique() if not found: @@ -149,7 +149,7 @@ def export(self, *args): unconnected = [i for i in range(len(self.xy)) if i not in inds_flat] if len(unconnected): warnings.warn( - f"You didn't connect all the bodyparts (which is fine!). This is just a note to let you know." + "You didn't connect all the bodyparts (which is fine!). This is just a note to let you know." ) self.cfg["skeleton"] = [tuple(self.bpts[list(pair)]) for pair in self.inds] write_config(self.config_path, self.cfg) diff --git a/deeplabcut/utils/video_processor.py b/deeplabcut/utils/video_processor.py index 72851eb786..3bb009e78c 100644 --- a/deeplabcut/utils/video_processor.py +++ b/deeplabcut/utils/video_processor.py @@ -25,7 +25,7 @@ import numpy as np -class VideoProcessor(object): +class VideoProcessor: """ Base class for a video processing unit, implementation is required for video loading and saving @@ -128,7 +128,7 @@ class VideoProcessorCV(VideoProcessor): """ def __init__(self, *args, **kwargs): - super(VideoProcessorCV, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def get_video(self): return cv2.VideoCapture(self.fname) diff --git a/deeplabcut/utils/visualization.py b/deeplabcut/utils/visualization.py index d50bb85e28..33d90990b7 100644 --- a/deeplabcut/utils/visualization.py +++ b/deeplabcut/utils/visualization.py @@ -23,16 +23,16 @@ import os from pathlib import Path +import matplotlib.patches as patches import matplotlib.pyplot as plt import numpy as np import pandas as pd from matplotlib.collections import LineCollection from matplotlib.colors import Colormap -import matplotlib.patches as patches -from skimage import io, color +from skimage import color, io from tqdm import trange -from deeplabcut.utils import auxiliaryfunctions, auxfun_videos +from deeplabcut.utils import auxfun_videos, auxiliaryfunctions def get_cmap(n: int, name: str = "hsv") -> Colormap: @@ -57,12 +57,14 @@ def make_labeled_image( bodyparts, colors, cfg, - labels=["+", ".", "x"], + labels=None, scaling=1, ax=None, ): """Creating a labeled image with the original human labels, as well as the DeepLabCut's!""" + if labels is None: + labels = ["+", ".", "x"] alphavalue = cfg["alphavalue"] # .5 dotsize = cfg["dotsize"] # =15 @@ -73,7 +75,7 @@ def make_labeled_image( h, w = np.shape(frame) _, ax = prepare_figure_axes(w, h, scaling) ax.imshow(frame, "gray") - for scorerindex, loopscorer in enumerate(Scorers): + for _scorerindex, loopscorer in enumerate(Scorers): for bpindex, bp in enumerate(bodyparts): if np.isfinite( DataCombined[loopscorer][bp]["y"].iloc[imagenr] + DataCombined[loopscorer][bp]["x"].iloc[imagenr] @@ -123,7 +125,7 @@ def make_multianimal_labeled_image( dotsize: float | int = 12, alphavalue: float = 0.7, pcutoff: float = 0.6, - labels: list = ["+", ".", "x"], + labels: list = None, ax: plt.Axes | None = None, bounding_boxes: tuple[np.ndarray, np.ndarray] | None = None, bboxes_cutoff: float = 0.6, @@ -154,13 +156,15 @@ def make_multianimal_labeled_image( matplotlib Axes object with plotted labels and predictions. """ + if labels is None: + labels = ["+", ".", "x"] if ax is None: h, w, _ = np.shape(frame) _, ax = prepare_figure_axes(w, h) ax.imshow(frame, "gray") if bounding_boxes is not None: - for i, (bbox, bbox_score) in enumerate(zip(bounding_boxes[0], bounding_boxes[1])): + for i, (bbox, bbox_score) in enumerate(zip(bounding_boxes[0], bounding_boxes[1], strict=False)): bbox_origin = (bbox[0], bbox[1]) (bbox_width, bbox_height) = (bbox[2], bbox[3]) if isinstance(bboxes_color, Colormap): @@ -180,7 +184,7 @@ def make_multianimal_labeled_image( ) ax.add_patch(rectangle) - for n, data in enumerate(zip(coords_truth, coords_pred, probs_pred)): + for n, data in enumerate(zip(coords_truth, coords_pred, probs_pred, strict=False)): color = colors(n) coord_gt, coord_pred, prob_pred = data @@ -334,7 +338,7 @@ def make_labeled_images_from_dataframe( draw_skeleton = draw_skeleton and cfg["skeleton"] # Only draw if a skeleton is defined if color_by == "bodypart": - map_ = bodyparts.map(dict(zip(bodypart_names, range(nbodyparts)))) + map_ = bodyparts.map(dict(zip(bodypart_names, range(nbodyparts), strict=False))) cmap = get_cmap(nbodyparts, cfg["colormap"]) colors = cmap(map_) elif color_by == "individual": @@ -343,7 +347,7 @@ def make_labeled_images_from_dataframe( individual_names = individuals.unique().to_list() nindividuals = len(individual_names) individuals = individuals[::2] - map_ = individuals.map(dict(zip(individual_names, range(nindividuals)))) + map_ = individuals.map(dict(zip(individual_names, range(nindividuals), strict=False))) cmap = get_cmap(nindividuals, cfg["colormap"]) colors = cmap(map_) except KeyError as e: @@ -360,8 +364,8 @@ def make_labeled_images_from_dataframe( match1.append(j) elif bp == bp2: match2.append(j) - bones.extend(zip(match1, match2)) - ind_bones = tuple(zip(*bones)) + bones.extend(zip(match1, match2, strict=False)) + ind_bones = tuple(zip(*bones, strict=False)) images_list = [os.path.join(cfg["project_path"], *tuple_) for tuple_ in df.index.tolist()] if not destfolder: @@ -396,7 +400,7 @@ def make_labeled_images_from_dataframe( if img.ndim == 2 or img.shape[-1] == 1: img = color.gray2rgb(ic[i]) im.set_data(img) - for pt, coord in zip(pts, coords): + for pt, coord in zip(pts, coords, strict=False): pt.set_data(*np.expand_dims(coord, axis=1)) if ind_bones: coll.set_segments(segs[ind]) @@ -417,7 +421,7 @@ def make_labeled_images_from_dataframe( h, w = image.shape[:2] fig, ax = prepare_figure_axes(w, h, scale, dpi) ax.imshow(image) - for coord, c in zip(coords, colors): + for coord, c in zip(coords, colors, strict=False): ax.plot(*coord, keypoint, ms=s, alpha=alpha, color=c) if ind_bones: coll = LineCollection(segs[ind], colors=cfg["skeleton_color"], alpha=alpha) @@ -503,7 +507,7 @@ def plot_evaluation_results( try: ground_truth = df_gt.to_numpy().reshape((individuals, bodyparts, 2)) predictions = df_predictions.to_numpy().reshape((individuals, bodyparts, 3)) - except ValueError as e: + except ValueError: # Handle cases where the actual data size doesn't match expected shape actual_size_gt = df_gt.size actual_size_pred = df_predictions.size @@ -514,7 +518,7 @@ def plot_evaluation_results( print(f" Expected: {individuals} individuals, {bodyparts} bodyparts") print(f" Ground truth: {actual_size_gt} elements (expected {expected_size_gt})") print(f" Predictions: {actual_size_pred} elements (expected {expected_size_pred})") - print(f" Skipping visualization for this image") + print(" Skipping visualization for this image") continue bboxes = bounding_boxes.get(row_index) @@ -528,7 +532,7 @@ def plot_evaluation_results( unique_predictions = ( row_unique[model_name].to_numpy().reshape((unique_individuals, unique_bodyparts, 3)) ) - except ValueError as e: + except ValueError: # Handle cases where unique bodyparts reshape fails print(f"Warning: Unique bodyparts reshape failed for {image}, skipping unique bodyparts") plot_unique_bodyparts = False diff --git a/docker/deeplabcut_docker.py b/docker/deeplabcut_docker.py index 0482fa0ae4..05753fc067 100644 --- a/docker/deeplabcut_docker.py +++ b/docker/deeplabcut_docker.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- """ DeepLabCut2.0-2.2 Toolbox (deeplabcut.org) © A. & M. Mathis Labs diff --git a/docs/recipes/flip_and_rotate.ipynb b/docs/recipes/flip_and_rotate.ipynb index c545e871be..32b44d41c5 100644 --- a/docs/recipes/flip_and_rotate.ipynb +++ b/docs/recipes/flip_and_rotate.ipynb @@ -85,7 +85,7 @@ "config_path = \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml\"\n", "\n", "# import tools for modifying our config file\n", - "from deeplabcut.utils.auxiliaryfunctions import read_config, edit_config" + "from deeplabcut.utils.auxiliaryfunctions import edit_config, read_config" ] }, { @@ -489,7 +489,7 @@ "# Get train and test pose config file paths from base project, for each shuffle\n", "list_base_train_pose_config_file_paths = []\n", "list_base_test_pose_config_file_paths = []\n", - "for shuffle_number, trainingsetindex in zip(shuffles, trainingsetindices):\n", + "for shuffle_number, trainingsetindex in zip(shuffles, trainingsetindices, strict=False):\n", " base_train_pose_config_file_path_TEMP, base_test_pose_config_file_path_TEMP, _ = (\n", " deeplabcut.return_train_network_path(config_path, shuffle=shuffle_number, trainingsetindex=trainingsetindex)\n", " ) # base_train_pose_config_file\n", @@ -497,7 +497,7 @@ " list_base_test_pose_config_file_paths.append(base_test_pose_config_file_path_TEMP)\n", "\n", "# Create subdirs for this augmentation method\n", - "model_prefix = \"_\".join([modelprefix_pre, \"{0:0=2d}\".format(model_number), daug_str]) # modelprefix_pre = aug_\n", + "model_prefix = \"_\".join([modelprefix_pre, f\"{model_number:0=2d}\", daug_str]) # modelprefix_pre = aug_\n", "aug_project_path = os.path.join(project_path, model_prefix)\n", "aug_dlc_models = os.path.join(\n", " aug_project_path,\n", @@ -512,7 +512,7 @@ " print(\"Skipping this one as it already exists\")\n", "\n", "# Copy base train pose config file to the directory of this augmentation method\n", - "for j, (shuffle, trainingsetindex) in enumerate(zip(shuffles, trainingsetindices)):\n", + "for j, (shuffle, trainingsetindex) in enumerate(zip(shuffles, trainingsetindices, strict=False)):\n", " one_train_pose_config_file_path, one_test_pose_config_file_path, _ = deeplabcut.return_train_network_path(\n", " config_path, shuffle=shuffle, trainingsetindex=trainingsetindex, modelprefix=model_prefix\n", " )\n", @@ -563,7 +563,7 @@ " }\n", ")\n", "\n", - "for shuffle, trainingsetindex in zip(shuffles, trainingsetindices):\n", + "for shuffle, trainingsetindex in zip(shuffles, trainingsetindices, strict=False):\n", " one_train_pose_config_file_path, _, _ = deeplabcut.return_train_network_path(\n", " config_path, shuffle=shuffle, trainingsetindex=trainingsetindex, modelprefix=model_prefix\n", " )\n", @@ -603,7 +603,7 @@ "trainingsetindices = [0, 1, 2, 3]\n", "\n", "# loop over shuffles and train each\n", - "for shuffle, trainingsetindex in zip(shuffles, trainingsetindices):\n", + "for shuffle, trainingsetindex in zip(shuffles, trainingsetindices, strict=False):\n", " deeplabcut.train_network(\n", " config_path,\n", " shuffle=shuffle,\n", @@ -641,7 +641,7 @@ "\n", "config_path = \"/home/juser/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/config.yaml\"\n", "\n", - "from deeplabcut.utils.auxiliaryfunctions import read_config, edit_config\n", + "from deeplabcut.utils.auxiliaryfunctions import edit_config, read_config\n", "\n", "edit_config(config_path, {\"snapshotindex\": \"all\"})" ] @@ -670,7 +670,7 @@ "Shuffles = [1, 2, 3, 4]\n", "trainingsetindices = [0, 1, 2, 3]\n", "\n", - "for shuffle, trainingsetindex in zip(Shuffles, trainingsetindices):\n", + "for shuffle, trainingsetindex in zip(Shuffles, trainingsetindices, strict=False):\n", " deeplabcut.evaluate_network(\n", " config_path, modelprefix=model_prefix, Shuffles=[shuffle], trainingsetindex=trainingsetindex\n", " )" @@ -706,13 +706,13 @@ "trainingsetindices = [0, 1, 2, 3]\n", "\n", "# We need pandas for creatig a nice list to parse\n", - "import pandas as pd\n", - "\n", "import sys\n", "\n", + "import pandas as pd\n", + "\n", "sys.path.append(\"..\") # my python file for this function is stored in the parent folder as I'm running this\n", - "from getErrorDistribution import getErrorDistribution # import the getErrorDistribution function\n", "import numpy as np\n", + "from getErrorDistribution import getErrorDistribution # import the getErrorDistribution function\n", "\n", "# Read the h5 file containing all the frames, (project_folder/training-datasets/iteration-0/UnaufmentedDataSet_project_folder/CollectedData_LabelerName.h5)\n", "df = pd.read_hdf(\n", @@ -730,7 +730,7 @@ "error_distributions = []\n", "error_distributions_pcut = []\n", "\n", - "for shuffle, trainFractionIndex in zip(Shuffles, trainingsetindices):\n", + "for shuffle, trainFractionIndex in zip(Shuffles, trainingsetindices, strict=False):\n", " error_distributions_temp = []\n", " error_distributions_pcut_temp = []\n", " for snapshot in [0, 1, 2]: # we saved three snapshots, one at 50k iteratinos, one at 100k, and one at 150k\n", @@ -863,13 +863,13 @@ "trainingsetindices = [0, 1, 2, 3]\n", "\n", "# We need pandas for creatig a nice list to parse\n", - "import pandas as pd\n", - "\n", "import sys\n", "\n", + "import pandas as pd\n", + "\n", "sys.path.append(\"..\") # my python file for this function is stored in the parent folder as I'm running this\n", - "from getErrorDistribution import getErrorDistribution # import the getErrorDistribution function\n", "import numpy as np\n", + "from getErrorDistribution import getErrorDistribution # import the getErrorDistribution function\n", "\n", "# Read the h5 file containing all the frames, (project_folder/training-datasets/iteration-0/UnaufmentedDataSet_project_folder/CollectedData_LabelerName.h5)\n", "df = pd.read_hdf(\n", @@ -1069,7 +1069,7 @@ "model_prefix = \"data_augm_00_base\"\n", "\n", "# we only want to plot the last snapshot (150k iterations)\n", - "from deeplabcut.utils.auxiliaryfunctions import read_config, edit_config\n", + "from deeplabcut.utils.auxiliaryfunctions import edit_config, read_config\n", "\n", "edit_config(config_path, {\"snapshotindex\": -1})\n", "\n", @@ -1156,7 +1156,7 @@ "# Get train and test pose config file paths from base project, for each shuffle\n", "list_base_train_pose_config_file_paths = []\n", "list_base_test_pose_config_file_paths = []\n", - "for shuffle_number, trainingsetindex in zip(shuffles, trainingsetindices):\n", + "for shuffle_number, trainingsetindex in zip(shuffles, trainingsetindices, strict=False):\n", " base_train_pose_config_file_path_TEMP, base_test_pose_config_file_path_TEMP, _ = (\n", " deeplabcut.return_train_network_path(config_path, shuffle=shuffle_number, trainingsetindex=trainingsetindex)\n", " ) # base_train_pose_config_file\n", @@ -1164,7 +1164,7 @@ " list_base_test_pose_config_file_paths.append(base_test_pose_config_file_path_TEMP)\n", "\n", "# Create subdirs for this augmentation method\n", - "model_prefix = \"_\".join([modelprefix_pre, \"{0:0=2d}\".format(model_number), daug_str]) # modelprefix_pre = aug_\n", + "model_prefix = \"_\".join([modelprefix_pre, f\"{model_number:0=2d}\", daug_str]) # modelprefix_pre = aug_\n", "aug_project_path = os.path.join(project_path, model_prefix)\n", "aug_dlc_models = os.path.join(\n", " aug_project_path,\n", @@ -1179,7 +1179,7 @@ " print(\"Skipping this one as it already exists\")\n", "\n", "# Copy base train pose config file to the directory of this augmentation method\n", - "for j, (shuffle, trainingsetindex) in enumerate(zip(shuffles, trainingsetindices)):\n", + "for j, (shuffle, trainingsetindex) in enumerate(zip(shuffles, trainingsetindices, strict=False)):\n", " one_train_pose_config_file_path, one_test_pose_config_file_path, _ = deeplabcut.return_train_network_path(\n", " config_path, shuffle=shuffle, trainingsetindex=trainingsetindex, modelprefix=model_prefix\n", " )\n", @@ -1270,7 +1270,7 @@ "edits_dict[\"symmetric_pairs\"] = (0, 14), (1, 12), (2, 13), (3, 11), (4, 9), (5, 10)\n", "edits_dict[\"fliplr\"] = True\n", "\n", - "for shuffle, trainingsetindex in zip(shuffles, trainingsetindices):\n", + "for shuffle, trainingsetindex in zip(shuffles, trainingsetindices, strict=False):\n", " one_train_pose_config_file_path, _, _ = deeplabcut.return_train_network_path(\n", " config_path, shuffle=shuffle, trainingsetindex=trainingsetindex, modelprefix=model_prefix\n", " )\n", @@ -1311,7 +1311,7 @@ "trainingsetindices = [0, 1, 2, 3]\n", "\n", "# loop over shuffles and train each\n", - "for shuffle, trainingsetindex in zip(shuffles, trainingsetindices):\n", + "for shuffle, trainingsetindex in zip(shuffles, trainingsetindices, strict=False):\n", " deeplabcut.train_network(\n", " config_path,\n", " shuffle=shuffle,\n", @@ -1359,7 +1359,7 @@ "# make sure we are testing all snapshots\n", "edit_config(config_path, {\"snapshotindex\": \"all\"})\n", "\n", - "for shuffle, trainingsetindex in zip(Shuffles, trainingsetindices):\n", + "for shuffle, trainingsetindex in zip(Shuffles, trainingsetindices, strict=False):\n", " deeplabcut.evaluate_network(\n", " config_path, modelprefix=model_prefix, Shuffles=[shuffle], trainingsetindex=trainingsetindex\n", " )" @@ -1382,13 +1382,13 @@ "trainingsetindices = [3, 2]\n", "\n", "# We need pandas for creatig a nice list to parse\n", - "import pandas as pd\n", - "\n", "import sys\n", "\n", + "import pandas as pd\n", + "\n", "sys.path.append(\"..\") # my python file for this function is stored in the parent folder as I'm running this\n", - "from getErrorDistribution import getErrorDistribution # import the getErrorDistribution function\n", "import numpy as np\n", + "from getErrorDistribution import getErrorDistribution # import the getErrorDistribution function\n", "\n", "# Read the h5 file containing all the frames, (project_folder/training-datasets/iteration-0/UnaufmentedDataSet_project_folder/CollectedData_LabelerName.h5)\n", "df = pd.read_hdf(\n", @@ -1405,7 +1405,7 @@ "\n", "error_distributions_pcut = []\n", "\n", - "for shuffle, trainFractionIndex in zip(Shuffles, trainingsetindices):\n", + "for shuffle, trainFractionIndex in zip(Shuffles, trainingsetindices, strict=False):\n", " error_distributions_pcut_temp = []\n", " if shuffle == 4:\n", " model_prefix = model_prefix_base\n", @@ -1510,7 +1510,7 @@ "# Get train and test pose config file paths from base project, for each shuffle\n", "list_base_train_pose_config_file_paths = []\n", "list_base_test_pose_config_file_paths = []\n", - "for shuffle_number, trainingsetindex in zip(shuffles, trainingsetindices):\n", + "for shuffle_number, trainingsetindex in zip(shuffles, trainingsetindices, strict=False):\n", " base_train_pose_config_file_path_TEMP, base_test_pose_config_file_path_TEMP, _ = (\n", " deeplabcut.return_train_network_path(config_path, shuffle=shuffle_number, trainingsetindex=trainingsetindex)\n", " ) # base_train_pose_config_file\n", @@ -1518,7 +1518,7 @@ " list_base_test_pose_config_file_paths.append(base_test_pose_config_file_path_TEMP)\n", "\n", "# Create subdirs for this augmentation method\n", - "model_prefix = \"_\".join([modelprefix_pre, \"{0:0=2d}\".format(model_number), daug_str]) # modelprefix_pre = aug_\n", + "model_prefix = \"_\".join([modelprefix_pre, f\"{model_number:0=2d}\", daug_str]) # modelprefix_pre = aug_\n", "aug_project_path = os.path.join(project_path, model_prefix)\n", "aug_dlc_models = os.path.join(\n", " aug_project_path,\n", @@ -1533,7 +1533,7 @@ " print(\"Skipping this one as it already exists\")\n", "\n", "# Copy base train pose config file to the directory of this augmentation method\n", - "for j, (shuffle, trainingsetindex) in enumerate(zip(shuffles, trainingsetindices)):\n", + "for j, (shuffle, trainingsetindex) in enumerate(zip(shuffles, trainingsetindices, strict=False)):\n", " one_train_pose_config_file_path, one_test_pose_config_file_path, _ = deeplabcut.return_train_network_path(\n", " config_path, shuffle=shuffle, trainingsetindex=trainingsetindex, modelprefix=model_prefix\n", " )\n", @@ -1585,7 +1585,7 @@ "edits_dict[\"fliplr\"] = True\n", "edits_dict[\"rotation\"] = 180\n", "\n", - "for shuffle, trainingsetindex in zip(shuffles, trainingsetindices):\n", + "for shuffle, trainingsetindex in zip(shuffles, trainingsetindices, strict=False):\n", " one_train_pose_config_file_path, _, _ = deeplabcut.return_train_network_path(\n", " config_path, shuffle=shuffle, trainingsetindex=trainingsetindex, modelprefix=model_prefix\n", " )\n", @@ -1626,7 +1626,7 @@ "trainingsetindices = [0, 1, 2, 3]\n", "\n", "# loop over shuffles and train each\n", - "for shuffle, trainingsetindex in zip(shuffles, trainingsetindices):\n", + "for shuffle, trainingsetindex in zip(shuffles, trainingsetindices, strict=False):\n", " deeplabcut.train_network(\n", " config_path,\n", " shuffle=shuffle,\n", @@ -1662,7 +1662,7 @@ "# make sure we are testing all snapshots\n", "edit_config(config_path, {\"snapshotindex\": \"all\"})\n", "\n", - "for shuffle, trainingsetindex in zip(Shuffles, trainingsetindices):\n", + "for shuffle, trainingsetindex in zip(Shuffles, trainingsetindices, strict=False):\n", " deeplabcut.evaluate_network(\n", " config_path, modelprefix=model_prefix, Shuffles=[shuffle], trainingsetindex=trainingsetindex, gputouse=3\n", " )" @@ -1691,13 +1691,13 @@ "trainingsetindices = [3, 2]\n", "\n", "# We need pandas for creatig a nice list to parse\n", - "import pandas as pd\n", - "\n", "import sys\n", "\n", + "import pandas as pd\n", + "\n", "sys.path.append(\"..\") # my python file for this function is stored in the parent folder as I'm running this\n", - "from getErrorDistribution import getErrorDistribution # import the getErrorDistribution function\n", "import numpy as np\n", + "from getErrorDistribution import getErrorDistribution # import the getErrorDistribution function\n", "\n", "# Read the h5 file containing all the frames, (project_folder/training-datasets/iteration-0/UnaufmentedDataSet_project_folder/CollectedData_LabelerName.h5)\n", "df = pd.read_hdf(\n", @@ -1714,7 +1714,7 @@ "\n", "error_distributions_pcut = []\n", "\n", - "for shuffle, trainFractionIndex in zip(Shuffles, trainingsetindices):\n", + "for shuffle, trainFractionIndex in zip(Shuffles, trainingsetindices, strict=False):\n", " error_distributions_pcut_temp = []\n", " if shuffle == 4:\n", " model_prefix = model_prefix_base\n", @@ -1795,13 +1795,13 @@ "trainingsetindices = [3, 2, 2]\n", "\n", "# We need pandas for creatig a nice list to parse\n", - "import pandas as pd\n", - "\n", "import sys\n", "\n", + "import pandas as pd\n", + "\n", "sys.path.append(\"..\") # my python file for this function is stored in the parent folder as I'm running this\n", - "from getErrorDistribution import getErrorDistribution # import the getErrorDistribution function\n", "import numpy as np\n", + "from getErrorDistribution import getErrorDistribution # import the getErrorDistribution function\n", "\n", "# Read the h5 file containing all the frames, (project_folder/training-datasets/iteration-0/UnaufmentedDataSet_project_folder/CollectedData_LabelerName.h5)\n", "df = pd.read_hdf(\n", diff --git a/docs/recipes/fmpose3d.ipynb b/docs/recipes/fmpose3d.ipynb index 212303e8e5..a5074f8076 100644 --- a/docs/recipes/fmpose3d.ipynb +++ b/docs/recipes/fmpose3d.ipynb @@ -55,8 +55,6 @@ "outputs": [], "source": [ "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "from mpl_toolkits.mplot3d import Axes3D\n", "\n", "from deeplabcut.modelzoo.fmpose_3d.fmpose3d import get_fmpose3d_inference_api" ] diff --git a/examples/COLAB/COLAB_3miceDemo.ipynb b/examples/COLAB/COLAB_3miceDemo.ipynb index 9152b43346..a7363d74c2 100644 --- a/examples/COLAB/COLAB_3miceDemo.ipynb +++ b/examples/COLAB/COLAB_3miceDemo.ipynb @@ -78,10 +78,11 @@ "outputs": [], "source": [ "# Download our demo project:\n", - "import requests\n", "from io import BytesIO\n", "from zipfile import ZipFile\n", "\n", + "import requests\n", + "\n", "url_record = \"https://zenodo.org/api/records/7883589\"\n", "response = requests.get(url_record)\n", "if response.status_code == 200:\n", @@ -112,9 +113,10 @@ }, "outputs": [], "source": [ - "import deeplabcut as dlc\n", "import os\n", "\n", + "import deeplabcut as dlc\n", + "\n", "project_path = \"/content/demo-me-2021-07-14\"\n", "config_path = os.path.join(project_path, \"config.yaml\")\n", "video = os.path.join(project_path, \"videos\", \"videocompressed1.mp4\")\n", diff --git a/examples/COLAB/COLAB_BUCTD_and_CTD_tracking.ipynb b/examples/COLAB/COLAB_BUCTD_and_CTD_tracking.ipynb index 81ef8b9062..55cc29ff0a 100644 --- a/examples/COLAB/COLAB_BUCTD_and_CTD_tracking.ipynb +++ b/examples/COLAB/COLAB_BUCTD_and_CTD_tracking.ipynb @@ -392,17 +392,18 @@ } ], "source": [ - "import requests\n", "import shutil\n", "from io import BytesIO\n", "from pathlib import Path\n", "from zipfile import ZipFile\n", "\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import requests\n", + "\n", "import deeplabcut\n", "import deeplabcut.pose_estimation_pytorch as dlc_torch\n", - "import deeplabcut.utils.auxiliaryfunctions as auxiliaryfunctions\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np" + "import deeplabcut.utils.auxiliaryfunctions as auxiliaryfunctions" ] }, { @@ -2277,9 +2278,10 @@ }, "outputs": [], "source": [ - "from IPython.display import HTML\n", "from base64 import b64encode\n", "\n", + "from IPython.display import HTML\n", + "\n", "\n", "def show_video(video_path, width=640):\n", " video_file = open(video_path, \"rb\").read()\n", diff --git a/examples/COLAB/COLAB_DEMO_SuperAnimal.ipynb b/examples/COLAB/COLAB_DEMO_SuperAnimal.ipynb index 2162f5a809..94741c7acb 100644 --- a/examples/COLAB/COLAB_DEMO_SuperAnimal.ipynb +++ b/examples/COLAB/COLAB_DEMO_SuperAnimal.ipynb @@ -182,8 +182,8 @@ "outputs": [], "source": [ "from base64 import b64encode\n", + "\n", "from IPython.display import HTML\n", - "import glob\n", "\n", "# Get the parent directory and stem (filename without extension)\n", "directory = video_path.parent\n", diff --git a/examples/COLAB/COLAB_DEMO_mouse_openfield.ipynb b/examples/COLAB/COLAB_DEMO_mouse_openfield.ipynb index 04aacb261c..a979855742 100644 --- a/examples/COLAB/COLAB_DEMO_mouse_openfield.ipynb +++ b/examples/COLAB/COLAB_DEMO_mouse_openfield.ipynb @@ -127,8 +127,8 @@ "# Automatically update some hyperparameters for training,\n", "# here rotations to +/- 180 degrees. This can be helpful for optimizing performance.\n", "# see Primer -- Mathis et al. Neuron 2020\n", - "from deeplabcut.core.config import read_config_as_dict\n", "import deeplabcut.pose_estimation_pytorch as dlc_torch\n", + "from deeplabcut.core.config import read_config_as_dict\n", "\n", "loader = dlc_torch.DLCLoader(\n", " config=path_config_file,\n", diff --git a/examples/COLAB/COLAB_DLC_ModelZoo.ipynb b/examples/COLAB/COLAB_DLC_ModelZoo.ipynb index 108bd64dac..9b238b7917 100644 --- a/examples/COLAB/COLAB_DLC_ModelZoo.ipynb +++ b/examples/COLAB/COLAB_DLC_ModelZoo.ipynb @@ -86,6 +86,7 @@ "outputs": [], "source": [ "import os\n", + "\n", "import deeplabcut" ] }, diff --git a/examples/COLAB/COLAB_HumanPose_with_RTMPose.ipynb b/examples/COLAB/COLAB_HumanPose_with_RTMPose.ipynb index bc32907a05..584b184eab 100644 --- a/examples/COLAB/COLAB_HumanPose_with_RTMPose.ipynb +++ b/examples/COLAB/COLAB_HumanPose_with_RTMPose.ipynb @@ -95,7 +95,6 @@ "source": [ "from pathlib import Path\n", "\n", - "import deeplabcut.pose_estimation_pytorch as dlc_torch\n", "import huggingface_hub\n", "import matplotlib.collections as collections\n", "import matplotlib.pyplot as plt\n", @@ -103,7 +102,9 @@ "import torch\n", "import torchvision.models.detection as detection\n", "from PIL import Image\n", - "from tqdm import tqdm" + "from tqdm import tqdm\n", + "\n", + "import deeplabcut.pose_estimation_pytorch as dlc_torch" ] }, { diff --git a/examples/COLAB/COLAB_YOURDATA_SuperAnimal.ipynb b/examples/COLAB/COLAB_YOURDATA_SuperAnimal.ipynb index 60e8081f2c..53f1d325ce 100644 --- a/examples/COLAB/COLAB_YOURDATA_SuperAnimal.ipynb +++ b/examples/COLAB/COLAB_YOURDATA_SuperAnimal.ipynb @@ -80,15 +80,15 @@ "\n", "import deeplabcut\n", "import deeplabcut.utils.auxiliaryfunctions as auxiliaryfunctions\n", - "from deeplabcut.pose_estimation_pytorch.apis import (\n", - " superanimal_analyze_images,\n", - ")\n", "from deeplabcut.modelzoo import build_weight_init\n", "from deeplabcut.modelzoo.utils import (\n", " create_conversion_table,\n", " read_conversion_table_from_csv,\n", ")\n", "from deeplabcut.modelzoo.video_inference import video_inference_superanimal\n", + "from deeplabcut.pose_estimation_pytorch.apis import (\n", + " superanimal_analyze_images,\n", + ")\n", "from deeplabcut.utils.pseudo_label import keypoint_matching" ] }, diff --git a/examples/COLAB/COLAB_transformer_reID.ipynb b/examples/COLAB/COLAB_transformer_reID.ipynb index 9125155333..12eb4f89ea 100644 --- a/examples/COLAB/COLAB_transformer_reID.ipynb +++ b/examples/COLAB/COLAB_transformer_reID.ipynb @@ -64,8 +64,9 @@ }, "outputs": [], "source": [ - "import deeplabcut\n", - "import os" + "import os\n", + "\n", + "import deeplabcut" ] }, { @@ -104,10 +105,11 @@ ], "source": [ "# Download our demo project:\n", - "import requests\n", "from io import BytesIO\n", "from zipfile import ZipFile\n", "\n", + "import requests\n", + "\n", "url_record = \"https://zenodo.org/api/records/7883589\"\n", "response = requests.get(url_record)\n", "if response.status_code == 200:\n", diff --git a/examples/JUPYTER/Demo_3D_DeepLabCut.ipynb b/examples/JUPYTER/Demo_3D_DeepLabCut.ipynb index 0e12af0e2e..8148f383e7 100644 --- a/examples/JUPYTER/Demo_3D_DeepLabCut.ipynb +++ b/examples/JUPYTER/Demo_3D_DeepLabCut.ipynb @@ -197,7 +197,6 @@ "metadata": {}, "outputs": [], "source": [ - "import matplotlib\n", "\n", "%matplotlib inline\n", "\n", diff --git a/examples/testscript.py b/examples/testscript.py index 5ca32b85de..e6d040173f 100644 --- a/examples/testscript.py +++ b/examples/testscript.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # DeepLabCut Toolbox (deeplabcut.org) # © A. & M.W. Mathis Labs @@ -28,6 +27,7 @@ import random from pathlib import Path +import matplotlib import numpy as np import pandas as pd import scipy.io as sio @@ -36,8 +36,6 @@ from deeplabcut.core.engine import Engine from deeplabcut.utils import auxiliaryfunctions -import matplotlib - matplotlib.use("Agg") # Non-interactive backend, for CI/CD on Windows USE_SHELVE = random.choice([True, False]) @@ -233,7 +231,7 @@ def make_frame(t): print("CREATE VIDEO") successful = deeplabcut.create_labeled_video(path_config_file, [newvideo], destfolder=DESTFOLDER, save_frames=True) - assert all(successful), f"Failed to create a labeled video!" + assert all(successful), "Failed to create a labeled video!" print("Making plots") deeplabcut.plot_trajectories(path_config_file, [newvideo], destfolder=DESTFOLDER) @@ -363,13 +361,13 @@ def make_frame(t): displaycropped=True, filtered=True, ) - assert all(successful), f"Failed to create a labeled video!" + assert all(successful), "Failed to create a labeled video!" print("Creating a Johansson video!") successful = deeplabcut.create_labeled_video( path_config_file, [newvideo2], destfolder=DESTFOLDER, keypoints_only=True ) - assert all(successful), f"Failed to create a labeled video!" + assert all(successful), "Failed to create a labeled video!" deeplabcut.plot_trajectories(path_config_file, [newvideo2], destfolder=DESTFOLDER, filtered=True) diff --git a/examples/testscript_3d.py b/examples/testscript_3d.py index 1475228172..bf6964267d 100644 --- a/examples/testscript_3d.py +++ b/examples/testscript_3d.py @@ -21,13 +21,14 @@ It produces nothing of interest scientifically. """ -import os, deeplabcut -import zipfile, urllib.request, shutil -from datetime import datetime as dt import glob -from pathlib import Path +import os +import shutil import subprocess +import zipfile +from pathlib import Path +import deeplabcut if __name__ == "__main__": print("Imported DLC!") @@ -145,7 +146,8 @@ cwd = os.getcwd() [os.remove(file) for file in os.listdir(cwd) if not file.endswith(".jpg")] - # change the file names for calibration images to match the name of cameras in config.yaml file.i.e. camera-1 and camera-2 + # change the file names for calibration images to match the name of + # cameras in config.yaml file.i.e. camera-1 and camera-2 cam1_images = glob.glob(os.path.join(cwd, "left*.jpg")) cam2_images = glob.glob(os.path.join(cwd, "right*.jpg")) # Sorting images @@ -154,13 +156,13 @@ for idx, name in enumerate(cam1_images): os.rename( name, - os.path.join(cwd, str("camera-1_" + "{0:0=2d}".format(idx + 1) + ".jpg")), + os.path.join(cwd, str("camera-1_" + f"{idx + 1:0=2d}" + ".jpg")), ) for idx, name in enumerate(cam2_images): os.rename( name, - os.path.join(cwd, str("camera-2_" + "{0:0=2d}".format(idx + 1) + ".jpg")), + os.path.join(cwd, str("camera-2_" + f"{idx + 1:0=2d}" + ".jpg")), ) # Removing some of the images where the corner was not detected diff --git a/examples/testscript_deterministicwithResNet152.py b/examples/testscript_deterministicwithResNet152.py index 819a79a795..f061067d65 100644 --- a/examples/testscript_deterministicwithResNet152.py +++ b/examples/testscript_deterministicwithResNet152.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # DeepLabCut Toolbox (deeplabcut.org) # © A. & M.W. Mathis Labs @@ -42,10 +41,12 @@ scorer = "Alex" # Enter the name of the experimenter/labeler -import os, subprocess, deeplabcut -from pathlib import Path -import pandas as pd +import os + import numpy as np +import pandas as pd + +import deeplabcut print("Imported DLC!") basepath = os.path.dirname(os.path.abspath("testscript.py")) diff --git a/examples/testscript_mobilenets.py b/examples/testscript_mobilenets.py index 2e43d91b93..c6a7a2d1c5 100644 --- a/examples/testscript_mobilenets.py +++ b/examples/testscript_mobilenets.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # DeepLabCut Toolbox (deeplabcut.org) # © A. & M.W. Mathis Labs @@ -26,10 +25,12 @@ import os os.environ["DLClight"] = "True" -import deeplabcut from pathlib import Path -import pandas as pd + import numpy as np +import pandas as pd + +import deeplabcut def Cuttrainingschedule(path_config_file, shuffle, trainingsetindex=0, initweights="imagenet", lastvalue=10): @@ -71,7 +72,7 @@ def Cuttrainingschedule(path_config_file, shuffle, trainingsetindex=0, initweigh ) print("CHANGING training parameters to end quickly!") - DLC_config = deeplabcut.auxiliaryfunctions.edit_config(posefile, edits) + deeplabcut.auxiliaryfunctions.edit_config(posefile, edits) return diff --git a/examples/testscript_multianimal.py b/examples/testscript_multianimal.py index 3ca2ce73fc..f96c09e79c 100644 --- a/examples/testscript_multianimal.py +++ b/examples/testscript_multianimal.py @@ -13,11 +13,10 @@ import random from pathlib import Path +import matplotlib import numpy as np import pandas as pd -import matplotlib - matplotlib.use("Agg") # Non-interactive backend, for CI/CD on Windows import deeplabcut @@ -25,7 +24,6 @@ from deeplabcut.utils import auxfun_multianimal, auxiliaryfunctions from deeplabcut.utils.auxfun_videos import VideoReader - MODELS = ["dlcrnet_ms5", "dlcr101_ms5", "efficientnet-b0"] diff --git a/examples/testscript_openfielddata.py b/examples/testscript_openfielddata.py index 47d894ae34..84829f966c 100644 --- a/examples/testscript_openfielddata.py +++ b/examples/testscript_openfielddata.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # DeepLabCut Toolbox (deeplabcut.org) # © A. & M.W. Mathis Labs @@ -30,9 +29,9 @@ The analysis of the video takes 41 seconds (batch size 32) and creating the frames 8 seconds (+ a few seconds for ffmpeg) to create the video. """ -import deeplabcut import os +import deeplabcut if __name__ == "__main__": # Loading example data set diff --git a/examples/testscript_openfielddata_augmentationcomparison.py b/examples/testscript_openfielddata_augmentationcomparison.py index 9a3ef1fa11..58e48f8054 100644 --- a/examples/testscript_openfielddata_augmentationcomparison.py +++ b/examples/testscript_openfielddata_augmentationcomparison.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # # DeepLabCut Toolbox (deeplabcut.org) # © A. & M.W. Mathis Labs @@ -58,8 +57,8 @@ import os os.environ["CUDA_VISIBLE_DEVICES"] = str(0) + import deeplabcut -import numpy as np # Loading example data set path_config_file = os.path.join(os.getcwd(), "openfield-Pranav-2018-10-30/config.yaml") diff --git a/examples/testscript_pretrained_models.py b/examples/testscript_pretrained_models.py index e183f0786a..8556a2f24a 100644 --- a/examples/testscript_pretrained_models.py +++ b/examples/testscript_pretrained_models.py @@ -13,10 +13,9 @@ """ -import os, subprocess, deeplabcut -from pathlib import Path -import pandas as pd -import numpy as np +import os + +import deeplabcut Task = "human_dancing" YourName = "teamDLC" diff --git a/examples/testscript_pytorch_multi_animal.py b/examples/testscript_pytorch_multi_animal.py index 2d3ccce0e5..c713b1d28c 100644 --- a/examples/testscript_pytorch_multi_animal.py +++ b/examples/testscript_pytorch_multi_animal.py @@ -14,19 +14,19 @@ from pathlib import Path -import deeplabcut.utils.auxiliaryfunctions as af -from deeplabcut.compat import Engine -from deeplabcut.pose_estimation_pytorch.config.utils import ( - is_model_top_down, - is_model_cond_top_down, -) - from utils import ( + SyntheticProjectParameters, cleanup, create_fake_project, log_step, run, - SyntheticProjectParameters, +) + +import deeplabcut.utils.auxiliaryfunctions as af +from deeplabcut.compat import Engine +from deeplabcut.pose_estimation_pytorch.config.utils import ( + is_model_cond_top_down, + is_model_top_down, ) diff --git a/examples/testscript_pytorch_single_animal.py b/examples/testscript_pytorch_single_animal.py index a2cbdebc1c..b0baac9d34 100644 --- a/examples/testscript_pytorch_single_animal.py +++ b/examples/testscript_pytorch_single_animal.py @@ -4,18 +4,18 @@ from pathlib import Path -import deeplabcut.utils.auxiliaryfunctions as af -from deeplabcut.compat import Engine - from utils import ( + SyntheticProjectParameters, cleanup, copy_project_for_test, create_fake_project, log_step, run, - SyntheticProjectParameters, ) +import deeplabcut.utils.auxiliaryfunctions as af +from deeplabcut.compat import Engine + def main( synthetic_data: bool, diff --git a/examples/testscript_superanimal_adaptation.py b/examples/testscript_superanimal_adaptation.py index f235abf999..e45a1265ca 100644 --- a/examples/testscript_superanimal_adaptation.py +++ b/examples/testscript_superanimal_adaptation.py @@ -12,9 +12,9 @@ Test script for super animal adaptation """ -import deeplabcut import os +import deeplabcut if __name__ == "__main__": basepath = os.path.dirname(os.path.realpath(__file__)) diff --git a/examples/testscript_superanimal_inference.py b/examples/testscript_superanimal_inference.py index 2ec86c1f21..5861896e33 100644 --- a/examples/testscript_superanimal_inference.py +++ b/examples/testscript_superanimal_inference.py @@ -13,9 +13,9 @@ """ -import deeplabcut import os +import deeplabcut if __name__ == "__main__": basepath = os.path.dirname(os.path.realpath(__file__)) diff --git a/examples/testscript_transreid.py b/examples/testscript_transreid.py index 5d6e2ec3b6..83b6aee58e 100644 --- a/examples/testscript_transreid.py +++ b/examples/testscript_transreid.py @@ -9,14 +9,16 @@ # Licensed under GNU Lesser General Public License v3.0 # import os -import deeplabcut -import numpy as np -import pandas as pd import pickle -from deeplabcut.utils import auxfun_multianimal, auxiliaryfunctions import random from pathlib import Path +import numpy as np +import pandas as pd + +import deeplabcut +from deeplabcut.utils import auxfun_multianimal, auxiliaryfunctions + # MODELS = ["dlcrnet_ms5", "dlcr101_ms5", "efficientnet-b0", "mobilenet_v2_0.35"] MODELS = [ "dlcrnet_ms5", diff --git a/examples/utils.py b/examples/utils.py index 385875d2c2..96f54dba58 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -22,13 +22,14 @@ matplotlib.use("Agg") # Non-interactive backend, for CI/CD on Windows import cv2 -import deeplabcut -import deeplabcut.utils.auxiliaryfunctions as af import numpy as np import pandas as pd +from PIL import Image + +import deeplabcut +import deeplabcut.utils.auxiliaryfunctions as af from deeplabcut.compat import Engine from deeplabcut.generate_training_dataset import get_existing_shuffle_indices -from PIL import Image def log_step(message: Any) -> None: @@ -255,7 +256,7 @@ def generate_video_from_images(image_dir: Path, output_video: Path) -> None: def create_fake_project(path: Path, params: SyntheticProjectParameters) -> None: if path.exists(): - raise ValueError(f"Cannot create a fake project at an existing path") + raise ValueError("Cannot create a fake project at an existing path") scorer = "synthetic" video_name = "cat" diff --git a/ruff-report.md b/ruff-report.md new file mode 100644 index 0000000000..2d82563faa --- /dev/null +++ b/ruff-report.md @@ -0,0 +1,6934 @@ +# Ruff manual-fix report + +Generated from: `.` + +Total remaining issues: **1437** + +## Summary + +| Rule | Count | Note | +|---|---:|---| +| `E501` | 333 | Line too long. Prefer wrapping expressions, splitting long strings/comments, or extracting variables. | +| `F401` | 331 | Unused import. Usually safe to delete; verify imports with side effects. | +| `B905` | 176 | | +| `F841` | 141 | | +| `E402` | 93 | Module import not at top of file. Move imports above executable code if possible. | +| `UP031` | 76 | Old `%` formatting. Convert to f-strings or `.format()` where appropriate. | +| `B007` | 51 | Unused loop variable. Rename to `_` or use it. | +| `B028` | 49 | | +| `F403` | 36 | `from x import *` makes names unclear. Replace with explicit imports. | +| `E712` | 22 | | +| `F821` | 22 | Undefined name. Usually a real bug or missing import. | +| `B904` | 19 | Inside `except`, use `raise ... from e` to preserve exception chaining. | +| `E722` | 19 | Bare `except:`. Catch `Exception` or a narrower exception type. | +| `F405` | 16 | Likely consequence of `import *`. Import the name explicitly. | +| `E721` | 14 | Avoid direct `type(x) == Y`; prefer `isinstance(x, Y)`. | +| `B006` | 12 | | +| `E711` | 7 | | +| `E731` | 4 | | +| `B008` | 3 | Function call in default arg. Use `None` + initialize inside the function. | +| `B023` | 2 | Function closes over loop variable. Bind it via default arg or helper. | +| `B024` | 2 | ABC without abstract method. Add `@abstractmethod` or remove ABC intent. | +| `F811` | 2 | Redefined while unused. Remove duplicate or rename. | +| `B011` | 1 | | +| `B012` | 1 | Jump statement in `finally` can swallow exceptions. Restructure flow. | +| `B016` | 1 | Raise an exception instance/class, not a literal. | +| `B017` | 1 | Use a more specific exception with `assertRaises`. | +| `B020` | 1 | Loop variable overrides iterator. Rename loop variables. | +| `B027` | 1 | Empty method in ABC without abstract decorator. Add `@abstractmethod` or implement it. | +| `UP028` | 1 | | + +## Suggested triage order + +1. `F403` — `from x import *` makes names unclear. Replace with explicit imports. +2. `F405` — Likely consequence of `import *`. Import the name explicitly. +3. `F821` — Undefined name. Usually a real bug or missing import. +4. `E722` — Bare `except:`. Catch `Exception` or a narrower exception type. +5. `B904` — Inside `except`, use `raise ... from e` to preserve exception chaining. +6. `E402` — Module import not at top of file. Move imports above executable code if possible. +7. `F401` — Unused import. Usually safe to delete; verify imports with side effects. +8. `E501` — Line too long. Prefer wrapping expressions, splitting long strings/comments, or extracting variables. + +## Table of contents by rule + +- [E501 (333)](#e501) +- [F401 (331)](#f401) +- [B905 (176)](#b905) +- [F841 (141)](#f841) +- [E402 (93)](#e402) +- [UP031 (76)](#up031) +- [B007 (51)](#b007) +- [B028 (49)](#b028) +- [F403 (36)](#f403) +- [E712 (22)](#e712) +- [F821 (22)](#f821) +- [B904 (19)](#b904) +- [E722 (19)](#e722) +- [F405 (16)](#f405) +- [E721 (14)](#e721) +- [B006 (12)](#b006) +- [E711 (7)](#e711) +- [E731 (4)](#e731) +- [B008 (3)](#b008) +- [B023 (2)](#b023) +- [B024 (2)](#b024) +- [F811 (2)](#f811) +- [B011 (1)](#b011) +- [B012 (1)](#b012) +- [B016 (1)](#b016) +- [B017 (1)](#b017) +- [B020 (1)](#b020) +- [B027 (1)](#b027) +- [UP028 (1)](#up028) + +## E501 + +Count: **333** +Hint: Line too long. Prefer wrapping expressions, splitting long strings/comments, or extracting variables. + +### Files affected + +| File | Count | +|---|---:| +| `deeplabcut\pose_estimation_tensorflow\predict_videos.py` | 42 | +| `deeplabcut\cli.py` | 30 | +| `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` | 19 | +| `deeplabcut\compat.py` | 16 | +| `deeplabcut\pose_estimation_tensorflow\core\evaluate.py` | 15 | +| `deeplabcut\create_project\modelzoo.py` | 13 | +| `deeplabcut\pose_estimation_3d\camera_calibration.py` | 13 | +| `deeplabcut\pose_estimation_3d\plotting3D.py` | 11 | +| `deeplabcut\utils\conversioncode.py` | 11 | +| `deeplabcut\utils\frameselectiontools.py` | 10 | +| `deeplabcut\pose_estimation_3d\triangulation.py` | 9 | +| `deeplabcut\pose_estimation_tensorflow\visualizemaps.py` | 9 | +| `deeplabcut\utils\auxfun_videos.py` | 9 | +| `deeplabcut\benchmark\benchmarks.py` | 8 | +| `deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py` | 8 | +| `deeplabcut\refine_training_dataset\outlier_frames.py` | 7 | +| `deeplabcut\utils\auxiliaryfunctions.py` | 6 | +| `docs\recipes\flip_and_rotate.ipynb` | 6 | +| `deeplabcut\utils\auxfun_multianimal.py` | 5 | +| `deeplabcut\create_project\add.py` | 3 | +| `deeplabcut\create_project\new.py` | 3 | +| `deeplabcut\create_project\new_3d.py` | 3 | +| `deeplabcut\generate_training_dataset\frame_extraction.py` | 3 | +| `deeplabcut\generate_training_dataset\multiple_individuals_trainingsetmanipulation.py` | 3 | +| `deeplabcut\gui\tracklet_toolbox.py` | 3 | +| `deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc_dataframe.py` | 3 | +| `deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py` | 3 | +| `deeplabcut\modelzoo\generalized_data_converter\datasets\single_dlc_dataframe.py` | 3 | +| `deeplabcut\pose_estimation_tensorflow\modelzoo\api\spatiotemporal_adapt.py` | 3 | +| `deeplabcut\refine_training_dataset\stitch.py` | 3 | +| `deeplabcut\utils\make_labeled_video.py` | 3 | +| `deeplabcut\gui\window.py` | 2 | +| `deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py` | 2 | +| `deeplabcut\modelzoo\utils.py` | 2 | +| `deeplabcut\modelzoo\video_inference.py` | 2 | +| `deeplabcut\pose_estimation_pytorch\config\make_pose_config.py` | 2 | +| `deeplabcut\pose_estimation_tensorflow\core\train_multianimal.py` | 2 | +| `deeplabcut\pose_estimation_tensorflow\training.py` | 2 | +| `deeplabcut\pose_tracking_pytorch\model\backbones\vit_pytorch.py` | 2 | +| `deeplabcut\pose_tracking_pytorch\processor\processor.py` | 2 | +| `deeplabcut\utils\auxfun_models.py` | 2 | +| `deeplabcut\utils\auxiliaryfunctions_3d.py` | 2 | +| `deeplabcut\utils\pseudo_label.py` | 2 | +| `tests\pose_estimation_pytorch\other\test_match_predictions_to_gt.py` | 2 | +| `testscript_cli.py` | 2 | +| `deeplabcut\__main__.py` | 1 | +| `deeplabcut\gui\tabs\extract_outlier_frames.py` | 1 | +| `deeplabcut\gui\tabs\modelzoo.py` | 1 | +| `deeplabcut\gui\tabs\refine_tracklets.py` | 1 | +| `deeplabcut\gui\tabs\train_network.py` | 1 | +| `deeplabcut\gui\widgets.py` | 1 | +| `deeplabcut\modelzoo\weight_initialization.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\models\modules\conv_block.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\models\necks\transformer.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\models\predictors\paf_predictor.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\post_processing\match_predictions_to_gt.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\core\train.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\datasets\pose_tensorpack.py` | 1 | +| `deeplabcut\post_processing\analyze_skeleton.py` | 1 | +| `deeplabcut\utils\visualization.py` | 1 | +| `examples\JUPYTER\Demo_yourowndata.ipynb` | 1 | +| `examples\testscript_3d.py` | 1 | +| `examples\testscript_deterministicwithResNet152.py` | 1 | +| `examples\testscript_mobilenets.py` | 1 | +| `examples\testscript_openfielddata.py` | 1 | +| `examples\testscript_pretrained_models.py` | 1 | + +### Details + +#### `deeplabcut\pose_estimation_tensorflow\predict_videos.py` (42) + +| Line | Col | Message | +|---:|---:|---| +| 76 | 121 | Line too long (173 > 120) | +| 115 | 121 | Line too long (235 > 120) | +| 151 | 121 | Line too long (150 > 120) | +| 165 | 121 | Line too long (136 > 120) | +| 226 | 121 | Line too long (205 > 120) | +| 230 | 121 | Line too long (138 > 120) | +| 233 | 121 | Line too long (205 > 120) | +| 334 | 121 | Line too long (153 > 120) | +| 335 | 121 | Line too long (155 > 120) | +| 336 | 121 | Line too long (150 > 120) | +| 337 | 121 | Line too long (145 > 120) | +| 499 | 121 | Line too long (235 > 120) | +| 535 | 121 | Line too long (150 > 120) | +| 549 | 121 | Line too long (136 > 120) | +| 643 | 121 | Line too long (184 > 120) | +| 646 | 121 | Line too long (205 > 120) | +| 650 | 121 | Line too long (138 > 120) | +| 653 | 121 | Line too long (205 > 120) | +| 663 | 121 | Line too long (129 > 120) | +| 902 | 121 | Line too long (143 > 120) | +| 1077 | 121 | Line too long (133 > 120) | +| 1152 | 121 | Line too long (126 > 120) | +| 1154 | 121 | Line too long (142 > 120) | +| 1155 | 121 | Line too long (145 > 120) | +| 1156 | 121 | Line too long (146 > 120) | +| 1157 | 121 | Line too long (128 > 120) | +| 1168 | 121 | Line too long (122 > 120) | +| 1174 | 121 | Line too long (136 > 120) | +| 1176 | 121 | Line too long (140 > 120) | +| 1180 | 121 | Line too long (123 > 120) | +| 1185 | 121 | Line too long (121 > 120) | +| 1188 | 121 | Line too long (122 > 120) | +| 1219 | 121 | Line too long (235 > 120) | +| 1460 | 121 | Line too long (155 > 120) | +| 1463 | 121 | Line too long (140 > 120) | +| 1470 | 121 | Line too long (136 > 120) | +| 1476 | 121 | Line too long (133 > 120) | +| 1512 | 121 | Line too long (146 > 120) | +| 1515 | 121 | Line too long (165 > 120) | +| 1573 | 121 | Line too long (235 > 120) | +| 1626 | 121 | Line too long (123 > 120) | +| 1741 | 121 | Line too long (193 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\predict_videos.py:76" +``` + +#### `deeplabcut\cli.py` (30) + +| Line | Col | Message | +|---:|---:|---| +| 52 | 121 | Line too long (187 > 120) | +| 63 | 121 | Line too long (139 > 120) | +| 65 | 121 | Line too long (177 > 120) | +| 70 | 121 | Line too long (149 > 120) | +| 73 | 121 | Line too long (158 > 120) | +| 76 | 121 | Line too long (161 > 120) | +| 143 | 121 | Line too long (132 > 120) | +| 144 | 121 | Line too long (132 > 120) | +| 156 | 121 | Line too long (132 > 120) | +| 161 | 121 | Line too long (193 > 120) | +| 175 | 121 | Line too long (136 > 120) | +| 190 | 121 | Line too long (169 > 120) | +| 208 | 121 | Line too long (148 > 120) | +| 334 | 121 | Line too long (180 > 120) | +| 341 | 121 | Line too long (140 > 120) | +| 342 | 121 | Line too long (158 > 120) | +| 343 | 121 | Line too long (129 > 120) | +| 344 | 121 | Line too long (144 > 120) | +| 353 | 121 | Line too long (133 > 120) | +| 361 | 121 | Line too long (127 > 120) | +| 362 | 121 | Line too long (150 > 120) | +| 369 | 121 | Line too long (152 > 120) | +| 400 | 121 | Line too long (130 > 120) | +| 406 | 121 | Line too long (128 > 120) | +| 419 | 121 | Line too long (134 > 120) | +| 422 | 121 | Line too long (165 > 120) | +| 425 | 121 | Line too long (175 > 120) | +| 439 | 121 | Line too long (127 > 120) | +| 537 | 121 | Line too long (132 > 120) | +| 609 | 121 | Line too long (145 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\cli.py:52" +``` + +#### `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` (19) + +| Line | Col | Message | +|---:|---:|---| +| 41 | 121 | Line too long (123 > 120) | +| 68 | 121 | Line too long (151 > 120) | +| 74 | 121 | Line too long (177 > 120) | +| 158 | 121 | Line too long (139 > 120) | +| 503 | 121 | Line too long (127 > 120) | +| 516 | 121 | Line too long (311 > 120) | +| 525 | 121 | Line too long (130 > 120) | +| 562 | 121 | Line too long (136 > 120) | +| 623 | 121 | Line too long (138 > 120) | +| 626 | 121 | Line too long (143 > 120) | +| 635 | 121 | Line too long (137 > 120) | +| 636 | 121 | Line too long (138 > 120) | +| 645 | 121 | Line too long (125 > 120) | +| 652 | 121 | Line too long (161 > 120) | +| 653 | 121 | Line too long (146 > 120) | +| 1062 | 121 | Line too long (131 > 120) | +| 1066 | 121 | Line too long (141 > 120) | +| 1121 | 121 | Line too long (164 > 120) | +| 1125 | 121 | Line too long (144 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\generate_training_dataset\trainingsetmanipulation.py:41" +``` + +#### `deeplabcut\compat.py` (16) + +| Line | Col | Message | +|---:|---:|---| +| 587 | 121 | Line too long (176 > 120) | +| 593 | 121 | Line too long (146 > 120) | +| 602 | 121 | Line too long (141 > 120) | +| 609 | 121 | Line too long (137 > 120) | +| 610 | 121 | Line too long (141 > 120) | +| 611 | 121 | Line too long (147 > 120) | +| 612 | 121 | Line too long (149 > 120) | +| 1427 | 121 | Line too long (155 > 120) | +| 1430 | 121 | Line too long (140 > 120) | +| 1437 | 121 | Line too long (136 > 120) | +| 1443 | 121 | Line too long (133 > 120) | +| 1598 | 121 | Line too long (137 > 120) | +| 1599 | 121 | Line too long (141 > 120) | +| 1600 | 121 | Line too long (147 > 120) | +| 1601 | 121 | Line too long (149 > 120) | +| 1737 | 121 | Line too long (141 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\compat.py:587" +``` + +#### `deeplabcut\pose_estimation_tensorflow\core\evaluate.py` (15) + +| Line | Col | Message | +|---:|---:|---| +| 53 | 121 | Line too long (141 > 120) | +| 204 | 121 | Line too long (176 > 120) | +| 207 | 121 | Line too long (146 > 120) | +| 216 | 121 | Line too long (141 > 120) | +| 223 | 121 | Line too long (137 > 120) | +| 224 | 121 | Line too long (141 > 120) | +| 225 | 121 | Line too long (147 > 120) | +| 226 | 121 | Line too long (149 > 120) | +| 249 | 121 | Line too long (139 > 120) | +| 350 | 121 | Line too long (178 > 120) | +| 879 | 121 | Line too long (131 > 120) | +| 883 | 121 | Line too long (130 > 120) | +| 915 | 121 | Line too long (154 > 120) | +| 935 | 121 | Line too long (262 > 120) | +| 938 | 121 | Line too long (140 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\core\evaluate.py:53" +``` + +#### `deeplabcut\create_project\modelzoo.py` (13) + +| Line | Col | Message | +|---:|---:|---| +| 102 | 121 | Line too long (160 > 120) | +| 206 | 121 | Line too long (127 > 120) | +| 209 | 121 | Line too long (148 > 120) | +| 212 | 121 | Line too long (182 > 120) | +| 336 | 121 | Line too long (135 > 120) | +| 339 | 121 | Line too long (156 > 120) | +| 342 | 121 | Line too long (190 > 120) | +| 361 | 121 | Line too long (138 > 120) | +| 366 | 121 | Line too long (151 > 120) | +| 534 | 121 | Line too long (138 > 120) | +| 537 | 121 | Line too long (159 > 120) | +| 540 | 121 | Line too long (193 > 120) | +| 647 | 121 | Line too long (126 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\create_project\modelzoo.py:102" +``` + +#### `deeplabcut\pose_estimation_3d\camera_calibration.py` (13) + +| Line | Col | Message | +|---:|---:|---| +| 29 | 121 | Line too long (184 > 120) | +| 31 | 121 | Line too long (151 > 120) | +| 33 | 121 | Line too long (172 > 120) | +| 34 | 121 | Line too long (152 > 120) | +| 36 | 121 | Line too long (132 > 120) | +| 51 | 121 | Line too long (127 > 120) | +| 52 | 121 | Line too long (121 > 120) | +| 55 | 121 | Line too long (155 > 120) | +| 119 | 121 | Line too long (166 > 120) | +| 159 | 121 | Line too long (226 > 120) | +| 264 | 121 | Line too long (317 > 120) | +| 271 | 121 | Line too long (146 > 120) | +| 286 | 121 | Line too long (157 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_3d\camera_calibration.py:29" +``` + +#### `deeplabcut\pose_estimation_3d\plotting3D.py` (11) + +| Line | Col | Message | +|---:|---:|---| +| 84 | 121 | Line too long (159 > 120) | +| 87 | 121 | Line too long (287 > 120) | +| 93 | 121 | Line too long (159 > 120) | +| 99 | 121 | Line too long (140 > 120) | +| 103 | 121 | Line too long (141 > 120) | +| 106 | 121 | Line too long (216 > 120) | +| 109 | 121 | Line too long (216 > 120) | +| 112 | 121 | Line too long (216 > 120) | +| 115 | 121 | Line too long (219 > 120) | +| 130 | 121 | Line too long (148 > 120) | +| 155 | 121 | Line too long (243 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_3d\plotting3D.py:84" +``` + +#### `deeplabcut\utils\conversioncode.py` (11) + +| Line | Col | Message | +|---:|---:|---| +| 35 | 121 | Line too long (129 > 120) | +| 42 | 121 | Line too long (139 > 120) | +| 45 | 121 | Line too long (134 > 120) | +| 53 | 121 | Line too long (138 > 120) | +| 97 | 121 | Line too long (155 > 120) | +| 108 | 121 | Line too long (139 > 120) | +| 216 | 121 | Line too long (181 > 120) | +| 217 | 121 | Line too long (137 > 120) | +| 218 | 121 | Line too long (137 > 120) | +| 233 | 121 | Line too long (131 > 120) | +| 274 | 121 | Line too long (131 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\conversioncode.py:35" +``` + +#### `deeplabcut\utils\frameselectiontools.py` (10) + +| Line | Col | Message | +|---:|---:|---| +| 32 | 121 | Line too long (125 > 120) | +| 75 | 121 | Line too long (125 > 120) | +| 124 | 121 | Line too long (125 > 120) | +| 125 | 121 | Line too long (126 > 120) | +| 128 | 121 | Line too long (130 > 120) | +| 172 | 121 | Line too long (128 > 120) | +| 214 | 121 | Line too long (125 > 120) | +| 215 | 121 | Line too long (126 > 120) | +| 218 | 121 | Line too long (130 > 120) | +| 222 | 121 | Line too long (140 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\frameselectiontools.py:32" +``` + +#### `deeplabcut\pose_estimation_3d\triangulation.py` (9) + +| Line | Col | Message | +|---:|---:|---| +| 49 | 121 | Line too long (140 > 120) | +| 77 | 121 | Line too long (220 > 120) | +| 85 | 121 | Line too long (268 > 120) | +| 116 | 121 | Line too long (124 > 120) | +| 188 | 121 | Line too long (126 > 120) | +| 298 | 121 | Line too long (201 > 120) | +| 304 | 121 | Line too long (177 > 120) | +| 501 | 121 | Line too long (165 > 120) | +| 509 | 121 | Line too long (130 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_3d\triangulation.py:49" +``` + +#### `deeplabcut\pose_estimation_tensorflow\visualizemaps.py` (9) + +| Line | Col | Message | +|---:|---:|---| +| 36 | 121 | Line too long (149 > 120) | +| 45 | 121 | Line too long (141 > 120) | +| 49 | 121 | Line too long (137 > 120) | +| 50 | 121 | Line too long (141 > 120) | +| 51 | 121 | Line too long (147 > 120) | +| 52 | 121 | Line too long (149 > 120) | +| 183 | 121 | Line too long (121 > 120) | +| 184 | 121 | Line too long (162 > 120) | +| 286 | 121 | Line too long (141 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\visualizemaps.py:36" +``` + +#### `deeplabcut\utils\auxfun_videos.py` (9) + +| Line | Col | Message | +|---:|---:|---| +| 410 | 121 | Line too long (139 > 120) | +| 412 | 121 | Line too long (124 > 120) | +| 465 | 121 | Line too long (127 > 120) | +| 467 | 121 | Line too long (167 > 120) | +| 496 | 121 | Line too long (121 > 120) | +| 533 | 121 | Line too long (151 > 120) | +| 535 | 121 | Line too long (139 > 120) | +| 574 | 121 | Line too long (132 > 120) | +| 603 | 121 | Line too long (223 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\auxfun_videos.py:410" +``` + +#### `deeplabcut\benchmark\benchmarks.py` (8) + +| Line | Col | Message | +|---:|---:|---| +| 27 | 121 | Line too long (776 > 120) | +| 29 | 121 | Line too long (149 > 120) | +| 55 | 121 | Line too long (1440 > 120) | +| 57 | 121 | Line too long (149 > 120) | +| 106 | 121 | Line too long (964 > 120) | +| 108 | 121 | Line too long (149 > 120) | +| 137 | 121 | Line too long (981 > 120) | +| 139 | 121 | Line too long (149 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\benchmark\benchmarks.py:27" +``` + +#### `deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py` (8) + +| Line | Col | Message | +|---:|---:|---| +| 441 | 121 | Line too long (127 > 120) | +| 443 | 121 | Line too long (142 > 120) | +| 444 | 121 | Line too long (145 > 120) | +| 450 | 121 | Line too long (155 > 120) | +| 453 | 121 | Line too long (242 > 120) | +| 455 | 121 | Line too long (237 > 120) | +| 458 | 121 | Line too long (164 > 120) | +| 461 | 121 | Line too long (180 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py:441" +``` + +#### `deeplabcut\refine_training_dataset\outlier_frames.py` (7) + +| Line | Col | Message | +|---:|---:|---| +| 267 | 121 | Line too long (163 > 120) | +| 477 | 121 | Line too long (131 > 120) | +| 563 | 121 | Line too long (134 > 120) | +| 574 | 121 | Line too long (140 > 120) | +| 598 | 121 | Line too long (134 > 120) | +| 798 | 121 | Line too long (128 > 120) | +| 840 | 121 | Line too long (124 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\refine_training_dataset\outlier_frames.py:267" +``` + +#### `deeplabcut\utils\auxiliaryfunctions.py` (6) + +| Line | Col | Message | +|---:|---:|---| +| 234 | 121 | Line too long (147 > 120) | +| 405 | 121 | Line too long (135 > 120) | +| 408 | 121 | Line too long (122 > 120) | +| 684 | 121 | Line too long (161 > 120) | +| 789 | 121 | Line too long (123 > 120) | +| 790 | 121 | Line too long (149 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\auxiliaryfunctions.py:234" +``` + +#### `docs\recipes\flip_and_rotate.ipynb` (6) + +| Line | Col | Message | +|---:|---:|---| +| 10 | 121 | Line too long (155 > 120) | +| 19 | 121 | Line too long (155 > 120) | +| 19 | 121 | Line too long (155 > 120) | +| 20 | 121 | Line too long (155 > 120) | +| 20 | 121 | Line too long (155 > 120) | +| 22 | 121 | Line too long (155 > 120) | + +Quick open commands: + +```powershell +code -g "docs\recipes\flip_and_rotate.ipynb:10" +``` + +#### `deeplabcut\utils\auxfun_multianimal.py` (5) + +| Line | Col | Message | +|---:|---:|---| +| 94 | 121 | Line too long (148 > 120) | +| 122 | 121 | Line too long (136 > 120) | +| 242 | 121 | Line too long (161 > 120) | +| 243 | 121 | Line too long (157 > 120) | +| 358 | 121 | Line too long (136 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\auxfun_multianimal.py:94" +``` + +#### `deeplabcut\create_project\add.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 39 | 121 | Line too long (122 > 120) | +| 42 | 121 | Line too long (163 > 120) | +| 45 | 121 | Line too long (203 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\create_project\add.py:39" +``` + +#### `deeplabcut\create_project\new.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 149 | 121 | Line too long (149 > 120) | +| 217 | 121 | Line too long (141 > 120) | +| 306 | 121 | Line too long (390 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\create_project\new.py:149" +``` + +#### `deeplabcut\create_project\new_3d.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 35 | 121 | Line too long (140 > 120) | +| 91 | 121 | Line too long (123 > 120) | +| 126 | 121 | Line too long (295 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\create_project\new_3d.py:35" +``` + +#### `deeplabcut\generate_training_dataset\frame_extraction.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 425 | 121 | Line too long (171 > 120) | +| 451 | 121 | Line too long (142 > 120) | +| 544 | 121 | Line too long (163 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\generate_training_dataset\frame_extraction.py:425" +``` + +#### `deeplabcut\generate_training_dataset\multiple_individuals_trainingsetmanipulation.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 261 | 121 | Line too long (128 > 120) | +| 262 | 121 | Line too long (213 > 120) | +| 268 | 121 | Line too long (210 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\generate_training_dataset\multiple_individuals_trainingsetmanipulation.py:261" +``` + +#### `deeplabcut\gui\tracklet_toolbox.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 85 | 121 | Line too long (180 > 120) | +| 909 | 121 | Line too long (126 > 120) | +| 913 | 121 | Line too long (127 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\gui\tracklet_toolbox.py:85" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc_dataframe.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 34 | 121 | Line too long (127 > 120) | +| 47 | 121 | Line too long (311 > 120) | +| 56 | 121 | Line too long (130 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc_dataframe.py:34" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 181 | 121 | Line too long (201 > 120) | +| 320 | 121 | Line too long (167 > 120) | +| 532 | 121 | Line too long (167 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py:181" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\datasets\single_dlc_dataframe.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 34 | 121 | Line too long (127 > 120) | +| 47 | 121 | Line too long (311 > 120) | +| 56 | 121 | Line too long (130 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\single_dlc_dataframe.py:34" +``` + +#### `deeplabcut\pose_estimation_tensorflow\modelzoo\api\spatiotemporal_adapt.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 53 | 121 | Line too long (167 > 120) | +| 55 | 121 | Line too long (182 > 120) | +| 57 | 121 | Line too long (169 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\modelzoo\api\spatiotemporal_adapt.py:53" +``` + +#### `deeplabcut\refine_training_dataset\stitch.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 1002 | 121 | Line too long (155 > 120) | +| 1005 | 121 | Line too long (140 > 120) | +| 1012 | 121 | Line too long (136 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\refine_training_dataset\stitch.py:1002" +``` + +#### `deeplabcut\utils\make_labeled_video.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 1168 | 121 | Line too long (140 > 120) | +| 1175 | 121 | Line too long (136 > 120) | +| 1180 | 121 | Line too long (124 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\make_labeled_video.py:1168" +``` + +#### `deeplabcut\gui\window.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 372 | 121 | Line too long (312 > 120) | +| 554 | 121 | Line too long (141 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\gui\window.py:372" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 85 | 121 | Line too long (125 > 120) | +| 130 | 121 | Line too long (191 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py:85" +``` + +#### `deeplabcut\modelzoo\utils.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 201 | 121 | Line too long (134 > 120) | +| 208 | 121 | Line too long (134 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\utils.py:201" +``` + +#### `deeplabcut\modelzoo\video_inference.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 480 | 121 | Line too long (122 > 120) | +| 549 | 121 | Line too long (134 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\video_inference.py:480" +``` + +#### `deeplabcut\pose_estimation_pytorch\config\make_pose_config.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 78 | 121 | Line too long (132 > 120) | +| 79 | 121 | Line too long (217 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\config\make_pose_config.py:78" +``` + +#### `deeplabcut\pose_estimation_tensorflow\core\train_multianimal.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 92 | 121 | Line too long (122 > 120) | +| 208 | 121 | Line too long (123 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\core\train_multianimal.py:92" +``` + +#### `deeplabcut\pose_estimation_tensorflow\training.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 28 | 121 | Line too long (136 > 120) | +| 176 | 121 | Line too long (161 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\training.py:28" +``` + +#### `deeplabcut\pose_tracking_pytorch\model\backbones\vit_pytorch.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 332 | 121 | Line too long (121 > 120) | +| 346 | 121 | Line too long (126 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_tracking_pytorch\model\backbones\vit_pytorch.py:332" +``` + +#### `deeplabcut\pose_tracking_pytorch\processor\processor.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 131 | 121 | Line too long (154 > 120) | +| 143 | 121 | Line too long (143 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_tracking_pytorch\processor\processor.py:131" +``` + +#### `deeplabcut\utils\auxfun_models.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 50 | 121 | Line too long (167 > 120) | +| 157 | 121 | Line too long (126 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\auxfun_models.py:50" +``` + +#### `deeplabcut\utils\auxiliaryfunctions_3d.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 206 | 121 | Line too long (121 > 120) | +| 228 | 121 | Line too long (139 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\auxiliaryfunctions_3d.py:206" +``` + +#### `deeplabcut\utils\pseudo_label.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 397 | 121 | Line too long (145 > 120) | +| 399 | 121 | Line too long (133 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\pseudo_label.py:397" +``` + +#### `tests\pose_estimation_pytorch\other\test_match_predictions_to_gt.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 15 | 121 | Line too long (122 > 120) | +| 78 | 121 | Line too long (125 > 120) | + +Quick open commands: + +```powershell +code -g "tests\pose_estimation_pytorch\other\test_match_predictions_to_gt.py:15" +``` + +#### `testscript_cli.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 149 | 121 | Line too long (221 > 120) | +| 173 | 121 | Line too long (133 > 120) | + +Quick open commands: + +```powershell +code -g "testscript_cli.py:149" +``` + +#### `deeplabcut\__main__.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 29 | 121 | Line too long (127 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\__main__.py:29" +``` + +#### `deeplabcut\gui\tabs\extract_outlier_frames.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 162 | 121 | Line too long (223 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\gui\tabs\extract_outlier_frames.py:162" +``` + +#### `deeplabcut\gui\tabs\modelzoo.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 103 | 121 | Line too long (130 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\gui\tabs\modelzoo.py:103" +``` + +#### `deeplabcut\gui\tabs\refine_tracklets.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 231 | 121 | Line too long (223 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\gui\tabs\refine_tracklets.py:231" +``` + +#### `deeplabcut\gui\tabs\train_network.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 97 | 121 | Line too long (121 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\gui\tabs\train_network.py:97" +``` + +#### `deeplabcut\gui\widgets.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 529 | 121 | Line too long (223 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\gui\widgets.py:529" +``` + +#### `deeplabcut\modelzoo\weight_initialization.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 77 | 121 | Line too long (122 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\weight_initialization.py:77" +``` + +#### `deeplabcut\pose_estimation_pytorch\models\modules\conv_block.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 29 | 121 | Line too long (123 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\models\modules\conv_block.py:29" +``` + +#### `deeplabcut\pose_estimation_pytorch\models\necks\transformer.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 30 | 121 | Line too long (122 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\models\necks\transformer.py:30" +``` + +#### `deeplabcut\pose_estimation_pytorch\models\predictors\paf_predictor.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 36 | 121 | Line too long (134 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\models\predictors\paf_predictor.py:36" +``` + +#### `deeplabcut\pose_estimation_pytorch\post_processing\match_predictions_to_gt.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 108 | 121 | Line too long (134 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\post_processing\match_predictions_to_gt.py:108" +``` + +#### `deeplabcut\pose_estimation_tensorflow\core\train.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 151 | 121 | Line too long (139 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\core\train.py:151" +``` + +#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 413 | 121 | Line too long (124 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py:413" +``` + +#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_tensorpack.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 227 | 121 | Line too long (125 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_tensorpack.py:227" +``` + +#### `deeplabcut\post_processing\analyze_skeleton.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 101 | 121 | Line too long (133 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\post_processing\analyze_skeleton.py:101" +``` + +#### `deeplabcut\utils\visualization.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 144 | 121 | Line too long (131 > 120) | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\visualization.py:144" +``` + +#### `examples\JUPYTER\Demo_yourowndata.ipynb` (1) + +| Line | Col | Message | +|---:|---:|---| +| 16 | 121 | Line too long (123 > 120) | + +Quick open commands: + +```powershell +code -g "examples\JUPYTER\Demo_yourowndata.ipynb:16" +``` + +#### `examples\testscript_3d.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 149 | 121 | Line too long (126 > 120) | + +Quick open commands: + +```powershell +code -g "examples\testscript_3d.py:149" +``` + +#### `examples\testscript_deterministicwithResNet152.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 118 | 121 | Line too long (223 > 120) | + +Quick open commands: + +```powershell +code -g "examples\testscript_deterministicwithResNet152.py:118" +``` + +#### `examples\testscript_mobilenets.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 17 | 121 | Line too long (137 > 120) | + +Quick open commands: + +```powershell +code -g "examples\testscript_mobilenets.py:17" +``` + +#### `examples\testscript_openfielddata.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 29 | 121 | Line too long (142 > 120) | + +Quick open commands: + +```powershell +code -g "examples\testscript_openfielddata.py:29" +``` + +#### `examples\testscript_pretrained_models.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 32 | 121 | Line too long (144 > 120) | + +Quick open commands: + +```powershell +code -g "examples\testscript_pretrained_models.py:32" +``` + +## F401 + +Count: **331** +Hint: Unused import. Usually safe to delete; verify imports with side effects. + +### Files affected + +| File | Count | +|---|---:| +| `deeplabcut\__init__.py` | 66 | +| `deeplabcut\pose_estimation_pytorch\__init__.py` | 51 | +| `deeplabcut\pose_estimation_pytorch\apis\__init__.py` | 22 | +| `deeplabcut\pose_estimation_pytorch\data\__init__.py` | 19 | +| `deeplabcut\pose_estimation_pytorch\runners\__init__.py` | 18 | +| `deeplabcut\gui\tabs\__init__.py` | 15 | +| `deeplabcut\pose_estimation_pytorch\config\__init__.py` | 12 | +| `deeplabcut\pose_estimation_pytorch\models\modules\__init__.py` | 12 | +| `deeplabcut\pose_estimation_pytorch\models\criterions\__init__.py` | 11 | +| `deeplabcut\pose_estimation_pytorch\models\__init__.py` | 9 | +| `deeplabcut\pose_estimation_pytorch\models\backbones\__init__.py` | 8 | +| `deeplabcut\pose_estimation_pytorch\models\target_generators\__init__.py` | 8 | +| `deeplabcut\modelzoo\generalized_data_converter\datasets\__init__.py` | 7 | +| `deeplabcut\pose_estimation_pytorch\models\heads\__init__.py` | 7 | +| `deeplabcut\pose_estimation_pytorch\models\predictors\__init__.py` | 7 | +| `deeplabcut\create_project\__init__.py` | 6 | +| `deeplabcut\pose_estimation_pytorch\modelzoo\__init__.py` | 6 | +| `deeplabcut\core\metrics\__init__.py` | 4 | +| `deeplabcut\pose_estimation_pytorch\models\detectors\__init__.py` | 4 | +| `deeplabcut\pose_tracking_pytorch\processor\__init__.py` | 4 | +| `deeplabcut\generate_training_dataset\__init__.py` | 3 | +| `deeplabcut\modelzoo\generalized_data_converter\__init__.py` | 3 | +| `deeplabcut\pose_estimation_pytorch\models\necks\__init__.py` | 3 | +| `deeplabcut\pose_tracking_pytorch\tracking_utils\__init__.py` | 3 | +| `deeplabcut\gui\window.py` | 2 | +| `deeplabcut\pose_estimation_pytorch\post_processing\__init__.py` | 2 | +| `deeplabcut\pose_estimation_tensorflow\__init__.py` | 2 | +| `deeplabcut\pose_tracking_pytorch\__init__.py` | 2 | +| `deeplabcut\pose_tracking_pytorch\model\__init__.py` | 2 | +| `deeplabcut\__main__.py` | 1 | +| `deeplabcut\gui\__init__.py` | 1 | +| `deeplabcut\gui\tabs\create_training_dataset.py` | 1 | +| `deeplabcut\modelzoo\__init__.py` | 1 | +| `deeplabcut\modelzoo\generalized_data_converter\conversion_table\__init__.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\lib\__init__.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\modelzoo\__init__.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\modelzoo\api\__init__.py` | 1 | +| `deeplabcut\pose_tracking_pytorch\datasets\__init__.py` | 1 | +| `deeplabcut\pose_tracking_pytorch\loss\__init__.py` | 1 | +| `deeplabcut\pose_tracking_pytorch\model\backbones\__init__.py` | 1 | +| `deeplabcut\pose_tracking_pytorch\solver\__init__.py` | 1 | +| `deeplabcut\post_processing\__init__.py` | 1 | + +### Details + +#### `deeplabcut\__init__.py` (66) + +| Line | Col | Message | +|---:|---:|---| +| 16 | 41 | `deeplabcut.version.__version__` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 21 | 46 | `deeplabcut.gui.launch_script.launch_dlc` imported but unused; consider using `importlib.util.find_spec` to test for availability | +| 23 | 9 | `deeplabcut.gui.tabs.label_frames.label_frames` imported but unused; consider using `importlib.util.find_spec` to test for availability | +| 24 | 9 | `deeplabcut.gui.tabs.label_frames.refine_labels` imported but unused; consider using `importlib.util.find_spec` to test for availability | +| 26 | 49 | `deeplabcut.gui.tracklet_toolbox.refine_tracklets` imported but unused; consider using `importlib.util.find_spec` to test for availability | +| 27 | 40 | `deeplabcut.gui.widgets.SkeletonBuilder` imported but unused; consider using `importlib.util.find_spec` to test for availability | +| 31 | 36 | `deeplabcut.core.engine.Engine` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 33 | 5 | `deeplabcut.create_project.add_new_videos` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 34 | 5 | `deeplabcut.create_project.create_new_project` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 35 | 5 | `deeplabcut.create_project.create_new_project_3d` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 36 | 5 | `deeplabcut.create_project.create_pretrained_human_project` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 37 | 5 | `deeplabcut.create_project.create_pretrained_project` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 38 | 5 | `deeplabcut.create_project.load_demo_data` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 41 | 5 | `deeplabcut.generate_training_dataset.adddatasetstovideolistandviceversa` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 42 | 5 | `deeplabcut.generate_training_dataset.check_labels` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 43 | 5 | `deeplabcut.generate_training_dataset.comparevideolistsanddatafolders` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 44 | 5 | `deeplabcut.generate_training_dataset.create_multianimaltraining_dataset` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 45 | 5 | `deeplabcut.generate_training_dataset.create_training_dataset` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 46 | 5 | `deeplabcut.generate_training_dataset.create_training_dataset_from_existing_split` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 47 | 5 | `deeplabcut.generate_training_dataset.create_training_model_comparison` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 48 | 5 | `deeplabcut.generate_training_dataset.dropannotationfileentriesduetodeletedimages` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 49 | 5 | `deeplabcut.generate_training_dataset.dropduplicatesinannotatinfiles` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 50 | 5 | `deeplabcut.generate_training_dataset.dropimagesduetolackofannotation` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 51 | 5 | `deeplabcut.generate_training_dataset.dropunlabeledframes` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 52 | 5 | `deeplabcut.generate_training_dataset.extract_frames` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 53 | 5 | `deeplabcut.generate_training_dataset.mergeandsplit` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 55 | 49 | `deeplabcut.modelzoo.video_inference.video_inference_superanimal` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 57 | 5 | `deeplabcut.utils.analyze_videos_converth5_to_csv` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 58 | 5 | `deeplabcut.utils.analyze_videos_converth5_to_nwb` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 59 | 5 | `deeplabcut.utils.auxfun_videos` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 60 | 5 | `deeplabcut.utils.auxiliaryfunctions` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 61 | 5 | `deeplabcut.utils.convert2_maDLC` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 62 | 5 | `deeplabcut.utils.convertcsv2h5` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 63 | 5 | `deeplabcut.utils.create_labeled_video` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 64 | 5 | `deeplabcut.utils.create_video_with_all_detections` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 65 | 5 | `deeplabcut.utils.plot_trajectories` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 69 | 50 | `deeplabcut.pose_tracking_pytorch.transformer_reID` imported but unused; consider using `importlib.util.find_spec` to test for availability | +| 82 | 5 | `deeplabcut.compat.analyze_images` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 83 | 5 | `deeplabcut.compat.analyze_time_lapse_frames` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 84 | 5 | `deeplabcut.compat.analyze_videos` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 85 | 5 | `deeplabcut.compat.convert_detections2tracklets` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 86 | 5 | `deeplabcut.compat.create_tracking_dataset` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 87 | 5 | `deeplabcut.compat.evaluate_network` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 88 | 5 | `deeplabcut.compat.export_model` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 89 | 5 | `deeplabcut.compat.extract_maps` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 90 | 5 | `deeplabcut.compat.extract_save_all_maps` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 91 | 5 | `deeplabcut.compat.return_evaluate_network_data` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 92 | 5 | `deeplabcut.compat.return_train_network_path` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 93 | 5 | `deeplabcut.compat.train_network` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 94 | 5 | `deeplabcut.compat.visualize_locrefs` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 95 | 5 | `deeplabcut.compat.visualize_paf` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 96 | 5 | `deeplabcut.compat.visualize_scoremaps` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 99 | 5 | `deeplabcut.pose_estimation_3d.calibrate_cameras` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 100 | 5 | `deeplabcut.pose_estimation_3d.check_undistortion` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 101 | 5 | `deeplabcut.pose_estimation_3d.create_labeled_video_3d` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 102 | 5 | `deeplabcut.pose_estimation_3d.triangulate` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 104 | 40 | `deeplabcut.post_processing.analyzeskeleton` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 104 | 57 | `deeplabcut.post_processing.filterpredictions` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 106 | 5 | `deeplabcut.refine_training_dataset.extract_outlier_frames` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 107 | 5 | `deeplabcut.refine_training_dataset.find_outliers_in_raw_data` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 108 | 5 | `deeplabcut.refine_training_dataset.merge_datasets` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 110 | 55 | `deeplabcut.refine_training_dataset.stitch.stitch_tracklets` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 112 | 5 | `deeplabcut.utils.auxfun_videos.CropVideo` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 113 | 5 | `deeplabcut.utils.auxfun_videos.DownSampleVideo` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 114 | 5 | `deeplabcut.utils.auxfun_videos.ShortenVideo` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 115 | 5 | `deeplabcut.utils.auxfun_videos.check_video_integrity` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\__init__.py:16" +``` + +#### `deeplabcut\pose_estimation_pytorch\__init__.py` (51) + +| Line | Col | Message | +|---:|---:|---| +| 11 | 53 | `deeplabcut.pose_estimation_pytorch.config` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 13 | 5 | `deeplabcut.pose_estimation_pytorch.apis.VideoIterator` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 14 | 5 | `deeplabcut.pose_estimation_pytorch.apis.analyze_image_folder` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 15 | 5 | `deeplabcut.pose_estimation_pytorch.apis.analyze_images` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 16 | 5 | `deeplabcut.pose_estimation_pytorch.apis.analyze_videos` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 17 | 5 | `deeplabcut.pose_estimation_pytorch.apis.build_predictions_dataframe` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 18 | 5 | `deeplabcut.pose_estimation_pytorch.apis.convert_detections2tracklets` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 19 | 5 | `deeplabcut.pose_estimation_pytorch.apis.create_labeled_images` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 20 | 5 | `deeplabcut.pose_estimation_pytorch.apis.create_tracking_dataset` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 21 | 5 | `deeplabcut.pose_estimation_pytorch.apis.evaluate` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 22 | 5 | `deeplabcut.pose_estimation_pytorch.apis.evaluate_network` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 23 | 5 | `deeplabcut.pose_estimation_pytorch.apis.extract_maps` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 24 | 5 | `deeplabcut.pose_estimation_pytorch.apis.extract_save_all_maps` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 25 | 5 | `deeplabcut.pose_estimation_pytorch.apis.get_detector_inference_runner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 26 | 5 | `deeplabcut.pose_estimation_pytorch.apis.get_pose_inference_runner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 27 | 5 | `deeplabcut.pose_estimation_pytorch.apis.predict` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 28 | 5 | `deeplabcut.pose_estimation_pytorch.apis.superanimal_analyze_images` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 29 | 5 | `deeplabcut.pose_estimation_pytorch.apis.train` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 30 | 5 | `deeplabcut.pose_estimation_pytorch.apis.train_network` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 31 | 5 | `deeplabcut.pose_estimation_pytorch.apis.video_inference` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 32 | 5 | `deeplabcut.pose_estimation_pytorch.apis.visualize_predictions` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 35 | 5 | `deeplabcut.pose_estimation_pytorch.config.available_detectors` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 36 | 5 | `deeplabcut.pose_estimation_pytorch.config.available_models` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 37 | 5 | `deeplabcut.pose_estimation_pytorch.config.is_model_cond_top_down` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 38 | 5 | `deeplabcut.pose_estimation_pytorch.config.is_model_top_down` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 41 | 5 | `deeplabcut.pose_estimation_pytorch.data.COLLATE_FUNCTIONS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 42 | 5 | `deeplabcut.pose_estimation_pytorch.data.COCOLoader` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 43 | 5 | `deeplabcut.pose_estimation_pytorch.data.DLCLoader` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 44 | 5 | `deeplabcut.pose_estimation_pytorch.data.GenerativeSampler` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 45 | 5 | `deeplabcut.pose_estimation_pytorch.data.GenSamplingConfig` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 46 | 5 | `deeplabcut.pose_estimation_pytorch.data.Loader` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 47 | 5 | `deeplabcut.pose_estimation_pytorch.data.PoseDataset` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 48 | 5 | `deeplabcut.pose_estimation_pytorch.data.PoseDatasetParameters` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 49 | 5 | `deeplabcut.pose_estimation_pytorch.data.Snapshot` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 50 | 5 | `deeplabcut.pose_estimation_pytorch.data.build_transforms` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 51 | 5 | `deeplabcut.pose_estimation_pytorch.data.list_snapshots` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 54 | 5 | `deeplabcut.pose_estimation_pytorch.runners.DetectorInferenceRunner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 55 | 5 | `deeplabcut.pose_estimation_pytorch.runners.DetectorTrainingRunner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 56 | 5 | `deeplabcut.pose_estimation_pytorch.runners.DynamicCropper` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 57 | 5 | `deeplabcut.pose_estimation_pytorch.runners.InferenceRunner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 58 | 5 | `deeplabcut.pose_estimation_pytorch.runners.PoseInferenceRunner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 59 | 5 | `deeplabcut.pose_estimation_pytorch.runners.PoseTrainingRunner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 60 | 5 | `deeplabcut.pose_estimation_pytorch.runners.TopDownDynamicCropper` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 61 | 5 | `deeplabcut.pose_estimation_pytorch.runners.TorchSnapshotManager` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 62 | 5 | `deeplabcut.pose_estimation_pytorch.runners.TrainingRunner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 63 | 5 | `deeplabcut.pose_estimation_pytorch.runners.build_inference_runner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 64 | 5 | `deeplabcut.pose_estimation_pytorch.runners.build_training_runner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 65 | 5 | `deeplabcut.pose_estimation_pytorch.runners.get_load_weights_only` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 66 | 5 | `deeplabcut.pose_estimation_pytorch.runners.set_load_weights_only` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 68 | 53 | `deeplabcut.pose_estimation_pytorch.task.Task` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 69 | 54 | `deeplabcut.pose_estimation_pytorch.utils.fix_seeds` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\__init__.py:11" +``` + +#### `deeplabcut\pose_estimation_pytorch\apis\__init__.py` (22) + +| Line | Col | Message | +|---:|---:|---| +| 13 | 5 | `deeplabcut.pose_estimation_pytorch.apis.analyze_images.analyze_image_folder` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 14 | 5 | `deeplabcut.pose_estimation_pytorch.apis.analyze_images.analyze_images` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 15 | 5 | `deeplabcut.pose_estimation_pytorch.apis.analyze_images.superanimal_analyze_images` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 18 | 5 | `deeplabcut.pose_estimation_pytorch.apis.evaluation.evaluate` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 19 | 5 | `deeplabcut.pose_estimation_pytorch.apis.evaluation.evaluate_network` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 20 | 5 | `deeplabcut.pose_estimation_pytorch.apis.evaluation.predict` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 21 | 5 | `deeplabcut.pose_estimation_pytorch.apis.evaluation.visualize_predictions` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 23 | 60 | `deeplabcut.pose_estimation_pytorch.apis.export.export_model` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 25 | 5 | `deeplabcut.pose_estimation_pytorch.apis.tracking_dataset.create_tracking_dataset` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 28 | 5 | `deeplabcut.pose_estimation_pytorch.apis.tracklets.convert_detections2tracklets` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 31 | 5 | `deeplabcut.pose_estimation_pytorch.apis.training.train` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 32 | 5 | `deeplabcut.pose_estimation_pytorch.apis.training.train_network` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 35 | 5 | `deeplabcut.pose_estimation_pytorch.apis.utils.build_predictions_dataframe` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 36 | 5 | `deeplabcut.pose_estimation_pytorch.apis.utils.get_detector_inference_runner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 37 | 5 | `deeplabcut.pose_estimation_pytorch.apis.utils.get_inference_runners` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 38 | 5 | `deeplabcut.pose_estimation_pytorch.apis.utils.get_pose_inference_runner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 41 | 5 | `deeplabcut.pose_estimation_pytorch.apis.videos.VideoIterator` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 42 | 5 | `deeplabcut.pose_estimation_pytorch.apis.videos.analyze_videos` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 43 | 5 | `deeplabcut.pose_estimation_pytorch.apis.videos.video_inference` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 46 | 5 | `deeplabcut.pose_estimation_pytorch.apis.visualization.create_labeled_images` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 47 | 5 | `deeplabcut.pose_estimation_pytorch.apis.visualization.extract_maps` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 48 | 5 | `deeplabcut.pose_estimation_pytorch.apis.visualization.extract_save_all_maps` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\apis\__init__.py:13" +``` + +#### `deeplabcut\pose_estimation_pytorch\data\__init__.py` (19) + +| Line | Col | Message | +|---:|---:|---| +| 11 | 58 | `deeplabcut.pose_estimation_pytorch.data.base.Loader` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 12 | 64 | `deeplabcut.pose_estimation_pytorch.data.cocoloader.COCOLoader` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 13 | 61 | `deeplabcut.pose_estimation_pytorch.data.collate.COLLATE_FUNCTIONS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 15 | 5 | `deeplabcut.pose_estimation_pytorch.data.dataset.PoseDataset` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 16 | 5 | `deeplabcut.pose_estimation_pytorch.data.dataset.PoseDatasetParameters` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 18 | 63 | `deeplabcut.pose_estimation_pytorch.data.dlcloader.DLCLoader` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 20 | 5 | `deeplabcut.pose_estimation_pytorch.data.generative_sampling.GenerativeSampler` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 21 | 5 | `deeplabcut.pose_estimation_pytorch.data.generative_sampling.GenSamplingConfig` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 23 | 59 | `deeplabcut.pose_estimation_pytorch.data.image.top_down_crop` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 25 | 5 | `deeplabcut.pose_estimation_pytorch.data.postprocessor.Postprocessor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 26 | 5 | `deeplabcut.pose_estimation_pytorch.data.postprocessor.build_bottom_up_postprocessor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 27 | 5 | `deeplabcut.pose_estimation_pytorch.data.postprocessor.build_detector_postprocessor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 28 | 5 | `deeplabcut.pose_estimation_pytorch.data.postprocessor.build_top_down_postprocessor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 31 | 5 | `deeplabcut.pose_estimation_pytorch.data.preprocessor.Preprocessor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 32 | 5 | `deeplabcut.pose_estimation_pytorch.data.preprocessor.build_bottom_up_preprocessor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 33 | 5 | `deeplabcut.pose_estimation_pytorch.data.preprocessor.build_top_down_preprocessor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 35 | 63 | `deeplabcut.pose_estimation_pytorch.data.snapshots.Snapshot` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 35 | 73 | `deeplabcut.pose_estimation_pytorch.data.snapshots.list_snapshots` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 36 | 64 | `deeplabcut.pose_estimation_pytorch.data.transforms.build_transforms` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\data\__init__.py:11" +``` + +#### `deeplabcut\pose_estimation_pytorch\runners\__init__.py` (18) + +| Line | Col | Message | +|---:|---:|---| +| 13 | 5 | `deeplabcut.pose_estimation_pytorch.runners.base.Runner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 14 | 5 | `deeplabcut.pose_estimation_pytorch.runners.base.attempt_snapshot_load` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 15 | 5 | `deeplabcut.pose_estimation_pytorch.runners.base.fix_snapshot_metadata` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 16 | 5 | `deeplabcut.pose_estimation_pytorch.runners.base.get_load_weights_only` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 17 | 5 | `deeplabcut.pose_estimation_pytorch.runners.base.set_load_weights_only` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 19 | 60 | `deeplabcut.pose_estimation_pytorch.runners.ctd.CTDTrackingConfig` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 21 | 5 | `deeplabcut.pose_estimation_pytorch.runners.dynamic_cropping.DynamicCropper` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 22 | 5 | `deeplabcut.pose_estimation_pytorch.runners.dynamic_cropping.TopDownDynamicCropper` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 25 | 5 | `deeplabcut.pose_estimation_pytorch.runners.inference.DetectorInferenceRunner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 26 | 5 | `deeplabcut.pose_estimation_pytorch.runners.inference.InferenceRunner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 27 | 5 | `deeplabcut.pose_estimation_pytorch.runners.inference.PoseInferenceRunner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 28 | 5 | `deeplabcut.pose_estimation_pytorch.runners.inference.build_inference_runner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 30 | 63 | `deeplabcut.pose_estimation_pytorch.runners.logger.LOGGER` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 31 | 66 | `deeplabcut.pose_estimation_pytorch.runners.snapshots.TorchSnapshotManager` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 33 | 5 | `deeplabcut.pose_estimation_pytorch.runners.train.DetectorTrainingRunner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 34 | 5 | `deeplabcut.pose_estimation_pytorch.runners.train.PoseTrainingRunner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 35 | 5 | `deeplabcut.pose_estimation_pytorch.runners.train.TrainingRunner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 36 | 5 | `deeplabcut.pose_estimation_pytorch.runners.train.build_training_runner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\runners\__init__.py:13" +``` + +#### `deeplabcut\gui\tabs\__init__.py` (15) + +| Line | Col | Message | +|---:|---:|---| +| 11 | 48 | `deeplabcut.gui.tabs.analyze_videos.AnalyzeVideos` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 12 | 48 | `deeplabcut.gui.tabs.create_project.ProjectCreator` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 13 | 57 | `deeplabcut.gui.tabs.create_training_dataset.CreateTrainingDataset` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 14 | 47 | `deeplabcut.gui.tabs.create_videos.CreateVideos` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 15 | 50 | `deeplabcut.gui.tabs.evaluate_network.EvaluateNetwork` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 16 | 48 | `deeplabcut.gui.tabs.extract_frames.ExtractFrames` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 17 | 56 | `deeplabcut.gui.tabs.extract_outlier_frames.ExtractOutlierFrames` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 18 | 46 | `deeplabcut.gui.tabs.label_frames.LabelFrames` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 19 | 48 | `deeplabcut.gui.tabs.manage_project.ManageProject` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 20 | 42 | `deeplabcut.gui.tabs.modelzoo.ModelZoo` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 21 | 46 | `deeplabcut.gui.tabs.open_project.OpenProject` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 22 | 50 | `deeplabcut.gui.tabs.refine_tracklets.RefineTracklets` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 23 | 47 | `deeplabcut.gui.tabs.train_network.TrainNetwork` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 24 | 58 | `deeplabcut.gui.tabs.unsupervised_id_tracking.UnsupervizedIdTracking` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 25 | 46 | `deeplabcut.gui.tabs.video_editor.VideoEditor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\gui\tabs\__init__.py:11" +``` + +#### `deeplabcut\pose_estimation_pytorch\config\__init__.py` (12) + +| Line | Col | Message | +|---:|---:|---| +| 13 | 5 | `deeplabcut.core.config.pretty_print` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 14 | 5 | `deeplabcut.core.config.read_config_as_dict` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 15 | 5 | `deeplabcut.core.config.write_config` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 18 | 5 | `deeplabcut.pose_estimation_pytorch.config.make_pose_config.make_basic_project_config` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 19 | 5 | `deeplabcut.pose_estimation_pytorch.config.make_pose_config.make_pytorch_pose_config` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 20 | 5 | `deeplabcut.pose_estimation_pytorch.config.make_pose_config.make_pytorch_test_config` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 23 | 5 | `deeplabcut.pose_estimation_pytorch.config.utils.available_detectors` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 24 | 5 | `deeplabcut.pose_estimation_pytorch.config.utils.available_models` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 25 | 5 | `deeplabcut.pose_estimation_pytorch.config.utils.is_model_cond_top_down` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 26 | 5 | `deeplabcut.pose_estimation_pytorch.config.utils.is_model_top_down` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 27 | 5 | `deeplabcut.pose_estimation_pytorch.config.utils.update_config` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 28 | 5 | `deeplabcut.pose_estimation_pytorch.config.utils.update_config_by_dotpath` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\config\__init__.py:13" +``` + +#### `deeplabcut\pose_estimation_pytorch\models\modules\__init__.py` (12) + +| Line | Col | Message | +|---:|---:|---| +| 11 | 75 | `deeplabcut.pose_estimation_pytorch.models.modules.coam_module.CoAMBlock` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 11 | 86 | `deeplabcut.pose_estimation_pytorch.models.modules.coam_module.SelfAttentionModule_CoAM` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 13 | 5 | `deeplabcut.pose_estimation_pytorch.models.modules.conv_block.AdaptBlock` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 14 | 5 | `deeplabcut.pose_estimation_pytorch.models.modules.conv_block.BasicBlock` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 15 | 5 | `deeplabcut.pose_estimation_pytorch.models.modules.conv_block.Bottleneck` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 18 | 5 | `deeplabcut.pose_estimation_pytorch.models.modules.conv_module.HighResolutionModule` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 21 | 5 | `deeplabcut.pose_estimation_pytorch.models.modules.gated_attention_unit.GatedAttentionUnit` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 24 | 5 | `deeplabcut.pose_estimation_pytorch.models.modules.kpt_encoders.KEYPOINT_ENCODERS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 25 | 5 | `deeplabcut.pose_estimation_pytorch.models.modules.kpt_encoders.BaseKeypointEncoder` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 26 | 5 | `deeplabcut.pose_estimation_pytorch.models.modules.kpt_encoders.ColoredKeypointEncoder` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 27 | 5 | `deeplabcut.pose_estimation_pytorch.models.modules.kpt_encoders.StackedKeypointEncoder` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 30 | 5 | `deeplabcut.pose_estimation_pytorch.models.modules.norm.ScaleNorm` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\models\modules\__init__.py:11" +``` + +#### `deeplabcut\pose_estimation_pytorch\models\criterions\__init__.py` (11) + +| Line | Col | Message | +|---:|---:|---| +| 12 | 5 | `deeplabcut.pose_estimation_pytorch.models.criterions.aggregators.WeightedLossAggregator` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 15 | 5 | `deeplabcut.pose_estimation_pytorch.models.criterions.base.CRITERIONS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 16 | 5 | `deeplabcut.pose_estimation_pytorch.models.criterions.base.LOSS_AGGREGATORS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 17 | 5 | `deeplabcut.pose_estimation_pytorch.models.criterions.base.BaseCriterion` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 18 | 5 | `deeplabcut.pose_estimation_pytorch.models.criterions.base.BaseLossAggregator` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 21 | 5 | `deeplabcut.pose_estimation_pytorch.models.criterions.dekr.DEKRHeatmapLoss` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 22 | 5 | `deeplabcut.pose_estimation_pytorch.models.criterions.dekr.DEKROffsetLoss` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 25 | 5 | `deeplabcut.pose_estimation_pytorch.models.criterions.kl_discrete.KLDiscreteLoss` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 28 | 5 | `deeplabcut.pose_estimation_pytorch.models.criterions.weighted.WeightedBCECriterion` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 29 | 5 | `deeplabcut.pose_estimation_pytorch.models.criterions.weighted.WeightedHuberCriterion` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 30 | 5 | `deeplabcut.pose_estimation_pytorch.models.criterions.weighted.WeightedMSECriterion` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\models\criterions\__init__.py:12" +``` + +#### `deeplabcut\pose_estimation_pytorch\models\__init__.py` (9) + +| Line | Col | Message | +|---:|---:|---| +| 11 | 70 | `deeplabcut.pose_estimation_pytorch.models.backbones.base.BACKBONES` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 13 | 5 | `deeplabcut.pose_estimation_pytorch.models.criterions.CRITERIONS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 14 | 5 | `deeplabcut.pose_estimation_pytorch.models.criterions.LOSS_AGGREGATORS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 16 | 65 | `deeplabcut.pose_estimation_pytorch.models.detectors.DETECTORS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 17 | 66 | `deeplabcut.pose_estimation_pytorch.models.heads.base.HEADS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 18 | 61 | `deeplabcut.pose_estimation_pytorch.models.model.PoseModel` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 19 | 66 | `deeplabcut.pose_estimation_pytorch.models.necks.base.NECKS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 20 | 66 | `deeplabcut.pose_estimation_pytorch.models.predictors.PREDICTORS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 22 | 5 | `deeplabcut.pose_estimation_pytorch.models.target_generators.TARGET_GENERATORS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\models\__init__.py:11" +``` + +#### `deeplabcut\pose_estimation_pytorch\models\backbones\__init__.py` (8) + +| Line | Col | Message | +|---:|---:|---| +| 12 | 5 | `deeplabcut.pose_estimation_pytorch.models.backbones.base.BACKBONES` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 13 | 5 | `deeplabcut.pose_estimation_pytorch.models.backbones.base.BaseBackbone` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 15 | 77 | `deeplabcut.pose_estimation_pytorch.models.backbones.cond_prenet.CondPreNet` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 16 | 73 | `deeplabcut.pose_estimation_pytorch.models.backbones.cspnext.CSPNeXt` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 17 | 71 | `deeplabcut.pose_estimation_pytorch.models.backbones.hrnet.HRNet` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 18 | 76 | `deeplabcut.pose_estimation_pytorch.models.backbones.hrnet_coam.HRNetCoAM` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 19 | 72 | `deeplabcut.pose_estimation_pytorch.models.backbones.resnet.DLCRNet` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 19 | 81 | `deeplabcut.pose_estimation_pytorch.models.backbones.resnet.ResNet` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\models\backbones\__init__.py:12" +``` + +#### `deeplabcut\pose_estimation_pytorch\models\target_generators\__init__.py` (8) + +| Line | Col | Message | +|---:|---:|---| +| 12 | 5 | `deeplabcut.pose_estimation_pytorch.models.target_generators.base.TARGET_GENERATORS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 13 | 5 | `deeplabcut.pose_estimation_pytorch.models.target_generators.base.BaseGenerator` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 14 | 5 | `deeplabcut.pose_estimation_pytorch.models.target_generators.base.SequentialGenerator` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 17 | 5 | `deeplabcut.pose_estimation_pytorch.models.target_generators.dekr_targets.DEKRGenerator` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 20 | 5 | `deeplabcut.pose_estimation_pytorch.models.target_generators.heatmap_targets.HeatmapGaussianGenerator` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 21 | 5 | `deeplabcut.pose_estimation_pytorch.models.target_generators.heatmap_targets.HeatmapPlateauGenerator` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 24 | 5 | `deeplabcut.pose_estimation_pytorch.models.target_generators.pafs_targets.PartAffinityFieldGenerator` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 27 | 5 | `deeplabcut.pose_estimation_pytorch.models.target_generators.sim_cc.SimCCGenerator` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\models\target_generators\__init__.py:12" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\datasets\__init__.py` (7) + +| Line | Col | Message | +|---:|---:|---| +| 11 | 19 | `.coco.COCOPoseDataset` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 12 | 21 | `.ma_dlc.MaDLCPoseDataset` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 13 | 31 | `.ma_dlc_dataframe.MaDLCDataFrame` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 14 | 26 | `.materialize.mat_func_factory` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 15 | 20 | `.multi.MultiSourceDataset` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 16 | 25 | `.single_dlc.SingleDLCPoseDataset` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 17 | 35 | `.single_dlc_dataframe.SingleDLCDataFrame` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\__init__.py:11" +``` + +#### `deeplabcut\pose_estimation_pytorch\models\heads\__init__.py` (7) + +| Line | Col | Message | +|---:|---:|---| +| 11 | 66 | `deeplabcut.pose_estimation_pytorch.models.heads.base.HEADS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 11 | 73 | `deeplabcut.pose_estimation_pytorch.models.heads.base.BaseHead` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 12 | 66 | `deeplabcut.pose_estimation_pytorch.models.heads.dekr.DEKRHead` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 13 | 69 | `deeplabcut.pose_estimation_pytorch.models.heads.dlcrnet.DLCRNetHead` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 14 | 72 | `deeplabcut.pose_estimation_pytorch.models.heads.rtmcc_head.RTMCCHead` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 15 | 73 | `deeplabcut.pose_estimation_pytorch.models.heads.simple_head.HeatmapHead` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 16 | 73 | `deeplabcut.pose_estimation_pytorch.models.heads.transformer.TransformerHead` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\models\heads\__init__.py:11" +``` + +#### `deeplabcut\pose_estimation_pytorch\models\predictors\__init__.py` (7) + +| Line | Col | Message | +|---:|---:|---| +| 12 | 5 | `deeplabcut.pose_estimation_pytorch.models.predictors.base.PREDICTORS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 13 | 5 | `deeplabcut.pose_estimation_pytorch.models.predictors.base.BasePredictor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 16 | 5 | `deeplabcut.pose_estimation_pytorch.models.predictors.dekr_predictor.DEKRPredictor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 19 | 5 | `deeplabcut.pose_estimation_pytorch.models.predictors.identity_predictor.IdentityPredictor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 22 | 5 | `deeplabcut.pose_estimation_pytorch.models.predictors.paf_predictor.PartAffinityFieldPredictor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 25 | 5 | `deeplabcut.pose_estimation_pytorch.models.predictors.sim_cc.SimCCPredictor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 28 | 5 | `deeplabcut.pose_estimation_pytorch.models.predictors.single_predictor.HeatmapPredictor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\models\predictors\__init__.py:12" +``` + +#### `deeplabcut\create_project\__init__.py` (6) + +| Line | Col | Message | +|---:|---:|---| +| 11 | 43 | `deeplabcut.create_project.add.add_new_videos` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 12 | 49 | `deeplabcut.create_project.demo_data.load_demo_data` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 14 | 5 | `deeplabcut.create_project.modelzoo.create_pretrained_human_project` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 15 | 5 | `deeplabcut.create_project.modelzoo.create_pretrained_project` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 17 | 43 | `deeplabcut.create_project.new.create_new_project` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 18 | 46 | `deeplabcut.create_project.new_3d.create_new_project_3d` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\create_project\__init__.py:11" +``` + +#### `deeplabcut\pose_estimation_pytorch\modelzoo\__init__.py` (6) + +| Line | Col | Message | +|---:|---:|---| +| 12 | 5 | `deeplabcut.pose_estimation_pytorch.modelzoo.utils.download_super_animal_snapshot` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 13 | 5 | `deeplabcut.pose_estimation_pytorch.modelzoo.utils.get_snapshot_folder_path` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 14 | 5 | `deeplabcut.pose_estimation_pytorch.modelzoo.utils.get_super_animal_model_config_path` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 15 | 5 | `deeplabcut.pose_estimation_pytorch.modelzoo.utils.get_super_animal_project_config_path` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 16 | 5 | `deeplabcut.pose_estimation_pytorch.modelzoo.utils.get_super_animal_snapshot_path` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 17 | 5 | `deeplabcut.pose_estimation_pytorch.modelzoo.utils.load_super_animal_config` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\modelzoo\__init__.py:12" +``` + +#### `deeplabcut\core\metrics\__init__.py` (4) + +| Line | Col | Message | +|---:|---:|---| +| 11 | 18 | `.api.compute_metrics` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 11 | 35 | `.api.prepare_evaluation_data` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 12 | 19 | `.bbox.compute_bbox_metrics` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 13 | 23 | `.identity.compute_identity_scores` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\core\metrics\__init__.py:11" +``` + +#### `deeplabcut\pose_estimation_pytorch\models\detectors\__init__.py` (4) + +| Line | Col | Message | +|---:|---:|---| +| 12 | 5 | `deeplabcut.pose_estimation_pytorch.models.detectors.base.DETECTORS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 13 | 5 | `deeplabcut.pose_estimation_pytorch.models.detectors.base.BaseDetector` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 15 | 76 | `deeplabcut.pose_estimation_pytorch.models.detectors.fasterRCNN.FasterRCNN` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 16 | 69 | `deeplabcut.pose_estimation_pytorch.models.detectors.ssd.SSDLite` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\models\detectors\__init__.py:12" +``` + +#### `deeplabcut\pose_tracking_pytorch\processor\__init__.py` (4) + +| Line | Col | Message | +|---:|---:|---| +| 13 | 5 | `.processor.default_device` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 14 | 5 | `.processor.do_dlc_inference` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 15 | 5 | `.processor.do_dlc_pair_inference` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 16 | 5 | `.processor.do_dlc_train` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_tracking_pytorch\processor\__init__.py:13" +``` + +#### `deeplabcut\generate_training_dataset\__init__.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 15 | 5 | `deeplabcut.generate_training_dataset.metadata.DataSplit` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 16 | 5 | `deeplabcut.generate_training_dataset.metadata.ShuffleMetadata` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 17 | 5 | `deeplabcut.generate_training_dataset.metadata.TrainingDatasetMetadata` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\generate_training_dataset\__init__.py:15" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\__init__.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 11 | 20 | `.utils.add_skeleton` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 11 | 34 | `.utils.create_modelprefix` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 11 | 54 | `.utils.customized_colormap` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\__init__.py:11" +``` + +#### `deeplabcut\pose_estimation_pytorch\models\necks\__init__.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 11 | 66 | `deeplabcut.pose_estimation_pytorch.models.necks.base.NECKS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 11 | 73 | `deeplabcut.pose_estimation_pytorch.models.necks.base.BaseNeck` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 12 | 73 | `deeplabcut.pose_estimation_pytorch.models.necks.transformer.Transformer` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\models\necks\__init__.py:11" +``` + +#### `deeplabcut\pose_tracking_pytorch\tracking_utils\__init__.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 12 | 5 | `.preprocessing.convert_coord_from_img_space_to_feature_space` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 13 | 5 | `.preprocessing.load_features_from_coord` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 14 | 5 | `.preprocessing.query_feature_by_coord_in_img_space` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_tracking_pytorch\tracking_utils\__init__.py:12" +``` + +#### `deeplabcut\gui\window.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 196 | 24 | `tensorflow` imported but unused; consider using `importlib.util.find_spec` to test for availability | +| 696 | 62 | `deeplabcut.pose_tracking_pytorch.transformer_reID` imported but unused; consider using `importlib.util.find_spec` to test for availability | + +Quick open commands: + +```powershell +code -g "deeplabcut\gui\window.py:196" +``` + +#### `deeplabcut\pose_estimation_pytorch\post_processing\__init__.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 12 | 5 | `deeplabcut.pose_estimation_pytorch.post_processing.match_predictions_to_gt.oks_match_prediction_to_gt` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 13 | 5 | `deeplabcut.pose_estimation_pytorch.post_processing.match_predictions_to_gt.rmse_match_prediction_to_gt` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\post_processing\__init__.py:12" +``` + +#### `deeplabcut\pose_estimation_tensorflow\__init__.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 18 | 15 | `._tf_legacy` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 28 | 58 | `deeplabcut.pose_estimation_tensorflow.export.export_model` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\__init__.py:18" +``` + +#### `deeplabcut\pose_tracking_pytorch\__init__.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 12 | 19 | `.apis.transformer_reID` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 15 | 33 | `.train_dlctransreid.train_tracking_transformer` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_tracking_pytorch\__init__.py:12" +``` + +#### `deeplabcut\pose_tracking_pytorch\model\__init__.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 12 | 25 | `.make_model.build_dlc_transformer` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | +| 12 | 48 | `.make_model.make_dlc_model` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_tracking_pytorch\model\__init__.py:12" +``` + +#### `deeplabcut\__main__.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 15 | 16 | `PySide6` imported but unused; consider using `importlib.util.find_spec` to test for availability | + +Quick open commands: + +```powershell +code -g "deeplabcut\__main__.py:15" +``` + +#### `deeplabcut\gui\__init__.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 15 | 8 | `qtpy` imported but unused | + +Quick open commands: + +```powershell +code -g "deeplabcut\gui\__init__.py:15" +``` + +#### `deeplabcut\gui\tabs\create_training_dataset.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 248 | 28 | `tensorflow` imported but unused; consider using `importlib.util.find_spec` to test for availability | + +Quick open commands: + +```powershell +code -g "deeplabcut\gui\tabs\create_training_dataset.py:248" +``` + +#### `deeplabcut\modelzoo\__init__.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 11 | 55 | `deeplabcut.modelzoo.weight_initialization.build_weight_init` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\__init__.py:11" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\conversion_table\__init__.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 11 | 31 | `.conversion_table.get_conversion_table` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\conversion_table\__init__.py:11" +``` + +#### `deeplabcut\pose_estimation_tensorflow\lib\__init__.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 15 | 8 | `deeplabcut.core.trackingutils` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\lib\__init__.py:15" +``` + +#### `deeplabcut\pose_estimation_tensorflow\modelzoo\__init__.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 11 | 18 | `.api.SpatiotemporalAdaptation` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\modelzoo\__init__.py:11" +``` + +#### `deeplabcut\pose_estimation_tensorflow\modelzoo\api\__init__.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 11 | 35 | `.spatiotemporal_adapt.SpatiotemporalAdaptation` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\modelzoo\api\__init__.py:11" +``` + +#### `deeplabcut\pose_tracking_pytorch\datasets\__init__.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 12 | 30 | `.make_dataloader.make_dlc_dataloader` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_tracking_pytorch\datasets\__init__.py:12" +``` + +#### `deeplabcut\pose_tracking_pytorch\loss\__init__.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 11 | 24 | `.make_loss.easy_triplet_loss` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_tracking_pytorch\loss\__init__.py:11" +``` + +#### `deeplabcut\pose_tracking_pytorch\model\backbones\__init__.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 11 | 26 | `.vit_pytorch.dlc_base_kpt_TransReID` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_tracking_pytorch\model\backbones\__init__.py:11" +``` + +#### `deeplabcut\pose_tracking_pytorch\solver\__init__.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 11 | 29 | `.make_optimizer.make_easy_optimizer` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_tracking_pytorch\solver\__init__.py:11" +``` + +#### `deeplabcut\post_processing\__init__.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 21 | 57 | `deeplabcut.post_processing.analyze_skeleton.analyzeskeleton` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | + +Quick open commands: + +```powershell +code -g "deeplabcut\post_processing\__init__.py:21" +``` + +## B905 + +Count: **176** + +### Files affected + +| File | Count | +|---|---:| +| `docs\recipes\flip_and_rotate.ipynb` | 18 | +| `deeplabcut\refine_training_dataset\stitch.py` | 12 | +| `deeplabcut\core\inferenceutils.py` | 8 | +| `deeplabcut\refine_training_dataset\tracklets.py` | 8 | +| `deeplabcut\utils\visualization.py` | 8 | +| `deeplabcut\core\crossvalutils.py` | 7 | +| `deeplabcut\utils\pseudo_label.py` | 6 | +| `deeplabcut\pose_estimation_pytorch\apis\prune_paf_graph.py` | 5 | +| `deeplabcut\utils\make_labeled_video.py` | 5 | +| `examples\COLAB\COLAB_HumanPose_with_RTMPose.ipynb` | 5 | +| `deeplabcut\pose_estimation_tensorflow\predict_multianimal.py` | 4 | +| `deeplabcut\pose_estimation_pytorch\data\preprocessor.py` | 3 | +| `tests\pose_estimation_pytorch\data\test_transforms.py` | 3 | +| `tests\pose_estimation_pytorch\runners\test_runners_inference.py` | 3 | +| `deeplabcut\core\metrics\distance_metrics.py` | 2 | +| `deeplabcut\core\trackingutils.py` | 2 | +| `deeplabcut\create_project\add.py` | 2 | +| `deeplabcut\create_project\new.py` | 2 | +| `deeplabcut\modelzoo\webapp\inference.py` | 2 | +| `deeplabcut\pose_estimation_pytorch\apis\analyze_images.py` | 2 | +| `deeplabcut\pose_estimation_pytorch\apis\evaluation.py` | 2 | +| `deeplabcut\pose_estimation_pytorch\apis\visualization.py` | 2 | +| `deeplabcut\pose_estimation_pytorch\data\postprocessor.py` | 2 | +| `deeplabcut\pose_estimation_pytorch\data\transforms.py` | 2 | +| `deeplabcut\pose_estimation_pytorch\runners\logger.py` | 2 | +| `deeplabcut\pose_estimation_pytorch\runners\train.py` | 2 | +| `deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py` | 2 | +| `tests\test_pose_multianimal_imgaug.py` | 2 | +| `tests\test_predict_supermodel.py` | 2 | +| `deeplabcut\benchmark\metrics.py` | 1 | +| `deeplabcut\core\metrics\bbox.py` | 1 | +| `deeplabcut\core\metrics\identity.py` | 1 | +| `deeplabcut\generate_training_dataset\frame_extraction.py` | 1 | +| `deeplabcut\generate_training_dataset\metadata.py` | 1 | +| `deeplabcut\generate_training_dataset\multiple_individuals_trainingsetmanipulation.py` | 1 | +| `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` | 1 | +| `deeplabcut\gui\tabs\create_videos.py` | 1 | +| `deeplabcut\gui\tabs\evaluate_network.py` | 1 | +| `deeplabcut\gui\tracklet_toolbox.py` | 1 | +| `deeplabcut\gui\widgets.py` | 1 | +| `deeplabcut\modelzoo\generalized_data_converter\utils.py` | 1 | +| `deeplabcut\modelzoo\utils.py` | 1 | +| `deeplabcut\pose_estimation_3d\plotting3D.py` | 1 | +| `deeplabcut\pose_estimation_3d\triangulation.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\apis\tracklets.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\apis\utils.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\data\utils.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\models\backbones\hrnet_coam.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\models\heads\dlcrnet.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\models\heads\simple_head.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\models\predictors\paf_predictor.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\modelzoo\memory_replay.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\modelzoo\utils.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\post_processing\identity.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\post_processing\nms.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\runners\schedulers.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\core\evaluate_multianimal.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\core\predict_multianimal.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\export.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\nnets\conv_blocks.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\visualizemaps.py` | 1 | +| `deeplabcut\pose_tracking_pytorch\solver\scheduler.py` | 1 | +| `deeplabcut\post_processing\analyze_skeleton.py` | 1 | +| `deeplabcut\post_processing\filtering.py` | 1 | +| `deeplabcut\utils\auxfun_videos.py` | 1 | +| `deeplabcut\utils\auxiliaryfunctions_3d.py` | 1 | +| `deeplabcut\utils\skeleton.py` | 1 | +| `examples\COLAB\COLAB_BUCTD_and_CTD_tracking.ipynb` | 1 | +| `examples\testscript_multianimal.py` | 1 | +| `examples\testscript_transreid.py` | 1 | +| `examples\utils.py` | 1 | +| `tests\generate_training_dataset\test_trainset_metadata.py` | 1 | +| `tests\pose_estimation_pytorch\data\test_data_ctd.py` | 1 | +| `tests\pose_estimation_pytorch\data\test_postprocessor.py` | 1 | +| `tests\pose_estimation_pytorch\data\test_preprocessor.py` | 1 | +| `tests\pose_estimation_pytorch\runners\test_dynamic_cropper.py` | 1 | +| `tests\test_inferenceutils.py` | 1 | +| `tests\test_stitcher.py` | 1 | + +### Details + +#### `docs\recipes\flip_and_rotate.ipynb` (18) + +| Line | Col | Message | +|---:|---:|---| +| 8 | 34 | `zip()` without an explicit `strict=` parameter | +| 14 | 34 | `zip()` without an explicit `strict=` parameter | +| 15 | 34 | `zip()` without an explicit `strict=` parameter | +| 15 | 34 | `zip()` without an explicit `strict=` parameter | +| 15 | 34 | `zip()` without an explicit `strict=` parameter | +| 15 | 34 | `zip()` without an explicit `strict=` parameter | +| 18 | 34 | `zip()` without an explicit `strict=` parameter | +| 26 | 34 | `zip()` without an explicit `strict=` parameter | +| 27 | 34 | `zip()` without an explicit `strict=` parameter | +| 35 | 36 | `zip()` without an explicit `strict=` parameter | +| 35 | 36 | `zip()` without an explicit `strict=` parameter | +| 35 | 41 | `zip()` without an explicit `strict=` parameter | +| 35 | 41 | `zip()` without an explicit `strict=` parameter | +| 35 | 41 | `zip()` without an explicit `strict=` parameter | +| 37 | 36 | `zip()` without an explicit `strict=` parameter | +| 58 | 49 | `zip()` without an explicit `strict=` parameter | +| 58 | 49 | `zip()` without an explicit `strict=` parameter | +| 58 | 49 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "docs\recipes\flip_and_rotate.ipynb:35" +``` + +#### `deeplabcut\refine_training_dataset\stitch.py` (12) + +| Line | Col | Message | +|---:|---:|---| +| 62 | 58 | `zip()` without an explicit `strict=` parameter | +| 526 | 30 | `zip()` without an explicit `strict=` parameter | +| 564 | 56 | `zip()` without an explicit `strict=` parameter | +| 627 | 31 | `zip()` without an explicit `strict=` parameter | +| 630 | 31 | `zip()` without an explicit `strict=` parameter | +| 631 | 31 | `zip()` without an explicit `strict=` parameter | +| 632 | 31 | `zip()` without an explicit `strict=` parameter | +| 678 | 38 | `zip()` without an explicit `strict=` parameter | +| 679 | 38 | `zip()` without an explicit `strict=` parameter | +| 915 | 36 | `zip()` without an explicit `strict=` parameter | +| 928 | 29 | `zip()` without an explicit `strict=` parameter | +| 950 | 30 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\refine_training_dataset\stitch.py:62" +``` + +#### `deeplabcut\core\inferenceutils.py` (8) + +| Line | Col | Message | +|---:|---:|---| +| 157 | 21 | `zip()` without an explicit `strict=` parameter | +| 423 | 49 | `zip()` without an explicit `strict=` parameter | +| 426 | 29 | `zip()` without an explicit `strict=` parameter | +| 460 | 21 | `zip()` without an explicit `strict=` parameter | +| 484 | 33 | `zip()` without an explicit `strict=` parameter | +| 757 | 24 | `zip()` without an explicit `strict=` parameter | +| 1033 | 25 | `zip()` without an explicit `strict=` parameter | +| 1084 | 24 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\core\inferenceutils.py:157" +``` + +#### `deeplabcut\refine_training_dataset\tracklets.py` (8) + +| Line | Col | Message | +|---:|---:|---| +| 109 | 30 | `zip()` without an explicit `strict=` parameter | +| 163 | 41 | `zip()` without an explicit `strict=` parameter | +| 199 | 25 | `zip()` without an explicit `strict=` parameter | +| 257 | 49 | `zip()` without an explicit `strict=` parameter | +| 259 | 52 | `zip()` without an explicit `strict=` parameter | +| 305 | 25 | `zip()` without an explicit `strict=` parameter | +| 318 | 21 | `zip()` without an explicit `strict=` parameter | +| 328 | 21 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\refine_training_dataset\tracklets.py:109" +``` + +#### `deeplabcut\utils\visualization.py` (8) + +| Line | Col | Message | +|---:|---:|---| +| 163 | 48 | `zip()` without an explicit `strict=` parameter | +| 183 | 30 | `zip()` without an explicit `strict=` parameter | +| 337 | 35 | `zip()` without an explicit `strict=` parameter | +| 346 | 41 | `zip()` without an explicit `strict=` parameter | +| 363 | 26 | `zip()` without an explicit `strict=` parameter | +| 364 | 23 | `zip()` without an explicit `strict=` parameter | +| 399 | 30 | `zip()` without an explicit `strict=` parameter | +| 420 | 29 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\visualization.py:163" +``` + +#### `deeplabcut\core\crossvalutils.py` (7) + +| Line | Col | Message | +|---:|---:|---| +| 146 | 47 | `zip()` without an explicit `strict=` parameter | +| 152 | 34 | `zip()` without an explicit `strict=` parameter | +| 225 | 17 | `zip()` without an explicit `strict=` parameter | +| 343 | 40 | `zip()` without an explicit `strict=` parameter | +| 345 | 17 | `zip()` without an explicit `strict=` parameter | +| 349 | 24 | `zip()` without an explicit `strict=` parameter | +| 371 | 27 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\core\crossvalutils.py:146" +``` + +#### `deeplabcut\utils\pseudo_label.py` (6) + +| Line | Col | Message | +|---:|---:|---| +| 265 | 24 | `zip()` without an explicit `strict=` parameter | +| 276 | 35 | `zip()` without an explicit `strict=` parameter | +| 307 | 32 | `zip()` without an explicit `strict=` parameter | +| 320 | 24 | `zip()` without an explicit `strict=` parameter | +| 420 | 57 | `zip()` without an explicit `strict=` parameter | +| 434 | 39 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\pseudo_label.py:265" +``` + +#### `deeplabcut\pose_estimation_pytorch\apis\prune_paf_graph.py` (5) + +| Line | Col | Message | +|---:|---:|---| +| 216 | 52 | `zip()` without an explicit `strict=` parameter | +| 222 | 34 | `zip()` without an explicit `strict=` parameter | +| 259 | 17 | `zip()` without an explicit `strict=` parameter | +| 263 | 24 | `zip()` without an explicit `strict=` parameter | +| 281 | 29 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\apis\prune_paf_graph.py:216" +``` + +#### `deeplabcut\utils\make_labeled_video.py` (5) + +| Line | Col | Message | +|---:|---:|---| +| 62 | 17 | `zip()` without an explicit `strict=` parameter | +| 1092 | 35 | `zip()` without an explicit `strict=` parameter | +| 1099 | 41 | `zip()` without an explicit `strict=` parameter | +| 1116 | 25 | `zip()` without an explicit `strict=` parameter | +| 1134 | 37 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\make_labeled_video.py:62" +``` + +#### `examples\COLAB\COLAB_HumanPose_with_RTMPose.ipynb` (5) + +| Line | Col | Message | +|---:|---:|---| +| 8 | 38 | `zip()` without an explicit `strict=` parameter | +| 35 | 49 | `zip()` without an explicit `strict=` parameter | +| 39 | 49 | `zip()` without an explicit `strict=` parameter | +| 61 | 37 | `zip()` without an explicit `strict=` parameter | +| 69 | 77 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "examples\COLAB\COLAB_HumanPose_with_RTMPose.ipynb:35" +``` + +#### `deeplabcut\pose_estimation_tensorflow\predict_multianimal.py` (4) + +| Line | Col | Message | +|---:|---:|---| +| 292 | 49 | `zip()` without an explicit `strict=` parameter | +| 318 | 49 | `zip()` without an explicit `strict=` parameter | +| 411 | 34 | `zip()` without an explicit `strict=` parameter | +| 428 | 34 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\predict_multianimal.py:292" +``` + +#### `deeplabcut\pose_estimation_pytorch\data\preprocessor.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 273 | 54 | `zip()` without an explicit `strict=` parameter | +| 276 | 89 | `zip()` without an explicit `strict=` parameter | +| 280 | 97 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\data\preprocessor.py:273" +``` + +#### `tests\pose_estimation_pytorch\data\test_transforms.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 59 | 32 | `zip()` without an explicit `strict=` parameter | +| 224 | 36 | `zip()` without an explicit `strict=` parameter | +| 272 | 30 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "tests\pose_estimation_pytorch\data\test_transforms.py:59" +``` + +#### `tests\pose_estimation_pytorch\runners\test_runners_inference.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 91 | 17 | `zip()` without an explicit `strict=` parameter | +| 143 | 17 | `zip()` without an explicit `strict=` parameter | +| 145 | 29 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "tests\pose_estimation_pytorch\runners\test_runners_inference.py:91" +``` + +#### `deeplabcut\core\metrics\distance_metrics.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 364 | 56 | `zip()` without an explicit `strict=` parameter | +| 402 | 56 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\core\metrics\distance_metrics.py:364" +``` + +#### `deeplabcut\core\trackingutils.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 447 | 29 | `zip()` without an explicit `strict=` parameter | +| 706 | 25 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\core\trackingutils.py:447" +``` + +#### `deeplabcut\create_project\add.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 77 | 25 | `zip()` without an explicit `strict=` parameter | +| 87 | 25 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\create_project\add.py:77" +``` + +#### `deeplabcut\create_project\new.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 185 | 25 | `zip()` without an explicit `strict=` parameter | +| 190 | 25 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\create_project\new.py:185" +``` + +#### `deeplabcut\modelzoo\webapp\inference.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 103 | 29 | `zip()` without an explicit `strict=` parameter | +| 106 | 71 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\webapp\inference.py:103" +``` + +#### `deeplabcut\pose_estimation_pytorch\apis\analyze_images.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 532 | 28 | `zip()` without an explicit `strict=` parameter | +| 541 | 80 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\apis\analyze_images.py:532" +``` + +#### `deeplabcut\pose_estimation_pytorch\apis\evaluation.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 94 | 36 | `zip()` without an explicit `strict=` parameter | +| 97 | 80 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\apis\evaluation.py:94" +``` + +#### `deeplabcut\pose_estimation_pytorch\apis\visualization.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 149 | 49 | `zip()` without an explicit `strict=` parameter | +| 366 | 43 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\apis\visualization.py:149" +``` + +#### `deeplabcut\pose_estimation_pytorch\data\postprocessor.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 395 | 54 | `zip()` without an explicit `strict=` parameter | +| 518 | 46 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\data\postprocessor.py:395" +``` + +#### `deeplabcut\pose_estimation_pytorch\data\transforms.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 498 | 31 | `zip()` without an explicit `strict=` parameter | +| 641 | 59 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\data\transforms.py:498" +``` + +#### `deeplabcut\pose_estimation_pytorch\runners\logger.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 241 | 48 | `zip()` without an explicit `strict=` parameter | +| 505 | 35 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\runners\logger.py:241" +``` + +#### `deeplabcut\pose_estimation_pytorch\runners\train.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 506 | 40 | `zip()` without an explicit `strict=` parameter | +| 638 | 72 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\runners\train.py:506" +``` + +#### `deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 90 | 44 | `zip()` without an explicit `strict=` parameter | +| 411 | 49 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py:90" +``` + +#### `tests\test_pose_multianimal_imgaug.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 73 | 49 | `zip()` without an explicit `strict=` parameter | +| 78 | 32 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "tests\test_pose_multianimal_imgaug.py:73" +``` + +#### `tests\test_predict_supermodel.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 26 | 46 | `zip()` without an explicit `strict=` parameter | +| 48 | 63 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "tests\test_predict_supermodel.py:26" +``` + +#### `deeplabcut\benchmark\metrics.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 58 | 29 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\benchmark\metrics.py:58" +``` + +#### `deeplabcut\core\metrics\bbox.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 100 | 28 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\core\metrics\bbox.py:100" +``` + +#### `deeplabcut\core\metrics\identity.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 67 | 53 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\core\metrics\identity.py:67" +``` + +#### `deeplabcut\generate_training_dataset\frame_extraction.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 498 | 32 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\generate_training_dataset\frame_extraction.py:498" +``` + +#### `deeplabcut\generate_training_dataset\metadata.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 140 | 45 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\generate_training_dataset\metadata.py:140" +``` + +#### `deeplabcut\generate_training_dataset\multiple_individuals_trainingsetmanipulation.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 387 | 59 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\generate_training_dataset\multiple_individuals_trainingsetmanipulation.py:387" +``` + +#### `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 1097 | 63 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\generate_training_dataset\trainingsetmanipulation.py:1097" +``` + +#### `deeplabcut\gui\tabs\create_videos.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 273 | 58 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\gui\tabs\create_videos.py:273" +``` + +#### `deeplabcut\gui\tabs\evaluate_network.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 50 | 37 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\gui\tabs\evaluate_network.py:50" +``` + +#### `deeplabcut\gui\tracklet_toolbox.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 772 | 46 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\gui\tracklet_toolbox.py:772" +``` + +#### `deeplabcut\gui\widgets.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 663 | 21 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\gui\widgets.py:663" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\utils.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 187 | 28 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\utils.py:187" +``` + +#### `deeplabcut\modelzoo\utils.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 184 | 17 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\utils.py:184" +``` + +#### `deeplabcut\pose_estimation_3d\plotting3D.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 265 | 31 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_3d\plotting3D.py:265" +``` + +#### `deeplabcut\pose_estimation_3d\triangulation.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 474 | 38 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_3d\triangulation.py:474" +``` + +#### `deeplabcut\pose_estimation_pytorch\apis\tracklets.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 276 | 51 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\apis\tracklets.py:276" +``` + +#### `deeplabcut\pose_estimation_pytorch\apis\utils.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 465 | 17 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\apis\utils.py:465" +``` + +#### `deeplabcut\pose_estimation_pytorch\data\utils.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 495 | 26 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\data\utils.py:495" +``` + +#### `deeplabcut\pose_estimation_pytorch\models\backbones\hrnet_coam.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 189 | 43 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\models\backbones\hrnet_coam.py:189" +``` + +#### `deeplabcut\pose_estimation_pytorch\models\heads\dlcrnet.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 120 | 63 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\models\heads\dlcrnet.py:120" +``` + +#### `deeplabcut\pose_estimation_pytorch\models\heads\simple_head.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 196 | 35 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\models\heads\simple_head.py:196" +``` + +#### `deeplabcut\pose_estimation_pytorch\models\predictors\paf_predictor.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 388 | 23 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\models\predictors\paf_predictor.py:388" +``` + +#### `deeplabcut\pose_estimation_pytorch\modelzoo\memory_replay.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 107 | 30 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\modelzoo\memory_replay.py:107" +``` + +#### `deeplabcut\pose_estimation_pytorch\modelzoo\utils.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 160 | 27 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\modelzoo\utils.py:160" +``` + +#### `deeplabcut\pose_estimation_pytorch\post_processing\identity.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 42 | 29 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\post_processing\identity.py:42" +``` + +#### `deeplabcut\pose_estimation_pytorch\post_processing\nms.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 91 | 39 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\post_processing\nms.py:91" +``` + +#### `deeplabcut\pose_estimation_pytorch\runners\schedulers.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 129 | 29 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\runners\schedulers.py:129" +``` + +#### `deeplabcut\pose_estimation_tensorflow\core\evaluate_multianimal.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 331 | 42 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\core\evaluate_multianimal.py:331" +``` + +#### `deeplabcut\pose_estimation_tensorflow\core\predict_multianimal.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 90 | 26 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\core\predict_multianimal.py:90" +``` + +#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 391 | 32 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py:391" +``` + +#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 472 | 32 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py:472" +``` + +#### `deeplabcut\pose_estimation_tensorflow\export.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 325 | 21 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\export.py:325" +``` + +#### `deeplabcut\pose_estimation_tensorflow\nnets\conv_blocks.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 368 | 50 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\nnets\conv_blocks.py:368" +``` + +#### `deeplabcut\pose_estimation_tensorflow\visualizemaps.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 256 | 22 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\visualizemaps.py:256" +``` + +#### `deeplabcut\pose_tracking_pytorch\solver\scheduler.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 96 | 35 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_tracking_pytorch\solver\scheduler.py:96" +``` + +#### `deeplabcut\post_processing\analyze_skeleton.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 60 | 54 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\post_processing\analyze_skeleton.py:60" +``` + +#### `deeplabcut\post_processing\filtering.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 57 | 39 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\post_processing\filtering.py:57" +``` + +#### `deeplabcut\utils\auxfun_videos.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 274 | 42 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\auxfun_videos.py:274" +``` + +#### `deeplabcut\utils\auxiliaryfunctions_3d.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 317 | 19 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\auxiliaryfunctions_3d.py:317" +``` + +#### `deeplabcut\utils\skeleton.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 171 | 21 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\skeleton.py:171" +``` + +#### `examples\COLAB\COLAB_BUCTD_and_CTD_tracking.ipynb` (1) + +| Line | Col | Message | +|---:|---:|---| +| 22 | 37 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "examples\COLAB\COLAB_BUCTD_and_CTD_tracking.ipynb:22" +``` + +#### `examples\testscript_multianimal.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 85 | 17 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "examples\testscript_multianimal.py:85" +``` + +#### `examples\testscript_transreid.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 81 | 17 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "examples\testscript_transreid.py:81" +``` + +#### `examples\utils.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 102 | 34 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "examples\utils.py:102" +``` + +#### `tests\generate_training_dataset\test_trainset_metadata.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 326 | 34 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "tests\generate_training_dataset\test_trainset_metadata.py:326" +``` + +#### `tests\pose_estimation_pytorch\data\test_data_ctd.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 151 | 91 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "tests\pose_estimation_pytorch\data\test_data_ctd.py:151" +``` + +#### `tests\pose_estimation_pytorch\data\test_postprocessor.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 304 | 28 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "tests\pose_estimation_pytorch\data\test_postprocessor.py:304" +``` + +#### `tests\pose_estimation_pytorch\data\test_preprocessor.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 156 | 49 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "tests\pose_estimation_pytorch\data\test_preprocessor.py:156" +``` + +#### `tests\pose_estimation_pytorch\runners\test_dynamic_cropper.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 166 | 47 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "tests\pose_estimation_pytorch\runners\test_dynamic_cropper.py:166" +``` + +#### `tests\test_inferenceutils.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 30 | 17 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "tests\test_inferenceutils.py:30" +``` + +#### `tests\test_stitcher.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 103 | 10 | `zip()` without an explicit `strict=` parameter | + +Quick open commands: + +```powershell +code -g "tests\test_stitcher.py:103" +``` + +## F841 + +Count: **141** + +### Files affected + +| File | Count | +|---|---:| +| `deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py` | 74 | +| `deeplabcut\utils\pseudo_label.py` | 7 | +| `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` | 5 | +| `deeplabcut\modelzoo\generalized_data_converter\utils.py` | 4 | +| `deeplabcut\pose_estimation_3d\triangulation.py` | 3 | +| `deeplabcut\pose_estimation_pytorch\models\predictors\dekr_predictor.py` | 3 | +| `deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py` | 3 | +| `deeplabcut\pose_estimation_tensorflow\export.py` | 3 | +| `deeplabcut\pose_estimation_tensorflow\predict_videos.py` | 3 | +| `deeplabcut\pose_estimation_pytorch\models\necks\layers.py` | 2 | +| `deeplabcut\pose_estimation_tensorflow\datasets\pose_tensorpack.py` | 2 | +| `deeplabcut\pose_estimation_tensorflow\visualizemaps.py` | 2 | +| `tests\pose_estimation_pytorch\runners\bottum_up.py` | 2 | +| `deeplabcut\generate_training_dataset\frame_extraction.py` | 1 | +| `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` | 1 | +| `deeplabcut\gui\tabs\modelzoo.py` | 1 | +| `deeplabcut\modelzoo\generalized_data_converter\conversion_table\conversion_table.py` | 1 | +| `deeplabcut\modelzoo\utils.py` | 1 | +| `deeplabcut\pose_estimation_3d\camera_calibration.py` | 1 | +| `deeplabcut\pose_estimation_3d\plotting3D.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\apis\videos.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\models\necks\transformer.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\modelzoo\memory_replay.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\nnets\multi.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\predict_multianimal.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\training.py` | 1 | +| `deeplabcut\pose_tracking_pytorch\model\backbones\vit_pytorch.py` | 1 | +| `deeplabcut\pose_tracking_pytorch\processor\processor.py` | 1 | +| `deeplabcut\pose_tracking_pytorch\train_dlctransreid.py` | 1 | +| `deeplabcut\refine_training_dataset\outlier_frames.py` | 1 | +| `deeplabcut\utils\auxfun_videos.py` | 1 | +| `examples\testscript_mobilenets.py` | 1 | +| `tests\pose_estimation_pytorch\apis\test_apis_evaluate.py` | 1 | +| `tests\pose_estimation_pytorch\config\test_make_pose_config.py` | 1 | +| `tests\pose_estimation_pytorch\data\test_transforms.py` | 1 | +| `tests\pose_estimation_pytorch\modelzoo\test_load_superanimal_models.py` | 1 | +| `tests\pose_estimation_pytorch\other\test_api_utils.py` | 1 | +| `tests\pose_estimation_pytorch\runners\test_runners_inference.py` | 1 | +| `tests\test_auxiliaryfunctions.py` | 1 | +| `tests\test_pose_multianimal_imgaug.py` | 1 | + +### Details + +#### `deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py` (74) + +| Line | Col | Message | +|---:|---:|---| +| 80 | 9 | Local variable `Task` is assigned to but never used | +| 81 | 9 | Local variable `project_path` is assigned to but never used | +| 82 | 9 | Local variable `scorer` is assigned to but never used | +| 83 | 9 | Local variable `date` is assigned to but never used | +| 84 | 9 | Local variable `video_sets` is assigned to but never used | +| 85 | 9 | Local variable `skeleton` is assigned to but never used | +| 86 | 9 | Local variable `bodyparts` is assigned to but never used | +| 87 | 9 | Local variable `start` is assigned to but never used | +| 88 | 9 | Local variable `stop` is assigned to but never used | +| 89 | 9 | Local variable `numframes2pick` is assigned to but never used | +| 90 | 9 | Local variable `skeleton_color` is assigned to but never used | +| 91 | 9 | Local variable `pcutoff` is assigned to but never used | +| 92 | 9 | Local variable `dotsize` is assigned to but never used | +| 93 | 9 | Local variable `alphavalue` is assigned to but never used | +| 94 | 9 | Local variable `colormap` is assigned to but never used | +| 95 | 9 | Local variable `TrainingFraction` is assigned to but never used | +| 96 | 9 | Local variable `iteration` is assigned to but never used | +| 97 | 9 | Local variable `default_net_type` is assigned to but never used | +| 98 | 9 | Local variable `default_augmenter` is assigned to but never used | +| 99 | 9 | Local variable `snapshotindex` is assigned to but never used | +| 100 | 9 | Local variable `batch_size` is assigned to but never used | +| 101 | 9 | Local variable `cropping` is assigned to but never used | +| 102 | 9 | Local variable `croppedtraining` is assigned to but never used | +| 103 | 9 | Local variable `multianimalproject` is assigned to but never used | +| 104 | 9 | Local variable `uniquebodyparts` is assigned to but never used | +| 105 | 9 | Local variable `x1` is assigned to but never used | +| 106 | 9 | Local variable `x2` is assigned to but never used | +| 107 | 9 | Local variable `y1` is assigned to but never used | +| 108 | 9 | Local variable `y2` is assigned to but never used | +| 109 | 9 | Local variable `corer2move2` is assigned to but never used | +| 110 | 9 | Local variable `move2corner` is assigned to but never used | +| 111 | 9 | Local variable `identity` is assigned to but never used | +| 127 | 9 | Local variable `Task` is assigned to but never used | +| 128 | 9 | Local variable `project_path` is assigned to but never used | +| 129 | 9 | Local variable `scorer` is assigned to but never used | +| 130 | 9 | Local variable `date` is assigned to but never used | +| 131 | 9 | Local variable `video_sets` is assigned to but never used | +| 132 | 9 | Local variable `individuals` is assigned to but never used | +| 133 | 9 | Local variable `multianimalbodyparts` is assigned to but never used | +| 134 | 9 | Local variable `skeleton` is assigned to but never used | +| 135 | 9 | Local variable `bodyparts` is assigned to but never used | +| 136 | 9 | Local variable `start` is assigned to but never used | +| 137 | 9 | Local variable `stop` is assigned to but never used | +| 138 | 9 | Local variable `numframes2pick` is assigned to but never used | +| 139 | 9 | Local variable `skeleton_color` is assigned to but never used | +| 140 | 9 | Local variable `pcutoff` is assigned to but never used | +| 141 | 9 | Local variable `dotsize` is assigned to but never used | +| 142 | 9 | Local variable `alphavalue` is assigned to but never used | +| 143 | 9 | Local variable `colormap` is assigned to but never used | +| 144 | 9 | Local variable `TrainingFraction` is assigned to but never used | +| 145 | 9 | Local variable `iteration` is assigned to but never used | +| 146 | 9 | Local variable `default_net_type` is assigned to but never used | +| 147 | 9 | Local variable `default_augmenter` is assigned to but never used | +| 148 | 9 | Local variable `snapshotindex` is assigned to but never used | +| 149 | 9 | Local variable `batch_size` is assigned to but never used | +| 150 | 9 | Local variable `cropping` is assigned to but never used | +| 151 | 9 | Local variable `croppedtraining` is assigned to but never used | +| 152 | 9 | Local variable `multianimalproject` is assigned to but never used | +| 153 | 9 | Local variable `uniquebodyparts` is assigned to but never used | +| 154 | 9 | Local variable `x1` is assigned to but never used | +| 155 | 9 | Local variable `x2` is assigned to but never used | +| 156 | 9 | Local variable `y1` is assigned to but never used | +| 157 | 9 | Local variable `y2` is assigned to but never used | +| 158 | 9 | Local variable `corer2move2` is assigned to but never used | +| 159 | 9 | Local variable `move2corner` is assigned to but never used | +| 160 | 9 | Local variable `identity` is assigned to but never used | +| 239 | 5 | Local variable `total_annotations` is assigned to but never used | +| 243 | 5 | Local variable `count` is assigned to but never used | +| 271 | 5 | Local variable `temp_count` is assigned to but never used | +| 375 | 5 | Local variable `nbodyparts` is assigned to but never used | +| 440 | 5 | Local variable `total_annotations` is assigned to but never used | +| 448 | 9 | Local variable `datasetname` is assigned to but never used | +| 488 | 9 | Local variable `freq` is assigned to but never used | +| 490 | 13 | Local variable `filename` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py:80" +``` + +#### `deeplabcut\utils\pseudo_label.py` (7) + +| Line | Col | Message | +|---:|---:|---| +| 53 | 5 | Local variable `arranged_preds_list` is assigned to but never used | +| 101 | 5 | Local variable `fps` is assigned to but never used | +| 130 | 5 | Local variable `heatmap` is assigned to but never used | +| 401 | 5 | Local variable `new_predictions` is assigned to but never used | +| 403 | 5 | Local variable `num_kpts` is assigned to but never used | +| 447 | 13 | Local variable `bbox_confidence` is assigned to but never used | +| 474 | 5 | Local variable `test_annotations` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\pseudo_label.py:53" +``` + +#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` (5) + +| Line | Col | Message | +|---:|---:|---| +| 129 | 9 | Local variable `num_kpts` is assigned to but never used | +| 315 | 9 | Local variable `num_images` is assigned to but never used | +| 705 | 13 | Local variable `j_x_sm` is assigned to but never used | +| 707 | 13 | Local variable `j_y_sm` is assigned to but never used | +| 709 | 13 | Local variable `map_j` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py:129" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\utils.py` (4) + +| Line | Col | Message | +|---:|---:|---| +| 110 | 9 | Local variable `pickle_obj` is assigned to but never used | +| 125 | 5 | Local variable `video_name` is assigned to but never used | +| 149 | 5 | Local variable `bodyparts` is assigned to but never used | +| 289 | 5 | Local variable `visited` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\utils.py:110" +``` + +#### `deeplabcut\pose_estimation_3d\triangulation.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 305 | 13 | Local variable `scorer_cam1` is assigned to but never used | +| 306 | 13 | Local variable `scorer_cam2` is assigned to but never used | +| 308 | 13 | Local variable `bodyparts` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_3d\triangulation.py:305" +``` + +#### `deeplabcut\pose_estimation_pytorch\models\predictors\dekr_predictor.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 250 | 9 | Local variable `pool1` is assigned to but never used | +| 252 | 9 | Local variable `pool3` is assigned to but never used | +| 253 | 9 | Local variable `map_size` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\models\predictors\dekr_predictor.py:250" +``` + +#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 471 | 17 | Local variable `j_x_sm` is assigned to but never used | +| 473 | 17 | Local variable `j_y_sm` is assigned to but never used | +| 474 | 17 | Local variable `map_j` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py:471" +``` + +#### `deeplabcut\pose_estimation_tensorflow\export.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 118 | 5 | Local variable `path_test_config` is assigned to but never used | +| 145 | 5 | Local variable `trainingsiterations` is assigned to but never used | +| 281 | 5 | Local variable `model_dir` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\export.py:118" +``` + +#### `deeplabcut\pose_estimation_tensorflow\predict_videos.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 183 | 5 | Local variable `pdindex` is assigned to but never used | +| 910 | 17 | Local variable `x0` is assigned to but never used | +| 910 | 21 | Local variable `y0` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\predict_videos.py:183" +``` + +#### `deeplabcut\pose_estimation_pytorch\models\necks\layers.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 176 | 9 | Local variable `b` is assigned to but never used | +| 176 | 12 | Local variable `n` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\models\necks\layers.py:176" +``` + +#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_tensorpack.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 420 | 9 | Local variable `mirror` is assigned to but never used | +| 426 | 9 | Local variable `im_file` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_tensorpack.py:420" +``` + +#### `deeplabcut\pose_estimation_tensorflow\visualizemaps.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 178 | 13 | Local variable `trainingsiterations` is assigned to but never used | +| 191 | 13 | Local variable `PredicteData` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\visualizemaps.py:178" +``` + +#### `tests\pose_estimation_pytorch\runners\bottum_up.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 57 | 5 | Local variable `template` is assigned to but never used | +| 86 | 5 | Local variable `runner` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "tests\pose_estimation_pytorch\runners\bottum_up.py:57" +``` + +#### `deeplabcut\generate_training_dataset\frame_extraction.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 466 | 9 | Local variable `video_dir` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\generate_training_dataset\frame_extraction.py:466" +``` + +#### `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 951 | 5 | Local variable `dlc_root_path` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\generate_training_dataset\trainingsetmanipulation.py:951" +``` + +#### `deeplabcut\gui\tabs\modelzoo.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 459 | 17 | Local variable `results` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\gui\tabs\modelzoo.py:459" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\conversion_table\conversion_table.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 131 | 9 | Local variable `super_bodyparts` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\conversion_table\conversion_table.py:131" +``` + +#### `deeplabcut\modelzoo\utils.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 226 | 5 | Local variable `available_projects` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\utils.py:226" +``` + +#### `deeplabcut\pose_estimation_3d\camera_calibration.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 414 | 13 | Local variable `norm` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_3d\camera_calibration.py:414" +``` + +#### `deeplabcut\pose_estimation_3d\plotting3D.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 133 | 5 | Local variable `start_path` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_3d\plotting3D.py:133" +``` + +#### `deeplabcut\pose_estimation_pytorch\apis\videos.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 517 | 5 | Local variable `detector_path` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\apis\videos.py:517" +``` + +#### `deeplabcut\pose_estimation_pytorch\models\necks\transformer.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 167 | 13 | Local variable `length` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\models\necks\transformer.py:167" +``` + +#### `deeplabcut\pose_estimation_pytorch\modelzoo\memory_replay.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 192 | 9 | Local variable `arranged_preds_list` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\modelzoo\memory_replay.py:192" +``` + +#### `deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 53 | 14 | Local variable `ratio_w` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py:53" +``` + +#### `deeplabcut\pose_estimation_tensorflow\nnets\multi.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 284 | 25 | Local variable `pre_stage_paf_output` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\nnets\multi.py:284" +``` + +#### `deeplabcut\pose_estimation_tensorflow\predict_multianimal.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 84 | 9 | Local variable `start` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\predict_multianimal.py:84" +``` + +#### `deeplabcut\pose_estimation_tensorflow\training.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 198 | 13 | Local variable `supermodels` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\training.py:198" +``` + +#### `deeplabcut\pose_tracking_pytorch\model\backbones\vit_pytorch.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 275 | 9 | Local variable `B` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_tracking_pytorch\model\backbones\vit_pytorch.py:275" +``` + +#### `deeplabcut\pose_tracking_pytorch\processor\processor.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 271 | 5 | Local variable `val_loss` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_tracking_pytorch\processor\processor.py:271" +``` + +#### `deeplabcut\pose_tracking_pytorch\train_dlctransreid.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 49 | 5 | Local variable `x_list` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_tracking_pytorch\train_dlctransreid.py:49" +``` + +#### `deeplabcut\refine_training_dataset\outlier_frames.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 718 | 5 | Local variable `videofolder` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\refine_training_dataset\outlier_frames.py:718" +``` + +#### `deeplabcut\utils\auxfun_videos.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 616 | 5 | Local variable `rs` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\auxfun_videos.py:616" +``` + +#### `examples\testscript_mobilenets.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 75 | 5 | Local variable `DLC_config` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "examples\testscript_mobilenets.py:75" +``` + +#### `tests\pose_estimation_pytorch\apis\test_apis_evaluate.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 228 | 5 | Local variable `num_unique` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "tests\pose_estimation_pytorch\apis\test_apis_evaluate.py:228" +``` + +#### `tests\pose_estimation_pytorch\config\test_make_pose_config.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 338 | 43 | Local variable `err_info` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "tests\pose_estimation_pytorch\config\test_make_pose_config.py:338" +``` + +#### `tests\pose_estimation_pytorch\data\test_transforms.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 99 | 5 | Local variable `aug` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "tests\pose_estimation_pytorch\data\test_transforms.py:99" +``` + +#### `tests\pose_estimation_pytorch\modelzoo\test_load_superanimal_models.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 31 | 13 | Local variable `snapshot` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "tests\pose_estimation_pytorch\modelzoo\test_load_superanimal_models.py:31" +``` + +#### `tests\pose_estimation_pytorch\other\test_api_utils.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 70 | 13 | Local variable `transformed` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "tests\pose_estimation_pytorch\other\test_api_utils.py:70" +``` + +#### `tests\pose_estimation_pytorch\runners\test_runners_inference.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 32 | 9 | Local variable `runner` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "tests\pose_estimation_pytorch\runners\test_runners_inference.py:32" +``` + +#### `tests\test_auxiliaryfunctions.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 22 | 5 | Local variable `n_ext` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "tests\test_auxiliaryfunctions.py:22" +``` + +#### `tests\test_pose_multianimal_imgaug.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 109 | 9 | Local variable `batch` is assigned to but never used | + +Quick open commands: + +```powershell +code -g "tests\test_pose_multianimal_imgaug.py:109" +``` + +## E402 + +Count: **93** +Hint: Module import not at top of file. Move imports above executable code if possible. + +### Files affected + +| File | Count | +|---|---:| +| `docs\recipes\flip_and_rotate.ipynb` | 36 | +| `deeplabcut\pose_estimation_tensorflow\__init__.py` | 13 | +| `deeplabcut\__init__.py` | 12 | +| `deeplabcut\benchmark\metrics.py` | 8 | +| `deeplabcut\pose_estimation_tensorflow\core\train.py` | 6 | +| `testscript_cli.py` | 6 | +| `deeplabcut\pose_estimation_3d\plotting3D.py` | 5 | +| `examples\testscript_deterministicwithResNet152.py` | 4 | +| `examples\COLAB\COLAB_DEMO_mouse_openfield.ipynb` | 2 | +| `tests\pose_estimation_pytorch\modelzoo\test_fmpose_integration.py` | 1 | + +### Details + +#### `docs\recipes\flip_and_rotate.ipynb` (36) + +| Line | Col | Message | +|---:|---:|---| +| 5 | 1 | Module level import not at top of cell | +| 7 | 1 | Module level import not at top of cell | +| 8 | 1 | Module level import not at top of cell | +| 8 | 1 | Module level import not at top of cell | +| 8 | 1 | Module level import not at top of cell | +| 8 | 1 | Module level import not at top of cell | +| 9 | 1 | Module level import not at top of cell | +| 9 | 1 | Module level import not at top of cell | +| 10 | 1 | Module level import not at top of cell | +| 11 | 1 | Module level import not at top of cell | +| 11 | 1 | Module level import not at top of cell | +| 11 | 1 | Module level import not at top of cell | +| 11 | 1 | Module level import not at top of cell | +| 11 | 1 | Module level import not at top of cell | +| 12 | 1 | Module level import not at top of cell | +| 12 | 1 | Module level import not at top of cell | +| 13 | 1 | Module level import not at top of cell | +| 13 | 1 | Module level import not at top of cell | +| 14 | 1 | Module level import not at top of cell | +| 14 | 1 | Module level import not at top of cell | +| 14 | 1 | Module level import not at top of cell | +| 14 | 1 | Module level import not at top of cell | +| 14 | 1 | Module level import not at top of cell | +| 14 | 1 | Module level import not at top of cell | +| 16 | 1 | Module level import not at top of cell | +| 16 | 1 | Module level import not at top of cell | +| 16 | 1 | Module level import not at top of cell | +| 17 | 1 | Module level import not at top of cell | +| 17 | 1 | Module level import not at top of cell | +| 17 | 1 | Module level import not at top of cell | +| 17 | 1 | Module level import not at top of cell | +| 18 | 1 | Module level import not at top of cell | +| 18 | 1 | Module level import not at top of cell | +| 19 | 1 | Module level import not at top of cell | +| 20 | 1 | Module level import not at top of cell | +| 55 | 1 | Module level import not at top of cell | + +Quick open commands: + +```powershell +code -g "docs\recipes\flip_and_rotate.ipynb:5" +``` + +#### `deeplabcut\pose_estimation_tensorflow\__init__.py` (13) + +| Line | Col | Message | +|---:|---:|---| +| 22 | 1 | Module level import not at top of file | +| 23 | 1 | Module level import not at top of file | +| 24 | 1 | Module level import not at top of file | +| 25 | 1 | Module level import not at top of file | +| 26 | 1 | Module level import not at top of file | +| 27 | 1 | Module level import not at top of file | +| 28 | 1 | Module level import not at top of file | +| 29 | 1 | Module level import not at top of file | +| 30 | 1 | Module level import not at top of file | +| 31 | 1 | Module level import not at top of file | +| 32 | 1 | Module level import not at top of file | +| 33 | 1 | Module level import not at top of file | +| 34 | 1 | Module level import not at top of file | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\__init__.py:22" +``` + +#### `deeplabcut\__init__.py` (12) + +| Line | Col | Message | +|---:|---:|---| +| 16 | 1 | Module level import not at top of file | +| 31 | 1 | Module level import not at top of file | +| 32 | 1 | Module level import not at top of file | +| 40 | 1 | Module level import not at top of file | +| 55 | 1 | Module level import not at top of file | +| 56 | 1 | Module level import not at top of file | +| 81 | 1 | Module level import not at top of file | +| 98 | 1 | Module level import not at top of file | +| 104 | 1 | Module level import not at top of file | +| 105 | 1 | Module level import not at top of file | +| 110 | 1 | Module level import not at top of file | +| 111 | 1 | Module level import not at top of file | + +Quick open commands: + +```powershell +code -g "deeplabcut\__init__.py:16" +``` + +#### `deeplabcut\benchmark\metrics.py` (8) + +| Line | Col | Message | +|---:|---:|---| +| 23 | 1 | Module level import not at top of file | +| 24 | 1 | Module level import not at top of file | +| 25 | 1 | Module level import not at top of file | +| 27 | 1 | Module level import not at top of file | +| 28 | 1 | Module level import not at top of file | +| 30 | 1 | Module level import not at top of file | +| 31 | 1 | Module level import not at top of file | +| 32 | 1 | Module level import not at top of file | + +Quick open commands: + +```powershell +code -g "deeplabcut\benchmark\metrics.py:23" +``` + +#### `deeplabcut\pose_estimation_tensorflow\core\train.py` (6) + +| Line | Col | Message | +|---:|---:|---| +| 25 | 1 | Module level import not at top of file | +| 27 | 1 | Module level import not at top of file | +| 28 | 1 | Module level import not at top of file | +| 32 | 1 | Module level import not at top of file | +| 33 | 1 | Module level import not at top of file | +| 34 | 1 | Module level import not at top of file | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\core\train.py:25" +``` + +#### `testscript_cli.py` (6) + +| Line | Col | Message | +|---:|---:|---| +| 14 | 1 | Module level import not at top of file | +| 15 | 1 | Module level import not at top of file | +| 17 | 1 | Module level import not at top of file | +| 18 | 1 | Module level import not at top of file | +| 23 | 1 | Module level import not at top of file | +| 24 | 1 | Module level import not at top of file | + +Quick open commands: + +```powershell +code -g "testscript_cli.py:14" +``` + +#### `deeplabcut\pose_estimation_3d\plotting3D.py` (5) + +| Line | Col | Message | +|---:|---:|---| +| 29 | 1 | Module level import not at top of file | +| 30 | 1 | Module level import not at top of file | +| 31 | 1 | Module level import not at top of file | +| 32 | 1 | Module level import not at top of file | +| 33 | 1 | Module level import not at top of file | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_3d\plotting3D.py:29" +``` + +#### `examples\testscript_deterministicwithResNet152.py` (4) + +| Line | Col | Message | +|---:|---:|---| +| 44 | 1 | Module level import not at top of file | +| 46 | 1 | Module level import not at top of file | +| 47 | 1 | Module level import not at top of file | +| 49 | 1 | Module level import not at top of file | + +Quick open commands: + +```powershell +code -g "examples\testscript_deterministicwithResNet152.py:44" +``` + +#### `examples\COLAB\COLAB_DEMO_mouse_openfield.ipynb` (2) + +| Line | Col | Message | +|---:|---:|---| +| 10 | 1 | Module level import not at top of cell | +| 11 | 1 | Module level import not at top of cell | + +Quick open commands: + +```powershell +code -g "examples\COLAB\COLAB_DEMO_mouse_openfield.ipynb:10" +``` + +#### `tests\pose_estimation_pytorch\modelzoo\test_fmpose_integration.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 19 | 1 | Module level import not at top of file | + +Quick open commands: + +```powershell +code -g "tests\pose_estimation_pytorch\modelzoo\test_fmpose_integration.py:19" +``` + +## UP031 + +Count: **76** +Hint: Old `%` formatting. Convert to f-strings or `.format()` where appropriate. + +### Files affected + +| File | Count | +|---|---:| +| `deeplabcut\pose_estimation_tensorflow\backbones\efficientnet_model.py` | 15 | +| `deeplabcut\pose_estimation_tensorflow\backbones\efficientnet_builder.py` | 13 | +| `deeplabcut\pose_estimation_tensorflow\predict_videos.py` | 11 | +| `deeplabcut\pose_estimation_3d\camera_calibration.py` | 7 | +| `deeplabcut\pose_estimation_tensorflow\export.py` | 4 | +| `deeplabcut\pose_estimation_tensorflow\backbones\mobilenet.py` | 3 | +| `deeplabcut\pose_estimation_tensorflow\nnets\utils.py` | 3 | +| `deeplabcut\create_project\new.py` | 2 | +| `deeplabcut\pose_estimation_tensorflow\nnets\conv_blocks.py` | 2 | +| `deeplabcut\create_project\add.py` | 1 | +| `deeplabcut\create_project\new_3d.py` | 1 | +| `deeplabcut\generate_training_dataset\frame_extraction.py` | 1 | +| `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` | 1 | +| `deeplabcut\pose_estimation_3d\plotting3D.py` | 1 | +| `deeplabcut\pose_estimation_3d\triangulation.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\core\evaluate.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\predict_multianimal.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\visualizemaps.py` | 1 | +| `deeplabcut\post_processing\analyze_skeleton.py` | 1 | +| `deeplabcut\post_processing\filtering.py` | 1 | +| `deeplabcut\refine_training_dataset\outlier_frames.py` | 1 | +| `examples\COLAB\COLAB_DEMO_SuperAnimal.ipynb` | 1 | + +### Details + +#### `deeplabcut\pose_estimation_tensorflow\backbones\efficientnet_model.py` (15) + +| Line | Col | Message | +|---:|---:|---| +| 256 | 35 | Use format specifiers instead of percent format | +| 268 | 35 | Use format specifiers instead of percent format | +| 273 | 35 | Use format specifiers instead of percent format | +| 276 | 35 | Use format specifiers instead of percent format | +| 294 | 35 | Use format specifiers instead of percent format | +| 345 | 35 | Use format specifiers instead of percent format | +| 350 | 35 | Use format specifiers instead of percent format | +| 364 | 35 | Use format specifiers instead of percent format | +| 478 | 35 | Use format specifiers instead of percent format | +| 489 | 46 | Use format specifiers instead of percent format | +| 493 | 47 | Use format specifiers instead of percent format | +| 500 | 32 | Use format specifiers instead of percent format | +| 502 | 36 | Use format specifiers instead of percent format | +| 505 | 40 | Use format specifiers instead of percent format | +| 507 | 44 | Use format specifiers instead of percent format | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\backbones\efficientnet_model.py:256" +``` + +#### `deeplabcut\pose_estimation_tensorflow\backbones\efficientnet_builder.py` (13) + +| Line | Col | Message | +|---:|---:|---| +| 77 | 13 | Use format specifiers instead of percent format | +| 78 | 13 | Use format specifiers instead of percent format | +| 79 | 13 | Use format specifiers instead of percent format | +| 80 | 13 | Use format specifiers instead of percent format | +| 81 | 13 | Use format specifiers instead of percent format | +| 82 | 13 | Use format specifiers instead of percent format | +| 83 | 13 | Use format specifiers instead of percent format | +| 86 | 25 | Use format specifiers instead of percent format | +| 190 | 35 | Use format specifiers instead of percent format | +| 242 | 43 | Use format specifiers instead of percent format | +| 243 | 25 | Use format specifiers instead of percent format | +| 244 | 25 | Use format specifiers instead of percent format | +| 245 | 25 | Use format specifiers instead of percent format | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\backbones\efficientnet_builder.py:77" +``` + +#### `deeplabcut\pose_estimation_tensorflow\predict_videos.py` (11) + +| Line | Col | Message | +|---:|---:|---| +| 106 | 13 | Use format specifiers instead of percent format | +| 121 | 11 | Use format specifiers instead of percent format | +| 489 | 13 | Use format specifiers instead of percent format | +| 505 | 11 | Use format specifiers instead of percent format | +| 663 | 9 | Use format specifiers instead of percent format | +| 1077 | 13 | Use format specifiers instead of percent format | +| 1210 | 13 | Use format specifiers instead of percent format | +| 1225 | 11 | Use format specifiers instead of percent format | +| 1317 | 23 | Use format specifiers instead of percent format | +| 1548 | 13 | Use format specifiers instead of percent format | +| 1579 | 11 | Use format specifiers instead of percent format | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\predict_videos.py:106" +``` + +#### `deeplabcut\pose_estimation_3d\camera_calibration.py` (7) + +| Line | Col | Message | +|---:|---:|---| +| 145 | 27 | Use format specifiers instead of percent format | +| 185 | 17 | Use format specifiers instead of percent format | +| 195 | 19 | Use format specifiers instead of percent format | +| 200 | 19 | Use format specifiers instead of percent format | +| 256 | 13 | Use format specifiers instead of percent format | +| 264 | 13 | Use format specifiers instead of percent format | +| 400 | 15 | Use format specifiers instead of percent format | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_3d\camera_calibration.py:145" +``` + +#### `deeplabcut\pose_estimation_tensorflow\export.py` (4) + +| Line | Col | Message | +|---:|---:|---| +| 126 | 13 | Use format specifiers instead of percent format | +| 270 | 27 | Use format specifiers instead of percent format | +| 289 | 20 | Use format specifiers instead of percent format | +| 299 | 35 | Use format specifiers instead of percent format | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\export.py:126" +``` + +#### `deeplabcut\pose_estimation_tensorflow\backbones\mobilenet.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 242 | 25 | Use format specifiers instead of percent format | +| 246 | 23 | Use format specifiers instead of percent format | +| 325 | 26 | Use format specifiers instead of percent format | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\backbones\mobilenet.py:242" +``` + +#### `deeplabcut\pose_estimation_tensorflow\nnets\utils.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 115 | 23 | Use format specifiers instead of percent format | +| 118 | 35 | Use format specifiers instead of percent format | +| 162 | 21 | Use format specifiers instead of percent format | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\nnets\utils.py:115" +``` + +#### `deeplabcut\create_project\new.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 202 | 43 | Use format specifiers instead of percent format | +| 306 | 9 | Use format specifiers instead of percent format | + +Quick open commands: + +```powershell +code -g "deeplabcut\create_project\new.py:202" +``` + +#### `deeplabcut\pose_estimation_tensorflow\nnets\conv_blocks.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 249 | 29 | Use format specifiers instead of percent format | +| 369 | 24 | Use format specifiers instead of percent format | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\nnets\conv_blocks.py:249" +``` + +#### `deeplabcut\create_project\add.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 100 | 43 | Use format specifiers instead of percent format | + +Quick open commands: + +```powershell +code -g "deeplabcut\create_project\add.py:100" +``` + +#### `deeplabcut\create_project\new_3d.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 126 | 9 | Use format specifiers instead of percent format | + +Quick open commands: + +```powershell +code -g "deeplabcut\create_project\new_3d.py:126" +``` + +#### `deeplabcut\generate_training_dataset\frame_extraction.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 363 | 23 | Use format specifiers instead of percent format | + +Quick open commands: + +```powershell +code -g "deeplabcut\generate_training_dataset\frame_extraction.py:363" +``` + +#### `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 332 | 11 | Use format specifiers instead of percent format | + +Quick open commands: + +```powershell +code -g "deeplabcut\generate_training_dataset\trainingsetmanipulation.py:332" +``` + +#### `deeplabcut\pose_estimation_3d\plotting3D.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 179 | 17 | Use format specifiers instead of percent format | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_3d\plotting3D.py:179" +``` + +#### `deeplabcut\pose_estimation_3d\triangulation.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 132 | 23 | Use format specifiers instead of percent format | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_3d\triangulation.py:132" +``` + +#### `deeplabcut\pose_estimation_tensorflow\core\evaluate.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 272 | 13 | Use format specifiers instead of percent format | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\core\evaluate.py:272" +``` + +#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 77 | 15 | Use format specifiers instead of percent format | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py:77" +``` + +#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 61 | 15 | Use format specifiers instead of percent format | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py:61" +``` + +#### `deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 387 | 19 | Use format specifiers instead of percent format | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py:387" +``` + +#### `deeplabcut\pose_estimation_tensorflow\predict_multianimal.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 215 | 15 | Use format specifiers instead of percent format | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\predict_multianimal.py:215" +``` + +#### `deeplabcut\pose_estimation_tensorflow\visualizemaps.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 142 | 17 | Use format specifiers instead of percent format | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\visualizemaps.py:142" +``` + +#### `deeplabcut\post_processing\analyze_skeleton.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 263 | 15 | Use format specifiers instead of percent format | + +Quick open commands: + +```powershell +code -g "deeplabcut\post_processing\analyze_skeleton.py:263" +``` + +#### `deeplabcut\post_processing\filtering.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 230 | 15 | Use format specifiers instead of percent format | + +Quick open commands: + +```powershell +code -g "deeplabcut\post_processing\filtering.py:230" +``` + +#### `deeplabcut\refine_training_dataset\outlier_frames.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 931 | 15 | Use format specifiers instead of percent format | + +Quick open commands: + +```powershell +code -g "deeplabcut\refine_training_dataset\outlier_frames.py:931" +``` + +#### `examples\COLAB\COLAB_DEMO_SuperAnimal.ipynb` (1) + +| Line | Col | Message | +|---:|---:|---| +| 23 | 5 | Use format specifiers instead of percent format | + +Quick open commands: + +```powershell +code -g "examples\COLAB\COLAB_DEMO_SuperAnimal.ipynb:23" +``` + +## B007 + +Count: **51** +Hint: Unused loop variable. Rename to `_` or use it. + +### Files affected + +| File | Count | +|---|---:| +| `deeplabcut\pose_tracking_pytorch\processor\processor.py` | 4 | +| `deeplabcut\core\trackingutils.py` | 3 | +| `deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py` | 3 | +| `deeplabcut\core\crossvalutils.py` | 2 | +| `deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py` | 2 | +| `deeplabcut\pose_estimation_pytorch\apis\visualization.py` | 2 | +| `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` | 2 | +| `deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py` | 2 | +| `deeplabcut\utils\frameselectiontools.py` | 2 | +| `tests\pose_estimation_pytorch\runners\test_dynamic_cropper.py` | 2 | +| `tests\pose_estimation_pytorch\runners\test_schedulers.py` | 2 | +| `deeplabcut\benchmark\utils.py` | 1 | +| `deeplabcut\core\inferenceutils.py` | 1 | +| `deeplabcut\core\metrics\matching.py` | 1 | +| `deeplabcut\gui\tabs\create_project.py` | 1 | +| `deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc.py` | 1 | +| `deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc_dataframe.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\data\base.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\data\utils.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\models\modules\conv_module.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\models\modules\gated_attention_unit.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\models\necks\transformer.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\runners\dynamic_cropping.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\core\evaluate.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\core\evaluate_multianimal.py` | 1 | +| `deeplabcut\refine_training_dataset\outlier_frames.py` | 1 | +| `deeplabcut\utils\visualization.py` | 1 | +| `examples\COLAB\COLAB_BUCTD_and_CTD_tracking.ipynb` | 1 | +| `tests\core\inferenceutils\test_map_computation.py` | 1 | +| `tests\core\metrics\test_metrics_map_computation.py` | 1 | +| `tests\create_project\test_video_set_configuration.py` | 1 | +| `tests\generate_training_dataset\test_trainset_metadata.py` | 1 | +| `tests\pose_estimation_pytorch\data\test_data_ctd.py` | 1 | +| `tests\pose_estimation_pytorch\other\test_api_utils.py` | 1 | +| `tests\test_auxfun_models.py` | 1 | +| `tests\test_auxiliaryfunctions.py` | 1 | + +### Details + +#### `deeplabcut\pose_tracking_pytorch\processor\processor.py` (4) + +| Line | Col | Message | +|---:|---:|---| +| 163 | 17 | Loop control variable `n_iter` not used within loop body | +| 218 | 9 | Loop control variable `n_iter` not used within loop body | +| 230 | 17 | Loop control variable `i` not used within loop body | +| 275 | 9 | Loop control variable `n_iter` not used within loop body | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_tracking_pytorch\processor\processor.py:163" +``` + +#### `deeplabcut\core\trackingutils.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 567 | 13 | Loop control variable `i` not used within loop body | +| 696 | 16 | Loop control variable `det` not used within loop body | +| 700 | 16 | Loop control variable `trk` not used within loop body | + +Quick open commands: + +```powershell +code -g "deeplabcut\core\trackingutils.py:567" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 100 | 13 | Loop control variable `dataset_name` not used within loop body | +| 172 | 13 | Loop control variable `k` not used within loop body | +| 191 | 13 | Loop control variable `dataset_name` not used within loop body | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py:100" +``` + +#### `deeplabcut\core\crossvalutils.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 229 | 9 | Loop control variable `i` not used within loop body | +| 281 | 16 | Loop control variable `imname` not used within loop body | + +Quick open commands: + +```powershell +code -g "deeplabcut\core\crossvalutils.py:229" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 503 | 13 | Loop control variable `idx` not used within loop body | +| 639 | 21 | Loop control variable `kpt_name` not used within loop body | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py:503" +``` + +#### `deeplabcut\pose_estimation_pytorch\apis\visualization.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 149 | 17 | Loop control variable `idx` not used within loop body | +| 465 | 17 | Loop control variable `image_idx` not used within loop body | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\apis\visualization.py:149" +``` + +#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 135 | 13 | Loop control variable `image_id` not used within loop body | +| 546 | 17 | Loop control variable `k` not used within loop body | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py:135" +``` + +#### `deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 163 | 21 | Loop control variable `scale_id` not used within loop body | +| 191 | 21 | Loop control variable `scale_id` not used within loop body | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py:163" +``` + +#### `deeplabcut\utils\frameselectiontools.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 291 | 30 | Loop control variable `index` not used within loop body | +| 305 | 30 | Loop control variable `index` not used within loop body | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\frameselectiontools.py:291" +``` + +#### `tests\pose_estimation_pytorch\runners\test_dynamic_cropper.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 166 | 10 | Loop control variable `start_1` not used within loop body | +| 166 | 37 | Loop control variable `end_2` not used within loop body | + +Quick open commands: + +```powershell +code -g "tests\pose_estimation_pytorch\runners\test_dynamic_cropper.py:166" +``` + +#### `tests\pose_estimation_pytorch\runners\test_schedulers.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 34 | 9 | Loop control variable `i` not used within loop body | +| 252 | 9 | Loop control variable `epoch` not used within loop body | + +Quick open commands: + +```powershell +code -g "tests\pose_estimation_pytorch\runners\test_schedulers.py:34" +``` + +#### `deeplabcut\benchmark\utils.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 66 | 9 | Loop control variable `loader` not used within loop body | + +Quick open commands: + +```powershell +code -g "deeplabcut\benchmark\utils.py:66" +``` + +#### `deeplabcut\core\inferenceutils.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 466 | 30 | Loop control variable `l` not used within loop body | + +Quick open commands: + +```powershell +code -g "deeplabcut\core\inferenceutils.py:466" +``` + +#### `deeplabcut\core\metrics\matching.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 102 | 14 | Loop control variable `pred` not used within loop body | + +Quick open commands: + +```powershell +code -g "deeplabcut\core\metrics\matching.py:102" +``` + +#### `deeplabcut\gui\tabs\create_project.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 134 | 17 | Loop control variable `entry` not used within loop body | + +Quick open commands: + +```powershell +code -g "deeplabcut\gui\tabs\create_project.py:134" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 76 | 17 | Loop control variable `individual_id` not used within loop body | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc.py:76" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc_dataframe.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 184 | 17 | Loop control variable `individual_id` not used within loop body | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc_dataframe.py:184" +``` + +#### `deeplabcut\pose_estimation_pytorch\data\base.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 337 | 17 | Loop control variable `i` not used within loop body | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\data\base.py:337" +``` + +#### `deeplabcut\pose_estimation_pytorch\data\utils.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 305 | 9 | Loop control variable `i` not used within loop body | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\data\utils.py:305" +``` + +#### `deeplabcut\pose_estimation_pytorch\models\modules\conv_module.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 115 | 13 | Loop control variable `i` not used within loop body | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\models\modules\conv_module.py:115" +``` + +#### `deeplabcut\pose_estimation_pytorch\models\modules\gated_attention_unit.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 42 | 9 | Loop control variable `i` not used within loop body | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\models\modules\gated_attention_unit.py:42" +``` + +#### `deeplabcut\pose_estimation_pytorch\models\necks\transformer.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 201 | 13 | Loop control variable `i` not used within loop body | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\models\necks\transformer.py:201" +``` + +#### `deeplabcut\pose_estimation_pytorch\runners\dynamic_cropping.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 528 | 13 | Loop control variable `i` not used within loop body | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\runners\dynamic_cropping.py:528" +``` + +#### `deeplabcut\pose_estimation_tensorflow\core\evaluate.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 109 | 13 | Loop control variable `pi` not used within loop body | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\core\evaluate.py:109" +``` + +#### `deeplabcut\pose_estimation_tensorflow\core\evaluate_multianimal.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 59 | 9 | Loop control variable `n` not used within loop body | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\core\evaluate_multianimal.py:59" +``` + +#### `deeplabcut\refine_training_dataset\outlier_frames.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 1096 | 9 | Loop control variable `findex` not used within loop body | + +Quick open commands: + +```powershell +code -g "deeplabcut\refine_training_dataset\outlier_frames.py:1096" +``` + +#### `deeplabcut\utils\visualization.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 76 | 9 | Loop control variable `scorerindex` not used within loop body | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\visualization.py:76" +``` + +#### `examples\COLAB\COLAB_BUCTD_and_CTD_tracking.ipynb` (1) + +| Line | Col | Message | +|---:|---:|---| +| 3 | 9 | Loop control variable `i` not used within loop body | + +Quick open commands: + +```powershell +code -g "examples\COLAB\COLAB_BUCTD_and_CTD_tracking.ipynb:3" +``` + +#### `tests\core\inferenceutils\test_map_computation.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 318 | 13 | Loop control variable `idv_id` not used within loop body | + +Quick open commands: + +```powershell +code -g "tests\core\inferenceutils\test_map_computation.py:318" +``` + +#### `tests\core\metrics\test_metrics_map_computation.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 294 | 13 | Loop control variable `idv_id` not used within loop body | + +Quick open commands: + +```powershell +code -g "tests\core\metrics\test_metrics_map_computation.py:294" +``` + +#### `tests\create_project\test_video_set_configuration.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 255 | 9 | Loop control variable `video_path` not used within loop body | + +Quick open commands: + +```powershell +code -g "tests\create_project\test_video_set_configuration.py:255" +``` + +#### `tests\generate_training_dataset\test_trainset_metadata.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 81 | 9 | Loop control variable `name` not used within loop body | + +Quick open commands: + +```powershell +code -g "tests\generate_training_dataset\test_trainset_metadata.py:81" +``` + +#### `tests\pose_estimation_pytorch\data\test_data_ctd.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 173 | 25 | Loop control variable `img_index` not used within loop body | + +Quick open commands: + +```powershell +code -g "tests\pose_estimation_pytorch\data\test_data_ctd.py:173" +``` + +#### `tests\pose_estimation_pytorch\other\test_api_utils.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 60 | 9 | Loop control variable `i` not used within loop body | + +Quick open commands: + +```powershell +code -g "tests\pose_estimation_pytorch\other\test_api_utils.py:60" +``` + +#### `tests\test_auxfun_models.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 25 | 32 | Loop control variable `expected_path` not used within loop body | + +Quick open commands: + +```powershell +code -g "tests\test_auxfun_models.py:25" +``` + +#### `tests\test_auxiliaryfunctions.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 39 | 14 | Loop control variable `ext` not used within loop body | + +Quick open commands: + +```powershell +code -g "tests\test_auxiliaryfunctions.py:39" +``` + +## B028 + +Count: **49** + +### Files affected + +| File | Count | +|---|---:| +| `deeplabcut\utils\auxfun_videos.py` | 5 | +| `deeplabcut\core\inferenceutils.py` | 4 | +| `deeplabcut\pose_estimation_pytorch\data\cocoloader.py` | 4 | +| `deeplabcut\pose_estimation_tensorflow\predict_videos.py` | 3 | +| `deeplabcut\create_project\new.py` | 2 | +| `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` | 2 | +| `deeplabcut\gui\widgets.py` | 2 | +| `deeplabcut\modelzoo\generalized_data_converter\conversion_table\conversion_table.py` | 2 | +| `deeplabcut\modelzoo\utils.py` | 2 | +| `deeplabcut\pose_estimation_pytorch\apis\tracklets.py` | 2 | +| `deeplabcut\pose_estimation_pytorch\data\transforms.py` | 2 | +| `deeplabcut\refine_training_dataset\stitch.py` | 2 | +| `deeplabcut\utils\skeleton.py` | 2 | +| `deeplabcut\__init__.py` | 1 | +| `deeplabcut\benchmark\base.py` | 1 | +| `deeplabcut\core\weight_init.py` | 1 | +| `deeplabcut\generate_training_dataset\multiple_individuals_trainingsetmanipulation.py` | 1 | +| `deeplabcut\modelzoo\generalized_data_converter\datasets\base.py` | 1 | +| `deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py` | 1 | +| `deeplabcut\pose_estimation_3d\triangulation.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\modelzoo\utils.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\runners\inference.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\runners\snapshots.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\core\train.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\datasets\factory.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\nnets\factory.py` | 1 | +| `deeplabcut\utils\auxfun_multianimal.py` | 1 | +| `deeplabcut\utils\auxiliaryfunctions.py` | 1 | + +### Details + +#### `deeplabcut\utils\auxfun_videos.py` (5) + +| Line | Col | Message | +|---:|---:|---| +| 61 | 13 | No explicit `stacklevel` keyword argument found | +| 69 | 17 | No explicit `stacklevel` keyword argument found | +| 113 | 13 | No explicit `stacklevel` keyword argument found | +| 158 | 13 | No explicit `stacklevel` keyword argument found | +| 189 | 13 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\auxfun_videos.py:61" +``` + +#### `deeplabcut\core\inferenceutils.py` (4) + +| Line | Col | Message | +|---:|---:|---| +| 263 | 13 | No explicit `stacklevel` keyword argument found | +| 343 | 13 | No explicit `stacklevel` keyword argument found | +| 351 | 13 | No explicit `stacklevel` keyword argument found | +| 367 | 13 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\core\inferenceutils.py:263" +``` + +#### `deeplabcut\pose_estimation_pytorch\data\cocoloader.py` (4) + +| Line | Col | Message | +|---:|---:|---| +| 145 | 17 | No explicit `stacklevel` keyword argument found | +| 152 | 13 | No explicit `stacklevel` keyword argument found | +| 203 | 13 | No explicit `stacklevel` keyword argument found | +| 223 | 13 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\data\cocoloader.py:145" +``` + +#### `deeplabcut\pose_estimation_tensorflow\predict_videos.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 824 | 13 | No explicit `stacklevel` keyword argument found | +| 1524 | 9 | No explicit `stacklevel` keyword argument found | +| 1561 | 9 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\predict_videos.py:824" +``` + +#### `deeplabcut\create_project\new.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 226 | 13 | No explicit `stacklevel` keyword argument found | +| 232 | 9 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\create_project\new.py:226" +``` + +#### `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 944 | 9 | No explicit `stacklevel` keyword argument found | +| 1491 | 9 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\generate_training_dataset\trainingsetmanipulation.py:944" +``` + +#### `deeplabcut\gui\widgets.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 559 | 13 | No explicit `stacklevel` keyword argument found | +| 643 | 13 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\gui\widgets.py:559" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\conversion_table\conversion_table.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 24 | 13 | No explicit `stacklevel` keyword argument found | +| 122 | 13 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\conversion_table\conversion_table.py:24" +``` + +#### `deeplabcut\modelzoo\utils.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 200 | 9 | No explicit `stacklevel` keyword argument found | +| 207 | 9 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\utils.py:200" +``` + +#### `deeplabcut\pose_estimation_pytorch\apis\tracklets.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 60 | 9 | No explicit `stacklevel` keyword argument found | +| 100 | 9 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\apis\tracklets.py:60" +``` + +#### `deeplabcut\pose_estimation_pytorch\data\transforms.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 52 | 13 | No explicit `stacklevel` keyword argument found | +| 422 | 13 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\data\transforms.py:52" +``` + +#### `deeplabcut\refine_training_dataset\stitch.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 671 | 13 | No explicit `stacklevel` keyword argument found | +| 726 | 17 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\refine_training_dataset\stitch.py:671" +``` + +#### `deeplabcut\utils\skeleton.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 73 | 13 | No explicit `stacklevel` keyword argument found | +| 151 | 13 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\skeleton.py:73" +``` + +#### `deeplabcut\__init__.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 73 | 5 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\__init__.py:73" +``` + +#### `deeplabcut\benchmark\base.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 120 | 13 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\benchmark\base.py:120" +``` + +#### `deeplabcut\core\weight_init.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 196 | 9 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\core\weight_init.py:196" +``` + +#### `deeplabcut\generate_training_dataset\multiple_individuals_trainingsetmanipulation.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 275 | 9 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\generate_training_dataset\multiple_individuals_trainingsetmanipulation.py:275" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\datasets\base.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 193 | 13 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\base.py:193" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 121 | 13 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py:121" +``` + +#### `deeplabcut\pose_estimation_3d\triangulation.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 297 | 17 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_3d\triangulation.py:297" +``` + +#### `deeplabcut\pose_estimation_pytorch\modelzoo\utils.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 178 | 9 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\modelzoo\utils.py:178" +``` + +#### `deeplabcut\pose_estimation_pytorch\runners\inference.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 242 | 17 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\runners\inference.py:242" +``` + +#### `deeplabcut\pose_estimation_pytorch\runners\snapshots.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 137 | 13 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\runners\snapshots.py:137" +``` + +#### `deeplabcut\pose_estimation_tensorflow\core\train.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 208 | 9 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\core\train.py:208" +``` + +#### `deeplabcut\pose_estimation_tensorflow\datasets\factory.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 26 | 17 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\datasets\factory.py:26" +``` + +#### `deeplabcut\pose_estimation_tensorflow\nnets\factory.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 21 | 17 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\nnets\factory.py:21" +``` + +#### `deeplabcut\utils\auxfun_multianimal.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 81 | 17 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\auxfun_multianimal.py:81" +``` + +#### `deeplabcut\utils\auxiliaryfunctions.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 292 | 9 | No explicit `stacklevel` keyword argument found | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\auxiliaryfunctions.py:292" +``` + +## F403 + +Count: **36** +Hint: `from x import *` makes names unclear. Replace with explicit imports. + +### Files affected + +| File | Count | +|---|---:| +| `deeplabcut\pose_estimation_tensorflow\__init__.py` | 12 | +| `deeplabcut\utils\__init__.py` | 8 | +| `deeplabcut\generate_training_dataset\__init__.py` | 3 | +| `deeplabcut\pose_estimation_3d\__init__.py` | 3 | +| `deeplabcut\pose_tracking_pytorch\__init__.py` | 2 | +| `deeplabcut\refine_training_dataset\__init__.py` | 2 | +| `deeplabcut\gui\window.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\lib\crossvalutils.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\lib\inferenceutils.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\lib\trackingutils.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\util\__init__.py` | 1 | +| `deeplabcut\post_processing\__init__.py` | 1 | + +### Details + +#### `deeplabcut\pose_estimation_tensorflow\__init__.py` (12) + +| Line | Col | Message | +|---:|---:|---| +| 22 | 1 | `from deeplabcut.pose_estimation_tensorflow.config import *` used; unable to detect undefined names | +| 23 | 1 | `from deeplabcut.pose_estimation_tensorflow.core.evaluate import *` used; unable to detect undefined names | +| 24 | 1 | `from deeplabcut.pose_estimation_tensorflow.core.test import *` used; unable to detect undefined names | +| 25 | 1 | `from deeplabcut.pose_estimation_tensorflow.core.train import *` used; unable to detect undefined names | +| 26 | 1 | `from deeplabcut.pose_estimation_tensorflow.datasets import *` used; unable to detect undefined names | +| 27 | 1 | `from deeplabcut.pose_estimation_tensorflow.default_config import *` used; unable to detect undefined names | +| 29 | 1 | `from deeplabcut.pose_estimation_tensorflow.models import *` used; unable to detect undefined names | +| 30 | 1 | `from deeplabcut.pose_estimation_tensorflow.nnets import *` used; unable to detect undefined names | +| 31 | 1 | `from deeplabcut.pose_estimation_tensorflow.predict_videos import *` used; unable to detect undefined names | +| 32 | 1 | `from deeplabcut.pose_estimation_tensorflow.training import *` used; unable to detect undefined names | +| 33 | 1 | `from deeplabcut.pose_estimation_tensorflow.util import *` used; unable to detect undefined names | +| 34 | 1 | `from deeplabcut.pose_estimation_tensorflow.visualizemaps import *` used; unable to detect undefined names | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\__init__.py:22" +``` + +#### `deeplabcut\utils\__init__.py` (8) + +| Line | Col | Message | +|---:|---:|---| +| 11 | 1 | `from deeplabcut.utils.auxfun_multianimal import *` used; unable to detect undefined names | +| 12 | 1 | `from deeplabcut.utils.auxfun_videos import *` used; unable to detect undefined names | +| 13 | 1 | `from deeplabcut.utils.auxiliaryfunctions import *` used; unable to detect undefined names | +| 14 | 1 | `from deeplabcut.utils.conversioncode import *` used; unable to detect undefined names | +| 15 | 1 | `from deeplabcut.utils.frameselectiontools import *` used; unable to detect undefined names | +| 16 | 1 | `from deeplabcut.utils.make_labeled_video import *` used; unable to detect undefined names | +| 17 | 1 | `from deeplabcut.utils.plotting import *` used; unable to detect undefined names | +| 18 | 1 | `from deeplabcut.utils.video_processor import *` used; unable to detect undefined names | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\__init__.py:11" +``` + +#### `deeplabcut\generate_training_dataset\__init__.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 13 | 1 | `from deeplabcut.generate_training_dataset.frame_extraction import *` used; unable to detect undefined names | +| 19 | 1 | `from deeplabcut.generate_training_dataset.multiple_individuals_trainingsetmanipulation import *` used; unable to detect undefined names | +| 20 | 1 | `from deeplabcut.generate_training_dataset.trainingsetmanipulation import *` used; unable to detect undefined names | + +Quick open commands: + +```powershell +code -g "deeplabcut\generate_training_dataset\__init__.py:13" +``` + +#### `deeplabcut\pose_estimation_3d\__init__.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 11 | 1 | `from deeplabcut.pose_estimation_3d.camera_calibration import *` used; unable to detect undefined names | +| 12 | 1 | `from deeplabcut.pose_estimation_3d.plotting3D import *` used; unable to detect undefined names | +| 13 | 1 | `from deeplabcut.pose_estimation_3d.triangulation import *` used; unable to detect undefined names | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_3d\__init__.py:11" +``` + +#### `deeplabcut\pose_tracking_pytorch\__init__.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 13 | 1 | `from .create_dataset import *` used; unable to detect undefined names | +| 14 | 1 | `from .tracking_utils.preprocessing import *` used; unable to detect undefined names | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_tracking_pytorch\__init__.py:13" +``` + +#### `deeplabcut\refine_training_dataset\__init__.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 13 | 1 | `from deeplabcut.refine_training_dataset.outlier_frames import *` used; unable to detect undefined names | +| 14 | 1 | `from deeplabcut.refine_training_dataset.tracklets import *` used; unable to detect undefined names | + +Quick open commands: + +```powershell +code -g "deeplabcut\refine_training_dataset\__init__.py:13" +``` + +#### `deeplabcut\gui\window.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 40 | 1 | `from deeplabcut.gui.tabs import *` used; unable to detect undefined names | + +Quick open commands: + +```powershell +code -g "deeplabcut\gui\window.py:40" +``` + +#### `deeplabcut\pose_estimation_tensorflow\lib\crossvalutils.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 13 | 1 | `from deeplabcut.core.crossvalutils import *` used; unable to detect undefined names | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\lib\crossvalutils.py:13" +``` + +#### `deeplabcut\pose_estimation_tensorflow\lib\inferenceutils.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 13 | 1 | `from deeplabcut.core.inferenceutils import *` used; unable to detect undefined names | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\lib\inferenceutils.py:13" +``` + +#### `deeplabcut\pose_estimation_tensorflow\lib\trackingutils.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 13 | 1 | `from deeplabcut.core.trackingutils import *` used; unable to detect undefined names | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\lib\trackingutils.py:13" +``` + +#### `deeplabcut\pose_estimation_tensorflow\util\__init__.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 19 | 1 | `from deeplabcut.pose_estimation_tensorflow.util.logging import *` used; unable to detect undefined names | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\util\__init__.py:19" +``` + +#### `deeplabcut\post_processing\__init__.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 22 | 1 | `from deeplabcut.post_processing.filtering import *` used; unable to detect undefined names | + +Quick open commands: + +```powershell +code -g "deeplabcut\post_processing\__init__.py:22" +``` + +## E712 + +Count: **22** + +### Files affected + +| File | Count | +|---|---:| +| `deeplabcut\pose_estimation_tensorflow\core\evaluate.py` | 4 | +| `deeplabcut\pose_estimation_3d\camera_calibration.py` | 3 | +| `deeplabcut\pose_estimation_tensorflow\predict_videos.py` | 3 | +| `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` | 2 | +| `deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py` | 2 | +| `deeplabcut\pose_estimation_3d\triangulation.py` | 2 | +| `deeplabcut\utils\auxfun_multianimal.py` | 2 | +| `tests\pose_estimation_pytorch\other\test_helper.py` | 2 | +| `deeplabcut\pose_estimation_tensorflow\predict_multianimal.py` | 1 | +| `deeplabcut\utils\auxiliaryfunctions_3d.py` | 1 | + +### Details + +#### `deeplabcut\pose_estimation_tensorflow\core\evaluate.py` (4) + +| Line | Col | Message | +|---:|---:|---| +| 282 | 8 | Avoid equality comparisons to `True`; use `rescale:` for truth checks | +| 369 | 20 | Avoid equality comparisons to `True`; use `show_errors:` for truth checks | +| 409 | 16 | Avoid equality comparisons to `True`; use `fulldata:` for truth checks | +| 430 | 12 | Avoid equality comparisons to `True`; use `fulldata:` for truth checks | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\core\evaluate.py:282" +``` + +#### `deeplabcut\pose_estimation_3d\camera_calibration.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 136 | 20 | Avoid equality comparisons to `True`; use `ret:` for truth checks | +| 163 | 8 | Avoid equality comparisons to `True`; use `calibrate:` for truth checks | +| 403 | 12 | Avoid equality comparisons to `True`; use `plot:` for truth checks | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_3d\camera_calibration.py:136" +``` + +#### `deeplabcut\pose_estimation_tensorflow\predict_videos.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 1019 | 12 | Avoid equality comparisons to `True`; use `cfg["cropping"]:` for truth checks | +| 1268 | 8 | Avoid equality comparisons to `True`; use `os.path.isdir(directory):` for truth checks | +| 1297 | 20 | Avoid equality comparisons to `True`; use `cfg["cropping"]:` for truth checks | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\predict_videos.py:1019" +``` + +#### `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 187 | 12 | Avoid equality comparisons to `True`; use `dropped:` for truth checks | +| 679 | 8 | Avoid equality comparisons to `True`; use `uniform:` for truth checks | + +Quick open commands: + +```powershell +code -g "deeplabcut\generate_training_dataset\trainingsetmanipulation.py:187" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 249 | 12 | Avoid equality comparisons to `True`; use `append_image_id:` for truth checks | +| 457 | 12 | Avoid equality comparisons to `True`; use `append_image_id:` for truth checks | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py:249" +``` + +#### `deeplabcut\pose_estimation_3d\triangulation.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 107 | 8 | Avoid equality comparisons to `True`; use `isinstance(video_path, str):` for truth checks | +| 149 | 20 | Avoid equality comparisons to `True`; use `flag:` for truth checks | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_3d\triangulation.py:107" +``` + +#### `deeplabcut\utils\auxfun_multianimal.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 281 | 12 | Avoid equality comparisons to `True`; use `userfeedback:` for truth checks | +| 366 | 12 | Avoid equality comparisons to `True`; use `userfeedback:` for truth checks | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\auxfun_multianimal.py:281" +``` + +#### `tests\pose_estimation_pytorch\other\test_helper.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 18 | 12 | Avoid equality comparisons to `True`; use `tmp_model.training:` for truth checks | +| 21 | 12 | Avoid equality comparisons to `False`; use `not tmp_model.training:` for false checks | + +Quick open commands: + +```powershell +code -g "tests\pose_estimation_pytorch\other\test_helper.py:18" +``` + +#### `deeplabcut\pose_estimation_tensorflow\predict_multianimal.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 194 | 12 | Avoid equality comparisons to `True`; use `cfg["cropping"]:` for truth checks | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\predict_multianimal.py:194" +``` + +#### `deeplabcut\utils\auxiliaryfunctions_3d.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 94 | 8 | Avoid equality comparisons to `True`; use `plot:` for truth checks | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\auxiliaryfunctions_3d.py:94" +``` + +## F821 + +Count: **22** +Hint: Undefined name. Usually a real bug or missing import. + +### Files affected + +| File | Count | +|---|---:| +| `deeplabcut\utils\conversioncode.py` | 6 | +| `deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py` | 5 | +| `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` | 4 | +| `examples\JUPYTER\Demo_3D_DeepLabCut.ipynb` | 4 | +| `deeplabcut\pose_estimation_tensorflow\core\openvino\session.py` | 2 | +| `deeplabcut\pose_estimation_3d\triangulation.py` | 1 | + +### Details + +#### `deeplabcut\utils\conversioncode.py` (6) + +| Line | Col | Message | +|---:|---:|---| +| 112 | 11 | Undefined name `dlc` | +| 124 | 18 | Undefined name `tqdm` | +| 149 | 27 | Undefined name `np` | +| 152 | 29 | Undefined name `np` | +| 173 | 43 | Undefined name `np` | +| 184 | 43 | Undefined name `np` | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\conversioncode.py:112" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py` (5) + +| Line | Col | Message | +|---:|---:|---| +| 218 | 38 | Undefined name `BasePoseDataset` | +| 219 | 37 | Undefined name `raw_2_imagename_with_id` | +| 220 | 37 | Undefined name `raw_2_imagename` | +| 222 | 36 | Undefined name `raw_2_imagename_with_id` | +| 223 | 36 | Undefined name `raw_2_imagename` | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py:218" +``` + +#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` (4) + +| Line | Col | Message | +|---:|---:|---| +| 757 | 52 | Undefined name `x` | +| 757 | 86 | Undefined name `y` | +| 759 | 36 | Undefined name `y` | +| 759 | 70 | Undefined name `x` | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py:757" +``` + +#### `examples\JUPYTER\Demo_3D_DeepLabCut.ipynb` (4) + +| Line | Col | Message | +|---:|---:|---| +| 1 | 30 | Undefined name `config_path3d` | +| 1 | 30 | Undefined name `config_path3d` | +| 4 | 31 | Undefined name `config_path3d` | +| 6 | 24 | Undefined name `config_path3d` | + +Quick open commands: + +```powershell +code -g "examples\JUPYTER\Demo_3D_DeepLabCut.ipynb:1" +``` + +#### `deeplabcut\pose_estimation_tensorflow\core\openvino\session.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 90 | 26 | Undefined name `out_id` | +| 107 | 18 | Undefined name `checkcropping` | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\core\openvino\session.py:90" +``` + +#### `deeplabcut\pose_estimation_3d\triangulation.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 140 | 21 | Undefined name `warnings` | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_3d\triangulation.py:140" +``` + +## B904 + +Count: **19** +Hint: Inside `except`, use `raise ... from e` to preserve exception chaining. + +### Files affected + +| File | Count | +|---|---:| +| `deeplabcut\pose_estimation_tensorflow\predict_videos.py` | 6 | +| `examples\testscript_3d.py` | 2 | +| `deeplabcut\generate_training_dataset\frame_extraction.py` | 1 | +| `deeplabcut\pose_estimation_3d\camera_calibration.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\registry.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\runners\schedulers.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\core\evaluate.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\export.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\visualizemaps.py` | 1 | +| `deeplabcut\pose_tracking_pytorch\train_dlctransreid.py` | 1 | +| `deeplabcut\refine_training_dataset\stitch.py` | 1 | +| `deeplabcut\utils\auxfun_models.py` | 1 | +| `deeplabcut\utils\conversioncode.py` | 1 | + +### Details + +#### `deeplabcut\pose_estimation_tensorflow\predict_videos.py` (6) + +| Line | Col | Message | +|---:|---:|---| +| 70 | 9 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | +| 105 | 9 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | +| 488 | 9 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | +| 953 | 13 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | +| 1209 | 9 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | +| 1547 | 9 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\predict_videos.py:70" +``` + +#### `examples\testscript_3d.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 108 | 9 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | +| 126 | 9 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | + +Quick open commands: + +```powershell +code -g "examples\testscript_3d.py:108" +``` + +#### `deeplabcut\generate_training_dataset\frame_extraction.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 470 | 13 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | + +Quick open commands: + +```powershell +code -g "deeplabcut\generate_training_dataset\frame_extraction.py:470" +``` + +#### `deeplabcut\pose_estimation_3d\camera_calibration.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 158 | 9 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_3d\camera_calibration.py:158" +``` + +#### `deeplabcut\pose_estimation_pytorch\registry.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 69 | 9 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\registry.py:69" +``` + +#### `deeplabcut\pose_estimation_pytorch\runners\schedulers.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 117 | 9 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\runners\schedulers.py:117" +``` + +#### `deeplabcut\pose_estimation_tensorflow\core\evaluate.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 271 | 9 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\core\evaluate.py:271" +``` + +#### `deeplabcut\pose_estimation_tensorflow\export.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 125 | 9 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\export.py:125" +``` + +#### `deeplabcut\pose_estimation_tensorflow\visualizemaps.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 141 | 13 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\visualizemaps.py:141" +``` + +#### `deeplabcut\pose_tracking_pytorch\train_dlctransreid.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 17 | 5 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_tracking_pytorch\train_dlctransreid.py:17" +``` + +#### `deeplabcut\refine_training_dataset\stitch.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 1162 | 17 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | + +Quick open commands: + +```powershell +code -g "deeplabcut\refine_training_dataset\stitch.py:1162" +``` + +#### `deeplabcut\utils\auxfun_models.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 177 | 13 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\auxfun_models.py:177" +``` + +#### `deeplabcut\utils\conversioncode.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 303 | 13 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\conversioncode.py:303" +``` + +## E722 + +Count: **19** +Hint: Bare `except:`. Catch `Exception` or a narrower exception type. + +### Files affected + +| File | Count | +|---|---:| +| `examples\testscript_3d.py` | 3 | +| `deeplabcut\pose_estimation_3d\camera_calibration.py` | 2 | +| `examples\testscript.py` | 2 | +| `deeplabcut\create_project\add.py` | 1 | +| `deeplabcut\create_project\new.py` | 1 | +| `deeplabcut\generate_training_dataset\frame_extraction.py` | 1 | +| `deeplabcut\modelzoo\generalized_data_converter\datasets\base.py` | 1 | +| `deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc.py` | 1 | +| `deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc_dataframe.py` | 1 | +| `deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py` | 1 | +| `deeplabcut\modelzoo\generalized_data_converter\utils.py` | 1 | +| `deeplabcut\pose_tracking_pytorch\model\backbones\vit_pytorch.py` | 1 | +| `deeplabcut\refine_training_dataset\outlier_frames.py` | 1 | +| `deeplabcut\utils\auxiliaryfunctions_3d.py` | 1 | +| `deeplabcut\utils\make_labeled_video.py` | 1 | + +### Details + +#### `examples\testscript_3d.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 92 | 5 | Do not use bare `except` | +| 107 | 5 | Do not use bare `except` | +| 125 | 5 | Do not use bare `except` | + +Quick open commands: + +```powershell +code -g "examples\testscript_3d.py:92" +``` + +#### `deeplabcut\pose_estimation_3d\camera_calibration.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 97 | 5 | Do not use bare `except` | +| 157 | 5 | Do not use bare `except` | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_3d\camera_calibration.py:97" +``` + +#### `examples\testscript.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 205 | 5 | Do not use bare `except` | +| 327 | 5 | Do not use bare `except` | + +Quick open commands: + +```powershell +code -g "examples\testscript.py:205" +``` + +#### `deeplabcut\create_project\add.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 115 | 9 | Do not use bare `except` | + +Quick open commands: + +```powershell +code -g "deeplabcut\create_project\add.py:115" +``` + +#### `deeplabcut\create_project\new.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 219 | 9 | Do not use bare `except` | + +Quick open commands: + +```powershell +code -g "deeplabcut\create_project\new.py:219" +``` + +#### `deeplabcut\generate_training_dataset\frame_extraction.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 469 | 9 | Do not use bare `except` | + +Quick open commands: + +```powershell +code -g "deeplabcut\generate_training_dataset\frame_extraction.py:469" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\datasets\base.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 221 | 13 | Do not use bare `except` | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\base.py:221" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 80 | 17 | Do not use bare `except` | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc.py:80" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc_dataframe.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 188 | 17 | Do not use bare `except` | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc_dataframe.py:188" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 472 | 13 | Do not use bare `except` | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py:472" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\utils.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 32 | 5 | Do not use bare `except` | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\utils.py:32" +``` + +#### `deeplabcut\pose_tracking_pytorch\model\backbones\vit_pytorch.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 329 | 13 | Do not use bare `except` | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_tracking_pytorch\model\backbones\vit_pytorch.py:329" +``` + +#### `deeplabcut\refine_training_dataset\outlier_frames.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 686 | 5 | Do not use bare `except` | + +Quick open commands: + +```powershell +code -g "deeplabcut\refine_training_dataset\outlier_frames.py:686" +``` + +#### `deeplabcut\utils\auxiliaryfunctions_3d.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 310 | 13 | Do not use bare `except` | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\auxiliaryfunctions_3d.py:310" +``` + +#### `deeplabcut\utils\make_labeled_video.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 1333 | 17 | Do not use bare `except` | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\make_labeled_video.py:1333" +``` + +## F405 + +Count: **16** +Hint: Likely consequence of `import *`. Import the name explicitly. + +### Files affected + +| File | Count | +|---|---:| +| `deeplabcut\gui\window.py` | 16 | + +### Details + +#### `deeplabcut\gui\window.py` (16) + +| Line | Col | Message | +|---:|---:|---| +| 559 | 15 | `ProjectCreator` may be undefined, or defined from star imports | +| 563 | 24 | `OpenProject` may be undefined, or defined from star imports | +| 577 | 25 | `ModelZoo` may be undefined, or defined from star imports | +| 611 | 31 | `ManageProject` may be undefined, or defined from star imports | +| 612 | 31 | `ExtractFrames` may be undefined, or defined from star imports | +| 613 | 29 | `LabelFrames` may be undefined, or defined from star imports | +| 614 | 40 | `CreateTrainingDataset` may be undefined, or defined from star imports | +| 619 | 30 | `TrainNetwork` may be undefined, or defined from star imports | +| 624 | 33 | `EvaluateNetwork` may be undefined, or defined from star imports | +| 629 | 31 | `AnalyzeVideos` may be undefined, or defined from star imports | +| 630 | 41 | `UnsupervizedIdTracking` may be undefined, or defined from star imports | +| 635 | 30 | `CreateVideos` may be undefined, or defined from star imports | +| 640 | 39 | `ExtractOutlierFrames` may be undefined, or defined from star imports | +| 645 | 33 | `RefineTracklets` may be undefined, or defined from star imports | +| 646 | 25 | `ModelZoo` may be undefined, or defined from star imports | +| 647 | 29 | `VideoEditor` may be undefined, or defined from star imports | + +Quick open commands: + +```powershell +code -g "deeplabcut\gui\window.py:559" +``` + +## E721 + +Count: **14** +Hint: Avoid direct `type(x) == Y`; prefer `isinstance(x, Y)`. + +### Files affected + +| File | Count | +|---|---:| +| `deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py` | 5 | +| `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` | 5 | +| `deeplabcut\modelzoo\generalized_data_converter\conversion_table\conversion_table.py` | 1 | +| `deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\data\dlcloader.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\datasets\pose_tensorpack.py` | 1 | + +### Details + +#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py` (5) + +| Line | Col | Message | +|---:|---:|---| +| 59 | 16 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | +| 150 | 16 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | +| 157 | 16 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | +| 191 | 16 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | +| 191 | 36 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py:59" +``` + +#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` (5) + +| Line | Col | Message | +|---:|---:|---| +| 211 | 16 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | +| 226 | 16 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | +| 234 | 16 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | +| 245 | 16 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | +| 245 | 36 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py:211" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\conversion_table\conversion_table.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 60 | 20 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\conversion_table\conversion_table.py:60" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 37 | 8 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py:37" +``` + +#### `deeplabcut\pose_estimation_pytorch\data\dlcloader.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 322 | 16 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\data\dlcloader.py:322" +``` + +#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_tensorpack.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 169 | 16 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_tensorpack.py:169" +``` + +## B006 + +Count: **12** + +### Files affected + +| File | Count | +|---|---:| +| `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` | 3 | +| `deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py` | 3 | +| `deeplabcut\utils\visualization.py` | 2 | +| `deeplabcut\modelzoo\fmpose_3d\fmpose3d.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\core\evaluate.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\core\evaluate_multianimal.py` | 1 | +| `deeplabcut\utils\make_labeled_video.py` | 1 | + +### Details + +#### `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 277 | 12 | Do not use mutable data structures for argument defaults | +| 1397 | 15 | Do not use mutable data structures for argument defaults | +| 1398 | 21 | Do not use mutable data structures for argument defaults | + +Quick open commands: + +```powershell +code -g "deeplabcut\generate_training_dataset\trainingsetmanipulation.py:277" +``` + +#### `deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py` (3) + +| Line | Col | Message | +|---:|---:|---| +| 125 | 16 | Do not use mutable data structures for argument defaults | +| 247 | 16 | Do not use mutable data structures for argument defaults | +| 426 | 16 | Do not use mutable data structures for argument defaults | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py:125" +``` + +#### `deeplabcut\utils\visualization.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 60 | 12 | Do not use mutable data structures for argument defaults | +| 126 | 20 | Do not use mutable data structures for argument defaults | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\visualization.py:60" +``` + +#### `deeplabcut\modelzoo\fmpose_3d\fmpose3d.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 21 | 27 | Do not use mutable data structures for argument defaults | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\fmpose_3d\fmpose3d.py:21" +``` + +#### `deeplabcut\pose_estimation_tensorflow\core\evaluate.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 487 | 14 | Do not use mutable data structures for argument defaults | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\core\evaluate.py:487" +``` + +#### `deeplabcut\pose_estimation_tensorflow\core\evaluate_multianimal.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 91 | 14 | Do not use mutable data structures for argument defaults | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\core\evaluate_multianimal.py:91" +``` + +#### `deeplabcut\utils\make_labeled_video.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 417 | 22 | Do not use mutable data structures for argument defaults | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\make_labeled_video.py:417" +``` + +## E711 + +Count: **7** + +### Files affected + +| File | Count | +|---|---:| +| `deeplabcut\modelzoo\generalized_data_converter\datasets\base_dlc.py` | 2 | +| `deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc_dataframe.py` | 2 | +| `deeplabcut\modelzoo\generalized_data_converter\datasets\single_dlc_dataframe.py` | 2 | +| `deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py` | 1 | + +### Details + +#### `deeplabcut\modelzoo\generalized_data_converter\datasets\base_dlc.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 25 | 29 | Comparison to `None` should be `cond is not None` | +| 25 | 54 | Comparison to `None` should be `cond is not None` | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\base_dlc.py:25" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc_dataframe.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 84 | 29 | Comparison to `None` should be `cond is not None` | +| 84 | 54 | Comparison to `None` should be `cond is not None` | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc_dataframe.py:84" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\datasets\single_dlc_dataframe.py` (2) + +| Line | Col | Message | +|---:|---:|---| +| 85 | 29 | Comparison to `None` should be `cond is not None` | +| 85 | 54 | Comparison to `None` should be `cond is not None` | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\single_dlc_dataframe.py:85" +``` + +#### `deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 43 | 30 | Comparison to `None` should be `cond is not None` | + +Quick open commands: + +```powershell +code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py:43" +``` + +## E731 + +Count: **4** + +### Files affected + +| File | Count | +|---|---:| +| `deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` | 1 | +| `deeplabcut\refine_training_dataset\tracklets.py` | 1 | +| `deeplabcut\utils\auxfun_videos.py` | 1 | + +### Details + +#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 144 | 9 | Do not assign a `lambda` expression, use a `def` | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py:144" +``` + +#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 181 | 9 | Do not assign a `lambda` expression, use a `def` | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py:181" +``` + +#### `deeplabcut\refine_training_dataset\tracklets.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 88 | 9 | Do not assign a `lambda` expression, use a `def` | + +Quick open commands: + +```powershell +code -g "deeplabcut\refine_training_dataset\tracklets.py:88" +``` + +#### `deeplabcut\utils\auxfun_videos.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 272 | 9 | Do not assign a `lambda` expression, use a `def` | + +Quick open commands: + +```powershell +code -g "deeplabcut\utils\auxfun_videos.py:272" +``` + +## B008 + +Count: **3** +Hint: Function call in default arg. Use `None` + initialize inside the function. + +### Files affected + +| File | Count | +|---|---:| +| `deeplabcut\core\inferenceutils.py` | 1 | +| `deeplabcut\pose_estimation_tensorflow\nnets\conv_blocks.py` | 1 | +| `examples\testscript_pytorch_single_animal.py` | 1 | + +### Details + +#### `deeplabcut\core\inferenceutils.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 1200 | 20 | Do not perform function call `np.linspace` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable | + +Quick open commands: + +```powershell +code -g "deeplabcut\core\inferenceutils.py:1200" +``` + +#### `deeplabcut\pose_estimation_tensorflow\nnets\conv_blocks.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 176 | 20 | Do not perform function call `expand_input_by_factor` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\nnets\conv_blocks.py:176" +``` + +#### `examples\testscript_pytorch_single_animal.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 29 | 57 | Do not perform function call `SyntheticProjectParameters` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable | + +Quick open commands: + +```powershell +code -g "examples\testscript_pytorch_single_animal.py:29" +``` + +## B023 + +Count: **2** +Hint: Function closes over loop variable. Bind it via default arg or helper. + +### Files affected + +| File | Count | +|---|---:| +| `deeplabcut\gui\tabs\train_network.py` | 1 | +| `deeplabcut\refine_training_dataset\stitch.py` | 1 | + +### Details + +#### `deeplabcut\gui\tabs\train_network.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 188 | 93 | Function definition does not bind loop variable `attribute` | + +Quick open commands: + +```powershell +code -g "deeplabcut\gui\tabs\train_network.py:188" +``` + +#### `deeplabcut\refine_training_dataset\stitch.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 1177 | 32 | Function definition does not bind loop variable `stitcher` | + +Quick open commands: + +```powershell +code -g "deeplabcut\refine_training_dataset\stitch.py:1177" +``` + +## B024 + +Count: **2** +Hint: ABC without abstract method. Add `@abstractmethod` or remove ABC intent. + +### Files affected + +| File | Count | +|---|---:| +| `deeplabcut\pose_estimation_pytorch\data\ctd.py` | 1 | +| `deeplabcut\pose_estimation_pytorch\runners\shelving.py` | 1 | + +### Details + +#### `deeplabcut\pose_estimation_pytorch\data\ctd.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 26 | 7 | `CondProvider` is an abstract base class, but it has no abstract methods or properties | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\data\ctd.py:26" +``` + +#### `deeplabcut\pose_estimation_pytorch\runners\shelving.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 21 | 7 | `ShelfManager` is an abstract base class, but it has no abstract methods or properties | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\runners\shelving.py:21" +``` + +## F811 + +Count: **2** +Hint: Redefined while unused. Remove duplicate or rename. + +### Files affected + +| File | Count | +|---|---:| +| `deeplabcut\pose_tracking_pytorch\processor\processor.py` | 1 | +| `tests\generate_training_dataset\test_trainset_metadata.py` | 1 | + +### Details + +#### `deeplabcut\pose_tracking_pytorch\processor\processor.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 26 | 5 | Redefinition of unused `dist` from line 19: `dist` redefined here | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_tracking_pytorch\processor\processor.py:26" +``` + +#### `tests\generate_training_dataset\test_trainset_metadata.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 246 | 5 | Redefinition of unused `test_add_shuffle` from line 210: `test_add_shuffle` redefined here | + +Quick open commands: + +```powershell +code -g "tests\generate_training_dataset\test_trainset_metadata.py:246" +``` + +## B011 + +Count: **1** + +### Files affected + +| File | Count | +|---|---:| +| `deeplabcut\pose_estimation_tensorflow\nnets\utils.py` | 1 | + +### Details + +#### `deeplabcut\pose_estimation_tensorflow\nnets\utils.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 115 | 16 | Do not `assert False` (`python -O` removes these calls), raise `AssertionError()` | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\nnets\utils.py:115" +``` + +## B012 + +Count: **1** +Hint: Jump statement in `finally` can swallow exceptions. Restructure flow. + +### Files affected + +| File | Count | +|---|---:| +| `deeplabcut\pose_estimation_tensorflow\predict_videos.py` | 1 | + +### Details + +#### `deeplabcut\pose_estimation_tensorflow\predict_videos.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 1053 | 9 | `return` inside `finally` blocks cause exceptions to be silenced | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_tensorflow\predict_videos.py:1053" +``` + +## B016 + +Count: **1** +Hint: Raise an exception instance/class, not a literal. + +### Files affected + +| File | Count | +|---|---:| +| `examples\testscript_3d.py` | 1 | + +### Details + +#### `examples\testscript_3d.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 126 | 16 | Cannot raise a literal. Did you intend to return it or raise an Exception? | + +Quick open commands: + +```powershell +code -g "examples\testscript_3d.py:126" +``` + +## B017 + +Count: **1** +Hint: Use a more specific exception with `assertRaises`. + +### Files affected + +| File | Count | +|---|---:| +| `tests\pose_estimation_pytorch\other\test_api_utils.py` | 1 | + +### Details + +#### `tests\pose_estimation_pytorch\other\test_api_utils.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 67 | 14 | Do not assert blind exception: `Exception` | + +Quick open commands: + +```powershell +code -g "tests\pose_estimation_pytorch\other\test_api_utils.py:67" +``` + +## B020 + +Count: **1** +Hint: Loop variable overrides iterator. Rename loop variables. + +### Files affected + +| File | Count | +|---|---:| +| `deeplabcut\pose_estimation_pytorch\models\heads\simple_head.py` | 1 | + +### Details + +#### `deeplabcut\pose_estimation_pytorch\models\heads\simple_head.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 196 | 13 | Loop control variable `out_channels` overrides iterable it iterates | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\models\heads\simple_head.py:196" +``` + +## B027 + +Count: **1** +Hint: Empty method in ABC without abstract decorator. Add `@abstractmethod` or implement it. + +### Files affected + +| File | Count | +|---|---:| +| `deeplabcut\pose_estimation_pytorch\models\modules\kpt_encoders.py` | 1 | + +### Details + +#### `deeplabcut\pose_estimation_pytorch\models\modules\kpt_encoders.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 47 | 5 | `BaseKeypointEncoder.num_channels` is an empty method in an abstract base class, but has no abstract decorator | + +Quick open commands: + +```powershell +code -g "deeplabcut\pose_estimation_pytorch\models\modules\kpt_encoders.py:47" +``` + +## UP028 + +Count: **1** + +### Files affected + +| File | Count | +|---|---:| +| `tools\update_license_headers.py` | 1 | + +### Details + +#### `tools\update_license_headers.py` (1) + +| Line | Col | Message | +|---:|---:|---| +| 32 | 13 | Replace `yield` over `for` loop with `yield from` | + +Quick open commands: + +```powershell +code -g "tools\update_license_headers.py:32" +``` diff --git a/tests/conftest.py b/tests/conftest.py index 1b29154f98..0d386336b7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,18 +8,19 @@ # # Licensed under GNU Lesser General Public License v3.0 # -import numpy as np import os import pickle -import pytest import shutil import urllib.request import zipfile -from deeplabcut.core import inferenceutils from io import BytesIO + +import numpy as np +import pytest from PIL import Image from tqdm import tqdm +from deeplabcut.core import inferenceutils TEST_DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data") diff --git a/tests/core/inferenceutils/test_map_computation.py b/tests/core/inferenceutils/test_map_computation.py index 5d67eee982..3cf4cfb9be 100644 --- a/tests/core/inferenceutils/test_map_computation.py +++ b/tests/core/inferenceutils/test_map_computation.py @@ -273,7 +273,7 @@ def _evaluate(gt: dict[str, np.ndarray], pred: dict[str, np.ndarray]): coco_pred = _to_coco_predictions(coco_gt, pred, bbox_margin=0) coco_oks = eval_coco(coco_gt, coco_pred, num_joints) print(20 * "-") - print(f"dlc mAP:") + print("dlc mAP:") for k, v in oks.items(): print(k) print(v) @@ -414,5 +414,5 @@ def eval_coco( coco_eval.summarize() return float(coco_eval.stats[0]) - except ModuleNotFoundError as err: - print(f"pycocotools is not installed") + except ModuleNotFoundError: + print("pycocotools is not installed") diff --git a/tests/core/metrics/test_metrics_map_computation.py b/tests/core/metrics/test_metrics_map_computation.py index be36f37416..1e7c1ae9bd 100644 --- a/tests/core/metrics/test_metrics_map_computation.py +++ b/tests/core/metrics/test_metrics_map_computation.py @@ -267,7 +267,7 @@ def _evaluate(gt: dict[str, np.ndarray], pred: dict[str, np.ndarray]): coco_pred = _to_coco_predictions(coco_gt, pred, bbox_margin=0) coco_oks = eval_coco(coco_gt, coco_pred, num_joints) print(20 * "-") - print(f"dlc mAP:") + print("dlc mAP:") for k, v in oks.items(): print(k) print(v) @@ -395,5 +395,5 @@ def eval_coco( coco_eval.summarize() return float(coco_eval.stats[0]) - except ModuleNotFoundError as err: - print(f"pycocotools is not installed") + except ModuleNotFoundError: + print("pycocotools is not installed") diff --git a/tests/create_project/test_video_set_configuration.py b/tests/create_project/test_video_set_configuration.py index 0b03f6840d..32d7552379 100644 --- a/tests/create_project/test_video_set_configuration.py +++ b/tests/create_project/test_video_set_configuration.py @@ -13,6 +13,7 @@ import warnings from pathlib import Path from unittest.mock import Mock, patch + import pytest import deeplabcut.create_project.new as new_module @@ -212,7 +213,7 @@ def test_invalid_video_removed_from_project( ): """Test that invalid videos are removed from the project""" # Mock VideoReader to raise IOError - mock_reader = Mock(side_effect=IOError("Cannot open video")) + mock_reader = Mock(side_effect=OSError("Cannot open video")) with patch("deeplabcut.create_project.new.VideoReader", mock_reader): with warnings.catch_warnings(record=True): diff --git a/tests/generate_training_dataset/test_trainset_metadata.py b/tests/generate_training_dataset/test_trainset_metadata.py index 4f75d80d7e..6683844d75 100644 --- a/tests/generate_training_dataset/test_trainset_metadata.py +++ b/tests/generate_training_dataset/test_trainset_metadata.py @@ -11,6 +11,7 @@ """Tests for deeplabcut/generate_training_dataset/metadata.py""" from __future__ import annotations + import pickle import pytest @@ -166,7 +167,7 @@ def test_save_metadata_simple(tmpdir, data): print(trainset_meta) trainset_meta.save() - with open(meta_path, "r") as f: + with open(meta_path) as f: meta = YAML().load(f) print(data) print(meta) diff --git a/tests/pose_estimation_pytorch/apis/test_apis_evaluate.py b/tests/pose_estimation_pytorch/apis/test_apis_evaluate.py index 99997fef63..c671265255 100644 --- a/tests/pose_estimation_pytorch/apis/test_apis_evaluate.py +++ b/tests/pose_estimation_pytorch/apis/test_apis_evaluate.py @@ -17,7 +17,6 @@ import deeplabcut.pose_estimation_pytorch.apis as apis import deeplabcut.pose_estimation_pytorch.data as data - PREDICT = Mock() diff --git a/tests/pose_estimation_pytorch/apis/test_apis_export.py b/tests/pose_estimation_pytorch/apis/test_apis_export.py index 66f3b5c695..d8dabdd36f 100644 --- a/tests/pose_estimation_pytorch/apis/test_apis_export.py +++ b/tests/pose_estimation_pytorch/apis/test_apis_export.py @@ -27,7 +27,7 @@ @pytest.fixture() def project_dir(tmp_path_factory) -> Path: project_dir = tmp_path_factory.mktemp("tmp-project") - print(f"\nTemporary project directory:") + print("\nTemporary project directory:") print(str(project_dir)) print("---") yield project_dir diff --git a/tests/pose_estimation_pytorch/apis/test_tracklets.py b/tests/pose_estimation_pytorch/apis/test_tracklets.py index 50823b0710..8e06b4a55e 100644 --- a/tests/pose_estimation_pytorch/apis/test_tracklets.py +++ b/tests/pose_estimation_pytorch/apis/test_tracklets.py @@ -95,6 +95,6 @@ def test_build_tracklets( if unique_bodyparts: assert "single" in tracklets else: - assert not "single" in tracklets + assert "single" not in tracklets assert isinstance(tracklets, dict) diff --git a/tests/pose_estimation_pytorch/data/test_data_ctd.py b/tests/pose_estimation_pytorch/data/test_data_ctd.py index 0739416c81..eeb6be8d7f 100644 --- a/tests/pose_estimation_pytorch/data/test_data_ctd.py +++ b/tests/pose_estimation_pytorch/data/test_data_ctd.py @@ -18,7 +18,6 @@ from deeplabcut.pose_estimation_pytorch.data.ctd import CondFromFile - CONDITIONS = [ np.zeros((4, 3, 3)).tolist(), np.ones((4, 3, 3)).tolist(), diff --git a/tests/pose_estimation_pytorch/data/test_preprocessor.py b/tests/pose_estimation_pytorch/data/test_preprocessor.py index 9ef21749ca..4e6077ef93 100644 --- a/tests/pose_estimation_pytorch/data/test_preprocessor.py +++ b/tests/pose_estimation_pytorch/data/test_preprocessor.py @@ -13,13 +13,12 @@ import albumentations as A import numpy as np import pytest -from albumentations import BaseCompose -from deeplabcut.pose_estimation_pytorch.data.transforms import build_resize_transforms from deeplabcut.pose_estimation_pytorch.data.preprocessor import ( AugmentImage, build_conditional_top_down_preprocessor, ) +from deeplabcut.pose_estimation_pytorch.data.transforms import build_resize_transforms @pytest.mark.parametrize( diff --git a/tests/pose_estimation_pytorch/models/target_generators/test_heatmap_targets.py b/tests/pose_estimation_pytorch/models/target_generators/test_heatmap_targets.py index c46447dca1..7f418f4912 100644 --- a/tests/pose_estimation_pytorch/models/target_generators/test_heatmap_targets.py +++ b/tests/pose_estimation_pytorch/models/target_generators/test_heatmap_targets.py @@ -11,8 +11,8 @@ """Tests the heatmap target generators (plateau and gaussian)""" import numpy as np -import torch import pytest +import torch from deeplabcut.pose_estimation_pytorch.models.target_generators.heatmap_targets import ( HeatmapGaussianGenerator, diff --git a/tests/pose_estimation_pytorch/models/target_generators/test_plateau_targets.py b/tests/pose_estimation_pytorch/models/target_generators/test_plateau_targets.py index e577959d45..d335fc5524 100644 --- a/tests/pose_estimation_pytorch/models/target_generators/test_plateau_targets.py +++ b/tests/pose_estimation_pytorch/models/target_generators/test_plateau_targets.py @@ -11,8 +11,8 @@ """Tests the heatmap target generators (plateau and gaussian)""" import numpy as np -import torch import pytest +import torch from deeplabcut.pose_estimation_pytorch.models.target_generators.heatmap_targets import ( HeatmapGenerator, diff --git a/tests/pose_estimation_pytorch/modelzoo/test_webapp.py b/tests/pose_estimation_pytorch/modelzoo/test_webapp.py index 6b7e33cde9..34a210b462 100644 --- a/tests/pose_estimation_pytorch/modelzoo/test_webapp.py +++ b/tests/pose_estimation_pytorch/modelzoo/test_webapp.py @@ -8,9 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -import os -import cv2 import numpy as np import pytest diff --git a/tests/pose_estimation_pytorch/other/test_data_helper.py b/tests/pose_estimation_pytorch/other/test_data_helper.py index bfb83dde7f..73db76316d 100644 --- a/tests/pose_estimation_pytorch/other/test_data_helper.py +++ b/tests/pose_estimation_pytorch/other/test_data_helper.py @@ -11,15 +11,15 @@ from __future__ import annotations import os -from unittest.mock import patch, Mock +from unittest.mock import Mock, patch from zipfile import Path import numpy as np import pytest +from deeplabcut.generate_training_dataset import create_training_dataset from deeplabcut.pose_estimation_pytorch.data.dlcloader import DLCLoader from deeplabcut.pose_estimation_pytorch.data.utils import merge_list_of_dicts -from deeplabcut.generate_training_dataset import create_training_dataset def mock_aux() -> Mock: diff --git a/tests/pose_estimation_pytorch/other/test_heatmap_plateau_targets.py b/tests/pose_estimation_pytorch/other/test_heatmap_plateau_targets.py index f4099889df..d3b66eaf8d 100644 --- a/tests/pose_estimation_pytorch/other/test_heatmap_plateau_targets.py +++ b/tests/pose_estimation_pytorch/other/test_heatmap_plateau_targets.py @@ -9,7 +9,6 @@ # Licensed under GNU Lesser General Public License v3.0 # -from typing import Tuple import pytest import torch @@ -21,7 +20,7 @@ def get_target( batch_size: int, num_animals: int, num_joints: int, - image_size: Tuple[int, int], + image_size: tuple[int, int], locref_std: float, pos_dist_thresh: int, ): @@ -81,7 +80,7 @@ def test_expected_output( batch_size: int, num_animals: int, num_joints: int, - image_size: Tuple[int, int], + image_size: tuple[int, int], locref_stdev: float, pos_dist_thresh: int, ): @@ -147,7 +146,7 @@ def test_single_animal( batch_size: int, num_animals: int, num_joints: int, - image_size: Tuple[int, int], + image_size: tuple[int, int], locref_stdev: float, pos_dist_thresh: int, ): diff --git a/tests/pose_estimation_pytorch/other/test_helper.py b/tests/pose_estimation_pytorch/other/test_helper.py index c7cd0fd6f1..afd5825df1 100644 --- a/tests/pose_estimation_pytorch/other/test_helper.py +++ b/tests/pose_estimation_pytorch/other/test_helper.py @@ -13,9 +13,9 @@ def test_train_valid_call(): tmp_model = torch.nn.Linear(3, 10) - to_train_mode = getattr(tmp_model, "train") + to_train_mode = tmp_model.train to_train_mode() assert tmp_model.training == True - to_valid_mode = getattr(tmp_model, "eval") + to_valid_mode = tmp_model.eval to_valid_mode() assert tmp_model.training == False diff --git a/tests/pose_estimation_pytorch/other/test_modelzoo.py b/tests/pose_estimation_pytorch/other/test_modelzoo.py index 4edb980b3d..18fddfd931 100644 --- a/tests/pose_estimation_pytorch/other/test_modelzoo.py +++ b/tests/pose_estimation_pytorch/other/test_modelzoo.py @@ -44,7 +44,7 @@ def test_video_inference_saves_file(video_paths, superanimal_name): if isinstance(video_paths, str): video_paths = [video_paths] for video_path in video_paths: - output_path = video_path.replace(".mp4", f"_labeled.mp4") + output_path = video_path.replace(".mp4", "_labeled.mp4") assert os.path.exists(output_path), "Output video file does not exist" assert os.stat(output_path).st_size > 0, "Output video file is empty" diff --git a/tests/pose_estimation_pytorch/other/test_pose_model.py b/tests/pose_estimation_pytorch/other/test_pose_model.py index 680e82604f..de36678c8c 100644 --- a/tests/pose_estimation_pytorch/other/test_pose_model.py +++ b/tests/pose_estimation_pytorch/other/test_pose_model.py @@ -15,7 +15,7 @@ import torch import deeplabcut.pose_estimation_pytorch.models as dlc_models -from deeplabcut.pose_estimation_pytorch.models import CRITERIONS, TARGET_GENERATORS, PREDICTORS +from deeplabcut.pose_estimation_pytorch.models import CRITERIONS, PREDICTORS, TARGET_GENERATORS from deeplabcut.pose_estimation_pytorch.models.criterions import LOSS_AGGREGATORS from deeplabcut.pose_estimation_pytorch.models.modules import AdaptBlock, BasicBlock diff --git a/tests/pose_estimation_pytorch/runners/bottum_up.py b/tests/pose_estimation_pytorch/runners/bottum_up.py index bd9e10e50c..74a513b3a8 100644 --- a/tests/pose_estimation_pytorch/runners/bottum_up.py +++ b/tests/pose_estimation_pytorch/runners/bottum_up.py @@ -11,25 +11,24 @@ """Tests for the bottom-up pytorch runner""" from pathlib import Path -from typing import Dict, Any +from typing import Any import pytest import torch -from deeplabcut.pose_estimation_pytorch.config import make_pytorch_pose_config - -from deeplabcut.pose_estimation_pytorch.models import PoseModel, LOSSES, PREDICTORS from deeplabcut.pose_estimation_pytorch.models.criterion import WeightedAggregateLoss + +from deeplabcut.pose_estimation_pytorch.config import make_pytorch_pose_config +from deeplabcut.pose_estimation_pytorch.models import LOSSES, PREDICTORS, PoseModel from deeplabcut.pose_estimation_pytorch.runners import RUNNERS from deeplabcut.pose_estimation_pytorch.runners.schedulers import LRListScheduler from deeplabcut.utils import auxiliaryfunctions - SINGLE_ANIMAL_NETS = ["resnet_50"] MULTI_ANIMAL_NETS = ["dekr_w18"] NETS = [(n, False) for n in SINGLE_ANIMAL_NETS] + [(n, True) for n in MULTI_ANIMAL_NETS] -def print_dict(data: Dict, indent: int = 0): +def print_dict(data: dict, indent: int = 0): for k, v in data.items(): if isinstance(v, dict): print_dict(v, indent=indent + 2) @@ -42,7 +41,7 @@ def test_build_bottom_up_runner( net_type: str, multianimal: bool, ) -> None: - project_cfg: Dict[str, Any] = {"multianimalproject": multianimal} + project_cfg: dict[str, Any] = {"multianimalproject": multianimal} if multianimal: project_cfg["bodyparts"] = "MULTI!" project_cfg["multianimalbodyparts"] = ["head", "shoulder", "knee", "toe"] diff --git a/tests/pose_estimation_pytorch/runners/test_dynamic_cropper.py b/tests/pose_estimation_pytorch/runners/test_dynamic_cropper.py index 2d90ecbba6..8c56f8883c 100644 --- a/tests/pose_estimation_pytorch/runners/test_dynamic_cropper.py +++ b/tests/pose_estimation_pytorch/runners/test_dynamic_cropper.py @@ -10,9 +10,8 @@ # """Tests the dynamic cropper""" -import pytest - import numpy as np +import pytest import torch from deeplabcut.pose_estimation_pytorch.runners.dynamic_cropping import ( diff --git a/tests/test_auxfun_models.py b/tests/test_auxfun_models.py index 5ed026c837..0684da7651 100644 --- a/tests/test_auxfun_models.py +++ b/tests/test_auxfun_models.py @@ -10,9 +10,9 @@ # +import unittest from pathlib import Path from tempfile import TemporaryDirectory -import unittest from unittest.mock import patch from deeplabcut.utils.auxfun_models import MODELTYPE_FILEPATH_MAP, check_for_weights diff --git a/tests/test_auxfun_multianimal.py b/tests/test_auxfun_multianimal.py index 91601d5927..8e8b7d28dc 100644 --- a/tests/test_auxfun_multianimal.py +++ b/tests/test_auxfun_multianimal.py @@ -8,12 +8,14 @@ # # Licensed under GNU Lesser General Public License v3.0 # +from itertools import combinations + import networkx as nx import numpy as np import pandas as pd import pytest + from deeplabcut.utils import auxfun_multianimal -from itertools import combinations def test_prune_paf_graph(): diff --git a/tests/test_auxiliaryfunctions.py b/tests/test_auxiliaryfunctions.py index a7a24571b2..323dc321c8 100644 --- a/tests/test_auxiliaryfunctions.py +++ b/tests/test_auxiliaryfunctions.py @@ -9,7 +9,9 @@ # Licensed under GNU Lesser General Public License v3.0 # from pathlib import Path + import pytest + from deeplabcut.utils import auxiliaryfunctions from deeplabcut.utils.auxfun_videos import SUPPORTED_VIDEOS diff --git a/tests/test_conversioncode.py b/tests/test_conversioncode.py index 3ec57e0197..ca287ba861 100644 --- a/tests/test_conversioncode.py +++ b/tests/test_conversioncode.py @@ -9,8 +9,10 @@ # Licensed under GNU Lesser General Public License v3.0 # import os + import pandas as pd from conftest import TEST_DATA_DIR + from deeplabcut.utils import conversioncode diff --git a/tests/test_crossvalutils.py b/tests/test_crossvalutils.py index 6cecdca53b..5c821bd03e 100644 --- a/tests/test_crossvalutils.py +++ b/tests/test_crossvalutils.py @@ -8,8 +8,10 @@ # # Licensed under GNU Lesser General Public License v3.0 # -import numpy as np import pickle + +import numpy as np + from deeplabcut.core import crossvalutils BEST_GRAPH = [14, 15, 16, 11, 22, 31, 61, 7, 59, 62, 64] diff --git a/tests/test_dataset_augmentation.py b/tests/test_dataset_augmentation.py index 5350791ea0..57d33ca909 100644 --- a/tests/test_dataset_augmentation.py +++ b/tests/test_dataset_augmentation.py @@ -11,6 +11,7 @@ import imgaug.augmenters as iaa import numpy as np import pytest + from deeplabcut.pose_estimation_tensorflow.datasets import augmentation diff --git a/tests/test_frame_selection_tools.py b/tests/test_frame_selection_tools.py index 6746a9bb65..1241415456 100644 --- a/tests/test_frame_selection_tools.py +++ b/tests/test_frame_selection_tools.py @@ -12,7 +12,9 @@ import math from unittest.mock import Mock + import pytest + import deeplabcut.utils.frameselectiontools as fst diff --git a/tests/test_inferenceutils.py b/tests/test_inferenceutils.py index 1684095c41..1a427bd3d5 100644 --- a/tests/test_inferenceutils.py +++ b/tests/test_inferenceutils.py @@ -8,15 +8,17 @@ # # Licensed under GNU Lesser General Public License v3.0 # -import numpy as np import os import pickle +from copy import deepcopy + +import numpy as np import pytest from conftest import TEST_DATA_DIR -from copy import deepcopy -from deeplabcut.core import inferenceutils from scipy.spatial.distance import squareform +from deeplabcut.core import inferenceutils + def test_conv_square_to_condensed_indices(): n = 5 diff --git a/tests/test_pose_multianimal_imgaug.py b/tests/test_pose_multianimal_imgaug.py index 5c7c8bf4fd..6fcc92fde5 100644 --- a/tests/test_pose_multianimal_imgaug.py +++ b/tests/test_pose_multianimal_imgaug.py @@ -8,14 +8,16 @@ # # Licensed under GNU Lesser General Public License v3.0 # -import numpy as np import os + +import numpy as np import pytest from conftest import TEST_DATA_DIR + from deeplabcut.pose_estimation_tensorflow.datasets import ( Batch, - pose_multianimal_imgaug, PoseDatasetFactory, + pose_multianimal_imgaug, ) from deeplabcut.utils import read_plainconfig diff --git a/tests/test_predict_multianimal.py b/tests/test_predict_multianimal.py index 85582a0275..9f7b342d53 100644 --- a/tests/test_predict_multianimal.py +++ b/tests/test_predict_multianimal.py @@ -10,8 +10,8 @@ # import numpy as np import tensorflow as tf -from deeplabcut.pose_estimation_tensorflow.core import predict_multianimal +from deeplabcut.pose_estimation_tensorflow.core import predict_multianimal RADIUS = 5 THRESHOLD = 0.01 diff --git a/tests/test_predict_supermodel.py b/tests/test_predict_supermodel.py index e10d211400..ae575cfabe 100644 --- a/tests/test_predict_supermodel.py +++ b/tests/test_predict_supermodel.py @@ -10,6 +10,7 @@ # import numpy as np import pytest + from deeplabcut.pose_estimation_tensorflow.modelzoo.api import superanimal_inference diff --git a/tests/test_stitcher.py b/tests/test_stitcher.py index 033552e54c..7422de34f6 100644 --- a/tests/test_stitcher.py +++ b/tests/test_stitcher.py @@ -11,8 +11,8 @@ import numpy as np import pandas as pd import pytest -from deeplabcut.refine_training_dataset.stitch import Tracklet, TrackletStitcher +from deeplabcut.refine_training_dataset.stitch import Tracklet, TrackletStitcher TRACKLET_LEN = 1000 TRACKLET_START = 50 diff --git a/tests/test_trackingutils.py b/tests/test_trackingutils.py index 7c7f51459d..b3dac6d263 100644 --- a/tests/test_trackingutils.py +++ b/tests/test_trackingutils.py @@ -10,6 +10,7 @@ # import numpy as np import pytest + from deeplabcut.core import trackingutils diff --git a/tests/test_trainingsetmanipulation.py b/tests/test_trainingsetmanipulation.py index 97a8caf69a..3737448e32 100644 --- a/tests/test_trainingsetmanipulation.py +++ b/tests/test_trainingsetmanipulation.py @@ -8,26 +8,25 @@ # # Licensed under GNU Lesser General Public License v3.0 # -import numpy as np import os + +import numpy as np import pandas as pd import pytest -from typing import List - from conftest import TEST_DATA_DIR +from skimage import color, io + from deeplabcut.generate_training_dataset import ( - read_image_shape_fast, SplitTrials, - format_training_data, format_multianimal_training_data, - trainingsetmanipulation, + format_training_data, multiple_individuals_trainingsetmanipulation, parse_video_filenames, + read_image_shape_fast, + trainingsetmanipulation, ) - from deeplabcut.utils.auxfun_videos import imread from deeplabcut.utils.conversioncode import guarantee_multiindex_rows -from skimage import color, io def test_read_image_shape_fast(tmp_path): @@ -106,6 +105,6 @@ def test_format_multianimal_training_data(monkeypatch): (["/a/v1.mp4", "/a/v2.mov", "/b/v2.mov", "/b/v3.mp4"], ["v1", "v2", "v3"]), ], ) -def test_parse_video_filenames(videos: List[str], expected_filenames: List[str]): +def test_parse_video_filenames(videos: list[str], expected_filenames: list[str]): filenames = parse_video_filenames(videos) assert filenames == expected_filenames diff --git a/tests/test_triangulation.py b/tests/test_triangulation.py index 02bf503447..a1b2fe382c 100644 --- a/tests/test_triangulation.py +++ b/tests/test_triangulation.py @@ -11,6 +11,7 @@ import numpy as np import pandas as pd import pytest + from deeplabcut.pose_estimation_3d import triangulation diff --git a/tests/test_video.py b/tests/test_video.py index 442c3d0b62..915933cf6c 100644 --- a/tests/test_video.py +++ b/tests/test_video.py @@ -9,10 +9,11 @@ # Licensed under GNU Lesser General Public License v3.0 # import os + import pytest from conftest import TEST_DATA_DIR -from deeplabcut.utils.auxfun_videos import VideoWriter +from deeplabcut.utils.auxfun_videos import VideoWriter POS_FRAMES = 1 # Equivalent to cv2.CAP_PROP_POS_FRAMES diff --git a/tests/utils/test_multiprocessing.py b/tests/utils/test_multiprocessing.py index a2133332ea..a5ab47dd5f 100644 --- a/tests/utils/test_multiprocessing.py +++ b/tests/utils/test_multiprocessing.py @@ -8,8 +8,10 @@ # # Licensed under GNU Lesser General Public License v3.0 # -import pytest import time + +import pytest + from deeplabcut.utils.multiprocessing import call_with_timeout diff --git a/testscript_cli.py b/testscript_cli.py index 40b371878e..b4ec3b02f1 100644 --- a/testscript_cli.py +++ b/testscript_cli.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- """ modified from: https://github.com/DeepLabCut/DeepLabCut-core/testscript_cli.py by Mackenzie. @@ -12,21 +11,18 @@ task = "Testcore" # Enter the name of your experiment Task scorer = "Mackenzie" # Enter the name of the experimenter/labeler -import os, subprocess, sys +import os +import platform +import numpy as np +import pandas as pd # def install(package): # subprocess.check_call([sys.executable, "-m", "pip", "install", package]) # install("tensorflow==1.13.1") - import deeplabcut as dlc from deeplabcut.core.engine import Engine -from pathlib import Path -import pandas as pd -import numpy as np -import platform - print("Imported DLC!") engine = Engine.PYTORCH diff --git a/tools/update_license_headers.py b/tools/update_license_headers.py index 4a05350e47..c06ae015b0 100644 --- a/tools/update_license_headers.py +++ b/tools/update_license_headers.py @@ -4,15 +4,16 @@ configuration, see the instructions in NOTICE.yml. """ -import tempfile -import glob -import yaml import fnmatch +import glob import subprocess +import tempfile + +import yaml def load_config(filename): - with open(filename, "r") as fh: + with open(filename) as fh: config = yaml.safe_load(fh) return config From e6a4637ade62f0fbe5d2eb83b25e3526165a6a92 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 17 Mar 2026 14:19:46 +0100 Subject: [PATCH 09/80] Enforce docstrings length and robustness in zips/warnings Standardize docstring punctuation and formatting across modules, add stacklevel=2 to warnings.warn calls for clearer warning locations, and make zip/dict(zip()) usages more robust by passing strict=False where appropriate. Also include minor loop variable/name cleanups and CLI help text formatting fixes. These changes improve error tolerance when iterables differ in length, produce more helpful warning tracebacks, and tidy user-facing documentation strings. --- deeplabcut/__init__.py | 3 +- deeplabcut/benchmark/base.py | 22 +++-- deeplabcut/benchmark/metrics.py | 16 ++- deeplabcut/benchmark/utils.py | 9 +- deeplabcut/cli.py | 55 ++++++----- deeplabcut/compat.py | 31 +++--- deeplabcut/core/config.py | 6 +- deeplabcut/core/crossvalutils.py | 18 ++-- deeplabcut/core/inferenceutils.py | 34 +++---- deeplabcut/core/metrics/bbox.py | 4 +- deeplabcut/core/metrics/matching.py | 10 +- deeplabcut/core/visualization.py | 4 +- deeplabcut/core/weight_init.py | 10 +- deeplabcut/create_project/add.py | 8 +- deeplabcut/create_project/modelzoo.py | 12 +-- deeplabcut/create_project/new.py | 4 +- deeplabcut/create_project/new_3d.py | 6 +- .../frame_extraction.py | 9 +- .../generate_training_dataset/metadata.py | 23 +++-- ...ple_individuals_trainingsetmanipulation.py | 10 +- .../trainingsetmanipulation.py | 59 +++++------ deeplabcut/gui/components.py | 7 +- .../gui/displays/selected_shuffle_display.py | 4 +- .../gui/displays/shuffle_metadata_viewer.py | 4 +- deeplabcut/gui/tabs/create_project.py | 6 +- .../gui/tabs/create_training_dataset.py | 12 +-- deeplabcut/gui/tabs/create_videos.py | 2 +- deeplabcut/gui/tabs/evaluate_network.py | 2 +- deeplabcut/gui/tabs/extract_frames.py | 11 +-- deeplabcut/gui/tabs/modelzoo.py | 4 +- deeplabcut/gui/tracklet_toolbox.py | 25 ++--- deeplabcut/gui/utils.py | 4 +- deeplabcut/gui/widgets.py | 4 +- deeplabcut/gui/window.py | 12 ++- deeplabcut/modelzoo/fmpose_3d/fmpose3d.py | 16 +-- .../datasets/base_dlc.py | 2 +- .../datasets/ma_dlc.py | 2 +- .../datasets/ma_dlc_dataframe.py | 3 +- .../datasets/materialize.py | 12 +-- .../datasets/multi.py | 20 ++-- .../datasets/single_dlc.py | 6 +- .../datasets/single_dlc_dataframe.py | 3 +- .../generalized_data_converter/utils.py | 27 +---- deeplabcut/modelzoo/utils.py | 11 +-- deeplabcut/modelzoo/video_inference.py | 6 +- deeplabcut/modelzoo/webapp/inference.py | 12 +-- .../pose_estimation_3d/camera_calibration.py | 12 +-- deeplabcut/pose_estimation_3d/plotting3D.py | 5 +- .../pose_estimation_3d/triangulation.py | 9 +- .../apis/analyze_images.py | 16 +-- .../pose_estimation_pytorch/apis/ctd.py | 6 +- .../apis/evaluation.py | 23 +++-- .../apis/prune_paf_graph.py | 14 +-- .../pose_estimation_pytorch/apis/tracklets.py | 12 +-- .../pose_estimation_pytorch/apis/training.py | 4 +- .../pose_estimation_pytorch/apis/utils.py | 26 +++-- .../pose_estimation_pytorch/apis/videos.py | 10 +- .../config/make_pose_config.py | 24 +++-- .../pose_estimation_pytorch/config/utils.py | 10 +- .../pose_estimation_pytorch/data/base.py | 24 +++-- .../data/cocoloader.py | 18 ++-- .../pose_estimation_pytorch/data/collate.py | 10 +- .../pose_estimation_pytorch/data/ctd.py | 6 +- .../pose_estimation_pytorch/data/dlcloader.py | 23 +++-- .../data/generative_sampling.py | 6 +- .../data/postprocessor.py | 44 ++++----- .../data/preprocessor.py | 51 +++++----- .../pose_estimation_pytorch/data/snapshots.py | 4 +- .../data/transforms.py | 30 +++--- .../pose_estimation_pytorch/data/utils.py | 53 ++++------ .../models/backbones/base.py | 6 +- .../models/backbones/cond_prenet.py | 10 +- .../models/backbones/hrnet_coam.py | 2 +- .../models/criterions/dekr.py | 6 +- .../models/criterions/kl_discrete.py | 2 +- .../models/criterions/weighted.py | 29 +++--- .../models/detectors/base.py | 14 ++- .../models/heads/base.py | 12 +-- .../models/heads/rtmcc_head.py | 6 +- .../models/heads/simple_head.py | 16 ++- .../models/heads/transformer.py | 8 +- .../pose_estimation_pytorch/models/model.py | 10 +- .../models/modules/coam_module.py | 40 ++++---- .../models/modules/conv_module.py | 2 +- .../models/modules/gated_attention_unit.py | 4 +- .../models/modules/kpt_encoders.py | 8 +- .../models/necks/base.py | 2 +- .../models/necks/layers.py | 2 +- .../models/necks/transformer.py | 9 +- .../models/predictors/dekr_predictor.py | 8 +- .../models/predictors/identity_predictor.py | 11 +-- .../models/predictors/paf_predictor.py | 9 +- .../models/predictors/sim_cc.py | 4 +- .../models/target_generators/base.py | 8 +- .../models/target_generators/dekr_targets.py | 5 +- .../target_generators/heatmap_targets.py | 26 +++-- .../models/target_generators/pafs_targets.py | 6 +- .../models/target_generators/sim_cc.py | 8 +- .../models/weight_init.py | 12 +-- .../modelzoo/inference.py | 12 +-- .../modelzoo/memory_replay.py | 9 +- .../pose_estimation_pytorch/modelzoo/utils.py | 9 +- .../pose_estimation_pytorch/registry.py | 13 ++- .../runners/dynamic_cropping.py | 26 +++-- .../runners/inference.py | 72 +++++++------- .../pose_estimation_pytorch/runners/logger.py | 45 ++++----- .../runners/shelving.py | 18 ++-- .../runners/snapshots.py | 9 +- .../pose_estimation_pytorch/runners/train.py | 20 ++-- deeplabcut/pose_estimation_pytorch/utils.py | 5 +- .../backbones/efficientnet_builder.py | 19 ++-- .../backbones/efficientnet_model.py | 40 +++++--- .../pose_estimation_tensorflow/config.py | 10 +- .../core/evaluate.py | 32 +++--- .../core/evaluate_multianimal.py | 8 +- .../core/openvino/session.py | 2 +- .../core/predict.py | 8 +- .../core/predict_multianimal.py | 4 +- .../datasets/pose_imgaug.py | 6 +- .../datasets/pose_multianimal_imgaug.py | 20 ++-- .../datasets/utils.py | 4 +- .../pose_estimation_tensorflow/export.py | 28 +++--- .../modelzoo/api/spatiotemporal_adapt.py | 10 +- .../pose_estimation_tensorflow/nnets/base.py | 3 +- .../nnets/efficientnet.py | 8 +- .../pose_estimation_tensorflow/nnets/multi.py | 1 - .../pose_estimation_tensorflow/nnets/utils.py | 6 +- .../predict_multianimal.py | 20 ++-- .../predict_videos.py | 22 ++--- .../pose_estimation_tensorflow/training.py | 4 +- .../visualizemaps.py | 4 +- deeplabcut/pose_tracking_pytorch/apis.py | 4 +- .../model/backbones/vit_pytorch.py | 14 +-- .../pose_tracking_pytorch/solver/cosine_lr.py | 2 +- .../pose_tracking_pytorch/solver/scheduler.py | 6 +- .../tracking_utils/meter.py | 2 +- .../train_dlctransreid.py | 1 - .../post_processing/analyze_skeleton.py | 13 ++- deeplabcut/post_processing/filtering.py | 10 +- .../refine_training_dataset/outlier_frames.py | 15 ++- deeplabcut/refine_training_dataset/stitch.py | 92 ++++++++--------- .../refine_training_dataset/tracklets.py | 21 ++-- deeplabcut/utils/auxfun_models.py | 19 ++-- deeplabcut/utils/auxfun_multianimal.py | 22 +++-- deeplabcut/utils/auxfun_videos.py | 33 +++---- deeplabcut/utils/auxiliaryfunctions.py | 72 +++++++------- deeplabcut/utils/auxiliaryfunctions_3d.py | 31 +++--- deeplabcut/utils/conversioncode.py | 31 +++--- deeplabcut/utils/make_labeled_video.py | 11 +-- deeplabcut/utils/plotting.py | 7 +- deeplabcut/utils/pseudo_label.py | 2 +- deeplabcut/utils/skeleton.py | 8 +- deeplabcut/utils/video_processor.py | 33 ++----- deeplabcut/utils/visualization.py | 17 ++-- docker/deeplabcut_docker.py | 18 ++-- .../COLAB/COLAB_BUCTD_and_CTD_tracking.ipynb | 3 +- examples/COLAB/COLAB_DEMO_SuperAnimal.ipynb | 5 +- .../COLAB/COLAB_HumanPose_with_RTMPose.ipynb | 12 ++- examples/JUPYTER/Demo_3D_DeepLabCut.ipynb | 1 - examples/testscript_multianimal.py | 2 +- examples/testscript_openfielddata.py | 3 +- ...pt_openfielddata_augmentationcomparison.py | 5 +- examples/testscript_pretrained_models.py | 5 +- examples/testscript_pytorch_multi_animal.py | 2 +- examples/testscript_pytorch_single_animal.py | 2 +- examples/testscript_superanimal_adaptation.py | 4 +- examples/testscript_superanimal_inference.py | 5 +- examples/testscript_transreid.py | 2 +- examples/utils.py | 6 +- ruff-report.md | 98 +++++++++---------- .../inferenceutils/test_map_computation.py | 4 +- .../metrics/test_metrics_map_computation.py | 4 +- .../test_video_set_configuration.py | 26 ++--- .../test_trainset_metadata.py | 26 ++--- .../apis/test_apis_evaluate.py | 2 +- .../apis/test_apis_export.py | 2 +- .../data/test_data_ctd.py | 5 +- .../data/test_preprocessor.py | 4 +- .../other/test_heatmap_plateau_targets.py | 11 +-- .../other/test_helper.py | 4 +- .../runners/bottum_up.py | 6 +- .../runners/test_dynamic_cropper.py | 4 +- tests/test_auxiliaryfunctions.py | 4 +- tests/test_frame_selection_tools.py | 2 +- tests/test_inferenceutils.py | 2 +- tests/test_pose_multianimal_imgaug.py | 6 +- tests/test_predict_supermodel.py | 4 +- tests/test_stitcher.py | 2 +- tools/update_license_headers.py | 9 +- 189 files changed, 1204 insertions(+), 1354 deletions(-) diff --git a/deeplabcut/__init__.py b/deeplabcut/__init__.py index abf01328d9..183aec16c4 100644 --- a/deeplabcut/__init__.py +++ b/deeplabcut/__init__.py @@ -74,7 +74,8 @@ """ As PyTorch is not installed, unsupervised identity learning will not be available. Please run `pip install torch`, or ignore this warning. - """ + """, + stacklevel=2, ) # Train, evaluate & predict functions / all require TF diff --git a/deeplabcut/benchmark/base.py b/deeplabcut/benchmark/base.py index 48a9850257..c19448ed9d 100644 --- a/deeplabcut/benchmark/base.py +++ b/deeplabcut/benchmark/base.py @@ -9,7 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # -"""Base classes for benchmark and result definition +"""Base classes for benchmark and result definition. Benchmarks subclass the abstract ``Benchmark`` class and are defined by ``name``, their ``keypoints`` names, as well as groundtruth and metadata necessary to run evaluation. @@ -46,9 +46,9 @@ class Benchmark(abc.ABC): def names(self): """A unique key to describe this submission, e.g. the model name. - This is also the name that will later appear in the benchmark table. - The name needs to be unique across the whole benchmark. Non-unique names - will raise an error during submission of a PR. + This is also the name that will later appear in the benchmark table. The name + needs to be unique across the whole benchmark. Non-unique names will raise an + error during submission of a PR. """ raise NotImplementedError() @@ -110,9 +110,10 @@ def evaluate(self, name: str, on_error="raise"): ) def _validate_predictions(self, name: str, predictions: dict) -> dict: - """Validates the submitted predictions object - Checks that there is a prediction for each test image, and raises a warning if - that is not the case. Returns only predictions made for test images. + """Validates the submitted predictions object Checks that there is a prediction + for each test image, and raises a warning if that is not the case. + + Returns only predictions made for test images. """ test_images = deeplabcut.benchmark.metrics.load_test_images(self.ground_truth, self.metadata) missing_images = set(test_images) - set(predictions.keys()) @@ -120,7 +121,8 @@ def _validate_predictions(self, name: str, predictions: dict) -> dict: warnings.warn( f"Missing {len(missing_images)} test images in the predictions for " f"{name}: {list(missing_images)} Metrics will be computed as if no " - "individuals were detected in those images." + "individuals were detected in those images.", + stacklevel=2, ) return {img: predictions.get(img, tuple()) for img in test_images} @@ -155,7 +157,7 @@ def primary_key(self) -> tuple[str]: @property def primary_key_names(self) -> tuple[str]: - """Names of the primary keys""" + """Names of the primary keys.""" return tuple(self._export_mapping.get(k) for k in self._primary_key) def __str__(self): @@ -185,7 +187,7 @@ def primary_key_names(self): return next(iter(self.results.values())).primary_key_names def toframe(self) -> pd.DataFrame: - """Convert results to pandas dataframe""" + """Convert results to pandas dataframe.""" return pd.DataFrame([result.todict() for result in self.results.values()]).set_index( list(self.primary_key_names) ) diff --git a/deeplabcut/benchmark/metrics.py b/deeplabcut/benchmark/metrics.py index 4cf8e96709..0c89590339 100644 --- a/deeplabcut/benchmark/metrics.py +++ b/deeplabcut/benchmark/metrics.py @@ -55,7 +55,7 @@ def _format_gt_data(h5file: str, test_indices: list[int] | None = None): meta = {"animals": animals, "keypoints": kpts, "n_unique": n_unique} return { - "annotations": dict(zip(file_paths, data)), + "annotations": dict(zip(file_paths, data, strict=False)), "metadata": meta, } @@ -108,10 +108,8 @@ def calc_prediction_errors(preds, gt): def _map(strings, substrings): - """ - Map image paths from predicted data to GT as the first are typically - absolute whereas the latter are relative to the project path. - """ + """Map image paths from predicted data to GT as the first are typically absolute + whereas the latter are relative to the project path.""" lookup = dict() strings_ = strings.copy() @@ -251,10 +249,8 @@ def calc_rmse_from_obj( def load_test_images(h5file: str, metadata: str) -> list[str]: - """ - Returns the names of the test images for the benchmark, in the order corresponding - to the test indices. - """ + """Returns the names of the test images for the benchmark, in the order + corresponding to the test indices.""" df = pd.read_hdf(h5file) test_indices = _load_test_indices(metadata) df_test = df.iloc[test_indices] @@ -267,7 +263,7 @@ def load_test_images(h5file: str, metadata: str) -> list[str]: def _load_test_indices(shuffle_metadata_path: str) -> list[int]: - """Returns the indices of test images in the training dataset dataframe""" + """Returns the indices of test images in the training dataset dataframe.""" with open(shuffle_metadata_path, "rb") as f: test_indices = set([int(i) for i in pickle.load(f)[2]]) return list(sorted(test_indices)) diff --git a/deeplabcut/benchmark/utils.py b/deeplabcut/benchmark/utils.py index c842ec67e2..9c93fcf579 100644 --- a/deeplabcut/benchmark/utils.py +++ b/deeplabcut/benchmark/utils.py @@ -9,8 +9,9 @@ # Licensed under GNU Lesser General Public License v3.0 # -"""Helper functions in this file are not affected by the main repositories -license. They are independent from the remainder of the benchmarking code. +"""Helper functions in this file are not affected by the main repositories license. + +They are independent from the remainder of the benchmarking code. """ import importlib @@ -50,7 +51,7 @@ def __init__(self): def import_submodules(package, recursive=True): - """Import all submodules of a module, recursively, including subpackages + """Import all submodules of a module, recursively, including subpackages. :param package: package (name or actual module) :type package: str | module @@ -63,7 +64,7 @@ def import_submodules(package, recursive=True): if isinstance(package, str): package = importlib.import_module(package) results = {} - for loader, name, is_pkg in pkgutil.walk_packages(package.__path__): + for _loader, name, is_pkg in pkgutil.walk_packages(package.__path__): full_name = package.__name__ + "." + name results[full_name] = importlib.import_module(full_name) if recursive and is_pkg: diff --git a/deeplabcut/cli.py b/deeplabcut/cli.py index ec2e024aec..3914485183 100644 --- a/deeplabcut/cli.py +++ b/deeplabcut/cli.py @@ -49,7 +49,9 @@ def main(ctx, verbose): # help='Directory to create project in. Default is cwd().') @click.pass_context def create_new_project(_, *args, **kwargs): - """Create a new project directory, sub-directories and a basic configuration file. The configuration file is loaded with default values. Change its parameters to your projects need.\n + """Create a new project directory, sub-directories and a basic configuration file. + The configuration file is loaded with default values. Change its parameters to your + projects need.\n. Options \n ---------- \n @@ -74,7 +76,6 @@ def create_new_project(_, *args, **kwargs): To create the project in another directory \n python3 dlc.py create_new_project reaching-task Tanmay /data/vies/mouse1.avi /data/videos/mouse2.avi /data/videos/mouse3.avi analysis/project -d home/project - """ from deeplabcut.create_project import new @@ -95,8 +96,7 @@ def create_new_project(_, *args, **kwargs): ) @click.pass_context def add_new_videos(_, *args, **kwargs): - """ - Add new videos to the config file at any stage of the project.\n + """Add new videos to the config file at any stage of the project.\n. Options\n ----------\n @@ -113,7 +113,6 @@ def add_new_videos(_, *args, **kwargs): Examples\n --------\n >>> python3 dlc.py add_new_videos /home/project/reaching-task-Tanmay-2018-08-23/config.yaml /data/videos/mouse5.avi - """ from deeplabcut.create_project import add @@ -139,9 +138,10 @@ def add_new_videos(_, *args, **kwargs): ) @click.pass_context def extract_frames(_, *args, **kwargs): - """ - Extracts frames from the videos in the config.yaml file. Only the videos in the config.yaml will be used to select the frames.\n - Use the function ``add_new_videos`` at any stage of the project to add new videos to the config file and extract their frames.\n + """Extracts frames from the videos in the config.yaml file. Only the videos in the + config.yaml will be used to select the frames.\n Use the function ``add_new_videos`` + at any stage of the project to add new videos to the config file and extract their + frames.\n. CONFIG : string \n Full path of the config.yaml file as a string. \n \n \n @@ -160,7 +160,6 @@ def extract_frames(_, *args, **kwargs): >>> deeplabcut.extract_frames /analysis/project/reaching-task/config.yaml manual \n While selecting the frames manually, you do not need to specify the cropping parameters. Rather, you will get a prompt in the graphic user interface to choose if you need to crop or not. \n -------- \n - """ from deeplabcut.generate_training_dataset import frameExtraction @@ -172,7 +171,9 @@ def extract_frames(_, *args, **kwargs): @click.argument("config") @click.pass_context def label_frames(_, config): - """Manually label/annotate the extracted frames. Update the list of body parts you want to localize in the config.yaml file first.\n + """Manually label/annotate the extracted frames. Update the list of body parts you + want to localize in the config.yaml file first.\n. + Example\n --------\n python3 dlc.py label_frames /analysis/project/reaching-task/config.yaml @@ -187,7 +188,11 @@ def label_frames(_, config): @click.argument("config") @click.pass_context def check_labels(_, config): - """Check if labels were stored correctly by plotting annotations and inspect them visually. If some are wrong, then use the refine_labels to correct the labels.\n""" + """Check if labels were stored correctly by plotting annotations and inspect them + visually. + + If some are wrong, then use the refine_labels to correct the labels.\n + """ from deeplabcut.generate_training_dataset import labelFrames labelFrames.check_labels(config) @@ -402,9 +407,9 @@ def analyze_videos(_, *args, **kwargs): ) @click.pass_context def extract_outlier_frames(_, *args, **kwargs): - """ - Extracts the outlier frames in case, the predictions are not correct for a certain video from the cropped video running from - start to stop as defined in config.yaml. + """Extracts the outlier frames in case, the predictions are not correct for a + certain video from the cropped video running from start to stop as defined in + config.yaml. Another crucial parameter in config.yaml is how many frames to extract 'numframes2extract'. @@ -424,7 +429,6 @@ def extract_outlier_frames(_, *args, **kwargs): for extracting the frames with kmeans and epsilon = 5 pixels.\n >>> python3 dlc.py extract_outlier_frames /analysis/project/reaching-task/config.yaml /analysis/project/video/reachinvideo1.avi --epsilon 5 --extractionalgorithm kmeans \n --------\n - """ from deeplabcut.refine_training_dataset import outlier_frames @@ -436,9 +440,10 @@ def extract_outlier_frames(_, *args, **kwargs): @click.argument("config") @click.pass_context def refine_labels(_, config): - """Refines the labels of the outlier frames extracted from the analyzed videos.\n Helps in augmenting the training dataset. - Use the function ``analyze_video`` to analyze a video and extracts the outlier frames using the function - ``extract_outlier_frames`` before refining the labels.\n + """Refines the labels of the outlier frames extracted from the analyzed videos.\n + Helps in augmenting the training dataset. Use the function ``analyze_video`` to + analyze a video and extracts the outlier frames using the function + ``extract_outlier_frames`` before refining the labels.\n. Examples \n --------\n @@ -491,8 +496,9 @@ def refine_labels(_, config): ) @click.pass_context def create_labeled_video(_, *args, **kwargs): - """ - Labels the bodyparts in a video. Make sure the video is already analyzed by the function 'analyze_video' + """Labels the bodyparts in a video. + + Make sure the video is already analyzed by the function 'analyze_video' """ from deeplabcut.utils import make_labeled_video @@ -528,8 +534,7 @@ def create_labeled_video(_, *args, **kwargs): ) @click.pass_context def plot_trajectories(_, *args, **kwargs): - """ - Plots the trajectories of various bodyparts across the video.\n + """Plots the trajectories of various bodyparts across the video.\n. Example\n --------\n @@ -604,9 +609,9 @@ def plot_trajectories(_, *args, **kwargs): ) @click.pass_context def export_model(_, *args, **kwargs): - """ - Export DLC models for the model zoo or for live inference.\n - Saves the pose configuration, snapshot files, and frozen graph of the model to a directory named exported-models within the project directory + """Export DLC models for the model zoo or for live inference.\n Saves the pose + configuration, snapshot files, and frozen graph of the model to a directory named + exported-models within the project directory. Parameters ----------- diff --git a/deeplabcut/compat.py b/deeplabcut/compat.py index 2cc5510864..13e2630c54 100644 --- a/deeplabcut/compat.py +++ b/deeplabcut/compat.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Compatibility file for methods available with either PyTorch or Tensorflow""" +"""Compatibility file for methods available with either PyTorch or Tensorflow.""" from __future__ import annotations @@ -87,8 +87,7 @@ def train_network( pose_threshold: float | None = 0.1, pytorch_cfg_updates: dict | None = None, ): - """ - Trains the network with the labels in the training dataset. + """Trains the network with the labels in the training dataset. Parameters ---------- @@ -316,9 +315,8 @@ def return_train_network_path( modelprefix: str = "", engine: Engine | None = None, ) -> tuple[Path, Path, Path]: - """ - Returns the training and test pose config file names as well as the folder where the - snapshot is + """Returns the training and test pose config file names as well as the folder where + the snapshot is. Parameters ---------- @@ -582,9 +580,10 @@ def return_evaluate_network_data( returnjustfns: bool = True, engine: Engine | None = None, ): - """ - Returns the results for (previously evaluated) network. deeplabcut.evaluate_network(..) - Returns list of (per model): [trainingsiterations,trainfraction,shuffle,trainerror,testerror,pcutoff,trainerrorpcutoff,testerrorpcutoff,Snapshots[snapindex],scale,net_type] + """Returns the results for (previously evaluated) network. + deeplabcut.evaluate_network(..) Returns list of (per model): [trainingsiterations,tr + ainfraction,shuffle,trainerror,testerror,pcutoff,trainerrorpcutoff,testerrorpcutoff, + Snapshots[snapindex],scale,net_type] This function is only implemented for tensorflow models/shuffles, and will throw an error if called with a PyTorch shuffle. @@ -1295,8 +1294,8 @@ def analyze_time_lapse_frames( modelprefix: str = "", engine: Engine | None = None, ): - """ - Analyzed all images (of type = frametype) in a folder and stores the output in one file. + """Analyzed all images (of type = frametype) in a folder and stores the output in + one file. You can crop the frames (before analysis), by changing 'cropping'=True and setting 'x1','x2','y1','y2' in the config file. @@ -1357,7 +1356,6 @@ def analyze_time_lapse_frames( Note: for test purposes one can extract all frames from a video with ffmeg, e.g. >>> ffmpeg -i testvideo.avi "thumb%04d.png" - """ if engine is None: engine = get_shuffle_engine( @@ -1415,8 +1413,8 @@ def convert_detections2tracklets( track_method: str = "", engine: Engine | None = None, ): - """ - This should be called at the end of deeplabcut.analyze_videos for multianimal projects! + """This should be called at the end of deeplabcut.analyze_videos for multianimal + projects! Parameters ---------- @@ -1497,7 +1495,6 @@ def convert_detections2tracklets( >>> ) -------- - """ if engine is None: engine = get_shuffle_engine( @@ -1565,8 +1562,7 @@ def extract_maps( modelprefix: str = "", engine: Engine | None = None, ): - """ - Extracts the scoremap, locref, partaffinityfields (if available). + """Extracts the scoremap, locref, partaffinityfields (if available). Returns a dictionary indexed by: trainingsetfraction, snapshotindex, and imageindex for those keys, each item contains: (image, scmap, locref, paf, bpt_names, @@ -1610,7 +1606,6 @@ def extract_maps( -------- If you want to extract the data for image 0 and 103 (of the training set) for model trained with shuffle 0. >>> deeplabcut.extract_maps(configfile,0,Indices=[0,103]) - """ if engine is None: engine = get_shuffle_engine( diff --git a/deeplabcut/core/config.py b/deeplabcut/core/config.py index 061a73e461..db222def3f 100644 --- a/deeplabcut/core/config.py +++ b/deeplabcut/core/config.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Simple helper methods related to configuration files stored in yaml files""" +"""Simple helper methods related to configuration files stored in yaml files.""" from __future__ import annotations @@ -33,7 +33,7 @@ def read_config_as_dict(config_path: str | Path) -> dict: def write_config(config_path: str | Path, config: dict, overwrite: bool = True) -> None: - """Writes a pose configuration file to disk + """Writes a pose configuration file to disk. Args: config_path: the path where the config should be saved @@ -55,7 +55,7 @@ def pretty_print( indent: int = 0, print_fn: Callable[[str], None] | None = None, ) -> None: - """Prints a model configuration in a pretty and readable way + """Prints a model configuration in a pretty and readable way. Args: config: the config to print diff --git a/deeplabcut/core/crossvalutils.py b/deeplabcut/core/crossvalutils.py index 0e6a324c28..e62f19ddcc 100644 --- a/deeplabcut/core/crossvalutils.py +++ b/deeplabcut/core/crossvalutils.py @@ -55,7 +55,7 @@ def _unsorted_unique(array): def find_closest_neighbors(query: np.ndarray, ref: np.ndarray, k: int = 3) -> np.ndarray: - """Greedy matching of predicted keypoints to ground truth keypoints + """Greedy matching of predicted keypoints to ground truth keypoints. Args: query: the query keypoints @@ -143,13 +143,13 @@ def _calc_within_between_pafs( coords = dict_["prediction"]["coordinates"][0] # Get animal IDs and corresponding indices in the arrays of detections lookup = dict() - for i, (coord, coord_gt) in enumerate(zip(coords, coords_gt)): + for i, (coord, coord_gt) in enumerate(zip(coords, coords_gt, strict=False)): inds = np.flatnonzero(np.all(~np.isnan(coord), axis=1)) inds_gt = np.flatnonzero(np.all(~np.isnan(coord_gt), axis=1)) if inds.size and inds_gt.size: neighbors = find_closest_neighbors(coord_gt[inds_gt], coord[inds], k=3) found = neighbors != -1 - lookup[i] = dict(zip(inds_gt[found], inds[neighbors[found]])) + lookup[i] = dict(zip(inds_gt[found], inds[neighbors[found]], strict=False)) costs = dict_["prediction"]["costs"] for k, v in costs.items(): @@ -222,7 +222,7 @@ def _benchmark_paf_graphs( idx = idx.drop("single", level="individuals") individuals = idx.get_level_values("individuals").unique() n_individuals = len(individuals) - map_ = dict(zip(individuals, range(n_individuals))) + map_ = dict(zip(individuals, range(n_individuals), strict=False)) # Form ground truth beforehand ground_truth = [] @@ -340,13 +340,15 @@ def _get_n_best_paf_graphs( if not any(between_train.values()): # Only 1 animal, let us return the full graph indices only - return ([existing_edges], dict(zip(existing_edges, [0] * len(existing_edges)))) + return ([existing_edges], dict(zip(existing_edges, [0] * len(existing_edges), strict=False))) - scores, _ = zip(*[_calc_separability(between_train[n], within_train[n], metric=metric) for n in existing_edges]) + scores, _ = zip( + *[_calc_separability(between_train[n], within_train[n], metric=metric) for n in existing_edges], strict=False + ) # Find minimal skeleton G = nx.Graph() - for edge, score in zip(existing_edges, scores): + for edge, score in zip(existing_edges, scores, strict=False): if np.isfinite(score): G.add_edge(*full_graph[edge], weight=score) if which == "best": @@ -368,7 +370,7 @@ def _get_n_best_paf_graphs( paf_inds = [root] for length in lengths: paf_inds.append(root + list(order[:length])) - return paf_inds, dict(zip(existing_edges, scores)) + return paf_inds, dict(zip(existing_edges, scores, strict=False)) def cross_validate_paf_graphs( diff --git a/deeplabcut/core/inferenceutils.py b/deeplabcut/core/inferenceutils.py index ebb82047ca..88479c3298 100644 --- a/deeplabcut/core/inferenceutils.py +++ b/deeplabcut/core/inferenceutils.py @@ -154,7 +154,7 @@ def soft_identity(self): unq, idx, cnt = np.unique(data[:, 3], return_inverse=True, return_counts=True) avg = np.bincount(idx, weights=data[:, 2]) / cnt soft = softmax(avg) - return dict(zip(unq.astype(int), soft)) + return dict(zip(unq.astype(int), soft, strict=False)) @property def affinity(self): @@ -260,7 +260,7 @@ def __init__( self.max_overlap = max_overlap self._has_identity = "identity" in self[0] if identity_only and not self._has_identity: - warnings.warn("The network was not trained with identity; setting `identity_only` to False.") + warnings.warn("The network was not trained with identity; setting `identity_only` to False.", stacklevel=2) self.identity_only = identity_only & self._has_identity self.nan_policy = nan_policy self.force_fusion = force_fusion @@ -340,7 +340,7 @@ def calibrate(self, train_data_file): pass n_bpts = len(df.columns.get_level_values("bodyparts").unique()) if n_bpts == 1: - warnings.warn("There is only one keypoint; skipping calibration...") + warnings.warn("There is only one keypoint; skipping calibration...", stacklevel=2) return xy = df.to_numpy().reshape((-1, n_bpts, 2)) @@ -348,7 +348,7 @@ def calibrate(self, train_data_file): # Only keeps skeletons that are more than 90% complete xy = xy[frac_valid >= 0.9] if not xy.size: - warnings.warn("No complete poses were found. Skipping calibration...") + warnings.warn("No complete poses were found. Skipping calibration...", stacklevel=2) return # TODO Normalize dists by longest length? @@ -364,7 +364,7 @@ def calibrate(self, train_data_file): self.safe_edge = True except np.linalg.LinAlgError: # Covariance matrix estimation fails due to numerical singularities - warnings.warn("The assembler could not be robustly calibrated. Continuing without it...") + warnings.warn("The assembler could not be robustly calibrated. Continuing without it...", stacklevel=2) def calc_assembly_mahalanobis_dist(self, assembly, return_proba=False, nan_policy="little"): if self._kde is None: @@ -420,10 +420,10 @@ def _flatten_detections(data_dict): ids = [np.ones(len(arr), dtype=int) * -1 for arr in confidence] else: ids = [arr.argmax(axis=1) for arr in ids] - for i, (coords, conf, id_) in enumerate(zip(coordinates, confidence, ids)): + for i, (coords, conf, id_) in enumerate(zip(coordinates, confidence, ids, strict=False)): if not np.any(coords): continue - for xy, p, g in zip(coords, conf, id_): + for xy, p, g in zip(coords, conf, id_, strict=False): joint = Joint(tuple(xy), p.item(), i, ind, g) ind += 1 yield joint @@ -457,13 +457,13 @@ def extract_best_links(self, joints_dict, costs, trees=None): conf = np.asarray([[det_s.confidence * det_t.confidence for det_t in dets_t] for det_s in dets_s]) rows, cols = np.where((conf >= self.pcutoff * self.pcutoff) & (aff >= self.min_affinity)) candidates = sorted( - zip(rows, cols, aff[rows, cols], lengths[rows, cols]), + zip(rows, cols, aff[rows, cols], lengths[rows, cols], strict=False), key=lambda x: x[2], reverse=True, ) i_seen = set() j_seen = set() - for i, j, w, l in candidates: + for i, j, w, _l in candidates: if i not in i_seen and j not in j_seen: i_seen.add(i) j_seen.add(j) @@ -481,7 +481,7 @@ def extract_best_links(self, joints_dict, costs, trees=None): keep_t = [ind for ind in inds_t if dets_t[ind].confidence >= self.pcutoff] aff = aff[np.ix_(keep_s, keep_t)] rows, cols = linear_sum_assignment(aff, maximize=True) - for row, col in zip(rows, cols): + for row, col in zip(rows, cols, strict=False): w = aff[row, col] if w >= self.min_affinity: links.append(Link(dets_s[keep_s[row]], dets_t[keep_t[col]], w)) @@ -754,7 +754,7 @@ def _assemble(self, data_dict, ind_frame): scores = [-self.calc_assembly_mahalanobis_dist(ass) for ass in assemblies] else: scores = [ass._affinity for ass in assemblies] - lst = list(zip(scores, assemblies)) + lst = list(zip(scores, assemblies, strict=False)) assemblies = [] while lst: temp = max(lst, key=lambda x: x[0]) @@ -882,7 +882,7 @@ def to_pickle(self, output_name): @dataclass class MatchedPrediction: - """A match between a prediction and a ground truth assembly + """A match between a prediction and a ground truth assembly. The ground truth assembly should be None f the prediction was not matched to any GT, and the OKS should be 0. @@ -956,7 +956,7 @@ def match_assemblies( greedy_matching: bool = False, greedy_oks_threshold: float = 0.0, ) -> tuple[int, list[MatchedPrediction]]: - """Matches assemblies to ground truth predictions + """Matches assemblies to ground truth predictions. Returns: int: the total number of valid ground truth assemblies @@ -1030,7 +1030,7 @@ def match_assemblies( if ~np.isnan(oks): mat[i, j] = oks rows, cols = linear_sum_assignment(mat, maximize=True) - for row, col in zip(rows, cols): + for row, col in zip(rows, cols, strict=False): matched[row].ground_truth = ground_truth[col] matched[row].oks = mat[row, col] _ = inds_true.remove(col) @@ -1081,7 +1081,7 @@ def find_outlier_assemblies(dict_of_assemblies, criterion="area", qs=(5, 95)): for frame_ind, assemblies in dict_of_assemblies.items(): for assembly in assemblies: tuples.append((frame_ind, getattr(assembly, criterion))) - frame_inds, vals = zip(*tuples) + frame_inds, vals = zip(*tuples, strict=False) vals = np.asarray(vals) lo, up = np.percentile(vals, qs, interpolation="nearest") inds = np.flatnonzero((vals < lo) | (vals > up)).tolist() @@ -1094,7 +1094,7 @@ def _compute_precision_and_recall( oks_threshold: float, recall_thresholds: np.ndarray, ) -> tuple[np.ndarray, np.ndarray]: - """Computes the precision and recall scores at a given OKS threshold + """Computes the precision and recall scores at a given OKS threshold. Args: num_gt_assemblies: the number of ground truth assemblies (used to compute false @@ -1134,7 +1134,7 @@ def evaluate_assembly_greedy( margin: int | float = 0, symmetric_kpts: list[tuple[int, int]] | None = None, ) -> dict: - """Runs greedy mAP evaluation, as done by pycocotools + """Runs greedy mAP evaluation, as done by pycocotools. Args: assemblies_gt: A dictionary mapping image ID (e.g. filepath) to ground truth diff --git a/deeplabcut/core/metrics/bbox.py b/deeplabcut/core/metrics/bbox.py index 428ea0fc72..5cc7a85e76 100644 --- a/deeplabcut/core/metrics/bbox.py +++ b/deeplabcut/core/metrics/bbox.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Bounding box metrics +"""Bounding box metrics. Metrics are currently computed using pycocotools, which can be installed with `pypi` (see https://github.com/ppwwyyxx/cocoapi/tree/master). @@ -97,7 +97,7 @@ def compute_bbox_metrics( } ) - for bbox, score in zip(detections[img]["bboxes"], detections[img]["scores"]): + for bbox, score in zip(detections[img]["bboxes"], detections[img]["scores"], strict=False): predictions.append(np.array([img_id, *bbox, score, 1])) if len(predictions) == 0: diff --git a/deeplabcut/core/metrics/matching.py b/deeplabcut/core/metrics/matching.py index 296affb2e5..791bcae97f 100644 --- a/deeplabcut/core/metrics/matching.py +++ b/deeplabcut/core/metrics/matching.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Algorithms to match predictions to ground truth labels""" +"""Algorithms to match predictions to ground truth labels.""" from __future__ import annotations @@ -54,7 +54,7 @@ def pixel_errors(self) -> np.ndarray: return np.linalg.norm(self.pose[:, :2] - self.gt[:, :2], axis=1) def match(self, gt: np.ndarray, oks: float) -> None: - """Adds a ground truth match to this PotentialMatch + """Adds a ground truth match to this PotentialMatch. Args: gt: The ground truth to which the prediction is matched. The ground truth @@ -81,7 +81,7 @@ def match_greedy_oks( oks_matrix: np.ndarray, oks_threshold: float = 0.0, ) -> list[PotentialMatch]: - """Greedy matching of ground truth individuals to predicted individuals using OKS + """Greedy matching of ground truth individuals to predicted individuals using OKS. This is done in the same way as done in pycocotools. The predictions must be sorted by score before being passed to this function. @@ -99,7 +99,7 @@ def match_greedy_oks( """ matches = [PotentialMatch.from_pose(pose=pred) for pred in predictions] matched_gt_indices = set() - for idx, pred in enumerate(predictions): + for idx, _pred in enumerate(predictions): oks = oks_matrix[idx] if np.all(np.isnan(oks)): continue @@ -125,7 +125,7 @@ def match_greedy_rmse( predictions: np.ndarray, keep_assemblies: bool = True, ) -> list[PotentialMatch]: - """Greedy matching of ground truth individuals to predicted individuals using RMSE + """Greedy matching of ground truth individuals to predicted individuals using RMSE. The predictions must be sorted by score before being passed to this function. diff --git a/deeplabcut/core/visualization.py b/deeplabcut/core/visualization.py index 4a01080c96..d9796f1c14 100644 --- a/deeplabcut/core/visualization.py +++ b/deeplabcut/core/visualization.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Visualization methods for""" +"""Visualization methods for.""" from __future__ import annotations @@ -19,7 +19,7 @@ def form_figure(nx, ny) -> tuple[plt.Figure, plt.Axes]: - """Forms a figure on which to plot images""" + """Forms a figure on which to plot images.""" fig, ax = plt.subplots(frameon=False) ax.set_xlim(0, nx) ax.set_ylim(0, ny) diff --git a/deeplabcut/core/weight_init.py b/deeplabcut/core/weight_init.py index 758da538b8..ae755223cf 100644 --- a/deeplabcut/core/weight_init.py +++ b/deeplabcut/core/weight_init.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Classes to configure how to initialize model weights""" +"""Classes to configure how to initialize model weights.""" from __future__ import annotations @@ -21,7 +21,7 @@ @dataclass class WeightInitialization: - """Configures weights initialization when transfer learning or fine-tuning models + """Configures weights initialization when transfer learning or fine-tuning models. Args: snapshot_path: The path to the snapshot used to initialize pose model weights @@ -124,7 +124,7 @@ def from_dict(data: dict) -> WeightInitialization: @staticmethod def from_dict_legacy(data: dict) -> WeightInitialization: - """Deals with weight initialization that were created before 3.0.0rc5""" + """Deals with weight initialization that were created before 3.0.0rc5.""" import deeplabcut.pose_estimation_pytorch.modelzoo.utils as utils conversion_array = data.get("conversion_array") @@ -157,7 +157,7 @@ def build( customized_pose_checkpoint: str | None = None, customized_detector_checkpoint: str | None = None, ) -> WeightInitialization: - """Builds a WeightInitialization for a project + """Builds a WeightInitialization for a project. `WeightInitialization.build` is deprecated and will be removed in a future version of DeepLabCut. Please use `build_weight_init` from `deeplabcut.modelzoo` @@ -193,7 +193,7 @@ def build( "future version of DeepLabCut. Please use `build_weight_init` from " "`deeplabcut.modelzoo` instead." ) - warnings.warn(deprecation_warning, DeprecationWarning) + warnings.warn(deprecation_warning, DeprecationWarning, stacklevel=2) return build_weight_init( cfg, diff --git a/deeplabcut/create_project/add.py b/deeplabcut/create_project/add.py index 165ab4e522..5f76792ed7 100644 --- a/deeplabcut/create_project/add.py +++ b/deeplabcut/create_project/add.py @@ -11,8 +11,7 @@ def add_new_videos(config, videos, copy_videos=False, coords=None, extract_frames=False): - """ - Add new videos to the config file at any stage of the project. + """Add new videos to the config file at any stage of the project. Parameters ---------- @@ -43,7 +42,6 @@ def add_new_videos(config, videos, copy_videos=False, coords=None, extract_frame Two videos will be added, with cropping dimensions [0,100,0,200] and [0,100,0,250], respectively. >>> deeplabcut.add_new_videos('/home/project/reaching-task-Tanmay-2018-08-23/config.yaml',['/data/videos/mouse5.avi','/data/videos/mouse6.avi'],copy_videos=False,coords=[[0,100,0,200],[0,100,0,250]]) - """ import os import shutil @@ -67,9 +65,7 @@ def add_new_videos(config, videos, copy_videos=False, coords=None, extract_frame dirs = [data_path / Path(i.stem) for i in videos] for p in dirs: - """ - Creates directory under data & perhaps copies videos (to /video) - """ + """Creates directory under data & perhaps copies videos (to /video)""" p.mkdir(parents=True, exist_ok=True) destinations = [video_path.joinpath(vp.name) for vp in videos] diff --git a/deeplabcut/create_project/modelzoo.py b/deeplabcut/create_project/modelzoo.py index b3aeefeab4..c3a5360e96 100644 --- a/deeplabcut/create_project/modelzoo.py +++ b/deeplabcut/create_project/modelzoo.py @@ -87,8 +87,7 @@ def create_pretrained_human_project( createlabeledvideo=True, analyzevideo=True, ): - """ - LEGACY FUNCTION will be deprecated. + """LEGACY FUNCTION will be deprecated. Use deeplabcut.create_pretrained_project(project, experimenter, videos, model='full_human', ..) @@ -133,8 +132,7 @@ def create_pretrained_project( net_name: str | None = None, detector_name: str | None = None, ): - r""" - Creates a new project directory, sub-directories and a basic configuration file. + r"""Creates a new project directory, sub-directories and a basic configuration file. Change its parameters to your projects need. The project will also be initialized with a pre-trained model from the DeepLabCut model zoo! @@ -265,8 +263,7 @@ def create_pretrained_project_pytorch( net_name: str | None = None, detector_name: str | None = None, ): - r""" - Method used specifically for Pytorch-based ModelZoo models. + r"""Method used specifically for Pytorch-based ModelZoo models. Creates a new project directory, sub-directories and a basic configuration file. Change its parameters to your projects need. @@ -484,8 +481,7 @@ def create_pretrained_project_tensorflow( createlabeledvideo: bool = True, trainFraction: float | None = None, ): - r""" - Method used specifically for Tensorflow-based ModelZoo models. + r"""Method used specifically for Tensorflow-based ModelZoo models. Creates a new project directory, sub-directories and a basic configuration file. Change its parameters to your projects need. diff --git a/deeplabcut/create_project/new.py b/deeplabcut/create_project/new.py index 2b03e8a48d..1c1029e089 100644 --- a/deeplabcut/create_project/new.py +++ b/deeplabcut/create_project/new.py @@ -175,9 +175,7 @@ def create_new_project( videos = collected_videos dirs = [data_path / i.stem for i in videos] for p in dirs: - """ - Creates directory under data - """ + """Creates directory under data.""" p.mkdir(parents=True, exist_ok=True) destinations = [video_path.joinpath(vp.name) for vp in videos] diff --git a/deeplabcut/create_project/new_3d.py b/deeplabcut/create_project/new_3d.py index 474c968df9..c526ce6276 100644 --- a/deeplabcut/create_project/new_3d.py +++ b/deeplabcut/create_project/new_3d.py @@ -17,8 +17,9 @@ def create_new_project_3d(project, experimenter, num_cameras=2, working_directory=None): - r"""Creates a new project directory, sub-directories and a basic configuration file for 3d project. - The configuration file is loaded with the default values. Adjust the parameters to your project's needs. + r"""Creates a new project directory, sub-directories and a basic configuration file + for 3d project. The configuration file is loaded with the default values. Adjust the + parameters to your project's needs. Parameters ---------- @@ -43,7 +44,6 @@ def create_new_project_3d(project, experimenter, num_cameras=2, working_director Windows: >>> deeplabcut.create_new_project('reaching-task','Bill',2) Users must format paths with either: r'C:\ OR 'C:\\ <- i.e. a double backslash \\ ) - """ from datetime import datetime as dt diff --git a/deeplabcut/generate_training_dataset/frame_extraction.py b/deeplabcut/generate_training_dataset/frame_extraction.py index 33412bfd19..f0a1f36953 100755 --- a/deeplabcut/generate_training_dataset/frame_extraction.py +++ b/deeplabcut/generate_training_dataset/frame_extraction.py @@ -11,11 +11,10 @@ def select_cropping_area(config, videos=None): - """ - Interactively select the cropping area of all videos in the config. - A user interface pops up with a frame to select the cropping parameters. - Use the left click to draw a box and hit the button 'set cropping parameters' - to store the cropping parameters for a video in the config.yaml file. + """Interactively select the cropping area of all videos in the config. A user + interface pops up with a frame to select the cropping parameters. Use the left click + to draw a box and hit the button 'set cropping parameters' to store the cropping + parameters for a video in the config.yaml file. Parameters ---------- diff --git a/deeplabcut/generate_training_dataset/metadata.py b/deeplabcut/generate_training_dataset/metadata.py index 3354b60eb9..1baa1cb430 100644 --- a/deeplabcut/generate_training_dataset/metadata.py +++ b/deeplabcut/generate_training_dataset/metadata.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""File containing methods to load and parse shuffle metadata""" +"""File containing methods to load and parse shuffle metadata.""" from __future__ import annotations @@ -27,7 +27,7 @@ @dataclass(frozen=True) class DataSplit: - """Class representing the metadata for a shuffle""" + """Class representing the metadata for a shuffle.""" train_indices: tuple[int, ...] test_indices: tuple[int, ...] @@ -47,7 +47,7 @@ def __post_init__(self) -> None: @dataclass(frozen=True) class ShuffleMetadata: - """Class representing the metadata for a shuffle""" + """Class representing the metadata for a shuffle.""" name: str train_fraction: float @@ -56,7 +56,7 @@ class ShuffleMetadata: split: DataSplit | None def load_split(self, cfg: dict, trainset_path: Path) -> ShuffleMetadata: - """Loads the data split for this shuffle + """Loads the data split for this shuffle. Args: cfg: the config for the DeepLabCut project @@ -91,7 +91,7 @@ def load_split(self, cfg: dict, trainset_path: Path) -> ShuffleMetadata: @dataclass(frozen=True) class TrainingDatasetMetadata: - """An immutable class containing the metadata for a dataset + """An immutable class containing the metadata for a dataset. When creating a new "training-datasets" folder (e.g., when creating the first training set for a project, or when creating the first training for a given @@ -137,7 +137,7 @@ def __post_init__(self) -> None: ValueError if the indices are not sorted in increasing order """ indices = [[s.train_fraction, s.index] for s in self.shuffles] - for (frac1, idx1), (frac2, idx2) in zip(indices[:-1], indices[1:]): + for (frac1, idx1), (frac2, idx2) in zip(indices[:-1], indices[1:], strict=False): if not (frac1 < frac2 or (frac1 == frac2 and idx1 < idx2)): raise RuntimeError( "The shuffles given must be sorted in order of ascending training " @@ -149,8 +149,7 @@ def add( shuffle: ShuffleMetadata, overwrite: bool = False, ) -> TrainingDatasetMetadata: - """ - Adds a new shuffle to the metadata file + """Adds a new shuffle to the metadata file. Args: shuffle: the shuffle to add @@ -202,7 +201,7 @@ def get(self, trainset_index: int = 0, index: int = 0) -> ShuffleMetadata: raise ValueError(f"Could not find a shuffle with trainingset fraction {train_fraction} and index {index}") def save(self) -> None: - """Saves the training dataset metadata to disk""" + """Saves the training dataset metadata to disk.""" metadata = {"shuffles": {}} data_splits: dict[DataSplit, int] = {} trainset_path = self.path(self.project_config).parent @@ -231,7 +230,7 @@ def load( config: str | Path | dict, load_splits: bool = False, ) -> TrainingDatasetMetadata: - """Loads the metadata from disk + """Loads the metadata from disk. Args: config: the config for the DeepLabCut project (or its path) @@ -265,7 +264,7 @@ def load( @staticmethod def create(config: str | Path | dict) -> TrainingDatasetMetadata: - """Function to create the metadata file + """Function to create the metadata file. Assumes that all existing shuffles use the TensorFlow engine, as this file should have already been created for PyTorch shuffles. @@ -345,7 +344,7 @@ def update_metadata( test_indices: list[int], overwrite: bool = False, ) -> None: - """Updates the metadata for a training-dataset + """Updates the metadata for a training-dataset. Args: cfg: the config for the DeepLabCut project diff --git a/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py b/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py index 25c4cbcbd9..bfd348737d 100755 --- a/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py +++ b/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py @@ -119,12 +119,10 @@ def create_multianimaltraining_dataset( engine: Engine | None = None, ctd_conditions: int | str | Path | tuple[int, str] | tuple[int, int] | None = None, ): - """ - Creates a training dataset for multi-animal datasets. Labels from all the extracted - frames are merged into a single .h5 file.\n - Only the videos included in the config file are used to create this dataset.\n - [OPTIONAL] Use the function 'add_new_videos' at any stage of the project to add more - videos to the project. + """Creates a training dataset for multi-animal datasets. Labels from all the + extracted frames are merged into a single .h5 file.\n Only the videos included in + the config file are used to create this dataset.\n [OPTIONAL] Use the function + 'add_new_videos' at any stage of the project to add more videos to the project. Important differences to standard: - stores coordinates with numdigits as many digits diff --git a/deeplabcut/generate_training_dataset/trainingsetmanipulation.py b/deeplabcut/generate_training_dataset/trainingsetmanipulation.py index 0e16f049b8..440fff8a1d 100755 --- a/deeplabcut/generate_training_dataset/trainingsetmanipulation.py +++ b/deeplabcut/generate_training_dataset/trainingsetmanipulation.py @@ -37,14 +37,13 @@ def comparevideolistsanddatafolders(config): - """ - Auxiliary function that compares the folders in labeled-data and the ones listed under video_sets (in the config file). + """Auxiliary function that compares the folders in labeled-data and the ones listed + under video_sets (in the config file). Parameter ---------- config : string String containing the full path of the config file in the project. - """ cfg = auxiliaryfunctions.read_config(config) videos = cfg["video_sets"].keys() @@ -64,9 +63,10 @@ def comparevideolistsanddatafolders(config): def adddatasetstovideolistandviceversa(config): - """ - First run comparevideolistsanddatafolders(config) to compare the folders in labeled-data and the ones listed under video_sets (in the config file). - If you detect differences this function can be used to maker sure each folder has a video entry & vice versa. + """First run comparevideolistsanddatafolders(config) to compare the folders in + labeled-data and the ones listed under video_sets (in the config file). If you + detect differences this function can be used to maker sure each folder has a video + entry & vice versa. It corrects this problem in the following way: @@ -123,15 +123,13 @@ def adddatasetstovideolistandviceversa(config): def dropduplicatesinannotatinfiles(config): - """ - - Drop duplicate entries (of images) in annotation files (this should no longer happen, but might be useful). + """Drop duplicate entries (of images) in annotation files (this should no longer + happen, but might be useful). Parameter ---------- config : string String containing the full path of the config file in the project. - """ cfg = auxiliaryfunctions.read_config(config) videos = cfg["video_sets"].keys() @@ -154,15 +152,14 @@ def dropduplicatesinannotatinfiles(config): def dropannotationfileentriesduetodeletedimages(config): - """ - Drop entries for all deleted images in annotation files, i.e. for folders of the type: /labeled-data/*folder*/CollectedData_*scorer*.h5 - Will be carried out iteratively for all *folders* in labeled-data. + """Drop entries for all deleted images in annotation files, i.e. for folders of the + type: /labeled-data/*folder*/CollectedData_*scorer*.h5 Will be carried out + iteratively for all *folders* in labeled-data. Parameter ---------- config : string String containing the full path of the config file in the project. - """ cfg = auxiliaryfunctions.read_config(config) videos = cfg["video_sets"].keys() @@ -237,15 +234,14 @@ def dropimagesduetolackofannotation(config): def dropunlabeledframes(config): - """ - Drop entries such that all the bodyparts are not labeled from the annotation files, i.e. h5 and csv files - Will be carried out iteratively for all *folders* in labeled-data. + """Drop entries such that all the bodyparts are not labeled from the annotation + files, i.e. h5 and csv files Will be carried out iteratively for all *folders* in + labeled-data. Parameter ---------- config : string String containing the full path of the config file in the project. - """ cfg = auxiliaryfunctions.read_config(config) videos = cfg["video_sets"].keys() @@ -453,7 +449,7 @@ def _robust_path_split(path): def parse_video_filenames(videos: list[str]) -> list[str]: - """Parses the names of all videos listed in a project's ``config.yaml`` file + """Parses the names of all videos listed in a project's ``config.yaml`` file. Goes through the paths all videos listed for a project, and removes entries with a duplicate video name (e.g. if a video is listed twice, once with the path @@ -497,8 +493,7 @@ def parse_video_filenames(videos: list[str]) -> list[str]: def merge_annotateddatasets(cfg, trainingsetfolder_full): - """ - Merges all the h5 files for all labeled-datasets (from individual videos). + """Merges all the h5 files for all labeled-datasets (from individual videos). This is a bit of a mess because of cross platform compatibility. @@ -561,10 +556,12 @@ def SplitTrials( trainFraction=0.8, enforce_train_fraction=False, ): - """Split a trial index into train and test sets. Also checks that the trainFraction is a two digit number between 0 an 1. The reason - is that the folders contain the trainfraction as int(100*trainFraction). - If enforce_train_fraction is True, train and test indices are padded with -1 - such that the ratio of their lengths is exactly the desired train fraction. + """Split a trial index into train and test sets. + + Also checks that the trainFraction is a two digit number between 0 an 1. The reason + is that the folders contain the trainfraction as int(100*trainFraction). If + enforce_train_fraction is True, train and test indices are padded with -1 such that + the ratio of their lengths is exactly the desired train fraction. """ if trainFraction > 1 or trainFraction < 0: print( @@ -619,8 +616,7 @@ def pad_train_test_indices(train_inds, test_inds, train_fraction): def mergeandsplit(config, trainindex=0, uniform=True): - """ - This function allows additional control over "create_training_dataset". + """This function allows additional control over "create_training_dataset". Merge annotated data sets (from different folders) and split data in a specific way, returns the split variables (train/test indices). Importantly, this allows one to freeze a split. @@ -654,7 +650,6 @@ def mergeandsplit(config, trainindex=0, uniform=True): You can then create two model instances that have the identical trainingset. Thereby you can assess the role of various parameters on the performance of DLC. >>> deeplabcut.create_training_dataset(config,Shuffles=[0,1],trainIndices=[trainIndices, trainIndices],testIndices=[testIndices, testIndices]) -------- - """ # Loading metadata from config file: cfg = auxiliaryfunctions.read_config(config) @@ -1575,9 +1570,8 @@ def create_training_dataset_from_existing_split( weight_init: WeightInitialization | None = None, engine: Engine | None = None, ) -> None | list[int]: - """ - Labels from all the extracted frames are merged into a single .h5 file. - Only the videos included in the config file are used to create this dataset. + """Labels from all the extracted frames are merged into a single .h5 file. Only the + videos included in the config file are used to create this dataset. Args: config: Full path of the ``config.yaml`` file as a string. @@ -1727,8 +1721,7 @@ def _compute_padding( num_train: int, num_test: int, ) -> tuple[int, int]: - """ - Computes the amount of padding to add to train/test indices such that + """Computes the amount of padding to add to train/test indices such that train_fraction = num_train / (num_train + num_test). Returns: diff --git a/deeplabcut/gui/components.py b/deeplabcut/gui/components.py index 6f93bf88f5..a47d57010c 100644 --- a/deeplabcut/gui/components.py +++ b/deeplabcut/gui/components.py @@ -71,10 +71,9 @@ def _create_grid_layout( def set_combo_items(combo_box: QtWidgets.QComboBox, items: list[str], index: int = 0): - """ - Safely replaces all items in a QComboBox and sets the current index, - ensuring that the `currentTextChanged` signal is emitted exactly once - (and only if items are present). + """Safely replaces all items in a QComboBox and sets the current index, ensuring + that the `currentTextChanged` signal is emitted exactly once (and only if items are + present). This method suppresses intermediate signal emissions that can be triggered by `clear()` and `addItems()` — both of which may emit multiple signals diff --git a/deeplabcut/gui/displays/selected_shuffle_display.py b/deeplabcut/gui/displays/selected_shuffle_display.py index 3ce7bf1d27..d1ae3732b1 100644 --- a/deeplabcut/gui/displays/selected_shuffle_display.py +++ b/deeplabcut/gui/displays/selected_shuffle_display.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Module to display information about the selected shuffle in the GUI""" +"""Module to display information about the selected shuffle in the GUI.""" from __future__ import annotations @@ -22,7 +22,7 @@ class SelectedShuffleDisplay(QtWidgets.QWidget): - """A widget displaying information about the selected shuffle""" + """A widget displaying information about the selected shuffle.""" pose_cfg_signal = QtCore.Signal(dict) diff --git a/deeplabcut/gui/displays/shuffle_metadata_viewer.py b/deeplabcut/gui/displays/shuffle_metadata_viewer.py index 911e91a2c2..ec44d5a725 100644 --- a/deeplabcut/gui/displays/shuffle_metadata_viewer.py +++ b/deeplabcut/gui/displays/shuffle_metadata_viewer.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Widget to display existing shuffles""" +"""Widget to display existing shuffles.""" from __future__ import annotations @@ -19,7 +19,7 @@ class ShuffleMetadataViewer(QtWidgets.QDialog): - """Viewer for shuffle metadata""" + """Viewer for shuffle metadata.""" def __init__(self, root: QtWidgets.QMainWindow, parent: QtWidgets.QWidget): super().__init__(parent) diff --git a/deeplabcut/gui/tabs/create_project.py b/deeplabcut/gui/tabs/create_project.py index 9c2fe96c40..615806df27 100644 --- a/deeplabcut/gui/tabs/create_project.py +++ b/deeplabcut/gui/tabs/create_project.py @@ -27,7 +27,7 @@ class DynamicTextList(QtWidgets.QWidget): - """Dynamically add text entries""" + """Dynamically add text entries.""" def __init__(self, label_text="bodyparts", parent=None): super().__init__(parent) @@ -131,7 +131,7 @@ def get_entries(self): return [entry[0].text() for entry in self.entries if entry[0].text()] def _update_indices(self): - for i, (entry, index_label) in enumerate(self.entries): + for i, (_entry, index_label) in enumerate(self.entries): index_label.setText(str(i + 1) + ".") @@ -177,7 +177,7 @@ def paintEvent(self, event): class ProjectCreator(QtWidgets.QDialog): - """Project creation dialog""" + """Project creation dialog.""" def __init__(self, parent): super().__init__(parent) diff --git a/deeplabcut/gui/tabs/create_training_dataset.py b/deeplabcut/gui/tabs/create_training_dataset.py index 2d75960588..af78cef535 100644 --- a/deeplabcut/gui/tabs/create_training_dataset.py +++ b/deeplabcut/gui/tabs/create_training_dataset.py @@ -356,8 +356,7 @@ def create_training_dataset(self): self.root.writer.write("Training dataset creation failed.") def _confirm_overwrite(self, shuffle: int, existing_indices: list[int]) -> bool: - """ - Asks the user to confirm that they want to overwrite a shuffle. + """Asks the user to confirm that they want to overwrite a shuffle. Args: shuffle: the shuffle the user wants to overwrite @@ -598,7 +597,7 @@ def view_shuffles(self) -> None: class WeightInitializationSelector(QtWidgets.QWidget): - """Widget to select weight initialization""" + """Widget to select weight initialization.""" def __init__(self, root): super().__init__() @@ -636,7 +635,7 @@ def memory_replay(self) -> bool: return self.memory_replay_box.isChecked() def update_choices(self, choices: list[str]) -> None: - """Updates the WeightInitialization methods that can be selected""" + """Updates the WeightInitialization methods that can be selected.""" set_combo_items( combo_box=self.weight_init_choice, items=choices, @@ -703,7 +702,8 @@ def _choice_changed(self, state: str) -> None: class DataSplitSelector(QtWidgets.QWidget): - """Allows users to create training sets with the same train/test split as another""" + """Allows users to create training sets with the same train/test split as + another.""" def __init__(self, root: QtWidgets.QMainWindow, parent: QtWidgets.QWidget): super().__init__() @@ -753,7 +753,7 @@ def selected(self) -> bool: @property def from_shuffle(self) -> int: - """The shuffle from which to copy the data split""" + """The shuffle from which to copy the data split.""" return self.shuffle_selector.value() def _checkbox_status_changed(self, state: int) -> None: diff --git a/deeplabcut/gui/tabs/create_videos.py b/deeplabcut/gui/tabs/create_videos.py index 2bd846f9a5..dcbec6a232 100644 --- a/deeplabcut/gui/tabs/create_videos.py +++ b/deeplabcut/gui/tabs/create_videos.py @@ -270,7 +270,7 @@ def create_videos(self): if all(videos_created): self.root.writer.write("Labeled videos created.") else: - failed_videos = [video for success, video in zip(videos_created, videos) if not success] + failed_videos = [video for success, video in zip(videos_created, videos, strict=False) if not success] failed_videos_str = ", ".join(failed_videos) self.root.writer.write(f"Failed to create videos from {failed_videos_str}.") diff --git a/deeplabcut/gui/tabs/evaluate_network.py b/deeplabcut/gui/tabs/evaluate_network.py index 1c6ff500db..7546187fe5 100644 --- a/deeplabcut/gui/tabs/evaluate_network.py +++ b/deeplabcut/gui/tabs/evaluate_network.py @@ -47,7 +47,7 @@ def __init__(self, image_paths, parent=None): self.canvas = FigureCanvas(self.figure) layout.addWidget(self.canvas) - for image_path, gridspec in zip(image_paths[:9], self.grid): + for image_path, gridspec in zip(image_paths[:9], self.grid, strict=False): ax = self.figure.add_subplot(gridspec) ax.set_axis_off() img = mpimg.imread(image_path) diff --git a/deeplabcut/gui/tabs/extract_frames.py b/deeplabcut/gui/tabs/extract_frames.py index 40beaf5366..9e8b92ba8f 100644 --- a/deeplabcut/gui/tabs/extract_frames.py +++ b/deeplabcut/gui/tabs/extract_frames.py @@ -27,11 +27,10 @@ def select_cropping_area(config, videos=None): - """ - Interactively select the cropping area of all videos in the config. - A user interface pops up with a frame to select the cropping parameters. - Use the left click to draw a box and hit the button 'set cropping parameters' - to store the cropping parameters for a video in the config.yaml file. + """Interactively select the cropping area of all videos in the config. A user + interface pops up with a frame to select the cropping parameters. Use the left click + to draw a box and hit the button 'set cropping parameters' to store the cropping + parameters for a video in the config.yaml file. Parameters ---------- @@ -268,7 +267,7 @@ def _show_success_message(self): self.root.writer.write(root_message) def _check_symlink(self, video_path: str | Path) -> Path: - """Checks that a video is in the DeepLabCut 'videos' folder + """Checks that a video is in the DeepLabCut 'videos' folder. This is required before launching manual frame extraction. When users select a symlink of a video using the VideoSelectionWidget, the path is resolved to the diff --git a/deeplabcut/gui/tabs/modelzoo.py b/deeplabcut/gui/tabs/modelzoo.py index cbff89e2e0..b40252354d 100644 --- a/deeplabcut/gui/tabs/modelzoo.py +++ b/deeplabcut/gui/tabs/modelzoo.py @@ -506,7 +506,7 @@ def signal_analysis_complete(self): msg.exec_() def stop_processes(self): - """Stop any running processes""" + """Stop any running processes.""" if self.thread and self.thread.isRunning(): print("Stopping running processes...") self.thread.quit() @@ -520,7 +520,7 @@ def stop_processes(self): self.root._progress_bar.hide() def closeEvent(self, event): - """Override closeEvent to stop processes when tab is closed""" + """Override closeEvent to stop processes when tab is closed.""" self.stop_processes() super().closeEvent(event) diff --git a/deeplabcut/gui/tracklet_toolbox.py b/deeplabcut/gui/tracklet_toolbox.py index ec76577f22..483a552d16 100644 --- a/deeplabcut/gui/tracklet_toolbox.py +++ b/deeplabcut/gui/tracklet_toolbox.py @@ -57,9 +57,7 @@ def connect(self): self.cidhover = self.point.figure.canvas.mpl_connect("motion_notify_event", self.on_hover) def on_press(self, event): - """ - Define the event for the button press! - """ + """Define the event for the button press!""" if event.inaxes != self.point.axes: return if DraggablePoint.lock is not None: @@ -68,9 +66,7 @@ def on_press(self, event): if not contains: return if event.button == 1: - """ - This button press corresponds to the left click - """ + """This button press corresponds to the left click.""" self.press = (self.point.center), event.xdata, event.ydata DraggablePoint.lock = self canvas = self.point.figure.canvas @@ -81,9 +77,11 @@ def on_press(self, event): axes.draw_artist(self.point) canvas.blit(axes.bbox) elif event.button == 2: - """ - To remove a predicted label. Internally, the coordinates of the selected predicted label is replaced with nan. The user needs to middle click for the event. After right - click the data point is removed from the plot. + """To remove a predicted label. + + Internally, the coordinates of the selected predicted label is replaced with + nan. The user needs to middle click for the event. After right click the + data point is removed from the plot. """ message = f"Do you want to remove the label {self.bodyParts}?" if self.likelihood is not None: @@ -106,9 +104,7 @@ def delete_data(self): self.point.figure.canvas.draw() def on_motion(self, event): - """ - During the drag! - """ + """During the drag!""" if DraggablePoint.lock is not self: return if event.inaxes != self.point.axes: @@ -145,9 +141,8 @@ def on_release(self, event): self.coords.append(self.final_point) def on_hover(self, event): - """ - Annotate the labels and likelihood when the user hovers over the data points. - """ + """Annotate the labels and likelihood when the user hovers over the data + points.""" vis = self.annot.get_visible() if event.inaxes == self.point.axes: diff --git a/deeplabcut/gui/utils.py b/deeplabcut/gui/utils.py index 852175c7fa..6d862c7670 100644 --- a/deeplabcut/gui/utils.py +++ b/deeplabcut/gui/utils.py @@ -58,9 +58,7 @@ def stop_thread(): def parse_version(version: str) -> tuple[int, int, int]: - """ - Parses a version string into a tuple of (major, minor, patch). - """ + """Parses a version string into a tuple of (major, minor, patch).""" match = re.search(r"(\d+)\.(\d+)\.(\d+)", version) if match: return tuple(int(part) for part in match.groups()) diff --git a/deeplabcut/gui/widgets.py b/deeplabcut/gui/widgets.py index 69cbc0b0da..33ece7c6cb 100644 --- a/deeplabcut/gui/widgets.py +++ b/deeplabcut/gui/widgets.py @@ -63,9 +63,7 @@ def __init__(self, parent, **kwargs): layout.addWidget(self.canvas) def getfigure(self): - """ - Returns the figure, axes and canvas - """ + """Returns the figure, axes and canvas.""" return self.figure, self.axes, self.canvas def resetView(self): diff --git a/deeplabcut/gui/window.py b/deeplabcut/gui/window.py index 9ab2ff52e6..880a21b32f 100644 --- a/deeplabcut/gui/window.py +++ b/deeplabcut/gui/window.py @@ -306,9 +306,10 @@ def video_files(self): return self.files def add_video_files(self, new_video_files): - """ - Add new video files to the existing set of files. This method ensures no duplicates are added. - Emits a signal to notify about the updated set of files. + """Add new video files to the existing set of files. + + This method ensures no duplicates are added. Emits a signal to notify about the + updated set of files. """ new_video_files = set(new_video_files) self.files.update(new_video_files) # Add new items to the existing set @@ -316,8 +317,9 @@ def add_video_files(self, new_video_files): self.logger.info(f"Videos added to analyze:\n{new_video_files}\nCurrent video files:\n{self.files}") def clear_video_files(self): - """ - Clear all video files from the existing set. Emits a signal to notify the change. + """Clear all video files from the existing set. + + Emits a signal to notify the change. """ self.files.clear() # Reset the set to be empty self.video_files_.emit(self.files) # Emit the empty set diff --git a/deeplabcut/modelzoo/fmpose_3d/fmpose3d.py b/deeplabcut/modelzoo/fmpose_3d/fmpose3d.py index 1bbcac3531..c5a1289d0a 100644 --- a/deeplabcut/modelzoo/fmpose_3d/fmpose3d.py +++ b/deeplabcut/modelzoo/fmpose_3d/fmpose3d.py @@ -1,8 +1,7 @@ -""" -DeepLabCut2.0-3.0 Toolbox (deeplabcut.org) -© A. & M. Mathis Labs -https://github.com/DeepLabCut/DeepLabCut -Please see AUTHORS for contributors. +"""DeepLabCut2.0-3.0 Toolbox (deeplabcut.org) © A. + +& M. Mathis Labs https://github.com/DeepLabCut/DeepLabCut Please see AUTHORS for +contributors. https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS Licensed under GNU Lesser General Public License v3.0 """ @@ -18,10 +17,9 @@ def get_fmpose3d_inference_api( model_type: SupportedModel = "fmpose3d_humans", snapshot_path: str | None = None, device: str | None = None, - config_kwargs: dict = {}, + config_kwargs: dict = None, ) -> FMPose3DInference: - """ - Get a FMPose3DInference API for a given model type and snapshot path. + """Get a FMPose3DInference API for a given model type and snapshot path. Args: model_type: one of the supported model types: "fmpose3d_humans", "fmpose3d_animals", @@ -47,6 +45,8 @@ def get_fmpose3d_inference_api( predictions_3d = fmpose.pose_3d(keypoints_2d=keypoints_2d) ``` """ + if config_kwargs is None: + config_kwargs = {} model_config = FMPose3DConfig(model_type=model_type, **config_kwargs) fmpose3d_api = FMPose3DInference(model_config, model_weights_path=snapshot_path, device=device) return fmpose3d_api diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/base_dlc.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/base_dlc.py index b262980979..bdae876717 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/base_dlc.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/base_dlc.py @@ -22,7 +22,7 @@ class BaseDLCPoseDataset(BasePoseDataset): def __init__(self, proj_root, dataset_name, shuffle=1, modelprefix=""): super().__init__() - assert proj_root != None and dataset_name != None + assert proj_root is not None and dataset_name is not None self.meta["dataset_name"] = dataset_name self.meta["proj_root"] = proj_root diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc.py index c88350d4a5..e7caae65de 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc.py @@ -73,7 +73,7 @@ def _df2generic(self, df, image_id_offset=0): image_id += 1 - for individual_id, individual in enumerate(individuals): + for _individual_id, individual in enumerate(individuals): category_id = 0 try: kpts = data.xs(individual, level="individuals").to_numpy().reshape((-1, 2)) diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc_dataframe.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc_dataframe.py index d6ce29a41a..2971bf999a 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc_dataframe.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc_dataframe.py @@ -26,8 +26,7 @@ def merge_annotateddatasets(cfg): - """ - Merges all the h5 files for all labeled-datasets (from individual videos). + """Merges all the h5 files for all labeled-datasets (from individual videos). This is a bit of a mess because of cross platform compatibility. diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/materialize.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/materialize.py index 60961c7579..519ed6cebe 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/materialize.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/materialize.py @@ -87,10 +87,8 @@ def create_cfg(self, proj_root, kwargs): class MaDLC_config: def __init__(self): - """ - Plain text only for generating templates - Some variables can be configured by the user later - """ + """Plain text only for generating templates Some variables can be configured by + the user later.""" self.cfg = {k: v for k, v in vars().items() if "__" not in k and "self" not in k} @@ -111,12 +109,12 @@ def _generic2madlc( full_image_path=True, append_image_id=True, ): - """ - Within DeepLabCut, if we don't explicitly call deeplabcut.create_traindataset(), the train and test split might just be arbitrarily messed up. So here we need to calculate train and test indices to + """Within DeepLabCut, if we don't explicitly call deeplabcut.create_traindataset(), + the train and test split might just be arbitrarily messed up. So here we need to + calculate train and test indices to. Args: proj_root where to materialize the data - """ assert full_image_path, "DLC wants full image path" diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/multi.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/multi.py index d89d4c8dd3..f044f3e92c 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/multi.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/multi.py @@ -105,11 +105,9 @@ def _build_maps(self): print(f"Max individual in {dataset_name} is {max_num}") def whether_anno_image_match(self, images, annotations): - """ - Every image id should be annotated at least once - There should not be any image that is not being annotated - There should not be any annotation for beyond the set of given images - """ + """Every image id should be annotated at least once There should not be any + image that is not being annotated There should not be any annotation for beyond + the set of given images.""" image_ids = set([image["id"] for image in images]) @@ -125,11 +123,11 @@ def whether_anno_image_match(self, images, annotations): # assert len(annotation_image_ids - image_ids) == 0, "You can't have annotation on non-existed images" def _update_imgids(self): - """ - update image ids for both image and annotation - - If datasets are merged, their image id, annotation id will conflict because they are defined within their own local scope. Therefore, we will need to put these ids in the global scope + """Update image ids for both image and annotation. + If datasets are merged, their image id, annotation id will conflict because they + are defined within their own local scope. Therefore, we will need to put these + ids in the global scope """ from collections import defaultdict @@ -177,11 +175,9 @@ def _update_imgids(self): print("size of the union", len(union)) def _merge_datasets(self, name2dataset): - """ - Merged datasets into common list + """Merged datasets into common list. # only do this when iid/ood split is done - """ merged_train_images = [] diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc.py index c6bff8bee4..405613362b 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc.py @@ -23,10 +23,8 @@ class SingleDLCPoseDataset(BaseDLCPoseDataset): - """ - The philosophy is to assume the dataset is already created so this class is not - responsible for creating training dataset - """ + """The philosophy is to assume the dataset is already created so this class is not + responsible for creating training dataset.""" def __init__(self, proj_root, dataset_name, shuffle=1, modelprefix=""): super().__init__(proj_root, dataset_name, shuffle=shuffle, modelprefix=modelprefix) diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc_dataframe.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc_dataframe.py index e99dce2991..587653d9ae 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc_dataframe.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc_dataframe.py @@ -26,8 +26,7 @@ def merge_annotateddatasets(cfg): - """ - Merges all the h5 files for all labeled-datasets (from individual videos). + """Merges all the h5 files for all labeled-datasets (from individual videos). This is a bit of a mess because of cross platform compatibility. diff --git a/deeplabcut/modelzoo/generalized_data_converter/utils.py b/deeplabcut/modelzoo/generalized_data_converter/utils.py index 012e770128..94025341c9 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/utils.py +++ b/deeplabcut/modelzoo/generalized_data_converter/utils.py @@ -58,9 +58,7 @@ def threshold_kpts(config_path, h5path, threshold_mean=0.9, threshold_min=0.1): def create_dummy_config_file_from_h5( proj_root, reference_h5, taskname="dummytask", scorer="dummyscorer", date="March30" ): - """ - Assuming at least labeled-data folder is there - """ + """Assuming at least labeled-data folder is there.""" cfg_template = SingleDLC_config() @@ -100,14 +98,12 @@ def create_dummy_config_file_from_pickle( scorer="dummyscorer", date="March30", ): - """ - Assuming at least labeled-data folder is there - """ + """Assuming at least labeled-data folder is there.""" cfg_template = SingleDLC_config() with open(reference_pickle, "rb") as f: - pickle_obj = pickle.load(f) + pickle.load(f) # bodyparts = pickle_obj['keypoint_names'] bodyparts = [ @@ -122,7 +118,7 @@ def create_dummy_config_file_from_pickle( "left ear", ] - video_name = video_path.split("/")[-1] + video_path.split("/")[-1] video_sets = {f"{video_path}": {"crop": "0, 400, 0, 400"}} @@ -146,18 +142,6 @@ def create_video_h5_from_pickle(proj_root, cfg, reference_pickle, videopath): # bodyparts = pickle_obj['keypoint_names'] - bodyparts = [ - "tail", - "spine4", - "spine3", - "spine2", - "spine1", - "head", - "nose", - "right ear", - "left ear", - ] - video_name = videopath.split("/")[-1] video_key = f"{video_name}" # .replace('.top.ir.mp4', '') @@ -184,7 +168,7 @@ def create_video_h5_from_pickle(proj_root, cfg, reference_pickle, videopath): data = np.zeros((len(imagenames), len(columnindex))) * np.nan df = pd.DataFrame(data, columns=columnindex, index=imagenames) - for imagename, kpts in zip(imagenames, detections): + for imagename, kpts in zip(imagenames, detections, strict=False): for kpt_id, kpt_name in enumerate(keypoint_names): df.loc[imagename][scorer, kpt_name, "x"] = kpts[kpt_id, 0] df.loc[imagename][scorer, kpt_name, "y"] = kpts[kpt_id, 1] @@ -286,7 +270,6 @@ def customized_colormap(config_path): colors = [cmap(i) for i in range(n_bodyparts)] - visited = set() for kpt_id in range(len(bodyparts)): bodypart = bodyparts[kpt_id] if "left" in bodypart: diff --git a/deeplabcut/modelzoo/utils.py b/deeplabcut/modelzoo/utils.py index 8fba813507..cce590995d 100644 --- a/deeplabcut/modelzoo/utils.py +++ b/deeplabcut/modelzoo/utils.py @@ -36,7 +36,7 @@ def dlc_modelzoo_path() -> Path: def get_super_animal_project_cfg(super_animal: str) -> dict: - """Gets the project configuration file for a SuperAnimal model + """Gets the project configuration file for a SuperAnimal model. Args: super_animal: the name of the SuperAnimal model for which to load the project @@ -103,9 +103,8 @@ def create_conversion_table( super_animal: str, project_to_super_animal: dict[str, str], ) -> ConversionTable: - """ - Creates a conversion table mapping bodyparts defined for a DeepLabCut project - to bodyparts defined for a SuperAnimal model. This allows to fine-tune SuperAnimal + """Creates a conversion table mapping bodyparts defined for a DeepLabCut project to + bodyparts defined for a SuperAnimal model. This allows to fine-tune SuperAnimal weights instead of transfer learning from ImageNet. The conversion table is directly added to the project's configuration file. @@ -143,7 +142,7 @@ def create_conversion_table( def get_conversion_table(cfg: dict | str | Path, super_animal: str) -> ConversionTable: - """Gets the conversion table from a project to a SuperAnimal model + """Gets the conversion table from a project to a SuperAnimal model. Args: cfg: The path to a project configuration file, or directly the project config. @@ -186,7 +185,7 @@ def read_conversion_table_from_csv(csv_path): def parse_project_model_name(superanimal_name: str) -> tuple[str, str]: - """Parses model zoo model names for SuperAnimal models + """Parses model zoo model names for SuperAnimal models. Args: superanimal_name: the name of the SuperAnimal model name to parse diff --git a/deeplabcut/modelzoo/video_inference.py b/deeplabcut/modelzoo/video_inference.py index d64a61eb10..2b2ccbfe68 100644 --- a/deeplabcut/modelzoo/video_inference.py +++ b/deeplabcut/modelzoo/video_inference.py @@ -35,8 +35,7 @@ def get_checkpoint_epoch(checkpoint_path): - """ - Load a PyTorch checkpoint and return the current epoch number. + """Load a PyTorch checkpoint and return the current epoch number. Args: checkpoint_path (str): Path to the checkpoint file @@ -80,8 +79,7 @@ def video_inference_superanimal( plot_bboxes: bool = True, create_labeled_video: bool = True, ): - """ - This function performs inference on videos using a pretrained SuperAnimal model. + """This function performs inference on videos using a pretrained SuperAnimal model. IMPORTANT: Note that since we have both TensorFlow and PyTorch Engines, we will route the engine based on the model you select: diff --git a/deeplabcut/modelzoo/webapp/inference.py b/deeplabcut/modelzoo/webapp/inference.py index 0e6b6eab05..8e586ba24a 100644 --- a/deeplabcut/modelzoo/webapp/inference.py +++ b/deeplabcut/modelzoo/webapp/inference.py @@ -17,7 +17,7 @@ class SingletonTopDownRunners: - """Singleton class for topdown runners + """Singleton class for topdown runners. This class is a singleton class for topdown runners. It is used to ensure that only one instance of the topdown runners is created. @@ -59,10 +59,10 @@ def __init__( class SuperanimalPyTorchInference: - """Superanimal inference class + """Superanimal inference class. - This class is used to perform inference on a superanimal model from the - DeepLabCut model zoo website. + This class is used to perform inference on a superanimal model from the DeepLabCut + model zoo website. """ def __init__( @@ -100,10 +100,10 @@ def predict(self, frames: dict[str, np.array]): input_images = np.array(list(frames.values()), dtype=float) bbox_predictions = self.models.detector_runner.inference(images=input_images) - input_images = list(zip(input_images, bbox_predictions)) + input_images = list(zip(input_images, bbox_predictions, strict=False)) predictions = self.models.pose_runner.inference(images=input_images) predictions = [{("markers" if k == "bodyparts" else k): v for k, v in d.items()} for d in predictions] - predictions = [{**item[1], "image_path": item[0]} for item in zip(frames.keys(), predictions)] + predictions = [{**item[1], "image_path": item[0]} for item in zip(frames.keys(), predictions, strict=False)] responses = { "joint_names": self.config["bodyparts"], "predictions": predictions, diff --git a/deeplabcut/pose_estimation_3d/camera_calibration.py b/deeplabcut/pose_estimation_3d/camera_calibration.py index 84bf270557..a276b3f38d 100644 --- a/deeplabcut/pose_estimation_3d/camera_calibration.py +++ b/deeplabcut/pose_estimation_3d/camera_calibration.py @@ -26,7 +26,9 @@ def calibrate_cameras(config, cbrow=8, cbcol=6, calibrate=False, alpha=0.4, search_window_size=(11, 11)): - """This function extracts the corners points from the calibration images, calibrates the camera and stores the calibration files in the project folder (defined in the config file). + """This function extracts the corners points from the calibration images, calibrates + the camera and stores the calibration files in the project folder (defined in the + config file). Make sure you have around 20-60 pairs of calibration images. The function should be used iteratively to select the right set of calibration images. @@ -66,7 +68,6 @@ def calibrate_cameras(config, cbrow=8, cbcol=6, calibrate=False, alpha=0.4, sear Once the right set of calibration images are selected, >>> deeplabcut.calibrate_camera(config,calibrate=True) - """ # Termination criteria criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001) @@ -264,9 +265,9 @@ def calibrate_cameras(config, cbrow=8, cbcol=6, calibrate=False, alpha=0.4, sear def check_undistortion(config, cbrow=8, cbcol=6, plot=True): - """ - This function undistorts the calibration images based on the camera matrices and stores them in the project folder(defined in the config file) - to visually check if the camera matrices are correct. + """This function undistorts the calibration images based on the camera matrices and + stores them in the project folder(defined in the config file) to visually check if + the camera matrices are correct. Parameters ---------- @@ -286,7 +287,6 @@ def check_undistortion(config, cbrow=8, cbcol=6, plot=True): -------- Linux/MacOs/Windows >>> deeplabcut.check_undistortion(config, cbrow = 8,cbcol = 6) - """ # Read the config file diff --git a/deeplabcut/pose_estimation_3d/plotting3D.py b/deeplabcut/pose_estimation_3d/plotting3D.py index b104116305..cbf5c0de24 100644 --- a/deeplabcut/pose_estimation_3d/plotting3D.py +++ b/deeplabcut/pose_estimation_3d/plotting3D.py @@ -72,8 +72,8 @@ def create_labeled_video_3d( fps=30, dpi=300, ): - """ - Creates a video with views from the two cameras and the 3d reconstruction for a selected number of frames. + """Creates a video with views from the two cameras and the 3d reconstruction for a + selected number of frames. Parameters ---------- @@ -128,7 +128,6 @@ def create_labeled_video_3d( To set the xlim, ylim, zlim and rotate the view of the 3d axis >>> deeplabcut.create_labeled_video_3d(config,['/data/project1/videos'],start=100, end=500,view=[30,90],xlim=[-12,12],ylim=[15,25],zlim=[20,30]) - """ os.getcwd() diff --git a/deeplabcut/pose_estimation_3d/triangulation.py b/deeplabcut/pose_estimation_3d/triangulation.py index 296e0ddcd8..b9945fcf8e 100644 --- a/deeplabcut/pose_estimation_3d/triangulation.py +++ b/deeplabcut/pose_estimation_3d/triangulation.py @@ -31,8 +31,7 @@ def triangulate( save_as_csv=False, track_method="", ): - """ - This function triangulates the detected DLC-keypoints from the two camera views + """This function triangulates the detected DLC-keypoints from the two camera views using the camera matrices (derived from calibration) to calculate 3D predictions. Parameters @@ -492,10 +491,8 @@ def _undistort_views(df_view_pairs, stereo_params): def undistort_points(config, dataframe, camera_pair): cfg_3d = auxiliaryfunctions.read_config(config) path_camera_matrix = auxiliaryfunctions_3d.Foldernames3Dproject(cfg_3d)[2] - """ - path_undistort = destfolder - filename_cam1 = Path(dataframe[0]).stem - filename_cam2 = Path(dataframe[1]).stem + """path_undistort = destfolder filename_cam1 = Path(dataframe[0]).stem filename_cam2 + = Path(dataframe[1]).stem. #currently no intermediate saving of this due to high speed. # check if the undistorted files are already present diff --git a/deeplabcut/pose_estimation_pytorch/apis/analyze_images.py b/deeplabcut/pose_estimation_pytorch/apis/analyze_images.py index 1ce8fb3762..bb82d5ab84 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/analyze_images.py +++ b/deeplabcut/pose_estimation_pytorch/apis/analyze_images.py @@ -63,8 +63,7 @@ def superanimal_analyze_images( customized_detector_checkpoint: str | Path | None = None, close_figure_after_save=True, ) -> dict[str, dict]: - """ - This function inferences a superanimal model on a set of images and saves the + """This function inferences a superanimal model on a set of images and saves the results as labeled images. Args: @@ -433,7 +432,7 @@ def analyze_image_folder( filtered_detector_config: dict | None = None, cond_provider: CondFromModel | None = None, ) -> dict[str, dict[str, np.ndarray | np.ndarray]]: - """Runs pose inference on a folder of images and returns the predictions + """Runs pose inference on a folder of images and returns the predictions. Args: model_cfg: The model config (or its path) used to analyze the images. @@ -529,7 +528,7 @@ def analyze_image_folder( if detector_runner is not None: detector_image_paths = tqdm(image_paths) if progress_bar else image_paths bbox_predictions = detector_runner.inference(images=detector_image_paths) - pose_inputs = list(zip(image_paths, bbox_predictions)) + pose_inputs = list(zip(image_paths, bbox_predictions, strict=False)) logging.info(f"Running pose estimation with {snapshot_path}") @@ -538,7 +537,9 @@ def analyze_image_folder( predictions = pose_runner.inference(pose_inputs) - return {image_path: image_predictions for image_path, image_predictions in zip(image_paths, predictions)} + return { + image_path: image_predictions for image_path, image_predictions in zip(image_paths, predictions, strict=False) + } def plot_images_coco( @@ -552,9 +553,8 @@ def plot_images_coco( max_individuals: int | None = None, cond_provider: CondFromModel | None = None, ) -> list[dict]: - """ - Runs pose inference on a folder of images from a COCO dataset, and plots all - predicted keypoints and bounding boxes + """Runs pose inference on a folder of images from a COCO dataset, and plots all + predicted keypoints and bounding boxes. Args: model_cfg: The model config (or its path) used to analyze the images. diff --git a/deeplabcut/pose_estimation_pytorch/apis/ctd.py b/deeplabcut/pose_estimation_pytorch/apis/ctd.py index b482f4d453..8f2ee0d8ef 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/ctd.py +++ b/deeplabcut/pose_estimation_pytorch/apis/ctd.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Methods to help with conditional top-down models""" +"""Methods to help with conditional top-down models.""" from pathlib import Path @@ -61,7 +61,7 @@ def get_conditions_provider_for_video( cond_provider: CondFromModel, video: str | Path, ) -> CondFromFile | None: - """Tries to create a conditions loader + """Tries to create a conditions loader. Args: cond_provider: The CondFromModel condition provider that will be used. The @@ -90,7 +90,7 @@ def get_conditions_provider_for_video( def load_conditions_for_evaluation(loader: data.Loader, images: list[str]) -> dict[str, np.ndarray]: - """Loads the conditions needed to evaluate a CTD model + """Loads the conditions needed to evaluate a CTD model. Args: loader: The Loader for the CTD model to evaluate. diff --git a/deeplabcut/pose_estimation_pytorch/apis/evaluation.py b/deeplabcut/pose_estimation_pytorch/apis/evaluation.py index 7b9d393ce4..9a15085ae7 100755 --- a/deeplabcut/pose_estimation_pytorch/apis/evaluation.py +++ b/deeplabcut/pose_estimation_pytorch/apis/evaluation.py @@ -56,7 +56,7 @@ def predict( mode: str, detector_runner: InferenceRunner | None = None, ) -> dict[str, dict[str, np.ndarray]]: - """Predicts poses on data contained in a loader + """Predicts poses on data contained in a loader. Args: pose_runner: The runner to use for pose estimation @@ -91,10 +91,12 @@ def predict( if context is not None: if len(context) != len(image_paths): raise ValueError(f"Missing context for some images: {len(context)} != {len(image_paths)}") - images_with_context = list(zip(image_paths, context)) + images_with_context = list(zip(image_paths, context, strict=False)) predictions = pose_runner.inference(images=tqdm(images_with_context)) - return {image_path: image_predictions for image_path, image_predictions in zip(image_paths, predictions)} + return { + image_path: image_predictions for image_path, image_predictions in zip(image_paths, predictions, strict=False) + } def evaluate( @@ -461,9 +463,8 @@ def evaluate_snapshot( detector_snapshot: Snapshot | None = None, pcutoff: float | list[float] | dict[str, float] | None = None, ) -> pd.DataFrame: - """Evaluates a snapshot. - The evaluation results are stored in the .h5 and .csv file under the subdirectory - 'evaluation_results'. + """Evaluates a snapshot. The evaluation results are stored in the .h5 and .csv file + under the subdirectory 'evaluation_results'. Args: cfg: the content of the project's config file @@ -830,8 +831,7 @@ def image_to_dlc_df_index(image: str) -> tuple[str, ...]: def save_evaluation_results(df_scores: pd.DataFrame, scores_path: Path, print_results: bool, pcutoff: float) -> None: - """ - Saves the evaluation results to a CSV file. Adds the evaluation results for the + """Saves the evaluation results to a CSV file. Adds the evaluation results for the model to the combined results file, or creates it if it does not yet exist. Args: @@ -862,8 +862,7 @@ def save_rmse_per_bodypart( output_path: Path, print_results: bool, ) -> None: - """ - Saves the evaluation results per bodypart to a CSV file. + """Saves the evaluation results per bodypart to a CSV file. Args: rmse_per_bodypart: The scores dataframe for a snapshot @@ -896,7 +895,7 @@ def _validate_pcutoff( unique_bpts: list[str], pcutoff: float | list[float], ) -> None: - """Checks that the given `pcutoff` value has the correct number of elements""" + """Checks that the given `pcutoff` value has the correct number of elements.""" if isinstance(pcutoff, (int, float)): return @@ -964,7 +963,7 @@ def _extract_rmse_per_bodypart( bodyparts: list[str], unique_bodyparts: list[str], ) -> dict[str, float]: - """Extracts the RMSE per bodypart metrics from the results dict + """Extracts the RMSE per bodypart metrics from the results dict. This method modifies the given dict in-place, removing all keys for RMSE per bodypart or unique bodypart. diff --git a/deeplabcut/pose_estimation_pytorch/apis/prune_paf_graph.py b/deeplabcut/pose_estimation_pytorch/apis/prune_paf_graph.py index 6214ab3f86..fe96e5f100 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/prune_paf_graph.py +++ b/deeplabcut/pose_estimation_pytorch/apis/prune_paf_graph.py @@ -36,7 +36,7 @@ def benchmark_paf_graphs( overwrite: bool = False, update_config: bool = True, ) -> list[dict]: - """Prunes the PAF graph to maximize performance + """Prunes the PAF graph to maximize performance. Args: loader: The loader for the model to prune. @@ -213,13 +213,13 @@ def compute_within_between_paf_costs( # Get animal IDs and corresponding indices in the arrays of detections lookup = dict() - for i, (coord_pred, coord_gt) in enumerate(zip(coords_pred, gt_pose)): + for i, (coord_pred, coord_gt) in enumerate(zip(coords_pred, gt_pose, strict=False)): inds = np.flatnonzero(np.all(~np.isnan(coord_pred), axis=1)) inds_gt = np.flatnonzero(np.all(~np.isnan(coord_gt), axis=1)) if inds.size and inds_gt.size: neighbors = find_closest_neighbors(coord_gt[inds_gt], coord_pred[inds], k=3) found = neighbors != -1 - lookup[i] = dict(zip(inds_gt[found], inds[neighbors[found]])) + lookup[i] = dict(zip(inds_gt[found], inds[neighbors[found]], strict=False)) for k, v in costs_pred.items(): paf = v["m1"] @@ -256,11 +256,13 @@ def get_n_best_paf_graphs( within_train, between_train = compute_within_between_paf_costs(model, ground_truth, preprocessor, device) existing_edges = list(set(k for k, v in within_train.items() if v)) - scores, _ = zip(*[_calc_separability(between_train[n], within_train[n], metric=metric) for n in existing_edges]) + scores, _ = zip( + *[_calc_separability(between_train[n], within_train[n], metric=metric) for n in existing_edges], strict=False + ) # Find minimal skeleton G = nx.Graph() - for edge, score in zip(existing_edges, scores): + for edge, score in zip(existing_edges, scores, strict=False): if np.isfinite(score): G.add_edge(*full_graph[edge], weight=score) @@ -278,4 +280,4 @@ def get_n_best_paf_graphs( best_edges.append(root_edges + list(order[:length])) model.heads.bodypart.predictor.return_preds = return_preds - return best_edges, dict(zip(existing_edges, scores)) + return best_edges, dict(zip(existing_edges, scores, strict=False)) diff --git a/deeplabcut/pose_estimation_pytorch/apis/tracklets.py b/deeplabcut/pose_estimation_pytorch/apis/tracklets.py index fbb1e7c3d9..d3fea4a220 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/tracklets.py +++ b/deeplabcut/pose_estimation_pytorch/apis/tracklets.py @@ -57,7 +57,7 @@ def convert_detections2tracklets( track_method = auxfun_multianimal.get_track_method(cfg, track_method=track_method) if len(cfg["multianimalbodyparts"]) == 1 and track_method != "box": - warnings.warn("Switching to `box` tracker for single point tracking...") + warnings.warn("Switching to `box` tracker for single point tracking...", stacklevel=2) track_method = "box" cfg["default_track_method"] = track_method auxiliaryfunctions.write_config(config, cfg) @@ -97,7 +97,7 @@ def convert_detections2tracklets( auxfun_multianimal.check_inferencecfg_sanity(cfg, inference_cfg) if len(cfg["multianimalbodyparts"]) == 1 and track_method != "box": - warnings.warn("Switching to `box` tracker for single point tracking...") + warnings.warn("Switching to `box` tracker for single point tracking...", stacklevel=2) track_method = "box" # Also ensure `boundingboxslack` is greater than zero, otherwise overlap # between trackers cannot be evaluated, resulting in empty tracklets. @@ -273,7 +273,7 @@ def build_tracklets( unique_ids, idx = np.unique(animal_pose[:, 3], return_inverse=True) total_scores = np.bincount(idx, weights=animal_pose[:, 2]) softmax_id_scores = softmax(total_scores) - for pred_id, softmax_score in zip(unique_ids.astype(int), softmax_id_scores): + for pred_id, softmax_score in zip(unique_ids.astype(int), softmax_id_scores, strict=False): mat[row, pred_id] = softmax_score inds = linear_sum_assignment(mat, maximize=True) @@ -307,10 +307,8 @@ def _create_tracklets_header(joints, dlc_scorer): def _conv_predictions_to_assemblies( image_names: list[str], predictions: dict[str, np.ndarray] ) -> dict[int, list[Assembly]]: - """ - Converts predictions to an assemblies dictionary - predictions shape (num_animals, num_keypoints, 2 or 3) - """ + """Converts predictions to an assemblies dictionary predictions shape (num_animals, + num_keypoints, 2 or 3)""" assemblies = {} if len(predictions) == 0: return assemblies diff --git a/deeplabcut/pose_estimation_pytorch/apis/training.py b/deeplabcut/pose_estimation_pytorch/apis/training.py index 962b3d8ed0..dc636bc9f1 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/training.py +++ b/deeplabcut/pose_estimation_pytorch/apis/training.py @@ -54,7 +54,7 @@ def train( max_snapshots_to_keep: int | None = None, load_head_weights: bool = True, ) -> None: - """Builds a model from a configuration and fits it to a dataset + """Builds a model from a configuration and fits it to a dataset. Args: loader: the loader containing the data to train on/validate with @@ -217,7 +217,7 @@ def train_network( pose_threshold: float | None = 0.1, pytorch_cfg_updates: dict | None = None, ) -> None: - """Trains a network for a project + """Trains a network for a project. Args: config : path to the yaml config file of the project diff --git a/deeplabcut/pose_estimation_pytorch/apis/utils.py b/deeplabcut/pose_estimation_pytorch/apis/utils.py index 1eec687b4a..8eb47886a7 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/utils.py +++ b/deeplabcut/pose_estimation_pytorch/apis/utils.py @@ -240,7 +240,7 @@ def get_scorer_name( snapshot_uid: str | None = None, modelprefix: str = "", ) -> str: - """Get the scorer name for a particular PyTorch DeepLabCut shuffle + """Get the scorer name for a particular PyTorch DeepLabCut shuffle. Args: cfg: The project configuration. @@ -339,8 +339,7 @@ def list_videos_in_folder( def ensure_multianimal_df_format(df_predictions: pd.DataFrame) -> pd.DataFrame: - """ - Convert dataframe to 'multianimal' format (with an "individuals" columns index) + """Convert dataframe to 'multianimal' format (with an "individuals" columns index) Args: df_predictions: the dataframe to convert @@ -364,10 +363,9 @@ def _image_names_to_df_index( image_names: list[str], image_name_to_index: Callable[[str], tuple[str, ...]] | None = None, ) -> pd.MultiIndex | list[str]: - """ - Creates index for predictions dataframe. - This method is used in build_predictions_dataframe, but also in build_bboxes_dict_for_dataframe. - It is important that these two methods return objects with the same index / keys. + """Creates index for predictions dataframe. This method is used in + build_predictions_dataframe, but also in build_bboxes_dict_for_dataframe. It is + important that these two methods return objects with the same index / keys. Args: image_names: list of image names @@ -386,8 +384,7 @@ def build_predictions_dataframe( parameters: PoseDatasetParameters, image_name_to_index: Callable[[str], tuple[str, ...]] | None = None, ) -> pd.DataFrame: - """ - Builds a pandas DataFrame from pose prediction data. The resulting DataFrame + """Builds a pandas DataFrame from pose prediction data. The resulting DataFrame includes properly formatted indices and column names for compatibility with DeepLabCut workflows. @@ -436,8 +433,7 @@ def build_bboxes_dict_for_dataframe( predictions: dict[str, dict[str, np.ndarray]], image_name_to_index: Callable[[str], tuple[str, ...]] | None = None, ) -> dict: - """ - Creates a dictionary with bounding boxes from predictions. + """Creates a dictionary with bounding boxes from predictions. The keys of the dictionary are the same as the index of the dataframe created by build_predictions_dataframe. Therefore, the structures returned by @@ -462,7 +458,7 @@ def build_bboxes_dict_for_dataframe( index = _image_names_to_df_index(image_names, image_name_to_index) - return dict(zip(index, bboxes_data)) + return dict(zip(index, bboxes_data, strict=False)) def get_inference_runners( @@ -482,7 +478,7 @@ def get_inference_runners( inference_cfg: InferenceConfig | dict | None = None, min_bbox_score: float | None = None, ) -> tuple[InferenceRunner, InferenceRunner | None]: - """Builds the runners for pose estimation + """Builds the runners for pose estimation. Args: model_config: the pytorch configuration file @@ -723,8 +719,8 @@ def get_filtered_coco_detector_inference_runner( inference_cfg: InferenceConfig | dict | None = None, min_bbox_score: float | None = None, ) -> DetectorInferenceRunner: - """ - Builds a detector inference runner using a pretrained COCO detector from torchvision. + """Builds a detector inference runner using a pretrained COCO detector from + torchvision. This function loads a pretrained object detection model from `torchvision.models.detection`, wraps it in a `FilteredDetector` that keeps only detections for a specified COCO category, diff --git a/deeplabcut/pose_estimation_pytorch/apis/videos.py b/deeplabcut/pose_estimation_pytorch/apis/videos.py index 424af46f72..4341fdb2b8 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/videos.py +++ b/deeplabcut/pose_estimation_pytorch/apis/videos.py @@ -47,7 +47,7 @@ class VideoIterator(VideoReader): - """A class to iterate over videos, with possible added context""" + """A class to iterate over videos, with possible added context.""" def __init__( self, @@ -129,7 +129,7 @@ def video_inference( robust_nframes: bool = False, show_gpu_memory: bool = False, ) -> list[dict[str, np.ndarray]]: - """Runs inference on a video + """Runs inference on a video. Args: video: The video to analyze @@ -514,7 +514,7 @@ def analyze_videos( ) detector_runner = None - detector_path, detector_snapshot = None, None + _detector_path, detector_snapshot = None, None if loader.pose_task == Task.TOP_DOWN and dynamic is None: if detector_snapshot_index is None: raise ValueError( @@ -770,7 +770,7 @@ def _generate_assemblies_file( num_bodyparts: int, num_unique_bodyparts: int, ) -> None: - """Generates the assemblies file from predictions""" + """Generates the assemblies file from predictions.""" if full_data_path.exists(): with open(full_data_path, "rb") as f: data = pickle.load(f) @@ -831,7 +831,7 @@ def _generate_assemblies_file( def _validate_destfolder(destfolder: str | None) -> None: - """Checks that the destfolder for video analysis is valid""" + """Checks that the destfolder for video analysis is valid.""" if destfolder is not None and destfolder != "": output_folder = Path(destfolder) if not output_folder.exists(): diff --git a/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py b/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py index 64aee73944..012590a21b 100644 --- a/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py +++ b/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Methods to create the configuration files for PyTorch DeepLabCut models""" +"""Methods to create the configuration files for PyTorch DeepLabCut models.""" from __future__ import annotations @@ -39,7 +39,7 @@ def make_pytorch_pose_config( save: bool = False, ctd_conditions: int | str | Path | tuple[int, str] | tuple[int, int] | None = None, ) -> dict: - """Creates a PyTorch pose configuration file for a DeepLabCut project + """Creates a PyTorch pose configuration file for a DeepLabCut project. The base/ folder contains default configurations, such as data augmentations or heatmap heads (that can be used to predict pose or identity based on visual @@ -249,7 +249,7 @@ def make_pytorch_test_config( test_config_path: str | Path, save: bool = False, ) -> dict: - """Creates the test configuration for a model + """Creates the test configuration for a model. Args: model_config: The PyTorch config for the model. @@ -349,7 +349,7 @@ def make_basic_project_config( def add_metadata(project_config: dict, config: dict, pose_config_path: str | Path) -> dict: - """Adds metadata to a pytorch pose configuration + """Adds metadata to a pytorch pose configuration. Args: project_config: the project configuration @@ -378,9 +378,8 @@ def create_backbone_with_heatmap_model( bodyparts: list[str], top_down: bool, ) -> dict: - """ - Creates a simple heatmap pose estimation model, composed of a backbone and a head - predicting heatmaps and location refinement maps + """Creates a simple heatmap pose estimation model, composed of a backbone and a head + predicting heatmaps and location refinement maps. Args: configs_dir: path to the DeepLabCut "configs" directory @@ -436,8 +435,7 @@ def create_backbone_with_paf_model( bodyparts: list[str], paf_parameters: dict, ) -> dict: - """ - Creates a pose estimation model, composed of a backbone and a head predicting + """Creates a pose estimation model, composed of a backbone and a head predicting heatmaps, location refinement maps and part affinity fields for multi-animal pose estimation. @@ -475,7 +473,7 @@ def add_detector( num_individuals: int, detector_type: str | None = None, ) -> dict: - """Adds a detector to a model + """Adds a detector to a model. Args: configs_dir: path to the DeepLabCut "configs" directory @@ -509,7 +507,7 @@ def add_unique_bodypart_head( num_unique_bodyparts: int, backbone_output_channels: int, ) -> dict: - """Adds a unique bodypart head to a model + """Adds a unique bodypart head to a model. Args: configs_dir: path to the DeepLabCut "configs" directory @@ -537,7 +535,7 @@ def add_identity_head( num_individuals: int, backbone_output_channels: int, ) -> dict: - """Adds an identity head to a model + """Adds an identity head to a model. Args: configs_dir: path to the DeepLabCut "configs" directory @@ -564,7 +562,7 @@ def _get_paf_parameters( num_limbs_threshold: int = 105, paf_graph_degree: int = 6, ) -> dict: - """Gets values for PAF parameters from the project configuration""" + """Gets values for PAF parameters from the project configuration.""" paf_graph = [[i, j] for i in range(len(bodyparts)) for j in range(i + 1, len(bodyparts))] num_limbs = len(paf_graph) # If the graph is unnecessarily large (with 15+ keypoints by default), diff --git a/deeplabcut/pose_estimation_pytorch/config/utils.py b/deeplabcut/pose_estimation_pytorch/config/utils.py index 4a17e7ae34..4994207487 100644 --- a/deeplabcut/pose_estimation_pytorch/config/utils.py +++ b/deeplabcut/pose_estimation_pytorch/config/utils.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Util functions to create pytorch pose configuration files""" +"""Util functions to create pytorch pose configuration files.""" from __future__ import annotations @@ -117,7 +117,7 @@ def get_updated_value(variable: str) -> int | list[int]: def update_config(config: dict, updates: dict, copy_original: bool = True) -> dict: - """Updates items in the configuration file + """Updates items in the configuration file. The configuration dict should only be composed of primitive Python types (dict, list and values). This is the case when reading the file using @@ -147,7 +147,7 @@ def update_config(config: dict, updates: dict, copy_original: bool = True) -> di def update_config_by_dotpath(config: dict, updates: dict, copy_original: bool = True) -> dict: - """Updates items in the configuration file using dot notation for nested keys + """Updates items in the configuration file using dot notation for nested keys. The configuration dict should only be composed of primitive Python types (dict, list and values). This is the case when reading the file using @@ -249,7 +249,7 @@ def available_models() -> list[str]: def is_model_top_down(net_type: str) -> bool: - """Checks whenever a given net_type is top-down or not""" + """Checks whenever a given net_type is top-down or not.""" if net_type not in available_models(): raise ValueError(f"Model {net_type} is not part of available models, which are {str(available_models())}") @@ -272,7 +272,7 @@ def is_model_top_down(net_type: str) -> bool: def is_model_cond_top_down(net_type: str) -> bool: - """Checks whether a given net_type is conditional top-down or not""" + """Checks whether a given net_type is conditional top-down or not.""" if net_type not in available_models(): raise ValueError(f"Model {net_type} is not part of available models, which are {str(available_models())}") diff --git a/deeplabcut/pose_estimation_pytorch/data/base.py b/deeplabcut/pose_estimation_pytorch/data/base.py index 1b06cd62b0..4795826ab4 100644 --- a/deeplabcut/pose_estimation_pytorch/data/base.py +++ b/deeplabcut/pose_estimation_pytorch/data/base.py @@ -35,8 +35,8 @@ class Loader(ABC): - """ - Abstract class that represents a blueprint for loading and processing dataset information. + """Abstract class that represents a blueprint for loading and processing dataset + information. Methods: load_data(mode: str = 'train') -> dict: @@ -93,7 +93,7 @@ def snapshots( return list_snapshots(self.model_folder, prefix, best_in_last=best_in_last) def update_model_cfg(self, updates: dict) -> None: - """Updates the model configuration + """Updates the model configuration. Args: updates: the items to update in the model configuration @@ -103,7 +103,8 @@ def update_model_cfg(self, updates: dict) -> None: @abstractmethod def load_data(self, mode: str = "train") -> dict[str, list[dict]]: - """Abstract method to convert the project configuration to a standard coco format. + """Abstract method to convert the project configuration to a standard coco + format. Raises: NotImplementedError: This method must be implemented in the derived classes. @@ -125,8 +126,7 @@ def image_filenames(self, mode: str = "train") -> list[str]: return [image["file_name"] for image in data["images"]] def ground_truth_keypoints(self, mode: str = "train", unique_bodypart: bool = False) -> dict[str, np.ndarray]: - """ - Creates a dictionary containing the ground truth data + """Creates a dictionary containing the ground truth data. TODO: make more efficient @@ -183,7 +183,7 @@ def ground_truth_keypoints(self, mode: str = "train", unique_bodypart: bool = Fa return ground_truth_dict def ground_truth_bboxes(self, mode: str = "train") -> dict[str, dict]: - """Creates a dictionary containing the ground truth bounding boxes + """Creates a dictionary containing the ground truth bounding boxes. Args: mode: {"train", "test"} whether to load train or test data @@ -231,8 +231,7 @@ def create_dataset( mode: str = "train", task: Task = Task.BOTTOM_UP, ) -> PoseDataset: - """ - Creates a PoseDataset based on provided arguments. + """Creates a PoseDataset based on provided arguments. Args: transform: Transformation to be applied on dataset. Defaults to None. @@ -268,8 +267,7 @@ def create_dataset( @abstractmethod def get_dataset_parameters(self) -> PoseDatasetParameters: - """ - Retrieves dataset parameters based on the instance's configuration. + """Retrieves dataset parameters based on the instance's configuration. Returns: An instance of the PoseDatasetParameters with the parameters set. @@ -278,7 +276,7 @@ def get_dataset_parameters(self) -> PoseDatasetParameters: @staticmethod def filter_annotations(annotations: list[dict], task: Task) -> list[dict]: - """Filters annotations based on the task, removing empty annotations + """Filters annotations based on the task, removing empty annotations. For pose estimation tasks, annotations with empty keypoints are removed. For detection task, annotations with no bounding boxes are removed @@ -334,7 +332,7 @@ def _compute_bboxes( return annotations elif method == "gt": - for i, annotation in enumerate(annotations): + for _i, annotation in enumerate(annotations): if "bbox" not in annotation: # or do something else? raise ValueError( diff --git a/deeplabcut/pose_estimation_pytorch/data/cocoloader.py b/deeplabcut/pose_estimation_pytorch/data/cocoloader.py index 57051b7de8..7fdea47f48 100644 --- a/deeplabcut/pose_estimation_pytorch/data/cocoloader.py +++ b/deeplabcut/pose_estimation_pytorch/data/cocoloader.py @@ -62,8 +62,7 @@ def __init__( self.test_json = self.load_json(self.project_root, self.test_json_filename) def get_dataset_parameters(self) -> PoseDatasetParameters: - """ - Retrieves dataset parameters based on the instance's configuration. + """Retrieves dataset parameters based on the instance's configuration. Returns: An instance of the PoseDatasetParameters with the parameters set. @@ -145,13 +144,15 @@ def validate_categories(coco_json: dict) -> dict: warnings.warn( f"Found a category with ID 0 ({cat}) in the COCO dataset. This is not" f" allowed, as category ID 0 is reserved as the background ID for" - f" torchvision detectors. All category IDs have been shifted by 1." + f" torchvision detectors. All category IDs have been shifted by 1.", + stacklevel=2, ) if len(coco_json["categories"]) > 1: warnings.warn( "Found more than 1 category in the project. This is currently not" - " supported in DeepLabCut. All annotations will be given category 1" + " supported in DeepLabCut. All annotations will be given category 1", + stacklevel=2, ) if cat_0: @@ -165,7 +166,7 @@ def validate_categories(coco_json: dict) -> dict: return coco_json def validate_images(self, coco_json: dict) -> dict: - """Goes over images and annotations to look for potential errors + """Goes over images and annotations to look for potential errors. This code tries to ensure that training a model on this project does not crash down the line @@ -200,7 +201,7 @@ def validate_images(self, coco_json: dict) -> dict: image_ids.add(image["id"]) if len(missing_images) > 0: - warnings.warn(f"There are {len(missing_images)} images that cannot be found (here are some):") + warnings.warn(f"There are {len(missing_images)} images that cannot be found (here are some):", stacklevel=2) for img_id, file_name in missing_images.items(): print(f" * {img_id}: {file_name}") @@ -221,7 +222,8 @@ def validate_images(self, coco_json: dict) -> dict: if len(coco_json["annotations"]) < len(validated_annotations): warnings.warn( - "Found some annotations for which the image ID was not in the images. Removing them from the dataset." + "Found some annotations for which the image ID was not in the images. Removing them from the dataset.", + stacklevel=2, ) print(f" All annotations: {len(coco_json['annotations'])}") print(f" Annotations with correct image IDs: {len(validated_annotations)}") @@ -308,7 +310,7 @@ def predictions_to_coco( predictions: dict[str, dict[str, np.ndarray]], mode: str = "train", ) -> list[dict]: - """Converts detections to COCO format + """Converts detections to COCO format. Args: predictions: a dictionary mapping image name to the predictions made for it diff --git a/deeplabcut/pose_estimation_pytorch/data/collate.py b/deeplabcut/pose_estimation_pytorch/data/collate.py index bf72a5bd25..fee4057253 100644 --- a/deeplabcut/pose_estimation_pytorch/data/collate.py +++ b/deeplabcut/pose_estimation_pytorch/data/collate.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Custom collate functions""" +"""Custom collate functions.""" from __future__ import annotations @@ -24,7 +24,7 @@ class CollateFunction(ABC): - """A class that can be called as a collate function""" + """A class that can be called as a collate function.""" @abstractmethod def __call__(self, batch) -> dict | list: @@ -33,7 +33,7 @@ def __call__(self, batch) -> dict | list: class ResizeCollate(CollateFunction, ABC): - """A collate function which resizes all images in a batch to the same size + """A collate function which resizes all images in a batch to the same size. Args: max_shift: The maximum shift, in pixels, to add to the random crop (this means @@ -83,7 +83,7 @@ def __call__(self, batch) -> dict | list: @COLLATE_FUNCTIONS.register_module class ResizeFromDataSizeCollate(ResizeCollate): - """A collate function which resizes all images in a batch to the same size + """A collate function which resizes all images in a batch to the same size. The target size is obtained by taking the size of the first image in the batch, and multiplying it by a scale taken uniformly at random from (min_scale, max_scale). @@ -164,7 +164,7 @@ def _sample_scale(self) -> int | tuple[int, int]: @COLLATE_FUNCTIONS.register_module class ResizeFromListCollate(ResizeCollate): - """A collate function which resizes all images in a batch to the same size + """A collate function which resizes all images in a batch to the same size. The target size image size is sampled from a list. If it's a list of integers, all images will be resized into squares. If it's a list of tuples, that will be the diff --git a/deeplabcut/pose_estimation_pytorch/data/ctd.py b/deeplabcut/pose_estimation_pytorch/data/ctd.py index 72c3fe2584..c789fb4611 100644 --- a/deeplabcut/pose_estimation_pytorch/data/ctd.py +++ b/deeplabcut/pose_estimation_pytorch/data/ctd.py @@ -93,7 +93,7 @@ def get_loader_and_snapshot( class CondFromFile(CondProvider): - """A class providing conditions for a CTD model from a file + """A class providing conditions for a CTD model from a file. Args: filepath: The path to the file containing the conditions for the CTD model. @@ -169,7 +169,7 @@ def load_conditions_h5( images: list[str] | None = None, path_prefix: str | Path | None = None, ) -> dict[str, np.ndarray] | list[np.ndarray]: - """Loads conditions for a model from a pandas DataFrame stored in an HDF file + """Loads conditions for a model from a pandas DataFrame stored in an HDF file. When loading conditions for individual images, the `images` must be provided (indicating which images to load conditions for). A dict is returned containing @@ -394,7 +394,7 @@ def load_conditions_json( @staticmethod def load_conditions_pickle(filepath: str | Path) -> list[np.ndarray]: - """Loads conditions from a `*_assemblies.pickle` file containing predictions + """Loads conditions from a `*_assemblies.pickle` file containing predictions. Args: filepath: Path to the Pickle file containing conditions. diff --git a/deeplabcut/pose_estimation_pytorch/data/dlcloader.py b/deeplabcut/pose_estimation_pytorch/data/dlcloader.py index 4c713e8970..5ce2941b1e 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dlcloader.py +++ b/deeplabcut/pose_estimation_pytorch/data/dlcloader.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Class implementing the Loader for DeepLabCut projects""" +"""Class implementing the Loader for DeepLabCut projects.""" from __future__ import annotations @@ -30,7 +30,7 @@ class DLCLoader(Loader): - """A Loader for DeepLabCut projects""" + """A Loader for DeepLabCut projects.""" def __init__( self, @@ -174,7 +174,7 @@ def get_dataset_parameters(self) -> PoseDatasetParameters: ) def load_data(self, mode: str = "train") -> dict: - """Loads DeepLabCut data into COCO-style annotations + """Loads DeepLabCut data into COCO-style annotations. This function reads data from h5 file, split the data and returns it in COCO-like format @@ -267,7 +267,7 @@ def load_split( trainset_index: int = 0, shuffle: int = 0, ) -> dict[str, list[int]]: - """Loads the train/test split for a DeepLabCut shuffle + """Loads the train/test split for a DeepLabCut shuffle. Args: config: the DeepLabCut project config @@ -338,8 +338,7 @@ def split_data( dlc_df: pd.DataFrame, split: dict[str, list[int]], ) -> dict[str, pd.DataFrame | None]: - """ - Splits a DeepLabCut DataFrame into train/test dataframes + """Splits a DeepLabCut DataFrame into train/test dataframes. Args: dlc_df: the dataframe containing the labeled data @@ -364,7 +363,7 @@ def to_coco( df: pd.DataFrame, parameters: PoseDatasetParameters, ) -> dict: - """Formerly Shaokai's function + """Formerly Shaokai's function. Args: project_root: the path to the project root @@ -494,7 +493,7 @@ def _remove_nans(coco_dict: dict) -> dict: @property def _dfs(self) -> dict[str, pd.DataFrame]: - """Lazy-loading of the training dataset dataframes""" + """Lazy-loading of the training dataset dataframes.""" if self._loaded_df is None: self._loaded_df, image_sizes = self.load_ground_truth( self._project_config, @@ -511,7 +510,7 @@ def _load_mat_dataset( scorer: str, params: PoseDatasetParameters, ) -> tuple[set[tuple[int, int]], pd.DataFrame]: - """Loads the training dataset stored as a .mat file + """Loads the training dataset stored as a .mat file. Returns: images_sizes, dlc_dataset images_sizes: all possible images sizes in the dataset @@ -571,7 +570,7 @@ def _load_pickle_dataset( scorer: str, params: PoseDatasetParameters, ) -> tuple[set[tuple[int, int]], pd.DataFrame]: - """Loads the training dataset stored as a .mat file + """Loads the training dataset stored as a .mat file. Returns: images_sizes, dlc_dataset images_sizes: all possible images sizes in the dataset @@ -643,7 +642,7 @@ def _validate_dataframes( df_train: pd.DataFrame, strict: bool = False, ) -> dict[str, pd.DataFrame]: - """Validates the training/test DataFrames + """Validates the training/test DataFrames. Performs the following validation steps: 1. Checks that the training data loaded from CollectedData.h5 matches the @@ -733,7 +732,7 @@ def build_dlc_dataframe_columns( parameters: PoseDatasetParameters, with_likelihood: bool, ) -> pd.MultiIndex: - """Builds the columns for a DeepLabCut DataFrame + """Builds the columns for a DeepLabCut DataFrame. Args: scorer: the scorer name diff --git a/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py b/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py index 1026550d34..94fc527005 100644 --- a/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py +++ b/deeplabcut/pose_estimation_pytorch/data/generative_sampling.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""A file containing code to perform generative sampling of keypoints for CTD +"""A file containing code to perform generative sampling of keypoints for CTD. This code comes from PoseFix (see https://arxiv.org/pdf/1812.03595.pdf), and was then adapted for BUCTD (github.com/amathislab/BUCTD/blob/main/lib/dataset/pose_synthesis.py, @@ -90,7 +90,7 @@ def to_dict(self) -> dict: class GenerativeSampler: - """Performs generative sampling of keypoints for CTD model training""" + """Performs generative sampling of keypoints for CTD model training.""" def __init__( self, @@ -137,7 +137,7 @@ def __call__( area: float, image_size: tuple[int, int], ) -> np.ndarray: - """Samples keypoints + """Samples keypoints. PoseFix uses conditional keypoints (estimated by a bottom-up model) when ground truth keypoints are not available. For simplicity, we omit that. See diff --git a/deeplabcut/pose_estimation_pytorch/data/postprocessor.py b/deeplabcut/pose_estimation_pytorch/data/postprocessor.py index 117bddd259..e408b9b498 100644 --- a/deeplabcut/pose_estimation_pytorch/data/postprocessor.py +++ b/deeplabcut/pose_estimation_pytorch/data/postprocessor.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Post-process predictions made by models""" +"""Post-process predictions made by models.""" from __future__ import annotations @@ -30,8 +30,7 @@ class Postprocessor(ABC): @abstractmethod def __call__(self, predictions: Any, context: Context) -> Any: - """ - Post-processes the outputs of a model into a single prediction. + """Post-processes the outputs of a model into a single prediction. Args: predictions: the predictions made by the model on a single image @@ -134,7 +133,7 @@ def build_top_down_postprocessor( num_unique_bodyparts: int, with_backbone_features: bool = False, ) -> Postprocessor: - """Creates a postprocessor for top-down pose estimation + """Creates a postprocessor for top-down pose estimation. Args: max_individuals: the maximum number of individuals in a single image @@ -192,7 +191,7 @@ def build_detector_postprocessor( max_individuals: int, min_bbox_score: float | None = None, ) -> Postprocessor: - """Creates a postprocessor for top-down pose estimation + """Creates a postprocessor for top-down pose estimation. Args: max_individuals: the maximum number of detections to keep in a single image @@ -227,10 +226,8 @@ def build_detector_postprocessor( class ComposePostprocessor(Postprocessor): - """ - Class to preprocess an image and turn it into a batch of - inputs before running inference - """ + """Class to preprocess an image and turn it into a batch of inputs before running + inference.""" def __init__(self, components: list[Postprocessor]) -> None: self.components = components @@ -242,7 +239,7 @@ def __call__(self, predictions: Any, context: Context) -> tuple[Any, Context]: class ConcatenateOutputs(Postprocessor): - """Checks that there is a single prediction for the image and returns it""" + """Checks that there is a single prediction for the image and returns it.""" def __init__( self, @@ -275,7 +272,7 @@ def __call__(self, predictions: Any, context: Context) -> tuple[dict[str, np.nda class PadOutputs(Postprocessor): - """Pads the outputs to have the maximum number of individuals""" + """Pads the outputs to have the maximum number of individuals.""" def __init__( self, @@ -313,7 +310,7 @@ def __call__(self, predictions: dict[str, np.ndarray], context: Context) -> tupl class TrimOutputs(Postprocessor): - """Ensures all outputs have at most `max_individuals` detections + """Ensures all outputs have at most `max_individuals` detections. Assumes that the outputs are sorted by decreasing score, such that the first `max_individuals` predictions are the ones to keep. @@ -332,7 +329,7 @@ def __call__(self, predictions: dict[str, np.ndarray], context: Context) -> tupl class RescaleAndOffset(Postprocessor): - """Rescales and offsets predictions back to their position in the original image + """Rescales and offsets predictions back to their position in the original image. This can be done in 3 ways: BBOX_XYWH: the data has shape (num_individuals, 4), in xywh format, and there @@ -392,7 +389,7 @@ def __call__(self, predictions: dict[str, np.ndarray], context: Context) -> tupl rescaled = outputs else: rescaled_individuals = [] - for output, scale, offset in zip(outputs, scales, offsets): + for output, scale, offset in zip(outputs, scales, offsets, strict=False): output_rescaled = output.copy() output_rescaled[:, :2] = output[:, :2] * scale + offset rescaled_individuals.append(output_rescaled) @@ -421,9 +418,8 @@ def __call__(self, predictions: dict[str, np.ndarray], context: Context) -> tupl class RemoveLowConfidenceBoxes(Postprocessor): - """ - Removes low confidence bounding boxes from detector output before they reach the pose estimator - """ + """Removes low confidence bounding boxes from detector output before they reach the + pose estimator.""" def __init__(self, bbox_score_thresh: float): super().__init__() @@ -455,10 +451,8 @@ def __call__(self, predictions: dict[str, np.ndarray], context: Context) -> tupl class AddContextToOutput(Postprocessor): - """ - Adds items from the context to the output, such as the bounding boxes contained - during top-down inference. - """ + """Adds items from the context to the output, such as the bounding boxes contained + during top-down inference.""" def __init__(self, keys: list[str]) -> None: super().__init__() @@ -476,7 +470,7 @@ def __call__( class PredictKeypointIdentities(Postprocessor): - """Assigns predicted identities to keypoints + """Assigns predicted identities to keypoints. The identity maps have shape (h, w, num_ids). @@ -515,7 +509,7 @@ def __call__(self, predictions: dict[str, np.ndarray], context: Context) -> tupl ys = np.clip(heatmap_indices[:, 1], 0, h - 1) # get the score from each identity heatmap at each predicted keypoint - for kpt_idx, (x, y) in enumerate(zip(xs, ys)): + for kpt_idx, (x, y) in enumerate(zip(xs, ys, strict=False)): id_score_matrix[pred_idx, kpt_idx] = identity_heatmap[y, x, :] predictions[self.identity_key] = id_score_matrix @@ -528,7 +522,7 @@ def __call__(self, predictions: dict[str, np.ndarray], context: Context) -> tupl class AssignIndividualIdentities(Postprocessor): - """Assigns predicted identities to individuals + """Assigns predicted identities to individuals. Attributes: identity_key: Key with which to add predicted identities in the predictions dict @@ -547,7 +541,7 @@ def __call__(self, predictions: dict[str, np.ndarray], context: Context) -> tupl class PrepareBackboneFeatures(Postprocessor): - """Adds backbone features for each individual and keypoint to the outputs + """Adds backbone features for each individual and keypoint to the outputs. Attributes: top_down: Whether the model is a top-down model. diff --git a/deeplabcut/pose_estimation_pytorch/data/preprocessor.py b/deeplabcut/pose_estimation_pytorch/data/preprocessor.py index 0c53e0a9c0..d3c6cd35b4 100644 --- a/deeplabcut/pose_estimation_pytorch/data/preprocessor.py +++ b/deeplabcut/pose_estimation_pytorch/data/preprocessor.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Helpers to run preprocess data before running inference""" +"""Helpers to run preprocess data before running inference.""" from __future__ import annotations @@ -29,18 +29,17 @@ class Preprocessor(ABC): - """ - Class to preprocess an image and turn it into a batch of inputs before running + """Class to preprocess an image and turn it into a batch of inputs before running inference. - As an example, a pre-processor can load an image, use a "bboxes" key from context - to crop bounding boxes for individuals (going from a (h, w, 3) array to a + As an example, a pre-processor can load an image, use a "bboxes" key from context to + crop bounding boxes for individuals (going from a (h, w, 3) array to a (num_individuals, h, w, 3) array), and convert it into a tensor ready for inference. """ @abstractmethod def __call__(self, image: Image, context: Context) -> tuple[Image, Context]: - """Pre-processes an image + """Pre-processes an image. Args: image: an image (containing height, width and channel dimensions) or a @@ -86,7 +85,7 @@ def build_top_down_preprocessor( top_down_crop_margin: int = 0, top_down_crop_with_context: bool = True, ) -> Preprocessor: - """Creates a preprocessor for top-down pose estimation + """Creates a preprocessor for top-down pose estimation. Creates a preprocessor that loads an image, crops all bounding boxes given as a context (through a "bboxes" key), runs some transforms on each cropped image (such @@ -125,7 +124,7 @@ def build_conditional_top_down_preprocessor( top_down_crop_margin: int = 0, top_down_crop_with_context: bool = False, ) -> Preprocessor: - """Creates a preprocessor for conditional top-down pose estimation + """Creates a preprocessor for conditional top-down pose estimation. Creates a preprocessor that loads an image, computes bounding boxes from conditional keypoints (given as a context (through a "cond_kpts" key), crops all bounding boxes, @@ -163,10 +162,8 @@ def build_conditional_top_down_preprocessor( class ComposePreprocessor(Preprocessor): - """ - Class to preprocess an image and turn it into a batch of - inputs before running inference - """ + """Class to preprocess an image and turn it into a batch of inputs before running + inference.""" def __init__(self, components: list[Preprocessor]) -> None: self.components = components @@ -178,7 +175,7 @@ def __call__(self, image: Image, context: Context) -> tuple[Image, Context]: class LoadImage(Preprocessor): - """Loads an image from a file, if not yet loaded""" + """Loads an image from a file, if not yet loaded.""" def __init__(self, color_mode: str = "RGB") -> None: self.color_mode = color_mode @@ -243,8 +240,8 @@ def update_scale(scale: tuple[float, float], new_scale: tuple[float, float]) -> @staticmethod def update_offsets_and_scales(context, new_offsets, new_scales) -> tuple: - """ - x = x' * scale' + offset' + """X = x' * scale' + offset'. + x' = x'' * scale'' + offset'' -> x = x'' * (scale' * scale'') + (scale' * offset'' + offset') """ @@ -270,14 +267,16 @@ def update_offsets_and_scales(context, new_offsets, new_scales) -> tuple: updated_offsets = [ AugmentImage.update_offset(offset, scale, new_offset) - for offset, scale, new_offset in zip(offsets, scales, new_offsets) + for offset, scale, new_offset in zip(offsets, scales, new_offsets, strict=False) ] updated_scales = [ - AugmentImage.update_scale(scale, new_scale) for scale, new_scale in zip(scales, new_scales) + AugmentImage.update_scale(scale, new_scale) + for scale, new_scale in zip(scales, new_scales, strict=False) ] else: updated_offsets = [ - AugmentImage.update_offset(offset, scale, new_offsets) for offset, scale in zip(offsets, scales) + AugmentImage.update_offset(offset, scale, new_offsets) + for offset, scale in zip(offsets, scales, strict=False) ] updated_scales = [AugmentImage.update_scale(scale, new_scales) for scale in scales] return updated_offsets, updated_scales @@ -325,7 +324,7 @@ def __call__(self, image: Image, context: Context) -> tuple[np.ndarray, Context] class ToTensor(Preprocessor): - """Transforms lists and numpy arrays into tensors""" + """Transforms lists and numpy arrays into tensors.""" def __call__(self, image: Image, context: Context) -> tuple[np.ndarray, Context]: image = torch.tensor(image, dtype=torch.float) @@ -339,9 +338,9 @@ def __call__(self, image: Image, context: Context) -> tuple[np.ndarray, Context] class ToBatch(Preprocessor): """Adds a batch dimension to the image tensor. - This preprocessor is used to convert a single image tensor into a batched format - by unsqueezing along the 0th dimension. This is typically required before passing - the image to models that expect batched input (i.e., shape `[B, C, H, W]`). + This preprocessor is used to convert a single image tensor into a batched format by + unsqueezing along the 0th dimension. This is typically required before passing the + image to models that expect batched input (i.e., shape `[B, C, H, W]`). """ def __call__(self, image: Image, context: Context) -> tuple[np.ndarray, Context]: @@ -349,8 +348,8 @@ def __call__(self, image: Image, context: Context) -> tuple[np.ndarray, Context] class FilterLowConfidencePoses(Preprocessor): - """ - Filters out poses with low confidence scores. + """Filters out poses with low confidence scores. + By default, the confidence associated to the pose is the max confidence value. """ @@ -406,7 +405,7 @@ def __call__(self, image: np.ndarray, context: Context) -> tuple[np.ndarray, Con class TopDownCrop(Preprocessor): - """Crops bounding boxes out of images for top-down pose estimation + """Crops bounding boxes out of images for top-down pose estimation. Args: output_size: The (width, height) of crops to output @@ -461,7 +460,7 @@ def __call__(self, image: np.ndarray, context: Context) -> tuple[np.ndarray, Con class ComputeBoundingBoxesFromCondKeypoints(Preprocessor): - """Generates bounding boxes from predicted keypoints + """Generates bounding boxes from predicted keypoints. Args: cond_kpt_key: The key under which cond. keypoints are stored in the context. diff --git a/deeplabcut/pose_estimation_pytorch/data/snapshots.py b/deeplabcut/pose_estimation_pytorch/data/snapshots.py index a4a6a7646a..d2f2ed6a47 100644 --- a/deeplabcut/pose_estimation_pytorch/data/snapshots.py +++ b/deeplabcut/pose_estimation_pytorch/data/snapshots.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Code to handle storing models""" +"""Code to handle storing models.""" from __future__ import annotations @@ -19,7 +19,7 @@ @dataclass(frozen=True) class Snapshot: - """A snapshot for a model""" + """A snapshot for a model.""" best: bool epochs: int | None diff --git a/deeplabcut/pose_estimation_pytorch/data/transforms.py b/deeplabcut/pose_estimation_pytorch/data/transforms.py index d3d7fb745e..ad030cd768 100644 --- a/deeplabcut/pose_estimation_pytorch/data/transforms.py +++ b/deeplabcut/pose_estimation_pytorch/data/transforms.py @@ -49,7 +49,10 @@ def build_transforms(augmentations: dict) -> A.BaseCompose: if symmetries is not None: transforms.append(HFlip(symmetries=symmetries, p=hflip_proba)) else: - warnings.warn("Be careful! Do not train pose models with horizontal flips if you have symmetric keypoints!") + warnings.warn( + "Be careful! Do not train pose models with horizontal flips if you have symmetric keypoints!", + stacklevel=2, + ) transforms.append(A.HorizontalFlip(p=hflip_proba)) if (affine := augmentations.get("affine")) is not None: @@ -159,8 +162,7 @@ def build_auto_padding( border_value: float | None = None, border_mask_value: float | None = None, ) -> A.PadIfNeeded: - """ - Create an albumentations PadIfNeeded transform from a config + """Create an albumentations PadIfNeeded transform from a config. Args: min_height: the minimum height of the image @@ -221,7 +223,7 @@ def build_resize_transforms(resize_cfg: dict) -> list[A.BasicTransform]: class HFlip(A.HorizontalFlip): - """Horizontal Flip which swaps symmetric keypoints""" + """Horizontal Flip which swaps symmetric keypoints.""" def __init__(self, symmetries: list[tuple[int, int]], *args, **kwargs) -> None: super().__init__(*args, **kwargs) @@ -236,7 +238,7 @@ def apply_to_keypoints(self, keypoints, **params): class KeypointAwareCrop(A.RandomCrop): - """Random crop for an image around keypoints + """Random crop for an image around keypoints. Args: width: Crop images down to this maximum width. @@ -333,15 +335,15 @@ def get_transform_init_args_names(self) -> tuple[str, ...]: class KeepAspectRatioResize(A.DualTransform): - """Resizes images while preserving their aspect ratio + """Resizes images while preserving their aspect ratio. - In 'pad' mode, the image will be rescaled to the largest possible size such that - it can be padded to the correct size (with PadIfNeeded). So we'll have: - output_width <= width, output_height <= height + In 'pad' mode, the image will be rescaled to the largest possible size such that it + can be padded to the correct size (with PadIfNeeded). So we'll have: output_width <= + width, output_height <= height In 'crop' mode, the image will be rescaled to the smallest possible size such that it can be cropped to the correct size (with any random crop you want), so: - output_width >= width, output_height >= height + output_width >= width, output_height >= height """ def __init__( @@ -419,7 +421,7 @@ def __init__( @staticmethod def _validate_alpha(val: float) -> float: if not 0.0 <= val <= 1.0: - warnings.warn("`alpha` will be clipped to the interval [0.0, 1.0].") + warnings.warn("`alpha` will be clipped to the interval [0.0, 1.0].", stacklevel=2) return min(1.0, max(0.0, val)) @property @@ -495,7 +497,7 @@ def apply_to_keypoints(self, keypoints: Sequence[float], random_state: int | Non sum_indices = np.sum(inds[:, None] * mask[None], axis=(2, 3)).T xy = sum_indices / div[:, None] new_keypoints = [] - for kp, new_coords in zip(keypoints, xy): + for kp, new_coords in zip(keypoints, xy, strict=False): kp = list(kp) kp[:2] = new_coords new_keypoints.append(tuple(kp)) @@ -553,7 +555,7 @@ def apply_to_keypoints( return new_keypoints def _keypoint_in_hole(self, keypoint, hole: tuple[int, int, int, int]) -> bool: - """Reimplemented from Albumentations as was removed in v1.4.0""" + """Reimplemented from Albumentations as was removed in v1.4.0.""" x1, y1, x2, y2 = hole x, y = keypoint[:2] return x1 <= x < x2 and y1 <= y < y2 @@ -638,7 +640,7 @@ def apply_to_bboxes(self, bboxes, **params): # add the extra information back; tuples for albumentations<=1.4.3 bboxes_out = [tuple(bbox) for bbox in bbox_xyxy] if bboxes_extra is not None: - bboxes_out = [bbox + extra for bbox, extra in zip(bboxes_out, bboxes_extra)] + bboxes_out = [bbox + extra for bbox, extra in zip(bboxes_out, bboxes_extra, strict=False)] return bboxes_out def get_transform_init_args_names(self): diff --git a/deeplabcut/pose_estimation_pytorch/data/utils.py b/deeplabcut/pose_estimation_pytorch/data/utils.py index 5bdc91bb9c..d4f6797a20 100644 --- a/deeplabcut/pose_estimation_pytorch/data/utils.py +++ b/deeplabcut/pose_estimation_pytorch/data/utils.py @@ -22,7 +22,7 @@ @cache def read_image_shape_fast(path: str | Path) -> tuple[int, int, int]: - """Blazing fast and does not load the image into memory""" + """Blazing fast and does not load the image into memory.""" with Image.open(path) as img: width, height = img.size return len(img.getbands()), height, width @@ -34,8 +34,7 @@ def bbox_from_keypoints( image_w: int, margin: int, ) -> np.ndarray: - """ - Computes bounding boxes from keypoints. + """Computes bounding boxes from keypoints. Args: keypoints: (..., num_keypoints, xy) the keypoints from which to get bboxes @@ -79,8 +78,7 @@ def bbox_from_keypoints( def merge_list_of_dicts(list_of_dicts: list[dict], keys_to_include: list[str]) -> dict[str, list]: - """ - Flattens a list of dictionaries into a dictionary with the lists concatenated. + """Flattens a list of dictionaries into a dictionary with the lists concatenated. Args: list_of_dicts: the dictionaries to merge @@ -104,8 +102,7 @@ def merge_list_of_dicts(list_of_dicts: list[dict], keys_to_include: list[str]) - def map_image_path_to_id(images: list[dict]) -> dict[str, int]: - """ - Binds the image paths to their respective IDs. + """Binds the image paths to their respective IDs. Args: images: List of dictionaries containing image data in COCO-like format. @@ -122,8 +119,7 @@ def map_image_path_to_id(images: list[dict]) -> dict[str, int]: def map_id_to_annotations(annotations: list[dict]) -> dict[int, list[int]]: - """ - Maps image IDs to their corresponding annotation indices. + """Maps image IDs to their corresponding annotation indices. Args: annotations: List of dictionaries containing annotation data. Each dictionary @@ -148,8 +144,7 @@ def _crop_and_pad_image( coords: tuple[tuple[int, int], tuple[int, int]], output_size: tuple[int, int], ) -> tuple[np.ndarray, tuple[int, int]]: - """ - Crop the image using the given coordinates and pad the larger dimension to change + """Crop the image using the given coordinates and pad the larger dimension to change the aspect ratio. Args: @@ -191,8 +186,7 @@ def _crop_and_pad_image( def _crop_and_pad_keypoints(keypoints: np.ndarray, coords: tuple[int, int], pad_size: tuple[int, int]): - """ - Adjust the keypoints after cropping and padding. + """Adjust the keypoints after cropping and padding. Parameters: keypoints: The original keypoints, typically a 2D array of shape (..., 2). @@ -255,8 +249,7 @@ def _compute_crop_bounds( image_shape: tuple[int, int, int], remove_empty: bool = True, ) -> np.ndarray: - """ - Compute the boundaries for cropping an image based on a COCO-format bounding box + """Compute the boundaries for cropping an image based on a COCO-format bounding box and image shape by clipping values so the bounding boxes are entirely in the image. Args: @@ -302,7 +295,7 @@ def _extract_keypoints_and_bboxes( anns_to_merge = [] unique_keypoints = None h, w = image_shape[:2] - for i, annotation in enumerate(anns): + for _i, annotation in enumerate(anns): keypoints_individual = _annotation_to_keypoints(annotation, h, w) if annotation["individual"] != "single": bbox_individual = annotation["bbox"] @@ -337,8 +330,7 @@ def _extract_keypoints_and_bboxes( def calc_area_from_keypoints(keypoints: np.ndarray) -> np.ndarray: - """ - Calculate the area from keypoints + """Calculate the area from keypoints. TODO: in the pups benchmark, there are 5 keypoints perfectly aligned so the area is 0. @@ -359,8 +351,7 @@ def calc_area_from_keypoints(keypoints: np.ndarray) -> np.ndarray: def calc_bbox_overlap(bbox1: np.ndarray, bbox2: np.ndarray) -> np.ndarray: - """ - Calculate the overlap between two bounding boxes + """Calculate the overlap between two bounding boxes. Args: bbox1: the first bounding box in the format (x, y, w, h) @@ -387,8 +378,7 @@ def calc_bbox_overlap(bbox1: np.ndarray, bbox2: np.ndarray) -> np.ndarray: def _annotation_to_keypoints(annotation: dict, h: int, w: int) -> np.array: - """ - Convert the coco annotations into array of keypoints returns the array of the + """Convert the coco annotations into array of keypoints returns the array of the keypoints' visibility. If keypoint is not visible, the value for (x,y) coordinates is set to 0. If the keypoints are outside of the image, they are also set to 0. @@ -400,7 +390,6 @@ def _annotation_to_keypoints(annotation: dict, h: int, w: int) -> np.array: Returns: keypoints: np.array where the first two columns are x and y coordinates of the - """ # we don't mess up visibility flags here return annotation["keypoints"].reshape(-1, 3) @@ -413,8 +402,7 @@ def apply_transform( bboxes: np.ndarray, class_labels: list[str], ) -> dict[str, np.ndarray]: - """ - Applies a transformation to the provided image and keypoints. + """Applies a transformation to the provided image and keypoints. Args: transform: The transformation to apply. @@ -467,8 +455,7 @@ def _apply_transform( bboxes: np.ndarray, class_labels: list[str], ) -> dict[str, np.ndarray]: - """ - Applies a transformation to the provided image and keypoints. + """Applies a transformation to the provided image and keypoints. Args: image : np.array or similar image data format @@ -492,7 +479,7 @@ def _apply_transform( ) bboxes_out = np.zeros(bboxes.shape) - for bbox, bbox_id in zip(transformed["bboxes"], transformed["bbox_labels"]): + for bbox, bbox_id in zip(transformed["bboxes"], transformed["bbox_labels"], strict=False): bboxes_out[bbox_id] = bbox transformed["bboxes"] = bboxes_out @@ -500,7 +487,7 @@ def _apply_transform( def out_of_bounds_keypoints(keypoints: np.ndarray, shape: tuple) -> np.ndarray: - """Computes which visible keypoints are outside an image + """Computes which visible keypoints are outside an image. Args: keypoints: A (N, 3) shaped array where N is the number of keypoints and each @@ -524,8 +511,7 @@ def out_of_bounds_keypoints(keypoints: np.ndarray, shape: tuple) -> np.ndarray: def pad_to_length(data: np.array, length: int, value: float) -> np.array: - """ - Pads the first dimension of an array with a given value + """Pads the first dimension of an array with a given value. Args: data: the array to pad, of shape (l, ...), where l <= length @@ -546,9 +532,8 @@ def pad_to_length(data: np.array, length: int, value: float) -> np.array: def safe_stack(data: list[np.ndarray], default_shape: tuple[int, ...]) -> np.ndarray: - """ - Stacks a list of arrays if there are any, otherwise returns an array of zeros - of a desired shape. + """Stacks a list of arrays if there are any, otherwise returns an array of zeros of + a desired shape. Args: data: the list of arrays to stack diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/base.py b/deeplabcut/pose_estimation_pytorch/models/backbones/base.py index 59c0827730..dedac0f709 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/base.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/base.py @@ -57,7 +57,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: pass def freeze_batch_norm_layers(self) -> None: - """Freezes batch norm layers + """Freezes batch norm layers. Running mean + var are always given to F.batch_norm, except when the layer is in `train` mode and track_running_stats is False, see @@ -85,7 +85,7 @@ def train(self, mode: bool = True) -> None: class HuggingFaceWeightsMixin: - """Mixin for backbones where the pretrained weights are stored on HuggingFace""" + """Mixin for backbones where the pretrained weights are stored on HuggingFace.""" def __init__( self, @@ -104,7 +104,7 @@ def __init__( self.repo_id = repo_id def download_weights(self, filename: str, force: bool = False) -> Path: - """Downloads the backbone weights from the HuggingFace repo + """Downloads the backbone weights from the HuggingFace repo. Args: filename: The name of the model file to download in the repo. diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py b/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py index 6f9ec2fe19..15f8f5307f 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/cond_prenet.py @@ -24,9 +24,10 @@ @BACKBONES.register_module class CondPreNet(BaseBackbone): - """ - Wrapper module that adds a conditional preNet before any backbone. - This allows to process image and condition features and prepare them for the main backbone. + """Wrapper module that adds a conditional preNet before any backbone. + + This allows to process image and condition features and prepare them for the main + backbone. """ def __init__( @@ -36,8 +37,7 @@ def __init__( img_size: tuple[int, int] = (256, 256), **kwargs, ): - """ - Initialize the PreNetWrapper. + """Initialize the PreNetWrapper. Args: backbone: The backbone model to wrap. diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py index 24bcca4691..8c9d274d7f 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/hrnet_coam.py @@ -186,6 +186,6 @@ def forward(self, x: torch.Tensor, cond_kpts: np.ndarray): if self.model.incre_modules is not None: raise NotImplementedError("Incremental HRNet modules not supported for HRNetCoAM") - x = [incre(f) for f, incre in zip(x, self.model.incre_modules)] + x = [incre(f) for f, incre in zip(x, self.model.incre_modules, strict=False)] return self.prepare_output(y) diff --git a/deeplabcut/pose_estimation_pytorch/models/criterions/dekr.py b/deeplabcut/pose_estimation_pytorch/models/criterions/dekr.py index 6e0343d813..e05e19f6af 100644 --- a/deeplabcut/pose_estimation_pytorch/models/criterions/dekr.py +++ b/deeplabcut/pose_estimation_pytorch/models/criterions/dekr.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Loss criterions for DEKR models""" +"""Loss criterions for DEKR models.""" from __future__ import annotations @@ -22,7 +22,7 @@ @CRITERIONS.register_module class DEKRHeatmapLoss(BaseCriterion): - """DEKR Heatmap loss""" + """DEKR Heatmap loss.""" def forward( self, @@ -47,7 +47,7 @@ def forward( @CRITERIONS.register_module class DEKROffsetLoss(BaseCriterion): - """DEKR Offset loss""" + """DEKR Offset loss.""" def __init__(self, beta: float = 1 / 9): super().__init__() diff --git a/deeplabcut/pose_estimation_pytorch/models/criterions/kl_discrete.py b/deeplabcut/pose_estimation_pytorch/models/criterions/kl_discrete.py index 44b5cb648a..677c4263d5 100644 --- a/deeplabcut/pose_estimation_pytorch/models/criterions/kl_discrete.py +++ b/deeplabcut/pose_estimation_pytorch/models/criterions/kl_discrete.py @@ -26,7 +26,7 @@ @CRITERIONS.register_module class KLDiscreteLoss(BaseCriterion): - """KLDiscrete loss + """KLDiscrete loss. Args: beta: Temperature for the softmax. diff --git a/deeplabcut/pose_estimation_pytorch/models/criterions/weighted.py b/deeplabcut/pose_estimation_pytorch/models/criterions/weighted.py index 5576e79d49..d2fcab46b6 100644 --- a/deeplabcut/pose_estimation_pytorch/models/criterions/weighted.py +++ b/deeplabcut/pose_estimation_pytorch/models/criterions/weighted.py @@ -21,7 +21,7 @@ class WeightedCriterion(BaseCriterion): - """Base class for weighted criterions""" + """Base class for weighted criterions.""" def __init__(self, criterion: nn.Module): super().__init__() @@ -55,13 +55,12 @@ def forward( @CRITERIONS.register_module class WeightedMSECriterion(WeightedCriterion): - """ - Weighted Mean Squared Error (MSE) Loss. + """Weighted Mean Squared Error (MSE) Loss. This loss computes the Mean Squared Error between the prediction and target tensors, - but it also incorporates weights to adjust the contribution of each element in the loss - calculation. The loss is computed element-wise, and elements with a weight of 0 (masked items) - are excluded from the loss calculation. + but it also incorporates weights to adjust the contribution of each element in the + loss calculation. The loss is computed element-wise, and elements with a weight of 0 + (masked items) are excluded from the loss calculation. """ def __init__(self) -> None: @@ -95,11 +94,10 @@ def forward( @CRITERIONS.register_module class WeightedHuberCriterion(WeightedCriterion): - """ - Weighted Huber Loss. + """Weighted Huber Loss. - This loss computes the Huber loss between the prediction and target tensors, - but it also incorporates weights to adjust the contribution of each element in the loss + This loss computes the Huber loss between the prediction and target tensors, but it + also incorporates weights to adjust the contribution of each element in the loss calculation. The loss is computed element-wise, and elements with a weight of 0 are excluded from the loss calculation. """ @@ -110,13 +108,12 @@ def __init__(self) -> None: @CRITERIONS.register_module class WeightedBCECriterion(WeightedCriterion): - """ - Weighted Binary Cross Entropy (BCE) Loss. + """Weighted Binary Cross Entropy (BCE) Loss. - This loss computes the Binary Cross Entropy loss between the prediction and target tensors, - but it also incorporates weights to adjust the contribution of each element in the loss - calculation. The loss is computed element-wise, and elements with a weight of 0 are - excluded from the loss calculation. + This loss computes the Binary Cross Entropy loss between the prediction and target + tensors, but it also incorporates weights to adjust the contribution of each element + in the loss calculation. The loss is computed element-wise, and elements with a + weight of 0 are excluded from the loss calculation. """ def __init__(self) -> None: diff --git a/deeplabcut/pose_estimation_pytorch/models/detectors/base.py b/deeplabcut/pose_estimation_pytorch/models/detectors/base.py index bf7157514e..9660b81881 100644 --- a/deeplabcut/pose_estimation_pytorch/models/detectors/base.py +++ b/deeplabcut/pose_estimation_pytorch/models/detectors/base.py @@ -26,7 +26,7 @@ def _build_detector( pretrained: bool = False, **kwargs, ) -> BaseDetector: - """Builds a detector using its configuration file + """Builds a detector using its configuration file. Args: cfg: The detector configuration. @@ -52,8 +52,8 @@ def _build_detector( class BaseDetector(ABC, nn.Module): - """ - Definition of the class BaseDetector object. + """Definition of the class BaseDetector object. + This is an abstract class defining the common structure and inference for detectors. """ @@ -72,8 +72,7 @@ def __init__( def forward( self, x: torch.Tensor, targets: list[dict[str, torch.Tensor]] | None = None ) -> tuple[dict[str, torch.Tensor], list[dict[str, torch.Tensor]]]: - """ - Forward pass of the detector + """Forward pass of the detector. Args: x: images to be processed @@ -87,8 +86,7 @@ def forward( @abstractmethod def get_target(self, labels: dict) -> list[dict]: - """ - Get the target for training the detector + """Get the target for training the detector. Args: labels: annotations containing keypoints, bounding boxes, etc. @@ -99,7 +97,7 @@ def get_target(self, labels: dict) -> list[dict]: pass def freeze_batch_norm_layers(self) -> None: - """Freezes batch norm layers + """Freezes batch norm layers. Running mean + var are always given to F.batch_norm, except when the layer is in `train` mode and track_running_stats is False, see diff --git a/deeplabcut/pose_estimation_pytorch/models/heads/base.py b/deeplabcut/pose_estimation_pytorch/models/heads/base.py index 612f7fceaf..c494eab916 100644 --- a/deeplabcut/pose_estimation_pytorch/models/heads/base.py +++ b/deeplabcut/pose_estimation_pytorch/models/heads/base.py @@ -31,7 +31,7 @@ class BaseHead(ABC, nn.Module): - """A head for pose estimation models + """A head for pose estimation models. Attributes: stride: The stride for the head (or neck + head pair), where positive values @@ -88,8 +88,7 @@ def __init__( @abstractmethod def forward(self, x: torch.Tensor) -> dict[str, torch.Tensor]: - """ - Given the feature maps for an image () + """Given the feature maps for an image () Args: x: the feature maps, of shape (b, c, h, w) @@ -104,8 +103,7 @@ def get_loss( outputs: dict[str, torch.Tensor], targets: dict[str, dict[str, torch.Tensor]], ) -> dict[str, torch.Tensor]: - """ - Computes the loss for this head + """Computes the loss for this head. Args: outputs: the outputs of this head @@ -126,7 +124,7 @@ def get_loss( return losses def _init_weights(self) -> None: - """Should be called once all modules for the class are created""" + """Should be called once all modules for the class are created.""" if self.weight_init is not None: self.weight_init.init_weights(self) @@ -149,7 +147,7 @@ def convert_weights( module_prefix: str, conversion: torch.Tensor, ) -> dict[str, torch.Tensor]: - """Converts pre-trained weights to be fine-tuned on another dataset + """Converts pre-trained weights to be fine-tuned on another dataset. Args: state_dict: the state dict for the pre-trained model diff --git a/deeplabcut/pose_estimation_pytorch/models/heads/rtmcc_head.py b/deeplabcut/pose_estimation_pytorch/models/heads/rtmcc_head.py index cb195fd10d..7c56f4c6e5 100644 --- a/deeplabcut/pose_estimation_pytorch/models/heads/rtmcc_head.py +++ b/deeplabcut/pose_estimation_pytorch/models/heads/rtmcc_head.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Modified SimCC head for the RTMPose model +"""Modified SimCC head for the RTMPose model. Based on the official ``mmpose`` RTMCC head implementation. For more information, see . @@ -38,7 +38,7 @@ @HEADS.register_module class RTMCCHead(BaseHead): - """RTMPose Coordinate Classification head + """RTMPose Coordinate Classification head. The RTMCC head is itself adapted from the SimCC head. For more information, see "SimCC: a Simple Coordinate Classification Perspective for Human Pose Estimation" @@ -138,7 +138,7 @@ def forward(self, x: torch.Tensor) -> dict[str, torch.Tensor]: @staticmethod def update_input_size(model_cfg: dict, input_size: tuple[int, int]) -> None: - """Updates an RTMPose model configuration file for a new image input size + """Updates an RTMPose model configuration file for a new image input size. Args: model_cfg: The model configuration to update in-place. diff --git a/deeplabcut/pose_estimation_pytorch/models/heads/simple_head.py b/deeplabcut/pose_estimation_pytorch/models/heads/simple_head.py index 09ebaa16de..98ffffc2b6 100644 --- a/deeplabcut/pose_estimation_pytorch/models/heads/simple_head.py +++ b/deeplabcut/pose_estimation_pytorch/models/heads/simple_head.py @@ -101,7 +101,7 @@ def convert_weights( module_prefix: str, conversion: torch.Tensor, ) -> dict[str, torch.Tensor]: - """Converts pre-trained weights to be fine-tuned on another dataset + """Converts pre-trained weights to be fine-tuned on another dataset. Args: state_dict: the state dict for the pre-trained model @@ -127,9 +127,7 @@ def convert_weights( class DeconvModule(nn.Module): - """ - Deconvolutional module to predict maps from the extracted features. - """ + """Deconvolutional module to predict maps from the extracted features.""" def __init__( self, @@ -180,8 +178,7 @@ def _make_layers( kernel_sizes: list[int], strides: list[int], ) -> list[nn.Module]: - """ - Helper function to create the deconvolutional layers. + """Helper function to create the deconvolutional layers. Args: in_channels: number of input channels to the module @@ -193,15 +190,14 @@ def _make_layers( the deconvolutional layers """ layers = [] - for out_channels, k, s in zip(out_channels, kernel_sizes, strides): + for out_channels, k, s in zip(out_channels, kernel_sizes, strides, strict=False): layers.append(nn.ConvTranspose2d(in_channels, out_channels, kernel_size=k, stride=s)) layers.append(nn.ReLU()) in_channels = out_channels return layers[:-1] def forward(self, x: torch.Tensor) -> torch.Tensor: - """ - Forward pass of the HeatmapHead + """Forward pass of the HeatmapHead. Args: x: input tensor @@ -219,7 +215,7 @@ def convert_weights( module_prefix: str, conversion: torch.Tensor, ) -> dict[str, torch.Tensor]: - """Converts pre-trained weights to be fine-tuned on another dataset + """Converts pre-trained weights to be fine-tuned on another dataset. Args: state_dict: the state dict for the pre-trained model diff --git a/deeplabcut/pose_estimation_pytorch/models/heads/transformer.py b/deeplabcut/pose_estimation_pytorch/models/heads/transformer.py index 4cf0276651..2ff37f23f8 100644 --- a/deeplabcut/pose_estimation_pytorch/models/heads/transformer.py +++ b/deeplabcut/pose_estimation_pytorch/models/heads/transformer.py @@ -23,9 +23,8 @@ @HEADS.register_module class TransformerHead(BaseHead): - """ - Transformer Head module to predict heatmaps using a transformer-based approach - """ + """Transformer Head module to predict heatmaps using a transformer-based + approach.""" def __init__( self, @@ -86,8 +85,7 @@ def forward(self, x: torch.Tensor) -> dict[str, torch.Tensor]: return {"heatmap": x} def _init_weights(self, m: nn.Module) -> None: - """ - Custom weight initialization for linear and layer normalization layers. + """Custom weight initialization for linear and layer normalization layers. Args: m: module to initialize diff --git a/deeplabcut/pose_estimation_pytorch/models/model.py b/deeplabcut/pose_estimation_pytorch/models/model.py index c5f7decf8e..a0b8dee13d 100644 --- a/deeplabcut/pose_estimation_pytorch/models/model.py +++ b/deeplabcut/pose_estimation_pytorch/models/model.py @@ -31,7 +31,7 @@ class PoseModel(nn.Module): - """A pose estimation model + """A pose estimation model. A pose estimation model is composed of a backbone, optionally a neck, and an arbitrary number of heads. Outputs are computed as follows: @@ -61,8 +61,7 @@ def __init__( self._strides = {name: _model_stride(self.backbone.stride, head.stride) for name, head in heads.items()} def forward(self, x: torch.Tensor, **backbone_kwargs) -> dict[str, dict[str, torch.Tensor]]: - """ - Forward pass of the PoseModel. + """Forward pass of the PoseModel. Args: x: input images @@ -230,8 +229,7 @@ def build( def filter_state_dict(state_dict: dict, module: str) -> dict[str, torch.Tensor]: - """ - Filters keys in the state dict for a module to only keep a given prefix. Removes + """Filters keys in the state dict for a module to only keep a given prefix. Removes the module from the keys (e.g. for module="backbone", "backbone.stage1.weight" will be converted to "stage1.weight" so the state dict can be loaded into the backbone directly). @@ -257,7 +255,7 @@ def filter_state_dict(state_dict: dict, module: str) -> dict[str, torch.Tensor]: def _model_stride(backbone_stride: int | float, head_stride: int | float) -> float: - """Computes the model stride from a backbone and a head""" + """Computes the model stride from a backbone and a head.""" if head_stride > 0: return backbone_stride / head_stride diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py b/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py index dfc522ab80..d1599d00e1 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/coam_module.py @@ -16,9 +16,7 @@ class CoAMBlock(nn.Module): - """ - Conditional Attention Module (CoAM) block. - """ + """Conditional Attention Module (CoAM) block.""" def __init__(self, spat_dims, channel_list, cond_enc, n_heads=1, channel_only=False): super().__init__() @@ -208,9 +206,7 @@ def forward(self, y_list, *args): # taken from: https://github.com/xmu-xiaoma666/External-Attention-pytorch/blob/master/model/attention/SelfAttention.py class ScaledDotProductAttention(nn.Module): - """ - Scaled dot-product attention - """ + """Scaled dot-product attention.""" def __init__(self, in_dim_q, in_dim_k, d_k, d_v, h, dropout=0.1, rev=False): """ @@ -254,13 +250,13 @@ def init_weights(self): init.constant_(m.bias, 0) def forward(self, queries, keys, values, attention_mask=None, attention_weights=None): - """ - Computes - :param queries: Queries (b_s, nq, d_model) - :param keys: Keys (b_s, nk, d_model) - :param values: Values (b_s, nk, d_model) - :param attention_mask: Mask over attention values (b_s, h, nq, nk). True indicates masking. - :param attention_weights: Multiplicative weights for attention values (b_s, h, nq, nk). + """Computes :param queries: Queries (b_s, nq, d_model) :param keys: Keys (b_s, + nk, d_model) :param values: Values (b_s, nk, d_model) :param attention_mask: + Mask over attention values (b_s, h, nq, nk). + + True indicates masking. + :param attention_weights: Multiplicative weights for attention values (b_s, h, + nq, nk). :return: """ b_s, nq = queries.shape[:2] @@ -285,9 +281,7 @@ def forward(self, queries, keys, values, attention_mask=None, attention_weights= # taken from: https://github.com/xmu-xiaoma666/External-Attention-pytorch/blob/master/model/attention/SimplifiedSelfAttention.py class SimplifiedScaledDotProductAttention(nn.Module): - """ - Scaled dot-product attention - """ + """Scaled dot-product attention.""" def __init__(self, d_model, h, dropout=0.1): """ @@ -323,13 +317,13 @@ def init_weights(self): init.constant_(m.bias, 0) def forward(self, queries, keys, values, attention_mask=None, attention_weights=None): - """ - Computes - :param queries: Queries (b_s, nq, d_model) - :param keys: Keys (b_s, nk, d_model) - :param values: Values (b_s, nk, d_model) - :param attention_mask: Mask over attention values (b_s, h, nq, nk). True indicates masking. - :param attention_weights: Multiplicative weights for attention values (b_s, h, nq, nk). + """Computes :param queries: Queries (b_s, nq, d_model) :param keys: Keys (b_s, + nk, d_model) :param values: Values (b_s, nk, d_model) :param attention_mask: + Mask over attention values (b_s, h, nq, nk). + + True indicates masking. + :param attention_weights: Multiplicative weights for attention values (b_s, h, + nq, nk). :return: """ b_s, nq = queries.shape[:2] diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/conv_module.py b/deeplabcut/pose_estimation_pytorch/models/modules/conv_module.py index 24778c44a6..e000b9f4a7 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/conv_module.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/conv_module.py @@ -112,7 +112,7 @@ def _make_one_branch( ) ) self.num_inchannels[branch_index] = num_channels[branch_index] * block.expansion - for i in range(1, num_blocks[branch_index]): + for _i in range(1, num_blocks[branch_index]): layers.append(block(self.num_inchannels[branch_index], num_channels[branch_index])) return nn.Sequential(*layers) diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/gated_attention_unit.py b/deeplabcut/pose_estimation_pytorch/models/modules/gated_attention_unit.py index f6c6f161fa..47c5a232d2 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/gated_attention_unit.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/gated_attention_unit.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Gated Attention Unit +"""Gated Attention Unit. Based on the building blocks used for the ``mmdetection`` CSPNeXt implementation. For more information, see . @@ -73,7 +73,7 @@ def forward(self, x): class GatedAttentionUnit(nn.Module): - """Gated Attention Unit (GAU) in RTMBlock""" + """Gated Attention Unit (GAU) in RTMBlock.""" def __init__( self, diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py index 17c6d87521..ce14ff363e 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py @@ -23,7 +23,7 @@ class BaseKeypointEncoder(ABC): - """Encodes keypoints into heatmaps + """Encodes keypoints into heatmaps. Modified from BUCTD/data/JointsDataset """ @@ -62,7 +62,7 @@ def __call__(self, keypoints: np.ndarray, size: tuple[int, int]) -> np.ndarray: raise NotImplementedError def blur_heatmap(self, heatmap: np.ndarray) -> np.ndarray: - """Applies a Gaussian blur to a heatmap + """Applies a Gaussian blur to a heatmap. Taken from BUCTD/data/JointsDataset, generate_heatmap @@ -90,7 +90,7 @@ def blur_heatmap(self, heatmap: np.ndarray) -> np.ndarray: @KEYPOINT_ENCODERS.register_module class StackedKeypointEncoder(BaseKeypointEncoder): - """Encodes keypoints into heatmaps, where each + """Encodes keypoints into heatmaps, where each. Modified from BUCTD/data/JointsDataset, get_stacked_condition """ @@ -152,7 +152,7 @@ def _get_condition_matrix(zero_matrix, kpts): @KEYPOINT_ENCODERS.register_module class ColoredKeypointEncoder(BaseKeypointEncoder): - """Encodes keypoints into a given number of color channels + """Encodes keypoints into a given number of color channels. Modified from BUCTD/data/JointsDataset, get_condition_image_colored """ diff --git a/deeplabcut/pose_estimation_pytorch/models/necks/base.py b/deeplabcut/pose_estimation_pytorch/models/necks/base.py index c4eea8234d..336b1a9ef4 100644 --- a/deeplabcut/pose_estimation_pytorch/models/necks/base.py +++ b/deeplabcut/pose_estimation_pytorch/models/necks/base.py @@ -18,7 +18,7 @@ class BaseNeck(ABC, torch.nn.Module): - """Base Neck class for pose estimation""" + """Base Neck class for pose estimation.""" def __init__(self): super().__init__() diff --git a/deeplabcut/pose_estimation_pytorch/models/necks/layers.py b/deeplabcut/pose_estimation_pytorch/models/necks/layers.py index e1916f9973..7dbd1125e7 100644 --- a/deeplabcut/pose_estimation_pytorch/models/necks/layers.py +++ b/deeplabcut/pose_estimation_pytorch/models/necks/layers.py @@ -173,7 +173,7 @@ def forward(self, x: torch.Tensor, mask: torch.Tensor = None): Returns: Output tensor. """ - b, n, _, h = *x.shape, self.heads + _b, _n, _, h = *x.shape, self.heads qkv = self.to_qkv(x).chunk(3, dim=-1) q, k, v = map(lambda t: rearrange(t, "b n (h d) -> b h n d", h=h), qkv) diff --git a/deeplabcut/pose_estimation_pytorch/models/necks/transformer.py b/deeplabcut/pose_estimation_pytorch/models/necks/transformer.py index f199aadd92..0bb2c478c9 100644 --- a/deeplabcut/pose_estimation_pytorch/models/necks/transformer.py +++ b/deeplabcut/pose_estimation_pytorch/models/necks/transformer.py @@ -25,11 +25,10 @@ @NECKS.register_module class Transformer(BaseNeck): - """Transformer Neck for pose estimation. - title={TokenPose: Learning Keypoint Tokens for Human Pose Estimation}, - author={Yanjie Li and Shoukui Zhang and Zhicheng Wang and Sen Yang and Wankou Yang and Shu-Tao Xia and Erjin Zhou}, - booktitle={IEEE/CVF International Conference on Computer Vision (ICCV)}, - year={2021} + """Transformer Neck for pose estimation. title={TokenPose: Learning Keypoint Tokens + for Human Pose Estimation}, author={Yanjie Li and Shoukui Zhang and Zhicheng Wang + and Sen Yang and Wankou Yang and Shu-Tao Xia and Erjin Zhou}, booktitle={IEEE/CVF + International Conference on Computer Vision (ICCV)}, year={2021} Args: feature_size: Size of the input feature map (height, width). diff --git a/deeplabcut/pose_estimation_pytorch/models/predictors/dekr_predictor.py b/deeplabcut/pose_estimation_pytorch/models/predictors/dekr_predictor.py index bc96097d6b..0ef0ea81c8 100644 --- a/deeplabcut/pose_estimation_pytorch/models/predictors/dekr_predictor.py +++ b/deeplabcut/pose_estimation_pytorch/models/predictors/dekr_predictor.py @@ -247,10 +247,10 @@ def max_pool(self, heatmap: torch.Tensor) -> torch.Tensor: # Assuming you have 'heatmap' tensor max_pooled_heatmap = predictor.max_pool(heatmap) """ - pool1 = torch.nn.MaxPool2d(3, 1, 1) + torch.nn.MaxPool2d(3, 1, 1) pool2 = torch.nn.MaxPool2d(5, 1, 2) - pool3 = torch.nn.MaxPool2d(7, 1, 3) - map_size = (heatmap.shape[1] + heatmap.shape[2]) / 2.0 + torch.nn.MaxPool2d(7, 1, 3) + (heatmap.shape[1] + heatmap.shape[2]) / 2.0 maxm = pool2(heatmap) # Here I think pool 2 is a good match for default 17 pos_dist_tresh return maxm @@ -281,7 +281,7 @@ def get_top_values(self, heatmap: torch.Tensor) -> tuple[torch.Tensor, torch.Ten def _update_pose_with_heatmaps(self, _poses: torch.Tensor, kpt_heatmaps: torch.Tensor): """If a heatmap center is close enough from the regressed point, the final - prediction is the center of this heatmap + prediction is the center of this heatmap. Args: poses: poses tensor, shape (batch_size, num_animals, num_keypoints, 2) diff --git a/deeplabcut/pose_estimation_pytorch/models/predictors/identity_predictor.py b/deeplabcut/pose_estimation_pytorch/models/predictors/identity_predictor.py index 35461e6af0..a70d06c213 100644 --- a/deeplabcut/pose_estimation_pytorch/models/predictors/identity_predictor.py +++ b/deeplabcut/pose_estimation_pytorch/models/predictors/identity_predictor.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Predictor to generate identity maps from head outputs""" +"""Predictor to generate identity maps from head outputs.""" import torch import torch.nn as nn @@ -22,7 +22,7 @@ @PREDICTORS.register_module class IdentityPredictor(BasePredictor): - """Predictor to generate identity maps from head outputs + """Predictor to generate identity maps from head outputs. Attributes: apply_sigmoid: Apply sigmoid to heatmaps. Defaults to True. @@ -38,10 +38,9 @@ def __init__(self, apply_sigmoid: bool = True): self.sigmoid = nn.Sigmoid() def forward(self, stride: float, outputs: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: - """ - Swaps the dimensions so the heatmap are (batch_size, h, w, num_individuals), - optionally applies a sigmoid to the heatmaps, and rescales it to be the size - of the original image (so that the identity scores of keypoints can be computed) + """Swaps the dimensions so the heatmap are (batch_size, h, w, num_individuals), + optionally applies a sigmoid to the heatmaps, and rescales it to be the size of + the original image (so that the identity scores of keypoints can be computed) Args: stride: the stride of the model diff --git a/deeplabcut/pose_estimation_pytorch/models/predictors/paf_predictor.py b/deeplabcut/pose_estimation_pytorch/models/predictors/paf_predictor.py index c67223a4fb..b625353f22 100644 --- a/deeplabcut/pose_estimation_pytorch/models/predictors/paf_predictor.py +++ b/deeplabcut/pose_estimation_pytorch/models/predictors/paf_predictor.py @@ -111,7 +111,8 @@ def __init__( ) def forward(self, stride: float, outputs: dict[str, torch.Tensor]) -> dict[str, torch.Tensor]: - """Forward pass of PartAffinityFieldPredictor. Gets predictions from model output. + """Forward pass of PartAffinityFieldPredictor. Gets predictions from model + output. Args: stride: the stride of the model @@ -435,8 +436,8 @@ def compute_peaks_and_costs( n_points: int = 10, n_decimals: int = 3, ) -> list[dict[str, NDArray]]: - """ - Compute refined peak coordinates, confidence scores, and PAF edge costs for pose estimation. + """Compute refined peak coordinates, confidence scores, and PAF edge costs for + pose estimation. Args: heatmaps: Smoothed heatmaps tensor with shape (batch_size, num_joints, height, width). @@ -518,7 +519,7 @@ def compute_peaks_and_costs( return peaks_and_costs def set_paf_edges_to_keep(self, edge_indices: list[int]) -> None: - """Sets the PAF edge indices to use to assemble individuals + """Sets the PAF edge indices to use to assemble individuals. Args: edge_indices: The indices of edges in the graph to keep. diff --git a/deeplabcut/pose_estimation_pytorch/models/predictors/sim_cc.py b/deeplabcut/pose_estimation_pytorch/models/predictors/sim_cc.py index 6bfe68b488..ad53061ccb 100644 --- a/deeplabcut/pose_estimation_pytorch/models/predictors/sim_cc.py +++ b/deeplabcut/pose_estimation_pytorch/models/predictors/sim_cc.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""SimCC predictor for the RTMPose model +"""SimCC predictor for the RTMPose model. Based on the official ``mmpose`` SimCC codec and RTMCC head implementation. For more information, see . @@ -27,7 +27,7 @@ @PREDICTORS.register_module class SimCCPredictor(BasePredictor): - """Class used to make pose predictions from RTMPose head outputs + """Class used to make pose predictions from RTMPose head outputs. The RTMPose model uses coordinate classification for pose estimation. For more information, see "SimCC: a Simple Coordinate Classification Perspective for Human diff --git a/deeplabcut/pose_estimation_pytorch/models/target_generators/base.py b/deeplabcut/pose_estimation_pytorch/models/target_generators/base.py index d4097356a2..9802ced451 100644 --- a/deeplabcut/pose_estimation_pytorch/models/target_generators/base.py +++ b/deeplabcut/pose_estimation_pytorch/models/target_generators/base.py @@ -21,13 +21,13 @@ class BaseGenerator(ABC, nn.Module): # TODO: Should this really be a module? - """Generates target maps from ground truth annotations to train models + """Generates target maps from ground truth annotations to train models. The outputs of the target generator are used to compute losses for model heads. If the head outputs "heatmap" and "offset" tensors, then the corresponding generator must output target "heatmap" and "offset" tensors. The targets themselves are - dictionaries, and passed as keyword-arguments to the criterions. This allows to - pass masks to the criterions. + dictionaries, and passed as keyword-arguments to the criterions. This allows to pass + masks to the criterions. Generally, this means that for each head output (such as "heatmap"), a dict will be generated with a "target" key (for the target heatmap) and optionally a "weights" @@ -42,7 +42,7 @@ def __init__(self, label_keypoint_key: str = "keypoints"): def forward( self, stride: float, outputs: dict[str, torch.Tensor], labels: dict ) -> dict[str, dict[str, torch.Tensor]]: - """Generates targets + """Generates targets. Args: stride: the stride of the model diff --git a/deeplabcut/pose_estimation_pytorch/models/target_generators/dekr_targets.py b/deeplabcut/pose_estimation_pytorch/models/target_generators/dekr_targets.py index 402f449aaf..84801df366 100644 --- a/deeplabcut/pose_estimation_pytorch/models/target_generators/dekr_targets.py +++ b/deeplabcut/pose_estimation_pytorch/models/target_generators/dekr_targets.py @@ -188,9 +188,8 @@ def forward( def dekr_heatmap_val(sigma: float, x: float, y: float, x0: float, y0: float) -> float: - """ - Calculates the corresponding heat value of point (x,y) given the heat distribution centered - at (x0,y0) and spread value of sigma. + """Calculates the corresponding heat value of point (x,y) given the heat + distribution centered at (x0,y0) and spread value of sigma. Args: sigma: controls the spread or width of the heat distribution diff --git a/deeplabcut/pose_estimation_pytorch/models/target_generators/heatmap_targets.py b/deeplabcut/pose_estimation_pytorch/models/target_generators/heatmap_targets.py index 3a47093905..93464ca5ee 100644 --- a/deeplabcut/pose_estimation_pytorch/models/target_generators/heatmap_targets.py +++ b/deeplabcut/pose_estimation_pytorch/models/target_generators/heatmap_targets.py @@ -33,10 +33,9 @@ class HeatmapGenerator(BaseGenerator): """ class Mode(Enum): - """ - KEYPOINT generates one heatmap per type of keypoint (for pose estimation heads) - INDIVIDUAL generates one heatmap per individual (for identification heads) - """ + """KEYPOINT generates one heatmap per type of keypoint (for pose estimation + heads) INDIVIDUAL generates one heatmap per individual (for identification + heads)""" INDIVIDUAL = "INDIVIDUAL" KEYPOINT = "KEYPOINT" @@ -106,9 +105,9 @@ def __init__( def forward( self, stride: float, outputs: dict[str, torch.Tensor], labels: dict ) -> dict[str, dict[str, torch.Tensor]]: - """ - Given the annotations and predictions of your keypoints, this function returns the targets, - a dictionary containing the heatmaps, locref_maps and locref_masks. + """Given the annotations and predictions of your keypoints, this function + returns the targets, a dictionary containing the heatmaps, locref_maps and + locref_masks. Args: stride: the stride of the model @@ -257,9 +256,8 @@ def update( locref_map: np.ndarray | None, locref_mask: np.ndarray | None, ) -> None: - """ - Updates the heatmap and locref targets in-place following an update rule (e.g., - Gaussian or Plateau). + """Updates the heatmap and locref targets in-place following an update rule + (e.g., Gaussian or Plateau). Args: heatmap: the heatmap to update of shape (height, width) @@ -275,7 +273,7 @@ def update( @TARGET_GENERATORS.register_module class HeatmapGaussianGenerator(HeatmapGenerator): - """Generates gaussian heatmaps (and locref) targets from keypoints""" + """Generates gaussian heatmaps (and locref) targets from keypoints.""" def update( self, @@ -285,7 +283,7 @@ def update( locref_map: np.ndarray | None, locref_mask: np.ndarray | None, ) -> None: - """Updates the heatmap (and locref if defined) with gaussian values""" + """Updates the heatmap (and locref if defined) with gaussian values.""" # revert keypoints to follow image convention: from x,y to y,x keypoint = keypoint.copy()[::-1] @@ -305,7 +303,7 @@ def update( @TARGET_GENERATORS.register_module class HeatmapPlateauGenerator(HeatmapGenerator): - """Generates plateau heatmaps (and locref) targets from keypoints""" + """Generates plateau heatmaps (and locref) targets from keypoints.""" def update( self, @@ -315,7 +313,7 @@ def update( locref_map: np.ndarray | None, locref_mask: np.ndarray | None, ) -> None: - """Updates the heatmap (and locref if defined) with plateau values""" + """Updates the heatmap (and locref if defined) with plateau values.""" # revert keypoints to follow image convention: from x,y to y,x keypoint = keypoint.copy()[::-1] dist = np.sum((grid - keypoint) ** 2, axis=2) diff --git a/deeplabcut/pose_estimation_pytorch/models/target_generators/pafs_targets.py b/deeplabcut/pose_estimation_pytorch/models/target_generators/pafs_targets.py index 1ac22d9e8a..a3dab0f945 100644 --- a/deeplabcut/pose_estimation_pytorch/models/target_generators/pafs_targets.py +++ b/deeplabcut/pose_estimation_pytorch/models/target_generators/pafs_targets.py @@ -23,10 +23,8 @@ @TARGET_GENERATORS.register_module class PartAffinityFieldGenerator(BaseGenerator): - """ - Generate part affinity field targets from ground truth keypoints in order - to train baseline multi-animal deeplabcut model (ResNet + Deconv) - """ + """Generate part affinity field targets from ground truth keypoints in order to + train baseline multi-animal deeplabcut model (ResNet + Deconv)""" def __init__(self, graph: list[list[int, int]], width: float): """ diff --git a/deeplabcut/pose_estimation_pytorch/models/target_generators/sim_cc.py b/deeplabcut/pose_estimation_pytorch/models/target_generators/sim_cc.py index cfb4426052..da938f3986 100644 --- a/deeplabcut/pose_estimation_pytorch/models/target_generators/sim_cc.py +++ b/deeplabcut/pose_estimation_pytorch/models/target_generators/sim_cc.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Modified SimCC target generator for the RTMPose model +"""Modified SimCC target generator for the RTMPose model. Based on the official ``mmpose`` SimCC codec and RTMCC head implementation. For more information, see . @@ -29,7 +29,7 @@ @TARGET_GENERATORS.register_module class SimCCGenerator(BaseGenerator): - """Class used generate targets from RTMPose head outputs + """Class used generate targets from RTMPose head outputs. The RTMPose model uses coordinate classification for pose estimation. For more information, see "SimCC: a Simple Coordinate Classification Perspective for Human @@ -168,7 +168,7 @@ def _generate_standard( def _map_coordinates( self, keypoints: np.ndarray, keypoints_visible: np.ndarray | None = None ) -> tuple[np.ndarray, np.ndarray]: - """Mapping keypoint coordinates into SimCC space""" + """Mapping keypoint coordinates into SimCC space.""" keypoints_split = keypoints.copy() # set non-visible keypoints to 0; deals with NaNs keypoints_split[keypoints_visible <= 0] = 0 @@ -180,7 +180,7 @@ def _map_coordinates( def _generate_gaussian( self, keypoints: np.ndarray, keypoints_visible: np.ndarray | None = None ) -> tuple[np.ndarray, np.ndarray, np.ndarray]: - """Encoding keypoints into SimCC labels with Gaussian Label Smoothing""" + """Encoding keypoints into SimCC labels with Gaussian Label Smoothing.""" N, K, _ = keypoints.shape w, h = self.input_size W = np.around(w * self.simcc_split_ratio).astype(int) diff --git a/deeplabcut/pose_estimation_pytorch/models/weight_init.py b/deeplabcut/pose_estimation_pytorch/models/weight_init.py index c660c5b294..6040b3b4c7 100644 --- a/deeplabcut/pose_estimation_pytorch/models/weight_init.py +++ b/deeplabcut/pose_estimation_pytorch/models/weight_init.py @@ -1,4 +1,4 @@ -"""Ways to initialize weights for PyTorch modules""" +"""Ways to initialize weights for PyTorch modules.""" from __future__ import annotations @@ -10,7 +10,7 @@ def _build_weight_init(cfg: str | dict, **kwargs) -> BaseWeightInitializer: - """Builds a BaseWeightInitializer using its config or the name of the initializer + """Builds a BaseWeightInitializer using its config or the name of the initializer. Args: cfg: Either the name of the initializer (e.g. 'normal') or the config @@ -28,7 +28,7 @@ def _build_weight_init(cfg: str | dict, **kwargs) -> BaseWeightInitializer: class BaseWeightInitializer(ABC): - """Class to used to initialize model weights""" + """Class to used to initialize model weights.""" @abstractmethod def init_weights(self, model: nn.Module) -> None: @@ -41,7 +41,7 @@ def init_weights(self, model: nn.Module) -> None: @WEIGHT_INIT.register_module class Normal(BaseWeightInitializer): - """Class to used to initialize model weights using a normal distribution + """Class to used to initialize model weights using a normal distribution. Weights are initialized with a normal distribution, and biases are initialized to 0. @@ -62,7 +62,7 @@ def init_weights(self, model: nn.Module) -> None: @WEIGHT_INIT.register_module class Dekr(BaseWeightInitializer): - """Class to used to initialize model weights in the same way as DEKR + """Class to used to initialize model weights in the same way as DEKR. Attributes: std: the standard deviation to use to initialize weights @@ -90,7 +90,7 @@ def init_weights(self, model: nn.Module) -> None: @WEIGHT_INIT.register_module class Rtmpose(BaseWeightInitializer): - """Class to used to initialize head weights in the same way as RTMPose""" + """Class to used to initialize head weights in the same way as RTMPose.""" def init_weights(self, model: nn.Module) -> None: for module in model.modules(): diff --git a/deeplabcut/pose_estimation_pytorch/modelzoo/inference.py b/deeplabcut/pose_estimation_pytorch/modelzoo/inference.py index b5985fd704..58d7130b75 100644 --- a/deeplabcut/pose_estimation_pytorch/modelzoo/inference.py +++ b/deeplabcut/pose_estimation_pytorch/modelzoo/inference.py @@ -31,7 +31,7 @@ class NumpyEncoder(json.JSONEncoder): - """Special json encoder for numpy types""" + """Special json encoder for numpy types.""" def default(self, obj): if isinstance(obj, np.ndarray): @@ -65,11 +65,11 @@ def _video_inference_superanimal( create_labeled_video: bool = True, torchvision_detector_name: str | None = None, ) -> dict: - """ - Perform inference on a video using a superanimal model from the model zoo specified by `superanimal_name`. - During inference, the video is analyzed using the specified model and the results are saved in the specified - destination folder. The predictions are saved in the form of a .h5 file. The video with the predictions is saved - in the form of a .mp4 file. + """Perform inference on a video using a superanimal model from the model zoo + specified by `superanimal_name`. During inference, the video is analyzed using the + specified model and the results are saved in the specified destination folder. The + predictions are saved in the form of a .h5 file. The video with the predictions is + saved in the form of a .mp4 file. WARNING: This function is an internal utility function and should not be called directly. It is designed to be used by deeplabcut.modelzoo.api.video_inference.py diff --git a/deeplabcut/pose_estimation_pytorch/modelzoo/memory_replay.py b/deeplabcut/pose_estimation_pytorch/modelzoo/memory_replay.py index c43456ed90..c0baeca0b3 100644 --- a/deeplabcut/pose_estimation_pytorch/modelzoo/memory_replay.py +++ b/deeplabcut/pose_estimation_pytorch/modelzoo/memory_replay.py @@ -44,7 +44,7 @@ def get_pose_predictions( max_individuals: int, device: str | None = None, ) -> dict[str, dict]: - """Gets predictions made by a SuperAnimal model on a DeepLabCut project + """Gets predictions made by a SuperAnimal model on a DeepLabCut project. Args: loader: The path to the root of the project. @@ -104,7 +104,7 @@ def get_pose_predictions( ] predictions = pose_runner.inference(pose_inputs) - for image, prediction in zip(images_to_process, predictions): + for image, prediction in zip(images_to_process, predictions, strict=False): sa_predictions[image] = prediction # save the updated SuperAnimal predictions @@ -134,9 +134,7 @@ def prepare_memory_replay_dataset( pose_threshold: float = 0.0, device: str | None = None, ): - """ - Need to first run inference on the source project train file - """ + """Need to first run inference on the source project train file.""" project_root = loader.project_path.resolve() source_dataset_folder = Path(source_dataset_folder).resolve() @@ -189,7 +187,6 @@ def xywh2xyxy(bbox): return temp_bbox def optimal_match(gts_list, preds_list): - arranged_preds_list = [] num_gts = len(gts_list) num_preds = len(preds_list) cost_matrix = np.zeros((num_gts, num_preds)) diff --git a/deeplabcut/pose_estimation_pytorch/modelzoo/utils.py b/deeplabcut/pose_estimation_pytorch/modelzoo/utils.py index 207b460865..abc343c07b 100644 --- a/deeplabcut/pose_estimation_pytorch/modelzoo/utils.py +++ b/deeplabcut/pose_estimation_pytorch/modelzoo/utils.py @@ -90,7 +90,7 @@ def load_super_animal_config( max_individuals: int = 30, device: str | None = None, ) -> dict: - """Loads the model configuration file for a model, detector and SuperAnimal + """Loads the model configuration file for a model, detector and SuperAnimal. Args: super_animal: The name of the SuperAnimal for which to create the model config. @@ -122,7 +122,7 @@ def load_super_animal_config( def download_super_animal_snapshot(dataset: str, model_name: str) -> Path: - """Downloads a SuperAnimal snapshot + """Downloads a SuperAnimal snapshot. Args: dataset: The name of the SuperAnimal dataset for which to download a snapshot. @@ -157,7 +157,7 @@ def get_gpu_memory_map(): encoding="utf-8", ) gpu_memory = [int(x) for x in result.strip().split("\n")] - gpu_memory_map = dict(zip(range(len(gpu_memory)), gpu_memory)) + gpu_memory_map = dict(zip(range(len(gpu_memory)), gpu_memory, strict=False)) return gpu_memory_map @@ -178,11 +178,12 @@ def raise_warning_if_called_directly(): warnings.warn( f"{caller_name} is intended for internal use only and should not be called directly.", UserWarning, + stacklevel=2, ) def update_config(config: dict, max_individuals: int, device: str): - """Loads the model configuration file for a model, detector and SuperAnimal + """Loads the model configuration file for a model, detector and SuperAnimal. Args: config: The default model configuration file. diff --git a/deeplabcut/pose_estimation_pytorch/registry.py b/deeplabcut/pose_estimation_pytorch/registry.py index 6d9f549103..7f880d1973 100644 --- a/deeplabcut/pose_estimation_pytorch/registry.py +++ b/deeplabcut/pose_estimation_pytorch/registry.py @@ -14,8 +14,9 @@ def build_from_cfg(cfg: dict, registry: "Registry", default_args: dict | None = None) -> Any: - """Builds a module from the configuration dictionary when it represents a class configuration, - or call a function from the configuration dictionary when it represents a function configuration. + """Builds a module from the configuration dictionary when it represents a class + configuration, or call a function from the configuration dictionary when it + represents a function configuration. Args: cfg: Configuration dictionary. It should at least contain the key "type". @@ -70,9 +71,9 @@ def build_from_cfg(cfg: dict, registry: "Registry", default_args: dict | None = class Registry: - """A registry to map strings to classes or functions. - Registered objects could be built from the registry. Meanwhile, registered - functions could be called from the registry. + """A registry to map strings to classes or functions. Registered objects could be + built from the registry. Meanwhile, registered functions could be called from the + registry. Args: name: Registry name. @@ -130,6 +131,7 @@ def __repr__(self): @staticmethod def split_scope_key(key): """Split scope and key. + The first scope will be split from key. Examples: >>> Registry.split_scope_key('mmdet.ResNet') @@ -302,6 +304,7 @@ def deprecated_register_module(self, cls=None, force=False): def register_module(self, name=None, force=False, module=None): """Register a module. + A record will be added to `self._module_dict`, whose key is the class name or the specified name, and value is the class itself. It can be used as a decorator or a normal function. diff --git a/deeplabcut/pose_estimation_pytorch/runners/dynamic_cropping.py b/deeplabcut/pose_estimation_pytorch/runners/dynamic_cropping.py index e1746f1c70..3a5e09b733 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/dynamic_cropping.py +++ b/deeplabcut/pose_estimation_pytorch/runners/dynamic_cropping.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Modules to dynamically crop individuals out of videos to improve video analysis""" +"""Modules to dynamically crop individuals out of videos to improve video analysis.""" import math from dataclasses import dataclass, field @@ -20,15 +20,14 @@ @dataclass class DynamicCropper: - """ - If the state is true, then dynamic cropping will be performed. That means that - if an object is detected (i.e. any body part > detection threshold), then object + """If the state is true, then dynamic cropping will be performed. That means that if + an object is detected (i.e. any body part > detection threshold), then object boundaries are computed according to the smallest/largest x position and - smallest/largest y position of all body parts. This window is expanded by the - margin and from then on only the posture within this crop is analyzed (until the - object is lost, i.e. < detection threshold). The current position is utilized for - updating the crop window for the next frame (this is why the margin is important - and should be set large enough given the movement of the animal). + smallest/largest y position of all body parts. This window is expanded by the margin + and from then on only the posture within this crop is analyzed (until the object is + lost, i.e. < detection threshold). The current position is utilized for updating the + crop window for the next frame (this is why the margin is important and should be + set large enough given the movement of the animal). Attributes: threshold: float @@ -148,12 +147,12 @@ def update(self, pose: torch.Tensor) -> torch.Tensor: return pose def reset(self) -> None: - """Resets the DynamicCropper to not crop the next frame""" + """Resets the DynamicCropper to not crop the next frame.""" self._crop = None @staticmethod def build(dynamic: bool, threshold: float, margin: int) -> Optional["DynamicCropper"]: - """Builds the DynamicCropper based on the given parameters + """Builds the DynamicCropper based on the given parameters. Args: dynamic: Whether dynamic cropping should be used @@ -505,8 +504,7 @@ def generate_patches(self) -> list[tuple[int, int, int, int]]: @staticmethod def split_array(size: int, n: int, overlap: int) -> list[tuple[int, int]]: - """ - Splits an array into n segments of equal size, where the overlap between each + """Splits an array into n segments of equal size, where the overlap between each segment is at least a given value. Args: @@ -525,7 +523,7 @@ def split_array(size: int, n: int, overlap: int) -> list[tuple[int, int]]: segment_size = (padded_size // n) + (padded_size % n > 0) segments = [] end = overlap - for i in range(n): + for _i in range(n): start = end - overlap end = start + segment_size if end > size: diff --git a/deeplabcut/pose_estimation_pytorch/runners/inference.py b/deeplabcut/pose_estimation_pytorch/runners/inference.py index aa8c9989f1..55f1c531c6 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/inference.py +++ b/deeplabcut/pose_estimation_pytorch/runners/inference.py @@ -114,10 +114,8 @@ def to_dict(self) -> dict: @dataclass class InferenceConfig: - """ - Top-level inference configuration that mirrors the `inference` block - in pytorch_config.yaml. - """ + """Top-level inference configuration that mirrors the `inference` block in + pytorch_config.yaml.""" multithreading: MultithreadingConfig = field(default_factory=MultithreadingConfig) compile: CompileConfig = field(default_factory=CompileConfig) @@ -126,8 +124,8 @@ class InferenceConfig: @classmethod def from_dict(cls, data: dict[str, Any] | None) -> InferenceConfig: - """ - Build an InferenceConfig from a dict, supporting: + """Build an InferenceConfig from a dict, supporting: + - nested dictionaries - dot-notation keys (e.g., {"compile.enabled": True}) Raises KeyError if a key does not exist. @@ -176,7 +174,7 @@ def to_dict(self) -> dict: class InferenceRunner(Runner, Generic[ModelType], metaclass=ABCMeta): - """Base class for inference runners + """Base class for inference runners. A runner takes a model and runs actions on it, such as training or inference """ @@ -241,7 +239,8 @@ def __init__( except Exception as e: warnings.warn( f"torch.compile failed with backend='{self.inference_cfg.compile.backend}', " - f"falling back to eager mode. Error: {e}" + f"falling back to eager mode. Error: {e}", + stacklevel=2, ) self._batch_list: list[torch.Tensor] = [] @@ -260,7 +259,7 @@ def __init__( @abstractmethod def predict(self, inputs: torch.Tensor, **kwargs) -> list[dict[str, dict[str, np.ndarray]]]: - """Makes predictions from a model input and output + """Makes predictions from a model input and output. Args: the inputs to the model, of shape (batch_size, ...) @@ -275,7 +274,7 @@ def inference( images: (Iterable[str | Path | np.ndarray] | Iterable[tuple[str | Path | np.ndarray, dict[str, Any]]]), shelf_writer: shelving.ShelfWriter | None = None, ) -> list[dict[str, np.ndarray]]: - """Run model inference on the given dataset + """Run model inference on the given dataset. TODO: Add an option to also return head outputs (such as heatmaps)? Can be super useful for debugging @@ -307,7 +306,7 @@ def _sequential_inference( images: (Iterable[str | Path | np.ndarray] | Iterable[tuple[str | Path | np.ndarray, dict[str, Any]]]), shelf_writer: shelving.ShelfWriter | None = None, ) -> list[dict[str, np.ndarray]]: - """Original sequential inference implementation""" + """Original sequential inference implementation.""" results = [] for data in images: self._prepare_inputs(data) @@ -326,7 +325,7 @@ def _async_inference( images: (Iterable[str | Path | np.ndarray] | Iterable[tuple[str | Path | np.ndarray, dict[str, Any]]]), shelf_writer: shelving.ShelfWriter | None = None, ) -> list[dict[str, np.ndarray]]: - """Async inference with pipeline parallelism""" + """Async inference with pipeline parallelism.""" # Reset state self._stop_event.clear() self._exception = None @@ -384,9 +383,8 @@ def _prepare_inputs( self, data: str | Path | np.ndarray | tuple[str | Path | np.ndarray, dict], ) -> None: - """ - Prepares inputs for an image and adds them to the data ready to be processed - """ + """Prepares inputs for an image and adds them to the data ready to be + processed.""" if isinstance(data, (str, Path, np.ndarray)): inputs, context = data, {} else: @@ -457,9 +455,10 @@ def _extract_results(self, shelf_writer: shelving.ShelfWriter) -> list: return results def _process_batch(self) -> None: - """ - Processes a batch. There must be inputs waiting to be processed before this is - called, otherwise this method will raise an error. + """Processes a batch. + + There must be inputs waiting to be processed before this is called, otherwise + this method will raise an error. """ batch = torch.stack(self._batch_list[: self.batch_size], dim=0) model_kwargs = {mk: v[: self.batch_size] for mk, v in self._model_kwargs.items()} @@ -479,7 +478,7 @@ def _inputs_waiting_for_processing(self) -> bool: return len(self._batch_list) > 0 def _safe_put(self, item: Any) -> bool: - """Put item in the queue, retrying until successful or stop_event is set""" + """Put item in the queue, retrying until successful or stop_event is set.""" while not self._stop_event.is_set(): try: self._input_queue.put(item, timeout=1.0) @@ -489,8 +488,8 @@ def _safe_put(self, item: Any) -> bool: return False def _safe_get(self) -> Any: - """ - Get the next item from the queue safely, retrying until successful or stop_event is set + """Get the next item from the queue safely, retrying until successful or + stop_event is set. Returns: The item from the queue, or None if the producer is dead or stop_signal is raised and queue empty. @@ -510,7 +509,7 @@ def _safe_get(self) -> Any: continue def _preprocessing_worker(self, images: Iterable) -> None: - """Background worker that prepares inputs and puts them in the input queue""" + """Background worker that prepares inputs and puts them in the input queue.""" try: for data in images: if self._stop_event.is_set(): @@ -546,7 +545,7 @@ def _preprocessing_worker(self, images: Iterable) -> None: self._safe_put(None) def __del__(self): - """Cleanup method to ensure threads are stopped""" + """Cleanup method to ensure threads are stopped.""" if hasattr(self, "_stop_event"): self._stop_event.set() if hasattr(self, "_preprocessing_thread") and self._preprocessing_thread is not None: @@ -554,7 +553,7 @@ def __del__(self): class PoseInferenceRunner(InferenceRunner[PoseModel]): - """Runner for pose estimation inference""" + """Runner for pose estimation inference.""" def __init__( self, @@ -568,7 +567,7 @@ def __init__( raise ValueError("Dynamic cropping can only be used with batch size 1. Please set your batch size to 1.") def predict(self, inputs: torch.Tensor, **kwargs) -> list[dict[str, dict[str, np.ndarray]]]: - """Makes predictions from a model input and output + """Makes predictions from a model input and output. Args: the inputs to the model, of shape (batch_size, ...) @@ -608,7 +607,7 @@ def predict(self, inputs: torch.Tensor, **kwargs) -> list[dict[str, dict[str, np class CTDInferenceRunner(PoseInferenceRunner): - """Runner for pose estimation inference + """Runner for pose estimation inference. Args: model: The CTD model to run inference with. @@ -655,7 +654,7 @@ def inference( images: (Iterable[str | Path | np.ndarray] | Iterable[tuple[str | Path | np.ndarray, dict[str, Any]]]), shelf_writer: shelving.ShelfWriter | None = None, ) -> list[dict[str, np.ndarray]]: - """Run CTD model inference on the given dataset + """Run CTD model inference on the given dataset. Args: images: the images to run inference on, optionally with context @@ -692,7 +691,7 @@ def inference( return results def predict(self, inputs: torch.Tensor, **kwargs) -> list[dict[str, dict[str, np.ndarray]]]: - """Makes predictions from a model input and output + """Makes predictions from a model input and output. Args: the inputs to the model, of shape (batch_size, ...) @@ -833,7 +832,10 @@ def _ctd_tracking_postprocess( predictions: dict[str, np.ndarray], image_size: tuple[int, int], ) -> None: - """Post-processes predictions. In-place changes to the predictions dict.""" + """Post-processes predictions. + + In-place changes to the predictions dict. + """ # reorder the previous poses so the indices match the track IDs if self._idx_to_id is not None: predictions["bodyparts"] = predictions["bodyparts"][self._idx_to_id] @@ -895,9 +897,8 @@ def _ctd_tracking_postprocess( self._idx_ages = None def _merge_conditions(self, bu_cond: np.ndarray) -> np.ndarray: - """ - Merges conditions made by a BU model with existing conditions from CTD tracking. - """ + """Merges conditions made by a BU model with existing conditions from CTD + tracking.""" # prepare the BU conditions for matching bu_cond = bu_cond.copy()[:, :, :3] # mask low-quality keypoints @@ -938,7 +939,7 @@ def _merge_conditions(self, bu_cond: np.ndarray) -> np.ndarray: class DetectorInferenceRunner(InferenceRunner[BaseDetector]): - """Runner for object detection inference""" + """Runner for object detection inference.""" def __init__(self, model: BaseDetector, **kwargs): """ @@ -949,7 +950,7 @@ def __init__(self, model: BaseDetector, **kwargs): super().__init__(model, **kwargs) def predict(self, inputs: torch.Tensor, **kwargs) -> list[dict[str, dict[str, np.ndarray]]]: - """Makes predictions from a model input and output + """Makes predictions from a model input and output. Args: the inputs to the model, of shape (batch_size, ...) @@ -993,8 +994,7 @@ def build_inference_runner( inference_cfg: InferenceConfig | dict | None = None, **kwargs, ) -> InferenceRunner: - """ - Build a runner object according to a pytorch configuration file + """Build a runner object according to a pytorch configuration file. Args: task: the inference task to run diff --git a/deeplabcut/pose_estimation_pytorch/runners/logger.py b/deeplabcut/pose_estimation_pytorch/runners/logger.py index 6d73af4a66..53608fcb23 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/logger.py +++ b/deeplabcut/pose_estimation_pytorch/runners/logger.py @@ -40,8 +40,7 @@ def setup_file_logging(filepath: Path) -> None: - """ - Sets up logging to a file + """Sets up logging to a file. Args: filepath: the path where logs should be saved @@ -61,7 +60,7 @@ def setup_file_logging(filepath: Path) -> None: def destroy_file_logging() -> None: - """Resets the logging module to log everything to the console""" + """Resets the logging module to log everything to the console.""" root = logging.getLogger() handlers = [h for h in root.handlers] for handler in handlers: @@ -69,11 +68,11 @@ def destroy_file_logging() -> None: class BaseLogger(ABC): - """Base class for logging training runs""" + """Base class for logging training runs.""" @abstractmethod def log_config(self, config: dict = None) -> None: - """Logs the configuration data for a training run + """Logs the configuration data for a training run. Args: config: the training configuration used for the run @@ -81,7 +80,7 @@ def log_config(self, config: dict = None) -> None: @abstractmethod def log(self, metrics: dict[str, Any], step: int | None = None) -> None: - """Logs data from a training run + """Logs data from a training run. Args: metrics: the metrics to log @@ -90,11 +89,11 @@ def log(self, metrics: dict[str, Any], step: int | None = None) -> None: @abstractmethod def save(self) -> None: - """Saves the current training logs""" + """Saves the current training logs.""" class ImageLoggerMixin(ABC): - """Mixin for loggers that can log images + """Mixin for loggers that can log images. Before starting training, you should call `select_images_to_log`, which will select a train and a test image for which inputs/outputs will always be logged. @@ -145,7 +144,7 @@ def log_images( targets: dict[str, dict[str, torch.Tensor]], step: int, ) -> None: - """Log images for a batch + """Log images for a batch. Args: inputs: the inputs for the model, containing at least an "image" key @@ -156,7 +155,7 @@ def log_images( pass def select_images_to_log(self, train: DataLoader, valid: DataLoader) -> None: - """Selects the train and test images to log + """Selects the train and test images to log. Args: train: the training dataloader @@ -210,7 +209,7 @@ def _prepare_image( return image.permute(1, 2, 0).numpy() def _heatmap_softmax(self, heatmaps: torch.Tensor) -> torch.Tensor: - """Applies a softmax to the heatmap channels""" + """Applies a softmax to the heatmap channels.""" return self._softmax(heatmaps.detach().cpu()) def _prepare_images( @@ -219,7 +218,7 @@ def _prepare_images( outputs: dict[str, dict[str, torch.Tensor]], targets: dict[str, dict[str, dict[str, torch.Tensor]]], ) -> dict[str, np.ndarray]: - """Prepares images for logging""" + """Prepares images for logging.""" image_logs = {} paths = inputs["path"] images_to_log = [(i, p) for i, p in enumerate(paths) if p in self._logged] @@ -238,7 +237,7 @@ def _prepare_images( if "heatmap" in head_outputs: head_heatmaps = self._heatmap_softmax(head_outputs["heatmap"][idx]) head_targets = targets[head]["heatmap"]["target"][idx] - for j, (h, t) in enumerate(zip(head_heatmaps, head_targets)): + for j, (h, t) in enumerate(zip(head_heatmaps, head_targets, strict=False)): h = self._prepare_image(h.unsqueeze(0)) t = self._prepare_image(t.unsqueeze(0)) image_logs[f"{base}.heatmap.{j}"] = np.concatenate([h, t]) @@ -278,7 +277,6 @@ def __init__( Example: logger = WandbLogger(project_name="mice", run_name="exp1", model=my_model) - """ super().__init__(image_log_interval=image_log_interval) @@ -318,7 +316,7 @@ def _save_wandb_info(self): logging.info(f"WandB run info saved to {output_path}") def log(self, metrics: dict[str, Any], step: int | None = None) -> None: - """Logs metrics from runs + """Logs metrics from runs. Args: metrics: the metrics to log @@ -337,7 +335,7 @@ def log_images( targets: dict[str, dict[str, dict[str, torch.Tensor]]], step: int, ) -> None: - """Log images for a batch + """Log images for a batch. Args: inputs: the inputs for the model, containing at least an "image" key @@ -383,14 +381,13 @@ def log_config(self, config: dict = None) -> None: logger = WandbLogger() config = {"learning_rate": 0.001, "batch_size": 32} logger.log_config(config) - """ self.run.config.update(config) @LOGGER.register_module class CSVLogger(BaseLogger): - """Logger saving stats and metrics to a CSV file""" + """Logger saving stats and metrics to a CSV file.""" def __init__(self, train_folder: str, log_filename: str) -> None: """Initialize the CSVLogger class. @@ -414,7 +411,7 @@ def __init__(self, train_folder: str, log_filename: str) -> None: self._load_existing_data() def log(self, metrics: dict[str, Any], step: int | None = None) -> None: - """Logs metrics from runs + """Logs metrics from runs. Args: metrics: the metrics to log @@ -436,14 +433,14 @@ def log(self, metrics: dict[str, Any], step: int | None = None) -> None: self.save() def save(self): - """Saves the metrics to the file system""" + """Saves the metrics to the file system.""" logs = self._prepare_logs() with open(self.log_file, "w", newline="") as f: writer = csv.writer(f) writer.writerows(logs) def log_config(self, config: dict = None) -> None: - """Does not do anything as the config should already be saved + """Does not do anything as the config should already be saved. Args: config: Experiment config file. @@ -451,7 +448,7 @@ def log_config(self, config: dict = None) -> None: pass def _load_existing_data(self) -> None: - """Loads existing CSV data if the log file exists""" + """Loads existing CSV data if the log file exists.""" logging.info(f"Loading existing CSV data from {self.log_file}") try: with open(self.log_file, newline="") as f: @@ -496,13 +493,13 @@ def _load_existing_data(self) -> None: self._metric_store.extend(metric_store) def _prepare_logs(self) -> list[list]: - """Prepares the data to log as a list of strings""" + """Prepares the data to log as a list of strings.""" if len(self._metric_store) == 0: return [] metrics = list(sorted(self._logged_metrics)) logs = [["step"] + metrics] - for step, step_metrics in zip(self._steps, self._metric_store): + for step, step_metrics in zip(self._steps, self._metric_store, strict=False): # Convert None values to empty strings for proper CSV formatting row = [step] + ["" if step_metrics.get(m) is None else step_metrics.get(m) for m in metrics] logs.append(row) diff --git a/deeplabcut/pose_estimation_pytorch/runners/shelving.py b/deeplabcut/pose_estimation_pytorch/runners/shelving.py index 5d218429f9..e5112005c6 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/shelving.py +++ b/deeplabcut/pose_estimation_pytorch/runners/shelving.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Modules used to read/write shelve data during video analysis in DeepLabCut 3.0""" +"""Modules used to read/write shelve data during video analysis in DeepLabCut 3.0.""" import pickle import shelve @@ -19,7 +19,7 @@ class ShelfManager(ABC): - """Class to manage shelf data""" + """Class to manage shelf data.""" def __init__(self, filepath: str | Path, flag: str = "r") -> None: self.filepath = Path(filepath) @@ -29,7 +29,7 @@ def __init__(self, filepath: str | Path, flag: str = "r") -> None: self._open: bool = False def open(self) -> None: - """Opens the shelf""" + """Opens the shelf.""" self._db = shelve.open( str(self.filepath), flag=self.flag, @@ -38,7 +38,7 @@ def open(self) -> None: self._open = True def close(self) -> None: - """Closes the shelf""" + """Closes the shelf.""" if not self._open: return @@ -57,7 +57,7 @@ def keys(self) -> list[str]: class ShelfReader(ShelfManager): - """Reads data from a shelf""" + """Reads data from a shelf.""" def __getitem__(self, item: str) -> dict: """Reads an item from the shelf. @@ -105,7 +105,7 @@ def add_prediction( identity_scores: np.ndarray | None = None, **kwargs, ) -> None: - """Adds the prediction for a frame to the shelf + """Adds the prediction for a frame to the shelf. Args: bodyparts: The predicted bodyparts. @@ -146,7 +146,7 @@ def add_prediction( self._frame_index += 1 def close(self) -> None: - """Closes the shelf and writes the updated metadata""" + """Closes the shelf and writes the updated metadata.""" if self._open and self._frame_index > 0: # Write updated metadata to shelf (top-level indexing required for shelve) metadata = self._db["metadata"] @@ -156,7 +156,7 @@ def close(self) -> None: super().close() def open(self) -> None: - """Opens the shelf""" + """Opens the shelf.""" super().open() self._frame_index = 0 @@ -199,7 +199,7 @@ def add_prediction( features: np.ndarray | None = None, **kwargs, ) -> None: - """Adds the prediction for a frame to the shelf + """Adds the prediction for a frame to the shelf. Args: bodyparts: The predicted bodyparts. diff --git a/deeplabcut/pose_estimation_pytorch/runners/snapshots.py b/deeplabcut/pose_estimation_pytorch/runners/snapshots.py index bb094218f3..7498de0a09 100755 --- a/deeplabcut/pose_estimation_pytorch/runners/snapshots.py +++ b/deeplabcut/pose_estimation_pytorch/runners/snapshots.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Code to handle storing models""" +"""Code to handle storing models.""" from __future__ import annotations @@ -24,7 +24,7 @@ @dataclass class TorchSnapshotManager: - """Class handling model checkpoint I/O + """Class handling model checkpoint I/O. Attributes: snapshot_prefix: The prefix to use when saving snapshots. @@ -76,7 +76,7 @@ def __post_init__(self): self._key = f"metrics/{self.key_metric}" def update(self, epoch: int, state_dict: dict, last: bool = False) -> None: - """Saves the model state dict if the epoch is one that requires a save + """Saves the model state dict if the epoch is one that requires a save. Args: epoch: the number of epochs the model was trained for @@ -136,7 +136,8 @@ def best(self) -> Snapshot | None: if len(best_snapshots) > 1: warnings.warn( f"TorchSnapshotManager.best(): found multiple best snapshots (" - f"{best_snapshots}), returning the last one." + f"{best_snapshots}), returning the last one.", + stacklevel=2, ) best_snapshot = best_snapshots[-1] diff --git a/deeplabcut/pose_estimation_pytorch/runners/train.py b/deeplabcut/pose_estimation_pytorch/runners/train.py index 4220054724..6e87751d53 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/train.py +++ b/deeplabcut/pose_estimation_pytorch/runners/train.py @@ -150,7 +150,7 @@ def state_dict(self) -> dict: @abstractmethod def step(self, batch: dict[str, Any], mode: str = "train") -> dict[str, torch.Tensor]: - """Perform a single epoch gradient update or validation step + """Perform a single epoch gradient update or validation step. Args: batch: the batch data on which to run a step @@ -165,7 +165,7 @@ def step(self, batch: dict[str, Any], mode: str = "train") -> dict[str, torch.Te @abstractmethod def _compute_epoch_metrics(self) -> dict[str, float]: - """Computes the metrics using the data accumulated during an epoch + """Computes the metrics using the data accumulated during an epoch. Returns: A dictionary containing the different losses for the step @@ -339,7 +339,7 @@ def _load_scheduler_state_dict(self, load_state_dict: bool, snapshot: dict) -> N class PoseTrainingRunner(TrainingRunner[PoseModel]): - """Runner to train pose estimation models""" + """Runner to train pose estimation models.""" def __init__( self, @@ -366,7 +366,7 @@ def load_snapshot( model: PoseModel, weights_only: bool | None = None, ) -> dict: - """Loads the state dict for a model from a file + """Loads the state dict for a model from a file. This method loads a file containing a DeepLabCut PyTorch model snapshot onto a given device, and sets the model weights using the state_dict. @@ -495,7 +495,7 @@ def _update_epoch_predictions( scales: torch.Tensor, offsets: torch.Tensor, ) -> None: - """Updates the stored predictions with a new batch""" + """Updates the stored predictions with a new batch.""" epoch_gt_metric = self._epoch_ground_truth.get(name, {}) epoch_metric = self._epoch_predictions.get(name, {}) assert len(gt_keypoints) == len(pred_keypoints) @@ -508,6 +508,7 @@ def _update_epoch_predictions( pred_keypoints, scales, offsets, + strict=False, ): ground_truth = gt.detach().cpu().numpy() pred = pred.copy() @@ -526,7 +527,7 @@ def _update_epoch_predictions( class DetectorTrainingRunner(TrainingRunner[BaseDetector]): - """Runner to train object detection models""" + """Runner to train object detection models.""" def __init__(self, model: BaseDetector, optimizer: torch.optim.Optimizer, **kwargs): """ @@ -634,9 +635,9 @@ def _update_epoch_predictions( scales: torch.Tensor, offsets: torch.Tensor, ) -> None: - """Updates the stored predictions with a new batch""" + """Updates the stored predictions with a new batch.""" for img_path, img_size, img_bboxes, img_pred, scale, offset in zip( - paths, sizes, bboxes, predictions, scales, offsets + paths, sizes, bboxes, predictions, scales, offsets, strict=False ): scale_x, scale_y = scale scale_factors = np.array([scale_x, scale_y, scale_x, scale_y]) @@ -679,8 +680,7 @@ def build_training_runner( load_head_weights: bool = True, logger: BaseLogger | None = None, ) -> TrainingRunner: - """ - Build a runner object according to a pytorch configuration file + """Build a runner object according to a pytorch configuration file. Args: runner_config: the configuration for the runner diff --git a/deeplabcut/pose_estimation_pytorch/utils.py b/deeplabcut/pose_estimation_pytorch/utils.py index b2891bf92c..8b39d8f0f4 100644 --- a/deeplabcut/pose_estimation_pytorch/utils.py +++ b/deeplabcut/pose_estimation_pytorch/utils.py @@ -28,8 +28,7 @@ def create_folder(path_to_folder): def fix_seeds(seed: int) -> None: - """ - Fixes the random seed for python, numpy and pytorch + """Fixes the random seed for python, numpy and pytorch. Args: seed: the seed to set @@ -42,7 +41,7 @@ def fix_seeds(seed: int) -> None: def resolve_device(model_config: dict) -> str: - """Determines which device should be used from the model config + """Determines which device should be used from the model config. When the device is set to 'auto': If an Nvidia GPU is available, selects the device as cuda:0. diff --git a/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_builder.py b/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_builder.py index 85af4d31c6..49f8a10c68 100644 --- a/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_builder.py +++ b/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_builder.py @@ -77,19 +77,20 @@ def _encode_block_string(self, block): "r%d" % block.num_repeat, "k%d" % block.kernel_size, "s%d%d" % (block.strides[0], block.strides[1]), - "e%s" % block.expand_ratio, + f"e{block.expand_ratio}", "i%d" % block.input_filters, "o%d" % block.output_filters, "c%d" % block.conv_type, ] if block.se_ratio > 0 and block.se_ratio <= 1: - args.append("se%s" % block.se_ratio) + args.append(f"se{block.se_ratio}") if block.id_skip is False: args.append("noskip") return "_".join(args) def decode(self, string_list): """Decodes a list of string notations to specify blocks inside the network. + Args: string_list: a list of strings, each string is a notation of block. Returns: @@ -103,6 +104,7 @@ def decode(self, string_list): def encode(self, blocks_args): """Encodes a list of Blocks to a list of strings. + Args: blocks_args: A list of namedtuples to represent blocks arguments. Returns: @@ -116,6 +118,7 @@ def encode(self, blocks_args): def swish(features, use_native=True): """Computes the Swish activation function. + The tf.nn.swish operation uses a custom gradient to reduce memory usage. Since saving custom gradients in SavedModel is currently not supported, and one would not be able to use an exported TF-Hub module for fine-tuning, we @@ -187,7 +190,7 @@ def get_model_params(model_name, override_params): width_coefficient, depth_coefficient, _, dropout_rate = efficientnet_params(model_name) blocks_args, global_params = efficientnet(width_coefficient, depth_coefficient, dropout_rate) else: - raise NotImplementedError("model name is not pre-defined: %s" % model_name) + raise NotImplementedError(f"model name is not pre-defined: {model_name}") if override_params: # ValueError will be raised here if override_params has fields not included @@ -209,6 +212,7 @@ def build_model( features_only=False, ): """A helper function to creates a model and returns predicted logits. + Args: images: input images tensor. model_name: string, the predefined model name. @@ -239,10 +243,10 @@ def build_model( if not tf.io.gfile.exists(model_dir): tf.io.gfile.makedirs(model_dir) with tf.io.gfile.GFile(param_file, "w") as f: - tf.compat.v1.logging.info("writing to %s" % param_file) - f.write("model_name= %s\n\n" % model_name) - f.write("global_params= %s\n\n" % str(global_params)) - f.write("blocks_args= %s\n\n" % str(blocks_args)) + tf.compat.v1.logging.info(f"writing to {param_file}") + f.write(f"model_name= {model_name}\n\n") + f.write(f"global_params= {str(global_params)}\n\n") + f.write(f"blocks_args= {str(blocks_args)}\n\n") with tf.compat.v1.variable_scope(model_name): model = efficientnet_model.Model(blocks_args, global_params) @@ -253,6 +257,7 @@ def build_model( def build_model_base(images, model_name, use_batch_norm=False, drop_out=False, override_params=None): """A helper function to create a base model and return global_pool. + Args: images: input images tensor. model_name: string, the predefined model name. diff --git a/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_model.py b/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_model.py index d39dc9f578..98e5e3a05c 100644 --- a/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_model.py +++ b/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_model.py @@ -14,6 +14,7 @@ # limitations under the License. # """Contains definitions for EfficientNet model. + [1] Mingxing Tan, Quoc V. Le EfficientNet: Rethinking Model Scaling for Convolutional Neural Networks. ICML'19, https://arxiv.org/abs/1905.11946 @@ -68,6 +69,7 @@ def conv_kernel_initializer(shape, dtype=None, partition_info=None): """Initialization for convolutional kernels. + The main difference with tf.variance_scaling_initializer is that tf.variance_scaling_initializer uses a truncated normal with an uncorrected standard deviation, whereas here we use a normal distribution. Similarly, @@ -88,6 +90,7 @@ def conv_kernel_initializer(shape, dtype=None, partition_info=None): def dense_kernel_initializer(shape, dtype=None, partition_info=None): """Initialization for dense kernels. + This initialization is equal to tf.variance_scaling_initializer(scale=1.0/3.0, mode='fan_out', distribution='uniform'). @@ -139,6 +142,7 @@ class MBConvBlock(tf.keras.layers.Layer): def __init__(self, block_args, global_params): """Initializes a MBConv block. + Args: block_args: BlockArgs, arguments to create a Block. global_params: GlobalParams, a set of global parameters. @@ -246,6 +250,7 @@ def _build(self): def _call_se(self, input_tensor): """Call Squeeze and Excitation layer. + Args: input_tensor: Tensor, a single input tensor for Squeeze/Excitation layer. Returns: @@ -253,11 +258,12 @@ def _call_se(self, input_tensor): """ se_tensor = tf.reduce_mean(input_tensor=input_tensor, axis=self._spatial_dims, keepdims=True) se_tensor = self._se_expand(self._relu_fn(self._se_reduce(se_tensor))) - tf.compat.v1.logging.info("Built Squeeze and Excitation with tensor shape: %s" % (se_tensor.shape)) + tf.compat.v1.logging.info(f"Built Squeeze and Excitation with tensor shape: {se_tensor.shape}") return tf.sigmoid(se_tensor) * input_tensor def call(self, inputs, use_batch_norm=False, drop_out=False, drop_connect_rate=None): """Implementation of call(). + Args: inputs: the inputs tensor. training: boolean, whether the model is constructed for training. @@ -265,15 +271,15 @@ def call(self, inputs, use_batch_norm=False, drop_out=False, drop_connect_rate=N Returns: A output tensor. """ - tf.compat.v1.logging.info("Block input: %s shape: %s" % (inputs.name, inputs.shape)) + tf.compat.v1.logging.info(f"Block input: {inputs.name} shape: {inputs.shape}") if self._block_args.expand_ratio != 1: x = self._relu_fn(self._bn0(self._expand_conv(inputs), training=use_batch_norm)) else: x = inputs - tf.compat.v1.logging.info("Expand: %s shape: %s" % (x.name, x.shape)) + tf.compat.v1.logging.info(f"Expand: {x.name} shape: {x.shape}") x = self._relu_fn(self._bn1(self._depthwise_conv(x), training=use_batch_norm)) - tf.compat.v1.logging.info("DWConv: %s shape: %s" % (x.name, x.shape)) + tf.compat.v1.logging.info(f"DWConv: {x.name} shape: {x.shape}") if self._has_se: with tf.compat.v1.variable_scope("se"): @@ -291,7 +297,7 @@ def call(self, inputs, use_batch_norm=False, drop_out=False, drop_connect_rate=N if drop_connect_rate: x = utils.drop_connect(x, drop_out, drop_connect_rate) x = tf.add(x, inputs) - tf.compat.v1.logging.info("Project: %s shape: %s" % (x.name, x.shape)) + tf.compat.v1.logging.info(f"Project: {x.name} shape: {x.shape}") return x @@ -335,6 +341,7 @@ def _build(self): def call(self, inputs, use_batch_norm=False, drop_out=False, drop_connect_rate=None): """Implementation of call(). + Args: inputs: the inputs tensor. training: boolean, whether the model is constructed for training. @@ -342,12 +349,12 @@ def call(self, inputs, use_batch_norm=False, drop_out=False, drop_connect_rate=N Returns: A output tensor. """ - tf.compat.v1.logging.info("Block input: %s shape: %s" % (inputs.name, inputs.shape)) + tf.compat.v1.logging.info(f"Block input: {inputs.name} shape: {inputs.shape}") if self._block_args.expand_ratio != 1: x = self._relu_fn(self._bn0(self._expand_conv(inputs), training=use_batch_norm)) else: x = inputs - tf.compat.v1.logging.info("Expand: %s shape: %s" % (x.name, x.shape)) + tf.compat.v1.logging.info(f"Expand: {x.name} shape: {x.shape}") self.endpoints = {"expansion_output": x} @@ -361,17 +368,19 @@ def call(self, inputs, use_batch_norm=False, drop_out=False, drop_connect_rate=N if drop_connect_rate: x = utils.drop_connect(x, drop_out, drop_connect_rate) x = tf.add(x, inputs) - tf.compat.v1.logging.info("Project: %s shape: %s" % (x.name, x.shape)) + tf.compat.v1.logging.info(f"Project: {x.name} shape: {x.shape}") return x class Model(tf.keras.Model): """A class implements tf.keras.Model for MNAS-like model. + Reference: https://arxiv.org/abs/1807.11626 """ def __init__(self, blocks_args=None, global_params=None): """Initializes an `Model` instance. + Args: blocks_args: A list of BlockArgs to construct block modules. global_params: GlobalParams, a set of global parameters. @@ -463,6 +472,7 @@ def _build(self): def call(self, inputs, use_batch_norm=False, drop_out=False, features_only=None): """Implementation of call(). + Args: inputs: input tensors. training: boolean, whether the model is constructed for training. @@ -475,7 +485,7 @@ def call(self, inputs, use_batch_norm=False, drop_out=False, features_only=None) # Calls Stem layers with tf.compat.v1.variable_scope("stem"): outputs = self._relu_fn(self._bn0(self._conv_stem(inputs), training=use_batch_norm)) - tf.compat.v1.logging.info("Built stem layers with output shape: %s" % outputs.shape) + tf.compat.v1.logging.info(f"Built stem layers with output shape: {outputs.shape}") self.endpoints["stem"] = outputs # Calls blocks. @@ -486,25 +496,25 @@ def call(self, inputs, use_batch_norm=False, drop_out=False, features_only=None) is_reduction = True reduction_idx += 1 - with tf.compat.v1.variable_scope("blocks_%s" % idx): + with tf.compat.v1.variable_scope(f"blocks_{idx}"): drop_rate = self._global_params.drop_connect_rate if drop_rate: drop_rate *= float(idx) / len(self._blocks) - tf.compat.v1.logging.info("block_%s drop_connect_rate: %s" % (idx, drop_rate)) + tf.compat.v1.logging.info(f"block_{idx} drop_connect_rate: {drop_rate}") outputs = block.call( outputs, use_batch_norm=use_batch_norm, drop_out=drop_out, drop_connect_rate=drop_rate, ) - self.endpoints["block_%s" % idx] = outputs + self.endpoints[f"block_{idx}"] = outputs if is_reduction: - self.endpoints["reduction_%s" % reduction_idx] = outputs + self.endpoints[f"reduction_{reduction_idx}"] = outputs if block.endpoints: for k, v in block.endpoints.items(): - self.endpoints["block_%s/%s" % (idx, k)] = v + self.endpoints[f"block_{idx}/{k}"] = v if is_reduction: - self.endpoints["reduction_%s/%s" % (reduction_idx, k)] = v + self.endpoints[f"reduction_{reduction_idx}/{k}"] = v self.endpoints["features"] = outputs if not features_only: diff --git a/deeplabcut/pose_estimation_tensorflow/config.py b/deeplabcut/pose_estimation_tensorflow/config.py index 2bbf60d53f..a629c103f8 100644 --- a/deeplabcut/pose_estimation_tensorflow/config.py +++ b/deeplabcut/pose_estimation_tensorflow/config.py @@ -19,10 +19,8 @@ def _merge_a_into_b(a, b): - """ - Merge config dictionary a into config dictionary b, clobbering the - options in b whenever they are also specified in a. - """ + """Merge config dictionary a into config dictionary b, clobbering the options in b + whenever they are also specified in a.""" for k, v in a.items(): # a must specify keys that are in b # if k not in b: @@ -43,9 +41,7 @@ def _merge_a_into_b(a, b): def cfg_from_file(filename): - """ - Load a config from file filename and merge it into the default options. - """ + """Load a config from file filename and merge it into the default options.""" with open(filename) as f: yaml_cfg = yaml.load(f, Loader=yaml.SafeLoader) diff --git a/deeplabcut/pose_estimation_tensorflow/core/evaluate.py b/deeplabcut/pose_estimation_tensorflow/core/evaluate.py index 556dbf2140..dc7b5b5c29 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/evaluate.py +++ b/deeplabcut/pose_estimation_tensorflow/core/evaluate.py @@ -22,7 +22,10 @@ def pairwisedistances(DataCombined, scorer1, scorer2, pcutoff=-1, bodyparts=None): - """Calculates the pairwise Euclidean distance metric over body parts vs. images""" + """Calculates the pairwise Euclidean distance metric over body parts vs. + + images + """ mask = DataCombined[scorer2].xs("likelihood", level=1, axis=1) >= pcutoff if bodyparts is None: Pointwisesquareddistance = (DataCombined[scorer1] - DataCombined[scorer2]) ** 2 @@ -165,7 +168,7 @@ def calculatepafdistancebounds(config, shuffle=0, trainingsetindex=0, modelprefi def Plotting(cfg, comparisonbodyparts, DLCscorer, trainIndices, DataCombined, foldername): - """Function used for plotting GT and predictions""" + """Function used for plotting GT and predictions.""" from deeplabcut.utils import visualization colors = visualization.get_cmap(len(comparisonbodyparts), name=cfg["colormap"]) @@ -199,9 +202,10 @@ def return_evaluate_network_data( modelprefix="", returnjustfns=True, ): - """ - Returns the results for (previously evaluated) network. deeplabcut.evaluate_network(..) - Returns list of (per model): [trainingsiterations,trainfraction,shuffle,trainerror,testerror,pcutoff,trainerrorpcutoff,testerrorpcutoff,Snapshots[snapindex],scale,net_type] + """Returns the results for (previously evaluated) network. + deeplabcut.evaluate_network(..) Returns list of (per model): [trainingsiterations,tr + ainfraction,shuffle,trainerror,testerror,pcutoff,trainerrorpcutoff,testerrorpcutoff, + Snapshots[snapindex],scale,net_type] If fulldata=True, also returns (the complete annotation and prediction array) Returns list of: (DataMachine, Data, data, trainIndices, testIndices, trainFraction, DLCscorer,comparisonbodyparts, cfg, Snapshots[snapindex]) @@ -441,7 +445,7 @@ def keypoint_error( train_indices: list[int], test_indices: list[int], ) -> pd.DataFrame: - """Computes the RMSE error for each bodypart + """Computes the RMSE error for each bodypart. The error dataframes can be in single animal format (non-hierarchical columns, one column for each bodypart) or multi-animal format (hierarchical columns with 3 @@ -947,10 +951,10 @@ def evaluate_network( def make_results_file(final_result, evaluationfolder, DLCscorer): - """ - Makes result file in csv format and saves under evaluation_results directory. - If the file exists (typically, when the network has already been evaluated), - newer results are appended to it. + """Makes result file in csv format and saves under evaluation_results directory. + + If the file exists (typically, when the network has already been evaluated), newer + results are appended to it. """ col_names = [ "Training iterations:", @@ -985,8 +989,7 @@ def get_available_requested_snapshots( requested_snapshots: list[str], available_snapshots: list[str], ) -> list[str]: - """ - Intersects the requested snapshot names with the available snapshots. + """Intersects the requested snapshot names with the available snapshots. Returns: snapshot names """ @@ -1010,8 +1013,9 @@ def get_snapshots_by_index( idx: int | str, available_snapshots: list[str], ) -> list[str]: - """ - Assume available_snapshots is ordered in ascending order. Returns snapshot names. + """Assume available_snapshots is ordered in ascending order. + + Returns snapshot names. """ if isinstance(idx, int) and -len(available_snapshots) <= idx < len(available_snapshots): return [available_snapshots[idx]] diff --git a/deeplabcut/pose_estimation_tensorflow/core/evaluate_multianimal.py b/deeplabcut/pose_estimation_tensorflow/core/evaluate_multianimal.py index 4692ce677d..42b4a7b63e 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/evaluate_multianimal.py +++ b/deeplabcut/pose_estimation_tensorflow/core/evaluate_multianimal.py @@ -56,7 +56,7 @@ def _compute_stats(df): def _calc_prediction_error(data): _ = data.pop("metadata", None) dists = [] - for n, dict_ in enumerate(tqdm(data.values())): + for _n, dict_ in enumerate(tqdm(data.values())): gt = np.concatenate(dict_["groundtruth"][1]) xy = np.concatenate(dict_["prediction"]["coordinates"][0]) p = np.concatenate(dict_["prediction"]["confidence"]) @@ -88,7 +88,7 @@ def _calc_train_test_error(data, metadata, pcutoff=0.3): def evaluate_multianimal_full( config, - Shuffles=[1], + Shuffles=None, trainingsetindex=0, plotting=False, show_errors=True, @@ -113,6 +113,8 @@ def evaluate_multianimal_full( conversioncode, ) + if Shuffles is None: + Shuffles = [1] if "TF_CUDNN_USE_AUTOTUNE" in os.environ: del os.environ["TF_CUDNN_USE_AUTOTUNE"] # was potentially set during training @@ -328,7 +330,7 @@ def evaluate_multianimal_full( temp["bodyparts"] = ( temp["bodyparts"] .replace( - dict(zip(joints, range(len(joints)))), + dict(zip(joints, range(len(joints)), strict=False)), ) .infer_objects(copy=False) ) diff --git a/deeplabcut/pose_estimation_tensorflow/core/openvino/session.py b/deeplabcut/pose_estimation_tensorflow/core/openvino/session.py index d09d2f357a..51490a90bc 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/openvino/session.py +++ b/deeplabcut/pose_estimation_tensorflow/core/openvino/session.py @@ -100,7 +100,7 @@ def completion_callback(request, inp_id): def GetPoseF_OV(cfg, dlc_cfg, sess, inputs, outputs, cap, nframes, batchsize): - """Prediction of pose""" + """Prediction of pose.""" PredictedData = np.zeros((nframes, 3 * len(dlc_cfg["all_joints_names"]))) ny, nx = int(cap.get(4)), int(cap.get(3)) if cfg["cropping"]: diff --git a/deeplabcut/pose_estimation_tensorflow/core/predict.py b/deeplabcut/pose_estimation_tensorflow/core/predict.py index 6c9d374808..9ff985d66e 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/predict.py +++ b/deeplabcut/pose_estimation_tensorflow/core/predict.py @@ -59,7 +59,7 @@ def setup_pose_prediction(cfg, allow_growth=False, collect_extra=False): def extract_cnn_output(outputs_np, cfg): - """extract locref + scmap from network""" + """Extract locref + scmap from network.""" scmap = outputs_np[0] scmap = np.squeeze(scmap) locref = None @@ -110,7 +110,7 @@ def multi_pose_predict(scmap, locref, stride, num_outputs): def getpose(image, cfg, sess, inputs, outputs, outall=False): - """Extract pose""" + """Extract pose.""" im = np.expand_dims(image, axis=0).astype(float) outputs_np = sess.run(outputs, feed_dict={inputs: im}) scmap, locref = extract_cnn_output(outputs_np, cfg) @@ -160,7 +160,9 @@ def get_top_values(scmap, n_top=5): def getposeNP(image, cfg, sess, inputs, outputs, outall=False): """Adapted from DeeperCut, performs numpy-based faster inference on batches. - Introduced in https://www.biorxiv.org/content/10.1101/457242v1""" + + Introduced in https://www.biorxiv.org/content/10.1101/457242v1 + """ num_outputs = cfg.get("num_outputs", 1) outputs_np = sess.run(outputs, feed_dict={inputs: image}) diff --git a/deeplabcut/pose_estimation_tensorflow/core/predict_multianimal.py b/deeplabcut/pose_estimation_tensorflow/core/predict_multianimal.py index 126753f1d9..95c426cfa2 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/predict_multianimal.py +++ b/deeplabcut/pose_estimation_tensorflow/core/predict_multianimal.py @@ -17,7 +17,7 @@ def extract_cnn_output(outputs_np, cfg): - """extract locref, scmap and partaffinityfield from network""" + """Extract locref, scmap and partaffinityfield from network.""" scmap = outputs_np[0] scmap = np.squeeze(scmap) if cfg["location_refinement"]: @@ -87,7 +87,7 @@ def compute_edge_costs( idx = np.arange(peaks.shape[0]) idx_per_bpt = {j: idx[bpt_inds == j].tolist() for j in range(n_bodyparts)} edges = [] - for k, (s, t) in zip(paf_inds, graph): + for k, (s, t) in zip(paf_inds, graph, strict=False): inds_s = idx_per_bpt[s] inds_t = idx_per_bpt[t] if not (inds_s and inds_t): diff --git a/deeplabcut/pose_estimation_tensorflow/datasets/pose_imgaug.py b/deeplabcut/pose_estimation_tensorflow/datasets/pose_imgaug.py index 413d544084..0841d8343c 100644 --- a/deeplabcut/pose_estimation_tensorflow/datasets/pose_imgaug.py +++ b/deeplabcut/pose_estimation_tensorflow/datasets/pose_imgaug.py @@ -8,9 +8,9 @@ # # Licensed under GNU Lesser General Public License v3.0 # -""" -Uses imgaug dataflow for flexible augmentation -Largely written by Mert Yüksekgönül during the summer in the Bethge lab -- Thanks! +"""Uses imgaug dataflow for flexible augmentation Largely written by Mert Yüksekgönül +during the summer in the Bethge lab -- Thanks! + https://imgaug.readthedocs.io/en/latest/ """ diff --git a/deeplabcut/pose_estimation_tensorflow/datasets/pose_multianimal_imgaug.py b/deeplabcut/pose_estimation_tensorflow/datasets/pose_multianimal_imgaug.py index 0456e57890..918671fbe3 100644 --- a/deeplabcut/pose_estimation_tensorflow/datasets/pose_multianimal_imgaug.py +++ b/deeplabcut/pose_estimation_tensorflow/datasets/pose_multianimal_imgaug.py @@ -126,13 +126,13 @@ def _load_pseudo_data_from_h5(self, cfg, threshold=0.5, mask_kpts_below_thresh=F assert os.path.exists(gt_file) path_ = Path(gt_file) print("Using gt file:", path_.name) - num_kpts = len(cfg["all_joints_names"]) + len(cfg["all_joints_names"]) df = pd.read_hdf(gt_file) video_name = path_.name.split("DLC")[0] video_root = str(path_.parents[0] / video_name) itemlist = [] - for image_id, imagename in enumerate(df.index): + for _image_id, imagename in enumerate(df.index): item = DataItem() data = df.loc[imagename] # 3 for likelihood @@ -178,7 +178,9 @@ def _load_pseudo_data_from_h5(self, cfg, threshold=0.5, mask_kpts_below_thresh=F def build_augmentation_pipeline(self, apply_prob=0.5): cfg = self.cfg - sometimes = lambda aug: iaa.Sometimes(apply_prob, aug) + def sometimes(aug): + return iaa.Sometimes(apply_prob, aug) + pipeline = iaa.Sequential(random_order=False) pre_resize = cfg.get("pre_resize") @@ -312,7 +314,7 @@ def get_aug_param(cfg_value): return pipeline def get_batch_from_video(self): - num_images = len(self.vid) + len(self.vid) batch_images = [] batch_joints = [] joint_ids = [] @@ -469,7 +471,7 @@ def next_batch(self, plotting=False): batch_joints_valid = [] joint_ids_valid = [] - for joints, ids in zip(batch_joints, joint_ids): + for joints, ids in zip(batch_joints, joint_ids, strict=False): # Invisible joints are represented by nans visible = ~np.isnan(joints[:, 0]) inside = np.logical_and.reduce( @@ -543,7 +545,7 @@ def compute_scmap_weights(self, scmap_shape, joint_id): cfg = self.cfg if cfg["weigh_only_present_joints"]: weights = np.zeros(scmap_shape) - for k, j_id in enumerate(np.concatenate(joint_id)): # looping over all animals + for _k, j_id in enumerate(np.concatenate(joint_id)): # looping over all animals weights[:, :, j_id] = 1.0 else: weights = np.ones(scmap_shape) @@ -702,11 +704,11 @@ def gaussian_scmap(self, joint_id, coords, data_item, size, scale): for k, j_id in enumerate(np.concatenate(joint_id)): joint_pt = coords[0][k, :] j_x = joint_pt[0].item() - j_x_sm = round((j_x - half_stride) / stride) + round((j_x - half_stride) / stride) j_y = joint_pt[1].item() - j_y_sm = round((j_y - half_stride) / stride) + round((j_y - half_stride) / stride) - map_j = grid.copy() + grid.copy() # Distance between the joint point and each coordinate dist = np.linalg.norm(grid - (j_y, j_x), axis=2) ** 2 scmap_j = np.exp(-dist / (2 * (std**2))) diff --git a/deeplabcut/pose_estimation_tensorflow/datasets/utils.py b/deeplabcut/pose_estimation_tensorflow/datasets/utils.py index 82ec06667b..454a876809 100644 --- a/deeplabcut/pose_estimation_tensorflow/datasets/utils.py +++ b/deeplabcut/pose_estimation_tensorflow/datasets/utils.py @@ -43,7 +43,9 @@ def mirror_joints_map(all_joints, num_joints): def crop_image(joints, im, Xlabel, Ylabel, cfg): """Randomly cropping image around xlabel,ylabel taking into account size of image. - Introduced in DLC 2.0 (Nature Protocols paper)""" + + Introduced in DLC 2.0 (Nature Protocols paper) + """ widthforward = int(cfg["minsize"] + np.random.randint(cfg["rightwidth"])) widthback = int(cfg["minsize"] + np.random.randint(cfg["leftwidth"])) hup = int(cfg["minsize"] + np.random.randint(cfg["topheight"])) diff --git a/deeplabcut/pose_estimation_tensorflow/export.py b/deeplabcut/pose_estimation_tensorflow/export.py index 5a38577a82..d1e716069a 100644 --- a/deeplabcut/pose_estimation_tensorflow/export.py +++ b/deeplabcut/pose_estimation_tensorflow/export.py @@ -57,9 +57,7 @@ def create_deploy_config_template(): def write_deploy_config(configname, cfg): - """ - - CURRENTLY NOT IMPLEMENTED + """CURRENTLY NOT IMPLEMENTED. Write structured config file. """ @@ -78,10 +76,8 @@ def write_deploy_config(configname, cfg): def load_model(cfg, shuffle=1, trainingsetindex=0, TFGPUinference=True, modelprefix=""): - """ - - Loads a tensorflow session with a DLC model from the associated configuration - Return a tensorflow session with DLC model given cfg and shuffle + """Loads a tensorflow session with a DLC model from the associated configuration + Return a tensorflow session with DLC model given cfg and shuffle. Parameters: ----------- @@ -115,7 +111,7 @@ def load_model(cfg, shuffle=1, trainingsetindex=0, TFGPUinference=True, modelpre cfg["project_path"], str(auxiliaryfunctions.get_model_folder(train_fraction, shuffle, cfg, modelprefix=modelprefix)), ) - path_test_config = os.path.normpath(model_folder + "/test/pose_cfg.yaml") + os.path.normpath(model_folder + "/test/pose_cfg.yaml") path_train_config = os.path.normpath(model_folder + "/train/pose_cfg.yaml") try: @@ -123,7 +119,7 @@ def load_model(cfg, shuffle=1, trainingsetindex=0, TFGPUinference=True, modelpre # dlc_cfg_train = load_config(str(path_train_config)) except FileNotFoundError: raise FileNotFoundError( - "It seems the model for shuffle %s and trainFraction %s does not exist." % (shuffle, train_fraction) + f"It seems the model for shuffle {shuffle} and trainFraction {train_fraction} does not exist." ) Snapshots = auxiliaryfunctions.get_snapshots_from_folder( @@ -142,7 +138,7 @@ def load_model(cfg, shuffle=1, trainingsetindex=0, TFGPUinference=True, modelpre # Check if data already was generated: dlc_cfg["init_weights"] = os.path.join(model_folder, "train", Snapshots[snapshotindex]) - trainingsiterations = (dlc_cfg["init_weights"].split(os.sep)[-1]).split("-")[-1] + (dlc_cfg["init_weights"].split(os.sep)[-1]).split("-")[-1] dlc_cfg["num_outputs"] = cfg.get("num_outputs", dlc_cfg.get("num_outputs", 1)) dlc_cfg["batch_size"] = None @@ -214,9 +210,7 @@ def export_model( wipepaths=False, modelprefix="", ): - """ - - Export DeepLabCut models for the model zoo or for live inference. + """Export DeepLabCut models for the model zoo or for live inference. Saves the pose configuration, snapshot files, and frozen TF graph of the model to directory named exported-models within the project directory @@ -267,7 +261,7 @@ def export_model( try: cfg = auxiliaryfunctions.read_config(cfg_path) except FileNotFoundError: - FileNotFoundError("The config.yaml file at %s does not exist." % cfg_path) + FileNotFoundError(f"The config.yaml file at {cfg_path} does not exist.") cfg["project_path"] = os.path.dirname(os.path.realpath(cfg_path)) cfg["iteration"] = iteration if iteration is not None else cfg["iteration"] @@ -278,7 +272,7 @@ def export_model( sess, input, output, dlc_cfg = load_model(cfg, shuffle, trainingsetindex, TFGPUinference, modelprefix) ckpt = dlc_cfg["init_weights"] - model_dir = os.path.dirname(ckpt) + os.path.dirname(ckpt) ### set up export directory @@ -296,7 +290,7 @@ def export_model( if os.path.isdir(full_export_dir): if not overwrite: - raise FileExistsError("Export directory %s already exists. Terminating export..." % full_export_dir) + raise FileExistsError(f"Export directory {full_export_dir} already exists. Terminating export...") else: os.mkdir(full_export_dir) @@ -322,7 +316,7 @@ def export_model( ckpt_files = glob.glob(ckpt + "*") ckpt_dest = [os.path.normpath(full_export_dir + "/" + os.path.basename(ckf)) for ckf in ckpt_files] - for ckf, ckd in zip(ckpt_files, ckpt_dest): + for ckf, ckd in zip(ckpt_files, ckpt_dest, strict=False): shutil.copy(ckf, ckd) ### create pbtxt and pb files for checkpoint in export directory diff --git a/deeplabcut/pose_estimation_tensorflow/modelzoo/api/spatiotemporal_adapt.py b/deeplabcut/pose_estimation_tensorflow/modelzoo/api/spatiotemporal_adapt.py index a0472dd56f..af2b0af958 100644 --- a/deeplabcut/pose_estimation_tensorflow/modelzoo/api/spatiotemporal_adapt.py +++ b/deeplabcut/pose_estimation_tensorflow/modelzoo/api/spatiotemporal_adapt.py @@ -36,8 +36,7 @@ def __init__( customized_pose_config="", init_weights="", ): - """ - This class supports video adaptation to a super model. + """This class supports video adaptation to a super model. Parameters ---------- @@ -73,8 +72,6 @@ def __init__( adapter.before_adapt_inference() adapter.adaptation_training() adapter.after_adapt_inference() - - """ if scale_list is None: scale_list = [] @@ -173,8 +170,9 @@ def train_without_project(self, pseudo_label_path, **kwargs): ) def adaptation_training(self, displayiters=500, saveiters=1000, **kwargs): - """ - There should be two choices, either taking a config, with is then assuming there is a DLC project. + """There should be two choices, either taking a config, with is then assuming + there is a DLC project. + Or we make up a fake one, then we use a light way convention to do adaptation """ diff --git a/deeplabcut/pose_estimation_tensorflow/nnets/base.py b/deeplabcut/pose_estimation_tensorflow/nnets/base.py index 55433acb66..1737ba22c4 100644 --- a/deeplabcut/pose_estimation_tensorflow/nnets/base.py +++ b/deeplabcut/pose_estimation_tensorflow/nnets/base.py @@ -124,6 +124,7 @@ def prediction_layers( def inference(self, inputs): """Direct TF inference on GPU. + Added with: https://arxiv.org/abs/1909.11229 """ heads = self.get_net(inputs) @@ -174,7 +175,7 @@ def inference(self, inputs): return {"pose": pose} def add_inference_layers(self, heads): - """initialized during inference""" + """Initialized during inference.""" prob = tf.sigmoid(heads["part_pred"]) nms_radius = int(self.cfg.get("nmsradius", 5)) diff --git a/deeplabcut/pose_estimation_tensorflow/nnets/efficientnet.py b/deeplabcut/pose_estimation_tensorflow/nnets/efficientnet.py index ca359be93e..3da6d3cbbf 100644 --- a/deeplabcut/pose_estimation_tensorflow/nnets/efficientnet.py +++ b/deeplabcut/pose_estimation_tensorflow/nnets/efficientnet.py @@ -8,12 +8,10 @@ # # Licensed under GNU Lesser General Public License v3.0 # -""" - -Effnet added by T. Biasi & AM -Efficient Nets added by T. Biasi & AM -See https://openaccess.thecvf.com/content/WACV2021/html/Mathis_Pretraining_Boosts_Out-of-Domain_Robustness_for_Pose_Estimation_WACV_2021_paper.html +"""Effnet added by T. +Biasi & AM Efficient Nets added by T. Biasi & AM See +https://openaccess.thecvf.com/content/WACV2021/html/Mathis_Pretraining_Boosts_Out-of-Domain_Robustness_for_Pose_Estimation_WACV_2021_paper.html """ import tensorflow as tf diff --git a/deeplabcut/pose_estimation_tensorflow/nnets/multi.py b/deeplabcut/pose_estimation_tensorflow/nnets/multi.py index 76fe20ab4a..28c08fdbde 100644 --- a/deeplabcut/pose_estimation_tensorflow/nnets/multi.py +++ b/deeplabcut/pose_estimation_tensorflow/nnets/multi.py @@ -281,7 +281,6 @@ def prediction_layers( stage_hm_output = stage1_hm_out for i in range(2, 5): - pre_stage_paf_output = stage_paf_output pre_stage_hm_output = stage_hm_output stage_paf_output = prediction_layer_stage( diff --git a/deeplabcut/pose_estimation_tensorflow/nnets/utils.py b/deeplabcut/pose_estimation_tensorflow/nnets/utils.py index 11cb7b214b..3bc6891148 100644 --- a/deeplabcut/pose_estimation_tensorflow/nnets/utils.py +++ b/deeplabcut/pose_estimation_tensorflow/nnets/utils.py @@ -112,7 +112,7 @@ def build_learning_rate( elif lr_decay_type == "constant": lr = initial_lr else: - assert False, "Unknown lr_decay_type : %s" % lr_decay_type + raise AssertionError(f"Unknown lr_decay_type : {lr_decay_type}") if warmup_epochs: tf.compat.v1.logging.info("Learning rate warmup_epochs: %d" % warmup_epochs) @@ -169,9 +169,7 @@ def _cross_replica_average(t, num_shards_per_group): def _moments(self, inputs, reduction_axes, keep_dims): """Compute the mean and variance: it overrides the original _moments.""" - shard_mean, shard_variance = super()._moments( - inputs, reduction_axes, keep_dims=keep_dims - ) + shard_mean, shard_variance = super()._moments(inputs, reduction_axes, keep_dims=keep_dims) num_shards = tpu_function.get_tpu_context().number_of_shards or 1 if num_shards <= 8: # Skip cross_replica for 2x2 or smaller slices. diff --git a/deeplabcut/pose_estimation_tensorflow/predict_multianimal.py b/deeplabcut/pose_estimation_tensorflow/predict_multianimal.py index df6a8b9daf..a8b7248c95 100644 --- a/deeplabcut/pose_estimation_tensorflow/predict_multianimal.py +++ b/deeplabcut/pose_estimation_tensorflow/predict_multianimal.py @@ -81,7 +81,7 @@ def extract_bpt_feature_from_video( nx, ny, ) - start = time.time() + time.time() print("Starting to extract posture") if int(dlc_cfg["batch_size"]) > 1: @@ -116,7 +116,7 @@ def AnalyzeMultiAnimalVideo( robust_nframes=False, use_shelve=False, ): - """Helper function for analyzing a video with multiple individuals""" + """Helper function for analyzing a video with multiple individuals.""" print("Starting to analyze % ", video) vname = Path(video).stem @@ -191,7 +191,7 @@ def AnalyzeMultiAnimalVideo( stop = time.time() - if cfg["cropping"] == True: + if cfg["cropping"]: coords = [cfg["x1"], cfg["x2"], cfg["y1"], cfg["y2"]] else: coords = [0, nx, 0, ny] @@ -212,7 +212,7 @@ def AnalyzeMultiAnimalVideo( "cropping_parameters": coords, } metadata = {"data": dictionary} - print("Video Analyzed. Saving results in %s..." % (destfolder)) + print(f"Video Analyzed. Saving results in {destfolder}...") if use_shelve: metadata_path = dataname.split(".h5")[0] + "_meta.pickle" @@ -252,7 +252,7 @@ def GetPoseandCostsF_from_assemblies( feature_dict, extra_dict, ): - """Batchwise prediction of pose""" + """Batchwise prediction of pose.""" strwidth = int(np.ceil(np.log10(nframes))) # width for strings batch_ind = 0 # keeps track of which image within a batch should be written to batch_num = 0 # keeps track of which batch you are at @@ -289,7 +289,7 @@ def GetPoseandCostsF_from_assemblies( continue D, features = preds - for i, (ind, data) in enumerate(zip(inds, D)): + for i, (ind, data) in enumerate(zip(inds, D, strict=False)): PredicteData["frame" + str(ind).zfill(strwidth)] = data raw_coords = assemblies.get(ind) if raw_coords is None: @@ -315,7 +315,7 @@ def GetPoseandCostsF_from_assemblies( continue D, features = preds - for i, (ind, data) in enumerate(zip(inds, D)): + for i, (ind, data) in enumerate(zip(inds, D, strict=False)): PredicteData["frame" + str(ind).zfill(strwidth)] = data raw_coords = assemblies.get(ind) if raw_coords is None: @@ -358,7 +358,7 @@ def GetPoseandCostsF( batchsize, shelf_path, ): - """Batchwise prediction of pose""" + """Batchwise prediction of pose.""" strwidth = int(np.ceil(np.log10(nframes))) # width for strings batch_ind = 0 # keeps track of which image within a batch should be written to batch_num = 0 # keeps track of which batch you are at @@ -408,7 +408,7 @@ def GetPoseandCostsF( inputs, outputs, ) - for ind, data in zip(inds, D): + for ind, data in zip(inds, D, strict=False): db["frame" + str(ind).zfill(strwidth)] = data del D batch_ind = 0 @@ -425,7 +425,7 @@ def GetPoseandCostsF( inputs, outputs, ) - for ind, data in zip(inds, D): + for ind, data in zip(inds, D, strict=False): db["frame" + str(ind).zfill(strwidth)] = data del D break diff --git a/deeplabcut/pose_estimation_tensorflow/predict_videos.py b/deeplabcut/pose_estimation_tensorflow/predict_videos.py index 0ccb2a48ad..c4f987bbd4 100644 --- a/deeplabcut/pose_estimation_tensorflow/predict_videos.py +++ b/deeplabcut/pose_estimation_tensorflow/predict_videos.py @@ -684,7 +684,7 @@ def checkcropping(cfg, cap): def GetPoseF(cfg, dlc_cfg, sess, inputs, outputs, cap, nframes, batchsize): - """Batchwise prediction of pose""" + """Batchwise prediction of pose.""" PredictedData = np.zeros((nframes, dlc_cfg["num_outputs"] * 3 * len(dlc_cfg["all_joints_names"]))) batch_ind = 0 # keeps track of which image within a batch should be written to batch_num = 0 # keeps track of which batch you are at @@ -801,7 +801,7 @@ def GetPoseS_GTF(cfg, dlc_cfg, sess, inputs, outputs, cap, nframes): def GetPoseF_GTF(cfg, dlc_cfg, sess, inputs, outputs, cap, nframes, batchsize): - """Batchwise prediction of pose""" + """Batchwise prediction of pose.""" PredictedData = np.zeros((nframes, 3 * len(dlc_cfg["all_joints_names"]))) batch_ind = 0 # keeps track of which image within a batch should be written to batch_num = 0 # keeps track of which batch you are at @@ -860,7 +860,8 @@ def getboundingbox(x, y, nx, ny, margin): def GetPoseDynamic(cfg, dlc_cfg, sess, inputs, outputs, cap, nframes, detectiontreshold, margin): - """Non batch wise pose estimation for video cap by dynamically cropping around previously detected parts.""" + """Non batch wise pose estimation for video cap by dynamically cropping around + previously detected parts.""" if cfg["cropping"]: ny, nx = checkcropping(cfg, cap) else: @@ -1056,7 +1057,7 @@ def AnalyzeVideo( def GetPosesofFrames(cfg, dlc_cfg, sess, inputs, outputs, directory, framelist, nframes, batchsize): - """Batchwise prediction of pose for frame list in directory""" + """Batchwise prediction of pose for frame list in directory.""" from deeplabcut.utils.auxfun_videos import imread print("Starting to extract posture") @@ -1149,8 +1150,8 @@ def analyze_time_lapse_frames( save_as_csv=False, modelprefix="", ): - """ - Analyzed all images (of type = frametype) in a folder and stores the output in one file. + """Analyzed all images (of type = frametype) in a folder and stores the output in + one file. You can crop the frames (before analysis), by changing 'cropping'=True and setting 'x1','x2','y1','y2' in the config file. @@ -1269,9 +1270,7 @@ def analyze_time_lapse_frames( ################################################## # checks if input is a directory if os.path.isdir(directory): - """ - Analyzes all the frames in the directory. - """ + """Analyzes all the frames in the directory.""" print("Analyzing all frames in the directory: ", directory) os.chdir(directory) framelist = np.sort([fn for fn in os.listdir(os.curdir) if (frametype in fn)]) @@ -1451,8 +1450,8 @@ def convert_detections2tracklets( identity_only=False, track_method="", ): - """ - This should be called at the end of deeplabcut.analyze_videos for multianimal projects! + """This should be called at the end of deeplabcut.analyze_videos for multianimal + projects! Parameters ---------- @@ -1518,7 +1517,6 @@ def convert_detections2tracklets( >>> deeplabcut.convert_detections2tracklets('/analysis/project/reaching-task/config.yaml',[]'/analysis/project/video1.mp4'], videotype='.mp4',track_method='box') -------- - """ cfg = auxiliaryfunctions.read_config(config) track_method = auxfun_multianimal.get_track_method(cfg, track_method=track_method) diff --git a/deeplabcut/pose_estimation_tensorflow/training.py b/deeplabcut/pose_estimation_tensorflow/training.py index 2e964fb321..118602074d 100644 --- a/deeplabcut/pose_estimation_tensorflow/training.py +++ b/deeplabcut/pose_estimation_tensorflow/training.py @@ -15,7 +15,9 @@ def return_train_network_path(config, shuffle=1, trainingsetindex=0, modelprefix=""): - """Returns the training and test pose config file names as well as the folder where the snapshot is + """Returns the training and test pose config file names as well as the folder where + the snapshot is. + Parameters ---------- config : string diff --git a/deeplabcut/pose_estimation_tensorflow/visualizemaps.py b/deeplabcut/pose_estimation_tensorflow/visualizemaps.py index 0f9d64cedb..449f63c321 100644 --- a/deeplabcut/pose_estimation_tensorflow/visualizemaps.py +++ b/deeplabcut/pose_estimation_tensorflow/visualizemaps.py @@ -29,8 +29,7 @@ def extract_maps( Indices=None, modelprefix="", ): - """ - Extracts the scoremap, locref, partaffinityfields (if available). + """Extracts the scoremap, locref, partaffinityfields (if available). Returns a dictionary indexed by: trainingsetfraction, snapshotindex, and imageindex for those keys, each item contains: (image,scmap,locref,paf,bpt names,partaffinity graph, imagename, True/False if this image was in trainingset) @@ -56,7 +55,6 @@ def extract_maps( -------- If you want to extract the data for image 0 and 103 (of the training set) for model trained with shuffle 0. >>> deeplabcut.extract_maps(configfile,0,Indices=[0,103]) - """ from pathlib import Path diff --git a/deeplabcut/pose_tracking_pytorch/apis.py b/deeplabcut/pose_tracking_pytorch/apis.py index a37baa8e50..627eaf7d11 100644 --- a/deeplabcut/pose_tracking_pytorch/apis.py +++ b/deeplabcut/pose_tracking_pytorch/apis.py @@ -24,8 +24,7 @@ def transformer_reID( modelprefix: str = "", destfolder: str = None, ): - """ - Enables tracking with transformer. + """Enables tracking with transformer. Substeps include: - Mines triplets from tracklets in videos (from another tracker) @@ -87,7 +86,6 @@ def transformer_reID( >>> track_method="transformer", >>> ) -------- - """ import os diff --git a/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py b/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py index 9d1b808b0a..a96b9cb6e1 100644 --- a/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py +++ b/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Vision Transformer (ViT) in PyTorch +"""Vision Transformer (ViT) in PyTorch. A PyTorch implement of Vision Transformers as described in 'An Image Is Worth 16 x 16 Words: Transformers for Image Recognition at Scale' - https://arxiv.org/abs/2010.11929 @@ -40,14 +40,14 @@ def drop_path(x, drop_prob: float = 0.0, training: bool = False): - """Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks). + """Drop paths (Stochastic Depth) per sample (when applied in main path of residual + blocks). This is the same as the DropConnect impl I created for EfficientNet, etc networks, however, the original name is misleading as 'Drop Connect' is a different form of dropout in a separate paper... See discussion: https://github.com/tensorflow/tpu/issues/494#issuecomment-532968956 ... I've opted for changing the layer and argument names to 'drop path' rather than mix DropConnect as a layer name and use 'survival rate' as the argument. - """ if drop_prob == 0.0 or not training: return x @@ -60,7 +60,8 @@ def drop_path(x, drop_prob: float = 0.0, training: bool = False): class DropPath(nn.Module): - """Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks).""" + """Drop paths (Stochastic Depth) per sample (when applied in main path of residual + blocks).""" def __init__(self, drop_prob=None): super().__init__() @@ -429,8 +430,9 @@ def norm_cdf(x): def trunc_normal_(tensor, mean=0.0, std=1.0, a=-2.0, b=2.0): # type: (Tensor, float, float, float, float) -> Tensor - r"""Fills the input Tensor with values drawn from a truncated - normal distribution. The values are effectively drawn from the + r"""Fills the input Tensor with values drawn from a truncated normal distribution. + + The values are effectively drawn from the normal distribution :math:`\mathcal{N}(\text{mean}, \text{std}^2)` with values outside :math:`[a, b]` redrawn until they are within the bounds. The method used for generating the random values works diff --git a/deeplabcut/pose_tracking_pytorch/solver/cosine_lr.py b/deeplabcut/pose_tracking_pytorch/solver/cosine_lr.py index 69091c5a33..246360d5b5 100644 --- a/deeplabcut/pose_tracking_pytorch/solver/cosine_lr.py +++ b/deeplabcut/pose_tracking_pytorch/solver/cosine_lr.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Cosine Scheduler +"""Cosine Scheduler. Cosine LR schedule with warmup, cycle/restarts, noise. diff --git a/deeplabcut/pose_tracking_pytorch/solver/scheduler.py b/deeplabcut/pose_tracking_pytorch/solver/scheduler.py index e724034922..8da7aa5ce8 100644 --- a/deeplabcut/pose_tracking_pytorch/solver/scheduler.py +++ b/deeplabcut/pose_tracking_pytorch/solver/scheduler.py @@ -14,8 +14,8 @@ class Scheduler: - """Parameter Scheduler Base Class - A scheduler base class that can be used to schedule any optimizer parameter groups. + """Parameter Scheduler Base Class A scheduler base class that can be used to + schedule any optimizer parameter groups. Unlike the builtin PyTorch schedulers, this is intended to be consistently called * At the END of each epoch, before incrementing the epoch count, to calculate next epoch's value @@ -93,7 +93,7 @@ def step_update(self, num_updates: int, metric: float = None): def update_groups(self, values): if not isinstance(values, (list, tuple)): values = [values] * len(self.optimizer.param_groups) - for param_group, value in zip(self.optimizer.param_groups, values): + for param_group, value in zip(self.optimizer.param_groups, values, strict=False): param_group[self.param_group_field] = value def _add_noise(self, lrs, t): diff --git a/deeplabcut/pose_tracking_pytorch/tracking_utils/meter.py b/deeplabcut/pose_tracking_pytorch/tracking_utils/meter.py index 655cfd2d27..3b56c2643d 100644 --- a/deeplabcut/pose_tracking_pytorch/tracking_utils/meter.py +++ b/deeplabcut/pose_tracking_pytorch/tracking_utils/meter.py @@ -9,7 +9,7 @@ # Licensed under GNU Lesser General Public License v3.0 # class AverageMeter: - """Computes and stores the average and current value""" + """Computes and stores the average and current value.""" def __init__(self): self.val = 0 diff --git a/deeplabcut/pose_tracking_pytorch/train_dlctransreid.py b/deeplabcut/pose_tracking_pytorch/train_dlctransreid.py index 0d5861ad72..a2daef1914 100644 --- a/deeplabcut/pose_tracking_pytorch/train_dlctransreid.py +++ b/deeplabcut/pose_tracking_pytorch/train_dlctransreid.py @@ -46,7 +46,6 @@ def set_seed(seed): def split_train_test(npy_list, train_frac): # with npy list form videos, split each to train and test - x_list = [] train_list = [] test_list = [] diff --git a/deeplabcut/post_processing/analyze_skeleton.py b/deeplabcut/post_processing/analyze_skeleton.py index 59820dcaa3..d39c3a7b85 100644 --- a/deeplabcut/post_processing/analyze_skeleton.py +++ b/deeplabcut/post_processing/analyze_skeleton.py @@ -27,7 +27,8 @@ # utility functions def calc_distance_between_points_two_vectors_2d(v1, v2): - """calc_distance_between_points_two_vectors_2d [pairwise distance between vectors points] + """calc_distance_between_points_two_vectors_2d [pairwise distance between vectors + points] Arguments: v1 {[np.array]} -- [description] @@ -62,9 +63,10 @@ def calc_distance_between_points_two_vectors_2d(v1, v2): def angle_between_points_2d_anticlockwise(p1, p2): - """angle_between_points_2d_clockwise [Determines the angle of a straight line drawn between point one and two. - The number returned, which is a double in degrees, tells us how much we have to rotate - a horizontal line anti-clockwise for it to match the line between the two points.] + """angle_between_points_2d_clockwise [Determines the angle of a straight line drawn + between point one and two. The number returned, which is a double in degrees, tells + us how much we have to rotate a horizontal line anti-clockwise for it to match the + line between the two points.] Arguments: p1 {[np.ndarray, list]} -- np.array or list [ with the X and Y coordinates of the point] @@ -98,7 +100,8 @@ def angle_between_points_2d_anticlockwise(p1, p2): def calc_angle_between_vectors_of_points_2d(v1, v2): - """calc_angle_between_vectors_of_points_2d [calculates the clockwise angle between each set of point for two 2d arrays of points] + """calc_angle_between_vectors_of_points_2d [calculates the clockwise angle between + each set of point for two 2d arrays of points] Arguments: v1 {[np.ndarray]} -- [2d array with X,Y position at each timepoint] diff --git a/deeplabcut/post_processing/filtering.py b/deeplabcut/post_processing/filtering.py index c1041b384e..cee8ef5ff4 100644 --- a/deeplabcut/post_processing/filtering.py +++ b/deeplabcut/post_processing/filtering.py @@ -22,10 +22,8 @@ def columnwise_spline_interp(data, max_gap=0): - """ - Perform cubic spline interpolation over the columns of *data*. - All gaps of size lower than or equal to *max_gap* are filled, - and data slightly smoothed. + """Perform cubic spline interpolation over the columns of *data*. All gaps of size + lower than or equal to *max_gap* are filled, and data slightly smoothed. Parameters ---------- @@ -54,7 +52,7 @@ def columnwise_spline_interp(data, max_gap=0): count = np.diff(inds) inds = inds[:-1] to_fill = np.ones_like(mask) - for ind, n, is_nan in zip(inds, count, ~mask[inds]): + for ind, n, is_nan in zip(inds, count, ~mask[inds], strict=False): if is_nan and n > max_gap: to_fill[ind : ind + n] = False y[~to_fill] = np.nan @@ -227,7 +225,7 @@ def filterpredictions( if destfolder is None: destfolder = str(Path(video).parents[0]) - print("Filtering with %s model %s" % (filtertype, video)) + print(f"Filtering with {filtertype} model {video}") vname = Path(video).stem try: diff --git a/deeplabcut/refine_training_dataset/outlier_frames.py b/deeplabcut/refine_training_dataset/outlier_frames.py index fbcb53dd8e..a7140a50a4 100644 --- a/deeplabcut/refine_training_dataset/outlier_frames.py +++ b/deeplabcut/refine_training_dataset/outlier_frames.py @@ -43,8 +43,8 @@ def find_outliers_in_raw_data( extraction_algo="kmeans", copy_videos=False, ): - """ - Extract outlier frames from either raw detections or assemblies of multiple animals. + """Extract outlier frames from either raw detections or assemblies of multiple + animals. Parameter ---------- @@ -75,7 +75,6 @@ def find_outliers_in_raw_data( copy_videos : bool, optional (default=False) If True, newly-added videos (from which outlier frames are extracted) are copied to the project folder. By default, symbolic links are created instead. - """ if extraction_algo not in ("kmeans", "uniform"): raise ValueError(f"Unsupported extraction algorithm {extraction_algo}.") @@ -120,8 +119,7 @@ def find_outliers_in_raw_data( def find_outliers_in_raw_detections(pickled_data, algo="uncertain", threshold=0.1, kept_keypoints=None): - """ - Find outlier frames from the raw detections of multiple animals. + """Find outlier frames from the raw detections of multiple animals. Parameter ---------- @@ -598,8 +596,8 @@ def FitSARIMAXModel(x, p, pcutoff, alpha, ARdegree, MAdegree, nforecast=0, disp= def compute_deviations(Dataframe, dataname, p_bound, alpha, ARdegree, MAdegree, storeoutput=None): - """Fits Seasonal AutoRegressive Integrated Moving Average with eXogenous regressors model to data and computes confidence interval - as well as mean fit.""" + """Fits Seasonal AutoRegressive Integrated Moving Average with eXogenous regressors + model to data and computes confidence interval as well as mean fit.""" print("Fitting state-space models with parameters:", ARdegree, MAdegree) df_x, df_y, df_likelihood = Dataframe.values.reshape((Dataframe.shape[0], -1, 3)).T @@ -654,8 +652,7 @@ def attempt_to_add_video( copy_videos: bool, coords: list | None, ) -> bool: - """ - Add new videos to the config file at any stage of the project. + """Add new videos to the config file at any stage of the project. Parameters ---------- diff --git a/deeplabcut/refine_training_dataset/stitch.py b/deeplabcut/refine_training_dataset/stitch.py index 3dea23d3c6..b80094ffd4 100644 --- a/deeplabcut/refine_training_dataset/stitch.py +++ b/deeplabcut/refine_training_dataset/stitch.py @@ -40,8 +40,7 @@ class Tracklet: def __init__(self, data, inds): - """ - Create a Tracklet object. + """Create a Tracklet object. Parameters ---------- @@ -108,11 +107,10 @@ def xy(self): @property def centroid(self): - """ - Return the instantaneous 2D position of the Tracklet centroid. - For Tracklets longer than 10 frames, the centroid is automatically - smoothed using an exponential moving average. - The result is cached for efficiency. + """Return the instantaneous 2D position of the Tracklet centroid. + + For Tracklets longer than 10 frames, the centroid is automatically smoothed + using an exponential moving average. The result is cached for efficiency. """ if self._centroid is None: self._update_centroid() @@ -189,8 +187,8 @@ def interpolate(self, max_gap=1): return self + sum(fills) def contains_duplicates(self, return_indices=False): - """ - Evaluate whether the Tracklet contains duplicate time indices. + """Evaluate whether the Tracklet contains duplicate time indices. + If `return_indices`, also return the indices of the duplicates. """ has_duplicates = len(set(self.inds)) != len(self.inds) @@ -199,10 +197,10 @@ def contains_duplicates(self, return_indices=False): return has_duplicates, np.flatnonzero(np.diff(self.inds) == 0) def calc_velocity(self, where="head", norm=True): - """ - Calculate the linear velocity of either the `head` - or `tail` of the Tracklet, computed over the last or first - three frames, respectively. If `norm`, return the absolute + """Calculate the linear velocity of either the `head` or `tail` of the Tracklet, + computed over the last or first three frames, respectively. + + If `norm`, return the absolute speed rather than a 2D vector. """ if where == "tail": @@ -221,11 +219,9 @@ def maximal_velocity(self): return np.sqrt(np.max(np.sum(vel**2, axis=1))) def calc_rate_of_turn(self, where="head"): - """ - Calculate the rate of turn (or angular velocity) of - either the `head` or `tail` of the Tracklet, computed over - the last or first three frames, respectively. - """ + """Calculate the rate of turn (or angular velocity) of either the `head` or + `tail` of the Tracklet, computed over the last or first three frames, + respectively.""" if where == "tail": v = np.diff(self.centroid[:3], axis=0) else: @@ -239,18 +235,16 @@ def is_continuous(self): return self.end - self.start + 1 == len(self) def immediately_follows(self, other_tracklet, max_gap=1): - """ - Test whether this Tracklet follows another within - a tolerance of`max_gap` frames. - """ + """Test whether this Tracklet follows another within a tolerance of`max_gap` + frames.""" return 0 < self.start - other_tracklet.end <= max_gap def distance_to(self, other_tracklet): - """ - Calculate the Euclidean distance between this Tracklet and another. - If the Tracklets overlap in time, this is the mean distance over - those frames. Otherwise, it is the distance between the head/tail - of one to the tail/head of the other. + """Calculate the Euclidean distance between this Tracklet and another. + + If the Tracklets overlap in time, this is the mean distance over those frames. + Otherwise, it is the distance between the head/tail of one to the tail/head of + the other. """ if self in other_tracklet: dist = ( @@ -264,11 +258,11 @@ def distance_to(self, other_tracklet): return np.sqrt(np.sum((self.centroid[0] - other_tracklet.centroid[-1]) ** 2)) def motion_affinity_with(self, other_tracklet): - """ - Evaluate the motion affinity of this Tracklet' with another one. - This evaluates whether the Tracklets could realistically be reached - by one another, knowing the time separating them and their velocities. - Return 0 if the Tracklets overlap. + """Evaluate the motion affinity of this Tracklet' with another one. + + This evaluates whether the Tracklets could realistically be reached by one + another, knowing the time separating them and their velocities. Return 0 if the + Tracklets overlap. """ time_gap = self.time_gap_to(other_tracklet) if time_gap > 0: @@ -349,13 +343,11 @@ def to_hankelet(self): return self.hankelize(self.centroid) def dynamic_dissimilarity_with(self, other_tracklet): - """ - Compute a dissimilarity score between Hankelets. - This metric efficiently captures the degree of alignment of - the subspaces spanned by the columns of both matrices. + """Compute a dissimilarity score between Hankelets. This metric efficiently + captures the degree of alignment of the subspaces spanned by the columns of both + matrices. - See Li et al., 2012. - Cross-view Activity Recognition using Hankelets. + See Li et al., 2012. Cross-view Activity Recognition using Hankelets. """ hk1 = self.to_hankelet() hk1 /= np.linalg.norm(hk1) @@ -367,12 +359,10 @@ def dynamic_dissimilarity_with(self, other_tracklet): return 2 - np.linalg.norm(temp1 + temp2) def dynamic_similarity_with(self, other_tracklet, tol=0.01): - """ - Evaluate the complexity of the tracklets' underlying dynamics - from the rank of their Hankel matrices, and assess whether - they originate from the same track. The idea is that if two - tracklets are part of the same track, they can be approximated - by a low order regressor. Conversely, tracklets belonging to + """Evaluate the complexity of the tracklets' underlying dynamics from the rank + of their Hankel matrices, and assess whether they originate from the same track. + The idea is that if two tracklets are part of the same track, they can be + approximated by a low order regressor. Conversely, tracklets belonging to different tracks will require a higher order regressor. See Dicle et al., 2013. @@ -386,12 +376,11 @@ def dynamic_similarity_with(self, other_tracklet, tol=0.01): return (rank1 + rank2) / joint_rank - 1 def estimate_rank(self, tol): - """ - Estimate the (low) rank of a noisy matrix via - hard thresholding of singular values. + """Estimate the (low) rank of a noisy matrix via hard thresholding of singular + values. - See Gavish & Donoho, 2013. - The optimal hard threshold for singular values is 4/sqrt(3) + See Gavish & Donoho, 2013. The optimal hard threshold for singular values is + 4/sqrt(3) """ mat = self.to_hankelet() if np.any(mat): # check that the matrix contains non-zero entries @@ -989,9 +978,8 @@ def stitch_tracklets( save_as_csv=False, **kwargs, ): - """ - Stitch sparse tracklets into full tracks via a graph-based, - minimum-cost flow optimization problem. + """Stitch sparse tracklets into full tracks via a graph-based, minimum-cost flow + optimization problem. Parameters ---------- diff --git a/deeplabcut/refine_training_dataset/tracklets.py b/deeplabcut/refine_training_dataset/tracklets.py index 6ae76f845d..c89e24efb9 100644 --- a/deeplabcut/refine_training_dataset/tracklets.py +++ b/deeplabcut/refine_training_dataset/tracklets.py @@ -85,7 +85,8 @@ def _load_tracklets(self, tracklets, auto_fill): if not len(temp): raise OSError("Tracklets are empty.") - get_frame_ind = lambda s: int(re.findall(r"\d+", s)[0]) + def get_frame_ind(s): + return int(re.findall(r"\d+", s)[0]) # Drop tracklets that are too short tracklets_sorted = [] @@ -106,7 +107,7 @@ def _load_tracklets(self, tracklets, auto_fill): tracklets_single = np.full((self.nframes, len(bodyparts_single) * 3), np.nan, np.float16) for _ in trange(len(tracklets_sorted)): tracklet = tracklets_sorted.pop() - inds, temp = zip(*[(get_frame_ind(k), v) for k, v in tracklet.items()]) + inds, temp = zip(*[(get_frame_ind(k), v) for k, v in tracklet.items()], strict=False) inds = np.asarray(inds) data = np.asarray(temp, dtype=np.float16) data_single = data[:, mask_single] @@ -160,7 +161,7 @@ def _load_tracklets(self, tracklets, auto_fill): better = np.flatnonzero(prob > 0) idx = closest[better] rows, cols = np.nonzero(has_data) - for i, j in zip(idx, better): + for i, j in zip(idx, better, strict=False): sl = slice(j * 3, j * 3 + 3) tracklets_multi[i, inds[rows[sl]], cols[sl]] = remaining.flat[sl] else: @@ -196,7 +197,7 @@ def _load_tracklets(self, tracklets, auto_fill): self.nindividuals ] * len(bodyparts_single) bps = bodyparts_multi + bodyparts_single - map_ = dict(zip(bps, range(len(bps)))) + map_ = dict(zip(bps, range(len(bps)), strict=False)) self.tracklet2bp = [map_[bp] for bp in self.bodyparts[::3]] self._label_pairs = self.get_label_pairs() else: @@ -254,9 +255,11 @@ def load_tracklets_from_hdf(self, filename): self.prob = self.data[:, :, 2] individuals = idx.get_level_values("individuals") self.individuals = individuals.unique().to_list() - self.tracklet2id = individuals.map(dict(zip(self.individuals, range(len(self.individuals))))).tolist()[::3] + self.tracklet2id = individuals.map( + dict(zip(self.individuals, range(len(self.individuals)), strict=False)) + ).tolist()[::3] bodyparts = self.bodyparts.unique() - self.tracklet2bp = self.bodyparts.map(dict(zip(bodyparts, range(len(bodyparts))))).tolist()[::3] + self.tracklet2bp = self.bodyparts.map(dict(zip(bodyparts, range(len(bodyparts)), strict=False))).tolist()[::3] self._label_pairs = list(idx.droplevel(["scorer", "coords"]).unique()) self._xy = self.xy.copy() @@ -302,7 +305,7 @@ def find_swapping_bodypart_pairs(self, force_find=False): temp_pairs = np.where(mat) # Get only those bodypart pairs that belong to different individuals pairs = [] - for a, b in zip(*temp_pairs): + for a, b in zip(*temp_pairs, strict=False): if self.tracklet2id[a] != self.tracklet2id[b]: pairs.append((a, b)) self.swapping_pairs = pairs @@ -315,7 +318,7 @@ def get_nonoverlapping_segments(self, tracklet1, tracklet2): swap_inds = self.get_swap_indices(tracklet1, tracklet2) inds = np.insert(swap_inds, [0, len(swap_inds)], [0, self.nframes]) mask = np.ones_like(self.times, dtype=bool) - for i, j in zip(inds[::2], inds[1::2]): + for i, j in zip(inds[::2], inds[1::2], strict=False): mask[i:j] = False return mask @@ -325,7 +328,7 @@ def flatten_data(self): def format_multiindex(self): scorer = self.scorer * len(self.bodyparts) - map_ = dict(zip(range(len(self.individuals)), self.individuals)) + map_ = dict(zip(range(len(self.individuals)), self.individuals, strict=False)) individuals = [map_[ind] for ind in self.tracklet2id for _ in range(3)] coords = ["x", "y", "likelihood"] * len(self.tracklet2id) return pd.MultiIndex.from_arrays( diff --git a/deeplabcut/utils/auxfun_models.py b/deeplabcut/utils/auxfun_models.py index 5aeb98820d..3042f53ba4 100644 --- a/deeplabcut/utils/auxfun_models.py +++ b/deeplabcut/utils/auxfun_models.py @@ -44,7 +44,10 @@ def check_for_weights(modeltype, parent_path): - """gets local path to network weights and checks if they are present. If not, downloads them from tensorflow.org""" + """Gets local path to network weights and checks if they are present. + + If not, downloads them from tensorflow.org + """ if modeltype not in MODELTYPE_FILEPATH_MAP.keys(): print( "Currently ResNet (50, 101, 152), MobilenetV2 (1, 0.75, 0.5 and 0.35) and EfficientNet (b0-b6) are supported, please change 'resnet' entry in config.yaml!" @@ -72,8 +75,9 @@ def check_for_weights(modeltype, parent_path): def download_weights(modeltype, model_path): - """ - Downloads the ImageNet pretrained weights for ResNets, MobileNets et al. from TensorFlow... + """Downloads the ImageNet pretrained weights for ResNets, MobileNets et al. + + from TensorFlow... """ import tarfile import urllib @@ -97,9 +101,7 @@ def download_weights(modeltype, model_path): def download_model(modelname, target_dir): - """ - Downloads a DeepLabCut Model Zoo Project - """ + """Downloads a DeepLabCut Model Zoo Project.""" import tarfile import urllib.request @@ -109,8 +111,9 @@ def show_progress(count, block_size, total_size): pbar.update(block_size) def tarfilenamecutting(tarf): - """' auxfun to extract folder path - ie. /xyz-trainsetxyshufflez/ + """' auxfun to extract folder path ie. + + /xyz-trainsetxyshufflez/ """ for memberid, member in enumerate(tarf.getmembers()): if memberid == 0: diff --git a/deeplabcut/utils/auxfun_multianimal.py b/deeplabcut/utils/auxfun_multianimal.py index d55baef648..ae4b185b2c 100644 --- a/deeplabcut/utils/auxfun_multianimal.py +++ b/deeplabcut/utils/auxfun_multianimal.py @@ -37,8 +37,7 @@ def reorder_individuals_in_df(df: pd.DataFrame, order: list) -> pd.DataFrame: - """ - Reorders data of df to match the order given in a list + """Reorders data of df to match the order given in a list. Parameters: ---------- @@ -92,7 +91,8 @@ def get_track_method(cfg, track_method=""): def IntersectionofIndividualsandOnesGivenbyUser(cfg, individuals): - """Returns all individuals when set to 'all', otherwise all bpts that are in the intersection of comparisonbodyparts and the actual bodyparts""" + """Returns all individuals when set to 'all', otherwise all bpts that are in the + intersection of comparisonbodyparts and the actual bodyparts.""" if "individuals" not in cfg: # Not a multi-animal project... return [""] all_indivs = extractindividualsandbodyparts(cfg)[0] @@ -150,8 +150,9 @@ def prune_paf_graph(list_of_edges, desired_n_edges=None, average_degree=None): def getpafgraph(cfg, printnames=True): - """Auxiliary function that turns skeleton (list of connected bodypart pairs) - into a list of corresponding indices (with regard to the stacked multianimal/uniquebodyparts) + """Auxiliary function that turns skeleton (list of connected bodypart pairs) into a + list of corresponding indices (with regard to the stacked + multianimal/uniquebodyparts) Convention: multianimalbodyparts go first! """ @@ -190,7 +191,8 @@ def graph2names(cfg, partaffinityfield_graph): def SaveFullMultiAnimalData(data, metadata, dataname, suffix="_full"): - """Save predicted data as h5 file and metadata as pickle file; created by predict_videos.py""" + """Save predicted data as h5 file and metadata as pickle file; created by + predict_videos.py.""" data_path = dataname.split(".h5")[0] + suffix + ".pickle" metadata_path = dataname.split(".h5")[0] + "_meta.pickle" @@ -202,7 +204,8 @@ def SaveFullMultiAnimalData(data, metadata, dataname, suffix="_full"): def LoadFullMultiAnimalData(dataname): - """Save predicted data as h5 file and metadata as pickle file; created by predict_videos.py""" + """Save predicted data as h5 file and metadata as pickle file; created by + predict_videos.py.""" data_file = dataname.split(".h5")[0] + "_full.pickle" try: with open(data_file, "rb") as handle: @@ -356,7 +359,10 @@ def convert2_maDLC(config, userfeedback=True, forceindividual=None): def convert_single2multiplelegacyAM(config, userfeedback=True, target=None): - """Convert multi animal to single animal code and vice versa. Note that by providing target='single'/'multi' this will be target!""" + """Convert multi animal to single animal code and vice versa. + + Note that by providing target='single'/'multi' this will be target! + """ cfg = auxiliaryfunctions.read_config(config) videos = cfg["video_sets"].keys() video_names = [Path(i).stem for i in videos] diff --git a/deeplabcut/utils/auxfun_videos.py b/deeplabcut/utils/auxfun_videos.py index c9ea80065e..6c7b34d898 100644 --- a/deeplabcut/utils/auxfun_videos.py +++ b/deeplabcut/utils/auxfun_videos.py @@ -204,8 +204,7 @@ def __init__(self, video_path, codec="h264", dpi=100, fps=None): self.fps = fps def shorten(self, start, end, suffix="short", dest_folder=None, validate_inputs=True): - """ - Shorten the video from start to end. + """Shorten the video from start to end. Parameter ---------- @@ -246,8 +245,7 @@ def validate_timestamp(stamp): return output_path def split(self, n_splits, suffix="split", dest_folder=None): - """ - Split a video into several shorter ones of equal duration. + """Split a video into several shorter ones of equal duration. Parameters ---------- @@ -353,7 +351,9 @@ def check_video_integrity(video_path): def imread(image_path, mode="skimage"): """Read image either with skimage or cv2. - Returns frame in uint with 3 color channels.""" + + Returns frame in uint with 3 color channels. + """ if mode == "skimage": image = io.imread(image_path) if image.ndim == 2 or image.shape[-1] == 1: @@ -378,9 +378,8 @@ def imresize(img, size=1.0, interpolationmethod=cv2.INTER_AREA): def ShortenVideo(vname, start="00:00:01", stop="00:01:00", outsuffix="short", outpath=None): - """ - Auxiliary function to shorten video and output with outsuffix appended. - to the same folder from start (hours:minutes:seconds) to stop (hours:minutes:seconds). + """Auxiliary function to shorten video and output with outsuffix appended. to the + same folder from start (hours:minutes:seconds) to stop (hours:minutes:seconds). Returns the full path to the shortened video! @@ -428,9 +427,8 @@ def CropVideo( outpath=None, useGUI=False, ): - """ - Auxiliary function to crop a video and output it to the same folder with "outsuffix" appended in its name. - Width and height will control the new dimensions. + """Auxiliary function to crop a video and output it to the same folder with + "outsuffix" appended in its name. Width and height will control the new dimensions. Returns the full path to the downsampled video! @@ -494,10 +492,10 @@ def DownSampleVideo( rotatecw="No", angle=0.0, ): - """ - Auxiliary function to downsample a video and output it to the same folder with "outsuffix" appended in its name. - Width and height will control the new dimensions. You can also pass only height or width and set the other one to -1, - this will keep the aspect ratio identical. + """Auxiliary function to downsample a video and output it to the same folder with + "outsuffix" appended in its name. Width and height will control the new dimensions. + You can also pass only height or width and set the other one to -1, this will keep + the aspect ratio identical. Returns the full path to the downsampled video! @@ -542,9 +540,8 @@ def DownSampleVideo( def rotate_video(vname, angle, rotatecw="Arbitrary", outsuffix="rotated", outpath=None): - """ - Auxiliary function to rotate a video and output it to the same folder with "outsuffix" appended in its name. - Angle is in degrees. + """Auxiliary function to rotate a video and output it to the same folder with + "outsuffix" appended in its name. Angle is in degrees. Returns the full path to the rotated video! diff --git a/deeplabcut/utils/auxiliaryfunctions.py b/deeplabcut/utils/auxiliaryfunctions.py index 5d20b52be7..84981d73d0 100644 --- a/deeplabcut/utils/auxiliaryfunctions.py +++ b/deeplabcut/utils/auxiliaryfunctions.py @@ -36,8 +36,9 @@ def create_config_template(multianimal=False): - """ - Creates a template for config.yaml file. This specific order is preserved while saving as yaml file. + """Creates a template for config.yaml file. + + This specific order is preserved while saving as yaml file. """ if multianimal: yaml_str = """\ @@ -163,8 +164,9 @@ def create_config_template(multianimal=False): def create_config_template_3d(): - """ - Creates a template for config.yaml file for 3d project. This specific order is preserved while saving as yaml file. + """Creates a template for config.yaml file for 3d project. + + This specific order is preserved while saving as yaml file. """ yaml_str = """\ # Project definitions (do not edit) @@ -196,9 +198,7 @@ def create_config_template_3d(): def read_config(configname): - """ - Reads structured config file defining a project. - """ + """Reads structured config file defining a project.""" ruamelFile = YAML() path = Path(configname) if os.path.exists(path): @@ -237,9 +237,7 @@ def read_config(configname): def write_config(configname, cfg): - """ - Write structured config file. - """ + """Write structured config file.""" with open(configname, "w") as cf: cfg_file, ruamelFile = create_config_template(cfg.get("multianimalproject", False)) for key in cfg.keys(): @@ -258,8 +256,7 @@ def write_config(configname, cfg): def edit_config(configname, edits, output_name=""): - """ - Convenience function to edit and save a config file from a dictionary. + """Convenience function to edit and save a config file from a dictionary. Parameters ---------- @@ -333,9 +330,7 @@ def get_unique_bodyparts(cfg: dict) -> list[str]: def write_config_3d(configname, cfg): - """ - Write structured 3D config file. - """ + """Write structured 3D config file.""" with open(configname, "w") as cf: cfg_file, ruamelFile = create_config_template_3d() for key in cfg.keys(): @@ -361,7 +356,10 @@ def write_plainconfig(configname, cfg): def attempt_to_make_folder(foldername, recursive=False): - """Attempts to create a folder with specified name. Does nothing if it already exists.""" + """Attempts to create a folder with specified name. + + Does nothing if it already exists. + """ try: os.path.isdir(foldername) except TypeError: # https://www.python.org/dev/peps/pep-0519/ @@ -377,13 +375,13 @@ def attempt_to_make_folder(foldername, recursive=False): def read_pickle(filename): - """Read the pickle file""" + """Read the pickle file.""" with open(filename, "rb") as handle: return pickle.load(handle) def write_pickle(filename, data): - """Write the pickle file""" + """Write the pickle file.""" with open(filename, "wb") as handle: pickle.dump(data, handle, protocol=pickle.HIGHEST_PROTOCOL) @@ -393,8 +391,8 @@ def get_list_of_videos( videotype: list[str] | str = "", in_random_order: bool = True, ) -> list[str]: - """Returns list of videos of videotype "videotype" in - folder videos or for list of videos. + """Returns list of videos of videotype "videotype" in folder videos or for list of + videos. NOTE: excludes keyword videos of the form: @@ -414,9 +412,7 @@ def get_list_of_videos( videos = [videos] if [os.path.isdir(i) for i in videos] == [True]: # checks if input is a directory - """ - Returns all the videos in the directory. - """ + """Returns all the videos in the directory.""" if not videotype: videotype = auxfun_videos.SUPPORTED_VIDEOS @@ -451,7 +447,8 @@ def get_list_of_videos( def save_data(PredicteData, metadata, dataname, pdindex, imagenames, save_as_csv): - """Save predicted data as h5 file and metadata as pickle file; created by predict_videos.py""" + """Save predicted data as h5 file and metadata as pickle file; created by + predict_videos.py.""" DataMachine = pd.DataFrame(PredicteData, columns=pdindex, index=imagenames) if save_as_csv: print("Saving csv poses!") @@ -480,7 +477,7 @@ def load_metadata(metadatafile): def get_immediate_subdirectories(a_dir): - """Get list of immediate subdirectories""" + """Get list of immediate subdirectories.""" return [name for name in os.listdir(a_dir) if os.path.isdir(os.path.join(a_dir, name))] @@ -497,8 +494,7 @@ def filter_files_by_patterns( contain_patterns: set[str] | None = None, end_patterns: set[str] | None = None, ) -> list[Path]: - """ - Filters files in a folder based on start, contain, and end patterns. + """Filters files in a folder based on start, contain, and end patterns. Args: folder (str | Path): The folder to search for files. @@ -533,7 +529,8 @@ def filter_files_by_patterns( def get_video_list(filename, videopath, videtype): - """Get list of videos in a path (if filetype == all), otherwise just a specific file.""" + """Get list of videos in a path (if filetype == all), otherwise just a specific + file.""" videos = list(grab_files_in_folder(videopath, videtype)) if filename == "all": return videos @@ -548,7 +545,7 @@ def get_video_list(filename, videopath, videtype): ## Various functions to get filenames, foldernames etc. based on configuration parameters. def get_training_set_folder(cfg: dict) -> Path: - """Training Set folder for config file based on parameters""" + """Training Set folder for config file based on parameters.""" Task = cfg["Task"] date = cfg["date"] iterate = "iteration-" + str(cfg["iteration"]) @@ -653,8 +650,7 @@ def get_evaluation_folder( def get_snapshots_from_folder(train_folder: Path) -> list[str]: - """ - Returns an ordered list of existing snapshot names in the train folder, sorted by + """Returns an ordered list of existing snapshot names in the train folder, sorted by increasing training iterations. Raises: @@ -674,14 +670,15 @@ def get_snapshots_from_folder(train_folder: Path) -> list[str]: def get_deeplabcut_path(): - """Get path of where deeplabcut is currently running""" + """Get path of where deeplabcut is currently running.""" import importlib.util return os.path.split(importlib.util.find_spec("deeplabcut").origin)[0] def intersection_of_body_parts_and_ones_given_by_user(cfg, comparisonbodyparts): - """Returns all body parts when comparisonbodyparts=='all', otherwise all bpts that are in the intersection of comparisonbodyparts and the actual bodyparts""" + """Returns all body parts when comparisonbodyparts=='all', otherwise all bpts that + are in the intersection of comparisonbodyparts and the actual bodyparts.""" # if "MULTI!" in allbpts: if cfg["multianimalproject"]: allbpts = cfg["multianimalbodyparts"] + cfg["uniquebodyparts"] @@ -793,8 +790,11 @@ def get_scorer_name( def check_if_post_processing(folder, vname, DLCscorer, DLCscorerlegacy, suffix="filtered"): - """Checks if filtered/bone lengths were already calculated. If not, figures - out if data was already analyzed (either with legacy scorer name or new one!)""" + """Checks if filtered/bone lengths were already calculated. + + If not, figures out if data was already analyzed (either with legacy scorer name or + new one!) + """ outdataname = os.path.join(folder, vname + DLCscorer + suffix + ".h5") sourcedataname = os.path.join(folder, vname + DLCscorer + ".h5") if os.path.isfile(outdataname): # was data already processed? @@ -880,7 +880,7 @@ def find_video_full_data(folder, videoname, scorer): def find_video_metadata(folder, videoname: str, scorer: str): - """For backward compatibility, let us search the substring 'meta'""" + """For backward compatibility, let us search the substring 'meta'.""" scorer_legacy = scorer.replace("DLC", "DeepCut") meta_files = filter_files_by_patterns( diff --git a/deeplabcut/utils/auxiliaryfunctions_3d.py b/deeplabcut/utils/auxiliaryfunctions_3d.py index f563c9bbd0..08acafc512 100644 --- a/deeplabcut/utils/auxiliaryfunctions_3d.py +++ b/deeplabcut/utils/auxiliaryfunctions_3d.py @@ -30,7 +30,7 @@ def Foldernames3Dproject(cfg_3d): - """Definitions of subfolders in 3D projects""" + """Definitions of subfolders in 3D projects.""" img_path = os.path.join(cfg_3d["project_path"], "calibration_images") path_corners = os.path.join(cfg_3d["project_path"], "corners") @@ -74,9 +74,7 @@ def create_empty_df(dataframe, scorer, flag): def compute_triangulation_calibration_images( stereo_matrix, projectedPoints1, projectedPoints2, path_undistort, cfg_3d, plot=True ): - """ - Performs triangulation of the calibration images. - """ + """Performs triangulation of the calibration images.""" triangulate = [] P1 = stereo_matrix["P1"] P2 = stereo_matrix["P2"] @@ -114,11 +112,11 @@ def triangulatePoints(P1, P2, x1, x2): def get_camerawise_videos(path, cam_names, videotype): - """ - This function returns the list of videos corresponding to the camera names specified in the cam_names. - e.g. if cam_names = ['camera-1','camera-2'] + """This function returns the list of videos corresponding to the camera names + specified in the cam_names. e.g. if cam_names = ['camera-1','camera-2'] - then it will return [['somename-camera-1-othername.avi', 'somename-camera-2-othername.avi']] + then it will return [['somename-camera-1-othername.avi', 'somename- + camera-2-othername.avi']] """ import glob from pathlib import Path @@ -164,9 +162,7 @@ def get_camerawise_videos(path, cam_names, videotype): def Get_list_of_triangulated_and_videoFiles(filepath, videotype, scorer_3d, cam_names, videofolder): - """ - Returns the list of triangulated h5 and the corresponding video files. - """ + """Returns the list of triangulated h5 and the corresponding video files.""" prefix = [] suffix = [] @@ -175,9 +171,7 @@ def Get_list_of_triangulated_and_videoFiles(filepath, videotype, scorer_3d, cam_ # Checks if filepath is a directory if [os.path.isdir(i) for i in filepath] == [True]: - """ - Analyzes all the videos in the directory. - """ + """Analyzes all the videos in the directory.""" print("Analyzing all the videos in the directory") videofolder = filepath[0] cwd = os.getcwd() @@ -277,9 +271,8 @@ def _reconstruct_tracks_as_tracklets(df): def _associate_paired_view_tracks(tracklets1, tracklets2, F): - """ - Computes the optimal matching between tracks in two cameras - using the xFx'=0 epipolar constraint equation. + """Computes the optimal matching between tracks in two cameras using the xFx'=0 + epipolar constraint equation. Parameters: ----------- @@ -322,9 +315,7 @@ def _associate_paired_view_tracks(tracklets1, tracklets2, F): def cross_view_match_dataframes(df1, df2, F): - """ - Computes the costs and matched voting for tracks between - a camera pair + """Computes the costs and matched voting for tracks between a camera pair. df: Data read from .h5 track file F: fundamental matrix from OpenCV diff --git a/deeplabcut/utils/conversioncode.py b/deeplabcut/utils/conversioncode.py index ab0345dca6..2ffa7b9c98 100644 --- a/deeplabcut/utils/conversioncode.py +++ b/deeplabcut/utils/conversioncode.py @@ -93,8 +93,10 @@ def convertcsv2h5(config, userfeedback=True, scorer=None): def adapt_labeled_data_to_new_project(config_path, remove_old_bodyparts=False, other_scorer=False, userfeedback=False): - """Given the config.yaml file, this function will convert the labels of an ancient project to a new project. - For this, the labeled data must be in the project folder, under the labeled-data folder and with the same configuration as all deeplabcut projects. + """Given the config.yaml file, this function will convert the labels of an ancient + project to a new project. For this, the labeled data must be in the project folder, + under the labeled-data folder and with the same configuration as all deeplabcut + projects. Parameters ---------- @@ -212,11 +214,13 @@ def adapt_labeled_data_to_new_project(config_path, remove_old_bodyparts=False, o def analyze_videos_converth5_to_csv(video_folder, videotype=".mp4", listofvideos=False): - """ - By default the output poses (when running analyze_videos) are stored as MultiIndex Pandas Array, which contains the name of the network, body part name, (x, y) label position \n - in pixels, and the likelihood for each frame per body part. These arrays are stored in an efficient Hierarchical Data Format (HDF) \n - in the same directory, where the video is stored. This functions converts hdf (h5) files to the comma-separated values format (.csv), - which in turn can be imported in many programs, such as MATLAB, R, Prism, etc. + """By default the output poses (when running analyze_videos) are stored as + MultiIndex Pandas Array, which contains the name of the network, body part name, (x, + y) label position \n in pixels, and the likelihood for each frame per body part. + These arrays are stored in an efficient Hierarchical Data Format (HDF) \n in the + same directory, where the video is stored. This functions converts hdf (h5) files to + the comma-separated values format (.csv), which in turn can be imported in many + programs, such as MATLAB, R, Prism, etc. Parameters ---------- @@ -232,7 +236,6 @@ def analyze_videos_converth5_to_csv(video_folder, videotype=".mp4", listofvideos Converts all pose-output files belonging to mp4 videos in the folder '/media/alex/experimentaldata/cheetahvideos' to csv files. deeplabcut.analyze_videos_converth5_to_csv('/media/alex/experimentaldata/cheetahvideos','.mp4') - """ if listofvideos: # can also be called with a list of videos (from GUI) @@ -254,8 +257,7 @@ def analyze_videos_converth5_to_nwb( videotype=".mp4", listofvideos=False, ): - """ - Convert all h5 output data files in `video_folder` to NWB format. + """Convert all h5 output data files in `video_folder` to NWB format. Parameters ---------- @@ -273,7 +275,6 @@ def analyze_videos_converth5_to_nwb( Converts all pose-output files belonging to mp4 videos in the folder '/media/alex/experimentaldata/cheetahvideos' to csv files. deeplabcut.analyze_videos_converth5_to_csv('/media/alex/experimentaldata/cheetahvideos','.mp4') - """ if listofvideos: # can also be called with a list of videos (from GUI) videos = video_folder # GUI gives a list of videos @@ -322,9 +323,11 @@ def _convert_h5_files_to(filetype, config, h5_files, videos): def merge_windowsannotationdataONlinuxsystem(cfg): - """If a project was created on Windows (and labeled there,) but ran on unix then the data folders - corresponding in the keys in cfg['video_sets'] are not found. This function gets them directly by - looping over all folders in labeled-data""" + """If a project was created on Windows (and labeled there,) but ran on unix then the + data folders corresponding in the keys in cfg['video_sets'] are not found. + + This function gets them directly by looping over all folders in labeled-data + """ AnnotationData = [] data_path = Path(cfg["project_path"], "labeled-data") diff --git a/deeplabcut/utils/make_labeled_video.py b/deeplabcut/utils/make_labeled_video.py index d2a524297a..79179b1be2 100644 --- a/deeplabcut/utils/make_labeled_video.py +++ b/deeplabcut/utils/make_labeled_video.py @@ -94,7 +94,7 @@ def CreateVideo( bboxes_pcutoff=0.6, bboxes_color: tuple | None = None, ): - """Creating individual frames with labeled body parts and making a video""" + """Creating individual frames with labeled body parts and making a video.""" bpts = Dataframe.columns.get_level_values("bodyparts") all_bpts = bpts.values[::3] if draw_skeleton: @@ -246,7 +246,7 @@ def CreateVideoSlow( bboxes_pcutoff=0.6, bboxes_color: str | None = None, ): - """Creating individual frames with labeled body parts and making a video""" + """Creating individual frames with labeled body parts and making a video.""" if displaycropped: ny, nx = y2 - y1, x2 - x1 @@ -809,7 +809,7 @@ def proc_video( plot_bboxes: bool = True, bboxes_pcutoff: float = 0.6, ): - """Helper function for create_videos + """Helper function for create_videos. Parameters ---------- @@ -1155,8 +1155,7 @@ def create_video_with_all_detections( plot_bboxes: bool = True, **kwargs, ): - """ - Create a video labeled with all the detections stored in a '*_full.pickle' file. + """Create a video labeled with all the detections stored in a '*_full.pickle' file. Parameters ---------- @@ -1412,7 +1411,7 @@ def _get_default_conf_to_alpha( confidence_to_alpha: bool, pcutoff: float, ) -> Callable[[float], float] | None: - """Creates the default confidence_to_alpha function""" + """Creates the default confidence_to_alpha function.""" if not confidence_to_alpha: return None diff --git a/deeplabcut/utils/plotting.py b/deeplabcut/utils/plotting.py index 6453af5837..18fb92abd2 100644 --- a/deeplabcut/utils/plotting.py +++ b/deeplabcut/utils/plotting.py @@ -58,7 +58,8 @@ def PlottingResults( resolution=100, linewidth=1.0, ): - """Plots poses vs time; pose x vs pose y; histogram of differences and likelihoods.""" + """Plots poses vs time; pose x vs pose y; histogram of differences and + likelihoods.""" pcutoff = cfg["pcutoff"] colors = visualization.get_cmap(len(bodyparts2plot), name=cfg["colormap"]) alphavalue = cfg["alphavalue"] @@ -432,8 +433,7 @@ def plot_edge_affinity_distributions( output_name="", figsize=(10, 7), ): - """ - Display the distribution of affinity costs of within- and between-animal edges. + """Display the distribution of affinity costs of within- and between-animal edges. Parameters ---------- @@ -450,7 +450,6 @@ def plot_edge_affinity_distributions( figsize: tuple Figure size in inches. - """ with open(eval_pickle_file, "rb") as file: diff --git a/deeplabcut/utils/pseudo_label.py b/deeplabcut/utils/pseudo_label.py index d9a42fc915..6fdceccce0 100644 --- a/deeplabcut/utils/pseudo_label.py +++ b/deeplabcut/utils/pseudo_label.py @@ -150,7 +150,7 @@ def keypoint_matching( device: str | None = None, train_file: str = "train.json", ): - """Runs the keypoint matching algorithm for a DeepLabCut project + """Runs the keypoint matching algorithm for a DeepLabCut project. Matches project keypoints to SuperAnimal keypoints automatically, by running SuperAnimal inference on all images in the dataset diff --git a/deeplabcut/utils/skeleton.py b/deeplabcut/utils/skeleton.py index 77635d4c74..4469cd7a14 100644 --- a/deeplabcut/utils/skeleton.py +++ b/deeplabcut/utils/skeleton.py @@ -72,7 +72,8 @@ def __init__(self, config_path): if not found: warnings.warn( f"A fully labeled animal could not be found. " - f"{', '.join(self.bpts[missing])} will need to be manually connected in the config.yaml." + f"{', '.join(self.bpts[missing])} will need to be manually connected in the config.yaml.", + stacklevel=2, ) self.tree = KDTree(self.xy) # Handle image previously annotated on a different platform @@ -149,7 +150,8 @@ def export(self, *args): unconnected = [i for i in range(len(self.xy)) if i not in inds_flat] if len(unconnected): warnings.warn( - "You didn't connect all the bodyparts (which is fine!). This is just a note to let you know." + "You didn't connect all the bodyparts (which is fine!). This is just a note to let you know.", + stacklevel=2, ) self.cfg["skeleton"] = [tuple(self.bpts[list(pair)]) for pair in self.inds] write_config(self.config_path, self.cfg) @@ -168,7 +170,7 @@ def on_select(self, verts): for lst in inds: if len(lst) and lst[0] not in inds_unique: inds_unique.append(lst[0]) - for pair in zip(inds_unique, inds_unique[1:]): + for pair in zip(inds_unique, inds_unique[1:], strict=False): pair_sorted = tuple(sorted(pair)) self.inds.add(pair_sorted) self.segs.add(tuple(map(tuple, self.xy[pair_sorted, :]))) diff --git a/deeplabcut/utils/video_processor.py b/deeplabcut/utils/video_processor.py index 3bb009e78c..5ce7bee90c 100644 --- a/deeplabcut/utils/video_processor.py +++ b/deeplabcut/utils/video_processor.py @@ -26,8 +26,8 @@ class VideoProcessor: - """ - Base class for a video processing unit, implementation is required for video loading and saving + """Base class for a video processing unit, implementation is required for video + loading and saving. sh and sw are the output height and width respectively. """ @@ -85,47 +85,32 @@ def frame_count(self): return self.nframes def get_video(self): - """ - implement your own - """ + """Implement your own.""" pass def get_info(self): - """ - implement your own - """ + """Implement your own.""" pass def create_video(self): - """ - implement your own - """ + """Implement your own.""" pass def _read_frame(self): - """ - implement your own - """ + """Implement your own.""" pass def save_frame(self, frame): - """ - implement your own - """ + """Implement your own.""" pass def close(self): - """ - implement your own - """ + """Implement your own.""" pass class VideoProcessorCV(VideoProcessor): - """ - OpenCV implementation of VideoProcessor - requires opencv-python==3.4.0.12 - """ + """OpenCV implementation of VideoProcessor requires opencv-python==3.4.0.12.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/deeplabcut/utils/visualization.py b/deeplabcut/utils/visualization.py index 33d90990b7..e815672153 100644 --- a/deeplabcut/utils/visualization.py +++ b/deeplabcut/utils/visualization.py @@ -61,7 +61,8 @@ def make_labeled_image( scaling=1, ax=None, ): - """Creating a labeled image with the original human labels, as well as the DeepLabCut's!""" + """Creating a labeled image with the original human labels, as well as the + DeepLabCut's!""" if labels is None: labels = ["+", ".", "x"] @@ -131,8 +132,8 @@ def make_multianimal_labeled_image( bboxes_cutoff: float = 0.6, bboxes_color: Colormap | str | None = None, ) -> plt.Axes: - """ - Plots groundtruth labels and predictions onto the matplotlib's axes, with the specified graphical parameters. + """Plots groundtruth labels and predictions onto the matplotlib's axes, with the + specified graphical parameters. Args: frame: image @@ -304,8 +305,8 @@ def make_labeled_images_from_dataframe( draw_skeleton=True, color_by="bodypart", ): - """ - Write labeled frames to disk from a DataFrame. + """Write labeled frames to disk from a DataFrame. + Parameters ---------- df : pd.DataFrame @@ -452,9 +453,8 @@ def plot_evaluation_results( bboxes_cutoff: float = 0.6, bounding_boxes_color: str = "auto", ) -> None: - """ - Creates labeled images using the results of inference, and saves them to an output - folder. + """Creates labeled images using the results of inference, and saves them to an + output folder. Args: df_combined: dataframe with multiindex rows ("labeled-data", video_name, @@ -480,7 +480,6 @@ def plot_evaluation_results( If set to "auto" (default value): - if mode is "bodypart", the bbox color will be a default color - if mode is "individual", each individual's color will be used for its bounding box - """ if bounding_boxes is None: bounding_boxes = {} diff --git a/docker/deeplabcut_docker.py b/docker/deeplabcut_docker.py index 05753fc067..296f4789d9 100644 --- a/docker/deeplabcut_docker.py +++ b/docker/deeplabcut_docker.py @@ -1,9 +1,8 @@ #!/usr/bin/env python3 -""" -DeepLabCut2.0-2.2 Toolbox (deeplabcut.org) -© A. & M. Mathis Labs -https://github.com/DeepLabCut/DeepLabCut -Please see AUTHORS for contributors. +"""DeepLabCut2.0-2.2 Toolbox (deeplabcut.org) © A. + +& M. Mathis Labs https://github.com/DeepLabCut/DeepLabCut Please see AUTHORS for +contributors. https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS Licensed under GNU Lesser General Public License v3.0 """ @@ -20,13 +19,13 @@ '.__/o o\__.' `{= ^ =}´ > u < - ____________________.""`-------`"".______________________ + ____________________.""`-------`"".______________________ \ ___ __ __ _____ __ / / / _ \ ___ ___ ___ / / ___ _ / / / ___/__ __ / /_ \ \ / // // -_)/ -_)/ _ \ / /__/ _ `// _ \/ /__ / // // __/ / //____/ \__/ \__// .__//____/\_,_//_.__/\___/ \_,_/ \__/ \ \_________________________________________________________/ - ___)( )(___ `-.___. + ___)( )(___ `-.___. (((__) (__))) ~` Welcome to DeepLabCut docker! @@ -62,7 +61,10 @@ def _parse_args(): def main(): - """Main entry point. Parse arguments and launch container.""" + """Main entry point. + + Parse arguments and launch container. + """ launch_args, docker_arguments = _parse_args() argv = ["deeplabcut_docker.sh", launch_args.container, *docker_arguments] print(_MOTD, file=sys.stderr) diff --git a/examples/COLAB/COLAB_BUCTD_and_CTD_tracking.ipynb b/examples/COLAB/COLAB_BUCTD_and_CTD_tracking.ipynb index 55cc29ff0a..36d88df518 100644 --- a/examples/COLAB/COLAB_BUCTD_and_CTD_tracking.ipynb +++ b/examples/COLAB/COLAB_BUCTD_and_CTD_tracking.ipynb @@ -1273,7 +1273,7 @@ "source": [ "def plot_generative_sampling(dataset: dlc_torch.PoseDataset) -> None:\n", " # Sample the same image 3 times and plot the results\n", - " for i in range(3):\n", + " for _i in range(3):\n", " item = dataset[0]\n", "\n", " # Remove ImageNet normalization from the image so it displays well\n", @@ -1296,6 +1296,7 @@ " axs,\n", " [\"Ground Truth Pose\", \"Pose Conditions\"],\n", " [gt_pose, gen_samples],\n", + " strict=False,\n", " ):\n", " ax.set_title(title)\n", " for x, y, vis in keypoints:\n", diff --git a/examples/COLAB/COLAB_DEMO_SuperAnimal.ipynb b/examples/COLAB/COLAB_DEMO_SuperAnimal.ipynb index 94741c7acb..c6c2d4581f 100644 --- a/examples/COLAB/COLAB_DEMO_SuperAnimal.ipynb +++ b/examples/COLAB/COLAB_DEMO_SuperAnimal.ipynb @@ -203,12 +203,11 @@ "\n", "data_url = \"data:video/mp4;base64,\" + b64encode(view_video).decode()\n", "HTML(\n", - " \"\"\"\n", + " f\"\"\"\n", "\n", "\"\"\"\n", - " % data_url\n", ")" ] } diff --git a/examples/COLAB/COLAB_HumanPose_with_RTMPose.ipynb b/examples/COLAB/COLAB_HumanPose_with_RTMPose.ipynb index 584b184eab..1e0ec0865f 100644 --- a/examples/COLAB/COLAB_HumanPose_with_RTMPose.ipynb +++ b/examples/COLAB/COLAB_HumanPose_with_RTMPose.ipynb @@ -570,7 +570,7 @@ " labels = predictions[\"labels\"].cpu().numpy()\n", "\n", " # Obtain the bounding boxes predicted for humans\n", - " human_bboxes = [bbox for bbox, label in zip(bboxes, labels) if label == 1]\n", + " human_bboxes = [bbox for bbox, label in zip(bboxes, labels, strict=False) if label == 1]\n", "\n", " # Convert bounding boxes to xywh format\n", " bboxes = np.zeros((0, 4))\n", @@ -596,7 +596,7 @@ ")\n", "\n", "print(\"Running pose estimation\")\n", - "predictions = runner.inference(tqdm(zip(image_paths, context)))\n", + "predictions = runner.inference(tqdm(zip(image_paths, context, strict=False)))\n", "\n", "\n", "#############################################\n", @@ -604,7 +604,9 @@ "print(\"Saving the predictions to a CSV file\")\n", "df = dlc_torch.build_predictions_dataframe(\n", " scorer=\"rtmpose-body7\",\n", - " predictions={img_path: img_predictions for img_path, img_predictions in zip(image_paths, predictions)},\n", + " predictions={\n", + " img_path: img_predictions for img_path, img_predictions in zip(image_paths, predictions, strict=False)\n", + " },\n", " parameters=dlc_torch.PoseDatasetParameters(\n", " bodyparts=pose_cfg[\"metadata\"][\"bodyparts\"],\n", " unique_bpts=pose_cfg[\"metadata\"][\"unique_bodyparts\"],\n", @@ -658,7 +660,7 @@ "plot_bounding_boxes = True\n", "marker_size = 12\n", "\n", - "for image_path, image_predictions in zip(image_paths, predictions):\n", + "for image_path, image_predictions in zip(image_paths, predictions, strict=False):\n", " image = Image.open(image_path).convert(\"RGB\")\n", "\n", " pose = image_predictions[\"bodyparts\"]\n", @@ -1032,7 +1034,7 @@ " labels = predictions[\"labels\"].cpu().numpy()\n", "\n", " # Obtain the bounding boxes predicted for humans\n", - " human_bboxes = [bbox for bbox, label in zip(bboxes, labels) if label == 1]\n", + " human_bboxes = [bbox for bbox, label in zip(bboxes, labels, strict=False) if label == 1]\n", "\n", " # Convert bounding boxes to xywh format\n", " bboxes = np.zeros((0, 4))\n", diff --git a/examples/JUPYTER/Demo_3D_DeepLabCut.ipynb b/examples/JUPYTER/Demo_3D_DeepLabCut.ipynb index 8148f383e7..7ac1a97f0a 100644 --- a/examples/JUPYTER/Demo_3D_DeepLabCut.ipynb +++ b/examples/JUPYTER/Demo_3D_DeepLabCut.ipynb @@ -197,7 +197,6 @@ "metadata": {}, "outputs": [], "source": [ - "\n", "%matplotlib inline\n", "\n", "deeplabcut.check_undistortion(config_path3d)" diff --git a/examples/testscript_multianimal.py b/examples/testscript_multianimal.py index f96c09e79c..e0a35924e7 100644 --- a/examples/testscript_multianimal.py +++ b/examples/testscript_multianimal.py @@ -82,7 +82,7 @@ bodyparts_multi, ) = auxfun_multianimal.extractindividualsandbodyparts(cfg) animals_id = [i for i in range(n_animals) for _ in bodyparts_multi] + [n_animals] * len(bodyparts_single) - map_ = dict(zip(range(len(animals)), animals)) + map_ = dict(zip(range(len(animals)), animals, strict=False)) individuals = [map_[ind] for ind in animals_id for _ in range(2)] scorer = [SCORER] * len(individuals) coords = ["x", "y"] * len(animals_id) diff --git a/examples/testscript_openfielddata.py b/examples/testscript_openfielddata.py index 84829f966c..37cd9f9001 100644 --- a/examples/testscript_openfielddata.py +++ b/examples/testscript_openfielddata.py @@ -9,8 +9,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -""" -Created on Mon Nov 5 18:06:13 2018 +"""Created on Mon Nov 5 18:06:13 2018. @author: alex diff --git a/examples/testscript_openfielddata_augmentationcomparison.py b/examples/testscript_openfielddata_augmentationcomparison.py index 58e48f8054..6e88425d40 100644 --- a/examples/testscript_openfielddata_augmentationcomparison.py +++ b/examples/testscript_openfielddata_augmentationcomparison.py @@ -9,9 +9,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -""" - -This is a test script to compare the loaders and models. +"""This is a test script to compare the loaders and models. This script creates one identical splits for the openfield test dataset and trains it with imgaug (default), scalecrop and the tensorpack loader. We also compare 3 backbones (mobilenet, resnet, efficientnet) @@ -51,7 +49,6 @@ Notice: despite the higher RMSE for imgaug due to the augmentation, the network performs much better on the testvideo (see Neuron Primer: https://www.cell.com/neuron/pdf/S0896-6273(20)30717-0.pdf) - """ import os diff --git a/examples/testscript_pretrained_models.py b/examples/testscript_pretrained_models.py index 8556a2f24a..6d3594fb8c 100644 --- a/examples/testscript_pretrained_models.py +++ b/examples/testscript_pretrained_models.py @@ -8,10 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -""" -Testscript human network - -""" +"""Testscript human network.""" import os diff --git a/examples/testscript_pytorch_multi_animal.py b/examples/testscript_pytorch_multi_animal.py index c713b1d28c..4a4de3c55a 100644 --- a/examples/testscript_pytorch_multi_animal.py +++ b/examples/testscript_pytorch_multi_animal.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Testscript for single animal PyTorch projects""" +"""Testscript for single animal PyTorch projects.""" from __future__ import annotations diff --git a/examples/testscript_pytorch_single_animal.py b/examples/testscript_pytorch_single_animal.py index b0baac9d34..ef247dcd17 100644 --- a/examples/testscript_pytorch_single_animal.py +++ b/examples/testscript_pytorch_single_animal.py @@ -1,4 +1,4 @@ -"""Testscript for single animal PyTorch projects""" +"""Testscript for single animal PyTorch projects.""" from __future__ import annotations diff --git a/examples/testscript_superanimal_adaptation.py b/examples/testscript_superanimal_adaptation.py index e45a1265ca..8bb93577a2 100644 --- a/examples/testscript_superanimal_adaptation.py +++ b/examples/testscript_superanimal_adaptation.py @@ -8,9 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -""" -Test script for super animal adaptation -""" +"""Test script for super animal adaptation.""" import os diff --git a/examples/testscript_superanimal_inference.py b/examples/testscript_superanimal_inference.py index 5861896e33..efbecf6826 100644 --- a/examples/testscript_superanimal_inference.py +++ b/examples/testscript_superanimal_inference.py @@ -8,10 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -""" -Testscript for super animal inference - -""" +"""Testscript for super animal inference.""" import os diff --git a/examples/testscript_transreid.py b/examples/testscript_transreid.py index 83b6aee58e..9964201ee5 100644 --- a/examples/testscript_transreid.py +++ b/examples/testscript_transreid.py @@ -78,7 +78,7 @@ bodyparts_multi, ) = auxfun_multianimal.extractindividualsandbodyparts(cfg) animals_id = [i for i in range(n_animals) for _ in bodyparts_multi] + [n_animals] * len(bodyparts_single) - map_ = dict(zip(range(len(animals)), animals)) + map_ = dict(zip(range(len(animals)), animals, strict=False)) individuals = [map_[ind] for ind in animals_id for _ in range(2)] scorer = [SCORER] * len(individuals) coords = ["x", "y"] * len(animals_id) diff --git a/examples/utils.py b/examples/utils.py index 96f54dba58..9390cea392 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -71,7 +71,7 @@ def sample_pose_random( img_h: int, img_w: int, ) -> np.ndarray: - """Fully random pose sampling""" + """Fully random pose sampling.""" xs = gen.choice(img_w, size=(num_individuals, num_bodyparts), replace=False) ys = gen.choice(img_h, size=(num_individuals, num_bodyparts), replace=False) pose = np.stack([xs, ys], axis=-1) @@ -97,9 +97,9 @@ def sample_pose_from_center( num_unique: int, radius: int = 25, ) -> np.ndarray: - """Sample keypoints from the center of each individual""" + """Sample keypoints from the center of each individual.""" pose = np.zeros((num_individuals, num_bodyparts, 2)) - for i, (xc, yc) in enumerate(zip(center_xs, center_ys)): + for i, (xc, yc) in enumerate(zip(center_xs, center_ys, strict=False)): if i < num_individuals: x_start, x_end = xc - radius + 1, xc + radius - 1 y_start, y_end = yc - radius + 1, yc + radius - 1 diff --git a/ruff-report.md b/ruff-report.md index 2d82563faa..f4abf20d74 100644 --- a/ruff-report.md +++ b/ruff-report.md @@ -83,8 +83,8 @@ Total remaining issues: **1437** ## E501 -Count: **333** -Hint: Line too long. Prefer wrapping expressions, splitting long strings/comments, or extracting variables. +Count: **333** +Hint: Line too long. Prefer wrapping expressions, splitting long strings/comments, or extracting variables. ### Files affected @@ -1232,8 +1232,8 @@ code -g "examples\testscript_pretrained_models.py:32" ## F401 -Count: **331** -Hint: Unused import. Usually safe to delete; verify imports with side effects. +Count: **331** +Hint: Unused import. Usually safe to delete; verify imports with side effects. ### Files affected @@ -2079,7 +2079,7 @@ code -g "deeplabcut\post_processing\__init__.py:21" ## B905 -Count: **176** +Count: **176** ### Files affected @@ -3226,7 +3226,7 @@ code -g "tests\test_stitcher.py:103" ## F841 -Count: **141** +Count: **141** ### Files affected @@ -3870,8 +3870,8 @@ code -g "tests\test_pose_multianimal_imgaug.py:109" ## E402 -Count: **93** -Hint: Module import not at top of file. Move imports above executable code if possible. +Count: **93** +Hint: Module import not at top of file. Move imports above executable code if possible. ### Files affected @@ -4095,8 +4095,8 @@ code -g "tests\pose_estimation_pytorch\modelzoo\test_fmpose_integration.py:19" ## UP031 -Count: **76** -Hint: Old `%` formatting. Convert to f-strings or `.format()` where appropriate. +Count: **76** +Hint: Old `%` formatting. Convert to f-strings or `.format()` where appropriate. ### Files affected @@ -4483,8 +4483,8 @@ code -g "examples\COLAB\COLAB_DEMO_SuperAnimal.ipynb:23" ## B007 -Count: **51** -Hint: Unused loop variable. Rename to `_` or use it. +Count: **51** +Hint: Unused loop variable. Rename to `_` or use it. ### Files affected @@ -4978,7 +4978,7 @@ code -g "tests\test_auxiliaryfunctions.py:39" ## B028 -Count: **49** +Count: **49** ### Files affected @@ -5374,8 +5374,8 @@ code -g "deeplabcut\utils\auxiliaryfunctions.py:292" ## F403 -Count: **36** -Hint: `from x import *` makes names unclear. Replace with explicit imports. +Count: **36** +Hint: `from x import *` makes names unclear. Replace with explicit imports. ### Files affected @@ -5566,7 +5566,7 @@ code -g "deeplabcut\post_processing\__init__.py:22" ## E712 -Count: **22** +Count: **22** ### Files affected @@ -5719,8 +5719,8 @@ code -g "deeplabcut\utils\auxiliaryfunctions_3d.py:94" ## F821 -Count: **22** -Hint: Undefined name. Usually a real bug or missing import. +Count: **22** +Hint: Undefined name. Usually a real bug or missing import. ### Files affected @@ -5825,8 +5825,8 @@ code -g "deeplabcut\pose_estimation_3d\triangulation.py:140" ## B904 -Count: **19** -Hint: Inside `except`, use `raise ... from e` to preserve exception chaining. +Count: **19** +Hint: Inside `except`, use `raise ... from e` to preserve exception chaining. ### Files affected @@ -6012,8 +6012,8 @@ code -g "deeplabcut\utils\conversioncode.py:303" ## E722 -Count: **19** -Hint: Bare `except:`. Catch `Exception` or a narrower exception type. +Count: **19** +Hint: Bare `except:`. Catch `Exception` or a narrower exception type. ### Files affected @@ -6223,8 +6223,8 @@ code -g "deeplabcut\utils\make_labeled_video.py:1333" ## F405 -Count: **16** -Hint: Likely consequence of `import *`. Import the name explicitly. +Count: **16** +Hint: Likely consequence of `import *`. Import the name explicitly. ### Files affected @@ -6263,8 +6263,8 @@ code -g "deeplabcut\gui\window.py:559" ## E721 -Count: **14** -Hint: Avoid direct `type(x) == Y`; prefer `isinstance(x, Y)`. +Count: **14** +Hint: Avoid direct `type(x) == Y`; prefer `isinstance(x, Y)`. ### Files affected @@ -6361,7 +6361,7 @@ code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_tensorpack.py:169" ## B006 -Count: **12** +Count: **12** ### Files affected @@ -6468,7 +6468,7 @@ code -g "deeplabcut\utils\make_labeled_video.py:417" ## E711 -Count: **7** +Count: **7** ### Files affected @@ -6534,7 +6534,7 @@ code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py:43" ## E731 -Count: **4** +Count: **4** ### Files affected @@ -6597,8 +6597,8 @@ code -g "deeplabcut\utils\auxfun_videos.py:272" ## B008 -Count: **3** -Hint: Function call in default arg. Use `None` + initialize inside the function. +Count: **3** +Hint: Function call in default arg. Use `None` + initialize inside the function. ### Files affected @@ -6648,8 +6648,8 @@ code -g "examples\testscript_pytorch_single_animal.py:29" ## B023 -Count: **2** -Hint: Function closes over loop variable. Bind it via default arg or helper. +Count: **2** +Hint: Function closes over loop variable. Bind it via default arg or helper. ### Files affected @@ -6686,8 +6686,8 @@ code -g "deeplabcut\refine_training_dataset\stitch.py:1177" ## B024 -Count: **2** -Hint: ABC without abstract method. Add `@abstractmethod` or remove ABC intent. +Count: **2** +Hint: ABC without abstract method. Add `@abstractmethod` or remove ABC intent. ### Files affected @@ -6724,8 +6724,8 @@ code -g "deeplabcut\pose_estimation_pytorch\runners\shelving.py:21" ## F811 -Count: **2** -Hint: Redefined while unused. Remove duplicate or rename. +Count: **2** +Hint: Redefined while unused. Remove duplicate or rename. ### Files affected @@ -6762,7 +6762,7 @@ code -g "tests\generate_training_dataset\test_trainset_metadata.py:246" ## B011 -Count: **1** +Count: **1** ### Files affected @@ -6786,8 +6786,8 @@ code -g "deeplabcut\pose_estimation_tensorflow\nnets\utils.py:115" ## B012 -Count: **1** -Hint: Jump statement in `finally` can swallow exceptions. Restructure flow. +Count: **1** +Hint: Jump statement in `finally` can swallow exceptions. Restructure flow. ### Files affected @@ -6811,8 +6811,8 @@ code -g "deeplabcut\pose_estimation_tensorflow\predict_videos.py:1053" ## B016 -Count: **1** -Hint: Raise an exception instance/class, not a literal. +Count: **1** +Hint: Raise an exception instance/class, not a literal. ### Files affected @@ -6836,8 +6836,8 @@ code -g "examples\testscript_3d.py:126" ## B017 -Count: **1** -Hint: Use a more specific exception with `assertRaises`. +Count: **1** +Hint: Use a more specific exception with `assertRaises`. ### Files affected @@ -6861,8 +6861,8 @@ code -g "tests\pose_estimation_pytorch\other\test_api_utils.py:67" ## B020 -Count: **1** -Hint: Loop variable overrides iterator. Rename loop variables. +Count: **1** +Hint: Loop variable overrides iterator. Rename loop variables. ### Files affected @@ -6886,8 +6886,8 @@ code -g "deeplabcut\pose_estimation_pytorch\models\heads\simple_head.py:196" ## B027 -Count: **1** -Hint: Empty method in ABC without abstract decorator. Add `@abstractmethod` or implement it. +Count: **1** +Hint: Empty method in ABC without abstract decorator. Add `@abstractmethod` or implement it. ### Files affected @@ -6911,7 +6911,7 @@ code -g "deeplabcut\pose_estimation_pytorch\models\modules\kpt_encoders.py:47" ## UP028 -Count: **1** +Count: **1** ### Files affected diff --git a/tests/core/inferenceutils/test_map_computation.py b/tests/core/inferenceutils/test_map_computation.py index 3cf4cfb9be..c2cd4fffe9 100644 --- a/tests/core/inferenceutils/test_map_computation.py +++ b/tests/core/inferenceutils/test_map_computation.py @@ -1,4 +1,4 @@ -"""Tests mAP computation from inferenceutils""" +"""Tests mAP computation from inferenceutils.""" from __future__ import annotations @@ -315,7 +315,7 @@ def _to_coco_ground_truth( images.append(dict(id=id_, file_name=path, width=w, height=h)) assert image_keypoints.shape[1] == num_joints - for idv_id, kpts in enumerate(image_keypoints): + for _idv_id, kpts in enumerate(image_keypoints): visible = kpts[:, 2] > 0 num_keypoints = visible.sum() diff --git a/tests/core/metrics/test_metrics_map_computation.py b/tests/core/metrics/test_metrics_map_computation.py index 1e7c1ae9bd..5ef6a64f75 100644 --- a/tests/core/metrics/test_metrics_map_computation.py +++ b/tests/core/metrics/test_metrics_map_computation.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Tests that mAP computation is correct""" +"""Tests that mAP computation is correct.""" from __future__ import annotations @@ -291,7 +291,7 @@ def _to_coco_ground_truth( images.append(dict(id=id_, file_name=path, width=w, height=h)) assert image_keypoints.shape[1] == num_joints - for idv_id, kpts in enumerate(image_keypoints): + for _idv_id, kpts in enumerate(image_keypoints): visible = kpts[:, 2] > 0 num_keypoints = visible.sum() diff --git a/tests/create_project/test_video_set_configuration.py b/tests/create_project/test_video_set_configuration.py index 32d7552379..5d163d367c 100644 --- a/tests/create_project/test_video_set_configuration.py +++ b/tests/create_project/test_video_set_configuration.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Unit tests for deeplabcut.create_project.new module""" +"""Unit tests for deeplabcut.create_project.new module.""" import warnings from pathlib import Path @@ -28,7 +28,7 @@ def project_directory(tmpdir_factory) -> Path: @pytest.fixture def mock_video_file(tmpdir_factory) -> Path: - """Create a mock video file for testing""" + """Create a mock video file for testing.""" fake_folder = tmpdir_factory.mktemp("some_video") video_path = Path(fake_folder) / "test_video.avi" video_path.write_bytes(b"fake video content") @@ -37,7 +37,7 @@ def mock_video_file(tmpdir_factory) -> Path: @pytest.fixture def mock_video_reader() -> VideoReader: - """Create a mock VideoReader""" + """Create a mock VideoReader.""" mock_reader = Mock(spec=VideoReader) mock_reader.get_bbox.return_value = (0, 640, 277, 624) return mock_reader @@ -45,7 +45,7 @@ def mock_video_reader() -> VideoReader: @pytest.fixture def video_directory(tmpdir_factory) -> Path: - """Create a directory with multiple video files""" + """Create a directory with multiple video files.""" video_dir = Path(tmpdir_factory.mktemp("some_videos")) video_dir.mkdir(exist_ok=True) @@ -63,7 +63,7 @@ def test_project_directory_creation_basic( mock_video_file: Path, mock_video_reader: VideoReader, ): - """Test that project directories are created correctly""" + """Test that project directories are created correctly.""" with patch("deeplabcut.create_project.new.VideoReader", return_value=mock_video_reader): config_path = new_module.create_new_project( project="test-project", @@ -88,7 +88,7 @@ def test_single_video_file( mock_video_reader: VideoReader, copy_videos: bool, ): - """Test adding a single video file""" + """Test adding a single video file.""" with patch("deeplabcut.create_project.new.VideoReader", return_value=mock_video_reader): config_path = new_module.create_new_project( project="test", @@ -114,7 +114,7 @@ def test_video_directory( mock_video_reader: VideoReader, copy_videos: bool, ): - """Test adding videos from a directory""" + """Test adding videos from a directory.""" with patch("deeplabcut.create_project.new.VideoReader", return_value=mock_video_reader): config_path = new_module.create_new_project( project="test", @@ -141,7 +141,7 @@ def test_mixed_video_files_and_directories( mock_video_reader: VideoReader, copy_videos: bool, ): - """Test adding both video files and directories""" + """Test adding both video files and directories.""" with patch("deeplabcut.create_project.new.VideoReader", return_value=mock_video_reader): config_path = new_module.create_new_project( project="test", @@ -163,7 +163,7 @@ def test_empty_video_directory( tmpdir: Path, mock_video_reader: VideoReader, ): - """Test handling of empty video directory""" + """Test handling of empty video directory.""" empty_dir = tmpdir / "empty_videos" empty_dir.mkdir() @@ -186,7 +186,7 @@ def test_valid_video_included_in_config( mock_video_file: Path, mock_video_reader: VideoReader, ): - """Test that valid videos are included in the config file""" + """Test that valid videos are included in the config file.""" with patch("deeplabcut.create_project.new.VideoReader", return_value=mock_video_reader): config_path = new_module.create_new_project( project="test", @@ -211,7 +211,7 @@ def test_invalid_video_removed_from_project( tmpdir: Path, mock_video_file: Path, ): - """Test that invalid videos are removed from the project""" + """Test that invalid videos are removed from the project.""" # Mock VideoReader to raise IOError mock_reader = Mock(side_effect=OSError("Cannot open video")) @@ -234,7 +234,7 @@ def test_config_file_video_sets_format( mock_video_file: Path, mock_video_reader: VideoReader, ): - """Test that video_sets in config has correct format""" + """Test that video_sets in config has correct format.""" with patch("deeplabcut.create_project.new.VideoReader", return_value=mock_video_reader): config_path = new_module.create_new_project( project="test", @@ -252,7 +252,7 @@ def test_config_file_video_sets_format( assert isinstance(cfg["video_sets"], dict) # Check format of video_sets entries - for video_path, video_info in cfg["video_sets"].items(): + for _video_path, video_info in cfg["video_sets"].items(): assert isinstance(video_info, dict) assert "crop" in video_info assert isinstance(video_info["crop"], str) diff --git a/tests/generate_training_dataset/test_trainset_metadata.py b/tests/generate_training_dataset/test_trainset_metadata.py index 6683844d75..6147134d1d 100644 --- a/tests/generate_training_dataset/test_trainset_metadata.py +++ b/tests/generate_training_dataset/test_trainset_metadata.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Tests for deeplabcut/generate_training_dataset/metadata.py""" +"""Tests for deeplabcut/generate_training_dataset/metadata.py.""" from __future__ import annotations @@ -66,7 +66,7 @@ ) @pytest.mark.parametrize("load_splits", [True, False]) def test_load_metadata(tmpdir, data: dict, load_splits: bool): - """Tests that loading the metadata from files doesn't fail""" + """Tests that loading the metadata from files doesn't fail.""" # write data to tmp file cfg, cfg_path, trainset_dir, meta_path = _create_project_with_config(tmpdir) with open(meta_path, "w") as f: @@ -78,7 +78,7 @@ def test_load_metadata(tmpdir, data: dict, load_splits: bool): print(data["splits"]) print() - for name, s in data["shuffles"].items(): + for _name, s in data["shuffles"].items(): split = data["splits"][s["split"]] train, test = split["train"], split["test"] _create_doc_data(cfg, trainset_dir, s["train_fraction"], s["index"], train, test) @@ -161,7 +161,7 @@ def test_load_metadata(tmpdir, data: dict, load_splits: bool): ], ) def test_save_metadata_simple(tmpdir, data): - """Tests that saving the metadata creates the expected file""" + """Tests that saving the metadata creates the expected file.""" cfg, cfg_path, trainset_dir, meta_path = _create_project_with_config(tmpdir) trainset_meta = metadata.TrainingDatasetMetadata(cfg, data["shuffles"]) print(trainset_meta) @@ -179,7 +179,7 @@ def test_save_metadata_simple(tmpdir, data): [[SHUFFLES[i] for i in indices] for indices in [[1], [1, 2], [1, 2, 3], [1, 2, 4], [1, 3, 4], [1, 2, 3, 4]]], ) def test_save_metadata(tmpdir, shuffles): - """Tests that saving the metadata and reloading it leads to the same instance""" + """Tests that saving the metadata and reloading it leads to the same instance.""" cfg, cfg_path, trainset_dir, meta_path = _create_project_with_config(tmpdir) for s in shuffles: train, test = ( @@ -208,7 +208,7 @@ def test_save_metadata(tmpdir, shuffles): def test_add_shuffle(tmpdir): - """Tests that a shuffle can be added correctlt""" + """Tests that a shuffle can be added correctlt.""" cfg, cfg_path, trainset_dir, meta_path = _create_project_with_config(tmpdir) trainset_meta = metadata.TrainingDatasetMetadata(cfg, (SHUFFLES[1],)) trainset_meta_added = trainset_meta.add(SHUFFLES[2]) @@ -218,7 +218,7 @@ def test_add_shuffle(tmpdir): def test_add_shuffle_twice(tmpdir): - """Tests that a shuffle can be added correctlt""" + """Tests that a shuffle can be added correctlt.""" cfg, cfg_path, trainset_dir, meta_path = _create_project_with_config(tmpdir) trainset_meta = metadata.TrainingDatasetMetadata(cfg, (SHUFFLES[1],)) trainset_meta_added = trainset_meta.add(SHUFFLES[2]) @@ -230,7 +230,7 @@ def test_add_shuffle_twice(tmpdir): def test_add_shuffle_sorts_to_correct_order(tmpdir): - """Tests that a shuffle can be added correctlt""" + """Tests that a shuffle can be added correctlt.""" cfg, cfg_path, trainset_dir, meta_path = _create_project_with_config(tmpdir) trainset_meta = metadata.TrainingDatasetMetadata(cfg, (SHUFFLES[1], SHUFFLES[3])) trainset_meta_added = trainset_meta.add(SHUFFLES[2]) @@ -244,7 +244,7 @@ def test_add_shuffle_sorts_to_correct_order(tmpdir): ) @pytest.mark.parametrize("shuffle_to_add", [1, 2, 3, 4]) def test_add_shuffle(tmpdir, shuffles, shuffle_to_add): - """Tests""" + """Tests.""" cfg, cfg_path, trainset_dir, meta_path = _create_project_with_config(tmpdir) trainset_meta = metadata.TrainingDatasetMetadata(cfg, tuple([SHUFFLES[i] for i in shuffles])) if shuffle_to_add in shuffles: @@ -273,7 +273,7 @@ def test_add_shuffle(tmpdir, shuffles, shuffle_to_add): ], ) def test_data_split_equality(split1, split2, equal): - """Tests that equality functions as expected for DataSplits""" + """Tests that equality functions as expected for DataSplits.""" print(split1) print(split2) print(equal) @@ -284,7 +284,7 @@ def test_data_split_equality(split1, split2, equal): @pytest.mark.parametrize("indices", [(2, 1), (10, 1), (1, 21, 20), (1, 2, 4, 3)]) @pytest.mark.parametrize("sorted_indices", [(1, 2), (10, 12), (3, 4), (1, 1000, 1200)]) def test_data_split_requires_sorted(split_idx: int, indices: tuple[int], sorted_indices: tuple[int]): - """Tests that equality functions as expected for DataSplits""" + """Tests that equality functions as expected for DataSplits.""" with pytest.raises(RuntimeError): metadata.DataSplit(train_indices=tuple(indices), test_indices=tuple(sorted_indices)) @@ -309,7 +309,7 @@ def test_data_split_requires_sorted(split_idx: int, indices: tuple[int], sorted_ ], ) def test_create_metadata_from_shuffles(tmpdir, shuffles): - """Tests that equality functions as expected for DataSplits""" + """Tests that equality functions as expected for DataSplits.""" cfg, cfg_path, trainset_dir, meta_path = _create_project_with_config(tmpdir) print(trainset_dir) for s in shuffles: @@ -323,7 +323,7 @@ def test_create_metadata_from_shuffles(tmpdir, shuffles): print(trainset_metadata) assert len(trainset_metadata.shuffles) == len(shuffles) - for shuffle_data, shuffle in zip(shuffles, trainset_metadata.shuffles): + for shuffle_data, shuffle in zip(shuffles, trainset_metadata.shuffles, strict=False): print(shuffle.index) assert shuffle_data["idx"] == shuffle.index assert shuffle_data["train_fraction"] == shuffle.train_fraction diff --git a/tests/pose_estimation_pytorch/apis/test_apis_evaluate.py b/tests/pose_estimation_pytorch/apis/test_apis_evaluate.py index c671265255..253841df54 100644 --- a/tests/pose_estimation_pytorch/apis/test_apis_evaluate.py +++ b/tests/pose_estimation_pytorch/apis/test_apis_evaluate.py @@ -225,7 +225,7 @@ def test_evaluate_with_pcutoff( num_idv = len(individuals) num_bodyparts = len(bodyparts) - num_unique = len(unique_bodyparts) + len(unique_bodyparts) gt, pred = {}, {} for img in images: diff --git a/tests/pose_estimation_pytorch/apis/test_apis_export.py b/tests/pose_estimation_pytorch/apis/test_apis_export.py index d8dabdd36f..5e9e75c17d 100644 --- a/tests/pose_estimation_pytorch/apis/test_apis_export.py +++ b/tests/pose_estimation_pytorch/apis/test_apis_export.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Tests exporting models""" +"""Tests exporting models.""" import copy import shutil diff --git a/tests/pose_estimation_pytorch/data/test_data_ctd.py b/tests/pose_estimation_pytorch/data/test_data_ctd.py index eeb6be8d7f..37bd834d03 100644 --- a/tests/pose_estimation_pytorch/data/test_data_ctd.py +++ b/tests/pose_estimation_pytorch/data/test_data_ctd.py @@ -148,7 +148,8 @@ def test_ctd_load_hdf_containing_rel_paths( idv_mask = ~np.all(keypoint_mask, axis=2) output_pose = [ - p[p_mask] if np.any(p_mask) else np.zeros((0, num_bodyparts, 3)) for p, p_mask in zip(output_pose, idv_mask) + p[p_mask] if np.any(p_mask) else np.zeros((0, num_bodyparts, 3)) + for p, p_mask in zip(output_pose, idv_mask, strict=False) ] # generate columns for the dataframe @@ -170,7 +171,7 @@ def test_ctd_load_hdf_containing_rel_paths( df.to_hdf(conditions_filepath, key="df_with_missing") conditions = CondFromFile.load_conditions_h5(conditions_filepath, images, path_prefix=path_prefix) - for idx, (img_path, img_index) in enumerate(data): + for idx, (img_path, _img_index) in enumerate(data): assert img_path in conditions np.testing.assert_allclose(output_pose[idx], conditions[img_path]) diff --git a/tests/pose_estimation_pytorch/data/test_preprocessor.py b/tests/pose_estimation_pytorch/data/test_preprocessor.py index 4e6077ef93..9a68d76fe7 100644 --- a/tests/pose_estimation_pytorch/data/test_preprocessor.py +++ b/tests/pose_estimation_pytorch/data/test_preprocessor.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Tests the pre-processors""" +"""Tests the pre-processors.""" import albumentations as A import numpy as np @@ -153,6 +153,6 @@ def deep_equal(a, b): elif isinstance(a, list) and isinstance(b, list): if len(a) != len(b): return False - return all(deep_equal(x, y) for x, y in zip(a, b)) + return all(deep_equal(x, y) for x, y in zip(a, b, strict=False)) else: return a == b diff --git a/tests/pose_estimation_pytorch/other/test_heatmap_plateau_targets.py b/tests/pose_estimation_pytorch/other/test_heatmap_plateau_targets.py index d3b66eaf8d..b44dc3f39f 100644 --- a/tests/pose_estimation_pytorch/other/test_heatmap_plateau_targets.py +++ b/tests/pose_estimation_pytorch/other/test_heatmap_plateau_targets.py @@ -24,8 +24,8 @@ def get_target( locref_std: float, pos_dist_thresh: int, ): - """Summary - Getting the target generator for certain annotations, predictions and image size. + """Summary Getting the target generator for certain annotations, predictions and + image size. Args: batch_size (int): number of images @@ -48,7 +48,6 @@ def get_target( locref_stdev = 7.2801 pos_dist_thresh = 17 output: - """ labels = { "keypoints": torch.randint(1, min(image_size), (batch_size, num_animals, num_joints, 2)) @@ -150,9 +149,9 @@ def test_single_animal( locref_stdev: float, pos_dist_thresh: int, ): - """Summary - Testing, for single animals experiments (num_animals=1) if the distance between the expected keypoints - and the annotations keypoints is smaller than the radius plateau. + """Summary Testing, for single animals experiments (num_animals=1) if the distance + between the expected keypoints and the annotations keypoints is smaller than the + radius plateau. 'argmax' function returns the indices of the max values of all elements in the input tensor. If there are multiple maximal values, such as in our case because it's a plateau, then the diff --git a/tests/pose_estimation_pytorch/other/test_helper.py b/tests/pose_estimation_pytorch/other/test_helper.py index afd5825df1..1dfa250109 100644 --- a/tests/pose_estimation_pytorch/other/test_helper.py +++ b/tests/pose_estimation_pytorch/other/test_helper.py @@ -15,7 +15,7 @@ def test_train_valid_call(): tmp_model = torch.nn.Linear(3, 10) to_train_mode = tmp_model.train to_train_mode() - assert tmp_model.training == True + assert tmp_model.training to_valid_mode = tmp_model.eval to_valid_mode() - assert tmp_model.training == False + assert not tmp_model.training diff --git a/tests/pose_estimation_pytorch/runners/bottum_up.py b/tests/pose_estimation_pytorch/runners/bottum_up.py index 74a513b3a8..3548e45f01 100644 --- a/tests/pose_estimation_pytorch/runners/bottum_up.py +++ b/tests/pose_estimation_pytorch/runners/bottum_up.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Tests for the bottom-up pytorch runner""" +"""Tests for the bottom-up pytorch runner.""" from pathlib import Path from typing import Any @@ -54,7 +54,7 @@ def test_build_bottom_up_runner( root_path = Path(auxiliaryfunctions.get_deeplabcut_path()) template_path = root_path / "pose_estimation_pytorch" / "apis" / "pytorch_config.yaml" - template = auxiliaryfunctions.read_plainconfig(str(template_path)) + auxiliaryfunctions.read_plainconfig(str(template_path)) pytorch_cfg = make_pytorch_pose_config(project_cfg, str(template_path), net_type) print_dict(pytorch_cfg) @@ -83,7 +83,7 @@ def test_build_bottom_up_runner( scheduler = None logger = None - runner = RUNNERS.build( + RUNNERS.build( dict( **pytorch_cfg["solver"], model=pose_model, diff --git a/tests/pose_estimation_pytorch/runners/test_dynamic_cropper.py b/tests/pose_estimation_pytorch/runners/test_dynamic_cropper.py index 8c56f8883c..7c1ebb162f 100644 --- a/tests/pose_estimation_pytorch/runners/test_dynamic_cropper.py +++ b/tests/pose_estimation_pytorch/runners/test_dynamic_cropper.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Tests the dynamic cropper""" +"""Tests the dynamic cropper.""" import numpy as np import pytest @@ -163,7 +163,7 @@ def test_tddc_array_split(size: int, n: int, overlap: int) -> None: assert len(set(sizes)) == 1 # check the overlap is big enough for each section - for (start_1, end_1), (start_2, end_2) in zip(sections[:-1], sections[1:]): + for (_start_1, end_1), (start_2, _end_2) in zip(sections[:-1], sections[1:], strict=False): assert end_1 >= start_2 assert end_1 - start_2 >= overlap diff --git a/tests/test_auxiliaryfunctions.py b/tests/test_auxiliaryfunctions.py index 323dc321c8..30e3348514 100644 --- a/tests/test_auxiliaryfunctions.py +++ b/tests/test_auxiliaryfunctions.py @@ -19,7 +19,7 @@ def test_find_analyzed_data(tmpdir_factory): fake_folder = tmpdir_factory.mktemp("videos") SUPPORTED_VIDEOS = ["avi"] - n_ext = len(SUPPORTED_VIDEOS) + len(SUPPORTED_VIDEOS) SCORER = "DLC_dlcrnetms5_multi_mouseApr11shuffle1_5" WRONG_SCORER = "DLC_dlcrnetms5_multi_mouseApr11shuffle3_5" @@ -117,7 +117,7 @@ def _create_fake_file(filename): def test_write_config_has_skeleton(tmpdir_factory): - """Required for backward compatibility""" + """Required for backward compatibility.""" fake_folder = tmpdir_factory.mktemp("fakeConfigs") fake_config_file = fake_folder / Path("fakeConfig") auxiliaryfunctions.write_config(fake_config_file, {}) diff --git a/tests/test_frame_selection_tools.py b/tests/test_frame_selection_tools.py index 1241415456..17b615ad61 100644 --- a/tests/test_frame_selection_tools.py +++ b/tests/test_frame_selection_tools.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Tests for frame selection tools""" +"""Tests for frame selection tools.""" import math from unittest.mock import Mock diff --git a/tests/test_inferenceutils.py b/tests/test_inferenceutils.py index 1a427bd3d5..cd52c8cd36 100644 --- a/tests/test_inferenceutils.py +++ b/tests/test_inferenceutils.py @@ -27,7 +27,7 @@ def test_conv_square_to_condensed_indices(): mat[rows, cols] = mat[cols, rows] = np.arange(1, len(rows) + 1) vec = squareform(mat) vals = [] - for i, j in zip(rows, cols): + for i, j in zip(rows, cols, strict=False): ind = inferenceutils._conv_square_to_condensed_indices(i, j, n) vals.append(vec[ind]) np.testing.assert_equal(vec, vals) diff --git a/tests/test_pose_multianimal_imgaug.py b/tests/test_pose_multianimal_imgaug.py index 6fcc92fde5..381681f36c 100644 --- a/tests/test_pose_multianimal_imgaug.py +++ b/tests/test_pose_multianimal_imgaug.py @@ -70,12 +70,12 @@ def test_get_batch(ma_dataset): ma_dataset.batch_size = batch_size batch_images, joint_ids, batch_joints, data_items = ma_dataset.get_batch() assert len(batch_images) == len(joint_ids) == len(batch_joints) == len(data_items) == batch_size - for data_item, joint_id, batch_joint in zip(data_items, joint_ids, batch_joints): + for data_item, joint_id, batch_joint in zip(data_items, joint_ids, batch_joints, strict=False): assert len(data_item.joints) == len(joint_id) assert len(batch_joint) == len(np.concatenate(joint_id)) start = 0 mask = ~np.isnan(batch_joint).any(axis=1) - for joints, id_ in zip(data_item.joints.values(), joint_id): + for joints, id_ in zip(data_item.joints.values(), joint_id, strict=False): inds = id_ + start mask_ = mask[inds] np.testing.assert_equal(joints[:, 0], id_[mask_]) @@ -106,4 +106,4 @@ def test_get_targetmaps(ma_dataset, num_idchannel): def test_batching(ma_dataset): for _ in range(10): - batch = ma_dataset.next_batch() + ma_dataset.next_batch() diff --git a/tests/test_predict_supermodel.py b/tests/test_predict_supermodel.py index ae575cfabe..1453984620 100644 --- a/tests/test_predict_supermodel.py +++ b/tests/test_predict_supermodel.py @@ -23,7 +23,7 @@ def test_get_multi_scale_frames(): heights, ) assert len(frames) == len(shapes) == len(heights) - assert all(shape[0] == h for shape, h in zip(shapes, heights)) + assert all(shape[0] == h for shape, h in zip(shapes, heights, strict=False)) assert all(round(shape[0] * ar) == shape[1] for shape in shapes) @@ -45,4 +45,4 @@ def test_project_pred_to_original_size(scale): ) coords_orig = preds_orig["coordinates"][0] assert len(coords_orig) == len(xs) - assert all([round(x * scale) == round(xy[0]) for xy, x in zip(coords_orig, xs)]) + assert all([round(x * scale) == round(xy[0]) for xy, x in zip(coords_orig, xs, strict=False)]) diff --git a/tests/test_stitcher.py b/tests/test_stitcher.py index 7422de34f6..699ba96d7f 100644 --- a/tests/test_stitcher.py +++ b/tests/test_stitcher.py @@ -100,7 +100,7 @@ def test_tracklet_data_access(tracklet): @pytest.mark.parametrize( "tracklet, where, norm", - list(zip(make_fake_tracklets(), ("head", "tail"), (False, True))), + list(zip(make_fake_tracklets(), ("head", "tail"), (False, True), strict=False)), ) def test_tracklet_calc_velocity(tracklet, where, norm): _ = tracklet.calc_velocity(where, norm) diff --git a/tools/update_license_headers.py b/tools/update_license_headers.py index c06ae015b0..ca5381deb2 100644 --- a/tools/update_license_headers.py +++ b/tools/update_license_headers.py @@ -1,7 +1,7 @@ """Apply copyright headers to all code files in the repository. -This file can be called as a python script without arguments. For -configuration, see the instructions in NOTICE.yml. +This file can be called as a python script without arguments. For configuration, see the +instructions in NOTICE.yml. """ import fnmatch @@ -19,7 +19,7 @@ def load_config(filename): def walk_directory(entry): - """Talk the directory""" + """Talk the directory.""" if "header" not in entry: raise ValueError("Current entry does not have a header.") @@ -29,8 +29,7 @@ def walk_directory(entry): def _list_include(): """List all files specified in the include list.""" for include_pattern in entry["include"]: - for filename in glob.iglob(include_pattern, recursive=True): - yield filename + yield from glob.iglob(include_pattern, recursive=True) def _filter_exclude(iterable): """Filter filenames from an iterator by the exclude patterns.""" From 6037535387412e701accd43d35aaccbed463deb5 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 17 Mar 2026 14:22:07 +0100 Subject: [PATCH 10/80] Remove ruff-report and ignore tool output Delete the generated ruff-report.md (large auto-generated lint report) and add tmp/* to .gitignore to avoid committing tool output. Also include an update to tools/ruff_report.py (script changes) alongside the cleanup. --- .gitignore | 3 + ruff-report.md | 6934 ------------------------------------------ tools/ruff_report.py | 2 +- 3 files changed, 4 insertions(+), 6935 deletions(-) delete mode 100644 ruff-report.md diff --git a/.gitignore b/.gitignore index ad9f192703..cf82bfb27e 100644 --- a/.gitignore +++ b/.gitignore @@ -138,3 +138,6 @@ ENV/ # mypy .mypy_cache/ + +# Tools output +tmp/* diff --git a/ruff-report.md b/ruff-report.md deleted file mode 100644 index f4abf20d74..0000000000 --- a/ruff-report.md +++ /dev/null @@ -1,6934 +0,0 @@ -# Ruff manual-fix report - -Generated from: `.` - -Total remaining issues: **1437** - -## Summary - -| Rule | Count | Note | -|---|---:|---| -| `E501` | 333 | Line too long. Prefer wrapping expressions, splitting long strings/comments, or extracting variables. | -| `F401` | 331 | Unused import. Usually safe to delete; verify imports with side effects. | -| `B905` | 176 | | -| `F841` | 141 | | -| `E402` | 93 | Module import not at top of file. Move imports above executable code if possible. | -| `UP031` | 76 | Old `%` formatting. Convert to f-strings or `.format()` where appropriate. | -| `B007` | 51 | Unused loop variable. Rename to `_` or use it. | -| `B028` | 49 | | -| `F403` | 36 | `from x import *` makes names unclear. Replace with explicit imports. | -| `E712` | 22 | | -| `F821` | 22 | Undefined name. Usually a real bug or missing import. | -| `B904` | 19 | Inside `except`, use `raise ... from e` to preserve exception chaining. | -| `E722` | 19 | Bare `except:`. Catch `Exception` or a narrower exception type. | -| `F405` | 16 | Likely consequence of `import *`. Import the name explicitly. | -| `E721` | 14 | Avoid direct `type(x) == Y`; prefer `isinstance(x, Y)`. | -| `B006` | 12 | | -| `E711` | 7 | | -| `E731` | 4 | | -| `B008` | 3 | Function call in default arg. Use `None` + initialize inside the function. | -| `B023` | 2 | Function closes over loop variable. Bind it via default arg or helper. | -| `B024` | 2 | ABC without abstract method. Add `@abstractmethod` or remove ABC intent. | -| `F811` | 2 | Redefined while unused. Remove duplicate or rename. | -| `B011` | 1 | | -| `B012` | 1 | Jump statement in `finally` can swallow exceptions. Restructure flow. | -| `B016` | 1 | Raise an exception instance/class, not a literal. | -| `B017` | 1 | Use a more specific exception with `assertRaises`. | -| `B020` | 1 | Loop variable overrides iterator. Rename loop variables. | -| `B027` | 1 | Empty method in ABC without abstract decorator. Add `@abstractmethod` or implement it. | -| `UP028` | 1 | | - -## Suggested triage order - -1. `F403` — `from x import *` makes names unclear. Replace with explicit imports. -2. `F405` — Likely consequence of `import *`. Import the name explicitly. -3. `F821` — Undefined name. Usually a real bug or missing import. -4. `E722` — Bare `except:`. Catch `Exception` or a narrower exception type. -5. `B904` — Inside `except`, use `raise ... from e` to preserve exception chaining. -6. `E402` — Module import not at top of file. Move imports above executable code if possible. -7. `F401` — Unused import. Usually safe to delete; verify imports with side effects. -8. `E501` — Line too long. Prefer wrapping expressions, splitting long strings/comments, or extracting variables. - -## Table of contents by rule - -- [E501 (333)](#e501) -- [F401 (331)](#f401) -- [B905 (176)](#b905) -- [F841 (141)](#f841) -- [E402 (93)](#e402) -- [UP031 (76)](#up031) -- [B007 (51)](#b007) -- [B028 (49)](#b028) -- [F403 (36)](#f403) -- [E712 (22)](#e712) -- [F821 (22)](#f821) -- [B904 (19)](#b904) -- [E722 (19)](#e722) -- [F405 (16)](#f405) -- [E721 (14)](#e721) -- [B006 (12)](#b006) -- [E711 (7)](#e711) -- [E731 (4)](#e731) -- [B008 (3)](#b008) -- [B023 (2)](#b023) -- [B024 (2)](#b024) -- [F811 (2)](#f811) -- [B011 (1)](#b011) -- [B012 (1)](#b012) -- [B016 (1)](#b016) -- [B017 (1)](#b017) -- [B020 (1)](#b020) -- [B027 (1)](#b027) -- [UP028 (1)](#up028) - -## E501 - -Count: **333** -Hint: Line too long. Prefer wrapping expressions, splitting long strings/comments, or extracting variables. - -### Files affected - -| File | Count | -|---|---:| -| `deeplabcut\pose_estimation_tensorflow\predict_videos.py` | 42 | -| `deeplabcut\cli.py` | 30 | -| `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` | 19 | -| `deeplabcut\compat.py` | 16 | -| `deeplabcut\pose_estimation_tensorflow\core\evaluate.py` | 15 | -| `deeplabcut\create_project\modelzoo.py` | 13 | -| `deeplabcut\pose_estimation_3d\camera_calibration.py` | 13 | -| `deeplabcut\pose_estimation_3d\plotting3D.py` | 11 | -| `deeplabcut\utils\conversioncode.py` | 11 | -| `deeplabcut\utils\frameselectiontools.py` | 10 | -| `deeplabcut\pose_estimation_3d\triangulation.py` | 9 | -| `deeplabcut\pose_estimation_tensorflow\visualizemaps.py` | 9 | -| `deeplabcut\utils\auxfun_videos.py` | 9 | -| `deeplabcut\benchmark\benchmarks.py` | 8 | -| `deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py` | 8 | -| `deeplabcut\refine_training_dataset\outlier_frames.py` | 7 | -| `deeplabcut\utils\auxiliaryfunctions.py` | 6 | -| `docs\recipes\flip_and_rotate.ipynb` | 6 | -| `deeplabcut\utils\auxfun_multianimal.py` | 5 | -| `deeplabcut\create_project\add.py` | 3 | -| `deeplabcut\create_project\new.py` | 3 | -| `deeplabcut\create_project\new_3d.py` | 3 | -| `deeplabcut\generate_training_dataset\frame_extraction.py` | 3 | -| `deeplabcut\generate_training_dataset\multiple_individuals_trainingsetmanipulation.py` | 3 | -| `deeplabcut\gui\tracklet_toolbox.py` | 3 | -| `deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc_dataframe.py` | 3 | -| `deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py` | 3 | -| `deeplabcut\modelzoo\generalized_data_converter\datasets\single_dlc_dataframe.py` | 3 | -| `deeplabcut\pose_estimation_tensorflow\modelzoo\api\spatiotemporal_adapt.py` | 3 | -| `deeplabcut\refine_training_dataset\stitch.py` | 3 | -| `deeplabcut\utils\make_labeled_video.py` | 3 | -| `deeplabcut\gui\window.py` | 2 | -| `deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py` | 2 | -| `deeplabcut\modelzoo\utils.py` | 2 | -| `deeplabcut\modelzoo\video_inference.py` | 2 | -| `deeplabcut\pose_estimation_pytorch\config\make_pose_config.py` | 2 | -| `deeplabcut\pose_estimation_tensorflow\core\train_multianimal.py` | 2 | -| `deeplabcut\pose_estimation_tensorflow\training.py` | 2 | -| `deeplabcut\pose_tracking_pytorch\model\backbones\vit_pytorch.py` | 2 | -| `deeplabcut\pose_tracking_pytorch\processor\processor.py` | 2 | -| `deeplabcut\utils\auxfun_models.py` | 2 | -| `deeplabcut\utils\auxiliaryfunctions_3d.py` | 2 | -| `deeplabcut\utils\pseudo_label.py` | 2 | -| `tests\pose_estimation_pytorch\other\test_match_predictions_to_gt.py` | 2 | -| `testscript_cli.py` | 2 | -| `deeplabcut\__main__.py` | 1 | -| `deeplabcut\gui\tabs\extract_outlier_frames.py` | 1 | -| `deeplabcut\gui\tabs\modelzoo.py` | 1 | -| `deeplabcut\gui\tabs\refine_tracklets.py` | 1 | -| `deeplabcut\gui\tabs\train_network.py` | 1 | -| `deeplabcut\gui\widgets.py` | 1 | -| `deeplabcut\modelzoo\weight_initialization.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\models\modules\conv_block.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\models\necks\transformer.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\models\predictors\paf_predictor.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\post_processing\match_predictions_to_gt.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\core\train.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\datasets\pose_tensorpack.py` | 1 | -| `deeplabcut\post_processing\analyze_skeleton.py` | 1 | -| `deeplabcut\utils\visualization.py` | 1 | -| `examples\JUPYTER\Demo_yourowndata.ipynb` | 1 | -| `examples\testscript_3d.py` | 1 | -| `examples\testscript_deterministicwithResNet152.py` | 1 | -| `examples\testscript_mobilenets.py` | 1 | -| `examples\testscript_openfielddata.py` | 1 | -| `examples\testscript_pretrained_models.py` | 1 | - -### Details - -#### `deeplabcut\pose_estimation_tensorflow\predict_videos.py` (42) - -| Line | Col | Message | -|---:|---:|---| -| 76 | 121 | Line too long (173 > 120) | -| 115 | 121 | Line too long (235 > 120) | -| 151 | 121 | Line too long (150 > 120) | -| 165 | 121 | Line too long (136 > 120) | -| 226 | 121 | Line too long (205 > 120) | -| 230 | 121 | Line too long (138 > 120) | -| 233 | 121 | Line too long (205 > 120) | -| 334 | 121 | Line too long (153 > 120) | -| 335 | 121 | Line too long (155 > 120) | -| 336 | 121 | Line too long (150 > 120) | -| 337 | 121 | Line too long (145 > 120) | -| 499 | 121 | Line too long (235 > 120) | -| 535 | 121 | Line too long (150 > 120) | -| 549 | 121 | Line too long (136 > 120) | -| 643 | 121 | Line too long (184 > 120) | -| 646 | 121 | Line too long (205 > 120) | -| 650 | 121 | Line too long (138 > 120) | -| 653 | 121 | Line too long (205 > 120) | -| 663 | 121 | Line too long (129 > 120) | -| 902 | 121 | Line too long (143 > 120) | -| 1077 | 121 | Line too long (133 > 120) | -| 1152 | 121 | Line too long (126 > 120) | -| 1154 | 121 | Line too long (142 > 120) | -| 1155 | 121 | Line too long (145 > 120) | -| 1156 | 121 | Line too long (146 > 120) | -| 1157 | 121 | Line too long (128 > 120) | -| 1168 | 121 | Line too long (122 > 120) | -| 1174 | 121 | Line too long (136 > 120) | -| 1176 | 121 | Line too long (140 > 120) | -| 1180 | 121 | Line too long (123 > 120) | -| 1185 | 121 | Line too long (121 > 120) | -| 1188 | 121 | Line too long (122 > 120) | -| 1219 | 121 | Line too long (235 > 120) | -| 1460 | 121 | Line too long (155 > 120) | -| 1463 | 121 | Line too long (140 > 120) | -| 1470 | 121 | Line too long (136 > 120) | -| 1476 | 121 | Line too long (133 > 120) | -| 1512 | 121 | Line too long (146 > 120) | -| 1515 | 121 | Line too long (165 > 120) | -| 1573 | 121 | Line too long (235 > 120) | -| 1626 | 121 | Line too long (123 > 120) | -| 1741 | 121 | Line too long (193 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\predict_videos.py:76" -``` - -#### `deeplabcut\cli.py` (30) - -| Line | Col | Message | -|---:|---:|---| -| 52 | 121 | Line too long (187 > 120) | -| 63 | 121 | Line too long (139 > 120) | -| 65 | 121 | Line too long (177 > 120) | -| 70 | 121 | Line too long (149 > 120) | -| 73 | 121 | Line too long (158 > 120) | -| 76 | 121 | Line too long (161 > 120) | -| 143 | 121 | Line too long (132 > 120) | -| 144 | 121 | Line too long (132 > 120) | -| 156 | 121 | Line too long (132 > 120) | -| 161 | 121 | Line too long (193 > 120) | -| 175 | 121 | Line too long (136 > 120) | -| 190 | 121 | Line too long (169 > 120) | -| 208 | 121 | Line too long (148 > 120) | -| 334 | 121 | Line too long (180 > 120) | -| 341 | 121 | Line too long (140 > 120) | -| 342 | 121 | Line too long (158 > 120) | -| 343 | 121 | Line too long (129 > 120) | -| 344 | 121 | Line too long (144 > 120) | -| 353 | 121 | Line too long (133 > 120) | -| 361 | 121 | Line too long (127 > 120) | -| 362 | 121 | Line too long (150 > 120) | -| 369 | 121 | Line too long (152 > 120) | -| 400 | 121 | Line too long (130 > 120) | -| 406 | 121 | Line too long (128 > 120) | -| 419 | 121 | Line too long (134 > 120) | -| 422 | 121 | Line too long (165 > 120) | -| 425 | 121 | Line too long (175 > 120) | -| 439 | 121 | Line too long (127 > 120) | -| 537 | 121 | Line too long (132 > 120) | -| 609 | 121 | Line too long (145 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\cli.py:52" -``` - -#### `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` (19) - -| Line | Col | Message | -|---:|---:|---| -| 41 | 121 | Line too long (123 > 120) | -| 68 | 121 | Line too long (151 > 120) | -| 74 | 121 | Line too long (177 > 120) | -| 158 | 121 | Line too long (139 > 120) | -| 503 | 121 | Line too long (127 > 120) | -| 516 | 121 | Line too long (311 > 120) | -| 525 | 121 | Line too long (130 > 120) | -| 562 | 121 | Line too long (136 > 120) | -| 623 | 121 | Line too long (138 > 120) | -| 626 | 121 | Line too long (143 > 120) | -| 635 | 121 | Line too long (137 > 120) | -| 636 | 121 | Line too long (138 > 120) | -| 645 | 121 | Line too long (125 > 120) | -| 652 | 121 | Line too long (161 > 120) | -| 653 | 121 | Line too long (146 > 120) | -| 1062 | 121 | Line too long (131 > 120) | -| 1066 | 121 | Line too long (141 > 120) | -| 1121 | 121 | Line too long (164 > 120) | -| 1125 | 121 | Line too long (144 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\generate_training_dataset\trainingsetmanipulation.py:41" -``` - -#### `deeplabcut\compat.py` (16) - -| Line | Col | Message | -|---:|---:|---| -| 587 | 121 | Line too long (176 > 120) | -| 593 | 121 | Line too long (146 > 120) | -| 602 | 121 | Line too long (141 > 120) | -| 609 | 121 | Line too long (137 > 120) | -| 610 | 121 | Line too long (141 > 120) | -| 611 | 121 | Line too long (147 > 120) | -| 612 | 121 | Line too long (149 > 120) | -| 1427 | 121 | Line too long (155 > 120) | -| 1430 | 121 | Line too long (140 > 120) | -| 1437 | 121 | Line too long (136 > 120) | -| 1443 | 121 | Line too long (133 > 120) | -| 1598 | 121 | Line too long (137 > 120) | -| 1599 | 121 | Line too long (141 > 120) | -| 1600 | 121 | Line too long (147 > 120) | -| 1601 | 121 | Line too long (149 > 120) | -| 1737 | 121 | Line too long (141 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\compat.py:587" -``` - -#### `deeplabcut\pose_estimation_tensorflow\core\evaluate.py` (15) - -| Line | Col | Message | -|---:|---:|---| -| 53 | 121 | Line too long (141 > 120) | -| 204 | 121 | Line too long (176 > 120) | -| 207 | 121 | Line too long (146 > 120) | -| 216 | 121 | Line too long (141 > 120) | -| 223 | 121 | Line too long (137 > 120) | -| 224 | 121 | Line too long (141 > 120) | -| 225 | 121 | Line too long (147 > 120) | -| 226 | 121 | Line too long (149 > 120) | -| 249 | 121 | Line too long (139 > 120) | -| 350 | 121 | Line too long (178 > 120) | -| 879 | 121 | Line too long (131 > 120) | -| 883 | 121 | Line too long (130 > 120) | -| 915 | 121 | Line too long (154 > 120) | -| 935 | 121 | Line too long (262 > 120) | -| 938 | 121 | Line too long (140 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\core\evaluate.py:53" -``` - -#### `deeplabcut\create_project\modelzoo.py` (13) - -| Line | Col | Message | -|---:|---:|---| -| 102 | 121 | Line too long (160 > 120) | -| 206 | 121 | Line too long (127 > 120) | -| 209 | 121 | Line too long (148 > 120) | -| 212 | 121 | Line too long (182 > 120) | -| 336 | 121 | Line too long (135 > 120) | -| 339 | 121 | Line too long (156 > 120) | -| 342 | 121 | Line too long (190 > 120) | -| 361 | 121 | Line too long (138 > 120) | -| 366 | 121 | Line too long (151 > 120) | -| 534 | 121 | Line too long (138 > 120) | -| 537 | 121 | Line too long (159 > 120) | -| 540 | 121 | Line too long (193 > 120) | -| 647 | 121 | Line too long (126 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\create_project\modelzoo.py:102" -``` - -#### `deeplabcut\pose_estimation_3d\camera_calibration.py` (13) - -| Line | Col | Message | -|---:|---:|---| -| 29 | 121 | Line too long (184 > 120) | -| 31 | 121 | Line too long (151 > 120) | -| 33 | 121 | Line too long (172 > 120) | -| 34 | 121 | Line too long (152 > 120) | -| 36 | 121 | Line too long (132 > 120) | -| 51 | 121 | Line too long (127 > 120) | -| 52 | 121 | Line too long (121 > 120) | -| 55 | 121 | Line too long (155 > 120) | -| 119 | 121 | Line too long (166 > 120) | -| 159 | 121 | Line too long (226 > 120) | -| 264 | 121 | Line too long (317 > 120) | -| 271 | 121 | Line too long (146 > 120) | -| 286 | 121 | Line too long (157 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_3d\camera_calibration.py:29" -``` - -#### `deeplabcut\pose_estimation_3d\plotting3D.py` (11) - -| Line | Col | Message | -|---:|---:|---| -| 84 | 121 | Line too long (159 > 120) | -| 87 | 121 | Line too long (287 > 120) | -| 93 | 121 | Line too long (159 > 120) | -| 99 | 121 | Line too long (140 > 120) | -| 103 | 121 | Line too long (141 > 120) | -| 106 | 121 | Line too long (216 > 120) | -| 109 | 121 | Line too long (216 > 120) | -| 112 | 121 | Line too long (216 > 120) | -| 115 | 121 | Line too long (219 > 120) | -| 130 | 121 | Line too long (148 > 120) | -| 155 | 121 | Line too long (243 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_3d\plotting3D.py:84" -``` - -#### `deeplabcut\utils\conversioncode.py` (11) - -| Line | Col | Message | -|---:|---:|---| -| 35 | 121 | Line too long (129 > 120) | -| 42 | 121 | Line too long (139 > 120) | -| 45 | 121 | Line too long (134 > 120) | -| 53 | 121 | Line too long (138 > 120) | -| 97 | 121 | Line too long (155 > 120) | -| 108 | 121 | Line too long (139 > 120) | -| 216 | 121 | Line too long (181 > 120) | -| 217 | 121 | Line too long (137 > 120) | -| 218 | 121 | Line too long (137 > 120) | -| 233 | 121 | Line too long (131 > 120) | -| 274 | 121 | Line too long (131 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\conversioncode.py:35" -``` - -#### `deeplabcut\utils\frameselectiontools.py` (10) - -| Line | Col | Message | -|---:|---:|---| -| 32 | 121 | Line too long (125 > 120) | -| 75 | 121 | Line too long (125 > 120) | -| 124 | 121 | Line too long (125 > 120) | -| 125 | 121 | Line too long (126 > 120) | -| 128 | 121 | Line too long (130 > 120) | -| 172 | 121 | Line too long (128 > 120) | -| 214 | 121 | Line too long (125 > 120) | -| 215 | 121 | Line too long (126 > 120) | -| 218 | 121 | Line too long (130 > 120) | -| 222 | 121 | Line too long (140 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\frameselectiontools.py:32" -``` - -#### `deeplabcut\pose_estimation_3d\triangulation.py` (9) - -| Line | Col | Message | -|---:|---:|---| -| 49 | 121 | Line too long (140 > 120) | -| 77 | 121 | Line too long (220 > 120) | -| 85 | 121 | Line too long (268 > 120) | -| 116 | 121 | Line too long (124 > 120) | -| 188 | 121 | Line too long (126 > 120) | -| 298 | 121 | Line too long (201 > 120) | -| 304 | 121 | Line too long (177 > 120) | -| 501 | 121 | Line too long (165 > 120) | -| 509 | 121 | Line too long (130 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_3d\triangulation.py:49" -``` - -#### `deeplabcut\pose_estimation_tensorflow\visualizemaps.py` (9) - -| Line | Col | Message | -|---:|---:|---| -| 36 | 121 | Line too long (149 > 120) | -| 45 | 121 | Line too long (141 > 120) | -| 49 | 121 | Line too long (137 > 120) | -| 50 | 121 | Line too long (141 > 120) | -| 51 | 121 | Line too long (147 > 120) | -| 52 | 121 | Line too long (149 > 120) | -| 183 | 121 | Line too long (121 > 120) | -| 184 | 121 | Line too long (162 > 120) | -| 286 | 121 | Line too long (141 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\visualizemaps.py:36" -``` - -#### `deeplabcut\utils\auxfun_videos.py` (9) - -| Line | Col | Message | -|---:|---:|---| -| 410 | 121 | Line too long (139 > 120) | -| 412 | 121 | Line too long (124 > 120) | -| 465 | 121 | Line too long (127 > 120) | -| 467 | 121 | Line too long (167 > 120) | -| 496 | 121 | Line too long (121 > 120) | -| 533 | 121 | Line too long (151 > 120) | -| 535 | 121 | Line too long (139 > 120) | -| 574 | 121 | Line too long (132 > 120) | -| 603 | 121 | Line too long (223 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\auxfun_videos.py:410" -``` - -#### `deeplabcut\benchmark\benchmarks.py` (8) - -| Line | Col | Message | -|---:|---:|---| -| 27 | 121 | Line too long (776 > 120) | -| 29 | 121 | Line too long (149 > 120) | -| 55 | 121 | Line too long (1440 > 120) | -| 57 | 121 | Line too long (149 > 120) | -| 106 | 121 | Line too long (964 > 120) | -| 108 | 121 | Line too long (149 > 120) | -| 137 | 121 | Line too long (981 > 120) | -| 139 | 121 | Line too long (149 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\benchmark\benchmarks.py:27" -``` - -#### `deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py` (8) - -| Line | Col | Message | -|---:|---:|---| -| 441 | 121 | Line too long (127 > 120) | -| 443 | 121 | Line too long (142 > 120) | -| 444 | 121 | Line too long (145 > 120) | -| 450 | 121 | Line too long (155 > 120) | -| 453 | 121 | Line too long (242 > 120) | -| 455 | 121 | Line too long (237 > 120) | -| 458 | 121 | Line too long (164 > 120) | -| 461 | 121 | Line too long (180 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py:441" -``` - -#### `deeplabcut\refine_training_dataset\outlier_frames.py` (7) - -| Line | Col | Message | -|---:|---:|---| -| 267 | 121 | Line too long (163 > 120) | -| 477 | 121 | Line too long (131 > 120) | -| 563 | 121 | Line too long (134 > 120) | -| 574 | 121 | Line too long (140 > 120) | -| 598 | 121 | Line too long (134 > 120) | -| 798 | 121 | Line too long (128 > 120) | -| 840 | 121 | Line too long (124 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\refine_training_dataset\outlier_frames.py:267" -``` - -#### `deeplabcut\utils\auxiliaryfunctions.py` (6) - -| Line | Col | Message | -|---:|---:|---| -| 234 | 121 | Line too long (147 > 120) | -| 405 | 121 | Line too long (135 > 120) | -| 408 | 121 | Line too long (122 > 120) | -| 684 | 121 | Line too long (161 > 120) | -| 789 | 121 | Line too long (123 > 120) | -| 790 | 121 | Line too long (149 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\auxiliaryfunctions.py:234" -``` - -#### `docs\recipes\flip_and_rotate.ipynb` (6) - -| Line | Col | Message | -|---:|---:|---| -| 10 | 121 | Line too long (155 > 120) | -| 19 | 121 | Line too long (155 > 120) | -| 19 | 121 | Line too long (155 > 120) | -| 20 | 121 | Line too long (155 > 120) | -| 20 | 121 | Line too long (155 > 120) | -| 22 | 121 | Line too long (155 > 120) | - -Quick open commands: - -```powershell -code -g "docs\recipes\flip_and_rotate.ipynb:10" -``` - -#### `deeplabcut\utils\auxfun_multianimal.py` (5) - -| Line | Col | Message | -|---:|---:|---| -| 94 | 121 | Line too long (148 > 120) | -| 122 | 121 | Line too long (136 > 120) | -| 242 | 121 | Line too long (161 > 120) | -| 243 | 121 | Line too long (157 > 120) | -| 358 | 121 | Line too long (136 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\auxfun_multianimal.py:94" -``` - -#### `deeplabcut\create_project\add.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 39 | 121 | Line too long (122 > 120) | -| 42 | 121 | Line too long (163 > 120) | -| 45 | 121 | Line too long (203 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\create_project\add.py:39" -``` - -#### `deeplabcut\create_project\new.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 149 | 121 | Line too long (149 > 120) | -| 217 | 121 | Line too long (141 > 120) | -| 306 | 121 | Line too long (390 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\create_project\new.py:149" -``` - -#### `deeplabcut\create_project\new_3d.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 35 | 121 | Line too long (140 > 120) | -| 91 | 121 | Line too long (123 > 120) | -| 126 | 121 | Line too long (295 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\create_project\new_3d.py:35" -``` - -#### `deeplabcut\generate_training_dataset\frame_extraction.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 425 | 121 | Line too long (171 > 120) | -| 451 | 121 | Line too long (142 > 120) | -| 544 | 121 | Line too long (163 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\generate_training_dataset\frame_extraction.py:425" -``` - -#### `deeplabcut\generate_training_dataset\multiple_individuals_trainingsetmanipulation.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 261 | 121 | Line too long (128 > 120) | -| 262 | 121 | Line too long (213 > 120) | -| 268 | 121 | Line too long (210 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\generate_training_dataset\multiple_individuals_trainingsetmanipulation.py:261" -``` - -#### `deeplabcut\gui\tracklet_toolbox.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 85 | 121 | Line too long (180 > 120) | -| 909 | 121 | Line too long (126 > 120) | -| 913 | 121 | Line too long (127 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\gui\tracklet_toolbox.py:85" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc_dataframe.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 34 | 121 | Line too long (127 > 120) | -| 47 | 121 | Line too long (311 > 120) | -| 56 | 121 | Line too long (130 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc_dataframe.py:34" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 181 | 121 | Line too long (201 > 120) | -| 320 | 121 | Line too long (167 > 120) | -| 532 | 121 | Line too long (167 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py:181" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\datasets\single_dlc_dataframe.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 34 | 121 | Line too long (127 > 120) | -| 47 | 121 | Line too long (311 > 120) | -| 56 | 121 | Line too long (130 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\single_dlc_dataframe.py:34" -``` - -#### `deeplabcut\pose_estimation_tensorflow\modelzoo\api\spatiotemporal_adapt.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 53 | 121 | Line too long (167 > 120) | -| 55 | 121 | Line too long (182 > 120) | -| 57 | 121 | Line too long (169 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\modelzoo\api\spatiotemporal_adapt.py:53" -``` - -#### `deeplabcut\refine_training_dataset\stitch.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 1002 | 121 | Line too long (155 > 120) | -| 1005 | 121 | Line too long (140 > 120) | -| 1012 | 121 | Line too long (136 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\refine_training_dataset\stitch.py:1002" -``` - -#### `deeplabcut\utils\make_labeled_video.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 1168 | 121 | Line too long (140 > 120) | -| 1175 | 121 | Line too long (136 > 120) | -| 1180 | 121 | Line too long (124 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\make_labeled_video.py:1168" -``` - -#### `deeplabcut\gui\window.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 372 | 121 | Line too long (312 > 120) | -| 554 | 121 | Line too long (141 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\gui\window.py:372" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 85 | 121 | Line too long (125 > 120) | -| 130 | 121 | Line too long (191 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py:85" -``` - -#### `deeplabcut\modelzoo\utils.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 201 | 121 | Line too long (134 > 120) | -| 208 | 121 | Line too long (134 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\utils.py:201" -``` - -#### `deeplabcut\modelzoo\video_inference.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 480 | 121 | Line too long (122 > 120) | -| 549 | 121 | Line too long (134 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\video_inference.py:480" -``` - -#### `deeplabcut\pose_estimation_pytorch\config\make_pose_config.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 78 | 121 | Line too long (132 > 120) | -| 79 | 121 | Line too long (217 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\config\make_pose_config.py:78" -``` - -#### `deeplabcut\pose_estimation_tensorflow\core\train_multianimal.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 92 | 121 | Line too long (122 > 120) | -| 208 | 121 | Line too long (123 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\core\train_multianimal.py:92" -``` - -#### `deeplabcut\pose_estimation_tensorflow\training.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 28 | 121 | Line too long (136 > 120) | -| 176 | 121 | Line too long (161 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\training.py:28" -``` - -#### `deeplabcut\pose_tracking_pytorch\model\backbones\vit_pytorch.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 332 | 121 | Line too long (121 > 120) | -| 346 | 121 | Line too long (126 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_tracking_pytorch\model\backbones\vit_pytorch.py:332" -``` - -#### `deeplabcut\pose_tracking_pytorch\processor\processor.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 131 | 121 | Line too long (154 > 120) | -| 143 | 121 | Line too long (143 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_tracking_pytorch\processor\processor.py:131" -``` - -#### `deeplabcut\utils\auxfun_models.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 50 | 121 | Line too long (167 > 120) | -| 157 | 121 | Line too long (126 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\auxfun_models.py:50" -``` - -#### `deeplabcut\utils\auxiliaryfunctions_3d.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 206 | 121 | Line too long (121 > 120) | -| 228 | 121 | Line too long (139 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\auxiliaryfunctions_3d.py:206" -``` - -#### `deeplabcut\utils\pseudo_label.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 397 | 121 | Line too long (145 > 120) | -| 399 | 121 | Line too long (133 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\pseudo_label.py:397" -``` - -#### `tests\pose_estimation_pytorch\other\test_match_predictions_to_gt.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 15 | 121 | Line too long (122 > 120) | -| 78 | 121 | Line too long (125 > 120) | - -Quick open commands: - -```powershell -code -g "tests\pose_estimation_pytorch\other\test_match_predictions_to_gt.py:15" -``` - -#### `testscript_cli.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 149 | 121 | Line too long (221 > 120) | -| 173 | 121 | Line too long (133 > 120) | - -Quick open commands: - -```powershell -code -g "testscript_cli.py:149" -``` - -#### `deeplabcut\__main__.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 29 | 121 | Line too long (127 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\__main__.py:29" -``` - -#### `deeplabcut\gui\tabs\extract_outlier_frames.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 162 | 121 | Line too long (223 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\gui\tabs\extract_outlier_frames.py:162" -``` - -#### `deeplabcut\gui\tabs\modelzoo.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 103 | 121 | Line too long (130 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\gui\tabs\modelzoo.py:103" -``` - -#### `deeplabcut\gui\tabs\refine_tracklets.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 231 | 121 | Line too long (223 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\gui\tabs\refine_tracklets.py:231" -``` - -#### `deeplabcut\gui\tabs\train_network.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 97 | 121 | Line too long (121 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\gui\tabs\train_network.py:97" -``` - -#### `deeplabcut\gui\widgets.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 529 | 121 | Line too long (223 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\gui\widgets.py:529" -``` - -#### `deeplabcut\modelzoo\weight_initialization.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 77 | 121 | Line too long (122 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\weight_initialization.py:77" -``` - -#### `deeplabcut\pose_estimation_pytorch\models\modules\conv_block.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 29 | 121 | Line too long (123 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\models\modules\conv_block.py:29" -``` - -#### `deeplabcut\pose_estimation_pytorch\models\necks\transformer.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 30 | 121 | Line too long (122 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\models\necks\transformer.py:30" -``` - -#### `deeplabcut\pose_estimation_pytorch\models\predictors\paf_predictor.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 36 | 121 | Line too long (134 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\models\predictors\paf_predictor.py:36" -``` - -#### `deeplabcut\pose_estimation_pytorch\post_processing\match_predictions_to_gt.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 108 | 121 | Line too long (134 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\post_processing\match_predictions_to_gt.py:108" -``` - -#### `deeplabcut\pose_estimation_tensorflow\core\train.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 151 | 121 | Line too long (139 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\core\train.py:151" -``` - -#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 413 | 121 | Line too long (124 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py:413" -``` - -#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_tensorpack.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 227 | 121 | Line too long (125 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_tensorpack.py:227" -``` - -#### `deeplabcut\post_processing\analyze_skeleton.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 101 | 121 | Line too long (133 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\post_processing\analyze_skeleton.py:101" -``` - -#### `deeplabcut\utils\visualization.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 144 | 121 | Line too long (131 > 120) | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\visualization.py:144" -``` - -#### `examples\JUPYTER\Demo_yourowndata.ipynb` (1) - -| Line | Col | Message | -|---:|---:|---| -| 16 | 121 | Line too long (123 > 120) | - -Quick open commands: - -```powershell -code -g "examples\JUPYTER\Demo_yourowndata.ipynb:16" -``` - -#### `examples\testscript_3d.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 149 | 121 | Line too long (126 > 120) | - -Quick open commands: - -```powershell -code -g "examples\testscript_3d.py:149" -``` - -#### `examples\testscript_deterministicwithResNet152.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 118 | 121 | Line too long (223 > 120) | - -Quick open commands: - -```powershell -code -g "examples\testscript_deterministicwithResNet152.py:118" -``` - -#### `examples\testscript_mobilenets.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 17 | 121 | Line too long (137 > 120) | - -Quick open commands: - -```powershell -code -g "examples\testscript_mobilenets.py:17" -``` - -#### `examples\testscript_openfielddata.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 29 | 121 | Line too long (142 > 120) | - -Quick open commands: - -```powershell -code -g "examples\testscript_openfielddata.py:29" -``` - -#### `examples\testscript_pretrained_models.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 32 | 121 | Line too long (144 > 120) | - -Quick open commands: - -```powershell -code -g "examples\testscript_pretrained_models.py:32" -``` - -## F401 - -Count: **331** -Hint: Unused import. Usually safe to delete; verify imports with side effects. - -### Files affected - -| File | Count | -|---|---:| -| `deeplabcut\__init__.py` | 66 | -| `deeplabcut\pose_estimation_pytorch\__init__.py` | 51 | -| `deeplabcut\pose_estimation_pytorch\apis\__init__.py` | 22 | -| `deeplabcut\pose_estimation_pytorch\data\__init__.py` | 19 | -| `deeplabcut\pose_estimation_pytorch\runners\__init__.py` | 18 | -| `deeplabcut\gui\tabs\__init__.py` | 15 | -| `deeplabcut\pose_estimation_pytorch\config\__init__.py` | 12 | -| `deeplabcut\pose_estimation_pytorch\models\modules\__init__.py` | 12 | -| `deeplabcut\pose_estimation_pytorch\models\criterions\__init__.py` | 11 | -| `deeplabcut\pose_estimation_pytorch\models\__init__.py` | 9 | -| `deeplabcut\pose_estimation_pytorch\models\backbones\__init__.py` | 8 | -| `deeplabcut\pose_estimation_pytorch\models\target_generators\__init__.py` | 8 | -| `deeplabcut\modelzoo\generalized_data_converter\datasets\__init__.py` | 7 | -| `deeplabcut\pose_estimation_pytorch\models\heads\__init__.py` | 7 | -| `deeplabcut\pose_estimation_pytorch\models\predictors\__init__.py` | 7 | -| `deeplabcut\create_project\__init__.py` | 6 | -| `deeplabcut\pose_estimation_pytorch\modelzoo\__init__.py` | 6 | -| `deeplabcut\core\metrics\__init__.py` | 4 | -| `deeplabcut\pose_estimation_pytorch\models\detectors\__init__.py` | 4 | -| `deeplabcut\pose_tracking_pytorch\processor\__init__.py` | 4 | -| `deeplabcut\generate_training_dataset\__init__.py` | 3 | -| `deeplabcut\modelzoo\generalized_data_converter\__init__.py` | 3 | -| `deeplabcut\pose_estimation_pytorch\models\necks\__init__.py` | 3 | -| `deeplabcut\pose_tracking_pytorch\tracking_utils\__init__.py` | 3 | -| `deeplabcut\gui\window.py` | 2 | -| `deeplabcut\pose_estimation_pytorch\post_processing\__init__.py` | 2 | -| `deeplabcut\pose_estimation_tensorflow\__init__.py` | 2 | -| `deeplabcut\pose_tracking_pytorch\__init__.py` | 2 | -| `deeplabcut\pose_tracking_pytorch\model\__init__.py` | 2 | -| `deeplabcut\__main__.py` | 1 | -| `deeplabcut\gui\__init__.py` | 1 | -| `deeplabcut\gui\tabs\create_training_dataset.py` | 1 | -| `deeplabcut\modelzoo\__init__.py` | 1 | -| `deeplabcut\modelzoo\generalized_data_converter\conversion_table\__init__.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\lib\__init__.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\modelzoo\__init__.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\modelzoo\api\__init__.py` | 1 | -| `deeplabcut\pose_tracking_pytorch\datasets\__init__.py` | 1 | -| `deeplabcut\pose_tracking_pytorch\loss\__init__.py` | 1 | -| `deeplabcut\pose_tracking_pytorch\model\backbones\__init__.py` | 1 | -| `deeplabcut\pose_tracking_pytorch\solver\__init__.py` | 1 | -| `deeplabcut\post_processing\__init__.py` | 1 | - -### Details - -#### `deeplabcut\__init__.py` (66) - -| Line | Col | Message | -|---:|---:|---| -| 16 | 41 | `deeplabcut.version.__version__` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 21 | 46 | `deeplabcut.gui.launch_script.launch_dlc` imported but unused; consider using `importlib.util.find_spec` to test for availability | -| 23 | 9 | `deeplabcut.gui.tabs.label_frames.label_frames` imported but unused; consider using `importlib.util.find_spec` to test for availability | -| 24 | 9 | `deeplabcut.gui.tabs.label_frames.refine_labels` imported but unused; consider using `importlib.util.find_spec` to test for availability | -| 26 | 49 | `deeplabcut.gui.tracklet_toolbox.refine_tracklets` imported but unused; consider using `importlib.util.find_spec` to test for availability | -| 27 | 40 | `deeplabcut.gui.widgets.SkeletonBuilder` imported but unused; consider using `importlib.util.find_spec` to test for availability | -| 31 | 36 | `deeplabcut.core.engine.Engine` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 33 | 5 | `deeplabcut.create_project.add_new_videos` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 34 | 5 | `deeplabcut.create_project.create_new_project` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 35 | 5 | `deeplabcut.create_project.create_new_project_3d` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 36 | 5 | `deeplabcut.create_project.create_pretrained_human_project` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 37 | 5 | `deeplabcut.create_project.create_pretrained_project` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 38 | 5 | `deeplabcut.create_project.load_demo_data` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 41 | 5 | `deeplabcut.generate_training_dataset.adddatasetstovideolistandviceversa` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 42 | 5 | `deeplabcut.generate_training_dataset.check_labels` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 43 | 5 | `deeplabcut.generate_training_dataset.comparevideolistsanddatafolders` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 44 | 5 | `deeplabcut.generate_training_dataset.create_multianimaltraining_dataset` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 45 | 5 | `deeplabcut.generate_training_dataset.create_training_dataset` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 46 | 5 | `deeplabcut.generate_training_dataset.create_training_dataset_from_existing_split` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 47 | 5 | `deeplabcut.generate_training_dataset.create_training_model_comparison` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 48 | 5 | `deeplabcut.generate_training_dataset.dropannotationfileentriesduetodeletedimages` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 49 | 5 | `deeplabcut.generate_training_dataset.dropduplicatesinannotatinfiles` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 50 | 5 | `deeplabcut.generate_training_dataset.dropimagesduetolackofannotation` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 51 | 5 | `deeplabcut.generate_training_dataset.dropunlabeledframes` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 52 | 5 | `deeplabcut.generate_training_dataset.extract_frames` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 53 | 5 | `deeplabcut.generate_training_dataset.mergeandsplit` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 55 | 49 | `deeplabcut.modelzoo.video_inference.video_inference_superanimal` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 57 | 5 | `deeplabcut.utils.analyze_videos_converth5_to_csv` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 58 | 5 | `deeplabcut.utils.analyze_videos_converth5_to_nwb` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 59 | 5 | `deeplabcut.utils.auxfun_videos` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 60 | 5 | `deeplabcut.utils.auxiliaryfunctions` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 61 | 5 | `deeplabcut.utils.convert2_maDLC` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 62 | 5 | `deeplabcut.utils.convertcsv2h5` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 63 | 5 | `deeplabcut.utils.create_labeled_video` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 64 | 5 | `deeplabcut.utils.create_video_with_all_detections` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 65 | 5 | `deeplabcut.utils.plot_trajectories` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 69 | 50 | `deeplabcut.pose_tracking_pytorch.transformer_reID` imported but unused; consider using `importlib.util.find_spec` to test for availability | -| 82 | 5 | `deeplabcut.compat.analyze_images` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 83 | 5 | `deeplabcut.compat.analyze_time_lapse_frames` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 84 | 5 | `deeplabcut.compat.analyze_videos` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 85 | 5 | `deeplabcut.compat.convert_detections2tracklets` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 86 | 5 | `deeplabcut.compat.create_tracking_dataset` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 87 | 5 | `deeplabcut.compat.evaluate_network` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 88 | 5 | `deeplabcut.compat.export_model` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 89 | 5 | `deeplabcut.compat.extract_maps` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 90 | 5 | `deeplabcut.compat.extract_save_all_maps` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 91 | 5 | `deeplabcut.compat.return_evaluate_network_data` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 92 | 5 | `deeplabcut.compat.return_train_network_path` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 93 | 5 | `deeplabcut.compat.train_network` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 94 | 5 | `deeplabcut.compat.visualize_locrefs` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 95 | 5 | `deeplabcut.compat.visualize_paf` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 96 | 5 | `deeplabcut.compat.visualize_scoremaps` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 99 | 5 | `deeplabcut.pose_estimation_3d.calibrate_cameras` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 100 | 5 | `deeplabcut.pose_estimation_3d.check_undistortion` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 101 | 5 | `deeplabcut.pose_estimation_3d.create_labeled_video_3d` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 102 | 5 | `deeplabcut.pose_estimation_3d.triangulate` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 104 | 40 | `deeplabcut.post_processing.analyzeskeleton` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 104 | 57 | `deeplabcut.post_processing.filterpredictions` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 106 | 5 | `deeplabcut.refine_training_dataset.extract_outlier_frames` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 107 | 5 | `deeplabcut.refine_training_dataset.find_outliers_in_raw_data` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 108 | 5 | `deeplabcut.refine_training_dataset.merge_datasets` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 110 | 55 | `deeplabcut.refine_training_dataset.stitch.stitch_tracklets` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 112 | 5 | `deeplabcut.utils.auxfun_videos.CropVideo` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 113 | 5 | `deeplabcut.utils.auxfun_videos.DownSampleVideo` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 114 | 5 | `deeplabcut.utils.auxfun_videos.ShortenVideo` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 115 | 5 | `deeplabcut.utils.auxfun_videos.check_video_integrity` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\__init__.py:16" -``` - -#### `deeplabcut\pose_estimation_pytorch\__init__.py` (51) - -| Line | Col | Message | -|---:|---:|---| -| 11 | 53 | `deeplabcut.pose_estimation_pytorch.config` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 13 | 5 | `deeplabcut.pose_estimation_pytorch.apis.VideoIterator` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 14 | 5 | `deeplabcut.pose_estimation_pytorch.apis.analyze_image_folder` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 15 | 5 | `deeplabcut.pose_estimation_pytorch.apis.analyze_images` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 16 | 5 | `deeplabcut.pose_estimation_pytorch.apis.analyze_videos` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 17 | 5 | `deeplabcut.pose_estimation_pytorch.apis.build_predictions_dataframe` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 18 | 5 | `deeplabcut.pose_estimation_pytorch.apis.convert_detections2tracklets` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 19 | 5 | `deeplabcut.pose_estimation_pytorch.apis.create_labeled_images` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 20 | 5 | `deeplabcut.pose_estimation_pytorch.apis.create_tracking_dataset` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 21 | 5 | `deeplabcut.pose_estimation_pytorch.apis.evaluate` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 22 | 5 | `deeplabcut.pose_estimation_pytorch.apis.evaluate_network` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 23 | 5 | `deeplabcut.pose_estimation_pytorch.apis.extract_maps` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 24 | 5 | `deeplabcut.pose_estimation_pytorch.apis.extract_save_all_maps` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 25 | 5 | `deeplabcut.pose_estimation_pytorch.apis.get_detector_inference_runner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 26 | 5 | `deeplabcut.pose_estimation_pytorch.apis.get_pose_inference_runner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 27 | 5 | `deeplabcut.pose_estimation_pytorch.apis.predict` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 28 | 5 | `deeplabcut.pose_estimation_pytorch.apis.superanimal_analyze_images` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 29 | 5 | `deeplabcut.pose_estimation_pytorch.apis.train` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 30 | 5 | `deeplabcut.pose_estimation_pytorch.apis.train_network` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 31 | 5 | `deeplabcut.pose_estimation_pytorch.apis.video_inference` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 32 | 5 | `deeplabcut.pose_estimation_pytorch.apis.visualize_predictions` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 35 | 5 | `deeplabcut.pose_estimation_pytorch.config.available_detectors` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 36 | 5 | `deeplabcut.pose_estimation_pytorch.config.available_models` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 37 | 5 | `deeplabcut.pose_estimation_pytorch.config.is_model_cond_top_down` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 38 | 5 | `deeplabcut.pose_estimation_pytorch.config.is_model_top_down` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 41 | 5 | `deeplabcut.pose_estimation_pytorch.data.COLLATE_FUNCTIONS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 42 | 5 | `deeplabcut.pose_estimation_pytorch.data.COCOLoader` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 43 | 5 | `deeplabcut.pose_estimation_pytorch.data.DLCLoader` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 44 | 5 | `deeplabcut.pose_estimation_pytorch.data.GenerativeSampler` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 45 | 5 | `deeplabcut.pose_estimation_pytorch.data.GenSamplingConfig` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 46 | 5 | `deeplabcut.pose_estimation_pytorch.data.Loader` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 47 | 5 | `deeplabcut.pose_estimation_pytorch.data.PoseDataset` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 48 | 5 | `deeplabcut.pose_estimation_pytorch.data.PoseDatasetParameters` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 49 | 5 | `deeplabcut.pose_estimation_pytorch.data.Snapshot` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 50 | 5 | `deeplabcut.pose_estimation_pytorch.data.build_transforms` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 51 | 5 | `deeplabcut.pose_estimation_pytorch.data.list_snapshots` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 54 | 5 | `deeplabcut.pose_estimation_pytorch.runners.DetectorInferenceRunner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 55 | 5 | `deeplabcut.pose_estimation_pytorch.runners.DetectorTrainingRunner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 56 | 5 | `deeplabcut.pose_estimation_pytorch.runners.DynamicCropper` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 57 | 5 | `deeplabcut.pose_estimation_pytorch.runners.InferenceRunner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 58 | 5 | `deeplabcut.pose_estimation_pytorch.runners.PoseInferenceRunner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 59 | 5 | `deeplabcut.pose_estimation_pytorch.runners.PoseTrainingRunner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 60 | 5 | `deeplabcut.pose_estimation_pytorch.runners.TopDownDynamicCropper` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 61 | 5 | `deeplabcut.pose_estimation_pytorch.runners.TorchSnapshotManager` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 62 | 5 | `deeplabcut.pose_estimation_pytorch.runners.TrainingRunner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 63 | 5 | `deeplabcut.pose_estimation_pytorch.runners.build_inference_runner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 64 | 5 | `deeplabcut.pose_estimation_pytorch.runners.build_training_runner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 65 | 5 | `deeplabcut.pose_estimation_pytorch.runners.get_load_weights_only` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 66 | 5 | `deeplabcut.pose_estimation_pytorch.runners.set_load_weights_only` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 68 | 53 | `deeplabcut.pose_estimation_pytorch.task.Task` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 69 | 54 | `deeplabcut.pose_estimation_pytorch.utils.fix_seeds` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\__init__.py:11" -``` - -#### `deeplabcut\pose_estimation_pytorch\apis\__init__.py` (22) - -| Line | Col | Message | -|---:|---:|---| -| 13 | 5 | `deeplabcut.pose_estimation_pytorch.apis.analyze_images.analyze_image_folder` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 14 | 5 | `deeplabcut.pose_estimation_pytorch.apis.analyze_images.analyze_images` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 15 | 5 | `deeplabcut.pose_estimation_pytorch.apis.analyze_images.superanimal_analyze_images` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 18 | 5 | `deeplabcut.pose_estimation_pytorch.apis.evaluation.evaluate` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 19 | 5 | `deeplabcut.pose_estimation_pytorch.apis.evaluation.evaluate_network` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 20 | 5 | `deeplabcut.pose_estimation_pytorch.apis.evaluation.predict` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 21 | 5 | `deeplabcut.pose_estimation_pytorch.apis.evaluation.visualize_predictions` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 23 | 60 | `deeplabcut.pose_estimation_pytorch.apis.export.export_model` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 25 | 5 | `deeplabcut.pose_estimation_pytorch.apis.tracking_dataset.create_tracking_dataset` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 28 | 5 | `deeplabcut.pose_estimation_pytorch.apis.tracklets.convert_detections2tracklets` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 31 | 5 | `deeplabcut.pose_estimation_pytorch.apis.training.train` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 32 | 5 | `deeplabcut.pose_estimation_pytorch.apis.training.train_network` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 35 | 5 | `deeplabcut.pose_estimation_pytorch.apis.utils.build_predictions_dataframe` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 36 | 5 | `deeplabcut.pose_estimation_pytorch.apis.utils.get_detector_inference_runner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 37 | 5 | `deeplabcut.pose_estimation_pytorch.apis.utils.get_inference_runners` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 38 | 5 | `deeplabcut.pose_estimation_pytorch.apis.utils.get_pose_inference_runner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 41 | 5 | `deeplabcut.pose_estimation_pytorch.apis.videos.VideoIterator` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 42 | 5 | `deeplabcut.pose_estimation_pytorch.apis.videos.analyze_videos` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 43 | 5 | `deeplabcut.pose_estimation_pytorch.apis.videos.video_inference` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 46 | 5 | `deeplabcut.pose_estimation_pytorch.apis.visualization.create_labeled_images` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 47 | 5 | `deeplabcut.pose_estimation_pytorch.apis.visualization.extract_maps` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 48 | 5 | `deeplabcut.pose_estimation_pytorch.apis.visualization.extract_save_all_maps` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\apis\__init__.py:13" -``` - -#### `deeplabcut\pose_estimation_pytorch\data\__init__.py` (19) - -| Line | Col | Message | -|---:|---:|---| -| 11 | 58 | `deeplabcut.pose_estimation_pytorch.data.base.Loader` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 12 | 64 | `deeplabcut.pose_estimation_pytorch.data.cocoloader.COCOLoader` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 13 | 61 | `deeplabcut.pose_estimation_pytorch.data.collate.COLLATE_FUNCTIONS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 15 | 5 | `deeplabcut.pose_estimation_pytorch.data.dataset.PoseDataset` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 16 | 5 | `deeplabcut.pose_estimation_pytorch.data.dataset.PoseDatasetParameters` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 18 | 63 | `deeplabcut.pose_estimation_pytorch.data.dlcloader.DLCLoader` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 20 | 5 | `deeplabcut.pose_estimation_pytorch.data.generative_sampling.GenerativeSampler` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 21 | 5 | `deeplabcut.pose_estimation_pytorch.data.generative_sampling.GenSamplingConfig` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 23 | 59 | `deeplabcut.pose_estimation_pytorch.data.image.top_down_crop` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 25 | 5 | `deeplabcut.pose_estimation_pytorch.data.postprocessor.Postprocessor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 26 | 5 | `deeplabcut.pose_estimation_pytorch.data.postprocessor.build_bottom_up_postprocessor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 27 | 5 | `deeplabcut.pose_estimation_pytorch.data.postprocessor.build_detector_postprocessor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 28 | 5 | `deeplabcut.pose_estimation_pytorch.data.postprocessor.build_top_down_postprocessor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 31 | 5 | `deeplabcut.pose_estimation_pytorch.data.preprocessor.Preprocessor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 32 | 5 | `deeplabcut.pose_estimation_pytorch.data.preprocessor.build_bottom_up_preprocessor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 33 | 5 | `deeplabcut.pose_estimation_pytorch.data.preprocessor.build_top_down_preprocessor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 35 | 63 | `deeplabcut.pose_estimation_pytorch.data.snapshots.Snapshot` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 35 | 73 | `deeplabcut.pose_estimation_pytorch.data.snapshots.list_snapshots` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 36 | 64 | `deeplabcut.pose_estimation_pytorch.data.transforms.build_transforms` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\data\__init__.py:11" -``` - -#### `deeplabcut\pose_estimation_pytorch\runners\__init__.py` (18) - -| Line | Col | Message | -|---:|---:|---| -| 13 | 5 | `deeplabcut.pose_estimation_pytorch.runners.base.Runner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 14 | 5 | `deeplabcut.pose_estimation_pytorch.runners.base.attempt_snapshot_load` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 15 | 5 | `deeplabcut.pose_estimation_pytorch.runners.base.fix_snapshot_metadata` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 16 | 5 | `deeplabcut.pose_estimation_pytorch.runners.base.get_load_weights_only` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 17 | 5 | `deeplabcut.pose_estimation_pytorch.runners.base.set_load_weights_only` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 19 | 60 | `deeplabcut.pose_estimation_pytorch.runners.ctd.CTDTrackingConfig` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 21 | 5 | `deeplabcut.pose_estimation_pytorch.runners.dynamic_cropping.DynamicCropper` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 22 | 5 | `deeplabcut.pose_estimation_pytorch.runners.dynamic_cropping.TopDownDynamicCropper` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 25 | 5 | `deeplabcut.pose_estimation_pytorch.runners.inference.DetectorInferenceRunner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 26 | 5 | `deeplabcut.pose_estimation_pytorch.runners.inference.InferenceRunner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 27 | 5 | `deeplabcut.pose_estimation_pytorch.runners.inference.PoseInferenceRunner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 28 | 5 | `deeplabcut.pose_estimation_pytorch.runners.inference.build_inference_runner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 30 | 63 | `deeplabcut.pose_estimation_pytorch.runners.logger.LOGGER` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 31 | 66 | `deeplabcut.pose_estimation_pytorch.runners.snapshots.TorchSnapshotManager` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 33 | 5 | `deeplabcut.pose_estimation_pytorch.runners.train.DetectorTrainingRunner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 34 | 5 | `deeplabcut.pose_estimation_pytorch.runners.train.PoseTrainingRunner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 35 | 5 | `deeplabcut.pose_estimation_pytorch.runners.train.TrainingRunner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 36 | 5 | `deeplabcut.pose_estimation_pytorch.runners.train.build_training_runner` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\runners\__init__.py:13" -``` - -#### `deeplabcut\gui\tabs\__init__.py` (15) - -| Line | Col | Message | -|---:|---:|---| -| 11 | 48 | `deeplabcut.gui.tabs.analyze_videos.AnalyzeVideos` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 12 | 48 | `deeplabcut.gui.tabs.create_project.ProjectCreator` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 13 | 57 | `deeplabcut.gui.tabs.create_training_dataset.CreateTrainingDataset` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 14 | 47 | `deeplabcut.gui.tabs.create_videos.CreateVideos` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 15 | 50 | `deeplabcut.gui.tabs.evaluate_network.EvaluateNetwork` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 16 | 48 | `deeplabcut.gui.tabs.extract_frames.ExtractFrames` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 17 | 56 | `deeplabcut.gui.tabs.extract_outlier_frames.ExtractOutlierFrames` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 18 | 46 | `deeplabcut.gui.tabs.label_frames.LabelFrames` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 19 | 48 | `deeplabcut.gui.tabs.manage_project.ManageProject` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 20 | 42 | `deeplabcut.gui.tabs.modelzoo.ModelZoo` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 21 | 46 | `deeplabcut.gui.tabs.open_project.OpenProject` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 22 | 50 | `deeplabcut.gui.tabs.refine_tracklets.RefineTracklets` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 23 | 47 | `deeplabcut.gui.tabs.train_network.TrainNetwork` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 24 | 58 | `deeplabcut.gui.tabs.unsupervised_id_tracking.UnsupervizedIdTracking` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 25 | 46 | `deeplabcut.gui.tabs.video_editor.VideoEditor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\gui\tabs\__init__.py:11" -``` - -#### `deeplabcut\pose_estimation_pytorch\config\__init__.py` (12) - -| Line | Col | Message | -|---:|---:|---| -| 13 | 5 | `deeplabcut.core.config.pretty_print` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 14 | 5 | `deeplabcut.core.config.read_config_as_dict` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 15 | 5 | `deeplabcut.core.config.write_config` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 18 | 5 | `deeplabcut.pose_estimation_pytorch.config.make_pose_config.make_basic_project_config` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 19 | 5 | `deeplabcut.pose_estimation_pytorch.config.make_pose_config.make_pytorch_pose_config` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 20 | 5 | `deeplabcut.pose_estimation_pytorch.config.make_pose_config.make_pytorch_test_config` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 23 | 5 | `deeplabcut.pose_estimation_pytorch.config.utils.available_detectors` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 24 | 5 | `deeplabcut.pose_estimation_pytorch.config.utils.available_models` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 25 | 5 | `deeplabcut.pose_estimation_pytorch.config.utils.is_model_cond_top_down` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 26 | 5 | `deeplabcut.pose_estimation_pytorch.config.utils.is_model_top_down` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 27 | 5 | `deeplabcut.pose_estimation_pytorch.config.utils.update_config` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 28 | 5 | `deeplabcut.pose_estimation_pytorch.config.utils.update_config_by_dotpath` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\config\__init__.py:13" -``` - -#### `deeplabcut\pose_estimation_pytorch\models\modules\__init__.py` (12) - -| Line | Col | Message | -|---:|---:|---| -| 11 | 75 | `deeplabcut.pose_estimation_pytorch.models.modules.coam_module.CoAMBlock` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 11 | 86 | `deeplabcut.pose_estimation_pytorch.models.modules.coam_module.SelfAttentionModule_CoAM` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 13 | 5 | `deeplabcut.pose_estimation_pytorch.models.modules.conv_block.AdaptBlock` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 14 | 5 | `deeplabcut.pose_estimation_pytorch.models.modules.conv_block.BasicBlock` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 15 | 5 | `deeplabcut.pose_estimation_pytorch.models.modules.conv_block.Bottleneck` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 18 | 5 | `deeplabcut.pose_estimation_pytorch.models.modules.conv_module.HighResolutionModule` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 21 | 5 | `deeplabcut.pose_estimation_pytorch.models.modules.gated_attention_unit.GatedAttentionUnit` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 24 | 5 | `deeplabcut.pose_estimation_pytorch.models.modules.kpt_encoders.KEYPOINT_ENCODERS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 25 | 5 | `deeplabcut.pose_estimation_pytorch.models.modules.kpt_encoders.BaseKeypointEncoder` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 26 | 5 | `deeplabcut.pose_estimation_pytorch.models.modules.kpt_encoders.ColoredKeypointEncoder` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 27 | 5 | `deeplabcut.pose_estimation_pytorch.models.modules.kpt_encoders.StackedKeypointEncoder` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 30 | 5 | `deeplabcut.pose_estimation_pytorch.models.modules.norm.ScaleNorm` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\models\modules\__init__.py:11" -``` - -#### `deeplabcut\pose_estimation_pytorch\models\criterions\__init__.py` (11) - -| Line | Col | Message | -|---:|---:|---| -| 12 | 5 | `deeplabcut.pose_estimation_pytorch.models.criterions.aggregators.WeightedLossAggregator` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 15 | 5 | `deeplabcut.pose_estimation_pytorch.models.criterions.base.CRITERIONS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 16 | 5 | `deeplabcut.pose_estimation_pytorch.models.criterions.base.LOSS_AGGREGATORS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 17 | 5 | `deeplabcut.pose_estimation_pytorch.models.criterions.base.BaseCriterion` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 18 | 5 | `deeplabcut.pose_estimation_pytorch.models.criterions.base.BaseLossAggregator` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 21 | 5 | `deeplabcut.pose_estimation_pytorch.models.criterions.dekr.DEKRHeatmapLoss` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 22 | 5 | `deeplabcut.pose_estimation_pytorch.models.criterions.dekr.DEKROffsetLoss` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 25 | 5 | `deeplabcut.pose_estimation_pytorch.models.criterions.kl_discrete.KLDiscreteLoss` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 28 | 5 | `deeplabcut.pose_estimation_pytorch.models.criterions.weighted.WeightedBCECriterion` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 29 | 5 | `deeplabcut.pose_estimation_pytorch.models.criterions.weighted.WeightedHuberCriterion` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 30 | 5 | `deeplabcut.pose_estimation_pytorch.models.criterions.weighted.WeightedMSECriterion` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\models\criterions\__init__.py:12" -``` - -#### `deeplabcut\pose_estimation_pytorch\models\__init__.py` (9) - -| Line | Col | Message | -|---:|---:|---| -| 11 | 70 | `deeplabcut.pose_estimation_pytorch.models.backbones.base.BACKBONES` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 13 | 5 | `deeplabcut.pose_estimation_pytorch.models.criterions.CRITERIONS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 14 | 5 | `deeplabcut.pose_estimation_pytorch.models.criterions.LOSS_AGGREGATORS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 16 | 65 | `deeplabcut.pose_estimation_pytorch.models.detectors.DETECTORS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 17 | 66 | `deeplabcut.pose_estimation_pytorch.models.heads.base.HEADS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 18 | 61 | `deeplabcut.pose_estimation_pytorch.models.model.PoseModel` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 19 | 66 | `deeplabcut.pose_estimation_pytorch.models.necks.base.NECKS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 20 | 66 | `deeplabcut.pose_estimation_pytorch.models.predictors.PREDICTORS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 22 | 5 | `deeplabcut.pose_estimation_pytorch.models.target_generators.TARGET_GENERATORS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\models\__init__.py:11" -``` - -#### `deeplabcut\pose_estimation_pytorch\models\backbones\__init__.py` (8) - -| Line | Col | Message | -|---:|---:|---| -| 12 | 5 | `deeplabcut.pose_estimation_pytorch.models.backbones.base.BACKBONES` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 13 | 5 | `deeplabcut.pose_estimation_pytorch.models.backbones.base.BaseBackbone` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 15 | 77 | `deeplabcut.pose_estimation_pytorch.models.backbones.cond_prenet.CondPreNet` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 16 | 73 | `deeplabcut.pose_estimation_pytorch.models.backbones.cspnext.CSPNeXt` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 17 | 71 | `deeplabcut.pose_estimation_pytorch.models.backbones.hrnet.HRNet` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 18 | 76 | `deeplabcut.pose_estimation_pytorch.models.backbones.hrnet_coam.HRNetCoAM` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 19 | 72 | `deeplabcut.pose_estimation_pytorch.models.backbones.resnet.DLCRNet` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 19 | 81 | `deeplabcut.pose_estimation_pytorch.models.backbones.resnet.ResNet` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\models\backbones\__init__.py:12" -``` - -#### `deeplabcut\pose_estimation_pytorch\models\target_generators\__init__.py` (8) - -| Line | Col | Message | -|---:|---:|---| -| 12 | 5 | `deeplabcut.pose_estimation_pytorch.models.target_generators.base.TARGET_GENERATORS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 13 | 5 | `deeplabcut.pose_estimation_pytorch.models.target_generators.base.BaseGenerator` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 14 | 5 | `deeplabcut.pose_estimation_pytorch.models.target_generators.base.SequentialGenerator` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 17 | 5 | `deeplabcut.pose_estimation_pytorch.models.target_generators.dekr_targets.DEKRGenerator` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 20 | 5 | `deeplabcut.pose_estimation_pytorch.models.target_generators.heatmap_targets.HeatmapGaussianGenerator` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 21 | 5 | `deeplabcut.pose_estimation_pytorch.models.target_generators.heatmap_targets.HeatmapPlateauGenerator` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 24 | 5 | `deeplabcut.pose_estimation_pytorch.models.target_generators.pafs_targets.PartAffinityFieldGenerator` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 27 | 5 | `deeplabcut.pose_estimation_pytorch.models.target_generators.sim_cc.SimCCGenerator` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\models\target_generators\__init__.py:12" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\datasets\__init__.py` (7) - -| Line | Col | Message | -|---:|---:|---| -| 11 | 19 | `.coco.COCOPoseDataset` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 12 | 21 | `.ma_dlc.MaDLCPoseDataset` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 13 | 31 | `.ma_dlc_dataframe.MaDLCDataFrame` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 14 | 26 | `.materialize.mat_func_factory` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 15 | 20 | `.multi.MultiSourceDataset` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 16 | 25 | `.single_dlc.SingleDLCPoseDataset` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 17 | 35 | `.single_dlc_dataframe.SingleDLCDataFrame` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\__init__.py:11" -``` - -#### `deeplabcut\pose_estimation_pytorch\models\heads\__init__.py` (7) - -| Line | Col | Message | -|---:|---:|---| -| 11 | 66 | `deeplabcut.pose_estimation_pytorch.models.heads.base.HEADS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 11 | 73 | `deeplabcut.pose_estimation_pytorch.models.heads.base.BaseHead` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 12 | 66 | `deeplabcut.pose_estimation_pytorch.models.heads.dekr.DEKRHead` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 13 | 69 | `deeplabcut.pose_estimation_pytorch.models.heads.dlcrnet.DLCRNetHead` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 14 | 72 | `deeplabcut.pose_estimation_pytorch.models.heads.rtmcc_head.RTMCCHead` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 15 | 73 | `deeplabcut.pose_estimation_pytorch.models.heads.simple_head.HeatmapHead` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 16 | 73 | `deeplabcut.pose_estimation_pytorch.models.heads.transformer.TransformerHead` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\models\heads\__init__.py:11" -``` - -#### `deeplabcut\pose_estimation_pytorch\models\predictors\__init__.py` (7) - -| Line | Col | Message | -|---:|---:|---| -| 12 | 5 | `deeplabcut.pose_estimation_pytorch.models.predictors.base.PREDICTORS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 13 | 5 | `deeplabcut.pose_estimation_pytorch.models.predictors.base.BasePredictor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 16 | 5 | `deeplabcut.pose_estimation_pytorch.models.predictors.dekr_predictor.DEKRPredictor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 19 | 5 | `deeplabcut.pose_estimation_pytorch.models.predictors.identity_predictor.IdentityPredictor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 22 | 5 | `deeplabcut.pose_estimation_pytorch.models.predictors.paf_predictor.PartAffinityFieldPredictor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 25 | 5 | `deeplabcut.pose_estimation_pytorch.models.predictors.sim_cc.SimCCPredictor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 28 | 5 | `deeplabcut.pose_estimation_pytorch.models.predictors.single_predictor.HeatmapPredictor` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\models\predictors\__init__.py:12" -``` - -#### `deeplabcut\create_project\__init__.py` (6) - -| Line | Col | Message | -|---:|---:|---| -| 11 | 43 | `deeplabcut.create_project.add.add_new_videos` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 12 | 49 | `deeplabcut.create_project.demo_data.load_demo_data` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 14 | 5 | `deeplabcut.create_project.modelzoo.create_pretrained_human_project` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 15 | 5 | `deeplabcut.create_project.modelzoo.create_pretrained_project` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 17 | 43 | `deeplabcut.create_project.new.create_new_project` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 18 | 46 | `deeplabcut.create_project.new_3d.create_new_project_3d` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\create_project\__init__.py:11" -``` - -#### `deeplabcut\pose_estimation_pytorch\modelzoo\__init__.py` (6) - -| Line | Col | Message | -|---:|---:|---| -| 12 | 5 | `deeplabcut.pose_estimation_pytorch.modelzoo.utils.download_super_animal_snapshot` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 13 | 5 | `deeplabcut.pose_estimation_pytorch.modelzoo.utils.get_snapshot_folder_path` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 14 | 5 | `deeplabcut.pose_estimation_pytorch.modelzoo.utils.get_super_animal_model_config_path` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 15 | 5 | `deeplabcut.pose_estimation_pytorch.modelzoo.utils.get_super_animal_project_config_path` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 16 | 5 | `deeplabcut.pose_estimation_pytorch.modelzoo.utils.get_super_animal_snapshot_path` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 17 | 5 | `deeplabcut.pose_estimation_pytorch.modelzoo.utils.load_super_animal_config` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\modelzoo\__init__.py:12" -``` - -#### `deeplabcut\core\metrics\__init__.py` (4) - -| Line | Col | Message | -|---:|---:|---| -| 11 | 18 | `.api.compute_metrics` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 11 | 35 | `.api.prepare_evaluation_data` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 12 | 19 | `.bbox.compute_bbox_metrics` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 13 | 23 | `.identity.compute_identity_scores` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\core\metrics\__init__.py:11" -``` - -#### `deeplabcut\pose_estimation_pytorch\models\detectors\__init__.py` (4) - -| Line | Col | Message | -|---:|---:|---| -| 12 | 5 | `deeplabcut.pose_estimation_pytorch.models.detectors.base.DETECTORS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 13 | 5 | `deeplabcut.pose_estimation_pytorch.models.detectors.base.BaseDetector` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 15 | 76 | `deeplabcut.pose_estimation_pytorch.models.detectors.fasterRCNN.FasterRCNN` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 16 | 69 | `deeplabcut.pose_estimation_pytorch.models.detectors.ssd.SSDLite` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\models\detectors\__init__.py:12" -``` - -#### `deeplabcut\pose_tracking_pytorch\processor\__init__.py` (4) - -| Line | Col | Message | -|---:|---:|---| -| 13 | 5 | `.processor.default_device` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 14 | 5 | `.processor.do_dlc_inference` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 15 | 5 | `.processor.do_dlc_pair_inference` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 16 | 5 | `.processor.do_dlc_train` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_tracking_pytorch\processor\__init__.py:13" -``` - -#### `deeplabcut\generate_training_dataset\__init__.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 15 | 5 | `deeplabcut.generate_training_dataset.metadata.DataSplit` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 16 | 5 | `deeplabcut.generate_training_dataset.metadata.ShuffleMetadata` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 17 | 5 | `deeplabcut.generate_training_dataset.metadata.TrainingDatasetMetadata` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\generate_training_dataset\__init__.py:15" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\__init__.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 11 | 20 | `.utils.add_skeleton` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 11 | 34 | `.utils.create_modelprefix` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 11 | 54 | `.utils.customized_colormap` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\__init__.py:11" -``` - -#### `deeplabcut\pose_estimation_pytorch\models\necks\__init__.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 11 | 66 | `deeplabcut.pose_estimation_pytorch.models.necks.base.NECKS` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 11 | 73 | `deeplabcut.pose_estimation_pytorch.models.necks.base.BaseNeck` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 12 | 73 | `deeplabcut.pose_estimation_pytorch.models.necks.transformer.Transformer` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\models\necks\__init__.py:11" -``` - -#### `deeplabcut\pose_tracking_pytorch\tracking_utils\__init__.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 12 | 5 | `.preprocessing.convert_coord_from_img_space_to_feature_space` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 13 | 5 | `.preprocessing.load_features_from_coord` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 14 | 5 | `.preprocessing.query_feature_by_coord_in_img_space` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_tracking_pytorch\tracking_utils\__init__.py:12" -``` - -#### `deeplabcut\gui\window.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 196 | 24 | `tensorflow` imported but unused; consider using `importlib.util.find_spec` to test for availability | -| 696 | 62 | `deeplabcut.pose_tracking_pytorch.transformer_reID` imported but unused; consider using `importlib.util.find_spec` to test for availability | - -Quick open commands: - -```powershell -code -g "deeplabcut\gui\window.py:196" -``` - -#### `deeplabcut\pose_estimation_pytorch\post_processing\__init__.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 12 | 5 | `deeplabcut.pose_estimation_pytorch.post_processing.match_predictions_to_gt.oks_match_prediction_to_gt` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 13 | 5 | `deeplabcut.pose_estimation_pytorch.post_processing.match_predictions_to_gt.rmse_match_prediction_to_gt` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\post_processing\__init__.py:12" -``` - -#### `deeplabcut\pose_estimation_tensorflow\__init__.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 18 | 15 | `._tf_legacy` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 28 | 58 | `deeplabcut.pose_estimation_tensorflow.export.export_model` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\__init__.py:18" -``` - -#### `deeplabcut\pose_tracking_pytorch\__init__.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 12 | 19 | `.apis.transformer_reID` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 15 | 33 | `.train_dlctransreid.train_tracking_transformer` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_tracking_pytorch\__init__.py:12" -``` - -#### `deeplabcut\pose_tracking_pytorch\model\__init__.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 12 | 25 | `.make_model.build_dlc_transformer` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | -| 12 | 48 | `.make_model.make_dlc_model` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_tracking_pytorch\model\__init__.py:12" -``` - -#### `deeplabcut\__main__.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 15 | 16 | `PySide6` imported but unused; consider using `importlib.util.find_spec` to test for availability | - -Quick open commands: - -```powershell -code -g "deeplabcut\__main__.py:15" -``` - -#### `deeplabcut\gui\__init__.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 15 | 8 | `qtpy` imported but unused | - -Quick open commands: - -```powershell -code -g "deeplabcut\gui\__init__.py:15" -``` - -#### `deeplabcut\gui\tabs\create_training_dataset.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 248 | 28 | `tensorflow` imported but unused; consider using `importlib.util.find_spec` to test for availability | - -Quick open commands: - -```powershell -code -g "deeplabcut\gui\tabs\create_training_dataset.py:248" -``` - -#### `deeplabcut\modelzoo\__init__.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 11 | 55 | `deeplabcut.modelzoo.weight_initialization.build_weight_init` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\__init__.py:11" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\conversion_table\__init__.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 11 | 31 | `.conversion_table.get_conversion_table` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\conversion_table\__init__.py:11" -``` - -#### `deeplabcut\pose_estimation_tensorflow\lib\__init__.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 15 | 8 | `deeplabcut.core.trackingutils` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\lib\__init__.py:15" -``` - -#### `deeplabcut\pose_estimation_tensorflow\modelzoo\__init__.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 11 | 18 | `.api.SpatiotemporalAdaptation` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\modelzoo\__init__.py:11" -``` - -#### `deeplabcut\pose_estimation_tensorflow\modelzoo\api\__init__.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 11 | 35 | `.spatiotemporal_adapt.SpatiotemporalAdaptation` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\modelzoo\api\__init__.py:11" -``` - -#### `deeplabcut\pose_tracking_pytorch\datasets\__init__.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 12 | 30 | `.make_dataloader.make_dlc_dataloader` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_tracking_pytorch\datasets\__init__.py:12" -``` - -#### `deeplabcut\pose_tracking_pytorch\loss\__init__.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 11 | 24 | `.make_loss.easy_triplet_loss` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_tracking_pytorch\loss\__init__.py:11" -``` - -#### `deeplabcut\pose_tracking_pytorch\model\backbones\__init__.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 11 | 26 | `.vit_pytorch.dlc_base_kpt_TransReID` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_tracking_pytorch\model\backbones\__init__.py:11" -``` - -#### `deeplabcut\pose_tracking_pytorch\solver\__init__.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 11 | 29 | `.make_optimizer.make_easy_optimizer` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_tracking_pytorch\solver\__init__.py:11" -``` - -#### `deeplabcut\post_processing\__init__.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 21 | 57 | `deeplabcut.post_processing.analyze_skeleton.analyzeskeleton` imported but unused; consider removing, adding to `__all__`, or using a redundant alias | - -Quick open commands: - -```powershell -code -g "deeplabcut\post_processing\__init__.py:21" -``` - -## B905 - -Count: **176** - -### Files affected - -| File | Count | -|---|---:| -| `docs\recipes\flip_and_rotate.ipynb` | 18 | -| `deeplabcut\refine_training_dataset\stitch.py` | 12 | -| `deeplabcut\core\inferenceutils.py` | 8 | -| `deeplabcut\refine_training_dataset\tracklets.py` | 8 | -| `deeplabcut\utils\visualization.py` | 8 | -| `deeplabcut\core\crossvalutils.py` | 7 | -| `deeplabcut\utils\pseudo_label.py` | 6 | -| `deeplabcut\pose_estimation_pytorch\apis\prune_paf_graph.py` | 5 | -| `deeplabcut\utils\make_labeled_video.py` | 5 | -| `examples\COLAB\COLAB_HumanPose_with_RTMPose.ipynb` | 5 | -| `deeplabcut\pose_estimation_tensorflow\predict_multianimal.py` | 4 | -| `deeplabcut\pose_estimation_pytorch\data\preprocessor.py` | 3 | -| `tests\pose_estimation_pytorch\data\test_transforms.py` | 3 | -| `tests\pose_estimation_pytorch\runners\test_runners_inference.py` | 3 | -| `deeplabcut\core\metrics\distance_metrics.py` | 2 | -| `deeplabcut\core\trackingutils.py` | 2 | -| `deeplabcut\create_project\add.py` | 2 | -| `deeplabcut\create_project\new.py` | 2 | -| `deeplabcut\modelzoo\webapp\inference.py` | 2 | -| `deeplabcut\pose_estimation_pytorch\apis\analyze_images.py` | 2 | -| `deeplabcut\pose_estimation_pytorch\apis\evaluation.py` | 2 | -| `deeplabcut\pose_estimation_pytorch\apis\visualization.py` | 2 | -| `deeplabcut\pose_estimation_pytorch\data\postprocessor.py` | 2 | -| `deeplabcut\pose_estimation_pytorch\data\transforms.py` | 2 | -| `deeplabcut\pose_estimation_pytorch\runners\logger.py` | 2 | -| `deeplabcut\pose_estimation_pytorch\runners\train.py` | 2 | -| `deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py` | 2 | -| `tests\test_pose_multianimal_imgaug.py` | 2 | -| `tests\test_predict_supermodel.py` | 2 | -| `deeplabcut\benchmark\metrics.py` | 1 | -| `deeplabcut\core\metrics\bbox.py` | 1 | -| `deeplabcut\core\metrics\identity.py` | 1 | -| `deeplabcut\generate_training_dataset\frame_extraction.py` | 1 | -| `deeplabcut\generate_training_dataset\metadata.py` | 1 | -| `deeplabcut\generate_training_dataset\multiple_individuals_trainingsetmanipulation.py` | 1 | -| `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` | 1 | -| `deeplabcut\gui\tabs\create_videos.py` | 1 | -| `deeplabcut\gui\tabs\evaluate_network.py` | 1 | -| `deeplabcut\gui\tracklet_toolbox.py` | 1 | -| `deeplabcut\gui\widgets.py` | 1 | -| `deeplabcut\modelzoo\generalized_data_converter\utils.py` | 1 | -| `deeplabcut\modelzoo\utils.py` | 1 | -| `deeplabcut\pose_estimation_3d\plotting3D.py` | 1 | -| `deeplabcut\pose_estimation_3d\triangulation.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\apis\tracklets.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\apis\utils.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\data\utils.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\models\backbones\hrnet_coam.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\models\heads\dlcrnet.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\models\heads\simple_head.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\models\predictors\paf_predictor.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\modelzoo\memory_replay.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\modelzoo\utils.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\post_processing\identity.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\post_processing\nms.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\runners\schedulers.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\core\evaluate_multianimal.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\core\predict_multianimal.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\export.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\nnets\conv_blocks.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\visualizemaps.py` | 1 | -| `deeplabcut\pose_tracking_pytorch\solver\scheduler.py` | 1 | -| `deeplabcut\post_processing\analyze_skeleton.py` | 1 | -| `deeplabcut\post_processing\filtering.py` | 1 | -| `deeplabcut\utils\auxfun_videos.py` | 1 | -| `deeplabcut\utils\auxiliaryfunctions_3d.py` | 1 | -| `deeplabcut\utils\skeleton.py` | 1 | -| `examples\COLAB\COLAB_BUCTD_and_CTD_tracking.ipynb` | 1 | -| `examples\testscript_multianimal.py` | 1 | -| `examples\testscript_transreid.py` | 1 | -| `examples\utils.py` | 1 | -| `tests\generate_training_dataset\test_trainset_metadata.py` | 1 | -| `tests\pose_estimation_pytorch\data\test_data_ctd.py` | 1 | -| `tests\pose_estimation_pytorch\data\test_postprocessor.py` | 1 | -| `tests\pose_estimation_pytorch\data\test_preprocessor.py` | 1 | -| `tests\pose_estimation_pytorch\runners\test_dynamic_cropper.py` | 1 | -| `tests\test_inferenceutils.py` | 1 | -| `tests\test_stitcher.py` | 1 | - -### Details - -#### `docs\recipes\flip_and_rotate.ipynb` (18) - -| Line | Col | Message | -|---:|---:|---| -| 8 | 34 | `zip()` without an explicit `strict=` parameter | -| 14 | 34 | `zip()` without an explicit `strict=` parameter | -| 15 | 34 | `zip()` without an explicit `strict=` parameter | -| 15 | 34 | `zip()` without an explicit `strict=` parameter | -| 15 | 34 | `zip()` without an explicit `strict=` parameter | -| 15 | 34 | `zip()` without an explicit `strict=` parameter | -| 18 | 34 | `zip()` without an explicit `strict=` parameter | -| 26 | 34 | `zip()` without an explicit `strict=` parameter | -| 27 | 34 | `zip()` without an explicit `strict=` parameter | -| 35 | 36 | `zip()` without an explicit `strict=` parameter | -| 35 | 36 | `zip()` without an explicit `strict=` parameter | -| 35 | 41 | `zip()` without an explicit `strict=` parameter | -| 35 | 41 | `zip()` without an explicit `strict=` parameter | -| 35 | 41 | `zip()` without an explicit `strict=` parameter | -| 37 | 36 | `zip()` without an explicit `strict=` parameter | -| 58 | 49 | `zip()` without an explicit `strict=` parameter | -| 58 | 49 | `zip()` without an explicit `strict=` parameter | -| 58 | 49 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "docs\recipes\flip_and_rotate.ipynb:35" -``` - -#### `deeplabcut\refine_training_dataset\stitch.py` (12) - -| Line | Col | Message | -|---:|---:|---| -| 62 | 58 | `zip()` without an explicit `strict=` parameter | -| 526 | 30 | `zip()` without an explicit `strict=` parameter | -| 564 | 56 | `zip()` without an explicit `strict=` parameter | -| 627 | 31 | `zip()` without an explicit `strict=` parameter | -| 630 | 31 | `zip()` without an explicit `strict=` parameter | -| 631 | 31 | `zip()` without an explicit `strict=` parameter | -| 632 | 31 | `zip()` without an explicit `strict=` parameter | -| 678 | 38 | `zip()` without an explicit `strict=` parameter | -| 679 | 38 | `zip()` without an explicit `strict=` parameter | -| 915 | 36 | `zip()` without an explicit `strict=` parameter | -| 928 | 29 | `zip()` without an explicit `strict=` parameter | -| 950 | 30 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\refine_training_dataset\stitch.py:62" -``` - -#### `deeplabcut\core\inferenceutils.py` (8) - -| Line | Col | Message | -|---:|---:|---| -| 157 | 21 | `zip()` without an explicit `strict=` parameter | -| 423 | 49 | `zip()` without an explicit `strict=` parameter | -| 426 | 29 | `zip()` without an explicit `strict=` parameter | -| 460 | 21 | `zip()` without an explicit `strict=` parameter | -| 484 | 33 | `zip()` without an explicit `strict=` parameter | -| 757 | 24 | `zip()` without an explicit `strict=` parameter | -| 1033 | 25 | `zip()` without an explicit `strict=` parameter | -| 1084 | 24 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\core\inferenceutils.py:157" -``` - -#### `deeplabcut\refine_training_dataset\tracklets.py` (8) - -| Line | Col | Message | -|---:|---:|---| -| 109 | 30 | `zip()` without an explicit `strict=` parameter | -| 163 | 41 | `zip()` without an explicit `strict=` parameter | -| 199 | 25 | `zip()` without an explicit `strict=` parameter | -| 257 | 49 | `zip()` without an explicit `strict=` parameter | -| 259 | 52 | `zip()` without an explicit `strict=` parameter | -| 305 | 25 | `zip()` without an explicit `strict=` parameter | -| 318 | 21 | `zip()` without an explicit `strict=` parameter | -| 328 | 21 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\refine_training_dataset\tracklets.py:109" -``` - -#### `deeplabcut\utils\visualization.py` (8) - -| Line | Col | Message | -|---:|---:|---| -| 163 | 48 | `zip()` without an explicit `strict=` parameter | -| 183 | 30 | `zip()` without an explicit `strict=` parameter | -| 337 | 35 | `zip()` without an explicit `strict=` parameter | -| 346 | 41 | `zip()` without an explicit `strict=` parameter | -| 363 | 26 | `zip()` without an explicit `strict=` parameter | -| 364 | 23 | `zip()` without an explicit `strict=` parameter | -| 399 | 30 | `zip()` without an explicit `strict=` parameter | -| 420 | 29 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\visualization.py:163" -``` - -#### `deeplabcut\core\crossvalutils.py` (7) - -| Line | Col | Message | -|---:|---:|---| -| 146 | 47 | `zip()` without an explicit `strict=` parameter | -| 152 | 34 | `zip()` without an explicit `strict=` parameter | -| 225 | 17 | `zip()` without an explicit `strict=` parameter | -| 343 | 40 | `zip()` without an explicit `strict=` parameter | -| 345 | 17 | `zip()` without an explicit `strict=` parameter | -| 349 | 24 | `zip()` without an explicit `strict=` parameter | -| 371 | 27 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\core\crossvalutils.py:146" -``` - -#### `deeplabcut\utils\pseudo_label.py` (6) - -| Line | Col | Message | -|---:|---:|---| -| 265 | 24 | `zip()` without an explicit `strict=` parameter | -| 276 | 35 | `zip()` without an explicit `strict=` parameter | -| 307 | 32 | `zip()` without an explicit `strict=` parameter | -| 320 | 24 | `zip()` without an explicit `strict=` parameter | -| 420 | 57 | `zip()` without an explicit `strict=` parameter | -| 434 | 39 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\pseudo_label.py:265" -``` - -#### `deeplabcut\pose_estimation_pytorch\apis\prune_paf_graph.py` (5) - -| Line | Col | Message | -|---:|---:|---| -| 216 | 52 | `zip()` without an explicit `strict=` parameter | -| 222 | 34 | `zip()` without an explicit `strict=` parameter | -| 259 | 17 | `zip()` without an explicit `strict=` parameter | -| 263 | 24 | `zip()` without an explicit `strict=` parameter | -| 281 | 29 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\apis\prune_paf_graph.py:216" -``` - -#### `deeplabcut\utils\make_labeled_video.py` (5) - -| Line | Col | Message | -|---:|---:|---| -| 62 | 17 | `zip()` without an explicit `strict=` parameter | -| 1092 | 35 | `zip()` without an explicit `strict=` parameter | -| 1099 | 41 | `zip()` without an explicit `strict=` parameter | -| 1116 | 25 | `zip()` without an explicit `strict=` parameter | -| 1134 | 37 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\make_labeled_video.py:62" -``` - -#### `examples\COLAB\COLAB_HumanPose_with_RTMPose.ipynb` (5) - -| Line | Col | Message | -|---:|---:|---| -| 8 | 38 | `zip()` without an explicit `strict=` parameter | -| 35 | 49 | `zip()` without an explicit `strict=` parameter | -| 39 | 49 | `zip()` without an explicit `strict=` parameter | -| 61 | 37 | `zip()` without an explicit `strict=` parameter | -| 69 | 77 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "examples\COLAB\COLAB_HumanPose_with_RTMPose.ipynb:35" -``` - -#### `deeplabcut\pose_estimation_tensorflow\predict_multianimal.py` (4) - -| Line | Col | Message | -|---:|---:|---| -| 292 | 49 | `zip()` without an explicit `strict=` parameter | -| 318 | 49 | `zip()` without an explicit `strict=` parameter | -| 411 | 34 | `zip()` without an explicit `strict=` parameter | -| 428 | 34 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\predict_multianimal.py:292" -``` - -#### `deeplabcut\pose_estimation_pytorch\data\preprocessor.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 273 | 54 | `zip()` without an explicit `strict=` parameter | -| 276 | 89 | `zip()` without an explicit `strict=` parameter | -| 280 | 97 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\data\preprocessor.py:273" -``` - -#### `tests\pose_estimation_pytorch\data\test_transforms.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 59 | 32 | `zip()` without an explicit `strict=` parameter | -| 224 | 36 | `zip()` without an explicit `strict=` parameter | -| 272 | 30 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "tests\pose_estimation_pytorch\data\test_transforms.py:59" -``` - -#### `tests\pose_estimation_pytorch\runners\test_runners_inference.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 91 | 17 | `zip()` without an explicit `strict=` parameter | -| 143 | 17 | `zip()` without an explicit `strict=` parameter | -| 145 | 29 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "tests\pose_estimation_pytorch\runners\test_runners_inference.py:91" -``` - -#### `deeplabcut\core\metrics\distance_metrics.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 364 | 56 | `zip()` without an explicit `strict=` parameter | -| 402 | 56 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\core\metrics\distance_metrics.py:364" -``` - -#### `deeplabcut\core\trackingutils.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 447 | 29 | `zip()` without an explicit `strict=` parameter | -| 706 | 25 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\core\trackingutils.py:447" -``` - -#### `deeplabcut\create_project\add.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 77 | 25 | `zip()` without an explicit `strict=` parameter | -| 87 | 25 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\create_project\add.py:77" -``` - -#### `deeplabcut\create_project\new.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 185 | 25 | `zip()` without an explicit `strict=` parameter | -| 190 | 25 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\create_project\new.py:185" -``` - -#### `deeplabcut\modelzoo\webapp\inference.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 103 | 29 | `zip()` without an explicit `strict=` parameter | -| 106 | 71 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\webapp\inference.py:103" -``` - -#### `deeplabcut\pose_estimation_pytorch\apis\analyze_images.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 532 | 28 | `zip()` without an explicit `strict=` parameter | -| 541 | 80 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\apis\analyze_images.py:532" -``` - -#### `deeplabcut\pose_estimation_pytorch\apis\evaluation.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 94 | 36 | `zip()` without an explicit `strict=` parameter | -| 97 | 80 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\apis\evaluation.py:94" -``` - -#### `deeplabcut\pose_estimation_pytorch\apis\visualization.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 149 | 49 | `zip()` without an explicit `strict=` parameter | -| 366 | 43 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\apis\visualization.py:149" -``` - -#### `deeplabcut\pose_estimation_pytorch\data\postprocessor.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 395 | 54 | `zip()` without an explicit `strict=` parameter | -| 518 | 46 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\data\postprocessor.py:395" -``` - -#### `deeplabcut\pose_estimation_pytorch\data\transforms.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 498 | 31 | `zip()` without an explicit `strict=` parameter | -| 641 | 59 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\data\transforms.py:498" -``` - -#### `deeplabcut\pose_estimation_pytorch\runners\logger.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 241 | 48 | `zip()` without an explicit `strict=` parameter | -| 505 | 35 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\runners\logger.py:241" -``` - -#### `deeplabcut\pose_estimation_pytorch\runners\train.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 506 | 40 | `zip()` without an explicit `strict=` parameter | -| 638 | 72 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\runners\train.py:506" -``` - -#### `deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 90 | 44 | `zip()` without an explicit `strict=` parameter | -| 411 | 49 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py:90" -``` - -#### `tests\test_pose_multianimal_imgaug.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 73 | 49 | `zip()` without an explicit `strict=` parameter | -| 78 | 32 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "tests\test_pose_multianimal_imgaug.py:73" -``` - -#### `tests\test_predict_supermodel.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 26 | 46 | `zip()` without an explicit `strict=` parameter | -| 48 | 63 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "tests\test_predict_supermodel.py:26" -``` - -#### `deeplabcut\benchmark\metrics.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 58 | 29 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\benchmark\metrics.py:58" -``` - -#### `deeplabcut\core\metrics\bbox.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 100 | 28 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\core\metrics\bbox.py:100" -``` - -#### `deeplabcut\core\metrics\identity.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 67 | 53 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\core\metrics\identity.py:67" -``` - -#### `deeplabcut\generate_training_dataset\frame_extraction.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 498 | 32 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\generate_training_dataset\frame_extraction.py:498" -``` - -#### `deeplabcut\generate_training_dataset\metadata.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 140 | 45 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\generate_training_dataset\metadata.py:140" -``` - -#### `deeplabcut\generate_training_dataset\multiple_individuals_trainingsetmanipulation.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 387 | 59 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\generate_training_dataset\multiple_individuals_trainingsetmanipulation.py:387" -``` - -#### `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 1097 | 63 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\generate_training_dataset\trainingsetmanipulation.py:1097" -``` - -#### `deeplabcut\gui\tabs\create_videos.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 273 | 58 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\gui\tabs\create_videos.py:273" -``` - -#### `deeplabcut\gui\tabs\evaluate_network.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 50 | 37 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\gui\tabs\evaluate_network.py:50" -``` - -#### `deeplabcut\gui\tracklet_toolbox.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 772 | 46 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\gui\tracklet_toolbox.py:772" -``` - -#### `deeplabcut\gui\widgets.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 663 | 21 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\gui\widgets.py:663" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\utils.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 187 | 28 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\utils.py:187" -``` - -#### `deeplabcut\modelzoo\utils.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 184 | 17 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\utils.py:184" -``` - -#### `deeplabcut\pose_estimation_3d\plotting3D.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 265 | 31 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_3d\plotting3D.py:265" -``` - -#### `deeplabcut\pose_estimation_3d\triangulation.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 474 | 38 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_3d\triangulation.py:474" -``` - -#### `deeplabcut\pose_estimation_pytorch\apis\tracklets.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 276 | 51 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\apis\tracklets.py:276" -``` - -#### `deeplabcut\pose_estimation_pytorch\apis\utils.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 465 | 17 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\apis\utils.py:465" -``` - -#### `deeplabcut\pose_estimation_pytorch\data\utils.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 495 | 26 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\data\utils.py:495" -``` - -#### `deeplabcut\pose_estimation_pytorch\models\backbones\hrnet_coam.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 189 | 43 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\models\backbones\hrnet_coam.py:189" -``` - -#### `deeplabcut\pose_estimation_pytorch\models\heads\dlcrnet.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 120 | 63 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\models\heads\dlcrnet.py:120" -``` - -#### `deeplabcut\pose_estimation_pytorch\models\heads\simple_head.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 196 | 35 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\models\heads\simple_head.py:196" -``` - -#### `deeplabcut\pose_estimation_pytorch\models\predictors\paf_predictor.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 388 | 23 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\models\predictors\paf_predictor.py:388" -``` - -#### `deeplabcut\pose_estimation_pytorch\modelzoo\memory_replay.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 107 | 30 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\modelzoo\memory_replay.py:107" -``` - -#### `deeplabcut\pose_estimation_pytorch\modelzoo\utils.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 160 | 27 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\modelzoo\utils.py:160" -``` - -#### `deeplabcut\pose_estimation_pytorch\post_processing\identity.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 42 | 29 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\post_processing\identity.py:42" -``` - -#### `deeplabcut\pose_estimation_pytorch\post_processing\nms.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 91 | 39 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\post_processing\nms.py:91" -``` - -#### `deeplabcut\pose_estimation_pytorch\runners\schedulers.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 129 | 29 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\runners\schedulers.py:129" -``` - -#### `deeplabcut\pose_estimation_tensorflow\core\evaluate_multianimal.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 331 | 42 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\core\evaluate_multianimal.py:331" -``` - -#### `deeplabcut\pose_estimation_tensorflow\core\predict_multianimal.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 90 | 26 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\core\predict_multianimal.py:90" -``` - -#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 391 | 32 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py:391" -``` - -#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 472 | 32 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py:472" -``` - -#### `deeplabcut\pose_estimation_tensorflow\export.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 325 | 21 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\export.py:325" -``` - -#### `deeplabcut\pose_estimation_tensorflow\nnets\conv_blocks.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 368 | 50 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\nnets\conv_blocks.py:368" -``` - -#### `deeplabcut\pose_estimation_tensorflow\visualizemaps.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 256 | 22 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\visualizemaps.py:256" -``` - -#### `deeplabcut\pose_tracking_pytorch\solver\scheduler.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 96 | 35 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_tracking_pytorch\solver\scheduler.py:96" -``` - -#### `deeplabcut\post_processing\analyze_skeleton.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 60 | 54 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\post_processing\analyze_skeleton.py:60" -``` - -#### `deeplabcut\post_processing\filtering.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 57 | 39 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\post_processing\filtering.py:57" -``` - -#### `deeplabcut\utils\auxfun_videos.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 274 | 42 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\auxfun_videos.py:274" -``` - -#### `deeplabcut\utils\auxiliaryfunctions_3d.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 317 | 19 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\auxiliaryfunctions_3d.py:317" -``` - -#### `deeplabcut\utils\skeleton.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 171 | 21 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\skeleton.py:171" -``` - -#### `examples\COLAB\COLAB_BUCTD_and_CTD_tracking.ipynb` (1) - -| Line | Col | Message | -|---:|---:|---| -| 22 | 37 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "examples\COLAB\COLAB_BUCTD_and_CTD_tracking.ipynb:22" -``` - -#### `examples\testscript_multianimal.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 85 | 17 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "examples\testscript_multianimal.py:85" -``` - -#### `examples\testscript_transreid.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 81 | 17 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "examples\testscript_transreid.py:81" -``` - -#### `examples\utils.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 102 | 34 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "examples\utils.py:102" -``` - -#### `tests\generate_training_dataset\test_trainset_metadata.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 326 | 34 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "tests\generate_training_dataset\test_trainset_metadata.py:326" -``` - -#### `tests\pose_estimation_pytorch\data\test_data_ctd.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 151 | 91 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "tests\pose_estimation_pytorch\data\test_data_ctd.py:151" -``` - -#### `tests\pose_estimation_pytorch\data\test_postprocessor.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 304 | 28 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "tests\pose_estimation_pytorch\data\test_postprocessor.py:304" -``` - -#### `tests\pose_estimation_pytorch\data\test_preprocessor.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 156 | 49 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "tests\pose_estimation_pytorch\data\test_preprocessor.py:156" -``` - -#### `tests\pose_estimation_pytorch\runners\test_dynamic_cropper.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 166 | 47 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "tests\pose_estimation_pytorch\runners\test_dynamic_cropper.py:166" -``` - -#### `tests\test_inferenceutils.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 30 | 17 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "tests\test_inferenceutils.py:30" -``` - -#### `tests\test_stitcher.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 103 | 10 | `zip()` without an explicit `strict=` parameter | - -Quick open commands: - -```powershell -code -g "tests\test_stitcher.py:103" -``` - -## F841 - -Count: **141** - -### Files affected - -| File | Count | -|---|---:| -| `deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py` | 74 | -| `deeplabcut\utils\pseudo_label.py` | 7 | -| `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` | 5 | -| `deeplabcut\modelzoo\generalized_data_converter\utils.py` | 4 | -| `deeplabcut\pose_estimation_3d\triangulation.py` | 3 | -| `deeplabcut\pose_estimation_pytorch\models\predictors\dekr_predictor.py` | 3 | -| `deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py` | 3 | -| `deeplabcut\pose_estimation_tensorflow\export.py` | 3 | -| `deeplabcut\pose_estimation_tensorflow\predict_videos.py` | 3 | -| `deeplabcut\pose_estimation_pytorch\models\necks\layers.py` | 2 | -| `deeplabcut\pose_estimation_tensorflow\datasets\pose_tensorpack.py` | 2 | -| `deeplabcut\pose_estimation_tensorflow\visualizemaps.py` | 2 | -| `tests\pose_estimation_pytorch\runners\bottum_up.py` | 2 | -| `deeplabcut\generate_training_dataset\frame_extraction.py` | 1 | -| `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` | 1 | -| `deeplabcut\gui\tabs\modelzoo.py` | 1 | -| `deeplabcut\modelzoo\generalized_data_converter\conversion_table\conversion_table.py` | 1 | -| `deeplabcut\modelzoo\utils.py` | 1 | -| `deeplabcut\pose_estimation_3d\camera_calibration.py` | 1 | -| `deeplabcut\pose_estimation_3d\plotting3D.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\apis\videos.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\models\necks\transformer.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\modelzoo\memory_replay.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\nnets\multi.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\predict_multianimal.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\training.py` | 1 | -| `deeplabcut\pose_tracking_pytorch\model\backbones\vit_pytorch.py` | 1 | -| `deeplabcut\pose_tracking_pytorch\processor\processor.py` | 1 | -| `deeplabcut\pose_tracking_pytorch\train_dlctransreid.py` | 1 | -| `deeplabcut\refine_training_dataset\outlier_frames.py` | 1 | -| `deeplabcut\utils\auxfun_videos.py` | 1 | -| `examples\testscript_mobilenets.py` | 1 | -| `tests\pose_estimation_pytorch\apis\test_apis_evaluate.py` | 1 | -| `tests\pose_estimation_pytorch\config\test_make_pose_config.py` | 1 | -| `tests\pose_estimation_pytorch\data\test_transforms.py` | 1 | -| `tests\pose_estimation_pytorch\modelzoo\test_load_superanimal_models.py` | 1 | -| `tests\pose_estimation_pytorch\other\test_api_utils.py` | 1 | -| `tests\pose_estimation_pytorch\runners\test_runners_inference.py` | 1 | -| `tests\test_auxiliaryfunctions.py` | 1 | -| `tests\test_pose_multianimal_imgaug.py` | 1 | - -### Details - -#### `deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py` (74) - -| Line | Col | Message | -|---:|---:|---| -| 80 | 9 | Local variable `Task` is assigned to but never used | -| 81 | 9 | Local variable `project_path` is assigned to but never used | -| 82 | 9 | Local variable `scorer` is assigned to but never used | -| 83 | 9 | Local variable `date` is assigned to but never used | -| 84 | 9 | Local variable `video_sets` is assigned to but never used | -| 85 | 9 | Local variable `skeleton` is assigned to but never used | -| 86 | 9 | Local variable `bodyparts` is assigned to but never used | -| 87 | 9 | Local variable `start` is assigned to but never used | -| 88 | 9 | Local variable `stop` is assigned to but never used | -| 89 | 9 | Local variable `numframes2pick` is assigned to but never used | -| 90 | 9 | Local variable `skeleton_color` is assigned to but never used | -| 91 | 9 | Local variable `pcutoff` is assigned to but never used | -| 92 | 9 | Local variable `dotsize` is assigned to but never used | -| 93 | 9 | Local variable `alphavalue` is assigned to but never used | -| 94 | 9 | Local variable `colormap` is assigned to but never used | -| 95 | 9 | Local variable `TrainingFraction` is assigned to but never used | -| 96 | 9 | Local variable `iteration` is assigned to but never used | -| 97 | 9 | Local variable `default_net_type` is assigned to but never used | -| 98 | 9 | Local variable `default_augmenter` is assigned to but never used | -| 99 | 9 | Local variable `snapshotindex` is assigned to but never used | -| 100 | 9 | Local variable `batch_size` is assigned to but never used | -| 101 | 9 | Local variable `cropping` is assigned to but never used | -| 102 | 9 | Local variable `croppedtraining` is assigned to but never used | -| 103 | 9 | Local variable `multianimalproject` is assigned to but never used | -| 104 | 9 | Local variable `uniquebodyparts` is assigned to but never used | -| 105 | 9 | Local variable `x1` is assigned to but never used | -| 106 | 9 | Local variable `x2` is assigned to but never used | -| 107 | 9 | Local variable `y1` is assigned to but never used | -| 108 | 9 | Local variable `y2` is assigned to but never used | -| 109 | 9 | Local variable `corer2move2` is assigned to but never used | -| 110 | 9 | Local variable `move2corner` is assigned to but never used | -| 111 | 9 | Local variable `identity` is assigned to but never used | -| 127 | 9 | Local variable `Task` is assigned to but never used | -| 128 | 9 | Local variable `project_path` is assigned to but never used | -| 129 | 9 | Local variable `scorer` is assigned to but never used | -| 130 | 9 | Local variable `date` is assigned to but never used | -| 131 | 9 | Local variable `video_sets` is assigned to but never used | -| 132 | 9 | Local variable `individuals` is assigned to but never used | -| 133 | 9 | Local variable `multianimalbodyparts` is assigned to but never used | -| 134 | 9 | Local variable `skeleton` is assigned to but never used | -| 135 | 9 | Local variable `bodyparts` is assigned to but never used | -| 136 | 9 | Local variable `start` is assigned to but never used | -| 137 | 9 | Local variable `stop` is assigned to but never used | -| 138 | 9 | Local variable `numframes2pick` is assigned to but never used | -| 139 | 9 | Local variable `skeleton_color` is assigned to but never used | -| 140 | 9 | Local variable `pcutoff` is assigned to but never used | -| 141 | 9 | Local variable `dotsize` is assigned to but never used | -| 142 | 9 | Local variable `alphavalue` is assigned to but never used | -| 143 | 9 | Local variable `colormap` is assigned to but never used | -| 144 | 9 | Local variable `TrainingFraction` is assigned to but never used | -| 145 | 9 | Local variable `iteration` is assigned to but never used | -| 146 | 9 | Local variable `default_net_type` is assigned to but never used | -| 147 | 9 | Local variable `default_augmenter` is assigned to but never used | -| 148 | 9 | Local variable `snapshotindex` is assigned to but never used | -| 149 | 9 | Local variable `batch_size` is assigned to but never used | -| 150 | 9 | Local variable `cropping` is assigned to but never used | -| 151 | 9 | Local variable `croppedtraining` is assigned to but never used | -| 152 | 9 | Local variable `multianimalproject` is assigned to but never used | -| 153 | 9 | Local variable `uniquebodyparts` is assigned to but never used | -| 154 | 9 | Local variable `x1` is assigned to but never used | -| 155 | 9 | Local variable `x2` is assigned to but never used | -| 156 | 9 | Local variable `y1` is assigned to but never used | -| 157 | 9 | Local variable `y2` is assigned to but never used | -| 158 | 9 | Local variable `corer2move2` is assigned to but never used | -| 159 | 9 | Local variable `move2corner` is assigned to but never used | -| 160 | 9 | Local variable `identity` is assigned to but never used | -| 239 | 5 | Local variable `total_annotations` is assigned to but never used | -| 243 | 5 | Local variable `count` is assigned to but never used | -| 271 | 5 | Local variable `temp_count` is assigned to but never used | -| 375 | 5 | Local variable `nbodyparts` is assigned to but never used | -| 440 | 5 | Local variable `total_annotations` is assigned to but never used | -| 448 | 9 | Local variable `datasetname` is assigned to but never used | -| 488 | 9 | Local variable `freq` is assigned to but never used | -| 490 | 13 | Local variable `filename` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py:80" -``` - -#### `deeplabcut\utils\pseudo_label.py` (7) - -| Line | Col | Message | -|---:|---:|---| -| 53 | 5 | Local variable `arranged_preds_list` is assigned to but never used | -| 101 | 5 | Local variable `fps` is assigned to but never used | -| 130 | 5 | Local variable `heatmap` is assigned to but never used | -| 401 | 5 | Local variable `new_predictions` is assigned to but never used | -| 403 | 5 | Local variable `num_kpts` is assigned to but never used | -| 447 | 13 | Local variable `bbox_confidence` is assigned to but never used | -| 474 | 5 | Local variable `test_annotations` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\pseudo_label.py:53" -``` - -#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` (5) - -| Line | Col | Message | -|---:|---:|---| -| 129 | 9 | Local variable `num_kpts` is assigned to but never used | -| 315 | 9 | Local variable `num_images` is assigned to but never used | -| 705 | 13 | Local variable `j_x_sm` is assigned to but never used | -| 707 | 13 | Local variable `j_y_sm` is assigned to but never used | -| 709 | 13 | Local variable `map_j` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py:129" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\utils.py` (4) - -| Line | Col | Message | -|---:|---:|---| -| 110 | 9 | Local variable `pickle_obj` is assigned to but never used | -| 125 | 5 | Local variable `video_name` is assigned to but never used | -| 149 | 5 | Local variable `bodyparts` is assigned to but never used | -| 289 | 5 | Local variable `visited` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\utils.py:110" -``` - -#### `deeplabcut\pose_estimation_3d\triangulation.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 305 | 13 | Local variable `scorer_cam1` is assigned to but never used | -| 306 | 13 | Local variable `scorer_cam2` is assigned to but never used | -| 308 | 13 | Local variable `bodyparts` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_3d\triangulation.py:305" -``` - -#### `deeplabcut\pose_estimation_pytorch\models\predictors\dekr_predictor.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 250 | 9 | Local variable `pool1` is assigned to but never used | -| 252 | 9 | Local variable `pool3` is assigned to but never used | -| 253 | 9 | Local variable `map_size` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\models\predictors\dekr_predictor.py:250" -``` - -#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 471 | 17 | Local variable `j_x_sm` is assigned to but never used | -| 473 | 17 | Local variable `j_y_sm` is assigned to but never used | -| 474 | 17 | Local variable `map_j` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py:471" -``` - -#### `deeplabcut\pose_estimation_tensorflow\export.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 118 | 5 | Local variable `path_test_config` is assigned to but never used | -| 145 | 5 | Local variable `trainingsiterations` is assigned to but never used | -| 281 | 5 | Local variable `model_dir` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\export.py:118" -``` - -#### `deeplabcut\pose_estimation_tensorflow\predict_videos.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 183 | 5 | Local variable `pdindex` is assigned to but never used | -| 910 | 17 | Local variable `x0` is assigned to but never used | -| 910 | 21 | Local variable `y0` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\predict_videos.py:183" -``` - -#### `deeplabcut\pose_estimation_pytorch\models\necks\layers.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 176 | 9 | Local variable `b` is assigned to but never used | -| 176 | 12 | Local variable `n` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\models\necks\layers.py:176" -``` - -#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_tensorpack.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 420 | 9 | Local variable `mirror` is assigned to but never used | -| 426 | 9 | Local variable `im_file` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_tensorpack.py:420" -``` - -#### `deeplabcut\pose_estimation_tensorflow\visualizemaps.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 178 | 13 | Local variable `trainingsiterations` is assigned to but never used | -| 191 | 13 | Local variable `PredicteData` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\visualizemaps.py:178" -``` - -#### `tests\pose_estimation_pytorch\runners\bottum_up.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 57 | 5 | Local variable `template` is assigned to but never used | -| 86 | 5 | Local variable `runner` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "tests\pose_estimation_pytorch\runners\bottum_up.py:57" -``` - -#### `deeplabcut\generate_training_dataset\frame_extraction.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 466 | 9 | Local variable `video_dir` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\generate_training_dataset\frame_extraction.py:466" -``` - -#### `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 951 | 5 | Local variable `dlc_root_path` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\generate_training_dataset\trainingsetmanipulation.py:951" -``` - -#### `deeplabcut\gui\tabs\modelzoo.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 459 | 17 | Local variable `results` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\gui\tabs\modelzoo.py:459" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\conversion_table\conversion_table.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 131 | 9 | Local variable `super_bodyparts` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\conversion_table\conversion_table.py:131" -``` - -#### `deeplabcut\modelzoo\utils.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 226 | 5 | Local variable `available_projects` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\utils.py:226" -``` - -#### `deeplabcut\pose_estimation_3d\camera_calibration.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 414 | 13 | Local variable `norm` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_3d\camera_calibration.py:414" -``` - -#### `deeplabcut\pose_estimation_3d\plotting3D.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 133 | 5 | Local variable `start_path` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_3d\plotting3D.py:133" -``` - -#### `deeplabcut\pose_estimation_pytorch\apis\videos.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 517 | 5 | Local variable `detector_path` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\apis\videos.py:517" -``` - -#### `deeplabcut\pose_estimation_pytorch\models\necks\transformer.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 167 | 13 | Local variable `length` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\models\necks\transformer.py:167" -``` - -#### `deeplabcut\pose_estimation_pytorch\modelzoo\memory_replay.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 192 | 9 | Local variable `arranged_preds_list` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\modelzoo\memory_replay.py:192" -``` - -#### `deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 53 | 14 | Local variable `ratio_w` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py:53" -``` - -#### `deeplabcut\pose_estimation_tensorflow\nnets\multi.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 284 | 25 | Local variable `pre_stage_paf_output` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\nnets\multi.py:284" -``` - -#### `deeplabcut\pose_estimation_tensorflow\predict_multianimal.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 84 | 9 | Local variable `start` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\predict_multianimal.py:84" -``` - -#### `deeplabcut\pose_estimation_tensorflow\training.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 198 | 13 | Local variable `supermodels` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\training.py:198" -``` - -#### `deeplabcut\pose_tracking_pytorch\model\backbones\vit_pytorch.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 275 | 9 | Local variable `B` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_tracking_pytorch\model\backbones\vit_pytorch.py:275" -``` - -#### `deeplabcut\pose_tracking_pytorch\processor\processor.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 271 | 5 | Local variable `val_loss` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_tracking_pytorch\processor\processor.py:271" -``` - -#### `deeplabcut\pose_tracking_pytorch\train_dlctransreid.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 49 | 5 | Local variable `x_list` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_tracking_pytorch\train_dlctransreid.py:49" -``` - -#### `deeplabcut\refine_training_dataset\outlier_frames.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 718 | 5 | Local variable `videofolder` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\refine_training_dataset\outlier_frames.py:718" -``` - -#### `deeplabcut\utils\auxfun_videos.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 616 | 5 | Local variable `rs` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\auxfun_videos.py:616" -``` - -#### `examples\testscript_mobilenets.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 75 | 5 | Local variable `DLC_config` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "examples\testscript_mobilenets.py:75" -``` - -#### `tests\pose_estimation_pytorch\apis\test_apis_evaluate.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 228 | 5 | Local variable `num_unique` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "tests\pose_estimation_pytorch\apis\test_apis_evaluate.py:228" -``` - -#### `tests\pose_estimation_pytorch\config\test_make_pose_config.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 338 | 43 | Local variable `err_info` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "tests\pose_estimation_pytorch\config\test_make_pose_config.py:338" -``` - -#### `tests\pose_estimation_pytorch\data\test_transforms.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 99 | 5 | Local variable `aug` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "tests\pose_estimation_pytorch\data\test_transforms.py:99" -``` - -#### `tests\pose_estimation_pytorch\modelzoo\test_load_superanimal_models.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 31 | 13 | Local variable `snapshot` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "tests\pose_estimation_pytorch\modelzoo\test_load_superanimal_models.py:31" -``` - -#### `tests\pose_estimation_pytorch\other\test_api_utils.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 70 | 13 | Local variable `transformed` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "tests\pose_estimation_pytorch\other\test_api_utils.py:70" -``` - -#### `tests\pose_estimation_pytorch\runners\test_runners_inference.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 32 | 9 | Local variable `runner` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "tests\pose_estimation_pytorch\runners\test_runners_inference.py:32" -``` - -#### `tests\test_auxiliaryfunctions.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 22 | 5 | Local variable `n_ext` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "tests\test_auxiliaryfunctions.py:22" -``` - -#### `tests\test_pose_multianimal_imgaug.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 109 | 9 | Local variable `batch` is assigned to but never used | - -Quick open commands: - -```powershell -code -g "tests\test_pose_multianimal_imgaug.py:109" -``` - -## E402 - -Count: **93** -Hint: Module import not at top of file. Move imports above executable code if possible. - -### Files affected - -| File | Count | -|---|---:| -| `docs\recipes\flip_and_rotate.ipynb` | 36 | -| `deeplabcut\pose_estimation_tensorflow\__init__.py` | 13 | -| `deeplabcut\__init__.py` | 12 | -| `deeplabcut\benchmark\metrics.py` | 8 | -| `deeplabcut\pose_estimation_tensorflow\core\train.py` | 6 | -| `testscript_cli.py` | 6 | -| `deeplabcut\pose_estimation_3d\plotting3D.py` | 5 | -| `examples\testscript_deterministicwithResNet152.py` | 4 | -| `examples\COLAB\COLAB_DEMO_mouse_openfield.ipynb` | 2 | -| `tests\pose_estimation_pytorch\modelzoo\test_fmpose_integration.py` | 1 | - -### Details - -#### `docs\recipes\flip_and_rotate.ipynb` (36) - -| Line | Col | Message | -|---:|---:|---| -| 5 | 1 | Module level import not at top of cell | -| 7 | 1 | Module level import not at top of cell | -| 8 | 1 | Module level import not at top of cell | -| 8 | 1 | Module level import not at top of cell | -| 8 | 1 | Module level import not at top of cell | -| 8 | 1 | Module level import not at top of cell | -| 9 | 1 | Module level import not at top of cell | -| 9 | 1 | Module level import not at top of cell | -| 10 | 1 | Module level import not at top of cell | -| 11 | 1 | Module level import not at top of cell | -| 11 | 1 | Module level import not at top of cell | -| 11 | 1 | Module level import not at top of cell | -| 11 | 1 | Module level import not at top of cell | -| 11 | 1 | Module level import not at top of cell | -| 12 | 1 | Module level import not at top of cell | -| 12 | 1 | Module level import not at top of cell | -| 13 | 1 | Module level import not at top of cell | -| 13 | 1 | Module level import not at top of cell | -| 14 | 1 | Module level import not at top of cell | -| 14 | 1 | Module level import not at top of cell | -| 14 | 1 | Module level import not at top of cell | -| 14 | 1 | Module level import not at top of cell | -| 14 | 1 | Module level import not at top of cell | -| 14 | 1 | Module level import not at top of cell | -| 16 | 1 | Module level import not at top of cell | -| 16 | 1 | Module level import not at top of cell | -| 16 | 1 | Module level import not at top of cell | -| 17 | 1 | Module level import not at top of cell | -| 17 | 1 | Module level import not at top of cell | -| 17 | 1 | Module level import not at top of cell | -| 17 | 1 | Module level import not at top of cell | -| 18 | 1 | Module level import not at top of cell | -| 18 | 1 | Module level import not at top of cell | -| 19 | 1 | Module level import not at top of cell | -| 20 | 1 | Module level import not at top of cell | -| 55 | 1 | Module level import not at top of cell | - -Quick open commands: - -```powershell -code -g "docs\recipes\flip_and_rotate.ipynb:5" -``` - -#### `deeplabcut\pose_estimation_tensorflow\__init__.py` (13) - -| Line | Col | Message | -|---:|---:|---| -| 22 | 1 | Module level import not at top of file | -| 23 | 1 | Module level import not at top of file | -| 24 | 1 | Module level import not at top of file | -| 25 | 1 | Module level import not at top of file | -| 26 | 1 | Module level import not at top of file | -| 27 | 1 | Module level import not at top of file | -| 28 | 1 | Module level import not at top of file | -| 29 | 1 | Module level import not at top of file | -| 30 | 1 | Module level import not at top of file | -| 31 | 1 | Module level import not at top of file | -| 32 | 1 | Module level import not at top of file | -| 33 | 1 | Module level import not at top of file | -| 34 | 1 | Module level import not at top of file | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\__init__.py:22" -``` - -#### `deeplabcut\__init__.py` (12) - -| Line | Col | Message | -|---:|---:|---| -| 16 | 1 | Module level import not at top of file | -| 31 | 1 | Module level import not at top of file | -| 32 | 1 | Module level import not at top of file | -| 40 | 1 | Module level import not at top of file | -| 55 | 1 | Module level import not at top of file | -| 56 | 1 | Module level import not at top of file | -| 81 | 1 | Module level import not at top of file | -| 98 | 1 | Module level import not at top of file | -| 104 | 1 | Module level import not at top of file | -| 105 | 1 | Module level import not at top of file | -| 110 | 1 | Module level import not at top of file | -| 111 | 1 | Module level import not at top of file | - -Quick open commands: - -```powershell -code -g "deeplabcut\__init__.py:16" -``` - -#### `deeplabcut\benchmark\metrics.py` (8) - -| Line | Col | Message | -|---:|---:|---| -| 23 | 1 | Module level import not at top of file | -| 24 | 1 | Module level import not at top of file | -| 25 | 1 | Module level import not at top of file | -| 27 | 1 | Module level import not at top of file | -| 28 | 1 | Module level import not at top of file | -| 30 | 1 | Module level import not at top of file | -| 31 | 1 | Module level import not at top of file | -| 32 | 1 | Module level import not at top of file | - -Quick open commands: - -```powershell -code -g "deeplabcut\benchmark\metrics.py:23" -``` - -#### `deeplabcut\pose_estimation_tensorflow\core\train.py` (6) - -| Line | Col | Message | -|---:|---:|---| -| 25 | 1 | Module level import not at top of file | -| 27 | 1 | Module level import not at top of file | -| 28 | 1 | Module level import not at top of file | -| 32 | 1 | Module level import not at top of file | -| 33 | 1 | Module level import not at top of file | -| 34 | 1 | Module level import not at top of file | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\core\train.py:25" -``` - -#### `testscript_cli.py` (6) - -| Line | Col | Message | -|---:|---:|---| -| 14 | 1 | Module level import not at top of file | -| 15 | 1 | Module level import not at top of file | -| 17 | 1 | Module level import not at top of file | -| 18 | 1 | Module level import not at top of file | -| 23 | 1 | Module level import not at top of file | -| 24 | 1 | Module level import not at top of file | - -Quick open commands: - -```powershell -code -g "testscript_cli.py:14" -``` - -#### `deeplabcut\pose_estimation_3d\plotting3D.py` (5) - -| Line | Col | Message | -|---:|---:|---| -| 29 | 1 | Module level import not at top of file | -| 30 | 1 | Module level import not at top of file | -| 31 | 1 | Module level import not at top of file | -| 32 | 1 | Module level import not at top of file | -| 33 | 1 | Module level import not at top of file | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_3d\plotting3D.py:29" -``` - -#### `examples\testscript_deterministicwithResNet152.py` (4) - -| Line | Col | Message | -|---:|---:|---| -| 44 | 1 | Module level import not at top of file | -| 46 | 1 | Module level import not at top of file | -| 47 | 1 | Module level import not at top of file | -| 49 | 1 | Module level import not at top of file | - -Quick open commands: - -```powershell -code -g "examples\testscript_deterministicwithResNet152.py:44" -``` - -#### `examples\COLAB\COLAB_DEMO_mouse_openfield.ipynb` (2) - -| Line | Col | Message | -|---:|---:|---| -| 10 | 1 | Module level import not at top of cell | -| 11 | 1 | Module level import not at top of cell | - -Quick open commands: - -```powershell -code -g "examples\COLAB\COLAB_DEMO_mouse_openfield.ipynb:10" -``` - -#### `tests\pose_estimation_pytorch\modelzoo\test_fmpose_integration.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 19 | 1 | Module level import not at top of file | - -Quick open commands: - -```powershell -code -g "tests\pose_estimation_pytorch\modelzoo\test_fmpose_integration.py:19" -``` - -## UP031 - -Count: **76** -Hint: Old `%` formatting. Convert to f-strings or `.format()` where appropriate. - -### Files affected - -| File | Count | -|---|---:| -| `deeplabcut\pose_estimation_tensorflow\backbones\efficientnet_model.py` | 15 | -| `deeplabcut\pose_estimation_tensorflow\backbones\efficientnet_builder.py` | 13 | -| `deeplabcut\pose_estimation_tensorflow\predict_videos.py` | 11 | -| `deeplabcut\pose_estimation_3d\camera_calibration.py` | 7 | -| `deeplabcut\pose_estimation_tensorflow\export.py` | 4 | -| `deeplabcut\pose_estimation_tensorflow\backbones\mobilenet.py` | 3 | -| `deeplabcut\pose_estimation_tensorflow\nnets\utils.py` | 3 | -| `deeplabcut\create_project\new.py` | 2 | -| `deeplabcut\pose_estimation_tensorflow\nnets\conv_blocks.py` | 2 | -| `deeplabcut\create_project\add.py` | 1 | -| `deeplabcut\create_project\new_3d.py` | 1 | -| `deeplabcut\generate_training_dataset\frame_extraction.py` | 1 | -| `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` | 1 | -| `deeplabcut\pose_estimation_3d\plotting3D.py` | 1 | -| `deeplabcut\pose_estimation_3d\triangulation.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\core\evaluate.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\predict_multianimal.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\visualizemaps.py` | 1 | -| `deeplabcut\post_processing\analyze_skeleton.py` | 1 | -| `deeplabcut\post_processing\filtering.py` | 1 | -| `deeplabcut\refine_training_dataset\outlier_frames.py` | 1 | -| `examples\COLAB\COLAB_DEMO_SuperAnimal.ipynb` | 1 | - -### Details - -#### `deeplabcut\pose_estimation_tensorflow\backbones\efficientnet_model.py` (15) - -| Line | Col | Message | -|---:|---:|---| -| 256 | 35 | Use format specifiers instead of percent format | -| 268 | 35 | Use format specifiers instead of percent format | -| 273 | 35 | Use format specifiers instead of percent format | -| 276 | 35 | Use format specifiers instead of percent format | -| 294 | 35 | Use format specifiers instead of percent format | -| 345 | 35 | Use format specifiers instead of percent format | -| 350 | 35 | Use format specifiers instead of percent format | -| 364 | 35 | Use format specifiers instead of percent format | -| 478 | 35 | Use format specifiers instead of percent format | -| 489 | 46 | Use format specifiers instead of percent format | -| 493 | 47 | Use format specifiers instead of percent format | -| 500 | 32 | Use format specifiers instead of percent format | -| 502 | 36 | Use format specifiers instead of percent format | -| 505 | 40 | Use format specifiers instead of percent format | -| 507 | 44 | Use format specifiers instead of percent format | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\backbones\efficientnet_model.py:256" -``` - -#### `deeplabcut\pose_estimation_tensorflow\backbones\efficientnet_builder.py` (13) - -| Line | Col | Message | -|---:|---:|---| -| 77 | 13 | Use format specifiers instead of percent format | -| 78 | 13 | Use format specifiers instead of percent format | -| 79 | 13 | Use format specifiers instead of percent format | -| 80 | 13 | Use format specifiers instead of percent format | -| 81 | 13 | Use format specifiers instead of percent format | -| 82 | 13 | Use format specifiers instead of percent format | -| 83 | 13 | Use format specifiers instead of percent format | -| 86 | 25 | Use format specifiers instead of percent format | -| 190 | 35 | Use format specifiers instead of percent format | -| 242 | 43 | Use format specifiers instead of percent format | -| 243 | 25 | Use format specifiers instead of percent format | -| 244 | 25 | Use format specifiers instead of percent format | -| 245 | 25 | Use format specifiers instead of percent format | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\backbones\efficientnet_builder.py:77" -``` - -#### `deeplabcut\pose_estimation_tensorflow\predict_videos.py` (11) - -| Line | Col | Message | -|---:|---:|---| -| 106 | 13 | Use format specifiers instead of percent format | -| 121 | 11 | Use format specifiers instead of percent format | -| 489 | 13 | Use format specifiers instead of percent format | -| 505 | 11 | Use format specifiers instead of percent format | -| 663 | 9 | Use format specifiers instead of percent format | -| 1077 | 13 | Use format specifiers instead of percent format | -| 1210 | 13 | Use format specifiers instead of percent format | -| 1225 | 11 | Use format specifiers instead of percent format | -| 1317 | 23 | Use format specifiers instead of percent format | -| 1548 | 13 | Use format specifiers instead of percent format | -| 1579 | 11 | Use format specifiers instead of percent format | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\predict_videos.py:106" -``` - -#### `deeplabcut\pose_estimation_3d\camera_calibration.py` (7) - -| Line | Col | Message | -|---:|---:|---| -| 145 | 27 | Use format specifiers instead of percent format | -| 185 | 17 | Use format specifiers instead of percent format | -| 195 | 19 | Use format specifiers instead of percent format | -| 200 | 19 | Use format specifiers instead of percent format | -| 256 | 13 | Use format specifiers instead of percent format | -| 264 | 13 | Use format specifiers instead of percent format | -| 400 | 15 | Use format specifiers instead of percent format | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_3d\camera_calibration.py:145" -``` - -#### `deeplabcut\pose_estimation_tensorflow\export.py` (4) - -| Line | Col | Message | -|---:|---:|---| -| 126 | 13 | Use format specifiers instead of percent format | -| 270 | 27 | Use format specifiers instead of percent format | -| 289 | 20 | Use format specifiers instead of percent format | -| 299 | 35 | Use format specifiers instead of percent format | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\export.py:126" -``` - -#### `deeplabcut\pose_estimation_tensorflow\backbones\mobilenet.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 242 | 25 | Use format specifiers instead of percent format | -| 246 | 23 | Use format specifiers instead of percent format | -| 325 | 26 | Use format specifiers instead of percent format | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\backbones\mobilenet.py:242" -``` - -#### `deeplabcut\pose_estimation_tensorflow\nnets\utils.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 115 | 23 | Use format specifiers instead of percent format | -| 118 | 35 | Use format specifiers instead of percent format | -| 162 | 21 | Use format specifiers instead of percent format | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\nnets\utils.py:115" -``` - -#### `deeplabcut\create_project\new.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 202 | 43 | Use format specifiers instead of percent format | -| 306 | 9 | Use format specifiers instead of percent format | - -Quick open commands: - -```powershell -code -g "deeplabcut\create_project\new.py:202" -``` - -#### `deeplabcut\pose_estimation_tensorflow\nnets\conv_blocks.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 249 | 29 | Use format specifiers instead of percent format | -| 369 | 24 | Use format specifiers instead of percent format | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\nnets\conv_blocks.py:249" -``` - -#### `deeplabcut\create_project\add.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 100 | 43 | Use format specifiers instead of percent format | - -Quick open commands: - -```powershell -code -g "deeplabcut\create_project\add.py:100" -``` - -#### `deeplabcut\create_project\new_3d.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 126 | 9 | Use format specifiers instead of percent format | - -Quick open commands: - -```powershell -code -g "deeplabcut\create_project\new_3d.py:126" -``` - -#### `deeplabcut\generate_training_dataset\frame_extraction.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 363 | 23 | Use format specifiers instead of percent format | - -Quick open commands: - -```powershell -code -g "deeplabcut\generate_training_dataset\frame_extraction.py:363" -``` - -#### `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 332 | 11 | Use format specifiers instead of percent format | - -Quick open commands: - -```powershell -code -g "deeplabcut\generate_training_dataset\trainingsetmanipulation.py:332" -``` - -#### `deeplabcut\pose_estimation_3d\plotting3D.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 179 | 17 | Use format specifiers instead of percent format | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_3d\plotting3D.py:179" -``` - -#### `deeplabcut\pose_estimation_3d\triangulation.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 132 | 23 | Use format specifiers instead of percent format | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_3d\triangulation.py:132" -``` - -#### `deeplabcut\pose_estimation_tensorflow\core\evaluate.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 272 | 13 | Use format specifiers instead of percent format | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\core\evaluate.py:272" -``` - -#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 77 | 15 | Use format specifiers instead of percent format | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py:77" -``` - -#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 61 | 15 | Use format specifiers instead of percent format | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py:61" -``` - -#### `deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 387 | 19 | Use format specifiers instead of percent format | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py:387" -``` - -#### `deeplabcut\pose_estimation_tensorflow\predict_multianimal.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 215 | 15 | Use format specifiers instead of percent format | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\predict_multianimal.py:215" -``` - -#### `deeplabcut\pose_estimation_tensorflow\visualizemaps.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 142 | 17 | Use format specifiers instead of percent format | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\visualizemaps.py:142" -``` - -#### `deeplabcut\post_processing\analyze_skeleton.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 263 | 15 | Use format specifiers instead of percent format | - -Quick open commands: - -```powershell -code -g "deeplabcut\post_processing\analyze_skeleton.py:263" -``` - -#### `deeplabcut\post_processing\filtering.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 230 | 15 | Use format specifiers instead of percent format | - -Quick open commands: - -```powershell -code -g "deeplabcut\post_processing\filtering.py:230" -``` - -#### `deeplabcut\refine_training_dataset\outlier_frames.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 931 | 15 | Use format specifiers instead of percent format | - -Quick open commands: - -```powershell -code -g "deeplabcut\refine_training_dataset\outlier_frames.py:931" -``` - -#### `examples\COLAB\COLAB_DEMO_SuperAnimal.ipynb` (1) - -| Line | Col | Message | -|---:|---:|---| -| 23 | 5 | Use format specifiers instead of percent format | - -Quick open commands: - -```powershell -code -g "examples\COLAB\COLAB_DEMO_SuperAnimal.ipynb:23" -``` - -## B007 - -Count: **51** -Hint: Unused loop variable. Rename to `_` or use it. - -### Files affected - -| File | Count | -|---|---:| -| `deeplabcut\pose_tracking_pytorch\processor\processor.py` | 4 | -| `deeplabcut\core\trackingutils.py` | 3 | -| `deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py` | 3 | -| `deeplabcut\core\crossvalutils.py` | 2 | -| `deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py` | 2 | -| `deeplabcut\pose_estimation_pytorch\apis\visualization.py` | 2 | -| `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` | 2 | -| `deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py` | 2 | -| `deeplabcut\utils\frameselectiontools.py` | 2 | -| `tests\pose_estimation_pytorch\runners\test_dynamic_cropper.py` | 2 | -| `tests\pose_estimation_pytorch\runners\test_schedulers.py` | 2 | -| `deeplabcut\benchmark\utils.py` | 1 | -| `deeplabcut\core\inferenceutils.py` | 1 | -| `deeplabcut\core\metrics\matching.py` | 1 | -| `deeplabcut\gui\tabs\create_project.py` | 1 | -| `deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc.py` | 1 | -| `deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc_dataframe.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\data\base.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\data\utils.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\models\modules\conv_module.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\models\modules\gated_attention_unit.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\models\necks\transformer.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\runners\dynamic_cropping.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\core\evaluate.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\core\evaluate_multianimal.py` | 1 | -| `deeplabcut\refine_training_dataset\outlier_frames.py` | 1 | -| `deeplabcut\utils\visualization.py` | 1 | -| `examples\COLAB\COLAB_BUCTD_and_CTD_tracking.ipynb` | 1 | -| `tests\core\inferenceutils\test_map_computation.py` | 1 | -| `tests\core\metrics\test_metrics_map_computation.py` | 1 | -| `tests\create_project\test_video_set_configuration.py` | 1 | -| `tests\generate_training_dataset\test_trainset_metadata.py` | 1 | -| `tests\pose_estimation_pytorch\data\test_data_ctd.py` | 1 | -| `tests\pose_estimation_pytorch\other\test_api_utils.py` | 1 | -| `tests\test_auxfun_models.py` | 1 | -| `tests\test_auxiliaryfunctions.py` | 1 | - -### Details - -#### `deeplabcut\pose_tracking_pytorch\processor\processor.py` (4) - -| Line | Col | Message | -|---:|---:|---| -| 163 | 17 | Loop control variable `n_iter` not used within loop body | -| 218 | 9 | Loop control variable `n_iter` not used within loop body | -| 230 | 17 | Loop control variable `i` not used within loop body | -| 275 | 9 | Loop control variable `n_iter` not used within loop body | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_tracking_pytorch\processor\processor.py:163" -``` - -#### `deeplabcut\core\trackingutils.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 567 | 13 | Loop control variable `i` not used within loop body | -| 696 | 16 | Loop control variable `det` not used within loop body | -| 700 | 16 | Loop control variable `trk` not used within loop body | - -Quick open commands: - -```powershell -code -g "deeplabcut\core\trackingutils.py:567" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 100 | 13 | Loop control variable `dataset_name` not used within loop body | -| 172 | 13 | Loop control variable `k` not used within loop body | -| 191 | 13 | Loop control variable `dataset_name` not used within loop body | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py:100" -``` - -#### `deeplabcut\core\crossvalutils.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 229 | 9 | Loop control variable `i` not used within loop body | -| 281 | 16 | Loop control variable `imname` not used within loop body | - -Quick open commands: - -```powershell -code -g "deeplabcut\core\crossvalutils.py:229" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 503 | 13 | Loop control variable `idx` not used within loop body | -| 639 | 21 | Loop control variable `kpt_name` not used within loop body | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py:503" -``` - -#### `deeplabcut\pose_estimation_pytorch\apis\visualization.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 149 | 17 | Loop control variable `idx` not used within loop body | -| 465 | 17 | Loop control variable `image_idx` not used within loop body | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\apis\visualization.py:149" -``` - -#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 135 | 13 | Loop control variable `image_id` not used within loop body | -| 546 | 17 | Loop control variable `k` not used within loop body | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py:135" -``` - -#### `deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 163 | 21 | Loop control variable `scale_id` not used within loop body | -| 191 | 21 | Loop control variable `scale_id` not used within loop body | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py:163" -``` - -#### `deeplabcut\utils\frameselectiontools.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 291 | 30 | Loop control variable `index` not used within loop body | -| 305 | 30 | Loop control variable `index` not used within loop body | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\frameselectiontools.py:291" -``` - -#### `tests\pose_estimation_pytorch\runners\test_dynamic_cropper.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 166 | 10 | Loop control variable `start_1` not used within loop body | -| 166 | 37 | Loop control variable `end_2` not used within loop body | - -Quick open commands: - -```powershell -code -g "tests\pose_estimation_pytorch\runners\test_dynamic_cropper.py:166" -``` - -#### `tests\pose_estimation_pytorch\runners\test_schedulers.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 34 | 9 | Loop control variable `i` not used within loop body | -| 252 | 9 | Loop control variable `epoch` not used within loop body | - -Quick open commands: - -```powershell -code -g "tests\pose_estimation_pytorch\runners\test_schedulers.py:34" -``` - -#### `deeplabcut\benchmark\utils.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 66 | 9 | Loop control variable `loader` not used within loop body | - -Quick open commands: - -```powershell -code -g "deeplabcut\benchmark\utils.py:66" -``` - -#### `deeplabcut\core\inferenceutils.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 466 | 30 | Loop control variable `l` not used within loop body | - -Quick open commands: - -```powershell -code -g "deeplabcut\core\inferenceutils.py:466" -``` - -#### `deeplabcut\core\metrics\matching.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 102 | 14 | Loop control variable `pred` not used within loop body | - -Quick open commands: - -```powershell -code -g "deeplabcut\core\metrics\matching.py:102" -``` - -#### `deeplabcut\gui\tabs\create_project.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 134 | 17 | Loop control variable `entry` not used within loop body | - -Quick open commands: - -```powershell -code -g "deeplabcut\gui\tabs\create_project.py:134" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 76 | 17 | Loop control variable `individual_id` not used within loop body | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc.py:76" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc_dataframe.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 184 | 17 | Loop control variable `individual_id` not used within loop body | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc_dataframe.py:184" -``` - -#### `deeplabcut\pose_estimation_pytorch\data\base.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 337 | 17 | Loop control variable `i` not used within loop body | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\data\base.py:337" -``` - -#### `deeplabcut\pose_estimation_pytorch\data\utils.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 305 | 9 | Loop control variable `i` not used within loop body | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\data\utils.py:305" -``` - -#### `deeplabcut\pose_estimation_pytorch\models\modules\conv_module.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 115 | 13 | Loop control variable `i` not used within loop body | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\models\modules\conv_module.py:115" -``` - -#### `deeplabcut\pose_estimation_pytorch\models\modules\gated_attention_unit.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 42 | 9 | Loop control variable `i` not used within loop body | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\models\modules\gated_attention_unit.py:42" -``` - -#### `deeplabcut\pose_estimation_pytorch\models\necks\transformer.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 201 | 13 | Loop control variable `i` not used within loop body | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\models\necks\transformer.py:201" -``` - -#### `deeplabcut\pose_estimation_pytorch\runners\dynamic_cropping.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 528 | 13 | Loop control variable `i` not used within loop body | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\runners\dynamic_cropping.py:528" -``` - -#### `deeplabcut\pose_estimation_tensorflow\core\evaluate.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 109 | 13 | Loop control variable `pi` not used within loop body | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\core\evaluate.py:109" -``` - -#### `deeplabcut\pose_estimation_tensorflow\core\evaluate_multianimal.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 59 | 9 | Loop control variable `n` not used within loop body | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\core\evaluate_multianimal.py:59" -``` - -#### `deeplabcut\refine_training_dataset\outlier_frames.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 1096 | 9 | Loop control variable `findex` not used within loop body | - -Quick open commands: - -```powershell -code -g "deeplabcut\refine_training_dataset\outlier_frames.py:1096" -``` - -#### `deeplabcut\utils\visualization.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 76 | 9 | Loop control variable `scorerindex` not used within loop body | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\visualization.py:76" -``` - -#### `examples\COLAB\COLAB_BUCTD_and_CTD_tracking.ipynb` (1) - -| Line | Col | Message | -|---:|---:|---| -| 3 | 9 | Loop control variable `i` not used within loop body | - -Quick open commands: - -```powershell -code -g "examples\COLAB\COLAB_BUCTD_and_CTD_tracking.ipynb:3" -``` - -#### `tests\core\inferenceutils\test_map_computation.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 318 | 13 | Loop control variable `idv_id` not used within loop body | - -Quick open commands: - -```powershell -code -g "tests\core\inferenceutils\test_map_computation.py:318" -``` - -#### `tests\core\metrics\test_metrics_map_computation.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 294 | 13 | Loop control variable `idv_id` not used within loop body | - -Quick open commands: - -```powershell -code -g "tests\core\metrics\test_metrics_map_computation.py:294" -``` - -#### `tests\create_project\test_video_set_configuration.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 255 | 9 | Loop control variable `video_path` not used within loop body | - -Quick open commands: - -```powershell -code -g "tests\create_project\test_video_set_configuration.py:255" -``` - -#### `tests\generate_training_dataset\test_trainset_metadata.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 81 | 9 | Loop control variable `name` not used within loop body | - -Quick open commands: - -```powershell -code -g "tests\generate_training_dataset\test_trainset_metadata.py:81" -``` - -#### `tests\pose_estimation_pytorch\data\test_data_ctd.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 173 | 25 | Loop control variable `img_index` not used within loop body | - -Quick open commands: - -```powershell -code -g "tests\pose_estimation_pytorch\data\test_data_ctd.py:173" -``` - -#### `tests\pose_estimation_pytorch\other\test_api_utils.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 60 | 9 | Loop control variable `i` not used within loop body | - -Quick open commands: - -```powershell -code -g "tests\pose_estimation_pytorch\other\test_api_utils.py:60" -``` - -#### `tests\test_auxfun_models.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 25 | 32 | Loop control variable `expected_path` not used within loop body | - -Quick open commands: - -```powershell -code -g "tests\test_auxfun_models.py:25" -``` - -#### `tests\test_auxiliaryfunctions.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 39 | 14 | Loop control variable `ext` not used within loop body | - -Quick open commands: - -```powershell -code -g "tests\test_auxiliaryfunctions.py:39" -``` - -## B028 - -Count: **49** - -### Files affected - -| File | Count | -|---|---:| -| `deeplabcut\utils\auxfun_videos.py` | 5 | -| `deeplabcut\core\inferenceutils.py` | 4 | -| `deeplabcut\pose_estimation_pytorch\data\cocoloader.py` | 4 | -| `deeplabcut\pose_estimation_tensorflow\predict_videos.py` | 3 | -| `deeplabcut\create_project\new.py` | 2 | -| `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` | 2 | -| `deeplabcut\gui\widgets.py` | 2 | -| `deeplabcut\modelzoo\generalized_data_converter\conversion_table\conversion_table.py` | 2 | -| `deeplabcut\modelzoo\utils.py` | 2 | -| `deeplabcut\pose_estimation_pytorch\apis\tracklets.py` | 2 | -| `deeplabcut\pose_estimation_pytorch\data\transforms.py` | 2 | -| `deeplabcut\refine_training_dataset\stitch.py` | 2 | -| `deeplabcut\utils\skeleton.py` | 2 | -| `deeplabcut\__init__.py` | 1 | -| `deeplabcut\benchmark\base.py` | 1 | -| `deeplabcut\core\weight_init.py` | 1 | -| `deeplabcut\generate_training_dataset\multiple_individuals_trainingsetmanipulation.py` | 1 | -| `deeplabcut\modelzoo\generalized_data_converter\datasets\base.py` | 1 | -| `deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py` | 1 | -| `deeplabcut\pose_estimation_3d\triangulation.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\modelzoo\utils.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\runners\inference.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\runners\snapshots.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\core\train.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\datasets\factory.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\nnets\factory.py` | 1 | -| `deeplabcut\utils\auxfun_multianimal.py` | 1 | -| `deeplabcut\utils\auxiliaryfunctions.py` | 1 | - -### Details - -#### `deeplabcut\utils\auxfun_videos.py` (5) - -| Line | Col | Message | -|---:|---:|---| -| 61 | 13 | No explicit `stacklevel` keyword argument found | -| 69 | 17 | No explicit `stacklevel` keyword argument found | -| 113 | 13 | No explicit `stacklevel` keyword argument found | -| 158 | 13 | No explicit `stacklevel` keyword argument found | -| 189 | 13 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\auxfun_videos.py:61" -``` - -#### `deeplabcut\core\inferenceutils.py` (4) - -| Line | Col | Message | -|---:|---:|---| -| 263 | 13 | No explicit `stacklevel` keyword argument found | -| 343 | 13 | No explicit `stacklevel` keyword argument found | -| 351 | 13 | No explicit `stacklevel` keyword argument found | -| 367 | 13 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\core\inferenceutils.py:263" -``` - -#### `deeplabcut\pose_estimation_pytorch\data\cocoloader.py` (4) - -| Line | Col | Message | -|---:|---:|---| -| 145 | 17 | No explicit `stacklevel` keyword argument found | -| 152 | 13 | No explicit `stacklevel` keyword argument found | -| 203 | 13 | No explicit `stacklevel` keyword argument found | -| 223 | 13 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\data\cocoloader.py:145" -``` - -#### `deeplabcut\pose_estimation_tensorflow\predict_videos.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 824 | 13 | No explicit `stacklevel` keyword argument found | -| 1524 | 9 | No explicit `stacklevel` keyword argument found | -| 1561 | 9 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\predict_videos.py:824" -``` - -#### `deeplabcut\create_project\new.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 226 | 13 | No explicit `stacklevel` keyword argument found | -| 232 | 9 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\create_project\new.py:226" -``` - -#### `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 944 | 9 | No explicit `stacklevel` keyword argument found | -| 1491 | 9 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\generate_training_dataset\trainingsetmanipulation.py:944" -``` - -#### `deeplabcut\gui\widgets.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 559 | 13 | No explicit `stacklevel` keyword argument found | -| 643 | 13 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\gui\widgets.py:559" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\conversion_table\conversion_table.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 24 | 13 | No explicit `stacklevel` keyword argument found | -| 122 | 13 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\conversion_table\conversion_table.py:24" -``` - -#### `deeplabcut\modelzoo\utils.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 200 | 9 | No explicit `stacklevel` keyword argument found | -| 207 | 9 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\utils.py:200" -``` - -#### `deeplabcut\pose_estimation_pytorch\apis\tracklets.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 60 | 9 | No explicit `stacklevel` keyword argument found | -| 100 | 9 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\apis\tracklets.py:60" -``` - -#### `deeplabcut\pose_estimation_pytorch\data\transforms.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 52 | 13 | No explicit `stacklevel` keyword argument found | -| 422 | 13 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\data\transforms.py:52" -``` - -#### `deeplabcut\refine_training_dataset\stitch.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 671 | 13 | No explicit `stacklevel` keyword argument found | -| 726 | 17 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\refine_training_dataset\stitch.py:671" -``` - -#### `deeplabcut\utils\skeleton.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 73 | 13 | No explicit `stacklevel` keyword argument found | -| 151 | 13 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\skeleton.py:73" -``` - -#### `deeplabcut\__init__.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 73 | 5 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\__init__.py:73" -``` - -#### `deeplabcut\benchmark\base.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 120 | 13 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\benchmark\base.py:120" -``` - -#### `deeplabcut\core\weight_init.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 196 | 9 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\core\weight_init.py:196" -``` - -#### `deeplabcut\generate_training_dataset\multiple_individuals_trainingsetmanipulation.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 275 | 9 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\generate_training_dataset\multiple_individuals_trainingsetmanipulation.py:275" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\datasets\base.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 193 | 13 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\base.py:193" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 121 | 13 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py:121" -``` - -#### `deeplabcut\pose_estimation_3d\triangulation.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 297 | 17 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_3d\triangulation.py:297" -``` - -#### `deeplabcut\pose_estimation_pytorch\modelzoo\utils.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 178 | 9 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\modelzoo\utils.py:178" -``` - -#### `deeplabcut\pose_estimation_pytorch\runners\inference.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 242 | 17 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\runners\inference.py:242" -``` - -#### `deeplabcut\pose_estimation_pytorch\runners\snapshots.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 137 | 13 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\runners\snapshots.py:137" -``` - -#### `deeplabcut\pose_estimation_tensorflow\core\train.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 208 | 9 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\core\train.py:208" -``` - -#### `deeplabcut\pose_estimation_tensorflow\datasets\factory.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 26 | 17 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\datasets\factory.py:26" -``` - -#### `deeplabcut\pose_estimation_tensorflow\nnets\factory.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 21 | 17 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\nnets\factory.py:21" -``` - -#### `deeplabcut\utils\auxfun_multianimal.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 81 | 17 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\auxfun_multianimal.py:81" -``` - -#### `deeplabcut\utils\auxiliaryfunctions.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 292 | 9 | No explicit `stacklevel` keyword argument found | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\auxiliaryfunctions.py:292" -``` - -## F403 - -Count: **36** -Hint: `from x import *` makes names unclear. Replace with explicit imports. - -### Files affected - -| File | Count | -|---|---:| -| `deeplabcut\pose_estimation_tensorflow\__init__.py` | 12 | -| `deeplabcut\utils\__init__.py` | 8 | -| `deeplabcut\generate_training_dataset\__init__.py` | 3 | -| `deeplabcut\pose_estimation_3d\__init__.py` | 3 | -| `deeplabcut\pose_tracking_pytorch\__init__.py` | 2 | -| `deeplabcut\refine_training_dataset\__init__.py` | 2 | -| `deeplabcut\gui\window.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\lib\crossvalutils.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\lib\inferenceutils.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\lib\trackingutils.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\util\__init__.py` | 1 | -| `deeplabcut\post_processing\__init__.py` | 1 | - -### Details - -#### `deeplabcut\pose_estimation_tensorflow\__init__.py` (12) - -| Line | Col | Message | -|---:|---:|---| -| 22 | 1 | `from deeplabcut.pose_estimation_tensorflow.config import *` used; unable to detect undefined names | -| 23 | 1 | `from deeplabcut.pose_estimation_tensorflow.core.evaluate import *` used; unable to detect undefined names | -| 24 | 1 | `from deeplabcut.pose_estimation_tensorflow.core.test import *` used; unable to detect undefined names | -| 25 | 1 | `from deeplabcut.pose_estimation_tensorflow.core.train import *` used; unable to detect undefined names | -| 26 | 1 | `from deeplabcut.pose_estimation_tensorflow.datasets import *` used; unable to detect undefined names | -| 27 | 1 | `from deeplabcut.pose_estimation_tensorflow.default_config import *` used; unable to detect undefined names | -| 29 | 1 | `from deeplabcut.pose_estimation_tensorflow.models import *` used; unable to detect undefined names | -| 30 | 1 | `from deeplabcut.pose_estimation_tensorflow.nnets import *` used; unable to detect undefined names | -| 31 | 1 | `from deeplabcut.pose_estimation_tensorflow.predict_videos import *` used; unable to detect undefined names | -| 32 | 1 | `from deeplabcut.pose_estimation_tensorflow.training import *` used; unable to detect undefined names | -| 33 | 1 | `from deeplabcut.pose_estimation_tensorflow.util import *` used; unable to detect undefined names | -| 34 | 1 | `from deeplabcut.pose_estimation_tensorflow.visualizemaps import *` used; unable to detect undefined names | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\__init__.py:22" -``` - -#### `deeplabcut\utils\__init__.py` (8) - -| Line | Col | Message | -|---:|---:|---| -| 11 | 1 | `from deeplabcut.utils.auxfun_multianimal import *` used; unable to detect undefined names | -| 12 | 1 | `from deeplabcut.utils.auxfun_videos import *` used; unable to detect undefined names | -| 13 | 1 | `from deeplabcut.utils.auxiliaryfunctions import *` used; unable to detect undefined names | -| 14 | 1 | `from deeplabcut.utils.conversioncode import *` used; unable to detect undefined names | -| 15 | 1 | `from deeplabcut.utils.frameselectiontools import *` used; unable to detect undefined names | -| 16 | 1 | `from deeplabcut.utils.make_labeled_video import *` used; unable to detect undefined names | -| 17 | 1 | `from deeplabcut.utils.plotting import *` used; unable to detect undefined names | -| 18 | 1 | `from deeplabcut.utils.video_processor import *` used; unable to detect undefined names | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\__init__.py:11" -``` - -#### `deeplabcut\generate_training_dataset\__init__.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 13 | 1 | `from deeplabcut.generate_training_dataset.frame_extraction import *` used; unable to detect undefined names | -| 19 | 1 | `from deeplabcut.generate_training_dataset.multiple_individuals_trainingsetmanipulation import *` used; unable to detect undefined names | -| 20 | 1 | `from deeplabcut.generate_training_dataset.trainingsetmanipulation import *` used; unable to detect undefined names | - -Quick open commands: - -```powershell -code -g "deeplabcut\generate_training_dataset\__init__.py:13" -``` - -#### `deeplabcut\pose_estimation_3d\__init__.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 11 | 1 | `from deeplabcut.pose_estimation_3d.camera_calibration import *` used; unable to detect undefined names | -| 12 | 1 | `from deeplabcut.pose_estimation_3d.plotting3D import *` used; unable to detect undefined names | -| 13 | 1 | `from deeplabcut.pose_estimation_3d.triangulation import *` used; unable to detect undefined names | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_3d\__init__.py:11" -``` - -#### `deeplabcut\pose_tracking_pytorch\__init__.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 13 | 1 | `from .create_dataset import *` used; unable to detect undefined names | -| 14 | 1 | `from .tracking_utils.preprocessing import *` used; unable to detect undefined names | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_tracking_pytorch\__init__.py:13" -``` - -#### `deeplabcut\refine_training_dataset\__init__.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 13 | 1 | `from deeplabcut.refine_training_dataset.outlier_frames import *` used; unable to detect undefined names | -| 14 | 1 | `from deeplabcut.refine_training_dataset.tracklets import *` used; unable to detect undefined names | - -Quick open commands: - -```powershell -code -g "deeplabcut\refine_training_dataset\__init__.py:13" -``` - -#### `deeplabcut\gui\window.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 40 | 1 | `from deeplabcut.gui.tabs import *` used; unable to detect undefined names | - -Quick open commands: - -```powershell -code -g "deeplabcut\gui\window.py:40" -``` - -#### `deeplabcut\pose_estimation_tensorflow\lib\crossvalutils.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 13 | 1 | `from deeplabcut.core.crossvalutils import *` used; unable to detect undefined names | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\lib\crossvalutils.py:13" -``` - -#### `deeplabcut\pose_estimation_tensorflow\lib\inferenceutils.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 13 | 1 | `from deeplabcut.core.inferenceutils import *` used; unable to detect undefined names | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\lib\inferenceutils.py:13" -``` - -#### `deeplabcut\pose_estimation_tensorflow\lib\trackingutils.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 13 | 1 | `from deeplabcut.core.trackingutils import *` used; unable to detect undefined names | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\lib\trackingutils.py:13" -``` - -#### `deeplabcut\pose_estimation_tensorflow\util\__init__.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 19 | 1 | `from deeplabcut.pose_estimation_tensorflow.util.logging import *` used; unable to detect undefined names | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\util\__init__.py:19" -``` - -#### `deeplabcut\post_processing\__init__.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 22 | 1 | `from deeplabcut.post_processing.filtering import *` used; unable to detect undefined names | - -Quick open commands: - -```powershell -code -g "deeplabcut\post_processing\__init__.py:22" -``` - -## E712 - -Count: **22** - -### Files affected - -| File | Count | -|---|---:| -| `deeplabcut\pose_estimation_tensorflow\core\evaluate.py` | 4 | -| `deeplabcut\pose_estimation_3d\camera_calibration.py` | 3 | -| `deeplabcut\pose_estimation_tensorflow\predict_videos.py` | 3 | -| `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` | 2 | -| `deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py` | 2 | -| `deeplabcut\pose_estimation_3d\triangulation.py` | 2 | -| `deeplabcut\utils\auxfun_multianimal.py` | 2 | -| `tests\pose_estimation_pytorch\other\test_helper.py` | 2 | -| `deeplabcut\pose_estimation_tensorflow\predict_multianimal.py` | 1 | -| `deeplabcut\utils\auxiliaryfunctions_3d.py` | 1 | - -### Details - -#### `deeplabcut\pose_estimation_tensorflow\core\evaluate.py` (4) - -| Line | Col | Message | -|---:|---:|---| -| 282 | 8 | Avoid equality comparisons to `True`; use `rescale:` for truth checks | -| 369 | 20 | Avoid equality comparisons to `True`; use `show_errors:` for truth checks | -| 409 | 16 | Avoid equality comparisons to `True`; use `fulldata:` for truth checks | -| 430 | 12 | Avoid equality comparisons to `True`; use `fulldata:` for truth checks | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\core\evaluate.py:282" -``` - -#### `deeplabcut\pose_estimation_3d\camera_calibration.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 136 | 20 | Avoid equality comparisons to `True`; use `ret:` for truth checks | -| 163 | 8 | Avoid equality comparisons to `True`; use `calibrate:` for truth checks | -| 403 | 12 | Avoid equality comparisons to `True`; use `plot:` for truth checks | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_3d\camera_calibration.py:136" -``` - -#### `deeplabcut\pose_estimation_tensorflow\predict_videos.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 1019 | 12 | Avoid equality comparisons to `True`; use `cfg["cropping"]:` for truth checks | -| 1268 | 8 | Avoid equality comparisons to `True`; use `os.path.isdir(directory):` for truth checks | -| 1297 | 20 | Avoid equality comparisons to `True`; use `cfg["cropping"]:` for truth checks | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\predict_videos.py:1019" -``` - -#### `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 187 | 12 | Avoid equality comparisons to `True`; use `dropped:` for truth checks | -| 679 | 8 | Avoid equality comparisons to `True`; use `uniform:` for truth checks | - -Quick open commands: - -```powershell -code -g "deeplabcut\generate_training_dataset\trainingsetmanipulation.py:187" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 249 | 12 | Avoid equality comparisons to `True`; use `append_image_id:` for truth checks | -| 457 | 12 | Avoid equality comparisons to `True`; use `append_image_id:` for truth checks | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py:249" -``` - -#### `deeplabcut\pose_estimation_3d\triangulation.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 107 | 8 | Avoid equality comparisons to `True`; use `isinstance(video_path, str):` for truth checks | -| 149 | 20 | Avoid equality comparisons to `True`; use `flag:` for truth checks | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_3d\triangulation.py:107" -``` - -#### `deeplabcut\utils\auxfun_multianimal.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 281 | 12 | Avoid equality comparisons to `True`; use `userfeedback:` for truth checks | -| 366 | 12 | Avoid equality comparisons to `True`; use `userfeedback:` for truth checks | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\auxfun_multianimal.py:281" -``` - -#### `tests\pose_estimation_pytorch\other\test_helper.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 18 | 12 | Avoid equality comparisons to `True`; use `tmp_model.training:` for truth checks | -| 21 | 12 | Avoid equality comparisons to `False`; use `not tmp_model.training:` for false checks | - -Quick open commands: - -```powershell -code -g "tests\pose_estimation_pytorch\other\test_helper.py:18" -``` - -#### `deeplabcut\pose_estimation_tensorflow\predict_multianimal.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 194 | 12 | Avoid equality comparisons to `True`; use `cfg["cropping"]:` for truth checks | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\predict_multianimal.py:194" -``` - -#### `deeplabcut\utils\auxiliaryfunctions_3d.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 94 | 8 | Avoid equality comparisons to `True`; use `plot:` for truth checks | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\auxiliaryfunctions_3d.py:94" -``` - -## F821 - -Count: **22** -Hint: Undefined name. Usually a real bug or missing import. - -### Files affected - -| File | Count | -|---|---:| -| `deeplabcut\utils\conversioncode.py` | 6 | -| `deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py` | 5 | -| `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` | 4 | -| `examples\JUPYTER\Demo_3D_DeepLabCut.ipynb` | 4 | -| `deeplabcut\pose_estimation_tensorflow\core\openvino\session.py` | 2 | -| `deeplabcut\pose_estimation_3d\triangulation.py` | 1 | - -### Details - -#### `deeplabcut\utils\conversioncode.py` (6) - -| Line | Col | Message | -|---:|---:|---| -| 112 | 11 | Undefined name `dlc` | -| 124 | 18 | Undefined name `tqdm` | -| 149 | 27 | Undefined name `np` | -| 152 | 29 | Undefined name `np` | -| 173 | 43 | Undefined name `np` | -| 184 | 43 | Undefined name `np` | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\conversioncode.py:112" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py` (5) - -| Line | Col | Message | -|---:|---:|---| -| 218 | 38 | Undefined name `BasePoseDataset` | -| 219 | 37 | Undefined name `raw_2_imagename_with_id` | -| 220 | 37 | Undefined name `raw_2_imagename` | -| 222 | 36 | Undefined name `raw_2_imagename_with_id` | -| 223 | 36 | Undefined name `raw_2_imagename` | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py:218" -``` - -#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` (4) - -| Line | Col | Message | -|---:|---:|---| -| 757 | 52 | Undefined name `x` | -| 757 | 86 | Undefined name `y` | -| 759 | 36 | Undefined name `y` | -| 759 | 70 | Undefined name `x` | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py:757" -``` - -#### `examples\JUPYTER\Demo_3D_DeepLabCut.ipynb` (4) - -| Line | Col | Message | -|---:|---:|---| -| 1 | 30 | Undefined name `config_path3d` | -| 1 | 30 | Undefined name `config_path3d` | -| 4 | 31 | Undefined name `config_path3d` | -| 6 | 24 | Undefined name `config_path3d` | - -Quick open commands: - -```powershell -code -g "examples\JUPYTER\Demo_3D_DeepLabCut.ipynb:1" -``` - -#### `deeplabcut\pose_estimation_tensorflow\core\openvino\session.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 90 | 26 | Undefined name `out_id` | -| 107 | 18 | Undefined name `checkcropping` | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\core\openvino\session.py:90" -``` - -#### `deeplabcut\pose_estimation_3d\triangulation.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 140 | 21 | Undefined name `warnings` | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_3d\triangulation.py:140" -``` - -## B904 - -Count: **19** -Hint: Inside `except`, use `raise ... from e` to preserve exception chaining. - -### Files affected - -| File | Count | -|---|---:| -| `deeplabcut\pose_estimation_tensorflow\predict_videos.py` | 6 | -| `examples\testscript_3d.py` | 2 | -| `deeplabcut\generate_training_dataset\frame_extraction.py` | 1 | -| `deeplabcut\pose_estimation_3d\camera_calibration.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\registry.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\runners\schedulers.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\core\evaluate.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\export.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\visualizemaps.py` | 1 | -| `deeplabcut\pose_tracking_pytorch\train_dlctransreid.py` | 1 | -| `deeplabcut\refine_training_dataset\stitch.py` | 1 | -| `deeplabcut\utils\auxfun_models.py` | 1 | -| `deeplabcut\utils\conversioncode.py` | 1 | - -### Details - -#### `deeplabcut\pose_estimation_tensorflow\predict_videos.py` (6) - -| Line | Col | Message | -|---:|---:|---| -| 70 | 9 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | -| 105 | 9 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | -| 488 | 9 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | -| 953 | 13 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | -| 1209 | 9 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | -| 1547 | 9 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\predict_videos.py:70" -``` - -#### `examples\testscript_3d.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 108 | 9 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | -| 126 | 9 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | - -Quick open commands: - -```powershell -code -g "examples\testscript_3d.py:108" -``` - -#### `deeplabcut\generate_training_dataset\frame_extraction.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 470 | 13 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | - -Quick open commands: - -```powershell -code -g "deeplabcut\generate_training_dataset\frame_extraction.py:470" -``` - -#### `deeplabcut\pose_estimation_3d\camera_calibration.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 158 | 9 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_3d\camera_calibration.py:158" -``` - -#### `deeplabcut\pose_estimation_pytorch\registry.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 69 | 9 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\registry.py:69" -``` - -#### `deeplabcut\pose_estimation_pytorch\runners\schedulers.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 117 | 9 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\runners\schedulers.py:117" -``` - -#### `deeplabcut\pose_estimation_tensorflow\core\evaluate.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 271 | 9 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\core\evaluate.py:271" -``` - -#### `deeplabcut\pose_estimation_tensorflow\export.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 125 | 9 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\export.py:125" -``` - -#### `deeplabcut\pose_estimation_tensorflow\visualizemaps.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 141 | 13 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\visualizemaps.py:141" -``` - -#### `deeplabcut\pose_tracking_pytorch\train_dlctransreid.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 17 | 5 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_tracking_pytorch\train_dlctransreid.py:17" -``` - -#### `deeplabcut\refine_training_dataset\stitch.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 1162 | 17 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | - -Quick open commands: - -```powershell -code -g "deeplabcut\refine_training_dataset\stitch.py:1162" -``` - -#### `deeplabcut\utils\auxfun_models.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 177 | 13 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\auxfun_models.py:177" -``` - -#### `deeplabcut\utils\conversioncode.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 303 | 13 | Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\conversioncode.py:303" -``` - -## E722 - -Count: **19** -Hint: Bare `except:`. Catch `Exception` or a narrower exception type. - -### Files affected - -| File | Count | -|---|---:| -| `examples\testscript_3d.py` | 3 | -| `deeplabcut\pose_estimation_3d\camera_calibration.py` | 2 | -| `examples\testscript.py` | 2 | -| `deeplabcut\create_project\add.py` | 1 | -| `deeplabcut\create_project\new.py` | 1 | -| `deeplabcut\generate_training_dataset\frame_extraction.py` | 1 | -| `deeplabcut\modelzoo\generalized_data_converter\datasets\base.py` | 1 | -| `deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc.py` | 1 | -| `deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc_dataframe.py` | 1 | -| `deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py` | 1 | -| `deeplabcut\modelzoo\generalized_data_converter\utils.py` | 1 | -| `deeplabcut\pose_tracking_pytorch\model\backbones\vit_pytorch.py` | 1 | -| `deeplabcut\refine_training_dataset\outlier_frames.py` | 1 | -| `deeplabcut\utils\auxiliaryfunctions_3d.py` | 1 | -| `deeplabcut\utils\make_labeled_video.py` | 1 | - -### Details - -#### `examples\testscript_3d.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 92 | 5 | Do not use bare `except` | -| 107 | 5 | Do not use bare `except` | -| 125 | 5 | Do not use bare `except` | - -Quick open commands: - -```powershell -code -g "examples\testscript_3d.py:92" -``` - -#### `deeplabcut\pose_estimation_3d\camera_calibration.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 97 | 5 | Do not use bare `except` | -| 157 | 5 | Do not use bare `except` | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_3d\camera_calibration.py:97" -``` - -#### `examples\testscript.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 205 | 5 | Do not use bare `except` | -| 327 | 5 | Do not use bare `except` | - -Quick open commands: - -```powershell -code -g "examples\testscript.py:205" -``` - -#### `deeplabcut\create_project\add.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 115 | 9 | Do not use bare `except` | - -Quick open commands: - -```powershell -code -g "deeplabcut\create_project\add.py:115" -``` - -#### `deeplabcut\create_project\new.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 219 | 9 | Do not use bare `except` | - -Quick open commands: - -```powershell -code -g "deeplabcut\create_project\new.py:219" -``` - -#### `deeplabcut\generate_training_dataset\frame_extraction.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 469 | 9 | Do not use bare `except` | - -Quick open commands: - -```powershell -code -g "deeplabcut\generate_training_dataset\frame_extraction.py:469" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\datasets\base.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 221 | 13 | Do not use bare `except` | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\base.py:221" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 80 | 17 | Do not use bare `except` | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc.py:80" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc_dataframe.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 188 | 17 | Do not use bare `except` | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc_dataframe.py:188" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 472 | 13 | Do not use bare `except` | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py:472" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\utils.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 32 | 5 | Do not use bare `except` | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\utils.py:32" -``` - -#### `deeplabcut\pose_tracking_pytorch\model\backbones\vit_pytorch.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 329 | 13 | Do not use bare `except` | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_tracking_pytorch\model\backbones\vit_pytorch.py:329" -``` - -#### `deeplabcut\refine_training_dataset\outlier_frames.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 686 | 5 | Do not use bare `except` | - -Quick open commands: - -```powershell -code -g "deeplabcut\refine_training_dataset\outlier_frames.py:686" -``` - -#### `deeplabcut\utils\auxiliaryfunctions_3d.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 310 | 13 | Do not use bare `except` | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\auxiliaryfunctions_3d.py:310" -``` - -#### `deeplabcut\utils\make_labeled_video.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 1333 | 17 | Do not use bare `except` | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\make_labeled_video.py:1333" -``` - -## F405 - -Count: **16** -Hint: Likely consequence of `import *`. Import the name explicitly. - -### Files affected - -| File | Count | -|---|---:| -| `deeplabcut\gui\window.py` | 16 | - -### Details - -#### `deeplabcut\gui\window.py` (16) - -| Line | Col | Message | -|---:|---:|---| -| 559 | 15 | `ProjectCreator` may be undefined, or defined from star imports | -| 563 | 24 | `OpenProject` may be undefined, or defined from star imports | -| 577 | 25 | `ModelZoo` may be undefined, or defined from star imports | -| 611 | 31 | `ManageProject` may be undefined, or defined from star imports | -| 612 | 31 | `ExtractFrames` may be undefined, or defined from star imports | -| 613 | 29 | `LabelFrames` may be undefined, or defined from star imports | -| 614 | 40 | `CreateTrainingDataset` may be undefined, or defined from star imports | -| 619 | 30 | `TrainNetwork` may be undefined, or defined from star imports | -| 624 | 33 | `EvaluateNetwork` may be undefined, or defined from star imports | -| 629 | 31 | `AnalyzeVideos` may be undefined, or defined from star imports | -| 630 | 41 | `UnsupervizedIdTracking` may be undefined, or defined from star imports | -| 635 | 30 | `CreateVideos` may be undefined, or defined from star imports | -| 640 | 39 | `ExtractOutlierFrames` may be undefined, or defined from star imports | -| 645 | 33 | `RefineTracklets` may be undefined, or defined from star imports | -| 646 | 25 | `ModelZoo` may be undefined, or defined from star imports | -| 647 | 29 | `VideoEditor` may be undefined, or defined from star imports | - -Quick open commands: - -```powershell -code -g "deeplabcut\gui\window.py:559" -``` - -## E721 - -Count: **14** -Hint: Avoid direct `type(x) == Y`; prefer `isinstance(x, Y)`. - -### Files affected - -| File | Count | -|---|---:| -| `deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py` | 5 | -| `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` | 5 | -| `deeplabcut\modelzoo\generalized_data_converter\conversion_table\conversion_table.py` | 1 | -| `deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\data\dlcloader.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\datasets\pose_tensorpack.py` | 1 | - -### Details - -#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py` (5) - -| Line | Col | Message | -|---:|---:|---| -| 59 | 16 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | -| 150 | 16 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | -| 157 | 16 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | -| 191 | 16 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | -| 191 | 36 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py:59" -``` - -#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` (5) - -| Line | Col | Message | -|---:|---:|---| -| 211 | 16 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | -| 226 | 16 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | -| 234 | 16 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | -| 245 | 16 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | -| 245 | 36 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py:211" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\conversion_table\conversion_table.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 60 | 20 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\conversion_table\conversion_table.py:60" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 37 | 8 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\materialize.py:37" -``` - -#### `deeplabcut\pose_estimation_pytorch\data\dlcloader.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 322 | 16 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\data\dlcloader.py:322" -``` - -#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_tensorpack.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 169 | 16 | Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_tensorpack.py:169" -``` - -## B006 - -Count: **12** - -### Files affected - -| File | Count | -|---|---:| -| `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` | 3 | -| `deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py` | 3 | -| `deeplabcut\utils\visualization.py` | 2 | -| `deeplabcut\modelzoo\fmpose_3d\fmpose3d.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\core\evaluate.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\core\evaluate_multianimal.py` | 1 | -| `deeplabcut\utils\make_labeled_video.py` | 1 | - -### Details - -#### `deeplabcut\generate_training_dataset\trainingsetmanipulation.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 277 | 12 | Do not use mutable data structures for argument defaults | -| 1397 | 15 | Do not use mutable data structures for argument defaults | -| 1398 | 21 | Do not use mutable data structures for argument defaults | - -Quick open commands: - -```powershell -code -g "deeplabcut\generate_training_dataset\trainingsetmanipulation.py:277" -``` - -#### `deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py` (3) - -| Line | Col | Message | -|---:|---:|---| -| 125 | 16 | Do not use mutable data structures for argument defaults | -| 247 | 16 | Do not use mutable data structures for argument defaults | -| 426 | 16 | Do not use mutable data structures for argument defaults | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\modelzoo\api\superanimal_inference.py:125" -``` - -#### `deeplabcut\utils\visualization.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 60 | 12 | Do not use mutable data structures for argument defaults | -| 126 | 20 | Do not use mutable data structures for argument defaults | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\visualization.py:60" -``` - -#### `deeplabcut\modelzoo\fmpose_3d\fmpose3d.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 21 | 27 | Do not use mutable data structures for argument defaults | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\fmpose_3d\fmpose3d.py:21" -``` - -#### `deeplabcut\pose_estimation_tensorflow\core\evaluate.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 487 | 14 | Do not use mutable data structures for argument defaults | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\core\evaluate.py:487" -``` - -#### `deeplabcut\pose_estimation_tensorflow\core\evaluate_multianimal.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 91 | 14 | Do not use mutable data structures for argument defaults | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\core\evaluate_multianimal.py:91" -``` - -#### `deeplabcut\utils\make_labeled_video.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 417 | 22 | Do not use mutable data structures for argument defaults | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\make_labeled_video.py:417" -``` - -## E711 - -Count: **7** - -### Files affected - -| File | Count | -|---|---:| -| `deeplabcut\modelzoo\generalized_data_converter\datasets\base_dlc.py` | 2 | -| `deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc_dataframe.py` | 2 | -| `deeplabcut\modelzoo\generalized_data_converter\datasets\single_dlc_dataframe.py` | 2 | -| `deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py` | 1 | - -### Details - -#### `deeplabcut\modelzoo\generalized_data_converter\datasets\base_dlc.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 25 | 29 | Comparison to `None` should be `cond is not None` | -| 25 | 54 | Comparison to `None` should be `cond is not None` | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\base_dlc.py:25" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc_dataframe.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 84 | 29 | Comparison to `None` should be `cond is not None` | -| 84 | 54 | Comparison to `None` should be `cond is not None` | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\ma_dlc_dataframe.py:84" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\datasets\single_dlc_dataframe.py` (2) - -| Line | Col | Message | -|---:|---:|---| -| 85 | 29 | Comparison to `None` should be `cond is not None` | -| 85 | 54 | Comparison to `None` should be `cond is not None` | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\single_dlc_dataframe.py:85" -``` - -#### `deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 43 | 30 | Comparison to `None` should be `cond is not None` | - -Quick open commands: - -```powershell -code -g "deeplabcut\modelzoo\generalized_data_converter\datasets\multi.py:43" -``` - -## E731 - -Count: **4** - -### Files affected - -| File | Count | -|---|---:| -| `deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` | 1 | -| `deeplabcut\refine_training_dataset\tracklets.py` | 1 | -| `deeplabcut\utils\auxfun_videos.py` | 1 | - -### Details - -#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 144 | 9 | Do not assign a `lambda` expression, use a `def` | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_imgaug.py:144" -``` - -#### `deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 181 | 9 | Do not assign a `lambda` expression, use a `def` | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\datasets\pose_multianimal_imgaug.py:181" -``` - -#### `deeplabcut\refine_training_dataset\tracklets.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 88 | 9 | Do not assign a `lambda` expression, use a `def` | - -Quick open commands: - -```powershell -code -g "deeplabcut\refine_training_dataset\tracklets.py:88" -``` - -#### `deeplabcut\utils\auxfun_videos.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 272 | 9 | Do not assign a `lambda` expression, use a `def` | - -Quick open commands: - -```powershell -code -g "deeplabcut\utils\auxfun_videos.py:272" -``` - -## B008 - -Count: **3** -Hint: Function call in default arg. Use `None` + initialize inside the function. - -### Files affected - -| File | Count | -|---|---:| -| `deeplabcut\core\inferenceutils.py` | 1 | -| `deeplabcut\pose_estimation_tensorflow\nnets\conv_blocks.py` | 1 | -| `examples\testscript_pytorch_single_animal.py` | 1 | - -### Details - -#### `deeplabcut\core\inferenceutils.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 1200 | 20 | Do not perform function call `np.linspace` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable | - -Quick open commands: - -```powershell -code -g "deeplabcut\core\inferenceutils.py:1200" -``` - -#### `deeplabcut\pose_estimation_tensorflow\nnets\conv_blocks.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 176 | 20 | Do not perform function call `expand_input_by_factor` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\nnets\conv_blocks.py:176" -``` - -#### `examples\testscript_pytorch_single_animal.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 29 | 57 | Do not perform function call `SyntheticProjectParameters` in argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable | - -Quick open commands: - -```powershell -code -g "examples\testscript_pytorch_single_animal.py:29" -``` - -## B023 - -Count: **2** -Hint: Function closes over loop variable. Bind it via default arg or helper. - -### Files affected - -| File | Count | -|---|---:| -| `deeplabcut\gui\tabs\train_network.py` | 1 | -| `deeplabcut\refine_training_dataset\stitch.py` | 1 | - -### Details - -#### `deeplabcut\gui\tabs\train_network.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 188 | 93 | Function definition does not bind loop variable `attribute` | - -Quick open commands: - -```powershell -code -g "deeplabcut\gui\tabs\train_network.py:188" -``` - -#### `deeplabcut\refine_training_dataset\stitch.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 1177 | 32 | Function definition does not bind loop variable `stitcher` | - -Quick open commands: - -```powershell -code -g "deeplabcut\refine_training_dataset\stitch.py:1177" -``` - -## B024 - -Count: **2** -Hint: ABC without abstract method. Add `@abstractmethod` or remove ABC intent. - -### Files affected - -| File | Count | -|---|---:| -| `deeplabcut\pose_estimation_pytorch\data\ctd.py` | 1 | -| `deeplabcut\pose_estimation_pytorch\runners\shelving.py` | 1 | - -### Details - -#### `deeplabcut\pose_estimation_pytorch\data\ctd.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 26 | 7 | `CondProvider` is an abstract base class, but it has no abstract methods or properties | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\data\ctd.py:26" -``` - -#### `deeplabcut\pose_estimation_pytorch\runners\shelving.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 21 | 7 | `ShelfManager` is an abstract base class, but it has no abstract methods or properties | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\runners\shelving.py:21" -``` - -## F811 - -Count: **2** -Hint: Redefined while unused. Remove duplicate or rename. - -### Files affected - -| File | Count | -|---|---:| -| `deeplabcut\pose_tracking_pytorch\processor\processor.py` | 1 | -| `tests\generate_training_dataset\test_trainset_metadata.py` | 1 | - -### Details - -#### `deeplabcut\pose_tracking_pytorch\processor\processor.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 26 | 5 | Redefinition of unused `dist` from line 19: `dist` redefined here | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_tracking_pytorch\processor\processor.py:26" -``` - -#### `tests\generate_training_dataset\test_trainset_metadata.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 246 | 5 | Redefinition of unused `test_add_shuffle` from line 210: `test_add_shuffle` redefined here | - -Quick open commands: - -```powershell -code -g "tests\generate_training_dataset\test_trainset_metadata.py:246" -``` - -## B011 - -Count: **1** - -### Files affected - -| File | Count | -|---|---:| -| `deeplabcut\pose_estimation_tensorflow\nnets\utils.py` | 1 | - -### Details - -#### `deeplabcut\pose_estimation_tensorflow\nnets\utils.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 115 | 16 | Do not `assert False` (`python -O` removes these calls), raise `AssertionError()` | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\nnets\utils.py:115" -``` - -## B012 - -Count: **1** -Hint: Jump statement in `finally` can swallow exceptions. Restructure flow. - -### Files affected - -| File | Count | -|---|---:| -| `deeplabcut\pose_estimation_tensorflow\predict_videos.py` | 1 | - -### Details - -#### `deeplabcut\pose_estimation_tensorflow\predict_videos.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 1053 | 9 | `return` inside `finally` blocks cause exceptions to be silenced | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_tensorflow\predict_videos.py:1053" -``` - -## B016 - -Count: **1** -Hint: Raise an exception instance/class, not a literal. - -### Files affected - -| File | Count | -|---|---:| -| `examples\testscript_3d.py` | 1 | - -### Details - -#### `examples\testscript_3d.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 126 | 16 | Cannot raise a literal. Did you intend to return it or raise an Exception? | - -Quick open commands: - -```powershell -code -g "examples\testscript_3d.py:126" -``` - -## B017 - -Count: **1** -Hint: Use a more specific exception with `assertRaises`. - -### Files affected - -| File | Count | -|---|---:| -| `tests\pose_estimation_pytorch\other\test_api_utils.py` | 1 | - -### Details - -#### `tests\pose_estimation_pytorch\other\test_api_utils.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 67 | 14 | Do not assert blind exception: `Exception` | - -Quick open commands: - -```powershell -code -g "tests\pose_estimation_pytorch\other\test_api_utils.py:67" -``` - -## B020 - -Count: **1** -Hint: Loop variable overrides iterator. Rename loop variables. - -### Files affected - -| File | Count | -|---|---:| -| `deeplabcut\pose_estimation_pytorch\models\heads\simple_head.py` | 1 | - -### Details - -#### `deeplabcut\pose_estimation_pytorch\models\heads\simple_head.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 196 | 13 | Loop control variable `out_channels` overrides iterable it iterates | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\models\heads\simple_head.py:196" -``` - -## B027 - -Count: **1** -Hint: Empty method in ABC without abstract decorator. Add `@abstractmethod` or implement it. - -### Files affected - -| File | Count | -|---|---:| -| `deeplabcut\pose_estimation_pytorch\models\modules\kpt_encoders.py` | 1 | - -### Details - -#### `deeplabcut\pose_estimation_pytorch\models\modules\kpt_encoders.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 47 | 5 | `BaseKeypointEncoder.num_channels` is an empty method in an abstract base class, but has no abstract decorator | - -Quick open commands: - -```powershell -code -g "deeplabcut\pose_estimation_pytorch\models\modules\kpt_encoders.py:47" -``` - -## UP028 - -Count: **1** - -### Files affected - -| File | Count | -|---|---:| -| `tools\update_license_headers.py` | 1 | - -### Details - -#### `tools\update_license_headers.py` (1) - -| Line | Col | Message | -|---:|---:|---| -| 32 | 13 | Replace `yield` over `for` loop with `yield from` | - -Quick open commands: - -```powershell -code -g "tools\update_license_headers.py:32" -``` diff --git a/tools/ruff_report.py b/tools/ruff_report.py index e66934a7a9..123f21e02c 100644 --- a/tools/ruff_report.py +++ b/tools/ruff_report.py @@ -64,7 +64,7 @@ def relpath(path: str) -> str: def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("paths", nargs="*", default=["."], help="Files/directories to scan") - parser.add_argument("--output", default="ruff-report.md", help="Markdown output path") + parser.add_argument("--output", default="tmp/ruff-report.md", help="Markdown output path") args = parser.parse_args() issues = run_ruff(args.paths) From 68c612811c0b355dd65bd5df25c2bf1fcdd2a00d Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 17 Mar 2026 14:40:34 +0100 Subject: [PATCH 11/80] Refactor __init__: lazy-load optional APIs Rework deeplabcut package initialization: use relative imports, add a logger and robust DEBUG env parsing, and replace eager imports of optional GUI/PyTorch features with a lazy-loading mechanism. Optional public names (GUI and transformer_reID) are resolved on first access via __getattr__ (with clear error messages when dependencies are missing) and cached for subsequent use; __dir__ and a consolidated __all__ improve discoverability. Removes previous noisy prints and light-mode fallbacks in favor of structured logging and explicit public API grouping. --- deeplabcut/__init__.py | 287 ++++++++++++++++++++++++++++++++--------- pyproject.toml | 9 +- 2 files changed, 228 insertions(+), 68 deletions(-) diff --git a/deeplabcut/__init__.py b/deeplabcut/__init__.py index 183aec16c4..9bec9ff673 100644 --- a/deeplabcut/__init__.py +++ b/deeplabcut/__init__.py @@ -9,27 +9,47 @@ # Licensed under GNU Lesser General Public License v3.0 # +from __future__ import annotations +import logging import os +from importlib import import_module +from typing import Any -DEBUG = True and "DEBUG" in os.environ and os.environ["DEBUG"] -from deeplabcut.version import VERSION, __version__ +logger = logging.getLogger(__name__) -print(f"Loading DLC {VERSION}...") +# DEBUG="", "0", "false", "no" -> False +DEBUG = os.environ.get("DEBUG", "").strip().lower() not in {"", "0", "false", "no"} -try: - from deeplabcut.gui.launch_script import launch_dlc - from deeplabcut.gui.tabs.label_frames import ( - label_frames, - refine_labels, - ) - from deeplabcut.gui.tracklet_toolbox import refine_tracklets - from deeplabcut.gui.widgets import SkeletonBuilder -except (ModuleNotFoundError, ImportError): - print("DLC loaded in light mode; you cannot use any GUI (labeling, relabeling and standalone GUI)") +from .version import VERSION, __version__ -from deeplabcut.core.engine import Engine -from deeplabcut.create_project import ( +if DEBUG: + logger.debug("Loading DLC %s", VERSION) + +# ----------------------------------------------------------------------------- +# Always-available public API +# ----------------------------------------------------------------------------- + +# Train / evaluate / predict functions (compat layer) +from .compat import ( + analyze_images, + analyze_time_lapse_frames, + analyze_videos, + convert_detections2tracklets, + create_tracking_dataset, + evaluate_network, + export_model, + extract_maps, + extract_save_all_maps, + return_evaluate_network_data, + return_train_network_path, + train_network, + visualize_locrefs, + visualize_paf, + visualize_scoremaps, +) +from .core.engine import Engine +from .create_project import ( add_new_videos, create_new_project, create_new_project_3d, @@ -37,7 +57,7 @@ create_pretrained_project, load_demo_data, ) -from deeplabcut.generate_training_dataset import ( +from .generate_training_dataset import ( adddatasetstovideolistandviceversa, check_labels, comparevideolistsanddatafolders, @@ -52,8 +72,21 @@ extract_frames, mergeandsplit, ) -from deeplabcut.modelzoo.video_inference import video_inference_superanimal -from deeplabcut.utils import ( +from .modelzoo.video_inference import video_inference_superanimal +from .pose_estimation_3d import ( + calibrate_cameras, + check_undistortion, + create_labeled_video_3d, + triangulate, +) +from .post_processing import analyzeskeleton, filterpredictions +from .refine_training_dataset import ( + extract_outlier_frames, + find_outliers_in_raw_data, + merge_datasets, +) +from .refine_training_dataset.stitch import stitch_tracklets +from .utils import ( analyze_videos_converth5_to_csv, analyze_videos_converth5_to_nwb, auxfun_videos, @@ -64,54 +97,180 @@ create_video_with_all_detections, plot_trajectories, ) - -try: - from deeplabcut.pose_tracking_pytorch import transformer_reID -except ModuleNotFoundError: - import warnings - - warnings.warn( - """ - As PyTorch is not installed, unsupervised identity learning will not be available. - Please run `pip install torch`, or ignore this warning. - """, - stacklevel=2, - ) - -# Train, evaluate & predict functions / all require TF -from deeplabcut.compat import ( - analyze_images, - analyze_time_lapse_frames, - analyze_videos, - convert_detections2tracklets, - create_tracking_dataset, - evaluate_network, - export_model, - extract_maps, - extract_save_all_maps, - return_evaluate_network_data, - return_train_network_path, - train_network, - visualize_locrefs, - visualize_paf, - visualize_scoremaps, -) -from deeplabcut.pose_estimation_3d import ( - calibrate_cameras, - check_undistortion, - create_labeled_video_3d, - triangulate, -) -from deeplabcut.post_processing import analyzeskeleton, filterpredictions -from deeplabcut.refine_training_dataset import ( - extract_outlier_frames, - find_outliers_in_raw_data, - merge_datasets, -) -from deeplabcut.refine_training_dataset.stitch import stitch_tracklets -from deeplabcut.utils.auxfun_videos import ( +from .utils.auxfun_videos import ( CropVideo, DownSampleVideo, ShortenVideo, check_video_integrity, ) + +# ----------------------------------------------------------------------------- +# Optional / lazy public API +# ----------------------------------------------------------------------------- +# These names are part of the public API, but importing them may require +# optional GUI or torch dependencies, so we lazy load them. +# +# Example: +# import deeplabcut as dlc +# dlc.launch_dlc() # imports GUI code lazily +# dlc.transformer_reID(...) # imports torch-dependent code lazily +# ----------------------------------------------------------------------------- + +_OPTIONAL_EXPORTS: dict[str, tuple[str, str]] = { + # GUI + "launch_dlc": (".gui.launch_script", "launch_dlc"), + "label_frames": (".gui.tabs.label_frames", "label_frames"), + "refine_labels": (".gui.tabs.label_frames", "refine_labels"), + "refine_tracklets": (".gui.tracklet_toolbox", "refine_tracklets"), + "SkeletonBuilder": (".gui.widgets", "SkeletonBuilder"), + # Optional torch feature + "transformer_reID": (".pose_tracking_pytorch", "transformer_reID"), +} + + +def __getattr__(name: str) -> Any: + """Lazily load optional public exports.""" + if name not in _OPTIONAL_EXPORTS: + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + module_name, attr_name = _OPTIONAL_EXPORTS[name] + + try: + module = import_module(module_name, package=__name__) + value = getattr(module, attr_name) + except (ModuleNotFoundError, ImportError) as exc: + if name in { + "launch_dlc", + "label_frames", + "refine_labels", + "refine_tracklets", + "SkeletonBuilder", + }: + raise AttributeError( + f"{name!r} is unavailable because DeepLabCut was loaded without GUI dependencies." + ) from exc + + if name == "transformer_reID": + raise AttributeError( + f"{name!r} is unavailable because the PyTorch-based tracking dependencies are not installed." + ) from exc + + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from exc + + # Cache the resolved object so future access is fast + globals()[name] = value + return value + + +def __dir__() -> list[str]: + """Improve IDE / autocomplete discoverability.""" + return sorted(set(globals()) | set(__all__)) + + +# ----------------------------------------------------------------------------- +# Public API +# ----------------------------------------------------------------------------- + +_VERSION_EXPORTS = [ + "__version__", + "VERSION", + "DEBUG", +] + +_CORE_EXPORTS = [ + "Engine", +] + +_PROJECT_EXPORTS = [ + "add_new_videos", + "create_new_project", + "create_new_project_3d", + "create_pretrained_human_project", + "create_pretrained_project", + "load_demo_data", +] + +_DATASET_EXPORTS = [ + "adddatasetstovideolistandviceversa", + "check_labels", + "comparevideolistsanddatafolders", + "create_multianimaltraining_dataset", + "create_training_dataset", + "create_training_dataset_from_existing_split", + "create_training_model_comparison", + "dropannotationfileentriesduetodeletedimages", + "dropduplicatesinannotatinfiles", + "dropimagesduetolackofannotation", + "dropunlabeledframes", + "extract_frames", + "mergeandsplit", +] + +_COMPAT_EXPORTS = [ + "analyze_images", + "analyze_time_lapse_frames", + "analyze_videos", + "convert_detections2tracklets", + "create_tracking_dataset", + "evaluate_network", + "export_model", + "extract_maps", + "extract_save_all_maps", + "return_evaluate_network_data", + "return_train_network_path", + "train_network", + "visualize_locrefs", + "visualize_paf", + "visualize_scoremaps", +] + +_UTIL_EXPORTS = [ + "analyze_videos_converth5_to_csv", + "analyze_videos_converth5_to_nwb", + "auxfun_videos", + "auxiliaryfunctions", + "convert2_maDLC", + "convertcsv2h5", + "create_labeled_video", + "create_video_with_all_detections", + "plot_trajectories", + "CropVideo", + "DownSampleVideo", + "ShortenVideo", + "check_video_integrity", +] + +_POST_PROCESSING_EXPORTS = [ + "analyzeskeleton", + "filterpredictions", + "extract_outlier_frames", + "find_outliers_in_raw_data", + "merge_datasets", + "stitch_tracklets", +] + +_THREE_D_EXPORTS = [ + "calibrate_cameras", + "check_undistortion", + "create_labeled_video_3d", + "triangulate", +] + +_MODELZOO_EXPORTS = [ + "video_inference_superanimal", +] + +_OPTIONAL_API_EXPORTS = list(_OPTIONAL_EXPORTS) + +__all__ = ( + _VERSION_EXPORTS + + _CORE_EXPORTS + + _PROJECT_EXPORTS + + _DATASET_EXPORTS + + _COMPAT_EXPORTS + + _UTIL_EXPORTS + + _POST_PROCESSING_EXPORTS + + _THREE_D_EXPORTS + + _MODELZOO_EXPORTS + + _OPTIONAL_API_EXPORTS +) diff --git a/pyproject.toml b/pyproject.toml index 17d926534b..3f227cbd6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -131,12 +131,13 @@ requires-dist = [] torch-backend = "auto" [tool.ruff] -lint.select = ["E", "F", "B", "I", "UP"] -lint.ignore = ["E741"] target-version = "py310" -fix = true line-length = 120 - +fix = true +[tool.ruff.lint] +select = [ "E", "F", "B", "I", "UP" ] +ignore = [ "E741" ] +per-file-ignores = { "__init__.py" = [ "F401", "E402" ] } [tool.ruff.lint.pydocstyle] convention = "google" From 918fdb9d78c9496a08b9793e6d07644d9f30a66d Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 17 Mar 2026 14:47:48 +0100 Subject: [PATCH 12/80] Re-run line length fixes --- deeplabcut/create_project/new.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/deeplabcut/create_project/new.py b/deeplabcut/create_project/new.py index 1c1029e089..7073cf7fea 100644 --- a/deeplabcut/create_project/new.py +++ b/deeplabcut/create_project/new.py @@ -102,7 +102,8 @@ def create_new_project( copy_videos=True, ) - Users must format paths with either: r'C:\ OR 'C:\\ <- i.e. a double backslash \ \ ) + Users must format paths with either: + r'C:\ OR 'C:\\ <- i.e. a double backslash \ \ ) """ from datetime import datetime as dt @@ -216,7 +217,7 @@ def create_new_project( # For windows os.path.realpath does not work and does not link to the real # video. [old: rel_video_path = os.path.realpath(video)] rel_video_path = str(Path.resolve(Path(video))) - except: + except Exception: rel_video_path = os.readlink(str(video)) try: @@ -304,6 +305,11 @@ def create_new_project( print('Generated "{}"'.format(project_path / "config.yaml")) print( - f"\nA new project with name {project_name} is created at {str(wd)} and a configurable file (config.yaml) is stored there. Change the parameters in this file to adapt to your project's needs.\n Once you have changed the configuration file, use the function 'extract_frames' to select frames for labeling.\n. [OPTIONAL] Use the function 'add_new_videos' to add new videos to your project (at any stage)." + f"\nA new project with name {project_name} is created at {str(wd)} " + "and a configurable file (config.yaml) is stored there. " + "Change the parameters in this file to adapt to your project's needs.\n " + "Once you have changed the configuration file, " + "use the function 'extract_frames' to select frames for labeling.\n. " + "[OPTIONAL] Use the function 'add_new_videos' to add new videos to your project (at any stage)." ) return projconfigfile From f1b715acf5b9a01d27cb21f0e75eab99adb1eeee Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 17 Mar 2026 14:59:55 +0100 Subject: [PATCH 13/80] Fix window.py --- deeplabcut/gui/window.py | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/deeplabcut/gui/window.py b/deeplabcut/gui/window.py index 880a21b32f..44e525a118 100644 --- a/deeplabcut/gui/window.py +++ b/deeplabcut/gui/window.py @@ -14,6 +14,7 @@ import sys import warnings from functools import cached_property +from importlib import import_module from importlib.resources import files from pathlib import Path from urllib.error import URLError @@ -37,7 +38,23 @@ from deeplabcut import VERSION, auxiliaryfunctions, compat from deeplabcut.core.engine import Engine from deeplabcut.gui import BASE_DIR, components, utils -from deeplabcut.gui.tabs import * +from deeplabcut.gui.tabs import ( + AnalyzeVideos, + CreateTrainingDataset, + CreateVideos, + EvaluateNetwork, + ExtractFrames, + ExtractOutlierFrames, + LabelFrames, + ManageProject, + ModelZoo, + OpenProject, + ProjectCreator, + RefineTracklets, + TrainNetwork, + UnsupervizedIdTracking, + VideoEditor, +) from deeplabcut.gui.widgets import StreamReceiver, StreamWriter from deeplabcut.utils.multiprocessing import call_with_timeout @@ -193,7 +210,7 @@ def engine(self, e: Engine) -> None: if e == e.TF: try: - import tensorflow + import_module("tensorflow") except ModuleNotFoundError as err: msg = QtWidgets.QMessageBox() msg.setIcon(QtWidgets.QMessageBox.Warning) @@ -371,7 +388,13 @@ def _generate_welcome_page(self): image_widget.setPixmap(pixmap.scaledToHeight(400, QtCore.Qt.SmoothTransformation)) self.layout.addWidget(image_widget) - description = "DeepLabCut™ is an open source tool for markerless pose estimation of user-defined body parts with deep learning.\nA. and M.W. Mathis Labs | http://www.deeplabcut.org\n\n To get started, create a new project, load an existing one, or try one of our pretrained models from the Model Zoo." + description = str( + "DeepLabCut™ is an open source tool for markerless " + "pose estimation of user-defined body parts with deep learning.\n" + "A. and M.W. Mathis Labs | http://www.deeplabcut.org\n\n " + "To get started, create a new project, load an existing one, " + "or try one of our pretrained models from the Model Zoo." + ) label = components._create_label_widget( description, "font-size:12px; text-align: center;", @@ -553,7 +576,9 @@ def _learn_dlc(self): dlg = QMessageBox(self) dlg.setWindowTitle("Learn DLC") dlg.setText( - """Learn DLC with our docs and how-to guides!""" + """Learn DLC with + + our docs and how-to guides!""" ) _ = dlg.exec() @@ -695,8 +720,7 @@ def _attempt_attribute_update(widget_name, updated_value): def is_transreid_available(self): if self.is_multianimal: try: - from deeplabcut.pose_tracking_pytorch import transformer_reID - + import_module("deeplabcut.pose_tracking_pytorch.transformer_reID") return True except ModuleNotFoundError: return False From 65dc094a5f999033fc4eff813077f78ee38ab885 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 17 Mar 2026 15:15:20 +0100 Subject: [PATCH 14/80] Remove unused imports --- deeplabcut/benchmark/metrics.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/deeplabcut/benchmark/metrics.py b/deeplabcut/benchmark/metrics.py index 0c89590339..91f34a457b 100644 --- a/deeplabcut/benchmark/metrics.py +++ b/deeplabcut/benchmark/metrics.py @@ -11,15 +11,6 @@ """Evaluation metrics for the DeepLabCut benchmark.""" -import sys -import unittest.mock - -# TODO(stes) mocking a few modules to rely in fewer dependencies, without -# causing import errors when using deeplabcut. -MOCK_MODULES = ["statsmodels", "statsmodels.api", "pytables"] -for mod_name in MOCK_MODULES: - sys.modules[mod_name] = unittest.mock.MagicMock() - import os import pickle from collections import defaultdict From 6936d1c3d59431c90603e654570a57e4da60c3ac Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 17 Mar 2026 15:16:49 +0100 Subject: [PATCH 15/80] Fix TF train.py --- deeplabcut/pose_estimation_tensorflow/core/train.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/deeplabcut/pose_estimation_tensorflow/core/train.py b/deeplabcut/pose_estimation_tensorflow/core/train.py index 7eb517463d..0f4e94a06b 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/train.py +++ b/deeplabcut/pose_estimation_tensorflow/core/train.py @@ -20,8 +20,6 @@ from pathlib import Path import tensorflow as tf - -tf.compat.v1.disable_eager_execution() import tf_slim as slim from deeplabcut.pose_estimation_tensorflow.config import load_config @@ -33,6 +31,8 @@ from deeplabcut.pose_estimation_tensorflow.util.logging import setup_logging from deeplabcut.utils import auxfun_models +tf.compat.v1.disable_eager_execution() + class LearningRate: def __init__(self, cfg): @@ -148,7 +148,8 @@ def train( net_type = cfg["net_type"] if cfg["dataset_type"] in ("scalecrop", "tensorpack", "deterministic"): print( - "Switching batchsize to 1, as tensorpack/scalecrop/deterministic loaders do not support batches >1. Use imgaug/default loader." + "Switching batchsize to 1, as tensorpack/scalecrop/deterministic loaders " + "do not support batches >1. Use imgaug/default loader." ) cfg["batch_size"] = 1 # in case this was edited for analysis.- From ea0fa8edff8273bdddd9cb77c2c85f337a0d7488 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 17 Mar 2026 15:29:07 +0100 Subject: [PATCH 16/80] Fix missing imports --- deeplabcut/utils/conversioncode.py | 38 ++++++++++++++++-------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/deeplabcut/utils/conversioncode.py b/deeplabcut/utils/conversioncode.py index 2ffa7b9c98..2995694cc9 100644 --- a/deeplabcut/utils/conversioncode.py +++ b/deeplabcut/utils/conversioncode.py @@ -8,22 +8,16 @@ # # Licensed under GNU Lesser General Public License v3.0 # -""" -DeepLabCut2.0 Toolbox (deeplabcut.org) -© A. & M. Mathis Labs -https://github.com/DeepLabCut/DeepLabCut -Please see AUTHORS for contributors. - -https://github.com/DeepLabCut/DeepLabCut/blob/master/AUTHORS -Licensed under GNU Lesser General Public License v3.0 -""" import os from itertools import islice from pathlib import Path +import numpy as np import pandas as pd +from tqdm import tqdm +import deeplabcut as dlc from deeplabcut.utils import auxiliaryfunctions SUPPORTED_FILETYPES = "csv", "nwb" @@ -32,17 +26,20 @@ def convertcsv2h5(config, userfeedback=True, scorer=None): """ Convert (image) annotation files in folder labeled-data from csv to h5. - This function allows the user to manually edit the csv (e.g. to correct the scorer name and then convert it into hdf format). + This function allows the user to manually edit the csv + (e.g. to correct the scorer name and then convert it into hdf format). WARNING: conversion might corrupt the data. config : string Full path of the config.yaml file as a string. userfeedback: bool, optional - If true the user will be asked specifically for each folder in labeled-data if the containing csv shall be converted to hdf format. + If true the user will be asked specifically + for each folder in labeled-data if the containing csv shall be converted to hdf format. scorer: string, optional - If a string is given, then the scorer/annotator in all csv and hdf files that are changed, will be overwritten with this name. + If a string is given, then the scorer/annotator + in all csv and hdf files that are changed, will be overwritten with this name. Examples -------- @@ -50,7 +47,8 @@ def convertcsv2h5(config, userfeedback=True, scorer=None): >>> deeplabcut.convertcsv2h5('/analysis/project/reaching-task/config.yaml') -------- - Convert csv annotation files for reaching-task project into hdf while changing the scorer/annotator in all annotation files to Albert! + Convert csv annotation files for reaching-task project into hdf + while changing the scorer/annotator in all annotation files to Albert! >>> deeplabcut.convertcsv2h5('/analysis/project/reaching-task/config.yaml',scorer='Albert') -------- """ @@ -107,7 +105,9 @@ def adapt_labeled_data_to_new_project(config_path, remove_old_bodyparts=False, o other_scorer : bool (default = False) If True, the labels will be converted to the new scorer. userfeedback : bool (default = True) - If true the user will be asked specifically for each folder in labeled-data if the containing csv shall be converted to hdf format. + If true the user will be asked specifically + for each folder in labeled-data if the containing csv + shall be converted to hdf format. """ # Load the config file @@ -234,7 +234,8 @@ def analyze_videos_converth5_to_csv(video_folder, videotype=".mp4", listofvideos Examples -------- - Converts all pose-output files belonging to mp4 videos in the folder '/media/alex/experimentaldata/cheetahvideos' to csv files. + Converts all pose-output files belonging to mp4 videos + in the folder '/media/alex/experimentaldata/cheetahvideos' to csv files. deeplabcut.analyze_videos_converth5_to_csv('/media/alex/experimentaldata/cheetahvideos','.mp4') """ @@ -273,7 +274,8 @@ def analyze_videos_converth5_to_nwb( Examples -------- - Converts all pose-output files belonging to mp4 videos in the folder '/media/alex/experimentaldata/cheetahvideos' to csv files. + Converts all pose-output files belonging to mp4 videos in the folder + '/media/alex/experimentaldata/cheetahvideos' to csv files. deeplabcut.analyze_videos_converth5_to_csv('/media/alex/experimentaldata/cheetahvideos','.mp4') """ if listofvideos: # can also be called with a list of videos (from GUI) @@ -300,8 +302,8 @@ def _convert_h5_files_to(filetype, config, h5_files, videos): if filetype == "nwb": try: from dlc2nwb.utils import convert_h5_to_nwb - except ImportError: - raise ImportError("The package `dlc2nwb` is missing. Please run `pip install dlc2nwb`.") + except ImportError as e: + raise ImportError("The package `dlc2nwb` is missing. Please run `pip install dlc2nwb`.") from e for video in videos: if "_labeled" in video: From 4d0980d4116572074ed79c0bbc88fbfd587e3627 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 17 Mar 2026 15:34:09 +0100 Subject: [PATCH 17/80] Update trackingutils.py --- deeplabcut/core/trackingutils.py | 44 +++++++++++++------------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/deeplabcut/core/trackingutils.py b/deeplabcut/core/trackingutils.py index ee294bee39..75934dc26b 100644 --- a/deeplabcut/core/trackingutils.py +++ b/deeplabcut/core/trackingutils.py @@ -175,15 +175,13 @@ def fit(self, xy): @staticmethod @jit(nopython=True) def _fit(x, y): - """ - Least Squares ellipse fitting algorithm - Fit an ellipse to a set of X- and Y-coordinates. - See Halir and Flusser, 1998 for implementation details + """Least Squares ellipse fitting algorithm Fit an ellipse to a set of X- and + Y-coordinates. See Halir and Flusser, 1998 for implementation details. :param x: ndarray, 1D trajectory :param y: ndarray, 1D trajectory - :return: 1D ndarray of 6 coefficients of the general quadratic curve: - ax^2 + 2bxy + cy^2 + 2dx + 2fy + g = 0 + :return: 1D ndarray of 6 coefficients of the general quadratic curve: ax^2 + + 2bxy + cy^2 + 2dx + 2fy + g = 0 """ D1 = np.vstack((x * x, x * y, y * y)) D2 = np.vstack((x, y, np.ones_like(x))) @@ -205,8 +203,7 @@ def _fit(x, y): @staticmethod @jit(nopython=True) def _fit_error(x, y, sd): - """ - Fit a sd-sigma covariance error ellipse to the data. + """Fit a sd-sigma covariance error ellipse to the data. :param x: ndarray, 1D input of X coordinates :param y: ndarray, 1D input of Y coordinates @@ -361,10 +358,8 @@ def state(self, bbox): @staticmethod def convert_x_to_bbox(x, score=None): - """ - Takes a bounding box in the centre form [x,y,s,r] and returns it in the form - [x1,y1,x2,y2] where x1,y1 is the top left and x2,y2 is the bottom right - """ + """Takes a bounding box in the centre form [x,y,s,r] and returns it in the form + [x1,y1,x2,y2] where x1,y1 is the top left and x2,y2 is the bottom right.""" w = np.sqrt(x[2] * x[3]) h = x[2] / w if score is None: @@ -374,11 +369,9 @@ def convert_x_to_bbox(x, score=None): @staticmethod def convert_bbox_to_z(bbox): - """ - Takes a bounding box in the form [x1,y1,x2,y2] and returns z in the form + """Takes a bounding box in the form [x1,y1,x2,y2] and returns z in the form [x,y,s,r] where x,y is the centre of the box and s is the scale/area and r is - the aspect ratio - """ + the aspect ratio.""" w = bbox[2] - bbox[0] h = bbox[3] - bbox[1] x = bbox[0] + w / 2.0 @@ -444,7 +437,7 @@ def track(self, poses, identities=None): unmatched_detections = [i for i, _ in enumerate(ellipses) if i not in row_indices] unmatched_trackers = [j for j, _ in enumerate(trackers) if j not in col_indices] matches = [] - for row, col in zip(row_indices, col_indices): + for row, col in zip(row_indices, col_indices, strict=False): val = cost_matrix[row, col] # diff = val - cost_matrix # diff[row, col] += val @@ -564,7 +557,7 @@ def track(self, poses): self.trackers.append(tracker) poses_ref = [] - for i, tracker in enumerate(self.trackers): + for _, tracker in enumerate(self.trackers): pose_ref = tracker.predict() poses_ref.append(pose_ref.reshape((-1, 2))) @@ -674,8 +667,7 @@ def track(self, dets): @staticmethod def match_detections_to_trackers(detections, trackers, iou_threshold): - """ - Assigns detections to tracked object (both represented as bounding boxes) + """Assigns detections to tracked object (both represented as bounding boxes) Returns 3 lists of matches, unmatched_detections and unmatched_trackers """ @@ -693,17 +685,17 @@ def match_detections_to_trackers(detections, trackers, iou_threshold): row_indices, col_indices = linear_sum_assignment(-iou_matrix) unmatched_detections = [] - for d, det in enumerate(detections): + for d, _ in enumerate(detections): if d not in row_indices: unmatched_detections.append(d) unmatched_trackers = [] - for t, trk in enumerate(trackers): + for t, _ in enumerate(trackers): if t not in col_indices: unmatched_trackers.append(t) # filter out matched with low IOU matches = [] - for row, col in zip(row_indices, col_indices): + for row, col in zip(row_indices, col_indices, strict=False): if iou_matrix[row, col] < iou_threshold: unmatched_detections.append(row) unmatched_trackers.append(col) @@ -745,9 +737,9 @@ def calc_bboxes_from_keypoints(data, slack=0, offset=0): def reconstruct_all_ellipses(data, sd): - """ - Reconstructs ellipses for multiple individuals based on their body part coordinates - across multiple frames. Each ellipse is fitted to the coordinates using an `EllipseFitter`. + """Reconstructs ellipses for multiple individuals based on their body part + coordinates across multiple frames. Each ellipse is fitted to the coordinates using + an `EllipseFitter`. Parameters ---------- From 9c96845d7d323f9b824652555ec09641189bf12a Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 17 Mar 2026 15:34:18 +0100 Subject: [PATCH 18/80] Update test_transforms.py --- tests/pose_estimation_pytorch/data/test_transforms.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/pose_estimation_pytorch/data/test_transforms.py b/tests/pose_estimation_pytorch/data/test_transforms.py index 9f264fa4fb..f85ce00ffe 100644 --- a/tests/pose_estimation_pytorch/data/test_transforms.py +++ b/tests/pose_estimation_pytorch/data/test_transforms.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Tests the custom transforms""" +"""Tests the custom transforms.""" import random @@ -56,7 +56,7 @@ def test_dlc_resize_pad_good_aspect_ratio(height, width, image_shapes): ) def test_dlc_resize_pad_bad_aspect_ratio(data): aug = transforms.KeepAspectRatioResize(width=data["width"], height=data["height"], mode="pad") - for in_shape, out_shape in zip(data["in_shapes"], data["out_shapes"]): + for in_shape, out_shape in zip(data["in_shapes"], data["out_shapes"], strict=False): fake_image = np.zeros(in_shape) transformed = aug(image=fake_image, keypoints=[]) assert transformed["image"].shape == out_shape @@ -96,7 +96,7 @@ def test_dlc_resize_pad_bad_aspect_ratio_with_keypoints(data): def test_coarse_dropout(): - aug = transforms.CoarseDropout( + transforms.CoarseDropout( max_holes=10, max_height=0.05, min_height=0.01, @@ -221,7 +221,7 @@ def test_random_bbox_transform_scale(data: dict) -> None: bboxes_out = np.asarray(output["bboxes"]) scale_low, scale_high = data["transform_config"]["scale_factor"] - for bbox_in_wh, bbox_out_wh in zip(bboxes[:, 2:], bboxes_out[:, 2:]): + for bbox_in_wh, bbox_out_wh in zip(bboxes[:, 2:], bboxes_out[:, 2:], strict=False): print("bbox_in_wh", bbox_in_wh) w, h = bbox_in_wh[0].item(), bbox_in_wh[1].item() w_low, w_high = w * scale_low, w * scale_high @@ -269,7 +269,7 @@ def test_random_bbox_transform_shift(data: dict) -> None: bboxes_out = np.asarray(output["bboxes"]) shift = data["transform_config"]["shift_factor"] - for bbox_in, bbox_out in zip(bboxes, bboxes_out): + for bbox_in, bbox_out in zip(bboxes, bboxes_out, strict=False): print("bbox_in", bbox_in) x, y, w, h = bbox_in x_out, y_out, w_out, h_out = bbox_out From c76dccb8aa39441cfb3cfe1d806a0b0ea89d75f8 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 17 Mar 2026 15:37:19 +0100 Subject: [PATCH 19/80] Update multi.py --- .../modelzoo/generalized_data_converter/datasets/multi.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/multi.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/multi.py index f044f3e92c..b3e2b4db7a 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/multi.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/multi.py @@ -10,6 +10,11 @@ # import warnings +from deeplabcut.modelzoo.generalized_data_converter.datasets.base import ( + BasePoseDataset, + raw_2_imagename, + raw_2_imagename_with_id, +) from deeplabcut.modelzoo.generalized_data_converter.datasets.materialize import ( mat_func_factory, ) @@ -98,7 +103,7 @@ def _build_maps(self): self.meta["imageid2datasetname"] = self.imageid2datasetname max_num = 0 - for dataset_name, dataset in self.name2genericdataset.items(): + for _dataset_name, dataset in self.name2genericdataset.items(): max_num = max(max_num, dataset.meta["max_individuals"]) self.meta["max_individuals"] = max_num dataset_name = self.meta["dataset_name"] From b44303676b57a07b694edf700e2f82391a001097 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 17 Mar 2026 17:12:25 +0100 Subject: [PATCH 20/80] Fix missing variables --- .../core/openvino/session.py | 4 +- .../datasets/pose_multianimal_imgaug.py | 12 +- .../predict_videos.py | 177 ++++++++++++------ examples/JUPYTER/Demo_3D_DeepLabCut.ipynb | 8 +- 4 files changed, 132 insertions(+), 69 deletions(-) diff --git a/deeplabcut/pose_estimation_tensorflow/core/openvino/session.py b/deeplabcut/pose_estimation_tensorflow/core/openvino/session.py index 51490a90bc..015d09218e 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/openvino/session.py +++ b/deeplabcut/pose_estimation_tensorflow/core/openvino/session.py @@ -15,6 +15,8 @@ import numpy as np from tqdm import tqdm +from deeplabcut.pose_estimation_tensorflow.predict_videos import checkcropping + try: from openvino.runtime import AsyncInferQueue, Core @@ -87,7 +89,7 @@ def run(self, out_name, feed_dict): def completion_callback(request, inp_id): output = next(iter(request.results.values())) - batch_output[out_id] = output + batch_output[inp_id] = output self.infer_queue.set_callback(completion_callback) diff --git a/deeplabcut/pose_estimation_tensorflow/datasets/pose_multianimal_imgaug.py b/deeplabcut/pose_estimation_tensorflow/datasets/pose_multianimal_imgaug.py index 918671fbe3..2e0bc03443 100644 --- a/deeplabcut/pose_estimation_tensorflow/datasets/pose_multianimal_imgaug.py +++ b/deeplabcut/pose_estimation_tensorflow/datasets/pose_multianimal_imgaug.py @@ -58,7 +58,7 @@ def __init__(self, cfg): self.data = self.load_dataset() self.num_images = len(self.data) self.batch_size = cfg["batch_size"] - print("Batch Size is %d" % self.batch_size) + print("Batch size is %d", self.batch_size) self._default_size = np.array(self.cfg.get("crop_size", (400, 400))) self.pipeline = self.build_augmentation_pipeline( apply_prob=cfg.get("apply_prob", 0.5), @@ -210,7 +210,7 @@ def sometimes(aug): if cfg.get("fliplr", False) and cfg.get("symmetric_pairs"): opt = cfg.get("fliplr", False) - if type(opt) == int: + if isinstance(opt, int): p = opt else: p = 0.5 @@ -225,7 +225,7 @@ def sometimes(aug): ) if cfg.get("rotation", False): opt = cfg.get("rotation", False) - if type(opt) == int: + if isinstance(opt, int): pipeline.add(sometimes(iaa.Affine(rotate=(-opt, opt)))) else: pipeline.add(sometimes(iaa.Affine(rotate=(-10, 10)))) @@ -233,7 +233,7 @@ def sometimes(aug): pipeline.add(sometimes(iaa.AllChannelsHistogramEqualization())) if cfg.get("motion_blur", False): opts = cfg.get("motion_blur", False) - if type(opts) == list: + if isinstance(opts, list): opts = dict(opts) pipeline.add(sometimes(iaa.MotionBlur(**opts))) else: @@ -244,7 +244,7 @@ def sometimes(aug): pipeline.add(sometimes(iaa.ElasticTransformation(sigma=5))) if cfg.get("gaussian_noise", False): opt = cfg.get("gaussian_noise", False) - if type(opt) == int or type(opt) == float: + if isinstance(opt, int) or isinstance(opt, float): pipeline.add(sometimes(iaa.AdditiveGaussianNoise(loc=0, scale=(0.0, opt), per_channel=0.5))) else: pipeline.add(sometimes(iaa.AdditiveGaussianNoise(loc=0, scale=(0.0, 0.05 * 255), per_channel=0.5))) @@ -699,6 +699,8 @@ def gaussian_scmap(self, joint_id, coords, data_item, size, scale): # Grid of coordinates grid = np.mgrid[:height, :width].transpose((1, 2, 0)) grid = grid * stride + half_stride + # NOTE @C-Achard 2026--03-17: x and y were never assigned, added here + y, x = np.rollaxis(grid, 2) # the animal id plays no role for scoremap + locref! # so let's just loop over all bpts. for k, j_id in enumerate(np.concatenate(joint_id)): diff --git a/deeplabcut/pose_estimation_tensorflow/predict_videos.py b/deeplabcut/pose_estimation_tensorflow/predict_videos.py index c4f987bbd4..3b9979b250 100644 --- a/deeplabcut/pose_estimation_tensorflow/predict_videos.py +++ b/deeplabcut/pose_estimation_tensorflow/predict_videos.py @@ -66,8 +66,10 @@ def create_tracking_dataset( ): try: from deeplabcut.pose_tracking_pytorch import create_triplets_dataset - except ModuleNotFoundError: - raise ModuleNotFoundError("Unsupervised identity learning requires PyTorch. Please run `pip install torch`.") + except ModuleNotFoundError as e: + raise ModuleNotFoundError( + "Unsupervised identity learning requires PyTorch. Please run `pip install torch`." + ) from e from deeplabcut.pose_estimation_tensorflow.predict_multianimal import ( extract_bpt_feature_from_video, @@ -103,10 +105,10 @@ def create_tracking_dataset( path_test_config = Path(modelfolder) / "test" / "pose_cfg.yaml" try: dlc_cfg = load_config(str(path_test_config)) - except FileNotFoundError: + except FileNotFoundError as e: raise FileNotFoundError( f"It seems the model for shuffle {shuffle} and trainFraction {trainFraction} does not exist." - ) + ) from e Snapshots = auxiliaryfunctions.get_snapshots_from_folder( train_folder=Path(modelfolder) / "train", @@ -114,7 +116,10 @@ def create_tracking_dataset( if cfg["snapshotindex"] == "all": print( - "Snapshotindex is set to 'all' in the config.yaml file. Running video analysis with all snapshots is very costly! Use the function 'evaluate_network' to choose the best the snapshot. For now, changing snapshot index to -1!" + "Snapshotindex is set to 'all' in the config.yaml file. " + "Running video analysis with all snapshots is very costly! " + "Use the function 'evaluate_network' to choose the best the snapshot. " + "For now, changing snapshot index to -1!" ) snapshotindex = -1 else: @@ -150,7 +155,9 @@ def create_tracking_dataset( TFGPUinference = False dlc_cfg["batch_size"] = 1 print( - "Switching batchsize to 1, num_outputs (per animal) to 1 and TFGPUinference to False (all these features are not supported in this mode)." + "Switching batchsize to 1, " + "num_outputs (per animal) to 1 and TFGPUinference to False " + "(all these features are not supported in this mode)." ) # Name for scorer: @@ -164,7 +171,8 @@ def create_tracking_dataset( if dlc_cfg["num_outputs"] > 1: if TFGPUinference: print( - "Switching to numpy-based keypoint extraction code, as multiple point extraction is not supported by TF code currently." + "Switching to numpy-based keypoint extraction code, " + "as multiple point extraction is not supported by TF code currently." ) TFGPUinference = False print("Extracting ", dlc_cfg["num_outputs"], "instances per bodypart") @@ -225,14 +233,17 @@ def create_tracking_dataset( os.chdir(str(start_path)) if "multi-animal" in dlc_cfg["dataset_type"]: print( - "If the tracking is not satisfactory for some videos, consider expanding the training set. You can use the function 'extract_outlier_frames' to extract a few representative outlier frames." + "If the tracking is not satisfactory for some videos, consider expanding the training set. " + "You can use the function 'extract_outlier_frames' to extract a few representative outlier frames." ) else: print( - "The videos are analyzed. Now your research can truly start! \n You can create labeled videos with 'create_labeled_video'" + "The videos are analyzed. Now your research can truly start! " + "\n You can create labeled videos with 'create_labeled_video'" ) print( - "If the tracking is not satisfactory for some videos, consider expanding the training set. You can use the function 'extract_outlier_frames' to extract a few representative outlier frames." + "If the tracking is not satisfactory for some videos, consider expanding the training set. " + "You can use the function 'extract_outlier_frames' to extract a few representative outlier frames." ) return DLCscorer # note: this is either DLCscorer or DLCscorerlegacy depending on what was used! else: @@ -333,10 +344,14 @@ def analyze_videos( Source: https://arxiv.org/abs/1909.11229 dynamic: tuple(bool, float, int) triple containing (state, detectiontreshold, margin) - If the state is true, then dynamic cropping will be performed. That means that if an object is detected (i.e. any body part > detectiontreshold), - then object boundaries are computed according to the smallest/largest x position and smallest/largest y position of all body parts. This window is - expanded by the margin and from then on only the posture within this crop is analyzed (until the object is lost, i.e. detectiontreshold), + then object boundaries are computed according to + the smallest/largest x position and smallest/largest y position of all body parts. + This window is expanded by the margin and from then on only the posture within + this crop is analyzed (until the object is lost, i.e. 1: if TFGPUinference: print( - "Switching to numpy-based keypoint extraction code, as multiple point extraction is not supported by TF code currently." + "Switching to numpy-based keypoint extraction code, " + "as multiple point extraction is not supported by TF code currently." ) TFGPUinference = False print("Extracting ", dlc_cfg["num_outputs"], "instances per bodypart") @@ -641,17 +662,21 @@ def analyze_videos( os.chdir(str(start_path)) if "multi-animal" in dlc_cfg["dataset_type"]: print( - "The videos are analyzed. Time to assemble animals and track 'em... \n Call 'create_video_with_all_detections' to check multi-animal detection quality before tracking." + "The videos are analyzed. Time to assemble animals and track 'em... \n" + " Call 'create_video_with_all_detections' to check multi-animal detection quality before tracking." ) print( - "If the tracking is not satisfactory for some videos, consider expanding the training set. You can use the function 'extract_outlier_frames' to extract a few representative outlier frames." + "If the tracking is not satisfactory for some videos, consider expanding the training set. " + "You can use the function 'extract_outlier_frames' to extract a few representative outlier frames." ) else: print( - "The videos are analyzed. Now your research can truly start! \n You can create labeled videos with 'create_labeled_video'" + "The videos are analyzed. Now your research can truly start! \n " + "You can create labeled videos with 'create_labeled_video'" ) print( - "If the tracking is not satisfactory for some videos, consider expanding the training set. You can use the function 'extract_outlier_frames' to extract a few representative outlier frames." + "If the tracking is not satisfactory for some videos, consider expanding the training set. " + "You can use the function 'extract_outlier_frames' to extract a few representative outlier frames." ) return DLCscorer # note: this is either DLCscorer or DLCscorerlegacy depending on what was used! else: @@ -661,9 +686,9 @@ def analyze_videos( def checkcropping(cfg, cap): print( - "Cropping based on the x1 = {} x2 = {} y1 = {} y2 = {}. You can adjust the cropping coordinates in the config.yaml file.".format( - cfg["x1"], cfg["x2"], cfg["y1"], cfg["y2"] - ) + "Cropping based on the " + f"x1 = {cfg['x1']} x2 = {cfg['x2']} y1 = {cfg['y1']} y2 = {cfg['y2']}. " + "You can adjust the cropping coordinates in the config.yaml file." ) nx = cfg["x2"] - cfg["x1"] ny = cfg["y2"] - cfg["y1"] @@ -902,7 +927,8 @@ def GetPoseDynamic(cfg, dlc_cfg, sess, inputs, outputs, cap, nframes, detectiont else: if ( detected and (x1 + y1 + y2 - ny + x2 - nx) != 0 - ): # was detected in last frame and dyn. cropping was performed >> but object lost in cropped variant >> re-run on full frame! + ): # was detected in last frame and dyn. cropping was performed >> + # but object lost in cropped variant >> re-run on full frame! # print("looking again, lost!") if cfg["cropping"]: frame = img_as_ubyte(originalframe[cfg["y1"] : cfg["y2"], cfg["x1"] : cfg["x2"]]) @@ -949,11 +975,11 @@ def AnalyzeVideo( vname = Path(video).stem try: _ = auxiliaryfunctions.load_analyzed_data(destfolder, vname, DLCscorer) - except FileNotFoundError: + except FileNotFoundError as e: print("Loading ", video) cap = cv2.VideoCapture(video) if not cap.isOpened(): - raise OSError("Video could not be opened. Please check that the the file integrity.") + raise OSError("Video could not be opened. Please check the file integrity.") from e # https://docs.opencv.org/2.4/modules/highgui/doc/reading_and_writing_images_and_video.html#videocapture-get fps = cap.get(cv2.CAP_PROP_FPS) nframes = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) @@ -1052,8 +1078,7 @@ def AnalyzeVideo( range(nframes), save_as_csv, ) - finally: - return DLCscorer + return DLCscorer def GetPosesofFrames(cfg, dlc_cfg, sess, inputs, outputs, directory, framelist, nframes, batchsize): @@ -1077,7 +1102,8 @@ def GetPosesofFrames(cfg, dlc_cfg, sess, inputs, outputs, directory, framelist, batch_num = 0 # keeps track of which batch you are at if cfg["cropping"]: print( - "Cropping based on the x1 = {} x2 = {} y1 = {} y2 = {}. You can adjust the cropping coordinates in the config.yaml file.".format( + "Cropping based on the x1 = {} x2 = {} y1 = {} y2 = {}. " + "You can adjust the cropping coordinates in the config.yaml file.".format( cfg["x1"], cfg["x2"], cfg["y1"], cfg["y2"] ) ) @@ -1153,12 +1179,17 @@ def analyze_time_lapse_frames( """Analyzed all images (of type = frametype) in a folder and stores the output in one file. - You can crop the frames (before analysis), by changing 'cropping'=True and setting 'x1','x2','y1','y2' in the config file. + You can crop the frames (before analysis), + by changing 'cropping'=True and setting 'x1','x2','y1','y2' in the config file. - Output: The labels are stored as MultiIndex Pandas Array, which contains the name of the network, body part name, (x, y) label position \n - in pixels, and the likelihood for each frame per body part. These arrays are stored in an efficient Hierarchical Data Format (HDF) \n - in the same directory, where the video is stored. However, if the flag save_as_csv is set to True, the data can also be exported in \n - comma-separated values format (.csv), which in turn can be imported in many programs, such as MATLAB, R, Prism, etc. + Output: + The labels are stored as MultiIndex Pandas Array, + which contains the name of the network, body part name, (x, y) label position \n + in pixels, and the likelihood for each frame per body part. + These arrays are stored in an efficient Hierarchical Data Format (HDF) \n + in the same directory, where the video is stored. + However, if the flag save_as_csv is set to True, the data can also be exported in \n + comma-separated values format (.csv), which in turn can be imported in many programs, such as MATLAB, R, Prism, etc. Parameters ---------- @@ -1169,27 +1200,35 @@ def analyze_time_lapse_frames( Full path to directory containing the frames that shall be analyzed frametype: string, optional - Checks for the file extension of the frames. Only images with this extension are analyzed. The default is ``.png`` + Checks for the file extension of the frames. + Only images with this extension are analyzed. The default is ``.png`` shuffle: int, optional An integer specifying the shuffle index of the training dataset used for training the network. The default is 1. trainingsetindex: int, optional - Integer specifying which TrainingsetFraction to use. By default the first (note that TrainingFraction is a list in config.yaml). + Integer specifying which TrainingsetFraction to use. + By default the first (note that TrainingFraction is a list in config.yaml). - gputouse: int, optional. Natural number indicating the number of your GPU (see number in nvidia-smi). If you do not have a GPU put None. + gputouse: int, optional. Natural number indicating the number of your GPU (see number in nvidia-smi). + If you do not have a GPU, set to None. See: https://nvidia.custhelp.com/app/answers/detail/a_id/3751/~/useful-nvidia-smi-queries save_as_csv: bool, optional - Saves the predictions in a .csv file. The default is ``False``; if provided it must be either ``True`` or ``False`` + Saves the predictions in a .csv file. The default is ``False``; + if provided it must be either ``True`` or ``False`` Examples -------- If you want to analyze all frames in /analysis/project/timelapseexperiment1 - >>> deeplabcut.analyze_videos('/analysis/project/reaching-task/config.yaml','/analysis/project/timelapseexperiment1') + >>> deeplabcut.analyze_videos( + '/analysis/project/reaching-task/config.yaml', + '/analysis/project/timelapseexperiment1' + ) -------- - Note: for test purposes one can extract all frames from a video with ffmeg, e.g. ffmpeg -i testvideo.avi thumb%04d.png + Note: for test purposes one can extract all frames from a video with ffmpeg, + e.g. ffmpeg -i testvideo.avi thumb%04d.png """ if "TF_CUDNN_USE_AUTOTUNE" in os.environ: del os.environ["TF_CUDNN_USE_AUTOTUNE"] # was potentially set during training @@ -1209,10 +1248,10 @@ def analyze_time_lapse_frames( path_test_config = Path(modelfolder) / "test" / "pose_cfg.yaml" try: dlc_cfg = load_config(str(path_test_config)) - except FileNotFoundError: + except FileNotFoundError as e: raise FileNotFoundError( f"It seems the model for shuffle {shuffle} and trainFraction {trainFraction} does not exist." - ) + ) from e Snapshots = auxiliaryfunctions.get_snapshots_from_folder( train_folder=Path(modelfolder) / "train", @@ -1220,7 +1259,10 @@ def analyze_time_lapse_frames( if cfg["snapshotindex"] == "all": print( - "Snapshotindex is set to 'all' in the config.yaml file. Running video analysis with all snapshots is very costly! Use the function 'evaluate_network' to choose the best the snapshot. For now, changing snapshot index to -1!" + "Snapshotindex is set to 'all' in the config.yaml file. " + "Running video analysis with all snapshots is very costly! " + "Use the function 'evaluate_network' to choose the best the snapshot. " + "For now, changing snapshot index to -1!" ) snapshotindex = -1 else: @@ -1459,24 +1501,28 @@ def convert_detections2tracklets( Full path of the config.yaml file as a string. videos : list - A list of strings containing the full paths to videos for analysis or a path to the directory, where all the videos with same extension are stored. + A list of strings containing the full paths to videos for analysis + or a path to the directory, where all the videos with same extension are stored. videotype: string, optional - Checks for the extension of the video in case the input to the video is a directory.\n Only videos with this extension are analyzed. + Checks for the extension of the video in case the input to the video is a directory.\n + Only videos with this extension are analyzed. If left unspecified, videos with common extensions ('avi', 'mp4', 'mov', 'mpeg', 'mkv') are kept. shuffle: int, optional - An integer specifying the shuffle index of the training dataset used for training the network. The default is 1. + An integer specifying the shuffle index of the training dataset used for training the network. + The default is 1. trainingsetindex: int, optional - Integer specifying which TrainingsetFraction to use. By default the first (note that TrainingFraction is a list in config.yaml). + Integer specifying which TrainingsetFraction to use. + By default the first (note that TrainingFraction is a list in config.yaml). overwrite: bool, optional. Overwrite tracks file i.e. recompute tracks from full detections and overwrite. destfolder: string, optional - Specifies the destination folder for analysis data (default is the path of the video). Note that for subsequent analysis this - folder also needs to be passed. + Specifies the destination folder for analysis data (default is the path of the video). + Note that for subsequent analysis this folder also needs to be passed. ignore_bodyparts: optional List of body part names that should be ignored during tracking (advanced). @@ -1511,10 +1557,19 @@ def convert_detections2tracklets( Examples -------- If you want to convert detections to tracklets: - >>> deeplabcut.convert_detections2tracklets('/analysis/project/reaching-task/config.yaml',[]'/analysis/project/video1.mp4'], videotype='.mp4') + >>> deeplabcut.convert_detections2tracklets( + '/analysis/project/reaching-task/config.yaml', + ['/analysis/project/video1.mp4'], + videotype='.mp4' + ) If you want to convert detections to tracklets based on box_tracker: - >>> deeplabcut.convert_detections2tracklets('/analysis/project/reaching-task/config.yaml',[]'/analysis/project/video1.mp4'], videotype='.mp4',track_method='box') + >>> deeplabcut.convert_detections2tracklets( + '/analysis/project/reaching-task/config.yaml', + ['/analysis/project/video1.mp4'], + videotype='.mp4', + track_method='box' + ) -------- """ @@ -1544,10 +1599,10 @@ def convert_detections2tracklets( path_test_config = Path(modelfolder) / "test" / "pose_cfg.yaml" try: dlc_cfg = load_config(str(path_test_config)) - except FileNotFoundError: + except FileNotFoundError as e: raise FileNotFoundError( f"It seems the model for shuffle {shuffle} and trainFraction {trainFraction} does not exist." - ) + ) from e if "multi-animal" not in dlc_cfg["dataset_type"]: raise ValueError("This function is only required for multianimal projects!") @@ -1571,7 +1626,10 @@ def convert_detections2tracklets( if cfg["snapshotindex"] == "all": print( - "Snapshotindex is set to 'all' in the config.yaml file. Running video analysis with all snapshots is very costly! Use the function 'evaluate_network' to choose the best the snapshot. For now, changing snapshot index to -1!" + "Snapshotindex is set to 'all' in the config.yaml file. " + "Running video analysis with all snapshots is very costly! " + "Use the function 'evaluate_network' to choose the best the snapshot. " + "For now, changing snapshot index to -1!" ) snapshotindex = -1 else: @@ -1740,7 +1798,8 @@ def convert_detections2tracklets( os.chdir(str(start_path)) print( - "The tracklets were created (i.e., under the hood deeplabcut.convert_detections2tracklets was run). Now you can 'refine_tracklets' in the GUI, or run 'deeplabcut.stitch_tracklets'." + "The tracklets were created (i.e., under the hood deeplabcut.convert_detections2tracklets was run). " + "Now you can 'refine_tracklets' in the GUI, or run 'deeplabcut.stitch_tracklets'." ) else: print("No video(s) found. Please check your path!") diff --git a/examples/JUPYTER/Demo_3D_DeepLabCut.ipynb b/examples/JUPYTER/Demo_3D_DeepLabCut.ipynb index 7ac1a97f0a..5443f784dd 100644 --- a/examples/JUPYTER/Demo_3D_DeepLabCut.ipynb +++ b/examples/JUPYTER/Demo_3D_DeepLabCut.ipynb @@ -151,7 +151,7 @@ "metadata": {}, "outputs": [], "source": [ - "deeplabcut.calibrate_cameras(config_path3d, cbrow=9, cbcol=6, calibrate=False, alpha=0.9)" + "deeplabcut.calibrate_cameras(config_path, cbrow=9, cbcol=6, calibrate=False, alpha=0.9)" ] }, { @@ -179,7 +179,7 @@ "metadata": {}, "outputs": [], "source": [ - "deeplabcut.calibrate_cameras(config_path3d, cbrow=9, cbcol=6, calibrate=True, alpha=0.9)" + "deeplabcut.calibrate_cameras(config_path, cbrow=9, cbcol=6, calibrate=True, alpha=0.9)" ] }, { @@ -199,7 +199,7 @@ "source": [ "%matplotlib inline\n", "\n", - "deeplabcut.check_undistortion(config_path3d)" + "deeplabcut.check_undistortion(config_path)" ] }, { @@ -243,7 +243,7 @@ "\n", "video_path = \"/home/yourname/videoFolder\"\n", "\n", - "deeplabcut.triangulate(config_path3d, video_path, videotype=\"mp4\")" + "deeplabcut.triangulate(config_path, video_path, videotype=\"mp4\")" ] }, { From 6273683e4daf5e4173500cc7bbb10fb161ba51f5 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 17 Mar 2026 17:36:25 +0100 Subject: [PATCH 21/80] Update bare except: --- deeplabcut/create_project/add.py | 16 ++++-- .../frame_extraction.py | 15 +++--- .../datasets/base.py | 37 ++++++-------- .../datasets/ma_dlc.py | 2 +- .../datasets/ma_dlc_dataframe.py | 14 +++-- .../datasets/materialize.py | 4 +- .../generalized_data_converter/utils.py | 2 +- .../pose_estimation_3d/camera_calibration.py | 51 +++++++++++++------ .../pose_estimation_tensorflow/config.py | 2 +- .../model/backbones/vit_pytorch.py | 2 +- .../refine_training_dataset/outlier_frames.py | 15 ++++-- deeplabcut/utils/auxiliaryfunctions_3d.py | 2 +- deeplabcut/utils/make_labeled_video.py | 33 +++++++----- docs/recipes/BatchProcessing.md | 2 +- 14 files changed, 121 insertions(+), 76 deletions(-) diff --git a/deeplabcut/create_project/add.py b/deeplabcut/create_project/add.py index 5f76792ed7..ef03b50986 100644 --- a/deeplabcut/create_project/add.py +++ b/deeplabcut/create_project/add.py @@ -35,13 +35,21 @@ def add_new_videos(config, videos, copy_videos=False, coords=None, extract_frame Examples -------- Video will be added, with cropping dimensions according to the frame dimensions of mouse5.avi - >>> deeplabcut.add_new_videos('/home/project/reaching-task-Tanmay-2018-08-23/config.yaml',['/data/videos/mouse5.avi']) + >>> deeplabcut.add_new_videos( + '/home/project/reaching-task-Tanmay-2018-08-23/config.yaml',['/data/videos/mouse5.avi'] + ) Video will be added, with cropping dimensions [0,100,0,200] - >>> deeplabcut.add_new_videos('/home/project/reaching-task-Tanmay-2018-08-23/config.yaml',['/data/videos/mouse5.avi'],copy_videos=False,coords=[[0,100,0,200]]) + >>> deeplabcut.add_new_videos( + '/home/project/reaching-task-Tanmay-2018-08-23/config.yaml', + ['/data/videos/mouse5.avi'],copy_videos=False,coords=[[0,100,0,200]] + ) Two videos will be added, with cropping dimensions [0,100,0,200] and [0,100,0,250], respectively. - >>> deeplabcut.add_new_videos('/home/project/reaching-task-Tanmay-2018-08-23/config.yaml',['/data/videos/mouse5.avi','/data/videos/mouse6.avi'],copy_videos=False,coords=[[0,100,0,200],[0,100,0,250]]) + >>> deeplabcut.add_new_videos( + '/home/project/reaching-task-Tanmay-2018-08-23/config.yaml', + ['/data/videos/mouse5.avi','/data/videos/mouse6.avi'], + copy_videos=False,coords=[[0,100,0,200],[0,100,0,250]]) """ import os import shutil @@ -108,7 +116,7 @@ def add_new_videos(config, videos, copy_videos=False, coords=None, extract_frame # For windows os.path.realpath does not work and does not link to the real video. video_path = str(Path.resolve(Path(video))) # video_path = os.path.realpath(video) - except: + except Exception: video_path = os.readlink(video) vid = VideoReader(video_path) diff --git a/deeplabcut/generate_training_dataset/frame_extraction.py b/deeplabcut/generate_training_dataset/frame_extraction.py index f0a1f36953..3605b9aca7 100755 --- a/deeplabcut/generate_training_dataset/frame_extraction.py +++ b/deeplabcut/generate_training_dataset/frame_extraction.py @@ -421,7 +421,8 @@ def extract_frames( io.imsave(img_name, image) if np.var(image) == 0: # constant image print( - "Seems like black/constant images are extracted from your video. Perhaps consider using opencv under the hood, by setting: opencv=True" + "Seems like black/constant images are extracted from your video." + "Perhaps consider using opencv under the hood, by setting: opencv=True" ) is_valid.append(True) except FileNotFoundError: @@ -447,7 +448,8 @@ def extract_frames( print("Frames were successfully extracted, for the videos listed in the config.yaml file.") print( "\nYou can now label the frames using the function 'label_frames' " - "(Note, you should label frames extracted from diverse videos (and many videos; we do not recommend training on single videos!))." + "(Note, you should label frames extracted from diverse videos " + "(and many videos; we do not recommend training on single videos!))." ) return has_failed @@ -465,10 +467,10 @@ def extract_frames( os.path.join(project_path, "videos/") try: cfg_3d = auxiliaryfunctions.read_config(config3d) - except: + except Exception as e: raise Exception( "You must create a 3D project and edit the 3D config file before extracting matched frames. \n" - ) + ) from e cams = cfg_3d["camera_names"] extCam_name = cams[extracted_cam] del cams[extracted_cam] @@ -540,6 +542,7 @@ def extract_frames( else: print( - "Invalid MODE. Choose either 'manual', 'automatic' or 'match'. Check ``help(deeplabcut.extract_frames)`` on python and ``deeplabcut.extract_frames?`` \ - for ipython/jupyter notebook for more details." + "Invalid MODE. Choose either 'manual', 'automatic' or 'match'. " + "Check ``help(deeplabcut.extract_frames)`` on python and ``deeplabcut.extract_frames?``" + " for ipython/jupyter notebook for more details." ) diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/base.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/base.py index 5a41f81f7c..cde1d07167 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/base.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/base.py @@ -23,10 +23,10 @@ def raw_2_imagename_with_id(image): - """ - raw image data has filename and id. - we modify the imagename such that itis composed of - both original imagename and image id + """Raw image data has filename and id. + + we modify the imagename such that itis composed of both original imagename and image + id """ file_name = image["file_name"] @@ -37,9 +37,7 @@ def raw_2_imagename_with_id(image): def raw_2_imagename(image): - """ - Only getting the imagename part from the image object - """ + """Only getting the imagename part from the image object.""" file_name = image["file_name"] image_name = file_name.split(os.sep)[-1] @@ -47,9 +45,10 @@ def raw_2_imagename(image): class BasePoseDataset: - """ - Dual representation of generic and raw data. For classes that inherits this class, - the raw data is kept but generic data is populated so you have dual representation. + """Dual representation of generic and raw data. + + For classes that inherits this class, the raw data is kept but generic data is + populated so you have dual representation. """ def __init__(self): @@ -175,11 +174,9 @@ def materialize( ) def whether_anno_image_match(self, images, annotations): - """ - Every image id should be annotated at least once - There should not be any image that is not being annotated - There should not be any annotation for beyond the set of given images - """ + """Every image id should be annotated at least once There should not be any + image that is not being annotated There should not be any annotation for beyond + the set of given images.""" image_ids = set([image["id"] for image in images]) @@ -190,7 +187,7 @@ def whether_anno_image_match(self, images, annotations): print("len(images-annotatinos)", len(image_ids - annotation_image_ids)) print("annotations-images", annotation_image_ids - image_ids) print("len(annotations-images)", len(annotation_image_ids - image_ids)) - warnings.warn("annotation and image ids do not match") + warnings.warn("annotation and image ids do not match", stacklevel=2) def get_keypoints(self): # TODO make sure it's always one element in a list @@ -218,7 +215,7 @@ def _proj(self, annotations, conversion_table): for anno in annotations: try: kpts = anno["keypoints"] - except: + except Exception: print(anno) new_kpts = np.zeros(len(master_keypoints) * 3) @@ -281,10 +278,8 @@ def adjust_bbox_and_area(self): annotation["area"] = area def project_with_conversion_table(self, table_path="", table_dict=None): - """ - Replace the generic annotations with those that are in superset keypoint space - - """ + """Replace the generic annotations with those that are in superset keypoint + space.""" print(f"Converting {self.meta['dataset_name']}") keypoints = self.get_keypoints() diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc.py index e7caae65de..081bbbb755 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc.py @@ -77,7 +77,7 @@ def _df2generic(self, df, image_id_offset=0): category_id = 0 try: kpts = data.xs(individual, level="individuals").to_numpy().reshape((-1, 2)) - except: + except Exception: # somehow there are duplicates. So only use the first occurrence data = data.iloc[0] kpts = data.xs(individual, level="individuals").to_numpy().reshape((-1, 2)) diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc_dataframe.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc_dataframe.py index 2971bf999a..b85e6adffb 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc_dataframe.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/ma_dlc_dataframe.py @@ -30,7 +30,8 @@ def merge_annotateddatasets(cfg): This is a bit of a mess because of cross platform compatibility. - Within platform comp. is straightforward. But if someone labels on windows and wants to train on a unix cluster or colab... + Within platform comp. is straightforward. + But if someone labels on windows and wants to train on a unix cluster or colab... """ AnnotationData = [] data_path = Path(os.path.join(cfg["project_path"], "labeled-data")) @@ -43,7 +44,11 @@ def merge_annotateddatasets(cfg): conversioncode.guarantee_multiindex_rows(data) if data.columns.levels[0][0] != cfg["scorer"]: print( - f"{file_path} labeled by a different scorer. This data will not be utilized in training dataset creation. If you need to merge datasets across scorers, see https://github.com/DeepLabCut/DeepLabCut/wiki/Using-labeled-data-in-DeepLabCut-that-was-annotated-elsewhere-(or-merge-across-labelers)" + f"{file_path} labeled by a different scorer. " + "This data will not be utilized in training dataset creation. " + "If you need to merge datasets across scorers, see " + "https://github.com/DeepLabCut/DeepLabCut/wiki/" + "Using-labeled-data-in-DeepLabCut-that-was-annotated-elsewhere-(or-merge-across-labelers)" ) continue AnnotationData.append(data) @@ -52,7 +57,8 @@ def merge_annotateddatasets(cfg): if not len(AnnotationData): print( - "Annotation data was not found by splitting video paths (from config['video_sets']). An alternative route is taken..." + "Annotation data was not found by splitting video paths (from config['video_sets'])." + " An alternative route is taken..." ) AnnotationData = conversioncode.merge_windowsannotationdataONlinuxsystem(cfg) if not len(AnnotationData): @@ -184,7 +190,7 @@ def _df2generic(self, df, image_id_offset=0): category_id = 0 try: kpts = data.xs(individual, level="individuals").to_numpy().reshape((-1, 2)) - except: + except Exception: # somehow there are duplicates. So only use the first occurrence data = data.iloc[0] kpts = data.xs(individual, level="individuals").to_numpy().reshape((-1, 2)) diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/materialize.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/materialize.py index 519ed6cebe..1098ce8afb 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/materialize.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/materialize.py @@ -34,7 +34,7 @@ def get_filename(filename): - if type(filename) == tuple: + if isinstance(filename, tuple): filename = os.path.join(*filename) return filename @@ -401,7 +401,7 @@ def _generic2sdlc( else: try: os.symlink(file_name, dest) - except: + except Exception: pass if dataset_name == "AwA-Pose": diff --git a/deeplabcut/modelzoo/generalized_data_converter/utils.py b/deeplabcut/modelzoo/generalized_data_converter/utils.py index 94025341c9..fafe97e46a 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/utils.py +++ b/deeplabcut/modelzoo/generalized_data_converter/utils.py @@ -29,7 +29,7 @@ def threshold_kpts(config_path, h5path, threshold_mean=0.9, threshold_min=0.1): scorer = df.columns.get_level_values("scorer").unique()[0] try: data = df[scorer]["individual0"] - except: + except Exception: data = df[scorer] cfg = auxiliaryfunctions.read_config(config_path) diff --git a/deeplabcut/pose_estimation_3d/camera_calibration.py b/deeplabcut/pose_estimation_3d/camera_calibration.py index a276b3f38d..75e6752a64 100644 --- a/deeplabcut/pose_estimation_3d/camera_calibration.py +++ b/deeplabcut/pose_estimation_3d/camera_calibration.py @@ -30,12 +30,17 @@ def calibrate_cameras(config, cbrow=8, cbcol=6, calibrate=False, alpha=0.4, sear the camera and stores the calibration files in the project folder (defined in the config file). - Make sure you have around 20-60 pairs of calibration images. The function should be used iteratively to select the right set of calibration images. + Make sure you have around 20-60 pairs of calibration images. + The function should be used iteratively to select the right set of calibration images. - A pair of calibration image is considered "correct", if the corners are detected correctly in both the images. It may happen that during the first run of this function, - the extracted corners are incorrect or the order of detected corners does not align for the corresponding views (i.e. camera-1 and camera-2 images). + A pair of calibration image is considered "correct", + if the corners are detected correctly in both the images. + It may happen that during the first run of this function, + the extracted corners are incorrect or the order of detected corners + does not align for the corresponding views (i.e. camera-1 and camera-2 images). - In such a case, remove those pairs of images and re-run this function. Once the right number of calibration images are selected, + In such a case, remove those pairs of images and re-run this function. + Once the right number of calibration images are selected, use the parameter ``calibrate=True`` to calibrate the cameras. Parameters @@ -50,11 +55,14 @@ def calibrate_cameras(config, cbrow=8, cbcol=6, calibrate=False, alpha=0.4, sear Integer specifying the number of columns in the calibration image. calibrate : bool - If this is set to True, the cameras are calibrated with the current set of calibration images. The default is ``False`` - Set it to True, only after checking the results of the corner detection method and removing dysfunctional images! + If this is set to True, the cameras are calibrated with the current set of calibration images. + The default is ``False`` + Set it to True, only after checking the results of the corner detection method + and removing dysfunctional images! alpha: float - Floating point number between 0 and 1 specifying the free scaling parameter. When alpha = 0, the rectified images with only valid pixels are stored + Floating point number between 0 and 1 specifying the free scaling parameter. + When alpha = 0, the rectified images with only valid pixels are stored i.e. the rectified images are zoomed in. When alpha = 1, all the pixels from the original images are retained. For more details: https://docs.opencv.org/2.4/modules/calib3d/doc/camera_calibration_and_3d_reconstruction.html @@ -95,7 +103,7 @@ def calibrate_cameras(config, cbrow=8, cbcol=6, calibrate=False, alpha=0.4, sear cfg_3d[str("config_file_" + cam_names[i])] = cfg_3d.pop(str("config_file_camera-" + str(i + 1))) for i in range(len(cam_names)): cfg_3d[str("shuffle_" + cam_names[i])] = cfg_3d.pop(str("shuffle_camera-" + str(i + 1))) - except: + except Exception: pass project_path = cfg_3d["project_path"] @@ -117,7 +125,9 @@ def calibrate_cameras(config, cbrow=8, cbcol=6, calibrate=False, alpha=0.4, sear images.sort(key=lambda f: int("".join(filter(str.isdigit, f)))) if len(images) == 0: raise Exception( - "No calibration images found. Make sure the calibration images are saved as .jpg and with prefix as the camera name as specified in the config.yaml file." + "No calibration images found. " + "Make sure the calibration images are saved as .jpg and " + "with prefix as the camera name as specified in the config.yaml file." ) skip_images = [] @@ -155,10 +165,13 @@ def calibrate_cameras(config, cbrow=8, cbcol=6, calibrate=False, alpha=0.4, sear try: h, w = img.shape[:2] - except: + except Exception as e: raise Exception( - "It seems that the name of calibration images does not match with the camera names in the config file. Please make sure that the calibration images are named with camera names as specified in the config.yaml file." - ) + "It seems that the name of calibration images does not match " + "with the camera names in the config file. " + "Please make sure that the calibration images are named" + " with camera names as specified in the config.yaml file." + ) from e # Perform calibration for each cameras and store the matrices as a pickle file if calibrate: @@ -183,7 +196,8 @@ def calibrate_cameras(config, cbrow=8, cbcol=6, calibrate=False, alpha=0.4, sear ), ) print( - f"Saving intrinsic camera calibration matrices for {cam} as a pickle file in {os.path.join(path_camera_matrix)}" + f"Saving intrinsic camera calibration matrices for {cam}" + f" as a pickle file in {os.path.join(path_camera_matrix)}" ) # Compute mean re-projection errors for individual cameras @@ -253,14 +267,18 @@ def calibrate_cameras(config, cbrow=8, cbcol=6, calibrate=False, alpha=0.4, sear } print( - f"Saving the stereo parameters for every pair of cameras as a pickle file in {str(os.path.join(path_camera_matrix))}" + "Saving the stereo parameters for every " + f"pair of cameras as a pickle file in {str(os.path.join(path_camera_matrix))}" ) auxiliaryfunctions.write_pickle(os.path.join(path_camera_matrix, "stereo_params.pickle"), stereo_params) print("Camera calibration done! Use the function ``check_undistortion`` to check the check the calibration") else: print( - f"Corners extracted! You may check for the extracted corners in the directory {str(path_corners)} and remove the pair of images where the corners are incorrectly detected. If all the corners are detected correctly with right order, then re-run the same function and use the flag ``calibrate=True``, to calbrate the camera." + f"Corners extracted! You may check for the extracted corners in the directory {str(path_corners)}" + " and remove the pair of images where the corners are incorrectly detected. " + "If all the corners are detected correctly with right order, " + "then re-run the same function and use the flag ``calibrate=True``, to calbrate the camera." ) @@ -281,7 +299,8 @@ def check_undistortion(config, cbrow=8, cbcol=6, plot=True): Int specifying the number of columns in the calibration image. plot : bool - If this is set to True, the results of undistortion are saved as plots. The default is ``True``; if provided it must be either ``True`` or ``False``. + If this is set to True, the results of undistortion are saved as plots. + The default is ``True``; if provided it must be either ``True`` or ``False``. Example -------- diff --git a/deeplabcut/pose_estimation_tensorflow/config.py b/deeplabcut/pose_estimation_tensorflow/config.py index a629c103f8..e44474eab1 100644 --- a/deeplabcut/pose_estimation_tensorflow/config.py +++ b/deeplabcut/pose_estimation_tensorflow/config.py @@ -33,7 +33,7 @@ def _merge_a_into_b(a, b): else: try: _merge_a_into_b(a[k], b[k]) - except: + except Exception: print(f"Error under config key: {k}") raise else: diff --git a/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py b/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py index a96b9cb6e1..a1e38510d0 100644 --- a/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py +++ b/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py @@ -327,7 +327,7 @@ def load_param(self, model_path): v = resize_pos_embed(v, self.pos_embed, self.patch_embed.num_y, self.patch_embed.num_x) try: self.state_dict()[k].copy_(v) - except: + except Exception: print("===========================ERROR=========================") print( f"shape do not match in k :{k}: param_dict{v.shape} vs self.state_dict(){ diff --git a/deeplabcut/refine_training_dataset/outlier_frames.py b/deeplabcut/refine_training_dataset/outlier_frames.py index a7140a50a4..32b535b1ee 100644 --- a/deeplabcut/refine_training_dataset/outlier_frames.py +++ b/deeplabcut/refine_training_dataset/outlier_frames.py @@ -262,7 +262,9 @@ def extract_outlier_frames( * ``'jump'`` identifies larger jumps than 'epsilon' in any body part * ``'uncertain'`` looks for frames with confidence below p_bound * ``'manual'`` launches a GUI from which the user can choose the frames - * ``'list'`` looks for user to provide a list of frame numbers to use, 'frames2use'. In this case, ``'extractionalgorithm'`` is forced to be ``'uniform.'`` + * ``'list'`` looks for user to provide a list of + frame numbers to use, 'frames2use'. + In this case, ``'extractionalgorithm'`` is forced to be ``'uniform.'`` frames2use: list[str], optional, default=None If ``'outlieralgorithm'`` is ``'list'``, provide the list of frames here. @@ -472,7 +474,8 @@ def extract_outlier_frames( frames2use = np.array(frames2use).astype("int") except ValueError(): print( - "Could not cast frames2use into np array, please check that frames2use is a simply a list of integers!" + "Could not cast frames2use into np array, " + "please check that frames2use is a simply a list of integers!" ) raise Indices.extend(frames2use) @@ -534,7 +537,8 @@ def extract_outlier_frames( print(e) print( "It seems the video has not been analyzed yet, or the video is not found! " - "You can only refine the labels after the a video is analyzed. Please run 'analyze_video' first. " + "You can only refine the labels after the a video is analyzed. " + "Please run 'analyze_video' first. " "Or, please double check your video file path" ) @@ -663,7 +667,8 @@ def attempt_to_add_video( Full path of the video to add to the project. copy_videos : bool, optional - If this is set to True, the videos will be copied to the project/videos directory. If False, the symlink of the + If this is set to True, the videos will be copied to the project/videos directory. + If False, the symlink of the videos will be copied instead. The default is ``False``; if provided it must be either ``True`` or ``False``. @@ -683,7 +688,7 @@ def attempt_to_add_video( try: add.add_new_videos(config, videos, coords=coords, copy_videos=copy_videos) - except: + except Exception: # can we make a catch here? - in fact we should drop indices from DataCombined # if they are in CollectedData.. [ideal behavior; currently pretty unlikely] print( diff --git a/deeplabcut/utils/auxiliaryfunctions_3d.py b/deeplabcut/utils/auxiliaryfunctions_3d.py index 08acafc512..af80e9e484 100644 --- a/deeplabcut/utils/auxiliaryfunctions_3d.py +++ b/deeplabcut/utils/auxiliaryfunctions_3d.py @@ -302,7 +302,7 @@ def _associate_paired_view_tracks(tracklets1, tracklets2, F): # Get average cost of the entire track cost = cost.mean() - except: + except Exception: # typically when dim 2 differs, with uniquebodyparts cost = 100000.0 diff --git a/deeplabcut/utils/make_labeled_video.py b/deeplabcut/utils/make_labeled_video.py index 79179b1be2..da6e1de160 100644 --- a/deeplabcut/utils/make_labeled_video.py +++ b/deeplabcut/utils/make_labeled_video.py @@ -566,14 +566,17 @@ def create_labeled_video( function used when True is f(x) = max(0, (x - pcutoff)/(1 - pcutoff)). plot_bboxes: bool, optional, default=True - If using Pytorch and in Top-Down mode, setting this to true will also plot the bounding boxes + If using Pytorch and in Top-Down mode, + setting this to true will also plot the bounding boxes bboxes_pcutoff, float, optional, default=None: - If plotting bounding boxes, this overrides the bboxes_pcutoff set in the model configuration. + If plotting bounding boxes, this overrides the bboxes_pcutoff + set in the model configuration. max_workers (int | None): - Maximum number of processes to use for multiprocessing. Set this parameter to limit the total RAM-usage of - simultaneous processes. Default: no maximum (i.e. number of spawned processes is based on the number of + Maximum number of processes to use for multiprocessing. + Set this parameter to limit the total RAM-usage of simultaneous processes. + Default: no maximum (i.e. number of spawned processes is based on the number of cores and the number of input videos). kwargs: additional arguments. @@ -1167,25 +1170,30 @@ def create_video_with_all_detections( where all the videos with same extension are stored. videotype: string, optional - Checks for the extension of the video in case the input to the video is a directory.\n Only videos with this extension are analyzed. + Checks for the extension of the video in case the input to the video is a directory.\n + Only videos with this extension are analyzed. If left unspecified, videos with common extensions ('avi', 'mp4', 'mov', 'mpeg', 'mkv') are kept. shuffle : int, optional Number of shuffles of training dataset. Default is set to 1. trainingsetindex: int, optional - Integer specifying which TrainingsetFraction to use. By default the first (note that TrainingFraction is a list in config.yaml). + Integer specifying which TrainingsetFraction to use. + By default the first (note that TrainingFraction is a list in config.yaml). displayedbodyparts: list of strings, optional - This selects the body parts that are plotted in the video. Either ``all``, then all body parts - from config.yaml are used orr a list of strings that are a subset of the full list. - E.g. ['hand','Joystick'] for the demo Reaching-Mackenzie-2018-08-30/config.yaml to select only these two body parts. + This selects the body parts that are plotted in the video. + Either ``all``, then all body parts from config.yaml are used or + a list of strings that are a subset of the full list. + E.g. ['hand','Joystick'] for the demo Reaching-Mackenzie-2018-08-30/config.yaml + to select only these two body parts. cropping: list[int], optional (default=None) If passed in, the [x1, x2, y1, y2] crop coordinates are used to shift detections appropriately. destfolder: string, optional - Specifies the destination folder that was used for storing analysis data (default is the path of the video). + Specifies the destination folder that was used for storing analysis data + (default is the path of the video). confidence_to_alpha: Union[bool, Callable[[float], float], default=False If False, all keypoints will be plot with alpha=1. Otherwise, this can be @@ -1194,7 +1202,8 @@ def create_video_with_all_detections( function used when True is f(x) = x. plot_bboxes: bool, optional (default=True) - If detections were produced using a Pytorch Top-Down model, setting this parameter to True will also plot + If detections were produced using a Pytorch Top-Down model, + setting this parameter to True will also plot the bounding boxes generated by the detector. kwargs: additional arguments. @@ -1332,7 +1341,7 @@ def create_video_with_all_detections( pass try: clip.save_frame(frame) - except: + except Exception: print(n, "frame writing error.") pass clip.close() diff --git a/docs/recipes/BatchProcessing.md b/docs/recipes/BatchProcessing.md index cd9dfef8a5..f157345f34 100644 --- a/docs/recipes/BatchProcessing.md +++ b/docs/recipes/BatchProcessing.md @@ -126,7 +126,7 @@ for project in Projects[model]: for vtype in ['.mp4','.m4v','.mpg']: try: deeplabcut.analyze_videos(config, [str(os.path.join(projectpath, "videos"))], shuffle=shuffle, videotype=vtype, save_as_csv=True) - except: + except Exception: pass print("DONE WITH ", project," resetting to original path") From cd37a7b88911cb045047ae5c4bc9bcee7571afbc Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 17 Mar 2026 17:36:48 +0100 Subject: [PATCH 22/80] Disable line too long and add specific exclusions --- pyproject.toml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3f227cbd6e..c2b1843457 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -136,8 +136,15 @@ line-length = 120 fix = true [tool.ruff.lint] select = [ "E", "F", "B", "I", "UP" ] -ignore = [ "E741" ] -per-file-ignores = { "__init__.py" = [ "F401", "E402" ] } +ignore = [ "E741", "E501" ] +[tool.ruff.lint.per-file-ignores] +"__init__.py" = [ "F401", "E402" ] +"deeplabcut/**/__init__.py" = [ "F403" ] +"deeplabcut/gui/window.py" = [ "F403" ] +"deeplabcut/pose_estimation_tensorflow/lib/crossvalutils.py" = [ "F403" ] +"deeplabcut/pose_estimation_tensorflow/lib/inferenceutils.py" = [ "F403" ] +"deeplabcut/pose_estimation_tensorflow/lib/trackingutils.py" = [ "F403" ] +"*.ipynb" = [ "E402" ] [tool.ruff.lint.pydocstyle] convention = "google" From 57bfbe8ed5904f506d4f40914582d18f15c5b5bc Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 17 Mar 2026 17:42:16 +0100 Subject: [PATCH 23/80] Update testscript_cli.py --- testscript_cli.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/testscript_cli.py b/testscript_cli.py index b4ec3b02f1..814b22cbd0 100644 --- a/testscript_cli.py +++ b/testscript_cli.py @@ -8,24 +8,23 @@ It produces nothing of interest scientifically. """ -task = "Testcore" # Enter the name of your experiment Task -scorer = "Mackenzie" # Enter the name of the experimenter/labeler - import os import platform import numpy as np import pandas as pd -# def install(package): -# subprocess.check_call([sys.executable, "-m", "pip", "install", package]) -# install("tensorflow==1.13.1") import deeplabcut as dlc from deeplabcut.core.engine import Engine +task = "Testcore" # Enter the name of your experiment Task +scorer = "Mackenzie" # Enter the name of the experimenter/labeler print("Imported DLC!") engine = Engine.PYTORCH +# def install(package): +# subprocess.check_call([sys.executable, "-m", "pip", "install", package]) +# install("tensorflow==1.13.1") basepath = os.path.dirname(os.path.abspath("testscript_cli.py")) videoname = "reachingvideo1" From 2440b58e2b3e8ffb6e905a00dc06bc26e772139d Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Tue, 17 Mar 2026 17:42:27 +0100 Subject: [PATCH 24/80] Disable minor loop variable check --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index c2b1843457..6a7cdfea81 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -136,7 +136,7 @@ line-length = 120 fix = true [tool.ruff.lint] select = [ "E", "F", "B", "I", "UP" ] -ignore = [ "E741", "E501" ] +ignore = [ "E741", "E501", "B007" ] [tool.ruff.lint.per-file-ignores] "__init__.py" = [ "F401", "E402" ] "deeplabcut/**/__init__.py" = [ "F403" ] From b4a8321390fea0ee00a206dcd6382c5c639bb9da Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 10:46:47 +0100 Subject: [PATCH 25/80] Fix old-style string format --- .../backbones/efficientnet_builder.py | 12 ++++++------ .../backbones/mobilenet.py | 6 +++--- .../datasets/pose_imgaug.py | 12 ++++++------ deeplabcut/pose_estimation_tensorflow/export.py | 11 +++-------- .../pose_estimation_tensorflow/nnets/conv_blocks.py | 10 ++++++---- deeplabcut/pose_estimation_tensorflow/nnets/utils.py | 10 ++++------ 6 files changed, 28 insertions(+), 33 deletions(-) diff --git a/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_builder.py b/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_builder.py index 49f8a10c68..d3ba8c89bf 100644 --- a/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_builder.py +++ b/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_builder.py @@ -74,13 +74,13 @@ def _decode_block_string(self, block_string): def _encode_block_string(self, block): """Encodes a block to a string.""" args = [ - "r%d" % block.num_repeat, - "k%d" % block.kernel_size, - "s%d%d" % (block.strides[0], block.strides[1]), + f"r{block.num_repeat}", + f"k{block.kernel_size}", + f"s{block.strides[0]}{block.strides[1]}", f"e{block.expand_ratio}", - "i%d" % block.input_filters, - "o%d" % block.output_filters, - "c%d" % block.conv_type, + f"i{block.input_filters}", + f"o{block.output_filters}", + f"c{block.conv_type}", ] if block.se_ratio > 0 and block.se_ratio <= 1: args.append(f"se{block.se_ratio}") diff --git a/deeplabcut/pose_estimation_tensorflow/backbones/mobilenet.py b/deeplabcut/pose_estimation_tensorflow/backbones/mobilenet.py index c382404dfd..4dd91a096d 100644 --- a/deeplabcut/pose_estimation_tensorflow/backbones/mobilenet.py +++ b/deeplabcut/pose_estimation_tensorflow/backbones/mobilenet.py @@ -239,11 +239,11 @@ def mobilenet_base( # pylint: disable=invalid-name else: params["use_explicit_padding"] = True - end_point = "layer_%d" % (i + 1) + end_point = f"layer_{i + 1}" try: net = opdef.op(net, **params) except Exception: - print("Failed to create op %i: %r params: %r" % (i, opdef, params)) + print(f"Failed to create op {i}: {opdef} params: {params}") raise end_points[end_point] = net scope = os.path.dirname(net.name) @@ -322,7 +322,7 @@ def mobilenet( is_training = mobilenet_args.get("is_training", False) input_shape = inputs.get_shape().as_list() if len(input_shape) != 4: - raise ValueError("Expected rank 4 input, was: %d" % len(input_shape)) + raise ValueError(f"Expected rank 4 input, was: {len(input_shape)}") with tf.compat.v1.variable_scope(scope, "Mobilenet", reuse=reuse) as scope: inputs = tf.identity(inputs, "input") diff --git a/deeplabcut/pose_estimation_tensorflow/datasets/pose_imgaug.py b/deeplabcut/pose_estimation_tensorflow/datasets/pose_imgaug.py index 0841d8343c..05d298fb96 100644 --- a/deeplabcut/pose_estimation_tensorflow/datasets/pose_imgaug.py +++ b/deeplabcut/pose_estimation_tensorflow/datasets/pose_imgaug.py @@ -56,7 +56,7 @@ def __init__(self, cfg): cfg["rotation"] = cfg.get("rotation", True) if cfg.get("rotation", True): # i.e. pm 10 degrees opt = cfg.get("rotation", False) - if type(opt) == int: + if isinstance(opt, int): cfg["rotation"] = cfg.get("rotation", 25) else: cfg["rotation"] = 25 @@ -74,7 +74,7 @@ def __init__(self, cfg): if cfg["motion_blur"]: cfg["motion_blur_params"] = dict(cfg.get("motion_blur_params", {"k": 7, "angle": (-90, 90)})) - print("Batch Size is %d" % self.batch_size) + print(f"Batch Size is {self.batch_size}") def load_dataset(self): cfg = self.cfg @@ -149,14 +149,14 @@ def sometimes(aug): cfg = self.cfg if cfg["mirror"]: opt = cfg["mirror"] # fliplr - if type(opt) == int: + if isinstance(opt, int): pipeline.add(sometimes(iaa.Fliplr(opt))) else: pipeline.add(sometimes(iaa.Fliplr(0.5))) if cfg.get("fliplr", False) and cfg.get("symmetric_pairs"): opt = cfg.get("fliplr", False) - if type(opt) == int: + if isinstance(opt, int): p = opt else: p = 0.5 @@ -190,7 +190,7 @@ def sometimes(aug): if cfg.get("gaussian_noise", False): opt = cfg.get("gaussian_noise", False) - if type(opt) == int or type(opt) == float: + if isinstance(opt, (int, float)): pipeline.add(sometimes(iaa.AdditiveGaussianNoise(loc=0, scale=(0.0, opt), per_channel=0.5))) else: pipeline.add(sometimes(iaa.AdditiveGaussianNoise(loc=0, scale=(0.0, 0.05 * 255), per_channel=0.5))) @@ -309,7 +309,7 @@ def get_batch(self): data_items.append(data_item) im_file = data_item.im_path - logging.debug("image %s", im_file) + logging.debug(f"image {im_file}") image = imread(os.path.join(self.cfg["project_path"], im_file), mode="skimage") if self.has_gt: diff --git a/deeplabcut/pose_estimation_tensorflow/export.py b/deeplabcut/pose_estimation_tensorflow/export.py index d1e716069a..7924b9af59 100644 --- a/deeplabcut/pose_estimation_tensorflow/export.py +++ b/deeplabcut/pose_estimation_tensorflow/export.py @@ -117,10 +117,10 @@ def load_model(cfg, shuffle=1, trainingsetindex=0, TFGPUinference=True, modelpre try: dlc_cfg = load_config(str(path_train_config)) # dlc_cfg_train = load_config(str(path_train_config)) - except FileNotFoundError: + except FileNotFoundError as e: raise FileNotFoundError( f"It seems the model for shuffle {shuffle} and trainFraction {train_fraction} does not exist." - ) + ) from e Snapshots = auxiliaryfunctions.get_snapshots_from_folder( train_folder=Path(model_folder) / "train", @@ -280,12 +280,7 @@ def export_model( if not os.path.isdir(export_dir): os.mkdir(export_dir) - sub_dir_name = "DLC_%s_%s_iteration-%d_shuffle-%d" % ( - cfg["Task"], - dlc_cfg["net_type"], - cfg["iteration"], - shuffle, - ) + sub_dir_name = f"DLC_{cfg['Task']}_{dlc_cfg['net_type']}_iteration-{cfg['iteration']}_shuffle-{shuffle}" full_export_dir = os.path.normpath(export_dir + "/" + sub_dir_name) if os.path.isdir(full_export_dir): diff --git a/deeplabcut/pose_estimation_tensorflow/nnets/conv_blocks.py b/deeplabcut/pose_estimation_tensorflow/nnets/conv_blocks.py index 8dc6f3bcb9..0f72ac187c 100644 --- a/deeplabcut/pose_estimation_tensorflow/nnets/conv_blocks.py +++ b/deeplabcut/pose_estimation_tensorflow/nnets/conv_blocks.py @@ -173,7 +173,7 @@ def expand_input_by_factor(n, divisible_by=8): def expanded_conv( input_tensor, num_outputs, - expansion_size=expand_input_by_factor(6), + expansion_size=None, stride=1, rate=1, kernel_size=(3, 3), @@ -244,9 +244,11 @@ def expanded_conv( tf.compat.v1.variable_scope(scope, default_name="expanded_conv") as s, tf.compat.v1.name_scope(s.original_name_scope), ): + if expansion_size is None: + expansion_size = expand_input_by_factor(6) prev_depth = input_tensor.get_shape().as_list()[3] if depthwise_location not in [None, "input", "output", "expansion"]: - raise TypeError("%r is unknown value for depthwise_location" % depthwise_location) + raise TypeError(f"{depthwise_location!r} is unknown value for depthwise_location") if use_explicit_padding: if padding != "SAME": raise TypeError('`use_explicit_padding` should only be used with "SAME" padding.') @@ -365,8 +367,8 @@ def split_conv(input_tensor, num_outputs, num_ways, scope, divisible_by=8, **kwa output_splits = _split_divisible(num_outputs, num_ways, divisible_by=divisible_by) inputs = tf.split(input_tensor, input_splits, axis=3, name="split_" + scope) base = scope - for i, (input_tensor, out_size) in enumerate(zip(inputs, output_splits)): - scope = base + "_part_%d" % (i,) + for i, (input_tensor, out_size) in enumerate(zip(inputs, output_splits, strict=False)): + scope = base + f"_part_{i}" n = slim.conv2d(input_tensor, out_size, [1, 1], scope=scope, **kwargs) n = tf.identity(n, scope + "_output") outs.append(n) diff --git a/deeplabcut/pose_estimation_tensorflow/nnets/utils.py b/deeplabcut/pose_estimation_tensorflow/nnets/utils.py index 3bc6891148..1087fef575 100644 --- a/deeplabcut/pose_estimation_tensorflow/nnets/utils.py +++ b/deeplabcut/pose_estimation_tensorflow/nnets/utils.py @@ -115,7 +115,7 @@ def build_learning_rate( raise AssertionError(f"Unknown lr_decay_type : {lr_decay_type}") if warmup_epochs: - tf.compat.v1.logging.info("Learning rate warmup_epochs: %d" % warmup_epochs) + tf.compat.v1.logging.info(f"Learning rate warmup_epochs: {warmup_epochs}") warmup_steps = int(warmup_epochs * steps_per_epoch) warmup_lr = initial_lr * tf.cast(global_step, tf.float32) / tf.cast(warmup_steps, tf.float32) lr = tf.cond( @@ -139,7 +139,7 @@ def build_optimizer(learning_rate, optimizer_name="rmsprop", decay=0.9, epsilon= tf.compat.v1.logging.info("Using RMSProp optimizer") optimizer = tf.compat.v1.train.RMSPropOptimizer(learning_rate, decay, momentum, epsilon) else: - tf.compat.v1.logging.fatal("Unknown optimizer:", optimizer_name) + tf.compat.v1.logging.fatal(f"Unknown optimizer: {optimizer_name}") return optimizer @@ -158,9 +158,7 @@ def _cross_replica_average(t, num_shards_per_group): group_assignment = None if num_shards_per_group > 1: if num_shards % num_shards_per_group != 0: - raise ValueError( - "num_shards: %d mod shards_per_group: %d, should be 0" % (num_shards, num_shards_per_group) - ) + raise ValueError(f"num_shards: {num_shards} mod shards_per_group: {num_shards_per_group}, should be 0") num_groups = num_shards // num_shards_per_group group_assignment = [ [x for x in range(num_shards) if x // num_shards_per_group == y] for y in range(num_groups) @@ -176,7 +174,7 @@ def _moments(self, inputs, reduction_axes, keep_dims): num_shards_per_group = 1 else: num_shards_per_group = max(8, num_shards // 8) - tf.compat.v1.logging.info("TpuBatchNormalization with num_shards_per_group %s", num_shards_per_group) + tf.compat.v1.logging.info(f"TpuBatchNormalization with num_shards_per_group {num_shards_per_group}") if num_shards_per_group > 1: # Compute variance using: Var[X]= E[X^2] - E[X]^2. shard_square_of_mean = tf.math.square(shard_mean) From 102cbbe8724258bf1abc165d576fed08b1f0319f Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 10:58:30 +0100 Subject: [PATCH 26/80] Add strict zip --- deeplabcut/core/metrics/distance_metrics.py | 10 ++++---- deeplabcut/core/metrics/identity.py | 4 +-- ...ple_individuals_trainingsetmanipulation.py | 13 +++++----- deeplabcut/pose_estimation_3d/plotting3D.py | 10 ++++---- .../apis/visualization.py | 25 ++++++++----------- .../models/heads/dlcrnet.py | 6 +++-- .../post_processing/identity.py | 4 +-- .../data/test_postprocessor.py | 14 +++++------ .../runners/test_runners_inference.py | 12 ++++----- 9 files changed, 49 insertions(+), 49 deletions(-) diff --git a/deeplabcut/core/metrics/distance_metrics.py b/deeplabcut/core/metrics/distance_metrics.py index 43a0dfc031..bd1c9728b7 100644 --- a/deeplabcut/core/metrics/distance_metrics.py +++ b/deeplabcut/core/metrics/distance_metrics.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Implementations of methods to compute distance metrics such as RMSE or OKS""" +"""Implementations of methods to compute distance metrics such as RMSE or OKS.""" from __future__ import annotations @@ -25,7 +25,7 @@ def compute_oks_matrix( oks_sigma: float | np.ndarray, oks_bbox_margin: float = 0.0, ) -> np.ndarray: - """Computes the OKS score for each (prediction, gt) pair in an image + """Computes the OKS score for each (prediction, gt) pair in an image. Args: ground_truth: The GT poses for an image, shape (n_individuals, n_kpts, 2) @@ -361,7 +361,7 @@ def compute_detection_rmse( image_gt = image_gt.transpose((1, 0, 2)) # to (num_bpts, num_gt_individuals, 3) image_pred = image_pred.transpose((1, 0, 2)) # to (num_bpts, num_pred, 3) - for bpt_index, (bpt_gt, bpt_pred) in enumerate(zip(image_gt, image_pred)): + for bpt_index, (bpt_gt, bpt_pred) in enumerate(zip(image_gt, image_pred, strict=True)): # filter NaNs and invalid values bpt_gt = bpt_gt[~np.any(np.isnan(bpt_gt), axis=1)] bpt_pred = bpt_pred[~np.any(np.isnan(bpt_pred), axis=1)] @@ -399,7 +399,7 @@ def compute_detection_rmse( if not isinstance(pcutoff, (int, float)): unique_cutoffs = pcutoff[-num_unique:] - for bpt_index, (gt, pred) in enumerate(zip(unique_gt, unique_pred)): + for bpt_index, (gt, pred) in enumerate(zip(unique_gt, unique_pred, strict=False), strict=True): dist = np.linalg.norm(gt[:2] - pred[:2]) distances.append(dist) @@ -433,7 +433,7 @@ def collect_pixel_errors( keypoint_scores: np.ndarray, pcutoff: float, ) -> tuple[float, int, float, int]: - """Collects pixel errors for RMSE computation + """Collects pixel errors for RMSE computation. Args: pixel_errors: The pixel errors to collect, of shape (num_matches, num_bodyparts) diff --git a/deeplabcut/core/metrics/identity.py b/deeplabcut/core/metrics/identity.py index 92353db9da..684b797213 100644 --- a/deeplabcut/core/metrics/identity.py +++ b/deeplabcut/core/metrics/identity.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Implementations of methods to compute identity prediction accuracy""" +"""Implementations of methods to compute identity prediction accuracy.""" from __future__ import annotations @@ -64,7 +64,7 @@ def compute_identity_scores( gt = gt.transpose((1, 0, 2)) pred = pred.transpose((1, 0, 2))[..., :2] id_scores = id_scores.transpose((1, 0, 2)) - for bpt, bpt_gt, bpt_pred, bpt_id_scores in zip(bodyparts, gt, pred, id_scores): + for bpt, bpt_gt, bpt_pred, bpt_id_scores in zip(bodyparts, gt, pred, id_scores, strict=True): # assign ground truth keypoints to the closest prediction, so the ID score # is the closest possible to the ID score computed with "ground truth" indices_gt = np.flatnonzero(np.all(~np.isnan(bpt_gt), axis=1)) diff --git a/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py b/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py index bfd348737d..1ae2f3bbfe 100755 --- a/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py +++ b/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py @@ -24,7 +24,13 @@ import deeplabcut.generate_training_dataset.metadata as metadata from deeplabcut.core.engine import Engine from deeplabcut.core.weight_init import WeightInitialization -from deeplabcut.generate_training_dataset import ( +from deeplabcut.utils import ( + auxfun_models, + auxfun_multianimal, + auxiliaryfunctions, +) + +from .trainingsetmanipulation import ( MakeInference_yaml, MakeTest_pose_yaml, MakeTrain_pose_yaml, @@ -34,11 +40,6 @@ read_image_shape_fast, validate_shuffles, ) -from deeplabcut.utils import ( - auxfun_models, - auxfun_multianimal, - auxiliaryfunctions, -) def format_multianimal_training_data( diff --git a/deeplabcut/pose_estimation_3d/plotting3D.py b/deeplabcut/pose_estimation_3d/plotting3D.py index cbf5c0de24..b28d82b3ea 100644 --- a/deeplabcut/pose_estimation_3d/plotting3D.py +++ b/deeplabcut/pose_estimation_3d/plotting3D.py @@ -16,7 +16,12 @@ import matplotlib.pyplot as plt import numpy as np import pandas as pd +from matplotlib import gridspec +from matplotlib.animation import FFMpegWriter from matplotlib.axes._axes import _log as matplotlib_axes_logger +from matplotlib.collections import LineCollection +from mpl_toolkits.mplot3d.art3d import Line3DCollection +from tqdm import tqdm from deeplabcut.utils import ( auxiliaryfunctions, @@ -26,11 +31,6 @@ from deeplabcut.utils.auxfun_videos import VideoReader matplotlib_axes_logger.setLevel("ERROR") -from matplotlib import gridspec -from matplotlib.animation import FFMpegWriter -from matplotlib.collections import LineCollection -from mpl_toolkits.mplot3d.art3d import Line3DCollection -from tqdm import tqdm def set_up_grid(figsize, xlim, ylim, zlim, view): diff --git a/deeplabcut/pose_estimation_pytorch/apis/visualization.py b/deeplabcut/pose_estimation_pytorch/apis/visualization.py index 788c4aae3f..c58a8a3cac 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/visualization.py +++ b/deeplabcut/pose_estimation_pytorch/apis/visualization.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Methods to help with visualization of model outputs""" +"""Methods to help with visualization of model outputs.""" from __future__ import annotations @@ -146,7 +146,7 @@ def create_labeled_images( if "bboxes" in image_predictions and "bbox_scores" in image_predictions: bboxes = image_predictions["bboxes"] bbox_scores = image_predictions["bbox_scores"] - for idx, (bbox, score) in enumerate(zip(bboxes, bbox_scores)): + for idx, (bbox, score) in enumerate(zip(bboxes, bbox_scores, strict=True)): if score <= bboxes_pcutoff: continue @@ -174,7 +174,7 @@ def extract_model_outputs( device: str = "auto", context: list[dict[str, np.ndarray]] | None = None, ) -> list[dict[str, np.ndarray]]: - """Obtains the outputs for a model for a list of images + """Obtains the outputs for a model for a list of images. Args: images: List of image paths for which to get model outputs. @@ -249,9 +249,8 @@ def extract_maps( snapshot_index: int | str | None = None, detector_snapshot_index: int | str | None = None, ) -> dict: - """ - Extracts the different maps output by DeepLabCut models, such as scoremaps, location - refinement fields and part-affinity fields. + """Extracts the different maps output by DeepLabCut models, such as scoremaps, + location refinement fields and part-affinity fields. Args: config: Full path of the config.yaml file as a string. @@ -363,7 +362,7 @@ def extract_maps( # key can be just image_idx, or (image_idx, bbox_idx) for TD models keys, images, outputs = _collect_model_outputs(loader.pose_task, result, image_idx) - for key, image, output in zip(keys, images, outputs): + for key, image, output in zip(keys, images, outputs, strict=False): parsed = _parse_model_outputs( image, output, @@ -404,10 +403,9 @@ def extract_save_all_maps( detector_snapshot_index: int | str | None = None, dest_folder: str | Path | None = None, ): - """ - Extracts the scoremap, location refinement field and part affinity field prediction - of the model. The maps will be rescaled to the size of the input image and stored - in the corresponding model folder in /evaluation-results-pytorch. + """Extracts the scoremap, location refinement field and part affinity field + prediction of the model. The maps will be rescaled to the size of the input image + and stored in the corresponding model folder in /evaluation-results-pytorch. Args: config: Full path of the config.yaml file as a string. @@ -441,7 +439,6 @@ def extract_save_all_maps( >>> shuffle=1, >>> indices=[0, 1, 33] >>> ) - """ cfg = read_config_as_dict(config) maps = extract_maps( @@ -511,7 +508,7 @@ def _get_context( detector_snapshot_index: int | str | None, device: str, ) -> list[dict] | None: - """Gets the context for top-down pose estimation models""" + """Gets the context for top-down pose estimation models.""" if loader.pose_task != Task.TOP_DOWN: return None @@ -645,7 +642,7 @@ def _get_maps_folder( model_prefix: str | None, dest_folder: str | Path | None, ) -> Path: - """Gets the destination folder for output maps""" + """Gets the destination folder for output maps.""" if dest_folder is None: project_path = Path(cfg["project_path"]) eval_folder = auxiliaryfunctions.get_evaluation_folder( diff --git a/deeplabcut/pose_estimation_pytorch/models/heads/dlcrnet.py b/deeplabcut/pose_estimation_pytorch/models/heads/dlcrnet.py index 03203fd892..4fc236c58d 100644 --- a/deeplabcut/pose_estimation_pytorch/models/heads/dlcrnet.py +++ b/deeplabcut/pose_estimation_pytorch/models/heads/dlcrnet.py @@ -29,7 +29,7 @@ @HEADS.register_module class DLCRNetHead(HeatmapHead): - """A head for DLCRNet models using Part-Affinity Fields to predict individuals""" + """A head for DLCRNet models using Part-Affinity Fields to predict individuals.""" def __init__( self, @@ -117,7 +117,9 @@ def forward(self, x: torch.Tensor) -> dict[str, torch.Tensor]: stage_in = stage2_in stage_paf_out = stage1_paf_out stage_hm_out = stage1_hm_out - for i, (hm_ref_layer, paf_ref_layer) in enumerate(zip(self.hm_ref_layers, self.paf_ref_layers)): + for i, (hm_ref_layer, paf_ref_layer) in enumerate( + zip(self.hm_ref_layers, self.paf_ref_layers, strict=True) + ): pre_stage_hm_out = stage_hm_out stage_hm_out = hm_ref_layer(stage_in) stage_paf_out = paf_ref_layer(stage_in) diff --git a/deeplabcut/pose_estimation_pytorch/post_processing/identity.py b/deeplabcut/pose_estimation_pytorch/post_processing/identity.py index 50ce049127..f56b888b6b 100644 --- a/deeplabcut/pose_estimation_pytorch/post_processing/identity.py +++ b/deeplabcut/pose_estimation_pytorch/post_processing/identity.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Functions to assign identity to predictions from an identity head""" +"""Functions to assign identity to predictions from an identity head.""" from __future__ import annotations @@ -39,7 +39,7 @@ def assign_identity(predictions: np.ndarray, identity_scores: np.ndarray) -> np. row_ind, col_ind = linear_sum_assignment(cost_matrix, maximize=True) new_order = np.zeros_like(row_ind) - for old_pos, new_pos in zip(row_ind, col_ind): + for old_pos, new_pos in zip(row_ind, col_ind, strict=True): new_order[new_pos] = old_pos return new_order diff --git a/tests/pose_estimation_pytorch/data/test_postprocessor.py b/tests/pose_estimation_pytorch/data/test_postprocessor.py index 70094d5947..7148fdf1c2 100644 --- a/tests/pose_estimation_pytorch/data/test_postprocessor.py +++ b/tests/pose_estimation_pytorch/data/test_postprocessor.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Tests the pre-processors""" +"""Tests the pre-processors.""" import numpy as np import pytest @@ -58,7 +58,7 @@ ], ) def test_rescale_topdown(data): - """expects x_processed = x * scale + offset""" + """Expects x_processed = x * scale + offset.""" postprocessor = RescaleAndOffset( keys_to_rescale=["bodyparts"], mode=RescaleAndOffset.Mode.KEYPOINT_TD, @@ -87,7 +87,7 @@ def test_rescale_topdown(data): ], ) def test_trim_outputs(data): - """expects x_processed = x * scale + offset""" + """Expects x_processed = x * scale + offset.""" postprocessor = TrimOutputs(max_individuals=data["max_individuals"]) context = {} predictions = {"bboxes": np.array(data["bboxes"]), "bbox_scores": np.array(data["bbox_scores"])} @@ -122,7 +122,7 @@ def test_trim_outputs(data): ], ) def test_rescale_bottom_up(data): - """expects x_processed = x * scale + offset""" + """Expects x_processed = x * scale + offset.""" postprocessor = RescaleAndOffset( keys_to_rescale=["bodyparts"], mode=RescaleAndOffset.Mode.KEYPOINT, @@ -167,7 +167,7 @@ def test_rescale_bottom_up(data): ], ) def test_rescale_detector(data): - """expects x_processed = x * scale + offset""" + """Expects x_processed = x * scale + offset.""" postprocessor = RescaleAndOffset( keys_to_rescale=["bboxes"], mode=RescaleAndOffset.Mode.BBOX_XYWH, @@ -301,7 +301,7 @@ def test_prepare_top_down_backbone_features(): assert len(predictions_out) == 2 assert len(context_out) == 1 - for preds, expected in zip(predictions_out, [[1, 2, 3], [11, 12, 13]]): + for preds, expected in zip(predictions_out, [[1, 2, 3], [11, 12, 13]], strict=True): assert "backbone" in preds assert "bodypart_features" in preds["backbone"] bodypart_features = preds["backbone"]["bodypart_features"] @@ -345,7 +345,7 @@ def test_prepare_top_down_backbone_features(): ], ) def test_remove_low_confidence_boxes(data): - """Tests that RemoveLowConfidenceBoxes filters boxes below threshold""" + """Tests that RemoveLowConfidenceBoxes filters boxes below threshold.""" postprocessor = RemoveLowConfidenceBoxes(bbox_score_thresh=data["threshold"]) context = {} diff --git a/tests/pose_estimation_pytorch/runners/test_runners_inference.py b/tests/pose_estimation_pytorch/runners/test_runners_inference.py index 431b851d12..f59831ca4e 100644 --- a/tests/pose_estimation_pytorch/runners/test_runners_inference.py +++ b/tests/pose_estimation_pytorch/runners/test_runners_inference.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Tests inference runners""" +"""Tests inference runners.""" from unittest.mock import Mock, patch @@ -29,7 +29,7 @@ def test_load_weights_only_with_build_training_runner(task: Task, weights_only: bool): with patch("deeplabcut.pose_estimation_pytorch.runners.base.torch.load") as load: snapshot = "snapshot.pt" - runner = inference.build_inference_runner( + inference.build_inference_runner( task=task, model=Mock(), device="cpu", @@ -42,7 +42,7 @@ def test_load_weights_only_with_build_training_runner(task: Task, weights_only: class MockInferenceRunner(inference.InferenceRunner): - """Mocks the predict function for an inference runner""" + """Mocks the predict function for an inference runner.""" def __init__( self, @@ -88,7 +88,7 @@ def test_mock_bottom_up(batch_size): _check_batch_shapes(batch_size, h, w, runner.batch_shapes) assert len(images) == len(predictions) - for i, p in zip(images, predictions): + for i, p in zip(images, predictions, strict=True): assert len(p) == 1 # only 1 output per image assert i[0, 0, 0, 0] == p[0]["mock"]["index"] @@ -140,9 +140,9 @@ def test_mock_top_down(batch_size, detections_per_image): _check_batch_shapes(batch_size, h, w, runner.batch_shapes) assert len(images) == len(predictions) - for i, p in zip(images, predictions): + for i, p in zip(images, predictions, strict=True): assert len(p) == len(i) # one prediction per input - for i_det, p_det in zip(i, p): + for i_det, p_det in zip(i, p, strict=True): print(i_det.shape) print(p_det["mock"]["index"]) assert i_det[0, 0, 0] == p_det["mock"]["index"] From 5e54292e51fd775fb54dcfebf9adf05f332ad512 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 10:59:02 +0100 Subject: [PATCH 27/80] Update ruff_report.py --- tools/ruff_report.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tools/ruff_report.py b/tools/ruff_report.py index 123f21e02c..6401235886 100644 --- a/tools/ruff_report.py +++ b/tools/ruff_report.py @@ -24,6 +24,7 @@ "F403": "`from x import *` makes names unclear. Replace with explicit imports.", "F405": "Likely consequence of `import *`. Import the name explicitly.", "F821": "Undefined name. Usually a real bug or missing import.", + "F841": "Local variable assigned but never used. Consider removing or using it.", "E722": "Bare `except:`. Catch `Exception` or a narrower exception type.", "B904": "Inside `except`, use `raise ... from e` to preserve exception chaining.", "B007": "Unused loop variable. Rename to `_` or use it.", @@ -38,6 +39,8 @@ "B017": "Use a more specific exception with `assertRaises`.", "B020": "Loop variable overrides iterator. Rename loop variables.", "B027": "Empty method in ABC without abstract decorator. Add `@abstractmethod` or implement it.", + "B028": "Method in ABC without implementation or abstract decorator. Add `@abstractmethod` or implement it.", + "B905": "Use `raise ... from None` to suppress context when re-raising an exception.", } From 0e0cf956363bce0d0bc1947ce77936aee14c7f4a Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 11:02:42 +0100 Subject: [PATCH 28/80] Raise from/bare except --- deeplabcut/pose_estimation_pytorch/registry.py | 2 +- .../runners/schedulers.py | 18 +++++++++--------- .../core/evaluate.py | 4 ++-- examples/testscript_3d.py | 10 +++++----- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/registry.py b/deeplabcut/pose_estimation_pytorch/registry.py index 7f880d1973..8cae38140b 100644 --- a/deeplabcut/pose_estimation_pytorch/registry.py +++ b/deeplabcut/pose_estimation_pytorch/registry.py @@ -67,7 +67,7 @@ def build_from_cfg(cfg: dict, registry: "Registry", default_args: dict | None = return obj_cls(**filtered_args) except Exception as e: # Normal TypeError does not print class name. - raise type(e)(f"{obj_cls.__name__}: {e}") + raise type(e)(f"{obj_cls.__name__}: {e}") from None class Registry: diff --git a/deeplabcut/pose_estimation_pytorch/runners/schedulers.py b/deeplabcut/pose_estimation_pytorch/runners/schedulers.py index a3270e07fd..029b42bd92 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/schedulers.py +++ b/deeplabcut/pose_estimation_pytorch/runners/schedulers.py @@ -17,11 +17,11 @@ class LRListScheduler(_LRScheduler): - """ - You can achieve increased performance and faster training by using a learning rate - that changes during training. A scheduler makes the learning rate adaptive. Given a - list of learning rates and milestones modifies the learning rate accordingly during - training. + """You can achieve increased performance and faster training by using a learning + rate that changes during training. + + A scheduler makes the learning rate adaptive. Given a list of learning rates and + milestones modifies the learning rate accordingly during training. """ def __init__(self, optimizer, milestones, lr_list, last_epoch=-1) -> None: @@ -62,7 +62,7 @@ def get_lr(self): def build_scheduler( scheduler_cfg: dict | None, optimizer: torch.optim.Optimizer ) -> torch.optim.lr_scheduler.LRScheduler | None: - """Builds a scheduler from a configuration, if defined + """Builds a scheduler from a configuration, if defined. Args: scheduler_cfg: the configuration of the scheduler to build @@ -92,7 +92,7 @@ def build_scheduler( def _parse_scheduler_param(param: Any, optimizer: torch.optim.Optimizer) -> Any: - """Parses parameters so they're built as schedulers if they're configured as one""" + """Parses parameters so they're built as schedulers if they're configured as one.""" if isinstance(param, dict) and "type" in param: param = build_scheduler(param, optimizer) @@ -114,7 +114,7 @@ def load_scheduler_state( try: scheduler.load_state_dict(state_dict) except Exception as err: - raise ValueError(f"Failed to load state dict: {err}") + raise ValueError("Failed to load state dict") from err param_groups = scheduler.optimizer.param_groups resume_lrs = scheduler.get_last_lr() @@ -126,5 +126,5 @@ def load_scheduler_state( ) # Update the learning rate for the optimizer based on the scheduler - for group, resume_lr in zip(param_groups, resume_lrs): + for group, resume_lr in zip(param_groups, resume_lrs, strict=False): group["lr"] = resume_lr diff --git a/deeplabcut/pose_estimation_tensorflow/core/evaluate.py b/deeplabcut/pose_estimation_tensorflow/core/evaluate.py index dc7b5b5c29..e387606f15 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/evaluate.py +++ b/deeplabcut/pose_estimation_tensorflow/core/evaluate.py @@ -271,10 +271,10 @@ def return_evaluate_network_data( try: test_pose_cfg = load_config(str(path_test_config)) - except FileNotFoundError: + except FileNotFoundError as e: raise FileNotFoundError( f"It seems the model for shuffle {shuffle} and trainFraction {trainFraction} does not exist." - ) + ) from e train_pose_cfg = load_config(str(path_train_config)) # Load meta data diff --git a/examples/testscript_3d.py b/examples/testscript_3d.py index bf6964267d..fef958a479 100644 --- a/examples/testscript_3d.py +++ b/examples/testscript_3d.py @@ -89,7 +89,7 @@ output2, ] ) - except: + except Exception: pass """ @@ -104,8 +104,8 @@ # checking if 2d test project is available try: config = glob.glob(os.path.join(basepath, "TEST*", "config.yaml"))[-1] - except: - raise RuntimeError("Please run the testscript.py first before testing for 3d") + except Exception as e: + raise RuntimeError("Please run the testscript.py first before testing for 3d") from e dfolder = None @@ -122,8 +122,8 @@ cfg["skeleton"] = [["bodypart1", "bodypart2"], ["objectA", "bodypart3"]] deeplabcut.auxiliaryfunctions.write_config_3d(path_config_file, cfg) - except: - raise ("Please delete the project and re-try.") # otherwise the cfg is an empty array! + except Exception as e: + raise RuntimeError("Please delete the project and re-try.") from e # otherwise the cfg is an empty array! """ # Creating the name of the project From 72f228241ec4ce903fd8a9c48c1cacb42407782e Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 11:03:57 +0100 Subject: [PATCH 29/80] Top level imports --- examples/testscript_deterministicwithResNet152.py | 6 ++---- .../modelzoo/test_fmpose_integration.py | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/examples/testscript_deterministicwithResNet152.py b/examples/testscript_deterministicwithResNet152.py index f061067d65..d0a6550fec 100644 --- a/examples/testscript_deterministicwithResNet152.py +++ b/examples/testscript_deterministicwithResNet152.py @@ -37,10 +37,6 @@ It produces nothing of interest scientifically. """ -task = "TEST-deterministic" # Enter the name of your experiment Task -scorer = "Alex" # Enter the name of the experimenter/labeler - - import os import numpy as np @@ -48,6 +44,8 @@ import deeplabcut +task = "TEST-deterministic" # Enter the name of your experiment Task +scorer = "Alex" # Enter the name of the experimenter/labeler print("Imported DLC!") basepath = os.path.dirname(os.path.abspath("testscript.py")) videoname = "reachingvideo1" diff --git a/tests/pose_estimation_pytorch/modelzoo/test_fmpose_integration.py b/tests/pose_estimation_pytorch/modelzoo/test_fmpose_integration.py index 9de3184af7..dabbc51d96 100644 --- a/tests/pose_estimation_pytorch/modelzoo/test_fmpose_integration.py +++ b/tests/pose_estimation_pytorch/modelzoo/test_fmpose_integration.py @@ -16,7 +16,7 @@ fmpose3d = pytest.importorskip("fmpose3d", reason="fmpose3d not installed") -from deeplabcut.modelzoo.fmpose_3d.fmpose3d import get_fmpose3d_inference_api +from deeplabcut.modelzoo.fmpose_3d.fmpose3d import get_fmpose3d_inference_api # noqa: E402 def _has_network(host="huggingface.co", port=443, timeout=3) -> bool: From 869a24f11fc76b8c8a4e49174d65ff312c3b88f5 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 11:05:20 +0100 Subject: [PATCH 30/80] Revert "Update ruff_report.py" This reverts commit 5e54292e51fd775fb54dcfebf9adf05f332ad512. --- tools/ruff_report.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tools/ruff_report.py b/tools/ruff_report.py index 6401235886..123f21e02c 100644 --- a/tools/ruff_report.py +++ b/tools/ruff_report.py @@ -24,7 +24,6 @@ "F403": "`from x import *` makes names unclear. Replace with explicit imports.", "F405": "Likely consequence of `import *`. Import the name explicitly.", "F821": "Undefined name. Usually a real bug or missing import.", - "F841": "Local variable assigned but never used. Consider removing or using it.", "E722": "Bare `except:`. Catch `Exception` or a narrower exception type.", "B904": "Inside `except`, use `raise ... from e` to preserve exception chaining.", "B007": "Unused loop variable. Rename to `_` or use it.", @@ -39,8 +38,6 @@ "B017": "Use a more specific exception with `assertRaises`.", "B020": "Loop variable overrides iterator. Rename loop variables.", "B027": "Empty method in ABC without abstract decorator. Add `@abstractmethod` or implement it.", - "B028": "Method in ABC without implementation or abstract decorator. Add `@abstractmethod` or implement it.", - "B905": "Use `raise ... from None` to suppress context when re-raising an exception.", } From 0616ddbca4088e610fb4834f76cda41e0b99dcd0 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 11:07:01 +0100 Subject: [PATCH 31/80] Warning stacklevel --- .../conversion_table/conversion_table.py | 24 ++++++------------- .../datasets/factory.py | 2 +- .../nnets/factory.py | 2 +- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/deeplabcut/modelzoo/generalized_data_converter/conversion_table/conversion_table.py b/deeplabcut/modelzoo/generalized_data_converter/conversion_table/conversion_table.py index 7a883d78db..4519a66b5b 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/conversion_table/conversion_table.py +++ b/deeplabcut/modelzoo/generalized_data_converter/conversion_table/conversion_table.py @@ -21,16 +21,14 @@ def __init__(self, raw_table_dict): def convert(self, kpt): if kpt not in self.table_dict: - warnings.warn(f"{kpt} is defined in src space but not appeared in the conversion table") + warnings.warn(f"{kpt} is defined in src space but not appeared in the conversion table", stacklevel=2) return None else: return self.table_dict[kpt] class ConversionTableFromCSV: - """ - Base class only reads the table - """ + """Base class only reads the table.""" def __init__(self, src_keypoints, table_path): self.table_path = table_path @@ -57,7 +55,7 @@ def __init__(self, src_keypoints, table_path): kpt_alias = set(kpts) for k in list(kpt_alias): - if type(k) != str: + if not isinstance(k, str): kpt_alias.remove(k) self.lookup_set.append(kpt_alias) @@ -89,11 +87,7 @@ def __init__(self, src_keypoints, table_path): self.check_inclusion() def _search(self, key): - """ - return -1 if not found - return kpt id if found - - """ + """Return -1 if not found return kpt id if found.""" # [TODO] if it can be mapped to two, I can randomly return one for kpt_id in range(len(self.lookup_set)): if key in self.lookup_set[kpt_id]: @@ -101,11 +95,7 @@ def _search(self, key): return -1 def check_inclusion(self): - """ - check if conversion table covers - every keypoint contained in src proj - - """ + """Check if conversion table covers every keypoint contained in src proj.""" count = 0 print("src keypoints") print(self.src_keypoints) @@ -119,7 +109,7 @@ def check_inclusion(self): def convert(self, kpt): if kpt not in self.table: - warnings.warn(f"{kpt} is defined in src space but not appeared in the conversion table") + warnings.warn(f"{kpt} is defined in src space but not appeared in the conversion table", stacklevel=2) return None else: return self.table[kpt] @@ -128,7 +118,7 @@ def get_subset(self, labname=""): bodyparts = self.df[labname] - super_bodyparts = self.df["MasterName"] + self.df["MasterName"] ret = [] diff --git a/deeplabcut/pose_estimation_tensorflow/datasets/factory.py b/deeplabcut/pose_estimation_tensorflow/datasets/factory.py index 2415f9990e..62d71efe2f 100644 --- a/deeplabcut/pose_estimation_tensorflow/datasets/factory.py +++ b/deeplabcut/pose_estimation_tensorflow/datasets/factory.py @@ -23,7 +23,7 @@ class PoseDatasetFactory: def register(cls, type_): def wrapper(dataset): if type_ in cls._datasets: - warnings.warn("Overwriting existing dataset {}.") + warnings.warn(f"Overwriting existing dataset {type_}.", stacklevel=2) cls._datasets[type_] = dataset return dataset diff --git a/deeplabcut/pose_estimation_tensorflow/nnets/factory.py b/deeplabcut/pose_estimation_tensorflow/nnets/factory.py index cbfe98ab7a..61501353b3 100644 --- a/deeplabcut/pose_estimation_tensorflow/nnets/factory.py +++ b/deeplabcut/pose_estimation_tensorflow/nnets/factory.py @@ -18,7 +18,7 @@ class PoseNetFactory: def register(cls, type_): def wrapper(net): if type_ in cls._nets: - warnings.warn("Overwriting existing network {}.") + warnings.warn(f"Overwriting existing network {type_}.", stacklevel=2) cls._nets[type_] = net return net From c23791c9850e857ceec1a6c833cbb697ffd19e85 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 11:14:05 +0100 Subject: [PATCH 32/80] Raise from + bind loop variable in def --- deeplabcut/pose_estimation_tensorflow/visualizemaps.py | 4 ++-- deeplabcut/pose_tracking_pytorch/train_dlctransreid.py | 4 ++-- deeplabcut/refine_training_dataset/stitch.py | 10 ++++++---- deeplabcut/utils/auxfun_models.py | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/deeplabcut/pose_estimation_tensorflow/visualizemaps.py b/deeplabcut/pose_estimation_tensorflow/visualizemaps.py index 449f63c321..e96e4b4a0c 100644 --- a/deeplabcut/pose_estimation_tensorflow/visualizemaps.py +++ b/deeplabcut/pose_estimation_tensorflow/visualizemaps.py @@ -135,10 +135,10 @@ def extract_maps( ) = auxiliaryfunctions.load_metadata(os.path.join(cfg["project_path"], metadatafn)) try: dlc_cfg = load_config(str(path_test_config)) - except FileNotFoundError: + except FileNotFoundError as e: raise FileNotFoundError( f"It seems the model for shuffle {shuffle} and trainFraction {trainFraction} does not exist." - ) + ) from e # change batch size, if it was edited during analysis! dlc_cfg["batch_size"] = 1 # in case this was edited for analysis. diff --git a/deeplabcut/pose_tracking_pytorch/train_dlctransreid.py b/deeplabcut/pose_tracking_pytorch/train_dlctransreid.py index a2daef1914..404618a334 100644 --- a/deeplabcut/pose_tracking_pytorch/train_dlctransreid.py +++ b/deeplabcut/pose_tracking_pytorch/train_dlctransreid.py @@ -13,8 +13,8 @@ try: import torch -except ModuleNotFoundError: - raise ModuleNotFoundError("Unsupervised identity learning requires PyTorch. Please run `pip install torch`.") +except ModuleNotFoundError as e: + raise ModuleNotFoundError("Unsupervised identity learning requires PyTorch. Please run `pip install torch`.") from e import glob import os from pathlib import Path diff --git a/deeplabcut/refine_training_dataset/stitch.py b/deeplabcut/refine_training_dataset/stitch.py index b80094ffd4..7d6b842291 100644 --- a/deeplabcut/refine_training_dataset/stitch.py +++ b/deeplabcut/refine_training_dataset/stitch.py @@ -1131,6 +1131,7 @@ def trans_weight_func(tracklet1, tracklet2, nframe, feature_dict): return -dist + base_weight_func = weight_func for video in vids: print("Processing... ", video) nframe = len(VideoWriter(video)) @@ -1146,8 +1147,8 @@ def trans_weight_func(tracklet1, tracklet2, nframe, feature_dict): try: feature_dict = shelve.open(feature_dict_path, flag="r") - except dbm.error: - raise FileNotFoundError(f"{feature_dict_path} does not exist. Did you run transformer_reID()?") + except dbm.error as err: + raise FileNotFoundError(f"{feature_dict_path} does not exist. Did you run transformer_reID()?") from err dataname = os.path.join(dest, vname + DLCscorer + ".h5") @@ -1157,10 +1158,11 @@ def trans_weight_func(tracklet1, tracklet2, nframe, feature_dict): stitcher = TrackletStitcher.from_pickle( pickle_file, n_tracks, min_length, split_tracklets, prestitch_residuals ) + current_weight_func = base_weight_func with_id = any(tracklet.identity != -1 for tracklet in stitcher) if with_id and weight_func is None: # Add in identity weighing before building the graph - def weight_func(t1, t2): + def current_weight_func(t1, t2, stitcher=stitcher): w = 0.01 if t1.identity == t2.identity else 1 return w * stitcher.calculate_edge_weight(t1, t2) @@ -1170,7 +1172,7 @@ def weight_func(t1, t2): weight_func=partial(trans_weight_func, nframe=nframe, feature_dict=feature_dict), ) else: - stitcher.build_graph(max_gap=max_gap, weight_func=weight_func) + stitcher.build_graph(max_gap=max_gap, weight_func=current_weight_func) stitcher.stitch() if transformer_checkpoint: diff --git a/deeplabcut/utils/auxfun_models.py b/deeplabcut/utils/auxfun_models.py index 3042f53ba4..f0af511ca8 100644 --- a/deeplabcut/utils/auxfun_models.py +++ b/deeplabcut/utils/auxfun_models.py @@ -179,7 +179,7 @@ def smart_restore(restorer, sess, checkpoint_path, net_type): _ = check_for_weights(net_type, Path(dlcparent_path)) restorer.restore(sess, checkpoint_path) else: - raise ValueError(e) + raise ValueError(e) from e # Aliases for backwards-compatibility From 3d5488df3331464dbd5553014b3e4403d6dfed23 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 11:15:39 +0100 Subject: [PATCH 33/80] Unused variables --- .../config/test_make_pose_config.py | 4 ++-- .../modelzoo/test_load_superanimal_models.py | 4 ++-- tests/pose_estimation_pytorch/other/test_api_utils.py | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/pose_estimation_pytorch/config/test_make_pose_config.py b/tests/pose_estimation_pytorch/config/test_make_pose_config.py index ad0b1c3fd4..ab48c4970a 100644 --- a/tests/pose_estimation_pytorch/config/test_make_pose_config.py +++ b/tests/pose_estimation_pytorch/config/test_make_pose_config.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Tests the pre-processors""" +"""Tests the pre-processors.""" import pytest @@ -335,7 +335,7 @@ def test_make_tokenpose_config( ) if identity or len(unique_bodyparts) > 0: - with pytest.raises(ValueError) as err_info: + with pytest.raises(ValueError) as _err_info: # Not yet implemented! _ = make_pytorch_pose_config( project_config, diff --git a/tests/pose_estimation_pytorch/modelzoo/test_load_superanimal_models.py b/tests/pose_estimation_pytorch/modelzoo/test_load_superanimal_models.py index bdd93a3e10..fab8e2fb3f 100644 --- a/tests/pose_estimation_pytorch/modelzoo/test_load_superanimal_models.py +++ b/tests/pose_estimation_pytorch/modelzoo/test_load_superanimal_models.py @@ -23,9 +23,9 @@ def test_load_superanimal_models_weights_only(): for detector in dlclibrary.get_available_detectors(super_animal): print(super_animal, detector) path = get_super_animal_snapshot_path(super_animal, detector) - snapshot = torch.load(path, map_location="cpu", weights_only=True) + _snapshot = torch.load(path, map_location="cpu", weights_only=True) for pose_model in dlclibrary.get_available_models(super_animal): print(super_animal, pose_model) path = get_super_animal_snapshot_path(super_animal, pose_model) - snapshot = torch.load(path, map_location="cpu", weights_only=True) + _snapshot = torch.load(path, map_location="cpu", weights_only=True) diff --git a/tests/pose_estimation_pytorch/other/test_api_utils.py b/tests/pose_estimation_pytorch/other/test_api_utils.py index a53960c3eb..0efbfbb52c 100644 --- a/tests/pose_estimation_pytorch/other/test_api_utils.py +++ b/tests/pose_estimation_pytorch/other/test_api_utils.py @@ -64,10 +64,10 @@ def test_build_transforms(transform_dict, size_image, num_keypoints, num_animals bboxes[:, 3] = h - bboxes[:, 1] keypoints = np.random.randint(0, min(w, h), (num_keypoints, 2)) - with pytest.raises(Exception): - transformed = transform_bbox_aug(image=test_image) - transformed = transform_bbox_aug(image=test_image, bboxes=bboxes.copy()) - transformed = transform_bbox_aug(image=test_image, keypoints=keypoints.copy(), bboxes=bboxes.copy()) + with pytest.raises(ValueError) as _err_info: + _ = transform_bbox_aug(image=test_image) + _ = transform_bbox_aug(image=test_image, bboxes=bboxes.copy()) + _ = transform_bbox_aug(image=test_image, keypoints=keypoints.copy(), bboxes=bboxes.copy()) transformed_with_bbox = transform_bbox_aug( image=test_image, From f5f9bd9e28b5f1fb98bcbbcbf476a195cc1f7af6 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 11:17:14 +0100 Subject: [PATCH 34/80] Fix broken string --- deeplabcut/utils/auxfun_models.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/deeplabcut/utils/auxfun_models.py b/deeplabcut/utils/auxfun_models.py index f0af511ca8..5c219c7bf8 100644 --- a/deeplabcut/utils/auxfun_models.py +++ b/deeplabcut/utils/auxfun_models.py @@ -157,9 +157,7 @@ def set_visible_devices(gputouse: int): n_devices = len(physical_devices) if gputouse >= n_devices: raise ValueError( - f"There are {n_devices} available GPUs: {physical_devices}\nPlease choose `gputouse` in { - list(range(n_devices)) - }." + f"There are {n_devices} available GPUs: {physical_devices}\nPlease choose `gputouse` in {list(range(n_devices))}." ) tf.config.set_visible_devices(physical_devices[gputouse], "GPU") From f93dc3eeec1c67a812b535f3c0476b198ea7e9fc Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 11:21:46 +0100 Subject: [PATCH 35/80] Mutable defaults --- deeplabcut/core/inferenceutils.py | 4 +++- examples/testscript_pytorch_single_animal.py | 10 ++++++---- .../config/test_make_pose_config.py | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/deeplabcut/core/inferenceutils.py b/deeplabcut/core/inferenceutils.py index 88479c3298..b7bb82f108 100644 --- a/deeplabcut/core/inferenceutils.py +++ b/deeplabcut/core/inferenceutils.py @@ -1197,12 +1197,14 @@ def evaluate_assembly( ass_pred_dict, ass_true_dict, oks_sigma=0.072, - oks_thresholds=np.linspace(0.5, 0.95, 10), + oks_thresholds=None, margin=0, symmetric_kpts=None, greedy_matching=False, with_tqdm: bool = True, ): + if oks_thresholds is None: + oks_thresholds = np.linspace(0.5, 0.95, 10) if greedy_matching: return evaluate_assembly_greedy( ass_true_dict, diff --git a/examples/testscript_pytorch_single_animal.py b/examples/testscript_pytorch_single_animal.py index ef247dcd17..8536f2be12 100644 --- a/examples/testscript_pytorch_single_animal.py +++ b/examples/testscript_pytorch_single_animal.py @@ -26,13 +26,15 @@ def main( batch_size: int = 1, device: str = "cpu", logger: dict | None = None, - synthetic_data_params: SyntheticProjectParameters = SyntheticProjectParameters( - multianimal=False, - num_bodyparts=6, - ), + synthetic_data_params: SyntheticProjectParameters = None, create_labeled_videos: bool = False, delete_after_test_run: bool = False, ) -> None: + if synthetic_data_params is None: + synthetic_data_params = SyntheticProjectParameters( + multianimal=False, + num_bodyparts=6, + ) engine = Engine.PYTORCH if synthetic_data: project_path = Path("synthetic-data-niels-single-animal").resolve() diff --git a/tests/pose_estimation_pytorch/config/test_make_pose_config.py b/tests/pose_estimation_pytorch/config/test_make_pose_config.py index ab48c4970a..a8fbc7b6ff 100644 --- a/tests/pose_estimation_pytorch/config/test_make_pose_config.py +++ b/tests/pose_estimation_pytorch/config/test_make_pose_config.py @@ -335,7 +335,7 @@ def test_make_tokenpose_config( ) if identity or len(unique_bodyparts) > 0: - with pytest.raises(ValueError) as _err_info: + with pytest.raises(ValueError) as _: # Not yet implemented! _ = make_pytorch_pose_config( project_config, From 9692954d8d010ea5ea85de076962d4314206db7b Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 11:25:33 +0100 Subject: [PATCH 36/80] ABC fixes --- deeplabcut/create_project/modelzoo.py | 10 ++++------ deeplabcut/pose_estimation_pytorch/data/ctd.py | 3 ++- deeplabcut/pose_estimation_pytorch/runners/shelving.py | 3 +-- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/deeplabcut/create_project/modelzoo.py b/deeplabcut/create_project/modelzoo.py index c3a5360e96..9e084e557b 100644 --- a/deeplabcut/create_project/modelzoo.py +++ b/deeplabcut/create_project/modelzoo.py @@ -355,16 +355,14 @@ def create_pretrained_project_pytorch( if net_name not in get_available_models(dataset): raise ValueError( - f"Invalid net_name '{net_name}' for dataset {dataset}. The following net types are available: { - get_available_models(dataset) - }" + f"Invalid net_name '{net_name}' for dataset {dataset}. " + f"The following net types are available: {get_available_models(dataset)}" ) if detector_name not in get_available_detectors(dataset): raise ValueError( - f"Invalid detector_name '{detector_name}' for dataset {dataset}. The following detectors are available: { - get_available_detectors(dataset) - }" + f"Invalid detector_name '{detector_name}' for dataset {dataset}. " + f"The following detectors are available: {get_available_detectors(dataset)}" ) # Create project diff --git a/deeplabcut/pose_estimation_pytorch/data/ctd.py b/deeplabcut/pose_estimation_pytorch/data/ctd.py index c789fb4611..5cd93b5608 100644 --- a/deeplabcut/pose_estimation_pytorch/data/ctd.py +++ b/deeplabcut/pose_estimation_pytorch/data/ctd.py @@ -12,7 +12,7 @@ import json import pickle -from abc import ABC +from abc import ABC, abstractmethod from pathlib import Path import numpy as np @@ -27,6 +27,7 @@ class CondProvider(ABC): """A class providing conditions for a CTD model.""" @classmethod + @abstractmethod def get_loader_and_snapshot( cls, config: str | Path, diff --git a/deeplabcut/pose_estimation_pytorch/runners/shelving.py b/deeplabcut/pose_estimation_pytorch/runners/shelving.py index e5112005c6..3d1459bbe7 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/shelving.py +++ b/deeplabcut/pose_estimation_pytorch/runners/shelving.py @@ -12,13 +12,12 @@ import pickle import shelve -from abc import ABC from pathlib import Path import numpy as np -class ShelfManager(ABC): +class ShelfManager: """Class to manage shelf data.""" def __init__(self, filepath: str | Path, flag: str = "r") -> None: From 589b41b0c5d115f275182cef35ac4220141c721e Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 11:26:55 +0100 Subject: [PATCH 37/80] isinstance --- deeplabcut/pose_estimation_pytorch/data/dlcloader.py | 2 +- .../pose_estimation_tensorflow/datasets/pose_tensorpack.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/data/dlcloader.py b/deeplabcut/pose_estimation_pytorch/data/dlcloader.py index 5ce2941b1e..50905e05a5 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dlcloader.py +++ b/deeplabcut/pose_estimation_pytorch/data/dlcloader.py @@ -319,7 +319,7 @@ def load_predictions( predictions = {} for idx in dlc_preds.index.unique(): - if type(idx) == tuple: + if isinstance(idx, tuple): img_path = pred_path.parent.parent / Path(*idx) else: img_path = pred_path.parent.parent / Path(idx) diff --git a/deeplabcut/pose_estimation_tensorflow/datasets/pose_tensorpack.py b/deeplabcut/pose_estimation_tensorflow/datasets/pose_tensorpack.py index c107d2e3ce..9558e64b46 100644 --- a/deeplabcut/pose_estimation_tensorflow/datasets/pose_tensorpack.py +++ b/deeplabcut/pose_estimation_tensorflow/datasets/pose_tensorpack.py @@ -166,7 +166,7 @@ def __init__(self, cfg): # range [-rotate_max_deg_abs; rotate_max_deg_abs] to augment training data if cfg.get("rotation", True): # i.e. pm 25 degrees - if type(cfg.get("rotation", False)) == int: + if isinstance(cfg.get("rotation", False), int): cfg["rotation"] = cfg.get("rotation", 25) else: cfg["rotation"] = 25 From 2dd7ed595c131b32434898c9d72ad82be2293670 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 11:39:50 +0100 Subject: [PATCH 38/80] Various fixes --- deeplabcut/__main__.py | 3 ++- deeplabcut/gui/tabs/create_training_dataset.py | 3 ++- deeplabcut/gui/tabs/train_network.py | 4 +++- deeplabcut/pose_estimation_3d/triangulation.py | 5 ++--- .../pose_estimation_pytorch/models/heads/simple_head.py | 6 +++--- .../pose_estimation_pytorch/models/modules/kpt_encoders.py | 1 + deeplabcut/pose_estimation_pytorch/post_processing/nms.py | 2 +- .../pose_tracking_pytorch/model/backbones/vit_pytorch.py | 4 +--- deeplabcut/pose_tracking_pytorch/processor/processor.py | 7 +++---- examples/testscript.py | 4 ++-- tests/generate_training_dataset/test_trainset_metadata.py | 2 +- 11 files changed, 21 insertions(+), 20 deletions(-) diff --git a/deeplabcut/__main__.py b/deeplabcut/__main__.py index f0903fcf97..12d7aff368 100644 --- a/deeplabcut/__main__.py +++ b/deeplabcut/__main__.py @@ -8,11 +8,12 @@ # # Licensed under GNU Lesser General Public License v3.0 # +from importlib import import_module def main(): try: - import PySide6 + import_module("PySide6") lite = False except ModuleNotFoundError: diff --git a/deeplabcut/gui/tabs/create_training_dataset.py b/deeplabcut/gui/tabs/create_training_dataset.py index af78cef535..ff3a35e45c 100644 --- a/deeplabcut/gui/tabs/create_training_dataset.py +++ b/deeplabcut/gui/tabs/create_training_dataset.py @@ -12,6 +12,7 @@ import os import re +from importlib import import_module from pathlib import Path import dlclibrary @@ -245,7 +246,7 @@ def create_training_dataset(self): detector_type = None ctd_conditions = None if engine == Engine.TF: - import tensorflow + import_module("tensorflow") # try importing TF so they can't create shuffles for it if they # don't have it installed diff --git a/deeplabcut/gui/tabs/train_network.py b/deeplabcut/gui/tabs/train_network.py index c18e09676f..b2c5294a8c 100644 --- a/deeplabcut/gui/tabs/train_network.py +++ b/deeplabcut/gui/tabs/train_network.py @@ -185,7 +185,9 @@ def _generate_layout_attributes(self) -> None: spin_box.setMinimum(attribute.min) spin_box.setMaximum(attribute.max) spin_box.setValue(attribute.default) - spin_box.valueChanged.connect(lambda new_val: self.log_attribute_change(attribute, new_val)) + spin_box.valueChanged.connect( + lambda new_val, attr=attribute: self.log_attribute_change(attr, new_val) + ) self._attribute_kwargs[engine][attribute.fn_key] = spin_box # Pad below to create spacing with other rows diff --git a/deeplabcut/pose_estimation_3d/triangulation.py b/deeplabcut/pose_estimation_3d/triangulation.py index b9945fcf8e..b94fc20fca 100644 --- a/deeplabcut/pose_estimation_3d/triangulation.py +++ b/deeplabcut/pose_estimation_3d/triangulation.py @@ -10,6 +10,7 @@ # import os +import warnings from pathlib import Path import cv2 @@ -136,7 +137,7 @@ def triangulate( # Get track_method and do related checks track_method = auxfun_multianimal.get_track_method(cfg, track_method=track_method) if len(cfg.get("multianimalbodyparts", [])) == 1 and track_method != "box": - warnings.warn("Switching to `box` tracker for single point tracking...") + warnings.warn("Switching to `box` tracker for single point tracking...", stacklevel=2) track_method = "box" # Get track method suffix @@ -291,8 +292,6 @@ def triangulate( path_stereo_file, ) = undistort_points(config, dataname, str(cam_names[0] + "-" + cam_names[1])) if len(dataFrame_camera1_undistort) != len(dataFrame_camera2_undistort): - import warnings - warnings.warn( "The number of frames do not match in the two videos. Please make sure that your videos have same number of frames and then retry! Excluding the extra frames from the longer video.", stacklevel=2, diff --git a/deeplabcut/pose_estimation_pytorch/models/heads/simple_head.py b/deeplabcut/pose_estimation_pytorch/models/heads/simple_head.py index 98ffffc2b6..0859ae6205 100644 --- a/deeplabcut/pose_estimation_pytorch/models/heads/simple_head.py +++ b/deeplabcut/pose_estimation_pytorch/models/heads/simple_head.py @@ -190,10 +190,10 @@ def _make_layers( the deconvolutional layers """ layers = [] - for out_channels, k, s in zip(out_channels, kernel_sizes, strides, strict=False): - layers.append(nn.ConvTranspose2d(in_channels, out_channels, kernel_size=k, stride=s)) + for out_c, k, s in zip(out_channels, kernel_sizes, strides, strict=False): + layers.append(nn.ConvTranspose2d(in_channels, out_c, kernel_size=k, stride=s)) layers.append(nn.ReLU()) - in_channels = out_channels + in_channels = out_c return layers[:-1] def forward(self, x: torch.Tensor) -> torch.Tensor: diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py index ce14ff363e..3654e3e644 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/kpt_encoders.py @@ -45,6 +45,7 @@ def __init__( self.img_size = img_size @property + @abstractmethod def num_channels(self): pass diff --git a/deeplabcut/pose_estimation_pytorch/post_processing/nms.py b/deeplabcut/pose_estimation_pytorch/post_processing/nms.py index 6bf9368d07..f8a1dad015 100644 --- a/deeplabcut/pose_estimation_pytorch/post_processing/nms.py +++ b/deeplabcut/pose_estimation_pytorch/post_processing/nms.py @@ -88,6 +88,6 @@ def nms_oks( for j in order ] to_keep = [s < oks_threshold and not np.isnan(s) for s in oks_scores] - order = [idx for idx, kept in zip(order, to_keep) if kept] + order = [idx for idx, kept in zip(order, to_keep, strict=False) if kept] return keep diff --git a/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py b/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py index a1e38510d0..a859ce55dd 100644 --- a/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py +++ b/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py @@ -330,9 +330,7 @@ def load_param(self, model_path): except Exception: print("===========================ERROR=========================") print( - f"shape do not match in k :{k}: param_dict{v.shape} vs self.state_dict(){ - self.state_dict()[k].shape - }" + f"shape do not match in k :{k}: param_dict{v.shape} vs self.state_dict(){self.state_dict()[k].shape}" ) diff --git a/deeplabcut/pose_tracking_pytorch/processor/processor.py b/deeplabcut/pose_tracking_pytorch/processor/processor.py index 3d88ea9ad6..96169a1935 100644 --- a/deeplabcut/pose_tracking_pytorch/processor/processor.py +++ b/deeplabcut/pose_tracking_pytorch/processor/processor.py @@ -16,21 +16,20 @@ import numpy as np import torch -import torch.distributed as dist import torch.nn as nn from ..tracking_utils.meter import AverageMeter from ..tracking_utils.metrics import R1_mAP_eval -def dist(a, b): +def custom_dist(a, b): return torch.sqrt(torch.sum((a - b) ** 2, dim=1)) def calc_correct(anchor, pos, neg): # cos = torch.cdist - ap_dist = dist(anchor, pos) - an_dist = dist(anchor, neg) + ap_dist = custom_dist(anchor, pos) + an_dist = custom_dist(anchor, neg) indices = ap_dist < an_dist return torch.sum(indices) diff --git a/examples/testscript.py b/examples/testscript.py index e6d040173f..65869d3b38 100644 --- a/examples/testscript.py +++ b/examples/testscript.py @@ -202,7 +202,7 @@ outsuffix="short", outpath=os.path.join(cfg["project_path"], "videos"), ) - except: # if ffmpeg is broken/missing + except Exception: # if ffmpeg is broken/missing print("using alternative method") newvideo = os.path.join(cfg["project_path"], "videos", videoname + "short.mp4") from moviepy.editor import VideoClip, VideoFileClip @@ -324,7 +324,7 @@ def make_frame(t): outpath=os.path.join(cfg["project_path"], "videos"), ) - except: # if ffmpeg is broken + except Exception: # if ffmpeg is broken newvideo2 = os.path.join(cfg["project_path"], "videos", videoname + "short2.mp4") from moviepy.editor import VideoClip, VideoFileClip diff --git a/tests/generate_training_dataset/test_trainset_metadata.py b/tests/generate_training_dataset/test_trainset_metadata.py index 6147134d1d..ecc99d8cb2 100644 --- a/tests/generate_training_dataset/test_trainset_metadata.py +++ b/tests/generate_training_dataset/test_trainset_metadata.py @@ -243,7 +243,7 @@ def test_add_shuffle_sorts_to_correct_order(tmpdir): "shuffles", [indices for indices in [[1], [1, 2], [1, 2, 3], [1, 2, 4], [1, 3, 4], [1, 2, 3, 4]]] ) @pytest.mark.parametrize("shuffle_to_add", [1, 2, 3, 4]) -def test_add_shuffle(tmpdir, shuffles, shuffle_to_add): +def test_add_shuffle_indices(tmpdir, shuffles, shuffle_to_add): """Tests.""" cfg, cfg_path, trainset_dir, meta_path = _create_project_with_config(tmpdir) trainset_meta = metadata.TrainingDatasetMetadata(cfg, tuple([SHUFFLES[i] for i in shuffles])) From e0735128ab5b66b874e65a04ffb709f67f0fcc1c Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 11:43:27 +0100 Subject: [PATCH 39/80] Fix invalid escaped str --- .../pose_tracking_pytorch/model/backbones/vit_pytorch.py | 4 +--- deeplabcut/utils/pseudo_label.py | 8 +++----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py b/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py index a859ce55dd..56a1129d3a 100644 --- a/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py +++ b/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py @@ -344,9 +344,7 @@ def resize_pos_embed(posemb, posemb_new, height, width): gs_old = int(math.sqrt(len(posemb_grid))) print( - f"Resized position embedding from size:{posemb.shape} to size: {posemb_new.shape} with height:{height} width: { - width - }" + f"Resized position embedding from size:{posemb.shape} to size: {posemb_new.shape} with height:{height} width: {width}" ) posemb_grid = posemb_grid.reshape(1, gs_old, gs_old, -1).permute(0, 3, 1, 2) posemb_grid = F.interpolate(posemb_grid, size=(height, width), mode="bilinear") diff --git a/deeplabcut/utils/pseudo_label.py b/deeplabcut/utils/pseudo_label.py index 6fdceccce0..72671827dc 100644 --- a/deeplabcut/utils/pseudo_label.py +++ b/deeplabcut/utils/pseudo_label.py @@ -395,11 +395,9 @@ def dlc3predictions_2_annotation_from_video( # Since the inference API does not return the image path, I assume the # predictions are provided in the same order as the frames in the video. - assert len(image_paths) == len( - predictions - ), f"number of images must be equal to number of predictions. image_paths: {len(image_paths)} , predictions: { - len(predictions) - }" + assert len(image_paths) == len(predictions), ( + f"number of images must be equal to number of predictions. image_paths: {len(image_paths)} , predictions: {len(predictions)}" + ) len(bodyparts) From f62a76b04bb978458f3d36ec232d33237c9c18d6 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 11:46:10 +0100 Subject: [PATCH 40/80] Add note about broken escaped f-strings Update usage text in tools/trim_lines.py to warn that trimming can produce broken escaped f-strings and provide guidance to find and fix them. Adds an example of a split multi-line f-string and suggests using the ^[ \t]*\}"[ \t]*$ regex to locate offending lines. --- tools/trim_lines.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/trim_lines.py b/tools/trim_lines.py index fd2d4bb82e..f962b609e3 100644 --- a/tools/trim_lines.py +++ b/tools/trim_lines.py @@ -5,6 +5,12 @@ python fix_e501_with_autopep8.py . --line-length 88 python fix_e501_with_autopep8.py src tests --line-length 100 --check +NOTE: if this creates broken escaped f-strings : +f"some string with a { + var +}" +Use the ^[ \t]*\}"[ \t]*$ regex to find and fix them. + Requirements: - ruff - autopep8 From c6d3896d30ae0a93c2c7692db1063a4ff2dc2aef Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 11:49:03 +0100 Subject: [PATCH 41/80] Fix broken strings --- .../pose_tracking_pytorch/processor/processor.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/deeplabcut/pose_tracking_pytorch/processor/processor.py b/deeplabcut/pose_tracking_pytorch/processor/processor.py index 96169a1935..05374b54b2 100644 --- a/deeplabcut/pose_tracking_pytorch/processor/processor.py +++ b/deeplabcut/pose_tracking_pytorch/processor/processor.py @@ -127,8 +127,10 @@ def do_dlc_train( if (n_iter + 1) % log_period == 0: logger.info( - f"Epoch[{epoch}] Iteration[{n_iter + 1}/{len(train_loader)}] Loss: { - loss_meter.avg:.3f}, , Base Lr: {scheduler._get_lr(epoch)[0]:.2e}" + f"Epoch[{epoch}] " + f"Iteration[{n_iter + 1}/{len(train_loader)}] " + f"Loss: {loss_meter.avg:.3f} " + f"Base Lr: {scheduler._get_lr(epoch)[0]:.2e}" ) end_time = time.time() @@ -140,8 +142,9 @@ def do_dlc_train( pass else: logger.info( - f"Epoch {epoch} done. Time per batch: {time_per_batch:.3f}[s] Speed: { - train_loader.batch_size / time_per_batch:.1f}[samples/s]" + f"Epoch {epoch} done. " + f"Time per batch: {time_per_batch:.3f}[s] " + f"Speed: {train_loader.batch_size / time_per_batch:.1f}[samples/s]" ) model_name = "dlc_transreid" From d67ad46e48c55df0a3ed871a7cabce172d8f687c Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 11:55:45 +0100 Subject: [PATCH 42/80] Add Ruff cleanup helper documentation Add a comprehensive README section describing two developer-focused utilities for Ruff lint cleanup: generate_ruff_report.py (produce human-readable Markdown reports from Ruff JSON) and fix_e501_with_autopep8.py (narrow pipeline to reduce E501 line-too-long violations using autopep8, ruff --fix --unsafe-fixes, and ruff format). Includes usage examples, requirements (python, ruff, autopep8), recommended contributor workflow, safety notes, and caveats (notably malformed multiline f-strings). Aims to aid incremental lint adoption and local cleanup work without changing CI/pre-commit behavior. --- tools/README.md | 325 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 325 insertions(+) diff --git a/tools/README.md b/tools/README.md index 07632283a3..1c5fdf5b7f 100644 --- a/tools/README.md +++ b/tools/README.md @@ -39,3 +39,328 @@ For coverage run: coverage run -m pytest coverage report ``` + +# Ruff Cleanup Helpers + +This document describes two small developer-focused utilities that help contributors work through Ruff lint issues in a large Python codebase: + +- `generate_ruff_report.py` — generate a readable Markdown report from Ruff JSON output +- `fix_e501_with_autopep8.py` — aggressively reduce `E501` (line-too-long) violations, then normalize with Ruff + +These tools are intended for **local cleanup workflows**, **incremental lint adoption**, and **one-off contributor maintenance work**. +They are especially useful when a repository already has a non-trivial Ruff backlog and you want to: + +1. understand what remains, +2. prioritize manual fixes, +3. and automate the highest-volume style cleanups safely enough for review. + +--- + +## Who should use these tools? + +These scripts are aimed at: + +- contributors doing lint cleanup PRs, +- maintainers reducing legacy Ruff debt, +- developers triaging a large number of remaining violations, +- anyone who wants a more readable workflow than raw CLI output. + +They are **not** intended to replace normal Ruff usage in CI or pre-commit. Instead, think of them as **cleanup helpers** around Ruff. + +--- + +## What each script does + +### `generate_ruff_report.py` + +Runs Ruff in JSON mode and turns the results into a **human-readable Markdown report**. + +It groups issues: + +- by Ruff rule, +- then by file, +- then by line/column/message. + +It also includes: + +- a summary table, +- short hints for common rules, +- a suggested triage order, +- simple `code -g file:line` commands to jump into affected files. + +This is useful when raw `ruff check` output is too noisy or when you want something that can be attached to an issue / PR / cleanup plan. +It also provides quick navigation and file open commands to help you jump into the right places in the codebase. + +--- + +### `fix_e501_with_autopep8.py` + +Finds files that still contain Ruff `E501` violations, then runs a narrow cleanup pipeline on those files only: + +1. `autopep8` to aggressively reflow long lines, +2. `ruff check --fix --unsafe-fixes` to apply available lint fixes, +3. `ruff format` to normalize formatting. + +This script is intentionally scoped to **files that Ruff already reports as having `E501`** so that it avoids unnecessary churn in unrelated files. + +> [!WARNING] +> This can be rather aggressive. One known issue is for f-strings that are wrapped across multiple lines, which may produce a broken pattern such as: +> ```python +> f"some string with a { +> var +> }" +> ``` +> If that happens, search for lines where `}"` appears by itself with only indentation around it. +> +> A useful regex is: +> ```regex +> ^[ \t]*\}"[ \t]*$ +> ``` + +--- + +## Requirements + +### Required tools + +Both scripts assume the following tools are available on your system PATH: + +- `python` +- `ruff` + +Additionally: + +- `fix_e501_with_autopep8.py` also requires `autopep8` + +### Install example + +```bash +python -m pip install ruff autopep8 +``` + +If you use `uv`: + +```bash +uv add --dev ruff autopep8 +``` + +--- + +## Script 1: `generate_ruff_report.py` + +### Purpose + +Generate a readable Markdown report from Ruff's JSON output. + +### Typical usage + +Run on the whole repository: + +```bash +python generate_ruff_report.py . --output tmp/ruff-report.md +``` + +Run on selected paths only: + +```bash +python generate_ruff_report.py src tests --output tmp/ruff-report.md +``` + +### Output + +By default the script writes to: + +```text +tmp/ruff-report.md +``` + +The output contains: + +- total issue count, +- summary table by rule, +- short notes for common rules, +- suggested triage order, +- per-rule sections, +- per-file counts, +- detailed line/column/message tables, +- quick-open commands for VS Code. + +### Example workflow + +```bash +ruff check . +python generate_ruff_report.py . --output tmp/ruff-report.md +``` + +Open the Markdown report, pick a rule family (for example `F403`, `F405`, `F821`, `E722`, `B904`), and work through the files systematically. + +--- + +## Script 2: `fix_e501_with_autopep8.py` + +### Purpose + +Reduce Ruff `E501` violations (`line-too-long`) using `autopep8`, then normalize those files with Ruff. + +### Typical usage + +Run on the whole repository: + +```bash +python fix_e501_with_autopep8.py . --line-length 88 +``` + +Run on selected paths only: + +```bash +python fix_e501_with_autopep8.py src tests --line-length 100 +``` + +Dry-run mode (show affected files only): + +```bash +python fix_e501_with_autopep8.py . --line-length 88 --check +``` + +### What it does internally + +For the given paths, the script: + +1. runs Ruff in JSON mode, +2. extracts the set of files that still contain `E501`, +3. runs `autopep8` only on those files, +4. runs `ruff check --fix --unsafe-fixes` on the same files, +5. runs `ruff format` on those same files, +6. prints how many files still contain `E501` afterwards. + +### Why this script is narrow by design + +`E501` cleanup can create a lot of diff noise if you run formatters indiscriminately. This tool tries to keep the blast radius smaller by only touching files already flagged by Ruff for line length issues. + +### Known caveat: malformed multiline f-strings + +In some cases, aggressive line wrapping may produce a broken multiline f-string pattern such as: + +```python +f"some string with a { + var +}" +``` + +If that happens, search for lines where `}"` appears by itself with only indentation around it. + +A useful regex is: + +```regex +^[ \t]*\}"[ \t]*$ +``` + +This can help you quickly find and manually repair those cases. + +### Good use cases + +- reducing a large backlog of `E501` violations before a more careful cleanup pass, +- "massaging" legacy code that was never formatter-cleaned consistently. + +--- + +## Recommended workflow for contributors + +If you are working on lint cleanup, a practical workflow is: + +### 1. Generate a report + +```bash +python generate_ruff_report.py . --output tmp/ruff-report.md +``` + +### 2. Reduce long lines first (optional but often useful) + +```bash +python fix_e501_with_autopep8.py . --line-length 88 +``` + +### 3. Re-run the report + +```bash +python generate_ruff_report.py . --output tmp/ruff-report.md +``` + +### 4. Triage remaining issues manually + +--- + +## Limitations + +### `generate_ruff_report.py` + +- only reports what Ruff emits, +- does not fix anything, +- hints are heuristic and intentionally brief. + +### `fix_e501_with_autopep8.py` + +- targets only `E501` files, +- depends on `autopep8` behavior, +- may create formatting diffs that require manual review, +- cannot infer semantic intent for every line wrap, +- may occasionally produce awkward formatting or broken multiline f-strings. + +--- + +## Safety notes + +Before committing results from `fix_e501_with_autopep8.py`: + +1. run Ruff again, +2. run the relevant test suite, +3. scan diff hunks involving long strings / f-strings / messages, +4. review any surprising changes in error messages, docstrings, or string interpolation. + +Suggested commands: + +```bash +ruff check . +ruff format --check . +pytest +``` + +--- + +## Examples + +### Generate a repo-wide manual-fix report + +```bash +python generate_ruff_report.py . --output tmp/ruff-report.md +``` + +### Generate a report only for Python package code + +```bash +python generate_ruff_report.py deeplabcut tests --output tmp/ruff-report.md +``` + +### See which files still have `E501` + +```bash +python fix_e501_with_autopep8.py . --line-length 120 --check +``` + +### Reduce long-line issues, then review the remaining backlog + +```bash +python fix_e501_with_autopep8.py . --line-length 120 +python generate_ruff_report.py . --output tmp/ruff-report.md +``` + +--- + +## Summary + +These scripts are small but practical helpers for maintaining a large Ruff-enabled Python repository: + +- `generate_ruff_report.py` turns Ruff output into a human-readable action plan +- `fix_e501_with_autopep8.py` helps shrink `E501` noise before manual cleanup + +Use them as **developer tools**, not as a substitute for understanding or reviewing changes. From 07e50a81e39a6e895a15241cd7b3c722d91c6cfe Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 13:17:12 +0100 Subject: [PATCH 43/80] Add import cycle detector script Add tools/find_import_cycles.py: a small utility that maps package modules, parses Python files with ast (including resolving relative imports), builds an internal import graph and performs a DFS to detect and print import cycles. The script defaults to scanning the 'deeplabcut' package (adjustable) and prints any found cycles or a no-cycles message. --- tools/find_import_cycles.py | 123 ++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 tools/find_import_cycles.py diff --git a/tools/find_import_cycles.py b/tools/find_import_cycles.py new file mode 100644 index 0000000000..743392dc15 --- /dev/null +++ b/tools/find_import_cycles.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import ast +from collections import defaultdict +from pathlib import Path + + +def path_to_module(root: Path, file: Path) -> str: + rel = file.relative_to(root) + parts = rel.with_suffix("").parts + if parts[-1] == "__init__": + parts = parts[:-1] + return ".".join((root.name, *parts)) if parts else root.name + + +def module_to_file_map(root: Path) -> dict[str, Path]: + mapping = {} + for file in root.rglob("*.py"): + mod = path_to_module(root, file) + mapping[mod] = file + return mapping + + +def resolve_relative_import(current_module: str, module: str | None, level: int) -> str | None: + parts = current_module.split(".") + if level > len(parts): + return None + base = parts[:-level] + if module: + return ".".join(base + module.split(".")) + return ".".join(base) + + +def extract_imports(file: Path, current_module: str) -> set[str]: + source = file.read_text(encoding="utf-8") + tree = ast.parse(source, filename=str(file)) + imports: set[str] = set() + + for node in ast.walk(tree): + if isinstance(node, ast.Import): + for alias in node.names: + imports.add(alias.name) + elif isinstance(node, ast.ImportFrom): + if node.level and current_module: + resolved = resolve_relative_import(current_module, node.module, node.level) + if resolved: + imports.add(resolved) + elif node.module: + imports.add(node.module) + + return imports + + +def internal_edges(root: Path) -> dict[str, set[str]]: + mod_to_file = module_to_file_map(root) + internal = set(mod_to_file) + edges: dict[str, set[str]] = defaultdict(set) + + for mod, file in mod_to_file.items(): + for imported in extract_imports(file, mod): + # Keep only imports that are inside the package + for candidate in internal: + if imported == candidate or imported.startswith(candidate + "."): + edges[mod].add(candidate) + break + + return edges + + +def find_cycles(edges: dict[str, set[str]]) -> list[list[str]]: + visited = set() + stack = [] + on_stack = set() + cycles = [] + + def dfs(node: str): + visited.add(node) + stack.append(node) + on_stack.add(node) + + for neighbor in edges.get(node, ()): + if neighbor not in visited: + dfs(neighbor) + elif neighbor in on_stack: + idx = stack.index(neighbor) + cycle = stack[idx:] + [neighbor] + cycles.append(cycle) + + stack.pop() + on_stack.remove(node) + + for node in edges: + if node not in visited: + dfs(node) + + # Deduplicate roughly + seen = set() + unique = [] + for cyc in cycles: + key = tuple(cyc) + if key not in seen: + seen.add(key) + unique.append(cyc) + return unique + + +def main(): + root = Path("deeplabcut") # change if needed + edges = internal_edges(root) + cycles = find_cycles(edges) + + if not cycles: + print("No cycles found.") + return + + print("Import cycles found:\n") + for cyc in cycles: + print(" -> ".join(cyc)) + + +if __name__ == "__main__": + main() From 59d95143268a3cc92e716ea962947d8947d19208 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 13:18:04 +0100 Subject: [PATCH 44/80] Fix circular imports Use internal, explicit imports instead of vague re-export based imports from __init__ --- deeplabcut/cli.py | 22 ++++++++++--------- deeplabcut/create_project/add.py | 6 ++--- ...ple_individuals_trainingsetmanipulation.py | 3 ++- .../core/openvino/session.py | 2 +- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/deeplabcut/cli.py b/deeplabcut/cli.py index 3914485183..6fbb0a650c 100644 --- a/deeplabcut/cli.py +++ b/deeplabcut/cli.py @@ -161,9 +161,9 @@ def extract_frames(_, *args, **kwargs): While selecting the frames manually, you do not need to specify the cropping parameters. Rather, you will get a prompt in the graphic user interface to choose if you need to crop or not. \n -------- \n """ - from deeplabcut.generate_training_dataset import frameExtraction + from deeplabcut.generate_training_dataset.frame_extraction import extract_frames as _extract_frames - frameExtraction.extract_frames(*args, **kwargs) + _extract_frames(*args, **kwargs) ########################################################################## @@ -178,9 +178,9 @@ def label_frames(_, config): --------\n python3 dlc.py label_frames /analysis/project/reaching-task/config.yaml """ - from deeplabcut.generate_training_dataset import labelFrames + from deeplabcut.gui.tabs.label_frames import label_frames as _label_frames - labelFrames.label_frames(config) + _label_frames(config) ########################################################################## @@ -193,9 +193,9 @@ def check_labels(_, config): If some are wrong, then use the refine_labels to correct the labels.\n """ - from deeplabcut.generate_training_dataset import labelFrames + from deeplabcut.generate_training_dataset.trainingsetmanipulation import check_labels as _check_labels - labelFrames.check_labels(config) + _check_labels(config) ########################################################################## @@ -221,9 +221,11 @@ def create_training_dataset(_, *args, **kwargs): To create a training dataset with only 2 shuffles python3 dlc.py create_training_dataset /analysis/project/reaching-task/config.yaml num_shuffles 2 """ - from deeplabcut.generate_training_dataset import labelFrames + from deeplabcut.generate_training_dataset.trainingsetmanipulation import ( + create_training_dataset as _create_training_dataset, + ) - labelFrames.create_training_dataset(*args, **kwargs) + _create_training_dataset(*args, **kwargs) ########################################################################## @@ -273,9 +275,9 @@ def evaluate_network(_, config, **kwargs): python3 dlc.py evaluate_network /home/project/reaching/config.yaml """ - from deeplabcut.pose_estimation_tensorflow import evaluate + from deeplabcut.pose_estimation_tensorflow.core.evaluate import evaluate_network as _evaluate_network - evaluate.evaluate_network(config, **kwargs) + _evaluate_network(config, **kwargs) ########################################################################## diff --git a/deeplabcut/create_project/add.py b/deeplabcut/create_project/add.py index ef03b50986..550c5a91e7 100644 --- a/deeplabcut/create_project/add.py +++ b/deeplabcut/create_project/add.py @@ -55,9 +55,9 @@ def add_new_videos(config, videos, copy_videos=False, coords=None, extract_frame import shutil from pathlib import Path - from deeplabcut.generate_training_dataset import frame_extraction - from deeplabcut.utils import auxiliaryfunctions - from deeplabcut.utils.auxfun_videos import VideoReader + from ..generate_training_dataset import frame_extraction + from ..utils import auxiliaryfunctions + from ..utils.auxfun_videos import VideoReader # Read the config file cfg = auxiliaryfunctions.read_config(config) diff --git a/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py b/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py index 1ae2f3bbfe..6de8d7ef51 100755 --- a/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py +++ b/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py @@ -624,9 +624,10 @@ def convert_cropped_to_standard_dataset( import pandas as pd - from deeplabcut.generate_training_dataset import trainingsetmanipulation from deeplabcut.utils import read_plainconfig, write_config + from . import trainingsetmanipulation + cfg = auxiliaryfunctions.read_config(config_path) videos_orig = cfg.pop("video_sets_original") is_cropped = cfg.pop("croppedtraining") diff --git a/deeplabcut/pose_estimation_tensorflow/core/openvino/session.py b/deeplabcut/pose_estimation_tensorflow/core/openvino/session.py index 015d09218e..b65d35c73a 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/openvino/session.py +++ b/deeplabcut/pose_estimation_tensorflow/core/openvino/session.py @@ -15,7 +15,7 @@ import numpy as np from tqdm import tqdm -from deeplabcut.pose_estimation_tensorflow.predict_videos import checkcropping +from ...predict_videos import checkcropping try: from openvino.runtime import AsyncInferQueue, Core From e22ee470badb3762bea58ab58daf21183034ff76 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 13:28:13 +0100 Subject: [PATCH 45/80] Import checkcropping only when needed Remove the top-level import of checkcropping and move it into GetPoseF_OV under the cfg["cropping"] branch. This defers the import to runtime so the helper is only loaded when cropping is enabled, avoiding unnecessary imports (and potential circular/dependency issues) at module import time. --- .../pose_estimation_tensorflow/core/openvino/session.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deeplabcut/pose_estimation_tensorflow/core/openvino/session.py b/deeplabcut/pose_estimation_tensorflow/core/openvino/session.py index b65d35c73a..c4c32ecc7a 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/openvino/session.py +++ b/deeplabcut/pose_estimation_tensorflow/core/openvino/session.py @@ -15,8 +15,6 @@ import numpy as np from tqdm import tqdm -from ...predict_videos import checkcropping - try: from openvino.runtime import AsyncInferQueue, Core @@ -106,6 +104,8 @@ def GetPoseF_OV(cfg, dlc_cfg, sess, inputs, outputs, cap, nframes, batchsize): PredictedData = np.zeros((nframes, 3 * len(dlc_cfg["all_joints_names"]))) ny, nx = int(cap.get(4)), int(cap.get(3)) if cfg["cropping"]: + from ...predict_videos import checkcropping + ny, nx = checkcropping(cfg, cap) sess._init_model(ny, nx) From 9b61e2556b51b419bb5a5ad0b3695e6288fedfd6 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 13:30:43 +0100 Subject: [PATCH 46/80] Re-run pre-commit on all --- .github/versioning/tf-ci-constraints.txt | 2 +- .github/workflows/format.yml | 2 +- CONTRIBUTING.md | 17 +- LICENSE | 1 - NOTICE.yml | 2 +- README.md | 36 ++-- deeplabcut/benchmark/benchmarks.py | 82 ++++++++-- deeplabcut/benchmark/mot.py | 11 +- deeplabcut/core/conversion_table.py | 6 +- deeplabcut/core/engine.py | 2 +- deeplabcut/core/metrics/api.py | 4 +- deeplabcut/create_project/demo_data.py | 9 +- deeplabcut/gui/displays/__init__.py | 2 - deeplabcut/gui/style.qss | 6 +- .../conversion_table_quadruped.csv | 2 +- deeplabcut/modelzoo/fmpose_3d/README.md | 2 +- deeplabcut/modelzoo/fmpose_3d/__init__.py | 9 +- .../fasterrcnn_resnet50_fpn_v2.yaml | 2 +- .../modelzoo/model_configs/ssdlite.yaml | 2 +- .../superanimal_humanbody.yaml | 2 +- deeplabcut/modelzoo/weight_initialization.py | 4 +- deeplabcut/pose_cfg.yaml | 2 +- deeplabcut/pose_estimation_pytorch/README.md | 66 ++++---- .../pose_estimation_pytorch/apis/export.py | 5 +- .../apis/tracking_dataset.py | 4 +- .../config/backbones/resnet_50.yaml | 2 +- .../config/ctd/ctd_coam_w32.yaml | 2 +- .../config/ctd/ctd_coam_w48.yaml | 2 +- .../config/ctd/ctd_coam_w48_human.yaml | 2 +- .../config/ctd/ctd_prenet_hrnet_w48.yaml | 2 +- .../config/dekr/dekr_w18.yaml | 2 +- .../config/dekr/dekr_w32.yaml | 2 +- .../config/dekr/dekr_w48.yaml | 2 +- .../pose_estimation_pytorch/data/dataset.py | 25 ++- .../pose_estimation_pytorch/data/helper.py | 11 +- .../pose_estimation_pytorch/data/image.py | 11 +- .../models/backbones/cspnext.py | 6 +- .../models/detectors/fasterRCNN.py | 2 +- .../models/detectors/filtered_detector.py | 3 +- .../models/detectors/ssd.py | 2 +- .../models/detectors/torchvision.py | 10 +- .../models/modules/csp.py | 4 +- .../models/modules/norm.py | 4 +- .../modelzoo/config.py | 7 +- .../modelzoo/train_from_coco.py | 2 +- .../match_predictions_to_gt.py | 3 +- .../pose_estimation_pytorch/runners/base.py | 9 +- .../pose_estimation_pytorch/runners/ctd.py | 2 +- deeplabcut/pose_estimation_pytorch/task.py | 4 +- deeplabcut/pose_estimation_tensorflow/LICENSE | 1 - .../lib/crossvalutils.py | 2 +- .../lib/inferenceutils.py | 2 +- .../lib/trackingutils.py | 2 +- .../models/pretrained/download.sh | 2 +- .../solver/scheduler_factory.py | 4 +- .../tracking_utils/preprocessing.py | 12 +- deeplabcut/reid_cfg.yaml | 2 +- deeplabcut/utils/frameselectiontools.py | 24 +-- docker/LICENSE | 1 - docker/Makefile | 2 +- docker/README.md | 40 ++--- docker/motd.sh | 4 +- docker/pyproject.toml | 4 +- docker/setup.cfg | 4 +- docs/HelperFunctions.md | 2 +- docs/Overviewof3D.md | 2 +- docs/README.md | 2 +- docs/UseOverviewGuide.md | 4 +- docs/beginner-guides/Training-Evaluation.md | 4 +- docs/beginner-guides/beginners-guide.md | 24 +-- docs/beginner-guides/labeling.md | 7 +- docs/beginner-guides/video-analysis.md | 2 +- docs/citation.md | 18 +- docs/course.md | 2 +- docs/dlc-live/dlc-live-gui/index.md | 2 +- .../cameras_backends/basler_backend.md | 2 +- .../cameras_backends/camera_support.md | 6 +- .../user_guide/misc/timestamp_format.md | 2 +- docs/docker.md | 22 +-- docs/installation.md | 40 ++--- docs/maDLC_UserGuide.md | 76 ++++----- docs/pytorch/Benchmarking_shuffle_guide.md | 54 +++--- docs/pytorch/architectures.md | 44 ++--- docs/pytorch/pytorch_config.md | 154 +++++++++--------- docs/pytorch/user_guide.md | 16 +- docs/pytorch_dlc.md | 34 ++-- docs/quick-start/single_animal_quick_guide.md | 12 +- docs/recipes/MegaDetectorDLCLive.md | 4 +- docs/recipes/OtherData.md | 12 +- docs/recipes/TechHardware.md | 4 +- docs/recipes/installTips.md | 12 +- docs/recipes/io.md | 16 +- docs/recipes/nn.md | 10 +- docs/recipes/pose_cfg_file_breakdown.md | 50 +++--- ...ng_notebooks_into_the_DLC_main_cookbook.md | 24 +-- docs/standardDeepLabCut_UserGuide.md | 22 +-- ...t_superanimal_create_pretrained_project.py | 5 +- ...estscript_superanimal_transfer_learning.py | 4 +- setup.py | 9 +- tests/core/metrics/test_metrics_api.py | 2 +- .../metrics/test_metrics_identity_accuracy.py | 2 +- .../metrics/test_metrics_rmse_computation.py | 2 +- .../test_trainingset_manipulation.py | 2 +- .../apis/test_create_tracking_dataset.py | 4 +- .../config/test_config_utils.py | 2 +- .../data/test_utils.py | 2 +- .../post_processing/test_identity.py | 2 +- .../test_postprocessing_nms.py | 4 +- ...test_filtered_detector_inference_runner.py | 6 +- .../runners/test_logger.py | 6 +- .../runners/test_runners_train.py | 7 +- .../runners/test_schedulers.py | 2 +- .../runners/test_shelving.py | 4 +- .../runners/test_task.py | 2 +- tests/test_evaluate.py | 3 +- 115 files changed, 638 insertions(+), 626 deletions(-) diff --git a/.github/versioning/tf-ci-constraints.txt b/.github/versioning/tf-ci-constraints.txt index b51e68e3a5..18d31a3ce1 100644 --- a/.github/versioning/tf-ci-constraints.txt +++ b/.github/versioning/tf-ci-constraints.txt @@ -1 +1 @@ -protobuf<7 \ No newline at end of file +protobuf<7 diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 8f1509a6d8..95450e5883 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -58,4 +58,4 @@ jobs: CHANGED_FILES: ${{ needs.detect_changes.outputs.changed }} run: | mapfile -t files <<< "$CHANGED_FILES" - pre-commit run --hook-stage manual --files "${files[@]}" --show-diff-on-failure \ No newline at end of file + pre-commit run --hook-stage manual --files "${files[@]}" --show-diff-on-failure diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4a9c907974..5c2ff62cd0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,13 +13,13 @@ In order to make changes to `deeplabcut`, you will need to [fork](https://guides If you are not familiar with `git`, we recommend reading up on [this guide](https://guides.github.com/introduction/git-handbook/#basic-git). -Here are guidelines for installing deeplabcut locally on your own computer, where you can make changes to the code! We often update the master deeplabcut code base on github, and then ~1 a month we push out a stable release on pypi. This is what most users turn to on a daily basis (i.e. pypi is where you get your `pip install deeplabcut` code from! +Here are guidelines for installing deeplabcut locally on your own computer, where you can make changes to the code! We often update the master deeplabcut code base on github, and then ~1 a month we push out a stable release on pypi. This is what most users turn to on a daily basis (i.e. pypi is where you get your `pip install deeplabcut` code from! -But, sometimes we add things to the repo that are not yet integrated, or you might want to edit the code yourself, or you will need to do this to contribute. Here, we show you how to do this. +But, sometimes we add things to the repo that are not yet integrated, or you might want to edit the code yourself, or you will need to do this to contribute. Here, we show you how to do this. **Step 1:** -- git clone the repo into a folder on your computer: +- git clone the repo into a folder on your computer: - click on this green button and copy the link: @@ -52,8 +52,8 @@ Now, please make a [pull request](https://github.com/DeepLabCut/DeepLabCut/pull/ - How you modified the code and what new functionality it has. - DOCSTRING update for your change -- A working example of how it works for users. -- If it's a function that also can be used in downstream steps (i.e. could be plotted) we ask you (1) highlight this, and (2) ideally you provide that functionality as well. If you have any questions, please reach out: admin@deeplabcut.org +- A working example of how it works for users. +- If it's a function that also can be used in downstream steps (i.e. could be plotted) we ask you (1) highlight this, and (2) ideally you provide that functionality as well. If you have any questions, please reach out: admin@deeplabcut.org **TestScript outputs:** @@ -62,16 +62,15 @@ Now, please make a [pull request](https://github.com/DeepLabCut/DeepLabCut/pull/ **Review & Formatting:** -- Please run black on the code to conform to our Black code style (see more at https://pypi.org/project/black/). +- Please run black on the code to conform to our Black code style (see more at https://pypi.org/project/black/). - Please assign a reviewer, typically @AlexEMG, @mmathislab, or @jeylau (i/e. the [core-developers](https://github.com/orgs/DeepLabCut/teams/core-developers/members)) **Code headers** - The code headers can be standardized by running `python tools/update_license_headers.py` -- Edit `NOTICE.yml` to update the header. +- Edit `NOTICE.yml` to update the header. **DeepLabCut is an open-source tool and has benefited from suggestions and edits by many individuals:** - the [authors](/AUTHORS) -- [code contributors](https://github.com/DeepLabCut/DeepLabCut/graphs/contributors) - +- [code contributors](https://github.com/DeepLabCut/DeepLabCut/graphs/contributors) diff --git a/LICENSE b/LICENSE index 341c30bda4..65c5ca88a6 100644 --- a/LICENSE +++ b/LICENSE @@ -163,4 +163,3 @@ whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. - diff --git a/NOTICE.yml b/NOTICE.yml index ec8df871b2..d2ced97e0f 100644 --- a/NOTICE.yml +++ b/NOTICE.yml @@ -109,7 +109,7 @@ - deeplabcut/pose_tracking_pytorch/solver/scheduler_factory.py - deeplabcut/pose_tracking_pytorch/model/backones/vit_pytorch.py -# PyTorch license +# PyTorch license - header: | See https://github.com/pytorch/pytorch/blob/main/LICENSE diff --git a/README.md b/README.md index b828abb684..5c177728ad 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@
- +

@@ -8,7 +8,7 @@

- + @@ -18,7 +18,7 @@ - + @@ -27,13 +27,13 @@ [🌎 Home Page](https://www.deeplabcut.org) | [🐿🐴🐁🐘🐆 Model Zoo](http://www.mackenziemathislab.org/deeplabcut/) | [🚨 News](https://deeplabcut.github.io/DeepLabCut/README.html#news-and-in-the-news) | -[🪲 Reporting Issues](https://github.com/DeepLabCut/DeepLabCut/issues) +[🪲 Reporting Issues](https://github.com/DeepLabCut/DeepLabCut/issues) -[🫶 Getting Assistance](https://deeplabcut.github.io/DeepLabCut/README.html#be-part-of-the-dlc-community) | -[∞ DeepLabCut Online Course](https://github.com/DeepLabCut/DeepLabCut-Workshop-Materials/blob/master/DLCcourse.md) | -[📝 Publications](https://deeplabcut.github.io/DeepLabCut/README.html#references) | -[👩🏾‍💻👨‍💻 DeepLabCut AI Residency](https://www.deeplabcutairesidency.org/) +[🫶 Getting Assistance](https://deeplabcut.github.io/DeepLabCut/README.html#be-part-of-the-dlc-community) | +[∞ DeepLabCut Online Course](https://github.com/DeepLabCut/DeepLabCut-Workshop-Materials/blob/master/DLCcourse.md) | +[📝 Publications](https://deeplabcut.github.io/DeepLabCut/README.html#references) | +[👩🏾‍💻👨‍💻 DeepLabCut AI Residency](https://www.deeplabcutairesidency.org/) ![Version](https://img.shields.io/badge/python_version-3.10-purple) @@ -55,7 +55,7 @@ # Welcome! 👋 -**DeepLabCut™️** is a toolbox for state-of-the-art markerless pose estimation of animals performing various behaviors. As long as you can see (label) what you want to track, you can use this toolbox, as it is animal and object agnostic. [Read a short development and application summary below](https://github.com/DeepLabCut/DeepLabCut#why-use-deeplabcut). +**DeepLabCut™️** is a toolbox for state-of-the-art markerless pose estimation of animals performing various behaviors. As long as you can see (label) what you want to track, you can use this toolbox, as it is animal and object agnostic. [Read a short development and application summary below](https://github.com/DeepLabCut/DeepLabCut#why-use-deeplabcut). # [Installation: how to install DeepLabCut](https://deeplabcut.github.io/DeepLabCut/docs/installation.html) @@ -63,7 +63,7 @@ Please click the link above for all the information you need to get started! Ple ## Quick start -Developers Stable Release: very quick start (Python 3.10+ required) to install +Developers Stable Release: very quick start (Python 3.10+ required) to install DeepLabCut with the PyTorch engine - [1] [Install PyTorch](https://pytorch.org/get-started/locally/) (**install and then select the desired @@ -77,15 +77,15 @@ conda install pytorch cudatoolkit=11.3 -c pytorch ```bash pip install --pre "deeplabcut[gui]" ``` -or `pip install --pre "deeplabcut"` (headless +or `pip install --pre "deeplabcut"` (headless version with PyTorch)! To use the TensorFlow (TF) engine (requires Python 3.10; TF up to v2.10 supported on Windows, -up to v2.12 on other platforms): you'll need to run `pip install "deeplabcut[gui,tf]"` +up to v2.12 on other platforms): you'll need to run `pip install "deeplabcut[gui,tf]"` (which includes all functions plus GUIs) or `pip install "deeplabcut[tf]"` (headless version with PyTorch and TensorFlow). We aim to depreciate the TF part in 2027. -We recommend using our conda file, see [here](https://github.com/DeepLabCut/DeepLabCut/blob/main/conda-environments/README.md) or the [`deeplabcut-docker` package](https://github.com/DeepLabCut/DeepLabCut/tree/main/docker). +We recommend using our conda file, see [here](https://github.com/DeepLabCut/DeepLabCut/blob/main/conda-environments/README.md) or the [`deeplabcut-docker` package](https://github.com/DeepLabCut/DeepLabCut/tree/main/docker). # [Documentation: The DeepLabCut Process](https://deeplabcut.github.io/DeepLabCut/README.html) @@ -105,11 +105,11 @@ See [more demos here](https://github.com/DeepLabCut/DeepLabCut/blob/main/example # Why use DeepLabCut? -DeepLabCut continues to be actively maintained and we strive to provide a user-friendly `GUI` and `API` for computer vision researchers and life scientists alike. This means we integrate state-of-the-art models and frameworks, while providing our "best-guess" defaults for life scientists. We highly encourage you to read our papers to get a better understanding of what to use and how to modify the models for your setting. +DeepLabCut continues to be actively maintained and we strive to provide a user-friendly `GUI` and `API` for computer vision researchers and life scientists alike. This means we integrate state-of-the-art models and frameworks, while providing our "best-guess" defaults for life scientists. We highly encourage you to read our papers to get a better understanding of what to use and how to modify the models for your setting. ## Performance 🔥 -In general, we provide all the tooling for you to train and use custom models with various high-performance backbones. +In general, we provide all the tooling for you to train and use custom models with various high-performance backbones. We also provide two foundation pretrained animal models: `SuperAnimal-Quadruped`, `SuperAnimal-TopViewMouse`. To gauge their *out-of-distribution* performance, we provide the following tables. These models are trained on the [SuperAnimal-Quadruped with AP-10K held out for out-of-domain testing]([https://cocodataset.org/](https://www.nature.com/articles/s41467-024-48792-2)) and the [SuperAnimal-TopViewMouse with DLC-openfield held out for out-of-distribution testing](https://www.nature.com/articles/s41467-024-48792-2). We provide models that include AP-10K in the API (and GUI). @@ -139,7 +139,7 @@ We currently provide state-of-the-art performance for animal pose estimation and

- +

@@ -169,7 +169,7 @@ DeepLabCut is an open-source tool and has benefited from suggestions and edits b |------------------------------------------------------------|-----------------------------------------------------------------------------|---------------------------|----------------------------------------| | GitHub DeepLabCut/[Issues](https://github.com/DeepLabCut/DeepLabCut/issues) | To report bugs and code issues🐛 (we encourage you to search issues first) | 2-5 days | DLC Core Dev Team | | GitHub DeepLabCut/[Contributing](https://github.com/DeepLabCut/DeepLabCut/blob/master/CONTRIBUTING.md) | To contribute your expertise and experience🙏💯 | 2-5 days | DLC Core Dev Team | -| 🚧 GitHub DeepLabCut/[Roadmap](https://github.com/DeepLabCut/DeepLabCut/blob/master/docs/roadmap.md) | To learn more about our journey✈️ | N/A | N/A +| 🚧 GitHub DeepLabCut/[Roadmap](https://github.com/DeepLabCut/DeepLabCut/blob/master/docs/roadmap.md) | To learn more about our journey✈️ | N/A | N/A | [![Image.sc forum](https://img.shields.io/badge/dynamic/json.svg?label=forum&url=https%3A%2F%2Fforum.image.sc%2Ftag%2Fdeeplabcut.json&query=%24.topic_list.tags.0.topic_count&colorB=brightgreen&&suffix=%20topics&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAABPklEQVR42m3SyyqFURTA8Y2BER0TDyExZ+aSPIKUlPIITFzKeQWXwhBlQrmFgUzMMFLKZeguBu5y+//17dP3nc5vuPdee6299gohUYYaDGOyyACq4JmQVoFujOMR77hNfOAGM+hBOQqB9TjHD36xhAa04RCuuXeKOvwHVWIKL9jCK2bRiV284QgL8MwEjAneeo9VNOEaBhzALGtoRy02cIcWhE34jj5YxgW+E5Z4iTPkMYpPLCNY3hdOYEfNbKYdmNngZ1jyEzw7h7AIb3fRTQ95OAZ6yQpGYHMMtOTgouktYwxuXsHgWLLl+4x++Kx1FJrjLTagA77bTPvYgw1rRqY56e+w7GNYsqX6JfPwi7aR+Y5SA+BXtKIRfkfJAYgj14tpOF6+I46c4/cAM3UhM3JxyKsxiOIhH0IO6SH/A1Kb1WBeUjbkAAAAAElFTkSuQmCC)](https://forum.image.sc/tag/deeplabcut)
🐭Tag: DeepLabCut | To ask help and support questions 👋 | Promptly🔥 | The DLC Community | |[![Gitter](https://badges.gitter.im/DeepLabCut/community.svg)](https://gitter.im/DeepLabCut/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) | To discuss with other users, share ideas and collaborate💡 | 2-5 days | The DLC Community | | [BluSky🦋](https://bsky.app/profile/deeplabcut.bsky.social) | To keep up with our latest news and updates 📢 | 2-5 days | DLC Team | @@ -217,7 +217,7 @@ VERSION 1.0: The initial, Nature Neuroscience version of [DeepLabCut](https://ww - June 2024: Our second DLC paper ['Using DeepLabCut for 3D markerless pose estimation across species and behaviors'](https://www.nature.com/articles/s41596-019-0176-0) in Nature Protocols has surpassed 1,000 Google Scholar citations! - May 2024: DeepLabCut was featured in Nature: ['DeepLabCut: the motion-tracking tool that went viral'](https://www.nature.com/articles/d41586-024-01474-x) -- January 2024: Our original paper ['DeepLabCut: markerless pose estimation of user-defined body parts with deep learning'](https://www.nature.com/articles/s41593-018-0209-y) in Nature Neuroscience has surpassed 3,000 Google Scholar citations! +- January 2024: Our original paper ['DeepLabCut: markerless pose estimation of user-defined body parts with deep learning'](https://www.nature.com/articles/s41593-018-0209-y) in Nature Neuroscience has surpassed 3,000 Google Scholar citations! - December 2023: DeepLabCut hit 600,000 downloads! - October 2023: DeepLabCut celebrates a milestone with 4,000 🌟 in Github! - July 2023: The user forum is very active with more than 1k questions and answers: [![Image.sc forum](https://img.shields.io/badge/dynamic/json.svg?label=forum&url=https%3A%2F%2Fforum.image.sc%2Ftag%2Fdeeplabcut.json&query=%24.topic_list.tags.0.topic_count&colorB=brightgreen&&suffix=%20topics&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAYAAAAfSC3RAAABPklEQVR42m3SyyqFURTA8Y2BER0TDyExZ+aSPIKUlPIITFzKeQWXwhBlQrmFgUzMMFLKZeguBu5y+//17dP3nc5vuPdee6299gohUYYaDGOyyACq4JmQVoFujOMR77hNfOAGM+hBOQqB9TjHD36xhAa04RCuuXeKOvwHVWIKL9jCK2bRiV284QgL8MwEjAneeo9VNOEaBhzALGtoRy02cIcWhE34jj5YxgW+E5Z4iTPkMYpPLCNY3hdOYEfNbKYdmNngZ1jyEzw7h7AIb3fRTQ95OAZ6yQpGYHMMtOTgouktYwxuXsHgWLLl+4x++Kx1FJrjLTagA77bTPvYgw1rRqY56e+w7GNYsqX6JfPwi7aR+Y5SA+BXtKIRfkfJAYgj14tpOF6+I46c4/cAM3UhM3JxyKsxiOIhH0IO6SH/A1Kb1WBeUjbkAAAAAElFTkSuQmCC)](https://forum.image.sc/tag/deeplabcut) diff --git a/deeplabcut/benchmark/benchmarks.py b/deeplabcut/benchmark/benchmarks.py index 6a76bad957..0701714a4a 100644 --- a/deeplabcut/benchmark/benchmarks.py +++ b/deeplabcut/benchmark/benchmarks.py @@ -24,9 +24,19 @@ class TriMouseBenchmark(deeplabcut.benchmark.base.Benchmark): """Datasets with three mice with a top-view camera. - Three wild-type (C57BL/6J) male mice ran on a paper spool following odor trails (Mathis et al 2018). These experiments were carried out in the laboratory of Venkatesh N. Murthy at Harvard University. Data were recorded at 30 Hz with 640 x 480 pixels resolution acquired with a Point Grey Firefly FMVU-03MTM-CS. One human annotator was instructed to localize the 12 keypoints (snout, left ear, right ear, shoulder, four spine points, tail base and three tail points). All surgical and experimental procedures for mice were in accordance with the National Institutes of Health Guide for the Care and Use of Laboratory Animals and approved by the Harvard Institutional Animal Care and Use Committee. 161 frames were labeled, making this a real-world sized laboratory dataset. - - Introduced in Lauer et al. "Multi-animal pose estimation, identification and tracking with DeepLabCut." Nature Methods 19, no. 4 (2022): 496-504. + Three wild-type (C57BL/6J) male mice ran on a paper spool following odor trails + (Mathis et al 2018). These experiments were carried out in the laboratory of + Venkatesh N. Murthy at Harvard University. Data were recorded at 30 Hz with 640 x + 480 pixels resolution acquired with a Point Grey Firefly FMVU-03MTM-CS. One human + annotator was instructed to localize the 12 keypoints (snout, left ear, right ear, + shoulder, four spine points, tail base and three tail points). All surgical and + experimental procedures for mice were in accordance with the National Institutes of + Health Guide for the Care and Use of Laboratory Animals and approved by the Harvard + Institutional Animal Care and Use Committee. 161 frames were labeled, making this a + real-world sized laboratory dataset. + + Introduced in Lauer et al. "Multi-animal pose estimation, identification and + tracking with DeepLabCut." Nature Methods 19, no. 4 (2022): 496-504. """ name = "trimouse" @@ -52,9 +62,27 @@ class TriMouseBenchmark(deeplabcut.benchmark.base.Benchmark): class ParentingMouseBenchmark(deeplabcut.benchmark.base.Benchmark): """Datasets with three mice, one parenting, two pups. - Parenting behavior is a pup directed behavior observed in adult mice involving complex motor actions directed towards the benefit of the offspring. These experiments were carried out in the laboratory of Catherine Dulac at Harvard University. The behavioral assay was performed in the homecage of singly housed adult female mice in dark/red light conditions. For these videos, the adult mice was monitored for several minutes in the cage followed by the introduction of pup (4 days old) in one corner of the cage. The behavior of the adult and pup was monitored for a duration of 15 minutes. Video was recorded at 30Hz using a Microsoft LifeCam camera (Part#: 6CH-00001) with a resolution of 1280 x 720 pixels or a Geovision camera (model no.: GV-BX4700-3V) also acquired at 30 frames per second at a resolution of 704 x 480 pixels. A human annotator labeled on the adult animal the same 12 body points as in the tri-mouse dataset, and five body points on the pup along its spine. Initially only the two ends were labeled, and intermediate points were added by interpolation and their positions was manually adjusted if necessary. All surgical and experimental procedures for mice were in accordance with the National Institutes of Health Guide for the Care and Use of Laboratory Animals and approved by the Harvard Institutional Animal Care and Use Committee. 542 frames were labeled, making this a real-world sized laboratory dataset. - - Introduced in Lauer et al. "Multi-animal pose estimation, identification and tracking with DeepLabCut." Nature Methods 19, no. 4 (2022): 496-504. + Parenting behavior is a pup directed behavior observed in adult mice involving + complex motor actions directed towards the benefit of the offspring. These + experiments were carried out in the laboratory of Catherine Dulac at Harvard + University. The behavioral assay was performed in the homecage of singly housed + adult female mice in dark/red light conditions. For these videos, the adult mice was + monitored for several minutes in the cage followed by the introduction of pup (4 + days old) in one corner of the cage. The behavior of the adult and pup was monitored + for a duration of 15 minutes. Video was recorded at 30Hz using a Microsoft LifeCam + camera (Part#: 6CH-00001) with a resolution of 1280 x 720 pixels or a Geovision + camera (model no.: GV-BX4700-3V) also acquired at 30 frames per second at a + resolution of 704 x 480 pixels. A human annotator labeled on the adult animal the + same 12 body points as in the tri-mouse dataset, and five body points on the pup + along its spine. Initially only the two ends were labeled, and intermediate points + were added by interpolation and their positions was manually adjusted if necessary. + All surgical and experimental procedures for mice were in accordance with the + National Institutes of Health Guide for the Care and Use of Laboratory Animals and + approved by the Harvard Institutional Animal Care and Use Committee. 542 frames were + labeled, making this a real-world sized laboratory dataset. + + Introduced in Lauer et al. "Multi-animal pose estimation, identification and + tracking with DeepLabCut." Nature Methods 19, no. 4 (2022): 496-504. """ name = "parenting" @@ -93,7 +121,7 @@ def compute_pose_map(self, results_objects): ) def _validate_predictions(self, name: str, predictions: dict) -> dict: - """Fixes filenames for predictions made on old versions of the dataset""" + """Fixes filenames for predictions made on old versions of the dataset.""" return super()._validate_predictions( name, {k.replace("Dummy", "D").replace("Dead pup", "DP"): v for k, v in predictions.items()}, @@ -103,9 +131,21 @@ def _validate_predictions(self, name: str, predictions: dict) -> dict: class MarmosetBenchmark(deeplabcut.benchmark.base.Benchmark): """Dataset with two marmosets. - All animal procedures are overseen by veterinary staff of the MIT and Broad Institute Department of Comparative Medicine, in compliance with the NIH guide for the care and use of laboratory animals and approved by the MIT and Broad Institute animal care and use committees. Video of common marmosets (Callithrix jacchus) was collected in the laboratory of Guoping Feng at MIT. Marmosets were recorded using Kinect V2 cameras (Microsoft) with a resolution of 1080p and frame rate of 30 Hz. After acquisition, images to be used for training the network were manually cropped to 1000 x 1000 pixels or smaller. The dataset is 7,600 labeled frames from 40 different marmosets collected from 3 different colonies (in different facilities). Each cage contains a pair of marmosets, where one marmoset had light blue dye applied to its tufts. One human annotator labeled the 15 marker points on each animal present in the frame (frames contained either 1 or 2 animals). - - Introduced in Lauer et al. "Multi-animal pose estimation, identification and tracking with DeepLabCut." Nature Methods 19, no. 4 (2022): 496-504. + All animal procedures are overseen by veterinary staff of the MIT and Broad + Institute Department of Comparative Medicine, in compliance with the NIH guide for + the care and use of laboratory animals and approved by the MIT and Broad Institute + animal care and use committees. Video of common marmosets (Callithrix jacchus) was + collected in the laboratory of Guoping Feng at MIT. Marmosets were recorded using + Kinect V2 cameras (Microsoft) with a resolution of 1080p and frame rate of 30 Hz. + After acquisition, images to be used for training the network were manually cropped + to 1000 x 1000 pixels or smaller. The dataset is 7,600 labeled frames from 40 + different marmosets collected from 3 different colonies (in different facilities). + Each cage contains a pair of marmosets, where one marmoset had light blue dye + applied to its tufts. One human annotator labeled the 15 marker points on each + animal present in the frame (frames contained either 1 or 2 animals). + + Introduced in Lauer et al. "Multi-animal pose estimation, identification and + tracking with DeepLabCut." Nature Methods 19, no. 4 (2022): 496-504. """ name = "marmosets" @@ -132,11 +172,23 @@ class MarmosetBenchmark(deeplabcut.benchmark.base.Benchmark): class FishBenchmark(deeplabcut.benchmark.base.Benchmark): - """Dataset with multiple fish, filmed from top-view - - Schools of inland silversides (Menidia beryllina, n=14 individuals per school) were recorded in the Lauder Lab at Harvard University while swimming at 15 speeds (0.5 to 8 BL/s, body length, at 0.5 BL/s intervals) in a flow tank with a total working section of 28 x 28 x 40 cm as described in previous work, at a constant temperature (18±1°C) and salinity (33 ppt), at a Reynolds number of approximately 10,000 (based on BL). Dorsal views of steady swimming across these speeds were recorded by high-speed video cameras (FASTCAM Mini AX50, Photron USA, San Diego, CA, USA) at 60-125 frames per second (feeding videos at 60 fps, swimming alone 125 fps). The dorsal view was recorded above the swim tunnel and a floating Plexiglas panel at the water surface prevented surface ripples from interfering with dorsal view videos. Five keypoints were labeled (tip, gill, peduncle, dorsal fin tip, caudal tip). 100 frames were labeled, making this a real-world sized laboratory dataset. - - Introduced in Lauer et al. "Multi-animal pose estimation, identification and tracking with DeepLabCut." Nature Methods 19, no. 4 (2022): 496-504. + """Dataset with multiple fish, filmed from top-view. + + Schools of inland silversides (Menidia beryllina, n=14 individuals per school) were + recorded in the Lauder Lab at Harvard University while swimming at 15 speeds (0.5 to + 8 BL/s, body length, at 0.5 BL/s intervals) in a flow tank with a total working + section of 28 x 28 x 40 cm as described in previous work, at a constant temperature + (18±1°C) and salinity (33 ppt), at a Reynolds number of approximately 10,000 (based + on BL). Dorsal views of steady swimming across these speeds were recorded by high- + speed video cameras (FASTCAM Mini AX50, Photron USA, San Diego, CA, USA) at 60-125 + frames per second (feeding videos at 60 fps, swimming alone 125 fps). The dorsal + view was recorded above the swim tunnel and a floating Plexiglas panel at the water + surface prevented surface ripples from interfering with dorsal view videos. Five + keypoints were labeled (tip, gill, peduncle, dorsal fin tip, caudal tip). 100 frames + were labeled, making this a real-world sized laboratory dataset. + + Introduced in Lauer et al. "Multi-animal pose estimation, identification and + tracking with DeepLabCut." Nature Methods 19, no. 4 (2022): 496-504. """ name = "fish" diff --git a/deeplabcut/benchmark/mot.py b/deeplabcut/benchmark/mot.py index ca37bd169f..4c0f8e6e29 100644 --- a/deeplabcut/benchmark/mot.py +++ b/deeplabcut/benchmark/mot.py @@ -22,9 +22,8 @@ def convert_bboxes_to_xywh(bboxes: NDArray, inplace: bool = False) -> NDArray: - """ - Converts bounding box coordinates from [x_min, y_min, x_max, y_max] format - to [x, y, width, height] format. + """Converts bounding box coordinates from [x_min, y_min, x_max, y_max] format to [x, + y, width, height] format. Parameters ---------- @@ -58,8 +57,7 @@ def convert_bboxes_to_xywh(bboxes: NDArray, inplace: bool = False) -> NDArray: def reconstruct_bboxes_from_bodyparts(data: pd.DataFrame, margin: float, to_xywh: bool = False) -> NDArray: - """ - Reconstructs bounding boxes from body part coordinates and likelihoods. + """Reconstructs bounding boxes from body part coordinates and likelihoods. Parameters ---------- @@ -103,8 +101,7 @@ def reconstruct_bboxes_from_bodyparts(data: pd.DataFrame, margin: float, to_xywh def reconstruct_all_bboxes(data: pd.DataFrame, margin: float, to_xywh: bool = False) -> NDArray: - """ - Reconstructs bounding boxes for multiple individuals from body part data. + """Reconstructs bounding boxes for multiple individuals from body part data. Parameters ---------- diff --git a/deeplabcut/core/conversion_table.py b/deeplabcut/core/conversion_table.py index faa01d82ba..d40ac940c4 100644 --- a/deeplabcut/core/conversion_table.py +++ b/deeplabcut/core/conversion_table.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Defines conversion tables mapping DeepLabCut project bodyparts to SA bodyparts""" +"""Defines conversion tables mapping DeepLabCut project bodyparts to SA bodyparts.""" from __future__ import annotations @@ -19,7 +19,7 @@ @dataclass class ConversionTable: - """Maps DLC project bodyparts to the corresponding SuperAnimal bodyparts + """Maps DLC project bodyparts to the corresponding SuperAnimal bodyparts. The conversion table must satisfy the following conditions (checked by validate): - All SuperAnimal bodyparts must be valid (defined for the SuperAnimal model) @@ -32,7 +32,7 @@ class ConversionTable: table: dict[str, str] def __post_init__(self): - """Validates the table""" + """Validates the table.""" self.validate() def to_array(self) -> np.ndarray: diff --git a/deeplabcut/core/engine.py b/deeplabcut/core/engine.py index dadad5871e..1f7a51d60b 100644 --- a/deeplabcut/core/engine.py +++ b/deeplabcut/core/engine.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Defines the deep learning frameworks available""" +"""Defines the deep learning frameworks available.""" from __future__ import annotations diff --git a/deeplabcut/core/metrics/api.py b/deeplabcut/core/metrics/api.py index 348d8c7abf..a00fc617b9 100644 --- a/deeplabcut/core/metrics/api.py +++ b/deeplabcut/core/metrics/api.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""API methods to get metrics for deep learning models""" +"""API methods to get metrics for deep learning models.""" from __future__ import annotations @@ -29,7 +29,7 @@ def compute_metrics( per_keypoint_rmse: bool = False, compute_detection_rmse: bool = True, ) -> dict: - """Computes pose estimation performance metrics + """Computes pose estimation performance metrics. Given ground truth pose labels and predictions on a dataset, computes RMSE and pose mAP/mAR using OKS. diff --git a/deeplabcut/create_project/demo_data.py b/deeplabcut/create_project/demo_data.py index 0ab9b70bba..30b58390f0 100644 --- a/deeplabcut/create_project/demo_data.py +++ b/deeplabcut/create_project/demo_data.py @@ -22,9 +22,8 @@ def load_demo_data( createtrainingset: bool = True, engine: Engine = Engine.PYTORCH, ): - """ - Loads the demo data -- subset from trail-tracking data in Mathis et al. 2018. - When loading, it sets paths correctly to run this project on your system + """Loads the demo data -- subset from trail-tracking data in Mathis et al. 2018. + When loading, it sets paths correctly to run this project on your system. Parameter ---------- @@ -52,8 +51,8 @@ def load_demo_data( def transform_data(config): - """ - This function adds the full path to labeling dataset. + """This function adds the full path to labeling dataset. + It also adds the correct path to the video file in the config file. """ diff --git a/deeplabcut/gui/displays/__init__.py b/deeplabcut/gui/displays/__init__.py index f511e6184a..117d127147 100644 --- a/deeplabcut/gui/displays/__init__.py +++ b/deeplabcut/gui/displays/__init__.py @@ -8,5 +8,3 @@ # # Licensed under GNU Lesser General Public License v3.0 # - - diff --git a/deeplabcut/gui/style.qss b/deeplabcut/gui/style.qss index 19164d5d57..feaf66e3d4 100644 --- a/deeplabcut/gui/style.qss +++ b/deeplabcut/gui/style.qss @@ -1,8 +1,8 @@ - /* + /* Variables used -------------- - widgets height: 25px + widgets height: 25px */ @@ -28,4 +28,4 @@ QComboBox{ QLineEdit{ height: 25px; -} \ No newline at end of file +} diff --git a/deeplabcut/modelzoo/conversion_tables/conversion_table_quadruped.csv b/deeplabcut/modelzoo/conversion_tables/conversion_table_quadruped.csv index 5060943b21..e23d1e280f 100644 --- a/deeplabcut/modelzoo/conversion_tables/conversion_table_quadruped.csv +++ b/deeplabcut/modelzoo/conversion_tables/conversion_table_quadruped.csv @@ -37,4 +37,4 @@ right_knee,right_back_knee,R_B_Knee,r_back_knee,,,back_right_knee right_back_paw,right_back_paw,R_B_Paw,r_back_paw,Offhindfoot,right_back_paw,back_right_paw ,,,,,,belly_bottom ,,,,,,body_middle_right -,,,,,,body_middle_left \ No newline at end of file +,,,,,,body_middle_left diff --git a/deeplabcut/modelzoo/fmpose_3d/README.md b/deeplabcut/modelzoo/fmpose_3d/README.md index 9a6ce4053a..f72d35f9af 100644 --- a/deeplabcut/modelzoo/fmpose_3d/README.md +++ b/deeplabcut/modelzoo/fmpose_3d/README.md @@ -9,4 +9,4 @@ Model weights are hosted on HuggingFace Hub and are downloaded automatically when no local path is provided. The library is installable via `pip install fmpose3d` and requires Python >= 3.8. -For a full overview and documentation on the API, see https://github.com/AdaptiveMotorControlLab/FMPose3D. +For a full overview and documentation on the API, see https://github.com/AdaptiveMotorControlLab/FMPose3D. diff --git a/deeplabcut/modelzoo/fmpose_3d/__init__.py b/deeplabcut/modelzoo/fmpose_3d/__init__.py index fc1ada573b..5034fcf0f2 100644 --- a/deeplabcut/modelzoo/fmpose_3d/__init__.py +++ b/deeplabcut/modelzoo/fmpose_3d/__init__.py @@ -1,8 +1,7 @@ -""" -DeepLabCut2.0-3.0 Toolbox (deeplabcut.org) -© A. & M. Mathis Labs -https://github.com/DeepLabCut/DeepLabCut -Please see AUTHORS for contributors. +"""DeepLabCut2.0-3.0 Toolbox (deeplabcut.org) © A. + +& M. Mathis Labs https://github.com/DeepLabCut/DeepLabCut Please see AUTHORS for +contributors. https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS Licensed under GNU Lesser General Public License v3.0 """ diff --git a/deeplabcut/modelzoo/model_configs/fasterrcnn_resnet50_fpn_v2.yaml b/deeplabcut/modelzoo/model_configs/fasterrcnn_resnet50_fpn_v2.yaml index 27d147e339..a78d93eb48 100644 --- a/deeplabcut/modelzoo/model_configs/fasterrcnn_resnet50_fpn_v2.yaml +++ b/deeplabcut/modelzoo/model_configs/fasterrcnn_resnet50_fpn_v2.yaml @@ -48,4 +48,4 @@ train_settings: dataloader_workers: 0 dataloader_pin_memory: false display_iters: 500 - epochs: 250 \ No newline at end of file + epochs: 250 diff --git a/deeplabcut/modelzoo/model_configs/ssdlite.yaml b/deeplabcut/modelzoo/model_configs/ssdlite.yaml index 04e694fa0a..307bf92ea4 100644 --- a/deeplabcut/modelzoo/model_configs/ssdlite.yaml +++ b/deeplabcut/modelzoo/model_configs/ssdlite.yaml @@ -47,4 +47,4 @@ train_settings: dataloader_workers: 0 dataloader_pin_memory: false display_iters: 500 - epochs: 250 \ No newline at end of file + epochs: 250 diff --git a/deeplabcut/modelzoo/project_configs/superanimal_humanbody.yaml b/deeplabcut/modelzoo/project_configs/superanimal_humanbody.yaml index d1e665c17f..e4d891b2ba 100644 --- a/deeplabcut/modelzoo/project_configs/superanimal_humanbody.yaml +++ b/deeplabcut/modelzoo/project_configs/superanimal_humanbody.yaml @@ -87,4 +87,4 @@ corner2move2: move2corner: # Conversion tables to fine-tune SuperAnimal weights -SuperAnimalConversionTables: \ No newline at end of file +SuperAnimalConversionTables: diff --git a/deeplabcut/modelzoo/weight_initialization.py b/deeplabcut/modelzoo/weight_initialization.py index e5bf7c7fae..da85f5b200 100644 --- a/deeplabcut/modelzoo/weight_initialization.py +++ b/deeplabcut/modelzoo/weight_initialization.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Functions to build weight initialization parameters for SuperAnimal models""" +"""Functions to build weight initialization parameters for SuperAnimal models.""" from pathlib import Path @@ -30,7 +30,7 @@ def build_weight_init( customized_pose_checkpoint: str | Path | None = None, customized_detector_checkpoint: str | Path | None = None, ) -> WeightInitialization: - """Builds the WeightInitialization from a SuperAnimal model for a project + """Builds the WeightInitialization from a SuperAnimal model for a project. Args: cfg: The project's configuration, or the path to the project configuration file. diff --git a/deeplabcut/pose_cfg.yaml b/deeplabcut/pose_cfg.yaml index 62e7681469..2142379da3 100644 --- a/deeplabcut/pose_cfg.yaml +++ b/deeplabcut/pose_cfg.yaml @@ -83,7 +83,7 @@ contrast: claheratio: 0.1 histeq: True histeqratio: 0.1 - + # dictionary with convolution parameters convolution: sharpen: False diff --git a/deeplabcut/pose_estimation_pytorch/README.md b/deeplabcut/pose_estimation_pytorch/README.md index bbd7e38d88..dc1cd27bf3 100644 --- a/deeplabcut/pose_estimation_pytorch/README.md +++ b/deeplabcut/pose_estimation_pytorch/README.md @@ -1,9 +1,9 @@ # PyTorch DeepLabCut API -This overview is primarily written for maintainers and expert users. +This overview is primarily written for maintainers and expert users. Here we detail the logic and structure for the DLC3.* PyTorch code. Furthermore, we -provide many practical examples to illustrate the usage of the code for developers. +provide many practical examples to illustrate the usage of the code for developers. ## Structure of the PyTorch DLC code @@ -27,12 +27,12 @@ Thus, they are also ideally suited for developers. ### Models We provide state-of-the-art pose estimation models such as DLCRNet, HRNet, DEKR, BUCTD -and more are coming! Object detection models are also available (and implemented in +and more are coming! Object detection models are also available (and implemented in `deeplabcut.pose_estimations_pytorch.models.detectors`). The `deeplabcut.pose_estimations_pytorch.models` package contains all components related -to building a model. Models are flexibly build from modular components: `backbone`, -`neck` (optional) and `head` (as discussed below). +to building a model. Models are flexibly build from modular components: `backbone`, +`neck` (optional) and `head` (as discussed below). You can check available models by running: @@ -50,15 +50,15 @@ print(deeplabcut.pose_estimation_pytorch.available_detectors()) Model architectures are built according to a configuration specified in a `yaml` file. This file (named `pytorch_cfg.yaml`) describes the architecture of the model you want to -train (but also hyperparameters, optimizer, ...). All code to manipulate PyTorch +train (but also hyperparameters, optimizer, ...). All code to manipulate PyTorch configuration files is in `deeplabcut.pose_estimations_pytorch.config`. -To generate a model configuration, you can call `make_pytorch_pose_config`. Note that -this does not save the configuration to a given filepath - it just returns it as a -dictionary. However, you can save it with `write_config`. +To generate a model configuration, you can call `make_pytorch_pose_config`. Note that +this does not save the configuration to a given filepath - it just returns it as a +dictionary. However, you can save it with `write_config`. -During a typical DeepLabCut project management workflow, these methods don't need to be -called, as `create_training_dataset` will create this configuration file and save it to +During a typical DeepLabCut project management workflow, these methods don't need to be +called, as `create_training_dataset` will create this configuration file and save it to disk. ```python @@ -97,7 +97,7 @@ adding models. ### Model Registry Registries are created for all model building blocks to make it easy to add new models. -All you need to do is add the decorator `REGISTRY.register_module` to be able to load +All you need to do is add the decorator `REGISTRY.register_module` to be able to load your model from a configuration file. Available registries are `BACKBONES`, `NECKS`, `HEADS`, `PREDICTORS` and `TARGET_GENERATORS`. Each building block has a base class that should be inherited by the class added to the model registry (`BaseBackbone`, @@ -116,11 +116,11 @@ from deeplabcut.pose_estimation_pytorch.models.backbones import BACKBONES, BaseB @BACKBONES.register_module class DummyBackbone(BaseBackbone): """A dummy backbone, simply max-pooling the input""" - + def __init__(self, kernel_size: int = 2): super().__init__(stride=kernel_size) self.kernel_size = kernel_size - + def forward(self, x: torch.Tensor) -> torch.Tensor: return F.max_pool2d(x, kernel_size=self.kernel_size) @@ -134,15 +134,15 @@ a head which takes as input the output of a backbone (which has shape `(num_chan H', W')`) and put it through a kernel-size 1 convolution, simply changing the number of channels. -Heads can output multiple tensors (such as heatmaps and location refinement fields). +Heads can output multiple tensors (such as heatmaps and location refinement fields). Therefore, their `forward(...)` method outputs a dictionary mapping strings to tensors. Here, we return the `heatmap` and `locref` tensors. A head must contain different: a `target_generator` to generate targets for its outputs and a `predictor` to convert model outputs to pose. Make sure that the keys output by the `target_generator` and the `head` match! Some `criterion` also needs to be -defined to compute the loss between the outputs and targets. When more than one output -is specified (such as in this case, where we're generating heatmaps and location +defined to compute the loss between the outputs and targets. When more than one output +is specified (such as in this case, where we're generating heatmaps and location refinement fields), a loss aggregator must also be given to combine all losses into one (this should simply be a `WeightedLossAggregator`, indicating the weight for each loss). @@ -171,7 +171,7 @@ from deeplabcut.pose_estimation_pytorch.models.target_generators import ( @HEADS.register_module class DummyHead(BaseHead): """A dummy backbone, simply max-pooling the input""" - + def __init__( self, num_input_channels: int, @@ -256,9 +256,9 @@ print(loader.df_train) print(loader.df_test) ``` -The `PoseDataset` class is an instance of -[torch.utils.Dataset](https://pytorch.org/docs/stable/data.html), which converts raw -images and keypoints to a tensor dataset for training and evaluation. You can generate +The `PoseDataset` class is an instance of +[torch.utils.Dataset](https://pytorch.org/docs/stable/data.html), which converts raw +images and keypoints to a tensor dataset for training and evaluation. You can generate an instance of training/test dataset with your `DLCLoader`: ```python3 @@ -281,9 +281,9 @@ valid_dataset = loader.create_dataset( ) ``` -A `COCOLoader` is also available, and allows you train models in DeepLabCut on +A `COCOLoader` is also available, and allows you train models in DeepLabCut on [COCO-format](https://medium.com/@manuktiwary/coco-format-what-and-how-5c7d22cf5301) -datasets. This essentially consists of having a folder containing your dataset in the +datasets. This essentially consists of having a folder containing your dataset in the format: ``` @@ -291,24 +291,24 @@ COCOProject └───annotations │ │ train.json │ │ test.json -│ +│ └───images │ img0000.png │ img0001.png │ ... ``` -In your `train.json` and `test.json` files, you can either specify your image -`"file_name"` with a relative path or with an absolute path. If a relative path is -used (e.g. `img0000.png` or `subfolder/img0000.png`), it will be resolved to the -`images` folder in your project (i.e. `/path/to/COCOProject/images/img0000.png` or +In your `train.json` and `test.json` files, you can either specify your image +`"file_name"` with a relative path or with an absolute path. If a relative path is +used (e.g. `img0000.png` or `subfolder/img0000.png`), it will be resolved to the +`images` folder in your project (i.e. `/path/to/COCOProject/images/img0000.png` or `/path/to/COCOProject/images/subfolder/img0000.png`). -If you specify an absolute path, the path to the image will not be resolved, and the +If you specify an absolute path, the path to the image will not be resolved, and the image will be loaded from the specified path. This allows you to keep data on different disks, or reuse the same images in different projects without having to duplicate them. -To train a DeepLabCut model on a COCO-format dataset, you'll need to specify a model +To train a DeepLabCut model on a COCO-format dataset, you'll need to specify a model configuration file (as described in [#model_configuration_files]). ```python3 @@ -363,7 +363,7 @@ valid_dataset = loader.create_dataset( ### Runners -The `deeplabcut.pose_estimations_pytorch.runners` contains code to get models, load +The `deeplabcut.pose_estimations_pytorch.runners` contains code to get models, load pretrained weights, and either train them or run inference with them. ## Code Examples @@ -460,8 +460,8 @@ predictions = dlc_torch.video_inference( When `deeplabcut.pose_estimation_pytorch.apis.videos.video_inference` is called with a top-down model, it is assumed that a detector snapshot is given as well to obtain -bounding boxes with which to run pose estimation. It's possible that you've already -obtained bounding boxes for your video (with another object detector or through some +bounding boxes with which to run pose estimation. It's possible that you've already +obtained bounding boxes for your video (with another object detector or through some other means), and you want to reuse those bounding boxes instead of running an object detector again. diff --git a/deeplabcut/pose_estimation_pytorch/apis/export.py b/deeplabcut/pose_estimation_pytorch/apis/export.py index 44d78ea5f2..3a061fc15f 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/export.py +++ b/deeplabcut/pose_estimation_pytorch/apis/export.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Code to export DeepLabCut models for DLCLive inference""" +"""Code to export DeepLabCut models for DLCLive inference.""" import copy from pathlib import Path @@ -175,8 +175,7 @@ def get_export_filename( def wipe_paths_from_model_config(model_cfg: dict) -> None: - """ - Removes all paths from the contents of the ``pytorch_config`` file. + """Removes all paths from the contents of the ``pytorch_config`` file. Args: model_cfg: The model configuration to wipe. diff --git a/deeplabcut/pose_estimation_pytorch/apis/tracking_dataset.py b/deeplabcut/pose_estimation_pytorch/apis/tracking_dataset.py index 3d4af32cc2..3455105f26 100644 --- a/deeplabcut/pose_estimation_pytorch/apis/tracking_dataset.py +++ b/deeplabcut/pose_estimation_pytorch/apis/tracking_dataset.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Code to create tracking datasets for ReID model training""" +"""Code to create tracking datasets for ReID model training.""" from pathlib import Path @@ -32,7 +32,7 @@ def build_feature_extraction_runner( device: str, batch_size: int = 1, ) -> runners.PoseInferenceRunner: - """Builds a runner to extract backbone features for poses of individuals + """Builds a runner to extract backbone features for poses of individuals. Args: loader: The loader for the model to use. diff --git a/deeplabcut/pose_estimation_pytorch/config/backbones/resnet_50.yaml b/deeplabcut/pose_estimation_pytorch/config/backbones/resnet_50.yaml index f4e8308793..21298e6cbd 100644 --- a/deeplabcut/pose_estimation_pytorch/config/backbones/resnet_50.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/backbones/resnet_50.yaml @@ -15,4 +15,4 @@ runner: type: LRListScheduler params: lr_list: [ [ 1e-4 ], [ 1e-5 ] ] - milestones: [ 90, 120 ] \ No newline at end of file + milestones: [ 90, 120 ] diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml index cc77370773..e19e774bec 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w32.yaml @@ -68,4 +68,4 @@ model: strides: [] final_conv: out_channels: "num_bodyparts x 2" - kernel_size: 1 \ No newline at end of file + kernel_size: 1 diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml index 3103b33359..b3b487390a 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48.yaml @@ -68,4 +68,4 @@ model: strides: [] final_conv: out_channels: "num_bodyparts x 2" - kernel_size: 1 \ No newline at end of file + kernel_size: 1 diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48_human.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48_human.yaml index b6266522ca..f43438a3e7 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48_human.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_coam_w48_human.yaml @@ -69,4 +69,4 @@ model: strides: [] final_conv: out_channels: "num_bodyparts x 2" - kernel_size: 1 \ No newline at end of file + kernel_size: 1 diff --git a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml index 3c755aea35..36cbaa4305 100644 --- a/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/ctd/ctd_prenet_hrnet_w48.yaml @@ -68,4 +68,4 @@ model: strides: [] final_conv: out_channels: "num_bodyparts x 2" - kernel_size: 1 \ No newline at end of file + kernel_size: 1 diff --git a/deeplabcut/pose_estimation_pytorch/config/dekr/dekr_w18.yaml b/deeplabcut/pose_estimation_pytorch/config/dekr/dekr_w18.yaml index 1960fe28b4..f116963677 100644 --- a/deeplabcut/pose_estimation_pytorch/config/dekr/dekr_w18.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/dekr/dekr_w18.yaml @@ -65,4 +65,4 @@ runner: params: lr_list: [ [ 1e-4 ], [ 1e-5 ] ] milestones: [ 90, 120 ] -with_center_keypoints: true \ No newline at end of file +with_center_keypoints: true diff --git a/deeplabcut/pose_estimation_pytorch/config/dekr/dekr_w32.yaml b/deeplabcut/pose_estimation_pytorch/config/dekr/dekr_w32.yaml index d467ffdc83..675347ac5b 100644 --- a/deeplabcut/pose_estimation_pytorch/config/dekr/dekr_w32.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/dekr/dekr_w32.yaml @@ -65,4 +65,4 @@ runner: params: lr_list: [ [ 1e-4 ], [ 1e-5 ] ] milestones: [ 90, 120 ] -with_center_keypoints: true \ No newline at end of file +with_center_keypoints: true diff --git a/deeplabcut/pose_estimation_pytorch/config/dekr/dekr_w48.yaml b/deeplabcut/pose_estimation_pytorch/config/dekr/dekr_w48.yaml index ffa3861b0b..789aee9f82 100644 --- a/deeplabcut/pose_estimation_pytorch/config/dekr/dekr_w48.yaml +++ b/deeplabcut/pose_estimation_pytorch/config/dekr/dekr_w48.yaml @@ -65,4 +65,4 @@ runner: params: lr_list: [ [ 1e-4 ], [ 1e-5 ] ] milestones: [ 90, 120 ] -with_center_keypoints: true \ No newline at end of file +with_center_keypoints: true diff --git a/deeplabcut/pose_estimation_pytorch/data/dataset.py b/deeplabcut/pose_estimation_pytorch/data/dataset.py index ffa67ff20c..9ca893a6cc 100644 --- a/deeplabcut/pose_estimation_pytorch/data/dataset.py +++ b/deeplabcut/pose_estimation_pytorch/data/dataset.py @@ -38,7 +38,7 @@ @dataclass(frozen=True) class PoseDatasetParameters: - """Parameters for a pose dataset + """Parameters for a pose dataset. Attributes: bodyparts: the names of bodyparts in the dataset @@ -76,7 +76,7 @@ def max_num_animals(self) -> int: @dataclass class PoseDataset(Dataset): - """A pose dataset""" + """A pose dataset.""" images: list[dict] annotations: list[dict] @@ -118,8 +118,7 @@ def __len__(self): return len(self.annotations) def _get_raw_item(self, index: int) -> tuple[str, list[dict], int]: - """ - Retrieve the image path and annotations for the specified index. + """Retrieve the image path and annotations for the specified index. Args: index (int): The index of the item to retrieve. @@ -142,9 +141,7 @@ def _get_raw_item_crop(self, index: int) -> tuple[str, list[dict], int]: return img["file_name"], [ann], img["id"] def _get_raw_item_crop_context(self, index: int) -> tuple[str, list[dict], int]: - """ - Includes keypoints from other individuals in the image ("context"). - """ + """Includes keypoints from other individuals in the image ("context").""" ann = self.annotations[index] img = self.images[self.img_id_to_index[ann["image_id"]]] near_anns = [] @@ -157,8 +154,7 @@ def _get_raw_item_crop_context(self, index: int) -> tuple[str, list[dict], int]: return img["file_name"], [ann] + near_anns, img["id"] def __getitem__(self, index: int) -> dict: - """ - Gets the item at the specified index from the dataset. + """Gets the item at the specified index from the dataset. Args: index: ordered number of the items in the dataset @@ -360,8 +356,7 @@ def _prepare_final_annotation_dict( } def _get_data_based_on_task(self, index: int) -> tuple[str, list[dict], int]: - """ - Retrieve data based on the specified task. + """Retrieve data based on the specified task. For the 'TD' (top-down pose estimation) task: - Provides a cropped image and its annotations. @@ -393,7 +388,7 @@ def apply_transform_all_keypoints( keypoints_unique: np.ndarray, bboxes: np.ndarray, ) -> dict[str, np.ndarray]: - """Transforms the image using this class's transform + """Transforms the image using this class's transform. Args: image: the image to transform @@ -444,8 +439,8 @@ def crop( coords: tuple[tuple[int, int], tuple[int, int]], output_size: tuple[int, int], ) -> tuple[np.ndarray, np.ndarray, tuple[int, int], tuple[int, int]]: - """ - Crop the image based on a given bounding box and resize it to the desired output size. + """Crop the image based on a given bounding box and resize it to the desired + output size. Args: image: the image to transform @@ -485,7 +480,7 @@ def extract_keypoints_and_bboxes( @staticmethod def add_center_keypoints(keypoints: np.ndarray) -> np.ndarray: - """Adds a keypoint in the mean of each individual + """Adds a keypoint in the mean of each individual. Args: keypoints: shape (num_idv, num_kpts, 3) diff --git a/deeplabcut/pose_estimation_pytorch/data/helper.py b/deeplabcut/pose_estimation_pytorch/data/helper.py index de1d632b5e..ce73a141ad 100644 --- a/deeplabcut/pose_estimation_pytorch/data/helper.py +++ b/deeplabcut/pose_estimation_pytorch/data/helper.py @@ -21,8 +21,7 @@ def _getter(cfg): def class_property(func, arg_func): - """ - Decorator to create a class property. + """Decorator to create a class property. Parameters: - func: Callable that represents the logic of the property. @@ -42,8 +41,8 @@ def wrapper(self): class PropertyMeta(type): - """ - Metaclass for creating class properties in a more organized and systematic manner. + """Metaclass for creating class properties in a more organized and systematic + manner. This metaclass allows a class to define its properties using a simple dictionary structure (`properties`). The dictionary keys represent the property names, @@ -77,8 +76,8 @@ def __new__(cls, name, bases, attrs): class CombinedPropertyMeta(ABCMeta, PropertyMeta): - """ - Combined metaclass that integrates the functionalities of both `ABCMeta` and `BasePropertyMeta`. + """Combined metaclass that integrates the functionalities of both `ABCMeta` and + `BasePropertyMeta`. This metaclass is useful in scenarios where a class needs to use both abstract methods (from `ABCMeta`) and the property definition utilities provided by `BasePropertyMeta`. diff --git a/deeplabcut/pose_estimation_pytorch/data/image.py b/deeplabcut/pose_estimation_pytorch/data/image.py index 58070d3758..09b20dbd7c 100644 --- a/deeplabcut/pose_estimation_pytorch/data/image.py +++ b/deeplabcut/pose_estimation_pytorch/data/image.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Classes and functions to manipulate images""" +"""Classes and functions to manipulate images.""" from __future__ import annotations @@ -24,7 +24,7 @@ def load_image(filepath: str | Path, color_mode: str = "RGB") -> np.ndarray: - """Loads an image from a file using cv2 + """Loads an image from a file using cv2. Args: filepath: the path of the file containing the image to load @@ -49,7 +49,7 @@ def resize_and_random_crop( max_size: int | None = None, max_shift: int | None = None, ) -> tuple[torch.tensor, dict]: - """Resizes images while preserving their aspect ratio + """Resizes images while preserving their aspect ratio. If size is an integer: resizes to square images. First, resizes the image so that it's short side is equal to `size`. If this @@ -214,9 +214,8 @@ def top_down_crop( center_padding: bool = False, crop_with_context: bool = True, ) -> tuple[np.array, tuple[int, int], tuple[float, float]]: - """ - Crops images around bounding boxes for top-down pose estimation. Computes offsets so - that coordinates in the original image can be mapped to the cropped one; + """Crops images around bounding boxes for top-down pose estimation. Computes offsets + so that coordinates in the original image can be mapped to the cropped one; x_cropped = (x - offset_x) / scale_x x_cropped = (y - offset_y) / scale_y diff --git a/deeplabcut/pose_estimation_pytorch/models/backbones/cspnext.py b/deeplabcut/pose_estimation_pytorch/models/backbones/cspnext.py index 3450e1195b..44af1960af 100644 --- a/deeplabcut/pose_estimation_pytorch/models/backbones/cspnext.py +++ b/deeplabcut/pose_estimation_pytorch/models/backbones/cspnext.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Implementation of the CSPNeXt Backbone +"""Implementation of the CSPNeXt Backbone. Based on the ``mmdetection`` CSPNeXt implementation. For more information, see: @@ -36,7 +36,7 @@ @dataclass(frozen=True) class CSPNeXtLayerConfig: - """Configuration for a CSPNeXt layer""" + """Configuration for a CSPNeXt layer.""" in_channels: int out_channels: int @@ -47,7 +47,7 @@ class CSPNeXtLayerConfig: @BACKBONES.register_module class CSPNeXt(HuggingFaceWeightsMixin, BaseBackbone): - """CSPNeXt Backbone + """CSPNeXt Backbone. Args: model_name: The model variant to build. If ``pretrained==True``, must be one of diff --git a/deeplabcut/pose_estimation_pytorch/models/detectors/fasterRCNN.py b/deeplabcut/pose_estimation_pytorch/models/detectors/fasterRCNN.py index f71248d4e8..a36d9e8fa9 100644 --- a/deeplabcut/pose_estimation_pytorch/models/detectors/fasterRCNN.py +++ b/deeplabcut/pose_estimation_pytorch/models/detectors/fasterRCNN.py @@ -20,7 +20,7 @@ @DETECTORS.register_module class FasterRCNN(TorchvisionDetectorAdaptor): - """A FasterRCNN detector + """A FasterRCNN detector. Faster R-CNN: Towards Real-Time Object Detection with Region Proposal Networks Ren, Shaoqing, Kaiming He, Ross Girshick, and Jian Sun. "Faster r-cnn: Towards diff --git a/deeplabcut/pose_estimation_pytorch/models/detectors/filtered_detector.py b/deeplabcut/pose_estimation_pytorch/models/detectors/filtered_detector.py index 7ea4da46a5..6d74c496d9 100644 --- a/deeplabcut/pose_estimation_pytorch/models/detectors/filtered_detector.py +++ b/deeplabcut/pose_estimation_pytorch/models/detectors/filtered_detector.py @@ -4,8 +4,7 @@ class FilteredDetector(nn.Module): def __init__(self, base_model: nn.Module, class_id: int): - """ - Wrap a torchvision detector to return predictions only for a single class. + """Wrap a torchvision detector to return predictions only for a single class. Args: base_model: A torchvision-style object detector. diff --git a/deeplabcut/pose_estimation_pytorch/models/detectors/ssd.py b/deeplabcut/pose_estimation_pytorch/models/detectors/ssd.py index 3c8a254b71..b4149eb423 100644 --- a/deeplabcut/pose_estimation_pytorch/models/detectors/ssd.py +++ b/deeplabcut/pose_estimation_pytorch/models/detectors/ssd.py @@ -20,7 +20,7 @@ @DETECTORS.register_module class SSDLite(TorchvisionDetectorAdaptor): - """An SSD object detection model""" + """An SSD object detection model.""" def __init__( self, diff --git a/deeplabcut/pose_estimation_pytorch/models/detectors/torchvision.py b/deeplabcut/pose_estimation_pytorch/models/detectors/torchvision.py index 37339e055a..5f7cdb12a8 100644 --- a/deeplabcut/pose_estimation_pytorch/models/detectors/torchvision.py +++ b/deeplabcut/pose_estimation_pytorch/models/detectors/torchvision.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Module to adapt torchvision detectors for DeepLabCut""" +"""Module to adapt torchvision detectors for DeepLabCut.""" from __future__ import annotations @@ -21,7 +21,7 @@ class TorchvisionDetectorAdaptor(BaseDetector): - """An adaptor for torchvision detectors + """An adaptor for torchvision detectors. This class is an adaptor for torchvision detectors to DeepLabCut detectors. Some of the models (from fastest to most powerful) available are: @@ -85,8 +85,7 @@ def __init__( def forward( self, x: torch.Tensor, targets: list[dict[str, torch.Tensor]] | None = None ) -> tuple[dict[str, torch.Tensor], list[dict[str, torch.Tensor]]]: - """ - Forward pass of the torchvision detector + """Forward pass of the torchvision detector. Args: x: images to be processed, of shape (b, c, h, w) @@ -99,8 +98,7 @@ def forward( return self.model(x, targets) def get_target(self, labels: dict) -> list[dict[str, torch.Tensor]]: - """ - Returns target in a format a torchvision detector can handle + """Returns target in a format a torchvision detector can handle. Args: labels: dict of annotations, must contain the keys: diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/csp.py b/deeplabcut/pose_estimation_pytorch/models/modules/csp.py index 49548cf039..43d6c65370 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/csp.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/csp.py @@ -37,7 +37,7 @@ def build_norm(norm: str, *args, **kwargs) -> nn.Module: class SPPBottleneck(nn.Module): - """Spatial pyramid pooling layer used in YOLOv3-SPP and (among others) CSPNeXt + """Spatial pyramid pooling layer used in YOLOv3-SPP and (among others) CSPNeXt. Args: in_channels: input channels to the bottleneck @@ -174,7 +174,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: return x def _init_weights(self) -> None: - """Same init as in convolutions""" + """Same init as in convolutions.""" nn.init.kaiming_normal_(self.conv.weight, a=0, nonlinearity="relu") if self.with_bias: nn.init.constant_(self.conv.bias, 0) diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/norm.py b/deeplabcut/pose_estimation_pytorch/models/modules/norm.py index ecaa94541c..e3874846df 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/norm.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/norm.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Normalization layers""" +"""Normalization layers.""" from __future__ import annotations @@ -17,7 +17,7 @@ class ScaleNorm(nn.Module): - """Implementation of ScaleNorm + """Implementation of ScaleNorm. ScaleNorm was introduced in "Transformers without Tears: Improving the Normalization of Self-Attention". diff --git a/deeplabcut/pose_estimation_pytorch/modelzoo/config.py b/deeplabcut/pose_estimation_pytorch/modelzoo/config.py index cda28f87fa..697231f6e3 100644 --- a/deeplabcut/pose_estimation_pytorch/modelzoo/config.py +++ b/deeplabcut/pose_estimation_pytorch/modelzoo/config.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Methods to create the configuration files to fine-tune SuperAnimal models""" +"""Methods to create the configuration files to fine-tune SuperAnimal models.""" from __future__ import annotations @@ -41,8 +41,7 @@ def make_super_animal_finetune_config( detector_name: str | None, save: bool = False, ) -> dict: - """ - Creates a PyTorch pose configuration file to finetune a SuperAnimal model on a + """Creates a PyTorch pose configuration file to finetune a SuperAnimal model on a downstream project. Args: @@ -115,7 +114,7 @@ def create_config_from_modelzoo( project_config: dict, pose_config_path: str | Path, ) -> dict: - """Creates a model configuration file to fine-tune a SuperAnimal model + """Creates a model configuration file to fine-tune a SuperAnimal model. Args: super_animal: The SuperAnimal dataset on which the model was trained. diff --git a/deeplabcut/pose_estimation_pytorch/modelzoo/train_from_coco.py b/deeplabcut/pose_estimation_pytorch/modelzoo/train_from_coco.py index 0e7eb345b0..2c722676c4 100644 --- a/deeplabcut/pose_estimation_pytorch/modelzoo/train_from_coco.py +++ b/deeplabcut/pose_estimation_pytorch/modelzoo/train_from_coco.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""File to train a model on a COCO dataset""" +"""File to train a model on a COCO dataset.""" from __future__ import annotations diff --git a/deeplabcut/pose_estimation_pytorch/post_processing/match_predictions_to_gt.py b/deeplabcut/pose_estimation_pytorch/post_processing/match_predictions_to_gt.py index ee679fe1e0..54f4885f7e 100644 --- a/deeplabcut/pose_estimation_pytorch/post_processing/match_predictions_to_gt.py +++ b/deeplabcut/pose_estimation_pytorch/post_processing/match_predictions_to_gt.py @@ -18,8 +18,7 @@ def rmse_match_prediction_to_gt(pred_kpts: np.ndarray, gt_kpts: np.ndarray) -> np.ndarray: - """ - Hungarian algorithm predicted individuals to ground truth ones, using root mean + """Hungarian algorithm predicted individuals to ground truth ones, using root mean squared error (rmse). The function provides a way to match predicted individuals to ground truth individuals based on the rmse distance between their corresponding keypoints. This algorithm is used to find the optimal matching, taking into account diff --git a/deeplabcut/pose_estimation_pytorch/runners/base.py b/deeplabcut/pose_estimation_pytorch/runners/base.py index eb9adff7cf..f7f0f3da96 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/base.py +++ b/deeplabcut/pose_estimation_pytorch/runners/base.py @@ -51,7 +51,7 @@ def set_load_weights_only(value: bool) -> None: class Runner(ABC, Generic[ModelType]): - """Runner base class + """Runner base class. A runner takes a model and runs actions on it, such as training or inference """ @@ -93,7 +93,7 @@ def load_snapshot( model: ModelType, weights_only: bool | None = None, ) -> dict: - """Loads the state dict for a model from a file + """Loads the state dict for a model from a file. This method loads a file containing a DeepLabCut PyTorch model snapshot onto a given device, and sets the model weights using the state_dict. @@ -183,7 +183,7 @@ def attempt_snapshot_load( def fix_snapshot_metadata(path: str | Path) -> None: - """Replace numpy floats in snapshot metrics + """Replace numpy floats in snapshot metrics. Only call this method with snapshots that you trust, as torch.load(...) is called with `weights_only=False`. For more information, see: @@ -209,8 +209,7 @@ def fix_snapshot_metadata(path: str | Path) -> None: def _add_numpy_to_torch_safe_globals(): - """ - Attempts tot add numpy classes allowing snapshots containing numpy floats in the + """Attempts tot add numpy classes allowing snapshots containing numpy floats in the metrics to be loaded without needing to change the `weights_only` argument. This fix only works for `numpy>=1.25.0`. diff --git a/deeplabcut/pose_estimation_pytorch/runners/ctd.py b/deeplabcut/pose_estimation_pytorch/runners/ctd.py index 715bcf79a3..a8ae4d27e2 100644 --- a/deeplabcut/pose_estimation_pytorch/runners/ctd.py +++ b/deeplabcut/pose_estimation_pytorch/runners/ctd.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Configuration for CTD tracking""" +"""Configuration for CTD tracking.""" from dataclasses import dataclass diff --git a/deeplabcut/pose_estimation_pytorch/task.py b/deeplabcut/pose_estimation_pytorch/task.py index 9104c711a5..37785df34c 100644 --- a/deeplabcut/pose_estimation_pytorch/task.py +++ b/deeplabcut/pose_estimation_pytorch/task.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Types of tasks that can be run by DeepLabCut pose estimation models""" +"""Types of tasks that can be run by DeepLabCut pose estimation models.""" from __future__ import annotations @@ -23,7 +23,7 @@ class TaskDataMixin: class Task(TaskDataMixin, Enum): - """A task to solve""" + """A task to solve.""" BOTTOM_UP = ("BU", "BottomUp"), "snapshot" DETECT = ("DT", "Detect"), "snapshot-detector" diff --git a/deeplabcut/pose_estimation_tensorflow/LICENSE b/deeplabcut/pose_estimation_tensorflow/LICENSE index 341c30bda4..65c5ca88a6 100644 --- a/deeplabcut/pose_estimation_tensorflow/LICENSE +++ b/deeplabcut/pose_estimation_tensorflow/LICENSE @@ -163,4 +163,3 @@ whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. - diff --git a/deeplabcut/pose_estimation_tensorflow/lib/crossvalutils.py b/deeplabcut/pose_estimation_tensorflow/lib/crossvalutils.py index 773f1b61fe..9a918bcaac 100644 --- a/deeplabcut/pose_estimation_tensorflow/lib/crossvalutils.py +++ b/deeplabcut/pose_estimation_tensorflow/lib/crossvalutils.py @@ -8,6 +8,6 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Backwards compatibility""" +"""Backwards compatibility.""" from deeplabcut.core.crossvalutils import * diff --git a/deeplabcut/pose_estimation_tensorflow/lib/inferenceutils.py b/deeplabcut/pose_estimation_tensorflow/lib/inferenceutils.py index 7a75c61440..f7603bb6f3 100644 --- a/deeplabcut/pose_estimation_tensorflow/lib/inferenceutils.py +++ b/deeplabcut/pose_estimation_tensorflow/lib/inferenceutils.py @@ -8,6 +8,6 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Backwards compatibility""" +"""Backwards compatibility.""" from deeplabcut.core.inferenceutils import * diff --git a/deeplabcut/pose_estimation_tensorflow/lib/trackingutils.py b/deeplabcut/pose_estimation_tensorflow/lib/trackingutils.py index 5459f03236..8fdd526ab1 100644 --- a/deeplabcut/pose_estimation_tensorflow/lib/trackingutils.py +++ b/deeplabcut/pose_estimation_tensorflow/lib/trackingutils.py @@ -8,6 +8,6 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Backwards compatibility""" +"""Backwards compatibility.""" from deeplabcut.core.trackingutils import * diff --git a/deeplabcut/pose_estimation_tensorflow/models/pretrained/download.sh b/deeplabcut/pose_estimation_tensorflow/models/pretrained/download.sh index 520da2fe5f..a7cb80a9e5 100644 --- a/deeplabcut/pose_estimation_tensorflow/models/pretrained/download.sh +++ b/deeplabcut/pose_estimation_tensorflow/models/pretrained/download.sh @@ -3,4 +3,4 @@ curl http://download.tensorflow.org/models/resnet_v1_50_2016_08_28.tar.gz | tar xvz curl http://download.tensorflow.org/models/resnet_v1_101_2016_08_28.tar.gz | tar xvz -curl http://download.tensorflow.org/models/resnet_v1_152_2016_08_28.tar.gz | tar xvz \ No newline at end of file +curl http://download.tensorflow.org/models/resnet_v1_152_2016_08_28.tar.gz | tar xvz diff --git a/deeplabcut/pose_tracking_pytorch/solver/scheduler_factory.py b/deeplabcut/pose_tracking_pytorch/solver/scheduler_factory.py index c341180f87..ed42cc8849 100644 --- a/deeplabcut/pose_tracking_pytorch/solver/scheduler_factory.py +++ b/deeplabcut/pose_tracking_pytorch/solver/scheduler_factory.py @@ -16,9 +16,7 @@ # Hacked together by / Copyright 2020 Ross Wightman # https://github.com/rwightman/pytorch-image-models/blob/main/timm/scheduler/scheduler_factory.py # -"""Scheduler Factory -Hacked together by / Copyright 2020 Ross Wightman -""" +"""Scheduler Factory Hacked together by / Copyright 2020 Ross Wightman.""" from .cosine_lr import CosineLRScheduler diff --git a/deeplabcut/pose_tracking_pytorch/tracking_utils/preprocessing.py b/deeplabcut/pose_tracking_pytorch/tracking_utils/preprocessing.py index a0b2df5a31..749b1b8ba1 100644 --- a/deeplabcut/pose_tracking_pytorch/tracking_utils/preprocessing.py +++ b/deeplabcut/pose_tracking_pytorch/tracking_utils/preprocessing.py @@ -12,7 +12,7 @@ def load_features_from_coord(feature, coords, valid_mask_for_fish=False): - """extract the deep feature at the location of the keypoint (x,y)""" + """Extract the deep feature at the location of the keypoint (x,y)""" if valid_mask_for_fish: mask = np.array([1, 2, 6]) coords = coords[mask, :] @@ -32,14 +32,8 @@ def load_features_from_coord(feature, coords, valid_mask_for_fish=False): def convert_coord_from_img_space_to_feature_space(arr, stride): - """ - if stride ==8: - stride = stride * 2 - elif stride == 4: - stride = stride *4 - elif stride ==2: - stride = stride *8 - """ + """If stride ==8: stride = stride * 2 elif stride == 4: stride = stride *4 elif + stride ==2: stride = stride *8.""" # More elegantly one can simply define: stride = 16 diff --git a/deeplabcut/reid_cfg.yaml b/deeplabcut/reid_cfg.yaml index 717ff9c670..f7977958da 100644 --- a/deeplabcut/reid_cfg.yaml +++ b/deeplabcut/reid_cfg.yaml @@ -51,4 +51,4 @@ log_period: 100 ########## Test ########## # Whether feature is normalized before test, if yes, it is equivalent to cosine distance -feat_norm: yes \ No newline at end of file +feat_norm: yes diff --git a/deeplabcut/utils/frameselectiontools.py b/deeplabcut/utils/frameselectiontools.py index 3e5bb0b0b8..0ee900fda7 100644 --- a/deeplabcut/utils/frameselectiontools.py +++ b/deeplabcut/utils/frameselectiontools.py @@ -28,9 +28,9 @@ def UniformFrames(clip, numframes2pick, start, stop, Index=None): - """Temporally uniformly sampling frames in interval (start,stop). - Visual information of video is irrelevant for this method. This code is fast and sufficient (to extract distinct frames), - when behavioral videos naturally covers many states. + """Temporally uniformly sampling frames in interval (start,stop). Visual information + of video is irrelevant for this method. This code is fast and sufficient (to extract + distinct frames), when behavioral videos naturally covers many states. The variable Index allows to pass on a subindex for the frames. """ @@ -71,9 +71,9 @@ def UniformFrames(clip, numframes2pick, start, stop, Index=None): # uses openCV def UniformFramescv2(cap, numframes2pick, start, stop, Index=None): - """Temporally uniformly sampling frames in interval (start,stop). - Visual information of video is irrelevant for this method. This code is fast and sufficient (to extract distinct frames), - when behavioral videos naturally covers many states. + """Temporally uniformly sampling frames in interval (start,stop). Visual information + of video is irrelevant for this method. This code is fast and sufficient (to extract + distinct frames), when behavioral videos naturally covers many states. The variable Index allows to pass on a subindex for the frames. """ @@ -127,7 +127,8 @@ def KmeansbasedFrameselection( Consider not extracting the frames from the whole video but rather set start and stop to a period around interesting behavior. - Note: this method can return fewer images than numframes2pick.""" + Note: this method can return fewer images than numframes2pick. + """ print( "Kmeans-quantization based extracting of frames from", @@ -210,10 +211,11 @@ def KmeansbasedFrameselectioncv2( max_iter=50, color=False, ): - """This code downsamples the video to a width of resizewidth. - The video is extracted as a numpy array, which is then clustered with kmeans, whereby each frames is treated as a vector. - Frames from different clusters are then selected for labeling. This procedure makes sure that the frames "look different", - i.e. different postures etc. On large videos this code is slow. + """This code downsamples the video to a width of resizewidth. The video is extracted + as a numpy array, which is then clustered with kmeans, whereby each frames is + treated as a vector. Frames from different clusters are then selected for labeling. + This procedure makes sure that the frames "look different", i.e. different postures + etc. On large videos this code is slow. Consider not extracting the frames from the whole video but rather set start and stop to a period around interesting behavior. diff --git a/docker/LICENSE b/docker/LICENSE index 341c30bda4..65c5ca88a6 100644 --- a/docker/LICENSE +++ b/docker/LICENSE @@ -163,4 +163,3 @@ whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. - diff --git a/docker/Makefile b/docker/Makefile index 39015360d7..01439891bc 100644 --- a/docker/Makefile +++ b/docker/Makefile @@ -6,7 +6,7 @@ prepare_build: python3 -m pip install --upgrade twine build cp ../docs/docker.md PYPI_README.md -build: clean prepare_build +build: clean prepare_build python3 -m build upload_test: prepare_build diff --git a/docker/README.md b/docker/README.md index d7c605d02a..3cad275b69 100644 --- a/docker/README.md +++ b/docker/README.md @@ -51,9 +51,9 @@ when calling `docker run`: deeplabcut-docker bash --gpus all -v /home/john:/home/john ``` -You can select which DeepLabCut version and CUDA version to use through the -`DLC_VERSION` and `CUDA_VERSION` environment variables. So to launch a container with -CUDA 12.1 and DLC 3.0.0, you can run: +You can select which DeepLabCut version and CUDA version to use through the +`DLC_VERSION` and `CUDA_VERSION` environment variables. So to launch a container with +CUDA 12.1 and DLC 3.0.0, you can run: ```bash DLC_VERSION=3.0.0 CUDA_VERSION=12.1 deeplabcut-docker bash --gpus all @@ -64,10 +64,10 @@ script if this is preferred over a python helper script.* ### Jupyter Notebooks Running on Remote Servers -Sometimes, we want to run Jupyter Notebooks on remote servers but connect to them +Sometimes, we want to run Jupyter Notebooks on remote servers but connect to them through the browser on our local machine. To do so, port forwarding needs to be used. This is straightforward, and there are many resources you can explore on how to do so ( -such as [this StackOverflow post](https://stackoverflow.com/a/69244262) or the [Jupyter +such as [this StackOverflow post](https://stackoverflow.com/a/69244262) or the [Jupyter Notebook docs](https://jupyter-notebook.readthedocs.io/en/4.x/public_server.html)). This can easily be done with `deeplabcut-docker`. To run a DeepLabCut notebook on a @@ -93,7 +93,7 @@ DLC_NOTEBOOK_PORT=8889 deeplabcut-docker notebook --gpus all ### Using Docker without `deeplabcut-docker` Docker images can also be run without the `deeplabcut-docker` package, for more expert -users. This is not the recommended, as many of the nice features (such as starting +users. This is not the recommended, as many of the nice features (such as starting the container with the current user instead of root) won't be there. The `core` image can simply be run by pulling the image and using `docker run`: @@ -103,12 +103,12 @@ docker pull deeplabcut/deeplabcut:3.0.0-core-cuda11.8-cudnn9 docker run -it --rm --gpus all deeplabcut/deeplabcut:3.0.0-core-cuda11.8-cudnn9 ``` -The `jupyter` image cannot be run in the same way. Notebook servers cannot be run as +The `jupyter` image cannot be run in the same way. Notebook servers cannot be run as the root user (which can be dangerous) without passing the `--allow-root` option, so -running `docker run deeplabcut/deeplabcut:3.0.0-jupyter-cuda11.8-cudnn9` will lead to an -error (`Running as root is not recommended. Use --allow-root to bypass`). What you can +running `docker run deeplabcut/deeplabcut:3.0.0-jupyter-cuda11.8-cudnn9` will lead to an +error (`Running as root is not recommended. Use --allow-root to bypass`). What you can do (and we do in the `deeplabcut-docker` package) is to build a docker image with the -`jupyter` image as a base. We would recommend doing this for the `core` images as well. +`jupyter` image as a base. We would recommend doing this for the `core` images as well. You can create the `Dockerfile`: ```dockerfile @@ -159,31 +159,31 @@ Images can be verified by running ``` docker/build.sh test -``` +``` Built images can be pushed to DockerHub by running ``` docker/build.sh push -``` +``` ## Prerequisites (if you don't have Docker installed already) **(1)** Install Docker. See https://docs.docker.com/install/ & for Ubuntu: https://docs.docker.com/install/linux/docker-ce/ubuntu/ -Test docker: +Test docker: $ sudo docker run hello-world - + The output should be: ``Hello from Docker! This message shows that your installation appears to be working correctly.`` -*if you get the error ``docker: Error response from daemon: Unknown runtime specified nvidia.`` just simply restart docker: - +*if you get the error ``docker: Error response from daemon: Unknown runtime specified nvidia.`` just simply restart docker: + $ sudo systemctl daemon-reload $ sudo systemctl restart docker - + **(2)** Add your user to the docker group (https://docs.docker.com/install/linux/linux-postinstall/#manage-docker-as-a-non-root-user) -Quick guide to create the docker group and add your user: +Quick guide to create the docker group and add your user: Create the docker group. $ sudo groupadd docker @@ -203,12 +203,12 @@ Ascii art in the MOTD is adapted from https://ascii.co.uk/art/mice and https://p '.__/o o\__.' `{= ^ =}´ > u < - ____________________.""`-------`"".______________________ + ____________________.""`-------`"".______________________ \ ___ __ __ _____ __ / / / _ \ ___ ___ ___ / / ___ _ / / / ___/__ __ / /_ \ \ / // // -_)/ -_)/ _ \ / /__/ _ `// _ \/ /__ / // // __/ / //____/ \__/ \__// .__//____/\_,_//_.__/\___/ \_,_/ \__/ \ \_________________________________________________________/ - ___)( )(___ `-.___. + ___)( )(___ `-.___. (((__) (__))) ~` ``` diff --git a/docker/motd.sh b/docker/motd.sh index 2628a2e2fd..4dc268ccc7 100644 --- a/docker/motd.sh +++ b/docker/motd.sh @@ -27,13 +27,13 @@ cat <<"EOF" '.__/o o\__.' `{= ^ =}´ > u < - ____________________.""`-------`"".______________________ + ____________________.""`-------`"".______________________ \ ___ __ __ _____ __ / / / _ \ ___ ___ ___ / / ___ _ / / / ___/__ __ / /_ \ \ / // // -_)/ -_)/ _ \ / /__/ _ `// _ \/ /__ / // // __/ / //____/ \__/ \__// .__//____/\_,_//_.__/\___/ \_,_/ \__/ \ \_________________________________________________________/ - ___)( )(___ `-.___. + ___)( )(___ `-.___. (((__) (__))) ~` EOF diff --git a/docker/pyproject.toml b/docker/pyproject.toml index 07de284aa5..b4f08b1697 100644 --- a/docker/pyproject.toml +++ b/docker/pyproject.toml @@ -1,3 +1,3 @@ [build-system] -requires = ["setuptools", "wheel"] -build-backend = "setuptools.build_meta" \ No newline at end of file +build-backend = "setuptools.build_meta" +requires = [ "setuptools", "wheel" ] diff --git a/docker/setup.cfg b/docker/setup.cfg index 6821ec6402..00d64ed987 100644 --- a/docker/setup.cfg +++ b/docker/setup.cfg @@ -8,7 +8,7 @@ maintainer_email = stes@hey.com description = A helper package to launch DeepLabCut docker images url = https://github.com/DeepLabCut/DeepLabCut/tree/main/docker project_urls = - Bug Tracker = https://github.com/DeepLabCut/DeepLabCut/issues + Bug Tracker = https://github.com/DeepLabCut/DeepLabCut/issues classifiers = Programming Language :: Python :: 3 License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3) @@ -22,7 +22,7 @@ platform = any [options] package_dir = - = . + = . py_modules = deeplabcut_docker python_requires = >=3.10 include_package_data = True diff --git a/docs/HelperFunctions.md b/docs/HelperFunctions.md index e4a5aeebe8..5d612e2282 100644 --- a/docs/HelperFunctions.md +++ b/docs/HelperFunctions.md @@ -40,7 +40,7 @@ Only videos with this extension are analyzed. The default is ``.avi`` ----------- Converts all pose-output files belonging to mp4 videos in the folder '/media/alex/experimentaldata/cheetahvideos' to csv files. - deeplabcut.analyze_videos_converth5_to_csv('/media/alex/experimentaldata/cheetahvideos','.mp4') + deeplabcut.analyze_videos_converth5_to_csv('/media/alex/experimentaldata/cheetahvideos','.mp4') ``` While some of the names are ridiculously long, we wanted them to be "self-explanatory." Here is a list diff --git a/docs/Overviewof3D.md b/docs/Overviewof3D.md index 6a83d7575e..8fec86b5c9 100644 --- a/docs/Overviewof3D.md +++ b/docs/Overviewof3D.md @@ -215,7 +215,7 @@ video filename must contain this naming, i.e. this could be named as `rig-1-mous `rig-1-mouse-day1-camera-2-date.avi`. - **Note** that to correctly pair the videos, the file names otherwise need to be the same! -- If helpful, [here is the software we use to record videos](https://github.com/AdaptiveMotorControlLab/Camera_Control). +- If helpful, [here is the software we use to record videos](https://github.com/AdaptiveMotorControlLab/Camera_Control). (**CRITICAL!**) You must also edit the **3D project config.yaml** file to denote which DeepLabCut projects have the information for the 2D views. diff --git a/docs/README.md b/docs/README.md index df606c21b6..ba9d821e84 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,3 +1,3 @@ -Please see https://deeplabcut.github.io/DeepLabCut for documentation on how to use this software. +Please see https://deeplabcut.github.io/DeepLabCut for documentation on how to use this software. This directory contains the source code for the docs. diff --git a/docs/UseOverviewGuide.md b/docs/UseOverviewGuide.md index 017b30dcc4..be744393dc 100644 --- a/docs/UseOverviewGuide.md +++ b/docs/UseOverviewGuide.md @@ -1,5 +1,5 @@ (overview)= -# 🥳 Get started with DeepLabCut: our key recommendations +# 🥳 Get started with DeepLabCut: our key recommendations Below we will first outline what you need to get started, the different ways you can use DeepLabCut, and then the full workflow. Note, we highly recommend you also read and follow our [Nature Protocols paper](https://www.nature.com/articles/s41596-019-0176-0), which is (still) fully relevant to standard DeepLabCut. @@ -32,7 +32,7 @@ We are primarily a package that enables deep learning-based pose estimation. We

- + **Additional Learning Resources:** - [TUTORIALS:](https://www.youtube.com/channel/UC2HEbWpC_1v6i9RnDMy-dfA?view_as=subscriber) video tutorials that demonstrate various aspects of using the code base. diff --git a/docs/beginner-guides/Training-Evaluation.md b/docs/beginner-guides/Training-Evaluation.md index 18ab3bf736..d1a6e55144 100644 --- a/docs/beginner-guides/Training-Evaluation.md +++ b/docs/beginner-guides/Training-Evaluation.md @@ -35,12 +35,12 @@ After training, it's time to see how well your model performs. - **Compare Bodyparts:** Opt to compare all the bodyparts for a comprehensive evaluation. 3. Click the **`Evaluate Network`** button, located on the right side of the main window. ->💡 Tip: If you wish to evaluate all saved snapshots, go to the configuration file and change the `snapshotindex` parameter to `all`. +>💡 Tip: If you wish to evaluate all saved snapshots, go to the configuration file and change the `snapshotindex` parameter to `all`. ### Understanding the Evaluation Results -- **Performance Metrics:** DLC will assess the latest snapshot of your model, generating a `.CSV` file with performance +- **Performance Metrics:** DLC will assess the latest snapshot of your model, generating a `.CSV` file with performance metrics. This file is stored in the **`evaluation-results`** (for TensorFlow models) or the **`evaluation-results-pytorch`** (for PyTorch models) folder within your project. diff --git a/docs/beginner-guides/beginners-guide.md b/docs/beginner-guides/beginners-guide.md index f3cfea194d..37cdba7eea 100644 --- a/docs/beginner-guides/beginners-guide.md +++ b/docs/beginner-guides/beginners-guide.md @@ -1,5 +1,5 @@ (beginners-guide)= -# Using DeepLabCut +# Using DeepLabCut DLC LIVE! This guide, and related pages, are meant as a very-new-to-python beginner guide to DeepLabCut. After you are comfortable with this material we recommend then jumping into the more detailed User Guides! @@ -15,20 +15,20 @@ Before you begin, make sure that DeepLabCut is installed on your system. ## Beginner User Guide If you are new to Python, the best way to get Python installed onto your computer is with Anaconda. [Head over here and download the version that is best for your computer](https://www.anaconda.com/download). -- "Conda", as it's often called, it a very nice way to create "environments (env)" on your computer. While there can be some cross-talk, in general, it allows you to separate the different tools you need to use to get your science done 💪. +- "Conda", as it's often called, it a very nice way to create "environments (env)" on your computer. While there can be some cross-talk, in general, it allows you to separate the different tools you need to use to get your science done 💪. ## Let's learn a bit and create a DeeplabCut env: -After you have installed Anaconda, open the new program (Anaconda Terminal). You will be in your "root" directory by default. +After you have installed Anaconda, open the new program (Anaconda Terminal). You will be in your "root" directory by default. -**(0) Create a fresh `conda environment`** +**(0) Create a fresh `conda environment`** In the terminal, type: ``` conda create -n deeplabcut python=3.10 ``` -You will be prompted (y/n) to install, and then wait for the magic to happen. At the end, check the terminal, it should prompt you to then type: +You will be prompted (y/n) to install, and then wait for the magic to happen. At the end, check the terminal, it should prompt you to then type: ``` conda activate deeplabcut @@ -48,7 +48,7 @@ pip install torch torchvision --index-url https://download.pytorch.org/whl/cu124 pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu ``` -**(2) Install DeepLabCut** +**(2) Install DeepLabCut** Alright! Next, we will install all the `deeplabcut` source code 🔥. Please decide which version you want (stable or alpha), then type: @@ -94,19 +94,19 @@ When you first launch the GUI, you'll find three primary main options: 1. **Launch New Project:** - When you start a new project, you'll be presented with an empty project window. In DLC3+ you will see a new option "Engine". - We recommend using the PyTorch Engine: - + ![DeepLabCut Engine](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1717780414978-17LOVBUJ8JR102QVSFDY/Screen+Shot+2024-06-07+at+7.13.14+PM.png?format=1500w)) 2. **Filling in Project Details:** - **Naming Your Project:** - Give a specific, well-defined name to your project. - + > **💡 Tip:** Avoid empty spaces in your project name. - **Naming the Experimenter:** - Fill in the name of the experimenter. This part of the data remains immutable. -3. **Determine Project Location:** +3. **Determine Project Location:** - By default, your project will be located on the **Desktop**. - To pick a different home, modify the path as needed. @@ -117,15 +117,15 @@ When you first launch the GUI, you'll find three primary main options: 5. **Adding Videos:** - First, click on **`Browse Videos`** button on the right side of the window, to search for the video contents. - Once the media selection tool opens, navigate and select the folder with your videos. - + > **💡 Tip:** DeepLabCut supports **`.mp4`**, **`.avi`**, **`.mkv`** and **`.mov`** files. - A list will be created with all the videos inside this folder. - Unselect the videos you wish to remove from the project. - + 6. **Create your project:** - Click on **`Create`** button on the bottom, right side of the main window. - A new folder named after your project's name will be created in the location you chose above. - + ### 📽 Video Tutorial: Setting Up Your Project in DeepLabCut diff --git a/docs/beginner-guides/labeling.md b/docs/beginner-guides/labeling.md index f3a0ec10d3..dbfd5d5f1c 100644 --- a/docs/beginner-guides/labeling.md +++ b/docs/beginner-guides/labeling.md @@ -53,18 +53,17 @@ Alright, you've got your extracted frames ready. Now comes the labeling! ### Completing the Set -Work through all the frames in the first folder. Then, proceed to the next, continuing this way until each folder in your **labeled-data** directory is done. +Work through all the frames in the first folder. Then, proceed to the next, continuing this way until each folder in your **labeled-data** directory is done. ## Checking Your Labels -After you've labeled all your frames, it's important to ensure they're accurate. +After you've labeled all your frames, it's important to ensure they're accurate. ### How to Check Your Labels -- **Return to the Main Window:** Once you're done with labeling, head back to DeepLabCut's main window, and click on **`Check Labels`**. +- **Return to the Main Window:** Once you're done with labeling, head back to DeepLabCut's main window, and click on **`Check Labels`**. - **Review the Labeled Folders:** The system will have created new folders for each labeled set inside your labeled-data folder. These folders contain your original frames overlaid with the keypoints you've added. ![Checking Labels in DeepLabCut](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/1717779615252-6BNW661XB2ULH85RTAD3/evaluation-example.png?format=500w) Take the time to go through each folder. Accurate labels are key. If there are mistakes, the model might learn incorrectly and mislabel your videos later on. It's all about setting the right foundation for accurate analysis. - diff --git a/docs/beginner-guides/video-analysis.md b/docs/beginner-guides/video-analysis.md index 126c7bdf9a..3744c30c68 100644 --- a/docs/beginner-guides/video-analysis.md +++ b/docs/beginner-guides/video-analysis.md @@ -31,5 +31,5 @@ After training and evaluating your model, the next step is to apply it to your v - Your labeled video will be in your video folder, named after the original video plus model details and 'labeled'. - Watch the video to assess the model's labeling accuracy. -## Happy DeepLabCutting! +## Happy DeepLabCutting! - Check out the more advanced user guides for even more options! diff --git a/docs/citation.md b/docs/citation.md index c427b1e223..0523f50cb8 100644 --- a/docs/citation.md +++ b/docs/citation.md @@ -31,7 +31,7 @@ DOIs (#ProTip, for helping you find citations for software, check out [CiteAs.or journal = {Nature Protocols}, year = {2019}, url = {https://doi.org/10.1038/s41596-019-0176-0}} - + @InProceedings{Mathis_2021_WACV, author = {Mathis, Alexander and Biasi, Thomas and Schneider, Steffen and Yuksekgonul, Mert and Rogers, Byron and Bethge, Matthias and Mathis, Mackenzie W.}, title = {Pretraining Boosts Out-of-Domain Robustness for Pose Estimation}, @@ -39,7 +39,7 @@ DOIs (#ProTip, for helping you find citations for software, check out [CiteAs.or month = {January}, year = {2021}, pages = {1859-1868}} - + @article{Lauer2022MultianimalPE, title={Multi-animal pose estimation, identification and tracking with DeepLabCut}, author={Jessy Lauer and Mu Zhou and Shaokai Ye and William Menegas and Steffen Schneider and Tanmay Nath and Mohammed Mostafizur Rahman and Valentina Di Santo and Daniel Soberanes and Guoping Feng and Venkatesh N. Murthy and George Lauder and Catherine Dulac and M. Mathis and Alexander Mathis}, @@ -90,7 +90,7 @@ DOIs (#ProTip, for helping you find citations for software, check out [CiteAs.or ## Methods Suggestion: -For body part tracking we used DeepLabCut (version 2.X.X)* [Mathis et al, 2018, Nath et al, 2019, Lauer et al. 2022]. Specifically, we labeled X number of frames taken from X videos/animals (then X% was used for training (default is 95%). We used a X-based neural network (i.e. X = ResNet-50, ResNet-101, MobileNetV2-0.35, MobileNetV2-0.5, MobileNetV2-0.75, MobileNetV2-1***) with default parameters* for X number of training iterations. We validated with X number of shuffles, and found the test error was: X pixels, train: X pixels (image size was X by X). We then used a p-cutoff of X (i.e. 0.9) to condition the X,Y coordinates for future analysis. This network was then used to analyze videos from similar experimental settings. +For body part tracking we used DeepLabCut (version 2.X.X)* [Mathis et al, 2018, Nath et al, 2019, Lauer et al. 2022]. Specifically, we labeled X number of frames taken from X videos/animals (then X% was used for training (default is 95%). We used a X-based neural network (i.e. X = ResNet-50, ResNet-101, MobileNetV2-0.35, MobileNetV2-0.5, MobileNetV2-0.75, MobileNetV2-1***) with default parameters* for X number of training iterations. We validated with X number of shuffles, and found the test error was: X pixels, train: X pixels (image size was X by X). We then used a p-cutoff of X (i.e. 0.9) to condition the X,Y coordinates for future analysis. This network was then used to analyze videos from similar experimental settings. > Mathis, A. et al. Deeplabcut: markerless pose estimation > of user-defined body parts with deep learning. Nature @@ -100,14 +100,14 @@ For body part tracking we used DeepLabCut (version 2.X.X)* [Mathis et al, 2018, > estimation across species and behaviors. Nature Protocols > 14, 2152–2176 (2019). -*If any defaults were changed in *`pose_config.yaml`*, mention them here. +*If any defaults were changed in *`pose_config.yaml`*, mention them here. -i.e. common things one might change: -* the loader (options are `default`, `imgaug`, `tensorpack`, `deterministic`). +i.e. common things one might change: +* the loader (options are `default`, `imgaug`, `tensorpack`, `deterministic`). * the `post_dist_threshold` (default is 17 and determines training resolution). -* optimizer: do you use the default `SGD` or `ADAM`? +* optimizer: do you use the default `SGD` or `ADAM`? -*** here, you could add additional citations. +*** here, you could add additional citations. If you use ResNets, consider citing Insafutdinov et al 2016 & He et al 2016. If you use the MobileNetV2s consider citing Mathis et al 2019, and Sandler et al, 2018. @@ -131,7 +131,7 @@ If you use ResNets, consider citing Insafutdinov et al 2016 & He et al 2016. If > 770–778 (2016). URL https://arxiv.org/abs/ > 1512.03385. -## Graphics +## Graphics We also have the network graphic freely available on SciDraw.io if you'd like to use it! https://scidraw.io/drawing/290 diff --git a/docs/course.md b/docs/course.md index ff25b8d6e4..3b12e3bf91 100644 --- a/docs/course.md +++ b/docs/course.md @@ -5,7 +5,7 @@ This course was designed for DLC 2. An updated version for DLC 3 is in the works. :::: -Do you have video of animal behaviors? Step 1: Get Poses ... +Do you have video of animal behaviors? Step 1: Get Poses ... DLC LIVE! diff --git a/docs/dlc-live/dlc-live-gui/index.md b/docs/dlc-live/dlc-live-gui/index.md index ad5877061b..5b519483e7 100644 --- a/docs/dlc-live/dlc-live-gui/index.md +++ b/docs/dlc-live/dlc-live-gui/index.md @@ -71,5 +71,5 @@ Before getting started, be aware of the following constraints: ## Feedback, issues, and contributions > *This project is under active development. Feedback from real experimental use is highly valued.* -> +> > [Please report issues, suggest features, or contribute to the codebase on GitHub !](https://github.com/DeepLabCut/DeepLabCut-live-GUI) diff --git a/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/basler_backend.md b/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/basler_backend.md index 0abc3dfd8e..f2ae9de1df 100644 --- a/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/basler_backend.md +++ b/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/basler_backend.md @@ -11,7 +11,7 @@ This backend requires the optional `pypylon` dependency. If `pypylon` is not ins --- -## Features & design +## Features & design - Native Basler camera support via **pypylon** (Pylon SDK bindings). - Best-effort device discovery without opening cameras (enumerates `DeviceInfo` entries). diff --git a/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/camera_support.md b/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/camera_support.md index a1f8b3b9a0..24681a5aa8 100644 --- a/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/camera_support.md +++ b/docs/dlc-live/dlc-live-gui/user_guide/cameras_backends/camera_support.md @@ -32,9 +32,9 @@ You can select the backend in the GUI from the "Backend" dropdown, or in your co Below are some general recommendations for backend selection based on your operating system and camera type. ```{note} -Please understand this may not reflect the exact capabilities for every setup. +Please understand this may not reflect the exact capabilities for every setup. -Let us know about your experience with different cameras and backends on different platforms to help us improve our documentation and support! +Let us know about your experience with different cameras and backends on different platforms to help us improve our documentation and support! ``` ### Windows @@ -89,5 +89,3 @@ Install vendor-provided camera drivers and SDK. CTI files are typically in: | Windows | ✅ | ✅ | ❌ | ✅ | | Linux | ✅ | ✅ | ✅ | ✅ | | macOS | ✅ | ❌ | ⚠️ | ✅ | - - diff --git a/docs/dlc-live/dlc-live-gui/user_guide/misc/timestamp_format.md b/docs/dlc-live/dlc-live-gui/user_guide/misc/timestamp_format.md index 1b715fdeaa..c4cf2d7489 100644 --- a/docs/dlc-live/dlc-live-gui/user_guide/misc/timestamp_format.md +++ b/docs/dlc-live/dlc-live-gui/user_guide/misc/timestamp_format.md @@ -92,4 +92,4 @@ for frame_idx, timestamp in enumerate(data['timestamps']): The encoded video is written with a fixed input frame rate configured when recording starts. -The timestamps reflect capture/enqueue timing and may not perfectly match the encoded frame pacing, especially if frames are dropped or capture timing varies. \ No newline at end of file +The timestamps reflect capture/enqueue timing and may not perfectly match the encoded frame pacing, especially if frames are dropped or capture timing varies. diff --git a/docs/docker.md b/docs/docker.md index 473cded14e..6de73aa5c0 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -10,8 +10,8 @@ running Jupyter notebooks with DeepLabCut pre-installed are shipped with the pro Docker images. The [`napari-deeplabcut` labelling GUI]( -https://deeplabcut.github.io/DeepLabCut/docs/gui/napari_GUI.html) can be used to label -your data, but it cannot be run in a Docker container: it should be installed as +https://deeplabcut.github.io/DeepLabCut/docs/gui/napari_GUI.html) can be used to label +your data, but it cannot be run in a Docker container: it should be installed as documented in the link above: `pip install napari-deeplabcut` (checkout the [workflow]( https://deeplabcut.github.io/DeepLabCut/docs/gui/napari_GUI.html#workflow) as well!). @@ -40,7 +40,7 @@ If you want to mount the whole directory could e.g., pass* If read-only access is enough, `deeplabcut-docker bash -v /home/mackenzie/DEEPLABCUT:/home/mackenzie/DEEPLABCUT:ro` -### Terminal mode +### Terminal mode You can run the light version of DeepLabCut and open a terminal by running @@ -48,7 +48,7 @@ You can run the light version of DeepLabCut and open a terminal by running $ deeplabcut-docker bash ``` -**Important:** if have GPUs on your machine and want to use them to train models, you +**Important:** if have GPUs on your machine and want to use them to train models, you need to pass the `--gpus all` argument to `deeplabcut-docker`: ``` bash @@ -67,7 +67,7 @@ $ ipython You can run DeepLabCut by starting a jupyter notebook server. The corresponding image can be pulled and started by running ``` bash -$ deeplabcut-docker notebook +$ deeplabcut-docker notebook ``` which will start a Jupyter notebook server. Follow the terminal instructions to open the notebook, by entering `http://127.0.0.1:8888` in your favorite browser. When prompted for a password, use `deeplabcut`, which is the pre-set option in the container. @@ -81,20 +81,20 @@ Advanced users and developers can visit the [`/docker` subdirectory](https://git ## Prerequisites (if you don't have Docker installed already) **(1)** Install Docker. See https://docs.docker.com/install/ & for Ubuntu: https://docs.docker.com/install/linux/docker-ce/ubuntu/ -Test docker: +Test docker: $ sudo docker run hello-world - + The output should be: ``Hello from Docker! This message shows that your installation appears to be working correctly.`` -*if you get the error ``docker: Error response from daemon: Unknown runtime specified nvidia.`` just simply restart docker: - +*if you get the error ``docker: Error response from daemon: Unknown runtime specified nvidia.`` just simply restart docker: + $ sudo systemctl daemon-reload $ sudo systemctl restart docker - + **(2)** Add your user to the docker group (https://docs.docker.com/install/linux/linux-postinstall/#manage-docker-as-a-non-root-user) -Quick guide to create the docker group and add your user: +Quick guide to create the docker group and add your user: Create the docker group. $ sudo groupadd docker diff --git a/docs/installation.md b/docs/installation.md index a6ef8ca929..bef35402b6 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -54,7 +54,7 @@ and makes it possible to use the GPU with both PyTorch and TensorFlow. - **CPU?** Great, jump to the next section below! - **NVIDIA GPU?** If you want to use your own GPU (i.e., a GPU is in your workstation), then you need to be sure you have a CUDA compatible GPU, CUDA, and cuDNN installed. Please note, which CUDA you install depends on what version of PyTorch you want to use. So, please check "GPU Support" below carefully. **Note, DeepLabCut is up to date with the latest CUDA and PyTorch!** - + - **Apple M-chip GPU?** Be sure to install miniconda3, and your GPU will be used by default. ```` @@ -122,13 +122,13 @@ NOTE: no need to run pip install deeplabcut, as it is already installed!!! :) ````{admonition} DeepLabCut TensorFlow Support :class: dropdown -As of June 2024 we have a PyTorch Engine backend and we will be depreciating the -TensorFlow backend by the end of 2024. Currently, if you want to use TensorFlow, you -need to run `pip install deeplabcut[tf]` in order to install the correct version of -TensorFlow in your conda env. Please note, we will be providing bug fixes, but we will +As of June 2024 we have a PyTorch Engine backend and we will be depreciating the +TensorFlow backend by the end of 2024. Currently, if you want to use TensorFlow, you +need to run `pip install deeplabcut[tf]` in order to install the correct version of +TensorFlow in your conda env. Please note, we will be providing bug fixes, but we will not be supporting new TensorFlow versions beyond 2.10 (Windows), and 2.12 for other OS. -Installing TensorFlow and getting it to have access to the GPU can be a bit tricky. +Installing TensorFlow and getting it to have access to the GPU can be a bit tricky. Check TensorFlow's [compatibility matrix](https://www.tensorflow.org/install/source#gpu) to know which version of CUDA and cuDNN you should install. @@ -163,7 +163,7 @@ pip install --pre deeplabcut ### Step 3: Really, that's it! Let's run DeepLabCut -Head over to the [User Guide Overview](https://deeplabcut.github.io/DeepLabCut/docs/UseOverviewGuide.html) for information. +Head over to the [User Guide Overview](https://deeplabcut.github.io/DeepLabCut/docs/UseOverviewGuide.html) for information. 🎉 Launch DeepLabCut in your new env by running `python -m deeplabcut` @@ -179,7 +179,7 @@ To git clone type: ``git clone https://github.com/DeepLabCut/DeepLabCut.git``). ### PIP: - Everything you need to build custom models within DeepLabCut (i.e., use our source code and our dependencies) can be installed with `pip install 'deeplabcut[gui]'` (for GUI support w/PyTorch) or without the gui: `pip install 'deeplabcut'`. -- If you want to use the SuperAnimal models, then please use `pip install 'deeplabcut[gui,modelzoo]'`. +- If you want to use the SuperAnimal models, then please use `pip install 'deeplabcut[gui,modelzoo]'`. ## DOCKER: @@ -192,23 +192,23 @@ More [installation ProTips](installation-tips) are also available. If you ever want to update your DLC, just run `pip install --upgrade deeplabcut` once you are inside your env. If you want to use a specific release, then you need to specify the version you want, such as `pip install deeplabcut==3.0`. Once installed, you can -check the version by running `import deeplabcut` `deeplabcut.__version__`. Don't be -afraid to update, DLC is backwards compatible with your 2.0+ projects and performance +check the version by running `import deeplabcut` `deeplabcut.__version__`. Don't be +afraid to update, DLC is backwards compatible with your 2.0+ projects and performance continues to get better and new features are added nearly monthly. **All of the data you labelled in version 2.X is also compatible with version 3+ and the PyTorch engine**! There is no change in the workflow or the way labels are handled: the big changes happen under-the-hood! If you've been working with DeepLabCut 2.X and want -to learn more about moving to the PyTorch engine, checkout our docs on [moving from +to learn more about moving to the PyTorch engine, checkout our docs on [moving from TensorFlow to PyTorch](dlc3-user-guide) Here are some conda environment management tips: [kapeli.com: Conda Cheat Sheet]( https://kapeli.com/cheat_sheets/Conda.docset/Contents/Resources/Documents/index) -**Pro Tip:** If you want to modify code and then test it, you can use our provided +**Pro Tip:** If you want to modify code and then test it, you can use our provided testscripts. This would mean you need to be up-to-date with the latest GitHub-based code though! Please see [here](installation-tips) on how to get the latest GitHub code, and -how to test your installation by following this video: +how to test your installation by following this video: https://www.youtube.com/watch?v=IOWtKn3l33s. ## Creating your own customized conda env (recommended route for Linux: Ubuntu, CentOS, Mint, etc.) @@ -234,16 +234,16 @@ The ONLY thing you need to do **first** if you have an NVIDIA GPU and the matchi ### The most common "new user" hurdle is installing and using your GPU, so don't get discouraged! -**CRITICAL:** If you have a GPU, you should FIRST **install an appropriate driver for +**CRITICAL:** If you have a GPU, you should FIRST **install an appropriate driver for your specific GPU**, then you can use the supplied conda file. You'll need an NVIDIA GPU -which is compatible with CUDA. To see a list of CUDA-enabled NVIDIA GPUs, please [see +which is compatible with CUDA. To see a list of CUDA-enabled NVIDIA GPUs, please [see their website](https://developer.nvidia.com/cuda-gpus). - Here we provide notes on how to install and check your GPU use with TensorFlow (which is used by DeepLabCut and already installed with the Anaconda files above). Thus, you do not need to independently install tensorflow. -**FIRST**, install a driver for your GPU. Find DRIVER HERE: +**FIRST**, install a driver for your GPU. Find DRIVER HERE: https://www.nvidia.com/download/index.aspx - Check which driver is installed by typing this into the terminal: ``nvidia-smi``. @@ -254,10 +254,10 @@ https://www.nvidia.com/download/index.aspx ### Notes: -- **As of version 3.0+ we moved to PyTorch. The Last supported version of TensorFlow is +- **As of version 3.0+ we moved to PyTorch. The Last supported version of TensorFlow is 2.10 (window users) and 2.12 for others (we have not tested beyond this).** - Please be mindful different versions of TensorFlow require different CUDA versions. -- As the combination of TensorFlow and CUDA matters, we strongly encourage you to +- As the combination of TensorFlow and CUDA matters, we strongly encourage you to **check your driver/cuDNN/CUDA/TensorFlow versions** [on this StackOverflow post]( https://stackoverflow.com/questions/30820513/what-is-version-of-cuda-for-nvidia-304-125/30820690#30820690 ). @@ -265,9 +265,9 @@ https://stackoverflow.com/questions/30820513/what-is-version-of-cuda-for-nvidia- `nvcc -V` to check your installed version(s). -- The best practice is to then run the supplied `testscript_pytorch_single_animal.py` +- The best practice is to then run the supplied `testscript_pytorch_single_animal.py` (or `testscript.py` for the TensorFlow engine); this is inside the examples folder you -acquired when you git cloned the repo. Here is more information/a short +acquired when you git cloned the repo. Here is more information/a short [video on running the testscript](https://www.youtube.com/watch?v=IOWtKn3l33s). - Additionally, if you want to use the bleeding edge, with your git clone you also get the latest code. While inside the main DeepLabCut folder, you can run `./reinstall.sh` diff --git a/docs/maDLC_UserGuide.md b/docs/maDLC_UserGuide.md index 9861a41242..54573b9eff 100644 --- a/docs/maDLC_UserGuide.md +++ b/docs/maDLC_UserGuide.md @@ -7,10 +7,10 @@ and it is here to support the scientific advances presented in [Lauer et al. 202 Note, we strongly encourage you to use the [Project Manager GUI](project-manager-gui) when you first start using multi-animal mode. Each tab is customized for multi-animal when you create or load a multi-animal project. As long as you follow the recommendations within the GUI, you should be good to go! ````{versionadded} 3.0.0 -PyTorch is now available as a deep learning engine for pose estimation models, along +PyTorch is now available as a deep learning engine for pose estimation models, along with new model architectures! For more information about moving from TensorFlow to -PyTorch (if you're already familiar with DeepLabCut & the TensorFlow engine), -check out [the PyTorch user guide](dlc3-user-guide). If you're just starting +PyTorch (if you're already familiar with DeepLabCut & the TensorFlow engine), +check out [the PyTorch user guide](dlc3-user-guide). If you're just starting out with DeepLabCut, we suggest you use the PyTorch backend. ```` @@ -78,7 +78,7 @@ config_path = '/thefulloutputpath/config.yaml' This set of arguments will create a project directory with the name **Name of the project+name of the experimenter+date of creation of the project** in the **Working directory** and creates the symbolic links to videos in the **videos** directory. The project directory will have subdirectories: **dlc-models**, **dlc-models-pytorch**, **labeled-data**, **training-datasets**, and **videos**. All the outputs generated during the course of a project will be stored in one of these subdirectories, thus allowing each project to be curated in separation from other projects. The purpose of the subdirectories is as follows: -**dlc-models** and **dlc-models-pytorch** have a similar structure: the first contains +**dlc-models** and **dlc-models-pytorch** have a similar structure: the first contains files for the TensorFlow engine while the second contains files for the PyTorch engine. At the top level in these directories, there are directories referring to different iterations of labels refinement (see below): **iteration-0**, **iteration-1**, etc. @@ -215,7 +215,7 @@ parameters in the config.yaml file. Also, the user can change the number of fram the numframes2extract in the config.yaml file. ```{TIP} -For maDLC, **be sure you have labeled frames with closely interacting animals**! +For maDLC, **be sure you have labeled frames with closely interacting animals**! Therefore, manually selecting some frames is a good idea if interactions are not highly frequent in the video. ``` @@ -232,7 +232,7 @@ deeplabcut.extract_frames(config_path, 'manual') // FIXME(niels) - add a napari frame extractor description. The user can use the *Load Video* button to load one of the videos in the project -configuration file, use the scroll bar to navigate across the video and *Grab a Frame*. +configuration file, use the scroll bar to navigate across the video and *Grab a Frame*. The user can also look at the extracted frames and e.g. delete frames (from the directory) that are too similar before reloading the set and then manually annotating them. @@ -252,8 +252,8 @@ deeplabcut.label_frames(config_path) The toolbox provides a function **label_frames** which helps the user to easily label all the extracted frames using an interactive graphical user interface (GUI). The user -should have already named the bodyparts to label (points of interest) in the -project’s configuration file by providing a list. The following command invokes the +should have already named the bodyparts to label (points of interest) in the +project’s configuration file by providing a list. The following command invokes the napari-deeplabcut labelling GUI. [🎥 DEMO](https://youtu.be/hsA9IB5r73E) @@ -274,7 +274,7 @@ occluded points should not be labeled by the user, unless you want to teach the to "guess" - this is possible, but could affect accuracy. If you don't want/or don't see a bodypart, they can simply be skipped by not applying the label anywhere on the frame. -OPTIONAL: In the event of adding more labels to the existing labeled dataset, the user +OPTIONAL: In the event of adding more labels to the existing labeled dataset, the user needs to append the new labels to the bodyparts in the config.yaml file. Thereafter, the user can call the function **label_frames**. A box will pop up and ask the user if they wish to display all parts, or only add in the new labels. Saving the labels after all @@ -283,10 +283,10 @@ the images are labelled will append the new labels to the existing labeled datas **maDeepLabCut CRITICAL POINT:** For multi-animal labeling, unless you can tell apart the animals, you do not need to worry about the "ID" of each animal. For example: if you have a white and black mouse label the white mouse as animal 1, and black as animal 2 -across all frames. If two black mice, then the ID label 1 or 2 can switch between +across all frames. If two black mice, then the ID label 1 or 2 can switch between frames - no need for you to try to identify them (but always label consistently within a frame). If you have 2 black mice but one always has an optical fiber (for example), then -DO label them consistently as animal1 and animal_fiber (for example). The point of +DO label them consistently as animal1 and animal_fiber (for example). The point of multi-animal DLC is to train models that can first group the correct bodyparts to individuals, then associate those points in a given video to a specific individual, which then also uses temporal information to link across the video frames. @@ -294,7 +294,7 @@ which then also uses temporal information to link across the video frames. Note, we also highly recommend that you use more bodyparts that you might otherwise have (see the example below). -For more information, checkout the [napari-deeplabcut docs](napari-gui) for +For more information, checkout the [napari-deeplabcut docs](napari-gui) for more information about the labelling workflow. ### (E) Check Annotated Frames @@ -363,13 +363,13 @@ files contain meta information with regard to the parameters of the feature dete Key parameters are listed in Box 2. **DATA AUGMENTATION:** At this stage you can also decide what type of augmentation to -use. Once you've called `create_training_dataset`, you can edit the +use. Once you've called `create_training_dataset`, you can edit the [**pytorch_config.yaml**](dlc3-pytorch-config) file that was created (or for the TensorFlow engine, the [**pose_cfg.yaml**]( https://github.com/DeepLabCut/DeepLabCut/blob/master/deeplabcut/pose_cfg.yaml) file). - PyTorch Engine: [Albumentations](https://albumentations.ai/docs/) is used for data -augmentation. Look at the [**pytorch_config.yaml**](dlc3-pytorch-config) for more +augmentation. Look at the [**pytorch_config.yaml**](dlc3-pytorch-config) for more information about image augmentation options. - TensorFlow Engine: The default augmentation works well for most tasks (as shown on www.deeplabcut.org), but there are many options, more data augmentation, intermediate @@ -390,11 +390,11 @@ allows the use of batch processing even on small GPUs that could not otherwise a **MODEL COMPARISON**: You can also test several models by creating the same train/test split for different networks. -You can easily do this in the Project Manager GUI (by selecting the "Use an existing +You can easily do this in the Project Manager GUI (by selecting the "Use an existing data split" option), which also lets you compare PyTorch and TensorFlow models. ````{versionadded} 3.0.0 -You can now create new shuffles using the same train/test split as +You can now create new shuffles using the same train/test split as existing shuffles with `create_training_dataset_from_existing_split`. This allows you to compare model performance (between different architectures or when using different training hyper-parameters) as the shuffles were trained on the same data, and evaluated @@ -441,11 +441,11 @@ deeplabcut.train_network(config_path, shuffle=1) ``` The set of arguments in the function starts training the network for the dataset created -for one specific shuffle. Note that you can change training parameters in the +for one specific shuffle. Note that you can change training parameters in the [**pytorch_config.yaml**](dlc3-pytorch-config) file (or **pose_cfg.yaml** for TensorFlow models) of the model that you want to train (before you start training). -At user specified iterations during training checkpoints are stored in the subdirectory +At user specified iterations during training checkpoints are stored in the subdirectory *train* under the respective iteration & shuffle directory. ````{admonition} Tips on training models with the PyTorch Engine @@ -473,7 +473,7 @@ training image exactly once. So if you have 64 training images for your network, epoch is 64 iterations with batch size 1 (or 32 iterations with batch size 2, 16 with batch size 4, etc.). -By default, the pretrained networks are not in the DeepLabCut toolbox (as they can be +By default, the pretrained networks are not in the DeepLabCut toolbox (as they can be more than 100MB), but they get downloaded automatically before you train. If the user wishes to restart the training at a specific checkpoint they can specify the @@ -482,7 +482,7 @@ full path of the checkpoint to the variable ``resume_training_from`` in the [ dlc3-pytorch-config) file (checkout the "Restarting Training at a Specific Checkpoint" section of the docs) under the *train* subdirectory. -**CRITICAL POINT:** It is recommended to train the networks **until the loss plateaus** +**CRITICAL POINT:** It is recommended to train the networks **until the loss plateaus** (depending on the dataset, model architecture and training hyper-parameters this happens after 100 to 250 epochs of training). @@ -491,7 +491,7 @@ dlc3-pytorch-config) file allows the user to alter how often the loss is display and how often the weights are stored. We suggest saving every 5 to 25 epochs. ```` -````{admonition} Tips on training models with the TensorFlow Engine +````{admonition} Tips on training models with the TensorFlow Engine :class: dropdown Example parameters that one can call: @@ -511,10 +511,10 @@ deeplabcut.train_network( ) ``` -By default, the pretrained networks are not in the DeepLabCut toolbox (as they are +By default, the pretrained networks are not in the DeepLabCut toolbox (as they are around 100MB each), but they get downloaded before you train. However, if not previously downloaded from the TensorFlow model weights, it will be downloaded and stored in a -subdirectory *pre-trained* under the subdirectory *models* in +subdirectory *pre-trained* under the subdirectory *models* in *Pose_Estimation_Tensorflow*. At user specified iterations during training checkpoints are stored in the subdirectory *train* under the respective iteration directory. @@ -526,7 +526,7 @@ file under the *train* subdirectory (see Box 2). until the loss plateaus (typically around **500,000**) if you use batch size 1, and **50-100K** if you use batchsize 8 (the default). -If you use **maDeepLabCut** the recommended training iterations is **20K-100K** +If you use **maDeepLabCut** the recommended training iterations is **20K-100K** (it automatically stops at 200K!), as we use Adam and batchsize 8; if you have to reduce the batchsize for memory reasons then the number of iterations needs to be increased. @@ -550,15 +550,15 @@ data. The bonus, training time is much less!!! ### (H) Evaluate the Trained Network -It is important to evaluate the performance of the trained network. This performance is +It is important to evaluate the performance of the trained network. This performance is measured by computing two metrics: - **Average root mean square error** (RMSE) between the manual labels and the ones predicted by your trained DeepLabCut model. The RMSE is proportional to the mean average -Euclidean error (MAE) between the manual labels and the ones predicted by DeepLabCut. +Euclidean error (MAE) between the manual labels and the ones predicted by DeepLabCut. The MAE is displayed for all pairs and only likely pairs (>p-cutoff). This helps to exclude, for example, occluded body parts. One of the strengths of DeepLabCut is that -due to the probabilistic output of the scoremap, it can, if sufficiently trained, also +due to the probabilistic output of the scoremap, it can, if sufficiently trained, also reliably report if a body part is visible in a given frame. (see discussions of finger tips in reaching and the Drosophila legs during 3D behavior in [Mathis et al, 2018]). - **Mean Average Precision** (mAP) and **Mean Average Recall** (mAR) for the individuals @@ -577,17 +577,17 @@ However, the notion of "correct prediction" for pose estimation is not straightf is a prediction correct if all predicted keypoints are within 5 pixels of the ground truth? Within 2 pixels of the ground truth? What if all pixels but one match the ground truth perfectly, but the wrong prediction is 50 pixels away? Mean average precision ( -and mean average recall) estimate the precision/recall of your models by setting +and mean average recall) estimate the precision/recall of your models by setting different "thresholds of correctness" and averaging results. How "correct" a prediction is can be evaluated through [object-keypoint similarity]( https://cocodataset.org/#keypoints-eval). A good resource to get a deeper understanding of mAP is the [Stanford CS230 course]( -https://cs230.stanford.edu/section/8/#object-detection-iou-ap-and-map). While it -describes mAP for object detection (where bounding boxes are predicted instead of -keypoints), the same metric can be computed for pose estimation, where similarity +https://cs230.stanford.edu/section/8/#object-detection-iou-ap-and-map). While it +describes mAP for object detection (where bounding boxes are predicted instead of +keypoints), the same metric can be computed for pose estimation, where similarity between predictions and ground truth is computed through [object-keypoint similarity]( -https://cocodataset.org/#keypoints-eval) instead of intersection-over-union (IoU). +https://cocodataset.org/#keypoints-eval) instead of intersection-over-union (IoU). ``` It's also important to visually inspect predictions on individual frames to assess the @@ -641,11 +641,11 @@ plotted as plus (‘+’), DeepLabCut’s predictions either as ‘.’ (for con ’x’ for (likelihood <= `pcutoff`). The evaluation results for each shuffle of the training dataset are stored in a unique -subdirectory in a newly created directory ‘evaluation-results-pytorch’ (or +subdirectory in a newly created directory ‘evaluation-results-pytorch’ (or ‘evaluation-results’ for TensorFlow models) in the project directory. The user can visually inspect if the distance between the labeled and the predicted body parts are acceptable. In the event of benchmarking with different shuffles of same training -dataset, the user can provide multiple shuffle indices to evaluate the corresponding +dataset, the user can provide multiple shuffle indices to evaluate the corresponding network. If the generalization is not sufficient, the user might want to: • check if the labels were imported correctly; i.e., invisible points are not labeled @@ -692,7 +692,7 @@ COLAB notebook. **-------------------- DECISION POINT -------------------** **ATTENTION!** -**Pose estimation and tracking should be thought of as separate steps.** If you do not +**Pose estimation and tracking should be thought of as separate steps.** If you do not have good pose estimation evaluation metrics at this point, stop, check original labels, add more data, etc --> don't move forward with this model. If you think you have a good model, please test the "raw" pose estimation performance on a video to validate @@ -710,14 +710,14 @@ Please note that you do **not** get the .h5/csv file you might be used to gettin comes after tracking). You will get a `pickle` file that is used in `create_video_with_all_detections`. -For models predicting part-affinity fields, another sanity check may be to +For models predicting part-affinity fields, another sanity check may be to examine the distributions of edge affinity costs using `deeplabcut.utils.plot_edge_affinity_distributions`. Easily separable distributions indicate that the model has learned strong links to group keypoints into distinct individuals — likely a necessary feature for the assembly stage (note that the amount of overlap will also depend on the amount of interactions between your animals in the dataset). All TensorFlow multi-animal models use part-affinity fields and PyTorch models consisting of just a backbone name (e.g. `resnet_50`, `resnet_101`) use part-affinity -fields. If you're unsure whether your PyTorch model has a one, check +fields. If you're unsure whether your PyTorch model has a one, check the **pytorch_config.yaml** for a `DLCRNetHead`. IF you have good clean out video, ending in `....full.mp4` (and the evaluation metrics @@ -773,7 +773,7 @@ the workflow (ideal for advanced users). ### IF auto_track = False: You can validate the tracking parameters. Namely, you can iteratively change the -parameters, run `convert_detections2tracklets` then load them in the GUI +parameters, run `convert_detections2tracklets` then load them in the GUI (`refine_tracklets`) if you want to look at the performance. If you want to edit these, you will need to open the `inference_cfg.yaml` file (or click button in GUI). The options are: diff --git a/docs/pytorch/Benchmarking_shuffle_guide.md b/docs/pytorch/Benchmarking_shuffle_guide.md index 8e4554ce77..aacd9ba84a 100644 --- a/docs/pytorch/Benchmarking_shuffle_guide.md +++ b/docs/pytorch/Benchmarking_shuffle_guide.md @@ -2,23 +2,23 @@ ## Reasoning for benchmarking models in DLC (across DLC versions and architectures) -DeepLabCut 3.0+ introduced using PyTorch 🔥 as a deep learning engine (and TensorFlow will be depreciated). +DeepLabCut 3.0+ introduced using PyTorch 🔥 as a deep learning engine (and TensorFlow will be depreciated). It is of importance for replicability of data analysis to benchmark existing models created using DeepLabCut versions prior to 3.0 against new models created in DeepLabCut 3.0+ and later versions. -When comparing different models, it's important to use the same train-test data -split to ensure fair comparisons. If the models are trained on different datasets, -their performance metrics can't be accurately compared. This is crucial when -comparing the performance of models with different architectures or different -sets of hyperparameters. For example, if we compare the RMSE of a model on an -"easy" test image with the RMSE of another model on a "hard" test image, it -doesn't determine whether a model is better than the other because the -architecture performs better or because the training images were "better" to -learn from. Thus, we not only need to compare the models based on metrics -computed on the same test images, but also train them on an identical fixed +When comparing different models, it's important to use the same train-test data +split to ensure fair comparisons. If the models are trained on different datasets, +their performance metrics can't be accurately compared. This is crucial when +comparing the performance of models with different architectures or different +sets of hyperparameters. For example, if we compare the RMSE of a model on an +"easy" test image with the RMSE of another model on a "hard" test image, it +doesn't determine whether a model is better than the other because the +architecture performs better or because the training images were "better" to +learn from. Thus, we not only need to compare the models based on metrics +computed on the same test images, but also train them on an identical fixed training set in order to "decouple" the dataset from the model architecture. -Creating a model using the same data split can be carried out using a GUI or +Creating a model using the same data split can be carried out using a GUI or using code, and this guide outlines the steps for both. ## Important files & folders @@ -30,7 +30,7 @@ dlc-project | |__ iterationX | |__ shuffleX | |__ pytorch_config.yaml -| +| |___training-datasets | |__ metadata.yaml | @@ -45,7 +45,7 @@ Creating a new shuffle with the same train/test split as an existing one: ### In the DeepLabCut GUI 1. Front page > Load project > Open project folder > choose *config.yaml* 2. Select *'Create training dataset'* tab -3. Tick *Use an existing data split* option +3. Tick *Use an existing data split* option ![create_from_existing]() 4. Click 'View existing shuffles': @@ -54,7 +54,7 @@ Creating a new shuffle with the same train/test split as an existing one: - train_fraction: The fraction of the dataset used for training. - index: The index of the shuffle. - split: The data split for the shuffle. The integer value on its own does not -hold any meaning, but this "split" value indicates which shuffles have the same split +hold any meaning, but this "split" value indicates which shuffles have the same split (as their results can then be compared) - engine: Whether it is a PyTorch or TensorFlow shuffle @@ -62,15 +62,15 @@ hold any meaning, but this "split" value indicates which shuffles have the same 5. Choose the index of the training shuffle to replicate. Let us assume we want to replicate the train-test split from OpenfieldOct30-trainset95shuffle3, in which `split: 3`. In this case, we insert in the *'From shuffle'* menu - + ![choose_existing_index]() 6. To create this new dataset, set the shuffle option to an un-used shuffle (here 4) - + ![choose_new_index]() -7. Click *'Create training dataset'* and move on to *'train network'*. Shuffle should be +7. Click *'Create training dataset'* and move on to *'train network'*. Shuffle should be set to the new shuffle entered at the previous step (in this case, 4) - + ![create_from_existing]() 8. To view/edit the specifications of the model you created, you can go to `pytoch_config.yaml` file at: ``` @@ -82,7 +82,7 @@ set to the new shuffle entered at the previous step (in this case, 4) |__ pytorch_config.yaml ``` -### In Code +### In Code With the `deeplabcut` module in Python, use the `create_training_dataset_from_existing_split()` method to create new shuffles from @@ -121,15 +121,15 @@ Now, we can compare performances with peace of mind! ### Good practices: naming shuffles created from existing ones -In a setting where one has multiple TensorFlow models and intends to benchmark -their performances against new PyTorch models, it is good practice to follow +In a setting where one has multiple TensorFlow models and intends to benchmark +their performances against new PyTorch models, it is good practice to follow a naming pattern for the shuffles we create. -Say we have TensorFlow shuffles 0, 1, and 2. We can create new PyTorch shuffles -from them by naming them 1000, 1001, and 1002. This allows us to quickly -recognize that the shuffles belonging to the 100x range are PyTorch shuffles -and that shuffle 1001, for example, has the same data split as TensorFlow -shuffle 1. This way, the comparison can be more straightforward and guaranteed +Say we have TensorFlow shuffles 0, 1, and 2. We can create new PyTorch shuffles +from them by naming them 1000, 1001, and 1002. This allows us to quickly +recognize that the shuffles belonging to the 100x range are PyTorch shuffles +and that shuffle 1001, for example, has the same data split as TensorFlow +shuffle 1. This way, the comparison can be more straightforward and guaranteed to be correct! This was contributed by the [2024 DLC AI Residents](https://www.deeplabcutairesidency.org/our-team)! diff --git a/docs/pytorch/architectures.md b/docs/pytorch/architectures.md index c1742b8221..823e344eda 100644 --- a/docs/pytorch/architectures.md +++ b/docs/pytorch/architectures.md @@ -20,7 +20,7 @@ print(available_detectors()) ## Neural Networks Architectures Several architectures are currently implemented in DeepLabCut PyTorch (more will come, -and you can add more easily in our new model registry). Also check out the explanations of bottom-up/top-down below. +and you can add more easily in our new model registry). Also check out the explanations of bottom-up/top-down below. **ResNets** - Adapted from [He, Kaiming, et al. "Deep residual learning for image recognition." Proceedings of the IEEE conference on Computer Vision and Pattern Recognition. 2016.](https://openaccess.thecvf.com/content_cvpr_2016/html/He_Deep_Residual_Learning_CVPR_2016_paper.html) and [Insafutdinov, Eldar et al. "DeeperCut: A Deeper, Stronger, and Faster Multi-Person Pose Estimation Model". European Conference on Computer Vision (ECCV) 2016.] @@ -29,7 +29,7 @@ and you can add more easily in our new model registry). Also check out the expla **HRNet** - Adapted from [Wang, Jingdong, et al. "Deep high-resolution representation learning for visual recognition." IEEE transactions on pattern analysis and machine intelligence 43.10 (2020): 3349-3364.](https://arxiv.org/abs/1908.07919) -- Current variants are `hrnet_w18`, `hrnet_w32`, `hrnet_w48`, +- Current variants are `hrnet_w18`, `hrnet_w32`, `hrnet_w48`, - Current top-down variants are `top_down_hrnet_w18`, `top_down_hrnet_w32`, `top_down_hrnet_w48` - Slower but typically more powerful than ResNets @@ -62,20 +62,20 @@ and you can add more easily in our new model registry). Also check out the expla ## Information on Single Animal Models -Single-animal models are composed of a backbone (encoder) and a head (decoder) +Single-animal models are composed of a backbone (encoder) and a head (decoder) predicting the position of keypoints. The default head contains a single deconvolutional layer. To create the single animal model composed of a backbone and head, you can call -`deeplabcut.create_training_dataset` with `net_type` set to the backbone name (e.g. +`deeplabcut.create_training_dataset` with `net_type` set to the backbone name (e.g. `resnet_50` or `hrnet_w32`). -If you want to add a second deconvolutional layer (which will make your model slower, +If you want to add a second deconvolutional layer (which will make your model slower, but it might improve performance), you can simply edit your `pytorch_config.yaml` file. Of course, any multi-animal model can also be used for single-animal projects! ## Approaches to Multi-Animal pose estimation -Single-animal pose estimation is quite straightforward: the model takes an image as +Single-animal pose estimation is quite straightforward: the model takes an image as input, and it outputs the predicted coordinate of each bodypart. Multi-animal pose estimation is more complex. Not only do you need to localize bodyparts @@ -91,38 +91,38 @@ same animal). ![Schema representing the bottom-up approach to pose estimation]( assets/bottom-up-approach.png) -### Backbones with Part-Affinity Fields +### Backbones with Part-Affinity Fields -As in DeepLabCut 2.X, the base multi-animal model is composed of a backbone (encoder) -and a head predicting keypoints and part-affinity fields (PAFs). These PAFs are used to +As in DeepLabCut 2.X, the base multi-animal model is composed of a backbone (encoder) +and a head predicting keypoints and part-affinity fields (PAFs). These PAFs are used to assemble keypoints for individuals. -Passing a backbone as a net type (e.g., `resnet_50`, `hrnet_w32`) for a multi-animal +Passing a backbone as a net type (e.g., `resnet_50`, `hrnet_w32`) for a multi-animal project will create a model consisting of a backbone and a heatmap + PAF head. ### Top-down estimation -The second approach, **top-down** pose estimation, uses a two-step approach. A first +The second approach, **top-down** pose estimation, uses a two-step approach. A first model (an object detector) is used to localize every animal present in the image through its bounding box. Then, the pose for each animal is determined by predicting bodyparts -in each bounding box. The pose estimation +in each bounding box. The pose estimation ![Schema representing the top-down approach to pose estimation]( assets/top-down-approach.png) -The top-down approach tends to be more accurate in less crowded scenes, as the pose -model only needs to process the pixels related to a single animal. However, in more -crowded scenes, the pose estimation task becomes ambiguous. Multiple overlapping -individuals will have very similar bounding boxes, and the pose model has no way of +The top-down approach tends to be more accurate in less crowded scenes, as the pose +model only needs to process the pixels related to a single animal. However, in more +crowded scenes, the pose estimation task becomes ambiguous. Multiple overlapping +individuals will have very similar bounding boxes, and the pose model has no way of knowing which animal it is supposed to predict keypoints for. The bottom-up approach does not have this ambiguïty, and also has the advantage of -only needing to run a pose estimation model, instead of needing to run an object +only needing to run a pose estimation model, instead of needing to run an object detector first. However, grouping keypoints is a difficult problem. Hence any single-animal model can be transformed into a top-down, multi-animal model. To -do so, simply prefix `top_down` to your single-animal model name. Currently, the +do so, simply prefix `top_down` to your single-animal model name. Currently, the following detectors are available: `ssdlite`, `fasterrcnn_mobilenet_v3_large_fpn`, `fasterrcnn_resnet50_fpn_v2`. @@ -130,15 +130,15 @@ following detectors are available: `ssdlite`, `fasterrcnn_mobilenet_v3_large_fpn ### Hybrid, Bottom-up (BU) plus a ``conditioned" Top-down (CTD) A new approach to pose estimation, named bottom-up conditioned top-down (or **BUCTD**), was -introduced in [Zhou, Stoffl, Mathis, Mathis. "Rethinking Pose Estimation in Crowds: -Overcoming the Detection Information Bottleneck and Ambiguity." Proceedings of the +introduced in [Zhou, Stoffl, Mathis, Mathis. "Rethinking Pose Estimation in Crowds: +Overcoming the Detection Information Bottleneck and Ambiguity." Proceedings of the IEEE/CVF International Conference on Computer Vision (ICCV). 2023]( https://openaccess.thecvf.com/content/ICCV2023/papers/Zhou_Rethinking_Pose_Estimation_in_Crowds_Overcoming_the_Detection_Information_Bottleneck_ICCV_2023_paper.pdf) . It's a hybrid two-stage approach leveraging the strengths of the bottom-up and top-down approaches to overcome the ambiguïty introduced through bounding boxes. Instead -of using an object detection model to localize individuals, it uses a bottom-up pose +of using an object detection model to localize individuals, it uses a bottom-up pose estimation model. The predictions made by the bottom-up model are given as proposals (or -_conditions_) to the pose estimation model. This is illustrated in the figure below. In modern language, one could state that CTD models are "pose-promptable". +_conditions_) to the pose estimation model. This is illustrated in the figure below. In modern language, one could state that CTD models are "pose-promptable". ![BUCTD](https://github.com/amathislab/BUCTD/raw/main/media/BUCTD_fig1.png) diff --git a/docs/pytorch/pytorch_config.md b/docs/pytorch/pytorch_config.md index 8d75e3947e..91c0075b0d 100644 --- a/docs/pytorch/pytorch_config.md +++ b/docs/pytorch/pytorch_config.md @@ -2,12 +2,12 @@ # The PyTorch Configuration file The `pytorch_config.yaml` file specifies the configuration for your PyTorch pose models, -from the model architecture to which optimizer will be used for training, how training +from the model architecture to which optimizer will be used for training, how training runs will be logged, the data augmentation that will be applied and which metric should -be used to save the "best" model snapshot. +be used to save the "best" model snapshot. -You can create default configurations for a shuffle using -`deeplabcut.create_training_set` or `deeplabcut.create_training_model_comparison`. This +You can create default configurations for a shuffle using +`deeplabcut.create_training_set` or `deeplabcut.create_training_model_comparison`. This will create a `pytorch_config.yaml` file for your selected net type. The basic structure of the file is as follows: @@ -36,10 +36,10 @@ resume_training_from: # optional: restart the training at the specific checkpoi There are a few singleton parameters defined in the PyTorch configuration file: -- `device`: The device to use for training/inference. The default is `auto`, which sets -the device to `cuda` if an NVIDIA GPU is available, and `cpu` otherwise. For users +- `device`: The device to use for training/inference. The default is `auto`, which sets +the device to `cuda` if an NVIDIA GPU is available, and `cpu` otherwise. For users running models on macOS with an M1/M2/M3 chip, this is set to `mps` for certain models -(not all operations are currently supported on Apple GPUs - so some models like HRNets +(not all operations are currently supported on Apple GPUs - so some models like HRNets need to be trained on CPU, while others like ResNets can take advantage of the GPU). - `method`: Either `bu` for bottom-up models, or `td` for top-down models. - `net_type`: The type of pose model configured by the file (e.g. `resnet_50`). @@ -62,7 +62,7 @@ The default configuration for a pose model is: data: bbox_margin: 20 colormode: RGB # should never be changed - inference: # the augmentations to apply to images during inference + inference: # the augmentations to apply to images during inference normalize_images: true # this should always be set to true train: affine: @@ -73,7 +73,7 @@ data: covering: true crop_sampling: width: 448 # if your images are very small or very large, you may need to edit! - height: 448 # see below for more information about crop_sampling! + height: 448 # see below for more information about crop_sampling! max_shift: 0.1 method: hybrid gaussian_noise: 12.75 @@ -84,7 +84,7 @@ data: The following transformations are available for the `train` and `inference` keys. **Affine**: Applies an affine (rotation, translation, scaling) transformation to the -images. +images. ```yaml affine: @@ -94,12 +94,12 @@ affine: translation: 40 # int: the maximum translation to apply to images (in pixels) ``` -**Auto-Padding**: Pads the image to some desired shape (e.g., a minimum height/width or +**Auto-Padding**: Pads the image to some desired shape (e.g., a minimum height/width or such that the height/width are divisible by a given number). Some backbones (such as HRNets) require the height and width of images to be multiples of 32. Setting up auto-padding with `pad_height_divisor: 32` and `pad_width_divisor: 32` ensures that is the case. Note that **not all keys need to be set**! The values shown are the default -values. Only one of 'min_height' and 'pad_height_divisor' parameters must be set, and +values. Only one of 'min_height' and 'pad_height_divisor' parameters must be set, and only one of 'min_width' and 'pad_width_divisor' parameters must be set. ```yaml @@ -116,7 +116,7 @@ auto_padding: **Covering**: Based on Albumentations's [CoarseDropout]( https://albumentations.ai/docs/api_reference/augmentations/dropout/coarse_dropout/#albumentations.augmentations.dropout.coarse_dropout) -augmentation, this "cuts" holes out of the image. As defined in +augmentation, this "cuts" holes out of the image. As defined in [Improved Regularization of Convolutional Neural Networks with Cutout]( https://arxiv.org/abs/1708.04552). @@ -124,23 +124,23 @@ https://arxiv.org/abs/1708.04552). covering: true # bool: if true, applies a coarse dropout with probability 50% ``` -**Gaussian Noise**: Applies gaussian noise to the input image. Can either be a float -(the standard deviation of the noise) or simply a boolean (the standard deviation of +**Gaussian Noise**: Applies gaussian noise to the input image. Can either be a float +(the standard deviation of the noise) or simply a boolean (the standard deviation of the noise will be set as 12.75). ```yaml gaussian_noise: 12.75 # bool, float: add gaussian noise ``` -**Horizontal Flips**: This flips the image horizontally around the y-axis. As the +**Horizontal Flips**: This flips the image horizontally around the y-axis. As the resulting image is mirrored, it does not preserve labels (the left hand would become the right hand, and vice versa). This augmentation should not be used for pose models if you have symmetric keypoints! However, it is safe to use it to train detectors. If you want -to use horizontal flips with symmetric keypoints, you need to specify them through the +to use horizontal flips with symmetric keypoints, you need to specify them through the `symmetries` parameter! ```yaml -# augmentation for object detectors or when no symmetric (left/right) keypoints exist: +# augmentation for object detectors or when no symmetric (left/right) keypoints exist: hflip: true # augmentation if your bodyparts are [snout, eye_L, eye_R, ear_L, ear_R] @@ -170,29 +170,29 @@ normalize_images: true # normalizes images ### Dealing with Variable Image Sizes ```{NOTE} -When training with batch size 1 (or if all images in your dataset have the same size), +When training with batch size 1 (or if all images in your dataset have the same size), you don't need to worry about any of this! However, you can still use `crop_sampling` which may help your model generalize. ``` When training with a batch size greater than 1, all images in a batch **must** have the -same size. PyTorch **collates** all images into one tensor of shape `[b, c, h, w]`, -where `b` is the batch size, `c` the number of channels in the image, `h` and `w` the +same size. PyTorch **collates** all images into one tensor of shape `[b, c, h, w]`, +where `b` is the batch size, `c` the number of channels in the image, `h` and `w` the height and width of images in the batches. There are a few different ways to ensure that all images in a batch have the same size: 1. **Crop sampling**. This is the default behavior for the PyTorch engine in DeepLabCut. -A part of each image (of a fixed size) is cropped and given to the model to train. See +A part of each image (of a fixed size) is cropped and given to the model to train. See below for more information. 2. **A custom collate function**. Collate functions define a way that images of different sizes can be combined into one tensor. This involves resizing and padding images to the same size and aspect ratio. Available collate functions are defined in -`deeplabcut/pose_estimation_pytorch/data/collate.py`. +`deeplabcut/pose_estimation_pytorch/data/collate.py`. 3. **Resizing all images**. All images can simply be resized to the same size. This usually doesn't lead to the best performance. **Resizing - Crop Sampling**: An alternative way to ensure all images have the same size -is through cropping. The `crop_sampling` crops images down to a maximum width and +is through cropping. The `crop_sampling` crops images down to a maximum width and height, with options to sample the center of the crop according to the positions of the keypoints. The methods to sample the center of the crop are as follows: @@ -203,20 +203,20 @@ keypoints. The methods to sample the center of the crop are as follows: ```yaml crop_sampling: - height: 400 # int: the height of the crop - width: 400 # int: the height of the crop + height: 400 # int: the height of the crop + width: 400 # int: the height of the crop max_shift: 0.4 # float: maximum allowed shift of the cropping center position as a fraction of the crop size. - method: hybrid # str: the center sampling method (one of 'uniform', 'keypoints', 'density', 'hybrid') + method: hybrid # str: the center sampling method (one of 'uniform', 'keypoints', 'density', 'hybrid') ``` **Collate**: Defines how images are collated into batches. The default way collate function to use is `ResizeFromDataSizeCollate` (other collate functions are defined in `deeplabcut/pose_estimation_pytorch/data/collate.py`). For each batch to collate, this implementation: -1. Selects the target width & height all images will be resized to by getting the size -of the first image in the batch, and multiplying it by a scale sampled uniformly at +1. Selects the target width & height all images will be resized to by getting the size +of the first image in the batch, and multiplying it by a scale sampled uniformly at random from `(min_scale, max_scale)`. -2. Resizes all images in the batch (while preserving their aspect ratio) such that they +2. Resizes all images in the batch (while preserving their aspect ratio) such that they are the smallest size such that the target size fits entirely in the image. 3. Crops each resulting image into the target size with a random crop. @@ -226,15 +226,15 @@ collate: # rescales the images when putting them in a batch max_shift: 10 # the maximum shift, in pixels, to add to the random crop (this means # there can be a slight border around the image) max_size: 1024 # the maximum size of the long edge of the image when resized. If the - # longest side will be greater than this value, resizes such that the longest side - # is this size, and the shortest side is smaller than the desired size. This is + # longest side will be greater than this value, resizes such that the longest side + # is this size, and the shortest side is smaller than the desired size. This is # useful to keep some information from images with extreme aspect ratios. min_scale: 0.4 # the minimum scale to resize the image with max_scale: 1.0 # the maximum scale to resize the image with min_short_side: 128 # the minimum size of the target short side max_short_side: 1152 # the maximum size of the target short side multiple_of: 32 # pads the target height, width such that they are multiples of 32 - to_square: false # instead of using the aspect ratio of the first image, only the + to_square: false # instead of using the aspect ratio of the first image, only the # short side of the first image will be used to sample a "side", and the images will # be cropped in squares ``` @@ -251,13 +251,13 @@ resize: ### Model -The model configuration is further split into a `backbone`, optionally a `neck` and a +The model configuration is further split into a `backbone`, optionally a `neck` and a number of heads. -Changing the `model` configuration should only be done by expert users, and in rare -occasions. When updating a model configuration (e.g. adding more deconvolution layers -to a `HeatmapHead`) must be done in a way where the model configuration still makes -sense for the project (e.g. the number of heatmaps output needs to match the number of +Changing the `model` configuration should only be done by expert users, and in rare +occasions. When updating a model configuration (e.g. adding more deconvolution layers +to a `HeatmapHead`) must be done in a way where the model configuration still makes +sense for the project (e.g. the number of heatmaps output needs to match the number of bodyparts in the project). An example model configuration for a single-animal HRNet would look something like: @@ -284,12 +284,12 @@ model: The `backbone`, `neck` and `head` configurations are loaded using the `deeplabcut.pose_estimation_pytorch.models.backbones.base.BACKBONES`, -`deeplabcut.pose_estimation_pytorch.models.necks.base.NECKS` and -`deeplabcut.pose_estimation_pytorch.models.heads.base.HEADS` registries. You specify +`deeplabcut.pose_estimation_pytorch.models.necks.base.NECKS` and +`deeplabcut.pose_estimation_pytorch.models.heads.base.HEADS` registries. You specify which type to load with the `type` parameter. Any argument for the head can then be used in the configuration. -So to use an `HRNet` backbone for your model (as defined in +So to use an `HRNet` backbone for your model (as defined in `deeplabcut.pose_estimation_pytorch.models.backbones.hrnet.HRNet`), you could set: ```yaml @@ -298,7 +298,7 @@ model: type: HRNet model_name: hrnet_w32 # creates an HRNet W32 pretrained: true # the backbone weights for training will be loaded from TIMM (pre-trained on ImageNet) - interpolate_branches: false # don't interpolate & concatenate channels from all branches + interpolate_branches: false # don't interpolate & concatenate channels from all branches increased_channel_count: true # use the incre_modules defined in the TIMM HRNet backbone_output_channels: 128 # number of channels output by the backbone ``` @@ -306,7 +306,7 @@ model: ### Runner The runner contains elements relating to the training runner to use (including the optimizer and -learning rate schedulers). Unless you're experienced with machine learning and training +learning rate schedulers). Unless you're experienced with machine learning and training models **it is not recommended to change the optimizer or scheduler**. ```yaml @@ -322,26 +322,26 @@ runner: load_scheduler_state_dict: true/false # whether to load scheduler state when resuming training from a snapshot, snapshots: # parameters for the TorchSnapshotManager max_snapshots: 5 # the maximum number of snapshots to save (the "best" model does not count as one of them) - save_epochs: 25 # the interval between each snapshot save + save_epochs: 25 # the interval between each snapshot save save_optimizer_state: false # whether the optimizer state should be saved with the model snapshots (very little reason to set to true) gpus: # GPUs to use to train the network - 0 - 1 ``` -**Key metric**: Every time the model is evaluated on the test set, metrics are computed -to see how the model is performing. The key metric is used to determine whether the -current model is the "best" so far. If it is, the snapshot is saved as `...-best.pt`. +**Key metric**: Every time the model is evaluated on the test set, metrics are computed +to see how the model is performing. The key metric is used to determine whether the +current model is the "best" so far. If it is, the snapshot is saved as `...-best.pt`. For pose models, metrics to choose from would be `test.mAP` (with `key_metric_asc: true` -) or `test.rmse` (with `key_metric_asc: false`). +) or `test.rmse` (with `key_metric_asc: false`). **Evaluation interval**: Evaluation slows down training (it takes time to go through all -the evaluation images, make predictions and log results!). So instead of evaluating +the evaluation images, make predictions and log results!). So instead of evaluating after every epoch, you could decide to evaluate every 5 epochs (by setting -`eval_interval: 5`). While this means you get coarser information about how your model +`eval_interval: 5`). While this means you get coarser information about how your model is training, it can speed up training on large datasets. -**Optimizer**: Any optimizer inheriting `torch.optim.Optimizer`. More information about +**Optimizer**: Any optimizer inheriting `torch.optim.Optimizer`. More information about optimizers can be found in [PyTorch's documentation]( https://pytorch.org/docs/stable/optim.html). Examples: @@ -364,8 +364,8 @@ https://pytorch.org/docs/stable/optim.html). Examples: **Scheduler**: You can use [any scheduler]( https://pytorch.org/docs/stable/optim.html#how-to-adjust-learning-rate) defined in -`torch.optim.lr_scheduler`, where the arguments given are arguments of the scheduler. -The default scheduler is an LRListScheduler, which changes the learning rates at each +`torch.optim.lr_scheduler`, where the arguments given are arguments of the scheduler. +The default scheduler is an LRListScheduler, which changes the learning rates at each milestone to the corresponding values in `lr_list`. Examples: ```yaml @@ -385,7 +385,7 @@ milestone to the corresponding values in `lr_list`. Examples: gamma: 0.1 ``` -You can also use schedulers that use other schedulers as parameters, such as a +You can also use schedulers that use other schedulers as parameters, such as a [`ChainedScheduler`]( https://pytorch.org/docs/stable/generated/torch.optim.lr_scheduler.ChainedScheduler.html) or a [`SequentialLR`]( @@ -425,19 +425,19 @@ warmup epochs, and a second scheduler later. An example usage would be: ### Train Settings -The `train_settings` key contains parameters that are specific to training. For more +The `train_settings` key contains parameters that are specific to training. For more information about the `dataloader_workers` and `dataloader_pin_memory` settings, see [Single- and Multi-process Data Loading]( https://pytorch.org/docs/stable/data.html#single-and-multi-process-data-loading) and [memory pinning](https://pytorch.org/docs/stable/data.html#memory-pinning). Setting `dataloader_workers: 0` uses single-process data loading, while setting it to 1 or more -will use multi-process data loading. You should always keep -`dataloader_pin_memory: true` when training on an NVIDIA GPU. +will use multi-process data loading. You should always keep +`dataloader_pin_memory: true` when training on an NVIDIA GPU. ```yaml train_settings: batch_size: 1 # the batch size used for training - dataloader_workers: 0 # the number of workers for the PyTorch Dataloader + dataloader_workers: 0 # the number of workers for the PyTorch Dataloader dataloader_pin_memory: true # pin DataLoader memory display_iters: 500 # the number of iterations (steps) between each log print epochs: 200 # the maximum number of epochs for which to train the model @@ -446,11 +446,11 @@ train_settings: ### Logger -Training runs are logged to the model folder (where the snapshots are stored) by +Training runs are logged to the model folder (where the snapshots are stored) by default. Additionally, you can log results to [Weights and Biases](https://wandb.ai/site), by adding a -`WandbLogger`. Just make sure you're logged in to your `wandb` account before starting +`WandbLogger`. Just make sure you're logged in to your `wandb` account before starting your training run (with `wandb login` from your shell). For more information, see their [tutorials](https://docs.wandb.ai/tutorials) and their documentation for [`wandb.init`](https://docs.wandb.ai/ref/python/init). @@ -465,19 +465,19 @@ logger: ... # any other argument you can pass to `wandb.init`, such as `tags: ["dekr", "split=0"]` ``` -If you set up a `WandbLogger`, the corresponding run info (`entity`, `project`, `run_id`) -will be saved in a `wandb_info.yaml` file in the model train directory, so that the WandB run +If you set up a `WandbLogger`, the corresponding run info (`entity`, `project`, `run_id`) +will be saved in a `wandb_info.yaml` file in the model train directory, so that the WandB run can be easily be recovered at a later stage. -You can also log images as they are seen by the model to `wandb` -with the `image_log_interval`. This logs a random train and test image, as well as the +You can also log images as they are seen by the model to `wandb` +with the `image_log_interval`. This logs a random train and test image, as well as the targets and heatmaps for that image. ### Restarting Training at a Specific Checkpoint If you wish to restart the training at a specific checkpoint, you can specify the -full path of the checkpoint to the `resume_training_from` variable, as shown below. In this -example, `snapshot-010.pt` will be loaded before training starts, and the model will +full path of the checkpoint to the `resume_training_from` variable, as shown below. In this +example, `snapshot-010.pt` will be loaded before training starts, and the model will continue to train from the 10th epoch on. ```yaml @@ -487,10 +487,10 @@ continue to train from the 10th epoch on. resume_training_from: /Users/john/dlc-project-2021-06-22/dlc-models-pytorch/iteration-0/dlcJun22-trainset95shuffle0/train/snapshot-010.pt ``` -When continuing to train a model, you may want to modify the learning rate scheduling +When continuing to train a model, you may want to modify the learning rate scheduling that was being used (by editing the configuration under the `scheduler` key). When doing -so, you *must set `load_scheduler_state_dict: false`* in your `runner` config! -Otherwise, the parameters for the scheduler your started training with will be loaded +so, you *must set `load_scheduler_state_dict: false`* in your `runner` config! +Otherwise, the parameters for the scheduler your started training with will be loaded from the state dictionary, and your edits might not be kept! ### Inference @@ -538,7 +538,7 @@ inference: ## Training Top-Down Models Top-down models are split into two main elements: a detector (localizing individuals in -the images) and a pose model predicting each individual's pose (once localization is +the images) and a pose model predicting each individual's pose (once localization is done, obtaining pose is just like getting pose in a single-animal model!). The "pose" part of the model configuration is exactly the same as for single-animal or @@ -548,7 +548,7 @@ configuration. ### Detector Configuration -When training top-down models, you also need to configure how the detector will be +When training top-down models, you also need to configure how the detector will be trained. All information relating to the detector is placed under the `detector` key. ```yaml @@ -578,9 +578,9 @@ detector: ``` Currently, the only detectors available are `FasterRCNN` and `SSDLite`. However, multiple variants of -`FasterRCNN` are available (you can view the different variants on -[torchvision's object detection page](https://pytorch.org/vision/stable/models.html#object-detection)). It's recommended to use the fastest -detector that brings enough performance. The recommended variants are the following +`FasterRCNN` are available (you can view the different variants on +[torchvision's object detection page](https://pytorch.org/vision/stable/models.html#object-detection)). It's recommended to use the fastest +detector that brings enough performance. The recommended variants are the following (from fastest to most powerful, taken from torchvision's documentation): | name | Box MAP (larger = more powerful) | Params (larger = more powerful) | GFLOPS (larger = slower) | @@ -606,7 +606,7 @@ detector: resume_training_from: /Users/john/dlc-project-2021-06-22/dlc-models-pytorch/iteration-0/dlcJun22-trainset95shuffle0/train/snapshot-detector-020.pt ``` -When continuing to train a detector, you may want to modify the learning rate scheduling +When continuing to train a detector, you may want to modify the learning rate scheduling that was being used (by editing the configuration under the `scheduler` key). When doing so, you *must set `load_scheduler_state_dict: false`* in your `detector`: `runner` config! Otherwise, the parameters for the scheduler your started training with will be @@ -619,10 +619,10 @@ To train object detection models (for top-down pose estimation), ground truth bo boxes are needed. As they are not annotated in DeepLabCut, they are generated from the ground truth pose: simply take the minimum and maximum for the x and y axes, add a small margin and you have your bounding box! The default setting adds a margin of 20 pixels -around the pose. This works well in most cases, but in some cases you should update this +around the pose. This works well in most cases, but in some cases you should update this value (e.g. when you have very small or large images). -You can edit that value in the `pytorch_config.yaml` for your model through the +You can edit that value in the `pytorch_config.yaml` for your model through the `data: bbox_margin` parameter for the detector: ```yaml diff --git a/docs/pytorch/user_guide.md b/docs/pytorch/user_guide.md index a30f7f6a7c..849dac340d 100644 --- a/docs/pytorch/user_guide.md +++ b/docs/pytorch/user_guide.md @@ -3,7 +3,7 @@ ## Using DeepLabCut 3.0 -**DeepLabCut 3.0 keeps the same high-level API that you know, but has a full new PyTorch backend. +**DeepLabCut 3.0 keeps the same high-level API that you know, but has a full new PyTorch backend. Moreover, it is a rewrite that is more developer friendly, more powerful, and built for modern deep learning-based computer vision applications.** @@ -32,18 +32,18 @@ and TensorFlow engine through the drop-down menu in the top right corner. ### Quick guide (standard API) -The standard use of DLC does not change (via the high-level API), as you can see in the standard guide: for [single](https://deeplabcut.github.io/DeepLabCut/docs/standardDeepLabCut_UserGuide) and [multiple individuals](https://deeplabcut.github.io/DeepLabCut/docs/maDLC_UserGuide). +The standard use of DLC does not change (via the high-level API), as you can see in the standard guide: for [single](https://deeplabcut.github.io/DeepLabCut/docs/standardDeepLabCut_UserGuide) and [multiple individuals](https://deeplabcut.github.io/DeepLabCut/docs/maDLC_UserGuide). -Also check out several COLAB notebooks on how you can use the code. +Also check out several COLAB notebooks on how you can use the code. -For the +For the ## Major changes ### From iterations to epochs -Pytorch models in DeepLabCut 3.0 are trained for a set number of `epochs`, instead of a -maximum number of `iterations`. An epoch is a single pass through the training dataset, +Pytorch models in DeepLabCut 3.0 are trained for a set number of `epochs`, instead of a +maximum number of `iterations`. An epoch is a single pass through the training dataset, which means your model has seen each training image exactly once. - So if you have 64 training images for your network, an epoch is 64 iterations with batch @@ -54,12 +54,12 @@ size 1 (or 32 iterations with batch size 2, 16 with batch size 4, etc.). ### Creating Shuffles and Model Configuration You can configure models using the `pytorch_config.yaml` file, as described -[here](dlc3-pytorch-config). You can use the same methods to create new shuffles in +[here](dlc3-pytorch-config). You can use the same methods to create new shuffles in DeepLabCut 3.0 as you did for Tensorflow models (`deeplabcut.create_training_dataset` and `deeplabcut.create_training_model_comparison`). More information about the different PyTorch model architectures available in DeepLabCut -is available [here](architectures). You can see a list of supported +is available [here](architectures). You can see a list of supported architectures/variants by using: ```python diff --git a/docs/pytorch_dlc.md b/docs/pytorch_dlc.md index 157a1c19af..18dbc0c9be 100644 --- a/docs/pytorch_dlc.md +++ b/docs/pytorch_dlc.md @@ -3,23 +3,23 @@ ## Modules - [data](https://github.com/nastya236/DLCdev/blob/69005057eeac3c1492712863303f8268cee776e6/deeplabcut/pose_estimation_pytorch/data/project.py#L7): -The `deeplabcut.pose_estimations_pytorch.data` package contains all code for pytorch +The `deeplabcut.pose_estimations_pytorch.data` package contains all code for pytorch dataset creation and test/train splitting. - `Project` class provides train and test splitting and converts dataset to required format. For instance, to [COCO]() format. - - `PoseTrainDataset` class is a [torch.utils.Dataset](https://pytorch.org/docs/stable/data.html) class, which converts raw + - `PoseTrainDataset` class is a [torch.utils.Dataset](https://pytorch.org/docs/stable/data.html) class, which converts raw images and keypoints to a tensor dataset for training and evaluation. - [models](https://github.com/nastya236/DLCdev/blob/69005057eeac3c1492712863303f8268cee776e6/deeplabcut/pose_estimation_pytorch/data/models): -The `deeplabcut.pose_estimations_pytorch.models` package contains all related to +The `deeplabcut.pose_estimations_pytorch.models` package contains all related to building a model with `backbone`, `neck` (optional) and `head`. - [train_module](https://github.com/nastya236/DLCdev/blob/69005057eeac3c1492712863303f8268cee776e6/deeplabcut/pose_estimation_pytorch/data/models): -The `deeplabcut.pose_estimations_pytorch.train_module` contains all classes for model +The `deeplabcut.pose_estimations_pytorch.train_module` contains all classes for model training and validation. ## API The PyTorch implementation of DeepLabCut is very similar to the Tensorflow multi-animal -implementation: the same steps need to be followed, just with slightly different API +implementation: the same steps need to be followed, just with slightly different API calls (and different model names). Up until it's time to create the training dataset, there are no changes to the way a @@ -37,15 +37,15 @@ deeplabcut.create_training_dataset( ``` This will create folders for the training dataset in the same way as the Tensorflow -version, with an addition configuration file in the `train` folder: -`pytorch_config.yaml`. This is the file that can be edited to modify the model +version, with an addition configuration file in the `train` folder: +`pytorch_config.yaml`. This is the file that can be edited to modify the model architecture or training parameters. There are currently two "families" of models implemented in PyTorch: DEKR (Geng, Zigang, -et al. "Bottom-up human pose estimation via disentangled keypoint regression." -Proceedings of the IEEE/CVF conference on computer vision and pattern recognition. +et al. "Bottom-up human pose estimation via disentangled keypoint regression." +Proceedings of the IEEE/CVF conference on computer vision and pattern recognition. 2021.) and Tokenpose (Li, Yanjie, et al. "Tokenpose: Learning keypoint tokens for human -pose estimation." Proceedings of the IEEE/CVF International conference on computer +pose estimation." Proceedings of the IEEE/CVF International conference on computer vision. 2021.). The choices of `net_type` that will create PyTorch training sets are: - `"dekr_16"` - `"dekr_32"` @@ -54,11 +54,11 @@ vision. 2021.). The choices of `net_type` that will create PyTorch training sets - `"token_pose_w32"` - `"token_pose_w48"` -Note that Tokenpose models cannot currently be used with projects that contain unique -keypoints. +Note that Tokenpose models cannot currently be used with projects that contain unique +keypoints. ### Training the network -Training a PyTorch model is done in a very similar manner as a tensorflow model, though +Training a PyTorch model is done in a very similar manner as a tensorflow model, though currently the PyTorch API needs to be called directly: ```python import deeplabcut.pose_estimation_pytorch.apis as api @@ -129,10 +129,10 @@ batch_size: the batch size to use for evaluation ### Analyzing novel videos One big difference between the PyTorch and Tensorflow implementations comes in the way animal assembly happens (for multi-animal models). While in Tensorflow, assembly was a -separate step that needed to be done from the keypoints, in the PyTorch version it's +separate step that needed to be done from the keypoints, in the PyTorch version it's integrated directly into the models. From an API standpoint, that does not change much. -Again, the PyTorch API needs to be invoked directly (it also has the `auto_track` +Again, the PyTorch API needs to be invoked directly (it also has the `auto_track` option). ```python import deeplabcut.pose_estimation_pytorch.apis as api @@ -140,7 +140,7 @@ api.analyze_videos(config_path, ["/fullpath/project/videos/test.mp4"], videotype ``` The PyTorch detections need to be converted to tracklets using the PyTorch API, but then -the original tracklet stitching can be used. +the original tracklet stitching can be used. ```python import deeplabcut import deeplabcut.pose_estimation_pytorch.apis as api @@ -156,7 +156,7 @@ deeplabcut.stitch_tracklets( ) ``` -Creating labeled videos can then be called in exactly the same way as before. +Creating labeled videos can then be called in exactly the same way as before. ```python import deeplabcut deeplabcut.create_labeled_video( diff --git a/docs/quick-start/single_animal_quick_guide.md b/docs/quick-start/single_animal_quick_guide.md index 307ec6d113..b8cd2428b4 100644 --- a/docs/quick-start/single_animal_quick_guide.md +++ b/docs/quick-start/single_animal_quick_guide.md @@ -15,32 +15,32 @@ Create a new project: ``` deeplabcut.create_new_project("project_name", "experimenter", ["path of video 1", "path of video2", ..]) ``` - + Set a config_path variable for ease of use + go edit this file!: ``` config_path = "yourdirectory/project_name/config.yaml" ``` - + Extract frames: ``` deeplabcut.extract_frames(config_path) ``` Label frames: -``` +``` deeplabcut.label_frames(config_path) ``` - + Check labels [OPTIONAL]: ``` deeplabcut.check_labels(config_path) ``` - + Create training dataset: ``` deeplabcut.create_training_dataset(config_path) ``` - + Train the network: ``` deeplabcut.train_network(config_path) diff --git a/docs/recipes/MegaDetectorDLCLive.md b/docs/recipes/MegaDetectorDLCLive.md index ecdf3432c7..a40373368f 100644 --- a/docs/recipes/MegaDetectorDLCLive.md +++ b/docs/recipes/MegaDetectorDLCLive.md @@ -68,9 +68,9 @@ All information seen on the output image is recorded on the **Download JSON file "file": "image0.jpg", //image filename uploaded "number_of_bb": 1, //number of bounding boxes detected on the image "dlc_model": "full_dog", //model used - "bb_0": { + "bb_0": { "corner_1": [ //top left corner - 76.08082580566406, //x + 76.08082580566406, //x 91.02932739257812 //y ], "corner_2": [ //bottom right corner diff --git a/docs/recipes/OtherData.md b/docs/recipes/OtherData.md index 73343284b3..5f2e88accd 100644 --- a/docs/recipes/OtherData.md +++ b/docs/recipes/OtherData.md @@ -7,24 +7,24 @@ Some users may have annotation data in different formats, yet want to use the DLC pipeline. In this case, you need to convert the data to our format. Simply, you can format your data in an excel sheet (.csv file) or pandas array (.h5 file). -Here is a guide to do this via the ".csv" route: (the pandas array route is identical, just format the pandas array in the same way). +Here is a guide to do this via the ".csv" route: (the pandas array route is identical, just format the pandas array in the same way). **Step 1**: create a project as describe in the user guide: https://github.com/DeepLabCut/DeepLabCut/blob/main/docs/UseOverviewGuide.md#create-a-new-project -**Step 2**: edit the ``config.yaml`` file to include the body part names, please take care that spelling, spacing, and capitalization are IDENTICAL to the "labeled data body part names". +**Step 2**: edit the ``config.yaml`` file to include the body part names, please take care that spelling, spacing, and capitalization are IDENTICAL to the "labeled data body part names". **Step 3**: Please inspect the excel formatted sheet (.csv) from our [demo project](https://github.com/DeepLabCut/DeepLabCut/tree/main/examples/Reaching-Mackenzie-2018-08-30/labeled-data/reachingvideo1) - i.e. this file: https://github.com/DeepLabCut/DeepLabCut/blob/main/examples/Reaching-Mackenzie-2018-08-30/labeled-data/reachingvideo1/CollectedData_Mackenzie.csv -**Step 4**: Edit the .csv file such that it contains the X, Y pixel coordinates, the body part names, the scorer name as well as the relative path to the image: e.g. /labeled-data/somefolder/img017.jpg -Then make sure the scorer name, and body parts are the same in the config.yaml file. +**Step 4**: Edit the .csv file such that it contains the X, Y pixel coordinates, the body part names, the scorer name as well as the relative path to the image: e.g. /labeled-data/somefolder/img017.jpg +Then make sure the scorer name, and body parts are the same in the config.yaml file. -Also add for each folder a video to the `video_set` in the config.yaml file. This can also be a dummy variable, but should be e.g. +Also add for each folder a video to the `video_set` in the config.yaml file. This can also be a dummy variable, but should be e.g. C://somefolder.avi if the folder is called somefolder. See demo config.yaml file for proper formatting. **Step 5**: When you are done, run ``deeplabcut.convertcsv2h5('path_to_config.yaml', scorer= 'experimenter')`` - - The scorer name must be identical to the input name for experimenter that you used when you created the project. This will automatically update "Mackenzie" to your name in the example demo notebook. + - The scorer name must be identical to the input name for experimenter that you used when you created the project. This will automatically update "Mackenzie" to your name in the example demo notebook. ## If you merge projects: diff --git a/docs/recipes/TechHardware.md b/docs/recipes/TechHardware.md index 6fb1add9bc..edd4d4dc20 100644 --- a/docs/recipes/TechHardware.md +++ b/docs/recipes/TechHardware.md @@ -29,7 +29,7 @@ tested **TensorFlow versions 1.0 to 1.15, and 2.0 to 2.12 (2.10 for Windows)**; recommend TF2.12 for MacOS/Ubuntu and 2.10 for Windows) for Python 3.10 with GPU support. -To note, is it possible to run DeepLabCut on your CPU, but it will be VERY slow (see: +To note, is it possible to run DeepLabCut on your CPU, but it will be VERY slow (see: [Mathis & Warren](https://www.biorxiv.org/content/early/2018/10/30/457242)). However, this is the preferred path if you want to test DeepLabCut on your own computer/data before purchasing a GPU, with the added benefit of a straightforward installation! Otherwise, use our COLAB notebooks for GPU access for @@ -38,5 +38,5 @@ testing. Docker: We highly recommend advanced users use the supplied [Docker container]( docker-containers). -NOTE: [Currently GPU support in Docker Desktop is only available on Windows with the +NOTE: [Currently GPU support in Docker Desktop is only available on Windows with the WSL2 backend.](https://docs.docker.com/desktop/features/gpu/) diff --git a/docs/recipes/installTips.md b/docs/recipes/installTips.md index ab4565a880..40f5a61f51 100644 --- a/docs/recipes/installTips.md +++ b/docs/recipes/installTips.md @@ -7,7 +7,7 @@ We often update the master deeplabcut code base on GitHub, and then ~1 a month w ### Method 1: -If you want to *use* the latest, you can use pip and add the specific tags, such as `gui`, etc. by modifying and running: +If you want to *use* the latest, you can use pip and add the specific tags, such as `gui`, etc. by modifying and running: ``` pip install --upgrade 'git+https://github.com/deeplabcut/deeplabcut.git#egg=deeplabcut[gui]' ``` @@ -18,13 +18,13 @@ which will download and update deeplabcut, and any dependencies that don't match pip install --upgrade --upgrade-strategy eager 'git+https://github.com/deeplabcut/deeplabcut.git#egg=deeplabcut[gui]' ``` -### Method 2: +### Method 2: If you want to be able to *edit* the source code of DeepLabCut, i.e., maybe add a feature or fix a 🐛, then you need to "clone" the source code: **Step 1:** -- git clone the repo into a folder on your computer: +- git clone the repo into a folder on your computer: - click on this green button and copy the link: @@ -289,7 +289,7 @@ Follow prompts! ## Troubleshooting: Note, if you get a failed build due to wxPython (note, this does not happen on Ubuntu 18, 16, etc), i.e.: ```{warning} -DeepLabCut no longer uses `wxpython` for its GUI - if you're getting such an error, +DeepLabCut no longer uses `wxpython` for its GUI - if you're getting such an error, you're likely installing an old version of DeepLabCut. ``` @@ -365,7 +365,7 @@ During training and analysis steps, DeepLabCut does not use the GPU processor he **On Windows**: -(1) Open the task manager. If it looks like the image below, click on "More Details" +(1) Open the task manager. If it looks like the image below, click on "More Details" ![](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/a0db3157-2228-4444-8084-36801659f272/installBrandon1.png?format=500w) @@ -373,7 +373,7 @@ During training and analysis steps, DeepLabCut does not use the GPU processor he ![](https://images.squarespace-cdn.com/content/v1/57f6d51c9f74566f55ecf271/117e3573-60bb-4599-b00b-c75276b24173/installBrandon2.png?format=500w) -(3) Click on the **Performance** tab. On that page, click on the small arrow under GPU (it might start as **3D**, and change it to **CUDA**. +(3) Click on the **Performance** tab. On that page, click on the small arrow under GPU (it might start as **3D**, and change it to **CUDA**. (4) During training, you should see the **Dedicated GPU memory usage** increase to near maximum, and you should see some activity in the **CUDA** graph. The graph below is the activity while running `testscript.py`. diff --git a/docs/recipes/io.md b/docs/recipes/io.md index e97238628b..8cab213082 100644 --- a/docs/recipes/io.md +++ b/docs/recipes/io.md @@ -15,12 +15,12 @@ clips = vid.split(n_splits=10) deeplabcut.analyze_videos(config_path, clips, ext) ``` -## Tips on video re-encoding and preprocessing +## Tips on video re-encoding and preprocessing -While moving videos between computers or from your computer to cloud storage you can encounter issues with `analyze_videos` or `create_labeled_video` due to video corruption. -The issue can present itself during those steps and you have to carefully review the traceback. Sometimes it might look like the videos were analyzed but in fact analysis stopped right before the end of the video (corruption of the metadata when more indices are assigned than there are actual frames in a video). +While moving videos between computers or from your computer to cloud storage you can encounter issues with `analyze_videos` or `create_labeled_video` due to video corruption. +The issue can present itself during those steps and you have to carefully review the traceback. Sometimes it might look like the videos were analyzed but in fact analysis stopped right before the end of the video (corruption of the metadata when more indices are assigned than there are actual frames in a video). To tackle this issue, the easiest solution might be to re-encode the video, this will not only help with corruption but can also – if you choose so – compress the video without perceivable loss of quality. Common package used for video processing is FFmpeg which you can use from the terminal inside your DEEPLABCUT environment (without going into iPython). -There are number of video codecs that can be used to re-encode your video and if you want to keep the video in the same container (`.avi`, `.mp4`, `.ts` etc.) you should check which codec allows encoding to a certain container. For instance, for `.avi` it will be MJPEG and for `.mp4` H264 and H265. +There are number of video codecs that can be used to re-encode your video and if you want to keep the video in the same container (`.avi`, `.mp4`, `.ts` etc.) you should check which codec allows encoding to a certain container. For instance, for `.avi` it will be MJPEG and for `.mp4` H264 and H265. To re-encode your video, simply use: ``` ffmpeg -i "path_to_video" -c:v codec_name "output_path" @@ -35,14 +35,14 @@ For `.avi` files you want to change the codec and the quality metric, since `crf ``` ffmpeg -i "path_to_video" -c:v mjpeg -q:v 10 "output_path" ``` -`-q:v` is a quality metric with values ranging from 1 to 31 with reasonable values being around 10. +`-q:v` is a quality metric with values ranging from 1 to 31 with reasonable values being around 10. If you want to compress all your recordings for easier storage or moving to cloud storage, you can use a for loop that will go through all videos in a directory that are in a certain container. Let’s say we want to transcode our `.avi` videos to `.mp4` and make them smaller without quality loss. Note, that the loop has be run from inside the folder the videos are in: ``` -for %i in (*.avi) do ffmpeg -i "%i" -c:v libx265 -preset fast -crf 18 "%~ni.mp4" +for %i in (*.avi) do ffmpeg -i "%i" -c:v libx265 -preset fast -crf 18 "%~ni.mp4" ``` This command will re-encode all of your videos into an `.mp4` container and save them with the same name as the original (without overwriting them). -Additionally, ffmpeg allows you to also crop or rescale the videos for possible improvement in inference speed further down the line in DLC workflow. To either crop or rescale you need to use -`-filter:v` parameter after which you’d add either `"crop=Xsize:Ysize:Xstart:Ystart"` for cropping or +Additionally, ffmpeg allows you to also crop or rescale the videos for possible improvement in inference speed further down the line in DLC workflow. To either crop or rescale you need to use +`-filter:v` parameter after which you’d add either `"crop=Xsize:Ysize:Xstart:Ystart"` for cropping or `"scale=Xsize:Ysize"` for rescale. Note that when using “scale” the values how be a result of integer division of the original video size. If you want to keep the aspect ratio, you can simply set either X or Y to `-1` and only give one of the or you can use `“scale=iw/2:ih/2”` which will simply make the video 2 times smaller in both dimensions. For instance, if you have a videos at 1920x1080 resolution and want to rescale it to 960x540 for faster inference while also reencoding from `.avi` and doing some compression in a loop, the command would be something like this: ``` for %i in (*.avi) do ffmpeg -i "%i" -c:v libx265 -preset fast -crf 18 -filter:v "scale= iw/2:ih/2" "%~ni.mp4" diff --git a/docs/recipes/nn.md b/docs/recipes/nn.md index 9377c446ca..93d9cfed5c 100644 --- a/docs/recipes/nn.md +++ b/docs/recipes/nn.md @@ -6,7 +6,7 @@ With TensorFlow, all GPU memory is allocated to training by default, preventing other Tensorflow processes from being run on the same machine. -A flexible solution to limiting memory usage is to call +A flexible solution to limiting memory usage is to call `deeplabcut.train(..., allow_growth=True)`, which dynamically grows the GPU memory region as it is needed. Another, stricter option is to explicitly cap GPU usage to only a fraction of the available memory. For example, allocating a maximum of 1/4 of the @@ -52,7 +52,7 @@ best. Put 'all' in the snapshots section of the `config.yaml` to do this. ## What neural network should I use? (Trade offs, speed performance, and considerations) You always select the network type when you create a training data set: i.e., standard -dlc: `deeplabcut.create_training_dataset(config, net_type=resnet_50)` , or maDLC: +dlc: `deeplabcut.create_training_dataset(config, net_type=resnet_50)` , or maDLC: `deeplabcut.create_multianimaltraining_dataset(config, net_type=dlcrnet_ms5)`. There is nothing else you should change. @@ -84,7 +84,7 @@ other on the open-field dataset):

-This is also one of the main result figures, generated with ResNet-50. BLUE is +This is also one of the main result figures, generated with ResNet-50. BLUE is training - RED is testing - BLACK is our best human-level performance, and 10 pixels is the width - of the mouse nose -so anything under that is good performance for us on this task! @@ -93,7 +93,7 @@ task!

-Here are also some speed stats for analyzing videos with ResNet-50, see +Here are also some speed stats for analyzing videos with ResNet-50, see https://www.biorxiv.org/content/early/2018/10/30/457242 for more details:

@@ -142,7 +142,7 @@ https://arxiv.org/abs/1905.11946) are an excellent choice if you want speed and performance. They do require more careful handling though! Especially for small datasets, you will need to tune the batch size and learning rates. So, we suggest these for more advanced users, or those willing to run experiments to find the best settings. -Here is the speed comparison, and for performance see our latest work at: +Here is the speed comparison, and for performance see our latest work at: http://horse10.deeplabcut.org

diff --git a/docs/recipes/pose_cfg_file_breakdown.md b/docs/recipes/pose_cfg_file_breakdown.md index 2f79ac28d3..0b4901f706 100644 --- a/docs/recipes/pose_cfg_file_breakdown.md +++ b/docs/recipes/pose_cfg_file_breakdown.md @@ -11,7 +11,7 @@ When you train, evaluate, and run inference with a neural network there are hype # 1. What is the *pose_cfg.yml* file? -- The `pose_cfg.yaml` file offers easy access to a range of training parameters that the user may want or have to adjust depending on the used dataset and task. +- The `pose_cfg.yaml` file offers easy access to a range of training parameters that the user may want or have to adjust depending on the used dataset and task. - You will find the file in the dlc-models > test and train sub-directories. There is also a button in the GUI to directly open this file. - This recipe is aimed at giving an average user an intuition on those hyperparameters and situations in which addressing them can be useful. @@ -40,11 +40,11 @@ When you train, evaluate, and run inference with a neural network there are hype - [References](#references) -## 2.1 Training Hyperparameters +## 2.1 Training Hyperparameters ### 2.1.A `max_input_size` and `min_input_size` -The default values are `1500` and `64`, respectively. +The default values are `1500` and `64`, respectively. 💡Pro-tip:💡 - change `max_input_size` when the resolution of the video is higher than 1500x1500 or when `scale_jitter_up` will possibly go over that value @@ -69,11 +69,11 @@ In both cases, you can increase the batchsize up to the limit of your GPU memory ___________________________________________________________________________________ -Values mentioned above and the augmentation parameters are often intuitive, and knowing our own data, we are able to decide on what will and won't be beneficial. Unfortunately, not all hyperparameters are this simple or intuitive. Two parameters that might require some tuning on challenging datasets are `pafwidth` and `pos_dist_thresh`. +Values mentioned above and the augmentation parameters are often intuitive, and knowing our own data, we are able to decide on what will and won't be beneficial. Unfortunately, not all hyperparameters are this simple or intuitive. Two parameters that might require some tuning on challenging datasets are `pafwidth` and `pos_dist_thresh`. ### 2.1.D `pos_dist_thresh` -The default value is `17`. It's the size of a window within which detections are considered positive training samples, meaning they tell the model that it's going in the right direction. +The default value is `17`. It's the size of a window within which detections are considered positive training samples, meaning they tell the model that it's going in the right direction. ### 2.1.E `pafwidth` @@ -83,7 +83,7 @@ The default value is `20`. PAF stands for part affinity fields. It is a method o ## 2.2 Data augmentation parameters In the simplest form, we can think of data augmentation as something similar to imagination or dreaming. Humans imagine different scenarios based on experience, ultimately allowing us to better understand our world. [2, 3, 4](#references) -Similarly, we train our models to different types of "imagined" scenarios, which we limit to the foreseeable ones, so we ultimately get a robust model that can more likely handle new data and scenes. +Similarly, we train our models to different types of "imagined" scenarios, which we limit to the foreseeable ones, so we ultimately get a robust model that can more likely handle new data and scenes. Classes of data augmentations, characterized by their nature, are given by: - [**Geometric transformations**](#geometric) @@ -127,7 +127,7 @@ During training, each image is randomly scaled within the range `[scale_jitter_l ### 2.1.2 `rotation` -*Rotation augmentations* are done by rotating the image right or left on an axis between $1^{\circ}$ and $359^{\circ}$. The safety of rotation augmentations is heavily determined by the rotation degree parameter. Slight rotations such as between $+1^{\circ}$ and $+20^{\circ}$ or $-1^{\circ}$ to $-20^{\circ}$ is generally an acceptable range. Keep in mind that as the rotation degree increases, the precision of the label placement can decrease +*Rotation augmentations* are done by rotating the image right or left on an axis between $1^{\circ}$ and $359^{\circ}$. The safety of rotation augmentations is heavily determined by the rotation degree parameter. Slight rotations such as between $+1^{\circ}$ and $+20^{\circ}$ or $-1^{\circ}$ to $-20^{\circ}$ is generally an acceptable range. Keep in mind that as the rotation degree increases, the precision of the label placement can decrease The image below, retrieved from [2](#ref2), illustrates the difference between the different rotation degrees. @@ -136,11 +136,11 @@ The image below, retrieved from [2](#ref2), illustrates the difference between t During training, each image is rotated $+/-$ the `rotation` degree parameter set. By default, this parameter is set to `25`, which means that the images are augmented with a $+25^{\circ}$ rotation of itself and a $-25^{\circ}$ degree rotation of itself. Should you want to opt out of this augmentation, set the rotation value to `False`. 💡Pro-tips:💡 -- ⭐If you have labelled all the possible rotations of your animal/s, keeping the **default** value **unchanged** is **enough** ✅ +- ⭐If you have labelled all the possible rotations of your animal/s, keeping the **default** value **unchanged** is **enough** ✅ - However, you may want to adjust this parameter if you want your model to: - - handle new data with new rotations of the animal subjects - - handle the possibly unlabelled rotations of your minimally-labeled data + - handle new data with new rotations of the animal subjects + - handle the possibly unlabelled rotations of your minimally-labeled data - But as a consequence, the more you increase the rotation degree, the more the original keypoint labels may not be preserved @@ -148,7 +148,7 @@ During training, each image is rotated $+/-$ the `rotation` degree parameter set This parameter in the DLC module is given by the percentage of sampled data to be augmented from your training data. The default value is set to `0.4` or $40\%$. This means that there is a $40\%$ chance that images within the current batch will be rotated. 💡Pro-tip:💡 -- ⭐ Generally, keeping the **default** value **unchanged** is **enough** ✅ +- ⭐ Generally, keeping the **default** value **unchanged** is **enough** ✅ ### 2.2.4 `fliplr` (or a horizontal flip) @@ -167,48 +167,48 @@ By default, this parameter is set to `False` especially on poses with mirror sym ### 2.2.5 `crop_size` - Cropping consists of removing unwanted pixels from the image, thus selecting a part of the image and discarding the rest, reducing the size of the input. + Cropping consists of removing unwanted pixels from the image, thus selecting a part of the image and discarding the rest, reducing the size of the input. In DeepLabCut *pose_config.yaml* file, by default, `crop_size` is set to (`400,400`), width, and height, respectively. This means it will cut out parts of an image of this size. 💡Pro-tip:💡 - If your images are very large, you could consider increasing the crop size. However, be aware that you'll need a strong GPU, or you will hit memory errors! - - If your images are very small, you could consider decreasing the crop size. + - If your images are very small, you could consider decreasing the crop size. ### 2.2.6 `crop_ratio` - Also, the number of frames to be cropped is defined by the variable `cropratio`, which is set to `0.4` by default. That means that there is a $40\%$ the images within the current batch will be cropped. By default, this value works well. + Also, the number of frames to be cropped is defined by the variable `cropratio`, which is set to `0.4` by default. That means that there is a $40\%$ the images within the current batch will be cropped. By default, this value works well. ### 2.2.7 `max_shift` The crop shift between each cropped image is defined by `max_shift` variable, which explains the max relative shift to the position of the crop centre. By default is set to `0.4`, which means it will be displaced 40% max from the center to not apply identical cropping each time the same image is encountered during training - this is especially important for `density` and `hybrid` cropping methods. - The image below is modified from - [2](#references). - + The image below is modified from + [2](#references). + ### 2.2.8 `crop_sampling` - Likewise, there are different cropping sampling methods (`crop_sampling`), we can use depending on how our image looks like. + Likewise, there are different cropping sampling methods (`crop_sampling`), we can use depending on how our image looks like. 💡Pro-tips💡 - - For highly crowded scenes, `hybrid` and `density` approaches will work best. + - For highly crowded scenes, `hybrid` and `density` approaches will work best. - `uniform` will take out random parts of the image, disregarding the annotations completely - 'keypoint' centers on a random keypoint and crops based on that location - might be best in preserving the whole animal (if reasonable `crop_size` is used) - ### Kernel transformations + ### Kernel transformations Kernel filters are very popular in image processing to sharpen and blur images. Intuitively, blurring an image might increase the motion blur resistance during testing. Otherwise, sharpening for data enhancement could result in capturing more detail on objects of interest. ### 2.2.9 `sharpening` and `sharpenratio` - In DeepLabCut *pose_config.yaml* file, by default, `sharpening` is set to `False`, but if we want to use this type of data augmentation, we can set it `True` and specify a value for `sharpenratio`, which by default is set to `0.3`. Blurring is not defined in the *pose_config.yaml*, but if the user finds it convenient, it can be added to the data augmentation pipeline. + In DeepLabCut *pose_config.yaml* file, by default, `sharpening` is set to `False`, but if we want to use this type of data augmentation, we can set it `True` and specify a value for `sharpenratio`, which by default is set to `0.3`. Blurring is not defined in the *pose_config.yaml*, but if the user finds it convenient, it can be added to the data augmentation pipeline. + + The image below is modified from + [2](#references). - The image below is modified from - [2](#references). - @@ -216,7 +216,7 @@ By default, this parameter is set to `False` especially on poses with mirror sym Concerning sharpness, we have an additional parameter, `edge` enhancement, which enhances the edge contrast of an image to improve its apparent sharpness. Likewise, by default, this parameter is set `False`, but if you want to include it, you just need to set it `True`. -# References +# References

  1. Cao, Z., Simon, T., Wei, S. E., & Sheikh, Y. (2017). Realtime multi-person 2d pose estimation using part affinity fields. In Proceedings of the IEEE conference on Computer Vision and Pattern Recognition (pp. 7291-7299).https://openaccess.thecvf.com/content_cvpr_2017/html/Cao_Realtime_Multi-Person_2D_CVPR_2017_paper.html
  2. Mathis, A., Schneider, S., Lauer, J., & Mathis, M. W. (2020). A Primer on Motion Capture with Deep Learning: Principles, Pitfalls, and Perspectives. In Neuron (Vol. 108, Issue 1, pp. 44-65). https://doi.org/10.1016/j.neuron.2020.09.017
  3. diff --git a/docs/recipes/publishing_notebooks_into_the_DLC_main_cookbook.md b/docs/recipes/publishing_notebooks_into_the_DLC_main_cookbook.md index 83dbb8c75e..5faa5f9a56 100644 --- a/docs/recipes/publishing_notebooks_into_the_DLC_main_cookbook.md +++ b/docs/recipes/publishing_notebooks_into_the_DLC_main_cookbook.md @@ -16,9 +16,9 @@ Hey there, DLC enthusiast! 🌟 Ready to sprinkle your magic into the main DLC c ## Structure of a Recipe When crafting your recipe, adhere to the following structure: - **Introduction**: Begin with an introductory paragraph that highlights the importance and relevance of the recipe. This sets the stage and gives readers context. - + - **Examples/Workflow**: Provide step-by-step instructions or a workflow, supported by examples. This makes it easy for readers to understand and follow along. - + - **Conclusion**: Conclude with a summary or highlight the key takeaways of your recipe. You can also provide references or further reading. @@ -27,7 +27,7 @@ Now, let's dive into the process of contributing your content to the DLC Jupyter 1. **Set-up your local environment.** You need `deeplabcut[docs]` installed: You can do this by running the following command: - ``` + ``` pip install deeplabcut[docs] ``` @@ -57,9 +57,9 @@ This command installs DeepLabCut along with the dependencies required to build t - **Craft with Care:** Remember, your notebook will be a reference for many. Begin with an engaging introduction, followed by well-structured content, and wrap it up with a conclusion. - **Interactive Elements:** One of the strengths of Jupyter notebooks is the ability to combine code, visuals, and narrative. Use interactive plots, widgets, or any other tools that enhance the content and make it engaging. - **Save Regularly:** Jupyter auto-saves your work, but it's a good habit to manually save your notebook frequently, especially after making significant changes. - - **Naming Convention:** Name your notebook in a way that reflects its content and is consistent with other notebook titles in the DLC Jupyter book. This makes it easier for readers to understand the topic at a glance. - - **Updating an existing notebook** - - Navigate to the location of the existing recipe within the directory: + - **Naming Convention:** Name your notebook in a way that reflects its content and is consistent with other notebook titles in the DLC Jupyter book. This makes it easier for readers to understand the topic at a glance. + - **Updating an existing notebook** + - Navigate to the location of the existing recipe within the directory: ``` [YOUR_REPO_DIRECTORY]/docs/recipes/ ``` @@ -76,15 +76,15 @@ This command installs DeepLabCut along with the dependencies required to build t - Navigate to the appropriate directory where the Jupyter notebooks are stored for the Jupyter book. - Add your Jupyter notebook (.ipynb file) to this directory. - + To copy via terminal: - + - Unix-based OS users - + ``` cp [YOUR_NOTEBOOK_FILENAME].ipynb [YOUR_REPO_DIRECTORY]/docs/recipes ``` - + - WinOS users: ``` copy new_recipe.ipynb [YOUR_REPO_DIRECTORY]\docs\recipes @@ -94,7 +94,7 @@ This command installs DeepLabCut along with the dependencies required to build t 8. **Update `[YOUR_REPO_DIRECTORY]/_toc.yml`** by adding under the *Tutorials & Cookbook* section a **new line** containing the path to your notebook. This creates a link to your notebook on the main DLC book sidebar. * For example: - ``` + ``` - file: docs/recipes/[YOUR_NOTEBOOK_FILENAME] ``` @@ -109,7 +109,7 @@ This command installs DeepLabCut along with the dependencies required to build t 10. **Commit your changes:** When everything is a-okay, commit your changes to your branch. If not, edit your file and go to back to step 1. - + ``` git add [YOUR_NOTEBOOK_FILENAME] git commit -m "Added a new notebook about [YOUR_TOPIC]" diff --git a/docs/standardDeepLabCut_UserGuide.md b/docs/standardDeepLabCut_UserGuide.md index f7e653e487..33a16cd7ec 100644 --- a/docs/standardDeepLabCut_UserGuide.md +++ b/docs/standardDeepLabCut_UserGuide.md @@ -261,7 +261,7 @@ labels to the bodyparts in the config.yaml file. Thereafter, the user can call t 2.0.5+: then a box will pop up and ask the user if they wish to display all parts, or only add in the new labels. Saving the labels after all the images are labelled will append the new labels to the existing labeled dataset. -For more information, checkout the [napari-deeplabcut docs](napari-gui) for +For more information, checkout the [napari-deeplabcut docs](napari-gui) for more information about the labelling workflow. ### (E) Check Annotated Frames @@ -298,7 +298,7 @@ saves file sets as both Linux and Windows for you). config.yaml file - that's it - no need to change the video paths, etc! Your project is fully portable. - Be aware you select your neural network backbone at this stage. As of DLC3+ we support PyTorch (and TensorFlow, but -this will be phased out). +this will be phased out). **OVERVIEW:** This function combines the labeled datasets from all the videos and splits them to create train and test datasets. The training data will be used to train the network, while the test data set will be used for evaluating the @@ -359,11 +359,11 @@ supervision, etc. Here are the available loaders: **MODEL COMPARISON**: You can also test several models by creating the same train/test split for different networks. -You can easily do this in the Project Manager GUI (by selecting the "Use an existing +You can easily do this in the Project Manager GUI (by selecting the "Use an existing data split" option), which also lets you compare PyTorch and TensorFlow models. ````{versionadded} 3.0.0 -You can now create new shuffles using the same train/test split as +You can now create new shuffles using the same train/test split as existing shuffles with `create_training_dataset_from_existing_split`. This allows you to compare model performance (between different architectures or when using different training hyper-parameters) as the shuffles were trained on the same data, and evaluated @@ -410,11 +410,11 @@ The function ‘train_network’ helps the user in training the network. It is u deeplabcut.train_network(config_path) ``` The set of arguments in the function starts training the network for the dataset created -for one specific shuffle. Note that you can change training parameters in the +for one specific shuffle. Note that you can change training parameters in the [**pytorch_config.yaml**](dlc3-pytorch-config) file (or **pose_cfg.yaml** for TensorFlow models) of the model that you want to train (before you start training). -At user specified iterations during training checkpoints are stored in the subdirectory +At user specified iterations during training checkpoints are stored in the subdirectory *train* under the respective iteration & shuffle directory. ````{admonition} Tips on training models with the PyTorch Engine @@ -442,7 +442,7 @@ training image exactly once. So if you have 64 training images for your network, epoch is 64 iterations with batch size 1 (or 32 iterations with batch size 2, 16 with batch size 4, etc.). -By default, the pretrained networks are not in the DeepLabCut toolbox (as they can be +By default, the pretrained networks are not in the DeepLabCut toolbox (as they can be more than 100MB), but they get downloaded automatically before you train. If the user wishes to restart the training at a specific checkpoint they can specify the @@ -451,7 +451,7 @@ full path of the checkpoint to the variable ``resume_training_from`` in the [ dlc3-pytorch-config) file (checkout the "Restarting Training at a Specific Checkpoint" section of the docs) under the *train* subdirectory. -**CRITICAL POINT:** It is recommended to train the networks **until the loss plateaus** +**CRITICAL POINT:** It is recommended to train the networks **until the loss plateaus** (depending on the dataset, model architecture and training hyper-parameters this happens after 100 to 250 epochs of training). @@ -460,7 +460,7 @@ dlc3-pytorch-config) file allows the user to alter how often the loss is display and how often the weights are stored. We suggest saving every 5 to 25 epochs. ```` -````{admonition} Tips on training models with the TensorFlow Engine +````{admonition} Tips on training models with the TensorFlow Engine :class: dropdown Example parameters that one can call: @@ -480,10 +480,10 @@ deeplabcut.train_network( ) ``` -By default, the pretrained networks are not in the DeepLabCut toolbox (as they are +By default, the pretrained networks are not in the DeepLabCut toolbox (as they are around 100MB each), but they get downloaded before you train. However, if not previously downloaded from the TensorFlow model weights, it will be downloaded and stored in a -subdirectory *pre-trained* under the subdirectory *models* in +subdirectory *pre-trained* under the subdirectory *models* in *Pose_Estimation_Tensorflow*. At user specified iterations during training checkpoints are stored in the subdirectory *train* under the respective iteration directory. diff --git a/examples/testscript_superanimal_create_pretrained_project.py b/examples/testscript_superanimal_create_pretrained_project.py index bc1234fbfb..b697a3e285 100644 --- a/examples/testscript_superanimal_create_pretrained_project.py +++ b/examples/testscript_superanimal_create_pretrained_project.py @@ -8,10 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -""" -Testscript for creating a pretrained project from a super animal model - -""" +"""Testscript for creating a pretrained project from a super animal model.""" import glob import shutil diff --git a/examples/testscript_superanimal_transfer_learning.py b/examples/testscript_superanimal_transfer_learning.py index 2fe20416b2..6d9c347e4f 100644 --- a/examples/testscript_superanimal_transfer_learning.py +++ b/examples/testscript_superanimal_transfer_learning.py @@ -8,9 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -""" -Test script for super animal adaptation -""" +"""Test script for super animal adaptation.""" import os diff --git a/setup.py b/setup.py index 7f1e5e5229..c3e301e85e 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,7 @@ -""" -DeepLabCut2.0-3.0 Toolbox (deeplabcut.org) -© A. & M. Mathis Labs -https://github.com/DeepLabCut/DeepLabCut -Please see AUTHORS for contributors. +"""DeepLabCut2.0-3.0 Toolbox (deeplabcut.org) © A. + +& M. Mathis Labs https://github.com/DeepLabCut/DeepLabCut Please see AUTHORS for +contributors. https://github.com/DeepLabCut/DeepLabCut/blob/main/AUTHORS Licensed under GNU Lesser General Public License v3.0 """ diff --git a/tests/core/metrics/test_metrics_api.py b/tests/core/metrics/test_metrics_api.py index 3c14af14f1..a38516ab58 100644 --- a/tests/core/metrics/test_metrics_api.py +++ b/tests/core/metrics/test_metrics_api.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""General tests for the metrics API""" +"""General tests for the metrics API.""" import numpy as np import pytest diff --git a/tests/core/metrics/test_metrics_identity_accuracy.py b/tests/core/metrics/test_metrics_identity_accuracy.py index 32ed30eb03..017653d59e 100644 --- a/tests/core/metrics/test_metrics_identity_accuracy.py +++ b/tests/core/metrics/test_metrics_identity_accuracy.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Tests for the scoring methods""" +"""Tests for the scoring methods.""" import numpy as np import pytest diff --git a/tests/core/metrics/test_metrics_rmse_computation.py b/tests/core/metrics/test_metrics_rmse_computation.py index 4b1bef589b..187279df31 100644 --- a/tests/core/metrics/test_metrics_rmse_computation.py +++ b/tests/core/metrics/test_metrics_rmse_computation.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Tests RMSE computation""" +"""Tests RMSE computation.""" import numpy as np import pytest diff --git a/tests/generate_training_dataset/test_trainingset_manipulation.py b/tests/generate_training_dataset/test_trainingset_manipulation.py index 4a907a927e..cf78ba3996 100644 --- a/tests/generate_training_dataset/test_trainingset_manipulation.py +++ b/tests/generate_training_dataset/test_trainingset_manipulation.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Tests for deeplabcut/generate_training_dataset/metadata.py""" +"""Tests for deeplabcut/generate_training_dataset/metadata.py.""" from __future__ import annotations diff --git a/tests/pose_estimation_pytorch/apis/test_create_tracking_dataset.py b/tests/pose_estimation_pytorch/apis/test_create_tracking_dataset.py index dcf26f2b19..e9ba4996d3 100644 --- a/tests/pose_estimation_pytorch/apis/test_create_tracking_dataset.py +++ b/tests/pose_estimation_pytorch/apis/test_create_tracking_dataset.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Tests method to create the tracking dataset in PyTorch""" +"""Tests method to create the tracking dataset in PyTorch.""" from pathlib import Path @@ -20,7 +20,7 @@ class MockLoader(dlc_torch.Loader): - """Mock loader for data""" + """Mock loader for data.""" def __init__(self, tmp_folder: Path, bodyparts: list[str] | None = None): if bodyparts is None: diff --git a/tests/pose_estimation_pytorch/config/test_config_utils.py b/tests/pose_estimation_pytorch/config/test_config_utils.py index 52f17f752e..4a28ef567a 100644 --- a/tests/pose_estimation_pytorch/config/test_config_utils.py +++ b/tests/pose_estimation_pytorch/config/test_config_utils.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Test util functions for config creation""" +"""Test util functions for config creation.""" import pytest diff --git a/tests/pose_estimation_pytorch/data/test_utils.py b/tests/pose_estimation_pytorch/data/test_utils.py index 7bc1a3b3b6..1494e01787 100644 --- a/tests/pose_estimation_pytorch/data/test_utils.py +++ b/tests/pose_estimation_pytorch/data/test_utils.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Tests data utils""" +"""Tests data utils.""" import numpy as np import pytest diff --git a/tests/pose_estimation_pytorch/post_processing/test_identity.py b/tests/pose_estimation_pytorch/post_processing/test_identity.py index 465f12fd01..3f16eade5f 100644 --- a/tests/pose_estimation_pytorch/post_processing/test_identity.py +++ b/tests/pose_estimation_pytorch/post_processing/test_identity.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Tests identity matching""" +"""Tests identity matching.""" import numpy as np import pytest diff --git a/tests/pose_estimation_pytorch/post_processing/test_postprocessing_nms.py b/tests/pose_estimation_pytorch/post_processing/test_postprocessing_nms.py index e6e98c7e6f..48f65fd5e1 100644 --- a/tests/pose_estimation_pytorch/post_processing/test_postprocessing_nms.py +++ b/tests/pose_estimation_pytorch/post_processing/test_postprocessing_nms.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Tests pose NMS""" +"""Tests pose NMS.""" import numpy as np import pytest @@ -98,7 +98,7 @@ ], ) def test_oks_nms_post_processing(poses, score_threshold, expected_kept): - """Tests pose NMS""" + """Tests pose NMS.""" kept = nms.nms_oks( predictions=np.asarray(poses), oks_threshold=0.9, diff --git a/tests/pose_estimation_pytorch/runners/test_filtered_detector_inference_runner.py b/tests/pose_estimation_pytorch/runners/test_filtered_detector_inference_runner.py index daf5218a74..f0ee29715a 100644 --- a/tests/pose_estimation_pytorch/runners/test_filtered_detector_inference_runner.py +++ b/tests/pose_estimation_pytorch/runners/test_filtered_detector_inference_runner.py @@ -1,7 +1,5 @@ #!/usr/bin/env python3 -""" -Test script for superanimal_humanbody with torchvision detector -""" +"""Test script for superanimal_humanbody with torchvision detector.""" from deeplabcut.pose_estimation_pytorch.apis.utils import ( TORCHVISION_DETECTORS, @@ -14,7 +12,7 @@ def test_torchvision_detector(): - """Test that the torchvision detector works with superanimal_humanbody""" + """Test that the torchvision detector works with superanimal_humanbody.""" for detector_name in TORCHVISION_DETECTORS: # Load the superanimal_humanbody config superanimal_config = load_super_animal_config( diff --git a/tests/pose_estimation_pytorch/runners/test_logger.py b/tests/pose_estimation_pytorch/runners/test_logger.py index fefe787f37..9b27314302 100644 --- a/tests/pose_estimation_pytorch/runners/test_logger.py +++ b/tests/pose_estimation_pytorch/runners/test_logger.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Tests loggers""" +"""Tests loggers.""" from pathlib import Path from typing import Any @@ -20,7 +20,7 @@ class MockImageLogger(logging.ImageLoggerMixin): - """Mock image logger""" + """Mock image logger.""" def log_images( self, @@ -78,7 +78,7 @@ def test_prepare_image(keypoints: list[list[float]], denormalize: bool) -> None: def test_csv_logger_resume(tmp_path: Path) -> None: - """Test CSVLogger preserves data when resuming from snapshot""" + """Test CSVLogger preserves data when resuming from snapshot.""" log_file = tmp_path / "learning_stats.csv" # Initial training: log some metrics diff --git a/tests/pose_estimation_pytorch/runners/test_runners_train.py b/tests/pose_estimation_pytorch/runners/test_runners_train.py index 1787a63e62..3fa0994217 100644 --- a/tests/pose_estimation_pytorch/runners/test_runners_train.py +++ b/tests/pose_estimation_pytorch/runners/test_runners_train.py @@ -238,9 +238,10 @@ def test_resuming_training_scheduler_every_epoch( ], ) def test_resuming_training_with_no_scheduler_state(runner_cls, test_cfg: SchedulerTestConfig, resume_epoch: int): - """ - Without a scheduler config, there is no way to set the initial LR. All we can do is - set the last_epoch value, and adjust correctly at milestones going forward. + """Without a scheduler config, there is no way to set the initial LR. + + All we can do is set the last_epoch value, and adjust correctly at milestones going + forward. """ runner = _fit_runner_and_check_lrs( runner_cls, diff --git a/tests/pose_estimation_pytorch/runners/test_schedulers.py b/tests/pose_estimation_pytorch/runners/test_schedulers.py index 9b4b3351ce..37e98ea6a1 100644 --- a/tests/pose_estimation_pytorch/runners/test_schedulers.py +++ b/tests/pose_estimation_pytorch/runners/test_schedulers.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Tests building schedulers from config""" +"""Tests building schedulers from config.""" import random from dataclasses import dataclass diff --git a/tests/pose_estimation_pytorch/runners/test_shelving.py b/tests/pose_estimation_pytorch/runners/test_shelving.py index 0697c048f5..741c3c53c4 100644 --- a/tests/pose_estimation_pytorch/runners/test_shelving.py +++ b/tests/pose_estimation_pytorch/runners/test_shelving.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Tests for ShelfWriter / ShelfReader""" +"""Tests for ShelfWriter / ShelfReader.""" from __future__ import annotations @@ -30,7 +30,7 @@ def _make_bodyparts(num_assemblies: int = 2, num_bpts: int = 3) -> np.ndarray: - """(num_assemblies, num_bpts, 3) — x, y, score""" + """(num_assemblies, num_bpts, 3) — x, y, score.""" rng = np.random.default_rng(0) return rng.random((num_assemblies, num_bpts, 3)).astype(np.float32) diff --git a/tests/pose_estimation_pytorch/runners/test_task.py b/tests/pose_estimation_pytorch/runners/test_task.py index 1d7ef3395f..2f821d0aa3 100644 --- a/tests/pose_estimation_pytorch/runners/test_task.py +++ b/tests/pose_estimation_pytorch/runners/test_task.py @@ -8,7 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # -"""Tests the Task enum""" +"""Tests the Task enum.""" import pytest diff --git a/tests/test_evaluate.py b/tests/test_evaluate.py index fbf50b6a86..fd4405bb61 100644 --- a/tests/test_evaluate.py +++ b/tests/test_evaluate.py @@ -204,7 +204,8 @@ def test_get_snapshots_by_index_int_ok(): def test_get_snapshots_by_index_error(): - """Test that a ValueError is raised when the index is out of range or invalid str.""" + """Test that a ValueError is raised when the index is out of range or invalid + str.""" available = ["snapshot-1", "snapshot-2", "snapshot-3"] # positive int From 980a44de892e62cde865816945cb202ac12a347d Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 13:36:41 +0100 Subject: [PATCH 47/80] Rename test file to test_bottom_up.py --- .../runners/{bottum_up.py => test_bottom_up.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/pose_estimation_pytorch/runners/{bottum_up.py => test_bottom_up.py} (100%) diff --git a/tests/pose_estimation_pytorch/runners/bottum_up.py b/tests/pose_estimation_pytorch/runners/test_bottom_up.py similarity index 100% rename from tests/pose_estimation_pytorch/runners/bottum_up.py rename to tests/pose_estimation_pytorch/runners/test_bottom_up.py From de702225f6cb53b138a44c8d4962d22d90256c8f Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 13:49:49 +0100 Subject: [PATCH 48/80] Fix zip args --- deeplabcut/core/metrics/distance_metrics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deeplabcut/core/metrics/distance_metrics.py b/deeplabcut/core/metrics/distance_metrics.py index bd1c9728b7..78d3f00573 100644 --- a/deeplabcut/core/metrics/distance_metrics.py +++ b/deeplabcut/core/metrics/distance_metrics.py @@ -361,7 +361,7 @@ def compute_detection_rmse( image_gt = image_gt.transpose((1, 0, 2)) # to (num_bpts, num_gt_individuals, 3) image_pred = image_pred.transpose((1, 0, 2)) # to (num_bpts, num_pred, 3) - for bpt_index, (bpt_gt, bpt_pred) in enumerate(zip(image_gt, image_pred, strict=True)): + for bpt_index, (bpt_gt, bpt_pred) in enumerate(zip(image_gt, image_pred, strict=False)): # filter NaNs and invalid values bpt_gt = bpt_gt[~np.any(np.isnan(bpt_gt), axis=1)] bpt_pred = bpt_pred[~np.any(np.isnan(bpt_pred), axis=1)] @@ -399,7 +399,7 @@ def compute_detection_rmse( if not isinstance(pcutoff, (int, float)): unique_cutoffs = pcutoff[-num_unique:] - for bpt_index, (gt, pred) in enumerate(zip(unique_gt, unique_pred, strict=False), strict=True): + for bpt_index, (gt, pred) in enumerate(zip(unique_gt, unique_pred, strict=False)): dist = np.linalg.norm(gt[:2] - pred[:2]) distances.append(dist) From d83cb0e91591e82515f52bcb920e97d330cc07e5 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 14:02:40 +0100 Subject: [PATCH 49/80] Update bottom-up runner test to new APIs Refactor tests/pose_estimation_pytorch/runners/test_bottom_up.py to use the updated model APIs and runner builder. Replace deprecated WeightedAggregateLoss/LOSSES/RUNNERS usage with WeightedLossAggregator/LOSS_AGGREGATORS and build_training_runner, adjust criterion construction, and build a runner_config passed to build_training_runner. Add a clarifying comment about the test not running in CI and remove outdated imports/usages to match current code structure. --- .../runners/test_bottom_up.py | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/tests/pose_estimation_pytorch/runners/test_bottom_up.py b/tests/pose_estimation_pytorch/runners/test_bottom_up.py index 3548e45f01..28c680645e 100644 --- a/tests/pose_estimation_pytorch/runners/test_bottom_up.py +++ b/tests/pose_estimation_pytorch/runners/test_bottom_up.py @@ -15,12 +15,12 @@ import pytest import torch -from deeplabcut.pose_estimation_pytorch.models.criterion import WeightedAggregateLoss from deeplabcut.pose_estimation_pytorch.config import make_pytorch_pose_config -from deeplabcut.pose_estimation_pytorch.models import LOSSES, PREDICTORS, PoseModel -from deeplabcut.pose_estimation_pytorch.runners import RUNNERS +from deeplabcut.pose_estimation_pytorch.models import LOSS_AGGREGATORS, PREDICTORS, PoseModel +from deeplabcut.pose_estimation_pytorch.models.criterions.aggregators import WeightedLossAggregator from deeplabcut.pose_estimation_pytorch.runners.schedulers import LRListScheduler +from deeplabcut.pose_estimation_pytorch.runners.train import build_training_runner from deeplabcut.utils import auxiliaryfunctions SINGLE_ANIMAL_NETS = ["resnet_50"] @@ -64,35 +64,37 @@ def test_build_bottom_up_runner( for head_cfg in pytorch_cfg["model"]["heads"]: crit_cfg = head_cfg["criterion"] criterion_weight = crit_cfg.get("weight", 1) - criterion = LOSSES.build({k: v for k, v in crit_cfg.items() if k != "weight"}) + criterion = LOSS_AGGREGATORS.build({k: v for k, v in crit_cfg.items() if k != "weight"}) head_criterions.append((criterion_weight, criterion)) - criterion = WeightedAggregateLoss(head_criterions) + criterion = WeightedLossAggregator(head_criterions) get_optimizer = getattr(torch.optim, pytorch_cfg["optimizer"]["type"]) optimizer = get_optimizer(params=pose_model.parameters(), **pytorch_cfg["optimizer"]["params"]) - predictor = PREDICTORS.build(dict(pytorch_cfg["model"]["predictor"])) + PREDICTORS.build(dict(pytorch_cfg["model"]["predictor"])) if pytorch_cfg.get("scheduler"): if pytorch_cfg["scheduler"]["type"] == "LRListScheduler": _scheduler = LRListScheduler else: _scheduler = getattr(torch.optim.lr_scheduler, pytorch_cfg["scheduler"]["type"]) - scheduler = _scheduler(optimizer=optimizer, **pytorch_cfg["scheduler"]["params"]) + _scheduler(optimizer=optimizer, **pytorch_cfg["scheduler"]["params"]) else: - scheduler = None + pass - logger = None - RUNNERS.build( - dict( - **pytorch_cfg["solver"], - model=pose_model, - criterion=criterion, - optimizer=optimizer, - predictor=predictor, - cfg=pytorch_cfg, - device=pytorch_cfg["device"], - scheduler=scheduler, - logger=logger, - ) + # NOTE: @C-Achard 2026-03-18 This file was not named with test_* as a prefix, + # so it never ran in CI. A lot of imports are outdated and non-existent + # FIX: replace RUNNERS registry with build_training_runner and remove unused imports + runner_config = { + **pytorch_cfg["solver"], + "optimizer": pytorch_cfg["optimizer"], + "scheduler": pytorch_cfg.get("scheduler"), + } + _ = build_training_runner( + runner_config=runner_config, + model_folder=Path("."), + task=pose_model.task, + model=pose_model, + device=pytorch_cfg["device"], + logger=None, ) From 9d4c6cc3d95c6f45068a87edd22a6aab5d1989f0 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 14:22:50 +0100 Subject: [PATCH 50/80] Use local template config in bottom-up test Switch test to use a repository-local template instead of querying auxiliaryfunctions.get_deeplabcut_path(): set root_path relative to the test file and point template_path to tests/other/test_configs/pytorch_config.yaml. Add template_path.resolve() and an assert that the template file exists to fail early if missing. This makes the test independent of an installed deeplabcut and ensures a consistent config is used. --- tests/pose_estimation_pytorch/runners/test_bottom_up.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/pose_estimation_pytorch/runners/test_bottom_up.py b/tests/pose_estimation_pytorch/runners/test_bottom_up.py index 28c680645e..722a4e428b 100644 --- a/tests/pose_estimation_pytorch/runners/test_bottom_up.py +++ b/tests/pose_estimation_pytorch/runners/test_bottom_up.py @@ -52,8 +52,10 @@ def test_build_bottom_up_runner( project_cfg["uniquebodyparts"] = [] project_cfg["individuals"] = ["tom"] - root_path = Path(auxiliaryfunctions.get_deeplabcut_path()) - template_path = root_path / "pose_estimation_pytorch" / "apis" / "pytorch_config.yaml" + root_path = Path(__file__).parent.parent + template_path = root_path / "other/test_configs/pytorch_config.yaml" + template_path.resolve() + assert template_path.is_file(), f"Template config not found at {template_path}" auxiliaryfunctions.read_plainconfig(str(template_path)) pytorch_cfg = make_pytorch_pose_config(project_cfg, str(template_path), net_type) print_dict(pytorch_cfg) From 3d7b645bc1adf55735c8975154beadc220c8ac5c Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 14:27:26 +0100 Subject: [PATCH 51/80] Tighten video path assertion and add logs Update test_video_set_configuration to use resolved Path comparison instead of substring matching for video keys, reducing false positives when checking whether the project video is present in cfg["video_sets"]. Add logging.debug statements (and import logging) to print config and video_set details to aid debugging. Changes are limited to tests/create_project/test_video_set_configuration.py. --- tests/create_project/test_video_set_configuration.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/create_project/test_video_set_configuration.py b/tests/create_project/test_video_set_configuration.py index 5d163d367c..03dcfdac62 100644 --- a/tests/create_project/test_video_set_configuration.py +++ b/tests/create_project/test_video_set_configuration.py @@ -10,6 +10,7 @@ # """Unit tests for deeplabcut.create_project.new module.""" +import logging import warnings from pathlib import Path from unittest.mock import Mock, patch @@ -199,12 +200,17 @@ def test_valid_video_included_in_config( from deeplabcut.utils import auxiliaryfunctions cfg = auxiliaryfunctions.read_config(config_path) + logging.debug(f"Config content: {cfg}") + logging.debug(f"Video sets in config: {cfg.get('video_sets', {})}") + logging.debug(f"Video sets keys: {list(cfg.get('video_sets', {}).keys())}") assert "video_sets" in cfg assert len(cfg["video_sets"]) > 0 # Check that video path is in video_sets - video_path_str = str(Path(mock_video_file).resolve()) - assert any(video_path_str in key for key in cfg["video_sets"].keys()) + video_keys = [Path(k) for k in cfg["video_sets"].keys()] + project_video = Path(config_path).parent / "videos" / mock_video_file.name + + assert any(k.resolve() == project_video.resolve() for k in video_keys) def test_invalid_video_removed_from_project( From 70ff090536bb5c4404afd2c1e1713adf4dbd681a Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 14:28:27 +0100 Subject: [PATCH 52/80] Remove outdated test code build_training_runner already does what was being tested manually --- .../runners/test_bottom_up.py | 32 +++---------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/tests/pose_estimation_pytorch/runners/test_bottom_up.py b/tests/pose_estimation_pytorch/runners/test_bottom_up.py index 722a4e428b..7d21c2f404 100644 --- a/tests/pose_estimation_pytorch/runners/test_bottom_up.py +++ b/tests/pose_estimation_pytorch/runners/test_bottom_up.py @@ -14,12 +14,9 @@ from typing import Any import pytest -import torch from deeplabcut.pose_estimation_pytorch.config import make_pytorch_pose_config -from deeplabcut.pose_estimation_pytorch.models import LOSS_AGGREGATORS, PREDICTORS, PoseModel -from deeplabcut.pose_estimation_pytorch.models.criterions.aggregators import WeightedLossAggregator -from deeplabcut.pose_estimation_pytorch.runners.schedulers import LRListScheduler +from deeplabcut.pose_estimation_pytorch.models import PoseModel from deeplabcut.pose_estimation_pytorch.runners.train import build_training_runner from deeplabcut.utils import auxiliaryfunctions @@ -40,8 +37,9 @@ def print_dict(data: dict, indent: int = 0): def test_build_bottom_up_runner( net_type: str, multianimal: bool, + tmp_path: Path, ) -> None: - project_cfg: dict[str, Any] = {"multianimalproject": multianimal} + project_cfg: dict[str, Any] = {"multianimalproject": multianimal, "project_path": str(tmp_path)} if multianimal: project_cfg["bodyparts"] = "MULTI!" project_cfg["multianimalbodyparts"] = ["head", "shoulder", "knee", "toe"] @@ -54,7 +52,7 @@ def test_build_bottom_up_runner( root_path = Path(__file__).parent.parent template_path = root_path / "other/test_configs/pytorch_config.yaml" - template_path.resolve() + template_path = template_path.resolve() assert template_path.is_file(), f"Template config not found at {template_path}" auxiliaryfunctions.read_plainconfig(str(template_path)) pytorch_cfg = make_pytorch_pose_config(project_cfg, str(template_path), net_type) @@ -62,28 +60,6 @@ def test_build_bottom_up_runner( pose_model = PoseModel.build(pytorch_cfg["model"]) - head_criterions = [] - for head_cfg in pytorch_cfg["model"]["heads"]: - crit_cfg = head_cfg["criterion"] - criterion_weight = crit_cfg.get("weight", 1) - criterion = LOSS_AGGREGATORS.build({k: v for k, v in crit_cfg.items() if k != "weight"}) - head_criterions.append((criterion_weight, criterion)) - criterion = WeightedLossAggregator(head_criterions) - - get_optimizer = getattr(torch.optim, pytorch_cfg["optimizer"]["type"]) - optimizer = get_optimizer(params=pose_model.parameters(), **pytorch_cfg["optimizer"]["params"]) - - PREDICTORS.build(dict(pytorch_cfg["model"]["predictor"])) - - if pytorch_cfg.get("scheduler"): - if pytorch_cfg["scheduler"]["type"] == "LRListScheduler": - _scheduler = LRListScheduler - else: - _scheduler = getattr(torch.optim.lr_scheduler, pytorch_cfg["scheduler"]["type"]) - _scheduler(optimizer=optimizer, **pytorch_cfg["scheduler"]["params"]) - else: - pass - # NOTE: @C-Achard 2026-03-18 This file was not named with test_* as a prefix, # so it never ran in CI. A lot of imports are outdated and non-existent # FIX: replace RUNNERS registry with build_training_runner and remove unused imports From 06ab7d322a29883efd8442993f7711d84ae9c341 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 14:35:58 +0100 Subject: [PATCH 53/80] Update bottom-up test to new runner API Bring tests/pose_estimation_pytorch/runners/test_bottom_up.py up to date with refactored API. - Replace deprecated deeplabcut.utils.auxiliaryfunctions import with deeplabcut.pose_estimation_pytorch.task.Task. - Inline and resolve template_path, assert file exists. - Remove obsolete auxiliaryfunctions.read_plainconfig and debug print of the config. - Switch to build_training_runner with pytorch_cfg['runner'], pass Task.BOTTOM_UP, model=pose_model and use tmp_path as model_folder. - Reformat project_cfg construction and add an assertion that the runner is created. - Add a commented pytest.mark.skip note indicating the test is outdated and needs revision. --- .../runners/test_bottom_up.py | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/tests/pose_estimation_pytorch/runners/test_bottom_up.py b/tests/pose_estimation_pytorch/runners/test_bottom_up.py index 7d21c2f404..ae6de38298 100644 --- a/tests/pose_estimation_pytorch/runners/test_bottom_up.py +++ b/tests/pose_estimation_pytorch/runners/test_bottom_up.py @@ -18,7 +18,7 @@ from deeplabcut.pose_estimation_pytorch.config import make_pytorch_pose_config from deeplabcut.pose_estimation_pytorch.models import PoseModel from deeplabcut.pose_estimation_pytorch.runners.train import build_training_runner -from deeplabcut.utils import auxiliaryfunctions +from deeplabcut.pose_estimation_pytorch.task import Task SINGLE_ANIMAL_NETS = ["resnet_50"] MULTI_ANIMAL_NETS = ["dekr_w18"] @@ -33,13 +33,19 @@ def print_dict(data: dict, indent: int = 0): print(f"{indent * ' '}{k}: {v}") +# @pytest.mark.skip(reason="This test is outdated and needs to be updated to reflect changes in the codebase.") + + @pytest.mark.parametrize("net_type, multianimal", NETS) def test_build_bottom_up_runner( net_type: str, multianimal: bool, tmp_path: Path, ) -> None: - project_cfg: dict[str, Any] = {"multianimalproject": multianimal, "project_path": str(tmp_path)} + project_cfg: dict[str, Any] = { + "multianimalproject": multianimal, + "project_path": str(tmp_path), + } if multianimal: project_cfg["bodyparts"] = "MULTI!" project_cfg["multianimalbodyparts"] = ["head", "shoulder", "knee", "toe"] @@ -51,28 +57,21 @@ def test_build_bottom_up_runner( project_cfg["individuals"] = ["tom"] root_path = Path(__file__).parent.parent - template_path = root_path / "other/test_configs/pytorch_config.yaml" - template_path = template_path.resolve() + template_path = (root_path / "other/test_configs/pytorch_config.yaml").resolve() assert template_path.is_file(), f"Template config not found at {template_path}" - auxiliaryfunctions.read_plainconfig(str(template_path)) - pytorch_cfg = make_pytorch_pose_config(project_cfg, str(template_path), net_type) - print_dict(pytorch_cfg) + pytorch_cfg = make_pytorch_pose_config(project_cfg, str(template_path), net_type) pose_model = PoseModel.build(pytorch_cfg["model"]) # NOTE: @C-Achard 2026-03-18 This file was not named with test_* as a prefix, # so it never ran in CI. A lot of imports are outdated and non-existent # FIX: replace RUNNERS registry with build_training_runner and remove unused imports - runner_config = { - **pytorch_cfg["solver"], - "optimizer": pytorch_cfg["optimizer"], - "scheduler": pytorch_cfg.get("scheduler"), - } - _ = build_training_runner( - runner_config=runner_config, - model_folder=Path("."), - task=pose_model.task, + runner = build_training_runner( + runner_config=pytorch_cfg["runner"], + model_folder=tmp_path, + task=Task.BOTTOM_UP, model=pose_model, device=pytorch_cfg["device"], logger=None, ) + assert runner is not None From 962deeb1c814f789fcbdc05cfcb1782e7b064c00 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 14:51:25 +0100 Subject: [PATCH 54/80] Init endpoints dict and merge block endpoints Set endpoints to an empty dict in MBConvBlock and Model, and always iterate over block.endpoints when building the model endpoints map. Previously endpoints could be None and required a conditional check before iterating; this change makes endpoints a dict and removes the guard so nested block endpoint entries (including reduction-prefixed keys) are consistently merged. Prevents TypeError on iteration and ensures a deterministic endpoints structure. --- .../backbones/efficientnet_model.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_model.py b/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_model.py index 98e5e3a05c..76d03becde 100644 --- a/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_model.py +++ b/deeplabcut/pose_estimation_tensorflow/backbones/efficientnet_model.py @@ -165,7 +165,7 @@ def __init__(self, block_args, global_params): global_params.use_se and self._block_args.se_ratio is not None and 0 < self._block_args.se_ratio <= 1 ) - self.endpoints = None + self.endpoints = {} # Builds the block accordings to arguments. self._build() @@ -395,7 +395,7 @@ def __init__(self, blocks_args=None, global_params=None): self._relu_fn = global_params.relu_fn or tf.nn.swish self._batch_norm = global_params.batch_norm - self.endpoints = None + self.endpoints = {} self._build() @@ -510,11 +510,12 @@ def call(self, inputs, use_batch_norm=False, drop_out=False, features_only=None) self.endpoints[f"block_{idx}"] = outputs if is_reduction: self.endpoints[f"reduction_{reduction_idx}"] = outputs - if block.endpoints: - for k, v in block.endpoints.items(): - self.endpoints[f"block_{idx}/{k}"] = v - if is_reduction: - self.endpoints[f"reduction_{reduction_idx}/{k}"] = v + + for k, v in block.endpoints.items(): + self.endpoints[f"block_{idx}/{k}"] = v + if is_reduction: + self.endpoints[f"reduction_{reduction_idx}/{k}"] = v + self.endpoints["features"] = outputs if not features_only: From b58daed06778e629a6e9cd875583f5c928ace563 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 15:00:16 +0100 Subject: [PATCH 55/80] Refine and split pre-commit hook stages Add default_stages and explicitly set stages for hooks so modifying hooks run locally (pre-commit) while checks can run in CI/manual stages. Add check-merge-conflict and validate-pyproject stages, adjust docformatter args and mark it as local-only, and reorganize ruff hooks into a local autofix variant and CI check-only variant with descriptive names. --- .pre-commit-config.yaml | 52 +++++++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 74e445484d..b433a1a992 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,33 +1,71 @@ +default_stages: [pre-commit] + repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: + # These are safe to run in both local & CI (they don't require "fix vs check" split) - id: check-added-large-files + stages: [pre-commit, manual] - id: check-yaml + stages: [pre-commit, manual] - id: check-toml - - id: end-of-file-fixer + stages: [pre-commit, manual] + - id: check-merge-conflict + stages: [pre-commit, manual] - id: name-tests-test args: [--pytest-test-first] + stages: [pre-commit, manual] + + # These modify files. Run locally only (pre-commit stage). + - id: end-of-file-fixer + stages: [pre-commit] - id: trailing-whitespace - - id: check-merge-conflict + stages: [pre-commit] + - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.18.1 + rev: v2.19.0 hooks: - id: pyproject-fmt + stages: [pre-commit] # modifies -> local only + - repo: https://github.com/abravalheri/validate-pyproject rev: v0.25 hooks: - id: validate-pyproject + stages: [pre-commit, manual] + - repo: https://github.com/PyCQA/docformatter rev: v1.7.7 hooks: - id: docformatter - args: ["--wrap-summaries=88", "--wrap-descriptions=88", "--in-place", "--black"] + args: [--wrap-descriptions=88, --wrap-summaries=88, --in-place, --black] + stages: [pre-commit] # modifies -> local only + - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.6 hooks: - # Run the formatter. + # -------------------------- + # LOCAL AUTOFIX (developers) + # -------------------------- + - id: ruff-check + name: ruff-check (fix) + args: [--fix, --unsafe-fixes] + stages: [pre-commit] + - id: ruff-format - # Run the linter. + name: ruff-format (write) + stages: [pre-commit] + + # -------------------------- + # CI CHECK-ONLY (no writes) + # -------------------------- - id: ruff-check - args: [--fix,--unsafe-fixes] + name: ruff-check (ci) + args: [--output-format=github] + stages: [manual] + + - id: ruff-format + name: ruff-format (ci) + args: [--check, --diff] + stages: [manual] From ddfaecdcc0da8526afd90972971be502b2e4345c Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 15:04:49 +0100 Subject: [PATCH 56/80] Add docformatter CI hook and name fix hook Give the existing pre-commit docformatter hook a name ('docformatter (fix)') and add a new 'docformatter (ci)' entry that runs with --check (no in-place edits) in the manual stage. This keeps the pre-commit hook as an auto-fix while providing a CI-friendly check-only variant. --- .pre-commit-config.yaml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b433a1a992..fa301ded7b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,8 +39,15 @@ repos: rev: v1.7.7 hooks: - id: docformatter + name: docformatter (fix) args: [--wrap-descriptions=88, --wrap-summaries=88, --in-place, --black] - stages: [pre-commit] # modifies -> local only + stages: [pre-commit] + + - id: docformatter + name: docformatter (ci) + args: [--wrap-descriptions=88, --wrap-summaries=88, --check, --black] + stages: [manual] + - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.6 From d514260b7d37ba401e3c71ef0be2a8a862babc32 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 15:17:43 +0100 Subject: [PATCH 57/80] Fix E501 line too long --- deeplabcut/cli.py | 36 +++++++++----- deeplabcut/compat.py | 49 +++++++++++-------- .../trainingsetmanipulation.py | 17 +++++-- 3 files changed, 65 insertions(+), 37 deletions(-) diff --git a/deeplabcut/cli.py b/deeplabcut/cli.py index 6fbb0a650c..07a89fd434 100644 --- a/deeplabcut/cli.py +++ b/deeplabcut/cli.py @@ -62,20 +62,25 @@ def create_new_project(_, *args, **kwargs): videos : list \n \tA list of string containing the full paths of the videos to include in the project.\n working_directory : string, optional \n - \tThe directory where the project will be created. The default is the ``current working directory``; if provided, it must be a string\n + \tThe directory where the project will be created. + The default is the ``current working directory``; if provided, it must be a string\n copy_videos : bool, optional \n - If this is set to True, the symlink of the videos are copied to the project/videos directory. The default is ``True``; if provided it must be either ``True`` or ``False`` \n + If this is set to True, the symlink of the videos are copied to the project/videos directory. + The default is ``True``; if provided it must be either ``True`` or ``False`` \n Example \n -------- \n To create the project in the current working directory \n - python3 dlc.py create_new_project reaching-task Tanmay /data/videos/mouse1.avi /data/videos/mouse2.avi /data/videos/mouse3.avi /analysis/project/ + python3 dlc.py create_new_project reaching-task + Tanmay /data/videos/mouse1.avi /data/videos/mouse2.avi /data/videos/mouse3.avi /analysis/project/ To create the project in the current working directory but do not want to create the symlinks \n - python3 dlc.py create_new_project reaching-task Tanmay /data/videos/mouse1.avi /data/videos/mouse2.avi /data/videos/mouse3.avi /analysis/project/ -c False + python3 dlc.py create_new_project reaching-task + Tanmay /data/videos/mouse1.avi /data/videos/mouse2.avi /data/videos/mouse3.avi /analysis/project/ -c False To create the project in another directory \n - python3 dlc.py create_new_project reaching-task Tanmay /data/vies/mouse1.avi /data/videos/mouse2.avi /data/videos/mouse3.avi analysis/project -d home/project + python3 dlc.py create_new_project reaching-task + Tanmay /data/vies/mouse1.avi /data/videos/mouse2.avi /data/videos/mouse3.avi analysis/project -d home/project """ from deeplabcut.create_project import new @@ -158,7 +163,8 @@ def extract_frames(_, *args, **kwargs): -------- \n for selecting frames manually, \n >>> deeplabcut.extract_frames /analysis/project/reaching-task/config.yaml manual \n - While selecting the frames manually, you do not need to specify the cropping parameters. Rather, you will get a prompt in the graphic user interface to choose if you need to crop or not. \n + While selecting the frames manually, you do not need to specify the cropping parameters. + Rather, you will get a prompt in the graphic user interface to choose if you need to crop or not. \n -------- \n """ from deeplabcut.generate_training_dataset.frame_extraction import extract_frames as _extract_frames @@ -210,7 +216,8 @@ def check_labels(_, config): ) @click.pass_context def create_training_dataset(_, *args, **kwargs): - """Combine frame and label information into a an array. Create training and test sets. Update parameters TrainFraction, iteration in config.yaml + """Combine frame and label information into a an array. Create training and test sets. + Update parameters TrainFraction, iteration in config.yaml Also update parameters for pose_config.yaml as wanted.\n CONFIG: Full path of the config.yaml file in the train directory of a project.\n Example \n @@ -338,7 +345,8 @@ def analyze_videos(_, *args, **kwargs): "--num_shuffles", "shuffle", default=1, - help="The shuffle index of training dataset. The extracted frames will be stored in the labeled-dataset for the corresponding shuffle of training dataset. Default is set to 1", + help="The shuffle index of training dataset. The extracted frames will be stored in the " + "labeled-dataset for the corresponding shuffle of training dataset. Default is set to 1", ) @click.option( "-outlier", @@ -346,7 +354,8 @@ def analyze_videos(_, *args, **kwargs): "outlieralgorithm", default="fitting", help="String specifying the algorithm used to detect the outliers. Currently, deeplabcut supports only sarimax (this will be updated). \ - This method fits a Seasonal AutoRegressive Integrated Moving Average with eXogenous regressors model to data and computes confidence interval. \ + This method fits a Seasonal AutoRegressive Integrated Moving Average with eXogenous regressors model \ + to data and computes confidence interval. \ Based on the fraction of data points outside the confidence interval and the average distance (compared to delta) \ the user can identify potential outlier frames. The default is set to ``fitting``. Other choices: `fitting`, `jump`, `uncertain`", ) @@ -426,10 +435,12 @@ def extract_outlier_frames(_, *args, **kwargs): >>> python3 dlc.py extract_outlier_frames /analysis/project/reaching-task/config.yaml /analysis/project/video/reachinvideo1.avi \n --------\n for extracting the frames with kmeans\n - >>> python3 dlc.py extract_outlier_frames /analysis/project/reaching-task/config.yaml /analysis/project/video/reachinvideo1.avi --extractionalgorithm 'kmeans' \n + >>> python3 dlc.py extract_outlier_frames /analysis/project/reaching-task/config.yaml + /analysis/project/video/reachinvideo1.avi --extractionalgorithm 'kmeans' \n --------\n for extracting the frames with kmeans and epsilon = 5 pixels.\n - >>> python3 dlc.py extract_outlier_frames /analysis/project/reaching-task/config.yaml /analysis/project/video/reachinvideo1.avi --epsilon 5 --extractionalgorithm kmeans \n + >>> python3 dlc.py extract_outlier_frames /analysis/project/reaching-task/config.yaml + /analysis/project/video/reachinvideo1.avi --epsilon 5 --extractionalgorithm kmeans \n --------\n """ from deeplabcut.refine_training_dataset import outlier_frames @@ -541,7 +552,8 @@ def plot_trajectories(_, *args, **kwargs): Example\n --------\n for labeling the frames\n - >>> python3 dlc.py plot_trajectories /analysis/project/reaching-task/config.yaml /analysis/project/videos/reachingvideo1.avi \n + >>> python3 dlc.py plot_trajectories /analysis/project/reaching-task/config.yaml + /analysis/project/videos/reachingvideo1.avi \n --------\n """ from deeplabcut.utils import plotting diff --git a/deeplabcut/compat.py b/deeplabcut/compat.py index 13e2630c54..eca6e149ed 100644 --- a/deeplabcut/compat.py +++ b/deeplabcut/compat.py @@ -589,7 +589,8 @@ def return_evaluate_network_data( an error if called with a PyTorch shuffle. If fulldata=True, also returns (the complete annotation and prediction array) - Returns list of: (DataMachine, Data, data, trainIndices, testIndices, trainFraction, DLCscorer,comparisonbodyparts, cfg, Snapshots[snapindex]) + Returns list of: + (DataMachine, Data, data, trainIndices, testIndices, trainFraction, DLCscorer,comparisonbodyparts, cfg, Snapshots[snapindex]) ---------- config : string Full path of the config.yaml file as a string. @@ -598,18 +599,19 @@ def return_evaluate_network_data( integers specifying shuffle index of the training dataset. The default is 0. trainingsetindex: int, optional - Integer specifying which TrainingsetFraction to use. By default the first (note that TrainingFraction is a list in config.yaml). This - variable can also be set to "all". + Integer specifying which TrainingsetFraction to use. By default the first (note that TrainingFraction is a list in config.yaml). + This variable can also be set to "all". comparisonbodyparts: list of bodyparts, Default is "all". The average error will be computed for those body parts only (Has to be a subset of the body parts). rescale: bool, default False - Evaluate the model at the 'global_scale' variable (as set in the test/pose_config.yaml file for a particular project). I.e. every - image will be resized according to that scale and prediction will be compared to the resized ground truth. The error will be reported - in pixels at rescaled to the *original* size. I.e. For a [200,200] pixel image evaluated at global_scale=.5, the predictions are calculated - on [100,100] pixel images, compared to 1/2*ground truth and this error is then multiplied by 2!. The evaluation images are also shown for the - original size! + Evaluate the model at the 'global_scale' variable (as set in the test/pose_config.yaml file for a particular project). + I.e. every image will be resized according to that scale and prediction will be compared to the resized ground truth. + The error will be reported in pixels at rescaled to the *original* size. + I.e. For a [200,200] pixel image evaluated at global_scale=.5, the predictions are calculated + on [100,100] pixel images, compared to 1/2*ground truth and this error is then multiplied by 2!. + The evaluation images are also shown for the original size! engine: Engine, optional, default = None. The default behavior loads the engine for the shuffle from the metadata. You can @@ -1422,23 +1424,28 @@ def convert_detections2tracklets( Full path of the config.yaml file as a string. videos : list - A list of strings containing the full paths to videos for analysis or a path to the directory, where all the videos with same extension are stored. + A list of strings containing the full paths to videos for analysis or a path to the directory, + where all the videos with same extension are stored. videotype: string, optional - Checks for the extension of the video in case the input to the video is a directory.\n Only videos with this extension are analyzed. + Checks for the extension of the video in case the input to the video is a directory.\n + Only videos with this extension are analyzed. If left unspecified, videos with common extensions ('avi', 'mp4', 'mov', 'mpeg', 'mkv') are kept. shuffle: int, optional - An integer specifying the shuffle index of the training dataset used for training the network. The default is 1. + An integer specifying the shuffle index of the training dataset used for training the network. T + he default is 1. trainingsetindex: int, optional - Integer specifying which TrainingsetFraction to use. By default the first (note that TrainingFraction is a list in config.yaml). + Integer specifying which TrainingsetFraction to use. + By default the first (note that TrainingFraction is a list in config.yaml). overwrite: bool, optional. Overwrite tracks file i.e. recompute tracks from full detections and overwrite. destfolder: string, optional - Specifies the destination folder for analysis data (default is the path of the video). Note that for subsequent analysis this + Specifies the destination folder for analysis data (default is the path of the video). + Note that for subsequent analysis this folder also needs to be passed. ignore_bodyparts: optional @@ -1591,11 +1598,12 @@ def extract_maps( https://pytorch.org/docs/stable/notes/cuda.html for more information. rescale: bool, default False - Evaluate the model at the 'global_scale' variable (as set in the test/pose_config.yaml file for a particular project). I.e. every - image will be resized according to that scale and prediction will be compared to the resized ground truth. The error will be reported - in pixels at rescaled to the *original* size. I.e. For a [200,200] pixel image evaluated at global_scale=.5, the predictions are calculated - on [100,100] pixel images, compared to 1/2*ground truth and this error is then multiplied by 2!. The evaluation images are also shown for the - original size! + Evaluate the model at the 'global_scale' variable (as set in the test/pose_config.yaml file for a particular project). + I.e. every image will be resized according to that scale and prediction will be compared to the resized ground truth. + The error will be reported in pixels at rescaled to the *original* size. + I.e. For a [200,200] pixel image evaluated at global_scale=.5, the predictions are calculated + on [100,100] pixel images, compared to 1/2*ground truth and this error is then multiplied by 2!. + The evaluation images are also shown for the original size! engine: Engine, optional, default = None. The default behavior loads the engine for the shuffle from the metadata. You can @@ -1729,8 +1737,9 @@ def extract_save_all_maps( integers specifying shuffle index of the training dataset. The default is 1. trainingsetindex: int, optional - Integer specifying which TrainingsetFraction to use. By default the first (note that TrainingFraction is a list in config.yaml). This - variable can also be set to "all". + Integer specifying which TrainingsetFraction to use. + By default the first (note that TrainingFraction is a list in config.yaml). + This variable can also be set to "all". comparisonbodyparts: list of bodyparts, Default is "all". The average error will be computed for those body parts only (Has to be a subset of the body parts). diff --git a/deeplabcut/generate_training_dataset/trainingsetmanipulation.py b/deeplabcut/generate_training_dataset/trainingsetmanipulation.py index 440fff8a1d..43b73b010e 100755 --- a/deeplabcut/generate_training_dataset/trainingsetmanipulation.py +++ b/deeplabcut/generate_training_dataset/trainingsetmanipulation.py @@ -71,7 +71,8 @@ def adddatasetstovideolistandviceversa(config): It corrects this problem in the following way: If a video entry in the config file does not contain a folder in labeled-data, then the entry is removed. - If a folder in labeled-data does not contain a video entry in the config file then the prefix path will be added in front of the name of the labeled-data folder and combined + If a folder in labeled-data does not contain a video entry in the config file then + the prefix path will be added in front of the name of the labeled-data folder and combined with the suffix variable as an ending. Width and height will be added as cropping variables as passed on. Handle with care! @@ -510,7 +511,9 @@ def merge_annotateddatasets(cfg, trainingsetfolder_full): conversioncode.guarantee_multiindex_rows(data) if data.columns.levels[0][0] != cfg["scorer"]: print( - f"{file_path} labeled by a different scorer. This data will not be utilized in training dataset creation. If you need to merge datasets across scorers, see https://github.com/DeepLabCut/DeepLabCut/wiki/Using-labeled-data-in-DeepLabCut-that-was-annotated-elsewhere-(or-merge-across-labelers)" + f"{file_path} labeled by a different scorer. This data will not be utilized in training dataset creation." + "If you need to merge datasets across scorers, see " + "https://github.com/DeepLabCut/DeepLabCut/wiki/Using-labeled-data-in-DeepLabCut-that-was-annotated-elsewhere-(or-merge-across-labelers)" ) continue AnnotationData.append(data) @@ -647,7 +650,8 @@ def mergeandsplit(config, trainindex=0, uniform=True): To freeze a (uniform) split (i.e. iid sampled from all the data): >>> trainIndices, testIndices=deeplabcut.mergeandsplit(config,trainindex=0,uniform=True) - You can then create two model instances that have the identical trainingset. Thereby you can assess the role of various parameters on the performance of DLC. + You can then create two model instances that have the identical trainingset. + Thereby you can assess the role of various parameters on the performance of DLC. >>> deeplabcut.create_training_dataset(config,Shuffles=[0,1],trainIndices=[trainIndices, trainIndices],testIndices=[testIndices, testIndices]) -------- """ @@ -1116,11 +1120,14 @@ def create_training_dataset( ) if trainposeconfigfile.is_file(): askuser = input( - "The model folder is already present. If you continue, it will overwrite the existing model (split). Do you want to continue?(yes/no): " + "The model folder is already present. " + "If you continue, it will overwrite the existing model (split). " + "Do you want to continue?(yes/no): " ) if askuser == "no" or askuser == "No" or askuser == "N" or askuser == "No": raise Exception( - "Use the Shuffles argument as a list to specify a different shuffle index. Check out the help for more details." + "Use the Shuffles argument as a list to specify a different shuffle index. " + "Check out the help for more details." ) #################################################### From d81bf5eb0eb957185da2efdbb9e132a7c3eb59db Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 15:22:26 +0100 Subject: [PATCH 58/80] Disable docformatter pre-commit; enforce E501 Comment out docformatter hooks in .pre-commit-config.yaml (with a note explaining it was disabled in favor of ruff) and remove docformatter entries. Also update pyproject.toml to stop ignoring E501 in ruff, so line-length violations will now be linted/enforced. --- .pre-commit-config.yaml | 27 ++++++++++++++++----------- pyproject.toml | 2 +- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fa301ded7b..80ec0a7bab 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,18 +35,23 @@ repos: - id: validate-pyproject stages: [pre-commit, manual] - - repo: https://github.com/PyCQA/docformatter - rev: v1.7.7 - hooks: - - id: docformatter - name: docformatter (fix) - args: [--wrap-descriptions=88, --wrap-summaries=88, --in-place, --black] - stages: [pre-commit] + # NOTE: @C-Achard 2026-03-18 disabled for now + # It had its use in introducing and enforcing linting, especially for docstrings + # but now ruff should be our de-facto linter. + # Only re-enable if we end up requiring large-scale docstring reformatting + # or we need some features from this in the future + # - repo: https://github.com/PyCQA/docformatter + # rev: v1.7.7 + # hooks: + # - id: docformatter + # name: docformatter (fix) + # args: [--wrap-descriptions=88, --wrap-summaries=88, --in-place, --black] + # stages: [pre-commit] - - id: docformatter - name: docformatter (ci) - args: [--wrap-descriptions=88, --wrap-summaries=88, --check, --black] - stages: [manual] + # - id: docformatter + # name: docformatter (ci) + # args: [--wrap-descriptions=88, --wrap-summaries=88, --check, --black] + # stages: [manual] - repo: https://github.com/astral-sh/ruff-pre-commit diff --git a/pyproject.toml b/pyproject.toml index 6a7cdfea81..af79bc20be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -136,7 +136,7 @@ line-length = 120 fix = true [tool.ruff.lint] select = [ "E", "F", "B", "I", "UP" ] -ignore = [ "E741", "E501", "B007" ] +ignore = [ "E741", "B007" ] [tool.ruff.lint.per-file-ignores] "__init__.py" = [ "F401", "E402" ] "deeplabcut/**/__init__.py" = [ "F403" ] From 467180bd869b3be6b475c14a3d4206a046fabfe8 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 15:26:45 +0100 Subject: [PATCH 59/80] Fix E501 --- .../core/evaluate.py | 53 +++++++++++++------ 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/deeplabcut/pose_estimation_tensorflow/core/evaluate.py b/deeplabcut/pose_estimation_tensorflow/core/evaluate.py index e387606f15..74fa483bc9 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/evaluate.py +++ b/deeplabcut/pose_estimation_tensorflow/core/evaluate.py @@ -53,7 +53,8 @@ def calculatepafdistancebounds(config, shuffle=0, trainingsetindex=0, modelprefi integers specifying shuffle index of the training dataset. The default is 0. trainingsetindex: int, optional - Integer specifying which TrainingsetFraction to use. By default the first (note that TrainingFraction is a list in config.yaml). This + Integer specifying which TrainingsetFraction to use. + By default the first (note that TrainingFraction is a list in config.yaml). This variable can also be set to "all". numdigits: number of digits to round for distances. @@ -208,7 +209,9 @@ def return_evaluate_network_data( Snapshots[snapindex],scale,net_type] If fulldata=True, also returns (the complete annotation and prediction array) - Returns list of: (DataMachine, Data, data, trainIndices, testIndices, trainFraction, DLCscorer,comparisonbodyparts, cfg, Snapshots[snapindex]) + Returns list of: + (DataMachine, Data, data, trainIndices, testIndices, trainFraction, + DLCscorer,comparisonbodyparts, cfg, Snapshots[snapindex]) ---------- config : string Full path of the config.yaml file as a string. @@ -217,18 +220,22 @@ def return_evaluate_network_data( integers specifying shuffle index of the training dataset. The default is 0. trainingsetindex: int, optional - Integer specifying which TrainingsetFraction to use. By default the first (note that TrainingFraction is a list in config.yaml). This - variable can also be set to "all". + Integer specifying which TrainingsetFraction to use. + By default the first (note that TrainingFraction is a list in config.yaml). + This variable can also be set to "all". comparisonbodyparts: list of bodyparts, Default is "all". The average error will be computed for those body parts only (Has to be a subset of the body parts). rescale: bool, default False - Evaluate the model at the 'global_scale' variable (as set in the test/pose_config.yaml file for a particular project). I.e. every - image will be resized according to that scale and prediction will be compared to the resized ground truth. The error will be reported - in pixels at rescaled to the *original* size. I.e. For a [200,200] pixel image evaluated at global_scale=.5, the predictions are calculated - on [100,100] pixel images, compared to 1/2*ground truth and this error is then multiplied by 2!. The evaluation images are also shown for the - original size! + Evaluate the model at the 'global_scale' variable + (as set in the test/pose_config.yaml file for a particular project). + I.e. every image will be resized according to that scale and + prediction will be compared to the resized ground truth. The error will be reported + in pixels at rescaled to the *original* size. + I.e. For a [200,200] pixel image evaluated at global_scale=.5, the predictions are calculated + on [100,100] pixel images, compared to 1/2*ground truth and this error is then multiplied by 2!. + The evaluation images are also shown for the original size! Examples -------- @@ -250,7 +257,12 @@ def return_evaluate_network_data( # Loading human annotatated data trainingsetfolder = auxiliaryfunctions.get_training_set_folder(cfg) - # Data=pd.read_hdf(os.path.join(cfg["project_path"],str(trainingsetfolder),'CollectedData_' + cfg["scorer"] + '.h5'),'df_with_missing') + # Data=pd.read_hdf( + # os.path.join( + # cfg["project_path"], + # str(trainingsetfolder + # ),'CollectedData_' + cfg["scorer"] + '.h5'),'df_with_missing' + # ) # Get list of body parts to evaluate network for comparisonbodyparts = auxiliaryfunctions.intersection_of_body_parts_and_ones_given_by_user(cfg, comparisonbodyparts) @@ -682,7 +694,8 @@ def evaluate_network( ) modelfolder = Path(cfg["project_path"]) / modelfolder_rel_path - # TODO: Unlike using create_training_dataset() If create_training_model_comparison() is used there won't + # TODO: Unlike using create_training_dataset() + # If create_training_model_comparison() is used there won't # necessarily be training fractions for every shuffle which will raise the FileNotFoundError.. # Not sure if this should throw an exception or just be a warning... if not modelfolder.exists(): @@ -884,11 +897,12 @@ def evaluate_network( ) if scale != 1: print( - "The predictions have been calculated for rescaled images (and rescaled ground truth). Scale:", - scale, + "The predictions have been calculated for" + f" rescaled images (and rescaled ground truth). Scale: {scale}" ) print( - "Thereby, the errors are given by the average distances between the labels by DLC and the scorer." + "Thereby, the errors are given by the average distances " + "between the labels by DLC and the scorer." ) if plotting: @@ -920,7 +934,9 @@ def evaluate_network( ) if not os.path.exists(foldername): print( - "Plotting...(attention scale might be inconsistent in comparison to when data was analyzed; i.e. if you used rescale)" + "Plotting..." + "(warning, scale might be inconsistent in comparison " + "to when data was analyzed; i.e. if you used rescale)" ) auxiliaryfunctions.attempt_to_make_folder(foldername) Plotting( @@ -940,10 +956,13 @@ def evaluate_network( "The network is evaluated and the results are stored in the subdirectory 'evaluation_results'." ) print( - "Please check the results, then choose the best model (snapshot) for prediction. You can update the config.yaml file with the appropriate index for the 'snapshotindex'.\nUse the function 'analyze_video' to make predictions on new videos." + "Please check the results, then choose the best model (snapshot) for prediction. " + "You can update the config.yaml file with the appropriate index for the 'snapshotindex'.\n" + "Use the function 'analyze_video' to make predictions on new videos." ) print( - "Otherwise, consider adding more labeled-data and retraining the network (see DeepLabCut workflow Fig 2, Nath 2019)" + "Otherwise, consider adding more labeled-data and retraining the network " + "(see DeepLabCut workflow Fig 2, Nath 2019)" ) # returning to initial folder From 9ecd4c7f2219cea153cee77834aa325de5b1f875 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 15:29:57 +0100 Subject: [PATCH 60/80] Fix E501 --- deeplabcut/pose_estimation_3d/plotting3D.py | 50 +++++++++++++++------ 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/deeplabcut/pose_estimation_3d/plotting3D.py b/deeplabcut/pose_estimation_3d/plotting3D.py index b28d82b3ea..6cfd430116 100644 --- a/deeplabcut/pose_estimation_3d/plotting3D.py +++ b/deeplabcut/pose_estimation_3d/plotting3D.py @@ -81,38 +81,56 @@ def create_labeled_video_3d( Full path of the config.yaml file as a string. path : list - A list of strings containing the full paths to triangulated files for analysis or a path to the directory, where all the triangulated files are stored. + A list of strings containing the full paths to triangulated files for analysis or a path to the directory, + where all the triangulated files are stored. videofolder: string - Full path of the folder where the videos are stored. Use this if the videos are stored in a different location other than where the triangulation files are stored. By default is ``None`` and therefore looks for video files in the directory where the triangulation file is stored. + Full path of the folder where the videos are stored. + Use this if the videos are stored in a different location other than + where the triangulation files are stored. + By default is ``None`` and therefore looks for video files in the + directory where the triangulation file is stored. start: int - Integer specifying the start of frame index to select. Default is set to 0. + Integer specifying the start of frame index to select. + Default is set to 0. end: int - Integer specifying the end of frame index to select. Default is set to None, where all the frames of the video are used for creating the labeled video. + Integer specifying the end of frame index to select. + Default is set to None, where all the frames of the video are used for creating the labeled video. trailpoints: int - Number of revious frames whose body parts are plotted in a frame (for displaying history). Default is set to 0. + Number of revious frames whose body parts are plotted in a frame (for displaying history). + Default is set to 0. videotype: string, optional - Checks for the extension of the video in case the input to the video is a directory.\n Only videos with this extension are analyzed. + Checks for the extension of the video in case the input to the video is a directory.\n + Only videos with this extension are analyzed. If left unspecified, videos with common extensions ('avi', 'mp4', 'mov', 'mpeg', 'mkv') are kept. view: list - A list that sets the elevation angle in z plane and azimuthal angle in x,y plane of 3d view. Useful for rotating the axis for 3d view + A list that sets the elevation angle in z plane and azimuthal angle in x,y plane of 3d view. + Useful for rotating the axis for 3d view xlim: list - A list of integers specifying the limits for xaxis of 3d view. By default it is set to [None,None], where the x limit is set by taking the minimum and maximum value of the x coordinates for all the bodyparts. + A list of integers specifying the limits for xaxis of 3d view. + By default it is set to [None,None], where the x limit is set by t + aking the minimum and maximum value of the x coordinates for all the bodyparts. ylim: list - A list of integers specifying the limits for yaxis of 3d view. By default it is set to [None,None], where the y limit is set by taking the minimum and maximum value of the y coordinates for all the bodyparts. + A list of integers specifying the limits for yaxis of 3d view. + By default it is set to [None,None], where the y limit is set by + taking the minimum and maximum value of the y coordinates for all the bodyparts. zlim: list - A list of integers specifying the limits for zaxis of 3d view. By default it is set to [None,None], where the z limit is set by taking the minimum and maximum value of the z coordinates for all the bodyparts. + A list of integers specifying the limits for zaxis of 3d view. + By default it is set to [None,None], where the z limit is set by + taking the minimum and maximum value of the z coordinates for all the bodyparts. draw_skeleton: bool - If ``True`` adds a line connecting the body parts making a skeleton on on each frame. The body parts to be connected and the color of these connecting lines are specified in the config file. By default: ``True`` + If ``True`` adds a line connecting the body parts making a skeleton on on each frame. + The body parts to be connected and the color of these connecting lines are specified in the config file. + By default: ``True`` color_by : string, optional (default='bodypart') Coloring rule. By default, each bodypart is colored differently. @@ -127,7 +145,8 @@ def create_labeled_video_3d( >>> deeplabcut.create_labeled_video_3d(config,['/data/project1/videos'],start=100, end=500) To set the xlim, ylim, zlim and rotate the view of the 3d axis - >>> deeplabcut.create_labeled_video_3d(config,['/data/project1/videos'],start=100, end=500,view=[30,90],xlim=[-12,12],ylim=[15,25],zlim=[20,30]) + >>> deeplabcut.create_labeled_video_3d(config,['/data/project1/videos'],start=100, + end=500,view=[30,90],xlim=[-12,12],ylim=[15,25],zlim=[20,30]) """ os.getcwd() @@ -151,7 +170,9 @@ def create_labeled_video_3d( print(file_list) if file_list == []: raise Exception( - "No corresponding video file(s) found for the specified triangulated file or folder. Did you specify the video file type? If videos are stored in a different location, please use the ``videofolder`` argument to specify their path." + "No corresponding video file(s) found for the specified triangulated file or folder. " + "Did you specify the video file type? If videos are stored in a different location, " + "please use the ``videofolder`` argument to specify their path." ) for file in file_list: @@ -175,7 +196,8 @@ def create_labeled_video_3d( cam1_scorer = metadata_["scorer_name"][cam_names[0]] cam2_scorer = metadata_["scorer_name"][cam_names[1]] print( - f"Creating 3D video from {Path(cam1_view_video).name} and {Path(cam2_view_video).name} using {Path(triangulate_file).name}" + f"Creating 3D video from {Path(cam1_view_video).name} " + f"and {Path(cam2_view_video).name} using {Path(triangulate_file).name}" ) # Read the video files and corresponfing h5 files From 390cccd2ed1a45647c3e6394b06074c26ac984fc Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 15:40:41 +0100 Subject: [PATCH 61/80] Fix E501 --- deeplabcut/create_project/modelzoo.py | 37 +++++++++++++++++++-------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/deeplabcut/create_project/modelzoo.py b/deeplabcut/create_project/modelzoo.py index 9e084e557b..ee877d6f16 100644 --- a/deeplabcut/create_project/modelzoo.py +++ b/deeplabcut/create_project/modelzoo.py @@ -98,7 +98,9 @@ def create_pretrained_human_project( Please make sure to cite it too if you use this code! """ print( - "LEGACY FUNCTION will be deprecated.... use deeplabcut.create_pretrained_project(project, experimenter, videos, model='full_human', ..) in the future!" + "LEGACY FUNCTION will be deprecated.... " + "use deeplabcut.create_pretrained_project(project, experimenter, videos, model='full_human', ..) " + "in the future!" ) create_pretrained_project( project, @@ -201,13 +203,17 @@ def create_pretrained_project( Example -------- Linux/MacOs loading full_human model and analyzing video /homosapiens1.avi - >>> deeplabcut.create_pretrained_project("humanstrokestudy", "Linus", ["/data/videos/homosapiens1.avi"], copy_videos=False) + >>> deeplabcut.create_pretrained_project("humanstrokestudy", "Linus", + ... ["/data/videos/homosapiens1.avi"], copy_videos=False) Loading full_cat model and analyzing video "felixfeliscatus3.avi" - >>> deeplabcut.create_pretrained_project("humanstrokestudy", "Linus", ["/data/videos/felixfeliscatus3.avi"], model="full_cat", engine=Engine.TF) + >>> deeplabcut.create_pretrained_project("humanstrokestudy", "Linus", + ... ["/data/videos/felixfeliscatus3.avi"], model="full_cat", engine=Engine.TF) Windows: - >>> deeplabcut.create_pretrained_project("humanstrokestudy", "Bill", [r'C:\yourusername\rig-95\Videos\reachingvideo1.avi'], r'C:\yourusername\analysis\project', copy_videos=True) + >>> deeplabcut.create_pretrained_project("humanstrokestudy", "Bill", + ... [r'C:\yourusername\rig-95\Videos\reachingvideo1.avi'], + ... r'C:\yourusername\analysis\project', copy_videos=True) Users must format paths with either: r'C:\ OR 'C:\\ <- i.e. a double backslash \ \ ) """ if engine == Engine.TF: @@ -330,13 +336,17 @@ def create_pretrained_project_pytorch( Example -------- Linux/MacOs loading full_human model and analyzing video /homosapiens1.avi - >>> deeplabcut.create_pretrained_project_pytorch("humanstrokestudy", "Linus", ["/data/videos/homosapiens1.avi"], copy_videos=False) + >>> deeplabcut.create_pretrained_project_pytorch("humanstrokestudy", "Linus", + ... ["/data/videos/homosapiens1.avi"], copy_videos=False) Loading full_cat model and analyzing video "felixfeliscatus3.avi" - >>> deeplabcut.create_pretrained_project_pytorch("humanstrokestudy", "Linus", ["/data/videos/felixfeliscatus3.avi"], model="full_cat", engine=Engine.TF) + >>> deeplabcut.create_pretrained_project_pytorch("humanstrokestudy", "Linus", + ... ["/data/videos/felixfeliscatus3.avi"], model="full_cat", engine=Engine.TF) Windows: - >>> deeplabcut.create_pretrained_project_pytorch("humanstrokestudy", "Bill", [r'C:\yourusername\rig-95\Videos\reachingvideo1.avi'], r'C:\yourusername\analysis\project', copy_videos=True) + >>> deeplabcut.create_pretrained_project_pytorch("humanstrokestudy", + ... "Bill", [r'C:\yourusername\rig-95\Videos\reachingvideo1.avi'], + ... r'C:\yourusername\analysis\project', copy_videos=True) Users must format paths with either: r'C:\ OR 'C:\\ <- i.e. a double backslash \ \ ) """ # Check arguments @@ -529,13 +539,17 @@ def create_pretrained_project_tensorflow( Example -------- Linux/MacOs loading full_human model and analyzing video /homosapiens1.avi - >>> deeplabcut.create_pretrained_project_tensorflow("humanstrokestudy", "Linus", ["/data/videos/homosapiens1.avi"], copy_videos=False) + >>> deeplabcut.create_pretrained_project_tensorflow("humanstrokestudy", + ... "Linus", ["/data/videos/homosapiens1.avi"], copy_videos=False) Loading full_cat model and analyzing video "felixfeliscatus3.avi" - >>> deeplabcut.create_pretrained_project_tensorflow("humanstrokestudy", "Linus", ["/data/videos/felixfeliscatus3.avi"], model="full_cat", engine=Engine.TF) + >>> deeplabcut.create_pretrained_project_tensorflow("humanstrokestudy", + ... "Linus", ["/data/videos/felixfeliscatus3.avi"], model="full_cat", engine=Engine.TF) Windows: - >>> deeplabcut.create_pretrained_project_tensorflow("humanstrokestudy", "Bill", [r'C:\yourusername\rig-95\Videos\reachingvideo1.avi'], r'C:\yourusername\analysis\project', copy_videos=True) + >>> deeplabcut.create_pretrained_project_tensorflow("humanstrokestudy", + ... "Bill", [r'C:\yourusername\rig-95\Videos\reachingvideo1.avi'], + ... r'C:\yourusername\analysis\project', copy_videos=True) Users must format paths with either: r'C:\ OR 'C:\\ <- i.e. a double backslash \ \ ) """ if not model: @@ -642,7 +656,8 @@ def create_pretrained_project_tensorflow( } auxiliaryfunctions.edit_config(cfg, dict_) - # downloading base encoder / not required unless on re-trains (but when a training set is created this happens anyway) + # downloading base encoder / not required unless on re-trains + # (but when a training set is created this happens anyway) # model_path = auxfun_models.check_for_weights(pose_cfg['net_type'], parent_path) # Updating training and test pose_cfg: From 79b715866fb9cbdd6dbdcd37a2aaaf19b467e193 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 15:43:14 +0100 Subject: [PATCH 62/80] Use FileLock and sentinel for test data setup Serialize test-data download using filelock and a sentinel file to avoid concurrent downloads. pytest_sessionstart now acquires LOCK_FILE and only unzips data if TEST_DATA_DIR and TEST_DATA_SENTINEL are not present. Removed session teardown that deleted test data and the unused shutil import. Also added tests/data/* to .gitignore. --- .gitignore | 3 +++ tests/conftest.py | 21 ++++++++++++--------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index cf82bfb27e..f9e72e1a5b 100644 --- a/.gitignore +++ b/.gitignore @@ -141,3 +141,6 @@ ENV/ # Tools output tmp/* + +# Test data +tests/data/* diff --git a/tests/conftest.py b/tests/conftest.py index 0d386336b7..5020894bdf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,19 +10,21 @@ # import os import pickle -import shutil import urllib.request import zipfile from io import BytesIO import numpy as np import pytest +from filelock import FileLock from PIL import Image from tqdm import tqdm from deeplabcut.core import inferenceutils TEST_DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data") +TEST_DATA_SENTINEL = os.path.join(TEST_DATA_DIR, ".download_complete") +LOCK_FILE = os.path.join(os.path.dirname(TEST_DATA_DIR), ".test_data.lock") def unzip_from_url(url, dest_folder): @@ -37,16 +39,17 @@ def unzip_from_url(url, dest_folder): pass -def pytest_sessionstart(session): - unzip_from_url( - "https://github.com/DeepLabCut/UnitTestData/raw/main/data.zip", - os.path.split(TEST_DATA_DIR)[0], - ) - session.__DATA_FOLDER = TEST_DATA_DIR +def _test_data_ready() -> bool: + return os.path.isdir(TEST_DATA_DIR) and os.path.isfile(TEST_DATA_SENTINEL) -def pytest_sessionfinish(session, exitstatus): - shutil.rmtree(session.__DATA_FOLDER) +def pytest_sessionstart(session): + with FileLock(LOCK_FILE): + if not _test_data_ready(): + unzip_from_url( + "https://github.com/DeepLabCut/UnitTestData/raw/main/data.zip", + os.path.split(TEST_DATA_DIR)[0], + ) @pytest.fixture(scope="function") From b812122d21306c2ffca0c11c16cdfdc2aa1548fa Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 16:11:16 +0100 Subject: [PATCH 63/80] Use autouse session fixture to ensure test data Replace the previous pytest_sessionstart + FileLock approach with an autouse, session-scoped fixture (ensure_test_data) that ensures required test files exist before tests run. Introduce TESTS_DIR and REQUIRED_TEST_FILES, update _test_data_ready to check for those files, and adjust unzip_from_url signature with type hints. This simplifies test-data setup, removes the file lock/sentinel mechanism, and ensures test data is extracted to the tests directory when missing. --- tests/conftest.py | 44 +++++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 5020894bdf..30bd45364d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,6 +8,7 @@ # # Licensed under GNU Lesser General Public License v3.0 # + import os import pickle import urllib.request @@ -16,18 +17,25 @@ import numpy as np import pytest -from filelock import FileLock from PIL import Image from tqdm import tqdm from deeplabcut.core import inferenceutils -TEST_DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data") -TEST_DATA_SENTINEL = os.path.join(TEST_DATA_DIR, ".download_complete") -LOCK_FILE = os.path.join(os.path.dirname(TEST_DATA_DIR), ".test_data.lock") +TESTS_DIR = os.path.dirname(os.path.realpath(__file__)) +TEST_DATA_DIR = os.path.join(TESTS_DIR, "data") + +REQUIRED_TEST_FILES = [ + os.path.join(TEST_DATA_DIR, "dets.pickle"), + os.path.join(TEST_DATA_DIR, "outputs.pickle"), + os.path.join(TEST_DATA_DIR, "image.png"), + os.path.join(TEST_DATA_DIR, "trimouse_assemblies.pickle"), + os.path.join(TEST_DATA_DIR, "montblanc_tracks.h5"), + os.path.join(TEST_DATA_DIR, "trimouse_calib.h5"), +] -def unzip_from_url(url, dest_folder): +def unzip_from_url(url: str, dest_folder: str) -> None: """Directly extract files without writing the archive to disk.""" os.makedirs(dest_folder, exist_ok=True) resp = urllib.request.urlopen(url) @@ -40,16 +48,22 @@ def unzip_from_url(url, dest_folder): def _test_data_ready() -> bool: - return os.path.isdir(TEST_DATA_DIR) and os.path.isfile(TEST_DATA_SENTINEL) - - -def pytest_sessionstart(session): - with FileLock(LOCK_FILE): - if not _test_data_ready(): - unzip_from_url( - "https://github.com/DeepLabCut/UnitTestData/raw/main/data.zip", - os.path.split(TEST_DATA_DIR)[0], - ) + return all(os.path.exists(path) for path in REQUIRED_TEST_FILES) + + +@pytest.fixture(scope="session", autouse=True) +def ensure_test_data(): + """Ensure shared test data exists once per pytest session. + + This is autouse so tests that directly open files under tests/data/ + keep working without being rewritten. + """ + if not _test_data_ready(): + unzip_from_url( + "https://github.com/DeepLabCut/UnitTestData/raw/main/data.zip", + TESTS_DIR, + ) + yield @pytest.fixture(scope="function") From 50a1faaa74de99a3fc39e6b61d8578270b63cd86 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 16:14:31 +0100 Subject: [PATCH 64/80] Fix E501 --- .../pose_estimation_3d/triangulation.py | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/deeplabcut/pose_estimation_3d/triangulation.py b/deeplabcut/pose_estimation_3d/triangulation.py index b94fc20fca..51ea6cb5ae 100644 --- a/deeplabcut/pose_estimation_3d/triangulation.py +++ b/deeplabcut/pose_estimation_3d/triangulation.py @@ -46,7 +46,8 @@ def triangulate( i.e. [['video1-camera-1.avi','video1-camera-2.avi']] videotype: string, optional - Checks for the extension of the video in case the input to the video is a directory.\n Only videos with this extension are analyzed. + Checks for the extension of the video in case the input to the video is a directory.\n + Only videos with this extension are analyzed. If left unspecified, videos with common extensions ('avi', 'mp4', 'mov', 'mpeg', 'mkv') are kept. @@ -74,7 +75,9 @@ def triangulate( >>> deeplabcut.triangulate(config,'/data/project1/videos/') To analyze only a few pairs of videos: - >>> deeplabcut.triangulate(config,[['/data/project1/videos/video1-camera-1.avi','/data/project1/videos/video1-camera-2.avi'],['/data/project1/videos/video2-camera-1.avi','/data/project1/videos/video2-camera-2.avi']]) + >>> deeplabcut.triangulate(config,[['/data/project1/videos/video1-camera-1.avi', + ... '/data/project1/videos/video1-camera-2.avi'],['/data/project1/videos/video2-camera-1.avi', + ... '/data/project1/videos/video2-camera-2.avi']]) Windows @@ -82,7 +85,10 @@ def triangulate( >>> deeplabcut.triangulate(config,'C:\\yourusername\\rig-95\\Videos') To analyze only a few pair of videos: - >>> deeplabcut.triangulate(config,[['C:\\yourusername\\rig-95\\Videos\\video1-camera-1.avi','C:\\yourusername\\rig-95\\Videos\\video1-camera-2.avi'],['C:\\yourusername\\rig-95\\Videos\\video2-camera-1.avi','C:\\yourusername\\rig-95\\Videos\\video2-camera-2.avi']]) + >>> deeplabcut.triangulate(config,[['C:\\yourusername\\rig-95\\Videos\\video1-camera-1.avi', + ... 'C:\\yourusername\\rig-95\\Videos\\video1-camera-2.avi'], + ... ['C:\\yourusername\\rig-95\\Videos\\video2-camera-1.avi', + ... 'C:\\yourusername\\rig-95\\Videos\\video2-camera-2.avi']]) """ from deeplabcut.compat import analyze_videos from deeplabcut.post_processing import filtering @@ -113,7 +119,8 @@ def triangulate( if video_list == []: print("No videos found in the specified video path.", video_path) print( - "Please make sure that the video names are specified with correct camera names as entered in the config file or" + "Please make sure that the video names are specified with" + " correct camera names as entered in the config file or" ) print( "perhaps the videotype is distinct from the videos in the path, I was looking for:", @@ -185,7 +192,8 @@ def triangulate( pd.read_hdf(output_filename + ".h5").to_csv(str(output_filename + ".csv")) print( - "Already analyzed...Checking the meta data for any change in the camera matrices and/or scorer names", + "Already analyzed..." + "Checking the meta data for any change in the camera matrices and/or scorer names", vname, ) pickle_file = str(output_filename + "_meta.pickle") @@ -293,14 +301,17 @@ def triangulate( ) = undistort_points(config, dataname, str(cam_names[0] + "-" + cam_names[1])) if len(dataFrame_camera1_undistort) != len(dataFrame_camera2_undistort): warnings.warn( - "The number of frames do not match in the two videos. Please make sure that your videos have same number of frames and then retry! Excluding the extra frames from the longer video.", + "The number of frames do not match in the two videos. " + "Please make sure that your videos have same number of frames and then retry! " + "Excluding the extra frames from the longer video.", stacklevel=2, ) if len(dataFrame_camera1_undistort) > len(dataFrame_camera2_undistort): dataFrame_camera1_undistort = dataFrame_camera1_undistort[: len(dataFrame_camera2_undistort)] if len(dataFrame_camera2_undistort) > len(dataFrame_camera1_undistort): dataFrame_camera2_undistort = dataFrame_camera2_undistort[: len(dataFrame_camera1_undistort)] - # raise Exception("The number of frames do not match in the two videos. Please make sure that your videos have same number of frames and then retry!") + # raise Exception("The number of frames do not match in the two videos. + # Please make sure that your videos have same number of frames and then retry!") dataFrame_camera1_undistort.columns.get_level_values(0)[0] dataFrame_camera2_undistort.columns.get_level_values(0)[0] @@ -495,7 +506,8 @@ def undistort_points(config, dataframe, camera_pair): #currently no intermediate saving of this due to high speed. # check if the undistorted files are already present - if os.path.exists(os.path.join(path_undistort,filename_cam1 + '_undistort.h5')) and os.path.exists(os.path.join(path_undistort,filename_cam2 + '_undistort.h5')): + if os.path.exists(os.path.join(path_undistort,filename_cam1 + \ + '_undistort.h5')) and os.path.exists(os.path.join(path_undistort,filename_cam2 + '_undistort.h5')): print("The undistorted files are already present at %s" % os.path.join(path_undistort,filename_cam1)) dataFrame_cam1_undistort = pd.read_hdf(os.path.join(path_undistort,filename_cam1 + '_undistort.h5')) dataFrame_cam2_undistort = pd.read_hdf(os.path.join(path_undistort,filename_cam2 + '_undistort.h5')) @@ -503,7 +515,8 @@ def undistort_points(config, dataframe, camera_pair): """ if len(dataframe) != 2: raise ValueError( - f"undistort_points(config, dataframe, camera_pair) needs filenames to two data frames, but got dataframe={dataframe}." + "undistort_points(config, dataframe, camera_pair) " + f"needs filenames to two data frames, but got dataframe={dataframe}." ) for filename in dataframe: if not os.path.exists(filename): From 19ba36372178b0cf81eed1a8fd8c729bc17daf12 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 16:18:01 +0100 Subject: [PATCH 65/80] Update trainingsetmanipulation.py --- .../trainingsetmanipulation.py | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/deeplabcut/generate_training_dataset/trainingsetmanipulation.py b/deeplabcut/generate_training_dataset/trainingsetmanipulation.py index 43b73b010e..6fff51d78d 100755 --- a/deeplabcut/generate_training_dataset/trainingsetmanipulation.py +++ b/deeplabcut/generate_training_dataset/trainingsetmanipulation.py @@ -498,7 +498,8 @@ def merge_annotateddatasets(cfg, trainingsetfolder_full): This is a bit of a mess because of cross platform compatibility. - Within platform comp. is straightforward. But if someone labels on windows and wants to train on a unix cluster or colab... + Within platform comp. is straightforward. + But if someone labels on windows and wants to train on a unix cluster or colab... """ AnnotationData = [] data_path = Path(os.path.join(cfg["project_path"], "labeled-data")) @@ -511,9 +512,11 @@ def merge_annotateddatasets(cfg, trainingsetfolder_full): conversioncode.guarantee_multiindex_rows(data) if data.columns.levels[0][0] != cfg["scorer"]: print( - f"{file_path} labeled by a different scorer. This data will not be utilized in training dataset creation." + f"{file_path} labeled by a different scorer. " + "This data will not be utilized in training dataset creation." "If you need to merge datasets across scorers, see " - "https://github.com/DeepLabCut/DeepLabCut/wiki/Using-labeled-data-in-DeepLabCut-that-was-annotated-elsewhere-(or-merge-across-labelers)" + "https://github.com/DeepLabCut/DeepLabCut/wiki/Using-labeled-data-in\ + -DeepLabCut-that-was-annotated-elsewhere-(or-merge-across-labelers)" ) continue AnnotationData.append(data) @@ -522,7 +525,8 @@ def merge_annotateddatasets(cfg, trainingsetfolder_full): if not len(AnnotationData): print( - "Annotation data was not found by splitting video paths (from config['video_sets']). An alternative route is taken..." + "Annotation data was not found by splitting video paths (from config['video_sets']). " + "An alternative route is taken..." ) AnnotationData = conversioncode.merge_windowsannotationdataONlinuxsystem(cfg) if not len(AnnotationData): @@ -621,10 +625,12 @@ def pad_train_test_indices(train_inds, test_inds, train_fraction): def mergeandsplit(config, trainindex=0, uniform=True): """This function allows additional control over "create_training_dataset". - Merge annotated data sets (from different folders) and split data in a specific way, returns the split variables (train/test indices). + Merge annotated data sets (from different folders) and split data in a specific way, + returns the split variables (train/test indices). Importantly, this allows one to freeze a split. - One can also either create a uniform split (uniform = True; thereby indexing TrainingFraction in config file) or leave-one-folder out split + One can also either create a uniform split (uniform = True; thereby indexing TrainingFraction in config file) + or leave-one-folder out split by passing the index of the corresponding video from the config.yaml file as variable trainindex. Parameter @@ -633,8 +639,10 @@ def mergeandsplit(config, trainindex=0, uniform=True): Full path of the config.yaml file as a string. trainindex: int, optional - Either (in case uniform = True) indexes which element of TrainingFraction in the config file should be used (note it is a list!). - Alternatively (uniform = False) indexes which folder is dropped, i.e. the first if trainindex=0, the second if trainindex =1, etc. + Either (in case uniform = True) indexes which element of TrainingFraction + in the config file should be used (note it is a list!). + Alternatively (uniform = False) indexes which folder is dropped, + i.e. the first if trainindex=0, the second if trainindex =1, etc. uniform: bool, optional Perform uniform split (disregarding folder structure in labeled data), or (if False) leave one folder out. @@ -643,7 +651,8 @@ def mergeandsplit(config, trainindex=0, uniform=True): -------- To create a leave-one-folder-out model: >>> trainIndices, testIndices=deeplabcut.mergeandsplit(config,trainindex=0,uniform=False) - returns the indices for the first video folder (as defined in config file) as testIndices and all others as trainIndices. + returns the indices for the first video folder (as defined in config file) + as testIndices and all others as trainIndices. You can then create the training set by calling (e.g. defining it as Shuffle 3): >>> deeplabcut.create_training_dataset(config,Shuffles=[3],trainIndices=trainIndices,testIndices=testIndices) @@ -652,7 +661,9 @@ def mergeandsplit(config, trainindex=0, uniform=True): You can then create two model instances that have the identical trainingset. Thereby you can assess the role of various parameters on the performance of DLC. - >>> deeplabcut.create_training_dataset(config,Shuffles=[0,1],trainIndices=[trainIndices, trainIndices],testIndices=[testIndices, testIndices]) + >>> deeplabcut.create_training_dataset( + ... config,Shuffles=[0,1],trainIndices=[trainIndices, trainIndices], + ... testIndices=[testIndices, testIndices]) -------- """ # Loading metadata from config file: @@ -1061,11 +1072,13 @@ def create_training_dataset( if posecfg_template: if net_type != prior_cfg["net_type"]: print( - "WARNING: Specified net_type does not match net_type from posecfg_template path entered. Proceed with caution." + "WARNING: Specified net_type does not match net_type from " + "posecfg_template path entered. Proceed with caution." ) if augmenter_type != prior_cfg["dataset_type"]: print( - "WARNING: Specified augmenter_type does not match dataset_type from posecfg_template path entered. Proceed with caution." + "WARNING: Specified augmenter_type does not match dataset_type " + "from posecfg_template path entered. Proceed with caution." ) # Loading the encoder (if necessary downloading from TF) From 60537282f5c9d233eab500974cca4f982514234e Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 16:22:04 +0100 Subject: [PATCH 66/80] Fix E501 --- deeplabcut/cli.py | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/deeplabcut/cli.py b/deeplabcut/cli.py index 07a89fd434..1daba042e9 100644 --- a/deeplabcut/cli.py +++ b/deeplabcut/cli.py @@ -158,7 +158,8 @@ def extract_frames(_, *args, **kwargs): for selecting frames automatically with 'kmeans' and do not want to crop the frames \n >>> python3 dlc.py extract_frames /analysis/project/reaching-task/config.yaml automatic --algo kmeans \n -------- \n - for selecting frames automatically with 'uniform' and want to crop the frames based on the ``crop`` parameters in config.yaml \n + for selecting frames automatically with 'uniform' and want to + crop the frames based on the ``crop`` parameters in config.yaml \n >>> python3 dlc.py extract_frames /analysis/project/reaching-task/config.yaml automatic --crop -------- \n for selecting frames manually, \n @@ -353,11 +354,14 @@ def analyze_videos(_, *args, **kwargs): "--outlier_algo", "outlieralgorithm", default="fitting", - help="String specifying the algorithm used to detect the outliers. Currently, deeplabcut supports only sarimax (this will be updated). \ - This method fits a Seasonal AutoRegressive Integrated Moving Average with eXogenous regressors model \ - to data and computes confidence interval. \ - Based on the fraction of data points outside the confidence interval and the average distance (compared to delta) \ - the user can identify potential outlier frames. The default is set to ``fitting``. Other choices: `fitting`, `jump`, `uncertain`", + help="String specifying the algorithm used to detect the outliers.\ + Currently, deeplabcut supports only sarimax (this will be updated). \ + This method fits a Seasonal AutoRegressive Integrated Moving Average with eXogenous regressors model \ + to data and computes confidence interval. \ + Based on the fraction of data points outside the confidence interval \ + and the average distance (compared to delta) \ + the user can identify potential outlier frames.\ + The default is set to ``fitting``. Other choices: `fitting`, `jump`, `uncertain`", ) @click.option( "-compare", @@ -366,7 +370,8 @@ def analyze_videos(_, *args, **kwargs): default="all", help="This select the body parts for which the comparisons with the outliers are carried out. Either ``all``, \ then all body parts from config.yaml are used orr a list of strings that are a subset of the full list.\ - E.g. [`hand`,`Joystick`] for the demo Reaching-Mackenzie-2018-08-30/config.yaml to select only these two body parts.", + E.g. [`hand`,`Joystick`]" + " for the demo Reaching-Mackenzie-2018-08-30/config.yaml to select only these two body parts.", ) @click.option( "-e", @@ -374,15 +379,18 @@ def analyze_videos(_, *args, **kwargs): "epsilon", default=20, help="Meaning depends on outlieralgoritm. The default is set to 20 pixels.For outlieralgorithm `fitting`: \ - Float bound according to which frames are picked when the (average) body part estimate deviates from model fit. \ - For outlieralgorithm `jump`: Float bound specifying the distance by which body points jump from one frame to next (Euclidean distance)", + Float bound according to which frames are picked when the (average)\ + body part estimate deviates from model fit. \ + For outlier algorithm `jump`:" + "Float bound specifying the distance by which body points jump from one frame to next (Euclidean distance)", ) @click.option( "-p", "--p_bound", "p_bound", default=0.01, - help="For outlieralgorithm `uncertain` this parameter defines the likelihood below, below which a body part will be flagged as a putative outlier.", + help="For outlieralgorithm `uncertain` this parameter defines the likelihood below, " + "below which a body part will be flagged as a putative outlier.", ) @click.option( "-ard", @@ -412,9 +420,11 @@ def analyze_videos(_, *args, **kwargs): "--extraction_algo", "extractionalgorithm", default="uniform", - help="String specifying the algorithm to use for selecting the frames from the identified outliers. \ - Currently, deeplabcut supports either ``kmeans`` or ``uniform`` based selection (same logic as for extract_frames).\ - The default is set to``uniform``, if provided it must be either ``uniform`` or ``kmeans``.", + help="String specifying the algorithm to use for selecting the frames from the identified outliers.\ + Currently, deeplabcut supports either ``kmeans`` or ``uniform``\ + based selection (same logic as for extract_frames).\ + The default is set to``uniform``,\ + if provided it must be either ``uniform`` or ``kmeans``.", ) @click.pass_context def extract_outlier_frames(_, *args, **kwargs): @@ -432,15 +442,16 @@ def extract_outlier_frames(_, *args, **kwargs): Example \n --------\n for extracting the frames with default settings\n - >>> python3 dlc.py extract_outlier_frames /analysis/project/reaching-task/config.yaml /analysis/project/video/reachinvideo1.avi \n + >>> python3 dlc.py extract_outlier_frames /analysis/project/reaching-task/config.yaml + ... /analysis/project/video/reachinvideo1.avi \n --------\n for extracting the frames with kmeans\n >>> python3 dlc.py extract_outlier_frames /analysis/project/reaching-task/config.yaml - /analysis/project/video/reachinvideo1.avi --extractionalgorithm 'kmeans' \n + ... /analysis/project/video/reachinvideo1.avi --extractionalgorithm 'kmeans' \n --------\n for extracting the frames with kmeans and epsilon = 5 pixels.\n >>> python3 dlc.py extract_outlier_frames /analysis/project/reaching-task/config.yaml - /analysis/project/video/reachinvideo1.avi --epsilon 5 --extractionalgorithm kmeans \n + ... /analysis/project/video/reachinvideo1.avi --epsilon 5 --extractionalgorithm kmeans \n --------\n """ from deeplabcut.refine_training_dataset import outlier_frames From e95c9913709f3952a6feb472fafd2afda10b4ae7 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 16:24:22 +0100 Subject: [PATCH 67/80] Fix E501 --- .../visualizemaps.py | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/deeplabcut/pose_estimation_tensorflow/visualizemaps.py b/deeplabcut/pose_estimation_tensorflow/visualizemaps.py index e96e4b4a0c..3cd8b3d6eb 100644 --- a/deeplabcut/pose_estimation_tensorflow/visualizemaps.py +++ b/deeplabcut/pose_estimation_tensorflow/visualizemaps.py @@ -32,7 +32,8 @@ def extract_maps( """Extracts the scoremap, locref, partaffinityfields (if available). Returns a dictionary indexed by: trainingsetfraction, snapshotindex, and imageindex - for those keys, each item contains: (image,scmap,locref,paf,bpt names,partaffinity graph, imagename, True/False if this image was in trainingset) + for those keys, each item contains: (image,scmap,locref,paf,bpt names,partaffinity graph, + imagename, True/False if this image was in trainingset) ---------- config : string Full path of the config.yaml file as a string. @@ -41,15 +42,19 @@ def extract_maps( integers specifying shuffle index of the training dataset. The default is 0. trainingsetindex: int, optional - Integer specifying which TrainingsetFraction to use. By default the first (note that TrainingFraction is a list in config.yaml). This - variable can also be set to "all". + Integer specifying which TrainingsetFraction to use. By default the first + (note that TrainingFraction is a list in config.yaml). + This variable can also be set to "all". rescale: bool, default False - Evaluate the model at the 'global_scale' variable (as set in the test/pose_config.yaml file for a particular project). I.e. every - image will be resized according to that scale and prediction will be compared to the resized ground truth. The error will be reported - in pixels at rescaled to the *original* size. I.e. For a [200,200] pixel image evaluated at global_scale=.5, the predictions are calculated - on [100,100] pixel images, compared to 1/2*ground truth and this error is then multiplied by 2!. The evaluation images are also shown for the - original size! + Evaluate the model at the 'global_scale' variable + (as set in the test/pose_config.yaml file for a particular project). + I.e. every image will be resized according to that scale + and prediction will be compared to the resized ground truth. + The error will be reported in pixels at rescaled to the *original* size. + I.e. For a [200,200] pixel image evaluated at global_scale=.5, the predictions are calculated + on [100,100] pixel images, compared to 1/2*ground truth and this error is then multiplied by 2!. + The evaluation images are also shown for the original size! Examples -------- @@ -178,8 +183,11 @@ def extract_maps( ] # read how many training siterations that corresponds to. # Name for deeplabcut net (based on its parameters) - # DLCscorer,DLCscorerlegacy = auxiliaryfunctions.GetScorerName(cfg,shuffle,trainFraction,trainingsiterations) - # notanalyzed, resultsfilename, DLCscorer=auxiliaryfunctions.CheckifNotEvaluated(str(evaluationfolder),DLCscorer,DLCscorerlegacy,Snapshots[snapindex]) + # DLCscorer,DLCscorerlegacy = + # auxiliaryfunctions.GetScorerName(cfg,shuffle,trainFraction,trainingsiterations) + # notanalyzed, resultsfilename, + # DLCscorer=auxiliaryfunctions.CheckifNotEvaluated(str(evaluationfolder), + # DLCscorer,DLCscorerlegacy,Snapshots[snapindex]) # print("Extracting maps for ", DLCscorer, " with # of trainingiterations:", trainingsiterations) # if notanalyzed: #this only applies to ask if h5 exists... @@ -281,8 +289,9 @@ def extract_save_all_maps( integers specifying shuffle index of the training dataset. The default is 1. trainingsetindex: int, optional - Integer specifying which TrainingsetFraction to use. By default the first (note that TrainingFraction is a list in config.yaml). This - variable can also be set to "all". + Integer specifying which TrainingsetFraction to use. + By default the first (note that TrainingFraction is a list in config.yaml). + This variable can also be set to "all". comparisonbodyparts: list of bodyparts, Default is "all". The average error will be computed for those body parts only (Has to be a subset of the body parts). From 6d66fef2a353bef64715e5d952256b514bec35b4 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 16:26:25 +0100 Subject: [PATCH 68/80] Fix E501 --- .../modelzoo/api/superanimal_inference.py | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/deeplabcut/pose_estimation_tensorflow/modelzoo/api/superanimal_inference.py b/deeplabcut/pose_estimation_tensorflow/modelzoo/api/superanimal_inference.py index c06cd1a5f7..0b9255dcb0 100644 --- a/deeplabcut/pose_estimation_tensorflow/modelzoo/api/superanimal_inference.py +++ b/deeplabcut/pose_estimation_tensorflow/modelzoo/api/superanimal_inference.py @@ -442,27 +442,39 @@ def _video_inference_superanimal( Makes prediction based on a super animal model. Note right now we only support single animal video inference - The index of the trained network is specified by parameters in the config file (in particular the variable 'snapshotindex') + The index of the trained network is specified by parameters in the config file + (in particular the variable 'snapshotindex') - Output: The labels are stored as MultiIndex Pandas Array, which contains the name of the network, body part name, (x, y) label position \n - in pixels, and the likelihood for each frame per body part. These arrays are stored in an efficient Hierarchical Data Format (HDF) \n + Output: The labels are stored as MultiIndex Pandas Array, + which contains the name of the network, body part name, (x, y) label position \n + in pixels, and the likelihood for each frame per body part. + These arrays are stored in an efficient Hierarchical Data Format (HDF) \n in the same directory, where the video is stored. Parameters ---------- videos: list - A list of strings containing the full paths to videos for analysis or a path to the directory, where all the videos with same extension are stored. + A list of strings containing the full paths to videos for analysis or a path to the directory, + where all the videos with same extension are stored. superanimal_name: str - The name of the superanimal model. In TensorFlow, we only support "superanimal_quadruped", "superanimal_topviewmouse". Check out the PyTorch version for active development, better performance and additional models (humans, birds, ...) + The name of the superanimal model. + In TensorFlow, we only support "superanimal_quadruped", "superanimal_topviewmouse". + Check out the PyTorch version for active development, + better performance and additional models (humans, birds, ...) scale_list: list - A list of int containing the target height of the multi scale test time augmentation. By default it uses the original size. Users are advised to try a wide range of scale list when the super model does not give reasonable results + A list of int containing the target height of the multi scale test time augmentation. + By default it uses the original size. + Users are advised to try a wide range of scale list when the super model does not give reasonable results videotype: string, optional - Checks for the extension of the video in case the input to the video is a directory.\n Only videos with this extension are analyzed. The default is ``.avi`` + Checks for the extension of the video in case the input to the video is a directory.\n + Only videos with this extension are analyzed. + The default is ``.avi`` video_adapt: bool, optional - Set True if you want to apply video adaptation to make the resulted video less jittering and better. However, adaptation training takes more time than usual video inference + Set True if you want to apply video adaptation to make the resulted video less jittering and better. + However, adaptation training takes more time than usual video inference plot_trajectories: bool, optional (default=True) By default, plot the trajectories of various body parts across the video. From 0a7817fd4e8e3cf0fcc18ffc4ff473efa0fea95e Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 16:30:00 +0100 Subject: [PATCH 69/80] Fix E501 --- deeplabcut/utils/auxfun_videos.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/deeplabcut/utils/auxfun_videos.py b/deeplabcut/utils/auxfun_videos.py index 6c7b34d898..3b873762ab 100644 --- a/deeplabcut/utils/auxfun_videos.py +++ b/deeplabcut/utils/auxfun_videos.py @@ -409,9 +409,11 @@ def ShortenVideo(vname, start="00:00:01", stop="00:01:00", outsuffix="short", ou Extracts (sub)video from 1st second to 1st minutes (default values) and saves it in /data/videos as mouse1short.avi Windows: - >>> deeplabcut.ShortenVideo('C:\\yourusername\\rig-95\\Videos\\reachingvideo1.avi', start='00:17:00',stop='00:22:00',outsuffix='brief') + >>> deeplabcut.ShortenVideo('C:\\yourusername\\rig-95\\Videos\\reachingvideo1.avi', + ... start='00:17:00',stop='00:22:00',outsuffix='brief') - Extracts (sub)video from minute 17 to 22 and and saves it in C:\\yourusername\\rig-95\\Videos as reachingvideo1brief.avi + Extracts (sub)video from minute 17 to 22 and and saves it in + C:\\yourusername\\rig-95\\Videos as reachingvideo1brief.avi """ writer = VideoWriter(vname) return writer.shorten(start, stop, outsuffix, outpath) @@ -463,9 +465,11 @@ def CropVideo( Crops the video using default values and saves it in /data/videos as mouse1cropped.avi Windows: - >>> =deeplabcut.CropVideo('C:\\yourusername\\rig-95\\Videos\\reachingvideo1.avi', width=220,height=320,outsuffix='cropped') + >>> =deeplabcut.CropVideo('C:\\yourusername\\rig-95\\Videos\\reachingvideo1.avi', + ... width=220,height=320,outsuffix='cropped') - Crops the video to a width of 220 and height of 320 starting at the origin (top left) and saves it in C:\\yourusername\\rig-95\\Videos as reachingvideo1cropped.avi + Crops the video to a width of 220 and height of 320 starting at the origin (top left) + and saves it in C:\\yourusername\\rig-95\\Videos as reachingvideo1cropped.avi """ writer = VideoWriter(vname) @@ -531,9 +535,11 @@ def DownSampleVideo( Downsamples the video using default values and saves it in /data/videos as mouse1cropped.avi Windows: - >>> shortenedvideoname=deeplabcut.DownSampleVideo('C:\\yourusername\\rig-95\\Videos\\reachingvideo1.avi', width=220,height=320,outsuffix='cropped') + >>> shortenedvideoname=deeplabcut.DownSampleVideo('C:\\yourusername\\rig-95\\Videos\\reachingvideo1.avi', + ... width=220,height=320,outsuffix='cropped') - Downsamples the video to a width of 220 and height of 320 and saves it in C:\\yourusername\\rig-95\\Videos as reachingvideo1cropped.avi + Downsamples the video to a width of 220 and height of 320 and + saves it in C:\\yourusername\\rig-95\\Videos as reachingvideo1cropped.avi """ writer = VideoWriter(vname) return writer.rescale(width, height, rotatecw, angle, outsuffix, outpath) @@ -571,9 +577,11 @@ def rotate_video(vname, angle, rotatecw="Arbitrary", outsuffix="rotated", outpat Rotates the video by 90 degrees and saves it in /data/videos as mouse1rotated.avi Windows: - >>> shortenedvideoname=deeplabcut.rotate_video('C:\\yourusername\\rig-95\\Videos\\reachingvideo1.avi', angle=180,rotatecw='Yes') + >>> shortenedvideoname=deeplabcut.rotate_video('C:\\yourusername\\rig-95\\Videos\\reachingvideo1.avi', + ... angle=180,rotatecw='Yes') - Rotates the video by 180 degrees and saves it in C:\\yourusername\\rig-95\\Videos as reachingvideo1rotated.avi + Rotates the video by 180 degrees and + saves it in C:\\yourusername\\rig-95\\Videos as reachingvideo1rotated.avi """ writer = VideoWriter(vname) return writer.rotate(angle, rotatecw, outsuffix, outpath) @@ -600,7 +608,10 @@ def validate_crop(*args): def display_help(*args): print( - "1. Use left click to select the region of interest. A red box will be drawn around the selected region. \n\n2. Use the corner points to expand the box and center to move the box around the image. \n\n3. Click " + "1. Use left click to select the region of interest. " + "A red box will be drawn around the selected region. \n\n" + "2. Use the corner points to expand the box and center to move the box around the image. \n\n" + "3. Click " ) fig = plt.figure() From 08445c38df523223b08769989ce5607f65f904d9 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 16:40:40 +0100 Subject: [PATCH 70/80] CI: add Ruff report and detect changed Python files Detect changed Python files in the workflow and generate a Ruff Markdown report for changed files. Bump actions (checkout@v6, setup-python@v6), install ruff alongside pre-commit, and add a step that runs tools/ruff_report.py to produce tmp/ruff-report.md. The workflow now publishes a short top-section to the GitHub Actions summary, uploads the full report as an artifact, and fails the job if pre-commit reports failures. Also add a dedicated step to output changed Python file paths and wire that into report generation, and adjust the pre-commit run to allow collecting results before explicitly failing the job. Update tools/ruff_report.py to invoke Ruff via `python -m ruff` (ensures the venv-installed tool is used) and fix usage examples. --- .github/workflows/format.yml | 65 +++++++++++++++++++++++++++++++++--- tools/ruff_report.py | 6 ++-- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index 95450e5883..974dfc255d 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -9,10 +9,11 @@ jobs: runs-on: ubuntu-latest outputs: changed: ${{ steps.changed_files.outputs.changed }} + changed_python: ${{ steps.changed_python.outputs.changed_python }} steps: - name: Checkout full history - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 @@ -28,11 +29,27 @@ jobs: echo "EOF" } >> "$GITHUB_OUTPUT" + - name: Detect changed Python files + id: changed_python + run: | + git fetch origin ${{ github.base_ref }} + CHANGED_PYTHON=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep -E '\.(py|pyi|ipynb)$' || true) + + { + echo "changed_python<> "$GITHUB_OUTPUT" + - name: Show changed files run: | echo "Changed files:" echo "${{ steps.changed_files.outputs.changed }}" + echo + echo "Changed Python files:" + echo "${{ steps.changed_python.outputs.changed_python }}" + precommit: needs: detect_changes runs-on: ubuntu-latest @@ -40,22 +57,60 @@ jobs: steps: - name: Checkout PR branch - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 ref: ${{ github.head_ref }} - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: "3.12" - - name: Install pre-commit - run: pip install pre-commit + - name: Install tooling + run: pip install pre-commit ruff - name: Run pre-commit (CI check-only stage) on changed files + id: precommit_run + continue-on-error: true env: CHANGED_FILES: ${{ needs.detect_changes.outputs.changed }} run: | mapfile -t files <<< "$CHANGED_FILES" pre-commit run --hook-stage manual --files "${files[@]}" --show-diff-on-failure + + - name: Generate Ruff Markdown report + id: ruff_report + if: ${{ always() && needs.detect_changes.outputs.changed_python != '' }} + env: + CHANGED_PYTHON: ${{ needs.detect_changes.outputs.changed_python }} + run: | + mkdir -p tmp + mapfile -t pyfiles <<< "$CHANGED_PYTHON" + python tools/ruff_report.py "${pyfiles[@]}" --output tmp/ruff-report.md + + - name: Add short Ruff report to GitHub Actions summary + if: ${{ always() && steps.precommit_run.outcome == 'failure' && needs.detect_changes.outputs.changed_python != '' }} + run: | + { + echo "# Lint summary" + echo + echo "## Ruff report (top section)" + echo + sed -n '1,80p' tmp/ruff-report.md + echo + echo "_Full report uploaded as workflow artifact: `ruff-report`_" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload Ruff report artifact + if: ${{ always() && needs.detect_changes.outputs.changed_python != '' }} + uses: actions/upload-artifact@v6 + with: + name: ruff-report + path: tmp/ruff-report.md + + - name: Fail job if pre-commit failed + if: ${{ steps.precommit_run.outcome == 'failure' }} + run: | + echo "pre-commit reported failures" + exit 1 diff --git a/tools/ruff_report.py b/tools/ruff_report.py index 123f21e02c..bfa7e6f206 100644 --- a/tools/ruff_report.py +++ b/tools/ruff_report.py @@ -2,8 +2,8 @@ """Generate a readable Markdown report from Ruff JSON output. Usage: - python generate_ruff_report.py . --output ruff-report.md - python generate_ruff_report.py src tests --output lint/ruff-report.md + python ruff_report.py . --output ruff-report.md + python ruff_report.py src tests --output lint/ruff-report.md """ from __future__ import annotations @@ -42,7 +42,7 @@ def run_ruff(paths: Iterable[str]) -> list[dict]: - cmd = ["ruff", "check", *paths, "--output-format=json", "--exit-zero"] + cmd = [sys.executable, "-m", "ruff", "check", *paths, "--output-format=json", "--exit-zero"] proc = subprocess.run(cmd, capture_output=True, text=True) if proc.returncode not in (0, 1): print(proc.stdout) From 5858257e16a64dbe7ba187f3827223efbc46cd68 Mon Sep 17 00:00:00 2001 From: Jaap de Ruyter van Steveninck <32810691+deruyter92@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:47:49 +0100 Subject: [PATCH 71/80] adjust line length to max 120 for ~75 files (#3248) Co-authored-by: Cyril Achard --- deeplabcut/__main__.py | 3 ++- deeplabcut/create_project/new_3d.py | 12 +++++++++--- ...iple_individuals_trainingsetmanipulation.py | 9 ++++++--- deeplabcut/gui/tabs/extract_outlier_frames.py | 3 ++- deeplabcut/gui/tabs/refine_tracklets.py | 3 ++- deeplabcut/gui/tabs/train_network.py | 3 ++- deeplabcut/gui/tracklet_toolbox.py | 3 ++- deeplabcut/gui/widgets.py | 3 ++- .../datasets/single_dlc_dataframe.py | 10 +++++++--- deeplabcut/modelzoo/utils.py | 6 ++++-- deeplabcut/modelzoo/weight_initialization.py | 3 ++- .../config/make_pose_config.py | 6 ++++-- .../models/modules/conv_block.py | 3 ++- .../models/predictors/paf_predictor.py | 3 ++- .../post_processing/match_predictions_to_gt.py | 3 ++- .../core/train_multianimal.py | 6 ++++-- .../datasets/pose_imgaug.py | 3 ++- .../datasets/pose_tensorpack.py | 3 ++- .../modelzoo/api/spatiotemporal_adapt.py | 9 ++++++--- .../pose_estimation_tensorflow/training.py | 6 ++++-- .../model/backbones/vit_pytorch.py | 5 +++-- deeplabcut/refine_training_dataset/stitch.py | 9 ++++++--- deeplabcut/utils/auxfun_models.py | 6 ++++-- deeplabcut/utils/auxfun_multianimal.py | 9 ++++++--- deeplabcut/utils/auxiliaryfunctions.py | 15 ++++++++++----- deeplabcut/utils/frameselectiontools.py | 18 ++++++++++++------ deeplabcut/utils/pseudo_label.py | 3 ++- deeplabcut/utils/visualization.py | 3 ++- docs/recipes/flip_and_rotate.ipynb | 18 ++++++++++++------ examples/JUPYTER/Demo_yourowndata.ipynb | 3 ++- .../testscript_deterministicwithResNet152.py | 3 ++- examples/testscript_mobilenets.py | 3 ++- examples/testscript_openfielddata.py | 3 ++- examples/testscript_pretrained_models.py | 3 ++- .../other/test_match_predictions_to_gt.py | 7 +++++-- testscript_cli.py | 6 ++++-- 36 files changed, 144 insertions(+), 70 deletions(-) diff --git a/deeplabcut/__main__.py b/deeplabcut/__main__.py index 12d7aff368..ea7e3ca1c6 100644 --- a/deeplabcut/__main__.py +++ b/deeplabcut/__main__.py @@ -27,7 +27,8 @@ def main(): launch_dlc() else: print( - "You installed DLC lite, thus GUI's cannot be used. If you need GUI support please: pip install 'deeplabcut[gui]''" + "You installed DLC lite, thus GUI's cannot be used. If you need GUI support please: pip install" + "'deeplabcut[gui]''" ) diff --git a/deeplabcut/create_project/new_3d.py b/deeplabcut/create_project/new_3d.py index c526ce6276..24be797a2d 100644 --- a/deeplabcut/create_project/new_3d.py +++ b/deeplabcut/create_project/new_3d.py @@ -33,7 +33,8 @@ def create_new_project_3d(project, experimenter, num_cameras=2, working_director An integer value specifying the number of cameras. working_directory : string, optional - The directory where the project will be created. The default is the ``current working directory``; if provided, it must be a string. + The directory where the project will be created. The default is the ``current working directory``; if provided, + it must be a string. Example @@ -88,7 +89,10 @@ def create_new_project_3d(project, experimenter, num_cameras=2, working_director cfg_file_3d["scorer"] = experimenter cfg_file_3d["date"] = d cfg_file_3d["project_path"] = str(project_path) - # cfg_file_3d['config_files']= [str('Enter the path of the config file ')+str(i)+ ' to include' for i in range(1,3)] + # cfg_file_3d['config_files']= [ + # str('Enter the path of the config file ') + str(i) + ' to include' + # for i in range(1, 3) + # ] # cfg_file_3d['config_files']= ['Enter the path of the config file 1'] cfg_file_3d["colormap"] = "jet" cfg_file_3d["dotsize"] = 15 @@ -123,6 +127,8 @@ def create_new_project_3d(project, experimenter, num_cameras=2, working_director print('Generated "{}"'.format(project_path / "config.yaml")) print( - f"\nA new project with name {project_name} is created at {wd} and a configurable file (config.yaml) is stored there. If you have not calibrated the cameras, then use the function 'calibrate_camera' to start calibrating the camera otherwise use the function ``triangulate`` to triangulate the dataframe" + f"\nA new project with name {project_name} is created at {wd} and a configurable file (config.yaml) is stored" + f"there. If you have not calibrated the cameras, then use the function 'calibrate_camera' to start calibrating" + f"the camera otherwise use the function ``triangulate`` to triangulate the dataframe" ) return projconfigfile diff --git a/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py b/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py index 6de8d7ef51..19b0fedc30 100755 --- a/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py +++ b/deeplabcut/generate_training_dataset/multiple_individuals_trainingsetmanipulation.py @@ -257,14 +257,17 @@ def create_multianimaltraining_dataset( It defines the conditions that will be used with the CTD model. It can be either: * A shuffle number (ctd_conditions: int), which must correspond to a bottom-up (BU) network type. - * A predictions file path (ctd_conditions: string | Path), which must correspond to a .json or .h5 predictions file. - * A shuffle number and a particular snapshot (ctd_conditions: tuple[int, str] | tuple[int, int]), which respectively correspond to a bottom-up (BU) network type and a particular snapshot name or index. + * A predictions file path (ctd_conditions: string | Path), which must correspond to a .json or .h5 + predictions file. + * A shuffle number and a particular snapshot (ctd_conditions: tuple[int, str] | tuple[int, int]), which + respectively correspond to a bottom-up (BU) network type and a particular snapshot name or index. Example -------- >>> deeplabcut.create_multianimaltraining_dataset('/analysis/project/reaching-task/config.yaml',num_shuffles=1) - >>> deeplabcut.create_multianimaltraining_dataset('/analysis/project/reaching-task/config.yaml', Shuffles=[0,1,2], trainIndices=[trainInd1, trainInd2, trainInd3], testIndices=[testInd1, testInd2, testInd3]) + >>> deeplabcut.create_multianimaltraining_dataset('/analysis/project/reaching-task/config.yaml', Shuffles=[0,1,2], + trainIndices=[trainInd1, trainInd2, trainInd3], testIndices=[testInd1, testInd2, testInd3]) Windows: >>> deeplabcut.create_multianimaltraining_dataset(r'C:\\Users\\Ulf\\looming-task\\config.yaml',Shuffles=[3,17,5]) diff --git a/deeplabcut/gui/tabs/extract_outlier_frames.py b/deeplabcut/gui/tabs/extract_outlier_frames.py index c43a4bae4e..8c3b1a9d38 100644 --- a/deeplabcut/gui/tabs/extract_outlier_frames.py +++ b/deeplabcut/gui/tabs/extract_outlier_frames.py @@ -159,7 +159,8 @@ def merge_dataset(self): msg = QtWidgets.QMessageBox() msg.setIcon(QtWidgets.QMessageBox.Warning) msg.setText( - "Make sure that you have refined all the labels before merging the dataset.If you merge the dataset, you need to re-create the training dataset before you start the training. Are you ready to merge the dataset?" + "Make sure that you have refined all the labels before merging the dataset.If you merge the dataset, you" + "need to re-create the training dataset before you start the training. Are you ready to merge the dataset?" ) msg.setWindowTitle("Warning") msg.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) diff --git a/deeplabcut/gui/tabs/refine_tracklets.py b/deeplabcut/gui/tabs/refine_tracklets.py index 32c60d041c..674e24c93e 100644 --- a/deeplabcut/gui/tabs/refine_tracklets.py +++ b/deeplabcut/gui/tabs/refine_tracklets.py @@ -228,7 +228,8 @@ def merge_dataset(self): msg = QtWidgets.QMessageBox() msg.setIcon(QtWidgets.QMessageBox.Warning) msg.setText( - "Make sure that you have refined all the labels before merging the dataset.If you merge the dataset, you need to re-create the training dataset before you start the training. Are you ready to merge the dataset?" + "Make sure that you have refined all the labels before merging the dataset.If you merge the dataset, you" + "need to re-create the training dataset before you start the training. Are you ready to merge the dataset?" ) msg.setWindowTitle("Warning") msg.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) diff --git a/deeplabcut/gui/tabs/train_network.py b/deeplabcut/gui/tabs/train_network.py index b2c5294a8c..c7b30595a1 100644 --- a/deeplabcut/gui/tabs/train_network.py +++ b/deeplabcut/gui/tabs/train_network.py @@ -94,7 +94,8 @@ def _set_page(self): self.resume_from_snapshot_label.setToolTip( "" "If you've already trained a model on this shuffle, you can continue training it instead of starting " - "from scratch again.
    When using top-down models, you can also choose a detector to resume training from." + "from scratch again.
    When using top-down models, you can also choose a detector to resume training" + "from." "
    " ) self.main_layout.addWidget(self.resume_from_snapshot_label) diff --git a/deeplabcut/gui/tracklet_toolbox.py b/deeplabcut/gui/tracklet_toolbox.py index 483a552d16..295b853dd2 100644 --- a/deeplabcut/gui/tracklet_toolbox.py +++ b/deeplabcut/gui/tracklet_toolbox.py @@ -901,7 +901,8 @@ def filter_low_prob(cols, prob): output_path = os.path.join(tmpfolder, f"CollectedData_{self.manager.cfg['scorer']}.h5") if os.path.isfile(output_path): print( - "A training dataset file is already found for this video. The refined machine labels are merged to this data!" + "A training dataset file is already found for this video. The refined machine labels are merged to this" + "data!" ) df_orig = pd.read_hdf(output_path) df_joint = pd.concat([df, df_orig]) diff --git a/deeplabcut/gui/widgets.py b/deeplabcut/gui/widgets.py index 33ece7c6cb..a0bd2913b2 100644 --- a/deeplabcut/gui/widgets.py +++ b/deeplabcut/gui/widgets.py @@ -524,7 +524,8 @@ def validate_crop(self, *args): def display_help(self, *args): print( - "1. Use left click to select the region of interest. A red box will be drawn around the selected region. \n\n2. Use the corner points to expand the box and center to move the box around the image. \n\n3. Click " + "1. Use left click to select the region of interest. A red box will be drawn around the selected region." + "\n\n2. Use the corner points to expand the box and center to move the box around the image. \n\n3. Click" ) diff --git a/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc_dataframe.py b/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc_dataframe.py index 587653d9ae..07896698a5 100644 --- a/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc_dataframe.py +++ b/deeplabcut/modelzoo/generalized_data_converter/datasets/single_dlc_dataframe.py @@ -30,7 +30,8 @@ def merge_annotateddatasets(cfg): This is a bit of a mess because of cross platform compatibility. - Within platform comp. is straightforward. But if someone labels on windows and wants to train on a unix cluster or colab... + Within platform comp. is straightforward. But if someone labels on windows and wants to train on a unix cluster or + colab... """ AnnotationData = [] data_path = Path(os.path.join(cfg["project_path"], "labeled-data")) @@ -43,7 +44,9 @@ def merge_annotateddatasets(cfg): conversioncode.guarantee_multiindex_rows(data) if data.columns.levels[0][0] != cfg["scorer"]: print( - f"{file_path} labeled by a different scorer. This data will not be utilized in training dataset creation. If you need to merge datasets across scorers, see https://github.com/DeepLabCut/DeepLabCut/wiki/Using-labeled-data-in-DeepLabCut-that-was-annotated-elsewhere-(or-merge-across-labelers)" + f"{file_path} labeled by a different scorer. This data will not be utilized in training dataset" + f"creation. If you need to merge datasets across scorers, see" + f"https://github.com/DeepLabCut/DeepLabCut/wiki/Using-labeled-data-in-DeepLabCut-that-was-annotated-elsewhere-(or-merge-across-labelers)" ) continue AnnotationData.append(data) @@ -52,7 +55,8 @@ def merge_annotateddatasets(cfg): if not len(AnnotationData): print( - "Annotation data was not found by splitting video paths (from config['video_sets']). An alternative route is taken..." + "Annotation data was not found by splitting video paths (from config['video_sets']). An alternative route" + "is taken..." ) AnnotationData = conversioncode.merge_windowsannotationdataONlinuxsystem(cfg) if not len(AnnotationData): diff --git a/deeplabcut/modelzoo/utils.py b/deeplabcut/modelzoo/utils.py index cce590995d..e3bc96c6c7 100644 --- a/deeplabcut/modelzoo/utils.py +++ b/deeplabcut/modelzoo/utils.py @@ -197,7 +197,8 @@ def parse_project_model_name(superanimal_name: str) -> tuple[str, str]: if superanimal_name == "superanimal_quadruped": warnings.warn( - f"{superanimal_name} is deprecated and will be removed in a future version. Use {superanimal_name}_model_suffix instead.", + f"{superanimal_name} is deprecated and will be removed in a future version. Use" + f"{superanimal_name}_model_suffix instead.", DeprecationWarning, stacklevel=2, ) @@ -205,7 +206,8 @@ def parse_project_model_name(superanimal_name: str) -> tuple[str, str]: if superanimal_name == "superanimal_topviewmouse": warnings.warn( - f"{superanimal_name} is deprecated and will be removed in a future version. Use {superanimal_name}_model_suffix instead.", + f"{superanimal_name} is deprecated and will be removed in a future version. Use" + f"{superanimal_name}_model_suffix instead.", DeprecationWarning, stacklevel=2, ) diff --git a/deeplabcut/modelzoo/weight_initialization.py b/deeplabcut/modelzoo/weight_initialization.py index da85f5b200..bb36e49809 100644 --- a/deeplabcut/modelzoo/weight_initialization.py +++ b/deeplabcut/modelzoo/weight_initialization.py @@ -74,7 +74,8 @@ def build_weight_init( """ if super_animal == "superanimal_humanbody": raise NotImplementedError( - "Weight Initialization, Transfer-Learning and Finetuning is currently not supported for superanimal_humanbody" + "Weight Initialization, Transfer-Learning and Finetuning is currently not supported for" + "superanimal_humanbody" ) if isinstance(cfg, (str, Path)): diff --git a/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py b/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py index 012590a21b..3cf316dd15 100644 --- a/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py +++ b/deeplabcut/pose_estimation_pytorch/config/make_pose_config.py @@ -75,8 +75,10 @@ def make_pytorch_pose_config( It defines the conditions that will be used with the CTD model. It can be either: * A shuffle number (ctd_conditions: int), which must correspond to a bottom-up (BU) network type. - * A predictions file path (ctd_conditions: string | Path), which must correspond to a .json or .h5 predictions file. - * A shuffle number and a particular snapshot (ctd_conditions: tuple[int, str] | tuple[int, int]), which respectively correspond to a bottom-up (BU) network type and a particular snapshot name or index. + * A predictions file path (ctd_conditions: string | Path), which must correspond to a .json or .h5 + predictions file. + * A shuffle number and a particular snapshot (ctd_conditions: tuple[int, str] | tuple[int, int]), which + respectively correspond to a bottom-up (BU) network type and a particular snapshot name or index. Returns: diff --git a/deeplabcut/pose_estimation_pytorch/models/modules/conv_block.py b/deeplabcut/pose_estimation_pytorch/models/modules/conv_block.py index 4e5c27de22..529db64956 100644 --- a/deeplabcut/pose_estimation_pytorch/models/modules/conv_block.py +++ b/deeplabcut/pose_estimation_pytorch/models/modules/conv_block.py @@ -26,7 +26,8 @@ class BaseBlock(ABC, nn.Module): """Abstract Base class for defining custom blocks. - This class defines an abstract base class for creating custom blocks used in the HigherHRNet for Human Pose Estimation. + This class defines an abstract base class for creating custom blocks used in the HigherHRNet for Human Pose + Estimation. Attributes: bn_momentum: Batch normalization momentum. diff --git a/deeplabcut/pose_estimation_pytorch/models/predictors/paf_predictor.py b/deeplabcut/pose_estimation_pytorch/models/predictors/paf_predictor.py index b625353f22..305195d628 100644 --- a/deeplabcut/pose_estimation_pytorch/models/predictors/paf_predictor.py +++ b/deeplabcut/pose_estimation_pytorch/models/predictors/paf_predictor.py @@ -33,7 +33,8 @@ class PartAffinityFieldPredictor(BasePredictor): Args: num_animals: Number of animals in the project. num_multibodyparts: Number of animal's body parts (ignoring unique body parts). - num_uniquebodyparts: Number of unique body parts. # FIXME - should not be needed here if we separate the unique bodypart head + num_uniquebodyparts: Number of unique body parts. # FIXME - should not be needed here if we separate the unique + bodypart head graph: Part affinity field graph edges. edges_to_keep: List of indices in `graph` of the edges to keep. locref_stdev: Standard deviation for location refinement. diff --git a/deeplabcut/pose_estimation_pytorch/post_processing/match_predictions_to_gt.py b/deeplabcut/pose_estimation_pytorch/post_processing/match_predictions_to_gt.py index 54f4885f7e..1c49d97558 100644 --- a/deeplabcut/pose_estimation_pytorch/post_processing/match_predictions_to_gt.py +++ b/deeplabcut/pose_estimation_pytorch/post_processing/match_predictions_to_gt.py @@ -104,7 +104,8 @@ def oks_match_prediction_to_gt(pred_kpts: np.array, gt_kpts: np.array, individua num_animals: Number of animals. num_keypoints: Number of keypoints. 3: (x, y, score) coordinates of each keypoint. - gt_kpts: Ground truth keypoints for each animal. The shape of the array is (num_animals, num_keypoints(+1 if with center), 2): + gt_kpts: Ground truth keypoints for each animal. The shape of the array is (num_animals, num_keypoints(+1 if + with center), 2): num_animals: Number of animals. num_keypoints: Number of keypoints. individual_names: names of individuals diff --git a/deeplabcut/pose_estimation_tensorflow/core/train_multianimal.py b/deeplabcut/pose_estimation_tensorflow/core/train_multianimal.py index b51353667f..88f7e9bc35 100644 --- a/deeplabcut/pose_estimation_tensorflow/core/train_multianimal.py +++ b/deeplabcut/pose_estimation_tensorflow/core/train_multianimal.py @@ -89,7 +89,8 @@ def train( if ( cfg["partaffinityfield_predict"] and "multi-animal" in cfg["dataset_type"] - # the PAF code currently just hijacks the pairwise net stuff (for the batch feeding via Batch.pairwise_targets: 5) + # the PAF code currently just hijacks the pairwise net stuff (for the batch feeding via Batch.pairwise_targets: + # 5) ): print("Activating limb prediction...") cfg["pairwise_predict"] = True @@ -206,7 +207,8 @@ def train( current_lr = lr_gen.get_lr(it - start_iter) lr_dict = {learning_rate: current_lr} - # [_, loss_val, summary] = sess.run([train_op, total_loss, merged_summaries],feed_dict={learning_rate: current_lr}) + # [_, loss_val, summary] = sess.run([train_op, total_loss, merged_summaries],feed_dict={learning_rate: + # current_lr}) [_, alllosses, loss_val, summary] = sess.run( [train_op, losses, total_loss, merged_summaries], feed_dict=lr_dict ) diff --git a/deeplabcut/pose_estimation_tensorflow/datasets/pose_imgaug.py b/deeplabcut/pose_estimation_tensorflow/datasets/pose_imgaug.py index 05d298fb96..0d05c24afb 100644 --- a/deeplabcut/pose_estimation_tensorflow/datasets/pose_imgaug.py +++ b/deeplabcut/pose_estimation_tensorflow/datasets/pose_imgaug.py @@ -412,7 +412,8 @@ def next_batch(self): # import imageio # for i in range(self.batch_size): # joints = batch_joints[i] - # kps = KeypointsOnImage([Keypoint(x=joint[0], y=joint[1]) for joint in joints], shape=batch_images[i].shape) + # kps = KeypointsOnImage([Keypoint(x=joint[0], y=joint[1]) for joint in joints], + # shape=batch_images[i].shape) # im = kps.draw_on_image(batch_images[i]) # imageio.imwrite('some_location/augmented/'+str(i)+'.png', im) diff --git a/deeplabcut/pose_estimation_tensorflow/datasets/pose_tensorpack.py b/deeplabcut/pose_estimation_tensorflow/datasets/pose_tensorpack.py index 9558e64b46..66a279d4e8 100644 --- a/deeplabcut/pose_estimation_tensorflow/datasets/pose_tensorpack.py +++ b/deeplabcut/pose_estimation_tensorflow/datasets/pose_tensorpack.py @@ -224,7 +224,8 @@ def __init__(self, cfg): # Number of datapoints to prefetch at a time during training cfg["num_prefetch"] = cfg.get("num_prefetch", 50) - # Auto cropping is new (was not in Nature Neuroscience 2018 paper, but introduced in Nath et al. Nat. Protocols 2019) + # Auto cropping is new (was not in Nature Neuroscience 2018 paper, but introduced in Nath et al. Nat. Protocols + # 2019) # and boosts performance by 2X, particularly on challenging datasets, like the cheetah in Nath et al. # Parameters for augmentation with regard to cropping: diff --git a/deeplabcut/pose_estimation_tensorflow/modelzoo/api/spatiotemporal_adapt.py b/deeplabcut/pose_estimation_tensorflow/modelzoo/api/spatiotemporal_adapt.py index af2b0af958..4702cb2617 100644 --- a/deeplabcut/pose_estimation_tensorflow/modelzoo/api/spatiotemporal_adapt.py +++ b/deeplabcut/pose_estimation_tensorflow/modelzoo/api/spatiotemporal_adapt.py @@ -49,11 +49,14 @@ def __init__( scale_list: list A list of different resolutions for the spatial pyramid videotype: string - Checks for the extension of the video in case the input to the video is a directory.\n Only videos with this extension are analyzed. The default is ``.avi`` + Checks for the extension of the video in case the input to the video is a directory.\n Only videos with this + extension are analyzed. The default is ``.avi`` adapt_iterations: int - Number of iterations for adaptation training. Empirically 1000 is sufficient. Training longer can cause worse performance depending whether there is occlusion in the video + Number of iterations for adaptation training. Empirically 1000 is sufficient. Training longer can cause worse + performance depending whether there is occlusion in the video modelfolder: string, optional - Because the API does not need a dlc project, the checkpoint and logs go to this temporary model folder, and otherwise model is saved to the current work place + Because the API does not need a dlc project, the checkpoint and logs go to this temporary model folder, and + otherwise model is saved to the current work place customized_pose_config: string, optional For future support of non modelzoo model diff --git a/deeplabcut/pose_estimation_tensorflow/training.py b/deeplabcut/pose_estimation_tensorflow/training.py index 118602074d..61790e6334 100644 --- a/deeplabcut/pose_estimation_tensorflow/training.py +++ b/deeplabcut/pose_estimation_tensorflow/training.py @@ -27,7 +27,8 @@ def return_train_network_path(config, shuffle=1, trainingsetindex=0, modelprefix Integer value specifying the shuffle index to select for training. trainingsetindex: int, optional - Integer specifying which TrainingsetFraction to use. By default the first (note that TrainingFraction is a list in config.yaml). + Integer specifying which TrainingsetFraction to use. By default the first (note that TrainingFraction is a list + in config.yaml). Returns the triple: trainposeconfigfile, testposeconfigfile, snapshotfolder """ @@ -175,7 +176,8 @@ def train_network( print("The training datafile ", poseconfigfile, " is not present.") print("Probably, the training dataset for this specific shuffle index was not created.") print( - "Try with a different shuffle/trainingsetfraction or use function 'create_training_dataset' to create a new trainingdataset with this shuffle index." + "Try with a different shuffle/trainingsetfraction or use function 'create_training_dataset' to create a new" + "trainingdataset with this shuffle index." ) else: # Set environment variables diff --git a/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py b/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py index 56a1129d3a..31476c2665 100644 --- a/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py +++ b/deeplabcut/pose_tracking_pytorch/model/backbones/vit_pytorch.py @@ -330,7 +330,7 @@ def load_param(self, model_path): except Exception: print("===========================ERROR=========================") print( - f"shape do not match in k :{k}: param_dict{v.shape} vs self.state_dict(){self.state_dict()[k].shape}" + f"shape do not match in k :{k}: param_dict{v.shape} vsself.state_dict(){self.state_dict()[k].shape}" ) @@ -344,7 +344,8 @@ def resize_pos_embed(posemb, posemb_new, height, width): gs_old = int(math.sqrt(len(posemb_grid))) print( - f"Resized position embedding from size:{posemb.shape} to size: {posemb_new.shape} with height:{height} width: {width}" + f"Resized position embedding from size:{posemb.shape} to size: {posemb_new.shape} with height:{height} width:" + f"{width}" ) posemb_grid = posemb_grid.reshape(1, gs_old, gs_old, -1).permute(0, 3, 1, 2) posemb_grid = F.interpolate(posemb_grid, size=(height, width), mode="bilinear") diff --git a/deeplabcut/refine_training_dataset/stitch.py b/deeplabcut/refine_training_dataset/stitch.py index 7d6b842291..86ce62b504 100644 --- a/deeplabcut/refine_training_dataset/stitch.py +++ b/deeplabcut/refine_training_dataset/stitch.py @@ -987,17 +987,20 @@ def stitch_tracklets( Path to the main project config.yaml file. videos : list - A list of strings containing the full paths to videos for analysis or a path to the directory, where all the videos with same extension are stored. + A list of strings containing the full paths to videos for analysis or a path to the directory, where all the + videos with same extension are stored. videotype: string, optional - Checks for the extension of the video in case the input to the video is a directory.\n Only videos with this extension are analyzed. + Checks for the extension of the video in case the input to the video is a directory.\n Only videos with this + extension are analyzed. If left unspecified, videos with common extensions ('avi', 'mp4', 'mov', 'mpeg', 'mkv') are kept. shuffle: int, optional An integer specifying the shuffle index of the training dataset used for training the network. The default is 1. trainingsetindex: int, optional - Integer specifying which TrainingsetFraction to use. By default the first (note that TrainingFraction is a list in config.yaml). + Integer specifying which TrainingsetFraction to use. By default the first (note that TrainingFraction is a list + in config.yaml). n_tracks : int, optional Number of tracks to reconstruct. By default, taken as the number diff --git a/deeplabcut/utils/auxfun_models.py b/deeplabcut/utils/auxfun_models.py index 5c219c7bf8..77cf85217d 100644 --- a/deeplabcut/utils/auxfun_models.py +++ b/deeplabcut/utils/auxfun_models.py @@ -50,7 +50,8 @@ def check_for_weights(modeltype, parent_path): """ if modeltype not in MODELTYPE_FILEPATH_MAP.keys(): print( - "Currently ResNet (50, 101, 152), MobilenetV2 (1, 0.75, 0.5 and 0.35) and EfficientNet (b0-b6) are supported, please change 'resnet' entry in config.yaml!" + "Currently ResNet (50, 101, 152), MobilenetV2 (1, 0.75, 0.5 and 0.35) and EfficientNet (b0-b6) are" + "supported, please change 'resnet' entry in config.yaml!" ) # Exit the function early if an unknown modeltype is provided. return parent_path @@ -157,7 +158,8 @@ def set_visible_devices(gputouse: int): n_devices = len(physical_devices) if gputouse >= n_devices: raise ValueError( - f"There are {n_devices} available GPUs: {physical_devices}\nPlease choose `gputouse` in {list(range(n_devices))}." + f"There are {n_devices} available GPUs: {physical_devices}\nPlease choose `gputouse` in" + f"{list(range(n_devices))}." ) tf.config.set_visible_devices(physical_devices[gputouse], "GPU") diff --git a/deeplabcut/utils/auxfun_multianimal.py b/deeplabcut/utils/auxfun_multianimal.py index ae4b185b2c..f398a7c597 100644 --- a/deeplabcut/utils/auxfun_multianimal.py +++ b/deeplabcut/utils/auxfun_multianimal.py @@ -120,7 +120,8 @@ def validate_paf_graph(cfg, paf_graph): raise ValueError( f"Unconnected {', '.join(multianimalbodyparts[i] for i in unconnected)}. " f"For multi-animal projects, all multianimalbodyparts should be connected. " - f"Ideally there should be at least one (multinode) path from each multianimalbodyparts to each other multianimalbodyparts. " + f"Ideally there should be at least one (multinode) path from each multianimalbodyparts to each other" + f"multianimalbodyparts." ) @@ -243,8 +244,10 @@ def convert2_maDLC(config, userfeedback=True, forceindividual=None): Full path of the config.yaml file as a string. userfeedback: bool, optional - If this is set to false during automatic mode then frames for all videos are extracted. The user can set this to true, which will result in a dialog, - where the user is asked for each video if (additional/any) frames from this video should be extracted. Use this, e.g. if you have already labeled + If this is set to false during automatic mode then frames for all videos are extracted. The user can set + this to true, which will result in a dialog, + where the user is asked for each video if (additional/any) frames from this video should be extracted. Use + this, e.g. if you have already labeled some folders and want to extract data for new videos. forceindividual: None default diff --git a/deeplabcut/utils/auxiliaryfunctions.py b/deeplabcut/utils/auxiliaryfunctions.py index 84981d73d0..d3ff794354 100644 --- a/deeplabcut/utils/auxiliaryfunctions.py +++ b/deeplabcut/utils/auxiliaryfunctions.py @@ -231,7 +231,8 @@ def read_config(configname): else: raise FileNotFoundError( - f"Config file at {path} not found. Please make sure that the file exists and/or that you passed the path of the config file correctly!" + f"Config file at {path} not found. Please make sure that the file exists and/or that you passed the path of" + f"the config file correctly!" ) return cfg @@ -400,10 +401,12 @@ def get_list_of_videos( *_full.videotype Args: - videos (list[str], str): List of video paths or a single path string. If string (or len() == 1 list of strings) is a directory, + videos (list[str], str): List of video paths or a single path string. If string (or len() == 1 list of strings) + is a directory, finds all videos whose extension matches ``videotype`` in the directory - videotype (list[str], str): File extension used to filter videos. Optional if ``videos`` is a list of video files, + videotype (list[str], str): File extension used to filter videos. Optional if ``videos`` is a list of video + files, and filters with common video extensions if a directory is passed in. in_random_order (bool): Whether or not to return a shuffled list of videos. @@ -783,8 +786,10 @@ def get_scorer_name( raise ValueError(f"Failed to abbreviate network name: {dlc_cfg['net_type']}") scorer = "DLC_" + netname + "_" + Task + str(date) + "shuffle" + str(shuffle) + "_" + str(trainingsiterations) - # legacy scorername until DLC 2.1. (cfg['resnet'] is deprecated / which is why we get the resnet_xyz name from dlc_cfg! - # scorer_legacy = 'DeepCut' + "_resnet" + str(cfg['resnet']) + "_" + Task + str(date) + 'shuffle' + str(shuffle) + '_' + str(trainingsiterations) + # legacy scorername until DLC 2.1. (cfg['resnet'] is deprecated / which is why we get the resnet_xyz name from + # dlc_cfg! + # scorer_legacy = 'DeepCut' + "_resnet" + str(cfg['resnet']) + "_" + Task + str(date) + 'shuffle' + str(shuffle) + + # '_' + str(trainingsiterations) scorer_legacy = scorer.replace("DLC", "DeepCut") return scorer, scorer_legacy diff --git a/deeplabcut/utils/frameselectiontools.py b/deeplabcut/utils/frameselectiontools.py index 0ee900fda7..ada9175d85 100644 --- a/deeplabcut/utils/frameselectiontools.py +++ b/deeplabcut/utils/frameselectiontools.py @@ -121,11 +121,14 @@ def KmeansbasedFrameselection( ): """This code downsamples the video to a width of resizewidth. - The video is extracted as a numpy array, which is then clustered with kmeans, whereby each frames is treated as a vector. - Frames from different clusters are then selected for labeling. This procedure makes sure that the frames "look different", + The video is extracted as a numpy array, which is then clustered with kmeans, whereby each frames is treated as a + vector. + Frames from different clusters are then selected for labeling. This procedure makes sure that the frames "look + different", i.e. different postures etc. On large videos this code is slow. - Consider not extracting the frames from the whole video but rather set start and stop to a period around interesting behavior. + Consider not extracting the frames from the whole video but rather set start and stop to a period around interesting + behavior. Note: this method can return fewer images than numframes2pick. """ @@ -170,7 +173,8 @@ def KmeansbasedFrameselection( for counter, index in tqdm(enumerate(Index)): if ncolors == 1: DATA[counter, :, :] = img_as_ubyte(clipresized.get_frame(index * 1.0 / clipresized.fps)) - else: # attention: averages over color channels to keep size small / perhaps you want to use color information? + else: # attention: averages over color channels to keep size small + # / perhaps you want to use color information? DATA[counter, :, :] = img_as_ubyte( np.array( np.mean(clipresized.get_frame(index * 1.0 / clipresized.fps), 2), @@ -217,11 +221,13 @@ def KmeansbasedFrameselectioncv2( This procedure makes sure that the frames "look different", i.e. different postures etc. On large videos this code is slow. - Consider not extracting the frames from the whole video but rather set start and stop to a period around interesting behavior. + Consider not extracting the frames from the whole video but rather set start and stop to a period around interesting + behavior. Note: this method can return fewer images than numframes2pick. - Attention: the flow of commands was not optimized for readability, but rather speed. This is why it might appear tedious and repetitive. + Attention: the flow of commands was not optimized for readability, but rather speed. This is why it might appear + tedious and repetitive. """ nframes = len(cap) nx, ny = cap.dimensions diff --git a/deeplabcut/utils/pseudo_label.py b/deeplabcut/utils/pseudo_label.py index 72671827dc..77139a172b 100644 --- a/deeplabcut/utils/pseudo_label.py +++ b/deeplabcut/utils/pseudo_label.py @@ -396,7 +396,8 @@ def dlc3predictions_2_annotation_from_video( # Since the inference API does not return the image path, I assume the # predictions are provided in the same order as the frames in the video. assert len(image_paths) == len(predictions), ( - f"number of images must be equal to number of predictions. image_paths: {len(image_paths)} , predictions: {len(predictions)}" + f"number of images must be equal to number of predictions. image_paths: {len(image_paths)} , predictions:" + f"{len(predictions)}" ) len(bodyparts) diff --git a/deeplabcut/utils/visualization.py b/deeplabcut/utils/visualization.py index e815672153..e0bbff0b5c 100644 --- a/deeplabcut/utils/visualization.py +++ b/deeplabcut/utils/visualization.py @@ -144,7 +144,8 @@ def make_multianimal_labeled_image( dotsize: size of dot alphavalue: transparency for the keypoints pcutoff: cut-off confidence value - labels: labels to use for ground truth, reliable predictions, and not reliable predictions (confidence below cut-off value) + labels: labels to use for ground truth, reliable predictions, and not reliable predictions (confidence below + cut-off value) ax: matplotlib plot's axes object bounding_boxes: bounding boxes (top-left corner, size) and their respective confidence levels, bboxes_cutoff: bounding boxes confidence cutoff threshold. diff --git a/docs/recipes/flip_and_rotate.ipynb b/docs/recipes/flip_and_rotate.ipynb index 32b44d41c5..ddcf323b95 100644 --- a/docs/recipes/flip_and_rotate.ipynb +++ b/docs/recipes/flip_and_rotate.ipynb @@ -280,7 +280,8 @@ "# We need pandas for creatig a nice list to parse\n", "import pandas as pd\n", "\n", - "# Read the h5 file containing all the frames, (project_folder/training-datasets/iteration-0/UnaufmentedDataSet_project_folder/CollectedData_LabelerName.h5)\n", + "# Read the h5 file containing all the frames:\n", + "# (project_folder/training-datasets/iteration-0/UnaufmentedDataSet_project_folder/CollectedData_LabelerName.h5)\n", "df = pd.read_hdf(\n", " \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/training-datasets/iteration-0/UnaugmentedDataSet_bat_augmentation_austin_2020_bat_dataAug18/CollectedData_DLC.h5\"\n", ")\n", @@ -714,7 +715,8 @@ "import numpy as np\n", "from getErrorDistribution import getErrorDistribution # import the getErrorDistribution function\n", "\n", - "# Read the h5 file containing all the frames, (project_folder/training-datasets/iteration-0/UnaufmentedDataSet_project_folder/CollectedData_LabelerName.h5)\n", + "# Read the h5 file containing all the frames:\n", + "# (project_folder/training-datasets/iteration-0/UnaufmentedDataSet_project_folder/CollectedData_LabelerName.h5)\n", "df = pd.read_hdf(\n", " \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/training-datasets/iteration-0/UnaugmentedDataSet_bat_augmentation_austin_2020_bat_dataAug18/CollectedData_DLC.h5\"\n", ")\n", @@ -871,7 +873,8 @@ "import numpy as np\n", "from getErrorDistribution import getErrorDistribution # import the getErrorDistribution function\n", "\n", - "# Read the h5 file containing all the frames, (project_folder/training-datasets/iteration-0/UnaufmentedDataSet_project_folder/CollectedData_LabelerName.h5)\n", + "# Read the h5 file containing all the frames:\n", + "# (project_folder/training-datasets/iteration-0/UnaufmentedDataSet_project_folder/CollectedData_LabelerName.h5)\n", "df = pd.read_hdf(\n", " \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/training-datasets/iteration-0/UnaugmentedDataSet_bat_augmentation_austin_2020_bat_dataAug18/CollectedData_DLC.h5\"\n", ")\n", @@ -1390,7 +1393,8 @@ "import numpy as np\n", "from getErrorDistribution import getErrorDistribution # import the getErrorDistribution function\n", "\n", - "# Read the h5 file containing all the frames, (project_folder/training-datasets/iteration-0/UnaufmentedDataSet_project_folder/CollectedData_LabelerName.h5)\n", + "# Read the h5 file containing all the frames:\n", + "# (project_folder/training-datasets/iteration-0/UnaufmentedDataSet_project_folder/CollectedData_LabelerName.h5)\n", "df = pd.read_hdf(\n", " \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/training-datasets/iteration-0/UnaugmentedDataSet_bat_augmentation_austin_2020_bat_dataAug18/CollectedData_DLC.h5\"\n", ")\n", @@ -1699,7 +1703,8 @@ "import numpy as np\n", "from getErrorDistribution import getErrorDistribution # import the getErrorDistribution function\n", "\n", - "# Read the h5 file containing all the frames, (project_folder/training-datasets/iteration-0/UnaufmentedDataSet_project_folder/CollectedData_LabelerName.h5)\n", + "# Read the h5 file containing all the frames:\n", + "# (project_folder/training-datasets/iteration-0/UnaufmentedDataSet_project_folder/CollectedData_LabelerName.h5)\n", "df = pd.read_hdf(\n", " \"/home/user/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/training-datasets/iteration-0/UnaugmentedDataSet_bat_augmentation_austin_2020_bat_dataAug18/CollectedData_DLC.h5\"\n", ")\n", @@ -1803,7 +1808,8 @@ "import numpy as np\n", "from getErrorDistribution import getErrorDistribution # import the getErrorDistribution function\n", "\n", - "# Read the h5 file containing all the frames, (project_folder/training-datasets/iteration-0/UnaufmentedDataSet_project_folder/CollectedData_LabelerName.h5)\n", + "# Read the h5 file containing all the frames:\n", + "# (project_folder/training-datasets/iteration-0/UnaufmentedDataSet_project_folder/CollectedData_LabelerName.h5)\n", "df = pd.read_hdf(\n", " \"/home/juser/projects/bat_augmentation_austin_2020_bat_data-DLC-2022-08-18/training-datasets/iteration-0/UnaugmentedDataSet_bat_augmentation_austin_2020_bat_dataAug18/CollectedData_DLC.h5\"\n", ")\n", diff --git a/examples/JUPYTER/Demo_yourowndata.ipynb b/examples/JUPYTER/Demo_yourowndata.ipynb index 5a7586c45b..23bc8e826b 100644 --- a/examples/JUPYTER/Demo_yourowndata.ipynb +++ b/examples/JUPYTER/Demo_yourowndata.ipynb @@ -93,7 +93,8 @@ ")\n", "\n", "# NOTE: The function returns the path, where your project is.\n", - "# You could also enter this manually (e.g. if the project is already created and you want to pick up, where you stopped...)\n", + "# You could also enter this manually (e.g. if the project is already created\n", + "# and you want to pick up where you stopped...)\n", "# Enter the path of the config file that was just created from the above step (check the folder):\n", "# path_config_file = \"/home/Mackenzie/Reaching/config.yaml\"" ] diff --git a/examples/testscript_deterministicwithResNet152.py b/examples/testscript_deterministicwithResNet152.py index d0a6550fec..01e033ee65 100644 --- a/examples/testscript_deterministicwithResNet152.py +++ b/examples/testscript_deterministicwithResNet152.py @@ -113,7 +113,8 @@ print("CREATING TRAININGSET") deeplabcut.create_training_dataset(path_config_file) -# posefile=os.path.join(cfg['project_path'],'dlc-models/iteration-'+str(cfg['iteration'])+'/'+ cfg['Task'] + cfg['date'] + '-trainset' + str(int(cfg['TrainingFraction'][0] * 100)) + 'shuffle' + str(1),'train/pose_cfg.yaml') +# posefile=os.path.join(cfg['project_path'],'dlc-models/iteration-'+str(cfg['iteration'])+'/'+ cfg['Task'] + cfg['date'] +# + '-trainset' + str(int(cfg['TrainingFraction'][0] * 100)) + 'shuffle' + str(1),'train/pose_cfg.yaml') shuffle = 1 posefile, _, _ = deeplabcut.return_train_network_path(path_config_file, shuffle=shuffle) diff --git a/examples/testscript_mobilenets.py b/examples/testscript_mobilenets.py index c6a7a2d1c5..0e814b9a08 100644 --- a/examples/testscript_mobilenets.py +++ b/examples/testscript_mobilenets.py @@ -14,7 +14,8 @@ @author: alex DEVELOPERS: -This script tests various functionalities (creating project ,training, evaluating, outlierextraction, retraining...) in an automatic way. +This script tests various functionalities (creating project ,training, evaluating, outlierextraction, retraining...) in +an automatic way. For that purpose, it trains ResNet and MobileNet briefly on a "fake" dataset. It should take about 4:15 minutes to run this in a CPU. (incl. downloading the ResNet + MobileNet weights) diff --git a/examples/testscript_openfielddata.py b/examples/testscript_openfielddata.py index 37cd9f9001..8faf1c3a5d 100644 --- a/examples/testscript_openfielddata.py +++ b/examples/testscript_openfielddata.py @@ -25,7 +25,8 @@ Results for 15001 training iterations: 95 1 train error: 2.89 pixels. Test error: 2.81 pixels. With pcutoff of 0.1 train error: 2.89 pixels. Test error: 2.81 pixels -The analysis of the video takes 41 seconds (batch size 32) and creating the frames 8 seconds (+ a few seconds for ffmpeg) to create the video. +The analysis of the video takes 41 seconds (batch size 32) and creating the frames 8 seconds (+ a few seconds for +ffmpeg) to create the video. """ import os diff --git a/examples/testscript_pretrained_models.py b/examples/testscript_pretrained_models.py index 6d3594fb8c..b907e99eab 100644 --- a/examples/testscript_pretrained_models.py +++ b/examples/testscript_pretrained_models.py @@ -26,7 +26,8 @@ """ configfile, path_train_config=deeplabcut.create_pretrained_human_project(Task, YourName,video, videotype='avi', analyzevideo=True, - createlabeledvideo=True, copy_videos=False) #must leave copy_videos=True + createlabeledvideo=True, copy_videos=False) + #must leave copy_videos=True """ # new way: configfile, path_train_config = deeplabcut.create_pretrained_project( diff --git a/tests/pose_estimation_pytorch/other/test_match_predictions_to_gt.py b/tests/pose_estimation_pytorch/other/test_match_predictions_to_gt.py index a31451d681..1ee4073fc8 100644 --- a/tests/pose_estimation_pytorch/other/test_match_predictions_to_gt.py +++ b/tests/pose_estimation_pytorch/other/test_match_predictions_to_gt.py @@ -12,7 +12,9 @@ import numpy as np import pytest -import deeplabcut.pose_estimation_pytorch.post_processing.match_predictions_to_gt as deeplabcut_torch_match_predictions_gt +from deeplabcut.pose_estimation_pytorch.post_processing import ( + match_predictions_to_gt as deeplabcut_torch_match_predictions_gt, +) @pytest.fixture @@ -75,7 +77,8 @@ def test_invalid_oks(animals_and_keypoints_invalid: tuple) -> None: Test if an invalid output really returns a ValueError in the oks function. Args: - animals_and_keypoints_invalid (tuple): containing predicted keypoints (pred_kpts), ground truth keypoints (gt_kpts) + animals_and_keypoints_invalid (tuple): containing predicted keypoints (pred_kpts), ground truth keypoints + (gt_kpts) and individual names (indv_names) """ pred_kpts, gt_kpts, indv_names = animals_and_keypoints_invalid diff --git a/testscript_cli.py b/testscript_cli.py index 814b22cbd0..295a68f729 100644 --- a/testscript_cli.py +++ b/testscript_cli.py @@ -145,7 +145,8 @@ dlc.create_training_dataset(path_config_file, Shuffles=[2],net_type=net_type,augmenter_type=augmenter_type2) cfg=dlc.auxiliaryfunctions.read_config(path_config_file) -posefile=os.path.join(cfg['project_path'],'dlc-models/iteration-'+str(cfg['iteration'])+'/'+ cfg['Task'] + cfg['date'] + '-trainset' + str(int(cfg['TrainingFraction'][0] * 100)) + 'shuffle' + str(2),'train/pose_cfg.yaml') +posefile=os.path.join(cfg['project_path'],'dlc-models/iteration-'+str(cfg['iteration'])+'/'+ cfg['Task'] + cfg['date'] + +'-trainset' + str(int(cfg['TrainingFraction'][0] * 100)) + 'shuffle' + str(2),'train/pose_cfg.yaml') DLC_config=dlc.auxiliaryfunctions.read_plainconfig(posefile) DLC_config['save_iters']=numiter DLC_config['display_iters']=1 @@ -169,5 +170,6 @@ dlc.export_model(path_config_file, shuffle=1, make_tar=False) print( - "ALL DONE!!! - default/imgaug cases of DLCcore training and evaluation are functional (no extract outlier or refinement tested)." + "ALL DONE!!! - default/imgaug cases of DLCcore training and evaluation are functional (no extract outlier or" + "refinement tested)." ) From 2c3e98bc230412cab864debe4fb914b57ebc6f91 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Wed, 18 Mar 2026 17:19:49 +0100 Subject: [PATCH 72/80] Fix E501 --- deeplabcut/compat.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/deeplabcut/compat.py b/deeplabcut/compat.py index eca6e149ed..7b8cbfeda2 100644 --- a/deeplabcut/compat.py +++ b/deeplabcut/compat.py @@ -590,24 +590,32 @@ def return_evaluate_network_data( If fulldata=True, also returns (the complete annotation and prediction array) Returns list of: - (DataMachine, Data, data, trainIndices, testIndices, trainFraction, DLCscorer,comparisonbodyparts, cfg, Snapshots[snapindex]) + (DataMachine, Data, data, trainIndices, + testIndices, trainFraction, DLCscorer, + comparisonbodyparts, cfg, Snapshots[snapindex] + ) ---------- config : string Full path of the config.yaml file as a string. shuffle: integer - integers specifying shuffle index of the training dataset. The default is 0. + integers specifying shuffle index of the training dataset. + The default is 0. trainingsetindex: int, optional - Integer specifying which TrainingsetFraction to use. By default the first (note that TrainingFraction is a list in config.yaml). + Integer specifying which TrainingsetFraction to use. + By default the first (note that TrainingFraction is a list in config.yaml). This variable can also be set to "all". comparisonbodyparts: list of bodyparts, Default is "all". - The average error will be computed for those body parts only (Has to be a subset of the body parts). + The average error will be computed for those body parts only + (Has to be a subset of the body parts). rescale: bool, default False - Evaluate the model at the 'global_scale' variable (as set in the test/pose_config.yaml file for a particular project). - I.e. every image will be resized according to that scale and prediction will be compared to the resized ground truth. + Evaluate the model at the 'global_scale' variable + (as set in the test/pose_config.yaml file for a particular project). + I.e. every image will be resized according to + that scale and prediction will be compared to the resized ground truth. The error will be reported in pixels at rescaled to the *original* size. I.e. For a [200,200] pixel image evaluated at global_scale=.5, the predictions are calculated on [100,100] pixel images, compared to 1/2*ground truth and this error is then multiplied by 2!. @@ -1598,8 +1606,10 @@ def extract_maps( https://pytorch.org/docs/stable/notes/cuda.html for more information. rescale: bool, default False - Evaluate the model at the 'global_scale' variable (as set in the test/pose_config.yaml file for a particular project). - I.e. every image will be resized according to that scale and prediction will be compared to the resized ground truth. + Evaluate the model at the 'global_scale' variable + (as set in the test/pose_config.yaml file for a particular project). + I.e. every image will be resized according to that scale and prediction + will be compared to the resized ground truth. The error will be reported in pixels at rescaled to the *original* size. I.e. For a [200,200] pixel image evaluated at global_scale=.5, the predictions are calculated on [100,100] pixel images, compared to 1/2*ground truth and this error is then multiplied by 2!. From 7a06b2719542a0c9fc9c341b4bfd4882733f34ac Mon Sep 17 00:00:00 2001 From: Jaap de Ruyter van Steveninck <32810691+deruyter92@users.noreply.github.com> Date: Thu, 19 Mar 2026 17:34:37 +0100 Subject: [PATCH 73/80] fix direct call of abstract base method in ctd.py --- .../pose_estimation_pytorch/data/ctd.py | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/deeplabcut/pose_estimation_pytorch/data/ctd.py b/deeplabcut/pose_estimation_pytorch/data/ctd.py index 5cd93b5608..1b054f9e84 100644 --- a/deeplabcut/pose_estimation_pytorch/data/ctd.py +++ b/deeplabcut/pose_estimation_pytorch/data/ctd.py @@ -121,12 +121,33 @@ def __init__( f"snapshot {bu_snapshot.path.name} for {kwargs['shuffle']} " f"was evaluated (which is when the predictions file is created)." ) + else: + filepath = Path(filepath) if not filepath.exists(): - raise ValueError("Conditions file {conditions_filepath} does not exist. Please check the given path.") + raise ValueError(f"Conditions file {filepath} does not exist. Please check the given path.") self.filepath = filepath + @classmethod + def get_loader_and_snapshot( + cls, + config: str | Path, + shuffle: int, + trainset_index: int = 0, + modelprefix: str = "", + snapshot: str | None = None, + snapshot_index: int | None = None, + ) -> tuple[DLCLoader, Snapshot]: + return super().get_loader_and_snapshot( + config=config, + shuffle=shuffle, + trainset_index=trainset_index, + modelprefix=modelprefix, + snapshot=snapshot, + snapshot_index=snapshot_index, + ) + def load_conditions( self, images: list[str] | None = None, @@ -465,3 +486,22 @@ def __init__( self.config_path = config_path self.snapshot_path = snapshot_path self.scorer = scorer + + @classmethod + def get_loader_and_snapshot( + cls, + config: str | Path, + shuffle: int, + trainset_index: int = 0, + modelprefix: str = "", + snapshot: str | None = None, + snapshot_index: int | None = None, + ) -> tuple[DLCLoader, Snapshot]: + return super().get_loader_and_snapshot( + config=config, + shuffle=shuffle, + trainset_index=trainset_index, + modelprefix=modelprefix, + snapshot=snapshot, + snapshot_index=snapshot_index, + ) From ee65c6c26c024f0ee6fd651cabdac3656bc37ce2 Mon Sep 17 00:00:00 2001 From: Jaap de Ruyter van Steveninck <32810691+deruyter92@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:11:13 +0100 Subject: [PATCH 74/80] move ruff_cleanup_helpers documentation to separate file --- tools/README.md | 345 ++-------------------------------- tools/ruff_cleanup_helpers.md | 0 2 files changed, 12 insertions(+), 333 deletions(-) create mode 100644 tools/ruff_cleanup_helpers.md diff --git a/tools/README.md b/tools/README.md index dfd67c85e6..8400652c5e 100644 --- a/tools/README.md +++ b/tools/README.md @@ -34,7 +34,13 @@ pre-commit run --all-files --- -## 2) License headers +## 2) Ruff cleanup helpers + +For **local Ruff backlog work** (not a substitute for CI or pre-commit), see [Ruff cleanup helpers](ruff_cleanup_helpers.md). It documents `generate_ruff_report.py` (Markdown report from Ruff JSON) and `fix_e501_with_autopep8.py` (targeted long-line cleanup plus Ruff fix/format). + +--- + +## 3) License headers Code headers can be standardized by running: @@ -46,7 +52,7 @@ Run from the repository root. Update `NOTICE.yml` to change header content. --- -## 3) Running tests locally +## 4) Running tests locally ### Run the full test suite @@ -68,334 +74,7 @@ coverage run -m pytest coverage report ``` -# Ruff Cleanup Helpers - -This document describes two small developer-focused utilities that help contributors work through Ruff lint issues in a large Python codebase: - -- `generate_ruff_report.py` — generate a readable Markdown report from Ruff JSON output -- `fix_e501_with_autopep8.py` — aggressively reduce `E501` (line-too-long) violations, then normalize with Ruff - -These tools are intended for **local cleanup workflows**, **incremental lint adoption**, and **one-off contributor maintenance work**. -They are especially useful when a repository already has a non-trivial Ruff backlog and you want to: - -1. understand what remains, -2. prioritize manual fixes, -3. and automate the highest-volume style cleanups safely enough for review. - ---- - -## Who should use these tools? - -These scripts are aimed at: - -- contributors doing lint cleanup PRs, -- maintainers reducing legacy Ruff debt, -- developers triaging a large number of remaining violations, -- anyone who wants a more readable workflow than raw CLI output. - -They are **not** intended to replace normal Ruff usage in CI or pre-commit. Instead, think of them as **cleanup helpers** around Ruff. - ---- - -## What each script does - -### `generate_ruff_report.py` - -Runs Ruff in JSON mode and turns the results into a **human-readable Markdown report**. - -It groups issues: - -- by Ruff rule, -- then by file, -- then by line/column/message. - -It also includes: - -- a summary table, -- short hints for common rules, -- a suggested triage order, -- simple `code -g file:line` commands to jump into affected files. - -This is useful when raw `ruff check` output is too noisy or when you want something that can be attached to an issue / PR / cleanup plan. -It also provides quick navigation and file open commands to help you jump into the right places in the codebase. - ---- - -### `fix_e501_with_autopep8.py` - -Finds files that still contain Ruff `E501` violations, then runs a narrow cleanup pipeline on those files only: - -1. `autopep8` to aggressively reflow long lines, -2. `ruff check --fix --unsafe-fixes` to apply available lint fixes, -3. `ruff format` to normalize formatting. - -This script is intentionally scoped to **files that Ruff already reports as having `E501`** so that it avoids unnecessary churn in unrelated files. - -> [!WARNING] -> This can be rather aggressive. One known issue is for f-strings that are wrapped across multiple lines, which may produce a broken pattern such as: -> ```python -> f"some string with a { -> var -> }" -> ``` -> If that happens, search for lines where `}"` appears by itself with only indentation around it. -> -> A useful regex is: -> ```regex -> ^[ \t]*\}"[ \t]*$ -> ``` - ---- - -## Requirements - -### Required tools - -Both scripts assume the following tools are available on your system PATH: - -- `python` -- `ruff` - -Additionally: - -- `fix_e501_with_autopep8.py` also requires `autopep8` - -### Install example - -```bash -python -m pip install ruff autopep8 -``` - -If you use `uv`: - -```bash -uv add --dev ruff autopep8 -``` - ---- - -## Script 1: `generate_ruff_report.py` - -### Purpose - -Generate a readable Markdown report from Ruff's JSON output. - -### Typical usage - -Run on the whole repository: - -```bash -python generate_ruff_report.py . --output tmp/ruff-report.md -``` - -Run on selected paths only: - -```bash -python generate_ruff_report.py src tests --output tmp/ruff-report.md -``` - -### Output - -By default the script writes to: - -```text -tmp/ruff-report.md -``` - -The output contains: - -- total issue count, -- summary table by rule, -- short notes for common rules, -- suggested triage order, -- per-rule sections, -- per-file counts, -- detailed line/column/message tables, -- quick-open commands for VS Code. - -### Example workflow - -```bash -ruff check . -python generate_ruff_report.py . --output tmp/ruff-report.md -``` - -Open the Markdown report, pick a rule family (for example `F403`, `F405`, `F821`, `E722`, `B904`), and work through the files systematically. - ---- - -## Script 2: `fix_e501_with_autopep8.py` - -### Purpose - -Reduce Ruff `E501` violations (`line-too-long`) using `autopep8`, then normalize those files with Ruff. - -### Typical usage - -Run on the whole repository: - -```bash -python fix_e501_with_autopep8.py . --line-length 88 -``` - -Run on selected paths only: - -```bash -python fix_e501_with_autopep8.py src tests --line-length 100 -``` - -Dry-run mode (show affected files only): - -```bash -python fix_e501_with_autopep8.py . --line-length 88 --check -``` - -### What it does internally - -For the given paths, the script: - -1. runs Ruff in JSON mode, -2. extracts the set of files that still contain `E501`, -3. runs `autopep8` only on those files, -4. runs `ruff check --fix --unsafe-fixes` on the same files, -5. runs `ruff format` on those same files, -6. prints how many files still contain `E501` afterwards. - -### Why this script is narrow by design - -`E501` cleanup can create a lot of diff noise if you run formatters indiscriminately. This tool tries to keep the blast radius smaller by only touching files already flagged by Ruff for line length issues. - -### Known caveat: malformed multiline f-strings - -In some cases, aggressive line wrapping may produce a broken multiline f-string pattern such as: - -```python -f"some string with a { - var -}" -``` - -If that happens, search for lines where `}"` appears by itself with only indentation around it. - -A useful regex is: - -```regex -^[ \t]*\}"[ \t]*$ -``` - -This can help you quickly find and manually repair those cases. - -### Good use cases - -- reducing a large backlog of `E501` violations before a more careful cleanup pass, -- "massaging" legacy code that was never formatter-cleaned consistently. - ---- - -## Recommended workflow for contributors - -If you are working on lint cleanup, a practical workflow is: - -### 1. Generate a report - -```bash -python generate_ruff_report.py . --output tmp/ruff-report.md -``` - -### 2. Reduce long lines first (optional but often useful) - -```bash -python fix_e501_with_autopep8.py . --line-length 88 -``` - -### 3. Re-run the report - -```bash -python generate_ruff_report.py . --output tmp/ruff-report.md -``` - -### 4. Triage remaining issues manually - ---- - -## Limitations - -### `generate_ruff_report.py` - -- only reports what Ruff emits, -- does not fix anything, -- hints are heuristic and intentionally brief. - -### `fix_e501_with_autopep8.py` - -- targets only `E501` files, -- depends on `autopep8` behavior, -- may create formatting diffs that require manual review, -- cannot infer semantic intent for every line wrap, -- may occasionally produce awkward formatting or broken multiline f-strings. - ---- - -## Safety notes - -Before committing results from `fix_e501_with_autopep8.py`: - -1. run Ruff again, -2. run the relevant test suite, -3. scan diff hunks involving long strings / f-strings / messages, -4. review any surprising changes in error messages, docstrings, or string interpolation. - -Suggested commands: - -```bash -ruff check . -ruff format --check . -pytest -``` - ---- - -## Examples - -### Generate a repo-wide manual-fix report - -```bash -python generate_ruff_report.py . --output tmp/ruff-report.md -``` - -### Generate a report only for Python package code - -```bash -python generate_ruff_report.py deeplabcut tests --output tmp/ruff-report.md -``` - -### See which files still have `E501` - -```bash -python fix_e501_with_autopep8.py . --line-length 120 --check -``` - -### Reduce long-line issues, then review the remaining backlog - -```bash -python fix_e501_with_autopep8.py . --line-length 120 -python generate_ruff_report.py . --output tmp/ruff-report.md -``` - ---- - -## Summary - -These scripts are small but practical helpers for maintaining a large Ruff-enabled Python repository: - -- `generate_ruff_report.py` turns Ruff output into a human-readable action plan -- `fix_e501_with_autopep8.py` helps shrink `E501` noise before manual cleanup - -Use them as **developer tools**, not as a substitute for understanding or reviewing changes. - ---- - -## 4) Intelligent test selection (local + CI) +## 5) Intelligent test selection (local + CI) The repository includes a deterministic test-selection tool to reduce CI runtime by running only the relevant workflows and tests based on changed files. @@ -512,7 +191,7 @@ Common causes include: --- -## 5) Docs: Jupyter Book build (local) +## 6) Docs: Jupyter Book build (local) The repo uses Jupyter Book for docs: @@ -526,7 +205,7 @@ jupyter-book build . --- -## 6) Testing the test selector +## 7) Testing the test selector The selector has dedicated tests covering: @@ -543,7 +222,7 @@ pytest tests/tools/test_selector/ --- -## 7) Troubleshooting tips +## 8) Troubleshooting tips - If a workflow run is unexpectedly selecting `full`, inspect the selector reports first. - If targeted tests fail due to missing dependencies, either: diff --git a/tools/ruff_cleanup_helpers.md b/tools/ruff_cleanup_helpers.md new file mode 100644 index 0000000000..e69de29bb2 From 8807c410f4dd6348be976e935f7f15c360cd911c Mon Sep 17 00:00:00 2001 From: Jaap de Ruyter van Steveninck <32810691+deruyter92@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:20:00 +0100 Subject: [PATCH 75/80] Apply linting to newest commits (intellligent CI and inference helpers) --- .../modelzoo/inference_helpers.py | 18 +-- docs/_static/custom.css | 1 - examples/testscript_3d.py | 8 +- .../modelzoo/test_inference_helpers.py | 12 +- ...test_filtered_detector_inference_runner.py | 4 +- .../test_selector/test_selector_decision.py | 9 +- .../test_selector/test_selector_validation.py | 12 +- tools/test_selector.py | 115 ++++++++---------- tools/test_selector_config.py | 35 ++---- 9 files changed, 75 insertions(+), 139 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/modelzoo/inference_helpers.py b/deeplabcut/pose_estimation_pytorch/modelzoo/inference_helpers.py index a9d9fdce6c..8f75401cf4 100644 --- a/deeplabcut/pose_estimation_pytorch/modelzoo/inference_helpers.py +++ b/deeplabcut/pose_estimation_pytorch/modelzoo/inference_helpers.py @@ -16,8 +16,6 @@ import logging from pathlib import Path -import torch - import deeplabcut.modelzoo.weight_initialization as weight_initialization from deeplabcut.core.config import read_config_as_dict from deeplabcut.pose_estimation_pytorch.apis.utils import ( @@ -51,11 +49,7 @@ def _build_humanbody_inference_runners( "A filtered torchvision detector runner is used instead." ) - torchvision_detector_name = ( - detector_name - if detector_name is not None - else "fasterrcnn_mobilenet_v3_large_fpn" - ) + torchvision_detector_name = detector_name if detector_name is not None else "fasterrcnn_mobilenet_v3_large_fpn" pose_snapshot_path = customized_pose_checkpoint if pose_snapshot_path is None: @@ -157,9 +151,7 @@ def create_superanimal_inference_runners( >>> print(len(pose_preds)) """ if model_name.lower().startswith("fmpose3d"): - raise NotImplementedError( - "FMPose3D is not supported in this helper. Use the FMPose3D inference API." - ) + raise NotImplementedError("FMPose3D is not supported in this helper. Use the FMPose3D inference API.") if device is None: device = "auto" @@ -192,11 +184,7 @@ def create_superanimal_inference_runners( # Top-down models typically need a detector for bbox generation. If no detector # is configured, the returned detector_runner will be None and callers should # provide bboxes in the pose input context. - if ( - Task(model_cfg["method"]) == Task.TOP_DOWN - and detector_name is None - and customized_detector_checkpoint is None - ): + if Task(model_cfg["method"]) == Task.TOP_DOWN and detector_name is None and customized_detector_checkpoint is None: logging.warning( "Top-down model configured without a detector. " "Returning detector_runner=None; pass bboxes in pose input context." diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 8aaee79994..c8df32f9cf 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -101,4 +101,3 @@ html[data-theme="dark"] { opacity: 0.8; z-index: 1; } - diff --git a/examples/testscript_3d.py b/examples/testscript_3d.py index cfa0c365c8..0ee29d095b 100644 --- a/examples/testscript_3d.py +++ b/examples/testscript_3d.py @@ -105,9 +105,7 @@ try: config = glob.glob(os.path.join(basepath, "TEST*", "config.yaml"))[-1] except Exception as e: - raise RuntimeError( - "Please run the testscript_tensorflow_single_animal.py first before testing for 3d" - ) from e + raise RuntimeError("Please run the testscript_tensorflow_single_animal.py first before testing for 3d") from e dfolder = None @@ -183,9 +181,7 @@ deeplabcut.triangulate(path_config_file, video_dir, save_as_csv=True) print("CREATING LABELED VIDEO 3-D") - deeplabcut.create_labeled_video_3d( - path_config_file, [video_dir], start=5, end=10, videotype=".avi" - ) + deeplabcut.create_labeled_video_3d(path_config_file, [video_dir], start=5, end=10, videotype=".avi") # output_path = [os.path.join(basepath,folder)] # deeplabcut.create_labeled_video_3d(path_config_file,output_path,start=5,end=10) diff --git a/tests/pose_estimation_pytorch/modelzoo/test_inference_helpers.py b/tests/pose_estimation_pytorch/modelzoo/test_inference_helpers.py index c3495a5545..50e58e234d 100644 --- a/tests/pose_estimation_pytorch/modelzoo/test_inference_helpers.py +++ b/tests/pose_estimation_pytorch/modelzoo/test_inference_helpers.py @@ -32,9 +32,7 @@ def fake_read_config_as_dict(path): return cfg monkeypatch.setattr(helpers, "read_config_as_dict", fake_read_config_as_dict) - monkeypatch.setattr( - helpers, "update_config", lambda config, max_individuals, device: config - ) + monkeypatch.setattr(helpers, "update_config", lambda config, max_individuals, device: config) monkeypatch.setattr( helpers, "get_inference_runners", @@ -108,9 +106,7 @@ def fake_update_config(config, max_individuals, device): @pytest.mark.parametrize("input_device", ["auto", None]) -def test_create_superanimal_inference_runners_auto_device_selection( - monkeypatch, input_device -): +def test_create_superanimal_inference_runners_auto_device_selection(monkeypatch, input_device): cfg = _dummy_cfg("TD") captured = {} @@ -164,9 +160,7 @@ def test_create_superanimal_inference_runners_propagates_unsupported_dataset_err monkeypatch.setattr( helpers, "load_super_animal_config", - lambda **kwargs: (_ for _ in ()).throw( - ValueError("Unsupported dataset for model zoo config") - ), + lambda **kwargs: (_ for _ in ()).throw(ValueError("Unsupported dataset for model zoo config")), ) with pytest.raises(ValueError, match="Unsupported dataset"): diff --git a/tests/pose_estimation_pytorch/runners/test_filtered_detector_inference_runner.py b/tests/pose_estimation_pytorch/runners/test_filtered_detector_inference_runner.py index f17454c757..4eaa6ad543 100644 --- a/tests/pose_estimation_pytorch/runners/test_filtered_detector_inference_runner.py +++ b/tests/pose_estimation_pytorch/runners/test_filtered_detector_inference_runner.py @@ -32,9 +32,7 @@ def test_torchvision_detector(): print("Torchvision detector loaded successfully!") # Test loading the FilteredDetector - person_detector = FilteredDetector( - coco_detector, class_id=COCO_PERSON_CATEGORY_ID - ) + person_detector = FilteredDetector(coco_detector, class_id=COCO_PERSON_CATEGORY_ID) person_detector.eval() print("Filtered detector loaded successfully!") diff --git a/tests/tools/test_selector/test_selector_decision.py b/tests/tools/test_selector/test_selector_decision.py index dd15ea3c30..b406f05cf6 100644 --- a/tests/tools/test_selector/test_selector_decision.py +++ b/tests/tools/test_selector/test_selector_decision.py @@ -94,10 +94,7 @@ def test_fast_multianimal_includes_functional(selector): assert "examples/testscript_tensorflow_multi_animal.py" in res.functional_scripts assert "multianimal" in res.provenance.pytest["tests/test_predict_multianimal.py"] - assert ( - "multianimal" - in res.provenance.scripts["examples/testscript_tensorflow_multi_animal.py"] - ) + assert "multianimal" in res.provenance.scripts["examples/testscript_tensorflow_multi_animal.py"] def test_fast_ci_workflows_uses_minimal_pytest(selector): @@ -209,9 +206,7 @@ def test_category_rule_rejects_invalid_repo_relative_paths(field_name, bad_value } kwargs[field_name] = [bad_value] - with pytest.raises( - ValidationError, match="repo-relative|path traversal|absolute path" - ): + with pytest.raises(ValidationError, match="repo-relative|path traversal|absolute path"): CategoryRule(**kwargs) diff --git a/tests/tools/test_selector/test_selector_validation.py b/tests/tools/test_selector/test_selector_validation.py index 1d720a42f2..41d2bfe912 100644 --- a/tests/tools/test_selector/test_selector_validation.py +++ b/tests/tools/test_selector/test_selector_validation.py @@ -4,7 +4,6 @@ import subprocess from pathlib import Path -import pydantic import pytest @@ -15,8 +14,7 @@ def _git(repo: Path, *args: str) -> str: proc = subprocess.run( ["git", *args], cwd=repo, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + capture_output=True, text=True, check=False, ) @@ -117,9 +115,7 @@ def test_determine_diff_range_push_uses_before_after(selector, tmp_path, monkeyp assert mode == selector.DiffMode.PUSH -def test_determine_diff_range_push_zero_sha_uses_empty_tree( - selector, tmp_path, monkeypatch -): +def test_determine_diff_range_push_zero_sha_uses_empty_tree(selector, tmp_path, monkeypatch): repo = _init_repo(tmp_path) after = _commit_file(repo, "initial.txt", "hello", "initial commit") @@ -135,9 +131,7 @@ def test_determine_diff_range_push_zero_sha_uses_empty_tree( assert mode == selector.DiffMode.INITIAL -def test_determine_diff_range_fallback_uses_head_parent( - selector, tmp_path, monkeypatch -): +def test_determine_diff_range_fallback_uses_head_parent(selector, tmp_path, monkeypatch): repo = _init_repo(tmp_path) prev = _commit_file(repo, "a.txt", "one", "first commit") diff --git a/tools/test_selector.py b/tools/test_selector.py index 4c18d3dff7..bf69ed2b01 100755 --- a/tools/test_selector.py +++ b/tools/test_selector.py @@ -53,9 +53,10 @@ import re import subprocess from collections import defaultdict +from collections.abc import Callable, Sequence from enum import Enum from pathlib import Path -from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple +from typing import Any from pydantic import BaseModel, ConfigDict, Field, ValidationError @@ -111,9 +112,7 @@ class LaneSelection(BaseModel): skip: bool = False # Skip all tests (e.g. lint-only changes only) docs: bool = False # Run docs build checks - fast: bool = ( - False # Run targeted pytest + optional functional scripts (single-lane) - ) + fast: bool = False # Run targeted pytest + optional functional scripts (single-lane) full: bool = False # Delegate to full test workflow/matrix @@ -122,8 +121,8 @@ class SelectionProvenance(BaseModel): model_config = ConfigDict(extra="forbid") - pytest: Dict[str, List[str]] = Field(default_factory=dict) - scripts: Dict[str, List[str]] = Field(default_factory=dict) + pytest: dict[str, list[str]] = Field(default_factory=dict) + scripts: dict[str, list[str]] = Field(default_factory=dict) class SelectorResult(BaseModel): @@ -136,17 +135,18 @@ class SelectorResult(BaseModel): lanes: LaneSelection = Field(default_factory=LaneSelection) - pytest_paths: List[str] = Field(default_factory=list) - functional_scripts: List[str] = Field(default_factory=list) + pytest_paths: list[str] = Field(default_factory=list) + functional_scripts: list[str] = Field(default_factory=list) provenance: SelectionProvenance = Field(default_factory=SelectionProvenance) - reasons: List[str] = Field(default_factory=list) - changed_files: List[str] = Field(default_factory=list) - lane_reasons: Dict[str, List[str]] = Field(default_factory=dict) + reasons: list[str] = Field(default_factory=list) + changed_files: list[str] = Field(default_factory=list) + lane_reasons: dict[str, list[str]] = Field(default_factory=dict) SelectorResult.model_rebuild() # Ensure model is fully built at import time for validation in main() + # ----------------------------- # Git helpers # ----------------------------- @@ -154,8 +154,7 @@ def _run_git(args: Sequence[str], cwd: Path) -> str: proc = subprocess.run( ["git", *args], cwd=str(cwd), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + capture_output=True, text=True, check=False, ) @@ -179,7 +178,7 @@ def _ensure_commit_exists(sha: str, cwd: Path) -> None: _run_git(["cat-file", "-e", f"{sha}^{{commit}}"], cwd) -def _load_github_event() -> Dict[str, Any]: +def _load_github_event() -> dict[str, Any]: path = os.environ.get("GITHUB_EVENT_PATH") if not path: return {} @@ -210,9 +209,7 @@ def _empty_tree(repo: Path) -> str: return _validate_sha("empty-tree", empty) -def determine_diff_range( - repo: Path, override_base: Optional[str], override_head: Optional[str] -) -> Tuple[str, str, DiffMode]: +def determine_diff_range(repo: Path, override_base: str | None, override_head: str | None) -> tuple[str, str, DiffMode]: """Return (base_commit, head_commit, mode).""" zero_sha = "0" * 40 event_name = os.environ.get("GITHUB_EVENT_NAME", "") @@ -257,9 +254,7 @@ def determine_diff_range( _ensure_commit_exists(head, repo) try: - prev = _validate_sha( - "HEAD^", _run_git(["rev-parse", "--verify", "HEAD^"], repo) - ) + prev = _validate_sha("HEAD^", _run_git(["rev-parse", "--verify", "HEAD^"], repo)) _ensure_commit_exists(prev, repo) return prev, head, DiffMode.FALLBACK except Exception: @@ -270,7 +265,7 @@ def determine_diff_range( return "", "", DiffMode.FALLBACK_NO_HEAD -def changed_files(repo: Path, base: str, head: str) -> List[str]: +def changed_files(repo: Path, base: str, head: str) -> list[str]: if not base or not head: return [] out = _run_git(["diff", "--name-only", "--diff-filter=ACMRTD", base, head], repo) @@ -290,7 +285,7 @@ def _is_safe_relpath(p: str) -> bool: def validate_selected_paths(res: SelectorResult, repo: Path) -> SelectorResult: - missing: List[str] = [] + missing: list[str] = [] # validate pytest paths (files/dirs) for p in res.pytest_paths: @@ -337,9 +332,9 @@ def _matches_any(path: str, preds: Sequence[Callable[[str], bool]]) -> bool: return False -def decide(files: List[str]) -> SelectorResult: - reasons: List[str] = [] - lane_reasons: Dict[str, List[str]] = {} +def decide(files: list[str]) -> SelectorResult: + reasons: list[str] = [] + lane_reasons: dict[str, list[str]] = {} lanes = LaneSelection() if not files: @@ -378,28 +373,22 @@ def decide(files: List[str]) -> SelectorResult: # Docs lane is orthogonal: if any routed file matches docs, enable docs lane. docs_rule = CATEGORY_RULE_BY_NAME.get("docs") - docs_touched = bool( - docs_rule and any(_matches_any(f, docs_rule.match_any) for f in routed_files) - ) - docs_matched_files = { - f for f in routed_files if docs_rule and _matches_any(f, docs_rule.match_any) - } + docs_touched = bool(docs_rule and any(_matches_any(f, docs_rule.match_any) for f in routed_files)) + docs_matched_files = {f for f in routed_files if docs_rule and _matches_any(f, docs_rule.match_any)} non_docs_routed_files = [f for f in routed_files if f not in docs_matched_files] - docs_pytests_sorted: List[str] = [] - docs_scripts_sorted: List[str] = [] + docs_pytests_sorted: list[str] = [] + docs_scripts_sorted: list[str] = [] if docs_touched: lanes.docs = True reasons.append("category:docs") lane_reasons["docs"] = ["category:docs"] docs_pytests_sorted = sorted(set(docs_rule.pytest_paths)) if docs_rule else [] - docs_scripts_sorted = ( - sorted(set(docs_rule.functional_scripts)) if docs_rule else [] - ) + docs_scripts_sorted = sorted(set(docs_rule.functional_scripts)) if docs_rule else [] # Full-suite triggers always win over fast, but docs lane can still remain enabled. - triggered: List[Tuple[str, str]] = [] + triggered: list[tuple[str, str]] = [] for f in routed_files: for name, pred in FULL_SUITE_TRIGGERS: if _matches_any(f, [pred]): @@ -436,10 +425,10 @@ def decide(files: List[str]) -> SelectorResult: for rule in matched_non_docs: reasons.append(f"category:{rule.name}") - pytest_paths_set: Set[str] = set() - functional_set: Set[str] = set() - pytest_sources: Dict[str, Set[str]] = defaultdict(set) - script_sources: Dict[str, Set[str]] = defaultdict(set) + pytest_paths_set: set[str] = set() + functional_set: set[str] = set() + pytest_sources: dict[str, set[str]] = defaultdict(set) + script_sources: dict[str, set[str]] = defaultdict(set) # Docs rules may contribute tests/scripts to the fast lane. if docs_touched: @@ -502,7 +491,7 @@ def decide(files: List[str]) -> SelectorResult: # Fast lane selected lanes.fast = True - fast_reasons: List[str] = [] + fast_reasons: list[str] = [] if docs_touched and (docs_pytests_sorted or docs_scripts_sorted): fast_reasons.append("category:docs") fast_reasons.extend(f"category:{rule.name}" for rule in matched_non_docs) @@ -527,17 +516,17 @@ def decide(files: List[str]) -> SelectorResult: # ----------------------------- # Outputs # ----------------------------- -def explain_changed_files(files: List[str]) -> Dict[str, Any]: +def explain_changed_files(files: list[str]) -> dict[str, Any]: """ Build an explanation structure for reporting: - per-file: full_trigger_matches, category_matches - grouped: full_triggers, by_category, uncategorized """ - per_file: Dict[str, Dict[str, Any]] = {} - by_category: Dict[str, List[str]] = defaultdict(list) - full_trigger_files: Dict[str, List[str]] = defaultdict(list) - lint_only_files: List[str] = [] - uncategorized: List[str] = [] + per_file: dict[str, dict[str, Any]] = {} + by_category: dict[str, list[str]] = defaultdict(list) + full_trigger_files: dict[str, list[str]] = defaultdict(list) + lint_only_files: list[str] = [] + uncategorized: list[str] = [] # Prep category predicates categories = [(r.name, r.match_any) for r in CATEGORY_RULES] @@ -597,7 +586,7 @@ def explain_changed_files(files: List[str]) -> Dict[str, Any]: def _render_file_line( f: str, - info: Dict[str, Any], + info: dict[str, Any], emoji: bool = False, add_tag: bool = True, add_marker: bool = False, @@ -629,7 +618,7 @@ def _render_file_line( return f"- {marker}`{f}`{tag_str}" -def _enabled_lane_names(res: SelectorResult) -> List[str]: +def _enabled_lane_names(res: SelectorResult) -> list[str]: order = ("skip", "docs", "fast", "full") return [name for name in order if getattr(res.lanes, name)] @@ -645,7 +634,7 @@ def _lane_label(name: str, emoji: bool = False) -> str: }.get(name, name) -def _compact_reasons(reasons: List[str]) -> List[str]: +def _compact_reasons(reasons: list[str]) -> list[str]: cats = sorted({r.split(":", 1)[1] for r in reasons if r.startswith("category:")}) other = [r for r in reasons if not r.startswith("category:")] out = [] @@ -672,7 +661,7 @@ def _render_decision_markdown( style: str = "minimal", emoji: bool = False, ) -> str: - def bullet(items: List[str], limit_: int = limit) -> str: + def bullet(items: list[str], limit_: int = limit) -> str: if not items: return "_(none)_" shown = items[:limit_] @@ -684,9 +673,7 @@ def bullet(items: List[str], limit_: int = limit) -> str: # Selection line (minimal, no emoji by default) selected_lanes = _enabled_lane_names(res) if emoji: - selected_lanes_label = ", ".join( - _lane_label(name, emoji=True) for name in selected_lanes - ) + selected_lanes_label = ", ".join(_lane_label(name, emoji=True) for name in selected_lanes) else: selected_lanes_label = ", ".join(f"`{name}`" for name in selected_lanes) @@ -695,7 +682,7 @@ def bullet(items: List[str], limit_: int = limit) -> str: diff_mode = f"{MODE_LABELS.get(res.diff_mode, res.diff_mode.value)}" - md: List[str] = [] + md: list[str] = [] md.append("# Test selection\n") md.append(f"**Selected workflows:** {selected_lanes_label}\n") md.append(f"**Diff mode:** `{diff_mode}`\n") @@ -727,9 +714,7 @@ def bullet(items: List[str], limit_: int = limit) -> str: # (Always collapsible if present; otherwise omit section.) if exp["full_trigger_files"]: total_triggered = sum(len(v) for v in exp["full_trigger_files"].values()) - md.append( - _details_open(f"Files that match full-suite triggers ({total_triggered})") - ) + md.append(_details_open(f"Files that match full-suite triggers ({total_triggered})")) for trig_name in sorted(exp["full_trigger_files"].keys()): files_for_trigger = exp["full_trigger_files"][trig_name] md.append(f"**{trig_name}** ({len(files_for_trigger)})") @@ -765,9 +750,7 @@ def bullet(items: List[str], limit_: int = limit) -> str: # Lint-only as collapsible if exp.get("lint_only"): lint_files = exp["lint_only"] - md.append( - _details_open(f"Lint-only ({len(lint_files)}) — ignored for test selection") - ) + md.append(_details_open(f"Lint-only ({len(lint_files)}) — ignored for test selection")) md.append("") for f in lint_files[:limit]: md.append(f"- `{f}`") @@ -836,7 +819,7 @@ def write_report_files( out_dir: Path, report_style: str = "minimal", no_emoji: bool = False, -) -> Tuple[Path, Path]: +) -> tuple[Path, Path]: out_dir.mkdir(parents=True, exist_ok=True) json_path = out_dir / "selection.json" md_path = out_dir / "decision.md" @@ -885,7 +868,7 @@ def j(v) -> str: f.write(f"provenance={j(res.provenance.model_dump())}\n") -def main(argv: Optional[Sequence[str]] = None) -> int: +def main(argv: Sequence[str] | None = None) -> int: ap = argparse.ArgumentParser(description="Deterministic DeepLabCut test selector") ap.add_argument("--json", action="store_true", help="Print JSON result to stdout") ap.add_argument( @@ -948,9 +931,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: # Always write report files for transparency report_dir = Path(args.report_dir) - json_path, md_path = write_report_files( - res, report_dir, report_style=args.report_style, no_emoji=args.no_emoji - ) + json_path, md_path = write_report_files(res, report_dir, report_style=args.report_style, no_emoji=args.no_emoji) if args.json: print(res.model_dump_json(indent=2)) diff --git a/tools/test_selector_config.py b/tools/test_selector_config.py index bd711e8122..871efd7b5d 100644 --- a/tools/test_selector_config.py +++ b/tools/test_selector_config.py @@ -1,9 +1,10 @@ """Test selector configuration.""" + from __future__ import annotations import re +from collections.abc import Callable from pathlib import PurePosixPath -from typing import Callable from pydantic import BaseModel, ConfigDict, Field, field_validator @@ -57,9 +58,7 @@ def _validate_relpath_string(value: str, field_name: str) -> str: value = value.replace("\\", "/") if value.startswith("/"): - raise ValueError( - f"{field_name} must be repo-relative, got absolute path: {value!r}" - ) + raise ValueError(f"{field_name} must be repo-relative, got absolute path: {value!r}") if re.match(r"^[A-Za-z]:/", value): raise ValueError(f"{field_name} must not be a Windows absolute path: {value!r}") @@ -82,10 +81,7 @@ class CategoryRule(BaseModel): name: str match_any: list[PathPred] = Field( min_length=1, - description=( - "List of predicates; if any predicate matches any changed file, " - "the rule is triggered." - ), + description=("List of predicates; if any predicate matches any changed file, the rule is triggered."), ) pytest_paths: list[str] = Field( default_factory=list, @@ -103,7 +99,7 @@ def validate_name(cls, value: str) -> str: if not value: raise ValueError("Rule name must not be empty") if not _RULE_NAME_RE.match(value): - raise ValueError("Rule name must match ^[a-z0-9_]+$ " f"(got {value!r})") + raise ValueError(f"Rule name must match ^[a-z0-9_]+$ (got {value!r})") return value @field_validator("match_any") @@ -113,9 +109,7 @@ def validate_match_any(cls, preds: list[PathPred]) -> list[PathPred]: raise ValueError("match_any must contain at least one predicate") for i, pred in enumerate(preds): if not callable(pred): - raise TypeError( - f"match_any[{i}] must be callable, got {type(pred).__name__}" - ) + raise TypeError(f"match_any[{i}] must be callable, got {type(pred).__name__}") return preds @field_validator("pytest_paths") @@ -136,10 +130,7 @@ def validate_category_rules(rules: list[CategoryRule]) -> list[CategoryRule]: for idx, rule in enumerate(rules): if rule.name in seen: first_idx = seen[rule.name] - raise ValueError( - f"Duplicate CategoryRule name {rule.name!r} " - f"at indexes {first_idx} and {idx}" - ) + raise ValueError(f"Duplicate CategoryRule name {rule.name!r} at indexes {first_idx} and {idx}") seen[rule.name] = idx return rules @@ -221,14 +212,14 @@ def validate_category_rules(rules: list[CategoryRule]) -> list[CategoryRule]: pytest_paths=[ "tests/test_predict_supermodel.py", "tests/pose_estimation_pytorch/modelzoo/", - "tests/pose_estimation_pytorch/other/test_modelzoo.py", # (currently all tests are skipped in this file..) + "tests/pose_estimation_pytorch/other/test_modelzoo.py", # (currently all tests are skipped in this file..) # noqa: E501 ], functional_scripts=[ - # TODO: decide which of these functional testscripts are useful and not too heavy - "examples/testscript_superanimal_adaptation.py", # (runs inference + video adaptation training on shortened video) - # "examples/testscript_superanimal_create_pretrained_project.py", # (runs inference on example videos) - # "examples/testscript_superanimal_inference.py", # (runs inference on multiple videos with multiple models) - # "examples/testscript_superanimal_transfer_learning.py", # (runs full standard training pipeline after weight init) + # TODO: decide which of these functional testscripts are useful and not too heavy # noqa: E501 + "examples/testscript_superanimal_adaptation.py", # (runs inference + video adaptation training on shortened video) # noqa: E501 + # "examples/testscript_superanimal_create_pretrained_project.py", # (runs inference on example videos) # noqa: E501 + # "examples/testscript_superanimal_inference.py", # (runs inference on multiple videos with multiple models) # noqa: E501 + # "examples/testscript_superanimal_transfer_learning.py", # (runs full standard training pipeline after weight init) # noqa: E501 ], ), CategoryRule( From 4fd852d18d10f43be2f67363a01278cb39d72cbf Mon Sep 17 00:00:00 2001 From: Jaap de Ruyter van Steveninck <32810691+deruyter92@users.noreply.github.com> Date: Fri, 20 Mar 2026 13:45:34 +0100 Subject: [PATCH 76/80] Update pre-commit & linting configuration (#3215) * Update pre-commit config * Update pyproject.toml with new linting * update pyproject.toml remove tool.isort (already specified via ruff). * Update uv.lock * align pre-commit config with PR #3216 * fix CI python-package workflow: remove unauthorized channel `defaults` * Align pyproject.toml with PR #3216 --------- Co-authored-by: Cyril Achard --- .github/workflows/python-package.yml | 2 +- pyproject.toml | 13 ------------- uv.lock | 27 +++++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 71d5df0efe..f5fd4f8137 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -67,7 +67,7 @@ jobs: - name: Set up Python uses: conda-incubator/setup-miniconda@v3 with: - channels: conda-forge,defaults + channels: conda-forge channel-priority: strict python-version: ${{ matrix.python-version }} diff --git a/pyproject.toml b/pyproject.toml index a328e5d6ed..a0031a9105 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -149,19 +149,6 @@ ignore = [ "E741", "B007" ] [tool.ruff.lint.pydocstyle] convention = "google" -[tool.isort] -multi_line_output = 3 -include_trailing_comma = true -force_sort_within_sections = false -lexicographical = true -single_line_exclusions = [ "typing" ] -order_by_type = false -group_by_package = true -line_length = 88 -skip = [ - "__init__.py", -] - [tool.pyproject-fmt] max_supported_python = "3.12" generate_python_version_classifiers = true diff --git a/uv.lock b/uv.lock index dd08a6906a..e5ee06b469 100644 --- a/uv.lock +++ b/uv.lock @@ -1140,6 +1140,7 @@ dev = [ { name = "pydantic" }, { name = "pytest" }, { name = "pytest-cov" }, + { name = "ruff" }, ] [package.metadata] @@ -1206,6 +1207,7 @@ dev = [ { name = "pydantic", specifier = ">=2,<3" }, { name = "pytest" }, { name = "pytest-cov" }, + { name = "ruff" }, ] [[package]] @@ -5022,6 +5024,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, ] +[[package]] +name = "ruff" +version = "0.15.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/22/9e4f66ee588588dc6c9af6a994e12d26e19efbe874d1a909d09a6dac7a59/ruff-0.15.7.tar.gz", hash = "sha256:04f1ae61fc20fe0b148617c324d9d009b5f63412c0b16474f3d5f1a1a665f7ac", size = 4601277, upload-time = "2026-03-19T16:26:22.605Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/2f/0b08ced94412af091807b6119ca03755d651d3d93a242682bf020189db94/ruff-0.15.7-py3-none-linux_armv6l.whl", hash = "sha256:a81cc5b6910fb7dfc7c32d20652e50fa05963f6e13ead3c5915c41ac5d16668e", size = 10489037, upload-time = "2026-03-19T16:26:32.47Z" }, + { url = "https://files.pythonhosted.org/packages/91/4a/82e0fa632e5c8b1eba5ee86ecd929e8ff327bbdbfb3c6ac5d81631bef605/ruff-0.15.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:722d165bd52403f3bdabc0ce9e41fc47070ac56d7a91b4e0d097b516a53a3477", size = 10955433, upload-time = "2026-03-19T16:27:00.205Z" }, + { url = "https://files.pythonhosted.org/packages/ab/10/12586735d0ff42526ad78c049bf51d7428618c8b5c467e72508c694119df/ruff-0.15.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7fbc2448094262552146cbe1b9643a92f66559d3761f1ad0656d4991491af49e", size = 10269302, upload-time = "2026-03-19T16:26:26.183Z" }, + { url = "https://files.pythonhosted.org/packages/eb/5d/32b5c44ccf149a26623671df49cbfbd0a0ae511ff3df9d9d2426966a8d57/ruff-0.15.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b39329b60eba44156d138275323cc726bbfbddcec3063da57caa8a8b1d50adf", size = 10607625, upload-time = "2026-03-19T16:27:03.263Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f1/f0001cabe86173aaacb6eb9bb734aa0605f9a6aa6fa7d43cb49cbc4af9c9/ruff-0.15.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87768c151808505f2bfc93ae44e5f9e7c8518943e5074f76ac21558ef5627c85", size = 10324743, upload-time = "2026-03-19T16:27:09.791Z" }, + { url = "https://files.pythonhosted.org/packages/7a/87/b8a8f3d56b8d848008559e7c9d8bf367934d5367f6d932ba779456e2f73b/ruff-0.15.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb0511670002c6c529ec66c0e30641c976c8963de26a113f3a30456b702468b0", size = 11138536, upload-time = "2026-03-19T16:27:06.101Z" }, + { url = "https://files.pythonhosted.org/packages/e4/f2/4fd0d05aab0c5934b2e1464784f85ba2eab9d54bffc53fb5430d1ed8b829/ruff-0.15.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0d19644f801849229db8345180a71bee5407b429dd217f853ec515e968a6912", size = 11994292, upload-time = "2026-03-19T16:26:48.718Z" }, + { url = "https://files.pythonhosted.org/packages/64/22/fc4483871e767e5e95d1622ad83dad5ebb830f762ed0420fde7dfa9d9b08/ruff-0.15.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4806d8e09ef5e84eb19ba833d0442f7e300b23fe3f0981cae159a248a10f0036", size = 11398981, upload-time = "2026-03-19T16:26:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/b0/99/66f0343176d5eab02c3f7fcd2de7a8e0dd7a41f0d982bee56cd1c24db62b/ruff-0.15.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5", size = 11242422, upload-time = "2026-03-19T16:26:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/5d/3a/a7060f145bfdcce4c987ea27788b30c60e2c81d6e9a65157ca8afe646328/ruff-0.15.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1852ce241d2bc89e5dc823e03cff4ce73d816b5c6cdadd27dbfe7b03217d2a12", size = 11232158, upload-time = "2026-03-19T16:26:42.321Z" }, + { url = "https://files.pythonhosted.org/packages/a7/53/90fbb9e08b29c048c403558d3cdd0adf2668b02ce9d50602452e187cd4af/ruff-0.15.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5f3e4b221fb4bd293f79912fc5e93a9063ebd6d0dcbd528f91b89172a9b8436c", size = 10577861, upload-time = "2026-03-19T16:26:57.459Z" }, + { url = "https://files.pythonhosted.org/packages/2f/aa/5f486226538fe4d0f0439e2da1716e1acf895e2a232b26f2459c55f8ddad/ruff-0.15.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b15e48602c9c1d9bdc504b472e90b90c97dc7d46c7028011ae67f3861ceba7b4", size = 10327310, upload-time = "2026-03-19T16:26:35.909Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/271afdffb81fe7bfc8c43ba079e9d96238f674380099457a74ccb3863857/ruff-0.15.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b4705e0e85cedc74b0a23cf6a179dbb3df184cb227761979cc76c0440b5ab0d", size = 10840752, upload-time = "2026-03-19T16:26:45.723Z" }, + { url = "https://files.pythonhosted.org/packages/bf/29/a4ae78394f76c7759953c47884eb44de271b03a66634148d9f7d11e721bd/ruff-0.15.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:112c1fa316a558bb34319282c1200a8bf0495f1b735aeb78bfcb2991e6087580", size = 11336961, upload-time = "2026-03-19T16:26:39.076Z" }, + { url = "https://files.pythonhosted.org/packages/26/6b/8786ba5736562220d588a2f6653e6c17e90c59ced34a2d7b512ef8956103/ruff-0.15.7-py3-none-win32.whl", hash = "sha256:6d39e2d3505b082323352f733599f28169d12e891f7dd407f2d4f54b4c2886de", size = 10582538, upload-time = "2026-03-19T16:26:15.992Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e9/346d4d3fffc6871125e877dae8d9a1966b254fbd92a50f8561078b88b099/ruff-0.15.7-py3-none-win_amd64.whl", hash = "sha256:4d53d712ddebcd7dace1bc395367aec12c057aacfe9adbb6d832302575f4d3a1", size = 11755839, upload-time = "2026-03-19T16:26:19.897Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e8/726643a3ea68c727da31570bde48c7a10f1aa60eddd628d94078fec586ff/ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2", size = 11023304, upload-time = "2026-03-19T16:26:51.669Z" }, +] + [[package]] name = "safetensors" version = "0.7.0" From 5bfdd3930ae179a665b74dd122c7a3cb1cbacb04 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Mon, 23 Mar 2026 11:28:25 -0500 Subject: [PATCH 77/80] Run pre-commit on latest --- .pre-commit-config.yaml | 2 +- deeplabcut/modelzoo/video_inference.py | 4 +- docs/README.md | 2 +- examples/COLAB/COLAB_DLC_ModelZoo.ipynb | 45 ---- .../modelzoo/test_fmpose_integration.py | 8 +- .../test_check_contracts.py | 38 +-- tools/docs_and_notebooks_check.py | 239 +++++++----------- tools/docs_and_notebooks_report_config.yml | 6 +- 8 files changed, 102 insertions(+), 242 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 93233ec537..315fabee9f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -100,4 +100,4 @@ repos: - "pydantic>=2,<3" - "pyyaml" - "nbformat>=5" - stages: [pre-commit, manual] + stages: [pre-commit, manual] diff --git a/deeplabcut/modelzoo/video_inference.py b/deeplabcut/modelzoo/video_inference.py index 33e0d3eeaa..b2ffc2174e 100644 --- a/deeplabcut/modelzoo/video_inference.py +++ b/deeplabcut/modelzoo/video_inference.py @@ -326,9 +326,7 @@ def video_inference_superanimal( if scale_list is None: scale_list = [] if not model_name.startswith("fmpose3d"): - print( - f"Running video inference on {videos} with {superanimal_name}_{model_name}" - ) + print(f"Running video inference on {videos} with {superanimal_name}_{model_name}") dlc_root_path = get_deeplabcut_path() modelzoo_path = os.path.join(dlc_root_path, "modelzoo") available_architectures = json.load(open(os.path.join(modelzoo_path, "models_to_framework.json"))) diff --git a/docs/README.md b/docs/README.md index 1114741159..2812d8f001 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,6 +4,6 @@ deeplabcut: last_metadata_updated: '2026-03-06' ignore: false --- -Please see https://deeplabcut.github.io/DeepLabCut for documentation on how to use this software. +Please see https://deeplabcut.github.io/DeepLabCut for documentation on how to use this software. This directory contains the source code for the docs. diff --git a/examples/COLAB/COLAB_DLC_ModelZoo.ipynb b/examples/COLAB/COLAB_DLC_ModelZoo.ipynb index 45da154439..3a48250c18 100644 --- a/examples/COLAB/COLAB_DLC_ModelZoo.ipynb +++ b/examples/COLAB/COLAB_DLC_ModelZoo.ipynb @@ -86,10 +86,7 @@ "outputs": [], "source": [ "import os\n", -<<<<<<< HEAD "\n", -======= ->>>>>>> origin/main "import deeplabcut" ] }, @@ -114,11 +111,7 @@ "\n", "uploaded = files.upload()\n", "for filepath, content in uploaded.items():\n", -<<<<<<< HEAD " print(f'User uploaded file \"{filepath}\" with length {len(content)} bytes')\n", -======= - " print(f'User uploaded file \"{filepath}\" with length {len(content)} bytes')\n", ->>>>>>> origin/main "video_path = os.path.abspath(filepath)\n", "\n", "# If this cell fails (e.g., when using Safari in place of Google Chrome),\n", @@ -148,14 +141,7 @@ "\n", "model_options = deeplabcut.create_project.modelzoo.Modeloptions\n", "model_selection = widgets.Dropdown(\n", -<<<<<<< HEAD " options=model_options, value=model_options[0], description=\"Choose a DLC ModelZoo model!\", disabled=False\n", -======= - " options=model_options,\n", - " value=model_options[0],\n", - " description=\"Choose a DLC ModelZoo model!\",\n", - " disabled=False\n", ->>>>>>> origin/main ")\n", "display(model_selection)" ] @@ -168,17 +154,10 @@ }, "outputs": [], "source": [ -<<<<<<< HEAD "project_name = \"myDLC_modelZoo\"\n", "your_name = \"teamDLC\"\n", "model2use = model_selection.value\n", "videotype = os.path.splitext(video_path)[-1].lstrip(\".\") # or MOV, or avi, whatever you uploaded!" -======= - "project_name = 'myDLC_modelZoo'\n", - "your_name = 'teamDLC'\n", - "model2use = model_selection.value\n", - "videotype = os.path.splitext(video_path)[-1].lstrip('.') #or MOV, or avi, whatever you uploaded!" ->>>>>>> origin/main ] }, { @@ -232,11 +211,7 @@ " model=model2use,\n", " analyzevideo=True,\n", " createlabeledvideo=True,\n", -<<<<<<< HEAD " copy_videos=True, # must leave copy_videos=True\n", -======= - " copy_videos=True, #must leave copy_videos=True\n", ->>>>>>> origin/main " engine=deeplabcut.Engine.TF,\n", ")" ] @@ -276,15 +251,9 @@ "source": [ "# Updating the plotting within the config.yaml file (without opening it ;):\n", "edits = {\n", -<<<<<<< HEAD " \"dotsize\": 7, # size of the dots!\n", " \"colormap\": \"spring\", # any matplotlib colormap!\n", " \"pcutoff\": 0.5, # the higher the more conservative the plotting!\n", -======= - " 'dotsize': 7, # size of the dots!\n", - " 'colormap': 'spring', # any matplotlib colormap!\n", - " 'pcutoff': 0.5, # the higher the more conservative the plotting!\n", ->>>>>>> origin/main "}\n", "deeplabcut.auxiliaryfunctions.edit_config(config_path, edits)" ] @@ -301,7 +270,6 @@ "project_path = os.path.dirname(config_path)\n", "full_video_path = os.path.join(\n", " project_path,\n", -<<<<<<< HEAD " \"videos\",\n", " os.path.basename(video_path),\n", ")\n", @@ -310,16 +278,6 @@ "deeplabcut.filterpredictions(config_path, [full_video_path], videotype=videotype)\n", "\n", "# re-create the video with your edits!\n", -======= - " 'videos',\n", - " os.path.basename(video_path),\n", - ")\n", - "\n", - "#filter predictions (should already be done above ;):\n", - "deeplabcut.filterpredictions(config_path, [full_video_path], videotype=videotype)\n", - "\n", - "#re-create the video with your edits!\n", ->>>>>>> origin/main "deeplabcut.create_labeled_video(config_path, [full_video_path], videotype=videotype, filtered=True)" ] } @@ -331,14 +289,11 @@ "provenance": [], "toc_visible": true }, -<<<<<<< HEAD -======= "deeplabcut": { "ignore": false, "last_content_updated": "2025-10-02", "last_metadata_updated": "2026-03-06" }, ->>>>>>> origin/main "gpuClass": "standard", "kernelspec": { "display_name": "Python 3", diff --git a/tests/pose_estimation_pytorch/modelzoo/test_fmpose_integration.py b/tests/pose_estimation_pytorch/modelzoo/test_fmpose_integration.py index b66246a848..e7608625fa 100644 --- a/tests/pose_estimation_pytorch/modelzoo/test_fmpose_integration.py +++ b/tests/pose_estimation_pytorch/modelzoo/test_fmpose_integration.py @@ -158,9 +158,7 @@ def pose_3d(self, keypoints_2d, image_size): poses_3d=np.zeros((n_frames, 26, 3), dtype=np.float32), ) - def _fake_create_df_from_prediction( - predictions, dlc_scorer, multi_animal, model_cfg, output_path, output_prefix - ): + def _fake_create_df_from_prediction(predictions, dlc_scorer, multi_animal, model_cfg, output_path, output_prefix): bodyparts = model_cfg["metadata"]["bodyparts"] individuals = model_cfg["metadata"]["individuals"] columns = pd.MultiIndex.from_product( @@ -175,9 +173,7 @@ def _fake_create_df_from_prediction( "get_fmpose3d_inference_api", lambda model_type, device: FakeAPI(), ) - monkeypatch.setattr( - fmp_inf, "create_df_from_prediction", _fake_create_df_from_prediction - ) + monkeypatch.setattr(fmp_inf, "create_df_from_prediction", _fake_create_df_from_prediction) monkeypatch.setattr( fmp_inf, "get_superanimal_colormaps", diff --git a/tests/tools/docs_and_notebooks_checks/test_check_contracts.py b/tests/tools/docs_and_notebooks_checks/test_check_contracts.py index 9f4aa2c767..3015b9972b 100644 --- a/tests/tools/docs_and_notebooks_checks/test_check_contracts.py +++ b/tests/tools/docs_and_notebooks_checks/test_check_contracts.py @@ -34,12 +34,8 @@ def tool() -> ModuleType: # ----------------------------- # Git helpers for a temp repo # ----------------------------- -def _run( - cmd: list[str], cwd: Path, env: dict | None = None -) -> subprocess.CompletedProcess: - return subprocess.run( - cmd, cwd=str(cwd), env=env, capture_output=True, text=True, check=True - ) +def _run(cmd: list[str], cwd: Path, env: dict | None = None) -> subprocess.CompletedProcess: + return subprocess.run(cmd, cwd=str(cwd), env=env, capture_output=True, text=True, check=True) def _git_init(repo: Path) -> None: @@ -201,7 +197,8 @@ def test_update_requires_ack_when_write(tool, tmp_path: Path): def test_update_set_content_date_from_git_only_changes_that_field(tool, tmp_path: Path): """ - Contract: update --set-content-date-from-git only sets last_content_updated (plus last_metadata_updated when writing), + Contract: update --set-content-date-from-git only sets last_content_updated + (plus last_metadata_updated when writing), does NOT override last_verified/verified_for unless explicitly provided. """ repo = tmp_path / "repo" @@ -209,14 +206,7 @@ def test_update_set_content_date_from_git_only_changes_that_field(tool, tmp_path _git_init(repo) rel = "docs/page.md" - initial = ( - "---\n" - "deeplabcut:\n" - " last_verified: 2020-02-02\n" - " verified_for: 3.0.0rc1\n" - "---\n" - "# hello\n" - ) + initial = "---\ndeeplabcut:\n last_verified: 2020-02-02\n verified_for: 3.0.0rc1\n---\n# hello\n" _write(repo, rel, initial) _git_commit(repo, "docs: initial content", "2020-01-01T12:00:00+00:00") @@ -309,14 +299,7 @@ def test_normalize_is_explicit_and_marks_would_change(tool, tmp_path: Path): rel = "docs/nbs/nb.ipynb" # Minimal notebook JSON but not in nbformat canonical formatting (indent/newline differences) - raw = ( - "{\n" - ' "cells": [],\n' - ' "metadata": {},\n' - ' "nbformat": 4,\n' - ' "nbformat_minor": 5\n' - "}\n" - ) + raw = '{\n "cells": [],\n "metadata": {},\n "nbformat": 4,\n "nbformat_minor": 5\n}\n' _write(repo, rel, raw) _git_commit(repo, "docs: add notebook", "2020-01-01T12:00:00+00:00") @@ -386,14 +369,7 @@ def test_notebook_missing_dlc_namespace_warns_missing_metadata(tool, tmp_path: P rel = "docs/nbs/nb.ipynb" # Valid minimal notebook, but no "deeplabcut" namespace under metadata - nb = ( - "{\n" - ' "cells": [],\n' - ' "metadata": {},\n' - ' "nbformat": 4,\n' - ' "nbformat_minor": 5\n' - "}\n" - ) + nb = '{\n "cells": [],\n "metadata": {},\n "nbformat": 4,\n "nbformat_minor": 5\n}\n' _write(repo, rel, nb) _git_commit(repo, "docs: add notebook", "2020-01-01T12:00:00+00:00") diff --git a/tools/docs_and_notebooks_check.py b/tools/docs_and_notebooks_check.py index de8569e478..82e12d1c4a 100644 --- a/tools/docs_and_notebooks_check.py +++ b/tools/docs_and_notebooks_check.py @@ -68,8 +68,10 @@ - PyYAML - nbformat>=5 to be installed in the environment. - Recommended : install in CI job directly (pip install pydantic pyyaml nbformat) rather than adding to requirements, since these are only needed for this tool. + Recommended : install in CI job directly (pip install pydantic pyyaml nbformat) + rather than adding to requirements, since these are only needed for this tool. """ + # tools/docs_and_notebooks_check.py from __future__ import annotations @@ -79,24 +81,15 @@ import os import re import subprocess +from collections.abc import Sequence from datetime import date, datetime, timezone from pathlib import Path -from typing import Any, Dict, List, Literal, Optional, Sequence, Tuple - -try: - import yaml # PyYAML -except Exception: - yaml = None - -try: - from pydantic import BaseModel, ConfigDict, Field, ValidationError -except Exception: # pragma: no cover - raise RuntimeError("Pydantic is required to run this script") -try: - import nbformat - from nbformat.validator import NotebookValidationError -except Exception: - raise RuntimeError("nbformat is required to read/write .ipynb files") +from typing import Any, Literal + +import nbformat +import yaml +from nbformat.validator import NotebookValidationError +from pydantic import BaseModel, ConfigDict, Field, ValidationError SCHEMA_VERSION = 1 DLC_NAMESPACE = "deeplabcut" @@ -127,23 +120,23 @@ class DLCMeta(BaseModel): model_config = ConfigDict(extra="allow") # Tool-managed: last meaningful content update date (excluding metadata commits) - last_content_updated: Optional[date] = None + last_content_updated: date | None = None # Optional tool-managed: last time metadata/normalization was performed - last_metadata_updated: Optional[date] = None + last_metadata_updated: date | None = None # Optional human-managed verification fields - last_verified: Optional[date] = None + last_verified: date | None = None # Version or other string indicating what this file was verified for (e.g. "3.0.0rc13") - verified_for: Optional[str] = None + verified_for: str | None = None # Extra metadata fields for later usage (e.g. allowlist tier classification), but not currently used by the tool - tier: Optional[str] = None + tier: str | None = None ignore: bool = False - notes: Optional[str] = None + notes: str | None = None class ScanConfig(BaseModel): - include: List[str] = Field(default_factory=list) - exclude: List[str] = Field(default_factory=list) + include: list[str] = Field(default_factory=list) + exclude: list[str] = Field(default_factory=list) class PolicyConfig(BaseModel): @@ -155,10 +148,10 @@ class PolicyConfig(BaseModel): fail_on_scan_errors: bool = False # Allowlists for strict checks (start empty; ratchet later) - require_metadata: List[str] = Field(default_factory=list) - require_recent_verification: List[str] = Field(default_factory=list) + require_metadata: list[str] = Field(default_factory=list) + require_recent_verification: list[str] = Field(default_factory=list) - require_notebook_normalized: List[str] = Field(default_factory=list) + require_notebook_normalized: list[str] = Field(default_factory=list) class ToolConfig(BaseModel): @@ -172,19 +165,19 @@ class FileRecord(BaseModel): kind: str # ipynb | md | other # Computed from git (excluding metadata-only commits) - last_content_updated: Optional[date] = None + last_content_updated: date | None = None # Debug-only: raw git last touched (may be metadata commit) - last_git_touched: Optional[date] = None + last_git_touched: date | None = None # Read from file metadata/frontmatter - meta: Optional[DLCMeta] = None + meta: DLCMeta | None = None # Derived - days_since_content_update: Optional[int] = None - days_since_verified: Optional[int] = None + days_since_content_update: int | None = None + days_since_verified: int | None = None - warnings: List[str] = Field(default_factory=list) - errors: List[str] = Field(default_factory=list) + warnings: list[str] = Field(default_factory=list) + errors: list[str] = Field(default_factory=list) # If update mode would change file would_change: bool = False @@ -196,8 +189,8 @@ class Report(BaseModel): repo_root: str config_path: str - totals: Dict[str, int] - records: List[FileRecord] + totals: dict[str, int] + records: list[FileRecord] # Rebuild models due to __future__ annotations @@ -216,12 +209,11 @@ def _iso_today() -> date: return datetime.now(timezone.utc).date() -def _run_git(args: Sequence[str], cwd: Path) -> Tuple[int, str, str]: +def _run_git(args: Sequence[str], cwd: Path) -> tuple[int, str, str]: p = subprocess.run( ["git", *args], cwd=str(cwd), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + capture_output=True, text=True, ) return p.returncode, p.stdout.strip(), p.stderr.strip() @@ -241,14 +233,14 @@ def find_repo_root(start: Path) -> Path: raise RuntimeError("Could not locate repository root") -def glob_paths(repo_root: Path, patterns: List[str]) -> List[Path]: - results: List[Path] = [] +def glob_paths(repo_root: Path, patterns: list[str]) -> list[Path]: + results: list[Path] = [] for pat in patterns: results.extend(repo_root.glob(pat)) return sorted({p.resolve() for p in results if p.is_file()}) -def is_excluded(rel_path: str, exclude_patterns: List[str]) -> bool: +def is_excluded(rel_path: str, exclude_patterns: list[str]) -> bool: return any(fnmatch.fnmatch(rel_path, pat) for pat in exclude_patterns) @@ -261,7 +253,7 @@ def file_kind(path: Path) -> str: return "other" -def _parse_git_iso_date(out: str) -> Optional[date]: +def _parse_git_iso_date(out: str) -> date | None: out = (out or "").strip() if not out: return None @@ -279,9 +271,7 @@ def _parse_git_iso_date(out: str) -> Optional[date]: return None -def _git_log_date( - repo_root: Path, rel_path: str, extra_args: Sequence[str] = () -) -> Optional[date]: +def _git_log_date(repo_root: Path, rel_path: str, extra_args: Sequence[str] = ()) -> date | None: args = [ "log", "-1", @@ -297,13 +287,11 @@ def _git_log_date( return _parse_git_iso_date(out) -def git_last_touched(repo_root: Path, rel_path: str) -> Optional[date]: +def git_last_touched(repo_root: Path, rel_path: str) -> date | None: return _git_log_date(repo_root, rel_path) -def git_last_content_updated( - repo_root: Path, rel_path: str -) -> Tuple[Optional[date], bool]: +def git_last_content_updated(repo_root: Path, rel_path: str) -> tuple[date | None, bool]: d = _git_log_date( repo_root, rel_path, @@ -322,7 +310,7 @@ def git_last_content_updated( FRONTMATTER_RE = re.compile(r"^---\s*$") -def read_md_frontmatter(text: str) -> Tuple[Optional[dict], str, Optional[str]]: +def read_md_frontmatter(text: str) -> tuple[dict | None, str, str | None]: lines = text.splitlines(keepends=True) if not lines or not FRONTMATTER_RE.match(lines[0]): return None, text, None @@ -397,7 +385,7 @@ def write_ipynb_meta(path: Path, nb: Any) -> None: path.write_text(text + "\n", encoding="utf-8") -def parse_dlc_meta(raw: Any) -> tuple[Optional[DLCMeta], bool]: +def parse_dlc_meta(raw: Any) -> tuple[DLCMeta | None, bool]: # returns (meta, valid) if raw is None or not isinstance(raw, dict): return None, False @@ -415,11 +403,11 @@ def meta_to_jsonable(meta: DLCMeta) -> dict: return meta.model_dump(mode="json", exclude_none=True) -def compute_days_since(d: Optional[date], today: date) -> Optional[int]: +def compute_days_since(d: date | None, today: date) -> int | None: return None if d is None else (today - d).days -def match_allowlist(rel_path: str, allowlist: List[str]) -> bool: +def match_allowlist(rel_path: str, allowlist: list[str]) -> bool: # Support exact matches or glob patterns return any(pat == rel_path or fnmatch.fnmatch(rel_path, pat) for pat in allowlist) @@ -436,12 +424,10 @@ def load_config(config_path: Path) -> ToolConfig: return ToolConfig.model_validate(raw) -def scan_files( - repo_root: Path, cfg: ToolConfig, targets: Optional[List[str]] = None -) -> List[FileRecord]: +def scan_files(repo_root: Path, cfg: ToolConfig, targets: list[str] | None = None) -> list[FileRecord]: today = _iso_today() paths = glob_paths(repo_root, cfg.scan.include) - records: List[FileRecord] = [] + records: list[FileRecord] = [] target_set = None if targets: target_set = set(t.replace(os.sep, "/") for t in targets) @@ -456,12 +442,8 @@ def scan_files( rec = FileRecord(path=rel, kind=kind) rec.last_git_touched = git_last_touched(repo_root, rel) - rec.last_content_updated, used_fallback = git_last_content_updated( - repo_root, rel - ) - rec.days_since_content_update = compute_days_since( - rec.last_content_updated, today - ) + rec.last_content_updated, used_fallback = git_last_content_updated(repo_root, rel) + rec.days_since_content_update = compute_days_since(rec.last_content_updated, today) if used_fallback: rec.warnings.append("content_date_fallback_to_git_touched") @@ -544,13 +526,8 @@ def scan_files( if last_verified is None and pol.missing_last_verified_is_warning: rec.warnings.append("missing_last_verified") - elif ( - rec.days_since_verified is not None - and rec.days_since_verified > pol.warn_if_verified_older_than_days - ): - rec.warnings.append( - f"verified_stale>{pol.warn_if_verified_older_than_days}d" - ) + elif rec.days_since_verified is not None and rec.days_since_verified > pol.warn_if_verified_older_than_days: + rec.warnings.append(f"verified_stale>{pol.warn_if_verified_older_than_days}d") records.append(rec) @@ -579,13 +556,13 @@ def _require_meta_marker_ack(write: bool, ack_marker: bool) -> None: def update_files( repo_root: Path, cfg: ToolConfig, - targets: Optional[List[str]], + targets: list[str] | None, write: bool, set_content_date_from_git: bool, - set_last_verified: Optional[date], - set_verified_for: Optional[str], + set_last_verified: date | None, + set_verified_for: str | None, ack_meta_commit_marker: bool, -) -> List[FileRecord]: +) -> list[FileRecord]: today = _iso_today() records = scan_files(repo_root, cfg, targets=targets) target_set = set(t.replace(os.sep, "/") for t in targets) if targets else None @@ -626,9 +603,7 @@ def update_files( if merged_base != prev: changed = True if write: - _require_meta_marker_ack( - write=True, ack_marker=ack_meta_commit_marker - ) + _require_meta_marker_ack(write=True, ack_marker=ack_meta_commit_marker) meta.last_metadata_updated = today desired_final = meta_to_jsonable(meta) @@ -659,9 +634,7 @@ def update_files( if merged_base != prev: changed = True if write: - _require_meta_marker_ack( - write=True, ack_marker=ack_meta_commit_marker - ) + _require_meta_marker_ack(write=True, ack_marker=ack_meta_commit_marker) meta.last_metadata_updated = today desired_final = meta_to_jsonable(meta) @@ -684,10 +657,10 @@ def update_files( def normalize_notebooks( repo_root: Path, cfg: ToolConfig, - targets: Optional[List[str]], + targets: list[str] | None, write: bool, ack_meta_commit_marker: bool, -) -> List[FileRecord]: +) -> list[FileRecord]: """ Normalize notebooks deterministically (canonical nbformat JSON). This is intentionally separated from update() because it causes churn. @@ -737,30 +710,22 @@ def normalize_notebooks( # ----------------------------- -def summarize(records: List[FileRecord]) -> Dict[str, int]: +def summarize(records: list[FileRecord]) -> dict[str, int]: return { "files": len(records), "warnings": sum(1 for r in records if r.warnings), "errors": sum(1 for r in records if r.errors), "missing_metadata": sum(1 for r in records if "missing_metadata" in r.warnings), - "missing_last_verified": sum( - 1 for r in records if "missing_last_verified" in r.warnings - ), - "content_stale": sum( - 1 for r in records if any(w.startswith("content_stale") for w in r.warnings) - ), - "verified_stale": sum( - 1 - for r in records - if any(w.startswith("verified_stale") for w in r.warnings) - ), + "missing_last_verified": sum(1 for r in records if "missing_last_verified" in r.warnings), + "content_stale": sum(1 for r in records if any(w.startswith("content_stale") for w in r.warnings)), + "verified_stale": sum(1 for r in records if any(w.startswith("verified_stale") for w in r.warnings)), } def to_markdown(report: Report, cfg: ToolConfig) -> str: pol = cfg.policy t = report.totals - lines: List[str] = [] + lines: list[str] = [] lines.append("# 🌡️ DeepLabCut freshness report\n") lines.append(f"Generated: {report.generated_at.isoformat()}\n") @@ -772,19 +737,13 @@ def to_markdown(report: Report, cfg: ToolConfig) -> str: lines.append(f"- Files with scanning errors: **{t['errors']}**\n") lines.append(f"- Missing metadata: **{t['missing_metadata']}**\n") lines.append(f"- Missing last_verified: **{t['missing_last_verified']}**\n") - lines.append( - f"- Content-stale (> {pol.warn_if_content_older_than_days}d): **{t['content_stale']}**\n" - ) - lines.append( - f"- Verification-stale (> {pol.warn_if_verified_older_than_days}d): **{t['verified_stale']}**\n\n" - ) + lines.append(f"- Content-stale (> {pol.warn_if_content_older_than_days}d): **{t['content_stale']}**\n") + lines.append(f"- Verification-stale (> {pol.warn_if_verified_older_than_days}d): **{t['verified_stale']}**\n\n") - def fmt_date(d: Optional[date]) -> str: + def fmt_date(d: date | None) -> str: return d.isoformat() if d else "-" - warn_recs = [ - r for r in report.records if r.warnings and not (r.meta and r.meta.ignore) - ] + warn_recs = [r for r in report.records if r.warnings and not (r.meta and r.meta.ignore)] warn_recs.sort( key=lambda r: ( -(r.days_since_verified or -1), @@ -805,9 +764,7 @@ def fmt_date(d: Optional[date]) -> str: if r.last_git_touched: lines.append(f" - last_git_touched: {fmt_date(r.last_git_touched)}\n") if meta and meta.last_metadata_updated: - lines.append( - f" - last_metadata_updated: {fmt_date(meta.last_metadata_updated)}\n" - ) + lines.append(f" - last_metadata_updated: {fmt_date(meta.last_metadata_updated)}\n") lv = meta.last_verified if meta else None lines.append( f" - last_verified: {fmt_date(lv)} " @@ -830,11 +787,10 @@ def fmt_date(d: Optional[date]) -> str: lines.append("\n") lines.append("## Notes\n") + lines.append("- 'Out of date' does not necessarily mean 'broken'. Use this as a triage signal.\n") lines.append( - "- 'Out of date' does not necessarily mean 'broken'. Use this as a triage signal.\n" - ) - lines.append( - "- last_git_touched / last_content_updated are computed from git history. last_verified is human-controlled.\n\n" + "- last_git_touched / last_content_updated are computed from git history. " + "last_verified is human-controlled.\n\n" ) lines.append( "- In `check` mode, scan/parsing errors are reported for visibility but do not " @@ -843,16 +799,14 @@ def fmt_date(d: Optional[date]) -> str: return "".join(lines) -def write_outputs(report: Report, cfg: ToolConfig, out_dir: Path) -> Tuple[Path, Path]: +def write_outputs(report: Report, cfg: ToolConfig, out_dir: Path) -> tuple[Path, Path]: out_dir.mkdir(parents=True, exist_ok=True) json_path = out_dir / f"{OUTPUT_FILENAME}.json" md_path = out_dir / f"{OUTPUT_FILENAME}.md" payload = report.model_dump(mode="json") - json_path.write_text( - json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" - ) + json_path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") md_path.write_text(to_markdown(report, cfg), encoding="utf-8") return json_path, md_path @@ -860,9 +814,9 @@ def write_outputs(report: Report, cfg: ToolConfig, out_dir: Path) -> Tuple[Path, # ----------------------------- # Check enforcement # ----------------------------- -def enforce(cfg: ToolConfig, records: List[FileRecord]) -> List[str]: +def enforce(cfg: ToolConfig, records: list[FileRecord]) -> list[str]: pol = cfg.policy - violations: List[str] = [] + violations: list[str] = [] today = _iso_today() for r in records: @@ -890,17 +844,12 @@ def enforce(cfg: ToolConfig, records: List[FileRecord]) -> List[str]: days = (today - lv).days if days > pol.warn_if_verified_older_than_days: violations.append( - f"{r.path}: last_verified is {days}d old " - f"(> {pol.warn_if_verified_older_than_days}d)" + f"{r.path}: last_verified is {days}d old (> {pol.warn_if_verified_older_than_days}d)" ) - if r.kind == "ipynb" and match_allowlist( - r.path, pol.require_notebook_normalized - ): + if r.kind == "ipynb" and match_allowlist(r.path, pol.require_notebook_normalized): if "notebook_not_normalized" in (r.warnings or []): - violations.append( - f"{r.path}: notebook is not normalized (run update/format)" - ) + violations.append(f"{r.path}: notebook is not normalized (run update/format)") return violations @@ -917,31 +866,23 @@ def parse_date_token(token: str) -> date: return date.fromisoformat(token) -def collect_scan_issues( - records: List[FileRecord], target: Literal["errors", "warnings"] -) -> List[str]: - items: List[str] = [] +def collect_scan_issues(records: list[FileRecord], target: Literal["errors", "warnings"]) -> list[str]: + items: list[str] = [] for r in records: for e in getattr(r, target, []): items.append(f"{r.path}: {e}") return items -def main(argv: Optional[Sequence[str]] = None) -> int: - parser = argparse.ArgumentParser( - description="DeepLabCut checks tool (docs + notebooks)" - ) - parser.add_argument( - "--config", default=str(DEFAULT_CFG), help="Path to YAML config file" - ) +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="DeepLabCut checks tool (docs + notebooks)") + parser.add_argument("--config", default=str(DEFAULT_CFG), help="Path to YAML config file") parser.add_argument( "--no-step-summary", action="store_true", help="Do not write to GITHUB_STEP_SUMMARY", ) - parser.add_argument( - "--out-dir", default=f"tmp/{OUTPUT_FILENAME}", help="Directory to write outputs" - ) + parser.add_argument("--out-dir", default=f"tmp/{OUTPUT_FILENAME}", help="Directory to write outputs") sub = parser.add_subparsers(dest="cmd", required=True) rep = sub.add_parser("report", help="Generate staleness report (read-only)") @@ -969,9 +910,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: help="Enable failure on scan/parsing errors (overrides config for this run)", ) - up = sub.add_parser( - "update", help="Update metadata/frontmatter (write mode requires --write)" - ) + up = sub.add_parser("update", help="Update metadata/frontmatter (write mode requires --write)") up.add_argument( "--write", action="store_true", @@ -982,9 +921,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: action="store_true", help="Set embedded last_content_updated from computed git content date", ) - up.add_argument( - "--targets", nargs="*", help="Optional list of relative file paths to update" - ) + up.add_argument("--targets", nargs="*", help="Optional list of relative file paths to update") up.add_argument("--set-last-verified", default=None, help="YYYY-MM-DD or 'today'") up.add_argument("--set-verified-for", default=None, help="String like 3.0.0rc13") up.add_argument( @@ -1023,9 +960,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: if args.cmd in {"report", "check"}: records = scan_files(repo_root, cfg, targets=getattr(args, "targets", None)) elif args.cmd == "update": - lv = ( - parse_date_token(args.set_last_verified) if args.set_last_verified else None - ) + lv = parse_date_token(args.set_last_verified) if args.set_last_verified else None records = update_files( repo_root, cfg, @@ -1096,7 +1031,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int: if args.cmd not in {"report", "check"} and any(r.errors for r in records): return 1 else: - print(f"\nReport generated:") + print("\nReport generated:") print(f"- JSON: {json_path}") print(f"- Markdown: {md_path}") diff --git a/tools/docs_and_notebooks_report_config.yml b/tools/docs_and_notebooks_report_config.yml index 5342f4dfb9..60dfcb21f8 100644 --- a/tools/docs_and_notebooks_report_config.yml +++ b/tools/docs_and_notebooks_report_config.yml @@ -16,10 +16,10 @@ policy: warn_if_verified_older_than_days: 365 missing_last_verified_is_warning: true - # Ratchet lists for tiered verification requirements. + # Ratchet lists for tiered verification requirements. # Tiers have to be determined, and crucial targets identified. - # Then specific policies can be set for each tier, - # e.g. requiring more recent verification for higher tiers, + # Then specific policies can be set for each tier, + # e.g. requiring more recent verification for higher tiers, # or requiring verification for more recent versions. require_metadata: [] require_recent_verification: [] From 8fd910ab354cc44e8d457f8c357a744bb601d386 Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Mon, 23 Mar 2026 11:28:49 -0500 Subject: [PATCH 78/80] Fix mutable default for config_kwargs Change get_fmpose3d_inference_api to use None as the default for config_kwargs and initialize it to an empty dict inside the function. This avoids the shared mutable default argument ({}), preventing accidental state leakage across calls. Updated in deeplabcut/pose_estimation_pytorch/modelzoo/fmpose_3d/fmpose3d.py. --- .../pose_estimation_pytorch/modelzoo/fmpose_3d/fmpose3d.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/deeplabcut/pose_estimation_pytorch/modelzoo/fmpose_3d/fmpose3d.py b/deeplabcut/pose_estimation_pytorch/modelzoo/fmpose_3d/fmpose3d.py index 41f835afb9..bb78a1c93a 100644 --- a/deeplabcut/pose_estimation_pytorch/modelzoo/fmpose_3d/fmpose3d.py +++ b/deeplabcut/pose_estimation_pytorch/modelzoo/fmpose_3d/fmpose3d.py @@ -99,7 +99,7 @@ def get_fmpose3d_inference_api( model_type: SupportedModel = "fmpose3d_humans", snapshot_path: str | None = None, device: str | None = None, - config_kwargs: dict = {}, + config_kwargs: dict = None, ) -> FMPose3DInference: """ Get a FMPose3DInference API for a given model type and snapshot path. @@ -128,6 +128,8 @@ def get_fmpose3d_inference_api( predictions_3d = fmpose.pose_3d(keypoints_2d=keypoints_2d) ``` """ + if config_kwargs is None: + config_kwargs = {} model_config = FMPose3DConfig(model_type=model_type, **config_kwargs) fmpose3d_api = FMPose3DInference( model_config, From b183029aa9337176d989b731e6bb4c1098a63bfb Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Mon, 23 Mar 2026 11:29:16 -0500 Subject: [PATCH 79/80] Refactor fmpose3d inference formatting Small refactor in fmpose_3d inference: bind outer variables (predictions_2d, all_poses_3d) as default args for the nested _process_batch to ensure correct capture when used as a callback, and tidy up several long-wrapped statements for readability (create_empty_df call, dest_folder handling, logger.info, and the poses_3d_serialisable list comprehension). No functional changes intended. --- .../modelzoo/fmpose_3d/inference.py | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/deeplabcut/pose_estimation_pytorch/modelzoo/fmpose_3d/inference.py b/deeplabcut/pose_estimation_pytorch/modelzoo/fmpose_3d/inference.py index eda263d76d..df993b332f 100644 --- a/deeplabcut/pose_estimation_pytorch/modelzoo/fmpose_3d/inference.py +++ b/deeplabcut/pose_estimation_pytorch/modelzoo/fmpose_3d/inference.py @@ -67,9 +67,7 @@ def _pose2d_to_dlc_predictions( # i/o migration to validated keypoint schemas (parquet) def _poses3d_to_dataframe(poses_3d: list[np.ndarray], df_2d, scorer_3d: str): """Create and fill a 3D dataframe using the shared auxiliary helper.""" - df_3d, scorer_3d, bodyparts = auxiliaryfunctions_3d.create_empty_df( - df_2d, scorer_3d, "3d" - ) + df_3d, scorer_3d, bodyparts = auxiliaryfunctions_3d.create_empty_df(df_2d, scorer_3d, "3d") n_frames = len(poses_3d) n_bodyparts = len(bodyparts) arr = np.full((n_frames, n_bodyparts, 3), np.nan, dtype=float) @@ -135,9 +133,7 @@ def _video_inference_fmpose3d( api = get_fmpose3d_inference_api(model_type=model_name, device=device) - dest_folder = ( - Path(video_paths[0]).parent if dest_folder is None else Path(dest_folder) - ) + dest_folder = Path(video_paths[0]).parent if dest_folder is None else Path(dest_folder) dest_folder.mkdir(parents=True, exist_ok=True) if create_labeled_video: @@ -156,7 +152,11 @@ def _video_inference_fmpose3d( all_poses_3d: list[np.ndarray] = [] warned_multi_person_2d = False - def _process_batch(frames: list[np.ndarray]) -> None: + def _process_batch( + frames: list[np.ndarray], + predictions_2d=predictions_2d, + all_poses_3d=all_poses_3d, + ) -> None: nonlocal warned_multi_person_2d pose_2d = api.prepare_2d(source=np.stack(frames)) num_detected = int(np.asarray(pose_2d.keypoints).shape[0]) @@ -180,9 +180,7 @@ def _process_batch(frames: list[np.ndarray]) -> None: ) all_poses_3d.extend(np.asarray(pose_3d.poses_3d)) except ValueError as e: - logger.info( - "Skipping 3D lifting for batch due to invalid 2D result: %s", e - ) + logger.info("Skipping 3D lifting for batch due to invalid 2D result: %s", e) all_poses_3d.extend([np.zeros((0, num_bodyparts, 3)) for _ in frames]) batch: list[np.ndarray] = [] @@ -224,10 +222,7 @@ def _process_batch(frames: list[np.ndarray]) -> None: with open(output_json, "w") as f: json.dump(predictions_2d, f, cls=NumpyEncoder) - poses_3d_serialisable = [ - pose.tolist() if isinstance(pose, np.ndarray) else pose - for pose in all_poses_3d - ] + poses_3d_serialisable = [pose.tolist() if isinstance(pose, np.ndarray) else pose for pose in all_poses_3d] output_3d_json = dest_folder / f"{output_prefix}_3d.json" with open(output_3d_json, "w") as f: json.dump( From d19eb98bc637d28f247ee7c1d71a20380776d79d Mon Sep 17 00:00:00 2001 From: Cyril Achard Date: Mon, 23 Mar 2026 11:52:20 -0500 Subject: [PATCH 80/80] Remove temporary yapf config from pyproject Clean up pyproject.toml by removing the temporary [tool.yapf] linting configuration and its related comment. This deletes the previous based_on_style and indent_width settings no longer needed after linting changes. --- pyproject.toml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6e72635884..a3d6a26813 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -164,8 +164,3 @@ markers = [ "unittest: fast unit-level tests", "functional: functional/integration-style tests", ] - -## TEMPORARY - TODO - Remove after linting changes are made -[tool.yapf] -based_on_style = "google" -indent_width = 4